Camera Models: Pinhole, Intrinsic/Extrinsic
A camera turns each 3D point into a pixel by shifting it into the camera frame and dividing by depth. One matrix per step does the work.
Why Does This Exist?
Every photo, video frame, and robot depth map starts as 3D points and ends as 2D pixels. Without a precise map between the two, a self-driving car cannot tell whether a pedestrian is 4 meters or 40 meters away, and a 3D reconstruction pipeline cannot fuse two views of one statue.
The pinhole model is that map. It says each 3D point travels in a straight line through a single center point and lands on the image plane, scaled by focal length over depth. Two matrices hold the numbers: the extrinsic matrix records where the camera sits in the world, and the intrinsic matrix records how the camera turns landed rays into pixel rows and columns.
This page covers one camera and its two matrices. Two cameras looking at one scene add a new constraint between their images, which belongs to epipolar geometry.
The one idea you need first
A linear transformation is a matrix times a vector: it rotates, scales, or shears the input but keeps the origin fixed and lines straight. Camera projection is almost one, with a single extra step. After the matrices multiply, you divide by depth, which is what makes far things small. That division is the only nonlinear part.
Think of It Like This
Tracing a statue on a window
Stand a statue behind a glass window and trace its outline with a marker while keeping your eye perfectly still. Your eye is the pinhole, the glass is the image plane, and each traced dot sits exactly where the straight line from your eye through the statue pierces the glass.
Move the statue twice as far and its tracing shrinks to half the size, because the piercing point slides toward the middle. That slide is the divide-by-depth step. A longer focal length is a window held further from your eye: the same statue traces larger.
Where the analogy stops: your eye sees in all directions at once while the model covers one rectangular frame, and real lenses bend rays the pinhole keeps straight. Calibration software estimates that bending separately.
How It Actually Works
Write a world point as and its pixel as . Projection runs in two matrix steps plus one division:
In plain words: is a rotation matrix saying which way the camera looks, is a 3-vector saying where the camera sits, and together moves the point from world coordinates into camera coordinates. is the intrinsic matrix holding focal length and optical center , and it turns camera coordinates into pixels. here means the homogeneous form so the translation can apply.
The two matrices split the unknowns cleanly. Extrinsics change every time the camera moves and stay fixed when it does not. Intrinsics stay fixed for one camera and change only when you zoom, crop, or switch lenses.
The divide that makes depth
Multiplying gives three numbers where equals the depth in front of the camera. The pixel is the division:
In plain words: each coordinate is focal length times the ratio of lateral position to depth, shifted by the center. Depth sits in the denominator, so doubling depth halves the offset from the center. That single division is why one photo cannot recover depth: many pairs give the same ratio.
Worked example
Take a camera with focal length , center , no rotation ( is the identity), and no translation (). The world point is , , .
Skip the matrices since pose is the identity and go straight to the division:
So the point lands at pixel : 125 pixels right and 250 pixels above the center. Move the same point to and the offsets halve to and , giving . Same statue, twice as far, half the size on screen.
Code
This runs the worked example with plain Python so the two steps stay visible: pose first, then intrinsics with the divide.
def project(point: tuple[float, float, float], f: float, cx: float, cy: float) -> tuple[float, float]: x, y, z = point # camera frame already: R is identity, t is zero u = f * x / z + cx v = f * y / z + cy return (u, v)
print(project((1.0, 2.0, 4.0), 500.0, 320.0, 240.0)) # -> (445.0, 490.0)print(project((1.0, 2.0, 8.0), 500.0, 320.0, 240.0)) # -> (382.5, 365.0)Both prints match the hand calculation. Doubling from 4 to 8 halves each offset from the center , which is the divide-by-depth law in one line of output.
Watch Out For
Forgetting the divide after the matrix multiply
returns , not pixels. Using directly as looks almost right for points near and drifts further off everywhere else, because you skipped dividing by . The symptom is a projection that scales with the scene instead of staying in the frame: points at land four times too far from the center.
Always divide by the third coordinate before comparing against pixels. If your reprojection error grows linearly with depth, the divide is the first line to check.
Reading the pose backwards
moves world points into the camera frame, not the camera into the world. Inverting the direction (or transposing without negating properly) places the scene behind the camera, where is negative and the image flips. The symptom is pixels mirrored across the center or a solver that converges to a camera looking away from every point.
The round-trip test catches it: send a known world point through and confirm its camera-frame is positive and its depth matches the tape measure. For reasoning about vectors and frames, see vectors, matrices, and tensors.
The Quick Version
- A pinhole camera maps to pixels with followed by a divide by depth.
- Extrinsics record camera position and direction; they change when the camera moves.
- Intrinsics hold focal length and optical center; they change only with zoom, crop, or a new lens.
- Pixel offsets from the center equal focal length times position over depth: and .
- With , point lands at , which is 125 right and 250 up from center.
- Doubling depth halves image size, so one view alone cannot recover depth.
- Always divide by the third coordinate, and check the pose sends points in front of the camera.