第三章 相机模型与标定:让无人机看懂世界

各位同学,欢迎来到第三章。前面我们聊了VIO的整体框架,今天要深入一个最基础也最容易踩坑的环节——相机模型与标定。

说实话,我见过太多团队在VIO上折腾半天,最后发现问题是相机没标好。你想想看,如果相机本身就有偏差,后面算出来的位姿能准吗?

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

先说说针孔相机模型。说白了,它就是模拟光通过一个小孔在暗箱里成像的过程。我当年在学校学这个的时候,觉得太简单了——不就是小孔成像嘛。但真正做项目才发现,这里面门道不少。

针孔模型的数学表达其实很直观:

// 世界坐标系到像素坐标的映射
// [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是主点偏移。我在项目中遇到过一个问题:很多人以为cx、cy就是图像中心,其实不是。它们代表的是光轴与成像平面的交点,通常离图像中心有几到十几个像素的偏差。

核心要点:针孔模型假设所有光线都通过一个点(光心),然后投影到成像平面。这个假设在理想情况下成立,但实际镜头会有畸变。

3.2 畸变模型:现实总是不完美的

为什么需要畸变模型?因为真实的镜头不是完美的针孔。你想想看,镜头为了收集更多光线,用了透镜组,这就带来了畸变。

畸变主要分两种:

  • 径向畸变:光线经过透镜边缘时弯曲更厉害,导致图像边缘的直线变弯。我见过最夸张的情况,一个广角镜头边缘畸变超过10%。
  • 切向畸变:透镜和成像平面不平行,导致图像倾斜。这个在手机镜头上比较常见,工业相机一般还好。

畸变模型通常用多项式来描述:

// 径向畸变
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)

// 切向畸变
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)

这里k1、k2、k3是径向畸变系数,p1、p2是切向畸变系数。嗯,这里要注意:对于大多数镜头,只用k1、k2就够了,k3一般用在鱼眼镜头。

避坑指南:我曾经在标定一个广角镜头时,用了5阶畸变模型,结果过拟合了。标定结果在标定板上误差很小,但实际场景反而更差。后来我改用3阶模型,效果反而更好。所以,别盲目追求高阶模型。

3.3 相机内参标定流程

标定说白了就是求解上面那些参数。我个人的习惯是用棋盘格标定板,因为角点检测稳定,而且OpenCV有现成的工具。

完整的标定流程是这样的:

  1. 准备标定板:打印一张棋盘格,贴在平整的硬板上。格子大小要精确测量,我一般用10cm的格子。
  2. 采集图像:从不同角度拍摄20-30张图像。注意:要覆盖视野的各个区域,特别是边缘。
  3. 角点检测:用OpenCV的findChessboardCorners检测角点。
  4. 参数优化:用最小二乘法优化内参和畸变参数。
  5. 验证结果:用标定结果去畸变一张新图像,看看直线是不是真的直了。

下面是我常用的标定代码片段:

import cv2
import numpy as np

# 准备标定板参数
chessboard_size = (9, 6)  # 内角点数量
square_size = 0.1  # 格子大小,单位米

# 准备世界坐标系中的点
objp = np.zeros((chessboard_size[0]*chessboard_size[1], 3), np.float32)
objp[:,:2] = np.mgrid[0:chessboard_size[0], 
                      0:chessboard_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, chessboard_size, None)
    
    if ret:
        objpoints.append(objp)
        imgpoints.append(corners)

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

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

个人经验:标定的时候,我建议每拍一张就检查一下角点检测结果。有时候光照不均匀会导致角点检测失败,及时重拍比事后补救要省事得多。

3.4 标定中的常见问题

做标定这么多年,我总结了一些常见问题:

问题 现象 解决方法
标定板不平整 重投影误差大 用玻璃板或铝板固定标定纸
图像数量不足 参数不稳定 至少拍20张,覆盖不同角度
标定板太近或太远 焦距参数偏差大 保持标定板在0.5-2米范围内
运动模糊 角点检测不准 用三脚架或提高快门速度

我曾经遇到一个案例:一个团队标定结果看起来很好,重投影误差只有0.3像素,但实际VIO跑起来就是飘。后来发现他们所有标定图像都是在同一个距离拍的,导致焦距参数虽然内符合很好,但外推能力很差。

3.5 知识体系总览

为了让大家更直观地理解本章的知识结构,我画了一张图:

相机模型与标定知识体系 针孔相机模型 畸变模型 标定流程 内参矩阵 K = [fx, fy, cx, cy] 世界坐标系 → 像素坐标系 径向畸变: k1, k2, k3 切向畸变: p1, p2 棋盘格角点检测 最小二乘参数优化 标定结果:内参矩阵 + 畸变系数 应用:图像去畸变 → VIO前端 → 特征匹配

这张图把三个核心模块串起来了。你看,从针孔模型到畸变模型,再到标定流程,最后落到实际应用。每一步都环环相扣。

总结一下:相机标定是VIO的基石。标定做不好,后面所有算法都是空中楼阁。我建议大家在项目初期就花时间把标定做扎实,这比后期花几周时间调参要划算得多。

好了,这一章就到这里。记住:标定不是一次性的工作,相机在使用过程中可能会因为温度变化、振动等原因发生微小变化。定期重新标定是个好习惯。


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