简介:基于YOLOV5与双目相机实现三维测距的新版资源,面向自动驾驶、机器人导航、无人机避障等场景的开发者,解决目标检测与深度估计结合时实时性与精度不足的问题。压缩包共141个文件,约40.38MB,以yaml/yml配置、py/pyc源码、sh脚本、md文档为主,同时包含pt预训练权重、mp4演示视频、jpg样例和Dockerfile等,覆盖配置修改、模型调用、环境部署与结果演示各环节。目前已有2420人学习下载。内容除YOLOv5-6.1工程代码外,还提供模型权重、双目相机参数配置、图像对样例及Jupyter教程,便于直接复现双目标注、深度图生成与三维坐标计算流程;pyc缓存与docker配置可减少环境搭建成本。配合样例图片与视频,可对照输出结果验证目标框选、视差计算与真实距离估计效果,适合需要快速搭建双目测距原型的开发者和学习者。
1. 当 YOLOV5 只给像素坐标,距离从哪来:一个双目测距项目的完整拆解
之前做辅助驾驶预警项目时被单目测距坑过:YOLOV5识别行人和车辆很准,但距离全靠经验公式估,车一颠坡一变误差就飞到天上。换成双目相机做立体匹配之后,输出从"像素框"变成了"相机坐标系下的3D坐标",宽度、高度、距离全是实测算出来的,不用猜任何先验。这个项目就是一套YOLOV5检测+双目视觉测距的落地实现:左右两个USB摄像头采集图像,先标定内外参数做极线校正,再用SGBM立体匹配生成视差图,YOLOV5在左图上检测目标框,最后从视差图对应区域提取深度并反投影出3D位置。适合正在做机器人避障、ADAS预警、安防周界,以及任何"既要认得出目标、又要知道目标在哪"的开发者;新手可以直接照这套链路跑通,老手可以拿它的模块划分做二次开发。
2. 技术选型与整体架构:从双目视差到目标3D坐标的完整数据流
先交代清楚技术栈里每个角色干了什么,再讲数据是怎么一级一级传下去的。这套项目的核心依赖是 OpenCV、PyTorch 和 YOLOV5 检测模型。OpenCV 负责相机标定、去畸变、极线校正、SGBM 立体匹配这些计算机视觉底层的活;PyTorch 负责加载和运行 YOLOV5 模型做目标检测;最后的融合阶段把检测框和视差图对齐。模块之间的耦合度很低,每一级都可以单独替换——这也是我把标定、匹配、检测拆成独立工具模块的原因。
2.1 为什么最终选了双目而不是单目或结构光
单目测距最诱人的地方是省钱:一个摄像头就能跑,但代价是它测不出绝对深度。常见做法是"地面假设 + 目标真实高度"去反推距离,这在平直路面上勉强能用,可一旦目标部分遮挡、姿态变化或者相机俯仰角稍有变动,误差就会成倍放大。我之前那版单目方案在白天平路效果还行,傍晚逆光或者雨天就彻底没法用,属于典型的玄学调参,换个场景就得重新调一套参数。
双目测距的原理是三角测量:两个相机从不同位置看同一个目标,目标在左右图像上的像素位置会有差异,这个差异叫视差。已知两个相机之间的基线长度和相机的焦距,就能用视差算深度。它的优势在于深度是物理测量出来的,不依赖任何目标先验,同一个YOLOV5检测框里,不管站的是人还是皮卡,只要视差算得准,距离就能对得上。
结构光方案精度很高,但工作距离一般在1米以内,户外强光下还会被太阳光干扰。双目在0.5米到十几米都有可用距离,而且民用USB摄像头就能干,成本比激光雷达低一个量级。综合精度、成本、距离范围三个维度,双目是这个场景下的最优解。说白了,这套方案的性价比在于:检测用现成的YOLO生态,测距用开源计算机视觉库,硬件只多花一个摄像头的钱。
2.2 整体数据流:六个环节逐级传递
从相机读入一帧左右图,到输出目标3D坐标,中间经过六个环节:
- 采集:左右摄像头同时曝光,得到一张left_frame和一张right_frame,最好用免驱动的USB UVC摄像头,两个必须型号一致、分辨率一致、帧率一致。
- 标定与去畸变:用棋盘格标定出每个相机的内参(焦距、光心)和畸变系数,再用cv2.undistort去掉镜头畸变,不然图像的桶形畸变会让远处的视差计算直接偏差。
- 极线校正:stereoRectify把左右图像重投影到同一个平面上,让左右图对应的特征点处在同一水平线上。这一步之后立体匹配才能用一行扫描的方式去搜索对应点。
- 立体匹配:对校正后的左右图做SGBM匹配,得到每个像素的视差值,除以16得到浮点视差,再通过 z = f*b/disp 转成深度图。
- 目标检测:YOLOV5模型在左图上推理,输出每个目标的类别、置信度和矩形框坐标。
- 融合输出:把检测框映射到深度图上,在框内提取有效的深度值,取中位数作为目标距离;结合相机内参反投影,算出目标中心点的3D坐标。
整个链路里最耗时的是YOLOV5推理,如果用yolov5s模型和CUDA加速,单帧推理大约在2到5毫秒;SGBM虽然只是CPU实现但全图搜索也不慢,640x480分辨率下大概几十毫秒。这个节奏做实时测距是够用的。如果后面要做更高分辨率的测距,优先优化的是SGBM的搜索范围,而不是换检测模型——检测模型已经有成熟的加速方案,SGBM的耗时反而容易被忽略。
2.3 目录结构:拿到源码先按这个顺序读
整个工程的目录划分是按功能走的,没有把检测和测距代码混在一起。拿到源码建议按这个顺序读:
project/ ├── checkpoints/ # 存放YOLOV5权重文件,如 yolov5s.pt ├── configs/ │ └── camera_config.py # 相机参数、SGBM参数的集中配置 ├── data/ │ ├── calibration/ # 标定用的棋盘格图片 │ └── test_video.mp4 # 测试视频 ├── utils/ │ ├── calibration.py # 标定、极线校正、参数保存 │ ├── stereo_matcher.py # SGBM立体匹配与深度图生成 │ ├── detector.py # YOLOV5模型加载与推理封装 │ └── geometry.py # 深度提取、3D反投影 ├── scripts/ │ ├── calibrate_camera.py # 离线标定入口 │ └── run_calibration_check.py # 标定结果可视化验证 ├── detect_3d.py # 主入口:实时检测+测距 └── requirements.txt配置集中放在camera_config.py里,包含图像分辨率、标定文件路径、基线长度、YOLOV5权重路径、置信度阈值和SGBM参数。我做项目习惯把所有"可能想调的数字"都集中到一个文件里,避免为了改一个阈值翻遍代码。改参数的时候只需要改配置文件,核心代码一个字符都不用动。这个习惯在调试双目的时候尤其有用,因为SGBM的参数一调就是一组,散落在代码里根本记不住哪个是哪个。
2.4 硬件准备:两个摄像头怎么选才不给自己挖坑
双目方案对相机一致性要求很高,这里说几个血泪经验。首先两个镜头必须完全相同:芯片型号、焦距、接口、分辨率都要一致,最省事的是直接买厂家做好的双目模组,它们出厂前已经做过对齐和固定,基线固定好也省得每次测距都量。如果自己用两个USB摄像头拼,最大的坑是自动曝光——左右相机各自根据环境自动调亮度,导致同一目标在左右图上亮度不一致,SGBM匹配精度直接崩。解决办法是在代码里关掉自动曝光,强制设置相同的手动曝光值。
其次,基线长度决定测距范围:基线越宽,远处测距精度越好,但近处的公共视野会变窄;基线太窄,远处的视差太小,深度噪声很大。我的经验是:室内机器人用60到90mm的基线,室外车辆预警用200到300mm。这个值标定之后会算到相机外参里,程序里也能手动配置,到时候根据实际场景调整就好。如果你不确定选多长,先按场景里最远需要测距的距离来估算:基线长度大约是最近测距距离的十分之一到十五分之一。
3. 核心模块拆解:标定、立体匹配与YOLOV5深度提取的逐段实现
这一章把六个环节里最难、最容易改错的三段代码拆开讲:离线标定、SGBM立体匹配、检测框与深度融合。每一段代码都可以直接抄进自己的工程里改着用。如果你只想先跑通不想深究,可以直接跳到第4章,但后面调试的时候大概率还得回来看这一章。
3.1 相机标定:棋盘格照片怎么变成内外参数
标定的本质是求解相机内参、畸变系数和左右相机的相对位姿。标定的输入是一组棋盘格照片,输出是一个npz参数文件。先看单个相机的标定循环:
# utils/calibration.py 节选 def calibrate_single_camera(image_paths, pattern_size, square_size): criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-6) 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) objp *= square_size # 每个格子的实际边长,单位毫米 obj_points = [] # 世界坐标系下的棋盘格角点 img_points = [] # 图像坐标系下的棋盘格角点 for path in image_paths: img = cv2.imread(path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners = cv2.findChessboardCorners(gray, pattern_size, None) if not ret: continue corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) obj_points.append(objp) img_points.append(corners2) ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None) return mtx, dist逻辑说明:findChessboardCorners先粗检测角点,cornerSubPix再在粗位置周围做亚像素精化,这样角点坐标能到亚像素级别,标定结果更稳。objp里每个角点的坐标是"格子数 × 格子边长",square_size必须用真实值,它是后续所有3D坐标的尺度基准——如果这里填错了,测出来的距离会整体按比例偏移,这个错误非常隐蔽,画面看起来一切正常,就是距离差了一个固定的倍数。
拿到左右各自的内参后,还要做双目标定,求解右相机相对左相机的旋转矩阵R和平移向量T,这个R、T决定了极线校正的参数,也是三角测量里基线长度的来源:
# 左右相机联合标定 ret, _, _, _, _, R, T, E, F = cv2.stereoCalibrate( obj_points_l, img_points_l, img_points_r, mtx_l, dist_l, mtx_r, dist_r, image_size, flags=cv2.CALIB_FIX_INTRINSIC)标定完成后,把所有参数存成npz:内参、畸变系数、R、T、以及stereoRectify算出来的映射表。常见做法是把映射表直接算好存起来,运行时用cv2.remap查表校正,比每帧现算快得多:
R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify( mtx_l, dist_l, mtx_r, dist_r, image_size, R, T, alpha=0, new_image_size=image_size) map1_l, map2_l = cv2.initUndistortRectifyMap( mtx_l, dist_l, R1, P1, image_size, cv2.CV_32FC1)这里再提醒一次:标定照片要拍20到30张,并且让棋盘格在画面里覆盖不同位置、不同倾斜角、不同距离。只拍正中央一种姿态的标定结果,内外参全都不可用,这是最容易把新手带进沟里的坑。标定文件丢失或者换了一个摄像头之后,别偷懒复用旧参数——每个摄像头的光学中心和畸变都不同,复用旧参数等于让测距结果随缘。
3.2 SGBM立体匹配:视差图是怎么算出来的
极线校正之后,左右图上同一个物理点在同一个水平线上,接下来要做的是逐像素匹配。OpenCV的StereoSGBM_create是现成且效果稳定的选择:
def create_matcher(config): block_size = config.sgbm_block_size # 建议 5~11,必须为奇数 stereo = cv2.StereoSGBM_create( minDisparity=config.min_disparity, numDisparities=config.num_disparities, # 必须能被16整除 blockSize=block_size, P1=8 * 3 * block_size ** 2, P2=32 * 3 * block_size ** 2, disp12MaxDiff=1, uniquenessRatio=10, speckleWindowSize=100, speckleRange=32, mode=cv2.STEREO_SGBM_MODE_SGBM ) return stereo # 每帧计算视差图 disp = stereo.compute(left_rect, right_rect).astype(np.float32) / 16.0 # 16是固定因子逻辑说明:SGBM本质上是在水平方向做块匹配,同时对整幅图像做能量最小化约束。numDisparities决定了搜索范围,它越大能测的近距离越近,但计算量也上升。blockSize是匹配窗口,越大视差图越平滑但边缘越糊,我一般先用11试,边缘不行再降到7。P1和P2是平滑惩罚系数,P2通常是P1的4倍,这两个值影响深度图的连续性。
disp12MaxDiff限制左右一致性检查的最大差值,大于0就认为无效匹配可以过滤掉。uniquenessRatio要求最佳匹配的代价比其他候选低一定的百分比,抑制纹理重复区域的误匹配。speckleWindowSize和speckleRange用来滤掉视差图上的小噪块——比如100像素范围内视差变化小于32的才认为是有效块,其余置0。
注意SGBM返回的原始值是整数,要除以16才是真正的浮点视差,单位是像素。这个16是OpenCV内部固定因子,新手常常忘记除,导致所有距离都偏小16倍——我在项目里专门写了个冒烟测试来抓这种低级错误。视差图转深度图就一行:
depth = np.zeros_like(disp, dtype=np.float32) valid = disp > 1.0 depth[valid] = f * baseline / (disp[valid] + 1e-6) # f是左相机焦距fx, baseline单位需与square_size一致参数说明:f来自标定的内参矩阵第0行第0列,baseline可以用标定的T向量模长算出来,单位与标定时square_size保持一致。这里加了一个1e-6的epsilon防止除以0。如果你的镜头是广角或者鱼眼,SGBM的效果会明显变差,这时候优先检查是不是真的完成了极线校正,而不是急着调SGBM参数。
3.3 YOLOV5检测:把PyTorch推理封装成一行调用
检测部分我习惯封装成一个detector类,让主程序里只有一个model.infer(frame)接口。加载模型用本地YOLOV5工程而不是在线hub,避免每次运行都要联网下权重:
import torch class Detector: def __init__(self, weights_path, conf_thres=0.45, iou_thres=0.45): self.model = torch.hub.load( 'yolov5', # 本地仓库目录 'custom', # 加载自定义权重 path=weights_path, source='local', # 强制本地加载 force_reload=False) self.model.conf = conf_thres self.model.iou = iou_thres self.model.classes = None # None表示识别全部类别 def infer(self, frame): results = self.model(frame) # 内部自动做letterbox预处理 preds = results.xyxy[0].cpu().numpy() # [x1, y1, x2, y2, conf, cls] return preds逻辑说明:YOLOV5的xyxy输出是预处理后图像坐标系里的坐标,而预处理letterbox会在原图边缘补灰边,所以这个坐标跟原图不是一一对应的。很多人在把检测框映射到深度图时发现位置错位,问题就出在忘了做letterbox的反变换。我的处理方式是用results.xyxy之前先记录原图的shape,再用results.ims里保存的原始尺寸信息做还原;更省事的做法是推理前自己手动resize成模型输入尺寸并记录缩放比例。
参数说明:conf_thres是置信度阈值,检测远距离小目标时适当降低到0.3能多框出目标,但误检也会变多;iou_thres是NMS的IoU阈值,目标重叠严重的场景可以调低到0.3。如果你用的是自己训练的权重,建议先花半天在不同距离和光照条件下测试置信度分布,再决定阈值,不要拍脑袋填0.5。
3.4 深度提取与3D坐标反投影:检测框如何变成可用的距离
这是整个项目最关键的一步,也最容易出错。检测框的坐标来自YOLOV5的输入图,深度图的坐标来自remap之后的校正图,两者坐标系虽然都是像素坐标系,但图像尺寸和缩放可能不一样。首先要把检测框映射到深度图的坐标系:
def extract_depth(disp, box, depth_scale=1.0): x1, y1, x2, y2 = [int(v * depth_scale) for v in box] roi = disp[y1:y2, x1:x2] valid = roi[roi > 1.0] # 过滤无效视差 if valid.size == 0: return None, None median_disp = float(np.median(valid)) # 取中位数而不是均值 return median_disp, valid.size def disp_to_depth(median_disp, fx, baseline): if median_disp <= 0: return None return fx * baseline / median_disp逻辑说明:为什么取中位数而不是均值?因为目标边缘、反光区域和背景交界处经常混入少量误匹配的异常视差,均值会被极端值拉偏,中位数稳健得多。如果你在测试视频里看到距离值一帧一个样、跳得很厉害,大概率就是用了均值还敢不滤噪。valid.size是有效像素数量,我一般会要求它至少占检测框面积的20%,否则直接判定距离不可靠。
深度拿到之后,目标中心的三维坐标通过相机内参反投影:
def project_to_3d(u, v, depth, fx, fy, cx, cy): x = (u - cx) * depth / fx y = (v - cy) * depth / fy return x, y, depth # 单位与baseline一致,通常为毫米 # 目标中心点 cx_ratio = (box[0] + box[2]) / 2.0 / frame_width u = int(cx_ratio * depth_width) v = int(((box[1] + box[3]) / 2.0 / frame_height) * depth_height) depth_value = fx_real * baseline / median_disp x, y, z = project_to_3d(u, v, depth_value, fx, fy, cx, cy)参数说明:fx、fy、cx、cy全部取自标定得到的内参矩阵,u和v是深度图坐标系下目标中心的像素坐标。这个3D坐标的单位由两个地方决定:标定时的square_size和baseline的单位。如果square_size用毫米、baseline也从标定结果里取毫米值,那最终的x、y、z单位就是毫米。输出之前如果想转成米,除以1000即可。这里算出来的x和y是相对相机光心的横向与纵向偏移,正值代表目标在光心右侧或下方。
4. 在自己的相机上跑起来:环境、标定流程与实时测距
前面把原理和核心函数讲完了,这一章是照着就能跑通的完整流程。从新建虚拟环境开始,到标定自己的相机,再到把YOLOV5权重接上跑出距离。
4.1 环境准备:Python依赖与YOLOV5仓库
推荐Python 3.8到3.10,PyTorch的CPU或CUDA版本都能跑,但如果你要用实时摄像头测距,强烈建议装CUDA版,否则YOLOV5推理一帧要几百毫秒,压根谈不上实时。依赖装在requirements.txt里:
python -m venv .venv source .venv/bin/activate # Windows用 .venv\Scripts\activate pip install torch opencv-python numpy pyyaml matplotlib pip install -r yolov5/requirements.txt # 如果本地有yolov5仓库参数说明:opencv-python的版本建议4.5以上,太老的版本没有StereoSGBM_create或者参数名对不上。torch版本根据你的显卡装对应CUDA版本,比如CUDA 11.8装torch 2.0.1。装好之后运行python -c "import cv2"验证没报错再进入下一步。
这里提一下我踩过的坑:pip install torch默认装的是CPU版,运行detect_3d.py时模型推理耗时显示几百毫秒,我还以为是模型太大,后来一看torch.cuda.is_available()返回False。所以环境装完第一件事,先跑这个检查。另外如果你用的是两个摄像头,插上电脑后先在系统里确认设备号,不要想当然地认为左边设备号是0右边是1——USB枚举顺序经常乱,画面上左右颠倒或者只有一个设备在线,都是设备号配错了。
4.2 标定自己的双目相机
把两个相机固定好,基线根据你要测的距离范围定。然后用棋盘格拍摄标定图片,打印一张6x9或者7x10的棋盘格,贴在完全平整的硬板上,从不同角度、不同距离拍30张左右,左右相机同步拍摄。文件命名用left_01.jpg、right_01.jpg这种方式,方便脚本配对。
python scripts/calibrate_camera.py \ --left_dir data/calibration/left \ --right_dir data/calibration/right \ --pattern_size 6 9 \ --square_size 24 \ --output configs/stereo_params.npz脚本会逐张检测棋盘格角点并显示可视化结果,检不出来的图片会跳过。跑完之后看一眼终端输出的重投影误差,一般小于0.5像素就算合格,超过1像素就需要检查棋盘格是不是不平整或者照片数量不够。
标定结果保存成npz之后,运行scripts/run_calibration_check.py做一次极线校正可视化:把左右校正图并排显示,在图上画几条水平辅助线,如果同一物体在左右图上的位置基本落在同一条水平线上,校正就是成功的。这一步非常关键,它验证的是从标定到remap的整条链路,不是只看标定误差。
提示:极线校正验证这一步不能省。左右图对应点不在同一水平线,后面所有视差计算都白做。
4.3 实时测距主入口
配置文件的参数确认无误后,运行:
python detect_3d.py \ --weights checkpoints/yolov5s.pt \ --calib configs/stereo_params.npz \ --left_cam 0 --right_cam 1 \ --vis 1主程序流程是:循环读左右帧 → remap校正 → SGBM算视差 → YOLOV5推理 → 检测框与深度图融合 → 在画面上绘制检测框和距离标签 → 按q退出。画面里每个目标框上方会显示类似 "person 2.35m" 的文本,左下角实时显示当前帧的FPS。
如果你手上没有双摄像头,也可以用录制好的左右同步视频文件测试:
python detect_3d.py --left_video data/test_left.mp4 \ --right_video data/test_right.mp4 --vis 1用视频文件调试有一个好处:摄像头实时预览时场景不可控,很难复现同一个目标在同一个位置的测距效果;视频文件可以反复暂停回放,改一次SGBM参数跑一遍,比较前后两帧的差异,效率高很多。我建议把调试过程录下来当基准,每次改参数都跑同一段视频,用固定画面对比结果。
4.4 调参与验证:不同距离下怎么判断结果靠不靠谱
拿到实时画面后,最简单的验证方式是拿卷尺放在目标实际位置,对比画面上的读数。最好分近、中、远三个距离段做测试——比如2米、5米、8米各测10次,记录平均值和误差。
| 实际距离(米) | 测距均值(米) | 误差 | 说明 |
|---|---|---|---|
| 2.0 | 2.04 | +2% | 近距离视差大,SGBM表现稳定 |
| 5.0 | 5.18 | +3.6% | 中间段基线够用,误差主要来自角点定位 |
| 8.0 | 8.45 | +5.6% | 距离越远视差越小,误差进一步放大 |
这个表格是我在某个项目里的实测记录,数据会随相机基线和分辨率变化,但趋势是一致的。如果你发现远处误差特别大,优先加长基线、提升图像分辨率、增大numDisparities。如果你的场景是近距机器人避障,300mm以上的基线会让近处目标超出公共视野,此时反过来要缩短基线。要记住测距误差是距离的平方级增长的,不要指望一个参数让全距离段都精准。
5. 避坑指南:双目测距最容易翻车的五个环节
写代码和调优的时候,最容易让人崩溃的往往不是算法本身,而是某些环节的细节。这一章把常见的五个坑按"现象→原因→解决"列出来。
5.1 坑一:标定重投影误差很大但找不到原因
现象:标定输出的重投影误差超过1像素,距离测量结果整体偏小或偏大。原因:棋盘格贴在不平的纸板或弯曲的杂志上;或者标定照片全部集中在画面中央,相机发生轻微位移导致左右标定文件不匹配。解决:把棋盘格用双面胶贴在玻璃板或铝合金平板上,拍摄时让棋盘格出现在画面四角和中间,远近交替拍。左右相机的标定图片必须是在同一时间内同步拍摄的帧,不能左边拍了一套右边又拍另一套,因为两个相机只要相对位置变了,R和T就全不对了。
5.2 坑二:SGBM参数不合法直接抛异常
现象:运行到stereo.compute直接报错,提示disp12MaxDiff或者blockSize非法。原因:OpenCV对参数有硬性约束——numDisparities必须能被16整除、blockSize必须为奇数且大于1。解决:在配置文件里做一次校验,启动时检查参数再创建matcher。我的做法是写一个小守卫函数:
def validate_sgbm_params(cfg): assert cfg.num_disparities % 16 == 0, "numDisparities必须是16的倍数" assert cfg.block_size % 2 == 1 and cfg.block_size > 1, "blockSize必须为奇数且>1" return True这个校验放在构造函数里,避免每次改配置都要翻源码才能发现参数错误。另外numDisparities也不要设得过大,它和minDisparity加起来的范围如果超过图像宽度,计算结果会浪费很多内存,速度还慢。
5.3 坑三:左右画面亮度不一致导致视差全是噪点
现象:视差图看起来像雪花屏,有效深度区域很少。原因:两个USB摄像头都开着自动曝光,左右各自适应亮度,同一目标在左右图上的亮度差异很大,SGBM的像素块匹配直接失效。解决:在打开摄像头之后立即关闭自动曝光并设置固定曝光值。如果是普通UVC摄像头,用cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0)关掉自动曝光,然后手动设置曝光值,比如100或200,具体值以画面不偏暗不偏亮为准。这个坑在室内灯光和室外阳光下反差最大,如果你看到视差图白天正常、傍晚全崩,先检查是不是自动曝光又偷偷打开了。
5.4 坑四:检测框和深度图错位
现象:目标框画在深度图上的位置明显偏移,或者目标距离时有时无。原因:YOLOV5的letterbox预处理和SGBM的remap尺寸不一致,检测框坐标没有映射回深度图坐标系。解决:在detector类里不要直接使用xyxy的结果,而是自己维护letterbox的缩放比例和padding偏移,推理后再按比例映射回原图。更稳妥的方案是直接对remap之后的左图做检测,这样YOLOV5输入的坐标系和深度图完全一致,不用做任何映射:
# 推荐做法:检测输入直接用校正后的左图 left_rect = cv2.remap(left_frame, map1_l, map2_l, cv2.INTER_LINEAR) preds = detector.infer(left_rect) # 检测框直接对应深度图坐标这个做法牺牲了一点检测精度(校正后的图像边缘有裁剪),但换来的是坐标对齐的简单可靠。如果你发现矫正图里目标形状被拉伸变形,检查一下stereoRectify的alpha参数,alpha=0会让校正图自动裁剪掉非公共区域,画面边缘的变形是正常的。
5.5 坑五:小目标和远距离测距完全失效
现象:YOLOV5框出了5米外的一个小瓶子或8米外的行人,但距离标签不显示。原因:目标在画面上只有几十个像素高,对应的视差搜索窗找不到足够多的有效匹配,深度图里那一块基本都是无效值。解决:对这类场景有三个方向的调整——加大numDisparities提升近距离小目标的视差范围;降低conf_thres到0.3让更多远目标被框出来;给深度提取加一个最小有效像素数量阈值,小于这个阈值就放弃测距并标记为"距离不可靠",避免输出噪声数据让下游控制器误判。
如果你在室外场景测试,顺光远距离的测距效果通常还可以,一转到逆光,小目标的有效深度就急剧减少。这不是代码bug,而是摄像头动态范围不够,换宽动态范围的摄像头或者降低测距目标尺寸要求才能缓解。
6. 二次开发方向:稳定测距、目标跟踪与轻量化部署
项目跑通只是起点,真正给业务用还得做三件事:让测距值稳定、让目标持续可追踪、把推理延迟降下来。
稳定测距最直接的手段是对深度值做时间域滤波。实测中即使取了中位数,远距离目标的距离值依然会有小幅抖动。我一般会维护一个长度为5的滑动窗口,取窗口内的中位数或一阶指数平均(alpha=0.3)作为输出值。注意是"先滤波再输出",不是每帧直接用原始值,这样控制端拿到的距离曲线才平滑,下游做紧急制动或者抓取动作时不会因为距离跳变误触发。
目标框中心点的位置同样会抖动,如果在3D坐标上直接做差算速度,噪声会被放大到不可用。更稳的做法是接入一个简单追踪器(IOU跟踪或者卡尔曼滤波),让每个目标有稳定的ID,再用相邻帧的深度差和相机帧间隔算速度,遇到遮挡时用追踪器的预测值补帧。卡尔曼滤波的参数不用调太复杂,状态量取x、y、z和它们的速度就够用,维度过高反而在目标快速加减速时跟不上。
轻量化部署方面,YOLOV5转ONNX再转TensorRT能在GPU上进一步提速,或者换成更小的yolov5n。如果你跑在边缘设备上,SGBM本身用CPU算也不会成为瓶颈,真正的瓶颈还是检测模型;把yolov5s换成yolov5n,精度掉一点但FPS能上来一截。另外一个容易被忽略的优化点是SGBM的输入分辨率——如果业务不要求看到50米外的小目标,把校正图缩放到640x360再算视差,耗时可以下降一大半。
最后说一个我从这次项目里养成的习惯:双目测距的任何改动,都要先在录制好的左右视频上跑一遍离线回归,确认距离误差没有增大,再接线到实时摄像头。因为实时调参很容易被现场光照和抖动干扰,误以为没改好,实际是环境变了。从那以后我每次改SGBM参数或者检测阈值,都强制走一遍离线回放再上真机,这套验证流程省下的调试时间比写代码的时间还多。希望帮到你。
本文还有配套的精品资源,点击获取