第二章:相机模型与标定——让机器看懂世界的“眼睛”

大家好,我是你们的老朋友。今天我们来聊聊视觉SLAM里最基础、也最容易被忽视的一环——相机模型与标定。

你想想看,SLAM系统要定位、要建图,所有信息都来自图像。如果相机本身没搞清楚,后面的算法再牛也是白搭。说白了,相机标定就是给机器配一副“度数准确”的眼镜。

核心观点:没有准确的相机内参,SLAM的精度就是空中楼阁。我见过太多新手,花大量时间调后端优化,结果发现是标定板没拍好——白忙一场。

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

先别急着上代码,我们得把原理搞透。针孔相机模型,说白了就是小孔成像的数学版本。

想象一个暗箱,前面戳个小孔,光线穿过小孔在后面的胶片上成像。嗯,这就是最原始的相机。但现实中,我们用的是透镜,不是小孔。不过数学上,我们可以等效成针孔模型。

核心公式就一个:

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

其中K就是内参矩阵,包含fx, fy(焦距),cx, cy(主点偏移)。

我个人习惯把内参矩阵写成这样:

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

fx和fy为什么可能不一样?因为像素不是正方形的,或者传感器有缩放。我在项目中遇到过一台工业相机,fx和fy差了3%,如果不标定,建图直接歪了。

小技巧:主点(cx, cy)理论上应该在图像中心,但实际总有偏差。我建议标定后检查一下,如果偏差超过10个像素,可能是标定板没放正。

2.2 畸变模型——镜头不是完美的

现实中的镜头都有畸变。你想想看,广角镜头拍出来的照片,边缘的直线都弯了。这就是畸变。

畸变主要分两种:

  • 径向畸变:光线穿过透镜时,边缘弯曲更厉害。用k1, k2, k3三个参数描述。
  • 切向畸变:透镜和传感器不平行。用p1, p2两个参数描述。

我曾经在调试一个扫地机器人时,发现地图边缘总是扭曲。查了两天,最后发现是切向畸变没标定——镜头装歪了0.5度。从那以后,我每次标定都会检查p1, p2的值。

畸变矫正的公式长这样:

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

看着复杂?其实你不需要手算。OpenCV和Matlab都帮你搞定了。

避坑指南:我曾经用一张标定板拍了20张图,结果标定出来的畸变参数特别大。后来发现,标定板上有污渍。记住:标定板必须干净、平整、无反光。

2.3 相机标定实战——用OpenCV搞定

好了,理论讲完了,咱们来点实际的。我用OpenCV标定过不下50台相机,流程早就烂熟于心。

核心步骤就三步:

  1. 准备标定板:打印一张棋盘格,贴在硬纸板上。格子数建议9x6或8x6。
  2. 采集图像:从不同角度拍15-20张。注意:标定板要占画面1/4以上,角度要多样。
  3. 运行标定:用OpenCV的calibrateCamera函数。

下面是我常用的标定代码,你直接拿去用:

import cv2
import numpy as np
import glob

# 棋盘格参数
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 = []  # 3D点
imgpoints = []  # 2D点

images = glob.glob('calib_images/*.jpg')

for fname in images:
    img = cv2.imread(fname)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    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)

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

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

跑完这段代码,你就得到了内参和畸变系数。我建议你再用重投影误差检查一下——误差小于0.5像素才算合格。

我的习惯:标定完成后,我会拍一张标定板放在画面边缘的照片,手动检查畸变矫正效果。如果边缘的直线还是弯的,说明标定有问题。

2.4 用Matlab标定——更直观的选择

如果你不想写代码,Matlab的Camera Calibrator是个好选择。我个人觉得它比OpenCV更直观,特别适合初学者。

操作步骤:

  1. 打开Matlab,输入 cameraCalibrator
  2. 导入标定图片
  3. 输入棋盘格尺寸(单位:毫米)
  4. 点击Calibrate,等几秒钟
  5. 查看结果:重投影误差、畸变矫正效果

Matlab会直接显示每张图片的误差,哪张拍得不好一目了然。我一般会剔除误差大于0.5像素的图片,重新标定。

工具 优点 缺点
OpenCV 免费、可集成到代码、批量处理 需要写代码、调试麻烦
Matlab 可视化好、操作简单、适合验证 收费、不能直接集成到嵌入式

我的建议是:先用Matlab验证标定板质量和拍摄方法,再用OpenCV集成到实际项目中。

2.5 标定中的常见坑——我踩过的都告诉你

标定看起来简单,但坑不少。我列几个最常见的:

  • 标定板不平:打印纸贴在弯曲的纸板上,标定出来全是错的。我建议用亚克力板或玻璃板。
  • 图片太少:少于10张,标定结果不稳定。我一般拍20张。
  • 角度不够:只拍正面,不拍倾斜角度。这样标定不出畸变参数。
  • 对焦没锁:标定过程中改变了焦距,内参全变了。记住:标定和实际使用时焦距必须一致。

我曾经踩过的坑:有一次标定双目相机,左右目分别标定都很好,但联合标定后基线长度差了5mm。查了半天,发现是标定板在左右目图像中位置不一致——标定板没固定好,拍摄时移动了。

2.6 本章知识体系

下面这张图总结了本章的核心内容,你可以把它当作一个快速参考:

相机模型与标定 针孔相机模型 内参矩阵 K = [fx, fy, cx, cy] 外参矩阵 [R|t] 投影方程:3D→2D 畸变模型 径向畸变:k1, k2, k3 切向畸变:p1, p2 畸变矫正公式 相机标定实战 OpenCV calibrateCamera Matlab Camera Calibrator 重投影误差 < 0.5像素 标定是SLAM的基石 内参不准 → 特征点位置错 → 位姿估计崩 常见坑:标定板不平、图片太少、角度不够、焦距改变

这张图把本章的知识点串起来了。你从针孔模型出发,理解内参外参;然后处理畸变;最后用工具标定。每一步都踩实了,后面的SLAM才能跑得稳。

最后说一句:标定这件事,看起来简单,但做扎实了能省你后面很多时间。我见过太多人花几周调SLAM参数,结果发现是标定没做好。嗯,别走弯路。


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