第三章:相机成像模型与标定

相机标定这件事,说白了就是搞清楚相机到底是怎么「看」世界的。我刚开始做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只在畸变特别严重时才需要。

我的经验:标定手机或运动相机时,建议用3个径向畸变参数。普通网络摄像头用2个就够。我曾经用2个参数标定GoPro,结果边缘还是歪的,加上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中常用的畸变模型。

注意:不要盲目使用所有畸变参数。参数越多,标定结果越不稳定。我见过有人用8个参数的模型,结果标定10次得到10组不同的值。够用就好。

3.3 张正友标定法

张正友标定法,可以说是相机标定领域的里程碑。为什么这么说?因为它只需要一个棋盘格,拍几张照片就能标定,简单又实用。

核心思路是这样的:

  1. 假设棋盘格在世界坐标系中Z=0,简化计算
  2. 通过单应性矩阵建立图像坐标和世界坐标的关系
  3. 利用多张图片的约束求解内参
  4. 考虑畸变,进行非线性优化

我个人觉得,这个方法最巧妙的地方在于——它把复杂的3D-2D对应问题,简化成了2D-2D的对应问题。你想想看,棋盘格是平面的,所有角点都在Z=0平面上,这就少了一个维度的未知数。

标定过程中有几个关键点:

  • 棋盘格要打印平整,贴在硬纸板上
  • 拍摄角度要多样:倾斜、旋转、远近都要有
  • 每张图片的棋盘格要完整出现在画面中
  • 至少拍10-20张,太少解不稳定
避坑指南:我曾经用A4纸打印棋盘格,贴在墙上标定。结果因为纸面不平,标定出来的焦距偏差了5%。后来改用亚克力板贴棋盘格,精度就上来了。记住——标定板的平整度直接影响标定精度。

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)
实用技巧:如果去畸变后图像边缘被裁剪太多,可以调整getOptimalNewCameraMatrix的alpha参数。alpha=0时裁剪最多但无黑边,alpha=1时保留全部像素但有黑边。我一般用0.5,平衡一下。

3.5 知识体系总览

下面这张图总结了相机标定的核心知识结构:

相机成像模型与标定知识体系 针孔相机模型 • 世界坐标系 → 相机坐标系 • 相机坐标系 → 图像坐标系 • 图像坐标系 → 像素坐标系 • 内参矩阵 (fx, fy, cx, cy) • 外参矩阵 (R, T) 畸变模型 • 径向畸变 (k1, k2, k3) - 桶形畸变(广角) - 枕形畸变(长焦) • 切向畸变 (p1, p2) • 畸变系数向量 [k1,k2,p1,p2,k3] 张正友标定法 • 棋盘格平面标定 • 单应性矩阵求解 • 闭合解 + 非线性优化 • 重投影误差评估 • 去畸变处理 OpenCV实战流程 ① 准备棋盘格 ② 拍摄图片 ③ 角点检测 ④ 执行标定 ⑤ 去畸变 关键评估指标 重投影误差 < 0.5 像素 → 标定合格

这张图把整个知识体系串起来了。从成像模型到畸变校正,再到标定方法和实战流程,每一步都环环相扣。我建议你按照这个结构来学习,先理解原理,再动手实践。

相机标定是3D重建的基石。标定不准,后面的立体匹配、点云生成、网格重建都会出问题。嗯,这里要注意——标定不是一次性的工作。镜头松动、温度变化都可能导致标定参数漂移。我一般每隔三个月重新标定一次,确保精度。

好了,这一章的内容就到这里。拿起你的相机,打印一张棋盘格,动手试试吧。

可可精品资料库