08 — Camera Calibration: Video Tutorials
How to use this page
Lecture 6 recommends nine short videos about camera calibration. This page turns each one into a tutorial: watch the video, read the notes in plain English, run the updated code, answer the quiz, then do the lab notebook made for that video. The videos are by other people; the notes follow what each video teaches and point out where the code needs changing for OpenCV 5 and Raspberry Pi OS Bookworm.
Show the camera a pattern whose real shape you know (a printed chessboard), see where its corners land in the picture, and solve for the numbers (focal length, image centre, lens distortion) that explain the difference. Then use those numbers to straighten every future picture and to measure in millimetres.
Analogy. An eye doctor shows you a chart whose letters she knows, sees how you read it, and writes down your prescription. The chessboard is the chart, the camera is your eye, and the calibration result (camera matrix K + distortion coefficients) is the camera's prescription.
Learning path
| Step | Video | You learn | Lab |
|---|---|---|---|
| 1 · Ideas | Intrinsics & extrinsics, intrinsic calibration (Cyrill Stachniss, 5 min each) | what the numbers mean | 6M |
| 2 · Maths | Zhang's method (Cyrill Stachniss, lecture) | how a chessboard gives K | |
| 3 · Overview | Calibration explained, OpenCV Python (Kevin Wood) | the full workflow & how to improve it | 6K |
| 4 · Code | In < 5 minutes, the script line by line (Nicolai Nielsen) | capture images, calibrate, save, undistort | 6J |
| 5 · Better targets | ChArUco boards (Nicolai Nielsen) | partial views, outliers, JSON | 6L |
| 6 · Your Pi | Calibrate a camera in 15 minutes (The Drone Dojo) | the Raspberry Pi workflow + a ruler test | 6I |
Before you start: you need Python with opencv-python numpy matplotlib (Colab has them), a printed
9 × 6 chessboard glued to something flat, and a ruler.
1 · Intrinsics & extrinsics
If the video does not play here, open it on YouTube.
Extrinsics say where the camera is and where it looks: 3 numbers for position and 3 for direction (6 degrees of freedom). Intrinsics say what the camera is like inside: how a ray of light becomes a pixel when the camera sits at the origin.
Key ideas
- The camera model is a mathematical description of how the camera works; intrinsics and extrinsics are its parameters (also called interior and exterior orientation).
- The location of the camera is the projection centre, the point where all rays meet in the pinhole model.
- Basic intrinsics: the camera constant (distance image plane ↔ projection centre, i.e. the focal length), a scale difference between x and y (equivalently fx and fy), the principal point (where the optical axis hits the image, near but never exactly at the centre) and a shear (≈ 0 for digital cameras).
- 6 extrinsic + 5 intrinsic = 11 degrees of freedom, all inside the 3 × 4 projection matrix P: x = P X (homogeneous coordinates). This ideal model has no lens distortion; real lenses need extra non-linear parameters.
- Estimating the intrinsics = calibration. Estimating only the extrinsics = camera localization.
Try it
import numpy as np, cv2
K = np.array([[800, 0, 640], [0, 800, 360], [0, 0, 1.0]]) # intrinsics: f = 800 px, principal point (640, 360)
R, _ = cv2.Rodrigues(np.array([0.0, 0.2, 0.0])) # extrinsics: turned 0.2 rad around y ...
t = np.array([[0.1], [0.0], [2.0]]) # ... and 2 m in front of the camera
P = K @ np.hstack([R, t]) # 3 x 4 projection matrix
X = np.array([0.0, 0.0, 0.0, 1.0]) # a world point (homogeneous)
x = P @ X
print("pixel:", x[:2] / x[2]) # divide by the 3rd coordinate✏️ Check yourself
- How many degrees of freedom does P have, and why not 12?
Show answer
11: 6 extrinsic + 5 intrinsic. P has 12 numbers, but it is only defined up to scale (multiplying P by 2 gives the same pixels).
- You know K and want to find where a drone camera is. Calibration or localization?
Show answer
Localization: only the extrinsics (R, t) are unknown, e.g. with
cv2.solvePnPon known markers.
2 · Intrinsic calibration
If the video does not play here, open it on YouTube.
The goal of calibration: for every pixel, know the exact direction of the ray it sees. Then a pixel tells you on which line in 3-D the object lies.
Key ideas
- Model: x = K R [I | −X0] X. K holds the intrinsics, R the viewing direction, X0 the projection centre.
- The DLT can estimate K, R, X0 together, but needs ≥ 6 points with known 3-D coordinates. That is hard to set up.
- Zhang's method needs only a flat checkerboard: each photo gets its own coordinate system on the board, so every corner has Z = 0 and known X, Y (you know the square size). At least 3 photos (more in practice).
- In practice: print the board, glue it flat, do the "calibration dance" in front of the camera, feed the photos to the algorithm.
- Lens effects such as barrel distortion are added as extra non-linear parameters and estimated by least squares; in general every pixel may get its own small correction.
✏️ Check yourself
- Why must the board be glued to something flat?
Show answer
The method assumes Z = 0 for every corner. A bent board breaks that assumption and biases K and the distortion.
- After calibration, what can you do that you could not before?
Show answer
Straighten (undistort) images so straight lines are straight, and turn pixels into exact 3-D ray directions for measuring and 3-D reconstruction.
3 · Zhang's method
If the video does not play here, open it on YouTube.
Each tilted photo of the flat board gives two clues about K. Three photos give six clues, enough to solve for the camera's inside numbers in closed form. Then a least-squares step polishes everything, including lens distortion.
The recipe, step by step
- One homography per photo. With Z = 0 the projection drops the 3rd column of R: [u v 1]ᵀ ≃ H [X Y 1]ᵀ with H = K [r1 r2 t]. Estimate H like the DLT (SVD); 4 points are enough, you have 54.
- Use what you know about rotations. r1 and r2 are perpendicular and have length 1. Writing ri = K⁻¹hi gives two equations per photo on the symmetric matrix B = K⁻ᵀK⁻¹:
h1ᵀ B h2 = 0 and h1ᵀ B h1 = h2ᵀ B h2. - Solve. B has 6 unknowns, known up to scale → 5 → 3 photos. Stack the equations as V b = 0 and take the SVD.
- Get K. B is symmetric positive-definite, so a Cholesky decomposition B = L Lᵀ gives K⁻¹ = Lᵀ.
- Refine. Add distortion parameters and minimise the reprojection error (detected corner − predicted corner) over everything with least squares. This is what
cv2.calibrateCameradoes.
Lab 6M implements steps 1–4 in about 30 lines of NumPy and recovers K within 0.1 %.
✏️ Check yourself
- Why does one photo give only two equations, not three?
Show answer
The orthonormality of r1, r2 gives exactly two constraints (perpendicular, equal length). r3 is not seen because Z = 0 removes it.
- What goes wrong if all photos are taken straight-on (board parallel to the sensor)?
Show answer
The equations become nearly dependent: you cannot tell focal length from distance. Tilted views are essential.
▶ Lab 6M in Colab View notebook Labs page DLT, homographies and Zhang's closed form in NumPy, then OpenCV's refinement on a real GoPro.
4 · Calibration explained (Kevin Wood)
If the video does not play here, open it on YouTube.
Chessboard pictures go in; camera parameters come out: intrinsics, extrinsics and distortion coefficients. Use them for precise measurement or to remove image distortion.
Step by step (as in the video)
- World frame → camera frame = extrinsics (rotation + translation). Camera frame → image frame = intrinsics (focal length, camera centre).
- Radial distortion: the image bulges out (barrel) or in (pincushion). Tangential: the sensor is tilted relative to the lens. Each moves a point's x, y; together they are the distortion coefficients (k's radial, p's tangential).
- Folder layout: a main folder → the calibration script, the 9 × 6 chessboard image, and a folder of calibration pictures (file names don't matter).
- Take pictures at different angles, covering different parts of the image.
- Install
numpy opencv-python matplotlib, run the script: it draws the corners, plots all pictures, prints the parameters and writes a JSON (reprojection error, camera matrix, distortion, rvecs, tvecs).
- Take more pictures.
- Vary the board's orientation and position more, including extreme cases.
- Make sure the entire board is in every picture; replace any that are cut off.
- Remove blurry pictures.
Lab 6K turns these four tips into an automatic calibration doctor.
5 · OpenCV Python (Kevin Wood)
If the video does not play here, open it on YouTube.
Key ideas
- x = P X: P (3 × 4) splits into the intrinsic matrix (f, cx, cy) and the extrinsic [R | t]. OpenCV stores R as a 3 × 1 rotation vector (Rodrigues).
- Used for AR, SLAM, 3-D reconstruction, or just undistorted images.
- Find only the inner corners, refine them with
cornerSubPix, then solve a system A h = 0, using Z = 0 for the board. - His result: reprojection error 0.17 px, "pretty good".
- Tips: count the corners correctly; take ≥ 10 images in different planes (otherwise a degenerate case); keep the whole board in view.
- To remove distortion he uses
getOptimalNewCameraMatrixwith alpha = 1 (keep all pixels), thenundistort, and draws a straight line to show the curve is gone.
The code (OpenCV 4 and 5)
import glob, cv2, numpy as np
nRows, nCols = 9, 6 # inner corners
termCriteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
worldPtsCur = np.zeros((nRows * nCols, 3), np.float32)
worldPtsCur[:, :2] = np.mgrid[0:nRows, 0:nCols].T.reshape(-1, 2)
worldPtsList, imgPtsList = [], []
for path in glob.glob("calibration/*.jpg"):
gray = cv2.cvtColor(cv2.imread(path), cv2.COLOR_BGR2GRAY)
found, corners = cv2.findChessboardCorners(gray, (nRows, nCols), None)
if found:
worldPtsList.append(worldPtsCur)
corners = cv2.cornerSubPix(gray, corners.reshape(-1, 1, 2), (11, 11), (-1, -1), termCriteria)
imgPtsList.append(corners) # reshape: OpenCV 5 returns (N, 2)
repError, camMatrix, distCoeff, rvecs, tvecs = cv2.calibrateCamera(
worldPtsList, imgPtsList, gray.shape[::-1], None, None)
print("Camera Matrix:", camMatrix, "Reproj Error (pixels): {:.4f}".format(repError), sep="\n")
img = cv2.imread("distorted.jpg"); h, w = img.shape[:2]
camMatrixNew, roi = cv2.getOptimalNewCameraMatrix(camMatrix, distCoeff, (w, h), 1, (w, h))
imgUndist = cv2.undistort(img, camMatrix, distCoeff, None, camMatrixNew)
cv2.line(imgUndist, (100, 300), (w - 100, 300), (0, 0, 255), 2) # a straight line to compare against✏️ Check yourself
- What does alpha = 1 mean in
getOptimalNewCameraMatrix?Show answer
Keep all source pixels (you get curved black borders). alpha = 0 zooms in until every output pixel is valid.
- Why can 10 images all taken in the same plane be useless?
Show answer
They give (almost) the same equations: a degenerate case where the solver cannot separate the parameters.
▶ Lab 6K in Colab View notebook Labs page His app on 36 GoPro images: all-corners plot, JSON, P = K[R|t] by hand, straight-line test, calibration doctor.
6 · In < 5 minutes (Nicolai Nielsen)
If the video does not play here, open it on YouTube.
The workflow
- Clone his repository (or use our copies in
LAB/Lab_6) and create a folderimages. - Run
get_images.py: the webcam opens; press s to save a picture, ESC to stop. He shows the board on an iPad; a print works too. Rotate and move it around the frame; 10–20 images are enough. - Set the chessboard size and frame size in
calibration.pyand run it. - It saves
calibration.pkl,cameraMatrix.pklanddist.pkl, prints a total error and undistorts with bothundistortandremap. Both look the same; remap is faster, so use it for real-time video.
python get_images.py # s = save, ESC = quit -> images/img0.png, img1.png, ...
python calibration.py # edit chessboardSize and frameSize firstA tablet screen works for a webcam, but avoid reflections and keep the screen brightness steady. Measure the square size on the screen if you need millimetres. For the best accuracy, use a print on a rigid board.
7 · The script, line by line
If the video does not play here, open it on YouTube.
What each part of the script does
chessboardSize = (24, 17) # inner corners of HIS board; ours is (9, 6)
frameSize = (1440, 1080) # (width, height) of the images
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) # stop rule for cornerSubPix
objp = np.zeros((chessboardSize[0] * chessboardSize[1], 3), np.float32) # ideal points (0,0,0), (1,0,0) ...
objp[:, :2] = np.mgrid[0:chessboardSize[0], 0:chessboardSize[1]].T.reshape(-1, 2)
objpoints, imgpoints = [], [] # 3-D points in the world, 2-D points in the image
for image in glob.glob("*.png"):
img = cv2.imread(image)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # OpenCV loads BGR; the detector wants grayscale
ret, corners = cv2.findChessboardCorners(gray, chessboardSize, None)
if ret:
objpoints.append(objp)
corners2 = cv2.cornerSubPix(gray, corners.reshape(-1, 1, 2), (11, 11), (-1, -1), criteria)
imgpoints.append(corners2)
cv2.drawChessboardCorners(img, chessboardSize, corners2, ret)
ret, cameraMatrix, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, frameSize, None, None)- He explains the pinhole model (3-D point → image plane through the optical centre; focal length = distance pinhole ↔ image plane) and why barrel/pincushion distortion ruins measurements such as stereo disparity.
- From Zhang's paper: the error drops a lot from 1 to 3–5 views and only a little after that. His demo uses 5 images, which is the minimum. For your project use 15–25.
His total error divides the L2 norm of all residuals of a view by the number of corners. It is about √N times smaller than the RMS reprojection error that calibrateCamera returns. On the dash-cam: total error 0.11 vs RMS 0.85 px. Always say which one you report. In this course: RMS in pixels.
✏️ Check yourself
- Why do we convert to grayscale before
findChessboardCorners?Show answer
The corner detector works on one intensity channel; colour adds nothing for a black-and-white board.
- Your images are 1280 × 720 but two are 1281 × 721. What happens?
Show answer
calibrateCamerastops with a size error (or worse, gives a wrong K). Resize or drop them. Lab 6J has exactly this case.
▶ Lab 6J in Colab View notebook Labs page His two scripts on the dash-cam set: pickle files, undistort vs remap timing, total error vs RMS, how many views.
8 · ChArUco boards
If the video does not play here, open it on YouTube.
A ChArUco board is a chessboard with a little ArUco marker (a square barcode) in every white square. Every corner then has its own ID, so even a partly visible board is useful, which a plain chessboard is not.
What the video does
- Capture 10–20 pictures with a small script (s saves, ESC quits) and try to cover about half the image each time.
- Upload them to his calibration web app (commercial), enter the board: 18 × 11 squares, square and marker size, dictionary 4 × 4, 100 markers.
- Get a JSON: focal lengths, optical centre, distortion, and the rvec/tvec of each board. Then look at the reprojection error per image: remove outliers and rerun. Under ~0.2 px is very good.
- A 3-D view shows where each board was relative to the camera. Then undistort an image with
getOptimalNewCameraMatrix+undistortand crop to the ROI. - Even ten "identical" cameras each need their own calibration: every unit is slightly different.
The same thing, free, in OpenCV
aruco_dict = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_100)
board = cv2.aruco.CharucoBoard((18, 11), 0.030, 0.022, aruco_dict) # squares x, y; square, marker (m)
cv2.imwrite("charuco_18x11.png", board.generateImage((3600, 2200), marginSize=80)) # print it
detector = cv2.aruco.CharucoDetector(board)
all_obj, all_img = [], []
for f in sorted(glob.glob("output_images/*.png")):
gray = cv2.imread(f, cv2.IMREAD_GRAYSCALE)
ch_corners, ch_ids, m_corners, m_ids = detector.detectBoard(gray)
if ch_ids is not None and len(ch_ids) >= 10:
obj, img = board.matchImagePoints(ch_corners, ch_ids)
all_obj.append(obj); all_img.append(img)
rms, K, D, rvecs, tvecs, _, _, per_view = cv2.calibrateCameraExtended(all_obj, all_img, gray.shape[::-1], None, None)
keep = [i for i, e in enumerate(per_view.ravel()) if e <= 2 * np.median(per_view)] # drop outliers, recalibrateThe old cv2.aruco.calibrateCameraCharuco and interpolateCornersCharuco are gone. Use CharucoDetector + matchImagePoints + calibrateCamera as above.
▶ Lab 6L in Colab View notebook Labs page The video's 18 × 11 board: detect IDs, per-image errors, remove 2 shaky shots, JSON export, 3-D board view, ROI crop.
9 · Raspberry Pi camera in 15 minutes
If the video does not play here, open it on YouTube.
The video calibrates a Raspberry Pi camera for a drone that must know exactly where objects are (pose estimation). A script grabs 20 good chessboard pictures by itself while you move the board, computes the camera matrix and distortion, and then an ArUco marker and a ruler prove the calibration measures real centimetres.
What you need
- Raspberry Pi 5 (or 4) with a Pi camera, Raspberry Pi OS Bookworm, and a desktop (monitor or VNC) because the script opens a camera window.
pattern.pngprinted on A4, glued to a flat cutting mat or board; a ruler.- An ArUco marker (ID 72 from the original dictionary in the video), printed and measured.
Step by step
- Log in. SSH for the terminal, VNC for the desktop. Check the camera works.
ssh pi@raspberrypi.local # or pi@<IP address> rpicam-hello --list-cameras # the camera must be listed - Get the scripts and the pattern. The video clones a repository with
calibrate.pyandpattern.png; our updated scripts are in the course repository:mkdir -p ~/calib && cd ~/calib for f in pi_calibrate_20.py aruco_pose_test.py; do wget https://raw.githubusercontent.com/Kazimbalti/intelligent-vision-systems/main/LAB/Lab_6/$f done wget https://raw.githubusercontent.com/opencv/opencv/4.x/doc/pattern.png sudo apt install -y python3-opencv python3-picamera2 # NOT "sudo pip install" (breaks Bookworm's Python) - Measure a square. In the video one square is 26 mm. Measure yours; this number turns the results into millimetres.
- Calibrate at the resolution you will use, 640 × 480 as in the video. Run from a terminal on the VNC desktop:
python3 pi_calibrate_20.py --mm 26 --width 640 --height 480 - Move the board. "Try to show the edges first, all the edges," then all other positions: near, far, tilted. The script only keeps a picture when the board is fully visible and in a new place (blue dots show the kept positions). After 20 it calibrates and saves
calibration_files/cameraMatrix.txt,cameraDistortion.txtandcamera.yaml. - Test with a marker and a ruler. Make and print the marker, measure the black square edge to edge (20 cm in the video), and run the test:
python3 aruco_pose_test.py --make-marker --id 72 # -> marker_id72.png, print it python3 aruco_pose_test.py --size 200 --id 72 --width 640 --height 480 - The ruler test. Move the marker until x ≈ 0 (centred on the camera axis). Slide it 6 cm along x with the ruler. The program should now say x ≈ 60 mm; a few mm off is human error. If it is far off, recalibrate.
| Video | Today |
|---|---|
sudo pip install opencv-python numpy | sudo apt install python3-opencv python3-picamera2 or a venv --system-site-packages: sudo pip is blocked or harmful on Bookworm |
| camera read as a webcam stream | Picamera2 (libcamera); --usb 0 still works for a USB webcam |
chmod 777 on the script | not needed with python3 script.py (and 777 gives everyone write access) |
cv2.aruco.estimatePoseSingleMarkers | cv2.solvePnP(..., cv2.SOLVEPNP_IPPE_SQUARE); the old function was removed in OpenCV 4.7 |
The test script must use the same width and height as the calibration (both 640 × 480 in the video). K is in pixels: at 1280 × 720 every entry is wrong.
✏️ Check yourself
- Why does the script ignore a frame if the board is in the same place as an earlier one?
Show answer
Twenty copies of the same view add almost no information. Different positions, sizes and tilts are what pin down K and the distortion.
- The ruler says 60 mm, the program says 75 mm. Name two likely causes.
Show answer
Wrong marker size passed to the script (or wrong square size during calibration), or a different resolution than the calibration. Also possible: a bad calibration (blurry or too similar frames).
- Why does the video show the board at the edges first?
Show answer
Distortion is largest near the edges; without corners there the model cannot learn it.
▶ Lab 6I in Colab View notebook Labs page The whole workflow on a virtual 640 × 480 Pi camera (frame filter, calibration, 60 mm ruler test), then on your real Pi.
Cheat sheet & troubleshooting
| Problem | Fix |
|---|---|
findChessboardCorners always returns False | Count inner corners (a 10 × 7-square board is 9 × 6); show the whole board; avoid glare |
| RMS > 1 px | Remove blurry/outlier views (per-view error), hold the board still, glue it flat |
| Edges still curved after undistort | Add views with the board near the image corners; wide lens → CALIB_RATIONAL_MODEL or cv2.fisheye |
| Distances wrong by a constant factor | Wrong square size or marker size |
| Everything wrong after changing resolution | Recalibrate, or scale fx, fy, cx, cy by the resize factor |
corners[0][0] is a number, not (x, y) | OpenCV 5 returns (N, 2): use corners.reshape(-1, 2) |
Pi camera not opening with VideoCapture(0) | Use Picamera2 (Bookworm uses libcamera) |
Back to the Lecture 6 slides · all Lab 6 notebooks.
Videos © their authors (Cyrill Stachniss, Kevin Wood, Nicolai Nielsen, The Drone Dojo), embedded from YouTube. Notes and code on this page were written for this course from the videos' content.