☰
YOLOv5双目测距实战:从标定到三维坐标的完整链路
2026/10/25 2:59:04 网站建设 项目流程

简介:面向计算机视觉学习者与工程实践者的三维测距实战资源,将YOLOV5目标检测与双目相机深度感知结合,解决复杂场景下目标定位与空间测距的精度和实时性问题,适用于自动驾驶、机器人导航、无人机避障等方向的中级进阶练习。压缩包共141个文件,约40.38MB,以37个py源码、40个yaml配置、22个pyc编译文件为主,辅以yml环境配置、sh脚本、md文档及少量xml、jpg样例与pt权重、mp4演示,覆盖数据预处理、模型训练、目标检测与深度计算全流程。已有2420人学习下载。资源内含整合YOLOV5与双目算法的源代码、预训练权重、相机与模型配置文件、双目图像对样例及使用说明,可帮助读者理解视差计算与立体匹配的衔接方式,快速复现检测加测距的完整链路,并在此基础上做二次训练与鲁棒性优化。

1. 双目测距落地:为什么 YOLOv5 加两个镜头就能出三维坐标

单目画面里一个检测框只能告诉你“那里有东西”,给不了“离我多远”。双目测距补的就是这一维:左右两个镜头同时拍,同一个目标在两张图上的像素位置有差异,这个差异叫视差,视差越大目标越近,用焦距、基线距离和视差一套三角关系就能反推深度。YOLOv5 负责在左图上把目标框出来,双目几何负责把框中心换算成米。这套组合在分拣、避障、体积粗测这类场景里很常见,因为它不需要昂贵的深度相机,两个标定好的普通镜头加一块能跑推理的板子就能起步。这份资源把检测、标定、立体匹配、坐标反投影串成一条可复现的链路,适合已经能跑通 YOLOv5 推理、想往三维空间再走一步的从业者。下面按“先立住几何、再动手复现、最后盯住坑”的顺序拆。

2. 双目几何与标定:把像素视差换成真实米数

2.1 三角测量公式到底在算什么

双目测距的核心只有一个式子:Z = f × B / d。Z 是目标到相机平面的深度,f 是焦距(像素单位),B 是两个镜头光心之间的距离叫基线,d 是同一个点在左右图上的横坐标差即视差。这个式子成立的前提是左右图已经过极线校正,两个镜头的成像平面共面、光轴平行,此时同一个目标点只在横坐标上有位移,纵坐标基本一致,找对应点就退化成沿同一行搜索,工程上才好做。

焦距用像素单位而不是毫米,是因为视差 d 本身就是像素差,单位要统一。f 的像素值等于物理焦距除以像元尺寸,标定出来的内参矩阵里 fx 就是它。基线 B 用米或毫米都行,只要和最终想要的 Z 单位一致。举个能抄去验算的数:fx = 800 像素,B = 0.12 米,某目标视差 d = 40 像素,则 Z = 800 × 0.12 / 40 = 2.4 米。视差掉到 20 像素,Z 就翻到 4.8 米。这就是双目测距的脾气——远处一点点视差误差会被放大成很大的距离误差,所以这套方案在近中距离靠谱,几十米外精度会明显下滑。

还有两个容易忽略的量。一是主点 cx、cy,反投影到相机坐标系时要用:X = (u - cx) × Z / fx,Y = (v - cy) × Z / fy,u、v 是目标在左图的像素坐标。二是畸变系数,广角镜头畸变不校正,视差匹配会系统性偏移,深度就整体偏。所以标定不是走形式,它直接决定后面所有数字的可信度。

2.2 标定流程:棋盘格、参数与验收标准

标定分两步:单目标定拿内参和畸变,双目标定拿右相机相对左相机的旋转平移,平移向量的模长就是基线 B。常见做法是用棋盘格,左右相机同时拍同一块板子的多组姿态,每组两张图,一般 15 到 25 组比较稳,板子要覆盖画面四角和中心,倾斜角度也要有变化,全平着拍标出来的参数会偏。

下面这段是 OpenCV 双目标定的骨架,参数含义写在注释里,可以直接改成自己的图路径跑:

import cv2 import numpy as np import glob # 棋盘格内角点数,注意是内角点不是格子数,10x7 的板子填 (9, 6) pattern = (9, 6) square_size = 0.025 # 每格边长,单位米,必须和实际板子一致 objp = np.zeros((pattern[0] * pattern[1], 3), np.float32) objp[:, :2] = np.mgrid[0:pattern[0], 0:pattern[1]].T.reshape(-1, 2) * square_size objpoints, imgpoints_l, imgpoints_r = [], [], [] left_imgs = sorted(glob.glob("calib/left/*.png")) right_imgs = sorted(glob.glob("calib/right/*.png")) for lp, rp in zip(left_imgs, right_imgs): gl = cv2.imread(lp, cv2.IMREAD_GRAYSCALE) gr = cv2.imread(rp, cv2.IMREAD_GRAYSCALE) ok_l, cl = cv2.findChessboardCorners(gl, pattern, None) ok_r, cr = cv2.findChessboardCorners(gr, pattern, None) if not (ok_l and ok_r): continue # 任一侧没找到就整组丢弃,不能只留单边 crit = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) cl = cv2.cornerSubPix(gl, cl, (11, 11), (-1, -1), crit) cr = cv2.cornerSubPix(gr, cr, (11, 11), (-1, -1), crit) objpoints.append(objp) imgpoints_l.append(cl) imgpoints_r.append(cr) # 单目标定,flags 里关掉不稳定的高阶项,样本少时更稳 ret_l, K1, D1, _, _ = cv2.calibrateCamera(objpoints, imgpoints_l, gl.shape[::-1], None, None, flags=cv2.CALIB_FIX_K3) ret_r, K2, D2, _, _ = cv2.calibrateCamera(objpoints, imgpoints_r, gr.shape[::-1], None, None, flags=cv2.CALIB_FIX_K3) # 双目标定,输出 R、T,T 的模长就是基线 ret, K1, D1, K2, D2, R, T, E, F = cv2.stereoCalibrate( objpoints, imgpoints_l, imgpoints_r, K1, D1, K2, D2, gl.shape[::-1], flags=cv2.CALIB_FIX_INTRINSIC, criteria=(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-5)) print("baseline(m):", np.linalg.norm(T)) print("reprojection error:", ret) # 重投影误差,单位像素

逻辑说明:先对每组图找角点并做亚像素细化,左右都成功才收进样本,避免单边样本污染;单目标定拿各自内参,双目标定用CALIB_FIX_INTRINSIC固定内参只优化外参,这样内外参不会互相拉扯。参数上square_size必须和实物一致,填错会让基线整体缩放,深度全错;pattern填内角点,填成格子数是最常见的翻车点。

验收看两个数:重投影误差一般要压到 0.5 像素以内,超过 1 像素说明样本质量差或板子不平;基线norm(T)要和卷尺量的两镜头光心距离对得上,差太多说明标定姿态不够丰富。标完还要用stereoRectify生成校正映射,后面所有图都先过一遍remap,保证极线水平,这一步不做,视差搜索就会错行。

3. YOLOv5 检测与视差匹配:把框中心变成深度值

3.1 检测框怎么和视差图对齐

YOLOv5 在左图上出框,拿到框中心 (u, v)。视差图是整幅图逐像素算出来的,直接取 (u, v) 处的视差值有两个风险:一是框中心可能落在目标内部无纹理区域,视差是空洞;二是遮挡边缘视差不可靠。稳妥做法是在框内取一个内缩区域,比如宽高各缩 20%,统计该区域内有效视差的中位数,空洞像素(值为 0 或负)先剔除。中位数比均值抗离群,边缘跳变不会把结果带偏。

这里有个顺序问题必须说清:视差匹配用的是校正后的左右图,检测用的是左图,两者必须是同一帧、同一校正参数。如果检测跑在原图上、视差跑在校正图上,坐标就对不上,深度会系统性偏移。我一般把校正后的左图同时喂给检测和匹配,保证像素坐标系一致。

3.2 视差计算:SGBM 参数怎么调

立体匹配常用 SGBM,参数多但真正影响结果的就几个。下面这段把校正、匹配、取框内中位数串起来:

import cv2 import numpy as np # 校正映射,标定阶段算好后存下来复用 map1x, map1y, map2x, map2y, Q = ... # 由 stereoRectify + initUndistortRectifyMap 得到 # SGBM 参数:numDisparities 必须是 16 的倍数,blockSize 必须是奇数 num_disp = 128 # 最大视差搜索范围,按最近目标距离估,太小近处会截断 block_size = 5 # 匹配块大小,纹理弱调大,边缘糊调小 sgbm = cv2.StereoSGBM_create( minDisparity=0, numDisparities=num_disp, blockSize=block_size, P1=8 * block_size ** 2, P2=32 * block_size ** 2, disp12MaxDiff=1, # 左右一致性检查阈值,压噪声 uniquenessRatio=10, # 唯一性,太低会出大量误匹配 speckleWindowSize=100, # 斑点滤波窗口,去孤立小块 speckleRange=2, mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY ) def depth_of_box(left_rect, right_rect, box, fx, baseline): disp = sgbm.compute(left_rect, right_rect).astype(np.float32) / 16.0 # SGBM 输出放大 16 倍 x1, y1, x2, y2 = box w, h = x2 - x1, y2 - y1 # 内缩 20%,避开框边和遮挡边缘 roi = disp[int(y1 + 0.2 * h):int(y2 - 0.2 * h), int(x1 + 0.2 * w):int(x2 - 0.2 * w)] valid = roi[roi > 0] if valid.size < 10: return None # 有效视差太少,判为不可测,别硬给数 d = float(np.median(valid)) return fx * baseline / d # 反投影到相机坐标系,得到三维点 def to_3d(u, v, Z, fx, fy, cx, cy): X = (u - cx) * Z / fx Y = (v - cy) * Z / fy return X, Y, Z

逻辑说明:numDisparities决定能测多近,视差超过这个范围的目标会被截断,按最近工作距离反推——最近 0.5 米、fx 800、B 0.12 时视差约 192,就得把numDisparities设到 208 以上。blockSize小则边缘锐但噪声多,大则平滑但小目标糊,5 到 11 之间试。uniquenessRatio调低会冒出大量误匹配,调高则弱纹理区大片空洞,10 到 15 是常用区间。SGBM 输出是定点数放大了 16 倍,忘了除 16 深度会小 16 倍,这个坑很经典。

to_3d里 u、v 用框中心,Z 用中位数深度,得到的是目标中心在左相机坐标系下的三维点。要转到世界坐标或机器人坐标,再乘一个手眼标定矩阵,那属于另一层,这份资源聚焦在相机坐标系输出,够大多数避障和抓取粗定位用。

4. 避坑与排查:双目测距最容易翻车的五处

4.1 深度整体偏大或偏小

现象:所有目标测出来都比实际远一截或近一截,比例大致固定。原因多半是基线或焦距单位不一致,比如标定square_size填的是毫米、代码里当米用,或者norm(T)出来是毫米却直接当米代进公式。解决:打印norm(T)和卷尺实测对比,确认单位;square_size统一用米,全链路不再换单位。

4.2 视差图大片空洞、框内取不到值

现象:纹理弱的目标(纯色箱体、白墙)视差几乎全 0,valid.size不够返回 None。原因:SGBM 靠局部纹理找对应点,无纹理区域本就无解。解决:调大blockSize、降uniquenessRatio能缓解但治标;工程上更稳的是给目标表面加纹理,或改用带结构光的方案;代码里必须保留“有效点不足就判不可测”的分支,别用 0 视差硬算出一个无穷大深度。

4.3 检测框和视差图错位

现象:深度值看着合理但明显对应到背景上。原因:检测跑原图、视差跑校正图,或两路用了不同分辨率。解决:统一先remap再同时喂检测和匹配,检测输入尺寸和视差图尺寸对齐,框坐标按同一缩放系数还原。

4.4 远处目标深度跳变严重

现象:5 米内挺稳,10 米外数字乱跳。原因:视差与深度成反比,远处视差只有几个像素,1 像素误差就放大成几十厘米。解决:这是原理性边界,不是 bug;按工作距离选基线,要测远就加大基线或换长焦,同时接受近处盲区变大,二者不可兼得。

4.5 标定重投影误差合格但测距仍偏

现象:stereoCalibrate返回 0.3 像素,实测深度还是差。原因:标定板不平、样本姿态单一,或标定后镜头被碰过。解决:标定板贴平硬板,样本覆盖远近和倾斜;标定完立刻用已知距离的物体做一次实测校验,别只看重投影误差就收工。

5. 精度验证与工程化收尾:一套可复用的自检习惯

资源跑通不等于能用,得有一套验证动作。我一般会做一张距离标定表:把目标放在 0.5、1、1.5、2、3、4 米处,每个距离测 20 帧取中位数和标准差,和卷尺值对比,算出各距离段的绝对误差和波动。这张表能直接告诉你这套双目在你的工作距离内能不能用、误差带多宽。典型结果是近处误差几厘米、远处涨到十几厘米,看到这个曲线心里就有数了。

实测距离测量中位数绝对误差帧间标准差结论
0.5 m0.51 m1 cm0.8 cm可用
1.0 m1.03 m3 cm1.5 cm可用
2.0 m2.08 m8 cm3 cm可用
4.0 m4.3 m30 cm9 cm仅作粗判

工程化上还有两个技巧。一是把标定结果(K、D、R、T、校正映射)序列化存盘,启动时加载,别每次跑都重标,标定参数是这套系统的“黑匣子”,丢了就得重来。二是深度输出加时间中值滤波,连续 5 帧取中位数再上报,能压掉单帧匹配噪声,代价是响应慢一两帧,避障场景完全能接受。三是给不可测目标留后悔药:depth_of_box返回 None 时上层逻辑要有降级策略,比如按上一帧深度外推或直接标记未知,别让一个空洞把整条链路带崩。

血泪经验是标定完一定做一次端到端实测再进现场,我见过重投影误差漂亮但基线量错 2 厘米、整批深度偏 15% 的情况,现场返工成本远高于实验室多花的那半小时。从那以后我每次改完镜头支架或动过相机,都强制重标并跑一遍距离表,确认误差带没漂才继续。希望这套流程帮到你,少走几段弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询