Featured image of post 给 AI 装上一双能测距的眼睛

给 AI 装上一双能测距的眼睛

YOLOv8 + MiDaS + 激光雷达的感知栈。核心决定是:LiDAR 只做深度标定,不做危险判定。

你戴着它,问一句「前面有什么」。

它需要回答的其实不是「前面有一个人」——而是「前面 0.8 米有一个人」。

这两句话对一个只会做目标检测的系统来说,差着一整个维度。


只有 2D 检测,是看不见「多远」的

YOLO 能给你一个框、一个类别、一个置信度。

1
person 0.89  [x1,y1,x2,y2]

但它不知道这个人在一米外还是五米外。

而这件事在真实场景里是致命的:

  • 「前面有人」→ 你继续走
  • 「前面 0.4 米有人」→ 你必须停

同一个检测结果,两种完全相反的行动。

所以我给这个系统加了两个东西:深度估计和激光雷达。


三层感知

VisionMate 感知管线

整个系统的骨架是这样的:

层干什么
① 输入文本指令,或 Whisper 实时语音转写
② 调度Orchestrator 把自然语言映射到一个视觉能力
③ 视觉YOLOv8n 检测 / MiDaS 深度 / EasyOCR 文字
④ 标定用 LiDAR 快照重标定深度尺度
⑤ 输出hazard_level + 人话 message + 结构化 meta

对外是 FastAPI 的 MCP 接口:

1
2
POST /api/v1/mcp
{"text": "描述周围"}

返回里既有给人看的句子,也有给程序用的字段。


五个能力,带优先级

调度器不猜——它把指令映射到一个明确的 function:

1
2
3
4
5
"vision.ocr_read":       "high"     # 读文字
"vision.detect_objects": "medium"   # 检测物体
"vision.distance_query": "medium"   # 问距离
"vision.describe_scene": "medium"   # 描述场景
"vision.check_hazards":  "low"      # 判断危险

check_hazards 优先级最低,因为它要调用前面全部能力才能回答——它是管线末端,不是入口。

意图分类有两路:

  1. 规则匹配(关键词、正则)—— 快、可预测
  2. sklearn 模型(intent_sklearn.joblib,965 KB)—— 处理规则覆盖不到的说法

规则优先,模型兜底。


核心设计:LiDAR 只做标定,不做判定

这是整个项目里我最想讲的一个决定。

先说结论(这也是代码注释里的原话):

基于视觉深度和 YOLO 距离统一评估危险,LiDAR 仅用于深度标定。

为什么不直接用雷达测距?

因为雷达虽然准,但它只有一条线。

forward_min_distance 是 ±30° 扇区内的最小距离——它告诉你「正前方最近的东西有多远」,但不告诉你是谁、有几个、在哪。

如果我拿这个数字直接判定危险,会有两个问题:

  1. 误报:地上一个 5 厘米的纸盒,距离 0.3 米 → 判定「危险」。但你可以直接跨过去。
  2. 漏报:右边 0.5 米有个台阶,不在 ±30° 扇区内 → 雷达看不到,可你会撞上。

雷达给的是标量,安全需要的是语义。

那怎么用雷达?

把它当标尺,不当裁判。

MiDaS 输出的是相对深度——它知道「这个人比那面墙近」,但不知道「近多少米」。所以:

1
2
3
1. 先做线性投影,把相对深度映射到一个先验区间 near=0.4m, far=8.0m
2. 再用 LiDAR 的 forward_min_distance 对这个投影做一次重标定
3. 如果雷达认为近,就动态放宽 YOLO 的置信度阈值(0.5 → 0.3)

第 3 步是个很实用的联动:雷达说近,就宁可多报一点。因为近距离下漏检的代价,远大于误检。

深度估计可视化

上图是 MiDaS 的输出叠加——颜色代表距离(暖色近、冷色远)。可以看出它能把空间关系画得很清楚,但具体数值需要标尺。


危险分级

最终判定基于两条证据:

1
2
3
if nearest_box_dist < 0.5:   hazard_level = "danger"   # 有物体且极近
elif nearest_box_dist < 1.0: hazard_level = "warning"  # 有物体但还远
else:                        hazard_level = "safe"

没有检测到物体时,退回到视觉深度给出的前方最小距离。

所以:

  • 有语义 → 用「谁 + 多远」
  • 无语义 → 用「最近的表面多远」

两条路兜底,不会出现沉默失败。


没有硬件也能跑:Stub 后端

这套系统依赖摄像头、激光雷达、YOLO 权重、MiDaS 权重。换一台机器就全没了。

所以每个后端都有两套实现:

真实后端Stub 后端
YOLOv8n固定框 + 固定距离
MiDaS合成深度图
EasyOCR固定文本
LiDAR模拟快照

配置文件一行就能切:

1
SIMULATION_MODE = 1

这不是为了偷懒,是为了让测试能跑。 有了 Stub,CI 里不需要 GPU、不需要串口、不需要摄像头,全套流程照样能验证。


测试:让「软融合」这个说法可被验证

三个测试文件,分别守一件事:

测试在验证什么
test_p0_smokeAPI 契约、分阶段耗时、雷达快照新鲜度
test_p1_lidar_soft_fusion紧急闸门、自适应阈值
test_p2_depth_metadata深度可用/缺失时的元数据与降级

其中最有价值的是 P1:

1
2
assert result.meta.get("hazard_level") == "high"
assert result.meta.get("forward_min_distance") == pytest.approx(0.4)
1
assert thresholds.get("yolo_conf_threshold") == pytest.approx(0.3)

这两条断言把「雷达软融合」从一个形容词变成了一个可验证的行为:

  • 雷达报 0.4 米 → 危险等级必须是 high
  • 雷达报近 → 置信度阈值必须降到 0.3

如果哪天我重构代码把这个逻辑弄丢了,测试会立刻失败。

P2 守的是另一件事——深度失败时必须明确降级,而不是静默给出错误的距离:

1
2
assert result.meta.get("depth_error") == "midas_missing"
assert all(box.dist is None for box in result.objects)

宁可承认「我不知道多远」,也不编一个数字。


为什么这个项目值得做

因为「AI 能看见」这件事,大多数人理解成「能识别」。

但识别和感知是两件事:

识别感知
这是什么这是什么 + 多远 + 算不算危险
一张图连续的一段时间
给人看给行动用

一个只会识别的系统,永远只能当玩具。

要让它能驱动机器人、能辅助视障者、能放进 XR 头显——它必须先知道距离。


技术栈

Python · FastAPI · YOLOv8n · MiDaS · EasyOCR · Whisper · RPLIDAR A1 · scikit-learn · pytest


这个项目最后教会我的,不是怎么调深度模型。

是「多一个传感器」不等于「多一份信任」——多一个传感器,意味着你要更清楚地决定该在什么时候听它的。

使用 Hugo 构建
主题 Stack 由 Jimmy 设计