4. 相机成像模型:针孔相机模型、内参矩阵、畸变模型

聊到手眼标定,绕不开的第一个硬核知识点就是相机成像模型。说白了,就是搞明白「三维世界里的一个点,是怎么跑到二维图像上那个像素位置的」。这个映射关系搞不清楚,后面标定出来的结果全是空中楼阁。

我个人习惯把相机模型拆成三块来讲:针孔模型(理想情况)、内参矩阵(数学表达)、畸变模型(现实修正)。这三块吃透了,相机在你眼里就不再是一个黑盒子。

4.1 针孔相机模型——最朴素的成像原理

先回到最原始的针孔相机。一个小孔,一堵暗箱,光线穿过小孔在底片上倒立成像。这就是所有相机模型的鼻祖。

为什么用针孔模型?因为它把复杂的透镜系统简化成了一个光心和一个成像平面。三维点 P(X, Y, Z) 经过光心投影到成像平面上的点 p(x, y),满足一个简单的相似三角形关系:

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

这里的 f 是焦距,单位是物理长度(毫米)。

我的经验: 刚接触时容易把「成像平面」和「像素平面」搞混。成像平面是物理世界,单位是毫米;像素平面是数字世界,单位是像素。中间差了一个「像素尺寸」的转换。

我在项目中遇到过一位同事,死活标定不准,最后发现是他把成像平面的物理坐标直接当像素坐标用了。嗯,这个坑其实挺常见的。

4.2 内参矩阵——从物理坐标到像素坐标

针孔模型给出的是物理坐标 (x, y),但相机最终输出的是像素坐标 (u, v)。怎么转?靠内参矩阵。

内参矩阵 K 长这样:

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

其中:

  • fx, fy:焦距在 x、y 方向上的像素量纲。fx = f / dx,dx 是每个像素的物理宽度。
  • cx, cy:主点坐标,也就是光轴与成像平面的交点。理想情况下在图像中心,但实际会有偏移。

完整的投影公式就是:

Z * [u, v, 1]^T = K * [X, Y, Z]^T

你想想看,这个公式把三维世界和二维像素联系起来了。手眼标定里,我们反复在用这个公式做正向投影和反向求解。

参数 含义 常见范围
fx x 方向焦距(像素) 500 ~ 2000
fy y 方向焦距(像素) 500 ~ 2000
cx 主点 x 坐标 图像宽度的一半 ± 几十像素
cy 主点 y 坐标 图像高度的一半 ± 几十像素
核心要点: 内参矩阵是相机的「出厂身份证」。同一个相机,内参是固定的(除非变焦或拆装)。手眼标定前,必须先标定内参。

4.3 畸变模型——现实世界的修正

针孔模型是理想情况。现实中的镜头有透镜,透镜会引入畸变。我见过最夸张的一个案例,标定板在图像边缘弯成了弧形——那就是畸变没校正。

畸变主要分两类:径向畸变和切向畸变。

4.3.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^2 = x^2 + y^2,k1, k2, k3 是径向畸变系数。一般用 k1, k2 就够了,鱼眼镜头才需要 k3。

4.3.2 切向畸变

切向畸变是透镜和成像平面不平行导致的。说白了就是镜头装歪了。

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]

p1, p2 是切向畸变系数。

避坑指南: 我曾经在标定一个工业相机时,只校正了径向畸变,结果手眼标定误差始终下不去。后来加上切向畸变校正,误差直接降了一个数量级。所以,两个畸变模型都要用,别偷懒。

4.4 完整成像流程——一张图看懂

把上面所有环节串起来,完整的成像流程是这样的:

  1. 世界坐标系下的三维点 P_w
  2. 通过外参(旋转+平移)变换到相机坐标系 P_c
  3. 通过针孔投影得到归一化平面上的物理坐标 (x, y)
  4. 应用畸变模型得到校正后的物理坐标 (x', y')
  5. 通过内参矩阵映射到像素坐标系 (u, v)

下面这张图把这个流程画清楚了:

相机成像完整流程 世界坐标系 相机坐标系 归一化平面 畸变校正 像素坐标 说明: 外参(R, t)将世界点变换到相机坐标系;针孔投影得到归一化坐标;畸变模型修正透镜误差;内参矩阵映射到像素。

4.5 畸变校正实战——OpenCV 代码示例

理论讲完了,来点实际的。用 OpenCV 做畸变校正,核心就三步:

import cv2
import numpy as np

# 假设你已经标定得到了内参和畸变系数
K = np.array([[fx, 0, cx],
              [0, fy, cy],
              [0,  0,  1]], dtype=np.float32)

dist = np.array([k1, k2, p1, p2, k3], dtype=np.float32)

# 读取一张畸变图像
img = cv2.imread('distorted.jpg')
h, w = img.shape[:2]

# 计算校正映射
new_K, roi = cv2.getOptimalNewCameraMatrix(K, dist, (w, h), 1, (w, h))
mapx, mapy = cv2.initUndistortRectifyMap(K, dist, None, new_K, (w, h), cv2.CV_32FC1)

# 应用映射
undistorted = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)

# 裁剪掉黑色边框
x, y, w_roi, h_roi = roi
undistorted = undistorted[y:y+h_roi, x:x+w_roi]

cv2.imwrite('undistorted.jpg', undistorted)
我的习惯: 用 getOptimalNewCameraMatrix 的 alpha 参数控制输出图像大小。alpha=1 保留所有像素(有黑边),alpha=0 裁剪到最大有效区域。我一般用 alpha=0,省得后面还要处理黑边。

4.6 本章小结

相机成像模型是手眼标定的地基。你想想看,如果连相机怎么把三维点变成二维像素都搞不清楚,后面标定出来的手眼矩阵怎么可能准?

记住三个核心:

  • 针孔模型是理想基础,理解相似三角形投影
  • 内参矩阵是物理到像素的桥梁,fx、fy、cx、cy 四个参数
  • 畸变模型是现实修正,径向畸变(k1, k2, k3)和切向畸变(p1, p2)都要考虑