4. 相机模型与图像处理:针孔相机模型、畸变模型、双目相机模型、OpenCV图像处理基础
各位同学好,我是你们的讲师。今天我们来聊聊视觉SLAM里最基础、也最绕不开的一个话题——相机模型与图像处理。
说实话,我刚开始做SLAM的时候,觉得相机模型不就是个小孔成像嘛,有啥好研究的?结果第一次在实际项目中标定相机,就被畸变参数折腾得够呛。嗯,从那以后我再也不敢小看这部分内容了。
4.1 针孔相机模型:SLAM的“眼睛”是怎么看世界的?
针孔相机模型,说白了就是初中物理课上学过的小孔成像。光从物体表面反射,穿过一个小孔,在后面的成像平面上形成一个倒立的像。这个模型虽然简单,但它是我们理解所有相机几何的基础。
在SLAM里,我们关心的是:三维空间中的一个点,是怎么投影到二维图像上的?
这个过程可以用一个公式来描述:
// 世界坐标系到像素坐标系的投影
// [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:焦距在x和y方向上的像素度量。我习惯叫它“像素焦距”
- cx, cy:光心在像素坐标系中的位置,通常接近图像中心
- R, t:相机的外参,描述相机在世界坐标系中的位姿
核心理解:内参是相机的“先天属性”,出厂就定了;外参是相机的“后天位置”,每次移动都会变。SLAM要做的,就是一边估计外参,一边优化地图点。
我在项目中遇到过一个问题:用手机摄像头做AR,发现虚拟物体总是飘。查了半天,原来是内参标定不准,fx和fy差了0.5个像素。你想想看,在10米远的距离上,这点误差就能让投影偏移好几厘米。所以,内参标定一定要认真做。
4.2 畸变模型:为什么直线会变弯?
理想很丰满,现实很骨感。针孔模型假设镜头是完美的,但实际镜头都有畸变。最常见的两种:
- 径向畸变:光线经过透镜边缘时弯曲得更厉害,导致图像边缘的直线变弯。分为桶形畸变和枕形畸变
- 切向畸变:镜头和成像平面不平行导致的,像个平行四边形被拉伸了
畸变矫正的数学模型长这样:
// 径向畸变
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是切向畸变参数。r是点到光心的距离。
避坑指南:我曾经在标定一个广角相机时,只用了k1和k2两个参数,结果边缘区域的误差一直降不下去。后来加了k3,效果立竿见影。对于普通相机,k1、k2就够了;但对于广角或鱼眼镜头,一定要用k3甚至更高阶的参数。
为什么会这样?因为广角镜头边缘的光线弯曲程度是非线性的,低阶模型拟合不了。你想想看,一个120度视场角的镜头,边缘的畸变可能达到几十个像素,不用高阶模型根本搞不定。
4.3 双目相机模型:如何用两只“眼睛”看深度?
单目相机有个致命问题:它不知道物体的真实尺度。一个近的小物体和一个远的大物体,在图像上可能一模一样。这就是所谓的“尺度不确定性”。
双目相机解决了这个问题。它用两个相机同时拍摄,通过左右图像的视差来计算深度。原理其实很简单——就是三角测量。
双目深度计算公式:
// 深度 Z = (f * b) / d
// 其中:
// f - 焦距(像素)
// b - 基线长度(两个相机光心之间的距离)
// d - 视差(同一个点在左右图像上的像素差)
关键洞察:视差越大,物体越近;视差越小,物体越远。当物体无限远时,视差趋近于0。所以双目相机对近处物体的深度估计很准,对远处物体就力不从心了。
我记得在做一个室内机器人项目时,用了基线只有5cm的双目相机。结果在3米以外的物体,深度误差就超过10%了。后来换了个基线20cm的相机,效果好了很多。所以选双目相机时,基线长度要根据你的应用场景来定。
双目相机的标定比单目复杂得多,不仅要标定每个相机的内参和畸变,还要标定两个相机之间的相对位姿(外参)。这个外参就是左右相机之间的旋转矩阵R和平移向量t。
4.4 OpenCV图像处理基础:SLAM的“预处理”流水线
拿到图像之后,不能直接扔给SLAM算法。得先做预处理。我习惯把预处理分成三步:
- 图像去畸变:用标定好的畸变参数,把畸变图像矫正为理想图像
- 图像缩放:为了实时性,通常把图像缩放到640x480或更小
- 图像增强:直方图均衡化、高斯滤波等,提高特征提取的稳定性
OpenCV里最常用的几个函数:
// 1. 去畸变
cv::Mat map1, map2;
cv::initUndistortRectifyMap(K, distCoeffs, cv::Mat(), K, imageSize, CV_32FC1, map1, map2);
cv::remap(image, undistorted, map1, map2, cv::INTER_LINEAR);
// 2. 图像缩放
cv::resize(image, resized, cv::Size(640, 480));
// 3. 直方图均衡化(对灰度图)
cv::equalizeHist(gray, equalized);
// 4. 高斯滤波
cv::GaussianBlur(image, blurred, cv::Size(5, 5), 1.5);
个人经验:去畸变时,我建议用initUndistortRectifyMap生成映射表,然后用remap一次性处理。这样比每帧都调用undistort函数要快得多。在嵌入式设备上,这个优化能省下好几毫秒。
还有一个容易被忽略的点:图像的数据类型。OpenCV默认用uint8存图像,但SLAM里经常需要做浮点运算。比如计算梯度、光流时,最好先把图像转成float类型,避免精度损失。
// 转成浮点型
cv::Mat floatImage;
image.convertTo(floatImage, CV_32F, 1.0/255.0);
嗯,这里要注意:转换时别忘了归一化到[0,1]区间,否则后续的梯度计算会出问题。我刚开始就踩过这个坑,算出来的梯度值大得离谱,还以为是算法写错了。
4.5 本章知识体系
为了让大家更直观地理解本章的知识结构,我画了一张图:
这张图把本章的核心内容串起来了。从三个相机模型出发,最终都汇聚到OpenCV这个工具上。说白了,相机模型是理论,OpenCV是实践,两者缺一不可。
好了,这一章的内容就到这里。记住:相机模型是SLAM的基石,理解得越深,后面的路走得越稳。下一章我们会聊特征点提取与匹配,到时候这些预处理的知识就会派上用场了。
公众号:蓝海资料掘金营,微信deep3321