EVERYTHING AIAI engineering, made visual
0/28 complete
LESSON 26 · COMPUTER VISION × AI · BUILD + USE

A picture has no depth.
A pretrained encoder plus a small head puts it back.

A depth map is a single-channel image where each pixel is a distance from the camera. For decades that channel required stereo or LiDAR. In 2026 a self-supervised ViT encoder and a DPT-style decoder get within a few percent of ground truth from one ordinary photograph — and the same map, back-projected through four numbers of camera intrinsics, becomes a 3D point cloud.

60 MIN · 7 CHAPTERS + CHECKPREREQ · PHASE 4 · LESSON 14 + 17 + 07
FIG. 26 / ONE FRAME → DEPTH → POINTS
depth / lifted frame encoder tokens
LESSON 26TYPE · BUILD + USE~60 MINPREREQ · PHASE 4 · LESSON 14 (VIT) · LESSON 17 (SELF-SUPERVISED) · LESSON 07 (U-NET)ORIGINAL LESSON ↗
THE 60-SECOND VERSIONSee the problem ↓
01 / ONE CHANNEL, ONE NUMBER PER PIXEL

A depth map is a grayscale image with units.

H × W × 1: at 518 × 518 that is 268,324 values, 1.07 MB as float32 or 268 KB as uint8. Near = light is a display choice, not data. Relative models give ordering and ratios; metric models give metres; a depth of 12.0 and a depth of 1.2 can look identical after normalisation.

depth ∝ 1/disparity · ratios survive, units do not
02 / PRETRAINED ENCODER, SMALL HEAD

The ViT already knows; the head learns the geometry.

DINOv2's self-supervised features encode scene structure, texture gradients and object semantics — none of it trained on depth labels. 518 / 14 = 37 patches per side, so 1,369 tokens go into a DPT-style decoder that regresses one depth per pixel. The head is what the depth data shapes most: DA3 fine-tunes the encoder at 5e-6 and its head at 5e-5, and freezes the backbone only in the 3DGS extension.

37 × 37 = 1,369 tokens → H × W × 1 depth
03 / FOUR NUMBERS LIFT IT TO 3D

Intrinsics turn pixels plus depth into points.

X = (u − cₓ)·Z/fₓ and Y = (v − c_y)·Z/f_y, with Z the depth — one multiply and one divide per pixel, and the whole cloud is in the camera's frame. A detection box becomes one 3D point at its centroid, or a 3D box when you also use its size. No calibration? Assume 60–70° FOV for visualisation, never for measurement.

X = (u − cₓ)·Z/fₓ · Y = (v − c_y)·Z/f_y · Z = Z
MENTAL MODEL IN ONE SENTENCE

The picture supplies ordering and ratios, the encoder supplies priors learned from billions of images, the head turns those into one number per pixel, the intrinsics turn numbers into metres, and scale alignment is the only honest way to compare a relative prediction with a ground-truth ruler.

By the end you will be able to say which models produce relative depth and which produce metres (MiDaS, Marigold, Depth Anything vs ZoeDepth, UniDepth, Metric3D); explain how a pretrained DINOv2 encoder plus a dense depth head generalises without depth labels — and where the backbone really gets frozen (the 3DGS extension); name the monocular cues that make depth possible and the four things it cannot recover; read the intrinsics formula and back-project a pixel by hand ((400, 340) at Z = 3 m is (0.88, 0.51, 3.00) m on this lesson’s camera); compute AbsRel and δ < 1.25 with and without scale-and-shift alignment (0.513 → 0.0175 on the worked example); and lift a 2D detection to a 3D point with a single depth value per box.

THE MISSING AXIS

The picture has two axes.
Depth is the third.

An RGB frame tells you where every surface projects. It does not tell you how far away anything is — and that one missing axis is the difference between a photo and a scene.

A camera flattens the world. Every point in the scene lands on a pixel, and the depth — the distance along the viewing ray — is thrown away by the projection. Two different worlds produce the same image: a 1.2 m crate at 3 m and a 4.8 m object at 12 m are pixel-for-pixel identical if everything else scales with them. A depth map is the axis put back: an image with the same width and height as the frame, but a single channel instead of three, where the value at each pixel is the distance from the camera to whatever surface that pixel sees.

For most of computer vision’s history that channel came from hardware. Stereo rigs triangulate a point from two views; LiDAR times a laser’s round trip; structured light measures a projected pattern’s deformation. Each is expensive, fragile or range-limited — and each is a second sensor to calibrate, synchronise and carry. Monocular depth estimation is the bet that the depth is already implied by the picture, and that a big enough model trained on enough images can read it out.

the shape of the answer RGB frame H × W × 3 518 × 518 × 3 = 804,972 values depth map H × W × 1 518 × 518 × 1 = 268,324 values stored as float32 268,324 × 4 B = 1.07 MB stored as uint8 268,324 × 1 B = 268 KB at 30 fps, uint8 268,324 B × 30 = 8.05 MB/s depth and disparity are inverses Z (m) 1/Z (m⁻¹) what a stereo rig sees, f = 480 px, B = 6 cm 1.0 1.000 disparity 28.80 px · half a pixel = ±1.7% depth 2.0 0.500 disparity 14.40 px · half a pixel = ±3.5% depth 4.0 0.250 disparity 7.20 px · half a pixel = ±6.9% depth 10.0 0.100 disparity 2.88 px · half a pixel = ±17.4% depth 100.0 0.010 disparity 0.29 px · half a pixel = ±174% depth

The stereo column is the whole case for monocular models. A depth estimate from triangulation is only as good as the disparity measurement divided by the baseline: at half a pixel of disparity error, the depth error grows as — 1.7% at one metre, 17.4% at ten, useless at a hundred. Monocular models do not triangulate anything: they learn a mapping from appearance to distance, so their error does not blow up with range in the same way. That is why a phone can now fake a portrait-mode blur with one lens and why a robot can estimate how far the wall is from a single cheap camera.

Two names to keep straight from the start. A relative depth map gives ordered values — pixel A is closer than pixel B, and the ratios are meaningful — but no unit: the number 0.42 says nothing until you know what it is 0.42 of. A metric depth map gives metres. The distinction is not academic: it decides which model you pick, what loss it trains with, and whether you are allowed to compare the output to a tape measure.

One scene, two images

Move the crate. The colour image changes as perspective dictates; the depth map changes with it — one number per pixel, and the crate’s depth moves along the hyperbola 1/Z.

crate distance Z 3.00 m screen height f·H/Z = 480 px · 1.20 m / 3.00 m = 192 px disparity 1/Z 0.333 m⁻¹ halve the disparity 0.167 m⁻¹ ⇢ Z doubles to 6.00 m depth at the crate 3.00 m · a single number, no units on its own depth map 518 × 518 × 1 channel = 268,324 pixels float32 1.07 MB · uint8 268 KB at 30 fps, uint8 8.05 MB/s of depth

The scene is a teaching cartoon with exact geometry, drawn with the lesson’s pinhole camera (fx = 480 px, 518-wide crop). The inverse law is not a coincidence: stereo triangulates 1/Z, which is why depth estimates get coarse with distance and disparity errors multiply by Z².

Quick check

A depth model outputs 0.8 for a chair and 0.4 for a wall behind it. What can you say without any extra information?

The rest of the lesson follows the depth map through its whole life. Which models produce which kind of depth and why (02); why a single image contains any depth signal at all (03); the things no single image can ever contain (04); the four numbers that lift a pixel into a 3D point (05); the two metrics and the alignment step that make relative predictions comparable (06); and the pipeline that turns a detector’s box plus a depth map into a 3D point (07).

RELATIVE OR METRIC?

Two families.
One architecture pattern.

Every production depth model answers one question: metres, or ordering? The architecture underneath is nearly always the same — a strong pretrained encoder and a small depth head — and the difference lives in the data and the loss.

Relative depth is what a model learns when its supervision is photometric or scale-invariant: pixels get ordered values and the loss does not care whether the whole map is stretched by a constant. MiDaS (paper 2019–2020, v3.1 in 2023) made this the default by mixing many datasets and evaluating after alignment. Marigold (2023) reframed the same target as conditional diffusion and produced the sharpest boundaries of the era. Depth Anything (V2 2024; V3 released 14 Nov 2025, ICLR 2026) scales the training data — 1.5 M labelled and 62 M unlabelled images — and became the model to reach for when the input is an arbitrary photo.

Metric depth is what a model learns when its targets are metres: ZoeDepth (2023) trains relative-depth features and then regresses metric bins, which is why it carries separate indoor and outdoor heads — the scale statistics of a bedroom and a highway are different distributions. UniDepth (2024) goes further and estimates the camera intrinsics along with the depth. The trade for metres is exposure: a metric model is only as good as its intrinsics and its domain coverage.

the pattern behind nearly all of them image (H × W × 3) │ ▼ ViT encoder · 14 px patches 518 / 14 = 37 → 37 × 37 = 1,369 tokens pretrained, self-supervised DINOv2 saw no depth labels; DA3 fine-tunes it at 5e-6 while its head moves at 5e-5 │ ▼ DPT-style depth head fuses four feature depths, upsamples to H × W the part depth data shapes most small next to the encoder │ ▼ depth (H × W × 1) relative: ratios, no unit · metric: metres a scale-invariant loss, in one line of prose score the prediction only after fitting a · pred + b to the target; a global rescaling of the whole map costs nothing, so the network is never punished for not knowing the metre it could not know — and it spends its capacity on the structures that do carry information: edges, planes, order.

The phrase “a strong pretrained encoder plus a lightweight head” is the heart of the modern recipe. The encoder’s job is to have already learnt what the visual world looks like — edges, textures, object parts, scene layout — from self-supervised pretraining on billions of images. DINOv2 did that with no labels at all: it was trained to give the same features to two augmented views of the same image. Those features turn out to encode exactly the structure that correlates with depth, so the head only has to learn the geometry: one linear-ish mapping from features to distances. In practice the encoder is not frozen — it is fine-tuned at a much lower rate than the head (Depth Anything 3: 5e-6 for the encoder against 5e-5 for the decoder), and that 10× gap is what keeps the broad pretrained features from being overwritten by the narrower depth dataset while the head learns the task. The encoder’s cross-domain generalisation — indoor, outdoor, medical, satellite — survives the fine-tune; the head is where the geometry lives.

It is worth saying plainly where the generations differ, because a quiz question has to survive contact with the actual code. Depth Anything V1 and V2 fine-tuned their DINOv2 encoders. DA3’s recipe is minimal rather than frozen: one plain transformer — a vanilla DINOv2 encoder with no architectural specialisation — plus a Dual-DPT head that predicts depth and ray maps from the same features, trained end to end. The frozen backbone shows up one level up: the feed-forward 3DGS extension initialises from the pretrained DA3 weights, freezes them, and tunes only the added GS-DPT head. What is constant is the division of labour: the encoder brings priors that generalise, the head brings the metric the task requires.

Depth Anything in three linespython
import numpy as np
from PIL import Image
from transformers import pipeline

pipe = pipeline(task="depth-estimation", model="depth-anything/Depth-Anything-V2-Small-hf")

image = Image.open("street.jpg").convert("RGB")
out = pipe(image)
depth_np = np.array(out["depth"])   # H x W, float or uint8 depending on the pipeline

scale = depth_np.max()              # relative: normalise to inspect, never to measure
normalised = depth_np / (scale if scale > 0 else 1)
out['depth'] is a single-channel PIL image — convert to numpy for math. The V3 model id changes; the shape of the call does not. No intrinsics, no calibration, no second view.

The model comparator

Pick the constraint you actually face. Relative or metric is the first fork; latency and boundary quality decide the rest. Every card is a dated example — the kind of model, not a permanent ranking.

the constraint
constraint arbitrary photos, no calibration, fastest PICK · Depth Anything V2 → V3 (V2 2024 · V3 14 Nov 2025 (ICLR 2026)) kind relative depth · up to scale speed V2-Small INT8 ≈ 30 fps at 518 × 518 on a consumer GPU Relative depth needs no intrinsics: the answer is an ordering plus ratios, and the model generalises across indoor, outdoor, medical and satellite images because its encoder was self-supervised on internet-scale data. V2-Small at INT8 runs at roughly 30 fps at 518 × 518; V3 adds multi-view consistency for free. the trade-off Every pick gives something up. Relative models skip calibration and cannot report metres; metric models report metres and inherit their training domain. Diffusion buys boundaries with latency. The DINO-era feed-forward models buy generality with a bigger encoder.

Dates matter because this field is a few years old in production terms: MiDaS set the baseline (2019–2023), ZoeDepth and Marigold split metric from quality (2023), Depth Anything became the default (2024–2026). Read the model page before trusting a ranking.

RELATIVE · paper 2019–2020 · v3.1 2023

MiDaS v3.1

relative depth · up to scale

BEiT-L/16 in v3.1; the 2020 TPAMI paper used a ResNet — 2019–2023

speed · tens of milliseconds on a GPU; runs on a phone

detail · smooth and robust, but visibly softer at object boundaries than the 2024+ models

best for · legacy baselines, embedded and CPU-only targets, and the depth-conditioned ControlNet lineage

isl-org/MiDaS
RELATIVE · 2023 · CVPR 2024

Marigold

relative depth · up to scale

Stable Diffusion 2 U-Net run as conditional image-to-image translation

speed · 10–50 denoising steps — roughly 10× slower than a feed-forward model

detail · the sharpest boundaries of the four; affine-invariant output, no metres

best for · offline quality work: compositing, matting, depth-conditioned generation

marigoldmonodepth.github.io
METRIC · 2023

ZoeDepth

metric depth · metres

DPT/BEiT with metric bins and separate indoor/outdoor heads

speed · fast and feed-forward — no denoising loop

detail · outputs metres, but is sensitive to intrinsics and to the indoor/outdoor domain

best for · metres in a scene you control: measurement, AR placement, robot reach

arXiv 2302.12288
RELATIVE · V2 2024 · V3 14 Nov 2025 (ICLR 2026)

Depth Anything V2 → V3

relative depth · up to scale

plain DINOv2 encoder + Dual-DPT head; V2-S 24.8 M, V2-L 335.3 M, DA3-BASE 0.12 B (dated)

speed · V2-Small INT8 ≈ 30 fps at 518 × 518 on a consumer GPU

detail · the generalisation benchmark of the era; V3 accepts any number of views, with or without poses, and also ships a metric variant

best for · the default for arbitrary photos and video: no calibration, no intrinsics, any domain

depth-anything.github.io
Quick check

DINOv2 was never trained on depth. Why do its self-supervised features still help a depth head so much?

One more distinction from the source worth keeping: Depth Anything V3 is described as predicting spatially consistent geometry from an arbitrary number of visual inputs, with or without known camera poses. In practice that means the same model architecture serves a single photo (this lesson’s main case) and a short video or photo set, where the extra views buy consistency the single-frame model cannot have. Relative depth from one image is the foundation; the multi-view case is the same idea with a stronger constraint.

WHY ONE IMAGE IS ENOUGH

Depth is not in the pixels.
It is in the regularities.

Recovering depth from one view is mathematically ill-posed — and empirically possible, because real scenes are not arbitrary. Five cues correlate with distance, and a ViT trained on enough images learns the correlations.

Perspective. Parallel lines in 3D converge in 2D. A 1 m-wide lane at 5 m spans f·1/5 = 96 px on this lesson’s camera (f = 480 px), at 10 m it spans 48 px, at 20 m it spans 24 px, and every such pair meets at a vanishing point. The convergence is a geometric consequence of the projection, so the network does not have to memorise it — the flat floor plane is enough of a prior to exploit it.

Texture gradient. Surfaces carry texture at a roughly constant physical scale, so the projected scale shrinks with distance: a 1 m patch of floor spans 480 px at 1 m and 48 px at 10 m. When texture density changes smoothly across a surface, that gradient is a distance ruler — the one a blank wall removes.

Occlusion order. Nearer surfaces cover farther ones, and the T-junction where a boundary ends tells you which side is in front. It is an ordering cue rather than a distance cue, but ordering is exactly what a relative model needs, and it costs nothing to extract.

Size constancy. Familiar objects bring approximate scale: a 1.7 m person spans 255 px at 3.2 m, 85 px at 9.6 m, 26 px at 31 m. When the pixels look like a person, the prior says “person-sized”, and the projected size resolves the ambiguity between small-and-near and large-and-far. This is the cue that quietly supplies the scale monocular models otherwise lack — and the reason it works only for objects the model has seen before.

Atmospheric perspective. Outdoors, distance scatters light: contrast falls and colours shift toward the haze. A network trained on outdoor photos picks the correlation up even though it is a property of the atmosphere, not the geometry.

five cues, with the arithmetic on this lesson's camera (f = 480 px) cue what varies with Z example at the numbers perspective size ∝ 1/Z, lines meet at a 1 m lane: 96 px at 5 m, vanishing point 48 px at 10 m, 24 px at 20 m texture gradient texel size ∝ 1/Z 1 m floor: 480 px at 1 m, 48 px at 10 m — a ruler occlusion order which surface wins the pixel the T-junction: front surface owns the boundary size constancy known height h → Z ≈ f·h / h_px 1.7 m person: 255 px at 3.2 m, 85 px at 9.6 m atmosphere contrast ↓, blue shift ↑ with Z haze is a prior, not a law what the model is really doing features from a self-supervised ViT already encode these cues; a scale-invariant loss teaches the head to combine them into one number per pixel — an ordering first, ratios second, metres only if the data said so.

The last paragraph is the honest version of “the network understands 3D”. Each cue is ambiguous on its own — a large object far away projects exactly like a small object nearby — but the cues co-occur in real scenes, and training on millions of images learns the joint distribution. That is why the model can be confident and right on a street it has never seen, and confident and wrong on a mirror: the same statistics that make the guess work are the reason the failure is invisible.

It also explains the shape of the training signal. The model is not asked to produce metres it cannot know; it is asked to produce values whose ratios match the truth, and the loss is written to ignore the global scale. The result is a map that is stable and useful — and has to be aligned before any ruler touches it.

Quick check

Which of these is NOT available to a model looking at a single still image?

Five cues, all static, all present in a single frame. What is missing is any guarantee: each one can be defeated by the right surface, and the next chapter is the list of defeats — scale, occlusion, reflection, sky — the places where the statistics of real scenes stop helping and a depth model is confidently wrong.

WHAT IT CANNOT RECOVER

The cues fail in four places.
None of them is a training bug.

A depth model is confidently wrong exactly where the picture stops containing the information — and no amount of extra data puts the information back. Learn the four, and you can predict the failures before you see them.

1. Absolute scale. Every cue from the last chapter is invariant to a global rescaling of the world: move every surface twice as far and make every object twice as big, and the image is unchanged. A relative model returns the ordering; a metric model guesses the scale from experienced object sizes and scene statistics, which is why its errors are domain-shaped. The lesson’s scale bench shows the arithmetic: the same relative map read at ×4 reports AbsRel 2.99 and not one pixel inside δ < 1.25³, while alignment recovers 0.0145. The model was not wrong about the structure — you were reading units that were never there.

2. Occluded geometry. The back of a chair, the space behind a counter, the far side of a lamp: nothing in the frame constrains it. A network can hallucinate a plausible completion (it has seen many chairs) but there is no measurement to be had. Downstream code that assumes the depth map is a full 3D scene will collide with the parts that were never observed.

3. Reflective and textureless surfaces. A mirror shows a scene with its own depth; glass shows what is behind it; a blank wall has no texture gradient at all. The bench’s numbers: a glass surface at 2.40 m is predicted at 5.80 m because the model reads the reflected hallway (AbsRel 1.42); a textureless wall at 4.00 m drifts smoothly from 2.70 m to 5.30 m because there is no ruler left to anchor it.

4. Anything without a surface. Sky has no distance. A relative model maps it to the farthest value it can express — the number that means “as far as I know” and nothing more — and a metric model caps it at a dataset-dependent maximum. Any pipeline that converts “farthest” into “one kilometre” invents a measurement, so sky and other no-return pixels get masked.

The failure board

Four places where a depth model is confidently wrong. None of them is a bug you can train away: each one deletes the cue the model needs.

the case
A mirrored wall 2.40 m away reflects a hallway whose content is 1–6 m deep.
case mirror / glass scene A mirrored wall 2.40 m away reflects a hallway whose content is 1–6 m deep. true depth 2.40 m — the glass surface predicted 5.80 m — the reflected hallway, treated as real space AbsRel 1.42 (|5.80 − 2.40| / 2.40) ratio 2.42 — outside even δ < 1.25³ = 1.953 why Every monocular cue the model uses is absent at the surface: no texture gradient, no perspective on the glass, and the occlusion order belongs to the reflected scene. The network reads the reflection the way a person does when they mistake a mirror for a doorway. what a pipeline does Fuse with stereo, LiDAR or a polarisation camera; or mask reflective regions with a segmentation model. No amount of depth training removes the ambiguity — a mirror genuinely shows a room.

Every row is a measurement the model cannot make from one picture, not a training-set gap. The fix is always additional information — a second view, another sensor, or a mask that tells the pipeline where not to trust the number.

The four failures share one shape: the model replaces missing information with its prior, smoothly and without complaint. That is also why the failures are invisible in a thumbnail — the map looks like a room. The practical response is never more training; it is more information — a second view, another sensor, a mask, or a known length — plus an honest report of what the map is allowed to claim.

PIXELS TO 3D POINTS

Four numbers turn depth
into geometry.

A pixel is a direction; depth is a distance along it. Multiply one by the other and you have a point in 3D — the camera model that does it is the pinhole, and it needs exactly four numbers.

Think of the camera as a point with an image plane in front of it. Every pixel (u, v) defines one ray: go right by (u − cx) pixels, down by (v − cy) pixels, forward by f pixels, and you have a direction in camera coordinates. Scaling that direction until the forward component equals the depth Z gives the point:

X = (u − cx) · Z / fx Y = (v − cy) · Z / fy Z = Z fx, fy focal length in pixels — how many pixels one unit of depth is "worth" at the image plane; samples: a phone's 26 mm lens on a 518-wide crop lands near 400–480 px cx, cy principal point, the pixel where the optical axis hits: usually near (W/2, H/2); this lesson's camera is 518 × 518 with fx = fy = 480 px and cx = cy = 259 px → a 56.7° horizontal FOV u, v the pixel; Z is the depth from the depth map worked example, by hand pixel (400, 340), Z = 3.00 m, fx = fy = 480, cx = cy = 259 X = (400 − 259) × 3.00 / 480 = 141 × 3.00 / 480 = 0.881 m Y = (340 − 259) × 3.00 / 480 = 81 × 3.00 / 480 = 0.506 m Z = 3.000 m range = √(0.88125² + 0.50625² + 3.000²) = 3.167 m · 18.7° off the axis same pixel, Z = 4.50 m → (1.322, 0.759, 4.500) m — every coordinate scales linearly with Z: one pixel owns one ray, and one ray owns a family of points

The formula makes the camera’s sensitivity obvious. X and Y are proportional to (u − cx)/fx, so a 1% focal error is a 1% lateral error, while Z comes straight from the depth map and never touches the intrinsics. Compare two plausible guesses on this camera: f = 480 px (the truth) puts pixel (400, 340) at 3 m, giving X = 0.881 m; f = 448.6 px — the value a 60° assumption would use — gives X = 0.943 m. That is a 7% lateral error from a five-degree miscalibration, invisible in a thumbnail and fatal in a measurement.

The four numbers come from one of four places, in descending order of trust: the camera’s own API or EXIF metadata; a calibration target (a checkerboard photographed from several angles); a monocular intrinsics estimator that predicts the field of view from image content (Perspective Fields, UniDepth); or an assumption. The source is blunt about the last one — a 60–70° FOV guess is usable for visualisation, not for measurement. The cloud will look right and be wrong by a few percent, which is exactly the error mode this lesson keeps returning to.

The whole lift, in one functionpython
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))
pc = depth_to_point_cloud(depth, (320.0, 320.0, 160.0, 120.0))
print(pc.shape)   # (240, 320, 3) — one 3-D point per pixel, in the camera frame
Two meshgrids and two multiplies: this is every '2D to 3D' application, from AR occlusion to robot grasping. Export the cloud to .ply and it opens in MeshLab or CloudCompare. At 518 × 518 the cloud is 268,324 points ≈ 3.2 MB of float32 xyz.

The back-projection bench

Every 3D lift is the same three lines: X = (u − cx)·Z/fx, Y = (v − cy)·Z/fy, Z = Z. Move the pixel or the depth and watch the point move; press lift to keep it.

pixel u = 400 px · v = 340 px (of 518 × 518) intrinsics fx = 480 px · fy = 480 px · cx = 259 · cy = 259 depth Z = 3.00 m X = (u − cx)·Z / fx = (400 − 259) · 3.00 / 480 = 0.881 m Y = (v − cy)·Z / fy = (340 − 259) · 3.00 / 480 = 0.506 m Z = 3.00 m point (0.881, 0.506, 3.000) m range 3.167 m · 18.7° off the optical axis same pixel, double Z (1.762, 1.012, 6.00) m — every coordinate scales with Z field of view 480 px on a 518-wide crop = 56.7° focal check a 60° lens here wants fx = 448.6 px; this setting is 7.0% away, and X, Y move by the inverse of that — Z never does cloud 0 points · clicking adds a new ray, the button keeps this one

The floor grid is drawn from the same formula run on the world side: project (X, 1.6 m, Z) into pixels and you get the floor. Lift enough pixels and the point cloud becomes the room — which is exactly what a real pipeline does with a predicted depth map and an intrinsics estimate.

Two details that save debugging time. The camera frame is x right, y down, z forward — y points down because image rows do, so a point above the optical axis has negative Y. And the depth map must be in the same frame: a network that predicts inverse depth or disparity needs one reciprocal before the lift, and a map produced on a centre crop needs the crop’s intrinsics, not the original frame’s. Both mistakes produce a cloud that looks like the scene but is subtly bent — the same failure signature as a wrong focal length. The points this chapter lifts are also what turns tracking metric: Lesson 27 takes each track’s box through this formula so identities are associated in metres, not just in pixels.

HOW WRONG IS IT?

Two metrics, one alignment,
and a lot of bookkeeping.

Depth evaluation looks simple and is not: the same prediction scores 0.0175 or 2.99 depending on whether you aligned it, and the wrong pixels can dominate the mean. Here is the protocol the papers actually use.

AbsRel — absolute relative error. For every valid pixel, divide the absolute error by the ground truth and average: mean(|d_pred − d_gt| / d_gt). Lower is better; 0.05–0.1 is the production band. Because the denominator is the truth, a 10 cm error at 1 m costs ten times as much as the same error at 10 m, which is the right weighting for most robotics and AR tasks: relative accuracy near the camera is what keeps you out of a wall.

δ < 1.25 — threshold accuracy. For every valid pixel, take max(pred/gt, gt/pred) and count it correct if the ratio is below 1.25 — within 25%. Higher is better, and the standard ladder reports three rungs: δ < 1.25, δ < 1.25² (1.5625) and δ < 1.25³ (1.953). State of the art sits at 0.9+ on the strict rung. The ladder matters: in the worked table below the one bad pixel fails the first rung and passes the second, and the pair of numbers together tell you it was a far outlier rather than a systematic bias.

Mask before you compute. Real depth datasets carry invalid pixels — sensor holes, saturated returns, missing ground truth — and both metrics divide by the ground truth, so a single zero explodes into a giant error term that swamps the mean. The source code takes a mask argument on both functions for exactly this reason, and the rule is: filter first, then measure. A sudden gap between your numbers and a paper’s is an evaluation bug far more often than a model bug.

worked example · 8 pixels, one badly wrong (the source's numbers, in full) pixel gt (m) pred (m) |pred − gt| / gt max(pred/gt, gt/pred) 1 2.0 1.90 0.050 1.053 2 4.0 4.20 0.050 1.050 3 8.0 8.40 0.050 1.050 4 2.0 2.10 0.050 1.050 5 4.0 3.70 0.075 1.081 6 8.0 7.40 0.075 1.081 7 3.0 2.85 0.050 1.053 8 6.0 4.20 0.300 1.429 AbsRel = 0.700 / 8 = 0.0875 production band, barely δ<1.25 = 7 / 8 = 0.875 one pixel outside the strict rung δ<1.25² = 8 / 8 = 1.000 ... but inside the second δ<1.25³ = 8 / 8 = 1.000 read it together: no systematic bias (every good pixel is inside 8%), one far outlier that predicted a 6 m wall at 4.2 m — 30% too close. scale-and-shift alignment, worked by hand pred 1.0 2.0 3.0 4.0 (relative units — arbitrary) gt 2.1 4.2 5.9 8.2 (metres) least squares: a·pred + b = gt → a = 2.000, b = 0.100 aligned 2.1 4.1 6.1 8.1 AbsRel before alignment 0.513 ← measures the unit, not the model AbsRel after alignment 0.0175 ← what the model actually got wrong δ<1.25 before / after 0.000 / 1.000 and on the lesson's scale bench (four surfaces, a small model error): s = ×1 ×2 ×4 ×8 raw AbsRel 0.038 · 0.995 · 2.990 · 6.980 aligned 0.0145 in every case, a = 0.939 / s

The alignment step is the part beginners skip and reviewers check first. For a relative model, fit the single linear transform a · pred + b = target that minimises squared error over the whole image, then score the fitted prediction. Two sums of squares, no matrix library. What it removes is exactly the scale and offset the relative model never claimed to know: for a perfect map, a = 1/s and b = 0, and the bench shows raw AbsRel falling from 2.99 at a ×4 mis-scaling to 0.0145 after the fit. What it does not remove is any real error: the residual after alignment is the model’s own.

The scale-ambiguity bench

One frame, two worlds: a 1.2 m crate at 3 m, or a 4.8 m object at 12 m — drawn to their own scale, the pictures are identical. Pick a candidate and see what it does to the metrics.

assumed world scale
assumed scale ×2 — the model's relative map read as 2× the true room anchor true Z relative assumed @×2 back wall 12.00 m 1.040 24.96 m ×2.08 crate front 3.00 m 0.235 5.64 m ×1.88 ball 5.15 m 0.442 10.61 m ×2.06 floor, bottom edge 2.97 m 0.243 5.82 m ×1.96 AbsRel, raw 0.995 — the scale mismatch, not the model AbsRel, aligned 0.0145 — least squares a·pred + b, a = 0.4693, b = 0.262 δ<1.25, raw 0.00 · aligned 1.00 (δ<1.25³ aligned: 1.00) all candidates ×1 raw 0.04 · ×2 raw 1.00 · ×4 raw 2.99 · ×8 raw 6.98 aligned ×1 0.0145 · ×2 0.0145 · ×4 0.0145 · ×8 0.0145 A single relative map, four metric readings, and the picture is identical in every one of them. Scale-and-shift alignment removes exactly the part the relative model never claimed to know — and nothing else: the residual 0.0145 is the model's own error.

This is why every MiDaS / Depth Anything paper reports metrics after alignment: without it you measure the arbitrary unit, not the model. It is also why relative depth cannot answer “how far is the wall?” — the same map fits the true room, a doll’s house, and a cathedral, pixel for pixel.

Quick check

Your relative-depth model reports AbsRel 0.51 on a benchmark before alignment and 0.018 after. Which statement is right?

The source's evaluation codepython
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()

def align_scale_shift(pred, target, mask=None):
    p = pred[mask] if mask is not None else pred.flatten()
    t = target[mask] if mask is not None else 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 whenever the model is relative (MiDaS, Marigold, Depth Anything). Always pass the mask. The clamp(min=1e-6) is what makes masking non-optional.

Before leaving measurement, keep the two protocols apart in your head. A relative model is scored after alignment — shape only. A metric model is scored raw, because metres were part of the claim. Comparing a raw relative number with a raw metric number, or an aligned number with an unaligned one, is the most common way a depth table becomes meaningless.

FROM BOXES TO 3D

Detection says where in the image.
Depth says where in the world.

The integration is five steps and one join. A detected box has pixels; the depth map has one number per pixel; lift the box through the depth and the 2D detection becomes a 3D point — the core of every AR occlusion system and every “pick up the cup” robot.

The pipeline the source describes is the one you will meet in production: (1) an RGB frame arrives; (2) a depth model produces the map; (3) a detector produces boxes; (4) each box’s centroid is lifted through the depth map to a 3D point and merged with the cloud; (5) the consumer acts — occlusion, planning, sizing, or replacing a stereo rig. The join in step 4 is the only new code, and it is the formula from the last chapter run once per box.

One depth value per box is enough for placement, and the choice of that value matters. A centroid is fine when the box is tight on one surface; a median over the box’s valid pixels is what you want in general, because a box on a person against a wall straddles two depths and the median lands on the object, not on the background the occlusion-edge failure bleeds in. The box’s size is the second free measurement: a 255-pixel-tall person box at depth 3.2 m with f = 480 px implies a real height of 255 × 3.2 / 480 = 1.70 m — which is f·H/Z run backwards, and a useful sanity check that the depth and the detector agree.

the join, with numbers · fx = fy = 480 px, cx = cy = 259 px detected box x1 = 290, y1 = 150, x2 = 410, y2 = 405 (255 px tall) centroid u = 350, v = 277.5 depth at the box median of the box's valid pixels = 3.20 m X = (350 − 259) × 3.20 / 480 = 91 × 3.20 / 480 = 0.607 m Y = (277.5 − 259) × 3.20 / 480 = 18.5 × 3.20 / 480 = 0.123 m Z = 3.200 m range = 3.259 m what the box measures, without a tape: height 255 px × 3.20 m / 480 px = 1.70 m (a person) width 120 px × 3.20 m / 480 px = 0.80 m one box → one 3-D point. Ten boxes → ten points. A whole frame of boxes → a labelled point cloud, which is what a planner or an AR renderer wants.
Lift a detection boxpython
import numpy as np

def lift_box(box, depth, intrinsics):
    x1, y1, x2, y2 = box
    u, v = (x1 + x2) / 2, (y1 + y2) / 2
    patch = depth[y1:y2, x1:x2]
    valid = patch[(patch > 0) & np.isfinite(patch)]
    z = float(np.median(valid))              # one depth value per box
    fx, fy, cx, cy = intrinsics
    X = (u - cx) * z / fx
    Y = (v - cy) * z / fy
    return np.array([X, Y, z]), z

point, z = lift_box((290, 150, 410, 405), depth_map, (480, 480, 259, 259))
print(point, z)   # [0.607 0.123 3.2] 3.2
Median, not the centroid pixel, and mask the invalid values first. For a relative depth map, align to metres before this function — or pass the relative values through and treat the result as an ordering with an unknown unit.

The real-time arithmetic is worth doing before promising a frame rate. At 30 fps the whole budget is 33.3 ms. Depth Anything V2 Small at INT8 hits roughly 30 fps at 518 × 518 on a consumer GPU — the depth alone can spend the entire budget before the detector runs. In practice you choose: run depth at 10 Hz and interpolate, run both models on a smaller crop, quantise further, or accept a lower frame rate and let the planner work with the depth it has. The 2026 default — V3-style models on a GPU — makes the depth step cheap enough to be one stage among several, which is the real reason monocular depth changed what products are possible.

That closes the arc: a single frame goes in, a one-channel map comes out, four numbers lift it into geometry, and the evaluation protocol tells you how much of it to trust. The pieces you will reuse most are the smallest ones — the depth/relative distinction, the back-projection formula, the alignment step, and the habit of asking what the picture could not have contained.

CHECK YOURSELF

Six questions.
Then the terms worth keeping.

Answer before you look. The relative-vs-metric question and the alignment question are the two that decide whether your next depth project produces a number or a unit — and the glass question is the one that tells you whether you trust the map too much.

0 / 6 answered · 0 correct

01What is the main difference between relative depth and metric depth?

02Depth Anything 3 (released 14 Nov 2025, ICLR 2026) uses a plain DINOv2 encoder plus a Dual-DPT head. What does its training recipe actually do?

03To 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?

04When evaluating a relative-depth model (MiDaS, Depth Anything), why do you apply scale-and-shift alignment before computing AbsRel?

05Your Depth Anything prediction on a glass-fronted reception desk reports believable but clearly wrong depth for the glass region. Why?

06Your own evaluation reports AbsRel 0.31 and δ < 1.25 = 0.41 where the model card claims 0.06 and 0.95, and you fed the metrics every pixel including zeros and NaNs. What should you fix first?

Key terms, demystified

Click a card to swap the lazy description for what it actually means.

Exercises from the lesson

Three problems with exact numbers: find a failure in your own photos and name the cue that broke, lift a full frame to a point cloud with real and guessed intrinsics, and measure a known 30 cm move with a metric model — including the uncertainty arithmetic that says how many pairs you need. Try first; a worked answer is one click away.

  1. Run a relative-depth model (Depth Anything V2 or V3) on ten photos of your desk. Save each map as a grayscale PNG and inspect it. Find one object whose predicted depth is clearly wrong and explain which monocular cue failed.
    Show one worked answer

    The procedure is three lines with the Transformers depth pipeline — load, `pipe(image)['depth']`, convert from PIL to numpy — then normalise and save. What you will find, almost every time: the order is right and the geometry is local. Look for the reflective object (a monitor, a mug, a phone screen), the very close object at the frame's edge, or a uniform surface. A monitor is the reliable failure: the model reads the content on the screen as depth, so a video of a corridor reports a corridor — predicted 4–5 m for a screen 0.6 m away, an AbsRel above 1. The cue that failed is texture gradient: the screen's content has perspective and texture that contradict its physical flatness, and nothing in a single frame says which is real. The check that makes the exercise worth doing: hold a tape measure to the screen and compare with the model's value after a scale fit — the ordering survives, the metres do not.

  2. Given RGB plus a relative depth map, lift the pixels to a point cloud and render it with Open3D. Do it twice: once with the camera intrinsics from EXIF, once with a guessed 60° FOV. Compare the two clouds and say which looks more believable.
    Show one worked answer

    Back-project every valid pixel: `X = (u − cx)·Z/fx`, `Y = (v − cy)·Z/fy`, `Z = d`, then stack into an (H·W, 3) array with the colours beside it. A 518 × 518 map is 268,324 points, which as float32 xyz is 268,324 × 12 B ≈ 3.2 MB plus 0.8 MB of uint8 colour — a `.ply` of roughly 4 MB, small enough to open on a laptop. With real intrinsics the room's floor comes out flat and walls meet at right angles; with a guessed 60° FOV (f = 259/tan 30° = 448.6 px instead of the true 480 px) the whole cloud is stretched laterally by 7% and the floor bends subtly — believable in isolation, wrong against a tape measure. The lesson's numbers: pixel (400, 340) at 3.00 m is (0.881, 0.506, 3.000) m with f = 480 and (0.943, 0.542, 3.000) m with f = 448.6. Ordering-quality clouds for visualisation, metric work for measured intrinsics — and if you cannot get them, estimate them from the image (Perspective Fields, UniDepth) instead of guessing.

  3. Take five pairs of images that differ only by a known object's position (e.g. a bottle moved 30 cm closer). Use a metric model (UniDepth or ZoeDepth) to predict depth on both and report the predicted distance delta against the true 30 cm. How many pairs do you need before the answer is stable?
    Show one worked answer

    Measure the object's distance in both frames by taking the median of the depth values over the bottle's pixels — never the centroid of a box, which mixes in the background. With a typical AbsRel of 0.06 at 2 m, one prediction carries ≈ 12 cm of error, so the 30 cm delta is a 12 cm signal in two 12 cm noises: a single pair can plausibly report anything from 0 to 50 cm. The uncertainty of a difference is √(σ₁² + σ₂²) ≈ 17 cm, so the signal-to-noise ratio on one pair is 30/17 ≈ 1.8 — detectable but not reliable. Average ten pairs and the noise on the mean falls to 17/√10 ≈ 5.4 cm, so the 30 cm delta is pinned to within about ±11 cm at two standard errors — ±36% of the effect, which is the kind of number a report can carry with the error bar attached. The lesson's three practical rules fall out: use a metric model if you have one, average many measurements when you do not, and quote an uncertainty with every delta — a delta from two single predictions has no error bar and therefore no meaning.

Terms this lesson borrows from later lessons (or outside)

You do not need to master these here. Each one gets a proper treatment in its own lesson; the one-line meaning is enough to keep reading. Orange dotted underlines in the prose point back to this list.

  • patch embeddings and the token gridA 518-wide image with 14-pixel patches is 37 patches per side — 1,369 tokens for the encoder. The patch grid is also why occlusion edges can blur: a boundary in the middle of a patch is one token's problem. Phase 4, Lesson 14.
  • self-supervised featuresDINOv2 learned its features without any labels, from augmented views of the same image. That is what lets a pretrained encoder transfer to depth across indoor, outdoor, medical and satellite domains. Phase 4, Lesson 17.
  • encoder–decoder and skip connectionsThe DPT head is a U-Net-shaped decoder: features from several encoder depths are fused and progressively upsampled, so fine boundaries survive. The same pattern from segmentation, reused for a regression target. Phase 4, Lesson 07.
  • object detection boxesA detector gives (x1, y1, x2, y2) per object; depth gives one number per pixel. Lifting a box is the join between them: take a depth (or the median of its pixels) and back-project the centroid. Phase 4, Lesson 06.
  • diffusion samplingMarigold runs depth as conditional image-to-image diffusion: 10–50 denoising steps for one map. The quality comes from the generative prior; the cost is that a feed-forward model answers in one pass. Phase 4, Lesson 10.
  • tensor shapes and broadcastingEvery piece is a shape: H × W × 3 in, H/14 × W/14 tokens, H × W × 1 out, then the (H, W, 3) cloud from numpy meshgrids. Depth pipelines fail on shape mistakes more often than on modelling mistakes. Phase 1, Lesson 12.
KEEP GOING

A picture is a start.
Practice is the rest.

This lesson is a port of an open course. Everything here traces back to it — and the next step is running the code yourself.

Original lessonMonocular Depth & Geometry EstimationAI Engineering from Scratch · the source text, quiz and main.py: the AbsRel / δ<1.25 metrics with their mask arguments, least-squares scale-and-shift alignment, depth_to_point_cloud, the synthetic room, the .ply writer and the three-line depth pipeline. This lesson ports all of it and adds the arithmetic behind each step.Original paperTowards Robust Monocular Depth Estimation: Mixing Datasets for Zero-Shot Cross-Dataset TransferRanftl, Lasinger, Hafner, Schindler & Koltun (TPAMI 2020) · the MiDaS line: relative depth trained on mixed datasets, calibrated by scale-invariant losses and evaluated after alignment. The baseline every later model measures itself against, still maintained as MiDaS v3.1.Project pageDepth Anything — and the V3 lineYang, Kang, Huang, Xu, Feng & Zhao (CVPR 2024) · the DINOv2 + DPT recipe that became the default: 1.5 M labelled plus 62 M unlabelled images, relative depth better than MiDaS v3.1, a metric fine-tune better than ZoeDepth, and model sizes from 24.8 M (V2-Small) to 335.3 M (V2-Large). Depth Anything 3 was released on 14 Nov 2025 and published at ICLR 2026 (arXiv 2511.10647): a plain DINOv2 encoder with no architectural specialisation, a Dual-DPT head over depth and ray maps, any number of views, with or without poses.Original paperRepurposing Diffusion-Based Image Generators for Monocular Depth EstimationKe, Obukhov, Huang, Metzger, Daudt & Schindler (CVPR 2024) · Marigold: depth as conditional image-to-image diffusion on a Stable Diffusion 2 backbone, 10–50 denoising steps per map, affine-invariant output, and the sharpest object boundaries of any model in this lesson.Original paperZoeDepth: Zero-shot Transfer by Combining Relative and Metric DepthBhat, Birkl, Wofk, Wonka & Müller (2023) · the metric branch: a relative-depth backbone plus learned metric bins, with separate indoor and outdoor heads — the reason a metric model's accuracy is inseparable from the domain and intrinsics it was fitted on.Original paperDINOv2: Learning Robust Visual Features without SupervisionOquab et al. (2023) · the self-supervised encoder underneath the depth head: features that transfer to dense prediction without labels, which is why a small head trained on depth data generalises across indoor, outdoor, medical and satellite images. DA3 fine-tunes it at 5e-6 rather than freezing it.

Lesson text adapted from AI Engineering from Scratch (Phase 04, Lesson 26) and the Math Foundations Notebook reference build. The five labs — the depth-map visualizer with its 1/Z hyperbola, the back-projection bench with the growing point cloud, the scale-ambiguity bench, the dated model comparator and the failure board — are original to this page, as is the arithmetic they compute: the depth-map bookkeeping (518 × 518 × 1 = 268,324 values = 1.07 MB float32 = 268 KB uint8, 8.05 MB/s at 30 fps), the stereo error law δZ = Z²δd/(f·B) at f = 480 px and a 6 cm baseline (28.80 px and 1.7% at 1 m, 2.88 px and 17.4% at 10 m, 0.29 px and 174% at 100 m), the patch arithmetic (518/14 = 37, so 1,369 tokens), the two hand-worked back-projections (pixel (400, 340) at 3.00 m → (0.881, 0.506, 3.000) m, range 3.167 m, 18.7° off axis; the 60°-FOV focal check at 448.6 px), the evaluation example (eight pixels → AbsRel 0.0875, δ<1.25 = 0.875), the alignment example (a = 2.000, b = 0.100, raw 0.513 → aligned 0.0175), the scale-bench numbers (raw 0.038/0.995/2.990/6.980 → aligned 0.0145), the detection lift (box (290, 150)–(410, 405) at 3.20 m → (0.607, 0.123, 3.200) m, 1.70 m tall and 0.80 m wide from the box) and the failure-board measurements (glass 2.40 m predicted at 5.80 m, a textureless wall drifting 2.70–5.30 m, a 4-pixel occlusion band at 3.50–6.00 m, an unmaskable sky). Every number shown is computed live by the labs or verified by hand in the prose.