4. 相机模型与图像处理:针孔相机模型、畸变模型、径向/切向、图像去畸变、双目相机模型、OpenCV基础操作

各位同学,欢迎来到第四章。这一章我们聊聊相机模型和图像处理。说实话,这是VIO系统里最基础但也最容易出坑的地方。我见过不少同学,算法推得飞起,结果标定没做好,整个系统直接崩掉。所以,咱们把基础打牢。

4.1 针孔相机模型:从三维到二维的映射

先说说针孔相机模型。说白了,它就是描述三维空间中的点怎么投影到二维图像上的。你想想看,光线通过一个小孔,在后面的成像平面上形成倒像。这就是最朴素的相机模型。

数学上,我们用四个坐标系来描述这个过程:世界坐标系、相机坐标系、图像坐标系、像素坐标系。嗯,这里要注意,像素坐标系和图像坐标系不是一回事。图像坐标系的原点在光轴与成像平面的交点,单位是毫米。而像素坐标系的原点在图像左上角,单位是像素。

投影公式其实很简单:

// 从相机坐标系到像素坐标系
// (X, Y, Z) 是相机坐标系下的三维点
// (u, v) 是像素坐标
// fx, fy 是焦距(像素单位)
// cx, cy 是主点坐标

u = fx * X / Z + cx
v = fy * Y / Z + cy

写成矩阵形式就是:

// 齐次坐标形式
// [u]   [fx  0  cx] [X]
// [v] = [ 0 fy  cy] [Y]
// [1]   [ 0   0   1] [Z]

这个矩阵,我们叫它内参矩阵K。每个相机都有自己的内参,需要通过标定得到。我在项目中遇到过,有人直接用厂家给的内参,结果精度差得离谱。记住,内参一定要自己标。

核心要点:针孔模型是理想模型,实际相机都有畸变。千万别直接用针孔模型去算,会出大问题。

4.2 畸变模型:径向畸变和切向畸变

实际相机不是完美的针孔。镜头有曲率,安装有偏差,这就产生了畸变。畸变主要分两种:径向畸变和切向畸变。

径向畸变:说白了就是镜头曲率导致的。光线经过透镜边缘时,弯曲程度和中心不一样。这又分两种:

  • 桶形畸变:图像向外凸出,像桶一样。广角镜头常见。
  • 枕形畸变:图像向内凹陷,像枕头。长焦镜头常见。

切向畸变:这个是因为镜头和成像平面不平行。嗯,说白了就是装配误差。我刚开始做VIO时,总觉得切向畸变影响不大,后来发现,在边缘区域,它带来的误差能到好几个像素。

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

// 径向畸变
// r^2 = x^2 + y^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是切向畸变系数。一共5个参数,我们叫它畸变参数D。

避坑指南:我曾经在标定时只用了k1和k2,忽略了k3。结果在图像边缘,去畸变后的点还是歪的。后来加上k3,问题就解决了。所以,如果镜头畸变比较大,建议用三个径向参数。

4.3 图像去畸变:把歪的掰正

去畸变,就是把畸变后的图像还原成理想针孔模型下的图像。怎么做呢?

核心思路是:对于目标图像中的每个像素,找到它在畸变图像中的对应位置,然后插值。注意,是反向映射,不是正向映射。为什么?因为正向映射会导致像素重叠或空洞。

具体步骤:

  1. 遍历目标图像的每个像素(u, v)
  2. 根据内参,反算到归一化平面(x, y)
  3. 用畸变模型计算畸变后的坐标(x_distorted, y_distorted)
  4. 再投影回像素坐标(u_distorted, v_distorted)
  5. 在畸变图像上插值,得到目标像素值

OpenCV里,一行代码就能搞定:

// OpenCV去畸变
cv::Mat map1, map2;
cv::initUndistortRectifyMap(K, D, cv::Mat(), K, image_size, CV_32FC1, map1, map2);
cv::remap(distorted_img, undistorted_img, map1, map2, cv::INTER_LINEAR);

这里我建议用initUndistortRectifyMap生成映射表,然后用remap去执行。因为映射表可以提前算好,后面每帧图像直接查表,效率高很多。

小技巧:如果你只需要去畸变后的特征点坐标,不需要整张图去畸变。直接对特征点坐标做去畸变计算就行,省时省力。

4.4 双目相机模型:两只眼睛看世界

双目相机,说白了就是两个相机固定在一起,模拟人的双眼。它的核心优势是:可以直接得到深度信息。

双目模型的关键是对极几何。两个相机之间存在一个旋转矩阵R和平移向量t。对于空间中的同一个点,它在左右相机中的投影点满足对极约束:

// 对极约束
// x2^T * E * x1 = 0
// 其中 E = t^ × R 是本征矩阵
// x1, x2 是归一化平面上的点

实际应用中,我们更关心的是视差。对于校正后的双目图像,左右图像的行是对齐的。那么,同一个点在左右图像中的列坐标差,就是视差d。深度Z和视差d的关系是:

// Z = f * B / d
// f: 焦距(像素单位)
// B: 基线长度(两个相机光心之间的距离)
// d: 视差(像素单位)

我在项目中遇到过,有人直接用原始的双目图像做匹配,结果匹配点不在同一行,视差算出来全是错的。记住,双目匹配前一定要做立体校正,让左右图像的行对齐。

关键点:双目相机的精度和基线长度成正比,和焦距成正比,和视差精度成反比。基线越长,测距越远,但视野重叠区域越小。这是个trade-off。

4.5 OpenCV基础操作:你离不开的工具箱

OpenCV是VIO开发中必不可少的工具。我整理了一些最常用的操作,你一定会用到。

图像读写与显示:

// 读取图像
cv::Mat img = cv::imread("image.jpg", cv::IMREAD_COLOR);

// 显示图像
cv::imshow("Window", img);
cv::waitKey(0);

// 保存图像
cv::imwrite("output.jpg", img);

图像预处理:

// 灰度化
cv::Mat gray;
cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY);

// 高斯滤波
cv::Mat blurred;
cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 1.5);

// 直方图均衡化
cv::Mat equalized;
cv::equalizeHist(gray, equalized);

特征点提取与匹配:

// ORB特征点
cv::Ptr<cv::ORB> orb = cv::ORB::create();
std::vector<cv::KeyPoint> keypoints;
cv::Mat descriptors;
orb->detectAndCompute(img, cv::Mat(), keypoints, descriptors);

// 特征匹配
cv::BFMatcher matcher(cv::NORM_HAMMING);
std::vector<cv::DMatch> matches;
matcher.match(descriptors1, descriptors2, matches);

相机标定:

// 标定板角点检测
cv::findChessboardCorners(img, board_size, corners);

// 相机标定
cv::calibrateCamera(object_points, image_points, 
                    image_size, K, D, rvecs, tvecs);

个人经验:我建议你建立一个OpenCV常用函数速查表。比如图像类型转换、坐标系变换、矩阵操作这些,写多了自然就熟了。刚开始可以写个工具类,把常用操作封装起来。

4.6 本章知识体系

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

第四章:相机模型与图像处理知识体系 相机模型 针孔相机模型 畸变模型 双目相机模型 内参矩阵K 投影公式 坐标系变换 齐次坐标 径向畸变 切向畸变 畸变参数D 去畸变 对极几何 视差计算 立体校正 深度估计 OpenCV基础操作 图像读写 图像预处理 特征提取 特征匹配 相机标定

这张图把本章的核心内容串起来了。从相机模型出发,到畸变处理,再到双目视觉,最后用OpenCV落地。每一步都是VIO系统中不可或缺的环节。

好了,这一章就到这里。记住,相机模型是VIO的基石,一定要吃透。下一章我们聊聊IMU模型和预积分,那是另一个重要的传感器。


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