0. Distortion Correction
In the previous pixel measurement, did you notice that when an object is more centered in the camera’s field of view, the measured pixel values are larger; when the object is closer to the edge of the camera’s view, the measured pixel values are smaller? This is mainly due to the effect of radial distortion, specifically barrel distortion. Radial distortion has two manifestations, which are distinguished as follows:
| Phenomenon | Distortion Type | Physical Cause |
|---|---|---|
| Pixel values of objects in the center area are larger or pixel values of objects in the edge area are smaller | Barrel Distortion | Lens magnification decreases as the field of view angle increases |
| Pixel values of objects in the center area are smaller or pixel values of objects in the edge area are larger | Pincushion Distortion | Lens magnification increases as the field of view angle increases |
To solve this problem, we need to perform distortion correction.
1. Saving and Retrieving Calibration Parameters
In the previous lesson’s code, we placed the calibration method inside a loop, which resulted in 25 sets of calibration parameters. The 25 sets of extrinsic parameters (rotation/translation) can be used for pose analysis of the calibration board, while we only need one set of intrinsic parameters. In this section, we will use it for distortion correction, so we only need the intrinsic parameters. We can place the calibration method outside the loop and save the parameters. Parameters can be saved using JSON or NumPy binary files.
# Method 1: Save as NumPy binary file
np.savez('camera_params.npz', mtx=mtx, dist=dist, rvecs=rvecs, tvecs=tvecs)
# Method 2: Save as JSON
params = {
"camera_matrix": mtx.tolist(),
"dist_coeffs": dist.tolist(),
"reprojection_error": float(ret)
}
with open('camera_params.json', 'w') as f:
json.dump(params, f, indent=4)
Wherever we need to use the parameters, we can load them using the following code:
# Method 1: Load NumPy binary file
params = np.load('camera_params.npz')
mtx = params['mtx']
dist = params['dist']
rvecs = params['rvecs']
tvecs = params['tvecs']
# Method 2: Load JSON file
with open('camera_params.json', 'r') as f:
params = json.load(f)
mtx = np.array(params['camera_matrix'])
dist = np.array(params['dist_coeffs'])
2. Correction Methods
OpenCV provides two correction methods:
- undistort(): Used for correcting monocular images.
- initUndistortRectifyMap() + remap(): Used for correcting stereo images. Before using the above methods, a function to compute the optimal new camera matrix is required:
2.1 Compute Optimal New Camera Matrix getOptimalNewCameraMatrix
newcameramatrix, roi = cv2.getOptimalNewCameraMatrix(
cameraMatrix, # Original camera intrinsic matrix
distCoeffs, # Distortion coefficients
imageSize, # Image size (width, height)
alpha, # Scaling factor (0-1)
newImgSize=None, # New image size
centerPrincipalPoint=False # Whether to force the principal point to be centered
)
For example:
img = cv.imread('left12.jpg')
h, w = img.shape[:2]
newcameramtx, roi = cv.getOptimalNewCameraMatrix(mtx, dist, (w,h), 1, (w,h))
2.2 Correct Image undistort
dst = cv2.undistort(
src, # Input image (single-channel or multi-channel)
cameraMatrix, # Camera intrinsic matrix (3x3)
distCoeffs, # Distortion coefficients (1xN or Nx1)
dst=None, # Output image (optional)
newCameraMatrix=None # New camera matrix (3x3)
)
For example:
img = cv.imread('left12.jpg')
h, w = img.shape[:2]
newcameramtx, roi = cv.getOptimalNewCameraMatrix(mtx, dist, (w,h), 1, (w,h))
# undistort
dst = cv.undistort(img, mtx, dist, None, newcameramtx)
# crop the image
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]
cv.imwrite('calibresult.png', dst)
2.3 Correct Image initUndistortRectifyMap + remap
map1, map2 = cv2.initUndistortRectifyMap(
cameraMatrix, # Original camera intrinsic matrix (3x3)
distCoeffs, # Distortion coefficients (1xN)
R, # Optional rotation matrix (3x3)
newCameraMatrix, # New camera matrix (3x3)
size, # Image size (width, height)
m1type, # Mapping table type (usually cv2.CV_32FC1 or cv2.CV_16SC2)
dstmap1=None, # Output mapping table 1 (usually created automatically)
dstmap2=None # Output mapping table 2 (usually created automatically)
)
dst = cv2.remap(
src, # Input image (single-channel or multi-channel)
map1, # Mapping table 1
map2, # Mapping table 2
interpolation, # Interpolation method (e.g., cv2.INTER_LINEAR)
borderMode, # Border mode (e.g., cv2.BORDER_CONSTANT)
borderValue=None # Border value (used only when borderMode is cv2.BORDER_CONSTANT)
)
For example:
img = cv.imread('left12.jpg')
h, w = img.shape[:2]
newcameramtx, roi = cv.getOptimalNewCameraMatrix(mtx, dist, (w,h), 1, (w,h))
# undistort
mapx, mapy = cv.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w,h), cv.CV_32FC1)
dst = cv.remap(img, mapx, mapy, cv.INTER_LINEAR)
# crop the image
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]
cv.imwrite('calibresult.png', dst)
3. Integrating into Size Measurement Gadget
import cv2
import numpy as np
# Configuration parameters
MAX_OBJECTS = 5 # Maximum number of objects to measure
MIN_AREA = 1000 # Minimum valid area (pixels)
MAX_AREA = 500000 # Maximum valid area (pixels)
COLORS = [(0, 255, 0), (0, 255, 255), # Color list for different objects
(255, 255, 0), (255, 0, 255),
(0, 165, 255)]
# Load calibration parameters
params = np.load('camera_params.npz')
mtx = params['mtx']
dist = params['dist']
cv2.namedWindow('image')
cap = cv2.VideoCapture(1, cv2.CAP_DSHOW)
while True:
ret, frame = cap.read()
if not ret:
break
# Distortion correction
h, w = frame.shape[:2]
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h))
frame = cv2.undistort(frame, mtx, dist, None, newcameramtx)
# Preprocessing
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV)
# Contour extraction and filtering
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Area filtering function
def is_valid_contour(cnt):
area = cv2.contourArea(cnt)
return MIN_AREA <= area <= MAX_AREA
# Filter and sort by area
valid_contours = sorted(
[c for c in contours if is_valid_contour(c)],
key=cv2.contourArea,
reverse=True
)[:MAX_OBJECTS]
# Measure each valid object
for i, contour in enumerate(valid_contours):
# Minimum enclosing rectangle measurement
rect = cv2.minAreaRect(contour)
box = cv2.boxPoints(rect)
box = np.intp(box)
# Calculate dimensions
(w, h) = rect[1]
long_side, short_side = max(w, h), min(w, h)
# Draw contours and dimension annotations
color = COLORS[i % len(COLORS)]
cv2.drawContours(frame, [box], -1, color, 2)
# Calculate direction vectors for long and short sides
def get_edge_vectors(box):
# Calculate lengths of all edges
edges = []
for i in range(4):
p1, p2 = box[i], box[(i + 1) % 4]
edge_length = np.linalg.norm(p2 - p1)
edges.append((p1, p2, edge_length))
# Find longest and shortest edges
long_edge = max(edges, key=lambda x: x[2])
short_edge = min(edges, key=lambda x: x[2])
return long_edge[:2], short_edge[:2]
# Get midpoints of long and short edges
def get_midpoint(p1, p2):
return (p1[0] + p2[0]) // 2, (p1[1] + p2[1]) // 2
(long_p1, long_p2), (short_p1, short_p2) = get_edge_vectors(box)
long_mid = get_midpoint(long_p1, long_p2)
short_mid = get_midpoint(short_p1, short_p2)
def draw_measurement(img, pos, text, color):
text_size = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)[0]
cv2.rectangle(img,
(pos[0] - 5, pos[1] - text_size[1] - 5),
(pos[0] + text_size[0] + 5, pos[1] + 5),
(0, 0, 0), -1)
cv2.putText(img, text, (pos[0], pos[1]),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
draw_measurement(frame, long_mid, f"L={long_side:.0f}px", color)
draw_measurement(frame, short_mid, f"W={short_side:.0f}px", color)
cv2.imshow('image', frame)
# Keyboard control
key = cv2.waitKey(100)
if key == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
4. Conclusion
Thus, through this size measurement gadget, we have learned about camera calibration, distortion correction, contour extraction, and size measurement. Next, we will connect OpenCV-related visual knowledge through some other gadgets.