Before this unit: Image formation

What Is PnP? Finding Where the Camera Is from Known Points

Lesson 4 of Camera Geometry. PnP finds where a calibrated camera stood and which way it faced from points whose positions are known and where they appear in one photo. On OpenCV's chessboard photos it returns the calibration's own pose to 0.00002°, four corners are enough for 0.27 px, and with five corners found wrong a plain fit moves the camera 2.5 squares where RANSAC moves it 0.008.

Luis Condados · · 5 min read Intermediate
The board's axes drawn from the camera pose PnP recovers in one photo.
The board's axes drawn from the camera pose PnP recovers in one photo.

PnP finds where a camera stood and which way it was facing, from a few points whose positions you know and where they appear in one photograph. Below, for each photo of a chessboard, the board’s axes drawn from the pose PnP recovers, and every camera position seen from above.

A person holding a chessboard at different angles, with red, green and blue arrows drawn on the board; beside it, a dark plot where a white line stands for the board and a dot is added for each camera position
PnP on each of OpenCV’s calibration photographs. Right: each camera centre in the board’s own frame, seen from above, in units of one chessboard square; the white line is the board. Calibration images from the OpenCV samples, Apache-2.0.

You have met it wherever a camera finds itself against something it recognises:

  • AR markers such as ArUco and AprilTag: four corners of known size give the camera’s pose every frame.
  • Drone landing pads and robot docking stations carry printed patterns for the same reason.
  • Robot arms with a camera locate the part they pick from its known corners.
  • Structure from motion adds each new photo to a 3-D model by PnP against the points already built.

Given a calibrated camera and a few points of known position, PnP returns where the camera stood and which way it faced; a robust version survives points that were found wrong. On OpenCV’s chessboard photos it matches the calibration’s own pose to 0.00002°, and with five of 54 corners wrong it moves the camera 0.008 squares where a plain fit moves it 2.5.

Where we are

Lesson 4, the last, of camera geometry, and step 6 of its pipeline with one photo: KK and the lens are known from lesson 3, and only [R∣t][R \mid t] is left. Lesson 2 pushed points through a given pose; this lesson runs it the other way [1].

Six unknowns, two equations per point

A pose is six numbers: three for the rotation and three for the position. Every point of known position seen in the photo gives two equations, one per pixel coordinate, so three points are the minimum. Three points give up to four candidate poses; a fourth point picks the right one, and for a flat target four is the usual minimum. With more points, OpenCV’s default solvePnP starts from a closed-form guess and adjusts RR and tt until the reprojection error, the same score as calibration’s, is as small as it gets [1] [3].

The rotation comes back as a three-number rotation vector; cv2.Rodrigues turns it into the 3×3 RR. The camera’s position on the board is not tt itself: tt is the board’s origin seen from the camera, and the camera seen from the board is C=−R⊤tC = -R^\top t.

Left: the first chessboard photo with red, green and blue arrows from the board's first corner along its axes. Right: a dark plot from above with a white line for the board and thirteen dots for camera positions, one in amber
Left: the board’s axes, three squares long, drawn through the pose PnP recovers. Right: all thirteen camera positions from above, the first in amber. Calibration images from the OpenCV samples, Apache-2.0.

Now you try

A square tag with known corners and their observed pixels. Drag the pose by hand to bring the predicted corners onto the observed ones and watch the error fall, then press solve and let the iterative fit do it.

Try it: find the camera pose

How close is this to OpenCV?

The cells run solvePnP on the first photo’s corners with the calibrated camera, then break five corners on purpose and compare the plain fit with solvePnPRansac, which fits on small random subsets and keeps the pose most corners agree with [2].

solvePnP on the first photo
import json
import numpy as np
import cv2

d = json.load(open("corners.json"))
obj = np.zeros((54, 3), np.float32)
obj[:, :2] = np.mgrid[0:9, 0:6].T.reshape(-1, 2)
seen = np.float32(d["corners"][0]).reshape(-1, 1, 2)

# the camera, from calibration
K = np.array([[536.0735, 0, 342.3705],
              [0, 536.0164, 235.5369], [0, 0, 1]])
dist = np.array([-0.26509, -0.04674, 0.00183,
                 -0.00031, 0.25231])

ok, rvec, tvec = cv2.solvePnP(obj, seen, K, dist)
R, _ = cv2.Rodrigues(rvec)
C = (-R.T @ tvec).ravel()

def rms(rv, tv, o=obj, s=seen):
    p, _ = cv2.projectPoints(o, rv, tv, K, dist)
    return float(np.sqrt(np.mean(np.sum(
        (p - s).reshape(-1, 2) ** 2, axis=1))))

print("camera centre, in squares:", C.round(3))
print("distance:", round(float(np.linalg.norm(C)), 2))
print("misses the corners by", round(rms(rvec, tvec), 3),
      "px RMS")
Output
camera centre, in squares: [  7.371   1.647 -15.059]
distance: 16.85
misses the corners by 0.193 px RMS
Five wrong corners: solvePnP against solvePnPRansac
# five corners moved 25 to 50 px, as a bad detection would
bad = seen.copy().reshape(-1, 2)
idx = [5, 17, 23, 38, 50]
bad[idx] += np.float32([[40, -25], [-35, 30], [30, 35],
                        [-40, -20], [25, -40]])
bad = bad.reshape(-1, 1, 2)
good = np.setdiff1d(np.arange(54), idx)

def shift(rv, tv):
    Rm, _ = cv2.Rodrigues(rv)
    return float(np.linalg.norm((-Rm.T @ tv).ravel() - C))

ok, rv1, tv1 = cv2.solvePnP(obj, bad, K, dist)
print(f"solvePnP:       moves {shift(rv1, tv1):.3f}, "
      f"{rms(rv1, tv1, obj[good], seen[good]):.3f} px")
ok, rv2, tv2, inl = cv2.solvePnPRansac(
    obj, bad, K, dist, reprojectionError=3.0)
print(f"solvePnPRansac: moves {shift(rv2, tv2):.4f}, "
      f"{rms(rv2, tv2, obj[good], seen[good]):.3f} px, "
      f"{len(inl)} inliers")
Output
solvePnP:       moves 2.517, 2.987 px
solvePnPRansac: moves 0.0079, 0.198 px, 49 inliers

The camera comes back at (7.371,1.647,−15.059)(7.371, 1.647, -15.059) squares and the corners are missed by 0.19 px, the calibration’s own pose: over all thirteen photos the two never differ by more than 0.00002° or 0.00001 squares. With five corners broken, the plain fit pulls the camera 2.517 squares off and misses even the good corners by 3 px; RANSAC finds the 49 good ones and stays within 0.008 squares.

On the other twelve photos

The thirteen poses put the camera between 11.9 and 16.85 squares from the board’s first corner, all on the same side of it and spread across its width, which is what the plot above shows: the person moved the board, not the camera, and PnP reads that as the camera moving around a still board.

Where this breaks

Noise grows with distance. One pixel of random error on every corner, 200 times, moves the recovered camera by a median of 0.145 squares at 16.85 squares away, under 1%. The farther the target or the smaller it is in the photo, the larger that share, because the same pixel covers more of the world.

Few points, or points in a line. The four outer corners alone give a pose that reproduces all 54 to 0.269 px, 0.073 squares from the full fit. Three points have up to four answers, and points that all lie on one line leave the rotation about that line undetermined.

A wrong KK is a wrong pose. PnP trusts the calibration. A focal length that is off moves the camera along its line of sight to compensate, and the reprojection error barely shows it.

Exercises

  1. Where the camera stood. Write centre(rvec, tvec), the camera’s position in the board’s frame, from the rotation vector and shift solvePnP returns.
Your turn
def centre(rvec, tvec):
    return np.zeros(3)  # camera centre, board units

print(centre(rvec, tvec))
  1. Predict, then run. Solve from the four outer corners only. How far does the camera move, and how well does that pose fit all 54 corners?
  2. Noise. Add 1 px of random noise to every corner, 50 times. What is the median move?
What you should see

These come from the “Lesson 4” cells of the unit notebook, camera_geometry.ipynb, and are the same on OpenCV 4.11 (this page) and 5.0.

  1. [7.371, 1.647, -15.059], the same CC as the cell above.
  2. 0.073 squares, and 0.269 px on all 54: four corners of a flat board are enough.
  3. About 0.15 squares (0.146 in the notebook’s 50 draws; 200 draws in the repository give 0.145).

Check yourself

Four questions on this lesson. Pick an answer to see why it is right or wrong.

  1. How many unknowns does a camera pose have, and how many equations does each known point give?

  2. Five of 54 corners are moved 25–50 px. What happens to the camera position?

  3. Are the four outer corners of a flat board enough for a pose?

  4. What does PnP need that it cannot check by itself?

Next

This closes the unit. Camera geometry puts the four lessons and their numbers side by side. Structure from motion runs PnP on photo after photo to build a 3-D model, and adjusts every pose and point together at the end.

References

[1] Szeliski, R. (2022). Computer Vision: Algorithms and Applications (2nd ed.), §11.2 “Pose estimation” (p. 693), including §11.2.1 “Linear algorithms” and §11.2.2 “Iterative non-linear algorithms”. Springer. Free PDF

[2] Fischler, M. A., & Bolles, R. C. (1981). Random Sample Consensus: A Paradigm for Model Fitting with Applications to Image Analysis and Automated Cartography. Communications of the ACM, 24(6), 381–395. doi:10.1145/358669.358692

[3] OpenCV Documentation (5.0). Camera Calibration and 3D Reconstruction (calib3d): solvePnP, solvePnPRansac, Rodrigues. docs.opencv.org/5.0