张正友相机标定:内参、畸变与重投影误差

同一块标尺放在画面中心和边缘,换算出来的长度不同;再把标尺倾斜,误差继续增大。单一 mm_per_px 只能在受限拍摄条件下换算距离,不能解释镜头畸变和透视。相机标定要先建立从三维点到像素的模型。

本文介绍张正友平面标定法及 OpenCV 实践,属于 ScrewMetrology 系列的进阶知识。项目当前的两点尺度模块没有实现相机标定或去畸变。返回系列导读。

API 说明以 OpenCV 4.13 为参考;配套 C++17 示例已在 AppleClang 21 / OpenCV 5.0.0 下验证。OpenCV 5 使用 calib 和 geometry,OpenCV 4 使用 calib3d;4.x 分支在本文环境中未实测。

1. 内参、外参和尺度分别是什么

忽略畸变时,针孔模型写为:

1
2
3
4
λ [u v 1]ᵀ = K [R | t] [X Y Z 1]ᵀ
K = [ fx 0 cx
0 fy cy
0 0 1 ]

K 把相机归一化坐标映射到像素,fx、fy 的单位是像素,cx、cy 是主点。R、t 把标定板坐标转换为相机坐标;t 的单位由标定板三维点的单位决定。λ 是投影尺度,不是全图共用的毫米/像素比例。

张正友方法的一般内参模型可以包含 skew;本文与 OpenCV 常用模型按零 skew 解释。内参描述固定镜头和成像设置,每张标定图却有自己的外参。solvePnP 返回的也是物体到相机的变换,不是相机在物体坐标系中的位姿;后者需要求逆。

2. 平面为什么仍能提供内参约束

把棋盘格放在 Z=0 的平面,模型变为:

1
2
λ [u v 1]ᵀ = K [r₁ r₂ t] [X Y 1]ᵀ
H = [h₁ h₂ h₃] ∼ K [r₁ r₂ t]

每张图提供一个平面单应矩阵 H。H 只有比例意义,但旋转矩阵的前两列单位长度且正交,消除了部分自由度。令 B=K⁻ᵀK⁻¹,有:

1
2
h₁ᵀ B h₂ = 0
h₁ᵀ B h₁ − h₂ᵀ B h₂ = 0

B 对称,六个独立元素组成向量 b。把上述两式展开为关于 b 的线性方程,每个视角贡献两条约束;堆叠得到 Vb=0,通过 SVD 求取零空间,再恢复 K。

一般内参模型需要至少三个非退化视角才能提供足够约束,实际应采集更多覆盖视野、倾斜和距离的图像。重复拍摄相同正视姿态不会因为图片数量多就自动增加可观测性。

得到 K 后,可以由 K⁻¹h₁、K⁻¹h₂ 恢复旋转列、从第三列恢复平移,构造第三个旋转列并投影到合法旋转矩阵。最后结合畸变参数,最小化所有角点的重投影误差。线性解负责初始化,非线性优化负责进一步拟合。

3. 畸变模型与去畸变的作用

常用模型包含径向 k₁、k₂、k₃ 和切向 p₁、p₂。对归一化坐标 (x,y),r²=x²+y²,径向项随距光轴的距离变化,切向项描述另一类偏移。

1
2
x_d = x(1+k₁r²+k₂r⁴+k₃r⁶) + 2p₁xy + p₂(r²+2x²)
y_d = y(1+k₁r²+k₂r⁴+k₃r⁶) + p₁(r²+2y²) + 2p₂xy

undistort 根据模型重采样图像;undistortPoints 校正点坐标,默认输出归一化坐标,若需要像素坐标需明确指定投影矩阵。处理后的图像可能使用不同的新内参和 ROI,后续测量必须使用一致的坐标定义。

去畸变不会自动把任意像素变成毫米。若工件处在已知测量平面,还需要平面姿态或单应映射;存在高度变化时,单个平面映射也不成立。

4. 实际棋盘格采集流程

棋盘参数使用内角点数量,不是格子数量。例如 9×6 内角点,每格边长 25 mm,则对象点可按行写为 (j*25,i*25,0)。图像点与对象点顺序必须一致。

1
2
3
4
5
6
7
8
9
10
11
// 嵌入实际采集程序的核心片段;gray 已检查为非空灰度图。
const cv::Size pattern(9, 6);
std::vector<cv::Point2f> corners;
bool found = cv::findChessboardCorners(gray, pattern, corners,
cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE);
if (found) {
cv::cornerSubPix(gray, corners, cv::Size(11,11), cv::Size(-1,-1),
cv::TermCriteria(cv::TermCriteria::COUNT | cv::TermCriteria::EPS,
30, 0.001));
// 将这一视角的对象点与 corners 一起保存;检测失败时两者都不加入。
}

所有图像需要一致的分辨率和固定焦距、对焦设置;避免大量模糊、过曝、几乎同姿态的图片。检查角点覆盖画面边缘和中心,观察不同倾角,保证棋盘格本身平整且尺寸可信。

检测失败的图像不能只跳过图像点却仍然添加对象点。可先使用 findChessboardCornersSB 等替代检测器比较角点质量,但仍要检查其角点排序和接口约定。

5. 可运行的合成标定实验

为了在没有相机照片时验证数据流,配套程序用已知 K、畸变和 15 个不同姿态,投影 9×6 平面角点,再通过 calibrateCamera 恢复参数。下载以下三个文件到同一目录:

1
2
3
4
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build -j 4
./build/camera_calibration
ctest --test-dir build --output-on-failure

若 CMake 无法找到安装,追加 -DOpenCV_DIR=你的OpenCV配置目录。配套 CMake 根据主版本选择模块。标定的核心调用如下,完整参数和数据生成见下载文件:

1
2
3
4
5
6
7
// 核心片段:objectPoints/imagePoints 已按视角配对。
cv::Mat K, D;
std::vector<cv::Mat> rvecs, tvecs;
double rms = cv::calibrateCamera(objectPoints, imagePoints, imageSize,
K, D, rvecs, tvecs, 0,
cv::TermCriteria(cv::TermCriteria::COUNT | cv::TermCriteria::EPS,
100, 1e-12));

程序另用 projectPoints 计算恢复参数的 RMS,并检查 K 的相对误差。通过条件为重投影 RMS 小于 0.001 px、K 相对误差小于 0.001;这是无人工角点噪声的合成测试阈值,不是实物相机精度要求。

camera_calibration --degenerate 使用重复正视姿态,程序依据生成时已知的姿态多样性提前拒绝并返回 1。CTest 将这个非零退出视作预期。该检查用于证明合成退化路径,不能直接作为真实图像的通用退化检测器。

6. 重投影误差小,为什么仍要检查模型

定义每个角点误差为观测像素与投影像素的欧氏距离,整体 RMS 为:

1
RMS = sqrt( sum ||observedᵢ − projectedᵢ||² / N_points )

同时查看逐图 RMS、误差分布、边缘覆盖以及内参是否合理。少量视角或复杂畸变模型可能过拟合;标定板翘曲、尺寸填写错误、角点排序错误也可能给出貌似可用的结果。

验证时保留独立拍摄图像,进一步检查去畸变后直线和平面尺寸的一致性。若改变分辨率,需要按图像变换调整内参;如果改变焦距或对焦导致模型变化,应重新标定。

7. 与 AX=XB 的衔接

相机标定建立三维标定板与图像的关系,给 solvePnP 提供 K 和畸变;每个机器人姿态下,PnP 再提供标定板到相机的变换。下一篇会把这些变换与机器人末端位姿配对,求相机与末端之间的固定关系。

张正友方法和 AX=XB 分别解决不同未知量。先明确坐标变换方向,才能把两条链正确连接。

8. 参考资料