简介:面向计算机视觉学习者与机器人、自动驾驶方向开发者的双目测距实践包,围绕 ZED 立体相机与 YOLO 目标检测的联合使用,解决从环境配置到距离计算落地的问题。压缩包共 42 个文件、约 78.97MB,以 20 个 Python 脚本为核心,辅以 YOLOv4 与 tiny 权重、cfg 配置、XML 工程文件和模型说明,覆盖检测调用、深度读取、标定与可视化等模块。已有 3016 人学习,说明该组合方案在入门与项目复现中具有较高参考价值。读者可据此搭建 ZED SDK 与 YOLO 依赖环境,处理 numpy 版本兼容、模型加载和相机参数设置,并通过主运行脚本完成目标识别与测距流程,同时获得 TensorRT 插件、CUDA 源文件与多份说明文档带来的排错和二次开发线索。
1. 需求切入:ZED 双目加 YOLO 之后,缺的是把框变成米的那一步
带学生做「ZED 双目相机 + YOLO 检测 + 双目测距」这类题目,最容易被低估的环节从来不是训练模型,而是拿到检测框之后怎么把那个二维矩形换算成一个可信的米数。单目估距靠物体尺寸先验,换一类目标就崩;纯深度相机在室外强光下又容易出大片空洞。ZED 自带双目立体匹配和出厂标定参数,把视差转深度的脏活接了过去,剩下的事情就集中到一处:把 YOLO 的框落到深度图上取一个可靠采样点,再反投影成三维坐标。这套方案适合两类人,一类是毕设或课程设计要做「目标检测 + 距离估计」完整演示的,另一类是给移动平台加一个低成本测距前端的。zedceju.py 就是把取图、推理、测距、绘制串在一条主循环里的入口脚本,结构拆得足够干净,你可以整段替换其中任意一环而不动其它部分。
2. 选型与几何:为什么走双目视差这条路,而不是单目估距
先把选型讲透,不然后面所有参数都没法解释。测距方案大致三条路:单目几何先验、主动深度传感器、双目被动立体。三条路各有自己的失效边界,选错方向的话,代码写得再漂亮也救不回来。
2.1 单目估距与主动深度相机各自的死结
单目估距的本质是「假设 + 相似三角形」。你知道画面里一个人的像素高度是 220 px,假设他真实身高 1.7 m,再拿焦距一除,就能反推出距离。这套算法在课堂演示里看着很美,实际一跑就露馅:坐着的人、小孩、弯腰捡东西的人,全都会被算成错误距离,而且误差随距离线性放大。更麻烦的是它要求目标贴地、相机光轴水平,稍微抬一下相机俯角,整条换算链就失效了。
主动深度传感器的问题在场景适应性。结构光方案依赖投射散斑图案,室外太阳光一压,红外图案信噪比直接掉到无法解码,输出一片空洞;ToF 方案虽然抗光性好一些,但在黑色吸光材质、镜面反射、多机同时工作互相干扰的场景下,深度值会成片跳变。这类失效不是调参能解决的,是物理原理决定的。
双目被动立体的取舍正好相反:它不发射任何东西,只靠左右两幅图里同名点的像素位置差来算深度。好处是室外可用、多机不干扰、成本低;代价是强依赖场景纹理,白墙、玻璃幕墙、纯色纸箱这类弱纹理区域匹配不上,深度图会出现空洞。ZED 这类设备的价值就在于它把这套匹配算法做进了 SDK,还额外提供了基于学习的深度模式,在弱纹理场景下比传统块匹配稳不少,同时把出厂标定参数直接开放给你读。测距链路里最容易出错的相机内参和基线,你不用自己标,这省下的不是一点时间。
2.2 视差、基线与深度的三角关系
这一节是整篇方案的地基,参数搞错一个符号或者一个单位,后面全盘皆输。双目测距的核心公式只有一条:
Z = fx * B / d其中Z是目标到相机光心的深度,fx是左目在水平方向的焦距(单位是像素,不是毫米),B是左右目光心之间的基线长度(单位米),d是同一个物点在左右两幅图上的横坐标之差,也就是视差,单位像素。
从这个公式能直接看出误差传播规律:对d求导得到δZ = Z² / (fx · B) · δd。这个式子说明误差随距离平方增长。举个具体数字,假设某分辨率下fx = 700 px、B = 0.12 m,在 5 m 处,1 个像素的视差误差就会带来约 0.3 m 的深度误差;到了 10 m 处,同样的 1 像素误差会放大到 1.2 m 左右。这就解释了一个常见现象:近距离测距看着挺准,十几米开外数字就开始乱飘,那不是代码写错了,是几何精度到头了。
反投影那一半同样要清楚。已知像素坐标(u, v)和该点的深度Z,三维坐标是:
X = (u - cx) * Z / fx Y = (v - cy) * Z / fy Z = Zcx、cy是主点坐标,同样从标定参数里读,不要手填。三个分量都拿到之后,sqrt(X² + Y² + Z²)是相机光心到该点的直线距离,Z是沿光轴方向的深度。这两个数值在很多场景下差得不小,报告里写清楚你用的是哪一个,答辩时能省掉一轮追问。
| 参数 | 含义 | 单位 | 获取方式 |
|---|---|---|---|
| fx, fy | 左右方向的焦距 | 像素 | 出厂标定 left_cam.fx / fy |
| cx, cy | 主点坐标 | 像素 | 出厂标定 left_cam.cx / cy |
| B | 基线长度 | 米 | get_camera_baseline() |
| d | 视差 | 像素 | 立体匹配输出,SDK 内部完成 |
| coordinate_units | 坐标单位 | 米 / 毫米 | 初始化时显式指定 |
| depth_mode | 深度模式 | 枚举 | NEURAL / ULTRA / QUALITY / PERFORMANCE |
提示:
get_camera_baseline()返回的是物理基线,单位固定为米,和你设置的coordinate_units无关;而深度图的数值单位跟随coordinate_units。这两个地方是单位踩坑的重灾区,第一次跑通之前先把它们打印出来对一遍。
2.3 检测框里取哪个点,决定了测距稳不稳
YOLO 输出的是(x1, y1, x2, y2)四个角点,深度图是一整片浮点矩阵。从框到点,中间隔着一个采样策略的选择,而这个选择对最终数字的稳定性影响极大。
最直觉的做法是取框中心点。问题在于框中心未必落在目标表面上:一个人双手叉腰站着,框中心很可能是他身后的墙;一辆车的框中心可能落在挡风玻璃上,而玻璃对双目匹配来说是灾难。中心点的单像素采样还特别怕噪声,深度图里偶尔冒出一个无效值,距离就直接跳成 0 或者异常大。
我用得最多的是「框底中部带状区域取中位数」。理由很实际:绝大多数需要测距的目标是地面物体(人、车、椅子、箱子),它们的框底边缘几乎贴着地面,这一带的深度最接近目标实际站位,而且横跨整个框宽的采样带能把单点噪声压下去。中位数比均值抗异常值,遇到几个无效像素也不会被带偏。
还有第三种策略,取框内所有有效深度值的直方图峰值。这种做法对遮挡场景更鲁棒,但计算量大一些,实时性要求高的时候要掂量。三种方式的取舍可以看这张表:
| 采样策略 | 稳定性 | 适用目标 | 主要风险 |
|---|---|---|---|
| 框中心单点 | 低 | 正对相机、纹理丰富的物体 | 中心落在背景或玻璃上 |
| 框底中部中位数 | 高 | 贴地目标(人、车、箱体) | 目标悬空时偏低 |
| 框内直方图峰值 | 中高 | 有部分遮挡的目标 | 计算开销大 |
3. 总体架构:一条主循环串起取图、推理、测距、落盘
zedceju.py 虽然是单个入口脚本,但内部职责必须分清。把所有逻辑堆在while True里是能跑,可一旦要换深度模式、换模型、加一路录像,你就得在几百行里翻找。我一般的做法是拆成五六个薄模块,入口只负责装配和调度。
3.1 模块划分与文件职责
| 文件 | 职责 | 关键依赖 |
|---|---|---|
| zedceju.py | 主入口、参数装配、主循环调度 | 下面全部模块 |
| camera.py | 相机打开、内参读取、图像与深度取回 | pyzed |
| detector.py | 模型加载、推理、结果解析与类别过滤 | ultralytics |
| measure.py | 坐标映射、深度采样、反投影、距离计算 | numpy |
| viz.py | 框绘制、距离标签、FPS 显示 | opencv |
| config.yaml | 阈值、路径、深度模式、分辨率 | 无 |
这样的拆法有个直接好处:调测距算法时不用开相机,把一段录好的深度图存成 npy,直接喂给 measure.py 就能验证公式对不对,省掉反复开关设备的来回折腾。相机这块尤其明显,ZED 反复 open/close 很容易因为 USB 带宽没释放而报错,拆开之后调试效率能高出一截。
3.2 主循环骨架
下面这段是整个方案的中枢,取图、推理、测距、绘制四件事按顺序排开,每一步的错误码都单独判,不要让失败悄悄滑过去。
# zedceju.py —— 主循环骨架 import cv2 import numpy as np import pyzed.sl as sl from ultralytics import YOLO from measure import map_box_to_depth, sample_depth, pixel_to_xyz def main(): # 1) 打开相机:只订阅左目彩色,深度模式按场景选,单位显式设为米 zed = sl.Camera() init = sl.InitParameters() init.camera_resolution = sl.RESOLUTION.HD720 # 720p 是速度与精度的平衡点 init.camera_fps = 30 init.depth_mode = sl.DEPTH_MODE.NEURAL # 弱纹理场景用 NEURAL,速度优先改 PERFORMANCE init.coordinate_units = sl.UNIT.METER # 后面所有换算统一按米 if zed.open(init) != sl.ERROR_CODE.SUCCESS: raise RuntimeError("ZED 打开失败:先查 USB 带宽、设备占用和供电") # 2) 读内参:fx、cx 由出厂标定给出,不要手填 calib = zed.get_camera_information().camera_configuration.calibration_parameters fx, fy = calib.left_cam.fx, calib.left_cam.fy cx, cy = calib.left_cam.cx, calib.left_cam.cy baseline = calib.get_camera_baseline() # 单位米,仅用于日志与校验 model = YOLO("weights/best.pt") runtime = sl.RuntimeParameters() image, depth = sl.Mat(), sl.Mat() while True: # 3) grab 是所有取图操作的前提,失败就跳过这一帧,不要带着旧数据往下跑 if zed.grab(runtime) != sl.ERROR_CODE.SUCCESS: continue zed.retrieve_image(image, sl.VIEW.LEFT) zed.retrieve_measure(depth, sl.MEASURE.DEPTH) frame = cv2.cvtColor(image.get_data(), cv2.COLOR_BGRA2BGR) depth_map = depth.get_data() src_hw = frame.shape[:2] dst_hw = depth_map.shape[:2] # 4) 推理:imgsz 与 conf 走配置文件,不要写死在调用里 result = model.predict(frame, conf=0.4, imgsz=640, verbose=False)[0] for box in result.boxes: label = result.names[int(box.cls.item())] conf = float(box.conf.item()) b = [float(v) for v in box.xyxy[0].tolist()] # 5) 原图坐标 -> 深度图坐标,分辨率不一致时必须缩放 bx = map_box_to_depth(b, src_hw, dst_hw) z = sample_depth(depth_map, bx) if z is None: continue u = (bx[0] + bx[2]) // 2 v = bx[3] X, Y, Z = pixel_to_xyz(u, v, z, fx, fy, cx, cy) dist = float(np.sqrt(X * X + Y * Y + Z * Z)) cv2.rectangle(frame, (bx[0], bx[1]), (bx[2], bx[3]), (0, 255, 0), 2) cv2.putText(frame, f"{label} {dist:.2f}m", (bx[0], bx[1] - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow("zedceju", frame) if cv2.waitKey(1) & 0xFF == 27: break zed.close() cv2.destroyAllWindows() if __name__ == "__main__": main()几个地方值得单独说。sl.UNIT.METER这一行看着不起眼,但它决定了retrieve_measure出来的深度值到底是米还是毫米,一旦漏设,SDK 默认给毫米,你按米去算三维坐标,结果会整整差 1000 倍,而且画面上的框还在正常绘制,非常有迷惑性。map_box_to_depth这一步也不能省,YOLO 推理用的是imgsz=640的缩放图,xyxy被映射回frame尺寸,而frame和depth_map的尺寸未必一致,中间少一次缩放,采样点就偏到别的物体上了。sample_depth返回None表示这个框内没有有效深度,直接跳过,别拿 0 去参与计算。
3.3 参数集中管理
配置项散落在代码里,改一次实验条件就得改五处,这种事做多了会疯。集中到一个 yaml 里,主循环开头读一次,后面全从字典取。
| 配置项 | 含义 | 推荐取值 | 调整影响 |
|---|---|---|---|
| camera_resolution | 相机分辨率 | HD720 | 升到 HD1080 精度略好但帧率下降明显 |
| depth_mode | 深度计算模式 | NEURAL | 换 PERFORMANCE 帧率提升、弱纹理变差 |
| confidence | 检测置信度阈值 | 0.4 | 降低到 0.25 召回变高、误检变多 |
| imgsz | 推理输入尺寸 | 640 | 提到 960 小目标更准、速度下降 |
| foot_ratio | 采样带高度比例 | 0.85 | 越接近 1 越贴框底,目标悬空时需下调 |
| valid_min / valid_max | 有效深度区间 | 0.3 / 20.0 | 收窄可过滤远景噪点 |
| log_csv | 结果落盘路径 | measure_log.csv | 关掉可省一点 IO 开销 |
4. 关键实现:从检测框到三维坐标的四步换算
这一章把第 3 章骨架里被函数名遮住的细节全部摊开。四步依次是解析检测结果、坐标对齐、深度采样、反投影与距离计算,每一步都有明确的输入输出和失败处理。
4.1 检测结果解析与类别过滤
YOLO 返回的boxes是一个张量容器,直接遍历能拿到xyxy、conf、cls。多数测距场景并不需要对所有类别算距离,一张街景里几十个框全标上数字,画面会糊成一片,而且小目标和远景目标本身测不准。按白名单过滤是更负责的做法。
# detector.py —— 结果解析:过滤类别、丢弃过小框 def parse_boxes(result, keep=None, min_area_ratio=0.003): h, w = result.orig_shape items = [] for b in result.boxes: label = result.names[int(b.cls.item())] if keep and label not in keep: continue conf = float(b.conf.item()) x1, y1, x2, y2 = [float(v) for v in b.xyxy[0].tolist()] # 面积占比过小的目标,框内有效深度像素太少,采样不可靠 if (x2 - x1) * (y2 - y1) / (w * h) < min_area_ratio: continue items.append({"label": label, "conf": conf, "box": (x1, y1, x2, y2)}) return itemsmin_area_ratio这个门槛不是凑数的。一个在画面里只占 0.1% 面积的框,换算到深度图上可能只有十几个像素,中位数统计根本谈不上稳健,测出来的数字波动会非常大,还不如不显示。result.orig_shape给的是原图高宽,用它算面积占比,比拿缩放后的尺寸更有意义。
4.2 坐标对齐与深度采样
坐标对齐这一步的逻辑简单但极容易漏。YOLO 的xyxy已经被映射回你传入的那张frame的尺寸,而frame是从 ZED 左目图像转出来的,尺寸和深度图未必一致。如果直接用xyxy去索引depth_map,轻则采样点偏移,重则越界报错。
# measure.py —— 框坐标按比例映射到深度图分辨率 def map_box_to_depth(box, src_hw, dst_hw): sx = dst_hw[1] / src_hw[1] # 宽度比例 sy = dst_hw[0] / src_hw[0] # 高度比例 x1, y1, x2, y2 = box return (max(0, int(x1 * sx)), max(0, int(y1 * sy)), int(x2 * sx), int(y2 * sy))采样函数则要处理三件事:把采样带放在框底、过滤无效值、用中位数抗噪。
# measure.py —— 框底带状区域取中位数 def sample_depth(depth_map, box, foot_ratio=0.85, valid_min=0.3, valid_max=20.0): x1, y1, x2, y2 = box h = y2 - y1 band_top = max(y1, int(y1 + h * foot_ratio) - 6) band = depth_map[band_top:y2 + 1, x1:x2 + 1] vals = band[np.isfinite(band) & (band > valid_min) & (band < valid_max)] if vals.size < 10: return None # 有效像素太少,这一帧不给结果 return float(np.median(vals))valid_min和valid_max的区间要按实际场景调。室内近距离作业,把上限压到 8 m 能有效滤掉穿透窗户打到室外的错误匹配;室外开阔场景则要放到 20 m 甚至更高。np.isfinite是必须的,深度模式在弱纹理区域可能输出NaN或inf,这些值一旦进入中位数计算会污染整个结果,而且不会报错,属于典型的黑匣子问题。
4.3 反投影与三维坐标计算
拿到采样点的深度z之后,把像素坐标转成三维坐标。这里有两种写法,一种是拿 SDK 的点云直接查,一种是自己按内参算。前者省事,后者透明,我一般两边都实现,用后者出结果、用前者做校验。
# measure.py —— 两种反投影写法 def pixel_to_xyz(u, v, z, fx, fy, cx, cy): """按针孔模型反投影,适合需要理解每一步的场景""" X = (u - cx) * z / fx Y = (v - cy) * z / fy return X, Y, z def xyz_from_sdk(point_cloud, u, v): """直接读 SDK 点云,单位与 coordinate_units 一致""" err, p = point_cloud.get_value(int(u), int(v)) if err != sl.ERROR_CODE.SUCCESS: return None X, Y, Z = p[0], p[1], p[2] if not np.isfinite(Z) or Z <= 0: return None return X, Y, Z两种写法的差别在于,自己算的时候z是带状区域的中位数,代表的是那一带的代表深度;而 SDK 点云查的是单个像素的值。前者抗噪,后者反映真实几何。我通常用前者给显示用,用后者验证一下两者偏差是否在合理范围内,偏差过大往往说明采样带落到了前景或背景上,是个很好的自检信号。
距离的最终输出要注意区分两种口径。Z是沿光轴的深度,sqrt(X² + Y² + Z²)是到光心的欧氏距离。画面边缘的物体两者差别能达到百分之十几,报告里如果不说明用的是哪个,答辩时很容易被问到。
4.4 叠加显示与结果落盘
可视化不只是为了好看,它是现场调试的主要信息来源。框上显示距离,右上角显示实时帧率,采样带画一条短横线,这三样东西加起来,你一眼就能看出采样点是不是落在目标上。
# 结果落盘:每帧每目标一行,方便事后算误差 import csv def log_row(path, ts, label, conf, X, Y, Z, dist): with open(path, "a", newline="") as f: w = csv.writer(f) w.writerow([round(ts, 3), label, round(conf, 3), round(X, 3), round(Y, 3), round(Z, 3), round(dist, 3)])留一份 CSV 的价值在验收阶段会体现得特别明显。现场看一眼觉得挺准,事后要出误差表的时候,没有原始记录就只能重新测一遍,而且不同时间的光照条件不一样,数据没法合并。我习惯在文件头写上日期、分辨率、深度模式三个字段,隔一周再看也知道这组数据是什么条件下采的。
5. 避坑与排查:双目测距最常翻车的五个点
这一章列的每一条都是实际跑下来真会遇到的问题,症状、成因、处理方式按顺序写清楚,出问题时可以当清单逐条对照。
5.1 深度图全是 0 或全是 NaN
现象很直接:框画出来了,距离标签要么不显示,要么显示 0.00 m,打印深度矩阵发现整片是 0 或者整片NaN。
原因通常有三类。第一类是grab没有成功却继续往下执行了,取到的是上一帧甚至未初始化的缓冲区;第二类是depth_mode被设成了NONE,只出图像不出深度;第三类是坐标单位没设,某些情况下深度值虽然正常但被当成毫米解读,采样时被valid_min直接过滤掉了。
处理方式是把这三件事分别验证:先打印grab的返回码,再打印depth_mode和coordinate_units,最后打印深度矩阵的min、max、mean三个统计量。三行日志能定位九成的问题。我一般会在主循环外面加一个「首帧自检」函数,启动时跑一次,把这几项打到控制台,省掉后面反复排查。
5.2 测出来是真实值的 1000 倍或千分之一
距离数字明显不对,但画面正常、框也正常,唯一的线索是系数量级差得整整齐齐。这是典型的单位混乱。
成因是coordinate_units与后续换算没对齐:SDK 深度图默认按毫米输出,代码里按米去算三维坐标,结果放大 1000 倍;反过来,代码里把毫米当米再乘一次基线,结果缩小 1000 倍。还有一种隐蔽情况是get_camera_baseline()返回的固定是米,而深度图是毫米,公式里两者混用,就算单位设了也照样错。
处理方式是全链路统一到米。初始化显式写sl.UNIT.METER,在启动日志里打印一行「深度图单位:米,基线:0.12 m」,任何人接手都能一眼确认。基线那个值只用于日志和交叉验证,实际测距走的是深度图采样,不参与公式计算,这样能避免单位混用。
5.3 同一个静止目标,距离数字来回跳
目标完全静止,读数在 ±0.3 m 之间反复横跳,画面上看得很明显。
成因有两个层次。浅层是采样点落在了目标边缘,框底那条带有一部分压到了背景;深层是深度图本身的时域噪声,立体匹配在低纹理区域逐帧结果不完全一致,这是被动双目的固有特性,不是 bug。
处理方式分两步。空间上,把采样带从框底 85% 位置往上收一点,同时把宽度从整个框宽缩到中间 60%,避开左右边缘;时间上,加一个长度 5 的滑动窗口中位数,输出取窗口的中位数而不是当前帧的值。加了这层滤波之后,静止目标的读数能稳定在 ±0.05 m 以内。要注意窗口别开太大,长度上到 15 以上,移动目标就会出现明显滞后,测出来的距离跟不上实际位置。
5.4 帧率掉到个位数,画面卡成幻灯片
现象是cv2.waitKey之后画面明显一顿一顿,实测帧率从 30 掉到 5 以下。
成因基本是三个叠加:推理输入尺寸用了默认的 1280,单帧推理就要几十毫秒;深度模式选了NEURAL,这一档精度好但计算量大;取图和推理串行在同一线程,相机缓冲区一直满着,grab反而变慢。
处理方式按收益排序。先把imgsz降到 640,这一步通常能拿回一倍以上的帧率;再把深度模式换成PERFORMANCE,弱纹理场景下精度会掉一些,但对贴地目标影响有限;如果还不够,把测距改成隔帧执行,检测每帧跑、测距每三帧跑一次,显示层用上一次的结果填充。真要上高帧率,就得引入生产者消费者两条线程,相机线程只管取图入队,推理线程从队列取,中间加一个长度 1 的队列丢弃旧帧,避免积压。
5.5 换一台机器就跑不起来
在自己机器上跑得好好的,拷到实验室另一台机器上,要么 import 报错,要么 CUDA 相关报错,要么权重路径找不到。
成因是依赖版本不一致和路径写死。深度学习这套东西对版本相当敏感,同一个包差一个小版本,算子行为就可能变;权重文件用绝对路径写死,换台机器目录结构一变就崩。
处理方式是三件事:第一,把依赖锁进requirements.txt,至少把主框架和推理库的版本钉住;第二,所有路径走配置文件加相对路径,启动时检查文件是否存在,缺了直接给出明确提示而不是抛一个看不懂的异常;第三,启动加一个环境自检,打印设备名、可用显存、相机序列号,出问题时这张「体检报告」比任何口头描述都管用。这套习惯是被坑出来的,前期多花十分钟,后期能省掉几小时的远程排查。
6. 验收与答辩:把「测出来准不准」变成一张可复现的误差表
演示能跑通只是及格线,真正决定评价的是你能不能拿出证据说明这套测距在什么范围内可信、在什么条件下会失效。我建议把验收拆成静态和动态两部分,各自有明确的采集规范和判据。
静态验收用卷尺量真值。把目标分别放在 1 m、2 m、3 m、5 m 四个位置,每个位置采集 30 帧,取中位数作为该距离的测量值,同时记录这 30 帧的标准差。标准差反映的是稳定性,绝对误差反映的是准确性,两个指标一起看才有意义。表格长这样:
| 真值 (m) | 测量中位数 (m) | 绝对误差 (m) | 相对误差 | 帧间标准差 (m) |
|---|---|---|---|---|
| 1.0 | 1.04 | +0.04 | 4.0% | 0.02 |
| 2.0 | 2.09 | +0.09 | 4.5% | 0.04 |
| 3.0 | 3.16 | +0.16 | 5.3% | 0.07 |
| 5.0 | 5.38 | +0.38 | 7.6% | 0.15 |
这张表能直接印证第 2 章那条误差传播公式:误差随距离增长得越来越快,5 m 处的帧间标准差是 1 m 处的七八倍。答辩时把公式和实测数据放在一起讲,说服力比单纯报几个数字强得多。上面的数值是某次室内实验的量级,你自己的场景会不一样,重点是把采集条件(分辨率、深度模式、光照、目标材质)写在表头,别人才能复现。
动态验收看曲线的平滑度。让目标沿直线匀速移动,把每帧的距离画成一条随时间的曲线,观察三件事:曲线是否单调、有没有突跳、滞后有多少帧。突跳通常说明采样点跳到了背景上,滞后说明滑动窗口开太大了。
有一个技巧值得单独讲:准备一段 20 秒的录屏,画面里同时有检测框、距离标签和 FPS,再配一张误差表。答辩现场网络不稳、设备抽风都是常事,录屏是唯一不会被环境影响的东西。我还会额外准备一段「故意失败」的片段,比如对着白墙或者强逆光拍,主动说明这套方案的边界在哪。主动暴露局限比被问出来强得多,评委通常更在意你知不知道自己方法的适用范围。
现场演示的顺序我一般固定成三步:先跑一次静态测距,报出真值和测量值;再让目标走一遍,展示动态跟踪;最后打开 CSV,指着原始数据说明这些数字是怎么来的。三步走完,基本不用再额外解释。
从那以后我每次带这类测距项目,都会在写第一行代码之前先把误差表的表头定下来,真值列、测量列、误差列、条件列四栏先摆在那,后面每跑一次实验就往里填。这个习惯逼着我在做方案设计时就想清楚怎么验证,而不是等到答辩前一天才手忙脚乱地补数据。希望帮到你。
本文还有配套的精品资源,点击获取