How to identify/derive the pixel coordinate of vertices of an object in the render of a scene?

I am using version 2.93.6 of Blender and I am fairly new to Blender. I am struggling to find correct functions/APIs which would help me achieve the following:

From the example below we can see a scene and the viewport of the camera which shows what would get rendered to an image if one would like to do so. I would calculate programatically X,Y in pixel coordinates of the vertices of the cube. Is there a ready API or, collection of API functions which would enable me to do so?

Any guidance would be most welcome.

• You might want to start here. Nov 28 '21 at 19:31

I'm not sure certain what the right rule is for rounding camera coordinates to individual pixels, but I think examining coordinates in Camera space using world_to_camera_view and multiplying this by the Camera resolution in each of the x, y dimensions should be very close to what you want to do:

from bpy_extras.object_utils import world_to_camera_view
x_res = bpy.context.scene.render.resolution_x
y_res = bpy.context.scene.render.resolution_y
for i, v in enumerate(bpy.context.object.data.vertices):
cam_coord_vec = world_to_camera_view(bpy.context.scene, bpy.data.objects['Camera'], bpy.context.object.matrix_world @ v.co)
print(f"vertex # {i} has camera coordinates vector: {cam_coord_vec}")
x_pixel = x_res * cam_coord_vec.x
y_pixel = y_res * cam_coord_vec.y
print(f"in pixels (unrounded): {x_pixel, y_pixel}")
if 0.0 <= x_pixel <= x_res and 0.0 <= y_pixel <= y_res:
print(f"in pixels (truncated): {int(x_pixel), int(y_pixel)}")
print(f"in pixels (rounded): {round(x_pixel), round(y_pixel)}")
else:
print(f"Vertex lies outside of camera view.")


Example output on a new file with the Default Cube and standard default camera view of it:

vertex # 0 has camera coordinates vector: <Vector (0.6812, 0.7147, 10.7735)>
in pixels (unrounded): (1307.944107055664, 771.8815183639526)
in pixels (truncated): (1307, 771)
in pixels (rounded): (1308, 772)
vertex # 1 has camera coordinates vector: <Vector (0.6674, 0.3192, 11.6640)>
in pixels (unrounded): (1281.3787078857422, 344.7615337371826)
in pixels (truncated): (1281, 344)
in pixels (rounded): (1281, 345)
vertex # 2 has camera coordinates vector: <Vector (0.4928, 0.5843, 9.5452)>
in pixels (unrounded): (946.1326217651367, 631.0685062408447)
in pixels (truncated): (946, 631)
in pixels (rounded): (946, 631)
vertex # 3 has camera coordinates vector: <Vector (0.4934, 0.1534, 10.4357)>
in pixels (unrounded): (947.3159980773926, 165.6905597448349)
in pixels (truncated): (947, 165)
in pixels (rounded): (947, 166)
vertex # 4 has camera coordinates vector: <Vector (0.5039, 0.8240, 12.0766)>
in pixels (unrounded): (967.4798583984375, 889.9529600143433)
in pixels (truncated): (967, 889)
in pixels (rounded): (967, 890)
vertex # 5 has camera coordinates vector: <Vector (0.5036, 0.4608, 12.9672)>
in pixels (unrounded): (966.9661331176758, 497.6470613479614)
in pixels (truncated): (966, 497)
in pixels (rounded): (967, 498)
vertex # 6 has camera coordinates vector: <Vector (0.3180, 0.7217, 10.8483)>
in pixels (unrounded): (610.5794334411621, 779.4238471984863)
in pixels (truncated): (610, 779)
in pixels (rounded): (611, 779)
vertex # 7 has camera coordinates vector: <Vector (0.3318, 0.3282, 11.7388)>
in pixels (unrounded): (637.0874977111816, 354.452440738678)
in pixels (truncated): (637, 354)
in pixels (rounded): (637, 354)


And, when zooming in so that exactly 1 individual vertex is in the camera view:

vertex # 0 has camera coordinates vector: <Vector (1.6310, 1.8400, 1.7263)>
in pixels (unrounded): (3131.4981079101562, 1987.1581935882568)
Vertex lies outside of camera view.
vertex # 1 has camera coordinates vector: <Vector (1.2461, -0.3058, 2.6168)>
in pixels (unrounded): (2392.500686645508, -330.248122215271)
Vertex lies outside of camera view.
vertex # 2 has camera coordinates vector: <Vector (0.3615, 2.1165, 0.4979)>
in pixels (unrounded): (694.1628456115723, 2285.783586502075)
Vertex lies outside of camera view.
vertex # 3 has camera coordinates vector: <Vector (0.4503, -2.1049, 1.3885)>
in pixels (unrounded): (864.6673393249512, -2273.3101558685303)
Vertex lies outside of camera view.
vertex # 4 has camera coordinates vector: <Vector (0.5155, 1.7917, 3.0294)>
in pixels (unrounded): (989.8183822631836, 1935.0868606567383)
Vertex lies outside of camera view.
vertex # 5 has camera coordinates vector: <Vector (0.5120, 0.3703, 3.9199)>
in pixels (unrounded): (983.0441665649414, 399.89579916000366)
in pixels (truncated): (983, 399)
in pixels (rounded): (983, 400)
vertex # 6 has camera coordinates vector: <Vector (-0.5962, 1.8353, 1.8010)>
in pixels (unrounded): (-1144.6806335449219, 1982.1323347091675)
Vertex lies outside of camera view.
vertex # 7 has camera coordinates vector: <Vector (-0.2335, -0.2493, 2.6916)>
in pixels (unrounded): (-448.320837020874, -269.2303079366684)
Vertex lies outside of camera view.


Caveats:

• as written, this code ignores shape keys (these change the way you need to compute the vertex's position in world space, before passing it to world_to_camera_view)
• this does the computation for the currently active object and an assumed-to-exist camera named "Camera" (swap out bpy.context.object for some other object reference if you want to do it on a different object, and swap out bpy.data.objects['Camera'] if you want to compute with a different camera)
• I wrote and tested this in 2.83.2, not 2.93.6
• I wrote/tested this in the Python REPL embedded inside the Scripting tab of a new Blender file, where Blender already pre-imports the bpy library. You'll need to explicitly import this if you're using this in code for an Add On you're writing or some such
• As I said initially, if it is important that you not just be very close to the right pixel (which this will achieve), but get the exact pixel value, then you'll need to figure out the proper rounding rule. Probably the simplest way to do this is to generate a few test renders and open them in software that lets you browse the image on a per-pixel level (e.g., PS or GIMP I think could do this), and to try a few different rounding rules until you find one that seems to consistently work correctly
• Thanks a lot for the code and tips to the APIs: world_to_camera_view as well as matrix_world in the extras module. I was not aware of those. I also notice that the above code need to be adjusted a bit because the rendered coordinates is "upside-down". For instance pixel (0,0) in a rendered image would be top-left while in world_to_camera_view it is bottom left. (i also note it is mentioned in the documentation). Thanks again ! Nov 30 '21 at 4:27
• Oh, yes, that's right; I forgot that people usually represent pixel-space with the origin in the top-left, and you're completely right that world_to_camera_view expresses in terms of a basis that places the origin in the bot-left. I'll amend the answer slightly later to note this, so new readers won't have to look in the comments Nov 30 '21 at 14:33