(超详细)张正友标定法:从单应性矩阵到畸变校正的完整推导与实战解析
1. 张正友标定法概述
相机标定是计算机视觉中最基础也最重要的任务之一。简单来说,它就像给相机做"体检"——通过分析相机拍摄的特定图案(如棋盘格),计算出相机的"视力参数"。这些参数决定了相机如何将三维世界转换为二维图像。
张正友标定法是其中最经典的方法,由张正友博士在1998年提出。它的巧妙之处在于:
- 只需要一个平面棋盘格(不需要昂贵的三维标定设备)
- 操作简单(手持棋盘格随意移动拍摄即可)
- 精度高(通过多幅图像和优化算法获得稳定结果)
我在实际项目中多次使用这个方法,发现它的鲁棒性确实很好。即使拍摄条件不理想(如光线不均匀、棋盘格部分遮挡),只要采集足够多的图像(建议15-20张),依然能得到准确的标定结果。
2. 单应性矩阵:连接平面与图像的桥梁
2.1 单应性矩阵的几何意义
想象你拿着手机对着桌面上的棋盘格拍照。棋盘格是一个平面,它在照片中的成像经历了三个关键变换:
- 棋盘格平面到相机坐标系的刚体变换(旋转+平移)
- 相机坐标系到图像平面的透视投影
- 图像平面到像素坐标系的仿射变换
这三个变换可以合并为一个3×3的单应性矩阵H。这个矩阵的神奇之处在于,它能把棋盘格上的任意点[X,Y]直接映射到像素坐标[u,v]:
[u] [h11 h12 h13][X] [v] = [h21 h22 h23][Y] [1] [h31 h32 h33][1]我在调试时常用这个性质验证单应性矩阵是否正确——把棋盘格角点的世界坐标乘以H,看是否得到对应的像素坐标。
2.2 单应性矩阵的求解方法
要解这个矩阵,至少需要4组对应点。为什么是4组?因为H有8个自由度(虽然矩阵有9个元素,但可以整体缩放),每组点提供两个方程。
实际操作中,我建议采集20-30个角点。这样即使部分点检测不准,依然能通过最小二乘法得到稳定的解。具体步骤:
- 检测棋盘格角点(OpenCV的findChessboardCorners就很好用)
- 构建线性方程组:
# 对于每组点(X,Y)<->(u,v) A = np.array([ [X, Y, 1, 0, 0, 0, -u*X, -u*Y], [0, 0, 0, X, Y, 1, -v*X, -v*Y] ]) b = np.array([u, v])- 用SVD分解求解Ah=0的最小二乘解
记得对数据做归一化!我遇到过因为像素坐标和世界坐标量级差太大导致数值不稳定的情况。简单做法是将坐标平移缩放,使均值为0,方差为√2。
3. 从单应性矩阵到相机内参
3.1 内参约束的推导
单应性矩阵H可以分解为: H = λA[r1 r2 t] 其中A是内参矩阵,[r1 r2 t]是外参,λ是比例因子。
利用旋转矩阵的正交性(r1ᵀr2=0,‖r1‖=‖r2‖=1),可以得到两个关键约束: h1ᵀA⁻ᵀA⁻¹h2 = 0 h1ᵀA⁻ᵀA⁻¹h1 = h2ᵀA⁻ᵀA⁻¹h2
这些约束中出现了B = A⁻ᵀA⁻¹,这个矩阵特别重要。通过它,我们可以绕过直接求A的困难。
3.2 内参的封闭解
B矩阵对称,只有6个独立元素。设b=[B11,B12,B22,B13,B23,B33]ᵀ,每个单应性矩阵提供两个方程:
[v12]ᵀb = 0 [(v11-v22)]ᵀb = 0
其中vij = [hi1hj1, hi1hj2+hi2hj1, hi2hj2, hi3hj1+hi1hj3, hi3hj2+hi2hj3, hi3hj3]ᵀ
至少需要3幅图像(6个方程)才能解出b。我建议用5幅以上图像构建超定方程组,通过SVD求解更稳定。
解出B后,内参矩阵A可以通过Cholesky分解得到:
# B = A⁻ᵀA⁻¹ A_inv = np.linalg.cholesky(B).T A = np.linalg.inv(A_inv)4. 相机外参的求解
4.1 旋转和平移的估计
有了内参A和单应性H,外参可以直接计算: λ = 1/‖A⁻¹h1‖ r1 = λA⁻¹h1 r2 = λA⁻¹h2 r3 = r1×r2 t = λA⁻¹h3
注意:这样得到的R可能不严格正交。我通常会在优化前用SVD强制正交化:
U, _, Vt = np.linalg.svd(R) R = U @ Vt4.2 外参的实际意义
每幅图像对应一组外参,表示:
- 棋盘格相对于相机的姿态(旋转矩阵R)
- 棋盘格相对于相机的位置(平移向量t)
在双目视觉系统中,这些外参可以用来计算两个相机之间的相对位置关系。我曾经用这个方法标定双目摄像头,精度能达到0.1mm以内。
5. 畸变校正:让图像不再弯曲
5.1 畸变模型
实际镜头会有明显的畸变,主要有两种:
- 径向畸变:图像像"鱼眼"一样弯曲 x_corrected = x(1 + k1r² + k2r⁴)
- 切向畸变:由镜头与传感器不平行引起 x_corrected += [2p1xy + p2(r²+2x²)]
张正友方法主要估计径向畸变k1,k2。对于普通镜头这已经足够,但鱼眼镜头可能需要更高阶模型。
5.2 畸变参数估计
通过最小化重投影误差求解:
# 理想坐标 u_ideal = u + (u-u0)*(k1*r² + k2*r⁴) v_ideal = v + (v-v0)*(k1*r² + k2*r⁴)构建线性方程组Dk=d,其中:
- D的每行是[(u-u0)r², (u-u0)r⁴]或[(v-v0)r², (v-v0)r⁴]
- d的每行是u_observed-u或v_observed-v
6. 完整标定流程与实战技巧
6.1 标定步骤总结
- 打印棋盘格并固定在平整表面
- 从不同角度拍摄15-20张照片(确保棋盘格充满画面)
- 检测角点并验证所有点都被正确识别
- 计算初始单应性矩阵
- 求解相机内参初始值
- 估计外参和畸变参数
- 联合优化所有参数
6.2 提高标定精度的技巧
- 棋盘格要平整:我试过用玻璃板压住纸张,效果比软质棋盘格好很多
- 覆盖整个视野:移动棋盘格到图像的四个角落和中心
- 不同倾斜角度:既有正面拍摄,也有大角度倾斜
- 光照均匀:避免反光和阴影影响角点检测
在Python中,用OpenCV实现整个流程非常方便:
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( object_points, image_points, image_size, None, None)7. 常见问题与解决方案
7.1 标定结果不稳定的可能原因
- 角点检测不准确:尝试调整findChessboardCorners的参数
- 棋盘格移动不足:确保有足够的旋转和平移变化
- 数值不稳定:检查数据归一化和矩阵条件数
7.2 验证标定质量
计算重投影误差:
mean_error = 0 for i in range(len(object_points)): imgpoints2, _ = cv2.projectPoints( object_points[i], rvecs[i], tvecs[i], mtx, dist) error = cv2.norm(image_points[i], imgpoints2, cv2.NORM_L2)/len(imgpoints2) mean_error += error print("平均重投影误差: {}".format(mean_error/len(object_points)))好的标定结果误差通常小于0.5像素。
8. 进阶话题:非传统标定板
虽然棋盘格最常用,但在某些场景下其他图案可能更好:
- 圆形网格:圆心定位精度更高
- 编码标记:适合动态标定或多相机系统
- 随机图案:可用于无标定板的自标定
我曾经在一个工业项目中尝试使用圆形网格,发现重投影误差比棋盘格降低了约15%。这是因为圆心的亚像素定位比角点更准确,特别是在图像模糊的情况下。
