# How can I get the camera's projection matrix?

I need a way to get the projection matrix of the active camera, via python.

EDIT: I think this question is different from the one asked here, because getting a matrix is different from setting the matrix.

Although, since this question was so quickly marked a duplicate, I guess the answer is the same.

• Yes. Getting is indeed not the same as setting, but AFAIK neither is possible through the python api. Commented Oct 6, 2014 at 18:19
• Your other question convinced me that getting is not the same as setting.. Voted to re-open. Commented Oct 7, 2014 at 5:18
• Thanks. When re-opened, I'll be able to provide an answer for this question (I basically just ported a chunk of C code that creates the perspective matrix, and that seems to produce desired results). Commented Oct 9, 2014 at 17:11

With some helpful references from Campbell (ideasman42), I was able to find the relevant Blender C code that creates the perspective matrix (from properties that were actually available via bpy), and I managed to port it to Python:

def view_plane(camd, winx, winy, xasp, yasp):
#/* fields rendering */
ycor = yasp / xasp
use_fields = False
if (use_fields):
ycor *= 2

def BKE_camera_sensor_size(p_sensor_fit, sensor_x, sensor_y):
#/* sensor size used to fit to. for auto, sensor_x is both x and y. */
if (p_sensor_fit == 'VERTICAL'):
return sensor_y;

return sensor_x;

if (camd.type == 'ORTHO'):
#/* orthographic camera */
#/* scale == 1.0 means exact 1 to 1 mapping */
pixsize = camd.ortho_scale
else:
#/* perspective camera */
sensor_size = BKE_camera_sensor_size(camd.sensor_fit, camd.sensor_width, camd.sensor_height)
pixsize = (sensor_size * camd.clip_start) / camd.lens

#/* determine sensor fit */
def BKE_camera_sensor_fit(p_sensor_fit, sizex, sizey):
if (p_sensor_fit == 'AUTO'):
if (sizex >= sizey):
return 'HORIZONTAL'
else:
return 'VERTICAL'

return p_sensor_fit

sensor_fit = BKE_camera_sensor_fit(camd.sensor_fit, xasp * winx, yasp * winy)

if (sensor_fit == 'HORIZONTAL'):
viewfac = winx
else:
viewfac = ycor * winy

pixsize /= viewfac

#/* extra zoom factor */
pixsize *= 1 #params->zoom

#/* compute view plane:
# * fully centered, zbuffer fills in jittered between -.5 and +.5 */
xmin = -0.5 * winx
ymin = -0.5 * ycor * winy
xmax =  0.5 * winx
ymax =  0.5 * ycor * winy

#/* lens shift and offset */
dx = camd.shift_x * viewfac # + winx * params->offsetx
dy = camd.shift_y * viewfac # + winy * params->offsety

xmin += dx
ymin += dy
xmax += dx
ymax += dy

#/* fields offset */
#if (params->field_second):
#    if (params->field_odd):
#        ymin -= 0.5 * ycor
#        ymax -= 0.5 * ycor
#    else:
#        ymin += 0.5 * ycor
#        ymax += 0.5 * ycor

#/* the window matrix is used for clipping, and not changed during OSA steps */
#/* using an offset of +0.5 here would give clip errors on edges */
xmin *= pixsize
xmax *= pixsize
ymin *= pixsize
ymax *= pixsize

return xmin, xmax, ymin, ymax

def projection_matrix(camd):
r = bpy.context.scene.render
left, right, bottom, top = view_plane(camd, r.resolution_x, r.resolution_y, 1, 1)

farClip, nearClip = camd.clip_end, camd.clip_start

Xdelta = right - left
Ydelta = top - bottom
Zdelta = farClip - nearClip

mat = [[0]*4 for i in range(4)]

mat[0][0] = nearClip * 2 / Xdelta
mat[1][1] = nearClip * 2 / Ydelta
mat[2][0] = (right + left) / Xdelta #/* note: negate Z  */
mat[2][1] = (top + bottom) / Ydelta
mat[2][2] = -(farClip + nearClip) / Zdelta
mat[2][3] = -1
mat[3][2] = (-2 * nearClip * farClip) / Zdelta

return sum([c for c in mat], [])


This call: projection_matrix(camera.data) will return the perspective projection matrix of the given camera object.

I don't know what the "fields" data is supposed to be, so I've set use_fields to False, and I didn't port the "fields offset" block (which you can see commented out, in view_plane function). Regardless, the resulting projection matrix seems correct, so I guess it's not all that important (for most people).

There is actually a better method than rolling your own code, as shown here:

    modelview_matrix = camera.matrix_world.inverted()
projection_matrix = camera.calc_matrix_camera(
render.resolution_x,
render.resolution_y,
render.pixel_aspect_x,
render.pixel_aspect_y,
)

• Doesn't the first line calculate the world->view matrix? Commented May 9, 2018 at 23:25
• I've personally used this code and it does appear to work as expected: github.com/kurocha/tagged-format/blob/… Commented May 17, 2018 at 23:38
• in 2.8 the calc_matrix_camera now requires depsgraph argument, which is on;y available in context, but not from cli. Commented Mar 15, 2019 at 0:07
• Why do you think they changed public method? Can you use bpy.context? Commented Mar 16, 2019 at 2:13
• Yes to be clear depsgraph = bpy.context.evaluated_depsgraph_get(), but apparently not available when running from the cli (background mode) Commented May 6, 2021 at 10:37

Starting from the answer of ioquatix (Thanks), I elaborated a more complete and self-contained solution.

The following function takes a camera, a point, and a render, and computes the 2d homogeneous coordinates. An example of conversion to Pixel coordinates follows in the main code.

# 2.79

# Small code and example to Project a 3D point into 2D pixel coordinates.
# Fabrizio Nunnari <[email protected]>
#
# - https://blender.stackexchange.com/questions/16472/how-can-i-get-the-cameras-projection-matrix

import bpy

from mathutils import Vector

def project_3d_point(camera: bpy.types.Object,
p: Vector,
render: bpy.types.RenderSettings = bpy.context.scene.render) -> Vector:
"""
Given a camera and its projection matrix M;
given p, a 3d point to project:

Compute P’ = M * P
P’= (x’, y’, z’, w')

Ignore z'
Normalize in:
x’’ = x’ / w’
y’’ = y’ / w’

x’’ is the screen coordinate in normalised range -1 (left) +1 (right)
y’’ is the screen coordinate in  normalised range -1 (bottom) +1 (top)

:param camera: The camera for which we want the projection
:param p: The 3D point to project
:param render: The render settings associated to the scene.
:return: The 2D projected point in normalized range [-1, 1] (left to right, bottom to top)
"""

if camera.type != 'CAMERA':
raise Exception("Object {} is not a camera.".format(camera.name))

if len(p) != 3:
raise Exception("Vector {} is not three-dimensional".format(p))

# Get the two components to calculate M
modelview_matrix = camera.matrix_world.inverted()
projection_matrix = camera.calc_matrix_camera(
render.resolution_x,
render.resolution_y,
render.pixel_aspect_x,
render.pixel_aspect_y,
)

# print(projection_matrix * modelview_matrix)

# Compute P’ = M * P
p1 = projection_matrix * modelview_matrix * Vector((p.x, p.y, p.z, 1))

# Normalize in: x’’ = x’ / w’, y’’ = y’ / w’
p2 = Vector(((p1.x/p1.w, p1.y/p1.w)))

return p2

#
# Test

camera = bpy.data.objects['Camera']  # or bpy.context.active_object
render = bpy.context.scene.render

P = Vector((-0.002170146, 0.409979939, 0.162410125))

print("Projecting point {} for camera '{:s}' into resolution {:d}x{:d}..."
.format(P, camera.name, render.resolution_x, render.resolution_y))

proj_p = project_3d_point(camera=camera, p=P, render=render)
print("Projected point (homogeneous coords): {}.".format(proj_p))

proj_p_pixels = Vector(((render.resolution_x-1) * (proj_p.x + 1) / 2, (render.resolution_y - 1) * (proj_p.y - 1) / (-2)))
print("Projected point (pixel coords): {}.".format(proj_p_pixels))

print("Done.")


# 2.8

import bpy

from mathutils import Vector

def project_3d_point(camera: bpy.types.Object,
p: Vector,
render: bpy.types.RenderSettings = bpy.context.scene.render) -> Vector:
"""
Given a camera and its projection matrix M;
given p, a 3d point to project:

Compute P’ = M * P
P’= (x’, y’, z’, w')

Ignore z'
Normalize in:
x’’ = x’ / w’
y’’ = y’ / w’

x’’ is the screen coordinate in normalised range -1 (left) +1 (right)
y’’ is the screen coordinate in  normalised range -1 (bottom) +1 (top)

:param camera: The camera for which we want the projection
:param p: The 3D point to project
:param render: The render settings associated to the scene.
:return: The 2D projected point in normalized range [-1, 1] (left to right, bottom to top)
"""

if camera.type != 'CAMERA':
raise Exception("Object {} is not a camera.".format(camera.name))

if len(p) != 3:
raise Exception("Vector {} is not three-dimensional".format(p))

# Get the two components to calculate M
modelview_matrix = camera.matrix_world.inverted()
projection_matrix = camera.calc_matrix_camera(
bpy.data.scenes["Scene"].view_layers["View Layer"].depsgraph,
x = render.resolution_x,
y = render.resolution_y,
scale_x = render.pixel_aspect_x,
scale_y = render.pixel_aspect_y,
)

# print(projection_matrix * modelview_matrix)

# Compute P’ = M * P
p1 = projection_matrix @ modelview_matrix @ Vector((p.x, p.y, p.z, 1))

# Normalize in: x’’ = x’ / w’, y’’ = y’ / w’
p2 = Vector(((p1.x/p1.w, p1.y/p1.w)))

return p2

camera = bpy.data.objects['Camera']  # or bpy.context.active_object
render = bpy.context.scene.render

P = Vector((-0.002170146, 0.409979939, 0.162410125))

print("Projecting point {} for camera '{:s}' into resolution {:d}x{:d}..."
.format(P, camera.name, render.resolution_x, render.resolution_y))

proj_p = project_3d_point(camera=camera, p=P, render=render)
print("Projected point (homogeneous coords): {}.".format(proj_p))

proj_p_pixels = Vector(((render.resolution_x-1) * (proj_p.x + 1) / 2, (render.resolution_y - 1) * (proj_p.y - 1) / (-2)))
print("Projected point (pixel coords): {}.".format(proj_p_pixels))

print("Done.")

• The Y coordinate returned from project_3d_point is correct w.r.t. the usual image convention, i.e. upper-left pixel is (0,0), but is inconsistent with the Y coordinate shown in Blender on a rendered image when inspecting pixels in the image view. This is because in a rendered image (0,0) is the bottom-left pixel. Commented Jul 17, 2018 at 17:48
• Regarding QwiglyDee's problem with depsgraph, I was able to retrieve it with the following expression: bpy.data.scenes["Scene"].view_layers["View Layer"].depsgraph You may need to adjust if you have different scene or view layer names. Also, it appears matrix * matrix no longer works; now it looks like it's matrix @ matrix. Commented May 27, 2020 at 17:18