3、数学基础(下):针孔相机模型、畸变模型、双目与RGB-D相机模型、张正友标定法实战

好,咱们接着聊。上一节我们把坐标系和刚体变换理清了,这节终于要碰相机本身了。

说实话,我刚开始做SLAM时,觉得相机模型不就是个小孔成像嘛,有啥好学的?结果第一次做标定,发现投影点怎么都对不上,折腾了两天才意识到是畸变参数没搞对。嗯,从那以后我再也不敢小看这部分了。

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

说白了,针孔相机模型就是初中物理的小孔成像。光从物体表面反射,穿过一个小孔,在后面的成像平面上形成倒像。但咱们做算法时,习惯把成像平面挪到小孔前面,这样图像就是正的了,计算也方便。

数学上怎么描述?很简单,四个坐标系之间的转换:

  • 世界坐标系:物体在真实世界的位置,单位是米
  • 相机坐标系:以相机光心为原点,Z轴朝前
  • 图像坐标系:在成像平面上,单位是毫米
  • 像素坐标系:最终图像上的行列索引,单位是像素

从世界到像素,核心公式就一个:

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

其中K是内参矩阵,长这样:

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

fx、fy是焦距在像素单位下的值,cx、cy是光心在像素坐标系中的位置。我习惯把K叫做「相机的身份证」,因为每台相机的K都是独一无二的。

核心要点:针孔模型假设所有光线都穿过一个理想小孔,没有透镜。但实际相机都有透镜,这就引出了畸变问题。

3.2 畸变模型——为什么直线会变弯?

你想想看,如果相机真的像针孔那样成像,那图像质量会非常差,因为进光量太少。所以实际相机都用了透镜来聚光。但透镜一加上,问题就来了——畸变。

畸变主要分两种:

  • 径向畸变:光线在透镜边缘弯曲得更厉害,导致直线变弯。越靠近图像边缘越明显。我见过最夸张的一次,一个广角镜头拍出来的门框,明明是直的,在图像里弯成了括号形。
  • 切向畸变:透镜和成像平面不平行,导致图像倾斜。这个在手机相机里比较常见,因为模组装配精度有限。

数学上怎么矫正?用多项式近似:

x_distorted = x * (1 + k1*r^2 + k2*r^4 + k3*r^6) + 2*p1*x*y + p2*(r^2 + 2*x^2)
y_distorted = y * (1 + k1*r^2 + k2*r^4 + k3*r^6) + p1*(r^2 + 2*y^2) + 2*p2*x*y

其中k1、k2、k3是径向畸变系数,p1、p2是切向畸变系数。r是点到光心的距离。

避坑提醒:我曾经在标定一个鱼眼镜头时,只用了k1、k2两个参数,结果矫正后的图像边缘还是弯的。后来加了k3才搞定。对于大广角镜头,建议至少用三个径向畸变参数。

3.3 双目相机模型——如何用两只眼睛看深度?

单目相机有个硬伤——它不知道物体离自己有多远。你想想看,一个近的小物体和一个远的大物体,在图像上可能一模一样。这就是所谓的「尺度不确定性」。

双目相机就是来解决这个问题的。它用两个相机同时拍,通过左右图像的视差来计算深度。原理其实很简单,就是三角测量:

depth = (f * baseline) / disparity

其中:

  • f是焦距(像素单位)
  • baseline是两个相机光心之间的距离
  • disparity是同一点在左右图像上的像素差

我在做双目SLAM时,发现一个很坑的事:baseline越大,深度测量越准,但两个相机的共同视野越小。这是个trade-off,一般室内用5-10cm,室外用20-30cm。

个人经验:双目匹配是瓶颈。我建议先用OpenCV的SGBM算法做快速验证,效果不错再考虑深度学习方案。别一上来就上神经网络,调试起来太痛苦。

3.4 RGB-D相机模型——直接给你深度图

RGB-D相机就省事多了,它直接输出深度信息。主流方案有两种:

  • 结构光(如Kinect v1):发射红外光斑,通过光斑变形计算深度。室内效果好,室外被阳光干扰就废了。
  • ToF(如Kinect v2、iPhone的LiDAR):发射红外脉冲,测量飞行时间。速度快,但分辨率低,而且多路径反射会引入误差。

RGB-D相机的核心问题是对齐。彩色图和深度图的分辨率、视场角都不一样,需要做配准。公式其实不复杂:

P_rgb = R * P_depth + t

其中R和t是深度相机到彩色相机的外参。这个参数在出厂时一般会标好,但如果你拆过相机再装回去,就得重新标了。

注意:RGB-D相机在强光下、透明物体、镜面反射场景中基本失效。我做过一个项目,在玻璃幕墙的大厅里用Kinect,深度图全是黑洞。最后只能换双目方案。

3.5 张正友标定法实战——手把手教你标定相机

好了,理论讲完了,咱们来点实际的。张正友标定法是目前最主流的相机标定方法,用一张棋盘格就能搞定。我标定过的相机没有一百也有八十了,流程早就烂熟于心。

核心思路就三步:

  1. 拍棋盘格:从不同角度拍10-20张。注意,棋盘格要在画面中占一定面积,别拍成一个小点。我习惯让棋盘格占画面的1/4到1/3。
  2. 提取角点:用OpenCV的findChessboardCorners函数。如果提取失败,检查一下棋盘格尺寸是否设置正确。
  3. 求解参数:用calibrateCamera函数,一步到位。

代码示例:

import cv2
import numpy as np

# 棋盘格参数
pattern_size = (9, 6)  # 内角点数量
square_size = 0.025    # 格子边长,单位米

# 准备世界坐标系中的点
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)
objp *= square_size

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

# 遍历所有图像
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)
        imgpoints.append(corners)
        
        # 可视化角点
        cv2.drawChessboardCorners(img, pattern_size, corners, ret)

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

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

我的习惯:标定完成后,一定要做重投影误差检查。一般误差小于0.5像素就算合格。如果误差太大,检查一下是不是有模糊的图像混进来了,或者棋盘格没放平。

标定完怎么验证?拿一张没参与标定的棋盘格图像,用得到的参数去畸变,看看直线是不是真的变直了。我一般会拍一张有门框或窗框的照片,矫正后看边缘是否笔直。

曾经踩过的坑:有一次我标定完,重投影误差只有0.3像素,但实际用起来效果很差。后来发现是棋盘格打印时没贴平,有褶皱。所以,棋盘格一定要贴在平整的硬板上,别用软纸。

3.6 本章知识体系

为了让你更直观地理解这节内容的结构,我画了一张图:

相机模型与标定知识体系 相机成像模型 针孔相机模型 畸变模型 双目相机模型 RGB-D模型 内参矩阵K 外参[R|t] 投影方程 径向畸变 切向畸变 畸变矫正 三角测量 视差计算 立体匹配 结构光 ToF 深度对齐 张正友标定法实战

这张图把整个知识脉络串起来了。从针孔模型出发,理解畸变是怎么来的,再扩展到双目和RGB-D,最后落到标定实战。每一步都有对应的数学公式和工程实现。

好了,这节的内容就到这。记住,相机模型是SLAM的「眼睛」,搞不清楚它,后面的所有算法都是空中楼阁。下一节我们会进入更核心的部分——特征点提取与匹配,那是SLAM前端的关键。


公众号:蓝海资料掘金营,微信deep3321