How to Calibrate a Camera and Convert Pixel Distance to Real-World Distance
This guide will walk you through the process of calibrating a camera and using the calibration data to convert pixel distances to real-world distances in millimeters (mm). Camera calibration corrects for lens distortions, allowing accurate measurements from images.
Part 1: Camera Calibration
Objective : Calibrate the camera to correct for lens distortions and save calibration data for later use.
Chessboard Image Generation
This code will generate an Chessboard Image that can be printed and used for a physical calibration of the camera.
import cv2 as cv
import numpy as np
import os
def generate_chessboard_image(square_size=25, chessboard_size=(8, 6), image_size=(200, 250), output_path='./images/calibration_chessboard.png'):
# Create a white background
chessboard_image = np.ones(image_size, dtype=np.uint8) * 255
square_color = 0 # Black color for the squares
# Calculate total chessboard width and height
chessboard_width = chessboard_size[0] * square_size
chessboard_height = chessboard_size[1] * square_size
# Calculate offsets to center the chessboard pattern
x_offset = (image_size[1] - chessboard_width) // 2
y_offset = (image_size[0] - chessboard_height) // 2
# Draw the chessboard pattern, centered in the image
for i in range(chessboard_size[1]): # Row iteration
for j in range(chessboard_size[0]): # Column iteration
if (i + j) % 2 == 0:
top_left = (x_offset + j * square_size, y_offset + i * square_size)
bottom_right = (x_offset + (j + 1) * square_size, y_offset + (i + 1) * square_size)
cv.rectangle(chessboard_image, top_left, bottom_right, square_color, -1)
# Ensure the output directory exists
os.makedirs(os.path.dirname(output_path), exist_ok=True)
# Save the generated chessboard
cv.imwrite(output_path, chessboard_image)
print(f"Chessboard saved to {output_path}")
generate_chessboard_image()
The camera calibration
You have to use the camera you are trying to calibrate, and take a wide variaty of sample pictures of the chessboard in different angles.
import numpy as np
import cv2 as cv
import glob
import json
import os
# Chessboard and image parameters
chessboardSize = (8, 6) # Inner corner count per row and column in your calibration board
frameSize = (1920, 1080) # Resolution of your images
size_of_chessboard_squares_mm = 25 # Actual size of each square in mm
# Termination criteria for corner refinement
criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 30, 0.001)
# Prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(7,5,0)
objp = np.zeros((chessboardSize[0] * chessboardSize[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:chessboardSize[0], 0:chessboardSize[1]].T.reshape(-1, 2) * size_of_chessboard_squares_mm
# Arrays to store object points and image points
objpoints = [] # 3D points in real-world space
imgpoints = [] # 2D points in image plane
# Specify the folder path for images and calibration files
folder_path = './images' # Update to your actual path
# Ensure the folder exists for saving images and calibration files
os.makedirs(folder_path, exist_ok=True)
# Load images from the specified folder
images = glob.glob(os.path.join(folder_path, '*.png'))
if not images:
raise FileNotFoundError("No images found in the specified folder. Ensure images are in .png format and in the correct directory.")
# Detect chessboard corners in each image
detected_images = 0
for image in images:
img = cv.imread(image)
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# Use binary thresholding for better contrast
_, binary = cv.threshold(gray, 127, 255, cv.THRESH_BINARY | cv.THRESH_OTSU)
ret, corners = cv.findChessboardCorners(binary, chessboardSize,
cv.CALIB_CB_ADAPTIVE_THRESH + cv.CALIB_CB_NORMALIZE_IMAGE + cv.CALIB_CB_FAST_CHECK)
if ret:
objpoints.append(objp)
corners2 = cv.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
imgpoints.append(corners2)
detected_images += 1
# Draw corners for visual confirmation
cv.drawChessboardCorners(img, chessboardSize, corners2, ret)
cv.imshow('Detected Corners', img)
cv.waitKey(500) # Display each image with detected corners for 500 ms
print(f"Chessboard corners detected in {image}")
else:
print(f"Warning: Chessboard not detected in {image}")
cv.destroyAllWindows()
# Confirm if corners were detected in any image
print(f"Number of images with detected corners: {detected_images}")
if detected_images == 0:
raise ValueError("No valid chessboard corners detected in any images. Check your images and chessboard settings.")
############## CALIBRATION #######################################################
# Calibrate the camera and get calibration data
ret, cameraMatrix, dist, rvecs, tvecs = cv.calibrateCamera(objpoints, imgpoints, frameSize, None, None)
if not ret:
raise RuntimeError("Camera calibration failed.")
# Save calibration data to JSON format
calibration_data = {
'camera_matrix': cameraMatrix.tolist(),
'dist_coeffs': dist.tolist(),
'rvecs': [rv.tolist() for rv in rvecs],
'tvecs': [tv.tolist() for tv in tvecs]
}
calibration_file_path = os.path.join(folder_path, 'calibration_data.json')
with open(calibration_file_path, 'w') as f:
json.dump(calibration_data, f)
print(f"Calibration data saved to {calibration_file_path}")
############## REPROJECTION ERROR ################################################
# Calculate the reprojection error
mean_error = 0
for i in range(len(objpoints)):
imgpoints2, _ = cv.projectPoints(objpoints[i], rvecs[i], tvecs[i], cameraMatrix, dist)
error = cv.norm(imgpoints[i], imgpoints2, cv.NORM_L2) / len(imgpoints2)
mean_error += error
print(f"Total reprojection error: {mean_error / len(objpoints)}")
############## UNDISTORTION FUNCTION #############################################
def undistort_image(image_filename):
"""
Undistort a given image using the saved camera calibration data.
Args:
image_filename (str): Filename of the image to undistort.
"""
# Load calibration data
with open(calibration_file_path, 'r') as f:
calibration_data = json.load(f)
cameraMatrix = np.array(calibration_data['camera_matrix'])
dist = np.array(calibration_data['dist_coeffs'])
# Load the image to undistort
img_path = os.path.join(folder_path, image_filename)
img = cv.imread(img_path)
if img is None:
print(f"Image '{image_filename}' not found in {folder_path}")
return
# Get optimal camera matrix for undistortion
h, w = img.shape[:2]
newCameraMatrix, roi = cv.getOptimalNewCameraMatrix(cameraMatrix, dist, (w, h), 1, (w, h))
# Undistort the image
dst = cv.undistort(img, cameraMatrix, dist, None, newCameraMatrix)
# Crop based on ROI and save the undistorted image
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]
# Save the undistorted image
undistorted_filename = f"undistorted_{image_filename}"
undistorted_path = os.path.join(folder_path, undistorted_filename)
cv.imwrite(undistorted_path, dst)
print(f"Undistorted image saved as {undistorted_filename}")
############## EXAMPLE UNDISTORTION CALL #########################################
# Specify an example image to undistort using the calibration data
example_image = 'img0.png' # Replace 'img0.png' with any existing image in the folder
undistort_image(example_image)
Explanation
- Chessboard Corners : The code finds chessboard corners in a set of calibration images.
- Calibration : With detected corners, OpenCV’s calibrateCamera function computes calibration parameters, which are saved as a .json file.
- Undistortion : The code demonstrates undistorting a test image using the calibration data.
Part 2: Convert Pixel Distance to Real-World Distance
Objective : Use the calibration data to calculate the real-world distance represented by a pixel distance.
Code Explanation
This function loads saved calibration data, uses an image with a detected checkerboard to calculate the scale (mm per pixel), and applies this scale to convert a pixel distance to millimeters.
import numpy as np
import json
import cv2 as cv
import os
def pixel_to_real_distance(pixel_distance, image_path, calibration_file_path='./images/calibration_data.json', real_checkerboard_size_mm=25, checkerboard_size=(8, 6)):
"""
Calculate the real-world distance in mm for a given pixel distance using calibration data.
Args:
pixel_distance (float): Distance in pixels to convert.
image_path (str): Path to the image containing the checkerboard pattern.
calibration_file_path (str): Path to the JSON file with calibration data.
real_checkerboard_size_mm (float): Real-world size of one checkerboard square in mm.
checkerboard_size (tuple): Number of inner corners per row and column on the checkerboard.
Returns:
float: Real-world distance in mm.
"""
# Load the saved camera calibration data from JSON
if os.path.exists(calibration_file_path):
with open(calibration_file_path, 'r') as f:
calibration_data = json.load(f)
camera_matrix = np.array(calibration_data['camera_matrix'])
dist_coeffs = np.array(calibration_data['dist_coeffs'])
else:
raise FileNotFoundError(f"{calibration_file_path} not found.")
# Read the image specified in the argument
img = cv.imread(image_path)
if img is None:
raise FileNotFoundError(f"Failed to load image at {image_path}")
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# Find the checkerboard corners
ret, corners = cv.findChessboardCorners(gray, checkerboard_size, None)
if ret:
# Refine corners
corners2 = cv.cornerSubPix(
gray, corners, (11, 11), (-1, -1),
(cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 30, 0.001)
)
# Calculate the distance between two adjacent corners in the checkerboard
pixel_checkerboard_distance = np.linalg.norm(corners2[0] - corners2[1])
# Real-world distance (mm) per pixel
mm_per_pixel = real_checkerboard_size_mm / pixel_checkerboard_distance
# Convert the pixel distance to real-world distance in mm
real_distance_mm = pixel_distance * mm_per_pixel
return real_distance_mm
else:
raise Exception("Checkerboard not detected in the image!")
Explanation
- Calibration Data: The code loads saved camera matrix and distortion coefficients.
- Checkerboard Detection: Detects checkerboard corners in the calibration image to determine the pixel distance between two adjacent squares.
- Conversion: Calculates mm per pixel and then converts the provided pixel distance to millimeters.
Summary
• Calibration: Use a chessboard pattern to calibrate the camera and save the calibration data.
• Distance Conversion: With calibration data, convert pixel distances in images to real-world measurements.
This approach ensures accurate measurements in robotics, computer vision, or any application requiring precise image-based measurements.