Linear Triangulation
Triangulation reconstructs a 3D point from its projections in two or more calibrated views. It is the inverse of the projection operation: given pixel observations and camera poses, recover the world point.
Problem Statement
Given: views with poses (world to camera ) and, in each, the bearing along which the point is seen: the unit ray from the camera's own unprojection of the observed pixel.
Find: 3D point in the world frame such that .
Assumptions:
- Camera poses and models are known
- Correspondences are correct
- The point is visible from all views (not occluded)
Working on bearings rather than on image points serves every camera model: a pinhole's bearing is of its normalized coordinates, and a fisheye's may point more than 90° from its axis, where normalized coordinates do not exist.
Derivation (DLT)
With and the point , the constraint says is parallel to , i.e. their cross product vanishes:
This gives three equations per view, of rank two. Stacking all views gives a system:
Solve via the smallest eigenvector of (the smallest-singular direction): is the right singular vector corresponding to the smallest singular value. Dehomogenize: . A near-zero second-smallest singular value (identical cameras, coincident rays) is reported as a degenerate system.
Limitations
- Algebraic error: The DLT minimizes algebraic error, not geometric (reprojection) error, and carries no covariance
- Baseline sensitivity: For small baselines (nearly parallel views), the triangulation is poorly conditioned — small pixel errors lead to large depth errors
- Outlier sensitivity: A single incorrect correspondence corrupts the result; no robustness mechanism
For accurate points, refine them on the reprojection error with their noise stated: the calibrated entry point, mvg::triangulation::triangulate, does so, and returns the points' covariance with the calibration's share (see Pose Recovery and Triangulation). The linear estimate is its starting point.
API
#![allow(unused)] fn main() { let X = triangulate_point_linear(&cam_se3_world, &bearings)?; }
Where cam_se3_world holds each view's Iso3 pose and bearings the matching rays (any nonzero length). The calibrated entry point, mvg::triangulation::triangulate, builds the bearings from pixels through a CalibratedRig, refines the points and adds diagnostics and covariance; see Pose Recovery and Triangulation.
In Python, vision_calibration.mvg.triangulate is the calibrated entry point; the raw linear solver is Rust only (numpy and OpenCV cover linear algebra on plain arrays).
Deterministic two-view solvers, all free functions in per-topic modules:
| Module | Functions |
|---|---|
homography | dlt_homography, dlt_homography_ransac |
epipolar | fundamental_8point, essential_5point |
triangulation | triangulate_point_linear |
camera_matrix | dlt_camera_matrix |
The facade re-exports the main solvers at the root of its mvg module (vision_calibration::mvg::dlt_homography, …); the full set is in the vision-geometry crate.