
简介面向计算机视觉开发者的一份实战项目资源基于YOLOX目标检测算法实现双目相机测距提供从图像采集、目标检测、立体匹配到距离转换的完整流程适用于教学、研究以及机器人导航、自动驾驶等场景。压缩包共47个文件以31个Python源码文件为主覆盖模型训练、目标检测、单点/多点距离预测及辅助工具等模块并附带bmp/jpg测试图像、训练验证标签、说明文档与日志文件整体大小约2.12MB轻量易用。已有208人学习下载。源码对关键步骤作了详细注释内置可调参数的用户界面通过具体实战案例演示系统搭建、参数调优与功能测试全过程可帮助开发者同步掌握YOLOX算法原理与双目视觉测距的工程实现。项目结构清晰便于按需查阅和二次开发不仅具有教学示范价值也能为机器人导航、自动驾驶等实际应用提供快速落地的参考。1. 双目测距遇上YOLOX为什么目标检测框的深度值才靠谱做双目测距最头疼的不是相机标定也不是深度图计算而是“算出深度之后怎么告诉用户前面那辆车有多远”。如果你直接对整幅深度图取平均值那对不起背景里的电线杆、地面、树叶全都混进来距离值能偏出一辆车那么长。把YOLOX目标检测框和双目视差结合起来本质上是做一件事先让检测网络告诉你“哪个像素区域属于目标”再让双目系统告诉你“这个区域离我多远”。两条技术路线各管一段检测负责语义测距负责几何组合起来才是一个能上车的距离感知方案。这篇笔记适合已经跑通过YOLOX目标检测、想给检测框加上真实距离值的开发者也适合打算从零做双目测距项目、需要一份可靠落地路径的初学者。我会按标定、检测框到深度图的映射、参数调试、避坑、完整脚本骨架的顺序讲清楚最后给你几个能把误差压到厘米级的实操技巧。全程不碰黑匣子每个参数都能调每步都能复现。2. 先搞定双目相机标定内参、畸变与立体校正2.1 标定板采集与标定命令双目测距的第一步不是YOLOX而是让左右两个相机“认识彼此”。很多初学者的翻车点就在这里直接用厂家给的标称焦距和两相机间距结果深度图像被揉过的纸团检测框再准也没用。正规做法是拿标定板拍20对以上左右目同步帧用OpenCV的stereoCalibrate同时求解左右相机内参、畸变系数和两相机间的旋转矩阵R、平移向量T。我一般把采集和标定拆成两个脚本采集时用cv2.VideoCapture同时打开两个摄像头按间隔帧保存左右图对。这里有个容易忽略的细节——两个摄像头最好用同一型号在硬件上固定同步触发如果没有同步触发至少要在采集时保证场景静止否则标定板动一下特征点对不上标定结果直接作废。下面是一段采集脚本的核心逻辑import cv2 import os left_cap cv2.VideoCapture(0) right_cap cv2.VideoCapture(1) os.makedirs(left, exist_okTrue) os.makedirs(right, exist_okTrue) frame_id 0 while frame_id 30: # 建议采集30对以上越多越好 ret_l, left left_cap.read() ret_r, right right_cap.read() if not (ret_l and ret_r): continue # 转为灰度后找棋盘格角点能用才保存避免无效数据 gray_l cv2.cvtColor(left, cv2.COLOR_BGR2GRAY) gray_r cv2.cvtColor(right, cv2.COLOR_BGR2GRAY) ret_l_found, corners_l cv2.findChessboardCorners(gray_l, (9, 6), None) ret_r_found, corners_r cv2.findChessboardCorners(gray_r, (9, 6), None) if ret_l_found and ret_r_found: cv2.imwrite(fleft/{frame_id:04d}.png, left) cv2.imwrite(fright/{frame_id:04d}.png, right) frame_id 1 print(fcaptured {frame_id} pairs) # 显示带角点的画面方便调整标定板位置 cv2.drawChessboardCorners(left, (9, 6), corners_l, ret_l_found) cv2.drawChessboardCorners(right, (9, 6), corners_r, ret_r_found) cv2.imshow(left, left) cv2.imshow(right, right) if cv2.waitKey(1) 27: break这段代码不是简单地把图像存下来而是在采集时就先用cv2.findChessboardCorners确认左右两幅图都能找到同一组角点。只有同时找到才保存这样标定时的数据质量有保障。参数(9, 6)是棋盘格内角点数量对应你的标定板实际规格如果你的板子是10x7要改成(9, 7)或者(10, 6)注意是内角点不是格子数这是最容易搞错的地方。拿到约30对图像后跑stereoCalibrate。这里我不贴完整代码直接说关键参数criteria建议设置(cv2.TERM_CRITERIA_MAX_ITER | cv2.TERM_CRITERIA_EPS, 100, 1e-6)迭代次数给到100次flags建议用cv2.CALIB_FIX_INTRINSIC先固定单目标定结果再优化外参这样左右相机的内参不会跑偏。标定完成后R,T,E,F分别对应旋转矩阵、平移向量、本质矩阵和基础矩阵。其中平移向量T的三个分量里最重要的就是T[0]也就是基线距。你的YOLOX检测框能不能算出准的距离很大程度上取决于这个基线距是否准确。2.2 立体校正与重投影矩阵Q标定完只是拿到了几何关系真正要算深度还得做立体校正stereo rectification。校正的目的是把左右图像通过单应变换重新投影让左右相机的极线水平对齐这样同一个目标点在左右图上的坐标只差一个水平方向的视差。你不需要手动实现极线几何OpenCV的stereoRectify会直接帮你计算左右校正映射矩阵和重投影矩阵Q。Q矩阵是核心它是一个4x4矩阵能把左右图像上的齐次坐标点投影到三维空间Q | 1 0 0 -cx | | 0 1 0 -cy | | 0 0 0 f | | 0 0 -1/Tx (cx - cx)/Tx |上面的cx,cy是左相机主点f是焦距像素单位Tx是基线距。这些值都来自stereoCalibrate结果。stereoRectify之后你需要用cv2.initUndistortRectifyMap生成左右两幅图的映射表再通过cv2.remap对输入图像做重映射。这一步做完左右图像才能用于后续的视差计算。立体校正的验证方法很简单在左右校正图上画同一条水平线看标定板的角点是否都落在同一条线上。落不上的话回头检查标定板采样时是不是有太大倾角或者图像模糊。校正后的图像还有一个作用你YOLOX检测框的左图和右图坐标必须基于校正后的图像坐标系。很多项目图省事直接在原始图上跑YOLOX然后在深度图上取值结果检测框中心和视差计算中心错位测距值忽远忽近。所以我的习惯是先对每一帧做remap校正再送进YOLOX检测器。虽然多一步变换但数据的坐标系是统一的后面做点对点映射时才不会踩坑。3. YOLOX检测框如何映射到深度图坐标对齐与ROI深度提取3.1 检测框到深度图的像素映射双目测距最终要的是深度图而深度图一般由stereoBM或SGBM算法计算得到。SGBM的视差图质量好但慢BM快得多但噪声大。我的经验是如果目标是实时测距先用cv2.StereoSGBM_create离线验证效果再替换成BM调参。不管用哪种算法你得到的都是一个和左图校正后分辨率相同的视差图disparity深度值计算公式是depth f * Tx / disparity。现在关键问题来了YOLOX输出的检测框坐标是在哪张图上的如果你跑YOLOX的输入是校正后的左图那检测框直接对应视差图坐标如果输入是原始左图那需要先把原始图校正再在图上跑YOLOX或者把检测框坐标做一次映射。最省事、避免坐标系混乱的做法是把remap之后的左图作为YOLOX的唯一输入这样检测框的(x1, y1, x2, y2)直接就是视差图上的像素区间不需要任何变换。下面是一段检测框到深度提取的示意代码import cv2 import numpy as np # 假设已经加载YOLOX模型且使用校正后的 left_rectified # left_rectified: (H, W, 3), disparity: (H, W) float32, Q: 4x4 boxes yolox_infer(left_rectified) # 返回 N x [x1, y1, x2, y2, cls, conf] for box in boxes: x1, y1, x2, y2, cls, conf box[:6] # 略去越界索引同时避免目标太靠近图像边缘导致深度噪声 x1 max(0, int(x1)); y1 max(0, int(y1)) x2 min(disparity.shape[1]-1, int(x2)); y2 min(disparity.shape[0]-1, int(y2)) # 提取检测框内所有有效视差剔除0和极大值 roi_disp disparity[y1:y2, x1:x2] valid_disp roi_disp[(roi_disp 0) (roi_disp 192)] if valid_disp.size 0: continue # 用中位数而非均值避免框内个别错误匹配点把距离拉偏 median_disp np.median(valid_disp) # Q矩阵投影法只需要视差和像素坐标 homo np.array([(x1x2)/2.0, (y1y2)/2.0, median_disp, 1.0]) point_3d Q homo z_distance point_3d[2] / point_3d[3] print(f目标距离: {z_distance:.2f} m)这段代码有两个参数需要你根据实际调。第一valid_disp的上限192是视差范围取决于你的相机配置近距离目标视差大远距离目标视差小最大值建议设为分辨率宽度的四分之一左右太大容易混入噪声。第二用的是np.median而不是np.mean因为视差图在物体边缘和低纹理区域会有离群噪点中位数能压掉这些离群值。如果你的检测框内包含大量背景见第4章的坑。3.2 深度值滤波与目标距离计算一次中位数滤波还不够因为单帧视差图本身有随机误差。我的做法是对连续N帧一般取5~10帧的同目标深度做滑动窗口滤波用窗口内的中位数作为最终输出。注意“同目标”三个字——跨帧匹配目标时不能用最简单的中心点最近邻要结合检测框的交并比IoU做目标跟踪。比如你先记录上一帧每个目标的框当前帧每个目标框计算与上一帧所有框的IoUIoU大于0.5的认为是同一目标然后把这个目标的深度值放进滑动窗口。这样就能避免突然有个误检框插进来把滤波窗口污染掉。深度计算时还有一个容易被忽略的点视差和深度不是线性关系。视差差1个像素在近距离比如0.5米可能对应几厘米的距离误差但在远距离比如5米可能对应几十厘米。所以你不能用固定的单位换算必须通过Q矩阵投影或者直接用f * Tx / disp公式。如果你的相机经过校正Q[3][2]就是-1/Tx投影法算出来的z_distance已经包含了焦距和主点修正比手工除法更稳。到这里你已经能把YOLOX检测到的每个目标输出一个稳定距离。但马上会遇到一堆细节问题为什么测出来忽远忽近为什么框越大距离越偏别急下一章讲避坑。4. 常见问题与避坑标定、对齐、测距误差的5个实战坑4.1 左右图像分辨率不一致导致检测框偏移现象左右相机型号相同但采集时因为摄像头驱动默认参数不同左图是640x480右图是1280x720校正后视差图和检测框对不上测距值乱跳。原因左右图像分辨率不同stereoRectify计算映射表时虽然内部会处理但你的YOLOX如果只跑了左边视差图按照较大分辨率生成检测框坐标就不能直接用。解决在采集脚本里强制将左右画面设置为相同分辨率例如cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)和cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)并且在生成映射表时统一使用同样的尺寸。我一般还会在标定前先打印左右帧的shape确认完全一致不一致就先修摄像头参数别急着往下走。4.2 深度图噪声大单帧测距不稳定现象同一目标静止不动测出来的距离每秒波动半米车还停着呢距离读数却像在跳舞。原因视差图本身有匹配噪声尤其是低纹理区域比如纯色车漆、重复纹理比如百叶窗。SGBM虽然比BM好但在光照变化和反射表面依然会出错。解决先做深度值的时间滤波用滑动窗口中位数窗口长度至少5帧。如果还是跳检查StereoSGBM_create的参数numDisparities和blockSize对噪声影响最大。blockSize越大视差越平滑但会损失细节numDisparities太小会截断近距离目标的视差。我常用numDisparities64、blockSize5起步再根据你的目标距离范围调整。另外给深度图做一个cv2.medianBlur核为5的预处理也能明显减少孤立噪声。4.3 检测框边缘包含背景深度值偏大现象检测一个人框把身后的墙也包进来了测出来的距离比真实距离多出1米多。原因YOLOX的检测框是矩形不可能完美贴合物体轮廓尤其在目标姿态不规则时框内会混入大量背景像素。矩形框越大背景占比越高。解决不要直接用整个框的深度中位数。先用灰度直方图或连通域分析把框内前景和背景分开。简单粗暴的做法是对框内深度值做聚类比如取深度值的直方图找峰值最多的区间作为前景。更稳的是只取检测框中心区域比如去掉边缘10%的像素来计算中位数。我一般用框内所有有效视差的5%到95%分位数区间作为有效范围再把中位数落在该区间内的像素挑出来重新计算。这个方法能明显修正框内背景干扰。4.4 标定时棋盘格角度太少校正后图像扭曲现象校正后的左右图像一致性很差水平线对不齐视差图边缘出现严重黑色区域。原因标定板数据拍摄时只让棋盘格正对着相机缺少不同角度的倾斜和旋转导致内参求解病态畸变系数拟合不准。解决重新采集标定数据。保证每对图像中棋盘格至少占画面1/3并且覆盖画面左右上下四个角落标定板尽量每次变换姿态比如左右旋转30度、上下俯仰20度、前后移动不同的距离。一个可用的检查方法标定完用cv2.calibrateCamera的重投影误差作为指标误差大于0.5像素就说明数据不够好重新采集别省这一步。4.5 测距结果与真值偏差大如何排查现象用卷尺测真值2米程序输出2.35米而且距离越远偏差越大接近线性增长。原因这种线性偏差大概率是基线距Tx不准或者焦距f不准。如果stereoRectify的Q矩阵是从stereoCalibrate结果算的那Tx可能是标定得到的优化值而非真实物理基线偏差被吸收进了标定误差。解决用一个已知距离的平板校验拍10个不同距离的点测量误差随距离的变化。如果误差随距离线性增加说明视差计算正确但焦点或基线参数有偏差。此时可以反推真实距离为Z_true你计算用的f和Tx真实误差来自两者的比例。重新标定并且优先检查标定板的角点精确度确保每张图的角点都被正确亚像素化。建议用cv2.cornerSubPix对检测到的角点做一步亚像素优化这一步能显著提升标定精度。5. 从标定到测距的完整流程一个可复现的脚本骨架5.1 离线标定参数保存与加载上面几章讲了很多细节现在把它们串成一个完整流程。我的习惯是标定一次性完成然后把参数存成.npz或.yaml文件运行时直接加载避免每次启动都重新标定。文件里保存的内容包括左右相机内参K_l、K_r畸变系数D_l、D_r旋转矩阵R、平移向量T以及由stereoRectify计算出的Q矩阵。保存代码很简单但要注意加载时必须用同一分辨率否则映射表要重新生成。如下是一个保存参数的示例import numpy as np import cv2 # K_l, D_l, K_r, D_r, R, T 来自 stereoCalibrate np.savez(./stereo_params.npz, K_lK_l, D_lD_l, K_rK_r, D_rD_r, RR, TT, QQ) # Q 来自 stereoRectify # 运行时加载 with np.load(./stereo_params.npz) as data: K_l data[K_l]; D_l data[D_l] K_r data[K_r]; D_r data[D_r] R data[R]; T data[T] Q data[Q]加载之后初始化左右映射表。如果运行时图像尺寸与标定时不同需要重新调用cv2.stereoRectify和initUndistortRectifyMap。这就是为什么我坚持让摄像头分辨率固定避免动态切换。把映射表创建放在初始化阶段不要在每一帧里重复创建否则性能会大打折扣。5.2 实时测距主循环以下是融合了立体校正、SGBM视差、YOLOX检测和深度滤波的实时主循环骨架。这里用伪协议接口替代具体YOLOX推理代码因为不同部署方式ONNX、TensorRT、PyTorch代码差异很大但逻辑相同。import cv2 import numpy as np # 省略加载YOLOX模型代码假设有 infer(left_rectified) 函数 # 省略映射表生成代码假设已有 map1_l, map2_l, map1_r, map2_r def compute_disparity(left, right): # 转灰度SGBM输入是单通道 gray_l cv2.cvtColor(left, cv2.COLOR_BGR2GRAY) gray_r cv2.cvtColor(right, cv2.COLOR_BGR2GRAY) sgbm cv2.StereoSGBM_create( minDisparity0, numDisparities64, # 必须是16的倍数 blockSize5, # 奇数3~11之间 P18 * 3 * 5 * 5, P232 * 3 * 5 * 5, disp12MaxDiff1, uniquenessRatio10, speckleWindowSize100, speckleRange32, ) disp sgbm.compute(gray_l, gray_r).astype(np.float32) / 16.0 return disp def get_distance(disparity, Q, box): x1, y1, x2, y2 box[:4] x1, y1 max(0, int(x1)), max(0, int(y1)) x2 min(disparity.shape[1] - 1, int(x2)) y2 min(disparity.shape[0] - 1, int(y2)) roi disparity[y1:y2, x1:x2] valid roi[(roi 0) (roi 192)] if valid.size 0: return -1.0 # 去掉框内深度直方图的极端值取中位数 median_disp np.median(valid) point np.array([(x1 x2) / 2.0, (y1 y2) / 2.0, median_disp, 1.0]) point_3d Q point if abs(point_3d[3]) 1e-6: return -1.0 return point_3d[2] / point_3d[3] while True: ret_l, left_raw left_cap.read() ret_r, right_raw right_cap.read() if not (ret_l and ret_r): break # 1. 立体校正统一坐标系 left cv2.remap(left_raw, map1_l, map2_l, cv2.INTER_LINEAR) right cv2.remap(right_raw, map1_r, map2_r, cv2.INTER_LINEAR) # 2. 计算视差图 disparity compute_disparity(left, right) # 3. YOLOX检测输入校正后的左图 detections yolox_infer(left) # 4. 对每个检测框计算距离 for det in detections: dist get_distance(disparity, Q, det) if dist 0: x1, y1, x2, y2 map(int, det[:4]) label f{det[4]} {dist:.2f}m cv2.rectangle(left, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(left, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow(depth, disparity / (192.0 / 255.0)) cv2.imshow(result, left) if cv2.waitKey(1) 27: break代码中compute_disparity的numDisparities64表示能计算的视差范围是0到64个像素根据你的目标距离范围调整目标越近视差越大可能需要128甚至256目标越远64就够。blockSize5是匹配窗口尺寸越大视差图越平滑但边缘会变糊对精准测距来说5到7是比较折中的选择。disp12MaxDiff1是左右一致性检查的阈值大于1会引入更多无效视差建议保持默认。主循环里我用interpolationcv2.INTER_LINEAR做重映射没有用INTER_NEAREST因为线性插值能让边缘平滑一些而最近邻插值会加剧视差图的锯齿。如果你追求最高精度可以考虑INTER_CUBIC但速度更慢。实际部署时如果发现视差图对检测框的依赖太大需要先输出中间结果验证——把视差图直接可视化看目标物体是否明显区分于背景。6. 把误差压到厘米级深度滤波、中值窗口与标定板优化技巧前面已经给了能跑通的基础方案但工程上真正难的是把误差从半米压到十厘米以内。这里我分享三个亲测有效的技巧你可以在自己项目里直接套用。第一个技巧是自适应深度滤波。不要对所有目标用同一个中值窗口大小而是根据目标的检测框尺寸动态调整。框越大说明目标占的像素越多你可以用更大的窗口取深度值框小的时候目标像素少深度值少如果还用大窗口会把背景拉进来。我的实现是当框的高度大于100像素时取框内所有有效视差值的15%~85%分位数之间的像素重新计算中位数当框高度小于50像素时直接取全框中位数但要额外做一个5帧时间滤波。这种按目标大小分级的策略比单一中位数在混合距离场景下稳定很多。第二个技巧是针对标定板数据的。很多人的标定误差不是来自算法而是来自标定板本身。建议打印在硬质铝板或树脂板上不要用普通A4纸纸板表面弯曲会引入系统误差。采集时不要只让标定板面对相机而是让标定板在画面中移动覆盖边缘区域。同时用cv2.cornerSubPix亚像素细化角点这一步能让重投影误差下降一半以上。我见过一个项目标定误差从0.8像素降到0.3像素后测距误差直接缩小了30%可见标定这个环节性价比极高。第三个技巧是使用“锚点校验”。在完成系统搭建后你需要在现场放三个已知距离的参照物比如0.5米、1米、3米的纸箱运行程序记录输出。如果三个点的误差方向一致都偏大或都偏小说明是系统性的标定偏差可以通过调整stereoRectify的alpha参数来优化校正边界或者直接重新标定。如果误差方向不一致比如近处偏大、远处偏小那多半是视差计算参数不适配距离范围需要调整numDisparities。这个锚点校验法能帮你快速定位是哪一环节出了问题而不是盲目调参。最后说一个我的血泪经验千万别在检测框刚出现的第一帧就给用户显示距离值。YOLOX偶尔会误检第一帧的深度也可能刚好是噪声峰值。我一开始懒没做这个延迟结果检测一个远处路牌时突然跳出一个0.3米的距离差点被甲方当成系统故障。后来我给每个目标强行设置了连续5帧稳定后才输出距离之后就没再翻过车。做测距功能稳定性比单帧精度更重要用户盯着的是读数稳不稳不是某一帧的精确值。希望这个经验对你的项目有帮助。本文还有配套的精品资源点击获取