ARTICLE DETAIL

资讯详情

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

OpenCV双目视觉测距实战:从相机标定到SGBM深度映射

OpenCV双目视觉测距实战:从相机标定到SGBM深度映射 简介一套使用Python与OpenCV开发的普通相机图像测距系统项目面向计算机视觉初学者及立体成像技术开发者。系统围绕单目相机内参标定、双目相机外参校正、特征匹配、视差计算和深度测量展开覆盖从标定板拍摄到距离输出的完整技术实现链路。压缩包共41个文件包括26张标定与左右视图图像、6张处理结果图、4个Python脚本、2个txt说明文件以及docx附赠文档整体约2.35MB目录结构按图像、代码和结果划分便于对照学习。目前已有179人学习下载。代码中的校准脚本、主流程脚本和说明文档可帮助快速复现测距实验也能作为课程设计或项目预研的参考通过阅读源码可深入掌握相机标定、视差图与深度图生成原理支持进一步扩展至工业检测、机器人导航等实际应用方向。1. 普通相机图像测距从两张照片到真实距离普通相机拍两张照片真能算出物体离镜头多远吗能前提不是靠单个“智能算法”一步到位而是老老实实完成三件事把相机内参标准、把两个相机的相对位置标标准、把左右图像的视差算准确。这套基于 Python 与 OpenCV 开发的普通相机图像测距系统正是把单目标定、双目标定与立体校正、SGBM 视差计算、深度映射这一整条链路打包成可直接运行的工程并附带 14 组左右图像样本用于复现验证。它适合正在做双目视觉课题、准备做视觉测量项目、或者手里只有普通 USB 相机却想实现测距的人。下面按实际跑工程的顺序拆解这套资源。2. 单目相机标定先把内参和畸变系数钉死2.1 为什么测距系统的第一关是单目标定图像测距的底层逻辑是三角测量同一个三维点被左右两个相机拍摄后投影到两张图像上的像素位置存在水平差这个差值叫视差。视差和距离成反比比例系数里包含两个关键量——相机焦距和两相机光心之间的距离基线。如果相机内参不准后面的视差计算、深度换算全都建立在一个错误的地基上。这里的“内参”指四个核心值fx、fy焦距在像素单位下的表示、cx、cy主点坐标外加镜头畸变系数 k1、k2、p1、p2。普通相机的出厂参数通常不精确镜头还带有径向畸变和切向畸变直接用理论值会导致远离画面中心的区域测量误差急剧放大。单目相机标定就是通过拍摄已知几何尺寸的棋盘格标定板反推出这组参数。另一个现实原因是成本。工业视觉里 HALCON 配合高精度标定板能拿到更高精度但硬件门槛高OpenCV 用普通打印棋盘格就能完成标定这对 Python 开发者和实验室场景是首选方案。这套资源里calibration.py做的就是这件事输入是left/或right/目录里的标定板照片输出是内参矩阵和畸变系数供下一步双目标定使用。2.2 跑通 calibration.py棋盘格图像怎么拍、参数怎么填打开工程里的calibration.py流程用一段带注释的代码可以概括import cv2 import numpy as np # 棋盘格内角点数量9x6 指内部交叉点不是格子数按你自己的标定板改 pattern_size (9, 6) # 单个格子的边长单位 mm必须与实际标定板一致 square_size 25.0 objpoints [] # 世界坐标系的角点坐标 imgpoints [] # 图像坐标系的角点坐标 # 固定 z0生成本轮标定板所有角点的三维坐标 objp np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32) objp[:, :2] np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) * square_size for path in image_paths: img cv2.imread(path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners( gray, pattern_size, flagscv2.CALIB_CB_ADAPTIVE_THRESH | cv2.CALIB_CB_FAST_CHECK ) if not ret: continue # 这张图找不到角点就跳过不中断 corners2 cv2.cornerSubPix( gray, corners, (5, 5), (-1, -1), (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.01) ) objpoints.append(objp) imgpoints.append(corners2) # 标定返回重投影误差 ret、内参 mtx、畸变 dist ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None ) print(重投影误差:, ret) print(内参矩阵:\n, mtx) print(畸变系数:\n, dist)几个参数在实战中有讲究。pattern_size必须是内角点数一个 10x7 格子的棋盘格内角点是 9x6填错findChessboardCorners永远返回 False。square_size决定了三维坐标的尺度这里写 25.0 表示每格 25mm如果你用 A4 纸打印标定板务必实际量一下格子边长后面双目深度输出的物理单位就由它决定。cv2.CALIB_CB_FAST_CHECK是一个快速预检 flag能先粗筛图像如果图片来源复杂建议保留对速度影响很小。cornerSubPix在整数角点附近做亚像素精化把角点坐标从像素级提升到亚像素级这一步跳过的话标定结果会明显变糙。实际拍摄时我一般会准备 15 到 20 张照片让标定板覆盖画面的四角和中心倾斜角度分散一些。只拍正对视线的照片会让 fx、fy、畸变参数彼此耦合解算不稳定这是新手最容易忽略的采集问题。2.3 重投影误差标定结果能不能用的第一道判断calibrateCamera返回的ret是重投影误差单位是像素代表“用标定出的参数把三维角点投影回图像和实际检测到的角点差了多少”。这个数怎么读基本是行内共识小于 0.3 像素说明标定质量优秀0.3 到 0.7 属于正常水平可以直接进入双目标定超过 1.0 就要怀疑角点检测有误或照片数量不达标。如果误差超标不要急着调参先做一件事把检测到的角点画到图像上用cv2.drawChessboardCorners逐张保存肉眼看有没有哪张图的角点标错位置。很多时候是一张低曝光或运动模糊的照片拖高了整体误差删掉重跑就能改善。单目标定阶段要顺手记录两个输出内参矩阵 K1、K2 和畸变系数 d1、d2。它们会被读入stereo_calibration.py作为双目标定的初始值和固定值使用。这套资源里左右相机各有一组标定图像单目标定建议对左侧和右侧分别做一遍因为在普通双目装置里两个镜头即使型号相同个体差异也可能导致 fx 相差几十个像素。3. 双目相机标定与立体校正让左右图像对齐到同一极线平面3.1 stereoCalibrate求两台相机的相对位置和姿态单目标定解决的是“每个相机自己怎么投影”的问题双目测距还要知道“两个相机之间是怎么摆放的”。stereoCalibrate做的就是这件事输出旋转矩阵 R 和平移向量 TR 描述右相机相对于左相机的姿态T 描述平移关系其中 T 的模长就是基线长度是深度公式里的核心参数。核心代码段如下# stereo_calibration.py 核心流程 import cv2 # 左右角点必须来自同一时刻拍摄的图像对 # object_points、left_points、right_points 由 findChessboardCorners 得到 ret, K1, d1, K2, d2, R, T, E, F cv2.stereoCalibrate( object_points, left_points, right_points, K1_init, d1_init, # 左相机单目标定结果 K2_init, d2_init, # 右相机单目标定结果 gray_size, # 图像分辨率 (width, height) flagscv2.CALIB_FIX_INTRINSIC ) print(旋转矩阵 R:\n, R) print(平移向量 T:\n, T) print(基线长度(mm):, (T[0] ** 2 T[1] ** 2 T[2] ** 2) ** 0.5)这里用CALIB_FIX_INTRINSIC表示内参固定为单目标定结果只优化外参。如果你的单目标定误差较大可以改用CALIB_USE_INTRINSIC_GUESS允许双目阶段微调内参代价是解算自由度增加需要更多图像对才不会过拟合。双目标定对图像对的要求比单目苛刻左右图必须在同一时刻拍摄场景不能有物体运动否则角点位置本身就不对应。这套资源里的left01.jpg到left14.jpg、right01.jpg到right14.jpg是按左右配对组织的文件名末尾相同数字就是一组跑脚本时按这个规则读取即可。立体校正有个前置条件两个相机要尽量避免一大一小或一正一斜的姿态差异。R 矩阵如果出现超过 30 度的旋转角说明装配太随意即使校正成功公共视野也会很小测距可用范围会受限。3.2 stereoRectify remap极线校正的实际操作双目标定得到外参后下一步是极线校正。所谓极线校正是把左右图像重投影到同一个虚拟平面上让同名点在左右图上出现在同一水平线上这样视差就从“二维搜索”降维成“水平一维搜索”计算量和误匹配率都大幅下降。# 立体校正输出校正映射表和 Q 矩阵 R1, R2, P1, P2, Q, validRoi1, validRoi2 cv2.stereoRectify( K1, d1, K2, d2, gray_size, R, T, alpha0.0 # 0 表示尽量裁剪黑边1 保留全部像素 ) # 生成映射表并重采样 map1x, map1y cv2.initUndistortRectifyMap(K1, d1, R1, P1, gray_size, cv2.CV_32FC1) map2x, map2y cv2.initUndistortRectifyMap(K2, d2, R2, P2, gray_size, cv2.CV_32FC1) rectified_left cv2.remap(left_img, map1x, map1y, cv2.INTER_LINEAR) rectified_right cv2.remap(right_img, map2x, map2y, cv2.INTER_LINEAR)alpha0.0会让校正后的图像去掉黑色无用区域视觉上更干净。但要注意裁掉黑边也意味着有效像素减少对测距来说更推荐alpha-1自动保留所有有效区域或者alpha0.2保留少量黑边避免过度裁剪。CV_32FC1映射表是浮点精度不要为了省内存改成整数格式否则重采样会引入像素偏移。校正结果怎么验证而不是“看完图觉得差不多”随手挑一个明显物体比如电线杆或桌角在左右校正图上分别点击同一点打印 y 坐标两个 y 值应该相等允许偏差在 1 到 2 像素内。这一步我用cv2.setMouseCallback写了个小工具专门验证极线对齐情况比肉眼看盲区可靠得多。3.3 Q 矩阵输出深度换算需要的最后一块拼图stereoRectify输出的 Q 是一个 4x4 投影矩阵它把校正后的齐次像素坐标转换成三维坐标。在使用 SGBM 得到视差图后通过cv2.reprojectImageTo3D可以直接得到每个像素的 x、y、z 坐标Q 矩阵的数学结构如下Q [[1, 0, 0, -cx], [0, 1, 0, -cy], [0, 0, 0, f], [0, 0, -1/Tx, (cx-cx_)/Tx]]公式里的 Tx 就是基线距离单位取决于标定时square_size写的是毫米还是厘米。如果你标定时填的是 25.0 且按毫米理解那么 Q 算出的 z 单位就是毫米。许多人在这一步翻车是因为标定板边长填了 2.5 而不是 25深度结果整体缩小十倍还不自知。Q 矩阵在工程里有两个用途一是直接给reprojectImageTo3D用二是在没有完整 Q 矩阵时手动换算。手动换算公式Z fx * 基线 / 视差中fx 必须是校正后 P 矩阵里的值而不是原始内参矩阵里的 fx二者在立体校正后会有细微差异。4. 视差计算与深度测量从 SGBM 参数到物理距离4.1 BM 与 SGBM为什么双目测距大多选 SGBM视差计算的核心是立体匹配。OpenCV 里有两个现成接口StereoBM和StereoSGBM。BM 走的局部块匹配路线速度快但对弱纹理区域非常敏感视差图容易产生条带状噪声SGBM 基于半全局匹配把多个方向的匹配代价聚合起来对噪声和光照差异的鲁棒性明显更好。这套资源在main.py里选 SGBM 是合理的。普通相机拍摄的场景往往包含大量弱纹理区域比如白墙、桌面、天空BM 在这些区域几乎无法输出有效视差SGBM 至少能靠邻域约束给出平滑估计。SGBM 的代价是计算量大单帧处理在 CPU 上可能耗时几百毫秒但测距应用通常不是视频流实时需求这个代价可以接受。如果你要部署到树莓派这类低算力设备再用 SGBM 会吃力那时候把StereoBM拿出来作为一个降级方案是常见做法。这也是工程思维先在 PC 上用 SGBM 验证算法链路再根据部署硬件决定是否降级。4.2 视差图生成StereoSGBM_create 的参数逐个过看过太多人在 SGBM 参数上“玄学调参”其实这几个参数是有明确职责的逐个说明# main.py 中生成视差图的核心段 import cv2 import numpy as np def compute_disparity(left, right): # 输入应为经过立体校正的彩色图 left_gray cv2.cvtColor(left, cv2.COLOR_BGR2GRAY) right_gray cv2.cvtColor(right, cv2.COLOR_BGR2GRAY) stereo cv2.StereoSGBM_create( minDisparity0, numDisparities128, # 视差搜索范围必须是 16 的倍数 blockSize11, # 匹配块尺寸必须是奇数 P18 * 3 * 11 ** 2, # 相邻像素视差变化 1 的惩罚 P232 * 3 * 11 ** 2, # 相邻像素视差变化 1 的惩罚 disp12MaxDiff1, # 左右一致性检查阈值 uniquenessRatio10, # 最佳匹配相比次佳的最小优势 speckleWindowSize100, # 视差点滤波窗口 speckleRange32, # 滤波窗口内允许的视差波动 modecv2.STEREO_SGBM_MODE_SGBM_3WAY ) # 输出是定点数除以 16 得到真实视差值 disparity stereo.compute(left_gray, right_gray).astype(np.float32) / 16.0 return disparitynumDisparities决定了能测量的最近距离。它越大搜索范围越广能计算的近距离物体越近但耗时和误匹配都增加。对 5 米以内的室内场景64 到 128 是常用区间如果想测 10 米外的物体视差只有几个像素再加大搜索范围意义也不大瓶颈在图像分辨率和基线长度。blockSize控制匹配块大小块越大对噪声越不敏感但会抹掉细节边缘而且必须为奇数。P1、P2是平滑惩罚项经验公式8 * 3 * blockSize**2和32 * 3 * blockSize**2是 OpenCV 文档推荐的基准值P2 通常设为 P1 的 4 到 5 倍。uniquenessRatio表示最佳匹配得分要比次佳高出一个百分比才被采纳值太大会导致大量像素没有有效视差值太小则会产生错误匹配。speckleWindowSize和speckleRange配合去散斑我习惯用 100 和 32对抑制视差图上的孤立噪点很有效。4.3 深度映射与可视化验证你的测距是否落在合理区间有了视差图深度映射有两条路手动公式和 Q 矩阵。手动公式适合调试阶段能直接看到参数影响# 深度映射视差 - 距离 # fx 取校正后投影矩阵 P1 的 P1[0,0]baseline 取平移向量 T 的模长单位与 square_size 一致 fx P1[0, 0] baseline (T[0] ** 2 T[1] ** 2 T[2] ** 2) ** 0.5 # 避免除零视差小于 1 像素视为无效 valid disparity 1.0 depth np.zeros_like(disparity) depth[valid] fx * baseline / disparity[valid]这里有个容易混淆的点公式中fx不是原始内参 (K1) 里的 fx而是立体校正后投影矩阵 P1 的 (P1[0,0])。校正过程中焦距发生了变化用错值会导致深度结果整体偏移百分之几这个误差在近距离测量中表现尤其明显。用 Q 矩阵的方式更直接OpenCV 把 2D 到 3D 的转换封装好了# 用 Q 矩阵直接把视差图转成三维点云坐标 points_3d cv2.reprojectImageTo3D(disparity, Q) # 取画面中心点的深度做冒烟测试 h, w disparity.shape center_depth points_3d[h // 2, w // 2, 2] print(f中心点距离: {center_depth:.1f} mm)视差图的直观验证把视差范围归一化到 0 到 255用cv2.applyColorMap转成伪彩色图。正常结果应该看到近处物体亮、远处暗物体边缘清晰。如果整幅图全是黑白噪点不要怀疑算法先回到 4.2 节逐一查参数。深度图里我还会叠加一个“有效像素占比”统计低于 30% 就说明参数或图像质量有问题这个习惯帮我排掉了不少问题。实测时找一面墙用卷尺量出距离把相机正对墙面采集然后读指定点的深度值。误差在 2% 以内属于正常水平超过 5% 优先检查标定板边长单位、fx 取值和基线长度这三个地方九成问题出在这里。5. 实跑避坑排查五个反复出现的翻车点5.1 环境层cv2 装上了却 import 失败现象pip install opencv-python执行成功但运行脚本时报ModuleNotFoundError: No module named cv2或者cv2.error: OpenCV(4.4.0)系列运行时错误。原因最常见的是 Python 环境不一致。电脑里装了 Anaconda、系统 Python、PyCharm 自带解释器pip 命令用的是 A 环境脚本解释器却是 B 环境包装到了别处。另一种可能是 conda 环境下用pip install opencv-python与已装的 opencv-contrib 版本冲突导致 import 时找不到符号。解决先确认解释器路径再安装我固定用如下命令组合python -m pip install opencv-python numpy python -c import cv2; print(cv2.__version__)python -m pip能保证 pip 和当前解释器属于同一个环境避免Command pip not found或装错位置的问题。输出 cv2 版本号后再用脚本实际读一张图测试不要只在import cv2通过后就往下走。如果你用 conda建议在目标环境里执行conda install opencv它能一并处理 numpy 依赖矩阵比混合 pip 安装省心。5.2 标定层棋盘格角点找不到或检错现象findChessboardCorners返回 False或者找到的角点位置你在图上画出来发现明显偏移重投影误差冲到 1.5 以上。原因棋盘格图像反光导致格子边缘被高光吞掉拍摄角度过大导致棋盘格变形严重图片分辨率太低或者pattern_size填错了。还有一个隐蔽原因图片是 JPEG 压缩率高的情况下格子边缘的锯齿会干扰亚像素精化。解决把 flag 换成CALIB_CB_ADAPTIVE_THRESH | CALIB_CB_NORMALIZE_IMAGE | CALIB_CB_FAST_CHECK自适应阈值能抗光照不均。对找不到角点的图先降分辨率重试FAST_CHECK在缩小的图上更容易命中。另外用一个打印好的、贴在硬纸板上的棋盘格不要用屏幕显示屏幕反光会让你白忙一场。角点检错的排查手段是cv2.drawChessboardCorners把角点画在原图上逐张保存肉眼扫一遍。这一步花五分钟能避免后面反复重标定。5.3 立体校正后左右图仍不对齐现象在左右校正图上点同一个物体y 坐标差超过 2 像素极线没有水平对齐视差图因此出现系统性错误。原因双目标定输入的内参和畸变系数不准确或者左右图像对不是同一时刻拍的。普通 USB 相机做双目如果只靠软件触发两路图像采集时间差可能导致手势、树叶等轻微移动在角点检测处引入 1 像素级的错位。解决采集标定图像对时固定相机保证场景完全静止用硬触发或连续采集中时间戳最接近的两帧。软件层面检查stereoCalibrate传入的left_points和right_points是否严格一一对应漏检必须整对跳过不能把左图第 i 帧配右图第 j 帧哪怕它们看起来是同一姿态。5.4 视差图大面积空洞或黑白噪点现象SGBM 跑完视差图上大片黑色无效区域或用normalize后看起来像雪花噪点物体轮廓完全分辨不出。原因室内白墙、光滑桌面等弱纹理区域本身就没有可匹配的灰度特征左右相机曝光不同导致同一表面灰度差异过大SGBM 参数里uniquenessRatio太高、P1/P2太小使匹配结果不稳定。解决图像预处理是性价比最高的手段。对校正后的灰度图做cv2.equalizeHist或cv2.createCLAHE增强局部对比度再进入 SGBM。我常用的预处理组合是clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) left_gray clahe.apply(left_gray) right_gray clahe.apply(right_gray)同时把uniquenessRatio从 10 降到 5P1/P2按公式适当放大弱纹理区域会明显改善。要接受一个现实纯白或无纹理表面在双目视觉里天生无解再调参也救不回来这类场景需要用结构光或投影纹理。5.5 深度值跳变同一物体上不同点测距差异大现象对同一块平整木板画面不同位置测出来的深度最大差 20%甚至出现局部凸起或凹陷的伪影。原因视差图本身有噪声物体表面纹理弱导致视差匹配在几个候选值之间跳跃深度映射时未对视差做滤波还有numDisparities设置过小导致近距离物体视差被截断。解决先对视差图做中值滤波再映射深度这是最简单的止血方案disparity cv2.medianBlur(disparity.astype(np.uint8), 5).astype(np.float32)但中值滤波会抹掉边缘细节更好的做法是结合空洞填充和边缘保持滤波。我常用的流程是先用speckleFilter去散斑再对无效像素用最近有效视差插值最后做 3x3 中值平滑。时间序列上如果你的测距系统在采集视频流对连续帧的深度值做滑动平均或卡尔曼滤波能压掉大部分随机抖动代价是响应变慢。这个取舍在机器人避障场景里通常是值得的。6. 进阶把精度从“能测”拉到“能交付”这套资源跑通后我建议做三件事来验证和提升精度。第一做一次量具级对比实验找 0.5 米到 4 米的五个距离点用激光测距仪作为真值每个点采集 20 帧深度求平均画出误差曲线。误差呈现系统性偏移且比例稳定说明是参数标定问题误差随机跳动说明是视差匹配或滤波问题。这条曲线决定你能不能把方案交付给实际项目。第二如果场景里有已知尺寸的平面物体比如 A4 纸或标准货箱可以加一步后处理用边缘检测找到物体轮廓取轮廓内所有深度点的中值作为最终测量结果而不是单点读数。这一个技巧通常能把抖动误差压掉一半以上。第三思考单目能否补充你的应用。这套系统以双目为主但在机器人抓取等场景里结合solvePnP对已知尺寸的目标做单目位姿估计能让系统多一条腿走路。关键在于双目测距不依赖目标先验尺寸单目测距依赖两者互补使用比二选一更实用。部署方面如果目标是树莓派opencv安装是个门槛。建议直接用pip install opencv-python的 arm 版 wheel避免源码编译同时把分辨率降到 640x480迭代次数减半SGBM 换成 BM实测帧率能到 8 到 12 帧足够低速巡检场景使用。从那以后我每次启动双目测距项目都会强制走一遍“标定板实拍 → 重投影误差记录 → 极线对齐验证 → 量具对比”四步流程再往下写业务逻辑。这套资源的坑我都替你趟过一遍剩下的路按章节顺序走就行希望帮到你。本文还有配套的精品资源点击获取
返回列表