Part IX: Radar, Lidar, Depth, Thermal, Event, and RF Sensing
Chapter 40: Depth and 3D Sensing Fundamentals

Calibration and coordinate frames

"Every one of my sensors is certain it sits at the center of the universe. My job is to talk them out of it before they collide."

A Diplomatic AI Agent

Prerequisites

This section assumes you can read a depth map and a point cloud as produced in Section 40.2, and that you have met the projective measurement model of a camera in Chapter 2. Rotation and rigid-body motion are developed in depth alongside inertial estimation in Chapter 24; here we need only matrix multiplication and the idea that a rotation is an orthonormal \(3\times 3\) matrix. Linear algebra as in Appendix A is enough.

The Big Picture

A depth sensor tells you where things are, but "where" is meaningless until you say where relative to what. A stereo rig reports points in its own optical frame, a lidar in its spinning-head frame, the wheels in a body frame, the map in a fixed world frame. None of these agree by default, and a system that fuses them without first nailing down the transforms between them is not fusing data, it is averaging fiction. Calibration is the unglamorous discipline that makes every sensor speak the same spatial language. Get it right and a lidar point, a camera pixel, and a robot gripper all land on the same physical millimeter. Get it wrong by half a degree and your obstacle appears a lane over at fifty meters.

Frames, poses, and the rigid-transform chain

A coordinate frame is an origin plus three orthonormal axes attached to a physical thing: the camera's optical center, the lidar's rotation axis, the vehicle's rear axle, a surveyed point on the factory floor. A 3D point has different numeric coordinates in each frame even though it is one point in the world. The what of calibration is finding, for every pair of frames you care about, the rigid transform that converts coordinates from one to the other; the why is that perception, planning, and control each live in a different frame and must exchange geometry constantly.

A rigid transform from frame \(B\) to frame \(A\) is a rotation \(R \in SO(3)\) and a translation \(t \in \mathbb{R}^3\), packed into a \(4\times 4\) homogeneous matrix so that composition becomes matrix multiplication:

$$ {}^{A}T_{B} = \begin{bmatrix} R & t \\ 0\;0\;0 & 1 \end{bmatrix}, \qquad {}^{A}p = {}^{A}T_{B}\, {}^{B}p . $$

The leading superscript names the frame the coordinates are expressed in. This bookkeeping is not pedantry: the single most common calibration bug is applying a transform backwards, and \({}^{A}T_{B}\) versus its inverse \({}^{B}T_{A} = {}^{A}T_{B}^{-1}\) differ by exactly that mistake. Transforms compose along a chain, \({}^{A}T_{C} = {}^{A}T_{B}\,{}^{B}T_{C}\), which is what lets you move a lidar point through the sensor mount, into the body frame, and out into the world in one product. Because \(R\) is orthonormal, its inverse is its transpose, and the inverse transform is \(R^\top\) with translation \(-R^\top t\), never a naive negation.

Key Insight

Calibration splits cleanly into two questions that are solved by different means. Intrinsics describe what happens inside one sensor: how a 3D ray becomes a pixel or a range. Extrinsics describe the rigid pose between sensors, or between a sensor and a body. Intrinsics are a property of the device and change only with temperature and age; extrinsics change every time someone bumps the mount. Diagnose them separately, because a reprojection error can come from either and blaming the wrong one wastes days.

Intrinsic calibration: turning pixels into rays

For a depth camera derived from a pinhole model, the intrinsics are the focal lengths \(f_x, f_y\) and principal point \((c_x, c_y)\) that map a 3D point in the camera frame to a pixel, plus lens-distortion coefficients that bend straight lines. The forward projection of a point \((X, Y, Z)\) is

$$ u = f_x \frac{X}{Z} + c_x, \qquad v = f_y \frac{Y}{Z} + c_y . $$

Inverting it is what makes depth 3D: given a pixel \((u,v)\) and its measured depth \(Z\), you recover \(X = (u - c_x)\,Z / f_x\) and \(Y = (v - c_y)\,Z / f_y\). This back-projection is the bridge between the depth map and the point cloud of Section 40.2, and every point cloud is only as accurate as the intrinsics behind it. A principal point wrong by five pixels or a focal length off by one percent shears the whole cloud, so a plane looks slightly bowled and registration in Section 40.4 never quite converges. Radial distortion, if left uncorrected, curves what should be straight walls, an error that grows toward the image edges precisely where wide-baseline stereo is already weakest.

Intrinsics are estimated by showing the sensor a known target, classically a checkerboard, from many poses and minimizing the reprojection error between detected corners and their projected model positions. The when: do this once per device at build time, then re-check whenever thermal cycling or a lens knock is suspected, because a depth camera's baseline can drift with a few degrees of temperature change.

Extrinsic calibration: putting every sensor in one frame

Extrinsic calibration finds \({}^{A}T_{B}\) between two sensors so their observations of the same scene overlap. The three cases you will meet constantly are camera-to-camera (the stereo baseline of Section 40.1), camera-to-depth (aligning a color image to a depth map so each pixel gets both), and camera-to-lidar or lidar-to-IMU for a full robot stack. Each is solved by observing shared structure: mutual views of a board for cameras, a board whose plane is visible to both camera and lidar for the cross-modal case, and a motion sequence for camera-to-IMU, where the classic hand-eye formulation \(AX = XB\) recovers the unknown mounting pose \(X\) from the two sensors' relative motions \(A\) and \(B\).

The code below composes an extrinsic and does the color-to-depth alignment that a wearable or an RGB-D camera performs on every frame: back-project depth pixels to 3D, move them into the color camera's frame, and project them into the color image so each depth sample inherits a pixel of color.

import numpy as np

def align_depth_to_color(depth, K_d, K_c, T_c_d):
    """Map each depth pixel into the color camera's image.
    depth: HxW depth in meters (depth-camera frame)
    K_d, K_c: 3x3 intrinsics for depth and color cameras
    T_c_d: 4x4 rigid transform, depth frame -> color frame
    """
    H, W = depth.shape
    u, v = np.meshgrid(np.arange(W), np.arange(H))
    z = depth.reshape(-1)
    valid = z > 0
    # back-project depth pixels to 3D rays in the depth frame
    x = (u.reshape(-1) - K_d[0, 2]) * z / K_d[0, 0]
    y = (v.reshape(-1) - K_d[1, 2]) * z / K_d[1, 1]
    pts_d = np.stack([x, y, z, np.ones_like(z)], axis=0)   # 4xN homogeneous
    pts_c = T_c_d @ pts_d                                    # into color frame
    # project into the color image
    proj = K_c @ pts_c[:3]
    uu = proj[0] / proj[2]
    vv = proj[1] / proj[2]
    return uu[valid], vv[valid], pts_c[2, valid]            # color pixels + their depth
Color-to-depth registration in one pass: the extrinsic T_c_d carries every back-projected depth sample into the color camera's frame before the intrinsic K_c reprojects it, so a wrong extrinsic and a wrong intrinsic produce visibly different misalignments (a rigid shift versus a shear).

Notice the single line pts_c = T_c_d @ pts_d is where extrinsic error enters and the two K matrices are where intrinsic error enters, which is exactly why the key insight above insists you separate them. The output pixels (uu, vv) tell you where each depth sample lands in color; a systematic constant offset means the extrinsic translation is off, while a fan-shaped spread that grows with depth means the rotation is off.

Let OpenCV and Open3D own the estimation

Writing a distortion-aware checkerboard detector, a Levenberg-Marquardt reprojection minimizer, and a hand-eye solver from scratch is several hundred lines and a week of edge cases. In practice you call the library: cv2.calibrateCamera returns intrinsics and distortion from a stack of board images, cv2.stereoCalibrate returns the stereo extrinsic, and cv2.calibrateHandEye solves \(AX = XB\), each in a handful of lines. Open3D's PinholeCameraIntrinsic plus create_from_depth_image collapses the entire back-projection above to two calls. You supply correspondences and coordinate conventions; the library owns the optimizer, the Jacobians, and the numerical stability. Roughly 300 lines become 15, and the 15 are correct.

The transform tree, time, and staying honest

A real platform has more than two frames, so the transforms are organized as a tree with the world at the root and each sensor a leaf, exactly the tf tree that middleware like ROS maintains. Any frame-to-frame query is answered by walking the tree and multiplying transforms along the path. Some edges are static (a bolted-down camera relative to the chassis) and some are dynamic (the chassis relative to the world, updated by odometry or SLAM). Keeping this tree consistent is a running concern, not a one-time setup, and it links tightly to the localization and mapping of Chapter 52.

Two failure modes dominate. The first is temporal: transforming a lidar point captured at time \(t\) using a body pose from time \(t + \delta\) smears the cloud whenever the platform moves, so calibration is inseparable from the timestamp synchronization of Chapter 3. At highway speed a 20 ms sync error is a 30 cm position error. The second is drift: mounts flex, thermal expansion shifts baselines, and vibration slowly rotates a bracket, so extrinsics that were perfect at the factory decay in the field. Mature systems therefore monitor calibration continuously, watching reprojection residuals or plane-fit consistency, and either recalibrate online or raise a health flag.

Automotive: the half-degree that moved a pedestrian

An autonomous shuttle team fused a roof lidar with a windshield camera to label objects with both range and class. In validation the fused boxes drifted: a pedestrian correctly seen by both sensors at 10 m agreed to within a pixel, but at 45 m the lidar box and the camera box separated by more than a body width, enough to assign the person to the wrong lane. Intrinsics checked out, so the team looked at the lidar-to-camera extrinsic and found the rotation was off by about \(0.4^\circ\), well inside what a coarse target-based calibration tolerates. That angular error projects to a lateral offset that grows linearly with range: negligible up close, dangerous far away. The fix was a target-free extrinsic refinement that maximized edge alignment between the projected lidar depth and the image gradients across a driving log, pulling the residual under \(0.05^\circ\). The lesson is structural: a tolerance that is invisible near the sensor becomes the dominant error at the ranges where you most need to trust it.

Exercise: forward, inverse, and the sign that bites

You are given T_c_d, the depth-to-color extrinsic, as a \(4\times 4\) matrix, and asked to render the color image into the depth camera's frame instead. (1) Write the transform you must apply and express it in terms of the rotation \(R\) and translation \(t\) of T_c_d, without calling a matrix-inverse routine. (2) A colleague implements the inverse as \([R^\top \mid -t]\). Give the specific geometric error this produces and the pixel-space symptom you would see. (3) Extend align_depth_to_color so that samples projecting outside the color image bounds or landing behind the camera (\(Z \le 0\)) are dropped, and explain why the behind-camera check is not optional.

Self-check

  1. Why does an error in the principal point \((c_x, c_y)\) distort a back-projected point cloud differently from an error in the focal length \(f_x\)? Describe the shape of each distortion.
  2. A fused lidar-camera box is perfectly aligned at 8 m but separates at 40 m. Is the more likely culprit the extrinsic rotation or the extrinsic translation, and why does range amplify one but not the other?
  3. Your point cloud smears only when the robot is turning, never when it drives straight or stands still. Is this a spatial-calibration bug or a timing bug, and what single measurement would confirm it?

What's Next

In Section 40.4, we take the clouds that calibration has placed in a common frame and clean them: removing outliers, downsampling for speed, estimating normals, and registering overlapping scans with ICP so that many partial views become one coherent model. Calibration puts sensors into approximate agreement; registration is how you refine that agreement point by point.