鱼眼图像校正:映射表生成、重映射、校正后图像裁剪与视角调整

好,我们进入第四章。前面几章我们把鱼眼畸变的数学模型和参数标定讲清楚了,现在终于到了「出图」的环节。说白了,前面算半天内参和畸变系数,最终目的就是把那张弯弯曲曲的鱼眼图拉直,变成人眼看着舒服的透视图像。

这一章我会把校正流程拆成四个步骤来讲:映射表生成 → 重映射 → 图像裁剪 → 视角调整。每一步都有坑,我当年第一次做的时候,出来的图不是缺角就是拉伸变形,折腾了好几天才理顺。

核心思路一句话: 鱼眼校正的本质,就是为输出图像上的每个像素,找到它在输入鱼眼图像上的对应位置,然后插值填色。

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 参数我通常传单位矩阵,表示不做额外旋转。如果你需要调整相机朝向,可以传一个旋转矩阵进去,后面会讲到。

我的习惯: map_x 和 map_y 用 CV_32FC1 类型,精度够用,内存也省。别用 CV_64F,除非你精度要求极其变态。

4.2 重映射:把鱼眼图「拉直」

映射表生成之后,重映射就简单了。OpenCV 的 cv::remap 函数一行搞定:

cv::Mat undistorted;
cv::remap(distorted, undistorted, map_x, map_y, cv::INTER_LINEAR);

插值方式我推荐 INTER_LINEAR(双线性插值),速度和画质平衡得最好。如果你对边缘锯齿特别敏感,可以试试 INTER_CUBIC,但速度会慢一些。

嗯,这里要注意:重映射后的图像边缘通常会有黑色区域。因为鱼眼图的视角范围是圆形,校正后圆形区域外的像素没有对应点,默认填 0(黑色)。这就是我们下一步要处理的问题。

我曾经踩过的坑: 有一次我直接用默认参数 remap,没检查 map_x 里有没有 NaN 值。结果某些像素映射到了鱼眼图的有效区域外,remap 直接崩了。建议生成映射表后,先检查一下 map_x 和 map_y 的数值范围是否合理。

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);
避坑指南: 我曾经直接用 boundingRect 的结果裁剪,结果边缘有一圈灰边(像素值接近 0 但不等于 0)。后来加了一个阈值判断,只保留像素值大于 5 的区域,问题就解决了。

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 甚至更低。

经验值: 对于 180° 鱼眼镜头,scale=0.85 时校正效果最自然,既保留了宽阔感,又不会让直线弯曲得太明显。

下面这张图展示了整个校正流程的脉络:

鱼眼图像校正完整流程 鱼眼原始图像 步骤1:生成映射表 (initUndistortRectifyMap) 步骤2:重映射 (remap) 步骤3:裁剪与视角调整 校正后图像 关键输入 内参矩阵 K 畸变系数 D 输出尺寸 + 缩放因子 输出产物 map_x (CV_32FC1) map_y (CV_32FC1) 校正后图像

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);
性能提示: 如果是在嵌入式设备上跑,建议把 map_x 和 map_y 合并成一张双通道图(CV_32FC2),remap 时用 cv::INTER_LINEAR_EXACT,速度能快 10%~15%。

好了,这一章的内容就这些。映射表生成、重映射、裁剪、视角调整——四个步骤环环相扣,缺一不可。你可以在自己的数据集上试试不同的 scale 值,看看效果差异。实践出真知,调几次参数你就找到感觉了。