☰
YOLOX双目测距实战:从相机标定到深度图融合
2026/10/7 1:38:27 网站建设 项目流程

简介:一份融合YOLOX目标检测与双目视觉测距的实战项目,面向计算机视觉开发者、科研人员及高校相关专业学生,解决双目相机下目标距离实时测量与算法落地问题。项目涵盖图像采集、预处理、YOLOX检测、立体匹配、视差计算到距离转换的完整流程,并提供用户友好界面与关键步骤注释,便于实际调参与二次开发。压缩包共47个文件,其中31个Python脚本覆盖训练、预测、单点/三点测距等核心逻辑,6个BMP与4个JPG为不同场景的测距结果图像,3个TXT包含数据集划分与标注信息,另附README、配置依赖与日志文件,整体仅2.12MB,结构精炼、便于快速部署。目前已有208人学习,适合用于机器人导航、自动驾驶、三维重建等场景的技术验证与教学演示,能帮助开发者深入理解双目测距原理并快速迁移到自身项目中。

1. 双目测距搭上YOLOX:先搞清楚这个项目解决什么问题

做到一半你就会有这种感觉:双目测距真正难的从来不是YOLOX检测本身,而是让两个摄像头告诉你同一个目标上的视差到底可不可信。这个基于YOLOX目标检测算法的双目相机测距项目,核心是把目标检测的2D框和双目视差算出来的深度图对齐——检测模块负责“它是什么、在哪”,双目模块负责“它离我多远”,两者缺一个都没法交付。适合三类人:刚接触双目相机标定的同学,想找一个能跑通全流程的目标检测实战项目的开发者,以及需要把检测结果带距离信息做告警或避障的工程人员。下面会按标定、视差、检测、融合、排错、验证的顺序,把每一步能抄的参数和代码直接摆出来。

2. 双目测距原理与相机标定:视差公式怎么变成能用的内参矩阵

2.1 为什么是双眼而不是单目:视差与深度的换算关系

双目测距的基本公式是 Z = f × B / d。Z 是目标到相机的距离,f 是焦距(像素单位),B 是左右相机光心的水平距离,d 是同名点在左右图上列坐标的差,也就是视差。单目能估距离靠的是先验尺寸,比如人脸平均宽度 15 厘米之类,这是一条在不同体型、不同姿态下随时会失灵的假设;双目不一样,它直接做三角测量,d 是从左右图里逐像素匹配出来的,所以同一套标定参数换场景不用重新训练任何模型。

关键在误差传播。对公式两边做微分,距离的相对误差约等于视差绝对误差除以视差本身,而 SGBM 这类匹配算法的视差读数误差基本恒定在 0.25 到 0.5 个像素。于是距离越远误差放得越狠:基线 12 厘米的模组,5 米内还能做到 3% 以内,到 10 米就容易超过 10%。这不是算法不行,是物理规律。所以做这个项目第一步就要明确使用距离,选多长的基线、设多大的视差搜索范围,都由这个上限反推出来。

2.2 用OpenCV跑通双目标定:角点提取、单目标定与stereoCalibrate

标定是双目测距里最容易被跳过、跳过后又回来补救的环节。常见做法是准备一块棋盘格,内角点数 9×7 或 8×6 都可以,格子边长要量准,单位用米。采集 25 张以上左右配对图,注意每次都要改变角度、距离和倾斜,不能只在一个位置平移。然后直接用 OpenCV 的 calibrateCamera 和 stereoCalibrate 走完整个流程。

import cv2 import numpy as np import glob # 棋盘格内角点数,不是格子总数;SQUARE_SIZE 是单个格子的实际边长,单位米 CHECKERBOARD = (9, 6) SQUARE_SIZE = 0.024 objp = np.zeros((CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32) objp[:, :2] = np.mgrid[0:CHECKERBOARD[1], 0:CHECKERBOARD[0]].T.reshape(-1, 2) objp *= SQUARE_SIZE objpoints, imgpoints_l, imgpoints_r = [], [], [] images_l = sorted(glob.glob("calib/left/*.png")) images_r = sorted(glob.glob("calib/right/*.png")) for lpath, rpath in zip(images_l, images_r): gray_l = cv2.cvtColor(cv2.imread(lpath), cv2.COLOR_BGR2GRAY) gray_r = cv2.cvtColor(cv2.imread(rpath), cv2.COLOR_BGR2GRAY) ret_l, corners_l = cv2.findChessboardCorners(gray_l, CHECKERBOARD, None) ret_r, corners_r = cv2.findChessboardCorners(gray_r, CHECKERBOARD, None) if not (ret_l and ret_r): continue # 左右图必须同时检出角点,否则这对数据不能用 objpoints.append(objp) criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-4) corners_l = cv2.cornerSubPix(gray_l, corners_l, (11, 11), (-1, -1), criteria) corners_r = cv2.cornerSubPix(gray_r, corners_r, (11, 11), (-1, -1), criteria) imgpoints_l.append(corners_l) imgpoints_r.append(corners_r) # 先做单目标定,拿到内参和畸变;直接双目标定容易落到局部最优 ret_l, mtx_l, dist_l, _, _ = cv2.calibrateCamera( objpoints, imgpoints_l, gray_l.shape[::-1], None, None) ret_r, mtx_r, dist_r, _, _ = cv2.calibrateCamera( objpoints, imgpoints_r, gray_r.shape[::-1], None, None) # 双目标定,CALIB_FIX_INTRINSIC 表示只优化外参,内参沿用单目标定结果 ret_s, mtx_l, dist_l, mtx_r, dist_r, R, T, E, F = cv2.stereoCalibrate( objpoints, imgpoints_l, imgpoints_r, mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], criteria=(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-5), flags=cv2.CALIB_FIX_INTRINSIC) np.savez("stereo_calib.npz", mtx_l=mtx_l, dist_l=dist_l, mtx_r=mtx_r, dist_r=dist_r, R=R, T=T) print("重投影误差:", ret_s)

代码里两个点要特别注意。一个是 cornerSubPix,findChessboardCorners 只能给整数像素精度,亚像素细化能让角点精度从约 1 像素收敛到 0.1 到 0.2 像素,这会直接影响后面重投影误差。另一个是 flags 选了 CALIB_FIX_INTRINSIC,因为单目标定对左右分开做时已经能拿到不错的内参,双目标定只解 R 和 T 会更稳;数据质量一般时,内参外参一起优化很容易翻车。ret_s 低于 0.3 像素算及格,低于 0.15 算好,高于 0.5 就应该回去补拍数据,不要硬往下走。

T 向量里第一项就是基线长度,单位和你 SQUARE_SIZE 一致。我一般会拿卷尺量一下实物基线,跟 T[0] 对一下差多少;如果相差超过 5%,说明标定板距离或者棋盘格尺寸写错了。这种错误会在后面深度计算里线性放大,属于必须提前排掉的雷。

2.3 立体校正与映射表:让左右图像对得齐的关键一步

标定只给了内外参,左右图像还是各自带畸变、光轴不平行。直接用原始图做视差匹配,匹配算法要找的“同一行”是不存在的。所以下一步用 stereoRectify 算校正矩阵,再用 initUndistortRectifyMap 生成映射表,最后 remap 一次把左右图变到同一平面上。

# R, T 来自 stereoCalibrate;alpha=0 表示对校正后的图像做最大裁剪 R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify( mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], R, T, alpha=0) map1_l, map2_l = cv2.initUndistortRectifyMap( mtx_l, dist_l, R1, P1, gray_l.shape[::-1], cv2.CV_32FC1) map1_r, map2_r = cv2.initUndistortRectifyMap( mtx_r, dist_r, R2, P2, gray_r.shape[::-1], cv2.CV_32FC1) rect_l = cv2.remap(img_l, map1_l, map2_l, cv2.INTER_LINEAR) rect_r = cv2.remap(img_r, map1_r, map2_r, cv2.INTER_LINEAR) # 校验:把左右图横向拼接,画水平辅助线,同名点应落在同一条线上 check = np.hstack((rect_l, rect_r)) for y in range(0, rect_l.shape[0], 40): cv2.line(check, (0, y), (check.shape[1], y), (0, 255, 0), 1)

Q 矩阵是后面从视差算三维坐标的关键,Q[2,3] 本质上是 -f×B 的组合量。alpha 参数决定校正后图像保留多少像素:alpha=0 会裁掉边缘无效区域,视差图更干净;alpha=1 保留全部原像素但边缘会有变形区。做测距我一般用 alpha=0,换来的是整幅图都能参与匹配,省掉后面处理无效边的麻烦。映射表算一次存成 npy 文件,运行时直接 remap,不要每次启动都重算一遍。标定参数不要当黑匣子存起来,把图像、角点数量和重投影误差一起归档,换设备时才能快速判断是哪里出了问题。

3. 视差图与深度图计算:SGBM参数是测距精度的分水岭

3.1 SGBM最小实现:从校正图到视差图的三步

OpenCV 里的 StereoSGBM_create 是最常用的半全局匹配实现,替代了早期又慢又噪的 BM。它做的是逐像素匹配代价计算加多方向路径聚合,输出的是整数视差图。关键点是这个输出的单位不是像素,而是 1/16 像素,所以 compute 完要除以 16 转成 float 才能用于后续计算。

import cv2 import numpy as np # rect_l、rect_r 是 remap 之后的校正图,必须是单通道灰度图 gray_l = cv2.cvtColor(rect_l, cv2.COLOR_BGR2GRAY) gray_r = cv2.cvtColor(rect_r, cv2.COLOR_BGR2GRAY) # blockSize 11 对应中等纹理场景;纹理少可以降到 5 或 7 block_size = 11 sgbm = cv2.StereoSGBM_create( minDisparity=0, numDisparities=64, # 覆盖的视差搜索范围,必须是16的倍数 blockSize=block_size, # 匹配窗口边长,必须是奇数 P1=8 * 3 * block_size ** 2, # 平滑惩罚,小梯度 P2=32 * 3 * block_size ** 2,# 平滑惩罚,大梯度 disp12MaxDiff=1, uniquenessRatio=10, speckleWindowSize=100, speckleRange=32, mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY, ) disp = sgbm.compute(gray_l, gray_r).astype(np.float32) / 16.0 # 用 Q 矩阵把视差转成三维坐标,视差为 0 或负的地方是无效匹配 points_3d = cv2.reprojectImageTo3D(disp, Q) depth = points_3d[:, :, 2] # 深度就是 Z 分量

numDisparities 是这里最重要的参数。它表示匹配时搜索的最大视差范围,最小值 0 对应无穷远,最大值对应最近的目标距离。范围太小,近处目标会黑掉;范围太大,远处噪声和计算量一起涨。我一般按“最近要测的距离”来估算:Z_min = f × B / numDisparities,把 Z_min 设成实际要测的最近距离,再向上取 16 的倍数。比如 f=700、B=0.12 时,要测 1 米最近距离,numDisparities 至少 84,取 96 比较稳妥。注意这个值是无止境加大的,每根扫描线多搜一个候选就是一条路径聚合的额外计算量,运行时长线性增长。

3.2 深度图后处理:WLS滤波、空洞填充与ROI裁剪

SGBM 出来的视差图噪声很大,直接算距离会得到一堆跳变的点。业界常用的组合是 WLS(加权最小二乘)滤波,它利用左右一致性阈值把高置信度区域的视差向周围传播,对边缘保留比双边滤波好。OpenCV 里的实现需要左右两张视差图,所以 compute 要跑两遍。

disp_l = sgbm.compute(gray_l, gray_r).astype(np.float32) / 16.0 disp_r = sgbm.compute(gray_r, gray_l).astype(np.float32) / 16.0 wls = cv2.ximgproc.createDisparityWLSFilter(sgbm) wls.setLambda(8000.0) wls.setSigmaColor(1.5) disp_filtered = wls.filter(disp_l, gray_l, disparity_map_right=disp_r) # 把无效视差填成 0,方便后面 ROI 统计时直接过滤 disp_filtered[disp_filtered <= 0] = 0 points_3d = cv2.reprojectImageTo3D(disp_filtered, Q) depth_map = points_3d[:, :, 2] # 深度有物理下界和上界,超出范围的直接置 0 depth_map[(depth_map < 0.2) | (depth_map > 50)] = 0

WLS 不是免费的:它要跑左右两轮 SGBM,耗时基本变两倍。如果你原本能做到 30fps,加了 WLS 就掉到 15 到 18fps。所以实时性吃紧的项目里,我会把 WLS 放在后台验证阶段用,交付时降级成中值滤波,只对检测框附近的小窗口做,效果接近但快很多。深度范围裁剪也属于必做项,室内小基线双目在 20 米以上的输出基本没有可信度,保留那些离谱的大数值只会污染后面的均值统计。

3.3 必调的5个SGBM参数:blockSize、numDisparities与P1/P2

SGBM 参数调起来像玄学,但有迹可循。我实测下来影响最大的是下面五个:

参数作用初始值调节方向
numDisparities视差搜索范围按最近距离估算远处发黑加大,近处大面积噪声减小
blockSize匹配窗口边长7 或 11纹理稀疏加大,边缘糊了减小
P1 / P2平滑惩罚项8×3×blockSize² / 32×3×blockSize²视差断裂多调大,过度平滑调小
uniquenessRatio唯一性比率10匹配误点多加到 15 到 20
disp12MaxDiff左右一致性阈值1想要更稠密放宽到 2 到 3

P1/P2 的经验公式不是死的。P1 比 P2 小,意思是小梯度变化给的惩罚轻,大梯度(物体边缘)给的重,这样视差图在边缘保持锐利,内部区域平滑。纹理特别贫乏的场景,比如单色墙面,可以把 P2 减半试试,代价是边缘会糊一点。记住每次只动一个参数,同时动两个,出了问题你不知道是哪一个改出来的。深度图上的判断标准很简单:目标内部应该是一整块接近的颜色,边缘应该跟物体轮廓重合,如果出现横条纹或者物体边缘有拖影,就是惩罚项和窗口尺寸配合出了问题。

4. 把YOLOX检测框与深度图融合:从像素坐标到真实距离

4.1 YOLOX推理的最小闭环:ONNX导出、预处理与后处理解码

YOLOX 的常见部署路径是官方 PyTorch 权重导出 ONNX 再用 onnxruntime 推理,省掉整套 PyTorch 环境也能跑,方便嵌入到“采集-测距”的主循环里。环境配置卡住的时候,十有八九是 onnxruntime 版本和 CUDA 版本不匹配,先切 CPU provider 验证逻辑,再回来调 GPU。预处理和 YOLOv5 一样用 letterbox 保持长宽比,避免直接 resize 导致目标变形。

import cv2 import numpy as np import onnxruntime as ort class YOLOXInference: def __init__(self, onnx_path, input_size=640, conf_thres=0.3, nms_thres=0.45): self.session = ort.InferenceSession( onnx_path, providers=["CUDAExecutionProvider", "CPUExecutionProvider"]) self.input_size = input_size self.conf_thres = conf_thres self.nms_thres = nms_thres self.input_name = self.session.get_inputs()[0].name def preprocess(self, bgr_img): img = cv2.cvtColor(bgr_img, cv2.COLOR_BGR2RGB) h, w = img.shape[:2] scale = min(self.input_size / h, self.input_size / w) self.scale = scale new_h, new_w = int(round(h * scale)), int(round(w * scale)) resized = cv2.resize(img, (new_w, new_h)) canvas = np.full((self.input_size, self.input_size, 3), 114, np.uint8) canvas[:new_h, :new_w] = resized # YOLOX 要求 CHW、归一化到 0~1、转 float32 blob = canvas.astype(np.float32) / 255.0 blob = blob.transpose(2, 0, 1)[None, ...] return blob def postprocess(self, output): # 输出形状 [1, 8400, 85]:8400 是三个尺度 anchor 总和 # 85 = 5(cx, cy, w, h, obj_conf)+ 80 类 preds = output[0][0] # [8400, 85] boxes_wh = preds[:, :4] scores = preds[:, 4:5] * preds[:, 5:] # 类别置信度 = 目标概率 × 类别概率 max_scores = scores.max(axis=1) keep = max_scores > self.conf_thres boxes = boxes_wh[keep] scores = scores[keep] cls_ids = scores.argmax(axis=1) score_vals = scores.max(axis=1) if len(boxes) == 0: return [] # 把 cx, cy, w, h 转成 x1, y1, x2, y2 并映射回原图坐标 x1 = (boxes[:, 0] - boxes[:, 2] / 2) / self.scale y1 = (boxes[:, 1] - boxes[:, 3] / 2) / self.scale x2 = (boxes[:, 0] + boxes[:, 2] / 2) / self.scale y2 = (boxes[:, 1] + boxes[:, 3] / 2) / self.scale dets = np.stack([x1, y1, x2, y2, score_vals, cls_ids], axis=1) indices = cv2.dnn.NMSBoxes( dets[:, :4].tolist(), dets[:, 4].tolist(), self.conf_thres, self.nms_thres) if len(indices) == 0: return [] return dets[indices.flatten()] def __call__(self, bgr_img): blob = self.preprocess(bgr_img) output = self.session.run(None, {self.input_name: blob})[0] return self.postprocess(output)

这段代码可以直接替换成官方 YOLOX demo 里的逻辑,核心是理解两件事。第一,letterbox 加灰边(填充 114)是为了不让整图缩放破坏目标比例,后处理时所有坐标都要除以 scale 映回原图,漏了这步检测框和深度图就对不上。第二,模型输出是密集 anchor 的解码结果,8400 个候选框,不做阈值过滤和 NMS,后面 ROI 采样会抓到一堆重复框。检测框质量问题还可以借助特征图热力图来排查:把 YOLOX 输出的特征热力图叠加到原图上,看高响应区域和物体实际位置是否对齐,这比猜置信度阈值靠谱得多。

4.2 检测框映射到深度:中心点采样、ROI均值与置信度过滤

检测框拿到了,深度图也拿到了,但直接把框中心那个像素的深度当成目标距离是不靠谱的。中心像素可能是目标上的一个反光点、一条边缘、甚至背景。正确做法是在框中心周围取一块小区域,统计有效深度值的分布后再给结果。

def box_to_distance(box, depth_map, roi_ratio=0.3, min_valid=10): x1, y1, x2, y2 = box[:4].astype(int) w, h = x2 - x1, y2 - y1 if w <= 0 or h <= 0: return None cx, cy = (x1 + x2) >> 1, (y1 + y2) >> 1 # ROI 取检测框中心附近宽高的 30%,绕过边缘误匹配 rw, rh = max(int(w * roi_ratio), 2), max(int(h * roi_ratio), 2) x0, y0 = max(cx - rw // 2, 0), max(cy - rh // 2, 0) roi = depth_map[y0:y0 + rh, x0:x0 + rw] valid = roi[(roi > 0) & np.isfinite(roi)] if len(valid) < min_valid: return None # 有效深度点太少,返回空让上层决定是否丢帧 med = np.median(valid) # 去掉偏离中位数超过 20% 的点,避免前景或背景混入 inlier = valid[np.abs(valid - med) < med * 0.2] if len(inlier) == 0: return None return float(np.mean(inlier))

ROI 的比例越小,深度越贴近目标中心,但越容易受单点噪声影响;越大越稳,但可能扫到背景。0.3 是我在室内人体目标上试出来的折中值,目标本身大可以提到 0.4。置信度过滤在调用这一层做:检测框置信度低于 0.5 的,距离结果只在连续三帧都出现时才输出,这是防止误检框带来的距离抖动。深度图的单位是米,来自标定时 SQUARE_SIZE 的单位,别跟厘米混了。

4.3 测距结果的时间平滑:滑动窗口与突发值剔除

单帧深度即使做了 ROI 滤波,还是会有突发噪点,特别是目标在移动或光照变化时。我会用一个固定长度的滑动窗口做中值平滑,这比均值平滑更抗离群值。

from collections import deque class DistanceSmoother: def __init__(self, window_size=5, max_jump=0.5): self.buffer = deque(maxlen=window_size) self.last_value = None self.max_jump = max_jump def push(self, value): if value is None: return None # 单帧跳变超过 0.5 米,认为是噪声或目标切换,不进入窗口 if self.last_value is not None and \ abs(value - self.last_value) > self.max_jump: return self.last_value self.buffer.append(value) self.last_value = float(np.median(self.buffer)) return self.last_value

窗口长度取 5 帧,30fps 下是 160ms 的延迟,人眼感知不到。max_jump 的作用是挡住目标切换瞬间的巨大跳变——A 目标消失、B 目标出现的瞬时值会被拦截,避免给后级告警造成毛刺。这里提醒一句:平滑器是有状态的,多目标场景要为每个跟踪 ID 维护独立的窗口,把检测框的 ID 关联做上去,不然两个目标换位置时会互相污染距离数据。

5. 双目测距系统的5个高频翻车点与排查记录

5.1 翻车点一:远距离测距跳动大

现象:3 米内误差 2%,到了 6 到 8 米误差飙到 15% 以上,数值左右乱跳。 原因:视差公式的误差传播是距离的平方关系,远处视差本身就小,SGBM 的量化误差被放大;另外基线太短也是帮凶,基线 6cm 和 12cm 的系统,同一个距离上的视差差一倍。 解决:确认基线是否匹配使用距离,大于 5 米的场景用 12cm 以上基线;对远处目标改用多帧中值而不是瞬时值;或者直接在算法里限制“超过 8 米的检测框只告警,不给精确距离”。

5.2 翻车点二:高光表面出现深度空洞

现象:汽车车身、瓷砖墙面在深度图里是一块块黑斑,测出来的距离严重偏小或干脆无效。 原因:SGBM 以灰度匹配为基础,高光区域左右视角下的亮度分布差异巨大,匹配代价没有低谷,算法只能随机挑一个视差。 解决:在相机前加偏振片是硬件手段;软件上把 ROI 采样改成优先取深度图中位数而不是均值,同时把空洞区域周围 5×5 的有效值扩散填充。如果目标表面大面积高光,考虑改结构光方案,双目被动测距解决不了这个物理问题。

5.3 翻车点三:左右图像亮度不一致导致匹配错乱

现象:左右镜头自动曝光各调各的,深度图半边亮半边暗,视差图出现横向条纹。 原因:双目相机如果开了自动曝光和自动白平衡,左右图亮度差异会在匹配代价里直接变成系统性误差。 解决:标定和运行都锁定手动曝光、手动白平衡、固定增益;更彻底的做法是同步曝光,硬件上两路 sensor 共用一路触发信号。如果你手里的模组不支持,就退而求其次在 SGBM 前对左右图做直方图匹配,能缓解但不会根治。

5.4 翻车点四:检测框中心落在背景

现象:目标在画面里只占一竖条,中心 ROI 采样到的其实是身后墙面的深度,距离值比真实值大出很多。 原因:检测框是轴对齐矩形,目标旋转或姿态把框撑大了,中心点偏到背景上。 解决:采样点从固定中心改成目标中心概率最大的区域——对行人取框的下三分之一,车辆取框的下半部中心;如果怀疑检测框位置偏移,先把 YOLOX 的特征热力图打出来对比,确认是检测问题还是采样问题,再决定调检测阈值还是改采样策略。这是从二维框方案过渡到三维目标检测之间的必经阶段。

5.5 翻车点五:标定板采集不规范,重投影误差失控

现象:标定完保存的参数,预览时左右校正线是歪的,深度图边缘有系统性扭曲。 原因:采集的标定图像少于 15 张,或者大部分在同一距离同一角度,外参约束不够,解出来的 R、T 是过拟合的。 解决:重拍,保证棋盘格覆盖画面 9 个分区、距离从近到远、倾斜角度覆盖正负 30 度,每张左右都必须同时检出完整角点。重投影误差大于 0.5 像素就当这批数据作废,这是血泪经验:凑合着往下走,后面所有测距结果都会被标定误差均匀污染,返工成本比重拍高得多。

6. 拿卷尺给系统打分:测距误差测量与后续优化方向

6.1 用网格标尺生成误差曲线

系统跑通之后,先用最朴素的方法验收:打印一张 A3 网格标尺,或者直接用卷尺。把目标放在 0.5 米到 8 米之间、每隔 0.5 米取一个位置,每个位置停 2 秒取 20 帧测距值求均值和标准差。画一条误差曲线你会看到典型的反比形态——近处误差小、远处线性放大。把这条曲线保存下来,它就是你交付时给业务方看的“产品规格”。标准差超过均值 10% 的距离点,就是这台设备的有效工作边界。这一步一定要做,因为 SGBM 参数调优有时会自我欺骗,只有实测距离才能证明确实可用。

6.2 从单帧推到实时流的工程建议

源码项目通常是单帧演示,交付时要处理的第一个工程问题是帧率。常见瓶颈拆下来看:remap 占 CPU、SGBM 占 CPU、WLS 占 CPU、YOLOX 占 GPU。我的习惯是把 numDisparities 和 blockSize 做成运行时可配置,根据实际场景在界面里降档。另一个普遍问题是线程模型:采集线程只负责拿帧,测距线程跑 SGBM,检测线程跑 YOLOX,主线程只做融合显示,三个线程通过队列解耦,避免 SGBM 的耗时把 YOLOX 推理拖垮。

6.3 后续优化方向

如果这个项目要往产品走,最值得投入的方向是:把 YOLOX 换成带关键点输出的检测模型,采样点精确落到躯干或车辆底盘上;或者直接上三维目标检测,让网络同时输出目标的 3D 中心点和尺寸,再用双目深度做约束。目标类别泛化可以关注 transformer 目标检测和开放词汇目标检测,它们在遮挡目标和未见类别上的表现普遍优于传统 anchor 类模型,但工程化程度不如 YOLOX,量力而行。说到底,我做了这么多测距项目,最深的教训是标定数据永远是命门,深度图后处理的每个参数都要记录当时场景的光照和距离条件,不然换个环境参数就失灵了。希望帮到你。

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

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

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

立即咨询