ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

YOLOv5双目测距:极线校正与SGBM框内中值采样实战

YOLOv5双目测距:极线校正与SGBM框内中值采样实战

简介:面向深度学习与计算机视觉学习者的一套 YOLOv5 双目视觉测距可运行工程,重点解决目标检测与立体测距衔接、环境配置和代码跑通问题,适合具备 PyTorch、OpenCV 基础、希望复现实时距离估计的中高级开发者,也可用于自动驾驶、机器人导航等场景的入门实验。压缩包共 106 个文件,约 20.88MB,以 25 个 Python 源码和 52 个编译缓存文件为主体,配有 18 个 YAML 配置、若干 Shell 与说明文档,并包含预训练权重、示例图像/视频及 Dockerfile,覆盖数据处理、模型推理、视差计算与结果可视化等环节。目前已有 2849 人学习下载。工程将 YOLOv5 检测与双目三角测量、立体匹配、视差图计算和距离估计整合为完整流程,读者可据此理解检测—匹配—测距的代码组织,参照配置完成环境搭建与运行验证,并在此基础上改造成自己的双目测距实验。

1. 双目测距最难的不是检测:为什么 YOLOv5 只能算半个方案

把 YOLOv5 和双目相机接在一起,最常见的翻车现场不是框画歪了,而是框画得特别漂亮、距离数字却乱跳:同一把人形靶,这一帧 1.8 米,下一帧 0.6 米,再过两帧直接变成 12 米。多数人第一反应是模型不行,去换 yolov5m、加大输入分辨率,折腾两天发现没有任何改善,因为问题从来不在检测头,在视差图。这套已经跑通的 YOLOv5 双目测距源码,真正有价值的地方不是把检测和测距拼在一起,而是把「检测框 → 极线校正 → SGBM 匹配 → 反投影 → 框内采样滤波」这条链路做成了一个能直接出米制数值的工程,左边画框带距离,右边出伪彩视差图。它适合做前向避障、移动平台测距、课程设计验证这类需要绝对距离而不是「谁离得近」的场景,前提是你得愿意先把标定和视差参数啃下来。

2. 环境与最小闭环:先把框和视差图同时点亮的四步

2.1 依赖组合的取舍:OpenCV 必须带 contrib

这套代码跑不起来的人里,一半死在 OpenCV 版本上。源码里用到了cv2.ximgproc下面的 WLS 视差滤波和加权中值,这部分只在opencv-contrib-python里,装纯opencv-python会在import cv2之后第一次调createDisparityWLSFilter时抛AttributeError: module 'cv2' has no attribute 'ximgproc'。

另一个隐性雷是 numpy。PyTorch 1.x 系列编译时链接的是 numpy 1.x,环境里一旦被别的包顶到 numpy 2.x,import torch会直接报A module that was compiled using NumPy 1.x cannot be run in NumPy 2.0.x。所以我在这个组合上一般会显式钉版本,不让 pip 自由发挥。

# Python 3.9 是这套依赖最稳的组合,3.12 上部分 wheel 还没有 conda create -n stereo-yolo python=3.9 -y conda activate stereo-yolo # 先 torch 后 opencv,顺序反了容易被依赖求解器把 numpy 顶版本 pip install torch==1.13.1 torchvision==0.14.1 # contrib 版本必须和 opencv-python 完全同号,混装会出现符号冲突 pip install opencv-contrib-python==4.8.1.78 numpy==1.23.5 pip install pyyaml tqdm pandas

这里opencv-python不用单独装,opencv-contrib-python已经包含主模块,两个都装反而容易互相覆盖cv2的.so。numpy 钉 1.23.5 的原因是它同时满足 torch 1.13 的 ABI 和reprojectImageTo3D对float32连续内存的要求,再高的版本在部分机器上会触发cv2.error: Layout of the output array。

提示:装完先跑一句python -c "import cv2; print(cv2.__version__, hasattr(cv2, 'ximgproc'))",两个值都对再往下走,能省掉后面一半的排错时间。

2.2 四步跑通最小闭环

源码包里的入口脚本通常叫demo_stereo.py或者main.py,参数命名各家不同,但需要的输入永远是四样:左图、右图、权重、标定文件。第一遍不要碰自己的相机,先用包里自带的示例图对,确认整条链路是通的,再换数据。

# 第一步:确认权重路径,yolov5s 够用,别一上来就上 x ls weights/ # 第二步:用示例图跑一遍,--show 会同时弹检测图和视差图两个窗口 python demo_stereo.py \ --left data/left/0001.png \ --right data/right/0001.png \ --weights weights/yolov5s.pt \ --calib calib/stereo_calib.npz \ --show # 第三步:批量跑一个目录,输出 csv 方便后面统计 python demo_stereo.py --left data/left --right data/right \ --weights weights/yolov5s.pt --calib calib/stereo_calib.npz \ --save-csv out/depth.csv

如果第二步窗口能弹出来但距离全是nan,先别改代码,去看out/下有没有生成disp.png;没有说明视差计算这一步就没过,问题在标定文件或者numDisparities,跟 YOLO 一点关系都没有。

2.3 加载模型这一步的三个参数

很多人拿到源码第一件事是改conf,把阈值从 0.25 调到 0.6,觉得这样框更准。实际对测距来说,conf调高只会让远处小目标漏检,不会让距离更准;真正影响距离稳定性的是classes和 NMS 的iou。

import cv2 import numpy as np import torch # source='local' 表示直接用本地 yolov5 目录,避免运行时去拉仓库 model = torch.hub.load('./yolov5', 'custom', path='weights/yolov5s.pt', source='local') model.conf = 0.35 # 0.25 会放进大量远处虚检,框内有效视差反而更少 model.iou = 0.45 # 默认 0.45,人群密集场景调到 0.5 减少框粘连 model.classes = [0] # 只保留 person;类别越少,后面采样越干净 left = cv2.imread('data/left/0001.png') right = cv2.imread('data/right/0001.png') # 左右图分辨率和通道数必须完全一致,否则 remap 会静默出错 assert left.shape == right.shape, f'{left.shape} vs {right.shape}' # hub 接口吃 RGB,OpenCV 读进来是 BGR,这一步不能省 res = model(left[..., ::-1]) boxes = res.xyxy[0].cpu().numpy() # 每行 [x1, y1, x2, y2, conf, cls] print(boxes.shape, boxes[:1])

conf=0.35是个经验值:低于 0.3 时,画面边缘和反光区域会冒出大量几十像素的小框,这些小框里有效视差像素常常不到 20 个,中值完全不可信;高于 0.5 时,3 米外的人形只剩半身框,采样区会切到背景上。iou保持默认即可,除非画面里目标互相遮挡严重。

left[..., ::-1]这一步看着不起眼,是新手最常见的错误来源之一:喂 BGR 进去模型照样能出框,位置大体也对,但置信度和类别会飘,而且不会报错,属于典型的黑匣子式错误。

注意:torch.hub.load第一次调用会在本地 yolov5 目录下生成缓存,如果你的 yolov5 目录是只读挂载,会直接失败,改成source='local'并把目录放在可写路径下。

3. 标定与极线校正:Q 矩阵差一个符号,3 米就变 0.3 米

3.1 为什么一定要先 rectify 再 match

双目测距的数学前提只有一句话:同一个物点在左右图里必须落在同一行像素上。真实相机的两个镜头不可能完全平行,总有零点几度的夹角和几毫米的轴向偏差,导致同一个点在右图里的纵坐标比左图高几个到几十个像素。SGBM 这类匹配算法是在同一行上滑动窗口找最优的,纵坐标差超过blockSize的一半,匹配就直接失败或者匹配到隔壁物体上。

cv2.stereoRectify干的事情就是算出两个旋转矩阵R1、R2和两个新投影矩阵P1、P2,把左右图各自「虚拟旋转」一次,让极线变成水平线,同时输出Q矩阵供后续反投影用。这一步做完,左右图的同一物点纵坐标差应该小于 1 像素。不做 rectify 直接匹配,视差图会是满屏雪花,你会误以为是 SGBM 参数没调好,然后在这条岔路上耗掉一整晚。

Q矩阵是 4×4 的,里面藏着一个关键量:Q[2][3] = f,即校正后的焦距(像素单位);Q[3][2] = -1/Tx,Tx就是基线长度(带符号)。reprojectImageTo3D输出的 Z 单位,跟你标定时棋盘格边长的单位完全一致。棋盘格边长填的是米,输出就是米;填的是毫米,输出就是毫米,差 1000 倍,这也是后面避坑章节里要单独拎出来说的一条。

3.2 双目标定脚本:从单目内参到 Q 矩阵

标定的输入是一批同步拍摄的棋盘格图对,左右目录里文件名必须一一对应,因为stereoCalibrate是按列表下标配对objp的,配错了不会报错,只会让外参算出一个荒谬的基线。

import cv2 import glob import numpy as np PATTERN = (9, 6) # 内角点数量,不是方格数量,数错一位整批白标 SQUARE = 0.025 # 方格边长,单位:米,这个单位决定后面一切输出的单位 def calibrate_one(files): 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 obj_pts, img_pts, size = [], [], None for f in files: gray = cv2.cvtColor(cv2.imread(f), cv2.COLOR_BGR2GRAY) size = gray.shape[::-1] # 注意是 (w, h),不是 (h, w) ok, corners = cv2.findChessboardCorners( gray, PATTERN, cv2.CALIB_CB_ADAPTIVE_THRESH | cv2.CALIB_CB_NORMALIZE_IMAGE) if not ok: continue # 亚像素精化,这一步直接影响重投影误差能不能压到 0.3px 以内 corners = cv2.cornerSubPix( gray, corners, (11, 11), (-1, -1), (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-3)) obj_pts.append(objp) img_pts.append(corners) rms, K, D, _, _ = cv2.calibrateCamera(obj_pts, img_pts, size, None, None) print(f'{len(img_pts)} 张有效, 单目 rms={rms:.3f}px') return K, D, obj_pts, img_pts, size K1, D1, op_l, ip_l, size = calibrate_one(sorted(glob.glob('calib/left/*.png'))) K2, D2, op_r, ip_r, _ = calibrate_one(sorted(glob.glob('calib/right/*.png'))) # 内参已经够稳就锁死,只优化 R 和 T,避免两组参数互相拉扯 rms, K1, D1, K2, D2, R, T, E, F = cv2.stereoCalibrate( op_l, ip_l, ip_r, K1, D1, K2, D2, size, flags=cv2.CALIB_FIX_INTRINSIC, criteria=(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-6)) print(f'双目 rms={rms:.3f}px, 基线={np.linalg.norm(T)*1000:.2f}mm') # alpha=0 裁掉校正后的黑边,保留有效像素;alpha=1 保留全部但会引入无效区 R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify( K1, D1, K2, D2, size, R, T, alpha=0, flags=cv2.CALIB_ZERO_DISPARITY) np.savez('calib/stereo_calib.npz', K1=K1, D1=D1, K2=K2, D2=D2, R1=R1, R2=R2, P1=P1, P2=P2, Q=Q, size=size, baseline=float(np.linalg.norm(T)))

alpha这个参数值得单独说:alpha=0会把校正后所有黑边裁掉,画面只剩两图共有的有效区域,好处是不会有「黑边区域视差全 0 被当成有效值」,坏处是视野变窄,画面边缘的目标可能直接被裁没。做前向避障我一般用alpha=0,因为边缘本来也不该作为测距依据;做全景或者视野完整性优先的场景用alpha=1,但要在采样时把disp==0的像素滤掉。

标定完要生成两张映射表并缓存下来,每帧都重算initUndistortRectifyMap会白白吃掉几毫秒:

d = np.load('calib/stereo_calib.npz') size = tuple(int(v) for v in d['size']) map1x, map1y = cv2.initUndistortRectifyMap(d['K1'], d['D1'], d['R1'], d['P1'], size, cv2.CV_32FC1) map2x, map2y = cv2.initUndistortRectifyMap(d['K2'], d['D2'], d['R2'], d['P2'], size, cv2.CV_32FC1) left_rect = cv2.remap(left, map1x, map1y, cv2.INTER_LINEAR) right_rect = cv2.remap(right, map2x, map2y, cv2.INTER_LINEAR)

后面所有视差计算和框坐标,都必须基于left_rect/right_rect,不能拿原始图上的框去left_rect上抠像素,因为 rectify 会改变像素位置,alpha=0时还会整体平移和缩放。这是移植到自己的相机上时最容易忽略的一处坐标错位,现象是距离整体偏大 10%~30%,怎么调 SGBM 都调不回来。

3.3 三根验收集:重投影误差、极线偏差、基线长度

标定跑完不能只看脚本没报错就完事,我一般会强制过三根尺子,任何一根不过就重新采图。

指标计算方式合格线不达标说明
双目重投影误差stereoCalibrate返回的 rms< 0.5 px棋盘姿态太单一或存在模糊帧
极线偏差校正后同一角点的 abs(y1-y2) 均值< 1 px外参没收敛,或左右图不同步
基线长度norm(T)与卷尺实测对比偏差 < 1%棋盘边长单位填错

极线偏差的验法很土但有效:取一张标定用的棋盘图,校正后findChessboardCorners分别找两图角点,逐点算纵坐标差。我见过最离谱的一次是均值 47 像素,原因是采集时左右相机不是硬同步,拍的是两个时刻的棋盘,外参自然算出来是乱的。

注意:T的符号有意义。norm(T)只给你长度,但Q矩阵里的Tx是带符号的,符号错了reprojectImageTo3D出来的 Z 会是负值。看到距离全是负数,先去查Q[3][2]的符号,不要去改代码里的取绝对值。

4. 从检测框到米:SGBM 六个参数与框内中值采样

4.1 Z = f·B/d 的量程与分辨率边界

测距主公式就一行:Z = f * B / d,f是校正后焦距(像素),B是基线(米),d是视差(像素)。这个公式同时给出了量程的上下界,理解了它,参数就不用瞎试。

下界由最大可测视差决定,也就是numDisparities。d_max = numDisparities,对应的Z_min = f*B/numDisparities。上界由视差的分辨能力决定:视差只有 1 像素的量化台阶,而按误差传播ΔZ = Z²/(f·B) · Δd,距离越远,同样 1 像素误差放大得越厉害。

拿一组具体数感受一下:f=700px、B=0.06m,则f·B=42。1 米处视差 42 像素,3 米处视差只剩 14 像素。如果视差能精确到 0.5 像素,3 米处的距离误差是9/42×0.5≈0.107m,超过 10 厘米。这就是为什么 3 米开外的双目测距必须靠亚像素插值和滤波,光靠整数视差在工程上不可交付。

# 量程估算:改基线或分辨率前先算一遍,别等跑起来才发现不够用 f_px, B, num_disp = 700.0, 0.06, 128 print('最近可测', round(f_px * B / num_disp, 3), 'm') print('3m 处 1px 误差对应', round(9 / (f_px * B), 3), 'm')

要拉远量程有两条路:加基线或者加焦距。基线从 60mm 加到 120mm,f·B翻倍,3 米处的误差减半,但代价是近处盲区变大、匹配难度上升(左右图透视差异变大)。我在移动平台上一般卡在 100~120mm,再大近处 0.5 米以内就没法用了。

4.2 SGBM 参数逐个拧

StereoSGBM_create有十来个参数,但真正决定成败的就六个。剩下的保持默认大概率能跑,只是边缘会碎。

def build_matcher(num_disp=128, block=5): return cv2.StereoSGBM_create( minDisparity=0, numDisparities=num_disp, # 必须能被 16 整除,否则 create 直接抛错 blockSize=block, # 必须奇数,5/7/9 常用,越大越平滑也越糊 P1=8 * 1 * block ** 2, # 视差小幅变化的惩罚,1 是单通道 P2=32 * 1 * block ** 2, # 视差大幅变化的惩罚,P2 越大越倾向平滑 disp12MaxDiff=1, # 左右一致性检查,>0 才能滤掉遮挡区错误 uniquenessRatio=10, # 最优/次优比,10~15 之间,低纹理区靠它兜底 speckleWindowSize=100, # 连通域去斑,小噪点面积超过它就抹掉 speckleRange=2, preFilterCap=63, # 预处理截断,光照差异大时降到 31 mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY)

逐个说取舍。numDisparities决定最近可测距离,128 对应 0.33 米,如果应用场景最近只到 0.5 米,用 64 就够,还能省一半计算量并且减少误匹配。blockSize是清晰度和鲁棒性的旋钮:5 保留细节但噪点多,11 在弱纹理墙面上更稳,代价是物体边缘会长出几像素的「拖尾」,这个拖尾在框边界采样时会直接污染距离。

P1/P2的经典取法是8*cn*block²和32*cn*block²,cn是通道数(灰度图为 1,彩色 3)。如果代码里把彩色图直接喂进 matcher 又按 1 算,平滑强度会不够,视差图会出现成片的锯齿。uniquenessRatio是低纹理区域的后悔药:白墙、地面这种地方匹配代价曲面很平,最优和次优几乎一样好,把这个值从 5 提到 15,能砍掉大量虚假视差,代价是本身很弱的有效像素也被砍掉。disp12MaxDiff建议永远给 1,遮挡区域左右一致性检查过不去,天然被标成无效,比事后滤更干净。

4.3 框内采样:为什么用中值而不是均值

拿到视差图之后,直接对检测框内所有有效视差取平均,是最常见也最容易出错的做法。一个 200×400 的人形框里,真正属于人体的像素可能只占 40%,剩下的地面、背景、旁边经过的车,视差各不相同,均值会被这些杂点整体拉偏,而且拉偏的方向不确定,看上去就是「距离在跳」。

我的做法是缩采样区加中值:横向只取中间 1/3,纵向从框高的一半到八分之七,避开头顶(常和远处背景混在一起)和脚下(地面视差和人体差得远)。

def box_depth(disp_px, box, f_px, baseline, min_valid=30): x1, y1, x2, y2 = [int(round(v)) for v in box[:4]] w, h = x2 - x1, y2 - y1 if w < 8 or h < 8: return None, 0.0 # 太小的框不参与,噪声远大于信号 cx1, cx2 = x1 + w // 3, x2 - w // 3 # 横向取中间 1/3 cy1, cy2 = y1 + h // 2, y2 - h // 8 # 纵向取下半段,避开头顶背景 roi = disp_px[cy1:cy2, cx1:cx2] valid = roi[roi > 0].astype(np.float32) # 0 是匹配失败占位,必须剔掉 if valid.size < min_valid: return None, valid.size / max(roi.size, 1) d_med = float(np.median(valid)) # 中值抗单侧杂点,均值不抗 if d_med <= 0: return None, 0.0 Z = f_px * baseline / d_med # 单位与 baseline 一致,这里是米 return round(Z, 3), valid.size / roi.size

函数返回两个值:距离和有效率。有效率这一项别丢,它是判断这次测量值不可信的开关。有效率低于 0.15 通常说明目标在低纹理区域、被遮挡或者超出量程,这时候返回的距离即使看起来合理也不要采信。我在实际部署里会把有效率低于 0.2 的框直接标灰,只显示框不显示数字,比显示一个错的数字安全得多。

f_px从标定文件里取,用P1[0, 0],不要用原始K1[0, 0]。rectify 之后焦距可能因为裁剪和重采样发生变化,用错了会引入百分之几的系统性偏差。也可以直接用Q矩阵走reprojectImageTo3D,两种方式等价,但要注意reprojectImageTo3D要求输入是float32的连续数组,且视差单位是像素;如果视差图是CV_16S定点格式(真实视差乘 16),要先除以 16。

5. 避坑实录:五个让测距变玄学的现场与处理

5.1 距离全是负数或者 0.03 米

现象:检测框正常,视差图看着也有纹理,但输出的距离要么是负的,要么稳定在 0.0x 米这个量级,换什么目标都一样。

原因:基线单位或棋盘格单位混用。标定时SQUARE填了 25(毫米)而不是 0.025(米),T的单位跟着变成毫米,f·B被放大 1000 倍,Z自然也大 1000 倍;如果代码里又做了一次/1000的「修正」,两次错误叠加就变成负数或者 0.03 这种离谱值。

解决:从源头统一。标定脚本里SQUARE一律用米,输出文件名里带上单位标注,baseline存进npz时也存米。改完之后拿 1 米处的目标验一次,数值落在 0.95~1.05 才算这条路走通。

5.2 标定误差很小,但实测距离整体偏大

现象:stereoCalibrate的 rms 只有 0.2 像素,基线也和实测对得上,但拿卷尺量 2 米,程序稳定输出 2.3 米左右,偏差接近 15%,而且重复性很好。

原因:左右图像不同步,或者标定板姿态过于集中在同一个平面。前者会让外参里混进一个虚假的旋转,rms 反而因为参数自由度高而显得很小;后者让R、T的可观测性不足,棋盘只在正前方一个距离上拍,焦距和基线会互相补偿,形成一组「能拟合标定数据但不能外推」的参数。

解决:采图时棋盘必须有倾斜(绕 x、y 各 ±20°)、有远近(覆盖 0.5 米到 3 米)、有画面四角位置,每种姿态至少 8 张,总计 25 张以上。同时确认采集是硬触发或者软件同一帧取图,不是先后各拍一张。

5.3 近处目标距离被「卡住」在某个固定值

现象:目标从 1.5 米走近到 0.4 米,输出距离到 0.66 米就不动了,再近还是 0.66,视差图上目标区域变成一整片纯色。

原因:numDisparities不够。目标近到一定程度,真实视差超过numDisparities,超出部分全部被截断到最大值,反算出来的距离自然卡在下界上。这里的下界就是f·B/numDisparities,0.66 米正好对应42/64。

解决:按最近作业距离反算需要的numDisparities:num_disp ≥ f·B/Z_min,再向上取到 16 的整数倍。要测到 0.3 米就需要42/0.3=140,取 144 或 160。加大之后计算量线性增长,帧率会掉,所以别一上来就填 256。

5.4 视差图满屏雪花,几乎全是噪点

现象:检测和标定都没问题,但视差图看起来像电视雪花,框内有效率常年低于 0.1,偶尔蹦出一个距离又马上跳走。

原因:左右图亮度或色彩不一致。双目模组两个镜头的自动曝光、自动增益是各自独立工作的,一个画面偏亮一个偏暗,SGBM 的 Census 代价直接失效。另一个常见诱因是左右图接反了,视差全为负,被roi > 0全部滤掉,表现出来也是「没有有效像素」。

解决:先关掉相机的自动曝光和自动增益,固定曝光时间和增益,两个镜头用同一组值;如果硬件不支持,就在采集端做一次直方图匹配。接反的问题用一张有明显左右位移的图验证:手指放在镜头前偏左,左图里手指应该更靠右边缘,确认无误再接。

5.5 静态很准,一动就跳

现象:目标静止时距离读数稳定在 ±1 厘米,一旦人或车移动,读数开始剧烈跳动,幅度能到半米。

原因:检测框在动,采样区跟着动,每帧采到的像素集合都不一样;再加上运动模糊让匹配质量下降,单帧中值本身就有几厘米的抖动,两者叠加就放大成大跳变。

解决:加一层时序滤波,但别用简单的滑动平均,它会把真实靠近的响应拖慢。用中值加变化率门限:缓存最近 5 帧的距离,取中值作为输出;如果新值和当前输出的相对变化超过 30%,先不采信,连续 3 帧都超才更新,这样既压住了单帧噪声,又不会把真实的快速接近误判成噪声。

6. 精度对表与工程化:把 3 米处的误差压进 3 厘米

标定和参数调完之后,必须做一次对表,否则你永远不知道自己这套东西到底能交付到什么精度。我的习惯是在地面贴五条标记线,0.5、1.0、1.5、2.0、3.0 米各一条,人形靶依次站过去,每个位置采 100 帧取中值,然后和卷尺实测对比。某次在 1280×720、基线 60mm 的模组上跑出来的一组数据是这样的:

实测距离中值视差解算距离绝对误差相对误差
0.50 m84.0 px0.500 m0.000 m0.0%
1.00 m41.6 px1.010 m+0.010 m1.0%
1.50 m27.6 px1.522 m+0.022 m1.5%
2.00 m20.5 px2.049 m+0.049 m2.4%
3.00 m13.6 px3.088 m+0.088 m2.9%

误差全是正的,说明存在系统性偏差,不是随机噪声。这类偏差优先怀疑标定(基线略小、焦距略大)而不是算法,重标一次往往就回来了。误差随距离平方增长这一点,和ΔZ = Z²/(f·B)·Δd完全吻合,所以别指望靠调 SGBM 把 3 米处的误差压到 1 厘米以内,物理上就不允许。

想再往下压,只有两条路。第一条是亚像素,StereoSGBM用MODE_SGBM_3WAY并在后处理里做一次抛物线拟合,把整数视差插值到 0.1 像素;或者直接用cv2.ximgproc.createDisparityWLSFilter做加权最小二乘联合滤波,它对边缘的保边效果比中值滤波好一大截,代价是多花几毫秒:

# 需要 opencv-contrib,WLS 能把视差边缘收得更紧,3m 处实测能削掉 2~3cm left_matcher = build_matcher(num_disp=128, block=5) right_matcher = cv2.ximgproc.createRightMatcher(left_matcher) wls = cv2.ximgproc.createDisparityWLSFilter(left_matcher) wls.setLambda(8000) # 越大越平滑,8000~15000 是常用区间 wls.setSigmaColor(1.5) # 边缘敏感度,纹理丰富时降到 1.0 disp_l = left_matcher.compute(left_rect, right_rect).astype(np.float32) / 16.0 disp_r = right_matcher.compute(right_rect, left_rect).astype(np.float32) / 16.0 disp_f = wls.filter(disp_l, left_rect, disparity_map_right=disp_r)

第二条是加基线,但这个受机械结构限制,而且会牺牲近处视野,不是能随时动的旋钮。所以工程上更实际的做法是接受远距离精度下降,在业务层设一个置信度阈值:2 米内输出具体数值,2~4 米只输出距离区间,超过 4 米只保留检测框不给数字。

从那以后我每次换相机或者换支架,都会强制走一遍「五点对表 + 极线偏差检查」,哪怕只是把相机从一个铝型材挪到另一个上,因为基线的微小变化足以让 3 米处的读数偏出 20 厘米,而这种偏差在实时画面里根本看不出来,只能靠对表暴露。源码包按简介里的入口取下来之后,建议先把示例图跑通,再换成自己相机的标定文件重跑那五条线,别跳过这一步直接上业务逻辑。希望帮到你。

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

返回列表