Computer Vision Chapter 26

Camera calibration

A camera maps 3D points to pixels through intrinsics (focal length, principal point) and distortion (radial/tangential lens effects). Zhang’s method estimates these from multiple views of a flat chessboard with known square size. OpenCV’s calibrateCamera returns cameraMatrix and distCoeffs for undistort, projectPoints, stereo, and pose estimation. Below: corner detection, calibration, undistortion, reprojection, and saving parameters—with several runnable snippets.

Chessboard object points

Define 3D coordinates of inner corners in board units (e.g. mm). For a board with cols × rows inner corners, Z = 0 on the plane.

import numpy as np

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

Find and refine corners

import cv2

img = cv2.imread("calib_01.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
pattern = (cols, rows)
found, corners = cv2.findChessboardCorners(gray, pattern, None)

if found:
    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
    corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
    cv2.drawChessboardCorners(img, pattern, corners2, found)

Adaptive flags for hard images

flags = cv2.CALIB_CB_ADAPTIVE_THRESH + cv2.CALIB_CB_NORMALIZE_IMAGE
found, corners = cv2.findChessboardCornersSB(gray, pattern, flags)  # often sharper

findChessboardCornersSB is available in newer OpenCV; fall back to findChessboardCorners if missing.

Run calibrateCamera

Collect objpoints (repeated objp per image) and imgpoints (detected corners). Image size is (width, height).

objpoints = []   # 3d in world
imgpoints = []   # 2d in image

# loop over images: append objp and corners2 when found
# ...

h, w = gray.shape[:2]
rms, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
    objpoints, imgpoints, (w, h), None, None,
    flags=cv2.CALIB_FIX_K3)  # optional: fix higher radial terms for wide FOV

print("RMS reprojection error (px):", rms)
print("K:\n", mtx)
print("dist:", dist.ravel())

Undistort

One-off

dst = cv2.undistort(img, mtx, dist)

Remap (faster for video)

newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h))
mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w, h), cv2.CV_32FC1)
undist = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
x, y, w2, h2 = roi
undist_cropped = undist[y:y+h2, x:x+w2]

projectPoints and error check

imgpts, _ = cv2.projectPoints(objp, rvecs[0], tvecs[0], mtx, dist)
err = cv2.norm(imgpoints[0], imgpts.reshape(-1, 2), cv2.NORM_L2) / len(imgpts)

Average per-corner error for one image; aggregate over the dataset to judge calibration quality.

Save and load

np.savez("calib.npz", mtx=mtx, dist=dist, rms=rms)
D = np.load("calib.npz")
mtx, dist = D["mtx"], D["dist"]

Write YAML with OpenCV

fs = cv2.FileStorage("calib.yml", cv2.FILE_STORAGE_WRITE)
fs.write("camera_matrix", mtx)
fs.write("dist_coeffs", dist)
fs.release()

Practical tips

  • Cover the full frame and multiple angles; avoid all images from the same pose.
  • Use sharp prints; matte board reduces glare.
  • For strong wide-angle / fisheye, consider cv2.fisheye routines instead of the pinhole + Brown model.

Takeaways

  • K and dist describe the camera; rvec, tvec per image place the board in camera coordinates.
  • Lower RMS → better fit; outliers usually mean bad corner detection.
  • Use remap when undistorting video streams.

Quick FAQ

Re-detect corners with sub-pixel refinement, remove blurry frames, verify pattern size (inner corners count), and ensure square size is consistent.

Normal—manufacturing and alignment shift cx, cy. Never force it to center unless you have a reason (some SLAM systems fix principal point with extra flags).