17
$\begingroup$

As asked.

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.

$\endgroup$
3
  • $\begingroup$ Yes. Getting is indeed not the same as setting, but AFAIK neither is possible through the python api. $\endgroup$
    – gandalf3
    Oct 6, 2014 at 18:19
  • 1
    $\begingroup$ Your other question convinced me that getting is not the same as setting.. Voted to re-open. $\endgroup$
    – gandalf3
    Oct 7, 2014 at 5:18
  • 1
    $\begingroup$ 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). $\endgroup$ Oct 9, 2014 at 17:11

3 Answers 3

21
$\begingroup$

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).

$\endgroup$
14
$\begingroup$

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

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]>
#
# Related links:
# - 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.")
$\endgroup$
2
  • 2
    $\begingroup$ 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. $\endgroup$
    – Paul Melis
    Jul 17, 2018 at 17:48
  • 1
    $\begingroup$ 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. $\endgroup$
    – defab67
    May 27, 2020 at 17:18

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .