ARTICLE DETAIL

资讯详情

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

双目+YOLOX测距:从相机标定到目标距离输出的工程实战

双目+YOLOX测距:从相机标定到目标距离输出的工程实战 简介这份资源是一套基于YOLOX目标检测算法的双目相机测距完整项目源码面向计算机视觉学习者、科研人员及机器人、自动驾驶等领域的开发者。项目整合了图像采集、预处理、目标检测、立体匹配、视差计算与距离转换全流程并针对YOLOX的Anchor Free特性和双目标定、立体匹配等关键环节提供了详细注释便于快速上手和二次开发。压缩包共47个文件以31个Python源码文件为核心辅以测试图片bmp/jpg、标注文件与训练配置txt、说明文档md及日志记录整体仅2.12MB轻量且结构清晰。目前已有208人学习实践价值突出。通过具体实战案例读者可系统掌握双目相机标定、YOLOX模型应用及测距参数调优方法为机器人导航、三维重建等场景提供可扩展的参考实现。1. 双目YOLOX测距检测框只告诉你“是什么”不告诉你“有多远”一个只做目标检测的系统输出的是二维框坐标和类别比如“行人 0.87、汽车 0.92”。但你问它这个目标离相机几米它答不上来——没有深度信息二维检测框只能用来画框不能用来做避障、抓取和测速。这个项目把两件事拼到一起左右两个普通USB相机组成双目系统通过视差算出每个像素的深度再用YOLOX目标检测算法把“人、车、箱子”从画面里框出来把检测框中心映射到深度图上最终输出“目标类别 目标距离”。这套东西不需要激光雷达不需要结构光成本就是两台几百块的工业相机加一个标定板特别适合机器人避障、工厂抓取、智能交通这类场景落地。你要从头复现核心是三段工序双目相机标定、立体匹配生成深度图、YOLOX检测结果与深度图融合。源码包给了你一个完整骨架但真正让距离读数从“跳得没法看”到“上下游标一致”靠的是你亲手调的那几步。2. 双目测距原理与标定基础视差公式、基线选型和YOLOX在这个系统里的位置2.1 视差公式和三个决定测距精度的参数双目测距不是玄学核心就是三角形相似。左右相机同时拍到同一个目标点这个点在左图和右图里的水平位置不一样这个差值叫视差d单位是像素。只要知道相机焦距f和两个相机光心之间的距离基线B距离Z就是Z f × B / d这个公式就是整个项目的“地基”。f的单位是像素不是毫米B的单位是米d的单位是像素算出来的Z是米。三个参数对测距精度的作用完全不同值得先想清楚再动手参数作用调大之后的影响焦距 f放大视差视差变大近距离精度变好但视场变窄基线 B放大视差视差变大远距离精度变好但相机重叠区域变小视差 d计算的输入视差误差1个像素距离误差随距离平方增长这里有个反直觉的结论视差误差一个像素在远距离会被放大到吓人的程度。拿一个室内场景举例假设f600像素B12厘米目标在10米外时视差只有7.2像素这时候视差错1个像素距离直接偏1.39米。所以双目测距系统最怕的不是分辨率不够而是视差算不准。很多新手拿到源码包先急着跑检测回头发现距离乱跳还以为是YOLOX的问题——实际上八成是视差图质量太差。2.2 相机标定到底标了什么内参、外参、畸变双目系统出厂时每个镜头都有自己的“脾气”焦距不完全一致、光心不在图像正中心、镜片有径向和切向畸变。两个相机装到支架上之后光轴也不是绝对平行。标定要做的事就是把这些参数全部量化出来内参fx、fy焦距像素单位、cx、cy光心畸变k1、k2、p1、p2、k3外参R右相机相对于左相机的旋转矩阵、T平移向量内参和畸变解决“这个镜头拍出来的图像怎么扭曲”的问题外参解决“左右两个相机之间是什么空间关系”的问题。有了这些才能做极线校正让左右图像中同一个目标点落在同一行上这样立体匹配只需要在水平方向搜索计算量大幅下降。为什么要自己标而不是用厂家出厂参数因为USB相机的出厂参数是给“显示”用的不是给“测量”用的误差可能有好几个像素。误差在显示上无所谓在测距里就是前面算过的——1个像素的视差抖动10米外能差一米多。2.3 为什么选YOLOX目标检测算法接入双目系统的边界YOLOX在这类项目里出现频率很高原因不是它比YOLOv5、YOLOv8都强而是它在精度和部署灵活性之间比较均衡。YOLOX是Anchor-free结构解耦头把分类和回归分开对遮挡目标和小目标的表现比同样体量的Anchor-based模型要稳一些。它的输出层有三个尺度分别对应原图下采样8倍、16倍、32倍的特征图覆盖了从近距离大目标到远距离小目标的范围这正好匹配双目测距“既要看近处的箱子也要看远处的行人”这个需求。YOLOX接入双目系统有个天然的工程优势它输出的是普通的边界框坐标不需要额外做关键点检测也不依赖单目深度估计那种“先分割再算深度”的复杂管线。检测器只负责回答“目标在哪里、是什么”深度信息完全由双目视差图提供两个模块解耦。这意味着你换检测模型或者换深度算法互相不影响排查问题的时候不用上下文来回翻。还要说清边界YOLOX在GPU上推理可以跑实时但双目立体匹配的SGBM算法是CPU计算两块一叠加系统帧率往往瓶颈不在检测而在SGBM。做架构设计的时候不用追求两边都上高分辨率检测输入640×640、视差图用640×480是最常见的取舍。2.4 项目源码的典型目录与运行顺序拿到这类源码包先不要急着双击demo。按我的落地习惯先顺着代码结构确认三个模块有没有齐标定模块生成相机参数文件、立体匹配模块生成深度图或视差图、检测融合模块把YOLOX的输出和深度图对齐。正常项目的运行顺序是标定在前立体匹配在中检测融合最后。标定产出的参数文件一般是一个像 stereo_params.npz 或 stereo_params.json 这样的存档里面存了内参、畸变、R、T和校正映射表后面两个模块启动时直接加载这个文件不需要每次开机重新标。源码包里最容易被忽略的是标定用的图片采集脚本。很多人以为标定是“一次性工作”实际上换镜头、换相机、动过支架都必须重新标。没有这个意识就会出现“昨天跑得好好的今天整个距离读数全偏”的翻车现场。3. 双目相机标定实操用OpenCV把左右相机内参、畸变和R、T一次标齐全3.1 标定板与采集规范覆盖视场、俯仰角、光照标定板用7×9棋盘格、方格边长2030毫米比较顺手。打印出来的A4纸不要直接拿着标纸会弯标出来的畸变参数会被纸面的弯曲带偏。把纸贴到硬塑料板或铝板上保证平整。另外棋盘格必须是“不对称”的比如7×9这样程序能判断棋盘的方向不会转个90度就分不清左右。采集左右图像对时要遵守几条硬规矩左右相机各拍2030张棋盘格在画面里的位置必须覆盖四角和边缘每个姿态都要让棋盘有俯仰、旋转和远近变化不能只拍正对相机的保证棋盘表面不反光光照均匀。我见过太多人在这里偷懒拍10张全是同一个角度标出来的内参畸变参数看着重投影误差挺小一上实测距离全跑偏——这是标定里最典型的“假收敛”。3.2 OpenCV标定脚本单目标定双目标定极线校正一起做下面这个脚本把“单目标定→双目标定→极线校正→保存参数”串成一条流水线是这类项目最常见的组织方式。运行前把左右图像放在left/和right/两个目录图像命名一一对应比如left_01.jpg对应right_01.jpg。import cv2 import numpy as np import glob # 7x9棋盘格每格25mm CHECKERBOARD (7, 9) square_size 0.025 # 25mm 0.025m # 生成棋盘格的世界坐标系坐标z0平面 objp np.zeros((CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32) objp[:, :2] np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1, 2) objp * square_size objpoints [] # 三维世界坐标 lpts [] # 左图角点像素坐标 rpts [] # 右图角点像素坐标 left_imgs sorted(glob.glob(left/*.jpg)) right_imgs sorted(glob.glob(right/*.jpg)) for lf, rf in zip(left_imgs, right_imgs): lgray cv2.imread(lf, cv2.IMREAD_GRAYSCALE) rgray cv2.imread(rf, cv2.IMREAD_GRAYSCALE) lret, lcorners cv2.findChessboardCorners(lgray, CHECKERBOARD, None) rret, rcorners cv2.findChessboardCorners(rgray, CHECKERBOARD, None) if lret and rret: objpoints.append(objp) lpts.append(lcorners) rpts.append(rcorners) # 1. 单目标定分别得到左右相机内参和畸变 _, K1, D1, _, _ cv2.calibrateCamera(objpoints, lpts, lgray.shape[::-1], None, None) _, K2, D2, _, _ cv2.calibrateCamera(objpoints, rpts, rgray.shape[::-1], None, None) # 2. 双目标定固定上面标好的内参只优化R和T flags cv2.CALIB_FIX_INTRINSIC criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-5) rms, K1, D1, K2, D2, R, T, E, F cv2.stereoCalibrate( objpoints, lpts, rpts, K1, D1, K2, D2, lgray.shape[::-1], criteriacriteria, flagsflags) # 3. 极线校正把左右图拉成行对齐 R1, R2, P1, P2, Q, roi1, roi2 cv2.stereoRectify( K1, D1, K2, D2, lgray.shape[::-1], R, T, alpha0) # 4. 生成校正映射表后续直接用 remap 应用 map1x, map1y cv2.initUndistortRectifyMap(K1, D1, R1, P1, lgray.shape[::-1], cv2.CV_32FC1) map2x, map2y cv2.initUndistortRectifyMap(K2, D2, R2, P2, lgray.shape[::-1], cv2.CV_32FC1) np.savez(stereo_params.npz, K1K1, D1D1, K2K2, D2D2, RR, TT, R1R1, R2R2, P1P1, P2P2, QQ, map1xmap1x, map1ymap1y, map2xmap2x, map2ymap2y) print(f重投影误差RMS: {rms:.3f}) print(f基线长度: {np.linalg.norm(T):.3f} m)这段脚本做了四件事提取角点、单目标定内参、双目标定外参、生成校正映射表。关键参数里CHECKERBOARD必须和实际打印的棋盘完全一致写反了角点检测直接失败flagscv2.CALIB_FIX_INTRINSIC的意思是内参已经由单目标定确定双目标定只优化R和T这样双目标定的数值更稳定不会出现内参和外参互相“抢误差”的情况alpha0会让校正后的图像裁剪掉黑色边角损失一点点视场但深度图更干净工业项目里我倾向保留alpha0。3.3 标定结果怎么才算好重投影误差、基线长度、极线校正验证标定脚本跑完先看两个数字。重投影误差RMS小于0.5像素说明角点提取和相机模型是自洽的——但注意RMS小不代表一定准前面说的“只拍正面姿态”也能得到很小的RMS但参数是脱离实际的。另一个是基线长度T的模长应该和你实际用尺子量的两相机光心距离接近差太多就说明标定板姿态太少外参没有收敛到真实物理关系上。标定完可以做一次极线校正验证校正后的左右图像上同一个角点的y坐标差应该在0.5像素以内。这个验证比看重投影误差更有说服力因为极线校正是立体匹配的地基地基歪了SGBM再怎么调参都白搭。4. 立体匹配与YOLOX目标距离提取从SGBM视差图到每个目标的米数读数4.1 从SGBM视差图开始关键参数和初始值立体匹配的目的就是求出每个像素的视差。OpenCV里最实用的是SGBM半全局块匹配它是BM和Elas之间的一个平衡点精度够用、CPU能跑、参数可解释。SGBM有九个参数要设堪称调参集中营但真正影响结果的只有下面这几个参数作用建议初始值numDisparities最大视差范围必须是16的倍数64或96minDisparity最小视差偏移0blockSize匹配窗口大小必须为奇数5或7P1、P2视差平滑惩罚P2 P1P18×通道数×窗口面积P232×通道数×窗口面积uniquenessRatio唯一性比率防止误匹配510speckleWindowSize去噪窗口大小100200speckleRange去噪允许的视差差1或2这里最容易翻车的设定是blockSize。窗口越大低纹理区域越不容易出现空洞但目标边缘的视差会被抹平检测框边缘的距离读数会偏。室内场景我一般从blockSize5起步图像噪声大再往上加。P2不要一开始就给太大否则目标边缘会全部被平滑成背景视差目标轮廓和背景融为一体YOLOX框出来的中心点深度值会被背景“带歪”。4.2 深度图预处理无效视差、边缘空洞和左右一致性检查SGBM算出来的原始视差图有两个常见毛病一是遮挡区域和低纹理区域视差为负或为零这些点对应无效值二是目标边缘有飞点噪声表现为距离突然跳变。直接拿原始视差图去算距离输出一定抖。两类处理是必须做的。第一是视差有效性检查把小于等于0的视差直接当无效值丢弃第二是左右一致性检查LRC即把右图作为基准再算一遍视差两次结果差的绝对值超过一个阈值就认为这个点匹配不可靠。OpenCV的StereoSGBM没有直接提供LRC选项需要自己实现一次双向匹配或者用WLS滤波替代一部分效果。WLS需要opencv-contrib-python好处是能对上边缘保持坏处是多一层参数要调我看到不少源码包里直接集成了WLS但如果你只是想先把流程跑通中值滤波就够了。4.3 把检测框映射到深度图取中值比取中心点靠谱检测框给出的是像素坐标(x1, y1, x2, y2)深度图是同一坐标系下的视差图。这一步最大的坑是“直接用中心点取视差”目标中心点可能落在纹理缺失区域也可能落在边界上单点取值会让距离读数一帧一个样。正确做法是在中心点周围开一个小窗口取窗口内有效视差的中位数没有有效值就扩大窗口再试一次最终仍未找到就用上一帧的结果顶着这比插值填充可靠得多。4.4 SGBM计算与YOLOX测距融合的最小实现import cv2 import numpy as np def compute_depth_maps(lframe, rframe, stereo_params): 输入左右校正图输出float32视差图单位像素 # 用标定阶段保存的map做像素级校正 l_rect cv2.remap(lframe, stereo_params[map1x], stereo_params[map1y], cv2.INTER_LINEAR) r_rect cv2.remap(rframe, stereo_params[map2x], stereo_params[map2y], cv2.INTER_LINEAR) # 转灰度SGBM只吃单通道 lg cv2.cvtColor(l_rect, cv2.COLOR_BGR2GRAY) rg cv2.cvtColor(r_rect, cv2.COLOR_BGR2GRAY) # h, w lg.shape sgbm cv2.StereoSGBM_create( minDisparity0, numDisparities64, blockSize5, P18 * 1 * 5 * 5, P232 * 1 * 5 * 5, uniquenessRatio10, speckleWindowSize150, speckleRange2, modecv2.STEREO_SGBM_MODE_SGBM) disp sgbm.compute(lg, rg).astype(np.float32) / 16.0 return disp这里的numDisparities64表示最多搜64个像素的视差对应多少米的最近距离要看f和B不是随便选的。以fx600、B0.12m为例最近可测距离Z_min f×B/64 ≈ 1.1米如果目标只有0.5米远就要把numDisparities加到128。SGBM输出的原始视差是整数乘以16编码的所以/16.0这步不能漏。P18*1*5*5里的1是灰度图的通道数5×5是blockSize面积这个公式是OpenCV给的参考起点。接下来是YOLOX检测结果和视差图融合的代码。假设你已经通过ONNX把YOLOX的原始输出解码成了boxesdef boxes_to_distance(boxes, scores, class_ids, disp, fx, baseline): 把YOLOX的检测框转成带距离的结果 results [] h, w disp.shape # 检测框坐标是原图分辨率视差图可能做了缩放这里假设同分辨率 for box, score, cls in zip(boxes, scores, class_ids): x1, y1, x2, y2 [int(v) for v in box] cx, cy (x1 x2) // 2, (y1 y2) // 2 # 以中心点为中心开一个7x7窗口取有效视差的中位数 margin 3 roi disp[max(0, cy-margin):cymargin1, max(0, cx-margin):cxmargin1] valid roi[(roi 0) np.isfinite(roi)] if len(valid) 0: continue # 窗口里没有有效视差丢掉这一帧的该目标 d_median np.median(valid) if d_median 0: continue distance fx * baseline / d_median results.append({ class_id: int(cls), score: float(score), distance_m: float(distance), box: [x1, y1, x2, y2], }) return results取7×7窗口中位数而不是中心点目标边缘哪怕有几行被背景视差污染中位数也能抓住目标主体。fx和baseline来自标定参数fx直接用K1[0,0]K1保存在stereo_params.npz里。窗口里没有有效视差时宁可直接跳过也不要拿上一帧或远距离视差补位否则系统静置时距离会在几个值之间来回跳比丢一帧还难受。4.5 YOLOX的推理接入ONNX加载、输入预处理和输出解码YOLOX的推理代码在整个项目里其实是最不用费心的部分。常见做法是把PyTorch模型导出成ONNX再用onnxruntime加载。用ONNX而不是直接PyTorch跑好处是部署时不需要装PyTorch而且ONNX Runtime在CPU上的推理速度比PyTorch原版更快这对双目系统很关键——GPU要留给视觉处理或者干脆没有GPU。输入预处理要遵循YOLOX的训练设定letterbox缩放保持宽高比640×640输入像素除以255归一化。输出是一个形状为(1, 8400, 85)的张量8400是三个尺度特征图上候选框的总数854个坐标1个目标度80类COCO类别得分。解码时跟YOLOv5类似按stride把网格坐标换算回原图坐标先过滤目标度阈值再按类别做NMS。需要说明的是如果你的源码包用的是自定义训练的YOLOX权重类别数变了最后一个维度就不是85解码时的类别偏移量要跟着改这是新手最容易忽略的地方。5. 双目YOLOX测距避坑指南项目源码跑通后最常翻车的5个排查点5.1 现象一目标静止但距离读数一帧一个样目标不动输出距离在2.5米和3.0米之间反复跳这不是传感器坏了。原因有两个一是YOLOX检测框在抖动中心点像素跟着动二是SGBM在目标边缘产生飞点中心窗口里的中位数受到了边界值干扰。解决分两层。先给检测框做时间平滑最简单的做法是保存上一帧的中心点当前帧中心点距离超过10个像素就认为目标发生了大幅移动否则取两帧平均值。再对距离输出做滑动平均滤波窗口取5帧响应延迟在200毫秒以内对人机交互和低速避障完全够用。5.2 现象二标定重投影误差很小距离却系统性偏近或偏远重投影RMS只有0.2但实际目标在3米外系统测出来2.6米而且误差方向一致。这个现象十有八九是标定板没贴平或者标定姿态覆盖不够。纸面稍微弯曲畸变参数就会把真实镜头的畸变“吸收”一部分重投影误差看着不大但视差和真实几何关系已经对不上了。解决是把标定板换成熟料板重新采集保证每个姿态的棋盘格在画面中占比不低于三分之一并且至少包含两个俯仰角超过30度的姿态。重标完再看T的模长和实际基线是否符合偏差超过10%基本可以断定标定板或姿态有问题。5.3 现象三低纹理区域视差图大片黑色或雪花墙面、地面、纯色纸箱这类地方左右图局部内容长得一模一样SGBM搜不到可靠的对应点输出就是无效视差。这是SGBM的物理极限不是参数没调好。解决思路按性价比排序先调大blockSize到7或9让匹配窗口覆盖更多纹理结构再调大P1和P2的比例让视差过渡更平滑如果还不行在跑SGBM之前对灰度图做一次直方图均衡化把对比度低的区域拉出纹理来。要注意直方图均衡化会影响视差精度的绝对值你的目标不是要“好看”的视差图而是要检测框中心附近有效的距离读数所以均衡化只建议用在墙上目标占比高的场景。5.4 现象四:检测框框住了目标但目标距离读成背景距离行人站在墙前面系统报出来是墙的距离。原因在SGBM的视差平滑惩罚上P2太大时目标边缘行人轮廓的视差会被强制拉成和背景一致目标在深度图里“融化”掉了中心点取到的中位数自然接近背景。解决是把P2调小让视差图在目标边缘保留更多的突变同时把取值的窗口从7×7缩到5×5降低背景对中位数的污染。注意blockSize5时P232×1×5×5800P2设成这个值的三分之一到二分之一试试图像噪声大的时候再往回加。5.5 现象五目标一动测距立刻失真目标静止时读数基本准移动起来距离就乱。这个现象的根子是左右相机不是全局快门卷帘快门导致左右图同一时刻拍到的目标位置不一致移动目标在左右图上的视差不再是真实的水平偏移SGBM自然匹配错。特别是行人和车辆这类横向运动的物体车速越快失真越明显。解决优先换带硬触发接口的全局快门工业相机左右相机接同一个触发信号同时曝光。硬件条件不满足的情况下只能限制测距目标的速度或者在程序里根据检测框的移动速度做视差补偿——后者属于亡羊补牢能改善但做不到精确。6. 精度验证三板斧棋盘格对比、重复性测试和距离修正曲线系统跑通后先别急着“能用就行”用三个手段把精度底细摸清楚。第一招是棋盘格验证。把标定板放到已知距离上比如1米、2米、3米各拍一次程序里检测棋盘格角点取棋盘中心点的深度值和激光测距仪或卷尺做对比记录偏差。这一步能快速判断是标定问题还是SGBM参数问题三个距离偏差如果不成规律先查标定如果偏差随距离线性增大基本可以判断是视差偏移或者基线不准。第二招是重复性测试。目标不动连续跑10秒统计同一个目标距离输出的均值和标准差。标准差除以均值得到变异系数电工场景要求低于5%机器人抓取场景建议低于2%。标准差偏大就回到第5章的滤波方案把平滑强度加上来。第三招是距离修正曲线。如果你的场景是固定安装比如生产线传送带可以放三个已知距离的标定物记录系统输出距离用一条线性或二阶曲线拟合真实距离和输出距离的映射关系在代码里输出前做一次修正。这本质上是把系统性误差吃掉一部分不改变视差计算逻辑但能让最终读数更可信。我自己每次标定完都会把标定板照片、重投影RMS、基线的物理实测值一起存档下次换镜头或者调整支架位置后先对照旧参数看看变化幅度很多“昨天正常今天全偏”的问题都能从这里找到答案。这个习惯帮我避开了不少没来由的翻车。希望帮到你。本文还有配套的精品资源点击获取
返回列表