← Notes

Notes

Interactive

Camera Geometry Playground

Nearly everything in geometric computer vision is one equation wearing different clothes. A point in the world, a camera somewhere looking at it, and a pixel where the two meet. The equation is short enough to write on a napkin, and it takes about a semester to actually believe.

This page is eight instruments built on that one equation. Every panel is live: move a slider and the matrices, the 3D frustum, and the image plane all update from the same code path, because they are all the same computation viewed from different angles. Nothing is precomputed and nothing is faked.

Optical bench

One equation, eight ways to break it

08 bays
live Β· client-side
x̃ the pixel, in homogeneous coordinates — known only up to scale
K intrinsics: focal length, principal point, pixel shape
[R | t] extrinsics: where the camera is and which way it faces
X̃ the world point, also homogeneous
∼ equality up to scale β€” the perspective divide hides here
[ 01 ]

The projection chain, end to end

live

Left: the world, with the modelled camera drawn as a frustum. Right: what that camera records. Every number below is computed from the sliders β€” K from the lens and sensor, [R | t] from the pose, and P = K[R | t] from both. Drag inside the 3D view to orbit; that moves your viewpoint, not the camera being modelled.

world Β· orbit to look around
image plane Β· 640 Γ— 480

Position Cm

Cx
Cy
Cz

Orientationdeg

yaw about Y
pitch
roll optical axis

Lens & sensor

f focal length
sensor width
cx principal pt
cy
perspective β†’ ortho
fxβ€”
horiz. fovβ€”
centre Cβ€”
K =
[R | t] =
P = K[R | t] =
ptXYZuvdepth

hover a table row to find that point in the imagered rows are behind the camera

  • Push Cz until points go red: they are behind the image plane, and the projection returns a pixel that is geometrically meaningless.
  • Slide principal point and watch only the last column of K move β€” it translates the image, nothing else.
  • Drag perspective β†’ ortho to 1. The cube's far face stops being smaller than its near face.
Takeaway P has 11 degrees of freedom, not 12 β€” it is only defined up to scale, which is exactly what the ∼ in the governing relation is telling you. Multiply every entry of P by two and every pixel stays where it was.
[ 02 ]

Why a longer lens sees less

live

Focal length is a distance, not an angle β€” but it induces an angle, because the sensor has a fixed width. The half-angle of the wedge on the left is atan(W / 2fβ‚“). Doubling f halves the tangent of that angle, so the wedge closes and fewer posts survive into the frame.

top-down Β· view wedge
what the camera sees

Lens

f on a 36 mm sensor

Compensation

With this on, the camera walks backwards as you zoom in, keeping the amber ring the same size. The subject is pinned β€” but the background compresses dramatically. That is the Vertigo shot, and it is pure geometry.

focal lengthβ€”
fxβ€”
horiz. fovβ€”
vert. fovβ€”
distanceβ€”
view wedge subject at z = 0 post in frame post outside frame
Takeaway "Zoom" and "move closer" are not the same operation. Zooming changes f and leaves [R | t] alone; walking closer changes [R | t] and leaves f alone. They produce the same subject size and completely different backgrounds β€” which is why perspective is a property of position, never of the lens.
[ 03 ]

The two matrices everybody mixes up

live

[R | t] is world-to-camera. Its translation column t is not where the camera is β€” it is where the world origin lands in camera coordinates. The camera centre is C = βˆ’Rα΅€t, and the pair [Rα΅€ | C] is the camera-to-world transform. Watch the two panels disagree as you move the camera. Drag the amber point.

top-down Β· Y = 0 slice

Camera pose

Cx
Cz
yaw

Point P

P in worldβ€”
P in cameraβ€”
depth (Zcam)β€”

Compare

t (w2c column)β€”
C = βˆ’Rα΅€tβ€”
world β†’ camera  [R | t] =
camera β†’ world  [Rα΅€ | C] =
  • Set Cx = 2.5, Cz = 3.5, yaw = 0. Note that t reads roughly (βˆ’2.5, 0, 3.5) while C reads (2.5, 0, 3.5) β€” same numbers, different signs and different meaning.
  • Now spin yaw. C does not move at all; t swings wildly. The camera stayed put β€” only the frame it reports coordinates in rotated.
Takeaway If your reconstruction is mirrored, inside-out, or orbiting the wrong point, you have almost certainly fed a camera-to-world matrix into something expecting world-to-camera. The two agree only when the camera sits at the origin with zero rotation, which is exactly the case people test with.
[ 04 ]

The part K cannot express

live

A projective camera maps straight lines to straight lines. Real glass does not. Distortion is applied to the normalised coordinates, after [R | t] and before K β€” which is why it is radially symmetric about the principal point rather than about the image centre. Move cx in bay 01 and you would see the whole distortion pattern travel with it.

image plane

Radial

k₁
kβ‚‚
k₃

Tangential

p₁
pβ‚‚

Presets

Overlays

max displacementβ€”
distortion vector [k₁ kβ‚‚ k₃ p₁ pβ‚‚]
  • Set k₁ negative for barrel (GoPro, wide phone lenses); positive for pincushion (older telephotos).
  • Turn on the displacement field and note the arrows grow with rΒ² β€” the corners move far more than the centre. Distortion is a corner problem.
  • Try Tilted lens: the pattern loses its symmetry entirely. That is what p₁, pβ‚‚ buy you, and why they are small in a well-built lens.
Takeaway Distortion is not invertible in closed form β€” undistorting is a fixed-point iteration, not a formula. That is why calibration toolboxes hand you a remap table rather than an inverse matrix, and why the honest place to compare a model against measurements is always in distorted pixel space.
[ 05 ]

Break the calibration, then watch it heal

live

The amber circles are measurements β€” pixels a real camera recorded, fixed forever. The cyan dots are where your current parameters say those points should have landed. The red segments between them are the residuals, and their RMS is the single number every calibration routine exists to minimise. Perturb anything, then press Re-calibrate and watch Gauss–Newton crawl back.

image plane Β· residuals

Intrinsic error

Ξ” focal length
Ξ” cx
Ξ” cy

Extrinsic error

Ξ” yaw
Ξ” pitch
Ξ” roll
Ξ” range
rms reprojectionβ€”px
worst pointβ€”px
observationsβ€”
stateβ€”
observed pixel predicted by current model residual
  • Nudge Ξ”cx to +30. RMS reads almost exactly 30 px, and every residual points the same way β€” a pure translation of the image.
  • Now try Ξ” focal length. The residuals fan outward from the principal point, growing with radius. Different error, different signature.
  • Trade Ξ” range against Ξ” focal length β€” you can cancel most of one with the other. That is the focal/distance ambiguity, and it is why calibrating from a single flat target is fragile.
  • Press Scramble, then Re-calibrate. Nine parameters, ~78 observations, and the error falls back to numerical zero in a few dozen steps.
Takeaway Reprojection error is measured in pixels, in the image, where the measurements actually live β€” never in world units. A sub-pixel RMS on a bad configuration can still hide a badly wrong camera, because several parameters trade off against each other. Low error is necessary, not sufficient.
[ 06 ]

A plane collapses depth into one 3Γ—3

live

For points on a plane, depth is redundant β€” it is determined by the other two coordinates. So the map between two images of that plane needs no 3D at all: a single H = K'(R + t nα΅€/d)K⁻¹ takes every pixel of the plane in view A to its pixel in view B. The green dashed grid in view B is view A's pixels pushed through H. It lands exactly on the real one.

view A
view B

Rig

camera separation
plane tilt

Overlays

Hup to scale

off-plane missβ€”
baselineβ€”
true projection in B H Β· (view A pixels) where H fails, off the plane
  • The green and amber grids stay locked together at every separation and every tilt. That is the homography being exact, not approximate.
  • The red cube is the same H applied to points that are not on the plane. It smears badly, and the miss grows with baseline β€” that residual is precisely the parallax a structure-from-motion pipeline feeds on.
  • Drop separation toward 5Β°. The red cube converges onto the real one: with almost no baseline, everything looks planar. This is why panorama stitching works handheld and fails when you walk.
Takeaway Two images are related by a homography in exactly two situations: the scene is planar, or the camera only rotated. Both kill parallax. If you can stitch two photos with a homography and get no ghosting, you have learned something real about the scene or the motion β€” not just about your stitcher.
[ 07 ]

One pixel in A is a line in B

live

Move your cursor over either image. A single pixel fixes a ray in space but not a point on it β€” so its match in the other view can be anywhere along that ray's image, which is a straight line: l' = F x. Every such line passes through the epipole (green cross), the image of the other camera's centre. This is what turns matching from a 2D search into a 1D one.

view A hover me
view B hover me

Stereo rig

separation
elevation
f of camera B

Uncheck to make the two cameras parallel. The epipoles shoot off to infinity and every epipolar line becomes horizontal β€” that is a rectified pair, and it is the whole reason stereo matchers rectify first.

Fup to scale

baselineβ€”
det Fβ€”
epipolar line epipole det F = 0 always β€” F has rank 2
  • Hover a cube corner in A and find the same corner in B. It sits on the red line, always. That is the epipolar constraint doing its job.
  • Sweep your cursor around A and watch every line in B pivot about one point. That pencil of lines is the epipole. At moderate separations it sits outside the frame β€” push separation past ~130Β° and it walks into view as a green cross.
  • Uncheck converge. All lines go horizontal and the epipole leaves the frame. Notice det F stays pinned at zero throughout β€” rank 2 is structural, not incidental.
Takeaway F knows nothing about where things are β€” only about how the two cameras are arranged. It is 7 degrees of freedom (9 entries, minus scale, minus det F = 0) and can be estimated from point matches alone, with no calibration whatsoever. That is why it is the first thing a structure-from-motion pipeline computes.
[ 08 ]

Why depth is always the noisy direction

live

Two rays that nearly agree on direction intersect at a badly determined point. Each red dot is one triangulation from observations jittered by Οƒ pixels of noise; the ellipse is the 2Οƒ contour of the resulting cloud. It is almost never a circle β€” it is a sliver, and the sliver points along the line of sight.

top-down Β· Monte Carlo over pixel noise

Geometry

baseline b
depth Z

Measurement

pixel noise Οƒ

First-order estimate

2ΟƒZ β‰ˆ 2√2Β·ZΒ²Οƒ / (fxΒ·b)β€”

The textbook form is Οƒ_Z β‰ˆ ZΒ²Οƒ_d/(fΒ·b) with Οƒ_d the noise on the disparity. Both views are measured independently here, so Οƒ_d = Οƒβˆš2 β€” and the panel draws a 2Οƒ contour, hence the extra factor of two.

spread along rayβ€”
spread across rayβ€”
anisotropyβ€”
baselineβ€”
  • Collapse baseline toward 0.15 m. The ellipse stretches enormously β€” the two rays become nearly parallel and their intersection stops being localised at all.
  • Double depth. The spread grows roughly four-fold, not two-fold: depth error goes as ZΒ². This is why a stereo rig that is excellent at 3 m is useless at 30 m.
  • Compare the measured spread to the first-order estimate in the rail. They track closely until the baseline gets small, where the linearisation stops being honest.
Takeaway Depth precision scales as ZΒ²Οƒ / (fΒ·b) β€” quadratic in range, and inversely proportional to baseline and focal length. You cannot fix a short baseline with a better feature detector: halving Οƒ buys a factor of two, while doubling b buys the same factor and costs nothing but geometry. Arrange the cameras well before you tune the matcher.

β€” end of bench β€”

What is actually running here

There is no server, no build step and no precomputed data. camera-geom.js holds the geometry β€” projection, distortion, homographies, the fundamental matrix, DLT triangulation, and a small Jacobi eigensolver so the null-space and covariance work needs no linear algebra dependency. Every panel above calls into that one module, which is why the matrices and the pictures can never disagree: they are literally the same numbers.

The conventions are fixed throughout, and stated here because half of all camera-geometry bugs are convention bugs: the world is right-handed and Y-up; the camera frame is x-right, y-down, z-forward; [R | t] is world-to-camera; and a camera with zero yaw, pitch and roll looks along world βˆ’Z. Distortion is applied to normalised coordinates, between [R | t] and K.

For the underlying theory, Foundations of Computer Vision (Torralba, Isola and Freeman) is the reference I would point at first β€” chapter 5 builds the pinhole model from similar triangles, which is where all of this starts.