Triangulation from Two Image Rays
Each camera turns a pixel into a ray in space. Triangulation is where those two rays come closest, and that closest point is the 3D answer.
Why Does This Exist?
Matches alone are flat. Given a left pixel, a right pixel, and known cameras, something must lift the pair back into 3D, and that lifter is triangulation. Every structure from motion pipeline, every stereo vision depth map, and every SLAM map point passes through it. Without it there are correspondences but no shape.
This page covers the two-view lift and its residual. The match constraint lives in the fundamental matrix; the joint refinement over many points lives in bundle adjustment.
Think of It Like This
Two laser pointers crossing in fog
Two friends shine laser pointers at the same balloon in fog. Each beam alone says direction only; where the beams cross says distance. When the fog jitters, the beams miss by a hair and the crossing becomes the shortest bridge between them.
Pixels are laser directions and triangulation is that bridge midpoint. The analogy stops at calibration: friends aim freely while cameras need known poses before any beam means anything.
How It Actually Works
A calibrated camera writes with , where is the homogeneous pixel and the homogeneous 3D point. The direct linear transform builds two rows per view from the cross product and solves the stacked system by SVD, taking the smallest singular vector as . In noise the rays skew, so the midpoint of the common perpendicular replaces the exact crossing.
Quality is reported as reprojection residual: project back into each view and measure pixel distance . Then comes the cheirality check: keep only when its camera-frame depth is positive in both views.
Worked numbers
Use , center , left camera at the origin and right camera at m. Left pixel gives ray slope ; right pixel from gives relative to its own center. Solving gives , so m and m. Reprojecting returns exactly , residual px; shifting to moves the residual to about px and depth to m.
Code
def triangulate_depth(u1: float, u2: float, f: float, cx: float, baseline: float) -> float: slope1 = (u1 - cx) / f # left ray direction X / Z slope2 = (u2 - cx) / f # right ray direction relative to its center return baseline / (slope1 - slope2) # depths where the rays meet
print(triangulate_depth(480.0, 400.0, 800.0, 320.0, 0.2)) # -> 2.0Watch Out For
Triangulating points at tiny parallax
When the two rays are nearly parallel, a one-pixel shift moves depth by meters. The symptom is far points with huge variance that still report sub-pixel residuals. Reject points whose parallax angle sits under about one degree, or down-weight them heavily in the next optimization.
Skipping the cheirality test
Algebra returns an even when it sits behind a camera, and its reprojection error can look excellent. Downstream code then fuses ghost geometry. Always transform into each camera frame and require twice.
The Quick Version
- Triangulation lifts one match plus two known cameras into one 3D point.
- The linear method stacks rows and solves by SVD.
- Real rays skew, so the answer is the midpoint of their common perpendicular.
- Reprojection residual in pixels is the quality number to watch.
- Pixels and with and baseline m meet at m.
- Tiny parallax and negative depth are the two traps to filter.