Proof of concept:

The following script lets you display an image directly in the 3D Viewport, attached to every visible empty object. It uses a custom GPU shader to draw the image so that it always faces the camera. The image automatically updates and stays in place as you move around the scene.
import bpy import gpu import gpu_extras import mathutils path = r'D:\Desktop\ilike2\NoAnimalTesting.png' image = bpy.data.images.load(path, check_existing=True) #image.alpha_mode = 'PREMUL' # useful for some alpha images texture = gpu.texture.from_image(image) vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") vert_out.smooth('VEC2', "uvInterp") shader_info = gpu.types.GPUShaderCreateInfo() shader_info.push_constant('MAT4', "the_matrix") shader_info.sampler(0, 'FLOAT_2D', "the_texture") shader_info.vertex_in(0, 'VEC2', "position") shader_info.vertex_in(1, 'VEC2', "uv") shader_info.vertex_out(vert_out) shader_info.fragment_out(0, 'VEC4', "FragColor") shader_info.vertex_source(""" void main() { uvInterp = uv; gl_Position = the_matrix * vec4(position, 0.0, 1.0); } """) shader_info.fragment_source(""" void main() { FragColor = texture(the_texture, uvInterp); } """) shader = gpu.shader.create_from_info(shader_info) batch = gpu_extras.batch.batch_for_shader( shader, 'TRI_FAN', { "position": ((-1, -1), (1, -1), (1, 1), (-1, 1)), "uv": ((0, 0), (1, 0), (1, 1), (0, 1)), }, ) def my_cool_draw(): """ draw an image for every empty object """ init_depth_test = gpu.state.depth_test_get() init_depth_mask = gpu.state.depth_mask_get() init_blend = gpu.state.blend_get() gpu.state.depth_test_set('LESS_EQUAL') #gpu.state.depth_test_set('ALWAYS') # draw in front gpu.state.depth_mask_set(False) gpu.state.blend_set('ALPHA') region_3d = bpy.context.region_data shader.uniform_sampler('the_texture', texture) view_point_location = region_3d.view_matrix.inverted().translation viewport_rotation = region_3d.view_matrix.transposed().to_quaternion() if region_3d.view_perspective == 'ORTHO': direction = mathutils.Vector((0,0,1)) direction.rotate(region_3d.view_matrix.inverted().to_quaternion()) view_point_location += direction * region_3d.view_distance the_matrix = region_3d.perspective_matrix @ mathutils.Matrix.Translation((0, 0, 0)) @ mathutils.Matrix.Scale(1, 4) empties = [o for o in bpy.data.objects if o.type == 'EMPTY' and o.visible_get()] empties.sort(key = lambda o: (o.location - view_point_location).length_squared, reverse=True) for empty in empties: loc, _ , sca = empty.matrix_world.decompose() shader.uniform_float('the_matrix', the_matrix @ mathutils.Matrix.LocRotScale(loc, viewport_rotation, sca)) batch.draw(shader) gpu.state.blend_set(init_blend) gpu.state.depth_mask_set(init_depth_mask) gpu.state.depth_test_set(init_depth_test) # set draw handler and delete the prev one if not hasattr(bpy.types.WindowManager, 'my_data'): bpy.types.WindowManager.my_data = {} wm = bpy.data.window_managers[0] region_type = 'WINDOW' try: bpy.types.SpaceView3D.draw_handler_remove(wm.my_data.get('prev_draw_handler'), region_type) except Exception: import traceback traceback.print_exc() finally: draw_handler = bpy.types.SpaceView3D.draw_handler_add(my_cool_draw, (), region_type, 'POST_VIEW') wm.my_data['prev_draw_handler'] = draw_handler # redraw to see changes without interacting with the viewport for area in bpy.context.window.screen.areas: if area.type == 'VIEW_3D': area.tag_redraw()
If you want it to display only for a specific empty, for example, one named "Empty_Reference", you can simply add another condition alongside o.type == 'EMPTY', like this: o.name == "Empty_Reference".