相机模型与标定:针孔相机模型、相机内参与外参、畸变校正、张正友标定法实战
说到立体匹配,很多人一上来就扎进视差计算里。但我得说句实在话——相机标定没做好,后面的一切都是空中楼阁。我自己刚入行时就吃过这个亏,辛辛苦苦调了半个月的匹配算法,结果换了个镜头全白干了。嗯,从那以后,我每次做立体视觉项目,第一件事就是老老实实把相机标定搞明白。
针孔相机模型:视觉世界的数学抽象
说白了,针孔相机模型就是用一个数学公式来描述「三维世界怎么投影到二维图像上」。你想想看,光线从物体表面反射,穿过镜头中心(我们叫它光心),最后落在成像平面上。这个过程,本质上就是一个中心投影。
公式很简单:
u = f * (X / Z)
v = f * (Y / Z)
其中 (X, Y, Z) 是三维空间中的点坐标,(u, v) 是图像上的像素坐标,f 是焦距。但实际相机没那么理想——像素不是正方形的,光心也不在图像正中心。所以我们需要更完整的模型。
核心要点:针孔模型假设光线直线传播,没有畸变。但真实镜头都有畸变,所以我们需要先校正畸变,再用针孔模型去描述。
相机内参:相机自己的「身份证」
内参矩阵描述了相机内部的几何特性。我习惯把它写成这样:
K = [fx 0 cx]
[ 0 fy cy]
[ 0 0 1]
这里面:
- fx, fy:分别是 x 和 y 方向的焦距(单位是像素)
- cx, cy:光心在图像上的坐标(主点偏移)
我在项目中遇到过一个问题:用手机摄像头做三维重建,发现 fx 和 fy 差了不少。后来一查,原来是传感器像素不是正方形的。所以千万别想当然认为 fx = fy,实际标定出来往往不一样。
相机外参:相机在空间中的「位置和姿态」
外参描述的是相机坐标系和世界坐标系之间的变换关系。说白了就是:相机放在哪?朝哪看?
外参由一个旋转矩阵 R 和一个平移向量 t 组成:
[R | t]
举个例子:如果你把相机放在三脚架上,从正前方拍摄一个物体,那 R 就是单位矩阵,t 就是相机到物体的距离。但如果你把相机歪着放,R 就复杂了。
我的小技巧:标定的时候,我习惯把棋盘格放在世界坐标系的 Z=0 平面上。这样能简化计算,而且张正友标定法就是这么设计的。
畸变校正:让直线变回直线
真实镜头都有畸变,最常见的是径向畸变和切向畸变。径向畸变就是「直线变弯」,尤其是广角镜头,边缘的弯曲特别明显。切向畸变则是镜头和成像平面不平行导致的。
畸变模型通常用多项式来描述:
x_distorted = x * (1 + k1*r^2 + k2*r^4 + k3*r^6) + 2*p1*x*y + p2*(r^2 + 2*x^2)
y_distorted = y * (1 + k1*r^2 + k2*r^4 + k3*r^6) + p1*(r^2 + 2*y^2) + 2*p2*x*y
其中 k1, k2, k3 是径向畸变系数,p1, p2 是切向畸变系数。
避坑指南:我曾经用了一个廉价镜头做标定,k3 标出来特别大。后来发现是标定板不平整导致的。所以标定之前,先确认标定板是平的!
张正友标定法实战
张正友标定法是目前最主流的相机标定方法。它的核心思路是:用一张棋盘格,从不同角度拍十几张照片,然后通过角点检测和优化求解,得到内参和外参。
具体步骤是这样的:
- 打印棋盘格:确保格子是正方形的,边长要精确测量
- 拍摄多张照片:从不同角度拍 10-20 张,棋盘格要占画面 1/3 以上
- 检测角点:用 OpenCV 的
findChessboardCorners找到所有内角点 - 亚像素优化:用
cornerSubPix提高角点精度 - 求解内参和外参:用
calibrateCamera一步到位
下面是我常用的标定代码:
import cv2
import numpy as np
import glob
# 棋盘格参数
chessboard_size = (9, 6) # 内角点数量
square_size = 25.0 # 格子边长,单位mm
# 准备世界坐标系中的点
objp = np.zeros((chessboard_size[0] * chessboard_size[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:chessboard_size[0], 0:chessboard_size[1]].T.reshape(-1, 2)
objp *= square_size
# 存储所有图像的点
objpoints = [] # 世界坐标系中的点
imgpoints = [] # 图像坐标系中的点
images = glob.glob('calib_images/*.jpg')
for fname in images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 查找角点
ret, corners = cv2.findChessboardCorners(gray, chessboard_size, None)
if ret:
objpoints.append(objp)
# 亚像素优化
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners2 = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria)
imgpoints.append(corners2)
# 标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
经验之谈:标定照片不要都在同一个距离拍。我一般会拍三种距离:近的(棋盘格占画面 2/3)、中的(占 1/2)、远的(占 1/3)。这样标定出来的参数泛化能力更强。
标定结果验证
标定完别急着用,先验证一下。我常用的方法是:
- 重投影误差:一般小于 0.5 像素就算合格
- 畸变校正效果:拍一张有直线的场景,校正后看直线是否变直
- 立体校正:如果是双目相机,还要验证左右图像的极线是否对齐
下面这个 SVG 图展示了整个标定流程的知识体系:
标定完成后,记得保存内参和畸变系数。我一般存成 JSON 文件,方便后续调用:
import json
calib_data = {
"camera_matrix": mtx.tolist(),
"dist_coeffs": dist.tolist(),
"image_size": gray.shape[::-1]
}
with open("calibration.json", "w") as f:
json.dump(calib_data, f, indent=4)
重要提醒:标定参数只对同一台相机有效。换镜头、调焦距、甚至温度变化都会影响标定结果。我每次项目开始前都会重新标定一次,确保参数是最新的。
好了,相机模型和标定就讲到这里。记住一句话:标定是立体匹配的基石,花再多时间在上面都值得。下一节我们会用标定好的参数来做立体校正,到时候你就知道标定有多重要了。
公众号:蓝海资料掘金营,微信deep3321