SensorsCalibration 环绕视图鱼眼相机自动标定(auto_calib_fisheye)实战指南
SensorsCalibration 环绕视图鱼眼相机自动标定(auto_calib_fisheye)实战指南
auto_calib_fisheye 是 SensorsCalibration 开源多传感器标定工具箱中针对环绕视图(Surround View,即 AVM 全景环视)鱼眼相机的自动标定模块,它基于四路鱼眼相机(前/后/左/右)的重叠区域纹理与光度一致性,自动恢复各相机相对地面(BEV 鸟瞰坐标系)的外参。本文将从编译运行、数据准备、整体流水线到 BEV 投影、纹理提取、三阶段随机搜索与光度代价函数等核心算法,逐层拆解该模块的完整实现,帮助读者既能直接跑通示例、也能理解其标定原理并针对自身场景做参数定制。
标定效果对比
该模块在示例数据上以固定的前视相机为基准,自动优化左、右、后三路相机的 6-DOF 外参。标定前(before_all_calib.png)与标定后(after_all_calib.png)的环绕视图拼接质量对比是验证效果最直观的方式:
标定前后这两张 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"。
它的核心思路可以概括为三条:
- 以地面平面为公共坐标系:四路鱼眼相机分别投影到地面 BEV(鸟瞰图)坐标系,形成统一尺寸(默认 1000×1000)的俯视图;
- 利用相邻相机重叠区的地面纹理:相邻两路相机的 BEV 公共可视区域包含了同一块地面纹理,是天然的匹配依据;
- 以光度误差为代价:将一个相机的 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};
- Eigen3:通过
- 产物结构:
- 所有
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/)。运行前需要满足两项前置条件:
- 输入图像:程序启动时直接读取四路鱼眼图像(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/ 并同步修改初始化参数(见下文)。
- 运行目录:可执行文件使用相对路径读写
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 负责把相机图像投影到地面平面:
- 生成地面网格点
p_G(尺寸rows×cols,即 BEV 分辨率); - 用地面内参
K_G与相机高度height还原为地面 3D 点:P_G = K_G.inverse() * p_G * height(fisheye 模式下 z 置零); - 经外参
T_CG(ground→camera)变换到相机坐标系; - 按相机模型畸变投影(fisheye/Ocam/pinhole 三种分支),得到像素坐标表;
- 通过
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:
- 公共视图二值化(
Binarization,L37-L54):逐像素判断两张图对应位置是否都非黑色,都有效则置 255,否则置 0,得到公共可视区域掩膜; - 形态学去噪与轮廓提取(
findcontours,L56-L82):先膨胀再腐蚀(5×5 结构元、腐蚀迭代 2 次),findContours取最大外轮廓,fillContour填充轮廓内部得到稠密像素集; - 纹理筛选与下采样(
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)的工作流程:
- 取某一方向的纹理像素(如
fl_pixels_texture)及其对应的地面 3D 点(PG_fl); - 用候选外参
T把地面 3D 点变换到目标相机坐标系,归一化后按相机模型投影到目标图像(pG2C); - 对每个像素计算损失并累加:
loss += pow(fabs(getPixelValue(&img1, x, y) / ncoef - getPixelValue(&img2, x1, y1)), 2)
其中 img1 是源 BEV 灰度图、img2 是目标相机灰度图,ncoef 为曝光补偿系数,getPixelValue 使用双线性插值采样(optimizer.cpp);
- 若像素投影到目标图外(
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 与每路及总标定耗时。
参数自定义与扩展建议
从源码结构看,将该工具迁移到自有车辆数据时,主要需要修改以下位置:
- 输入图像:替换 imgs 下的四张图像,保持
Front.jpg/Back.jpg/Left.jpg/Right.jpg命名; - 内参与畸变:在
initializeK/initializeD(optimizer.cpp)中替换为自有相机的标定内参和畸变系数;若使用不同鱼眼模型(Ocam)或针孔相机,可切换camera_model为 1 或 2; - 外参初值与安装误差:在
initializePose中按实际安装位姿设置T_*G,并视需要调整叠加的扰动幅度(模拟不同安装精度); - BEV 参数:按地面视野需求调整
bev_rows/bev_cols(分辨率)与K_G的 0.15 m/像素比例,同时更新相机高度与尾尺寸sizef/sizel/sizeb/sizer; - 搜索强度:
coarse_search_flag控制是否执行 100000 次迭代的粗搜索(时间更短可置 0);各阶段迭代次数与线程数集中在CameraOptimization中,可按算力调整; - 曝光处理:
extractor的exposure_flag控制是否启用灰度均值曝光补偿;edge_flag控制是否过滤重叠区边缘点,纹理差异大时可开启(示例中 bl/br 方向即开启了edge_flag=1)。
总体而言,auto_calib_fisheye 提供了一条无需标定板、完全基于地面纹理光度一致性的环绕鱼眼相机自动标定路径,其"固定一基准相机 + 逐路随机搜索 6-DOF"的框架清晰、可复现,适合作为量产 AVM 标定流程的参考实现。

