3、OpenCV标定实战:核心函数与结果评估
好,咱们直接进入正题。这一章我带你手撸一遍OpenCV的标定流程。说白了,就是教会计算机「看」到棋盘格上的角点,然后算出相机的内参和外参。
我个人习惯把标定分成三步:找角点、算参数、验结果。下面一个一个来。
3.1 cv2.findChessboardCorners() 使用
这个函数是标定的第一步。它的任务很简单——在图像里找到棋盘格的角点。
核心参数:
- image:输入图像,最好是灰度图。彩色图也能用,但内部会转成灰度处理。
- patternSize:棋盘格的内角点数,格式是 (cols, rows)。注意!不是格子数,是角点数。比如 9×6 的棋盘,内角点是 (8, 5)。
- flags:可选标志位,我常用 CALIB_CB_ADAPTIVE_THRESH + CALIB_CB_NORMALIZE_IMAGE。
举个例子:
import cv2
import numpy as np
# 棋盘格内角点尺寸 (列数, 行数)
pattern_size = (9, 6)
# 读取图像
img = cv2.imread('chessboard.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 找角点
ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)
if ret:
print(f"找到 {len(corners)} 个角点")
# 绘制角点
cv2.drawChessboardCorners(img, pattern_size, corners, ret)
else:
print("没找到角点,检查图像或棋盘格尺寸")
我的经验: 如果棋盘格反光严重,或者图像模糊,这个函数很容易失败。我曾经在产线上遇到过一批反光严重的棋盘格,后来加了自适应阈值才搞定。
这里有个坑——角点顺序。OpenCV返回的角点是从左上角开始,按行扫描的。如果你自己写标定程序,一定要保证所有图像的角点顺序一致,否则标定结果会乱套。
3.2 cv2.calibrateCamera() 参数详解
找到角点之后,就该算参数了。这个函数是标定的核心,参数比较多,我一个个说。
| 参数 | 类型 | 说明 |
|---|---|---|
| objectPoints | list of np.array | 世界坐标系中的3D点。每个棋盘格一个数组,格式 (N, 1, 3) |
| imagePoints | list of np.array | 图像坐标系中的2D点。对应每个棋盘格的角点坐标,格式 (N, 1, 2) |
| imageSize | tuple | 图像尺寸 (width, height),用于初始化内参矩阵 |
| cameraMatrix | np.array | 输出:内参矩阵,3×3 |
| distCoeffs | np.array | 输出:畸变系数,通常为 (1, 5) 或 (1, 8) |
| rvecs | list of np.array | 输出:每个棋盘格的旋转向量 |
| tvecs | list of np.array | 输出:每个棋盘格的平移向量 |
| flags | int | 可选标志,控制优化方式 |
| criteria | tuple | 迭代终止条件 (type, max_iter, epsilon) |
注意: objectPoints 的 Z 坐标通常设为0,因为棋盘格是平面的。但 X 和 Y 坐标必须与实际物理尺寸对应。比如格子边长30mm,那相邻角点的距离就是30。
实际调用时,我习惯这样写:
# 准备3D点
objp = np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2)
# 如果格子边长30mm,就乘以30
objp *= 30.0
# 收集所有图像的角点
obj_points = [] # 3D点列表
img_points = [] # 2D点列表
for img_path in image_list:
img = cv2.imread(img_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)
if ret:
obj_points.append(objp)
img_points.append(corners)
# 标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
obj_points, img_points, gray.shape[::-1], None, None
)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
这里有个细节——flags参数。默认情况下,OpenCV会同时优化内参和畸变系数。但如果你知道畸变模型比较简单(比如只有径向畸变),可以用 CALIB_FIX_K1、CALIB_FIX_K2 等标志固定某些系数。我在做手机摄像头标定时,经常固定高阶畸变系数,只优化前两个,效果反而更好。
3.3 标定结果评估(重投影误差)
标定完了,怎么知道结果好不好?最直观的指标就是重投影误差。
什么叫重投影?就是把3D点通过标定得到的参数投影回图像平面,然后算投影点和实际检测到的角点之间的距离。这个距离越小,说明标定越准。
计算公式:
重投影误差 = sqrt( sum( || 投影点 - 实际点 ||² ) / 总点数 )
单位:像素。通常小于0.5像素就算不错,小于0.3像素算优秀。
计算代码:
def compute_reprojection_error(obj_points, img_points, rvecs, tvecs, mtx, dist):
total_error = 0
total_points = 0
for i in range(len(obj_points)):
# 投影3D点到图像平面
img_points_proj, _ = cv2.projectPoints(
obj_points[i], rvecs[i], tvecs[i], mtx, dist
)
# 计算误差
error = cv2.norm(img_points[i], img_points_proj, cv2.NORM_L2)
total_error += error
total_points += len(obj_points[i])
mean_error = total_error / total_points
return mean_error
error = compute_reprojection_error(obj_points, img_points, rvecs, tvecs, mtx, dist)
print(f"重投影误差: {error:.4f} 像素")
我的经验: 如果误差大于1个像素,先别急着调参数。检查一下是不是有图像没找到角点,或者棋盘格本身有变形。我曾经遇到一次误差特别大,最后发现是打印的棋盘格受潮变形了。
为什么会这样?你想想看,重投影误差其实反映了标定模型的「拟合程度」。如果误差大,说明模型和实际数据不匹配。常见原因有:
- 图像数量不够:至少10-20张不同角度的图像
- 棋盘格覆盖区域不全:边缘和角落都要拍到
- 图像模糊或反光:角点检测不准
- 畸变模型选择不当:比如用了5参数模型但实际需要8参数
嗯,这里要注意——重投影误差只是参考,不是标准。有时候误差0.3像素,但实际校正效果很差,因为误差可能集中在某个区域。我建议你同时看误差分布图,看看是不是所有区域的误差都均匀。
最后说一句——标定不是一次性的。换镜头、调光圈、改分辨率,都得重新标。我习惯把标定结果保存成文件,每次部署时直接加载,省得重复计算。