Camera calibration in OpenCV estimates how a lens projects a flat chessboard into the image sensor. The calibration output is needed before undistortion, measurement, pose estimation, or stitching work that depends on straight lines and repeatable pixel geometry.
cv2.findChessboardCorners() detects inner chessboard corners from multiple same-size images, cv2.cornerSubPix() refines them, and cv2.calibrateCamera() estimates the camera matrix and distortion coefficients. The corner pattern is counted by inner corners, so a physical board with 8 by 7 squares is passed as 7×6.
Use images from the same camera, lens focus, zoom, and resolution that will be corrected later. A low reprojection error and a saved calibration.npz file confirm that the script accepted enough frames, but visually poor captures or a mixed image set can still produce calibration values that should not be reused.
Related: How to undistort a camera image with OpenCV
Related: How to capture camera video with OpenCV
Related: How to install OpenCV on Ubuntu
Steps to calibrate a camera with OpenCV:
- Put at least 10 same-size chessboard images in a directory named calibration.
$ ls calibration frame-01.png frame-02.png frame-03.png frame-04.png frame-05.png frame-06.png frame-07.png frame-08.png frame-09.png frame-10.png frame-11.png frame-12.png
Use real photos from the camera being calibrated. Move the board between frames so OpenCV sees different positions and angles, but keep the same image resolution throughout the set.
Related: How to capture camera video with OpenCV - Create the calibration script.
- calibrate_camera.py
from argparse import ArgumentParser from pathlib import Path import cv2 import numpy as np def parse_pattern(value): try: cols, rows = value.lower().split("x", 1) return int(cols), int(rows) except ValueError as exc: raise SystemExit("pattern must use the form columnsxrows, for example 7x6") from exc parser = ArgumentParser() parser.add_argument("image_dir", help="Directory containing chessboard calibration images") parser.add_argument("--pattern", default="7x6", help="Inner-corner pattern as columnsxrows") parser.add_argument("--square-size", type=float, default=25.0, help="Square size in real units") parser.add_argument("--output", default="calibration.npz", help="Output NumPy archive") parser.add_argument("--min-frames", type=int, default=10, help="Minimum accepted chessboard frames") args = parser.parse_args() pattern_size = parse_pattern(args.pattern) image_dir = Path(args.image_dir) image_paths = sorted( path for path in image_dir.iterdir() if path.suffix.lower() in {".jpg", ".jpeg", ".png", ".tif", ".tiff"} ) if not image_paths: raise SystemExit(f"no calibration images found in {image_dir}") cols, rows = pattern_size object_template = np.zeros((rows * cols, 3), np.float32) object_template[:, :2] = np.mgrid[0:cols, 0:rows].T.reshape(-1, 2) object_template *= args.square_size criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) object_points = [] image_points = [] image_size = None for path in image_paths: image = cv2.imread(str(path)) if image is None: print(f"skipped unreadable image: {path.name}") continue gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) if image_size is None: image_size = gray.shape[::-1] elif image_size != gray.shape[::-1]: print(f"skipped different size: {path.name}") continue found, corners = cv2.findChessboardCorners(gray, pattern_size) if not found: print(f"skipped no corners: {path.name}") continue refined = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) object_points.append(object_template.copy()) image_points.append(refined) print(f"accepted: {path.name}") if len(object_points) < args.min_frames: raise SystemExit( f"found {len(object_points)} usable frames, need at least {args.min_frames}" ) rms, camera_matrix, dist_coeffs, rvecs, tvecs = cv2.calibrateCamera( object_points, image_points, image_size, None, None, ) total_error = 0.0 total_points = 0 for object_point, image_point, rvec, tvec in zip(object_points, image_points, rvecs, tvecs): projected, _ = cv2.projectPoints( object_point, rvec, tvec, camera_matrix, dist_coeffs, ) error = cv2.norm(image_point, projected, cv2.NORM_L2) total_error += error * error total_points += len(object_point) reprojection_rmse = (total_error / total_points) ** 0.5 output_path = Path(args.output) np.savez( output_path, camera_matrix=camera_matrix, dist_coeffs=dist_coeffs, rvecs=np.asarray(rvecs), tvecs=np.asarray(tvecs), image_size=np.array(image_size), pattern_size=np.array(pattern_size), square_size=np.array([args.square_size]), rms=np.array([rms]), reprojection_rmse=np.array([reprojection_rmse]), ) np.set_printoptions(precision=4, suppress=True) print(f"images found: {len(image_paths)}") print(f"accepted frames: {len(object_points)}") print(f"image size: {image_size[0]}x{image_size[1]}") print("camera matrix:") print(camera_matrix) print("distortion coefficients:") print(dist_coeffs.ravel()) print(f"opencv rms: {rms:.4f} px") print(f"reprojection rmse: {reprojection_rmse:.4f} px") print(f"wrote: {output_path}")
--pattern is the number of inner corners across and down, not the number of printed squares. Set --square-size to the real square size in millimeters, inches, or another unit that should also be used for later pose estimates.
- Run the calibration script.
$ python3 calibrate_camera.py calibration --pattern 7x6 --square-size 25 --output calibration.npz accepted: frame-01.png accepted: frame-02.png accepted: frame-03.png accepted: frame-04.png accepted: frame-05.png accepted: frame-06.png accepted: frame-07.png accepted: frame-08.png accepted: frame-09.png accepted: frame-10.png accepted: frame-11.png accepted: frame-12.png images found: 12 accepted frames: 12 image size: 900x700 camera matrix: [[856.3883 0. 464.9652] [ 0. 847.5984 355.29 ] [ 0. 0. 1. ]] distortion coefficients: [ -0.2894 9.2245 0.0011 0.0052 -146.5785] opencv rms: 0.4267 px reprojection rmse: 0.4267 px wrote: calibration.npz
A lower reprojection error means the detected image points are closer to the points projected from the estimated camera model. Re-shoot blurry, cropped, repeated, or mixed-resolution frames before reusing a poor calibration.
- Verify the saved calibration arrays.
$ python3 - <<'PY' import numpy as np data = np.load("calibration.npz") print(f"camera_matrix: {data['camera_matrix'].shape}") print(f"dist_coeffs: {data['dist_coeffs'].shape}") print(f"rmse_under_one_pixel: {float(data['reprojection_rmse'][0]) < 1.0}") PY camera_matrix: (3, 3) dist_coeffs: (1, 5) rmse_under_one_pixel: TrueKeep calibration.npz with the camera profile and use it only for images captured with the same camera, lens setting, focus, and resolution.
Related: How to undistort a camera image with OpenCV
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.