拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Kannala-Brandt鱼眼相机模型:原理、标定与工程实践全解析

1. 项目概述从“鱼眼”到“世界”在计算机视觉和机器人领域我们常常需要让机器“看见”并理解它周围的世界。普通的针孔相机模型就像我们手机的标准镜头视野有限看到的只是世界的一小片。而鱼眼相机则像给机器装上了一只“广角眼”甚至“全景眼”它能以接近甚至超过180度的超广视角捕捉场景这对于自动驾驶中的环视感知、机器人导航、VR/AR的全景拼接以及安防监控等场景至关重要。这个项目我们称之为“K8B鱼眼相机模型”其核心目标就是建立一套精确的数学模型来描述光线是如何通过鱼眼镜头最终在相机传感器CMOS/CCD上形成我们所看到的扭曲图像的。简单说就是为鱼眼镜头这种特殊的“眼睛”建立一份“视力报告”和“矫正处方”。只有拿到了这份精确的报告我们才能将鱼眼相机拍到的扭曲图像还原成符合我们人眼透视习惯的正常图像这个过程称为“去畸变”或“矫正”进而进行后续的立体视觉、三维重建、目标检测等高级任务。我接触过不少项目从车载环视系统到无人机全景巡检但凡涉及到超广角视觉鱼眼相机模型的精度直接决定了上层应用的成败。一个粗糙的模型会导致图像边缘矫正后依然扭曲测距误差巨大而一个精准的模型则能让机器在超广视野下依然保持“火眼金睛”。接下来我就结合多年的实战经验拆解K8B模型的核心并手把手带你走通从理论到代码实现的完整链路。2. 鱼眼相机模型的核心原理与选型逻辑为什么普通的针孔模型不适用于鱼眼镜头这得从光线传播路径说起。针孔模型假设所有光线都通过一个“点”光心直线传播到成像平面其成像高度与入射角呈正切tan关系。当视角超过90度时tan值会趋向无穷大这意味着成像点会跑到无限远物理上无法实现。而鱼眼镜头通过一系列复杂的光学镜片组有意地让光线产生严重的畸变使得超大视角的光线也能被“压缩”到有限的传感器尺寸内。其成像高度与入射角的关系通常用多项式来近似描述。在众多鱼眼畸变模型中Kannala-Brandt模型常简称为KB模型或K8B其中8可能指代其参数数量或版本是学术界和工业界广泛认可和采用的一种通用模型。它之所以能从Scaramuzza模型、Field of View模型等竞争对手中脱颖而出成为许多视觉库如OpenCV的fisheye模块的默认或推荐选项背后有深刻的考量。2.1 为何选择Kannala-Brandt模型首先KB模型是一个基于投影函数的模型它不依赖于具体的镜头物理设计细节而是用一个奇次多项式来直接描述入射角 θ光线与光轴的夹角与成像点距图像中心距离 r 之间的关系r(θ) θ k1 * θ^3 k2 * θ^5 k3 * θ^7 k4 * θ^9这里k1, k2, k3, k4就是我们需要标定的畸变参数。这种形式的优势在于物理意义清晰θ 是真实的几何量模型直接建模几何畸变易于理解和解释。对称性保证模型天然保证了径向畸变的对称性这对于大多数旋转对称的镜头是合理的假设。强大的拟合能力高阶奇次多项式可以非常灵活地拟合各种鱼眼镜头的非线性畸变曲线从轻微桶形畸变到严重的“门眼”畸变都能覆盖。数值稳定性好相比一些包含偶数次项或更复杂分式的模型KB模型在标定和反投影计算时更不容易出现数值问题。2.2 K8B模型参数全解析一个完整的K8B相机模型通常包含以下参数构成了相机的“内参”投影参数 (k1, k2, k3, k4)如上所述描述径向畸变。中心点 (cx, cy)图像中光轴与成像平面的交点即畸变中心。它不一定在图像的正中心工艺安装偏差会导致其有微小偏移。焦距 (fx, fy)分别表示x轴和y轴方向的焦距以像素为单位。由于传感器像素可能不是完美的正方形以及镜头组装误差fx和fy通常略有不同。可选切向畸变参数 (p1, p2)有些KB模型的实现会额外引入两个切向畸变参数以补偿镜头与传感器不平行造成的畸变。这就是“K8B”中“8”的常见来源fx, fy, cx, cy, k1, k2, k3, k4。若包含p1, p2则是10参数模型。注意在实际标定中尤其是对于高质量的鱼眼镜头切向畸变通常很小。是否引入需要根据标定板的残差和实际矫正效果来判断。盲目增加参数可能导致过拟合。2.3 与针孔畸变模型对比OpenCV的标准标定流程使用针孔模型叠加Brown-Conrady畸变模型含径向k1,k2,k3,k4,k5,k6和切向p1,p2。对于鱼眼镜头这种方法在边缘视野会失效因为其基础投影几何就不对。KB模型是更本质的建模。你可以这样类比针孔Brown模型是“先假设画面应该是直的再描述它怎么弯”而KB模型是“承认画面本来就是弯的并描述这种弯的规律”。显然后者对鱼眼更贴切。3. 标定实战获取你的相机“指纹”理论再好也需要落地。标定就是通过拍摄已知图案通常是棋盘格或圆点网格标定板来求解上述模型参数的过程。下面我以最常用的棋盘格标定板为例详细说明实操步骤和核心要点。3.1 硬件与数据准备标定板建议使用高精度打印的棋盘格例如10x7内角点。材质要硬挺平整粘贴在硬质背板上。棋盘格方块的实际物理尺寸如30mm必须精确测量并记录。拍摄环境光照均匀避免反光和阴影。标定板纹理要清晰。拍摄姿势手持或移动标定板围绕相机拍摄15-25张图像。要点是覆盖整个视野标定板要出现在图像的各个位置特别是四个角落和边缘区域这对鱼眼模型标定至关重要。多样姿态标定板要有不同程度的倾斜、旋转和平移。不同距离既有近景也有远景。清晰对焦确保标定板图像清晰角点不模糊。3.2 使用OpenCV进行标定代码详解OpenCV从3.0版本开始提供了cv::fisheye命名空间专门用于KB模型的标定。以下是核心代码步骤和解读。#include opencv2/opencv.hpp #include opencv2/calib3d.hpp #include iostream #include vector int main() { // 步骤1: 准备数据 std::vectorstd::vectorcv::Point3f objectPoints; // 世界坐标系中的3D点 std::vectorstd::vectorcv::Point2f imagePoints; // 图像坐标系中的2D点 cv::Size imageSize; // 图像尺寸 cv::Size boardSize(9, 6); // 棋盘格内角点数量 (width, height) float squareSize 30.0f; // 棋盘格方块实际大小毫米 // 生成标定板角点的世界坐标 (Z0) std::vectorcv::Point3f obj; for (int i 0; i boardSize.height; i) for (int j 0; j boardSize.width; j) obj.push_back(cv::Point3f(j * squareSize, i * squareSize, 0)); // 步骤2: 遍历所有标定图像提取角点 std::vectorcv::String filenames; cv::glob(calibration_images/*.jpg, filenames); for (const auto filename : filenames) { cv::Mat img cv::imread(filename, cv::IMREAD_GRAYSCALE); if (img.empty()) continue; imageSize img.size(); std::vectorcv::Point2f corners; bool found cv::findChessboardCorners(img, boardSize, corners); if (found) { // 亚像素级角点精确化提升标定精度 cv::cornerSubPix(img, corners, cv::Size(11, 11), cv::Size(-1, -1), cv::TermCriteria(cv::TermCriteria::EPS cv::TermCriteria::MAX_ITER, 30, 0.1)); imagePoints.push_back(corners); objectPoints.push_back(obj); // 每张图对应的世界坐标是一样的 // 可视化可选 cv::drawChessboardCorners(img, boardSize, corners, found); cv::imshow(Corners, img); cv::waitKey(100); } } cv::destroyAllWindows(); // 步骤3: 执行鱼眼相机标定 cv::Mat K cv::Mat::eye(3, 3, CV_64F); // 内参矩阵 cv::Mat D; // 畸变参数矩阵 (k1, k2, k3, k4) std::vectorcv::Mat rvecs, tvecs; // 每张图的旋转和平移向量 int flags cv::fisheye::CALIB_RECOMPUTE_EXTRINSIC | cv::fisheye::CALIB_CHECK_COND | cv::fisheye::CALIB_FIX_SKEW; // CALIB_FIX_SKEW 假设传感器像素是矩形的即skew0对于现代相机通常成立可以简化模型。 double rms cv::fisheye::calibrate(objectPoints, imagePoints, imageSize, K, D, rvecs, tvecs, flags, cv::TermCriteria(cv::TermCriteria::EPS cv::TermCriteria::MAX_ITER, 100, 1e-6)); std::cout 标定完成重投影误差 (RMS): rms 像素 std::endl; std::cout 内参矩阵 K:\n K std::endl; std::cout 畸变系数 D (k1, k2, k3, k4):\n D std::endl; // 步骤4: 保存标定结果 cv::FileStorage fs(camera_params.yaml, cv::FileStorage::WRITE); fs K K D D image_size imageSize; fs.release(); return 0; }3.3 标定关键参数与技巧标定板选择圆点网格标定板如Charuco板的圆心提取精度可能比棋盘格角点更高尤其对于自动检测。但棋盘格更常见。确保标定板平整是关键。图像数量与质量至少需要10-15张有效图片。图像太少或姿态覆盖不全标定结果会不稳定特别是边缘区域的畸变参数。模糊或过曝/欠曝的图片坚决不用。flags参数详解CALIB_RECOMPUTE_EXTRINSIC每次迭代都重新计算外参通常建议开启。CALIB_CHECK_COND检查条件数有助于发现病态数据如标定板姿态太单一。CALIB_FIX_SKEW固定内参矩阵中的倾斜系数为0。对于绝大多数数字相机这是一个安全且合理的假设能减少一个待估参数提高稳定性。CALIB_FIX_K1...CALIB_FIX_K4可以固定某些高阶畸变系数。对于视角大于180度的鱼眼通常需要k1, k2, k3, k4四个参数全用上。对于视角较小的鱼眼可以尝试先固定k3, k4为0。重投影误差 (RMS)这是评价标定精度的核心指标。一般要求RMS误差小于0.5像素对于高清图像可放宽至1像素以内。如果误差过大需要检查角点提取是否准确、标定板是否平整、图像是否清晰、姿态是否多样。4. 图像矫正与三维反投影让扭曲的世界变直拿到标定好的K和D参数后我们就能做两件核心的事矫正图像去畸变和将二维图像点反投影到三维空间。4.1 鱼眼图像矫正去畸变矫正的目标是生成一张新的图像看起来像是用一个虚拟的理想针孔相机拍摄的。OpenCV提供了cv::fisheye::undistortImage函数但更常用且灵活的是initUndistortRectifyMap和remap的组合因为可以预先计算映射表对视频流实时处理更高效。// 步骤1: 计算矫正映射表 cv::Mat K_new K; // 可以调整K_new来改变矫正后图像的视野和缩放 // 如果想保持整个鱼眼视野通常需要扩大焦距否则矫正后图像四周会有黑边。 // K_new cv::getOptimalNewCameraMatrix(K, D, imageSize, 0.8, imageSize); // 0.8是缩放因子保留80%的视野 cv::Mat map1, map2; cv::fisheye::initUndistortRectifyMap(K, D, cv::Mat::eye(3,3,CV_64F), K_new, imageSize, CV_16SC2, map1, map2); // 步骤2: 实时矫正在视频循环中 cv::Mat distortedFrame, undistortedFrame; // ... 从相机捕获 distortedFrame ... cv::remap(distortedFrame, undistortedFrame, map1, map2, cv::INTER_LINEAR, cv::BORDER_CONSTANT);实操心得getOptimalNewCameraMatrix可以帮助自动计算一个新的内参以最大化保留有效像素区域减少黑边。但代价是矫正后的图像看起来像被“放大”了边缘的物体分辨率会下降。在自动驾驶环视拼接中为了后续的鸟瞰图变换有时会选择保留黑边确保矫正几何的一致性。4.2 三维反投影从2D像素到3D方向向量这是许多视觉应用如SLAM、三维测量的关键一步。对于针孔模型反投影得到的是一个三维射线。对于鱼眼KB模型反投影得到的是一个单位方向向量表示该像素对应的入射光线的方向。// 已知一个像素点 (u, v)求其对应的单位方向向量 (x, y, z) 在相机坐标系下 cv::Point2f pixel(u, v); cv::Matx31f direction_vec; // 方法使用 cv::fisheye::undistortPoints std::vectorcv::Point2f distorted_points {pixel}; std::vectorcv::Point2f undistorted_points; // 这里得到的是归一化平面上的点 (x, y)对应的是去除了畸变的投影点但还不是3D方向。 cv::fisheye::undistortPoints(distorted_points, undistorted_points, K, D); // 对于KB模型undistortPoints 返回的点 (x, y) 满足r sqrt(x^2 y^2), θ atan(r) // 对应的单位方向向量为[x, y, 1] / sqrt(x^2 y^2 1) 吗 不对 // 实际上KB模型的 undistortPoints 输出的是在 **单位球面** 上的投影点坐标 (x_s, y_s, z_s)其中 z_s cos(θ)。 // 更准确地说它返回的是 (x_s, y_s)。我们需要根据模型计算 z_s。 float x_s undistorted_points[0].x; float y_s undistorted_points[0].y; float r_s std::sqrt(x_s*x_s y_s*y_s); float theta std::atan(r_s); // 或者通过求解多项式 r(θ) 的反函数得到但atan近似对于小畸变可用。 float z_s std::cos(theta); // 单位方向向量 direction_vec(0) x_s; direction_vec(1) y_s; direction_vec(2) z_s; // 需要归一化 direction_vec direction_vec / cv::norm(direction_vec);4.3 矫正效果评估与可视化标定完成后不能只看RMS误差。一定要进行视觉评估直线检验拍摄一张有明显直线的场景如建筑墙角、桌面边缘的鱼眼图矫正后观察直线是否变直。这是最直观的检验方法。重投影误差图将标定板角点的重投影点用标定参数重新投影计算得到的图像点与检测到的角点画在同一张图上观察偏差向量的方向和大小。理想情况下所有偏差应随机分布且很小。多位置检验确保图像中心、边缘、四个角落的矫正效果都良好。边缘区域的矫正质量是鱼眼标定成败的关键。5. 工程化实践常见陷阱与性能优化在实际项目中仅仅跑通标定demo是远远不够的。下面分享几个踩过坑才总结出的经验。5.1 标定失败或精度差的常见原因标定板提取失败图像模糊、光照不均、标定板占画面比太小、棋盘格被遮挡。解决方案优化光照确保标定板清晰、完整、占据图像足够大区域建议大于1/4。角点提取顺序不一致findChessboardCorners有时会以不同的起点或方向排列角点顺序导致与预设的世界坐标顺序不匹配。解决方案在提取角点后手动或通过寻找最大四边形等方法统一角点的起始点通常是左上角。模型欠拟合或过拟合欠拟合RMS误差大矫正后直线仍弯曲可能是标定板姿态覆盖不全特别是缺少边缘和倾斜姿态或使用的畸变参数阶数不够如只用了k1,k2但镜头畸变很复杂。过拟合RMS误差很小但矫正后图像出现波浪形扭曲可能是使用了过高阶的畸变参数如对轻度鱼眼用了k3,k4或者标定板图像数量太少模型“记住”了噪声。解决方案增加多样化的标定图像数量尝试固定高阶参数k3,k4使用交叉验证留出部分图像不参与标定仅用于验证。镜头对焦变化标定时光圈和对焦环一旦变动内参就会变绝对禁忌标定完成后切勿再调整镜头的对焦环。对于定焦镜头使用胶水或标记固定对焦环。对于自动对焦镜头在标定和后续使用中必须锁定对焦或设置为手动对焦于固定距离。5.2 实时矫正的性能优化鱼眼矫正的remap操作计算量较大尤其是高分辨率图像。在嵌入式平台如Jetson、树莓派或需要处理多路视频时必须优化。降分辨率处理如果后续算法如目标检测不需要全分辨率可以先对原图下采样再计算映射表和矫正大幅减少计算量。查找表LUT优化initUndistortRectifyMap生成的map1和map2本身就是浮点型的映射表。可以将其转换为固定点如CV_16SC2来加速remap。更进一步对于分辨率固定的应用可以预先计算好LUT并存储在内存或文件中启动时直接加载。GPU加速使用OpenCV的CUDA模块 (cv::cuda::remap) 或OpenCL路径将矫正任务offload到GPU。ROI感兴趣区域矫正如果后续算法只关心图像中心区域例如前视自动驾驶可以只矫正该矩形区域而不是整张图。5.3 多相机系统标定对于环视系统4个或6个鱼眼相机除了每个相机的内参标定单目标定还需要进行外参标定多相机之间的相对位置关系。流程通常是分别标定每个相机K_i, D_i。将所有相机同时拍摄一个共有的、大型的标定场景标定板需要足够大或使用多个标定板。利用场景中的共同特征点通过Bundle Adjustment光束法平差联合优化所有相机的内参可固定和外参。OpenCV的stereoCalibrate可用于双目标定更多相机需要借助如Kalibr等专业工具或自行编写优化代码。5.4 参数存储与加载标定参数是相机的核心“指纹”需要妥善管理。建议使用YAML或JSON格式存储包含所有内参、图像尺寸、标定日期、镜头型号等信息。在程序中应设计一个统一的CameraModel类来封装加载参数、矫正图像、反投影等所有功能。class KannalaBrandtCameraModel { public: bool loadParams(const std::string yaml_path); cv::Mat undistortImage(const cv::Mat distorted) const; cv::Vec3f pixelToUnitRay(const cv::Point2f pixel) const; cv::Point2f projectPoint(const cv::Vec3f point_3d) const; // 三维点投影到图像 // ... 其他工具函数 private: cv::Mat K_, D_; cv::Size image_size_; cv::Mat map1_, map2_; // 缓存的矫正映射 bool maps_initialized_ false; };6. 超越标定板自标定与在线标定初探在某些极端情况下我们可能无法使用标定板如相机已安装在密闭设备中、或需要快速部署。这时可以考虑自标定技术。其核心思想是利用场景自身的几何信息如直线应被映射为直线、消失点约束、多视图之间的对应关系等。基于直线约束的方法从鱼眼图像中提取线段假设它们在真实世界中是直线通过优化畸变参数使得这些线段在矫正图像上尽可能直。这种方法对场景有一定要求但无需特定标定物。基于运动的方法SLAM中的在线标定在视觉SLAM系统中将相机内参包括鱼眼参数作为状态变量的一部分与相机位姿、地图点一同进行非线性优化。这需要足够的相机运动和场景纹理。ORB-SLAM3等现代SLAM系统已经支持鱼眼相机模型和在线标定。重要提示自标定和在线标定的精度和鲁棒性通常低于基于标定板的方法更适合作为初始值估计或参数微调。对于高精度测量应用强烈推荐使用物理标定板进行精细标定。最后我想强调的是鱼眼相机模型标定是一个“失之毫厘谬以千里”的工作。参数微小的误差在图像边缘会被急剧放大。在整个流程中数据质量标定板平整度、图像清晰度、姿态覆盖度永远是第一位的。花时间采集一套好的标定数据比后期调试算法参数要有效得多。当你拿到一组RMS误差小于0.3像素的标定参数并看到矫正后的图像横平竖直时那种成就感就是对工程师耐心和细致的最佳回报。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门