ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

鱼眼相机标定实战:棋盘格与内外参联合优化在自动驾驶中的应用

鱼眼相机标定实战:棋盘格与内外参联合优化在自动驾驶中的应用 简介面向计算机视觉与自动驾驶感知方向的开发者这份资源聚焦OpenCV C实现的立体鱼眼相机标定方案采用棋盘格模板完成内外参数联合优化可解决广角镜头桶形畸变与透视畸变交织、双相机协同标定困难等问题适合具备一定C与视觉基础的中高级学习者研究参考。压缩包共76个文件约9.65MB以58张左右相机棋盘格采集图像为主体另含cpp与h源码、CMakeLists构建脚本、README说明文档及若干备份文件覆盖从图像采集到参数计算、结果验证的完整流程。已有85人学习关注。方案通过多参数耦合优化模型同步求解内参、外参与畸变系数并设计边缘角点检测补偿机制在190°视场角下重投影误差可控制在0.3像素以内读者可据此理解非迭代初始值估计、畸变建模与并行计算等实现思路并迁移至车载环视或机器人导航等宽视场测量场景。1. 鱼眼标定这件事为什么棋盘格加联合优化才是自动驾驶的入场券做自动驾驶感知的同行大概率都经历过这个场景环视鱼眼相机装车后近场拼接图看着还行可一旦把目标检测框投影回自车坐标系十米外的锥桶位置就飘了半米。问题往往不在模型而在标定。鱼眼镜头为了拿到 180° 甚至 200° 的视场角引入了严重的桶形畸变普通针孔模型那套内参加径向切向畸变的组合根本压不住边缘的弯曲。OpenCV 里那套cv::fisheye命名空间下的标定接口配合棋盘格标定板是目前工业界最稳的落地路径。而所谓内外参数联合优化指的是不把外参当成标定完内参后再单独求的附属品而是让内参、外参、畸变系数在同一个优化问题里一起收敛。这套方法直接决定了你的自动驾驶视觉系统能不能在量产车上跑出可用的 BEV 鸟瞰图。这篇文章面向的是已经会用 C 写 OpenCV、但被鱼眼边缘精度折磨过的工程师我会把从标定板选型、数据采集、代码实现到联合优化参数配置的完整链路拆开讲中间那些翻车的坑也一并交代。2. 鱼眼模型与棋盘格标定从投影公式到 OpenCV 的 Kannala-Brandt 实现2.1 为什么针孔模型在鱼眼上必然失效普通相机标定用的是针孔投影加 Brown-Conrady 畸变模型径向畸变只取 k1、k2、k3 三个系数切向取 p1、p2。这套模型假设光线在像平面上的偏移是半径的多项式函数在视场角小于 120° 时拟合得很好。但鱼眼镜头的入射角 θ 可以到 90° 以上投影关系不再是简单的 r f·tanθ而是 r f·θ 或者更复杂的多项式。Kannala-Brandt 模型把 θ 的奇次多项式作为投影函数的核心r(θ) k1·θ k2·θ³ k3·θ⁵ k4·θ⁷这里的 k1 到 k4 就是鱼眼畸变系数OpenCV 的cv::fisheye::calibrate内部用的正是这个模型。如果你硬拿cv::calibrateCamera去标鱼眼边缘的角点重投影误差能到 5 个像素以上而鱼眼标定正常应该压在 0.5 像素以内。这个差距在自动驾驶的近场感知里就是能不能用的分界线。2.2 棋盘格标定板的选型与采集规范棋盘格标定板不是什么高科技但选错了直接导致标定失败。常见做法是买氧化铝或者玻璃基底的棋盘格尺寸根据你的工作距离定。对于环视鱼眼推荐 7×10 或者 9×12 的内角点布局方格边长 30mm 到 50mm 之间。太小了在鱼眼边缘成像后角点模糊太大了在近距离拍不全。采集时有个血泪经验不要只在一个距离拍。鱼眼标定的畸变系数需要覆盖整个视场角标定板必须出现在图像的四个角落和中心区域。我一般会拍 25 到 40 张分布策略是中心区域 5 张四个角落各 3 到 4 张边缘中间位置各 2 张不同倾斜角度再补几张。每张图里棋盘格必须完整可见不能有遮挡不能过曝。光照均匀性比你想的重要反光会导致角点检测偏移半个像素联合优化时这个误差会被放大。2.3 用 OpenCV C 跑通单目标定的最小代码下面这段代码是单目鱼眼标定的核心流程我把它拆成可复用的函数。注意cv::fisheye::calibrate的 flags 参数cv::fisheye::CALIB_RECOMPUTE_EXTRINSIC和cv::fisheye::CALIB_FIX_SKEW是常用的组合。#include opencv2/opencv.hpp #include opencv2/calib3d.hpp #include vector #include iostream int main() { // 棋盘格内角点数量注意是内角点不是方格数 cv::Size boardSize(9, 6); // 方格实际物理尺寸单位米 float squareSize 0.04f; std::vectorstd::vectorcv::Point3f objectPoints; std::vectorstd::vectorcv::Point2f imagePoints; // 生成棋盘格的世界坐标Z0平面 std::vectorcv::Point3f obj; for (int i 0; i boardSize.height; i) { for (int j 0; j boardSize.width; j) { obj.emplace_back(j * squareSize, i * squareSize, 0.0f); } } // 读取标定图片列表 std::vectorcv::String files; cv::glob(calib_images/*.jpg, files); for (const auto f : files) { cv::Mat img cv::imread(f); cv::Mat gray; cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY); std::vectorcv::Point2f corners; // 鱼眼图像边缘畸变大用亚像素角点检测前先做自适应阈值 bool found cv::findChessboardCorners(gray, boardSize, corners, cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE); if (found) { // 亚像素精化窗口大小对鱼眼边缘要适当放大 cv::cornerSubPix(gray, corners, cv::Size(7, 7), cv::Size(-1, -1), cv::TermCriteria(cv::TermCriteria::EPS cv::TermCriteria::MAX_ITER, 30, 0.001)); imagePoints.push_back(corners); objectPoints.push_back(obj); } } cv::Mat K, D; std::vectorcv::Vec3d rvecs, tvecs; // 鱼眼标定核心函数 int flags cv::fisheye::CALIB_RECOMPUTE_EXTRINSIC | cv::fisheye::CALIB_FIX_SKEW; cv::fisheye::calibrate(objectPoints, imagePoints, cv::Size(1920, 1080), K, D, rvecs, tvecs, flags, cv::TermCriteria(cv::TermCriteria::EPS cv::TermCriteria::MAX_ITER, 100, 1e-6)); std::cout 内参矩阵 K:\n K std::endl; std::cout 畸变系数 D:\n D std::endl; return 0; }这段代码里几个参数值得展开。boardSize是内角点数如果你买的是 10×7 的方格板内角点就是 9×6。squareSize必须和实际物理尺寸一致单位用米因为后续外参平移向量的单位会跟着走。cornerSubPix的搜索窗口我用了 7×7比常规的 5×5 大原因是鱼眼边缘的角点梯度更平缓窗口太小容易收敛到局部极值。cv::fisheye::calibrate的flags里CALIB_FIX_SKEW假设像素轴严格垂直现代工业相机这个假设基本成立固定它可以减少一个自由度让优化更稳。终止条件设到 1e-6 是为了让联合优化充分收敛代价是标定时间从几秒变成十几秒这个时间花得值。2.4 重投影误差怎么看才算合格标定完必须看重投影误差。cv::fisheye::calibrate返回的rvecs和tvecs是每张图的外参你可以用cv::fisheye::projectPoints把世界坐标重新投影回图像和检测到的角点算欧氏距离。合格线是平均误差小于 0.5 像素最大误差小于 1.5 像素。如果平均误差在 0.8 到 1.2 之间检查是不是有某几张图的角点检测偏了把误差最大的两张剔掉重新标。如果平均误差超过 2 像素大概率是棋盘格尺寸填错了或者标定板不平整。3. 内外参数联合优化把外参从附属品变成优化变量3.1 联合优化和分步标定的本质区别分步标定的逻辑是先用单目鱼眼标定求出内参 K 和畸变 D然后固定它们用cv::solvePnP对每张图单独求外参。问题在于单目标定时内参的优化目标是最小化所有图的角点重投影误差它并不知道你后续要用这些外参做什么。而联合优化把内参、畸变、所有图的外参放在一个大的最小二乘问题里目标函数是全局重投影误差。对于自动驾驶的多相机系统这意味着你可以把多个相机的外参约束比如环视相机之间的固定安装关系也加进去让整个系统的一致性更好。OpenCV 本身没有直接暴露一个“联合优化”的 API但你可以用cv::fisheye::calibrate得到初值然后用cv::fisheye::stereoCalibrate或者自己写一个基于ceres-solver的优化。对于双目或者多目的场景cv::fisheye::stereoCalibrate会同时优化两个相机的内参、畸变和它们之间的外参这就是联合优化的一种形式。3.2 用 stereoCalibrate 做双鱼眼联合标定的代码假设你有前后两个鱼眼相机想标定它们之间的旋转和平移。先各自单目标定拿到初值再跑联合优化。// 假设左右相机已经各自单目标定得到 K1, D1, K2, D2 // imagePoints1 和 imagePoints2 是同一时刻两个相机拍到的棋盘格角点 cv::Mat K1, D1, K2, D2; cv::Mat R, T, E, F; cv::Size imgSize(1920, 1080); // 联合优化同时优化内参、畸变、外参 int flags cv::fisheye::CALIB_FIX_INTRINSIC; // 如果内参已经标得很准固定它们 // 如果想联合优化内参去掉 CALIB_FIX_INTRINSIC加上 CALIB_USE_INTRINSIC_GUESS cv::fisheye::stereoCalibrate(objectPoints, imagePoints1, imagePoints2, K1, D1, K2, D2, imgSize, R, T, E, F, flags, cv::TermCriteria(cv::TermCriteria::EPS cv::TermCriteria::MAX_ITER, 100, 1e-6)); std::cout 旋转矩阵 R:\n R std::endl; std::cout 平移向量 T:\n T std::endl;这里的flags选择是个关键决策。如果你单目标定的重投影误差已经小于 0.3 像素用CALIB_FIX_INTRINSIC固定内参只优化外参收敛快且稳定。如果单目标定误差在 0.5 到 1 像素之间建议去掉CALIB_FIX_INTRINSIC加上CALIB_USE_INTRINSIC_GUESS让联合优化去微调内参。代价是优化变量变多需要更多的标定图对一般建议至少 20 对同步采集的图像。3.3 联合优化的参数配置与收敛判断联合优化的终止条件我一般设成MAX_ITER100EPS1e-6。但光看迭代次数不够你要看每次迭代的重投影误差下降曲线。如果误差在 20 次迭代后就平了说明初值给得好如果到 80 次还在缓慢下降可能是初值偏差大或者有某几张图的角点检测有系统偏差。另一个参数是cv::fisheye::CALIB_RECOMPUTE_EXTRINSIC它让每次迭代后重新计算外参初值对鱼眼这种非线性强的模型很有用。但如果你发现优化震荡可以去掉这个 flag让外参在初值附近微调。对于自动驾驶的多相机系统我通常会把所有相机的外参放在一个统一的优化框架里用ceres-solver自定义残差块。残差就是每个角点的重投影误差参数块包括每个相机的内参、畸变、以及相机之间的外参。这样做的额外好处是可以加入车辆运动约束比如相机安装位置在车体坐标系下的已知关系进一步压低边缘区域的误差。3.4 联合优化后的验证方法标定完不能只看重投影误差必须做实际场景验证。我常用的方法是在车前方 5 米、10 米、15 米处各放一个已知尺寸的标靶用标定参数把图像上的角点投影到地面坐标系看投影位置和实际测量位置的偏差。对于鱼眼相机10 米处的横向偏差应该小于 10 厘米15 米处小于 20 厘米。如果超了回去检查标定板的平整度和采集时的光照。4. 自动驾驶视觉系统里的鱼眼标定避坑清单4.1 角点检测在鱼眼边缘集体翻车现象标定板放在图像角落时cv::findChessboardCorners返回 false或者检测到的角点顺序错乱。原因鱼眼边缘的畸变导致棋盘格线条弯曲严重OpenCV 默认的角点检测器基于局部梯度弯曲的线条让梯度方向不一致检测器找不到稳定的角点响应。解决先对图像做去畸变再检测角点但去畸变需要内参这就成了鸡生蛋问题。实际做法是先用一个粗略的内参比如厂家给的标称值做一次去畸变在去畸变图上检测角点然后把角点坐标映射回原始畸变图用这些点做正式标定。或者换用cv::findChessboardCornersSB它对弯曲边缘的鲁棒性更好。4.2 标定板尺寸填错导致外参平移量整体缩放现象标定出来的平移向量 T 的模长和实际测量值差一个固定比例比如实际 1.2 米标定出来 0.8 米。原因squareSize填的是方格边长但有人填成了内角点间距或者单位用厘米但代码里按米算。解决拿一把卡尺量一下棋盘格的实际方格边长精确到 0.1 毫米。单位统一用米。标定完后用卷尺量一下两个相机之间的实际距离和 T 的模长对比偏差应该在 1% 以内。4.3 联合优化不收敛重投影误差震荡现象stereoCalibrate跑完重投影误差比单目标定时还大或者迭代过程中误差上下跳。原因初值太差或者标定图对里有某几张的角点检测有系统偏差联合优化被这些坏点带偏了。解决先用单目标定拿到内参和每张图的外参算每张图的重投影误差把误差最大的 10% 的图剔掉。然后用剩下的图重新单目标定再跑联合优化。如果还震荡检查flags里是不是同时用了CALIB_FIX_INTRINSIC和CALIB_USE_INTRINSIC_GUESS这两个是互斥的。4.4 温度变化导致标定参数漂移现象冬天标定的参数夏天跑的时候边缘误差变大。原因相机模组的镜头座和传感器用不同材料温度变化导致两者之间的相对位置微变内参和畸变系数跟着漂。解决在自动驾驶量产项目里标定参数不能一劳永逸。常见做法是在相机模组里加温度传感器建立温度-参数查找表运行时根据温度做补偿。如果硬件不支持至少要在高低温环境下各标一次看参数漂移量是否在可接受范围。4.5 同步采集没做好联合优化变成联合猜现象双鱼眼联合标定后用标定参数做双目深度估计近处还行远处深度值跳变。原因两个相机的图像不是同一时刻采集的标定板在运动导致角点对不齐。解决用硬件触发同步或者至少用软触发加时间戳对齐确保两个相机曝光时刻差小于 1 毫秒。采集标定图时让标定板静止别手持晃。5. 从标定到 BEV把鱼眼参数喂给自动驾驶感知的进阶技巧标定参数最终要落到 BEV 鸟瞰图生成或者目标检测的坐标变换里。这里有个容易被忽略的点cv::fisheye::undistortImage用的是全局去畸变对整张图做映射但自动驾驶的感知区域往往只关心地面附近。我一般会自己写一个查找表只对 ROI 区域做去畸变这样能省 60% 的计算量。具体做法是用cv::fisheye::initUndistortRectifyMap生成映射表但把输出图像的尺寸和 ROI 对齐只计算地面投影需要的像素。另一个技巧是联合优化时加入地面约束。在标定场景里棋盘格放在地面上它的世界坐标 Z0 是已知的。你可以把这个约束写进优化目标让外参的俯仰角和高度更准。代码上就是在残差里加一项棋盘格角点的世界坐标 Z 必须等于 0。这个约束对自动驾驶特别有用因为 BEV 对俯仰角误差极其敏感1 度的俯仰角误差在 10 米处就是 17 厘米的纵向偏移。验证标定参数是否适合 BEV我有个土办法在车四周地面上摆一圈锥桶用标定参数把锥桶在图像上的位置投影到 BEV 图看它们是不是形成一个正圆。如果圆变成了椭圆或者偏心说明外参的滚转或俯仰有问题。这个办法比看重投影误差直观得多。最后说个习惯每次标定完把 K、D、R、T 和对应的重投影误差、温度、标定日期存成一个 YAML 文件用 OpenCV 的cv::FileStorage读写。量产项目里参数版本管理比标定本身还重要出了问题要能回溯到是哪次标定、什么条件下标的。我吃过这个亏一次参数更新后 BEV 拼接错位查了两天才发现是有人手动改了一个畸变系数没记录。希望帮到你。本文还有配套的精品资源点击获取
返回列表