TAI BUI
Back to curriculum
BuildPython

Monocular Depth & Geometry Estimation

A depth map is a single-channel image where every pixel is a distance from the camera. Predicting one from a single RGB frame used to be impossible without stereo or LiDAR. In 2026 a frozen ViT encoder plus a lightweight head gets within a few percent of ground truth.

TIME: ~60 minutesPREREQUISITES: Phase 4 Lesson 14 (ViT), Phase 4 Lesson 17 (Self-Supervised Vision), Phase 4 Lesson 07 (U-Net)

Learning Objectives

  • Distinguish relative and metric depth and state which one each production model (MiDaS, Marigold, Depth Anything V3, ZoeDepth) solves
  • Use Depth Anything V3 (DINOv2 backbone) to predict depth for arbitrary single images with no calibration
  • Explain why monocular depth works at all from a single image (perspective cues, texture gradients, learned priors) and what it cannot recover (absolute scale, occluded geometry)
  • Lift 2D detections to 3D points using a depth map and pinhole camera intrinsics

The Problem

Depth is the missing axis in 2D computer vision. RGB tells you where something sits in the image plane, not how far away it is. Depth sensors (stereo rigs, LiDAR, time-of-flight) solve that directly, but they're expensive, fragile, and limited in range.

Monocular depth estimation, predicting depth from a single RGB frame, used to produce blurry, unreliable output. By 2026 large pretrained encoders changed that. Depth Anything V3 uses a frozen DINOv2 backbone and produces depth maps that generalise across indoor, outdoor, medical, and satellite domains. Marigold reframes depth as a conditional diffusion problem, and ZoeDepth regresses true metric distances.

Depth is also the bridge between 2D detection and 3D understanding: multiply a detected box's pixels by its depth value and you've lifted a 2D object into a 3D point cloud. That's the core of every AR occlusion system, every obstacle-avoidance pipeline, and every "pick up the cup" robot.

Warm-up0/3
1. Stereo rigs, LiDAR, and time-of-flight sensors solve one specific problem that a plain RGB camera doesn't. What is it?Warm-up
An RGB camera and a depth sensor labelled stereo, LiDAR or ToF, both pointed with dashed lines at the same object, with the camera's output given as a color image and the sensor's output left as a question mark.
2. A depth estimate is off by 1 m. For which true distance is that mistake the largest relative error?Warm-up
Three panels, each giving a true distance of 2, 10 or 50 metres with the same 1 metre miss, and each leaving its relative error as a question mark.
3. You attach a small new head to a large pretrained network and keep the pretrained weights fixed while training. What is the main practical benefit?Warm-up
A pretrained network labelled weights kept fixed with a locked-parameters band and an arrow into a small new head box labelled trains, with the main practical benefit of training this way left as a question mark.
Check: The Problem0/2
1. What is monocular depth estimation?Define
A dashed question mark box standing for the input, an arrow into a box labelled monocular depth estimation, then an arrow into a shaded depth map grid, with a note that the output side is one distance per pixel.
2. Why does multiplying a detected box's pixels by its depth value matter for 3D applications?Explain
A dashed 2D detection box around an object with its centre pixel marked and the depth at that pixel noted as d, with an arrow into a dashed question mark box, under a line asking what adding depth gives beyond pixel position.

The Concept

Relative vs metric depth

  • Relative depth: ordered z values without a real-world unit. "Pixel A is closer than pixel B" holds, but the ratio between them isn't anchored to metres.
  • Metric depth: absolute distance in metres from the camera. The model has to have learnt the statistical relationship between image cues and real-world distance.

MiDaS and Depth Anything V3 produce relative depth, and so does Marigold. ZoeDepth, UniDepth, and Metric3D produce metric depth instead. Metric models are sensitive to camera intrinsics; relative models aren't.

The encoder-decoder pattern

Mermaid diagram

Depth Anything V3 freezes the encoder and trains only a DPT-style decoder (DPT: Dense Prediction Transformer, a convolutional head built for exactly this job). The encoder supplies rich features, and the decoder interpolates them back to image resolution and regresses depth.

Why a single image produces depth at all

A 2D image carries several monocular cues that correlate with depth:

  • Perspective: parallel lines in 3D converge in 2D.
  • Texture gradient: surfaces far away have smaller, denser texture.
  • Occlusion order: nearer objects occlude farther ones.
  • Size constancy: known objects (cars, humans) give approximate scale.
  • Atmospheric perspective: distant objects look hazier and bluer in outdoor scenes.

A ViT trained on billions of images internalises these cues on its own. Given enough data and a strong backbone, monocular depth hits reasonable accuracy with no explicit 3D supervision at all.

What monocular depth cannot do

  • Absolute metric scale without intrinsics or a known object in the scene. The network can predict "the cup is twice as far as the spoon" without knowing whether the cup is 1 m or 10 m away.
  • Occluded geometry: the back of a chair is unseen and can't be inferred reliably.
  • Truly untextured or reflective surfaces: mirrors, glass, uniform walls. The network reports a plausible depth, and it's wrong.

Depth Anything V3 in 2026

  • Vanilla DINOv2 ViT-L/14 as encoder (frozen).
  • DPT decoder.
  • Trained on posed image pairs from diverse sources (no explicit depth supervision needed beyond photometric consistency).
  • Predicts spatially consistent geometry from an arbitrary number of visual inputs, with or without known camera poses.
  • SOTA across monocular depth, any-view geometry, visual rendering, camera pose estimation.

This is the model to reach for by default when you need depth in 2026.

Marigold: diffusion for depth

Marigold (Ke et al., CVPR 2024) reframes depth estimation as conditional image-to-image diffusion: it conditions on RGB and denoises toward a depth map, using a pretrained Stable Diffusion 2 U-Net as its backbone. The payoff is depth maps that are exceptionally sharp at object boundaries. The cost is slower inference than feed-forward models: 10-50 denoising steps instead of one forward pass.

Intrinsics and the pinhole camera

To lift a pixel (u, v) with depth d to a 3D point (X, Y, Z) in camera coordinates:

fx, fy, cx, cy = camera intrinsics
X = (u - cx) * d / fx
Y = (v - cy) * d / fy
Z = d

Intrinsics come from EXIF metadata, a calibration pattern, or a monocular intrinsics estimator (Perspective Fields, UniDepth). Without intrinsics, you can still render a point cloud by assuming a 60-70° FOV and moderate-resolution principals. That's fine for visualisation, but not for measurement.

Evaluation

Two standard metrics:

  • AbsRel (absolute relative error): mean(|d_pred - d_gt| / d_gt). Lower is better. Production models land around 0.05-0.1.
  • delta < 1.25 (threshold accuracy): fraction of pixels where max(d_pred/d_gt, d_gt/d_pred) < 1.25. Higher is better. SOTA models clear 0.9+.

For relative depth (Depth Anything V3, MiDaS), evaluation uses scale-and-shift invariant versions of both metrics.

Check: The Concept0/16
1. What is the main difference between relative depth and metric depth?Define
A near object and a far object joined by a dashed line, each with a card giving the model's value as 1 and 2, under a line asking whether the far object being twice the near value means it is twice as many metres away.
2. What is metric depth?Define
A measuring tape giving an illustrative true distance of 3.0 metres, a camera pointed at it, and the model's output card left as a question mark, under a note that everyday pixel position does not tell you how far something is.
The 3.0 m figure is illustrative, not taken from the lesson's code.
3. What does the AbsRel metric measure?Define
A three by three grid of predicted depth values beside a three by three grid of ground-truth values for the same patch, with AbsRel over the nine pixels left as a question mark.
Illustrative depth values, not taken from the lesson's code.
4. What is delta accuracy (delta < 1.25)?Define
A five-row table listing a predicted depth and a ground-truth depth for pixels p1 to p5, with delta less than 1.25 over those five pixels left as a question mark.
Illustrative depth values, not taken from the lesson's code.
5. To lift a pixel (u, v) with predicted depth d to 3D, you use X = (u - cx) * d / fx, Y = (v - cy) * d / fy, Z = d. What are fx, fy, cx, cy?Define
The stem's own pinhole lifting formula in a code box, with one card listing the per-pixel inputs u, v and d, and a second card for fx, fy, cx and cy left as a question mark.
6. What is DPT in a Depth Anything style model?Define
A frozen ViT encoder box, an arrow into a dashed question mark box, then an arrow into a shaded depth map grid, with the blank stage left unnamed.
7. What is the role of the DINOv2 backbone in Depth Anything V3?Define
An image split into patches, an arrow into a box naming the frozen DINOv2 ViT-L/14 backbone, then an arrow into a dashed question mark box standing for what the backbone contributes.
8. Which pairing correctly matches a depth model to whether it outputs relative or metric depth?Explain
A two-column list of the five model names the lesson gives, MiDaS, Depth Anything V3, Marigold, ZoeDepth and UniDepth, each with its relative-or-metric column left as a question mark.
9. Depth Anything V3 uses a frozen DINOv2 encoder plus a DPT-style decoder. Why freeze the encoder?Explain
A frozen DINOv2 encoder box and a trainable DPT box in sequence, ending in a shaded depth map grid, with the benefit of never updating the left half left as a question mark.
10. Which cues explain how a single RGB image can produce depth at all?Explain
A plain street scene inside one frame: a road running to the horizon, lane markings, a near vehicle with a pole in front of it, a small vehicle far away, a building with windows, and a washed-out skyline.
11. How does Marigold estimate depth, as distinct from feed-forward models like Depth Anything V3?Explain
Two RGB-frame icons, one with an arrow to a card naming Depth Anything V3 and noting RGB in and depth out, the other with an arrow to a dashed box labelled Marigold holding a question mark.
12. Why is Marigold slower at inference than Depth Anything V3?Explain
Two horizontal bars comparing the time to produce one depth map, a short bar for Depth Anything V3 and a much longer bar for Marigold, under a line asking what Marigold is doing that costs the extra time.
Bar lengths are illustrative. The lesson gives no per-image timings.
13. Beyond reflective surfaces, what two things does monocular depth fundamentally fail to recover?Explain
A simple scene with a mug, a desk and a monitor in one frame, an arrow into a shaded depth map grid, and two cards: one saying a depth value comes back for every pixel, the other leaving what does not come back as two question marks.
14. Your Depth Anything prediction on a glass-fronted reception desk reports believable but clearly wrong depth for the glass region. Why?Analyze
A reception scene with a back wall, a desk top and a hatched glass front panel, next to a dashed question mark box standing for the depth read off the glass region, under a line restating that the reading looked believable and is wrong.
15. A team runs Depth Anything V3 on a video feed, reads the outputs as metres, and their stop-distance logic is consistently off by a constant factor, though near and far objects are ordered correctly. What is the likely cause?Analyze
A near dot and a far dot joined by a dashed line with a note that their order is correct, next to a card saying the readings logged as metres are each off by the same factor, under a line asking for the cause.
16. A point cloud built from a depth map looks correct on screen, but widths measured from it are wrong. The team had assumed a 60-70 degree FOV because the camera had no metadata. What went wrong?Analyze
A scattered point cloud inside a panel captioned looks right on screen, next to a note that a 60 to 70 degree field of view was assumed because the camera had no metadata, a dashed question mark box, and a line stating the measured width came out wrong.

Build It

Step 1: Depth metrics

python
import torch
 
def abs_rel_error(pred, target, mask=None):
    if mask is not None:
        pred = pred[mask]
        target = target[mask]
    return (torch.abs(pred - target) / target.clamp(min=1e-6)).mean().item()
 
 
def delta_accuracy(pred, target, threshold=1.25, mask=None):
    if mask is not None:
        pred = pred[mask]
        target = target[mask]
    ratio = torch.maximum(pred / target.clamp(min=1e-6), target / pred.clamp(min=1e-6))
    return (ratio < threshold).float().mean().item()

Always mask invalid depth pixels (zero, NaN, saturated) before evaluation.

Step 2: Scale-and-shift alignment

For relative-depth models, align prediction to ground truth before computing metrics. Least-squares fit of a * pred + b = target:

python
def align_scale_shift(pred, target, mask=None):
    if mask is not None:
        p = pred[mask]
        t = target[mask]
    else:
        p = pred.flatten()
        t = target.flatten()
    A = torch.stack([p, torch.ones_like(p)], dim=1)
    coeffs, *_ = torch.linalg.lstsq(A, t.unsqueeze(-1))
    a, b = coeffs[:2, 0]
    return a * pred + b

Run align_scale_shift before abs_rel_error when evaluating MiDaS / Depth Anything.

Step 3: Lift depth to a point cloud

python
import numpy as np
 
def depth_to_point_cloud(depth, intrinsics):
    H, W = depth.shape
    fx, fy, cx, cy = intrinsics
    v, u = np.meshgrid(np.arange(H), np.arange(W), indexing="ij")
    z = depth
    x = (u - cx) * z / fx
    y = (v - cy) * z / fy
    return np.stack([x, y, z], axis=-1)
 
 
depth = np.random.uniform(0.5, 4.0, (240, 320))
intr = (320.0, 320.0, 160.0, 120.0)
pc = depth_to_point_cloud(depth, intr)
print(f"point cloud shape: {pc.shape}  (H, W, 3)")

One function, every 3D-lifted application. Export the point cloud to .ply and open in MeshLab or CloudCompare.

Step 4: Smoke test with a synthetic depth scene

python
def synthetic_depth(size=96):
    yy, xx = np.meshgrid(np.arange(size), np.arange(size), indexing="ij")
    # Floor: linear gradient from near (top) to far (bottom)
    depth = 1.0 + (yy / size) * 4.0
    # Box in the middle: closer
    mask = (np.abs(xx - size / 2) < size / 6) & (np.abs(yy - size * 0.6) < size / 6)
    depth[mask] = 2.0
    return depth.astype(np.float32)
 
 
gt = torch.from_numpy(synthetic_depth(96))
pred = gt + 0.3 * torch.randn_like(gt)  # simulated prediction
aligned = align_scale_shift(pred, gt)
print(f"before align  absRel = {abs_rel_error(pred, gt):.3f}")
print(f"after align   absRel = {abs_rel_error(aligned, gt):.3f}")

Step 5: Depth Anything usage (reference)

python
import numpy as np
import torch
from transformers import pipeline
from PIL import Image
 
pipe = pipeline(task="depth-estimation", model="depth-anything/Depth-Anything-V2-Large-hf")
 
image = Image.open("street.jpg").convert("RGB")
out = pipe(image)
depth_np = np.array(out["depth"])

Three lines. out["depth"] is a PIL grayscale; convert to numpy for math. The snippet loads Depth Anything V2, the release the transformers depth pipeline loads directly. V3 is a separate release with its own loading code, so this snippet does not cover it.

Check: Build It0/8
1. When evaluating a relative-depth model (MiDaS, Depth Anything), why do you apply scale-and-shift alignment before computing AbsRel?Explain
A scatter of sampled pixels with ground-truth depth on the horizontal axis and the model's value on the vertical axis, lying well above the dashed equal line, under a line asking whether computing AbsRel directly on this pair is fair.
Ground truth from the lesson's synthetic_depth(96), scaled by the lesson's own 3.0 * pred + 0.7, with numpy noise in place of the lesson's torch draw.
2. Why does the lesson say to mask invalid depth pixels (zero, NaN, saturated) before computing a metric?Explain
A ground-truth depth map grid with three cells marked zero in a warning colour, labelled as the zero, NaN or saturated cells the stem lists, with the effect of scoring the map without masking them left as a question.
3. depth_to_point_cloud receives a depth array of shape (240, 320) and intrinsics (320.0, 320.0, 160.0, 120.0). What shape does the returned array have?Apply
A code box setting a depth array shape of 240 by 320 and intrinsics 320, 320, 160, 120, calling depth_to_point_cloud and leaving the returned pc.shape as a question mark.
4. With intrinsics (fx, fy, cx, cy) = (320, 320, 160, 120), what 3D point does depth_to_point_cloud give for pixel u = 260, v = 120 with depth 2.0?Apply
A code box giving intrinsics 320, 320, 160, 120 and a pixel u equals 260, v equals 120 with depth 2.0, restating the stem's own formula lines, with the resulting X, Y, d triple left as a question mark.
5. In synthetic_depth(96), what depth value is at the centre of the box, pixel row 58 and column 48?Apply
A code box giving the floor gradient formula and the box mask condition from synthetic_depth, then asking for synthetic_depth(96) at row 58, column 48, left as a question mark.
6. Suppose pred = 2 * target + 1 exactly, with no noise. What does align_scale_shift(pred, target) return?Apply
A code box setting pred equal to 2 times target plus 1 with no noise, fitting a times pred plus b equals target, with align_scale_shift's return value left as a question mark.
7. delta_accuracy is called with pred = [1.0, 2.0, 3.0], target = [1.1, 2.0, 4.0], and the default threshold 1.25. What does it return?Apply
A code box giving pred 1.0, 2.0, 3.0 and target 1.1, 2.0, 4.0 with threshold 1.25, leaving delta_accuracy's return value as a question mark.
8. You score a MiDaS prediction directly with abs_rel_error and get a poor value, yet the depth map's near-to-far ordering looks right. What step did you most likely skip?Analyze
A line chart with a ground-truth curve and a MiDaS curve that follows the same shape but sits higher throughout, under a line noting the ordering matches yet AbsRel comes back poor, asking which step was skipped.
Illustrative curves, not taken from the lesson's code.

Use It

  • Depth Anything V3 (Meta AI / ByteDance, 2024-2026): the default for relative depth, and the fastest ViT-large-backbone model in production.
  • Marigold (ETH, 2024): the highest visual quality, at the cost of slow inference.
  • UniDepth (ETH, 2024): metric depth with camera intrinsics estimation built in.
  • ZoeDepth (Intel, 2023): metric depth, older than the others but still reliable.
  • MiDaS v3.1: legacy at this point, but stable enough to use as a baseline for comparison.

Typical integration pattern:

  1. RGB frame arrives.
  2. Depth model produces a depth map.
  3. Detector produces boxes.
  4. Lift box centroids through depth to 3D, then merge with a point cloud if one's available.
  5. Downstream: AR occlusion, path planning, object-size estimation, stereo replacement.

For real-time use, Depth Anything V2 Small (INT8 quantised) hits roughly 30 fps on a consumer GPU at 518x518.

Check: Use It0/2
1. You must report object distances in metres from single images, and the camera has no known intrinsics. Which design fits best?Design
A requirement card listing reporting object distances in metres from single images and having no known camera intrinsics, a camera icon as the only input, a card marking fx, fy, cx and cy unknown beside a card asking for a distance in metres, and the best-fit model left as a question mark.
2. You are building an offline compositing tool where depth edges around objects must be as clean as possible, and per-image latency does not matter. Which model fits?Design
A requirement card listing clean depth edges and latency not mattering for an offline tool, next to two depth-edge crops, one blurred across the boundary and one with a clean boundary, with the best-fit model left as a question mark.

Ship It

This lesson produces:

  • outputs/prompt-depth-model-picker.md: picks between Depth Anything V3, Marigold, UniDepth, and MiDaS given latency, metric-vs-relative need, and scene type.
  • outputs/skill-depth-to-pointcloud.md: a skill that builds point clouds from depth maps with correct intrinsics handling and exports them to .ply.
Author's Note

The mistake I see most often with monocular depth: someone runs Depth Anything on a video feed, the numbers come back looking like meters, and they start making real decisions off them, stopping a robot arm 0.4 m from an obstacle, say. Depth Anything V3 and MiDaS never promised meters. They promise correct ordering and correct ratios, nothing more. If you need an actual distance, reach for a metric model (ZoeDepth, UniDepth, Metric3D), or anchor a relative prediction with one known measurement in the scene and run the scale-and-shift fit yourself. Skip that step and nothing crashes. You just get confident, wrong numbers, which is worse than an error.

Exercises

  1. (Easy) Run Depth Anything V2 on any 10 images of your desk. Save depth as grayscale PNGs and inspect. Identify one object whose predicted depth looks wrong and explain why the monocular cues failed.
  2. (Medium) Given RGB + depth from Depth Anything V2, lift to a point cloud and render with open3d. Compare two scenes (indoor / outdoor) and note which looks more believable.
  3. (Hard) Take five pairs of images that differ only by a known object's position (e.g. bottle moved 30 cm closer). Use UniDepth to predict metric depth on both. Report the predicted distance delta vs the true 30 cm.

Key Terms

TermWhat people sayWhat it actually means
Monocular depth"Single-image depth"Depth estimation from one RGB frame, no stereo or LiDAR
Relative depth"Ordered depth"Ordered z-values without real-world units
Metric depth"Absolute distance"Depth in metres; requires calibration or a model trained with metric supervision
AbsRel"Absolute relative error"Mean of
Delta accuracy"delta < 1.25"Fraction of pixels with prediction within 25% of ground truth
Pinhole camera"fx, fy, cx, cy"The camera model used to lift (u, v, d) to (X, Y, Z)
DPT"Dense Prediction Transformer"The conv-based decoder used on top of frozen ViT encoders for depth
DINOv2 backbone"The reason it works"Self-supervised features that generalise across domains without depth labels

Further Reading