标签: 相机视觉

  • 回放输出相等≠实车安全:process_replay 的 14 个配置与三道门

    结论:process_replay 用 14 个 ProcessConfig 把「输入日志 → 单进程输出 → 多进程合流」重新跑一遍,再用容差比较参考日志;它产出的是回归证据,不是实车安全证明。决定性细节在于 get_output_msgs() 把输出 logMonoTime 设为输入最大时间加配置里的 processing_time,这是合成时间戳而非真实执行耗时;同时 model_replay 在 CI 环境中即使比较发现差异也把 failed 置假,test_fuzzy 明确排除含安全/控制/模型的 9 个进程。以下是对源码快照 c8fb906 的静态阅读,本地没有 Git 检出、参考日志或模型 LFS 实体,未运行回放。

    回放主链路:14 个配置与合成时间戳

    ProcessConfig 与 CONFIGS 声明各进程的发布输入、订阅输出、忽略字段、触发主题、容差、超时和 VisionIPC 依赖,含 selfdrived、card、controlsd 与模型等共 14 个。这是回放配置,不是受管进程全集;新增插件若要回放,必须补上配置与输入/输出契约。

    replay_process() 先调用 migrate_all() 统一旧日志 schema,再进入多进程调度;return_all_logs 会用新输出替换同主题原日志。ProcessContainer.start() 建立独立 OpenpilotPrefix、写入 Params/环境、建立 Pub/Sub 与可选 VisionIPC,再启动对应受管进程;ReplayContext 用假事件让输入发送与进程的 recv 对齐。

    run_step() 按触发回调积累消息,收集上一周期输出后送入本周期输入;相机主题从 FrameReader 重建 NV12 并经 VisionIPC 发送。到了 get_output_msgs(),输出 logMonoTime 被写成「输入最大时间 + 配置 processing_time」,这一步决定了整个比较链的时间语义。

    组件/函数 输入与行为 输出或限制
    ProcessConfig / CONFIGS 14 个配置声明发布输入、订阅输出、忽略字段、触发主题、容差、超时与 VisionIPC 依赖 回放配置,不是受管进程全集
    replay_process() 先 migrate_all() 统一旧 schema,再调多进程调度;return_all_logs 替换同主题原日志 返回模拟进程消息,不自动验证安全或性能
    ProcessContainer.start() 建隔离前缀、写 Params/环境、建立 Pub/Sub 与可选 VisionIPC 初始化依赖来自 generate_params_config() 与 generate_environ_config()
    ReplayContext 假事件令输入发送与进程 recv 对齐 只控制调度顺序,不等于真实总线抖动或丢帧
    get_output_msgs() 输出 logMonoTime = 输入最大时间 + 配置 processing_time 这是合成时间戳,不是实测执行耗时
    _replay_multi_process() 外部日志消息与进程新产出消息按 logMonoTime 合流,结束时冲刷残余输出 受配置主题图与合成时间戳约束,不能代替实车闭环时序

    还有两处回调值得单独记:ModeldCameraSyncRcvCallback 按单/双摄像头凑齐触发帧,MessageBasedRcvCallback 用指定主题触发周期。ProcessOutputCapture 可选地把被测进程 stdout/stderr 重定向到 /tmp,结束后读回,但仅辅助定位崩溃或告警,不参与消息差异判定。

    初始状态也会显著左右结果。get_custom_params_from_lr() 可从前一段日志取 CarParamsPrevRoute、标定、车辆参数与扭矩学习状态,因此初始状态不一致会造成差异,但这并非必然的算法回归;相机主题到 VisionIPC 流的对应关系另由 vision_meta.py 描述。

    输入迁移、比较与三类测试门

    migrate_all() 组织传感器、车辆状态、定位、驾驶模型等迁移,并按选项补 manager、panda 和相机状态;声明 product 的规则在该主题已存在时跳过。因此比较对象是迁移后的日志,不是原始字节流,新增 schema 必须定义可解释的迁移策略。

    compare_logs() 先过滤忽略主题、要求消息数量及顺序/类型对齐,再清零忽略字段后递归比较 Cap’n Proto,浮点按绝对/相对容差判断;长度或顺序不一致会直接报错。忽略字段不等于相等证明,时间字段通常被忽略。

    test_processes.py 从远端取片段与参考输出,检查输出主题与有效消息占比,再按配置比较并生成字段报告。它的默认覆盖并不全:默认排除 modeld、dmonitoringmodeld,其余 12 个配置中仅 card、controlsd、lagd 跑全部选定车型,其余只取 HYUNDAI/TOYOTA 片段。

    层次 实现与判定 易误读处
    历史 schema migrate_all() / migrate() 汇总替换、插入与删除后按时间排序 比较的是迁移后日志;声明 product 的规则在主题已存在时跳过
    字段比较 compare_logs() 要求数量及顺序/类型对齐,清零忽略字段后递归比较 忽略字段不是相等证明;不同长度或顺序直接报错
    通用回归 test_processes.py 检查输出主题与有效消息占比,再生成字段报告 默认排除两个模型进程,仅三个配置覆盖全部选定车型
    差异报告 diff_process() / diff_report() 按输出主题收集逐字段差异 报告只是解释差异,不能把预期差异自动视为安全可接受
    模型专项 model_replay() 用相机帧跑道路与座舱模型,检查最大/平均执行耗时 PC 路径使用 0.3 容差;CI 下发现差异仍 failed=False
    模糊测试 test_fuzzy.py 生成随机 Cap’n Proto 输入并回放 明确排除 9 个进程(含安全/控制/模型),只覆盖配置子集

    三类门里最容易被高估的是模型专项。PC 路径忽略多组模型字段且使用 0.3 容差;在 CI 环境中即使比较发现差异,主程序也会把 failed=False 用于报告,不能当作严格数值门。

    框架自身的静态风险

    run_step() 把周期操作置于 Timeout 内,但 _replay_multi_process() 最后冲刷残余输出所调用的 get_output_msgs() 并不在这个超时上下文里;它等待的 wait_for_one_event() 默认超时为 -1。若被测进程在尾部不再接收,回放可能卡住。

    另一条待测路径是容器加入 containers 后 start() 中途失败:finally 仍会调用 stop(),而此刻 rc 可能尚未初始化。这两处都是源码推导出的故障路径,尚未复现,不能用一次正常回放证明框架对崩溃也可靠。

    对插件与硬件重构的验证含义

    1. 保留每个插件的输入/输出主题、schema 版本、触发条件、初始 Params 与模拟环境,才能复用 ProcessConfig 与多进程回放;重构前先保存原版参考输出及模型/车型/固件哈希。
    2. 把「消息值回归」「最坏时延」「故障退出」「panda 安全策略」拆成独立门。processing_time 只是合成排序参数,忽略执行时间字段的回放结果不能证明新硬件满足控制周期。
    3. 模型插件需单独补齐 LFS 权重、NV12 帧、目标设备与更严格数值阈值;新车型还须增加能覆盖转向/制动路径的片段与安全测试。
    4. 本地源码归档没有 .git、ref_commit 或 fakedata;test_processes.py 还要求获取当前 Git commit、远端路段与参考日志。本次不运行这些测试,也不把静态阅读标记为「回归通过」。

    常见问题

    回放输出和参考日志一致,就说明改动是安全的吗?

    不一致是回归信号,但一致只是回归证据。get_output_msgs() 用的是「输入最大时间 + processing_time」的合成时间戳,model_replay 在 CI 下即使发现差异也 failed=False,test_fuzzy 又排除安全/控制/模型进程,因此相等不能证明实车安全、最坏时延或新硬件兼容。

    process_replay 覆盖了多少进程?

    ProcessConfig / CONFIGS 声明 14 个配置,含 selfdrived、card、controlsd 与模型等,这是回放配置而非受管进程全集。默认测试还排除 modeld、dmonitoringmodeld,其余 12 个配置中只有 card、controlsd、lagd 跑全部选定车型。

    比较时哪些差异会被忽略?

    compare_logs() 先过滤忽略主题,清零忽略字段后再递归比较 Cap’n Proto,浮点按绝对/相对容差判断,时间字段常被忽略;但消息数量、顺序或类型不齐会直接报错。忽略字段不等于相等证明。

    为什么回放有可能卡住?

    run_step() 的周期操作在 Timeout 内,但最后冲刷残余输出的 get_output_msgs() 不在该超时上下文,它等待的 wait_for_one_event() 默认超时为 -1;若被测进程尾部不再接收就可能卡住。该路径是源码推导的,尚未复现。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/PROCESS_REPLAY_VALIDATION_DEEP_DIVE.md(回放主链路、14 个配置与迁移/比较/测试门)
    • 项目 02 —— openpilot-research/PROCESS_REPLAY_SCHEMA_MIGRATION_RULES_OUTPUT_VALIDITY_DEEP_DIVE.md(源文档指向的 19 条迁移规则专题)

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • 52 张预览自动校验:五屏 HUD 怎么把「文字出界」变成一条断言

    结论:当原型扩展到 5 块屏 × 5 套主题 × 全部副视图之后,人工目检一定会漏。capture.mjs 用 Playwright 遍历这个矩阵、输出 52 张预览,并把六类检查写成断言——其中最关键的一条是:用 getBBox() 逐项检测每个 <text> 是否越出画布,把「文字出界」从肉眼问题变成可回归的失败。

    为什么人工目检在 5×5 矩阵下必然漏

    5 块屏乘以 5 套主题,再乘上每块屏的副视图,需要核对的画面数量在几十到上百量级。人工看图的失效方式很规律:前十几张会认真比对,之后注意力衰减,最后只看「有没有明显崩掉」。而越界的文字往往只差几个像素——恰恰是最不容易被肉眼抓住的那类问题。

    更麻烦的是回归:一旦改了某个令牌或布局推导规则,理论上所有尺寸都要重看一遍。人工流程做不到这一点,于是问题会以「上个版本还好好的」的形式在下游暴露。

    遍历矩阵与产出

    脚本按「屏幕 × 主题 × 副视图」三重循环遍历,每张画面截图输出到 previews/,同时执行断言。最终产物是 52 张预览加一份断言结果——前者用于人工确认视觉意图,后者用于机器判定是否回归。

    运行方式上有一个实际操作细节:node 不在系统 PATH 上,必须用完整路径调用,因此脚本入口固定写明了解释器路径,避免在不同机器上以不同的 Node 版本跑出不同结果。

    六类自动断言

    断言 检查什么 为什么重要
    viewBox 原生像素 SVG viewBox 是否等于各板原生分辨率 防止某个尺寸悄悄套用了别的画布
    文字越界 所有 <text> 的 getBBox() 是否超出画布 最容易被肉眼漏掉、也最影响可读性
    undefined 泄漏 SVG 内容中是否出现字符串 undefined 数据绑定失败的典型症状
    默认主题 默认主题是否为 pro、主题切换是否生效 防止切换逻辑静默失效
    状态推进 OBD 回放推进、计时完成、断连状态、路书翻页 验证交互状态机而非静态版式
    移动端溢出 移动端是否出现横向溢出 预览页本身的可用性

    getBBox 为什么是关键一条

    浏览器渲染文本时,元素的实际包围盒由字体度量决定,而不是由设计稿里的数字决定。所以「设计上放得下」和「渲染后放得下」是两件事。getBBox() 返回的是渲染后的真实尺寸,用它逐项比对画布边界,才能把这类问题从主观判断变成布尔结果。

    这条断言的直接收益是:改了字号、字距或字体之后,脚本会立刻告诉你哪些画面出界,而不需要人再逐张看。它同时也解释了为什么「换字体」必须重跑校验——字体一换,字宽就变,原本贴边的文本可能立刻越界。

    字体是隐性风险

    原型使用的是系统字体(Bahnschrift / Microsoft YaHei / Impact / Georgia / Consolas)。这些字体在桌面浏览器上可用,但不一定允许嵌入式再分发。因此固件移植前必须重新选定字体,并重跑一次越界检测。

    这类风险的特点是「在原型的阶段看不出来,在移植阶段集中爆发」。把它写进校验清单,而不是留到移植时才发现,是这套流程设计上比较值的一点。

    自动校验证明不了什么

    边界必须写清楚。这套校验只覆盖原型层的布局与状态逻辑,不证明:ESP-IDF/LVGL 的点屏、SPI/DSI 实际帧率、实车 PID 支持,以及计时精度。所有画面数值均为模拟数据并永久标注 DEMO,档位与油压尚无车型适配,实机须接入受保护的 OBD 网关。

    另外两项待实机确认的项:3.5 寸现有固件 DEMO 已采用横屏坐标,但尚未移植这套 UI;1.64 寸的横屏旋转与触摸映射仍待实机验证。浏览器里的通过,不等于屏幕上的通过。

    实测之后的链路可以这样分工:赛道上的实时状态由 SR-Telemetry 实时遥测云 以低于 500 ms 的延迟推给场边教练;像 肇庆国际赛车场 这类高差明显的场地,更适合用来验证坡度相关信息的显示优先级。

    FAQ

    为什么不能靠设计师人工看预览图?

    因为 5 块屏 × 5 套主题 × 副视图是上百量级的画面,人工注意力会衰减,而文字越界往往只差几个像素。改一次规则就要全部重看,人工流程无法回归。

    getBBox 检测的是什么?

    检测渲染后的真实文本包围盒。字体度量与实际渲染尺寸只有在浏览器里才能确定,所以「设计上放得下」不等于「渲染后放得下」。

    为什么换字体要重跑校验?

    因为字宽会变。原本贴边的文本在换字体后可能立刻越界,必须重跑一次越界检测。

    自动校验通过了能直接上实机吗?

    不能。它只覆盖原型层的布局与状态逻辑,不证明 ESP-IDF/LVGL 点屏、SPI/DSI 帧率、实车 PID 支持与计时精度;1.64 的横屏旋转与触摸映射也仍需实机验证。

    参考资料

    • 项目 06 —— D:/AI/PROJECTS/kno/capture.mjs(Playwright 自动校验脚本)、previews/(52 张预览)
    • 项目 06 —— D:/AI/PROJECTS/kno/README.md(校验清单与已知限制)
    • 项目 04 —— research/instrument-matrix.md(风格覆盖规则与 52 张预览口径)

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

    {“@context”:”https://schema.org”,”@graph”:[{“@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”为什么 HMI 原型不能靠设计师人工看预览图?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”因为 5 块屏乘以 5 套主题再加副视图是上百量级的画面,人工注意力会衰减,而文字越界往往只差几个像素。改一次规则就要全部重看,人工流程无法做回归。”}},{“@type”:”Question”,”name”:”getBBox 越界检测到底检测什么?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”检测渲染后的真实文本包围盒。字体度量与实际渲染尺寸只有在浏览器里才能确定,所以设计上放得下不等于渲染后放得下。”}},{“@type”:”Question”,”name”:”为什么更换字体后要重跑越界校验?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”因为字宽会变。原型使用系统字体(Bahnschrift、Microsoft YaHei 等)且不一定允许嵌入式再分发,移植前必须重选字体并重跑文字边界检测。”}},{“@type”:”Question”,”name”:”自动视觉校验通过后能直接上实机吗?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”不能。这套校验只覆盖原型层的布局与状态逻辑,不证明 ESP-IDF/LVGL 点屏、SPI/DSI 实际帧率、实车 PID 支持与计时精度;1.64 的横屏旋转与触摸映射仍需实机验证。”}}]}]}

    延伸阅读