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像素,但实际校正效果很差,因为误差可能集中在某个区域。我建议你同时看误差分布图,看看是不是所有区域的误差都均匀。

标定流程与评估体系 数据准备 角点检测 参数计算 结果评估 重投影误差 误差分布图 校正效果验证 关键指标:重投影误差 < 0.5 像素为合格,< 0.3 像素为优秀 误差过大时,优先检查图像质量、棋盘格状态和角点检测结果

最后说一句——标定不是一次性的。换镜头、调光圈、改分辨率,都得重新标。我习惯把标定结果保存成文件,每次部署时直接加载,省得重复计算。