3D Vision Basics: SfM, Point Clouds, and Pose Estimation
In this series (30 parts)
- Computer Vision Roadmap: From Basics to Real Projects
- What is Computer Vision? From Pixels to Decisions
- A Short History of Computer Vision: 1545 to Now
- Math for CV Beginners: Vectors, Matrices, Convolutions
- Image Fundamentals: Color, Histograms, Noise, Filtering
- How Images and Video Are Stored: Colour, JPEG, Frames
- OpenCV Setup + First 10 Tasks in Python
- Vision Metrics: Accuracy, Precision, Recall, mAP, IoU
- CV Project Workflow: Dataset, Baseline, Error Iteration
- Intensity Transforms and Frequency-Domain Filtering
- Edge Detection and Thresholding That Actually Work
- Morphology, Contours, and Shape Analysis for Real Images
- Feature Matching (SIFT/ORB) and Image Stitching
- Camera Calibration and Perspective Correction
- Epipolar Geometry, Stereo Vision, and Depth Estimation
- Optical Flow and Motion Tracking in Video
- HOG, HOF and MBH: Descriptors Before Deep Learning
- Your First Image Classifier (PyTorch + Transfer Learning)
- Data Pipelines and Augmentation for Vision Models
- CNN Architectures Explained: From LeNet to ResNet
- YOLO Detection Pipeline: Data to Inference
- Semantic and Instance Segmentation: U-Net to Mask R-CNN
- Vision Transformers (ViT) and When to Use Them
- CV Explainability: Grad-CAM, Failures, Bias Checks
- 3D Vision Basics: SfM, Point Clouds, and Pose Estimation
- Multimodal Vision: CLIP, Embeddings, and Retrieval Systems
- Video Understanding: Flow Networks, Interpolation, Stabilisation
- Real-Time CV Systems: Tracking, Latency, Streaming
- Deploying CV Models: ONNX, TensorRT, Edge, and APIs
- Responsible CV: Privacy, Fairness, Security, Governance
A construction site needs to know how much gravel is in a stockpile. The old way is a surveyor with a total station, half a day, and a bill. The new way is a drone flying a grid pattern for eight minutes, then software that turns 40 overlapping photos into a 3D surface and reports 2,847 cubic metres.
Prerequisites: Epipolar geometry and stereo vision and camera calibration.
Two different questions
| # | Question | You know | You want | Method target |
|---|---|---|---|---|
| 1 | Where is my camera? | 3D points and their image positions | camera pose | PnP |
| 2 | Where are the points? | camera poses and image positions | 3D points | Triangulation |
| 3 | Both, from nothing? | just overlapping photos | poses and points | Structure from Motion |
| 4 | How do two scans line up? | two point clouds | rigid transform between them | ICP |
Four problems in 3D vision. SfM is the hard one because it solves for both unknowns at once.
Stereo vision from the previous post assumed you knew the baseline and both cameras’ orientations. SfM drops that assumption. A drone photographs a stockpile from 40 unknown positions, and the software works out both where the drone was and what it was looking at.
The SfM pipeline
flowchart TD
A["40 overlapping photos"] --> B["Detect features<br/>~2,000 SIFT keypoints each"]
B --> C["Match pairs<br/>ratio test + RANSAC"]
C --> D["Build feature tracks<br/>same point across many images"]
D --> E["Initialise: pick best pair<br/>estimate relative pose"]
E --> F["Triangulate initial points"]
F --> G["Add next camera via PnP"]
G --> H["Triangulate new points"]
H --> I["Bundle adjustment<br/>refine everything"]
I --> J{"Cameras left?"}
J -->|yes| G
J -->|no| K["Sparse cloud: ~18,500 points"]
K --> L["Multi-view stereo<br/>dense reconstruction"]
L --> M["Dense cloud: ~12M points"]
M --> N["Mesh / height map / volume"]
Every step before bundle adjustment is feature matching applied at scale. The new part is doing it consistently across 40 images instead of 2.
feature track One physical 3D point, followed through every image it appears in. A track spanning 12 images gives 12 constraints on that single point's position.Track length is the quality signal that matters. A point seen in 2 images is barely constrained and often wrong. A point seen in 12 images is solid.
| # | Track length | Number of tracks | Median reprojection error target | Keep? |
|---|---|---|---|---|
| 1 | 2 images | 41200 | 3.84 px | no — discard |
| 2 | 3 images | 12800 | 1.92 px | marginal |
| 3 | 4–6 images | 9400 | 0.71 px | yes |
| 4 | 7–11 images | 6300 | 0.44 px | yes |
| 5 | 12+ images | 2800 | 0.31 px | yes, these anchor the reconstruction |
Track length against accuracy on a 40-image job. Two-image tracks are 12x worse than 12-image tracks.
Bundle adjustment
The heart of SfM. Adjust every camera pose and every 3D point simultaneously so that projected points land as close as possible to the observed image points.
where is 1 if point was observed in image .
reprojection error The pixel distance between where a 3D point projects into an image and where it was actually observed. It is the only error measure available, since the true 3D positions are unknown.One reprojection error, computed. A 3D point projects to using the current pose estimate. It was observed at :
Bundle adjustment nudges the pose and the point to shrink that, summed over every observation.
The size of the problem
| # | Quantity | Count | Parameters each | Total target |
|---|---|---|---|---|
| 1 | Cameras | 40 | 6 (3 rotation + 3 translation) | 240 |
| 2 | 3D points | 18500 | 3 (x, y, z) | 55500 |
| 3 | Total unknowns | 55740 | ||
| 4 | Observations | 142000 | 2 (u, v residual) | 284000 |
A modest 40-image reconstruction. 284,000 equations for 55,740 unknowns — over-constrained by about 5x.
Over-constrained is what you want. Five times more equations than unknowns means noise averages out rather than being absorbed into a perfect but meaningless fit.
The problem is only solvable because the Jacobian is extremely sparse. Point 4,201 was seen by 6 of the 40 cameras, so its rows are zero for the other 34. Solvers exploit that structure with the Schur complement, which is why a 55,740-parameter optimisation runs in seconds rather than hours.
Scale: the thing everyone forgets
SfM cannot recover absolute scale. A large stockpile photographed from far away and a small model photographed from close up produce mathematically identical reconstructions.
scale ambiguity A reconstruction from images alone is correct up to an unknown multiplicative factor. Shapes and ratios are right; sizes are not.The fix is one object of known length in the scene. Place a 1.200 m survey target, then measure its two endpoints in the reconstruction:
Multiply every coordinate by 31.81 and the model is in metres. Every length, area, and volume you compute afterwards inherits this one number’s accuracy.
| # | Scale source | Typical accuracy target | Effort | Note |
|---|---|---|---|---|
| 1 | Known-length target in scene | ±0.5% | low | Measure it carefully — error propagates to everything |
| 2 | Multiple ground control points with GPS | ±0.02 m absolute | high | Also fixes position and orientation |
| 3 | Drone GPS from image EXIF | ±2–5 m absolute | none | Fine for scale, useless for precision |
| 4 | RTK GPS on the drone | ±0.02 m | moderate | Standard for commercial survey work |
| 5 | Nothing | unknown | none | Reconstruction is unitless |
Ways to give a reconstruction real units.
Pose estimation with PnP
Once you have 3D points, finding a new camera’s position is a separate, much easier problem.
PnP (Perspective-n-Point) Given n known 3D points and their observed 2D image positions, find the camera's rotation and translation.The counting argument is worth understanding. A camera pose has 6 degrees of freedom. Each observed point gives 2 equations, so 3 points give 6 equations for 6 unknowns — exactly determined. But the geometry admits up to 4 valid solutions, so a 4th point picks the right one.
| # | Method | Minimum points target | Handles | Use for |
|---|---|---|---|---|
| 1 | P3P | 3 (+1 to disambiguate) | calibrated camera | RANSAC hypothesis generation |
| 2 | EPnP | 4 | calibrated camera | Fast, good general default |
| 3 | DLT | 6 | uncalibrated — solves K too | When intrinsics are unknown |
| 4 | Iterative (LM) | 4 | refines an initial guess | Final polish after RANSAC |
PnP variants. In practice you run P3P inside RANSAC, then refine with iterative on the inliers.
ok, rvec, tvec, inliers = cv2.solvePnPRansac(
object_points, # (N,3) 3D points in world coords
image_points, # (N,2) their observed pixel positions
K, dist_coeffs,
reprojectionError=3.0, # inlier threshold in pixels
flags=cv2.SOLVEPNP_EPNP)
R, _ = cv2.Rodrigues(rvec) # 3x3 rotation matrix
camera_position = -R.T @ tvec # camera centre in world coordinates
That last line catches nearly everyone. tvec is not the camera’s position. It is the translation part of the world-to-camera transform, so the camera centre in world coordinates is .
Aligning two clouds with ICP
The site is scanned in March and again in June. To compare them, they must be in the same coordinate frame.
ICP (Iterative Closest Point) Repeatedly pair each source point with its nearest target point, solve for the rigid transform that best aligns those pairs, apply it, and repeat until the change is small.One iteration, worked. Source points and target points , in 2D for clarity:
| # | Pair | Source P | Target Q |
|---|---|---|---|
| 1 | 1 | (1.0, 1.0) | (4.0, 2.0) |
| 2 | 2 | (2.0, 1.0) | (5.0, 2.0) |
| 3 | 3 | (1.5, 2.0) | (4.5, 3.0) |
Three matched point pairs.
Step 1 — centroids.
Step 2 — centre both sets.
| # | Pair | P − P̄ | Q − Q̄ |
|---|---|---|---|
| 1 | 1 | (−0.500, −0.333) | (−0.500, −0.333) |
| 2 | 2 | (0.500, −0.333) | (0.500, −0.333) |
| 3 | 3 | (0.000, 0.667) | (0.000, 0.667) |
The centred sets are identical, so the cross-covariance matrix is symmetric and its SVD gives R = I.
Step 3 — rotation. The SVD of the cross-covariance yields , no rotation.
Step 4 — translation.
Apply it and the clouds coincide exactly. Real data is never this clean — the correspondences are wrong on the first iteration, so the transform is approximate, which makes the next round of correspondences better, and so on.
| # | ICP iteration | Mean point-to-point distance (m) target | Correspondences correct |
|---|---|---|---|
| 1 | 0 (initial) | 2.847 | 31% |
| 2 | 1 | 0.912 | 58% |
| 3 | 2 | 0.341 | 79% |
| 4 | 4 | 0.088 | 94% |
| 5 | 8 | 0.031 | 98% |
| 6 | 16 | 0.029 | 98% |
ICP convergence. It plateaus at 0.029 m, which is the actual sensor noise floor — no further iterations help.
ICP finds a local minimum only. If the two clouds start far apart or badly rotated, it converges confidently to the wrong answer. Give it a rough initial alignment from GPS, a manual click of three corresponding points, or a global method like FPFH feature matching.
| Property | Photogrammetry (SfM) | LiDAR | Depth camera |
|---|---|---|---|
| Hardware cost | any camera | £3,000–£100,000 | £150–£500 |
| Works outdoors in sun | yes | yes | no (IR washes out) |
| Works on textureless surfaces | poorly | yes | yes |
| Absolute scale | needs a reference | built in | built in |
| Range | any, with enough baseline | 50–300 m | 0.5–8 m |
| Colour | yes, native | no (needs a camera too) | usually |
| Processing time | minutes to hours | near real time | real time |
From point cloud to cubic metres
The deliverable. Grid the cloud into a height map, take the height above a base plane in each cell, and sum.
Cell size 0.5 × 0.5 m, so each cell covers . Heights above the base plane:
Row sums: , , , . Total height sum:
Scaled up to the real stockpile’s 11,388 cells the same arithmetic gives 2,847 m³.
The base plane is where the judgement lives. A stockpile sits on ground that is rarely flat, so fitting a plane through the surrounding terrain points — rather than assuming — is what separates a usable number from a guess.
| # | Base plane choice | Volume (m³) | Error vs surveyed truth target |
|---|---|---|---|
| 1 | z = 0 (assumed flat) | 3104 | +9.0% |
| 2 | Mean height of surrounding ring | 2891 | +1.5% |
| 3 | Least-squares plane through ring | 2847 | 0.0% (reference) |
| 4 | Triangulated terrain surface | 2839 | −0.3% |
Same point cloud, four base-plane assumptions, a 265 cubic metre spread. The model was never the uncertain part.
- The surfaces have texture — rock, soil, vegetation, buildings
- You can fly or walk a path with 75%+ overlap
- You need colour along with geometry
- Hardware budget matters more than turnaround time
- Surfaces are smooth, glossy, or uniformly coloured — no features to match
- You need results in real time
- The scene moves while you capture it
- You cannot place any scale reference and need absolute measurements
Practice task
Use COLMAP, which is free, and any object you can walk around.
- Take 40 photos with clear overlap. Include a ruler or a measured object in view.
- Run COLMAP’s automatic reconstruction. Note how many of the 40 cameras it registers.
- Read the reported mean reprojection error. Anything above 1.5 px means something is wrong.
- Plot the histogram of track lengths. Compare it against the table above.
- Measure your reference object in the reconstruction and compute the scale factor.
- Verify by measuring a second known distance after scaling. How far off is it?
- Redo the capture with only 20 photos. Compare registered cameras and reprojection error.
- Export the dense cloud, grid it into a height map, and compute a volume two ways: assuming a flat base, and fitting a plane. Compare.
Step 7 is the one that changes how you capture. The failure with 20 photos is usually not graceful degradation — it is whole cameras failing to register and the reconstruction splitting into disconnected pieces.
Summary
SfM recovers camera poses and 3D structure together from unposed photos. Feature tracks are the raw material, and track length predicts accuracy directly: 2-image tracks had 3.84 px reprojection error, 12-image tracks had 0.31 px. That is the entire argument for 80% overlap.
Bundle adjustment refined 55,740 parameters — 40 cameras at 6 DOF plus 18,500 points at 3 — against 284,000 residuals. One reprojection error worked out to 2.746 px from a 2.3 and 1.5 pixel offset.
The reconstruction has no scale until you give it one. A 1.200 m target measuring 0.03772 units gave a factor of 31.81 m per unit, and because volume scales cubically, a 1% error there is 3% on the volume.
PnP finds a new camera’s pose from known 3D points, needing 3 plus 1 to disambiguate — and tvec is not the camera position, is.
ICP aligned two scans by alternating correspondence and transform: centroids at and gave . On real data it converged from 2.847 m to 0.029 m in 8 iterations.
The volume came from height times cell area: 9.1 m of summed height over cells of 0.25 m² gave 2.275 m³. The base plane choice moved the full-site answer by 265 m³.
- SfM solves for camera poses and 3D points simultaneously from ordinary overlapping photos.
- Track length is the accuracy driver: 2-image tracks are 12x worse than 12-image tracks.
- Fly 75-80% overlap. The extra minutes are far cheaper than a failed reconstruction.
- Bundle adjustment is huge but sparse — 55,740 parameters solve in seconds via the Schur complement.
- Reprojection error is the only available error measure, since true 3D positions are unknown.
- SfM output is unitless. One known length gives the scale factor for the whole model.
- Volume scales as the cube, so a 1% scale error is a 3% volume error.
- PnP needs 3 points plus 1 to disambiguate; camera position is −Rᵀt, not tvec.
- ICP converges to a local minimum only — it needs a rough initial alignment to be trusted.
- The base plane choice moved a stockpile volume by 9%. The model was never the uncertain part.
What comes next
Everything so far has connected pixels to geometry. Multimodal vision connects pixels to language instead — putting images and text in one shared space so you can search a photo library by typing a description, and classify things the model was never explicitly trained on.