Search…

Camera Calibration and Perspective Correction

In this series (30 parts)
  1. Computer Vision Roadmap: From Basics to Real Projects
  2. What is Computer Vision? From Pixels to Decisions
  3. A Short History of Computer Vision: 1545 to Now
  4. Math for CV Beginners: Vectors, Matrices, Convolutions
  5. Image Fundamentals: Color, Histograms, Noise, Filtering
  6. How Images and Video Are Stored: Colour, JPEG, Frames
  7. OpenCV Setup + First 10 Tasks in Python
  8. Vision Metrics: Accuracy, Precision, Recall, mAP, IoU
  9. CV Project Workflow: Dataset, Baseline, Error Iteration
  10. Intensity Transforms and Frequency-Domain Filtering
  11. Edge Detection and Thresholding That Actually Work
  12. Morphology, Contours, and Shape Analysis for Real Images
  13. Feature Matching (SIFT/ORB) and Image Stitching
  14. Camera Calibration and Perspective Correction
  15. Epipolar Geometry, Stereo Vision, and Depth Estimation
  16. Optical Flow and Motion Tracking in Video
  17. HOG, HOF and MBH: Descriptors Before Deep Learning
  18. Your First Image Classifier (PyTorch + Transfer Learning)
  19. Data Pipelines and Augmentation for Vision Models
  20. CNN Architectures Explained: From LeNet to ResNet
  21. YOLO Detection Pipeline: Data to Inference
  22. Semantic and Instance Segmentation: U-Net to Mask R-CNN
  23. Vision Transformers (ViT) and When to Use Them
  24. CV Explainability: Grad-CAM, Failures, Bias Checks
  25. 3D Vision Basics: SfM, Point Clouds, and Pose Estimation
  26. Multimodal Vision: CLIP, Embeddings, and Retrieval Systems
  27. Video Understanding: Flow Networks, Interpolation, Stabilisation
  28. Real-Time CV Systems: Tracking, Latency, Streaming
  29. Deploying CV Models: ONNX, TensorRT, Edge, and APIs
  30. Responsible CV: Privacy, Fairness, Security, Governance

Every measurement you take from a photo is wrong until you calibrate. The lens bends straight lines, and a pixel has no fixed size in the real world. Calibration fixes both, and it takes about twenty photos of a printed checkerboard.

Prerequisites: Feature matching and homography, and matrix multiplication.

The problem: measuring gaskets with a phone

A small workshop cuts rubber gaskets and needs to check their diameter to within a millimetre. Buying a measuring microscope is expensive. A phone on a stand, pointing down at a lit surface, costs nothing.

Photograph a gasket you know is exactly 80 mm across. Measure it in the image and you get 340 pixels — but move the gasket to the corner of the frame and the same gasket measures 328 pixels. It has not changed size. The lens has bent it.

# Gasket position in frame Measured width (px) Implied diameter at 0.235 mm/px Error target
1 Centre 340 79.9 mm −0.1 mm
2 Halfway to the edge 336 79.0 mm −1.0 mm
3 Near the corner 328 77.1 mm −2.9 mm

The same 80 mm gasket measured at three positions in an uncalibrated frame

A 2.9 mm error on an 80 mm part is a 3.6% error, well outside tolerance. That error is not random noise you can average away — it is systematic, it depends on position, and calibration removes it exactly.

The pinhole model

Strip a camera down to its simplest form: a box with a tiny hole. Light from a point in the world travels in a straight line through the hole and lands on the sensor behind it. Similar triangles give the whole model:

x=fXZ,y=fYZx = f \cdot \frac{X}{Z}, \qquad y = f \cdot \frac{Y}{Z}

X,Y,ZX, Y, Z are the point’s position in the world, ff is the distance from the hole to the sensor, and x,yx, y is where it lands.

Try it. A part 100 mm wide sits 2,000 mm from a camera whose focal length is 800 pixels:

x=800×1002000=800×0.05=40 pixelsx = 800 \times \frac{100}{2000} = 800 \times 0.05 = 40 \text{ pixels}

Move the part to 1,000 mm and it doubles to 80 pixels. Move it to 4,000 mm and it halves to 20 pixels.

That curve carries an important warning. Image size depends on the ratio X/ZX/Z, so a 100 mm part at 2,000 mm and a 200 mm part at 4,000 mm both measure 40 pixels. One camera cannot separate size from distance. The workshop gets away with it only because the camera is on a fixed stand, so ZZ is known and constant.

focal length in pixels The focal length expressed in sensor pixels rather than millimetres. It combines the physical lens focal length with the pixel pitch, and it is what calibration actually recovers.

The intrinsic matrix

The pinhole equations assume the image origin sits at the optical axis. In a real image, (0,0)(0,0) is the top-left corner. Two more terms fix that, and the whole thing packs into a 3×3 matrix:

K=[fx0cx0fycy001]K = \begin{bmatrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{bmatrix}

# Parameter Meaning Typical value on a 1920×1080 phone
1 fxf_x Focal length in horizontal pixels 1240
2 fyf_y Focal length in vertical pixels 1238
3 cxc_x Principal point, horizontal 960 (near image centre)
4 cyc_y Principal point, vertical 540 (near image centre)

The four intrinsic parameters

fxf_x and fyf_y differ slightly because sensor pixels are not perfectly square. cx,cyc_x, c_y land near the image centre but rarely exactly on it, because the sensor is never mounted perfectly concentric with the lens.

Project a real point. The part’s corner sits at X=120X = 120 mm, Y=80Y = -80 mm, Z=1500Z = 1500 mm:

u=fxXZ+cx=1240×1201500+960=1240×0.08+960=99.2+960=1059.2u = f_x \frac{X}{Z} + c_x = 1240 \times \frac{120}{1500} + 960 = 1240 \times 0.08 + 960 = 99.2 + 960 = 1059.2

v=fyYZ+cy=1238×801500+540=1238×(0.0533)+540=66.0+540=474.0v = f_y \frac{Y}{Z} + c_y = 1238 \times \frac{-80}{1500} + 540 = 1238 \times (-0.0533) + 540 = -66.0 + 540 = 474.0

So that corner appears at pixel (1059,474)(1059, 474).

The intrinsic matrix also tells you the field of view:

FOVx=2arctan(w2fx)=2arctan(19202480)=2×37.7°=75.5°\text{FOV}_x = 2 \arctan\left(\frac{w}{2 f_x}\right) = 2 \arctan\left(\frac{1920}{2480}\right) = 2 \times 37.7° = 75.5°

A useful sanity check. If calibration returns an FOV of 140° for a normal phone lens, the calibration is wrong.

Extrinsics (RR and tt) say where the camera is in the world; they change every time the camera moves. Intrinsics (KK) and distortion describe the camera itself; they stay fixed as long as you do not change the lens or zoom. Calibration recovers all of them, but only the intrinsics and distortion are worth saving.

Distortion, worked out radius by radius

A real lens is not a pinhole. It bends light more at the edges, so straight lines bow. The standard model corrects a point in normalised coordinates by a radial factor:

xd=x(1+k1r2+k2r4+k3r6),r2=x2+y2x_d = x \left(1 + k_1 r^2 + k_2 r^4 + k_3 r^6\right), \qquad r^2 = x^2 + y^2

Take a typical phone lens with k1=0.25k_1 = -0.25 and k2=0.08k_2 = 0.08, and a point at normalised (0.4,0.3)(0.4, 0.3):

r2=0.42+0.32=0.16+0.09=0.25,r=0.5r^2 = 0.4^2 + 0.3^2 = 0.16 + 0.09 = 0.25, \qquad r = 0.5 factor=1+(0.25)(0.25)+(0.08)(0.252)=10.0625+0.005=0.9425\text{factor} = 1 + (-0.25)(0.25) + (0.08)(0.25^2) = 1 - 0.0625 + 0.005 = 0.9425 xd=0.4×0.9425=0.377,yd=0.3×0.9425=0.2828x_d = 0.4 \times 0.9425 = 0.377, \qquad y_d = 0.3 \times 0.9425 = 0.2828

In pixels with fx=1240f_x = 1240 and cx=960c_x = 960, the ideal position is 1240(0.4)+960=14561240(0.4) + 960 = 1456, but the actual position is 1240(0.377)+960=14271240(0.377) + 960 = 1427. The point is pulled 29 pixels toward the centre.

Now compute the same displacement across the whole radius:

# Radius rr k1r2+k2r4k_1 r^2 + k_2 r^4 Radial shift (px) target Where in a 1920×1080 frame
1 0.1 −0.00249 −0.31 very near centre
2 0.2 −0.00987 −2.45 inner quarter
3 0.3 −0.02185 −8.13 mid frame
4 0.4 −0.03795 −18.8 outer third
5 0.5 −0.05750 −35.7 near the short edge
6 0.6 −0.07963 −59.2 outer edge
7 0.7 −0.10329 −89.7 corners

Radial displacement for k1=-0.25, k2=0.08, fx=1240. Distortion is invisible at the centre and enormous at the corners.

The shape of that curve is the practical lesson. At r=0.1r = 0.1 the error is a third of a pixel and you would never notice. At r=0.7r = 0.7 it is 90 pixels. If you only ever measure things in the middle 20% of the frame, you can skip calibration. For anything else, you cannot.

# Coefficient Name Sign and effect
1 k1k_1 First radial Negative = barrel (bulges out). Positive = pincushion.
2 k2k_2 Second radial Corrects the residual at large radius
3 k3k_3 Third radial Only needed for fisheye and very wide lenses
4 p1,p2p_1, p_2 Tangential Corrects a sensor not perfectly parallel to the lens; usually tiny

The five standard distortion coefficients

Ideal: straight lines (7×7)
1
1
1
1
1
1
1
1
0
0
0
0
0
1
1
0
1
1
1
0
1
1
0
1
0
1
0
1
1
0
1
1
1
0
1
1
0
0
0
0
0
1
1
1
1
1
1
1
1
distort
Barrel (k1 < 0): corners pulled in (7×7)
0
1
1
1
1
1
0
1
1
0
0
0
1
1
1
0
1
1
1
0
1
1
0
1
0
1
0
1
1
0
1
1
1
0
1
1
1
0
0
0
1
1
0
1
1
1
1
1
0

Look at the corners. In the ideal grid they are filled; under barrel distortion they are empty, because the corner content has been pulled toward the centre. That is exactly the 90-pixel shift from the table above.

Calibrating with a checkerboard

A checkerboard works because its corners are detectable to sub-pixel precision and their real-world spacing is known exactly. Every corner gives two equations, so a 9×6 board provides 54 corners and 108 equations per photo.

import cv2
import numpy as np
import glob

PATTERN = (9, 6)          # inner corners, not squares
SQUARE_MM = 25.0          # measure your printed board, do not assume

# Real-world corner positions, z = 0 because the board is flat
objp = np.zeros((PATTERN[0] * PATTERN[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:PATTERN[0], 0:PATTERN[1]].T.reshape(-1, 2)
objp *= SQUARE_MM

obj_points, img_points = [], []
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)

for path in glob.glob("calib/*.jpg"):
    gray = cv2.cvtColor(cv2.imread(path), cv2.COLOR_BGR2GRAY)
    found, corners = cv2.findChessboardCorners(gray, PATTERN, None)
    if not found:
        print(f"no board in {path}")
        continue
    corners = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
    obj_points.append(objp)
    img_points.append(corners)

rms, K, dist, rvecs, tvecs = cv2.calibrateCamera(
    obj_points, img_points, gray.shape[::-1], None, None)

print(f"RMS reprojection error: {rms:.3f} px")
print(f"fx={K[0,0]:.1f} fy={K[1,1]:.1f} cx={K[0,2]:.1f} cy={K[1,2]:.1f}")
print(f"distortion: {dist.ravel()}")
np.savez("calibration.npz", K=K, dist=dist)

Note SQUARE_MM = 25.0. Measure the printed board with a ruler. Printers scale to fit page margins, and a board you believe is 25 mm but is actually 24.2 mm makes every distance you compute 3% wrong, with no other symptom.

How to shoot the calibration photos

# Rule Why it matters
1 Take 15–25 photos Fewer than 10 leaves the solution underdetermined
2 Tilt the board 20°–45° in different directions Front-on views cannot separate focal length from distance
3 Cover all four corners of the frame Distortion is only observable where it is large
4 Fill 30–60% of the frame with the board Too small gives imprecise corners; too large misses the edges
5 Keep the board perfectly flat Tape it to glass or a clipboard — a bent board corrupts everything
6 Lock focus and zoom Both change fx, so a changing lens is a changing camera
7 Light it evenly, no glare Specular highlights break corner detection

Calibration photo checklist. Most bad calibrations come from breaking rows 2 or 3.

The tilt requirement is the one people get wrong. Photograph the board flat-on twenty times and the solver cannot tell a long lens far away from a short lens up close — both produce the same image. Tilting breaks that ambiguity, because perspective foreshortening depends on the focal length in a way scale alone does not.

Reading the reprojection error

calibrateCamera returns an RMS error in pixels: on average, how far the model’s predicted corner positions are from the detected ones.

# RMS error Verdict target Likely cause
1 < 0.3 px Excellent
2 0.3–0.5 px Good, usable for measurement
3 0.5–1.0 px Acceptable for alignment, not for metrology Too few angles, or blur
4 1.0–2.0 px Poor Bent board, wrong square size, or autofocus moved
5 > 2.0 px Failed Wrong pattern size, or a mis-detected board slipped in

Interpreting the RMS reprojection error

# Find which individual photos are dragging the error up
for i in range(len(obj_points)):
    proj, _ = cv2.projectPoints(obj_points[i], rvecs[i], tvecs[i], K, dist)
    err = cv2.norm(img_points[i], proj, cv2.NORM_L2) / len(proj)
    print(f"image {i}: {err:.3f} px")

If one photo shows 2.5 px while the rest sit at 0.3 px, drop it and recalibrate. It is almost always a blurred frame or a board that flexed.

Undistorting and measuring

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

img = cv2.imread("gasket.jpg")
h, w = img.shape[:2]

newK, roi = cv2.getOptimalNewCameraMatrix(K, dist, (w, h), alpha=0)
undistorted = cv2.undistort(img, K, dist, None, newK)

x, y, rw, rh = roi
undistorted = undistorted[y:y+rh, x:x+rw]

alpha controls what happens to the edges. At alpha=0 the output is cropped so every pixel is valid — use this for measurement. At alpha=1 all original pixels are kept and the borders curve, leaving black wedges — use this when you must not lose any field of view.

From pixels to millimetres

Undistortion removes the position-dependent error. Converting to real units needs one known reference in the scene, at the same distance as what you are measuring.

Place a 100 mm calibration bar next to the gasket. It spans 412 pixels:

scale=100 mm412 px=0.2427 mm/px\text{scale} = \frac{100 \text{ mm}}{412 \text{ px}} = 0.2427 \text{ mm/px}

The gasket now measures 340 pixels in the undistorted image:

340×0.2427=82.5 mm340 \times 0.2427 = 82.5 \text{ mm}

# Position in frame Measured (px) Before calibration After calibration target True
1 Centre 340 79.9 mm 82.5 mm 82.5 mm
2 Halfway out 336 79.0 mm 82.4 mm 82.5 mm
3 Near corner 328 77.1 mm 82.3 mm 82.5 mm

The same gasket, three positions. After calibration the reading is consistent to 0.2 mm.

The spread collapsed from 2.8 mm to 0.2 mm. That is the entire value of calibration in one row.

Perspective correction without calibration

There is a shortcut for one common case. If you are photographing a flat object and you know the real-world coordinates of four points on it, you can rectify to a top-down view with a homography — no calibration needed.

# Four corners of a document as they appear in the photo
src = np.float32([[214, 88], [1102, 141], [1183, 856], [147, 782]])

# Where they should be: an A4 page at 2 px per mm
W, H = 420, 594
dst = np.float32([[0, 0], [W, 0], [W, H], [0, H]])

M = cv2.getPerspectiveTransform(src, dst)
flat = cv2.warpPerspective(img, M, (W, H))

Four point pairs give exactly eight equations for the homography’s eight degrees of freedom, so getPerspectiveTransform solves it directly with no RANSAC needed.

QuestionFull calibrationFour-point homography
Setup needed~20 checkerboard photosFour clicked corners
Removes lens distortionYesNo
Works on non-flat scenesYesNo — flat objects only
Gives real-world unitsYes, with known ZYes, if the object size is known
Valid after the camera movesYesNo, must be redone
Needed for stereo or 3DYesNo
Time to set up20 minutes1 minute
Full calibration versus a four-point homography

Common calibration failures

# Symptom Cause Fix
1 findChessboardCorners returns False on most photos Pattern size counts squares, not inner corners A board with 10×7 squares has 9×6 inner corners
2 RMS error above 2 px A blurred or bent board slipped into the set Print per-image errors and drop the worst
3 Undistorted image looks more curved, not less Board never reached the frame corners Reshoot with the board at all four corners
4 Distances all off by the same percentage SQUARE_MM does not match the printed board Measure the print with a ruler and rerun
5 Calibration drifts between sessions Autofocus or zoom changed Lock focus and zoom, recalibrate after any change
6 fx and fy differ by more than 5% Bad solution, usually from too little tilt Add tilted views and reshoot

Calibration failures by symptom

Practice task

Print a 9×6 checkerboard and tape it flat to a clipboard.

  1. Measure one square with a ruler. Write down the real number, not the intended one.
  2. Take 20 photos: varied tilt, varied distance, board reaching all four frame corners.
  3. Run the calibration. Record the RMS error, fxf_x, fyf_y, cxc_x, cyc_y, and k1k_1.
  4. Compute the field of view from fxf_x and check it looks sensible for your lens.
  5. Print the per-image error. Drop anything above 1.0 px and recalibrate. How much did RMS improve?
  6. Now recalibrate using only 5 photos, all flat-on. Compare fxf_x to the good calibration.
  7. Photograph a ruler at the frame centre and again at the corner. Measure a 100 mm span in pixels, before and after undistortion.

Step 6 is the eye-opener. The flat-on calibration reports a small reprojection error and looks fine, but its fxf_x can be off by 20% or more. A low reprojection error does not mean a correct calibration — it only means the model fits the data you gave it, and flat-on data cannot constrain focal length.

Summary

The pinhole model reduces a camera to x=fX/Zx = fX/Z. A 100 mm part at 2,000 mm with f=800f = 800 px lands 40 pixels wide, and because only the ratio X/ZX/Z matters, one camera can never separate size from distance.

The intrinsic matrix KK holds fx,fy,cx,cyf_x, f_y, c_x, c_y. You projected a point at (120,80,1500)(120, -80, 1500) mm to pixel (1059,474)(1059, 474) and derived a 75.5° field of view from fx=1240f_x = 1240.

Radial distortion follows 1+k1r2+k2r4+k3r61 + k_1r^2 + k_2r^4 + k_3r^6. You computed a factor of 0.9425 at r=0.5r = 0.5, a 29-pixel inward shift, and tabulated the displacement rising from 0.31 px at r=0.1r=0.1 to 89.7 px at r=0.7r=0.7. That curve is why the gasket shrank as it moved toward the corner, and why undistortion collapsed the measurement spread from 2.8 mm to 0.2 mm.

Calibrate from about 20 tilted checkerboard photos covering the whole frame. Check the RMS error, but remember it only tells you the model fits your photos — flat-on views produce a small error and a badly wrong focal length.

What comes next

Calibration gives you one camera’s geometry, which removes distortion and enables measurement at a known distance — but still not depth. Epipolar geometry and stereo vision adds a second camera and turns the disparity between two views into an actual distance in millimetres. Calibration is a hard prerequisite there: stereo depth is only as accurate as the intrinsics feeding it.

Test your understanding
You calibrate with 20 photos, all taken with the board parallel to the sensor at varied distances. RMS error is 0.28 px. When you use the result to measure parts, everything is off by about 18%. What went wrong?
Test your understanding
You need to read text from photos of flat forms held at an angle. You do not need any real-world measurements. What is the minimum setup?

Frequently asked questions

How many photos do I need to calibrate?
Fifteen to twenty-five is the practical range. Below ten the solution is underdetermined and unstable, while beyond about thirty the improvement is negligible. What matters far more than the count is variety: different tilts in different directions, different distances, and coverage of all four frame corners. Twenty varied photos beat a hundred similar ones every time.
Do I need to recalibrate if I move the camera?
No. Intrinsics and distortion describe the camera and lens themselves, so they survive any amount of moving, panning, or remounting. You must recalibrate if you change the lens, change the zoom, let autofocus shift significantly, or change the capture resolution — resolution changes scale fx, fy, cx and cy proportionally. Extrinsics do change when the camera moves, but you rarely reuse those.
My reprojection error is 0.3 px. Is my calibration correct?
It is a necessary condition, not a sufficient one. A low error means the model fits the photos you supplied, which can happen even when the parameters are badly wrong — the classic case is a set of flat-on photos, which yields a tiny error and a focal length off by twenty percent. Verify separately by measuring a known object and by checking that the computed field of view matches what your lens should give.
Should I undistort before running a neural network?
Usually not. Networks trained on ordinary photos have learned to cope with typical lens distortion, and undistorting adds an interpolation step, a small runtime cost, and a cropped field of view for no accuracy gain. The exceptions are strongly distorted lenses such as fisheye and action cameras, and any pipeline where the network's box coordinates feed into a geometric calculation afterwards.
What is the difference between intrinsics and extrinsics?
Intrinsics describe the camera: focal length, principal point, and distortion. They are fixed as long as the lens and zoom are fixed, so you calibrate once and save them. Extrinsics describe where the camera is in the world, as a rotation and a translation, and they change whenever the camera moves. Calibration recovers extrinsics for each checkerboard photo as a by-product, but only the intrinsics are worth keeping.
Start typing to search across all content
navigate Enter open Esc close