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 and the wider two-view setup in multi-view geometry; 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
The projection equation
A calibrated camera writes with , where is the homogeneous pixel, is the homogeneous 3D point, holds the focal length and center , and is the camera pose. Back-projection inverts the direction half: pixel becomes the ray in the camera frame, a line of candidate depths through the optical center. One ray says direction, never distance; distance needs the second ray.
The linear solve and the midpoint
The direct linear transform builds two rows per view from the cross product and stacks them into a system solved by SVD, taking the smallest singular vector as . In exact arithmetic the rays cross; in noise they skew past each other, so the answer becomes the midpoint of their common perpendicular, the shortest bridge between the lines. 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 : left , right , residual px. Shifting to moves the slope to and the depth to m, a 3 cm move from a 1 px nudge, which is why far points wobble.
Code
The point of this snippet is the closed-form depth for the rectified pair in the worked example: slope difference in the denominator, baseline on top.
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.0print(round(triangulate_depth(480.0, 401.0, 800.0, 320.0, 0.2), 4))# -> 2.0253The first print replays the m answer. The second shows one extra pixel pushing depth to m: tiny parallax converts pixel noise into meters.
Watch 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.