第二章 相机成像模型与标定

做结构光三维重建,相机就是我们的「眼睛」。但说实话,这双眼睛天生有缺陷——它看到的图像和真实世界之间,存在着一套复杂的映射关系。搞懂这套关系,就是我们今天要聊的核心。

我个人习惯把相机成像模型比作一个「翻译官」。它把三维世界的点,翻译成二维图像上的像素。翻译得准不准,直接决定了我们重建出来的三维模型精度。嗯,这里要注意,翻译过程中会有三个关键角色:相机内参、相机外参、畸变参数。

2.1 针孔相机模型

先说说最基础的针孔相机模型。你想想看,一个暗箱,前面戳个小孔,光线透过小孔在后面的感光面上成像——这就是最朴素的相机原理。

数学上怎么描述?很简单,一个三维点 P(X, Y, Z) 映射到图像平面上的点 p(x, y):

x = f * X / Z
y = f * Y / Z

这里的 f 就是焦距。我在项目中遇到过,很多人以为焦距就是镜头上的那个参数,其实不完全对。这里的 f 是物理焦距,单位是毫米,但最终要换算成像素单位。

核心要点:针孔模型是理想模型,实际相机因为镜头制造工艺、装配误差等原因,会引入畸变。但所有复杂模型都是在针孔模型基础上修正的。

2.2 相机内参

相机内参,说白了就是描述相机「内部属性」的参数。包括焦距、主点、畸变系数。我刚开始做标定时,总觉得内参就是个矩阵,后来踩了坑才明白,每个参数都有物理意义。

2.2.1 焦距 (fx, fy)

焦距决定了物体在图像上的缩放比例。注意,现代相机通常用 fx 和 fy 两个值,因为像素不一定是正方形。我曾经在标定一个工业相机时,发现 fx 和 fy 差了 0.5%,排查了半天才发现是传感器制造公差。

2.2.2 主点 (cx, cy)

主点是光轴与图像平面的交点。理想情况下应该在图像正中心,但实际总有偏移。我记得有次做高精度重建,主点偏移了 3 个像素,导致重建误差直接大了 0.2mm。嗯,这个坑我记忆犹新。

2.2.3 内参矩阵

把上面这些参数组合起来,就得到了内参矩阵 K:

K = [fx  0  cx]
    [ 0  fy  cy]
    [ 0   0   1]

个人经验:标定内参时,建议至少拍 15-20 张不同角度的棋盘格图像。我见过有人只拍 5 张就敢用,结果重建出来的点云全是扭曲的。

2.3 相机外参

外参描述的是相机在世界坐标系中的位置和朝向。说白了,就是相机「放在哪里、朝哪看」。外参由旋转矩阵 R 和平移向量 t 组成。

2.3.1 旋转矩阵 R

旋转矩阵是一个 3x3 的正交矩阵,描述相机坐标系相对于世界坐标系的旋转。你想想看,相机可以绕 X、Y、Z 三个轴旋转,每个轴对应一个角度——这就是欧拉角的概念。

但实际项目中,我更推荐用旋转矩阵或四元数。为什么?欧拉角有万向锁问题。我曾经在调试一个机械臂上的结构光系统时,就因为欧拉角导致姿态解算突然跳变,排查了整整两天。

2.3.2 平移向量 t

平移向量 t 是一个 3x1 的向量,描述相机光心在世界坐标系中的位置。单位通常和世界坐标系一致,比如毫米。

外参矩阵可以写成:

[R | t]  =  [r11 r12 r13 | tx]
            [r21 r22 r23 | ty]
            [r31 r32 r33 | tz]

避坑指南:我曾经在标定双目系统时,左右相机的外参一直对不上。后来发现是旋转矩阵的坐标系定义不一致——一个用右手系,一个用左手系。所以,标定前一定要统一坐标系约定。

2.4 张正友标定法

张正友标定法,可以说是相机标定领域的「标准答案」。它只需要拍摄一个平面棋盘格,就能同时标定出内参和外参。我做了这么多年结构光,90% 的项目都在用这个方法。

2.4.1 基本原理

核心思想很简单:利用棋盘格上已知的角点坐标,和它们在图像上的投影坐标,建立约束方程。然后通过最小二乘法求解。

具体步骤:

  1. 拍摄多张不同角度的棋盘格图像
  2. 提取每张图像的角点坐标
  3. 利用单应性矩阵建立约束
  4. 求解内参矩阵的初始值
  5. 考虑畸变,进行非线性优化

2.4.2 代码实现

OpenCV 已经封装好了,但理解底层实现很重要。下面是我常用的标定流程:

import cv2
import numpy as np

# 准备棋盘格角点坐标
pattern_size = (9, 6)  # 内角点数量
objp = np.zeros((pattern_size[0]*pattern_size[1], 3), np.float32)
objp[:,:2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1,2)

# 存储所有图像的点
objpoints = []  # 世界坐标系中的点
imgpoints = []  # 图像坐标系中的点

# 遍历所有标定图像
for fname in images:
    img = cv2.imread(fname)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # 查找棋盘格角点
    ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)
    
    if ret:
        objpoints.append(objp)
        imgpoints.append(corners)

# 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
    objpoints, imgpoints, gray.shape[::-1], None, None
)

print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)

我的建议:标定完成后,一定要做重投影误差检查。如果平均误差超过 0.5 像素,说明标定质量有问题。我一般要求控制在 0.3 像素以内。

2.5 畸变校正

畸变是真实相机和针孔模型之间的偏差。主要有两种:径向畸变和切向畸变。

2.5.1 径向畸变

径向畸变是镜头曲率引起的。靠近图像边缘,畸变越明显。分为桶形畸变和枕形畸变。数学上用泰勒级数展开来建模:

x_corrected = x * (1 + k1*r^2 + k2*r^4 + k3*r^6)
y_corrected = y * (1 + k1*r^2 + k2*r^4 + k3*r^6)

其中 r 是像素到主点的距离,k1、k2、k3 是径向畸变系数。

2.5.2 切向畸变

切向畸变是镜头和传感器不平行引起的。用 p1、p2 两个参数描述:

x_corrected = x + [2*p1*x*y + p2*(r^2 + 2*x^2)]
y_corrected = y + [p1*(r^2 + 2*y^2) + 2*p2*x*y]

2.5.3 畸变校正实现

OpenCV 提供了现成的函数:

# 畸变校正
img_undistorted = cv2.undistort(img, mtx, dist, None, mtx)

# 或者用 remap 方式(效率更高)
mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, mtx, 
                                          (w, h), cv2.CV_32FC1)
img_undistorted = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)

注意:畸变校正会改变图像的像素位置,导致图像边缘出现黑边。我一般会配合 getOptimalNewCameraMatrix 来调整,保留更多有效区域。

2.6 本章知识体系

下面这张图总结了相机成像模型与标定的核心逻辑:

相机成像模型与标定知识体系 相机成像模型 针孔相机模型 相机内参 (K矩阵) 相机外参 (R|t) 焦距 (fx, fy) 主点 (cx, cy) 畸变系数 径向畸变 (k1,k2,k3) 切向畸变 (p1,p2) 张正友标定法 棋盘格角点提取 → 单应性矩阵 → 非线性优化 输出:内参矩阵 + 畸变系数 + 外参

从这张图可以看得很清楚:相机成像模型以针孔模型为基础,内参描述相机内部属性,外参描述相机位姿,畸变则是真实世界的修正项。张正友标定法把这一切串联起来,最终输出我们需要的所有参数。

总结一下:相机标定是结构光系统的基石。内参决定了「看多清楚」,外参决定了「看哪里」,畸变校正保证了「看得准」。这三者缺一不可。我见过太多项目,重建精度上不去,最后发现是标定没做好——白白浪费了时间和精力。

专注资料整理