鱼眼图像校正:映射表生成、重映射、校正后图像裁剪与视角调整
好,我们进入第四章。前面几章我们把鱼眼畸变的数学模型和参数标定讲清楚了,现在终于到了「出图」的环节。说白了,前面算半天内参和畸变系数,最终目的就是把那张弯弯曲曲的鱼眼图拉直,变成人眼看着舒服的透视图像。
这一章我会把校正流程拆成四个步骤来讲:映射表生成 → 重映射 → 图像裁剪 → 视角调整。每一步都有坑,我当年第一次做的时候,出来的图不是缺角就是拉伸变形,折腾了好几天才理顺。
4.1 映射表生成:从数学到像素的桥梁
校正的第一步,是生成一张「映射表」。这张表记录了输出图像每个像素点,对应到输入鱼眼图像的坐标。为什么要提前算好?因为重映射操作非常频繁,如果每帧都实时计算畸变公式,性能扛不住。提前算好存成两张浮点图(map_x, map_y),后面直接查表,效率高得多。
映射表生成的核心逻辑,我习惯用 OpenCV 的 cv::fisheye::initUndistortRectifyMap 来完成。它的输入就是我们之前标定得到的内参矩阵 K 和畸变系数 D,输出就是 map_x 和 map_y。
// C++ 示例
cv::Mat map_x, map_y;
cv::fisheye::initUndistortRectifyMap(
K, D, cv::Mat::eye(3,3,CV_64F), K,
cv::Size(output_width, output_height),
CV_32FC1, map_x, map_y
);
这里有个细节:R 参数我通常传单位矩阵,表示不做额外旋转。如果你需要调整相机朝向,可以传一个旋转矩阵进去,后面会讲到。
4.2 重映射:把鱼眼图「拉直」
映射表生成之后,重映射就简单了。OpenCV 的 cv::remap 函数一行搞定:
cv::Mat undistorted;
cv::remap(distorted, undistorted, map_x, map_y, cv::INTER_LINEAR);
插值方式我推荐 INTER_LINEAR(双线性插值),速度和画质平衡得最好。如果你对边缘锯齿特别敏感,可以试试 INTER_CUBIC,但速度会慢一些。
嗯,这里要注意:重映射后的图像边缘通常会有黑色区域。因为鱼眼图的视角范围是圆形,校正后圆形区域外的像素没有对应点,默认填 0(黑色)。这就是我们下一步要处理的问题。
4.3 校正后图像裁剪:去掉黑边,保留有效区域
校正后的图像,四周一圈黑边,看着很不专业。裁剪的目的就是去掉这些无效区域,同时尽可能保留有效画面。
裁剪策略有两种:
| 策略 | 方法 | 优点 | 缺点 |
|---|---|---|---|
| 内接矩形裁剪 | 取校正后图像中最大内接矩形 | 无黑边,画面完整 | 损失视角较大 |
| 外接矩形裁剪 | 取包含所有有效像素的最小矩形 | 保留最大视角 | 四角可能有少量黑边 |
| 自适应裁剪 | 根据有效像素分布动态计算裁剪框 | 平衡视角与黑边 | 实现稍复杂 |
我个人最常用的是自适应裁剪。具体做法是:遍历校正后图像,找到所有非零像素的行列范围,然后向外扩展几个像素作为安全边距。这样既去掉了大部分黑边,又不会切掉有效内容。
// 自适应裁剪示例(伪代码)
cv::Mat mask = undistorted > 0; // 有效像素掩膜
cv::Rect roi = cv::boundingRect(mask);
// 向外扩展 10 像素作为安全边距
roi.x = std::max(0, roi.x - 10);
roi.y = std::max(0, roi.y - 10);
roi.width = std::min(undistorted.cols - roi.x, roi.width + 20);
roi.height = std::min(undistorted.rows - roi.y, roi.height + 20);
cv::Mat cropped = undistorted(roi);
4.4 视角调整:用缩放因子控制视野
裁剪之后,图像尺寸变了,但视角还是固定的。有时候我们需要调整输出图像的视野范围——比如从 180° 鱼眼校正成 120° 的广角画面,或者只取中心区域做特写。
视角调整的核心参数是缩放因子 scale。在生成映射表时,我们可以通过修改内参矩阵 K 的焦距分量来控制输出视角:
// 调整视角:scale < 1 放大视野,scale > 1 缩小视野
double scale = 0.8; // 放大视野,看到更多边缘
cv::Mat new_K = K.clone();
new_K.at<double>(0,0) *= scale;
new_K.at<double>(1,1) *= scale;
cv::fisheye::initUndistortRectifyMap(
K, D, cv::Mat::eye(3,3,CV_64F), new_K,
cv::Size(output_width, output_height),
CV_32FC1, map_x, map_y
);
scale 的含义:
- scale < 1:等效焦距变小,视野变宽,能看到更多鱼眼边缘的内容,但边缘畸变残留会多一些。
- scale > 1:等效焦距变大,视野变窄,相当于只取中心区域,畸变校正更彻底,但损失了视角。
- scale = 1:保持原始焦距,校正后视角与标定时的视角一致。
我在项目中通常把 scale 设为 0.8~0.9,这样既能保留大部分视野,又不会让边缘畸变太明显。如果你做的是车载环视系统,可能更关心近处的盲区,scale 可以设到 0.7 甚至更低。
下面这张图展示了整个校正流程的脉络:
4.5 完整代码示例:一条龙校正
把上面所有步骤串起来,就是一个完整的校正函数。我习惯把它封装成一个类,方便在视频流中反复调用。
class FisheyeUndistorter {
public:
FisheyeUndistorter(cv::Mat K, cv::Mat D, cv::Size out_size, double scale=0.85) {
cv::Mat new_K = K.clone();
new_K.at<double>(0,0) *= scale;
new_K.at<double>(1,1) *= scale;
cv::fisheye::initUndistortRectifyMap(
K, D, cv::Mat::eye(3,3,CV_64F), new_K,
out_size, CV_32FC1, map_x_, map_y_
);
}
cv::Mat Undistort(const cv::Mat& src) {
cv::Mat dst;
cv::remap(src, dst, map_x_, map_y_, cv::INTER_LINEAR);
// 自适应裁剪
cv::Mat gray;
if (dst.channels() == 3) cv::cvtColor(dst, gray, cv::COLOR_BGR2GRAY);
else gray = dst.clone();
cv::Mat mask = gray > 5;
cv::Rect roi = cv::boundingRect(mask);
roi.x = std::max(0, roi.x - 5);
roi.y = std::max(0, roi.y - 5);
roi.width = std::min(dst.cols - roi.x, roi.width + 10);
roi.height = std::min(dst.rows - roi.y, roi.height + 10);
return dst(roi).clone();
}
private:
cv::Mat map_x_, map_y_;
};
使用时只需要:
FisheyeUndistorter und(K, D, cv::Size(1280, 960), 0.85);
cv::Mat result = und.Undistort(fisheye_frame);
cv::INTER_LINEAR_EXACT,速度能快 10%~15%。
好了,这一章的内容就这些。映射表生成、重映射、裁剪、视角调整——四个步骤环环相扣,缺一不可。你可以在自己的数据集上试试不同的 scale 值,看看效果差异。实践出真知,调几次参数你就找到感觉了。