SensorsCalibration 环绕视图鱼眼相机自动标定(auto_calib_fisheye)实战指南

原创2026-10-15 20:40:471,882 阅读
文章标签:自动驾驶计算机视觉点云图像处理

SensorsCalibration 环绕视图鱼眼相机自动标定(auto_calib_fisheye)实战指南

auto_calib_fisheye 是 SensorsCalibration 开源多传感器标定工具箱中针对环绕视图(Surround View,即 AVM 全景环视)鱼眼相机的自动标定模块,它基于四路鱼眼相机(前/后/左/右)的重叠区域纹理与光度一致性,自动恢复各相机相对地面(BEV 鸟瞰坐标系)的外参。本文将从编译运行、数据准备、整体流水线到 BEV 投影、纹理提取、三阶段随机搜索与光度代价函数等核心算法,逐层拆解该模块的完整实现,帮助读者既能直接跑通示例、也能理解其标定原理并针对自身场景做参数定制。

标定效果对比

该模块在示例数据上以固定的前视相机为基准,自动优化左、右、后三路相机的 6-DOF 外参。标定前(before_all_calib.png)与标定后(after_all_calib.png)的环绕视图拼接质量对比是验证效果最直观的方式:

标定前的环绕视图 BEV 拼接结果(存在明显的接缝错位)

标定后的环绕视图 BEV 拼接结果(重叠区域纹理对齐,接缝明显改善)

标定前后这两张 1000×1000 的图片由程序自动生成:before_all_calib.png 由初始(含人为扰动误差)外参投影生成,after_all_calib.png 由优化后的外参投影生成,对应代码分别位于 calibration.cpp 与 Optimizer::SaveOptResult(optimizer.cpp)。

工具定位:基于 BEV 重叠区光度一致性的自动标定

该模块对应仓库目录 surround-camera/auto_calib_fisheye,官方 README 将其定位为 "an auto-calibration tool based on surround-view fisheye cameras"。

它的核心思路可以概括为三条:

  1. 以地面平面为公共坐标系:四路鱼眼相机分别投影到地面 BEV(鸟瞰图)坐标系,形成统一尺寸(默认 1000×1000)的俯视图;
  2. 利用相邻相机重叠区的地面纹理:相邻两路相机的 BEV 公共可视区域包含了同一块地面纹理,是天然的匹配依据;
  3. 以光度误差为代价:将一个相机的 BEV 纹理像素反投影到相邻相机图像上,比较灰度值差异,通过随机搜索最小化该误差,从而恢复精确外参。

从代码看,模块由三个类协作完成(见 include):

  • Optimizer(optimizer.h):负责内参/外参/畸变初始化、BEV 投影、代价函数计算、随机搜索与结果保存;
  • extractor(texture_extractor.h):负责从两张相邻 BEV 图中提取公共视图纹理像素;
  • TransformUtil(transform_util.h):提供欧拉角↔旋转矩阵、GetDeltaT 等刚体变换工具函数。

编译:依赖与构建命令

原文档给出的编译方式为:

mkdir -p build && cd build
cmake .. && make

从 CMakeLists.txt 可以看到完整的构建细节:

  • C++ 标准:-std=c++17 -pthread,并要求 Release 构建(SET(CMAKE_BUILD_TYPE "Release"));
  • 依赖项:
    • Eigen3:通过 include_directories("/usr/include/eigen3") 引用(需要系统已安装 Eigen,或调整该路径);
    • OpenCV:通过 find_package(OpenCV REQUIRED) 查找,并链接 ${OpenCV_LIBS};
  • 产物结构:
    • 所有 src/*.cpp 被编译为静态库 online_Calibration;
    • 入口 src/calibration.cpp 被编译为可执行文件 run_AVM_Calibration;
    • CMAKE_RUNTIME_OUTPUT_DIRECTORY 指定为 ${PROJECT_SOURCE_DIR}/bin,因此可执行文件生成在模块根目录下的 bin/ 文件夹中。

依赖不满足时,可优先检查 Eigen 头文件路径(/usr/include/eigen3)以及 OpenCV 的安装与 find_package 查找路径。

运行示例与数据准备

原文档的运行方式为:

cd ~./auto_calib/
./bin/run_AVM_Calibration

其中 cd ~./auto_calib/ 为示例路径,实际应从仓库中的 surround-camera/auto_calib_fisheye 目录进入(可执行文件位于其下的 bin/)。运行前需要满足两项前置条件:

  1. 输入图像:程序启动时直接读取四路鱼眼图像(calibration.cpp):
Mat imgf = cv::imread("./imgs/Front.jpg");
Mat imgl = cv::imread("./imgs/Left.jpg");
Mat imgb = cv::imread("./imgs/Back.jpg");
Mat imgr = cv::imread("./imgs/Right.jpg");

仓库自带示例数据位于 imgs(Front/Back/Left/Right 四张 1280×1080 鱼眼图),所以直接在模块根目录下执行即可复现;若要标定自己的车辆,需将同名四路图像放入 imgs/ 并同步修改初始化参数(见下文)。

  1. 运行目录:可执行文件使用相对路径读写 imgs/、输出 *.png 与 *.csv,因此应在 surround-camera/auto_calib_fisheye 目录下启动,避免路径解析失败。

程序运行时会依次打印初始化内参/外参矩阵、每轮优化的光度损失(luminosity loss)与耗时,并在结束时输出 total calibration time。同时会把中间结果(before_all_calib.png、after_all_calib.png、texture_*.png、fl.csv/fr.csv/bl.csv/br.csv 等)写到当前目录。

从 main 看整体标定流水线

calibration.cpp 的 main 函数完整串联了整条流水线,可分为六个阶段:

① 配置标定参数(L299-L320):

// camera_model:0-fisheye;1-Ocam;2-pinhole
int camera_model = 0;                 // 相机模型
int bev_rows = 1000, bev_cols = 1000; // BEV 分辨率
int coarse_search_flag = 1;           // 是否启用第 1 阶段粗搜索
string data_set = "fisheye";          // 数据集类型:fisheye / common
string fixed = "front";               // 固定相机:front / behind
Optimizer opt(&imgf, &imgl, &imgb, &imgr, camera_model, bev_rows, bev_cols,
              fixed, coarse_search_flag, data_set);
  • camera_model:0 走 fisheye::distortPoints,1 走自定义 Ocam 投影,2 走无畸变 pinhole 投影(optimizer.cpp);
  • data_set:决定内参、畸变、外参、相机高度、尾尺寸等初始化数值(initializeK/initializeD/initializePose/initializeKG/initializeHeight/initializetailsize);
  • fixed:选择固定哪路相机作为基准,其余三路依次优化。

② 生成标定前环绕视图(L322-L342):用初始外参把四路图像逐一 project_on_ground 投影到 BEV,再经 tail 处理、generate_surround_view 加权叠加,写出 before_all_calib.png。

③ 提取前/左、前/右重叠区纹理(L344-L387):构造两个 extractor(GF-GL、GF-GR),完成二值化、轮廓提取、纹理点采样,并把纹理像素反投影为地面 3D 点(PG_fl/PG_fr),同时计算曝光补偿系数 ncoef_fl/ncoef_fr。

④ 依次优化右、左相机(L389-L397):CameraOptimization(opt, "right") 与 CameraOptimization(opt, "left"),在固定前相机的约束下恢复右、左两路 6-DOF 外参。

⑤ 提取后/左、后/右重叠区纹理并优化后相机(L399-L451):此时左、右相机已更新,opt.imgl_bev、opt.imgr_bev 是优化后的 BEV 图,再分别与后相机 BEV 构造 extractor 提取 bl/br 纹理,最后执行 CameraOptimization(opt, "behind")。

⑥ 输出最终环绕视图(L453-L459):opt.SaveOptResult("./after_all_calib.png"),基于四路优化后的 BEV RGB 图重新生成全景俯视图。

值得说明的是,示例中 fixed == "front",因此优化顺序为 right → left → behind;代码路径同样支持 fixed == "behind" 的对称流程(此时最后优化前相机)。

核心算法原理

BEV 投影:project_on_ground

project_on_ground 负责把相机图像投影到地面平面:

  1. 生成地面网格点 p_G(尺寸 rows×cols,即 BEV 分辨率);
  2. 用地面内参 K_G 与相机高度 height 还原为地面 3D 点:P_G = K_G.inverse() * p_G * height(fisheye 模式下 z 置零);
  3. 经外参 T_CG(ground→camera)变换到相机坐标系;
  4. 按相机模型畸变投影(fisheye/Ocam/pinhole 三种分支),得到像素坐标表;
  5. 通过 remap 从原图采样生成 BEV 图。

其中地面内参 K_G 在 fisheye 数据集下定义为(optimizer.cpp):

K_G(0, 0) = 1 / 0.15;      // 每像素 0.15m
K_G(1, 1) = -1 / 0.15;
K_G(0, 2) = bcols / 2;     // 中心 (500, 500)
K_G(1, 2) = brows / 2;

即 BEV 的物理分辨率为 0.15 m/像素,图像中心对应地面原点。tail 函数(optimizer.cpp)负责把每路 BEV 裁剪到自身可视角范围并补黑边,确保四图尺寸一致、重叠区位置对齐。

纹理提取:公共视图 + 轮廓 + 采样

extractor 类在两张相邻 BEV 图上提取“公共视图”纹理,核心步骤见 texture_extractor.cpp:

  1. 公共视图二值化(Binarization,L37-L54):逐像素判断两张图对应位置是否都非黑色,都有效则置 255,否则置 0,得到公共可视区域掩膜;
  2. 形态学去噪与轮廓提取(findcontours,L56-L82):先膨胀再腐蚀(5×5 结构元、腐蚀迭代 2 次),findContours 取最大外轮廓,fillContour 填充轮廓内部得到稠密像素集;
  3. 纹理筛选与下采样(extrac_textures_and_save,L142-L206):
    • edge_flag=1 时,过滤距离相机中心过远(超出 size*4.5/5)的边缘点,规避拼接边界不可靠区域;
    • 计算像素梯度(与左、上邻域的灰度差),delta.norm() < 15 的弱纹理点被剔除;
    • 从剩余点中随机采样 500 个(down_sample = 500)作为优化用纹理像素,同时绘制并保存 texture_*.png。

光度代价函数:back_camera_and_compute_loss

优化的核心代价是重叠区纹理的光度一致性。back_camera_and_compute_loss(optimizer.cpp)的工作流程:

  1. 取某一方向的纹理像素(如 fl_pixels_texture)及其对应的地面 3D 点(PG_fl);
  2. 用候选外参 T 把地面 3D 点变换到目标相机坐标系,归一化后按相机模型投影到目标图像(pG2C);
  3. 对每个像素计算损失并累加:
loss += pow(fabs(getPixelValue(&img1, x, y) / ncoef - getPixelValue(&img2, x1, y1)), 2)

其中 img1 是源 BEV 灰度图、img2 是目标相机灰度图,ncoef 为曝光补偿系数,getPixelValue 使用双线性插值采样(optimizer.cpp);

  1. 若像素投影到目标图外(x1/x2 越界)且累计失败超过 30 个,直接返回 INT_MAX,把该候选外参判为无效。

CostFunction(L614-L651)按相机角色组装上述代价:优化右/左相机时,用前相机 BEV 与右/左相机灰度图比较(fr/fl);优化后相机时,则累加左、右两路 BEV 与后相机灰度图的损失(lb+rb)。

三阶段随机搜索:粗搜 → 精搜 → 微调

CameraOptimization(calibration.cpp)对每路相机执行三阶段搜索,全部基于随机采样 + 多线程并行:

阶段 迭代次数 线程数 角度搜索范围(roll/pitch/yaw,度) 平移搜索范围(tx/ty/tz,米)
1st 粗搜索(仅 coarse_flag=1 时执行) 100000 7 ±3(并拆分为正/负/全区间并行) ±0.01
2nd 精搜索 15000 6 ±1 或 ±0.5 ±0.005 ~ ±0.01
3rd 微调 8000 3 ±0.01 ±0.001 ~ ±0.002

每阶段结束后会输出该路相机的 luminosity loss 与最优外参,并保存中间可视化(after_*_calib1/2/3.png)。

random_search_params 与 fine_random_search_params(optimizer.cpp)实现搜索逻辑:

  • 变量为 6 个:roll, pitch, yaw, tx, ty, tz(varName[6]),角度单位为度、平移单位为米;
  • 粗搜用 uniform_int_distribution(乘 100 再除以 100,量化到 0.01° 精度),精搜用 uniform_real_distribution 连续采样;
  • 每个候选通过 TransformUtil::GetDeltaT 转成刚体变换增量,乘到外参上再算代价;
  • 用 std::mutex 保护各相机的最优值更新(mutexfront/mutexleft/mutexright/mutexbehind),多线程共享同一个最优值,实现并行随机搜索的收敛;
  • 每阶段搜索完成后,用最优外参重新 project_on_ground 生成对应相机的新 BEV 图,供后续阶段与其他相机使用。

fine_random_search_params 还支持跳过粗搜(coarse_flag=0):此时先把 extrinsic_*_opt 初始化为初始外参,直接进入精搜/微调阶段。

曝光补偿与灰度预处理

真实车载多相机存在曝光差异,直接比较灰度会产生系统性偏差。代码通过两个机制处理:

  • 提取纹理时,若 exposure_flag=1,则计算两图灰度均值的比值作为补偿系数:ncoef = mean(gray1) / mean(gray2)(texture_extractor.cpp);
  • 代价函数中用 img1 / ncoef 归一化后再比较(见上节公式);
  • 图像灰度化时使用 gray_gamma 做对比度拉伸(contrast=1.1, delta=30,optimizer.cpp),增强纹理对比度。

关键初始化参数速查(fisheye 数据集)

以下数值均来自 Optimizer 构造函数调用的各初始化函数(optimizer.cpp),是理解示例数据标定的基础:

相机内参 K(fx, fy, cx, cy):

相机 fx fy cx cy
Front 422.13163849 421.10340889 612.82890504 545.05656249
Left 420.60079305 418.94827188 650.54173853 527.27178143
Back 422.61569350 421.27373079 632.46019501 548.34673288
Right 421.64203585 420.26647020 640.09362064 529.05566315

鱼眼畸变参数 D(4 阶多项式系数):

相机 k1 k2 k3 k4
Front -0.07031853 0.00387505 -0.00333139 0.00056406
Left -0.0658382798 -0.00200728513 -0.000372535694 0.00000181851668
Back -0.06553861 -0.00094857 -0.00150748 0.000325
Right -0.07289752 0.01254629 -0.01300477 0.00361266

BEV 相关:分辨率 1000×1000;K_G 每像素 0.15 m;相机高度 hf=hl=hb=hr=1(米);尾尺寸 sizef=340, sizel=390, sizeb=380, sizer=390(像素)。

外参初始值与人为扰动:initializePose 为四路相机设置了 ground→camera 的初始外参 T_FG/T_LG/T_BG/T_RG(optimizer.cpp),并故意叠加了安装误差扰动以模拟真实标定场景,例如左相机叠加欧拉角扰动 (0.95°, 1.25°, -2.86°) 与平移 (0.0095, 0.0025, -0.0086),右相机叠加 (-2.95°, 1.25°, -2.8°) 与 (0.0065, -0.0075, 0.0095),后相机叠加 (-1.75°, 2.95°, -1.8°) 与 (-0.002, -0.0076, 0.0096)。标定即是从这些含误差的外参出发,通过搜索恢复精确值。

注意:data_set == "common" 时使用另一套针孔相机参数(内参 fx=fy≈390.43、无畸变、高度 5.1 m、BEV 尾尺寸 450/350),说明该模块设计上同时兼容普通(非鱼眼)环绕相机的数据。

输出与结果解读

程序的主要产物包括:

  • before_all_calib.png / after_all_calib.png:标定前后全景环绕视图,可直接目视检验重叠区接缝质量;
  • after_{left,right,behind}_calib{1,2,3}.png:每路相机三阶段搜索过程中的 BEV 融合可视化(由 Optimizer::show 用 addWeighted 加权生成);
  • texture_{fl,fr,bl,br}.png:提取的公共视图纹理点标注图;
  • fl.csv / fr.csv / bl.csv / br.csv:纹理像素坐标(x y 两列),由 extractor::writetocsv 写出,便于离线分析;
  • 控制台日志:包含初始/优化后外参矩阵、欧拉角、三阶段 luminosity loss 与每路及总标定耗时。

参数自定义与扩展建议

从源码结构看,将该工具迁移到自有车辆数据时,主要需要修改以下位置:

  1. 输入图像:替换 imgs 下的四张图像,保持 Front.jpg/Back.jpg/Left.jpg/Right.jpg 命名;
  2. 内参与畸变:在 initializeK / initializeD(optimizer.cpp)中替换为自有相机的标定内参和畸变系数;若使用不同鱼眼模型(Ocam)或针孔相机,可切换 camera_model 为 1 或 2;
  3. 外参初值与安装误差:在 initializePose 中按实际安装位姿设置 T_*G,并视需要调整叠加的扰动幅度(模拟不同安装精度);
  4. BEV 参数:按地面视野需求调整 bev_rows/bev_cols(分辨率)与 K_G 的 0.15 m/像素比例,同时更新相机高度与尾尺寸 sizef/sizel/sizeb/sizer;
  5. 搜索强度:coarse_search_flag 控制是否执行 100000 次迭代的粗搜索(时间更短可置 0);各阶段迭代次数与线程数集中在 CameraOptimization 中,可按算力调整;
  6. 曝光处理:extractor 的 exposure_flag 控制是否启用灰度均值曝光补偿;edge_flag 控制是否过滤重叠区边缘点,纹理差异大时可开启(示例中 bl/br 方向即开启了 edge_flag=1)。

总体而言,auto_calib_fisheye 提供了一条无需标定板、完全基于地面纹理光度一致性的环绕鱼眼相机自动标定路径,其"固定一基准相机 + 逐路随机搜索 6-DOF"的框架清晰、可复现,适合作为量产 AVM 标定流程的参考实现。

登录后查看全文
SensorsCalibration