用视觉模型和家庭助手记录口岸拥堵
作者用 Home Assistant 定时抓取公开交通摄像头画面,让视觉模型判断排队情况,再保存为可绘图的传感器数据。文章覆盖抓图检查、模型调用、存储、运行时段控制及故障排查。
AI 深度解读
作者想解决一个很具体的问题:每年开车回家,边境口岸到底哪天、哪个时段更少排队?导航只能告诉他眼下堵不堵,于是他把公开摄像头、Home Assistant 和视觉模型接起来,持续积累可比较的历史记录。
这套系统没有用 AI 预测车流,而是让模型给截图打分,再由代码决定哪些结果可以进入图表。值得程序员借鉴的,是它如何处理坏图片、缺失数据、采集成本和自动化测试。不过,系统已经跑通,不等于它已经证明哪天出发最好。
先按行驶方向收集,图表才回答得了问题
作者的路线经过多个口岸,每个口岸又有不同方向的摄像头。最初按「入境、出境」分组,后来发现不适合整条路线:穿过不同国家时,入境和出境会交替,真正需要比较的是沿路线向南还是向北行驶。
因此,每个摄像头对应「口岸+行驶方向」,最终做成两张历史图,每张图在同一时间轴上展示一个方向的各个口岸。
采集时间也按方向划分。作者根据学校假期安排窗口:去程通常从首个地区放假前的周五开始,返程通常在首个地区假期结束前一周开始。德国这类各地区错峰放假的国家,车流可能分批出现。这里是作者安排窗口的依据,并不是文中已经验证的交通规律。
系统接入了 13 个摄像头,只在窗口内、白天每 30 分钟采集一次,不需要 GPU。自动化用 minutes: "/30" 触发,以 sun.sun 的 above_horizon 状态限制白天执行,并设置 mode: single。
摄像头通过 Generic Camera 集成接入,只需要静态图片 URL。全部放进 Traffic Cams 区域,脚本遍历区域里的摄像头,后续增加摄像头就能通过 UI 完成。
这个设计也有维护成本:实体 ID 来自添加时填写的名字,修改显示名称并不等于修改实体 ID。作者踩过重命名后旧引用失效的坑,因此建议把采样间隔、日期、供应商这些可能变化的信息留在配置里。
模型负责看图,代码负责判定数据能不能用
完整流程是:camera.snapshot 下载截图,验证文件,调用 ai_task.generate_data,再写入传感器。
第一道检查就抓到了实际故障。作者审计的 65 次抓取中,有 1 次拿到 HTTP 500,文件内容只有 26 字节,却可能以 .jpg 保存。请求或动作成功,不能证明拿到的是图片。
下面这段检查要求文件存在、至少 5000 字节,并以 JPEG 魔数开头:
shell_command:
# 仅当文件存在、至少 5000 字节且以 JPEG 魔数开头时输出 "ok"
traffic_cams_probe: >-
sh -c 'f="{{ path }}"; [ -f "$f" ] && [ "$(wc -c < "$f")" -ge 5000 ] &&
[ "$(head -c 3 "$f" | od -An -tx1 | tr -d " \n")" = "ffd8ff" ] && echo ok'
要留意 sh -c 这一行。作者指出,模板化的 shell_command 不会自动提供这里需要的 shell 执行语义,管道和 && 要靠它解释;容器里也需要有 wc、head、od、tr。检查失败后重试一次,再失败就记录缺失,不调用模型。
文件有效,还不代表画面有效。离线提示、宣传海报和黑屏,都可能被模型误判成「没有车」。
作者通过 OpenRouter 接入一个小型低价视觉模型,使用 ai_task.generate_data 的 structure 返回结构化字段:
congestion是 0 到 4 的拥堵评分,0 表示空闲畅通,4 表示长队停滞。image_kind可取live、poster、offline、night、unclear。note保存画面描述,overlay_time保存模型读出的画面时钟。
代码随后执行硬规则:image_kind != live 就写入 -1;只有 live 才把评分限制在 0..4。
模型返回了一个数字,不代表这个数字就有资格进入统计。
overlay_time 也只是辅助证据。作者做过一次 12 分钟审计,分 5 轮、每轮间隔 3 分钟;所有图片都在变化,但一个摄像头时钟慢了 26 分钟,另一个快了两小时,还有一个画面倒置。因此,画面时钟异常不能直接等同于视频冻结。
缺失要留空,测试要能证明系统真的执行过
每个摄像头写入四个 helper:拥堵值使用范围为 -1 到 4 的 input_number,另外三个字段使用 input_text。图表读取的是上层模板传感器:
template:
- sensor:
- name: "Crossing A south→north"
unique_id: traffic_crossing_a_sn
state_class: measurement # 保留长期统计,便于之后导出 CSV
state: >-
{% set v = states('input_number.traffic_crossing_a_sn_congestion') | float(-1) %}
{{ v if v >= 0 else none }}
attributes:
crossing: Crossing A
direction_group: southbound
image_kind: "{{ states('input_text.traffic_crossing_a_sn_image_kind') }}"
关键是 {{ v if v >= 0 else none }}:把缺失标记转成空值,让图表断开,而不是画出一个表示畅通的假零。state_class: measurement 用于长期统计,避免只依赖默认保留 10 天的 recorder 历史;展示卡片使用 history-graph,示例设置为 hours_to_show: 72。
成本主要靠减少无用采集来控制。按方向设置窗口,相比整个夏天采集所有方向,模型调用量大约减半;从每 20 分钟改成每 30 分钟,又减少三分之一。
作者测试共花费 $0.04,调用次数没有精确统计,约 85 次,据此估算每次 $0.0005、整个季节约 $11。这不是完整季节的实测账单。他建议把 API key 的额度设为预算的约两倍。截图每张 15 到 55 KB,预计一季约 640 MB,全部保留,方便以后重新评分。
测试也围绕确定性规则展开。作者用 Jinja2 渲染 YAML 中的真实模板,完成约 300 项检查,包括重复键、helper 是否齐全、评分限制、非实时画面拦截,以及窗口起止日和重叠日期。
脚本另有 ignore_windows 测试开关。窗口外正常执行应当没有新文件,强制执行则应当每个摄像头产生一张图片和一次传感器更新。
这里有个实测陷阱:同一条描述连续出现时,Home Assistant 不会更新 last_updated。因此,「这次有没有运行」应检查新截图等会变化的产物,不能只看描述字段的更新时间。
我的判断
我认同这套方案对职责的划分:视觉模型处理难以手写规则的画面理解,代码处理范围、分支和缺失值。比起让模型直接决定是否保存结果,这样更方便测试,也更容易更换模型。
但它目前更像一个历史观察工具,还不能当作可靠的出发决策器。作者没有给出人工标注与模型评分的对照,也没有展示使用图表后实际少排了多久。0 到 4 来自提示词,单帧能显示车辆密度,却难以证明车速和等待时间。不同摄像头的视角、清晰度也可能影响评分可比性。
这篇验证了采集管道能工作,还没有验证评分足以指导出行。
作者自己提出的后续改进很关键:人工标注几十张图进行校准;每条记录保存模型和提示词版本;通过感知哈希检测连续 N 次不变的画面。现有文件检查和 image_kind 都不能可靠识别「看着像实时画面、实际上已经冻结」的输入。
假期窗口也有取舍。它省钱,却可能漏掉预想之外的高峰。作者建议全年每 3 小时低频采集一次,再依据数据确定窗口,但这还是改进设想。原文也没有公开具体模型名,不能据此保证其他模型有同样的效果或费用。
能拿来做什么
- 做 Node 视觉任务时,可以复用这条处理顺序:先检查文件类型和大小,再请求模型,最后用代码验证字段;下载失败、画面不可用和业务评分应分别保存。
- 给前端历史图表接入 AI 结果时,要区分缺失和零值。无法判断的截图应该显示空档,不能让接口异常变成「一路畅通」。
- 用 AI 编程助手生成这类自动化脚本后,优先验证日期边界、非法分数和强制执行开关。测试尽量调用实际配置中的规则,减少测试与运行逻辑各写一份造成的偏差。
- 准备长期比较模型输出时,从第一天就保留原图、模型标识和提示词版本。更换模型后,才能判断曲线变化来自真实车流,还是评分口径改变。