第3章:相机成像模型与标定

大家好,我是你们的老朋友。今天咱们来聊聊相机标定——这个在感知融合里绕不开的话题。

说实话,我刚入行那会儿,觉得相机标定就是拍几张棋盘格照片,跑个算法就完事了。后来在实际项目中吃过亏,才明白这里面门道不少。今天我就把这些年踩过的坑、积累的经验,一次性讲清楚。

3.1 针孔相机模型

先说说最基础的针孔相机模型。你想想看,相机成像的本质是什么?说白了,就是把三维世界里的点,投影到二维图像平面上。

针孔模型假设光线是直线传播的,穿过一个小孔后,在后面的成像平面上形成倒像。数学上,这个过程可以用一个3x4的投影矩阵来描述:

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

其中:

  • (u, v) 是图像像素坐标
  • (X, Y, Z) 是世界坐标系下的三维点
  • K 是相机内参矩阵
  • [R | t] 是相机外参(旋转和平移)
  • s 是尺度因子

内参矩阵K长这样:

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

fx、fy是焦距(以像素为单位),cx、cy是主点坐标。我在项目中遇到过,有些同学直接拿厂家给的标称值用,结果融合出来的点云总是对不上。嗯,这里要注意——内参一定要自己标定,因为镜头安装、温度变化都会影响实际值。

3.2 畸变模型

理想很丰满,现实很骨感。针孔模型假设镜头是完美的,但实际镜头都有畸变。主要分两种:

3.2.1 径向畸变

径向畸变是光线通过镜头边缘时发生的弯曲。离中心越远,畸变越明显。我见过最夸张的一次,是某款广角镜头,边缘畸变达到了15个像素——这种数据直接拿去用,融合出来的结果根本没法看。

径向畸变的数学模型:

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

其中r是点到主点的距离,k1、k2、k3是径向畸变系数。一般用k1、k2就够了,鱼眼镜头才需要k3。

3.2.2 切向畸变

切向畸变是因为镜头和成像平面不平行造成的。说白了,就是镜头装歪了。这种情况在量产车上比较常见,毕竟装配精度有限。

切向畸变的模型:

x_distorted = x + [2*p1*x*y + p2*(r^2 + 2*x^2)]
y_distorted = y + [p1*(r^2 + 2*y^2) + 2*p2*x*y]

p1、p2是切向畸变系数。

重要提醒:畸变参数一共5个(k1, k2, k3, p1, p2),但实际标定时,k3和p2往往很小。我建议先标定k1、k2、p1,如果重投影误差还大,再加k3和p2。

3.3 张正友标定法

说到相机标定,就不得不提张正友标定法。这个方法牛在哪里?它只需要你拍几张棋盘格照片,就能同时标定出内参和畸变参数。

核心思想是这样的:

  1. 假设棋盘格是平面(Z=0),建立世界坐标系
  2. 通过角点检测,得到图像坐标
  3. 利用单应性矩阵,求解内参初值
  4. 用最大似然估计,优化所有参数

我曾经在一个项目中,用张正友法标定了20台相机,每台拍15张照片,总共花了3个小时。结果发现有一台相机的重投影误差特别大——后来排查发现,是棋盘格贴歪了。所以啊,标定板的平整度至关重要

3.4 相机内参标定实战

好了,理论说完了,咱们来点实际的。下面是我常用的标定流程:

3.4.1 准备工作

  • 打印一张棋盘格(建议A3纸,格子数9x6或12x9)
  • 贴在平整的硬板上(我一般用亚克力板)
  • 准备一个光照均匀的环境

避坑指南:我曾经用普通打印纸贴在三合板上,结果纸受潮变形了,标定出来的内参偏差很大。后来改用防水相纸+铝塑板,再也没出过问题。

3.4.2 采集图像

拍摄时要注意:

  • 棋盘格要占画面1/4到1/2
  • 角度要多样:左右倾斜、上下倾斜、旋转
  • 距离要变化:近、中、远各拍几张
  • 至少拍15-20张

我习惯用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 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)
        # 亚像素精确化
        corners2 = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1),
                                    criteria)
        imgpoints.append(corners2)

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

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

3.4.3 结果评估

标定完成后,一定要检查重投影误差:

total_error = 0
for i in range(len(objpoints)):
    imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
    error = cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2)
    total_error += error

print("平均重投影误差: ", total_error/len(objpoints))

一般来说,重投影误差小于0.5像素就算不错了。如果超过1个像素,建议检查标定板是否平整,或者重新采集图像。

我的经验:标定完成后,最好用一张没参与标定的图像做验证。把棋盘格角点投影到图像上,看看偏差大不大。这一步能帮你发现很多隐藏问题。

3.5 常见问题与解决方案

问题 可能原因 解决方案
重投影误差大 标定板不平、图像模糊 换硬质标定板、提高快门速度
内参不稳定 图像数量不够、角度单一 增加图像、丰富角度
畸变校正后图像变形 畸变参数过拟合 减少畸变参数(只用k1、k2)
角点检测失败 光照不均、棋盘格太小 均匀光照、增大棋盘格

好了,这一章的内容就到这里。相机标定看似简单,但要做好其实不容易。我建议你多动手实践,多拍几组数据对比一下。下一章咱们聊聊激光雷达的标定,那个更有意思。

记住一句话:标定做不好,融合全白搞。共勉!


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