11
$\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$
  • $\begingroup$ Yes. Getting is indeed not the same as setting, but AFAIK neither is possible through the python api. $\endgroup$ – gandalf3 Oct 6 '14 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 '14 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$ – Goran Milovanovic Oct 9 '14 at 17:11
16
$\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$
10
$\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$
  • $\begingroup$ Doesn't the first line calculate the world->view matrix? $\endgroup$ – jdk1.0 May 9 '18 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 '18 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$ – qMax Mar 15 at 0:07
  • $\begingroup$ Why do you think they changed public method? Can you use bpy.context? $\endgroup$ – ioquatix Mar 16 at 2:13
5
$\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.

# Small code and example to Project a 3D point into 2D pixel coordinates.
# Fabrizio Nunnari <fab.nunnari@gmail.com>
#
# 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.")
$\endgroup$
  • 1
    $\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 '18 at 17:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.