3. 相机标定原理:针孔模型、内参、外参、畸变参数,张正友标定法

相机标定,说白了就是搞清楚相机到底是怎么“看”世界的。你想想看,一个三维空间里的点,怎么就能映射到一张二维照片上?这中间经历了什么变换?又引入了哪些误差?

我个人习惯把标定分成两步走:先理解理想模型(针孔模型),再修正现实偏差(畸变参数)。张正友标定法,就是连接这两步最实用的桥梁。

3.1 针孔模型:理想世界的映射

针孔模型是最基础的成像模型。它假设光线从物体表面反射,穿过一个极小的孔(光心),在后面的成像平面上形成倒立的像。虽然简单,但它是所有标定理论的起点。

这个模型涉及四个坐标系的转换:

  • 世界坐标系:你放物体的大空间,单位是米或毫米。
  • 相机坐标系:以相机光心为原点,Z轴指向镜头前方。
  • 图像坐标系:在成像平面上,以物理尺寸(毫米)为单位。
  • 像素坐标系:最终图像,以像素为单位,原点通常在左上角。

整个映射过程可以概括为:

世界点 (Xw, Yw, Zw) → 相机点 (Xc, Yc, Zc) → 图像点 (x, y) → 像素点 (u, v)

核心公式(齐次坐标形式):

s * [u, v, 1]^T = K * [R | t] * [Xw, Yw, Zw, 1]^T

其中,K 是内参矩阵,[R | t] 是外参矩阵。

3.2 内参:相机自己的“基因”

内参是相机本身的固有属性,一旦出厂,理论上就不变了。它描述了从相机坐标系到像素坐标系的映射关系。

参数 符号 含义
焦距 fx, fy 以像素为单位的焦距,通常 fx ≈ fy
主点 cx, cy 光轴与成像平面的交点,理想情况在图像中心
倾斜因子 s 像素坐标系x轴与y轴的不垂直度,现代相机几乎为0

内参矩阵 K 长这样:

K = | fx   s  cx |
    |  0  fy  cy |
    |  0   0   1 |

我的经验: 我在项目中遇到过一台工业相机,标定出来的 cx 和 cy 明显偏离图像中心。一开始以为是标定板没放正,后来查了 datasheet 才发现,这款相机的 sensor 在封装时就有 2-3 个像素的偏移。所以,永远不要假设主点就在图像中心,标定出来的值才是真实的。

3.3 外参:相机在空间中的“位置”

外参描述的是相机相对于世界坐标系的位姿。说白了,就是相机放在哪、朝哪看。

  • 旋转矩阵 R:3x3 的正交矩阵,表示相机的朝向。
  • 平移向量 t:3x1 的向量,表示相机在世界坐标系中的位置。

外参将世界坐标系下的点变换到相机坐标系下:

Xc = R * Xw + t

这里有个容易混淆的地方:R 和 t 是相机坐标系相对于世界坐标系的变换。如果你把相机固定在一个机械臂末端,那么外参就是相机与机械臂末端之间的位姿关系——这就是手眼标定的核心。

避坑指南: 我曾经在做一个移动机器人项目时,把外参的平移向量 t 理解反了。结果机器人明明应该往前走,相机却显示它在后退。后来检查发现,我把世界坐标系和相机坐标系的转换方向搞反了。记住:外参是把世界点“拉”到相机坐标系下,不是把相机“推”到世界坐标系下。

3.4 畸变参数:现实世界的“偏差”

针孔模型是理想情况,但真实镜头有畸变。主要分两类:

  • 径向畸变:光线通过镜头边缘时弯曲程度不同,导致直线变弯。分为桶形畸变和枕形畸变。
  • 切向畸变:镜头与成像平面不平行,导致图像倾斜。

畸变模型通常用多项式来描述:

径向畸变:
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)

切向畸变:
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]

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

畸变类型 参数 典型表现
径向畸变 k1, k2, k3 直线变弯,越靠近边缘越明显
切向畸变 p1, p2 图像倾斜,像被“拧”了一下

我的习惯: 标定时我一般只估计 k1, k2, p1, p2 四个参数。k3 通常只在鱼眼镜头或畸变特别大的镜头下才需要。参数太多反而容易过拟合,标定结果不稳定。

3.5 张正友标定法:从棋盘格到参数

张正友标定法是目前最主流的标定方法。它只需要拍摄不同角度的棋盘格图像,就能同时解出内参、外参和畸变参数。

核心思路分三步:

  1. 检测角点:在每张棋盘格图像中提取亚像素精度的角点坐标。
  2. 求解单应性矩阵:每张图像都可以计算一个单应性矩阵 H,它连接了棋盘格平面和图像平面。
  3. 分解参数:利用多个 H 矩阵的约束,通过最小二乘法解出内参,再反推外参和畸变参数。

关键约束: 张正友标定法利用了棋盘格是平面这一事实。棋盘格上的所有角点都在同一个平面上(Zw=0),这大大简化了计算。你想想看,如果标定板是三维的,那问题就复杂多了。

下面是我用 OpenCV 做标定的标准流程:

import cv2
import numpy as np

# 1. 准备棋盘格角点的世界坐标
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)

# 2. 存储所有图像的世界坐标和像素坐标
objpoints = []  # 世界坐标
imgpoints = []  # 像素坐标

# 3. 遍历图像,检测角点
for fname in image_files:
    img = cv2.imread(fname)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    ret, corners = cv2.findChessboardCorners(gray, pattern_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)

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

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

避坑指南: 我曾经只拍了 5 张棋盘格图像就做标定,结果内参波动很大。后来发现,图像数量至少要在 10-15 张以上,而且角度要覆盖:正对、左倾、右倾、上仰、下俯、旋转。棋盘格要占图像面积的 1/4 到 1/2,太小了角点检测不准,太大了容易超出视野。

3.6 标定流程与知识体系

下面这张图总结了相机标定的完整知识体系:

相机标定知识体系 标定目标:获取内参、外参、畸变参数 针孔模型 畸变模型 张正友标定法 坐标系变换 世界 → 相机 → 图像 → 像素 内参矩阵 K:fx, fy, cx, cy 外参矩阵 [R|t]:旋转+平移 畸变校正 径向畸变:k1, k2, k3 切向畸变:p1, p2 校正公式:多项式拟合 标定流程 1. 拍摄多角度棋盘格 2. 检测角点 3. 求解单应性矩阵 → 分解参数 输出:内参矩阵 + 畸变系数 + 每张图像的外参 💡 标定质量检查 重投影误差 < 0.5 像素为合格;内参矩阵 fx 与 fy 应接近;畸变系数 k1 通常为负(桶形畸变) 如果标定结果异常,先检查棋盘格角点是否检测正确,再检查图像数量是否足够

3.7 标定质量评估

标定完怎么知道结果好不好?我一般看三个指标:

  • 重投影误差:将世界坐标点通过标定结果投影到图像上,计算与检测到的像素坐标的误差。通常要求小于 0.5 像素。
  • 内参合理性:fx 和 fy 应该接近,cx 和 cy 应该在图像中心附近。
  • 畸变系数大小:k1 通常为负(桶形畸变),值一般在 0.1 到 0.5 之间。如果 k1 的值大于 1,说明镜头畸变很大,或者标定有问题。

我的经验: 有一次标定结果重投影误差只有 0.3 像素,但实际用起来就是不准。后来发现是棋盘格打印精度不够,角点世界坐标本身就有误差。所以,标定板的精度直接影响标定结果。建议用玻璃基板或陶瓷基板的高精度棋盘格,别用普通打印纸贴硬纸板。

相机标定是 3D 视觉的基石。内参、外参、畸变参数,这三个概念贯穿了整个 3D 视觉定位系统。后面的手眼标定、深度估计、点云配准,全都依赖这里标出来的参数。嗯,这一章的内容就到这里,把基础打牢,后面才能走稳。