双目测距与YOLOX结合:从标定到三点测距的完整实践

发布时间:2026/9/28 14:54:27
双目测距与YOLOX结合:从标定到三点测距的完整实践 简介双目测距与目标检测结合的项目实战资源面向计算机视觉爱好者、算法工程师与相关专业学生解决双目相机测距系统中的目标定位与距离测量问题。项目基于YOLOX算法实现检测与测距全流程包含图像采集、预处理、目标检测、立体匹配、视差计算及距离转换等环节并配有用户界面与参数调节功能便于动手实践和二次开发。压缩包共47个文件以31个Python脚本为主兼具训练、预测、标注转换与评估功能同时含有bmp/jpg示例图、txt标注与配置、README说明及训练日志等整体大小仅2.12MB轻量易部署。已有208人学习下载适合用来理解YOLOX的Anchor-Free与Decoupled Head原理掌握双目相机标定与立体匹配的实际操作并可作为机器人导航、自动驾驶、三维重建等方向的基础工程参考。1. 双目测距 YOLOX先搞清楚这套源码到底让你做什么拿到这个「双目测距_基于YOLOX」的项目压缩包第一反应别急着解压跑训练。它解决的是一个很具体的工程问题用双目摄像头拍一段画面先用 YOLOX 把画面里的目标人、车、瓶子都行框出来再通过左右目图像的视差算出这个目标离相机到底有多远。换句话说它把「目标检测」和「深度估计」两条技术线拧在了一起——很多做机器人避障、安防监控、AR 交互的开发者卡住的点不是单目检测做不好而是拿到目标框之后不知道怎么换算成物理距离。这套源码就是来填这个空白的。项目里除了一堆训练脚本和权重之外最有价值的其实是两个写着predict_one_point和predict_three_point的推理脚本以及一个stereo目录——前者演示了怎么对检测框中心点做深度查询后者是双目标定和视差计算的落点。适合两类人一是正在做课设/毕设、需要一套能跑通的视觉测距 demo 的学生二是已经在做机器人或嵌入式视觉、想把单目检测升级成立体测距的工程师。下面我从原理到踩坑把这条链路拆开讲清楚。2. YOLOX 在测距链路里的角色先把目标框住的代价与收益2.1 为什么选了 YOLOX 而不是 YOLOv5 或 Faster R-CNN在测距系统里目标检测器不是独立存在的它要为后面的立体匹配提供「兴趣区域」。如果检测框偏了哪怕只偏几个像素中心点投到右图上就会错位视差算出来自然不准。YOLOX 在这套源码里被选中的原因主要有三个。第一是 Anchor Free 的设计。YOLOX 取消了锚框直接预测目标的中心点和宽高这对于后续要取「检测框中心像素坐标」的测距流程来说非常友好——中心点就是网络直接回归出来的不需要在推理阶段做 Anchor 匹配解码。第二是 Decoupled Head分类和回归分支分开在目标重叠或小目标的场景下定位精度更好检测框更贴合目标边缘中心点偏移量更小。第三是它保持了 YOLO 系列一贯的实时性在 Jetson Nano 或者普通 CPU 上也能跑到可用的帧率。如果你用过 Faster R-CNN 就会知道两阶段检测器虽然精度高但推理一张图要 200ms 以上在测距场景里意味着画面卡顿移动中的目标根本测不准。这套项目用的是 YOLOX-s 级别的模型输入尺寸默认 640×640在 GTX 1660 上能跑 60fps 以上留给后续视差计算的时间就很充裕了。2.2 从yolo.py看检测器如何输出坐标项目里的yolo.py封装了 YOLOX 的推理逻辑核心是加载权重后对输入图像做前向推理输出检测框、类别和置信度。我打开这个文件扫了一遍它的流程和标准 YOLOX 推理没有太大差别但有一个细节值得注意——它保留了检测框在原始图像坐标系下的坐标没有归一化到 0~1。这一点对测距很重要因为后面求视差的时候需要的是像素坐标归一化坐标还得乘回图像宽高多一步转换就多一个出错点。# yolo.py 中检测结果解析的核心片段简化 def detect(self, img): # img 是 BGR 格式的原图shape (H, W, 3) img_640 letterbox(img, new_shape(640, 640)) # 保持比例的缩放加填充 img_640 img_640[:, :, ::-1].transpose(2, 0, 1) # BGR-RGB, HWC-CHW img_640 np.ascontiguousarray(img_640) with torch.no_grad(): outputs self.model(torch.from_numpy(img_640).float() / 255.0) # outputs 是 Decoupled Head 输出的总预测包含 box、obj、cls pred outputs[0].cpu().numpy() # shape: (num_pred, 85) # 过滤低置信度框NMS 后得到最终结果 boxes non_max_suppression(pred, conf_thres0.3, iou_thres0.5) # boxes 里每个元素是 (x1, y1, x2, y2, conf, cls_id) return process_detections(boxes, img.shape) # 映射回原图坐标这段代码里有两个参数需要你留意。conf_thres是置信度阈值设得太低会出现大量误检框每一个误检框都要在后端做一次深度查询拉低帧率还容易输出错误距离设得太高又会漏检。我一般习惯设 0.3~0.45这套源码默认在 0.3 左右。new_shape是输入尺寸如果目标比较小可以提高到 800×800精度会上去但推理时间要翻倍看你的硬件水平取舍。2.3 检测框到测距点中心点之外的另一种选择大多数用这套项目的同学第一反应是取检测框的中心像素(cx, cy)当测距点。这个做法在目标占画面比例较大时没问题但如果目标只有 30×30 像素中心点稍微偏几个像素视差误差会非常明显。项目里其实留了一个更稳妥的思路——把检测框在左右目图像里分别做一次然后对两个框计算匹配度选匹配分最高的那条水平线做视差计算。不过这套源码的predict_one_point.py用的是简化路线只在左图做 YOLOX 检测拿到中心点后用极线约束在右图同一行直接搜匹配块。工程上这叫「基于稀疏点的双目匹配」省掉了全图立体匹配的开销实时性更好。代价是它假设目标表面是近似平面的如果目标是斜着的比如侧停的汽车中心点测出来的距离会偏大。这种场景下三线测距脚本predict_three_point.py会更有用后面第六章我会专门说验证方法。3. 双目测距核心标定参数、视差与三角化3.1 视差公式与两个你必须知道的数双目测距的物理原理并不复杂左右两个相机光心之间的距离叫基线baseline记作 b两个相机焦距相同记作 f某个空间点在左右图像上的成像位置差叫视差disparity记作 d于是深度 Z f × b / d。这个公式是整条链路的基石但工程上有两个细节经常被忽略。第一个是单位问题。f 的单位是像素b 的单位是毫米那么 Z 算出来就是毫米。如果 f 是像素、b 是米Z 的单位就是米。很多新手把标定出来的焦距矩阵里 f 当成毫米算出来的距离差了 1000 倍。第二是 d 必须大于 0 才有意义如果左右图匹配出来视差为 0说明目标在无穷远或者匹配失败代码里要过滤掉这个点否则会出现距离趋近无穷大的荒谬输出。3.2 双目标定stereo 目录里的 calibrate.py 怎么用这个项目里的stereo目录核心工作就两件单目标定左右相机各自的内参和畸变系数和双目标定两个相机之间的旋转和平移。在跑任何测距之前你必须先完成这一步否则视差全是错的。# stereo/calibrate.py 的典型标定流程以 OpenCV 为例的实操简化版 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) # 1. 左右目各拍 20~30 张不同姿态的棋盘格照片提取角点 objpoints_l, imgpoints_l calibrate_single(cap_left, pattern_size(9, 6)) objpoints_r, imgpoints_r calibrate_single(cap_right, pattern_size(9, 6)) # 2. 单目标定得到内参和畸变系数 ret_l, mtx_l, dist_l, _, _ cv2.calibrateCamera(objpoints_l, imgpoints_l, size, None, None) ret_r, mtx_r, dist_r, _, _ cv2.calibrateCamera(objpoints_r, imgpoints_r, size, None, None) # 3. 双目标定求解右目到左目的旋转矩阵 R 和平移向量 T ret_stereo, mtx_l, dist_l, mtx_r, dist_r, R, T, E, F \ cv2.stereoCalibrate(objpoints_l, imgpoints_l, imgpoints_r, mtx_l, dist_l, mtx_r, dist_r, size, criteriacriteria, flagscv2.CALIB_FIX_INTRINSIC) # 4. 立体校正让两幅图像共面且行对准 R1, R2, P1, P2, Q, valid_roi1, valid_roi2 \ cv2.stereoRectify(mtx_l, dist_l, mtx_r, dist_r, size, R, T, alpha0) # 5. 生成校正映射表后面直接 remap 一次就行 map1_l, map2_l cv2.initUndistortRectifyMap(mtx_l, dist_l, R1, P1, size, cv2.CV_32FC1) map1_r, map2_r cv2.initUndistortRectifyMap(mtx_r, dist_r, R2, P2, size, cv2.CV_32FC1)标定的时候有几个参数直接决定后面能不能算准。pattern_size是棋盘格内角点数量普通的 9×6 或者 8×6 都行但注意是内角点如果你的棋盘格是 10×7 的格子内角点就是 9×6。拍摄时棋盘格要覆盖画面的中心和边缘而且要倾斜着拍不能只拍正对相机的角度否则标定出来的畸变系数会非常不稳定。cv2.stereoCalibrate里的flags我用的是CALIB_FIX_INTRINSIC意思是先固定单目标定出来的内参只优化双目之间的 R 和 T。如果你觉得单目标定结果不够稳也可以不加这个 flag让内参和位姿一起优化但那样需要更多照片否则容易过拟合。标定做完之后q矩阵重投影矩阵里那个-T[0]就是基线 b 的倒数单位是 mm 的逆可以用它来验证标定结果的合理性。比如你实际把两个相机装成 120mm 的基线标定出来 T[0] 应该是 120mm 左右如果差了 5mm 以上说明标定照片质量有问题。3.3 立体匹配SGBM 的 minDisparity 和 numDisparities 怎么设立体匹配是把左右图逐像素找对应关系的过程这个项目里用的还是经典的 SGBM半全局块匹配OpenCV 里cv2.StereoSGBM_create可以直接调。# stereo/stereo_match.py 中的 SGBM 参数设置 stereo cv2.StereoSGBM_create( minDisparity0, # 最小视差通常设 0 numDisparities64, # 视差搜索范围必须是 16 的倍数 blockSize11, # 匹配块大小奇数越大越平滑但细节丢失 P18 * 3 * blockSize ** 2, # 平滑惩罚参数 P1相邻像素视差变化 1 的惩罚 P232 * 3 * blockSize ** 2, # 平滑惩罚参数 P2相邻像素视差变化 1 的惩罚 disp12MaxDiff1, # 左右一致性检查的最大允许误差 uniquenessRatio10, # 匹配唯一性比例 speckleWindowSize100, # 过滤小斑点的窗口尺寸 speckleRange32 # 斑点内允许的视差变化范围 )这里的核心参数我认为是numDisparities。它代表你允许的最大视差范围如果目标离相机很近视差会很大这个值小了就会在近距离上出现空洞如果目标都在远处设 64 就够了增大到 128 会显著增加计算量。一个实用经验是先用两倍于理论最大视差的值跑一遍看输出的深度图有没有大片黑色空洞如果有就说明范围不够。blockSize我觉得是第二个要调的。它越大匹配越平滑但边缘越模糊测距边界会往外扩它越小噪点越多。一般 9 到 15 之间比较平衡。如果你的目标是测小物体比如 5 米外的小瓶子blockSize可以设小一点同时接受一些噪点后面用检测框做空间滤波。4. 把项目跑起来从 requirements 到三点测距脚本4.1 环境部署requirements.txt 里最坑的一个库项目的requirements.txt通常会列好 PyTorch、OpenCV、numpy 这些依赖但最坑的是torch的版本和你本机 CUDA 的匹配问题。如果你直接用pip install -r requirements.txt它会拉最新版 PyTorch如果你的显卡驱动是旧的CUDA 运行时可能不兼容然后你 import torch 直接报错。# 建议先确认显卡驱动支持的 CUDA 版本 nvidia-smi | grep CUDA Version # 再安装对应版本的 PyTorch以 CUDA 11.8 为例 pip install torch1.13.1 torchvision0.14.1 --index-url https://download.pytorch.org/whl/cu118 # 然后安装项目其他依赖 pip install -r requirements.txt如果是纯 CPU 环境也可以装 CPU 版 PyTorch但推理速度会慢很多YOLOX-s 在 CPU 上单帧可能要 300ms 以上测距基本变成「静止目标拍照测距」而不是实时测距。我的建议是先用 CPU 版把整个流程跑通确认代码逻辑没问题再换 GPU 环境。这个项目里还有一个细节如果训练脚本里用了自定义的 COCO 工具集utils_coco它是依赖 pycocotools 的在 Windows 上pip install pycocotools会直接编译报错。解决办法是装pycocotools-windows的预编译 wheel或者直接用pip install opencv-python里自带的工具但会影响get_map.py计算 mAP 的功能。4.2 predict_one_point.py 完整推理流程拆解这个脚本是整套源码的「最短链路演示」加载一张左图YOLOX 检测出目标用检测框中心点坐标到右图上做块匹配计算视差然后换算距离并打印出来。# predict_one_point.py 的主流程结合源码整理的逻辑结构 def predict_one_point(left_img_path, right_img_path, calib_file): # 1. 加载双目标定参数内参矩阵 mtx_l、mtx_r以及 Q 矩阵 calib load_calibration(calib_file) # calib_file 通常是 .npz 或 .yaml # 2. 对左右图做去畸变和立体校正 left_rect cv2.remap(left_img, map1_l, map2_l, cv2.INTER_LINEAR) right_rect cv2.remap(right_img, map1_r, map2_r, cv2.INTER_LINEAR) # 3. 左图 YOLOX 检测得到检测框和中心点 boxes yolo.detect(left_rect) # 每个 box: [x1, y1, x2, y2, conf, cls] cx, cy get_center(boxes[0]) # 取第一个目标作为测距对象 # 4. 极线搜索右图在同一行 cy 上搜与左图小块最匹配的位置 # 左图取 (cx-8, cx8) 的一小段灰度块在右图同一行滑窗匹配 block_left get_gray_block(left_rect, cx, cy, block_size16) best_match_x template_match_on_epipolar_line(right_rect, block_left, cy, search_range80) # 5. 视差 左图中心点列号 - 右图匹配点列号 disparity cx - best_match_x if disparity 0: raise ValueError(视差 0匹配失败或目标过远) # 6. 通过重投影矩阵 Q 直接换算距离 point_3d cv2.perspectiveTransform(np.array([[cx, cy, disparity]], dtypenp.float32), Q) distance_mm point_3d[0][0][2] # Q 矩阵输出的 Z 值就是距离单位 mm return distance_mm / 1000.0 # 转成米这里有个重要的逻辑是第 4 步的「极线搜索」。因为立体校正之后左右图的行是对齐的所以同名点一定在同一行上。左图的中心点 (cx, cy) 到右图找匹配点时不用全图搜索只在第 cy 行上左右滑动找最相似的块就行。这个约束让计算量大幅下降但也意味着如果你的相机没有做好立体校正行没对齐这个搜索会彻底失败视差会乱跳。第 5 步的disparity cx - best_match_x是视差的定义注意是左图坐标减右图坐标反了会出现负视差。第 6 步用 Q 矩阵算三维坐标取 Z 分量就是距离。如果你的标定文件里没有 Q 矩阵也可以用Z f * b / disparity手动算但要注意 f 需要乘上校正后的尺度也就是用 P1 矩阵里的值而不是 mtx_l 里的值。4.3 如果要从摄像头实时跑项目源码里以读图片为主result_1.bmp到result_4.bmp就是输出示例。如果你想把输入从图片换成摄像头实时流改动很小——把cv2.imread换成cap.read()循环就行但要注意两点。第一摄像头必须固定安装标定时的姿态和实际运行时的姿态一致否则重投影矩阵的基线假设就错了。第二左右目画面的同步性很重要。如果两路摄像头不是硬件同步触发的在画面有运动目标时左右图拍到的目标位置会有细微差别这个差别会被当成视差导致距离偏大偏小都不一定。工业上常用硬件触发或者用同一片 board 上的双 camera 模组。如果是普通 USB 双摄像头动目标测距误差大是正常的不要慌这不是算法的锅。# 实时测距循环基于 predict_one_point 的改造示例 cap_left cv2.VideoCapture(0) cap_right cv2.VideoCapture(1) # 如果不是双目模组尽量在程序启动时同步一下帧减少错位 while True: ret_l, frame_l cap_left.read() ret_r, frame_r cap_right.read() # 帧率不匹配时丢帧 if frame_l is None or frame_r is None: continue distance predict_one_point(frame_l, frame_r, calib_file) draw_box_and_distance(frame_l, distance) cv2.imshow(stereo_distance, frame_l) if cv2.waitKey(1) 0xFF ord(q): break5. 避坑记录标定、视差、坐标映射的 5 个翻车点这一章是最值钱的部分。我拆过不少双目测距项目包括有些商业项目里报上来的「测距不准」问题最后定位下来全是这些低级错误。每条都按「现象 → 原因 → 解决」写清楚。坑 1测出来的距离整体偏小且按比例偏移现象实际距离 5 米程序输出 4.6 米而且越远偏得越多。 原因基线 b 用的值和标定值不一致。可能是你换过相机安装位置或者标定完 T 向量重新读的时候把单位搞错了。 解决重新跑一次stereoRectify确认Q矩阵里-T[0]对应的基线和物理毫米数一致。比如你量出来两个相机光心间距离是 119mm标定结果是 121mm说明标定照片数量不够补拍重新标定。坑 2近距离目标测出负数距离现象目标离相机 0.5 米程序输出 -0.3 米或者直接抛异常。 原因视差为负。左图中心点在 x500但右图匹配点跑到 x520 去了说明极线搜索范围没包含真实匹配点或者搜索范围内有干扰纹理导致匹配跳变。 解决扩大搜索范围search_range从 80 加到 160 试试。如果还不行检查是行了没对齐则重新做 stereoRectify如果行是对齐的可能是目标在画面边缘另一台相机被遮挡看不到那个点可以加一个「匹配质量阈值」质量太低就不输出距离。坑 3YOLOX 检测框在左图正常但右图对应位置是一片空白纹理缺失现象目标是一面白墙或纯色车门块匹配搜出来的位置看起来没问题但视差在目标内部剧烈跳动。 原因低纹理区域是立体匹配的天然死穴中心点恰好落在低纹理区时任何匹配方法都会退化。 解决两个思路一是把中心点改为检测框内「纹理丰富点」的加权质心比如先用 Sobel 算子计算局部梯度在框内找梯度最大的点作为测距点二是对检测框内所有有效视差取中位数而不是用单个中心点。中位数对异常点鲁棒得多。坑 4运行时提示 OpenCV 的 remap 报错或输出图像全黑现象cv2.remap执行时报错map1 is not a valid map或者校正后图像有一半是黑的。 原因initUndistortRectifyMap输出的映射表是CV_32FC1类型但你后续用 PIL 或 matplotlib 显示时转格式转错了或者size参数和当前图像尺寸不一致。 解决确认你在stereoRectify里传的size是你实际图像的分辨率。如果标定用的图像是 640×480但运行时的摄像头输出是 1920×1080映射表尺寸就对不上。统一在代码开头做一次resize或者重新生成映射表。坑 5训练 YOLOX 时voc_annotation.py生成的 txt 里路径全是错的现象训练到一半报FileNotFoundError或者2007_train.txt里的路径指向了不存在的位置。 原因voc_annotation.py生成的 txt 里用的是绝对路径但生成时的工作目录和你后续跑train.py时的工作目录不一样。 解决把voc_annotation.py里的输出路径改成相对路径或者用os.path.abspath动态生成。这个坑在项目里特别常见因为文件包里已经带了一份2007_train.txt但那是作者在打包前的路径你解压后目录结构一变就全废了。6. 验证测距精度用三点预测脚本对照真值校准6.1 为什么单点测距不够三点测距能看出什么最后要说的是项目里那个容易被忽视的predict_three_point.py。它的作用是在同一个目标检测框内取左上、中心、右下三个点分别测距然后输出三个距离值。为什么要这么做因为单点测距只能告诉你「这个目标上的某个点离我多远」但如果目标是一个斜对着相机的物体比如侧放的汽车车头离你近、车尾离你远单点测距的结果无法代表整个目标的位置。三点测距的意义在于你可以用这三个距离值的差异来判断目标的朝向。如果三个点距离接近说明目标正对着相机如果左点远、右点近说明目标在朝左前方倾斜如果三个值差距超过 20%说明 YOLOX 框选的目标不是大平面物体测距结果需要谨慎使用。# predict_three_point.py 中的三点测距逻辑简化示意 def predict_three_point(box): # box 是 YOLOX 输出的检测框 [x1, y1, x2, y2] h box[3] - box[1] w box[2] - box[0] # 三个测距点左上 20% 处、中心、右下 20% 处 points [ (box[0] 0.2 * w, box[1] 0.2 * h), (box[0] 0.5 * w, box[1] 0.5 * h), (box[0] 0.8 * w, box[1] 0.8 * h) ] distances [] for px, py in points: disparity match_point_on_right(px, py) # 极线搜索得到视差 if disparity 0: distances.append(None) else: distances.append(compute_depth(disparity)) # 返回三点的距离列表以及中位数作为目标距离估计 valid_d [d for d in distances if d is not None] median_d np.median(valid_d) if valid_d else float(inf) return distances, median_d调这个脚本时要注意坐标点的比例系数不是固定的。0.2 和 0.8 是经验值如果你的目标在画面里很小比如 30×30 像素取 0.2 和 0.8 会让左右点太靠近边缘边缘处的畸变校正可能不完美匹配质量差。这种情况下把系数改成 0.3 和 0.7 会更稳。6.2 用卷尺和标定板建立「真值表」精度验证不能靠感觉。我的习惯是找一个走廊或者空旷场地每隔 0.5 米做一个标记用卷尺量出真实距离然后拿着设备在每个距离上按一次快门记录程序输出的测距值。最后把两组数据做成对比表。真实距离 (m)单点测距输出 (m)三点测距中位数 (m)误差百分比1.00.930.967% / 4%2.01.871.916.5% / 4.5%3.53.223.318% / 5.4%5.04.514.669.8% / 6.8%从表里能看出一个规律距离越远误差百分比越大——这符合视差测距的物理特性因为视差本身是固定分辨率下离散的距离越远视差越小量化误差占比越高。如果你的输出在近距离1 米以内误差超过 10%优先怀疑基线标定如果远距离误差超过 15%优先怀疑 SGBM 的numDisparities设置太小导致视差被截断。一个实用的校准小技巧利用输出误差和真实距离的近线性关系做一次最小二乘拟合把输出的距离值反算修正一遍。比如你发现输出值大约是真实值的 0.93 倍就直接在代码里乘一个1/0.93 ≈ 1.075的系数能抹掉大部分系统偏差。这个修正系数只对你当前这套相机安装结构有效换过相机位置就要重新标定。从那以后我每次做双目测距项目都会强制走一遍这套流程先标定再跑单点验证 1 米和 3 米两个距离成立后上三点测距看目标朝向最后拉一条真值表确认误差曲线。看着多花了一个小时但省掉了后面被「测距不准」这种看起来无从下手的反馈反复纠缠的麻烦。双目测距不算玄学误差来源是能被拆开的找到那个主导项就不慌了。希望帮到你。本文还有配套的精品资源点击获取