Lecture 10 — Calibration & Stereo

Lecture 10

Dr. Muhammad Kazim

Department of Intelligent Systems, University of Lahore

Today’s Roadmap

  • L19 — Calibration: intrinsic/extrinsic parameters; lens distortion models; Zhang’s planar calibration; reprojection error; undistortion & rectification.
  • L20 — Stereo: epipolar geometry; fundamental & essential matrices; rectification; block-matching & SGBM disparity; depth from disparity; RGB-D sensors.

Reading: Szeliski 2e §11.1–12.1. Lab 10 this week: calibration, stereo & depth on the Pi.

The Pinhole Camera, Formally

A 3-D point \(X = (X, Y, Z)\) projects to pixel \(x = (u, v)\) through:

\[ s \begin{bmatrix} u \\ v \\ 1 \end{bmatrix} = K \, [R \mid t] \begin{bmatrix} X \\ Y \\ Z \\ 1 \end{bmatrix}, \qquad K = \begin{bmatrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{bmatrix}

  • \(K\) — intrinsic matrix: focal lengths \((f_x, f_y)\) in pixels, principal point \((c_x, c_y)\).
  • \([R \mid t]\) — extrinsic matrix: the camera’s rotation and translation relative to the world frame.
  • \(s\) — the (unknown) depth/scale factor, recovered from \(Z\) after dividing through.

Why Calibrate?

  • Every consumer/embedded lens introduces geometric distortion — straight lines in the world bow in the image.
  • Metric tasks — measuring object size, stereo depth, AR overlay, SLAM — are meaningless without knowing \(K\) and the distortion coefficients.
  • Calibration is a one-time-per-camera-setup step: it must be redone if the lens, focus, or sensor changes.

Lens Distortion Models

Radial distortion (barrel/pincushion), the dominant term for most lenses:

\[ x_{\text{distorted}} = x(1 + k_1 r^2 + k_2 r^4 + k_3 r^6) \]

Tangential distortion (lens not perfectly parallel to the sensor):

\[ x_{\text{distorted}} = x + [2 p_1 x y + p_2(r^2 + 2x^2)] \]

OpenCV’s standard distortion vector is \((k_1, k_2, p_1, p_2, k_3)\). Wide-angle / fisheye lenses (common on embedded cameras) need the separate fisheye model — cv2.fisheye.*.

Zhang’s Planar Calibration Method

Zhang (2000) recovers \(K\) and distortion from ≥ 2 views of a flat pattern (checkerboard) at different unknown poses — no special calibration rig needed.

  1. Capture 15–25 images of a checkerboard from varied angles/distances, covering the frame corners as well as the centre.
  2. Detect chessboard corners in each image (cv2.findChessboardCorners), refine to sub-pixel accuracy.
  3. Each view gives a homography between the known planar board and the image; combine homographies across views to solve for \(K\) (closed form), then refine \(K\), distortion, and every view’s \([R\mid t]\) jointly by non-linear least squares (Levenberg–Marquardt).

Reprojection Error

The calibration’s optimisation objective — and its quality metric — is the reprojection error:

\[ E = \frac{1}{N}\sum_{i=1}^{N} \big\lVert x_i - \hat{x}_i(K, R, t, k) \big\rVert^2 \]

where \(x_i\) are the detected corners and \(\hat{x}_i\) is where the calibrated model predicts they should project.

  • Target: RMS reprojection error < 0.5 px. Above ~1 px, suspect a bad board size, blurry images, or too few varied poses.
  • Always calibrate at the resolution/focus you will actually run at — intrinsics change with both.

Code — Chessboard Calibration

import cv2, numpy as np, glob

cols, rows = 9, 6                       # inner corners
objp = np.zeros((rows * cols, 3), np.float32)
objp[:, :2] = np.mgrid[0:cols, 0:rows].T.reshape(-1, 2)

objpoints, imgpoints = [], []
for f in glob.glob("calib/*.jpg"):
    gray = cv2.imread(f, cv2.IMREAD_GRAYSCALE)
    ok, corners = cv2.findChessboardCorners(gray, (cols, rows))
    if ok:
        corners = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1),
            (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001))
        objpoints.append(objp); imgpoints.append(corners)

rms, K, dist, rvecs, tvecs = cv2.calibrateCamera(
    objpoints, imgpoints, gray.shape[::-1], None, None)
print(f"RMS reprojection error: {rms:.3f} px")
np.savez("camera_intrinsics.npz", K=K, dist=dist)

Undistortion & Rectification

data = np.load("camera_intrinsics.npz")
K, dist = data["K"], data["dist"]

frame = cv2.imread("test.jpg")
h, w = frame.shape[:2]
newK, roi = cv2.getOptimalNewCameraMatrix(K, dist, (w, h), alpha=0)
undist = cv2.undistort(frame, K, dist, None, newK)
x, y, w2, h2 = roi
undist = undist[y:y+h2, x:x+w2]           # crop the black border

Undistortion removes lens distortion from a single image. Rectification (stereo) additionally re-projects a pair of images onto a common plane so corresponding points share the same row — the prerequisite for fast disparity search, covered next.

Two-View / Epipolar Geometry

For a point \(X\) seen by two cameras, its projections \(x, x'\) and the two camera centres are coplanar — the epipolar constraint:

\[ x'^{\top} F x = 0 \]

  • \(F\) — the fundamental matrix (3×3, rank 2): relates pixel coordinates in uncalibrated cameras.
  • \(E = K'^{\top} F K\) — the essential matrix: relates normalised (calibrated) coordinates; encodes exactly the relative rotation and translation \((R, t)\) between the two cameras (up to scale).
  • Given a point in the left image, its match in the right image must lie on the corresponding epipolar line — this turns 2-D search into 1-D search.

Stereo Rectification

Rectification warps both images so that epipolar lines become horizontal and aligned row-by-row — then correspondence search is a simple 1-D scan along each row.

  1. Calibrate both cameras individually, then jointly (cv2.stereoCalibrate) to get \((R, t)\) between them.
  2. cv2.stereoRectify computes rectifying rotations and new projection matrices.
  3. cv2.initUndistortRectifyMap + cv2.remap apply the warp to each frame.

After rectification: \(\text{disparity} = u_{\text{left}} - u_{\text{right}}\) for the same row \(v\).

Disparity & Depth

For a rectified stereo pair with baseline \(B\) and focal length \(f\) (pixels):

\[ Z = \frac{f B}{d}, \qquad d = u_{\text{left}} - u_{\text{right}} \]

  • Depth is inversely proportional to disparity — nearby objects have large disparity (easy, precise); far objects have small disparity (hard, noisy).
  • Wider baseline → better far-range precision, but a larger “blind” minimum range and more occlusion.

Block-Matching & SGBM Disparity

  • Block matching (BM): for each pixel, slide a small window along the corresponding epipolar row in the other image and pick the offset with minimum cost (SAD/SSD). Fast, but noisy in textureless regions.
  • Semi-Global Block Matching (SGBM): adds a smoothness penalty aggregated along several 1-D directions through the disparity image, approximating a full 2-D global optimisation efficiently. Much better edges and textureless-region behaviour, at higher compute cost.
stereo = cv2.StereoSGBM_create(
    minDisparity=0, numDisparities=128, blockSize=5,
    P1=8 * 3 * 5**2, P2=32 * 3 * 5**2,
    disp12MaxDiff=1, uniquenessRatio=10,
    speckleWindowSize=100, speckleRange=32)
disp = stereo.compute(left_rect, right_rect).astype(np.float32) / 16.0
depth = (fx * baseline) / (disp + 1e-6)

RGB-D Sensors

Active alternatives to passive stereo, useful when the scene lacks texture for block matching:

  • Structured light (e.g. early Kinect) — project a known IR pattern, measure its distortion.
  • Time-of-Flight (ToF) — measure the phase/time delay of a modulated IR signal per pixel.
  • Active stereo (e.g. Intel RealSense) — stereo pair + an IR projector to add texture to blank surfaces.

Trade-offs vs passive stereo: works in the dark/on blank walls, but limited outdoor range (IR swamped by sunlight), extra power/cost, and a fixed baseline you can’t choose.

Common Pitfalls / Practitioner Notes

  • Calibrating with too few or too-similar board poses — vary tilt, distance and in-frame position; include the corners of the field of view.
  • A blurry or partially-out-of-frame board silently degrades \(K\) — always print the RMS error and re-shoot if > 0.5–1 px.
  • Forgetting to re-calibrate after changing focus/zoom — intrinsics are focus-dependent on most lenses.
  • Skipping rectification and running SGBM directly on raw stereo pairs — disparity search assumes row-aligned epipolar lines.
  • Textureless surfaces (a white wall) starve block matching of correspondences — project texture, use RGB-D, or fall back to monocular depth cues.

This Week’s Lab

Lab 10 — Calibration, Stereo & Depth on the Pi.

  • Calibrate the Pi camera; save camera_intrinsics.npz; report RMS reprojection error.
  • Undistort a live/still frame and confirm straight lines are straight.
  • Stereo-calibrate a pair (or use a supplied rectified pair) and compute a disparity map with SGBM.
  • Convert disparity to a coloured depth map.

Deliverable: a depth/disparity map on a real scene.

Key Takeaways

  • The camera model factors into intrinsics \(K\), distortion, and extrinsics \([R\mid t]\) — Zhang’s method recovers all of them from a moving planar checkerboard.
  • Reprojection error is both the calibration objective and your quality gate (< 0.5 px target).
  • The fundamental/essential matrices encode epipolar geometry; rectification turns 2-D stereo correspondence into a 1-D search.
  • Depth from disparity: \(Z = fB/d\) — precise near, noisy far, and undefined at zero disparity.

Further Reading

  • Szeliski, Computer Vision: Algorithms and Applications, 2e, §11.1–12.1.
  • Zhang, “A Flexible New Technique for Camera Calibration”, TPAMI 2000.
  • Hartley & Zisserman, Multiple View Geometry in Computer Vision, 2e — the definitive epipolar-geometry reference.
  • OpenCV docs: Camera Calibration and 3D Reconstruction module.