第三章:相机成像模型与标定
相机标定这件事,说白了就是搞清楚相机到底是怎么「看」世界的。我刚开始做3D重建时,总觉得标定是个麻烦事,随便找个棋盘格拍几张就完事了。结果重建出来的模型歪歪扭扭,根本没法用。后来才明白——标定不准,后面全是白干。
3.1 针孔相机模型
先聊聊最基础的成像模型。你想想看,相机本质上就是一个黑盒子,前面有个小孔,光线穿过小孔在后面的感光元件上成像。这就是针孔相机模型的核心思想。
数学上怎么描述?其实就四个坐标系之间的转换:
- 世界坐标系:我们生活的三维空间,单位是米
- 相机坐标系:以相机光心为原点,Z轴指向镜头前方
- 图像坐标系:成像平面上的物理坐标,单位还是米
- 像素坐标系:最终图像上的像素位置,单位是像素
从世界坐标到像素坐标,中间经历了两次变换。第一次是刚体变换(旋转+平移),把世界坐标转到相机坐标系下。第二次是透视投影,把三维点映射到二维成像平面上。
投影公式很简单:
// 从相机坐标到像素坐标
u = fx * X/Z + cx
v = fy * Y/Z + cy
这里的fx、fy是焦距参数,cx、cy是主点偏移。这4个参数就是相机的内参矩阵。我在项目中遇到过一个问题——很多人以为主点就在图像正中心,其实不是的。由于装配误差,主点通常会偏移几十个像素。
3.2 畸变模型
理想很丰满,现实很骨感。针孔模型假设镜头是完美的,但实际镜头都有畸变。我见过最夸张的一次,用手机广角镜头拍的棋盘格,边缘的格子都快弯成弧形了。
径向畸变
径向畸变是镜头形状引起的。说白了就是光线经过透镜边缘时弯曲得更厉害。分为两种:
- 桶形畸变:图像向外膨胀,广角镜头常见
- 枕形畸变:图像向内收缩,长焦镜头常见
数学上用泰勒级数来建模:
x_distorted = x * (1 + k1*r² + k2*r⁴ + k3*r⁶)
y_distorted = y * (1 + k1*r² + k2*r⁴ + k3*r⁶)
r是像素到图像中心的距离。k1、k2、k3就是径向畸变系数。一般用前两个就够了,k3只在畸变特别严重时才需要。
切向畸变
切向畸变是镜头和成像平面不平行造成的。说白了就是镜头装歪了。这个在工业相机中比较常见,消费级相机一般很小。
x_distorted = x + [2*p1*x*y + p2*(r² + 2*x²)]
y_distorted = y + [p1*(r² + 2*y²) + 2*p2*x*y]
p1、p2就是切向畸变参数。总共5个畸变参数(k1、k2、p1、p2、k3),组成了OpenCV中常用的畸变模型。
3.3 张正友标定法
张正友标定法,可以说是相机标定领域的里程碑。为什么这么说?因为它只需要一个棋盘格,拍几张照片就能标定,简单又实用。
核心思路是这样的:
- 假设棋盘格在世界坐标系中Z=0,简化计算
- 通过单应性矩阵建立图像坐标和世界坐标的关系
- 利用多张图片的约束求解内参
- 考虑畸变,进行非线性优化
我个人觉得,这个方法最巧妙的地方在于——它把复杂的3D-2D对应问题,简化成了2D-2D的对应问题。你想想看,棋盘格是平面的,所有角点都在Z=0平面上,这就少了一个维度的未知数。
标定过程中有几个关键点:
- 棋盘格要打印平整,贴在硬纸板上
- 拍摄角度要多样:倾斜、旋转、远近都要有
- 每张图片的棋盘格要完整出现在画面中
- 至少拍10-20张,太少解不稳定
3.4 实战:使用OpenCV进行相机标定
理论说完了,来点实际的。下面是我常用的标定流程,用OpenCV实现。
第一步:准备棋盘格
打印一个棋盘格,格子数量建议用9×6或8×5。为什么?因为OpenCV的角点检测函数要求内角点数量,也就是格子交叉点的数量。9×6的棋盘格,内角点是8×5。
第二步:拍摄标定图片
拿着棋盘格在相机前变换姿势,拍15-20张。注意:
- 棋盘格要占画面1/4以上
- 角度变化要大,从正面到侧面都要有
- 保持光照均匀,不要有反光
第三步:编写标定代码
import cv2
import numpy as np
import glob
# 棋盘格内角点数量
CHECKERBOARD = (8, 5)
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
# 准备世界坐标中的点
objp = np.zeros((CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1, 2)
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, CHECKERBOARD, None)
if ret:
objpoints.append(objp)
corners2 = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria)
imgpoints.append(corners2)
# 可视化角点
cv2.drawChessboardCorners(img, CHECKERBOARD, corners2, ret)
cv2.imshow('img', img)
cv2.waitKey(500)
cv2.destroyAllWindows()
# 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
第四步:评估标定结果
标定完别急着用,先看看重投影误差。这个值越小越好,一般小于0.5像素就算不错了。
# 计算重投影误差
mean_error = 0
for i in range(len(objpoints)):
imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
error = cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2)
mean_error += error
print("平均重投影误差: ", mean_error/len(objpoints))
第五步:去畸变
标定完成后,就可以对图像进行去畸变了:
# 去畸变
img = cv2.imread('test.jpg')
h, w = img.shape[:2]
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w,h), 1, (w,h))
# 方法1:直接去畸变
dst = cv2.undistort(img, mtx, dist, None, newcameramtx)
# 方法2:使用remap(效率更高)
mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w,h), 5)
dst = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
cv2.imshow('original', img)
cv2.imshow('undistorted', dst)
cv2.waitKey(0)
3.5 知识体系总览
下面这张图总结了相机标定的核心知识结构:
这张图把整个知识体系串起来了。从成像模型到畸变校正,再到标定方法和实战流程,每一步都环环相扣。我建议你按照这个结构来学习,先理解原理,再动手实践。
相机标定是3D重建的基石。标定不准,后面的立体匹配、点云生成、网格重建都会出问题。嗯,这里要注意——标定不是一次性的工作。镜头松动、温度变化都可能导致标定参数漂移。我一般每隔三个月重新标定一次,确保精度。
好了,这一章的内容就到这里。拿起你的相机,打印一张棋盘格,动手试试吧。
可可精品资料库