3. 相机模型与成像原理

做视觉引导抓取,第一步就是搞清楚相机是怎么「看」世界的。说白了,相机就是把三维空间里的点,映射到二维图像上。这个映射关系搞不明白,后面所有坐标转换都是空中楼阁。

我个人习惯把相机模型分成三块来理解:针孔模型讲理想情况,畸变校正处理现实误差,内参标定则是把前两者量化出来。咱们一个一个说。

3.1 针孔模型:最朴素的成像原理

针孔模型是最基础的相机模型。你想想看,一个小孔,光线穿过它,在后面的平面上形成倒像。这就是相机最原始的原理。

数学上怎么描述?四个坐标系来回倒腾:

  • 世界坐标系(Xw, Yw, Zw)——物体在真实世界的位置
  • 相机坐标系(Xc, Yc, Zc)——以相机光心为原点
  • 图像物理坐标系(x, y)——成像平面上的实际位置
  • 图像像素坐标系(u, v)——我们最终看到的像素坐标

整个流程就是:世界点 → 刚体变换到相机坐标系 → 透视投影到成像平面 → 离散化成像素坐标。

核心公式(投影矩阵):

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

其中 K 是内参矩阵,[R | t] 是外参矩阵。这个公式你最好背下来,后面所有手眼标定、位姿估计都绕不开它。

我在项目中遇到过一个问题:用公式算出来的投影点和实际图像点总差几个像素。当时排查了好久,最后发现是忽略了镜头畸变。嗯,这就引出了下一个话题。

3.2 畸变校正:镜头不是完美的

现实中的镜头都有畸变。主要分两类:

畸变类型 产生原因 视觉效果
径向畸变 透镜形状不完美,边缘光线偏折更大 直线变弯,像鱼眼效果
切向畸变 镜头与成像平面不平行 图像倾斜,像梯形

径向畸变用三个参数(k1, k2, k3)来描述,切向畸变用两个参数(p1, p2)。校正公式长这样:

// 径向畸变校正
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]

我曾经踩过的坑:标定完发现畸变校正后图像边缘还是有弯曲。后来发现是标定板拍得太少了,尤其是边缘区域。建议至少拍20张不同角度的棋盘格,并且让棋盘格出现在画面的各个角落。

3.3 内参标定:把相机参数算出来

内参标定就是求相机的内部参数:焦距(fx, fy)、主点(cx, cy)和畸变系数。最常用的方法是张正友标定法。

流程其实不复杂:

  1. 准备标定板——棋盘格或圆点阵,尺寸要精确测量
  2. 采集图像——不同角度、不同距离拍15-25张
  3. 提取角点——用OpenCV的findChessboardCorners()
  4. 求解内参——用calibrateCamera()一步到位
  5. 评估误差——看重投影误差,一般要小于0.5像素

我的个人习惯:标定完我会做两件事。第一,把标定板放在工作空间中心位置拍一张,手动检查畸变校正效果。第二,用标定结果去投影几个已知尺寸的物体,验证尺度是否正确。别完全相信重投影误差,实际效果才是硬道理。

3.4 知识结构总览

下面这张图把相机模型、畸变校正和内参标定的关系串起来了。你跟着箭头走一遍,就能理解整个流程。

相机模型与成像原理知识结构 针孔模型 畸变校正 内参标定 4个坐标系 投影矩阵公式 内参/外参矩阵 径向畸变(k1,k2,k3) 切向畸变(p1,p2) 校正公式 张正友标定法 角点提取 重投影误差评估 输出:内参矩阵K + 畸变系数 标定质量关键:标定板精度、图像数量(≥15张)、覆盖整个视场

3.5 实战:用OpenCV做标定

理论说完了,上代码。这是我常用的标定流程,你直接拿去用就行。

import cv2
import numpy as np
import glob

# 1. 准备棋盘格角点坐标
CHECKERBOARD = (9, 6)  # 内角点数量
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 = []  # 图像坐标系中的点

# 2. 读取标定图像
images = glob.glob('calib_images/*.jpg')

for fname in images:
    img = cv2.imread(fname)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # 3. 查找棋盘格角点
    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)

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

print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
print("重投影误差:", ret)

我建议:标定完一定要做畸变校正验证。用cv2.undistort()处理一张图像,看看边缘的直线是不是变直了。如果还有弯曲,要么增加标定图像,要么检查标定板尺寸是否量对了。

3.6 标定质量检查清单

标定做完了,怎么知道好不好?我一般按这个清单检查:

检查项 合格标准 不合格怎么办
重投影误差 < 0.5 像素 增加图像数量,剔除模糊图像
畸变校正效果 边缘直线无明显弯曲 检查标定板是否覆盖边缘区域
主点位置 接近图像中心 ±5% 检查相机安装是否倾斜
焦距稳定性 多次标定差异 < 2% 检查镜头是否松动或变焦

嗯,到这里相机模型和标定的内容就差不多了。记住一句话:标定精度决定了视觉引导的上限。后面做手眼标定、位姿估计,所有精度都建立在这一步的基础上。别嫌麻烦,多拍几张,多检查几遍,后面能省很多事。