资讯动态

双目相机标定与立体匹配:从视差图到深度计算实战

发布时间:2026/9/13 2:43:56 来源:尧图企业网站定制
简介面向计算机视觉入门者与自动驾驶、3D重建等方向的开发者这套代码演示了从双目标定到立体匹配、视差图生成再到深度计算的完整链路内置相机内外参求解、棋盘格特征匹配、SAD/SGM等匹配方法以及三角测量深度换算等关键实现能帮助读者快速理解视差与深度反比关系等技术要点。资源包共8个文件以多个cpp源码文件为主附带Visual Studio工程与属性配置文件vcxproj、sln、props等可在VS环境下直接打开编译便于边读代码边跑通流程。工程整体仅17KB代码体量精简、结构清晰适合用于课程设计或项目预研参考。借助该示例读者可熟悉相机标定参数含义、立体匹配的代价计算流程以及由视差图计算三维坐标的三角测量公式从而建立从图像到深度信息的完整认知。已有1561人学习下载对于希望从零掌握双目视觉基础实现的学习者具有不错的借鉴价值。1. 双目标定后的立体匹配从双目图像到视差图很多刚开始做双目视觉的人以为双目标定跑完拿到旋转矩阵和平移向量就算完工实际上真正花时间的是后面立体匹配这关。双目标定只是把左右两台相机的相对位置关系固定下来告诉你同一个三维点在左右图里为什么会出现一左一右的偏移立体匹配则负责逐像素找出这个偏移量形成视差图再结合焦距、基线才能算出深度。视差图的边界是否干净、遮挡区噪点是否可控直接影响Z坐标精度。这套流程适合正在搭双目测距、3D重建、机器人抓取系统的工程师新手可以照着工程一步步复现熟手可以把注意力放在参数调优和剔除异常标定角点上。2. 相机标定原理与双目外参从单目标定到旋转矩阵、平移向量2.1 单目标定内参矩阵与畸变系数双目标定的前提是先把两个相机的内参标稳。单目标定要做的是估计焦距、主点坐标、畸变系数以及每张标定板图像对应的外参。这里说的畸变主要是径向畸变和切向畸变径向畸变来自镜头形状切向畸变来自镜头与成像平面不平行。一个典型做法是用棋盘格做靶标移动标定板采集 15 到 20 张不同角度和距离的图片通过findChessboardCorners提取角点再用cornerSubPix做亚像素细化。// objectPoints: 棋盘格角点的世界坐标假设Z0 // imagePoints: 对应角点在图像上的像素坐标 std::vectorstd::vectorcv::Point3f objectPoints; std::vectorstd::vectorcv::Point2f imagePoints; cv::Mat K, D; std::vectorcv::Mat rvecs, tvecs; double rms cv::calibrateCamera( objectPoints, imagePoints, imageSize, K, D, rvecs, tvecs, cv::CALIB_FIX_K3 | cv::CALIB_ZERO_TANGENT_DIST );objectPoints需要根据棋盘格实际格子尺寸生成单位是毫米imagePoints来自角点提取结果。代码里CALIB_FIX_K3表示不估计第三阶径向畸变CALIB_ZERO_TANGENT_DIST表示假设切向畸变为零这在小焦距工业镜头上通常是可接受的简化。K是 3x3 内参矩阵D是畸变系数返回的rms是重投影均方根误差一般小于 0.2 像素算理想工程上低于 0.5 像素就能继续往下走。内参矩阵 K 的形状是[fx, 0, cx; 0, fy, cy; 0, 0, 1]fx 和 fy 通常很接近但不要直接假设相等因为传感器像元宽高比不完全一致。标定完成后还要检查 cx 和 cy 是否落在图像中心附近如果偏离很大说明标定板图片覆盖不全或者某张图的角点提错了。建议把每张图的rms单独打印出来删掉误差特别大的图像对再重新标定。参数含义常见量级fx, fy以像素为单位的焦距几百到几千cx, cy主点坐标接近图像中心k1, k2径向畸变一阶、二阶10^-2 到 10^-4p1, p2切向畸变通常接近 0rms重投影均方根误差小于 0.3 像素为佳单目标定容易犯的错是把标定板只放在画面中央四周区域的图像没有覆盖到导致畸变参数外推不可靠。我一般会让标定板分别出现在四个角点和中心位置每个位置再倾斜 15 到 30 度这样内参约束更完整。注意标定板不要离镜头太近否则棋盘格角点可能超过光学系统分辨率角点亚像素定位会失效。2.2 双目标定R、T以及重投影误差单目标定完成后做双目标定。双目标定求解的是右相机相对左相机的旋转矩阵 R 和平移向量 T使得同一个三维点在左右相机坐标系下的投影关系满足对极几何约束。OpenCV 中对应函数是stereoCalibrate。它接收左右相机的单目标定内参和畸变系数把内参固定住只优化外参这样可以避免内外参同时迭代导致参数漂移。cv::Mat R, T, E, F; double stereoRms cv::stereoCalibrate( objectPoints, leftImgsPoints, rightImgsPoints, K1, D1, K2, D2, imageSize, R, T, E, F, cv::CALIB_FIX_INTRINSIC, cv::TermCriteria(cv::TermCriteria::COUNT cv::TermCriteria::EPS, 100, 1e-6) );objectPoints和左右图像角点必须一一对应数量不一致的图像对要在传入前剔除。CALIB_FIX_INTRINSIC是双目标定最常用的 flag表示使用单目标定得到的 K1、D1、K2、D2 不再修改只估计 R 和 T。E是本征矩阵F是基础矩阵它们在双目标定里用于校验极线约束后面立体校正也会用到。这里的stereoRms需要重点关注。双目标定重投影误差如果大于 1 像素多半是左右摄像头固定支架有微变形或者标定板太近导致视差过大匹配无法稳定落在同一物理点上。这时候不要盲目增加迭代次数先检查左右图像角点提取结果。有一个容易被忽略的点R 和 T 的物理单位。OpenCV 要求所有世界坐标都使用同一单位常用毫米。得到的 T 是在左相机坐标系下右相机光学中心的平移向量单位与标定板尺寸一致。深度计算公式里的基线 B 就是norm(T)单位必须统一不然深度算出来直接差一个单位量级。2.3 双目相机标定剔除不合格角点保证后续立体匹配稳定采集标定图像时角点会被噪声、运动模糊、反光、遮挡干扰。尤其是棋盘格部分出现在画面外时OpenCV 可能仍然返回 true但那些角点坐标已经到了图像边缘亚像素迭代根本不收敛。网上的现成工具通常只做findChessboardCorners不会帮你剔除不合格角点这会造成重投影误差值虚低但实际立体校正歪掉。我一般会先检查两件事。第一左右图像中检测到的角点数量是否等于棋盘格角点数(w-1)*(h-1)第二stereoCalibrate之后逐对图像计算重投影误差把误差超过阈值的图像对直接剔除再重新运行双目标定。std::vectorcv::Point2f corners; bool found cv::findChessboardCornersSB(img, boardSize, corners); // 左右图角点数必须一致且数量要与棋盘格角点数相同 if (!found || corners.size() ! (size_t)boardSize.width * boardSize.height) { // 直接丢弃这一对标定图 continue; }findChessboardCornersSB是findChessboardCorners的改进版在低纹理、运动模糊和光照不均时更稳定。找到角点后还要用cornerSubPix细化到亚像素。不合格角点最常见的表现是棋盘格区域被反光遮盖此时角点邻域灰度梯度分布异常细化后坐标会偏离真实物理位置。对于双目系统最少保留 10 到 15 对质量好的图像否则 R 和 T 的约束不够立体匹配阶段会出现行对齐误差。注意不要把CALIB_ZERO_TANGENT_DIST无条件用于所有镜头。切向畸变较大的模组如果要硬设为零深度计算结果会在图像边缘出现系统性偏差。先用完整畸变模型试再决定固定哪些参数。3. 立体匹配算法核心SAD、SGBM与视差图生成3.1 SAD/SSD区域匹配的代价计算立体匹配的第一步是找一个代价函数衡量左右图像两个像素块的相似程度。最直接的是 SAD也就是绝对误差和SSD 是平方误差和。两者都基于一个固定窗口内的灰度差累加窗口越大对弱纹理越鲁棒但深度边界会被平滑掉物体边缘出现“膨胀”效果。SAD 的代价计算可以理解为在左图像上取一个像素为中心设定一个窗口大小比如 11x11然后沿极线在右图像对应扫描行上搜索候选视差每次计算窗口内所有对应像素灰度差的绝对值之和得到匹配代价。视差搜索范围越大计算量越大所以实际使用时要根据物体深度范围设定最小视差和最大视差。这种局部匹配方法简单但对光照变化敏感。左右相机如果存在自动曝光差异SAD 的代价分布会整体偏移导致视差图上出现分片错误。工程上尽量避免左右相机使用自动曝光固定曝光时间和增益比调代价函数更有效。3.2 SGBM半全局匹配与视差约束SGM 的思路是在像素匹配代价基础上加入相邻视差的平滑约束。OpenCV 里的 SGBM 是对 SGM 的一种改进实现它不直接使用互信息而是用 Birchfield-Tomasi 代价计算量比传统 SGM 小很多同时保留了对弱纹理区域的路径聚合能力。SGBM 把图像从多个方向做一维动态规划每条路径都会累积代价最终把不同方向的代价相加。这样做的好处是能够改善低纹理区域的匹配稳定性代价是引入了更多参数。最关键的参数是P1和P2分别惩罚相邻像素之间 1 个像素的视差跳变和更大跳变。如果P2太小视差图会有很多横向条纹状噪声P2太大物体边界容易被抹平深度边缘不锐利。还有个参数是uniquenessRatio它要求最佳匹配代价明显优于第二佳否则标记为不确定这能有效抑制重复纹理区域出现的错误匹配。speckleWindowSize和speckleRange则用来过滤掉小块的孤立视差区域这些参数设了以后能去掉不少椒盐噪声。3.3 OpenCV 中生成视差图的代码与参数调节OpenCV 提供两种常用实现StereoBM和StereoSGBM。StereoBM基于归一化互相关速度最快适合实时性要求高但弱纹理较少的场景StereoSGBM是半全局匹配质量更高适合测量类系统。我是优先用 SGBM然后再根据帧率要求决定要不要换 BM。cv::Ptrcv::StereoSGBM sgbm cv::StereoSGBM::create( 0, // minDisparity最小视差 128, // numDisparities视差范围必须能被16整除 11, // blockSize匹配窗口大小必须为奇数 8 * 3 * 11 * 11, // P1相邻像素视差变化1时的惩罚 32 * 3 * 11 * 11, // P2相邻像素视差变化较大时的惩罚 1, // disp12MaxDiff左右一致性检查最大允许误差 63, // preFilterCap预滤波截断值 10, // uniquenessRatio唯一性比例 100, // speckleWindowSize过滤斑点窗口大小 32 // speckleRange斑点视差范围 ); cv::Mat disp16S, disp8U; sgbm-compute(leftRect, rightRect, disp16S); // disp16S是CV_16S低12位是视差值需要除以16得到真实像素视差 disp16S.convertTo(disp8U, CV_8U, 255.0 / (128 * 16));这段代码里numDisparities128表示最大搜索范围是 128 个像素。P1、P2的经验初始值通常按8 * number_of_image_channels * blockSize^2和32 * number_of_image_channels * blockSize^2设置如果左右图是灰度图通道数为 1。要注意的是disp16S里保存的是一个定点数真实视差是disp16S / 16所以后续计算深度时不要忘了除。参数作用推荐调节方向numDisparities视差搜索范围越大能测越近物体计算量越大blockSize匹配窗口太大边缘模糊太小弱纹理噪声多P1 / P2平滑惩罚项纹理复杂场景调大 P2 保持边缘uniquenessRatio唯一性比例值越大误匹配越少但会牺牲弱纹理覆盖disp12MaxDiff左右一致性检查阈值设为 1 或 2能有效去掉遮挡区误匹配speckleWindowSize孤立斑点过滤窗太大可能滤掉细小物体视差图生成后先用getValidDisparityMask只保留有效的视差像素再计算深度。不要直接拿disp8U做算法验证因为convertTo会改变数值范围很多人在这一步把单位搞错最后深度曲线看起来完全不对。4. 从视差图到深度三角测量、深度公式与三维坐标还原4.1 视差与深度的线性反比关系双目标定和立体匹配最终要落到距离计算。对于经过立体校正的左右图像两个相机光轴平行像平面共面此时同一个三维点在左右图像上的 y 坐标相同只有 x 坐标存在偏移这个偏移量就是视差 d。设左相机焦点出发的光线与右相机焦点出发的光线相交于物体点 P根据三角形相似可以得到Z f * B / d。公式里的 f 是左相机内参矩阵中的fx单位是像素B 是基线长度也就是双目标定得到的平移向量 T 的模单位是毫米d 是视差单位是像素。所以深度 Z 的单位与 B 保持一致。如果标定棋盘格定义的单位是毫米最后算出来的 Z 就是毫米。注意不要用反了物体距离越近视差越大距离越远视差趋近于零。所以在量化深度精度时远处目标的视差变化非常小哪怕一个像素的视差误差都会导致深度偏差明显变大。这是双目系统的物理特性不是算法能完全消除的。4.2 立体校正与重投影矩阵Q在生成视差图之前需要先做立体校正。双目标定得到 R 和 T 后使用stereoRectify计算左右相机的校正旋转矩阵R1、R2新的投影矩阵P1、P2以及重投影矩阵Q然后通过initUndistortRectifyMap生成映射表再用remap把原始图像校正成行对齐图像。cv::Mat R1, R2, P1, P2, Q; cv::stereoRectify( K1, D1, K2, D2, imageSize, R, T, R1, R2, P1, P2, Q, cv::CALIB_ZERO_DISPARITY, -1, imageSize ); cv::Mat mapL1, mapL2, mapR1, mapR2; cv::initUndistortRectifyMap(K1, D1, R1, P1, imageSize, CV_16SC2, mapL1, mapL2); cv::initUndistortRectifyMap(K2, D2, R2, P2, imageSize, CV_16SC2, mapR1, mapR2); cv::remap(leftRaw, leftRect, mapL1, mapL2, cv::INTER_LINEAR); cv::remap(rightRaw, rightRect, mapR1, mapR2, cv::INTER_LINEAR);CALIB_ZERO_DISPARITY会让主点经过校正后保持在同一像素坐标这样视差为零的点对应无穷远处。Q矩阵形如[1, 0, 0, -cx; 0, 1, 0, -cy; 0, 0, 0, f; 0, 0, -1/Tx, 0]最终可以把视差图转成三维点云。在执行立体匹配前一定要先输出校正后的左右图用鼠标点同一行上的一个特征点确认 y 坐标一致否则后续匹配代价全部算错。4.3 深度图后处理与三维坐标还原SGBM 输出的视差图通常包含无效视差和离群点。先用左右一致性检查去除遮挡区误匹配然后结合filterSpeckles去掉连通的小块噪声区域。这些后处理不是可选项深度测量系统里不处理后期点云会有大量飞点。// 去除孤立小区域 cv::filterSpeckles(disp16S, 0, 100, 32); // 使用Q矩阵生成三维点云coordType为true时以毫米为单位 cv::Mat points3D; cv::reprojectImageTo3D(disp16S, points3D, Q, true);reprojectImageTo3D会把每个有效视差像素转换成三维坐标结果保存在CV_32FC3的矩阵中。如果想要平滑深度图我对points3D中 z 通道做双边滤波而不是直接对视差图做模糊避免物体边缘被拉长。一个值得关注的点是filterSpeckles的第二个参数是newVal也就是无效值填充值。填 0 会让那些像素在后续计算时被视为无穷远或无效区域。对深度图做中值滤波也可以修复部分遮挡区但窗口不要超过 5x5否则细长物体会被完全抹掉。最终输出时建议同时保存视差图、深度图和三维点云方便复盘参数变化。5. 双目标定实战中的坑剔除不合格角点与参数调试顺序5.1 用重投影误差把关别只看标定板数量很多人以为采集了 50 对标定图就一定比 15 对更稳实际上一张角点提取错的图会把整组标定结果拉偏。我的做法是先在双目标定循环里逐对统计重投影误差剔除超过 0.5 像素的图像对如果剔完少于 12 对就重新采集不要将就。这里的重投影误差用projectPoints把标定板角点投影回图像坐标再计算与检测角点的欧氏距离。5.2 校验立体校正后的棋盘格是否严格对齐保存矩阵之后重新拿一张标定板放在两个相机视野中间用校正映射表处理后在左右校正图上画一条水平扫描线。如果棋盘格横线在左右图上没有对齐到同一 y 像素说明 R 或 T 有偏差视差图会整体出现行向偏移SGBM 再怎么调参数都弥补不了。这个手工检查比单纯看视差图更容易判断问题来源。5.3 把参数写进配置文件的落地手法参数调好以后不要每次启动都重新标定。用cv::FileStorage把内参、畸变系数、R、T、Q写入 yaml 文件再写一个加载函数。这样换机器或换分辨率时只需要重新生成映射表不用改动立体匹配主流程。cv::FileStorage fs(stereo_calib.yml, cv::FileStorage::WRITE); fs K1 K1; fs D1 D1; fs K2 K2; fs D2 D2; fs R R; fs T T; fs Q Q; fs.release();加载时注意文件路径不要写死成绝对路径配合 Visual Studio 工程属性里的工作目录使用相对路径不然换一台电脑运行就会找不到配置。真正落地时我会把加载配置、初始化映射表、生成视差图这三步分别封装成函数便于在调试时单独替换模块也方便后续接入 GPU 加速版本。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价