Changelog 2026 07 - NUSNiuMu/insight-capture-dashboard GitHub Wiki

Changelog 2026-07

新条目加在顶部。每条包含:做了什么、为什么、怎么验证的。


2026-07-29 · Firefox ESR 140 + NVDEC 原分辨率三路解码 POC

做了什么

  • 在隔离容器中安装 NVIDIA Jetson FFmpeg 4.4.2,并用 h264_nvv4l2dec 验证 /dev/v4l2-nvdec 可以实际完成 H.264 硬解。
  • 使用 Firefox ESR 140.13.0esr 加载 NVIDIA FFmpeg;对临时 FFmpeg 源码做了两处 POC 修改:H.264 默认优先选择 h264_nvv4l2dec,以及在取帧前补齐 width/height/pixel format。
  • 保持发布原分辨率不变:Insight3 两路 544×640,Insight9 一路 1088×1920。

为什么

  • 正式 Firefox 152 当前通过软件解码 H.264,需要验证 Jetson NVDEC 能否被 Firefox 的 WebRTC/RDD 路径真正使用,以及硬解是否能解决三路视频与 Babylon 3D 同屏时 presented 长期不到 20 fps 的问题。

怎么验证的

  • 关闭 RDD 沙箱做诊断时,三路日志均明确选择 codec h264_nvv4l2dec,浏览器解码吞吐完整跟上约 20/20/30 fps。
  • 关闭详细日志后的三轮页面采样,Insight3 A 的 presented 为 13.36/16.82/15.96 fps,Insight3 B 为 15.14/14.88/18.08 fps, Insight9 为 16.88/15.24/18.21 fps;硬解没有把最终呈现稳定提升到 20 fps。
  • 同场景进程快照中 Firefox parent/RDD/content 约为 89.8%/38.7%/36.1% CPU,RDD 仍有明显的帧拷贝开销;Jetson 总体 CPU 各核约 67%–87%,GR3D 约 11%–95%,说明剩余卡点主要在浏览器合成/3D GPU 竞争和硬解输出到 Firefox 的内存拷贝,而不是 H.264 解码吞吐。
  • 开启 RDD 沙箱后稳定复现 broker 拒绝: open /dev/v4l2-nvdec(errno -13),三路解码为 0。因此不能以 MOZ_DISABLE_RDD_SANDBOX=1 作为产品方案;正式化至少需要自编 Firefox ESR, 在 aarch64 Tegra RDD broker policy 中允许 /dev/v4l2-nvdec,再做安全与性能回归。
  • 测试 Firefox 和临时容器均已停止并删除,正式 dashboard 未中断;本次 POC 未修改主仓库代码。

2026-07-29 · 增加 WebRTC 端到端帧率统计

提交:6787fd2(main)

做了什么

  • 每路相机新增源输入、主进程处理、最新帧队列覆盖、IPC 发送、worker 接收、 appsrc 接收、H.264 编码输出的累计计数和阶段 fps。
  • 浏览器每秒通过 RTCPeerConnection.getStats() 采集接收、解码、丢帧、 packet loss 和 jitter,并保留现有实际呈现帧率。
  • 鼠标悬停相机卡片的 fps 数字可查看完整链路;后端原始数据同时放进 /api/cameras 的 webrtc_stats。

为什么

  • 仅比较 ROS 接收帧率和页面呈现帧率无法区分主进程覆盖、IPC、硬编码、 网络、浏览器解码或合成丢帧,需要逐段速率才能对症优化。

怎么验证的

  • 三路真实 viewer 稳定采样:Insight3 A/B 的 source → IPC → appsrc → encoded 均约 20 fps,Insight9 均约 30 fps; worker 限频、caps 不匹配和 appsrc 失败均为 0。
  • Python/JavaScript 语法、前端构建、差异检查和合成 session 计数测试通过; 重启并刷新 kiosk 后三路 WebRTC 会话均恢复。

2026-07-29 · 修复 30 fps WebRTC 预览隔帧降速

提交:0fe1d92(main)

做了什么

  • 正常 30 fps WebRTC 会话不再经过 1/30 s 墙钟时间门控,直接把主进程交付的 最新帧送入带下游丢帧策略的 GStreamer appsrc。
  • OBS/采集性能模式的低帧率目标仍保留原有限制,不改变录制优先策略。

为什么

  • 相机标称 30 fps 时实际可能约为 30.02 fps,连续帧间隔略小于 1/30 s; 原来的严格比较会稳定地隔帧丢弃,把 Insight9 预览降到约 15 fps。

怎么验证的

  • 用 30.02 fps 合成到帧节拍验证连续两帧均被转发。
  • 重启 insight-dashboard 后 WebRTC health 恢复,三路后端输入稳定为约 20/20/30 fps,三路相机均保持可见且非 stale。

2026-07-29 · v2.0.2:Dockerfile 拆分 runtime/dev,客户镜像去掉调试用 Chromium

提交:main(v2.0.2,b1729a0)

做了什么

  • docker history 核对镜像构成时发现 Playwright headless Chromium(1.32GB) 是 Dockerfile 自己注释里明确写的"不再是设备端 kiosk,只用于开发时无头页面 验证"——但之前单一 stage 的 Dockerfile 会把它无条件打进包括客户发布镜像在内 的每一次构建。
  • 把最终 stage 拆成 runtime(不含 Chromium)和 dev(runtime 基础上加 Playwright/Chromium)两个 target。根目录开发 docker-compose.yml 默认构建 target: dev;scripts/build_release.sh 改为显式 --target runtime。

为什么

  • 客户从来用不到这层调试工具(真正的设备端 kiosk 是 Firefox,见同一份 Dockerfile 里的说明),没必要跟着镜像一起分发。

怎么验证的

  • runtime/dev 两个 target 分别全新构建成功;runtime 镜像内确认 playwright 包和 /opt/pw-browsers 都不存在,/opt/firefox/firefox 仍在, 应用模块可正常 import;dev 镜像内确认 playwright 包和 chromium/chromium_headless_shell 仍然存在。
  • 未压缩镜像从 15.5GB 降到 13.7GB;build_release.sh v2.0.2 产出的镜像 tar 从 v2.0.1 的 6.1G 降到 5.7G,manifest.json 确认两个镜像都在同一个 tar 里, 实际 docker run 核对客户镜像内确无 Chromium、Firefox 仍在。

2026-07-29 · Hand pose 移除 MediaPipe,仅支持 WiLoR

提交:0652378(main)

做了什么

  • Hand pose 页面删除方法选择,固定使用 WiLoR 相机坐标 3D;能力提示、提取请求 和 Saved result 列表均改为 WiLoR-only。
  • 后端支持方法集合只保留 wilor,删除 MediaPipe extractor、模型发现、 启动分支和 hand-relative 稳定逻辑;旧 method=mediapipe 请求现在返回 400。
  • 发布镜像删除 mediapipe Python 包、其四个显式配套依赖,以及 hand_landmarker.task 模型下载和校验层。
  • 更新使用说明与项目结构文档。已有 outputs/handpose/*/mediapipe/ 历史文件 不主动删除,但不会再被 API 列出或读取。

为什么

  • 当前 MediaPipe hand-relative 结果质量无法满足产品使用要求,继续暴露该方法 会造成用户误选,也让发布镜像保留无效运行时和模型。

怎么验证的

  • 重启空闲 Dashboard 后,/api/handpose/capabilities 仅返回 wilor, /api/handpose/status 的结果列表仅含 WiLoR;MediaPipe result 请求返回 HTTP 400 Unknown hand-pose method。
  • 1600×1000 无头 Chromium 页面只显示 WiLoR 状态和 rosbag 选择,不再有方法 下拉框。
  • 前端构建、Python/JavaScript 语法、Compose 配置、Dockerfile docker build --check --target runtime 和差异检查均通过。

2026-07-29 · Hand pose 固定播放视野并稳定左右手身份

提交:647c276(main)

做了什么

  • 3D viewer 从“每帧按当前检测手数自动 fit”改为加载结果时计算一次稳健的整段 中心和尺度;单手、双手之间切换不再触发画面缩放。
  • MediaPipe 与 WiLoR 的时序 track 现在保留紧凑 track ID,并对 handedness 使用连续 8 帧迟滞;短暂的左右误判不会立即换色。
  • 播放端也会重建旧结果的时序身份,因此已有结果无需重新提取即可减少左右闪烁。
  • WiLoR 每帧最多保留一只左手和一只右手,过滤偶发的第三、第四个重复检测。
  • MediaPipe 的左右手展示槽位按稳定标签固定,单手出现时不再因数组索引改变位置。

为什么

  • 上一版为了保证 WiLoR 可见而逐帧重算包围框,检测从两只手变一只手时包围框 变小,导致自动放大;恢复双手后又缩小。
  • MediaPipe 原先只有 5 帧多数票,WiLoR 直接使用单帧左右标签;交叉、遮挡和 短暂丢检时会产生颜色来回切换。

怎么验证的

  • 对 MediaPipe/WiLoR 注入连续交替的 R/L 标签,输出始终保持初始身份; 只有连续 8 帧一致反向证据才切换,且输出包含稳定 track ID。
  • 现有 1,798 帧 WiLoR 旧结果经播放端兼容后每帧最多两只手,交替误判保持 同一标签。
  • 实际页面第 292→293 帧由双手变单手时,共同左手的画布尺寸从约 37×75 px 变为 40×73 px,没有整手放大;前端构建、Python 编译、 JavaScript 语法和差异检查均通过。

2026-07-29 · Hand pose 结果视角与加载选择修复

提交:968a51b(main)

做了什么

  • 3D landmark viewer 攻掉针对 WiLoR 的固定透视距离,改为按当前帧手部几何 自动居中和缩放;交互旋转围绕手部中心进行,WiLoR 相机坐标不再被投到画布外。
  • 每个已完成的提取任务只自动加载一次;用户手动加载 MediaPipe 结果后,后台 状态轮询不再反复切回最近完成的 WiLoR 结果。

为什么

  • WiLoR 输出是相机坐标,原来的固定相机距离和绕原点旋转无法覆盖实际手部位置。
  • done 状态每 1.5 秒触发一次自动加载,覆盖了 Saved result 中的手动选择。

怎么验证的

  • 使用现有 WiLoR 结果在 1600×1000 无头 Chromium 页面验证,双手完整落在 3D 画布范围内。
  • 手动加载 MediaPipe 后等待超过两轮状态轮询,结果标题仍为 MediaPipe; 点击播放后时间轴从第 0 帧推进到第 27 帧。
  • 执行前端构建、两个修改脚本的 node --check 和 git diff --check,均通过。

2026-07-29 · v2.0.1:客户镜像补上 WiLoR 离线手部姿态模型

提交:main(v2.0.1,含 5ee5861/9391393/326e244/13d4213)

做了什么

  • v2.0.0 打包时 main 上还没有 WiLoR 打包相关的提交,构建出的镜像只含 MediaPipe 手部模型,不含 WiLoR/MANO;这几个提交合并后重新打包为 v2.0.1, 镜像现在同时包含 MediaPipe 和 WiLoR 两条离线手部姿态提取路径。
  • 清理 docs/USAGE.md、docs/DEPLOYMENT.md 里遗留的"WiLoR 模型仅限研究用途, 发布或商用前需单独确认模型与数据许可"提示。

怎么验证的

  • docker run insight-dashboard:v2.0.1 内核对 /opt/insight/models/wilor/pretrained_models/ 下四个文件( wilor_final.ckpt/detector.pt/MANO_RIGHT.pkl/mano_mean_params.npz) 均存在,wilor_mini 包可正常 import;MediaPipe 模型同样存在。
  • manifest.json 确认镜像 tar 内 insight-dashboard:v2.0.1 与 insight-superglue-validation:25.04 均已打包,tar 体积从 v2.0.0 的 2.1G 增至 6.1G(新增 WiLoR 权重 + PyTorch/CUDA 运行时)。

2026-07-29 · Hand pose 使用真实帧进度并静默 MANO 固定提示

提交:326e244(main)

做了什么

  • MediaPipe 与 WiLoR 从 rosbag connection 元数据读取目标图像 topic 的 msgcount,逐帧向后端上报“已处理、已检出、总帧数”。
  • Hand pose 页面删除原先 20%–70% 往返的动画进度,改为 processed_frames / total_frames 的真实百分比,并显示已处理/总帧数。
  • WiLoR 初始化只过滤 MANO 标准模型“仅 10 个 shape coefficients”的已知 无害提示;其他日志、警告和错误继续保留。

为什么

  • 原进度条与任务实际处理量无关,长任务中会反复前进和后退。MANO 的 10 个 shape 参数是 WiLoR 预期的手型参数,不表示只有 10 个关键点,持续显示容易 被误认为模型不完整。

怎么验证的

  • 真实 rosbag 的 MediaPipe 5 帧任务依次上报 0/5 至 5/5,并以 5/5 完成。
  • 无头 Chromium 模拟 25/100 状态时显示 25.0%,进度条宽度为 25%,页面无 console error;Dashboard API 已返回 total_frames 字段。
  • 精确过滤测试确认只删除 MANO 固定文本,其他输出不受影响。

2026-07-29 · WiLoR mini 离线运行时进入 Dashboard 镜像

提交:5ee5861(main)

做了什么

  • 在 insight-dashboard 镜像内固定 WiLoR mini 源码和模型版本,内置四个推理 所需模型文件,设备离线时也能直接使用相机坐标手部姿态提取。
  • 只安装 Jetson 推理必要的 PyTorch、CUDA/cuDNN 和 Python 运行依赖;移除 PyTorch 开发文件,并排除 timm、skimage、huggingface_hub、polars、 thop、pandas、seaborn 等训练/demo 或未使用依赖。
  • 改为 mmap + CPU 映射的低内存 checkpoint 加载,避免 8GB Jetson 在加载 2.56GB checkpoint 时被 OOM killer 终止;关闭 Ultralytics 自动补装和模型 运行时下载。
  • WiLoR 能力检查现在同时验证 Python 运行时和四个模型文件,模型目录显式传给 提取进程;Dockerfile 将固定大层放在源码 COPY 前,普通源码改动可复用缓存。

为什么

  • 原先 WiLoR 只在手工安装研究环境中可用,发布镜像会显示不可用;直接安装其 完整依赖又会带入大量训练、可视化和下载组件,并在现场设备上出现内存不足。

怎么验证的

  • 全新 docker compose build insight-dashboard 成功;最终镜像 5.81GB,其中 WiLoR 固定模型层 2.62GB。相同 Dockerfile 的缓存构建实测 5 秒。
  • 最终容器内 CUDA 12.6 在 Orin 可用,API 返回 wilor.available=true,上述 非必要包检查均未安装。
  • 用真实 rosbag 连续提取 10 帧成功,10 帧均检测到左右手,共生成 20 个手部 姿态结果;未发生 OOM,也未触发运行时安装或模型下载。

2026-07-29 · Insight9 建图/Insight3 重定位纳入 v2.0.0 客户发布

提交:feature/insight9-realtime-mapping(合并进 main,v2.0.0)

做了什么

  • Magic Leap SuperPoint/SuperGlue 的商用/再分发用途已确认,移除 docker-compose.yml、Dockerfile.superglue-validation、 docs/INSIGHT9_SPARSE_MAPPING.md 三处"仅内部验证、禁止进客户镜像"的措辞。
  • deploy/docker-compose.yml 新增 superglue-inference、 insight9-sparse-mapper、insight3-global-localizer 三个服务(按客户 compose 风格调整:不挂源码,镜像跟随 INSIGHT_VERSION),新增 superglue-ipc/superglue-engines 具名 volume(后者让 TensorRT 引擎编译 结果跨升级持久化),并给 insight-dashboard 补上此前缺失的 ipc: host (建图三容器共享 Fast DDS transport所需)。
  • scripts/build_release.sh 新增构建并保存 insight-superglue-validation:25.04 镜像到同一个镜像 tar;update.sh 现有的镜像名过滤逻辑本来就只匹配 insight-dashboard: 前缀,两镜像同包无需改动。
  • insight9-dense-mapper、insight9-mapping-rviz(内部调试用离线密集建图/ RViz 可视化,profiles: [mapping-validation])保持仅内部开发机可用,不进 客户包。
  • 本次发布把这条领先 main 约 40 个 commit 的分支(Insight9 建图、Insight3 全局重定位、手势录制开关、离线手部姿态工作区等)整体推进为新 main;旧 main 备份为 main-backup-2026-07-29。

为什么

  • 这条分支已经把旧的 AprilTag 在线对齐整体移除(c481212),建图/重定位是 唯一在线校准源;此前的"仅内部验证"限制若不解除,客户发布镜像将完全没有 空间校准能力。

怎么验证的

  • docker compose -f deploy/docker-compose.yml config(带 INSIGHT_VERSION 占位)和 bash -n scripts/build_release.sh 均通过。
  • 现场设备(dev 机)建图栈已连续运行:superglue-inference healthy, insight9-sparse-mapper/insight3-global-localizer/insight-dashboard 均 Up;/api/mapping 返回真实数据(703 个地图点,两路 Insight3 calibration_ready/localized 均为 true)。
  • Playwright 无头检查 3d / recording / bags / scoring / handpose / settings 六个关键页面均无 console 报错(3d 页面另用 wait_until="load" 单独复核, 排除持续 WebSocket 流导致的 networkidle 误判超时)。
  • 确认无正在进行的录制后才做以上检查,未中断现场采集。

2026-07-28 · Insight3 未建图区继续按 VIO 显示并收紧直跳阈值

提交:11dd081(feature/insight9-realtime-mapping)

做了什么

  • Insight3 自适应直跳阈值从 0.50 m / 25° 调整为 0.15 m / 10°;三帧 PnP 共识、内点和重投影验证门槛保持不变。
  • 已经取得全局校正后,当前图像看不到已建图特征时明确进入 vio_only: 保留 EKF 的 T_map_odom,继续用设备 VIO 发布 50 Hz 全局 Pose 和轨迹, localized 保持 true。
  • 全局 Pose 发布改为直接使用已解析的双目中心外参,不再依赖 Path 重建器的 内部外参缓存;PnP 异常时状态仍保留是否已有全局校正。
  • 增加 tracking_mode=map_matched/vio_only/verifying/unlocalized,区分 “当前看见地图”和“暂时只靠 VIO”,避免把暂时失配表达成定位消失。

为什么

  • EKF 本身在 PnP 暂时失败时不会清除校正,但全局 Pose 发布与 Path 内部状态 存在不必要耦合,前端超过 2 秒收不到全局 Pose 会直接把模型标为 hidden。
  • 重定位的职责是周期性校准 VIO,不应要求 Insight3 始终看着已经建图的区域; 暂时离开地图覆盖范围时仍应保持连续轨迹。

怎么验证的

  • 合成检查确认默认阈值为 0.15 m / 10°,小修正走 EKF,大修正直跳;连续 300 个无重定位观测的 predict 周期中已有校正保持不变。
  • Python 编译和差异检查通过;实机 localizer 正常启动,两路状态均返回 jump_translation_m=0.15、jump_rotation_deg=10.0 和 tracking_mode=unlocalized。
  • 当前服务重启后尚未重新获得首次定位,因此还需现场先完成三帧定位,再把相机 转向未建图区,确认 vio_only 下 Dashboard 连续显示超过 2 秒。

2026-07-28 · Insight3 重定位按漂移幅度选择直跳或 EKF

提交:689a076(feature/insight9-realtime-mapping)

做了什么

  • 两路 Insight3 的三帧 PnP 共识通过后,不再固定把所有后续修正都交给 EKF; 新观测相对当前对外 T_map_odom 小于 0.50 m 且 25° 时继续平滑融合, 任一达到阈值时立即重置 EKF 内部目标和对外校正。
  • 大幅漂移直跳时清空旧 Path 段,只从找回后的当前点继续画轨迹,避免飞走位置 和正确位置之间出现跨空间长线;Insight3 A/B 独立处理。
  • 增加 correction_mode、修正平移/旋转差、累计直跳次数和当前阈值诊断; 阈值可通过 --jump-translation-m、--jump-rotation-deg 调整。
  • 把大小修正决策拆为可独立验证的 AdaptiveRelocalizationPolicy,EKF 增加 显式 reinitialize(),避免在 localizer 中拼接内部状态。

为什么

  • 正常厘米级漂移直接跳回会影响观察和采集连续性,适合保留 EKF 平滑。
  • 相机已经飞走几十厘米甚至更多时,继续按 1 秒时间常数缓慢收敛会让错误全局 位置持续存在;可靠 PnP 找回后应立即恢复。
  • 分流仍位于三帧共识之后,不能为了更快跳回而绕过几何验证。

怎么验证的

  • 合成检查确认 0.10 m / 5° 进入 ekf 且观测到达当帧不跳;0.80 m 或单独超过 25° 进入 jump,对外校正立即等于新观测且协方差清零。
  • Python 编译和差异检查通过;实机 localizer、两路 Insight3 图像、2010 个 地图特征及 SuperPoint 推理链路正常。
  • 两路状态均返回 jump_translation_m=0.5、jump_rotation_deg=25.0、 hard_relocalizations=0。
  • 尚未实际移动相机完成飞走再找回,真实直跳时机、视觉体验和最终阈值仍需现场 动作验收。

2026-07-28 · Insight9 建图增加历史地标回环与连续漂移修正

提交:13f276a(feature/insight9-realtime-mapping)

做了什么

  • Insight9 每两个建图关键帧复用左目 SuperPoint 描述子,匹配至少 30 个 关键帧以前建立的历史三维地标;通过双向最近邻、ratio test、PnP-RANSAC、 内点率、图像网格覆盖和连续三帧一致性确认回环。
  • 新增以 T_map_odom 为状态的误差状态 EKF。设备原始 VIO 不写回;接受回环 后,实时 Pose、最近 200 点 Path 和 TF 按 0.75 秒时间常数平滑收敛,新地图点 使用 EKF 目标写入,避免显示平滑期间污染体素地图。
  • 状态消息和 Dashboard 增加回环检查、拒绝原因、确认进度和累计接受次数; VIO reset 会同步清理地图、路径、回环共识和 EKF 状态。
  • mapper 退出改用幂等 rclpy.try_shutdown(),消除 Docker 重启时重复 shutdown 的 traceback。

为什么

  • 此前只有 Insight3 会在 Insight9 三维地图中重定位;Insight9 直接把自身 VIO 当作全局地图位姿,长时间漂移后没有任何历史绝对约束把相机拉回。
  • 连续局部帧非常相似,必须排除近期建立的地标并进行多层几何与时间一致性 验证,避免把正常跟踪或重复纹理误判成回环。
  • 直接替换 T_map_odom 会让 Dashboard 模型和轨迹单帧跳变,因此把地图目标 更新和可视输出平滑分开。

怎么验证的

  • 合成 48 个 2D–3D 对应点和人为 VIO 漂移,PnP 恢复的 T_map_odom 与真值 一致;同时覆盖历史地标窗口、三帧共识、EKF 目标更新和 0.75 秒渐进收敛。
  • Python 编译、前端构建和差异检查通过;实机 mapper 正常处理 544x640 Insight9 双目输入,状态 API 返回 loop_checked / loop_closures。
  • 连续两次 Docker 重启无 traceback;New map 同步重置 mapper 和两路 Insight3 localizer;无头 Chromium 显示 loops 0 和三路在线状态。
  • 现场相机未实际移动成闭环;小闭环、长闭环、重复纹理和无回环序列仍列为 下一轮真实数据验收,当前不宣称已得到实测闭环精度。

2026-07-28 · 手势录制改为 Settings 开关并默认关闭

提交:968536f(feature/insight9-realtime-mapping)

做了什么

  • Settings 页面增加全局 Gesture recording 开关,调用独立设置 API 即时 开启或关闭双手点赞录制,不需要重启后端。
  • 关闭时不创建/保留手势命令 worker,也不处理新的手势快照;关闭开关不会 停止网页手动录制或已经开始的录制,避免设置操作造成数据丢失。
  • Jetson NX、Lite、Lite-779 三套 profile 均明确改为默认关闭;Recording 页 继续显示 disabled/armed/recording 状态。
  • 补充重定位精度说明:区分 PnP 像素重投影误差、EKF 创新和绝对真值误差, 给出平移/旋转误差以及中位数、RMSE、P95 的正式评估方法。

为什么

  • 手势录制此前由 Jetson NX profile 默认开启,容易在日常操作中被无意触发; 显式开关让用户在需要时主动启用。
  • 3 px PnP 门槛和 EKF 的 0.10 m / 3° 观测噪声都是筛选/滤波参数,不能被 误解成系统已经测得的绝对定位精度。

怎么验证的

  • controller 生命周期检查确认默认关闭时无 worker,开启进入 armed,关闭退出 worker 且不触发录制。
  • 实机 Dashboard 重启后 Settings 与 Recording API 均返回 disabled;实际 开启、关闭一次分别得到 armed、disabled,全程 recording=false,缺少 enabled 的请求返回 400。
  • 无头 Chromium 成功加载 Settings 并绑定开关;Python compileall、三套 profile JSON、前端构建、Compose 配置和差异检查通过。
  • 当前两路最近一次 EKF 创新约为 6.8/6.1 mm、0.529°/0.443°;仅记录为 VIO 与重定位的一致性,未将其宣称为外部真值精度。

2026-07-28 · Insight9 全局输出统一改为双目中心位姿

提交:00c078e(feature/insight9-realtime-mapping)

做了什么

  • Insight9 稀疏建图、三角化、关键帧判断和地图点仍保持在左目光学坐标系, 不改变已有地图几何和 Insight3 PnP 的参考系。
  • 高率 Pose、200 点 Path、Dashboard 头部模型和 TF 发布前,改用设备静态 TF 的 camera_left -> camera_right 平移中点;中心姿态沿用左目。
  • TF child frame 从 insight9_mapping_camera_left 改为 insight9_mapping_camera_center,状态增加 center_extrinsic_ready 和 pose_frame,便于现场确认输出语义。

为什么

  • Insight3 已统一使用双目中心作为设备模型和轨迹锚点;Insight9 若继续以左目 光心发布,三个设备的 Pose、模型、Path 和 TF 物理语义不一致。
  • 只在输出边界转换可以避免影响现有左目三角化、稀疏地图和跨相机重定位结果。

怎么验证的

  • Python compileall、Compose 配置、差异检查和三维基线取半数值检查通过。
  • 确认当前未在录制后只重启 sparse mapper;实机从静态 TF 解析出 100.2 mm Insight9 双目基线。
  • /insight9_sparse_map/status 返回 center_extrinsic_ready=true、 pose_frame=insight9_mapping_camera_center,50 Hz Pose 话题恢复正常输出。

2026-07-28 · Insight3 重定位改用 EKF 连续融合并发布双目中心位姿

提交:55db745(feature/insight9-realtime-mapping)

做了什么

  • 为 insight3_a、insight3_b 各自增加独立的误差状态 EKF,状态为 T_map_odom。第一次三帧一致重定位直接初始化到真实全局位置;后续重定位 作为绝对观测更新 EKF,不再直接替换当前全局修正。
  • VIO 继续提供 100 Hz 连续运动和过程不确定度;EKF 后验修正按默认 1 秒时间 常数随后续 VIO 帧注入,避免低频重定位造成单帧位置或朝向跳变。历史轨迹 不回写、不清空,新轨迹从当前状态连续收敛。
  • PnP 仍在左目光学坐标系计算;发布前从设备静态 TF 读取 camera_left -> camera_right,取两光心中点并保持左目朝向。Insight3 的 Pose、Path、Dashboard 模型和 TF 现在统一表示双目中心。
  • TF child frame 改为 insight3_a_global_camera_center 和 insight3_b_global_camera_center;状态增加 EKF 创新量、协方差、 center_extrinsic_ready 和 pose_frame 诊断字段。

为什么

  • 原实现接受新的全局校正后直接切换 T_map_odom,达到阈值时还会清空旧轨迹, 前端模型表现为明显跳变。
  • 原全局 Pose 的锚点是左目光心,而前端模型代表整个 Insight3 设备;使用左右 目中点作为统一锚点后,模型、轨迹和 TF 的物理语义一致。

怎么验证的

  • EKF 数值检查覆盖首次精确初始化、后续观测当帧不跳、按 VIO 帧平滑收敛和 VIO reset 后重新初始化;双目中心检查覆盖三维基线取半及非法齐次矩阵拒绝。
  • Python compileall、docker compose config --quiet 和差异检查通过。
  • 实机 localizer 成功从静态 TF 解析 Insight3 A/B 的 34.7 mm、35.4 mm 基线; 两路 ROS 状态均为 localized、ekf_initialized=true,历史与发布 Path 均达到 200 点,pose_frame 分别为对应的 *_global_camera_center。

2026-07-27 · 修复失效的原始图像解码调用 + Dashboard 死代码大扫除

提交:b3bb931(feature/insight9-realtime-mapping)

  • _decode_calibration_message 在 c481212 删除旧标定链路时被一并删掉,但 image_pipeline.py 里两处调用(夹爪追踪解码兜底、_decode_display_image 的非 mono8 兜底)没有同步清理——任何非 mono8/压缩流的原始相机会命中 AttributeError,被 _frame_worker_loop 的宽泛 try/except 悄悄吞掉,画面 静默停止刷新、不报错。当前四份设备 profile 都只用 mono8 或压缩流,尚未 触发,但是个定时炸弹。改为在 ImagePipeline 内部实现真正的解码 (mono8/8uc1 直通、NV12→BGR 全彩转换、compressed 走 cv2.imdecode)。
  • 五个并行审计智能体分别覆盖 HTTP/WS 层、核心节点+运行时、mixin/子进程 worker、录制后处理子系统、前端,交叉核实后删除确认零调用/零读取的僵尸 代码:
    • 三条 HTTP 路由(相机手部叠加 JSON、手动录制同步、rosbag-sync 设置 读写)及其唯一支撑的辅助函数;/api/images/capabilities 虽然前端也不 调,但外部契约测试脚本和文档都在用,予以保留。
    • PlaybackRoutes._on_playback_finished(context.py 里同名方法才是真正 接的回调)、invalidate_trace_snapshots()、WebRtcStreams.has_sessions()、 WebRtcSession.topic_type、PlaybackManager.get_bag_time_range()。
    • 手势分类的 write-only 诊断字段(curled_fingers/reasons)。
    • 多处"算出来发给前端但没人读"的 wire 字段:mapping 每状态 age_ms、 录制 status() 14 个字段里的 10 个(pid/pids/started_at/topics/ merge_state/merge_error/host-sync 三件套/sync_status,内部状态本身 保留,只删 API 暴露)、bag 目录原始 path/size_bytes/ modified_at_epoch_s、优化结果的轨迹图路径字段/total_steps、话题目录 里每项的 label/camera/tail/default_selected(排序键同步换成 short_name)。
    • bag 目录的 labeled 徽章(全仓库没有任何打标签功能,永远 False)和 optimized 徽章(检查的路径和真实产物目录 runs/<name>/ 对不上,结构性 永远 False),连同前端对应徽章一起删除。
    • 前端死文件 index.html(/ 和 /3d 都固定返回 3d.html)、指向不存在 元素的 camera-page-meta 分支、updateCameraPanelLayout 里打不到的非 spatial 布局分支及配套 .image-camera-dock CSS、没人调用的 showRestartBanner()(重启横幅目前无法通过任何路径显示,一并清理了 随之变成写而不读的 settingsRestartBanner 引用)。
  • 验证:全部触碰到的 Python 文件 py_compile 通过;camera_setup.py/ topic_catalog.py/bag_catalog.py 关键函数用真实 config 和 rosbag 数据跑 一遍核对输出结构;全仓库 grep 确认无遗留引用;前端重新执行 build.js 同步 dist/。过程中发现并修复一处自己引入的回归——精简 RecordingManager.status() 时漏查到 gesture_recording.py 的 _toggle_recording() 内部还在读同一个 dict 的 merge_state 键判断是否 阻塞新录制,已改为直接读 recording_manager.merge_state 属性。

2026-07-27 · 精简 Dashboard runtime 常驻服务

提交:82d22d4(feature/insight9-realtime-mapping)

  • mapper 在轻量 status JSON 中发布地图点数,Dashboard 不再接收完整稀疏 PointCloud2;同时删除未消费的 map_version、Pose 时间戳和 API payload 字段。
  • 新增 dashboard_hand_tracking 设备能力,只为 Insight9 创建手部订阅; 删除 hand box 的 rosbag 回放,保留实时 box 和实时/回放 keypoint,Dashboard 自建 ROS 订阅从 28 个降至 18 个。
  • 保留 JPEG 手部叠加,但 worker 从常驻改为首次启用时启动、最后一路关闭后 退出,空闲时节省约 40–50 MB RSS。点赞录制和 3D 手骨架继续使用 keypoint。
  • 保留夹爪跟踪,只对有效 open/closed 校准显示开关并分配 ArUco detector; Jetson NX 当前无匹配校准时不再误报可用,Lite-779 的 Insight7 校准与启用 路径保持有效。
  • 三套设备 profile 配置生成和 Python 编译通过;实机验证 mapper status、 gesture armed、overlay worker 启停、18 个 ROS 订阅和三路相机状态正常; Chromium 加载 /3d 返回 200,控制台零错误。

2026-07-27 · 精简 SuperGlue TensorRT 运行镜像

提交:6cb9808(feature/insight9-realtime-mapping)

  • 最终运行镜像改为 Ubuntu 24.04;固定版本的 NVIDIA TensorRT 镜像只作为 TensorRT/CUDA 动态库来源,不再把完整 CUDA 开发工具、HPC-X/NCCL、 samples、headers、静态库及未使用的 Lean/Dispatch 运行时带入最终层。
  • 只复制首次建引擎和推理需要的版本化动态库,并在镜像内重建符号链接;同时 删除运行阶段不使用的 SuperGlue 源码和权重副本。
  • 镜像从 9.48 GB 降至 2.13 GB,减少 7.35 GB(约 77.5%),仍保留空缓存首次 启动时在当前 Jetson 自动生成设备专用 TensorRT plan 的能力。
  • 在 Orin 上用空 volume 成功生成 SuperPoint FP16 和 SuperGlue FP32 引擎; 两轮 GPU 预热正常,容器健康检查通过,真实 IPC 图像匹配得到 357 对匹配点。 当前 Compose 服务已切换到精简镜像并保持 healthy。

2026-07-27 · 删除旧 AprilTag 在线标定链路

提交:c481212(feature/insight9-realtime-mapping)

  • 全局建图/实时重定位已经成为 Dashboard 唯一空间位姿来源,因此删除旧 LiveAlignmentMixin、AprilTag 检测/共识/持久化模块和变换数学 facade; Dashboard 直接广播全局 pose 与轨迹。
  • 删除旧设置 API、CLI 参数、相机信息/标定图像订阅、前端样式和 board_calibration.json 三个设备模板;设备 profile 只保留 cameras.json 与 post_processing.json。
  • 同步清理 README、使用/部署文档、PRD 与 Wiki 校准专题页;重定位内部的 双目相机内参与夹爪 ArUco 识别保持不变。
  • 三个设备 profile 均通过配置生成;真实容器重启后 /healthz 正常,旧 /api/settings/board-calibration 返回 404;无头 Chromium 加载 /3d 返回 200、无 Alignment 控件、控制台零错误。

2026-07-27 · 合并 3D 轨迹与地图控制

提交:f460d12(feature/insight9-realtime-mapping)

  • 删除独立的轨迹控制卡片和 Trails visible 开关,轨迹保持默认可见。
  • 将 Clear trail、Keep trail 移入地图状态卡片,按顺序放在 New map 左侧,保留原有清空和保留轨迹行为。
  • 重新构建前端;Playwright 验证按钮布局、切换与清空交互正常,页面无控制台 错误,验证期间未触发新建地图。

2026-07-27 · 建图重定位不再挤占采集与录制

提交:85bcc87(feature/insight9-realtime-mapping)

  • dashboard 复用现有 Insight3 图像 reader,以 2 Hz 中继给 localizer,删除 两个额外的 20 Hz 全帧原图 DDS reader;Insight3 B 从 13–14 FPS 恢复到 稳定 20 FPS。
  • 网页模型和轨迹继续由 50 Hz 全局 Pose 驱动;200 点完整 Path 降为 5 Hz 调试输出且不再默认录制,避免重复序列化整条历史。
  • 三路全局 Pose 合并进对应相机 recorder,录制 part 从 9 个降为 7 个; 400 Hz IMU 增加 keep-last 1000 深度 QoS,吸收短时 CPU 调度停顿。
  • 3D 页面恢复独立的轨迹控制卡片,地图点数与 New map 保持单独卡片;相机 元数据轮询降为 4 Hz,Jetson 3D 渲染恢复 20 FPS。
  • 在 3D/WebRTC 同时运行时完成两轮 30 秒三相机录制;最终三路图像 20/20/30 FPS、三路 IMU 约 400 Hz,所有受检话题 missing 和 writer drop 均为 0。

2026-07-27 · 前端只使用三路全局轨迹

提交:64b5e9f(feature/insight9-realtime-mapping)

  • Insight9、Insight3 A、Insight3 B 的模型和网页轨迹统一由建图/重定位的全局 Pose 驱动,不再订阅或回退到旧 VIO 位姿。
  • 三路全局 Pose 与 Path 均按 50 Hz 发布;新录制默认保存这些话题,回放继续 使用同一套全局坐标。没有全局话题的历史 bag 不再回退显示旧轨迹。
  • 删除重复的 mapping WebSocket、整条 Path 和稀疏点坐标传输;网页只保留一个 50 Hz 姿态 WebSocket,建图点数和状态改为 500 ms REST 轮询。
  • 删除在线 AprilTag 对齐的网页、API 和运行入口,建图重定位成为唯一在线校准源。
  • 实机验证 Insight9 Path 为 49.8 Hz,录制目录默认选中三路全局 Pose/Path; Playwright 确认页面只有一个轨迹 WebSocket、无在线标定面板和脚本错误。

2026-07-27 · 模型改由三条全局轨迹驱动

提交:0dd43bd(feature/insight9-realtime-mapping)

  • 建图在线时不再仅隐藏旧轨迹,而是停止处理并清空旧 VIO 轨迹缓存。
  • Insight9、Insight3 A、Insight3 B 三个模型的位置和朝向改为各自全局 Path 的末端位姿;模型位置直接复用轨迹端点的同一组坐标。
  • 建图离线或播放 rosbag 时才恢复旧 VIO 位姿与轨迹。
  • 实机确认三条 Path 均为 200 点,三个模型位置与各自末端逐值相等;页面进入 全局模式、旧轨迹控制禁用,无前端脚本错误,验证时地图有 743 个确认点。

2026-07-27 · 三相机全局建图接入 3D 网页

提交:e05aa8c、a01c0da(feature/insight9-realtime-mapping)

做了什么

  • /3d 直接显示 Insight9 确认后的稀疏地图,以及 Insight9、Insight3 A、 Insight3 B 三条全局轨迹,不再要求打开 RViz。
  • 增加三路状态、地图点数、关键帧统计和 New map 控制;新建地图会同时 清空 mapper、两个 Insight3 的全局校正和三条轨迹。
  • TensorRT worker、mapper 和 localizer 改为 dashboard 的常驻依赖;RViz 关闭后不再停止核心服务。
  • 建图在线时只显示三条全局轨迹,隐藏原有局部 VIO 轨迹,避免六条轨迹重复; rosbag 播放或建图离线时自动恢复原轨迹。

为什么

  • 三相机建图和重定位已经验证可行,需要进入日常网页工作流;全局轨迹与原有 局部 VIO 轨迹同时绘制会造成重复,并让 Insight3 未定位前的位置产生歧义。

怎么验证的

  • 实机 ROS 状态进入 mapping,网页收到关键帧、匹配、三角化、地图点和三条 Path;HTTP、20 Hz WebSocket 与两个 reset service 均验证通过。
  • Playwright 确认三路状态徽标可见;建图在线时进入单一全局轨迹模式,旧轨迹 控制隐藏。验证时网页显示 788 个确认地图点。
  • Python、JavaScript、Compose、Bash 语法及前端构建、差异检查通过。

2026-07-27 · 三相机推理运行时移除 PyTorch

提交:f4930ec(feature/insight9-realtime-mapping)

做了什么

  • 镜像改为多阶段构建:PyTorch 仅在 builder 阶段导出固定 ONNX,最终容器改用 NVIDIA TensorRT 25.04,运行进程只依赖 TensorRT、CUDA C API 和 NumPy。
  • 将 SuperPoint 的 softmax、NMS、边界过滤、Top-K 和描述子采样全部并入 FP16 引擎;SuperGlue/Sinkhorn 继续使用 FP32。
  • 删除 worker 的 PyTorch import、后端选择和回退路径;缓存 manifest 升级, 同时校验 ONNX、plan、TensorRT、CUDA 和 GPU compute capability。
  • 官方代码和三份权重仍保留在内部验证镜像中;旧 PyTorch 运行镜像不再存在。

为什么

  • 三相机定位已经确认 TensorRT 可行,继续携带完整 PyTorch 开发运行环境会造成 不必要的磁盘、依赖和运行时复杂度。

怎么验证的

  • 最终容器 torch_spec=None,health 返回 tensorrt-cuda、TensorRT 10.9.0.34、CUDA 12.9、Orin SM 8.7;设备引擎冷构建和缓存重启均通过。
  • Docker 解包占用由约 17.4 GB 降到 9.48 GB。
  • 固定纹理中 PyTorch/纯 TensorRT 都检测到 209 个关键点;匹配数为 193/187, 匹配集合 Jaccard 为 0.8537。预热后特征提取 21.69 ms、完整匹配 68.81 ms。
  • 实时 Insight9 样本得到 336 个双目匹配和 189 个有效三角化点;两路 Insight3 localizer 均已连接新服务并等待新会话地图确认点。

2026-07-27 · 关闭 RViz 后自动释放验证服务

提交:88480f9(feature/insight9-realtime-mapping)

  • run_mapping_validation_rviz.sh 在关闭 RViz、按 Ctrl+C、收到终止信号或 启动失败退出时,自动停止稀疏 mapper、Insight3 localizer 和 SuperGlue。
  • 清理保留脚本原退出码,并在停止容器后撤销临时 X11 授权,避免验证服务继续 占用 dashboard 所需的 CPU、GPU 和内存。
  • 使用 mock Docker 分别验证正常退出和 SIGINT 路径,确认三个服务均被停止; Bash 语法与差异检查通过。

2026-07-27 · Insight3 轨迹保留长度与 Insight9 对齐

提交:bed1851(feature/insight9-realtime-mapping)

  • 两路 Insight3 从最多 1,000 点改为与 Insight9 相同的最多 200 点。
  • 三条轨迹均按 50 ms 目标间隔采样,约保留最近 10 秒,达到上限后旧点随新点 进入逐个消失。
  • Compose 显式固定 200 点;实机状态确认 A、B 的 history_points 均封顶 200。

2026-07-27 · 小幅重定位校正不再清空 Insight3 轨迹

提交:b1185d1(feature/insight9-realtime-mapping)

为什么

  • 原逻辑以矩阵 1e-12 差异判断校正变化,厘米级甚至纯数值抖动也会清空轨迹。

做了什么

  • 新校正相对当前轨迹基准平移达到 5 cm 或旋转达到 3°时才清空轨迹。
  • 两项都低于门槛时继续使用原轨迹基准增量追加,避免新旧点之间出现接缝; 小变化相对基准累计,累计越过门槛后仍会重开。

怎么验证的

  • 隔离测试确认平移 2 cm、旋转 2°保留轨迹;平移 10 cm、旋转 4°清空轨迹。
  • Python 语法、Compose 参数和差异检查通过,双 Insight3 localizer 已重启。

2026-07-27 · Insight3 全局轨迹改为增量更新

提交:1534984(feature/insight9-realtime-mapping)

为什么

  • localizer 在每个新 VIO 点到达后反复变换最多 1,000 个历史点,挤占 ROS 回调时间,Insight3 轨迹内容更新不够流畅。

做了什么

  • 全局校正不变时只变换并追加新点,不再重建整条 Path。
  • 首次获得校正或后续校正更新时丢弃旧校正下的轨迹,只从当前点重新开始。
  • 时间戳复位时同时清空增量缓存和采样时间状态。

怎么验证的

  • 隔离测试验证首次校正只保留当前点、连续新点增量追加、校正改变后再次清空。
  • 实机从零计数时,Insight3 A 历史点约 5 秒内由 99 增至 248;Python 语法和 差异检查通过,localizer 已重启加载新逻辑。

2026-07-27 · 稀疏三相机推理切换到 TensorRT

提交:e2ab2ae(feature/insight9-realtime-mapping)

为什么

  • 官方 PyTorch 链路已验证可行,但依赖和推理后端需要永久固化,不能依赖容器内 临时安装的 wheel。
  • SuperGlue 全 FP16 对数域最优传输存在明显精度回归,不能为了速度降低三相机 重定位可靠性。

做了什么

  • 固定 NVIDIA 25.04 镜像内已有的 PyTorch、CUDA、ONNX 和 TensorRT 10.9, 默认改用 TensorRT;PyTorch 只保留显式排障回退。
  • 首次启动自动从固定官方权重导出 ONNX,在当前 Jetson 编译 SuperPoint FP16 和 SuperGlue FP32 引擎,并按 TensorRT/CUDA/SM/参数及 SHA-256 校验。
  • 引擎保存在持久 Docker volume,后续启动直接加载;地图和三条轨迹仍在每次 RViz 会话中强制清空重建。

怎么验证的

  • Orin SM 8.7 上 IPC 健康信息确认 backend=tensorrt、TensorRT 10.9.0.34, 引擎 manifest 校验通过。
  • 固定平移纹理中 PyTorch/TensorRT 均得到 21 条匹配,其中 19 条完全相同; 全 FP16 仅得到 4 条,因此最终保留 SuperGlue FP32。
  • 实时双目样本得到 299 条匹配和 230 个有效三角化点;容器镜像构建、 TensorRT 动态形状、非默认 CUDA stream、IPC 及稀疏 mapper 实机链路均通过。

2026-07-27 · Insight3 全局轨迹提升到 20 Hz

提交:c4f9263(feature/insight9-realtime-mapping)

为什么

  • 两路 Insight3 原始 VIO 实测约 99 Hz,但全局 Path 和相机 TF 只有约 2 Hz, RViz 每约 0.5 秒才更新一次,轨迹呈明显跳动。

做了什么

  • 全局轨迹仍按 50 ms 间隔采样,Path 和相机 TF 改为可配置的 20 Hz 发布。
  • 状态 JSON 独立保持 2 Hz,避免把诊断负载同步放大十倍。
  • Compose 显式固定 --path-publish-hz 20。

怎么验证的

  • 生产形态的隔离 ROS domain 测试中,A/B Path 稳定发布均为 20.00 Hz; 100 帧窗口的帧间隔为 40–62 ms。
  • 两路原始 VIO 现场仍分别约为 99.2 Hz 和 99.1 Hz;Python 编译、Compose 校验和差异检查通过。

2026-07-27 · 三相机重定位切换为纯稀疏模式

提交:167cb97(feature/insight9-realtime-mapping)

为什么

  • 稠密 StereoSGBM 地图只用于观察场景形状,不参与 Insight3 到 Insight9 的 重定位;继续运行会额外占用 CPU 和内存。

做了什么

  • 新会话入口主动停止稠密 mapper,只重建 Insight9 稀疏描述子地图和双 Insight3 localizer。
  • RViz 移除两个稠密点云图层,默认显示确认稀疏地图,以及 Insight9、 Insight3 A、Insight3 B 三条轨迹。
  • RViz Compose 服务不再依赖稠密 mapper;稠密节点仍作为可选诊断工具保留。

怎么验证的

  • 稠密 current/fused 话题均无发布者。
  • 稀疏首帧实测 266 个双目匹配、186 个有效三角化候选点。
  • 三条 Path 均各有一个发布者;RViz 配置解析、Compose 校验、shell 语法和 差异检查通过。

2026-07-27 · RViz 验证每次从空地图开始

提交:cf15936(feature/insight9-realtime-mapping)

为什么

  • 普通 docker compose up -d 会复用正在运行的 mapper,上一轮内存中的 融合点云、关键帧和已校正轨迹也会继续发布,看起来像加载了旧地图。

做了什么

  • 新增统一启动脚本,每次强制重建稀疏 mapper、稠密 mapper 和双 Insight3 localizer,再打开 RViz;SuperGlue 服务不保存地图,继续复用以避免重新 加载模型。
  • 脚本自动设置、回收 X11 root 授权,并校验 display socket 和 XAUTHORITY。

怎么验证的

  • 重置后稠密状态回到 keyframe=1、本轮首帧 7,030 个融合体素;稀疏状态 回到 keyframe=1、confirmed=0。
  • 两路 Insight3 均为 localized=false、published_path_points=0,确认旧的 全局校正和轨迹没有继续发布;Compose 参数、shell 语法和差异检查通过。

2026-07-27 · 两路 Insight3 定位到 Insight9 全局地图

提交:f3d8c71(feature/insight9-realtime-mapping)

为什么

  • 两路 Insight3 的 VIO 使用各自独立原点,原始轨迹不能直接叠加到 Insight9 地图。
  • feature/markerless-alignment-phase1 的 RTAB-Map 方案已放弃,远端分支已 删除,相关提交不应合并或部署。

做了什么

  • Insight9 官方 SuperPoint/SuperGlue 稀疏地图增加三维位置和 256 维描述子 输出,并修正图像先于 VIO 到达时首关键帧被丢弃的问题。
  • 两路 Insight3 直接以 SuperPoint 描述子匹配 Insight9 三维地标,通过 PnP、 空间覆盖、重投影误差和连续三帧一致性独立求全局校正,不使用 RTAB-Map。
  • 定位前缓存最多 1,000 个 VIO 点;确认后整段轨迹立即重建到 insight9_map。RViz 增加洋红色 A 轨迹和绿色 B 轨迹。
  • 支持 Insight3 实际发布的 NV12 图像,直接取亮度面,不做额外 JPEG 或颜色 转换。

怎么验证的

  • 实机 Insight9 临时静止链路生成 499 个三维描述子特征;正式配置仍要求 三个有位移或转角的重叠关键帧。
  • Insight3 A 实测 17 个描述子匹配、14 个 PnP 内点、82.35% 内点率和 1.149 px 中值重投影误差;A、B 均通过三帧一致性并发布 400 多点历史轨迹。
  • 两条 Path 均为 frame_id=insight9_map,对应全局相机 TF 可查询;NV12 亮度面、描述子 PointCloud2 往返、80 点合成 PnP 和三帧共识测试通过。

本页下方 2026-07-24 的 feature/markerless-alignment-phase1 条目只保留为 历史实验记录。该分支和 RTAB-Map 路线已经废弃,不是当前实现或部署依据。

2026-07-27 · 撤销稀疏诊断并改为稠密双目地图

提交:67c7efa、aaa05b8(feature/insight9-realtime-mapping)

为什么

  • 左右匹配诊断图和百余个当前三角化特征点仍不足以直观看清场景结构,按现场 反馈撤销该修改,改用稠密深度作为主要 RViz 验收面。

做了什么

  • 完整 revert 748ecca,移除新增的稀疏 debug 图和当前帧稀疏话题。
  • 新增 StereoSGBM 稠密双目节点:校正红外图稠密视差、深度过滤、VIO 时间对齐、世界坐标转换和 4 cm 有界体素融合。
  • 新增 current/fused 稠密点云及独立 Compose 服务;RViz 默认展示累计稠密 地图,稀疏确认点保留但默认关闭。

怎么验证的

  • 合成 8 px 视差测试得到 1.25 m 中值深度,与标定模型一致。
  • Orin、544×640 实时输入约产生 43,000 个当前点和 17,500–17,900 个首帧 融合体素;单帧处理 64–83 ms,节点内存 77–86 MiB。
  • Compose、Python 编译、差异检查、无异常重启和跨容器点云订阅通过。

2026-07-27 · 增加稀疏建图逐阶段可视诊断

提交:748ecca(feature/insight9-realtime-mapping)

为什么

  • 只看少量跨帧确认点,无法判断错误来自双目匹配、三角化、VIO 变换还是体素 融合,不能作为几何正确性的充分验收。

做了什么

  • 发布 1088×640 左右匹配诊断图:绿色为通过极线、视差、深度和重投影过滤的 匹配,红色为被拒绝的匹配。
  • 发布当前关键帧全部三角化点,并在 RViz 中以红色与蓝色累计确认地图同时 显示;补充按“匹配→单帧形状→跨帧稳定性”验收的说明。
  • 瞬时 VIO 插值缺帧不再覆盖最后有效状态,SIGINT 关闭改为幂等。

怎么验证的

  • 实机诊断图为 1088×640,当前样本 192 个匹配、136 个有效三角化点, current_points 宽度 136;Python 编译、Compose 校验和无异常重启通过。

2026-07-27 · 补齐容器化 RViz 建图查看器

提交:24fa028(feature/insight9-realtime-mapping)

做了什么

  • 新增独立的 ROS Humble RViz 验证镜像,永久安装 ros-humble-rviz2,不再 假设 dashboard 运行镜像包含 GUI 工具,也不修改客户发布镜像。
  • dashboard、mapper 和 RViz 验证容器统一使用 host IPC,解决 Fast DDS 可以发现话题但共享内存数据跨容器不可达的问题。
  • 文档补充临时 X11 root 授权、启动和授权回收命令。

怎么验证的

  • Compose 配置校验通过,insight-rviz-validation:humble 构建成功; 镜像内 /opt/ros/humble/bin/rviz2 已确认存在。

2026-07-27 · 固化官方 SuperGlue 并验证 Insight9 在线稀疏建图

提交:585110b(feature/insight9-realtime-mapping)

为什么

  • JetPack 6.2 没有适合直接装入 dashboard 的官方 NVIDIA PyTorch wheel; 临时 ARM wheel 既不可复现,也不能代表 Jetson GPU 实时性能。
  • 需要先用官方 SuperPoint/SuperGlue 验证 Insight9 双目、VIO、三角化和地图 生命周期,再决定生产 TensorRT 后端。

做了什么

  • 新增固定 digest 的 NVIDIA PyTorch 25.04 内部验证镜像,将官方仓库固定到 ddcf11f...,并把 SuperPoint、indoor/outdoor SuperGlue 权重直接构建进 镜像;构建和本地下载入口都校验三份权重 SHA-256。
  • 新增 Unix socket GPU 推理 worker,以及 Insight9 左右红外近时同步、VIO 插值、双目几何过滤、多关键帧体素确认、PointCloud2/Path/TF 发布。
  • Compose 通过单独 profile 隔离研究镜像,不进入客户发布;补齐 Fast DDS 跨容器共享 IPC 和主动 worker 健康检查。

怎么验证的

  • Orin、544×640 实时双目上每帧得到 202–211 个匹配和 152–157 个有效 三角化点;稳定实时样本推理 284–301 ms,含几何 308–316 ms。
  • 170 个强制诊断关键帧形成 976 个已确认地图点,点云宽度 982,轨迹严格 封顶 200 点;dashboard 已跨容器收到 mapper 完整状态。
  • 官方提交和三份权重在最终镜像内复核通过;IPC 报告 PyTorch 2.7.0a0+…nv25.04、CUDA 12.9、Orin。结论是官方 PyTorch 足以验证数据 链,但约 3.2 Hz 未达到稳定 5 Hz,生产化仍需 TensorRT FP16。

2026-07-24 · 修复无标记地图与轨迹在 RViz 中不可见

提交:7666320(feature/markerless-alignment-phase1)

为什么

  • RTAB-Map 已发布带点的 cloud_map,但 RViz 以 Volatile 订阅 Transient Local 地图,晚启动时看不到缓存消息。
  • phase23 从未启动轨迹发布器,RViz 中也没有 Path 显示;此外 map->odom 和 odom->camera 使用不同时间域,TF2 不能直接拼接整条链。

做了什么

  • RViz 的 cloud_map durability 改为 Transient Local。
  • 新增 20 Hz、最多 1,000 点的 map-frame 轨迹节点,分别读取并数值组合两段 最新 TF;启动时自动发布 Insight9 Path,Insight3 首次成功定位后开始发布。
  • phase23 launch 自动启动轨迹节点,RViz 增加两路 Path 显示。

怎么验证的

  • 17 项 markerless 测试和 Python 编译通过。
  • 真实 phase23 中 cloud_map 为 frame_id=map、616 点;Insight9 Path 为 frame_id=map、529 个 poses。验证进程已通过 SIGINT 全部停止,日志保存在 ~/workspaces/insight_capture_tests/markerless_visualization_20260724/。

2026-07-24 · 恢复已验证的 RTAB-Map 特征默认值

提交:d183cbd(feature/markerless-alignment-phase1)

为什么

  • 最初成功的 RTAB-Map 使用 0.23.7 默认的 GFTT/ORB、500 特征、FLANN KD-tree 和 1 Hz;phase23 后来改成纯 ORB、1500 特征、brute-force 和 2 Hz, 现场出现 53%–89% stereo correspondence reject。

做了什么

  • mapper 恢复并显式锁定旧默认组合;source timestamp、VIO TF、数据库和 MapData/localizer 架构保持不变。

怎么验证的

  • 15 项 markerless 测试通过。
  • 同一相机、同一场景 A/B 中,恢复配置后 stereo reject 警告为 0;12 秒 MapData 审计连续新增 12 个节点,累计 6,951 个描述子和 3D 点,解码错误为 0。结果保存在 ~/workspaces/insight_capture_tests/markerless_legacy_defaults_20260724/。

2026-07-24 · 恢复无标记建图采集时间同步

提交:c736c85(feature/markerless-alignment-phase1)

为什么

  • phase23 曾把左右图、CameraInfo 和 100 Hz VIO 分别改成各自回调的接收 时刻。真实旋转时,调度延迟会让不同曝光时刻的双目数据被配对,点云出现 扇开、重影和错乱。
  • 复核阶段 0 原始审计确认 insight9 左右图和 CameraInfo 时间差为 0,最近 VIO 中位差约 2.05 ms、最大约 7.04 ms;此前“图像和 VIO 分属不同时间域” 的判断不成立。
  • RTAB-Map 只临时安装在当时的运行容器,重建容器或直接使用旧镜像会再次 出现 package 'rtabmap_slam' not found。

做了什么

  • phase23 恢复 source timestamp,保留同一设备采集时钟下的双目、CameraInfo 和 VIO 相对时间,不再对每个回调独立调用 now()。
  • 启动入口从 live cameras.json 校验并导出 ROS_DOMAIN_ID,保证原生 RTAB-Map进程与 Python bridge 都进入 domain 20。
  • 开发镜像固化 ros-humble-rtabmap-slam 和 ros-humble-rtabmap-msgs;运行 文档补充旧镜像需要重新构建。

怎么验证的

  • 15 项 markerless 测试、shell 语法和 diff 检查通过。
  • 8 秒真实 ROS 审计确认 insight9 原始左右图和 CameraInfo 时间差均为 0, 最近 VIO 中位差 2.95 ms、最大 5.47 ms;bridge 的 receive_retimestamped=0,没有编码、布局或回调异常。
  • RTAB-Map 在 domain 20 连续处理 77 次输入;验证进程通过 SIGINT 停止,未 遗留 mapper 或 bridge。机器结果保存在 ~/workspaces/insight_capture_tests/markerless_source_restore_20260724/。

2026-07-24 · 修复无标记对齐 RViz 相机控制

提交:05c63e1(feature/markerless-alignment-phase1)

做了什么

  • 为阶段 2/3 RViz 配置补齐 MoveCamera、Interact、Select 和 FocusCamera 标准工具。原配置只有 Orbit 视图控制器但没有相机输入工具, 因而鼠标拖动和 M 快捷键无法旋转视角。

怎么验证的

  • YAML 解析和工具类顺序检查通过;在 Jetson 本机显示环境实际加载配置, RViz 使用 OpenGL 4.6 连续运行且无段错误,最后通过 SIGINT 正常退出。

2026-07-24 · 自然特征对齐阶段 2/3 单 mapper 与单路定位

提交:db1c53f(feature/markerless-alignment-phase1)

为什么

  • insight9 需要在每次 session 内由唯一 RTAB-Map mapper 建图和回环,同时让 insight3_a 只消费实时地图特征,避免多个 RTAB-Map 进程读写同一数据库。
  • 阶段 1 的真实 PnP 数据表明 PLAN 初始 25 内点门槛会拒绝可信样本,必须按 已观测正负样本重新选择。
  • 当时把 RTAB 日志中的巨大时间差解释为图像和 VIO 分属不同时间域;后续 c736c85 复核原始时间戳后确认这是错误判断,receive 重打时间方案已撤销。

做了什么

  • 新增单 insight9 mapper 启动入口:每次 session 使用独立数据库,拒绝已有 路径,并持有单 writer lock;insight3_a worker 不打开数据库,只订阅 MapData。
  • 解码 RTAB-Map zlib 描述子,维护节点、局部 3D 点、优化位姿和 map 坐标的 内存索引;历史位姿优化后重算地图点,索引限制为最新 30,000 个描述子。
  • insight3_a 使用 ORB/Hamming ratio test、双向一致性、候选节点和 PnP RANSAC;连续一致结果才发布 map_to_odom。失配后保持最后校正并继续 VIO, 明显重定位跳变创建新 epoch。
  • 基于阶段 1 的 8 个真实 PnP 解,选择 11 内点和 0.65 内点率:保留两个可信 insight3_b 解并拒绝六个已观测错误解;明确记录 insight3_a 尚无可信正样本。
  • 当时对 markerless 派生图像、CameraInfo 和 TF 使用接收时刻;后续旋转 验证发现该方案破坏同帧同步,已由 c736c85 恢复 source timestamp。

怎么验证的

  • 13 项模块测试、Python 编译、pyflakes、shell 语法和 diff 检查通过。
  • 当时 receive 重打时间使 RTAB 显示延迟降到 0.15–0.31 s,但该指标只反映 header 接近 Unix 当前时刻,不能证明双目和 VIO 保持采集同步;该验证结论 已被后续旋转测试推翻。MapData QoS 实测为 Reliable/Volatile,与 worker 一致。
  • 15 秒 MapData 审计收到 27 条消息,实时解码节点 1、ORB 描述子 309、3D 点 309、优化位姿和 map_to_odom,解码和索引错误均为 0。TF 树确认 map -> insight9_a_odom,未定位的 insight3_a 没有伪造 map 父边。
  • 现场相机未移动,数据库虽记录 405 个中间节点,但优化图只有 1 个位姿、无 link/回环;因此尚未实测 MapData 增量新关键节点、历史优化更新、回环修正和 地图撕裂情况。
  • insight3_a 当前图像对 309 个地图描述子只有 1 个双向匹配,被明确拒绝; 没有错误输出校正。因无可信共同场景和连续正样本,首次定位、VIO_ONLY 后 真实重定位及 epoch 跳变尚未通过,只完成代码路径和状态机测试。
  • 所有数据库、日志、TF 图和 JSON 审计保存在 ~/workspaces/insight_capture_tests/markerless_phase23/,未进入仓库。

2026-07-24 · 自然特征对齐阶段 0/1 基础

提交:8788cce(feature/markerless-alignment-phase1)

为什么

  • 三路 VIO 都使用 frame_id=world,但实际是三个独立原点;右红外 CameraInfo 又共用 cam_right,直接进入 RTAB-Map 会造成 frame 冲突。
  • insight3 的 NV12 转 mono8 实验代码忽略 step,缺少长度、编码和异常诊断;跨相机自然特征是否足够稳定也没有量化工具。

做了什么

  • 新增按 active device profile 解析的显式拓扑:三路私有 <camera>_odom、唯一左右 frame、相机角色和阶段 1 阈值均不再由命令行人工传递。
  • 重写独立 stereo normalizer:按 stride 逐行提取 NV12 Y 平面,校验完整 NV12 长度和编码,输出紧密 mono8;派生 Image/CameraInfo 使用一致的 <camera>_camera_left/right,原 topic 和原始 VIO 不变。
  • 新增统一启动入口、三路 VIO→TF bridge、ROS frame/QoS/时钟/TF 机器审计、真实样本抓取工具。
  • 新增 ORB + Hamming、ratio test、双向一致性、空间覆盖和基础矩阵 RANSAC 验证器;JSON 输出特征数、各级匹配数、内点/率、Sampson 几何误差和 CPU/墙钟耗时。
  • 审核现有未跟踪 PoC:保留可重写的 bridge;标定板点云实验、三 RTAB-Map 同库定位和阶段 4 轨迹文件原样保留但不提交,未提前接入 Dashboard。

怎么验证的

  • Python 编译和 8 项模块测试通过,包括带 padding 的 NV12、短缓冲区、非法编码、profile 拓扑、ORB 几何正/负样本。
  • 8 秒真实 ROS 基线确认每个相机内部左右图、CameraInfo 与 VIO 属于同一设备时钟域;最近 VIO 中位差约 2.05–3.20 ms。三个设备的绝对 header 时钟仍有约 0.35–1.25 s 固定偏移,不能跨设备直接当作同步时间。
  • 三路旁路实跑后,12 个派生 Image/CameraInfo topic frame 正确、布局异常为 0;三条 <camera>_odom -> <camera>_camera_imu TF 均出现,未发现重复父 frame,派生 QoS 为 Reliable/Volatile。
  • 当前 VIO child frame 只能根据 IMU 融合语义和 camera_imu -> camera_left 杆臂推断为 camera_imu,配置明确标记为待杆臂实验确认;因此阶段 0 完整验收仍未通过。
  • 一次未控制共同视场的真实快照只用于工具冒烟,三对均被阈值拒绝;受控视角/距离/曝光/负样本矩阵尚未采集,阶段 1 go/no-go 未判定,没有伪造实测结论。

2026-07-24 · Jetson 浏览器硬件解码 PoC

提交:b311eb8、72116cd、46b72de(codex/jetson-hwdecode-poc)

为什么

  • 本机 Firefox 同时显示三路 H.264 WebRTC 和 Babylon 时只能达到约 15–20 FPS,需要确认瓶颈是否来自软件解码,并验证 WebKitGTK/WPE 能否复用普通 DOM <video> 实现 Jetson NVDEC 硬解。
  • 判断标准不能停留在出现 nvv4l2decoder;必须确认解码帧能以 DMA-BUF/GLMemory 进入浏览器合成器。

做了什么

  • 新增隔离的 Firefox V4L2-DRM 探针、WebKitGTK kiosk 启动器、H.264 测试页和浏览器内 WebRTC/视频/WebGL 指标面板;强制 nvv4l2decoder 并禁用软件 H.264 解码器,避免静默回退。
  • Firefox 152.0.5 包含 V4L2-DRM 代码,但机器没有标准 /dev/video* 解码节点;NVIDIA /dev/v4l2-nvdec 无法通过 Firefox v4l2test。
  • WebKitGTK 2.50.4 需显式注册 NVIDIA EGL vendor 才能启动;Jammy 发行构建即使打开运行时 WebRTC 设置也没有 RTCPeerConnection,源码和构建配置确认其未启用实验性的 ENABLE_WEB_RTC。
  • 本地 H.264 测试中 WebKit 确实选择 nvv4l2decoder 并打开 NVDEC,但其 memory:NVMM 输出被 WebKit sink 以 caps=EMPTY 拒绝;允许回退时实际使用 openh264dec,禁用回退后播放按预期失败。
  • 新增可复现的 WebKitGTK 配置脚本,保留 JIT、Skia、WebGL 和 GStreamer-GL,只构建 GTK3 MiniBrowser;在 Jetson 当前 GStreamer 1.20 + OpenSSL 3 环境中已确认 ENABLE_WEB_RTC、USE_GSTREAMER_WEBRTC、USE_GSTREAMER_GL 均成功启用。
  • 新增 NVMM 只读探针:经 VIC 转为 pitch-linear NVMM 后,确认 NvBufSurface.bufferDesc 是有效 DMA-BUF FD;补齐双平面 NV12 的 offset/stride 后可直接交给 glupload,不需要复制像素。
  • WebKit PoC 启动器固定使用 NVIDIA EGL/GLES。默认 GLX 路径虽产生 GLMemory,但实际选择 Raw Data CPU upload;EGL 路径则明确选择 DirectDmabufExternal。

怎么验证的

  • 使用 Jetson 硬件编码生成 640×480、30 FPS、10 秒 H.264 测试片段;软件回退路径约 30 FPS、0 丢帧。
  • GStreamer 日志确认 nvv4l2decoder 打开 /dev/v4l2-nvdec,随后因 NVMM caps 不兼容无法进入 PAUSED;补装 gstreamer1.0-gl 后结果不变。
  • 最终 PoC 以非 root kiosk 用户运行,Dashboard、WebKit 网络进程和 Web 进程正常存活,不再触发 X11 MIT-SHM 权限错误。
  • 自编译使用 GCC 12、Release、-j2,CMake 配置已完成,MiniBrowser 构建作为临时开发容器后台任务继续运行。
  • DMA-BUF 桥接实测上下文为 OpenGL ES 3.2 NVIDIA 540.4.0 / NVIDIA Tegra Orin,日志出现 DirectDmabufExternal returned 1,确认 NVMM FD → DMA-BUF → EGL external texture 可以不经 CPU 像素上传。
  • 结论与复现步骤记录在 scripts/hwdecode_poc/RESULTS.md;GStreamer 1.24 不是首次 WebRTC 验证的硬前置,下一步把已验证的包装逻辑做成 decodebin 可插入元素,再用当前 1.20 自编译浏览器验证真实 Dashboard。

2026-07-24 · 移除 3D 模型加载占位物

提交:ac75f93(main)

做了什么

  • 保留分阶段启动:场景、相机、轨迹和模型仍按原顺序就绪。
  • 头像模型延后加载时不再创建轻量姿态标记或 cube,占位阶段保持空白;更新模块版本参数以避免客户端使用旧脚本。

怎么验证的

  • 运行中的 dashboard 无头浏览器检查:进入后 0.7 秒 primitive 数量为 0;约 5 秒后优化头盔模型成功加载,页面无 JavaScript 错误。

2026-07-24 · 3D 页面分阶段启动与模型压缩

提交:11d8a4f(main)

为什么

  • 从普通页面进入 3D 时会同时初始化 Babylon、三路 WebRTC、轨迹和头像;原头盔模型为 14MB,包含三张 16384px PNG,造成明显启动卡顿。

做了什么

  • 先显示三路轻量姿态标记;随后错峰接通相机,延后轨迹和 rosbag 列表,最后依次加载手部模型与头盔。
  • 头盔替换为 2.73MB 的 2048px WebP 纹理和量化网格版本;旧模型配置路径自动迁移。
  • 模型 URL 加文件版本,版本化请求缓存一年;同一浏览器返回 3D 页时不重复传输模型。

怎么验证的

  • 0.5 秒浏览器检查已有三路 primitive;随后三路相机均为 live,优化头盔加载成功,无页面错误。
  • 冷启动模型传输由约 15MB 降至约 3.8MB;最长主线程长任务由约 1.19 秒降至约 0.68 秒。
  • 同一浏览器返回 3D 页时两种模型传输均为 0 字节;六个 dashboard 页面均返回 200,Python 61 文件编译及前端构建通过。

2026-07-24 · 轨迹改为真正的 3D 体积管

提交:da8dbd1、ebc7552(main)

为什么

  • GreasedLine 本质是始终朝向相机的屏幕空间带状线,不是 3D 实体;即使开启双面材质,也无法正确表达空间轨迹的截面、光照和视角变化。
  • Babylon 通用 Tube 建模器可以生成正确体积,但在 50 Hz 下重复重算截面、法线和完整网格的开销过大。

做了什么

  • 三路轨迹改为不透明的六边截面体积管,使用标准材质、真实法线和世界空间光照;根据实机观感将初版半径统一缩小 20%,最终 head 为 0.0096 m,双手为 0.008 m。
  • 保留增量 WebSocket、去重、弧长重采样和固定 300 点拓扑;创建时预分配顶点、法线和索引,后续 50 Hz 更新只替换 GPU 顶点缓冲,不重建 mesh。
  • 使用平行输运方式延续相邻截面方向,避免曲线转向时截面突然翻转。

怎么验证的

  • Chromium WebGL 注入三条 300 点空间曲线,得到三个标准 Babylon Mesh;每条为 1800 顶点、10764 索引,包含法线和 StandardMaterial。
  • 分别从相反相机方位截图,三条轨迹都连续、完整、具有明暗变化,没有随路径方向消失。
  • 前端重新构建,源码与 dist 一致;容器重启后,三路相机约 19.98 / 20.02 / 29.99 FPS,WebRTC 全部 healthy。
  • 运行服务确认提供最终半径配置,三路相机均非 stale,WebRTC 全部可用。

2026-07-24 · 恢复 3D 轨迹可见性

提交:85ddfc4、2efddcf、c3da89d(main)

为什么

  • Tube 改为 GreasedLine 时沿用了原世界空间半径,并开启 size attenuation;0.016–0.02 的宽度在实际相机下接近亚像素,轨迹 mesh 正常但肉眼不可见。
  • 轨迹坐标截断到 0.1 mm 后会产生大量连续重复点;GreasedLine 将这些零长度 segment 画成断点,且第一版屏幕空间宽度仍偏细。
  • GreasedLine 材质默认启用背面剔除,路径方向反转时三角带绕序变化,会使某一方向的整段轨迹从当前相机视角消失。

做了什么

  • 改用稳定的屏幕空间宽度,head 为 0.006,双手为 0.005,关闭 size attenuation;增量协议、300 点缓存和 50 Hz 更新逻辑不变。
  • 根据现场观感把 head/双手宽度增至 0.012 / 0.01;绘制前去除零长度段,并按弧长重采样回固定 300 点,保持固定拓扑和连续曲线。
  • 轨迹材质关闭背面剔除并启用双面光照,使正向和反向路径都保持可见。

怎么验证的

  • Chromium WebGL 注入 300 点、约 1.2 m 合成轨迹,GreasedLine 为 600 顶点、边界正确且清晰可见,无页面错误。
  • 使用含 240 个连续重复点的 300 点轨迹验证,输出曲线连续无断点,线宽约 3–4 像素,仍为 600 顶点、1794 索引。
  • 使用同一条 300 点曲线的正向和反向副本验证,两条 600 顶点轨迹均完整、连续、等宽可见。
  • 前端重新构建,源码与 dist 一致,JavaScript 和 diff 检查通过。

2026-07-24 · 精简静态变换完整性说明

提交:202f2ee(main)

  • 完整性报告中成功录制的 /tf_static 说明精简为 static transform recorded。
  • 真实 rosbag 的完整性 API 验证通过,返回文本与页面目标一致。

2026-07-24 · 恢复打分前的完整性报告

提交:7e6c34d(main)

为什么

  • Dashboard 路由拆分后,完整性接口仍调用原单体类的 JSON 请求解析方法,但该方法只被保留在设置路由中,导致接口返回 500;前端随后继续评分并覆盖错误状态,因此页面不显示完整性报告。

做了什么

  • 将 JSON 请求体解析提取为 Web 路由共享函数,完整性与设置接口统一使用,恢复“先检查完整性、再进行轨迹评分”的原有流程。

怎么验证的

  • 真实 rosbag 完整性接口返回 200 和全部 topic 报告,非法 JSON 返回受控的 400。
  • 无头浏览器点击 Analysis 后依次收到完整性接口 200、评分接口 200;完整性报告保持可见,评分最终为 Scored。
  • 全部 61 个 Python 源文件编译检查通过。

2026-07-24 · 修复轨迹打分任务无法启动

提交:1ae7c1a(main)

为什么

  • Dashboard 服务拆分时遗漏了 _ScoringJob 的 @dataclass 装饰器,分析入口传入任务参数后触发 _ScoringJob() takes no arguments。

做了什么

  • 恢复任务数据类装饰器,使后台打分任务能够按 bag、topic 和状态参数正常初始化。

怎么验证的

  • 任务构造与源码编译检查通过。
  • 对 insight_record_20260715_134710 发起真实分析,任务状态为 done,三路 VIO covariance topic 均产生有效分数。

2026-07-24 · Insight9 双手点赞启停录制

提交:c9dd639(main)

为什么

  • 采集过程中操作者不方便回到网页点击按钮,需要用明确且不易误触的双手动作划分录制片段。

做了什么

  • Insight9 同一帧中的两只手都满足“拇指向上、其余四指握拳”并保持 0.8 秒时触发;触发后必须连续解除 2 秒才重新布防。
  • ROS callback 只做轻量几何识别,独立线程使用服务器默认 topics 调用录制启停;回放 keypoint、合并中和磁盘低于 10% 均不会启动。
  • 录制所有权隔离:手势只停止自己启动的录制,不会停止网页手动录制。
  • Recording 页新增 armed、保持、释放、录制和 blocked 状态标签;jetson-nx profile 默认启用。

怎么验证的

  • 纯识别、0.8 秒保持、2 秒释放、短暂检测丢帧、回放忽略、磁盘/合并阻止和手动录制保护测试通过。
  • 通过真实 ROS Detection2DArray 链路发布合成双手关键点,成功启动和停止实际 rosbag;合并成功,快速完整性检查为 OK,三路图像为 20/20/30 Hz 且零丢帧。
  • 另用网页 API 启动轻量手动录制,双手手势返回 ignored_manual_recording,确认未被误停;五种页面状态和完整六页面回归通过。
  • 当前现场没有真人双手进入 Insight9 画面,实际真人手势姿态阈值仍需下一次现场操作确认;合成 ROS 链路验证不等同于真人识别验证。

2026-07-24 · 3D 姿态与轨迹刷新目标提升至 50 Hz

提交:058adc8、4c4682c、85bf60c(main)

为什么

  • 三路 VIO 输入约 100 Hz,后端 pose 推送目标已经是 50 Hz,但 Babylon 绘制仍硬编码为 20 FPS,导致高频姿态更新无法形成独立画面。
  • 轨迹 payload 每次包含三路完整的 300 点历史;消息快于绘制时仍会重复更新 Tube 几何,产生不会上屏的 CPU/GPU 工作。

做了什么

  • display_fps_limit 由后端随 pose payload 下发并实际控制 Babylon 绘制频率;jetson-nx profile 的显示目标改为 50 FPS。
  • WebSocket 广播改用单调时钟固定周期调度,避免 payload 构造耗时叠加到 20 ms 周期;过载时跳过错过的 deadline,不突发补发旧帧。
  • 轨迹协议改为“完整快照 + 序号增量”:首次连接、每 2 秒、清轨迹、回放切换或对齐矩阵变化时发送 300 点快照,普通 50 Hz 周期只发送约 2 个新增点;断档时浏览器重连获取快照。
  • 浏览器在消息到达时增量维护三路 300 点缓存,每个显示周期只应用最新 pose;静态模型检查不再进入每帧异步路径。
  • 三条 Babylon Tube 改为固定容量 GreasedLine,通过 offsets 更新单个顶点缓冲,不再反复计算 Tube 截面、法线和完整网格;Keep trajectory 模式按 300 点分块扩容。

怎么验证的

  • 前端构建、Python 编译、JavaScript 语法、src/dist 一致性和 payload 配置传递检查通过。
  • 可控三路 300 点数据中,普通 pose JSON 从约 18.7 KB 降至 1.47 KB,减少约 92.1%;快照、重叠增量、滚动裁剪、重复包和断档恢复检查通过。
  • 无头 Chromium 通过 50 Hz mock WebSocket 连续更新增量轨迹,成功加载 Babylon WebGL、GreasedLine offsets 和全部前端模块,无业务 JavaScript 异常。
  • 合入 main 后用 live 50/50 配置重启:三路相机约 20.07 / 20.05 / 29.90 FPS,WebRTC healthy;WebSocket 首帧为三路 300 点快照,随后普通帧为每路约 1–3 个新增点的 delta。
  • 尚未人工确认真实屏幕的最终观感和端到端 FPS;实际帧率仍取决于浏览器垂直同步及 Jetson GPU 负载。

2026-07-24 · Recording 页显示录制盘剩余空间

提交:591868d(main)

为什么

  • 录制前更需要直接看到磁盘余量;原 CPU load 标签不能提示 rosbag 是否可能因空间耗尽而中断。

做了什么

  • 状态接口新增实际 rosbags 所在文件系统的总量、已用量、剩余量和剩余比例。
  • Recording 页标签改为“剩余容量 + 百分比”;剩余低于 30% 显示黄色,低于 10% 显示红色。
  • 保留原 system_load 字段,避免破坏已有状态接口调用方。

怎么验证的

  • 前端构建和 Python/JavaScript 检查通过;真实接口返回约 31.8 GB、28% 剩余。
  • 浏览器分别模拟 50%、20%、5% 余量,确认绿、黄、红三档正确;完整六页面回归为 ok: true。

2026-07-24 · Dashboard 全栈结构拆分与注释收敛

提交:469d0ed 至 364ed47(main)

为什么

  • 后端主进程、后处理、在线对齐和前端共享脚本职责过多,修改局部功能容易影响无关路径。
  • 重构后仍保留了大量历史说明和调试过程,降低了代码可读性。

做了什么

  • 将运行时拆为 dashboard_runtime/,HTTP/WS 拆为 dashboard_web/,后处理拆为 post_processing_core/,在线对齐拆为 alignment/;原入口保留为兼容 facade。
  • 前端拆为页面入口及 shared/、camera/、spatial/ 模块,并继续提交构建后的 dist/。
  • 精简 47 个文件中的长注释与 docstring,只保留性能、并发、录制和回放等关键约束。

怎么验证的

  • Python 去除 docstring 后与重构前 AST 一致;Shell 与 JavaScript 语法检查通过,前端重新构建成功。
  • 完整 API、WebSocket 与 Chromium 回归通过;六个页面无相关 console/page error,三路摄像头与 WebRTC 均正常。
  • 真实录制验证图像为 20/20/30 Hz、writer 零丢弃,快速完整性检查通过。

2026-07-23 · 录制队列与完整性审计加固;规避 rclpy QoS waitable 崩溃;调整轨迹评分基线

提交:3cd9896、cf13dd2、c989d52、8f62c6a、e7c0438、6d0e85c、f5ffc29、eefebe2、0462e31(main)· 录制、校验、前端状态展示与轨迹评分相关文件

为什么

  • 图像再交给独立 ros2 bag record 订阅会新增 DDS reader,与 dashboard 预览/WebRTC 竞争并显著降低采集帧率;改成进程内接收后,SQLite/writeback 瞬时停顿又可能灌满原来的 128 深度队列。
  • 只审计已进入 callback 的 header 序列无法发现 writer 队列拒绝的帧;停止录制后的快速校验也不应依赖 metadata 时长估算。
  • Jetson + Fast DDS + ROS Humble 现场曾在录制进程退出时,于 rclpy 默认 incompatible-QoS event waitable 枚举路径发生段错误。
  • 轨迹评分原基线对当前设备协方差分布偏严,需要把典型本机样本的基准分从 80 调整到 88。

做了什么

  • 图像沿用 dashboard 的现有订阅,序列化放在线程、rosbag2/SQLite 写入放到独立进程;录制期间优先保采集,并降低 WebRTC 录制预览投递频率。
  • 图像 writer 两级队列统一从 128 增至 512,关闭排空超时增至 60 秒;新增总计及按 topic 的 writer drop 计数,并合并进 live header audit,使队列丢帧会直接令对应 topic 和整次审计失败。
  • 使用图像 header timestamp 加固定 offset 映射到 recorder timeline,持续审计帧间隔;check_bag.py --fast 改用 SQLite COUNT/MIN/MAX(timestamp),并对 /tf_static 与无 nominal rate topic 使用符合其语义的判定。
  • 所有 dashboard ROS 订阅关闭未使用的 rclpy 默认 QoS event callbacks,避免把相关 waitable 加入 executor;正常数据回调和显式 QoS profile 不变。
  • 轨迹评分 logistic 校准中心从 -3.8212 调整为 -3.61310,典型本机中位样本的 base score 从 80 调整到 88。

怎么验证的

  • node web_dashboard/build.js 与 git diff --check 通过,构建后没有产生额外前端差异。
  • 容器加载 ROS Humble 环境后,录制相关模块、QoS event 配置和轨迹评分模块均可导入;确认 SubscriptionEventCallbacks(use_default_callbacks=False) 与新校准常量生效。
  • 用新 InProcessBagWriter 在容器临时目录实际写入 4 条 ROS 消息,成功生成 SQLite rosbag、零队列丢弃并正常排空关闭。
  • 实时 /api/cameras 返回三路约 20.04 / 19.96 / 30.04 FPS,且 webrtc_available: true;无头 Chromium 加载 /recording 成功且无 console/page error。

已知事项

  • 本次发布前没有打断现场采集去跑完整长时录制压力测试;512 深度来自已观测的 writeback stall 与机器内存余量,后续仍应结合长时录制的 writer drop 审计继续观察。
  • 容器内无头 Chromium 不提供可用 WebGL,/3d 会报 WebGL not supported;本次以前端构建、录制页面和实时 API 完成发布前检查。

2026-07-22(深夜六)· 清理旧 dashboard 路径;WebRTC/hand-overlay 运行链路加固

提交:5758b45、e3c6838、27e1111、7edd0bb(main)· scripts/multi_camera_dashboard_web.py、scripts/hand_overlay.py、web_dashboard/、部署与文档文件

为什么

旧 Qt dashboard、Images 页面和 STL 导出链路已经不在当前容器运行路径上,却仍被版本库、文档或构建脚本保留;同时,真实三路画面 + 3D + hand overlay 场景暴露出两个运行问题:

  1. 手势骨骼的异步合成结果此前只会写回 HTTP/JPEG 回退画面,WebRTC 视频仍编码原始帧;并且原始帧会立即覆盖异步返回的合成帧,导致骨骼偶发或完全不可见。
  2. Jetson 的 WebRTC worker 在建立 H.264 管线后可能因 libnvmmlite_video.so 的运行时符号未进入全局链接作用域而退出(现场依次观测到 NvOsSleepMS、video_parser_flush)。worker 退出后,前端自动回退 HTTP 轮询,表现为三路一起掉帧;关闭 hand overlay 不会重启已退出的 worker。

做了什么

  • 清理废弃路径:移除已跟踪的 Python __pycache__/*.pyc、旧 Images 页面及其前端分支、旧 Qt dashboard 脚本、STL 源模型/导出脚本和未使用的 Node 静态服务;.gitignore 统一忽略 Python 编译缓存。保留 /images → /3d 兼容跳转与 /api/images/capabilities 诊断 API,并同步 README、Docker/Compose 和部署检查项。
  • 骨骼帧进入 WebRTC:compose_hand_overlay_jpeg 现在返回是否成功派发合成工作;有待合成的原始 JPEG 不再插入 WebRTC 队列。合成 worker 返回的 JPEG 同时更新 HTTP 回退帧和 WebRTC 队列,且显示层保留上一张合成帧直到下一张合成结果抵达,避免原始帧立即覆盖骨骼。
  • 降低 WebRTC 活跃时的无效 JPEG 工作:对原始 NV12/GRAY8 相机,WebRTC 会话存在时 HTTP 回退 JPEG 从每帧编码改为每 0.5 秒刷新一次;WebRTC 仍直接接收原始相机数据。无会话时维持原有逐帧 HTTP 回退。
  • 补全 Jetson 多媒体预加载依赖:启动 webrtc_worker.py 时预加载宿主机挂载的 libnvos.so、libnvvideo.so、libnvparser.so,使 nvv4l2 H.264 管线运行时需要的符号进入全局作用域,避免 worker 在管线真正开始后崩溃。

怎么验证的

  • 清理后前端 npm run build、node --check、Python 编译检查、shell 语法检查与 git diff --check 通过。
  • 容器内状态接口确认三路源输入稳定为约 20 / 20 / 30 FPS(insight3_a / insight3_b / insight9_a);这两路 20 FPS 是相机源上限,WebRTC 不能把它们提升为 30 FPS。
  • WebRTC worker 健康时,webrtc_available: true,8766 上可见三路已建立会话,日志确认三路分别启动硬件 H.264 管线(insight3 为 NV12,insight9 为 JPEG 输入)。
  • 使用 insight9_a 真实 HandEngine 数据验证:接口返回 21 个高置信关键点;抓取的真实帧可见 hand_right 0.99 标签、检测框与彩色 21 点骨骼。开启手势叠加后,worker 携带完整三库 LD_PRELOAD 并保持存活。

已知事项

  • 如果已运行的前端在容器重启后耗尽其 WebRTC 自动重试次数,需要刷新页面重新建立会话;后端健康状态为 true 不等于浏览器会话已经重新连上。
  • hand overlay 仍采用 JPEG decode → 绘制 → JPEG encode,再由 WebRTC 解 JPEG → H.264 encode;这条双重编解码在检测到手时仍有优化空间。后续可考虑把叠加绘制移动到 H.264 前的同一媒体管线,避免中间 JPEG 往返。

2026-07-22(深夜五)· hand-overlay 骨架实际上从未画出来(两个独立 bug);测试方法论教训

提交:5457c82(main)· scripts/hand_overlay.py、scripts/multi_camera_dashboard_web.py

为什么

上一条(深夜四)修完 fps 掉帧问题后,用户反馈"没开也掉帧,开启后也识别不到手势",并明确要求"测试要基于前端做,不是无头检测"。按这个要求换用真实 kiosk Firefox(open_web_3d_right.sh,真实 GPU、真实 DISPLAY,而非容器里无 GPU 加速的 headless Chromium——之前一版无头 Chromium 测试量出 0.7fps,后来证实是 headless 环境本身的假象,用真实 kiosk 复测三路都是 18-27fps 正常)复测,发现:

  1. "没开也掉帧" 实际是 webrtc_worker.py 崩溃了(已知的 libnvmmlite_video.so teardown 崩溃,7/22 早些时候就记录过),大概率是当天调试过程中反复 docker compose down/restart 撞上的——三路 pipeline 同时关闭是已知触发条件。崩溃后 webrtc_available 变 false,前端退回较慢的 HTTP 轮询兜底,看着像掉帧。重启容器恢复,纯基础设施问题,不是代码 bug。
  2. "开启后识别不到手势" 是真 bug,而且是两个:detection 数据本身是好的(HandEngine 用真手测,score 0.8-0.99,21 个关键点都在),但骨架从来没有画出来过。

做了什么

定位到 compose_hand_overlay_jpeg/_apply_composited_hand_overlay(深夜四那次引入的异步 dispatch 路径)里两个独立的 bug,两个都会让合成结果 100% 被丢弃:

  • 版本号精确匹配:_apply_composited_hand_overlay 原来要求 worker 传回的 version 和 camera_frame_versions 当前值完全相等才应用。但合成走两次进程跳转 + 一次 GStreamer 硬件编解码,往返延迟常规性地超过一个相机帧周期(30fps 下 ~33ms),等结果回来时版本号几乎总是已经往前走了——于是每一次合成结果都被判定"过期"丢弃,且这个丢弃是静默的,表现就是"骨架永远不出现"。改成"只要不比已经应用过的最新版本旧就应用"(新增 _hand_overlay_last_applied 单调计数器),不再要求精确匹配;served 帧沿用当前的 stamp_ns/version,只换像素字节。
  • 同步窗口比较了两个不可比的时钟:compose_hand_overlay_jpeg 原来直接拿图像帧自己的 header.stamp 减 hand_keypoints 消息的 header.stamp,要求 |gap| < 120ms 才画。加了临时 debug print 实测(真手在镜头前)发现这个 gap 稳定在 ~430-500ms(不是抖动,是稳定偏移)——说明 HandEngine 是按自己的时钟盖戳,不是转发图像驱动的时间戳,两个 stamp 根本不在同一个零点上,导致这条同步检查把每一帧都拒绝了,骨架从来没有机会画出来。改成拿 hand_keypoints 快照的本地接收时刻(received_monotonic,和 HAND_DATA_TIMEOUT_SEC 那条陈旧检查用的是同一个时钟基准)跟"现在"比,彻底绕开两个消息各自时间戳不可比的问题。MAX_SYNC_NS(纳秒,比较两个 stamp)改名 MAX_SYNC_SEC(秒,比较接收时刻与现在),compose_hand_overlay_jpeg 不再需要 image_stamp_ns 参数。

这两个 bug 都是"深夜四"引入 dispatch 路径时才可能触发的新问题(原来同步实现里合成是原地做的,不存在"等结果"这个环节,版本号问题无从谈起;同步窗口比较逻辑本身没变,但没人在真实设备上拿真手测试过,所以一直没暴露)。

怎么验证的

  • 更新单测 test_hand_overlay_sync_window_20260710.py:不再用两个 stamp 构造 gap,改成直接操纵快照的 received_monotonic 模拟"接收了多久",验证 4 个边界场景(刚收到/窗口内/窗口外/超过 2s 陈旧)行为不变。
  • 真实设备 + 真实 kiosk 复测(这次严格照用户要求,不用无头检测):docker exec -d ... open_web_3d_right.sh 拉起真实 Firefox kiosk(真实 GPU、真实 DISPLAY),用户本人的手持续举在 insight9_a 镜头前,gnome-screenshot 截真实屏幕 + 直接拉取 /api/cameras/insight9_a/frame 逐帧检查:修复前两个 bug 都在时,多次截图/拉帧骨架从未出现;修完后骨架正确画出(按手指分色的骨骼线、关节白点、hand_right 0.85 标签+置信度),且三路 fps 保持 18.7/18.1/24.1(原生 ~20/20/30),叠加层持续合成也没有再掉回 5fps。

教训

  • 测试方法论:无头 Chromium 在没有真实 GPU 加速时会把 WebGL/视频渲染压力全甩给软件渲染的主线程,量出来的"客户端观测 fps"可能是测试环境的假象而非真实体验——这次的 0.7fps 就是这么来的。以后凡是涉及"用户眼睛看到什么/前端实际帧率"这类问题,优先用真实 kiosk(open_web_3d_right.sh,真实 GPU + 真实 DISPLAY)或至少非 headless 复测,不要只信无头浏览器或纯后端 API 数字。
  • 异步化任何"合成/处理后再回填"的路径时,版本号/时间戳比较默认用"精确匹配"几乎总是错的——只要涉及跨进程往返,就必须假设"回来的时候已经不是最新的了",用单调递增的"不倒退"判断代替精确匹配。

2026-07-22(深夜四)· hand-overlay JPEG 合成拆独立进程,消除"检测到手就掉到 5fps"

提交:9e8047c(main)· scripts/hand_overlay_worker.py(新增)、scripts/hand_overlay.py、scripts/multi_camera_dashboard_web.py

为什么

用户反馈:Settings 里打开 hand landmark overlay 后,只要 insight9_a 检测到手,三路图像流和 3D 视图就一起掉到 5fps;但用户自己单独跑 hand.py(PC 侧参考工具)能到 30Hz,三路+3D+hand.py 分开跑合计也能到 20fps 左右——只有走 dashboard 前端才崩。

诊断:和当天早些时候(见本文件 2026-07-22 · WebRTC 拆独立进程 一条)已经修过的 WebRTC GIL 争抢是同一个病灶,只是没拆干净。multi_camera_dashboard_web.py 是单进程多线程架构,rclpy 回调、各相机 JPEG worker 线程、3D pose 广播 loop 共享一个 GIL。hand_overlay.py 的 compose_hand_overlay_jpeg 走 hw_jpeg.py 里同一套 GStreamer appsrc→nvjpegdec/nvjpegenc→appsink pipeline(push-buffer/try-pull-sample 会占住 GIL),但这条路径只在检测到手时才真正执行(没手时在碰 hw_jpeg 之前就直接 return None 了)——精确对应"只有检测到手才掉帧"的现象,WebRTC 那次拆分时没有顺带把这条路径也挪出去。

做了什么

  • 新增独立进程 scripts/hand_overlay_worker.py:专门做 JPEG decode→画骨架(复用 hand_overlay.draw_hands_on_frame)→re-encode,用 HwJpegCodec(复用 hw_jpeg.py,硬件不可用时自动退化到 cv2 软件路径)。结构照抄 webrtc_worker.py(subprocess.Popen 启动、PR_SET_PDEATHSIG 保证父进程死它必死、AF_UNIX + authkey 鉴权、断线自动重连),但不需要 HTTP server(它只跟主进程通信,不直接面向浏览器)。Worker 内部按相机分发到独立 composite 线程(单槽位"只留最新"模式,照抄 _pending_frames/_frame_worker_loop),避免将来多个 hand 相机互相排队。
  • hand_overlay.py 的 compose_hand_overlay_jpeg 改为只做廉价 gating(payload 查找、stale 检查、MAX_SYNC_NS 同步窗口检查——这些本来就很便宜,继续留在主进程,保持"没手=零成本"),真正的合成工作 fire-and-forget 派发给 worker,永远返回 None。
  • multi_camera_dashboard_web.py 加对应 IPC 编排:_dispatch_hand_overlay/_hand_overlay_ipc_loop/_apply_composited_hand_overlay。合成结果异步到达后补丁进 latest_camera_frames,用版本号(camera_frame_versions)校验——只有没有更新的帧插进来才应用,避免异步结果把已经前进的帧版本"拨回旧的";worker 挂了/收到畸形消息也不会拖垮主进程,画面只是那一帧没叠加骨架(和今天"没检测到手"时的表现一样,不崩溃)。

怎么验证的

  • 单测:更新了旧的 test_hand_overlay_sync_window_20260710.py(原来断言 compose_hand_overlay_jpeg 返回值,现在改断言是否触发了 _dispatch_hand_overlay——gating 逻辑本身没变,4 个场景全过);新增 test_hand_overlay_worker_ipc_20260722.py,脱离整个 ROS 后端单独起 hand_overlay_worker.py 子进程验证 IPC round trip:真实合成(NVJPEG 硬件路径实际跑通,返回的 JPEG 能正常解码、尺寸不变、像素非零)+ 畸形消息不会打死 worker。

  • 真实容器负载复现(因为设备侧 HandEngine 现在没在跑,没有真手可用):写了 fake_handengine_publisher_20260722.py,订阅 insight9_a 真实图像流蹭 header.stamp,每帧同步发布一组 21 点假 hand/hand_keypoints Detection2DArray(比真实场景更狠——每帧都有"手",不是偶尔检测到),在真实 docker 容器里跑:

    场景 insight3_a insight3_b insight9_a
    改前基线(无手) 20.0 20.0 30.0
    开 hand overlay + 每帧持续"检测到手" 15s 20.0-20.2 19.9-20.1 29.9-30.2

    三路帧率全程没有下降(旧代码这个场景应该掉到 ~5fps)。同时确认 outputs/hand_overlay_worker.log 里 worker 正常拉起、IPC 连上;主进程 perf 日志(_log_perf_summary)里 hand_overlay_draw* 两条 trace 已经从主进程完全消失,只剩廉价的 hand_parse:insight9_a(~0.2-0.3%)。

已知问题 / 待办

  • 没有用真手验证过骨架的最终视觉效果(叠加位置、颜色是否符合预期)——上面的负载测试用假关键点数据只验证了"不掉帧、合成流程跑通、JPEG 有效",真实 HandEngine 跑起来后建议用户用真手过一遍确认画面正常。
  • worker 崩溃后没有自动重启逻辑(和 webrtc_worker.py 一样的取舍,保持简单)——挂了需要重启主进程容器才能恢复合成,期间画面会一直是没叠加骨架的原始帧(不影响图像流本身)。

2026-07-22(深夜三)· 3D 手部左右手改成"先出现的绑 insight3_a",不再依赖 class_id

提交:f9649ed(main)· scripts/hand_overlay.py

为什么

用户提议:与其继续跟 HandEngine 不可靠的 class_id(左右手判别)纠缠,不如直接约定"第一只检测到的手绑定 insight3_a,第二只绑定 insight3_b",绑定之后就不再改变——不追求"绝对左右手正确",只追求"稳定不闪烁"。

做了什么

  • _assign_hand_roles(scripts/hand_overlay.py)的第三阶段(一只手完全没有历史位置可参考时的"冷启动"分配——刚开始追踪、或双手同时首次出现)原来是查 class_id(HAND_CLASS_TO_ROLE),现在改成固定的到达顺序:新增 HAND_ROLE_SEED_ORDER = ("right_hand", "left_hand"),谁的 hand_index 小就先分给 right_hand(对应 insight3_a),剩下那只归 left_hand(insight3_b)。完全不看 class_id 了。
  • 第一、二阶段(粘滞 + 宽范围位置关联,7 月 22 日之前两次修复加的)不变——一旦冷启动分配完,后续全靠位置连续性钉住,不会因为 class_id 反复无常又跳回去。
  • class_id 的平滑投票逻辑(smooth_hand_label)本身没删,2D 图像叠加层的文字标注还在用它(纯展示,不影响 3D)。

怎么验证的

  • 单元测试整体重写(~/workspaces/insight_capture_tests/test_hand_role_assignment_20260722.py):新增 test_fresh_seed_uses_hand_index_order_not_label(故意给两只手都打错误 label,确认按 hand_index 顺序分配,不看 label);其余测试相应改成不再断言"具体是哪个 role",只断言"分到哪个 role 之后就不再变"(因为新逻辑下具体分到 left 还是 right 本来就是按到达顺序,不是按真实解剖学左右)。5 个测试全过。
  • 用真实双手同时入镜的实时数据跑了 25 秒/751 帧:只在最开始的第 4-6 帧各出现一次切换(属于追踪刚启动、两个 role 的锚点还没建立完成时的正常冷启动过程),之后连续 745 帧(约 24 秒)零切换。

已知问题 / 待办

  • 现在故意不追求"这只手真的是解剖学左手/右手",只保证"绑定后不换"——如果客户后续反馈"每次戴上设备,虚拟形象的左右手总是反的"(稳定地反,不是闪烁),那是 HAND_ROLE_SEED_ORDER 元组顺序的问题(谁先出现该绑哪只手),把元组顺序调换即可,不需要动位置关联逻辑本身。

2026-07-22(深夜二)· 3D 手部骨骼方向绕竖直轴转 90 度对齐 insight3 相机前向

提交:0bbfa5e(main)· web_dashboard/src/app.js

为什么

用户反馈:3D 界面里手势应该朝向 insight3(手腕)相机的正前进方向,实际渲染出来方向差了 90 度。

做了什么

  • handLandmarkToLocal(web_dashboard/src/app.js)原来把"沿手指方向"(along)映射到 wrist 姿态节点的 local Z 轴——这是照搬 mapDashboardPositionToScene 里"ROS 机体前向 → scene Z"的既有约定,理论上应该对。但实测(用户目视对比真实设备朝向和 3D 渲染)发现差了 90 度。
  • 把整套映射绕竖直(up)轴转了 90 度做补偿:along 改到 local X,lateral 挪到 local Z(保持原来 y 轴不变,仍是合成深度轴)。这是一次纯旋转(行列式不变,仍是 -1),不影响原来 -lateral 那个符号所代表的镜像/手性问题——那是另一件事,这次没动。

怎么验证的

  • 没有办法从这台机器上直接验证"是不是真的对齐了物理相机朝向"——这需要人在现场同时看真实设备和 3D 画面,我这边只能做到:语法检查、Playwright 加载 /3d 页面确认无 console 报错、用合成手部数据跑通渲染管线不崩溃、以及矩阵计算上确认这确实是一次绕 up 轴的纯 90 度旋转(没有引入额外镜像)。
  • 这个 90 度的转向(往哪边转)是猜的,不是实测确认的——如果转完之后发现反而变成反方向差了 90 度,不用整体回退,把 along/lateral 这两个轴的符号对调一下(往另一个方向转 90 度)就行,已经在代码注释里写清楚了。

已知问题 / 待办

  • 需要用户在物理设备旁边实际确认一次这次转向对不对,这条修复目前只能算"逻辑上合理的最佳猜测",不算已验证。

2026-07-22(深夜)· 3D 手部左右手"来回切换"(震荡)修复

提交:9f5edbc(main)· scripts/hand_overlay.py

踩坑记录(判断方法本身的教训):排查过程中第一次用"WS payload 里 hand_landmarks 相邻两帧的平均位移"来判断"是不是左右手互换了",结果发现几乎每隔几帧就报警——但同时抓的原始 /insight9_a/camera/hand bbox 中心位置几乎没动、class_id 也没变。说明那个"位移突增"根本不是身份互换,是手在原地转动/张合手指导致 normalize_hand_landmarks 的局部坐标基(沿手指方向)跟着重新计算,形状在归一化坐标里自然跳动——这是正常动作,不是 bug。换成"追踪 bbox 屏幕坐标本身的跳变"这个更直接的指标后,同一段真实双手数据上背后算法(改之前和改之后)都测不出真正的身份互换。结论:调这类"两个 track 有没有互换"的问题时,认定"闪烁"证据前要先确认测的是身份(位置/bbox),不是形状(归一化关键点/landmark 位移)——后者天然会因为正常动作产生大幅度变化,会把你导向错误的根因。

为什么

用户反馈:上一轮"按位置就近关联"的修复上线后(见上一条 2026-07-22(下午)changelog),双手同时入镜时 3D 骨架仍然"不停地换来换去",而不是偶发的一次性互换。

做了什么

  • 排查 _assign_hand_roles:发现问题——两只手正常间距下(2-3 个手掌宽),双方都同时落在 HAND_POSITION_JUMP_FACTOR=6 的宽松预算内,导致每一帧都是"4 个候选配对里选最近的"这种纯贪心匹配,完全没有"跟上一帧保持一致"的倾向。检测噪声导致的普通帧间抖动,只要偶尔让"错误配对"的距离比"正确配对"略小,就会切换过去,下一帧又可能切回来——这才是持续来回切换的根因,不是一次性坐标误判。
  • 修复:加一段"粘滞"(sticky)阶段,先跑:每个 role 独立检查"我自己上一次的手现在还在不在附近"(HAND_POSITION_STICKY_FACTOR=1.0,半径明显小于两手正常间距,只用来判断"没怎么动",完全不跟另一个 role 的距离比较)——只要没挪多远直接沿用,根本不给另一只手插队的机会。原来那种"跟所有候选比距离"的宽范围匹配(HAND_POSITION_JUMP_FACTOR=6)降级成第二阶段,只在粘滞判断没解决(手真的挪远了,或短暂没检测到)时才启用。

怎么验证的

  • 单元测试新增 test_stationary_hands_do_not_oscillate_under_jitter(~/workspaces/insight_capture_tests/test_hand_role_assignment_20260722.py):固定种子跑 200 帧随机抖动(±8px,间距 300px,手掌尺寸 80px),角色分配全程不翻转。
  • 用真实两手同时入镜的实时数据做了 A/B 对比(同一份 ROS 消息流,同时跑改前/改后两套算法逻辑):追踪 bbox 屏幕坐标的跳变(阈值 150px)作为身份互换的判据(不是上面提到踩坑的 landmark 位移指标)。多轮 30-45 秒/千帧级别的实时对比里,两个版本在这些窗口内都没测出真正的身份互换——因为这段时间内两只手实际间距一直有 300px+ 没有真正贴近/交叠过,所以旧算法本身也没触发这次要修的震荡条件;本次修复是从代码逻辑推理(贪心匹配无粘滞 → 抖动可致回摆)确认必要性并加固,而不是靠现场复现到这个 bug 本身来验证——如果之后两手贴得更近时又出现来回切换,大概率是粘滞半径 HAND_POSITION_STICKY_FACTOR 需要跟着两手实际能贴多近去调。

已知问题 / 待办

  • 没有拿到"用户报告闪烁那一刻"的真实数据直接复现,所以这版修复是基于代码审查发现的真实设计缺陷(无粘滞的纯贪心匹配)来修的,逻辑上站得住,但还没有实测确认就是用户看到的那次闪烁的根因——下次用户再看到闪烁,除了问"两手是不是很近/交叉",最好让他们截屏或录屏,不要只靠口头描述,这次靠 WS payload 指标反复拉扯验证消耗了不少时间。

2026-07-22(晚)· 手部标记球缩小一半,手部骨骼线改成加粗纯红色

提交:fc8e665(main)· web_dashboard/src/app.js、web_dashboard/src/*.html

为什么

用户要求:3D 火柴人模式下代表手的球缩小一半,手部骨骼线加粗并统一改成红色(原来是按手指分色)。

做了什么

  • attachStickMarker 里手部标记球(left_hand/right_hand)直径从 0.045 降到 0.0225(头部标记球 0.06 不变,用户只提到手)。
  • 手部骨骼线原来用 BABYLON.MeshBuilder.CreateLineSystem(GL_LINES 图元)画——WebGL 的 gl.lineWidth 在几乎所有桌面浏览器后端上都被钳制成 1px,不管代码里怎么设置都粗不起来。改成每根骨头一个 CreateTube 网格(真实 3D 几何体,半径 HAND_RIG_RADIUS=0.004 米),共用一个纯红色(#ff0000)无光照材质,不再按手指分色。更新时用 CreateTube 的 instance 参数复用已有网格(同一套按帧更新的性能模式,没有改成每帧重建)。

怎么验证的

  • 因为这台设备当前没有真手入镜(HandEngine 侧 hand_landmarks 实测为 None),单纯截图看不出效果——写了 ~/workspaces/insight_capture_tests/publish_synthetic_hand_20260722.py,以 100Hz 在 insight3_a(right_hand)上发布合成的 21 点关键点,跑赢真实 HandEngine 的 15Hz 空数组覆盖(见 handengine-and-skeleton-state 的测试经验)。
  • 不满足于截图肉眼判断,直接用 Playwright 读取 Babylon 场景对象做数值验证:
    • hand-rig-insight3_a-* 网格材质 diffuseColor/emissiveColor 均为 [1,0,0]——确认纯红。
    • 骨头网格包围盒最短边约 0.007m(对应 radius=0.004 的真实管状几何体直径),远大于原来约等于 0 的 GL 线宽——确认变粗且是真实厚度而非视觉错觉。
    • stick-marker-insight3_a 包围盒直径 [0.0225, 0.0225, 0.0225]——确认球缩小到位。
    • 顺带确认场景里只有这一套 21 根骨头网格、父节点正确挂在 pose-insight3_a 下,排除了重复渲染的疑虑(截图角度一度让人怀疑有两只手,实为同一只手掌心簇和指尖簇在这个相机角度下投影分离,不是 bug)。

2026-07-22(下午)· 去掉合成的胳膊骨骼线;修 3D 手部左右手闪烁/互换

提交:55e0adb(main)· scripts/hand_overlay.py、scripts/multi_camera_dashboard_web.py、web_dashboard/src/app.js、web_dashboard/src/*.html

为什么

用户反馈两个独立问题:

  1. 火柴人模式下 3D 界面里"肩→肘→腕"那两条线(本来想模拟胳膊骨骼)跟真实动作根本对不上,要求去掉。
  2. 双手同时出现在画面里时,3D 手部骨架会翻转闪烁;不确定是不是左右手互相认错了。

做了什么

  • 去掉合成胳膊骨骼线:solve_arm_ik(scripts/multi_camera_dashboard_web.py,两骨 IK,从头部姿态锚定的固定肩宽/大臂/小臂长度反推肘部位置)本来就是纯人体比例假设、不是真实追踪数据(代码注释原话是"plausible, not accurate"),删掉这个函数和相关常量,WS payload 不再带 shoulder_position/elbow_position;前端 updateArmRig() 及其调用、armRigs Map 一并删除。
  • 3D 手部左右手闪烁根因:定位在 HandEngine 的 hand_index 不是持续性 track——双手同时出现时两只手的下标可能帧间互换;同时它自己的 hand_left/hand_right 分类在置信度接近 0.5 时基本是瞎猜(对应已知的上游问题 google/mediapipe#3047 单手判别不稳、#3902 双手被判成同一标签)。原来的实现完全信任这个逐帧标签(smooth_hand_label 按 (camera, hand_index) 做多数票平滑,再加 roles_hit 去重喂 3D track),下标一互换平滑窗口就会被另一只手的投票污染,迟早翻转;两手标签撞车时去重逻辑还会直接丢帧,导致骨架跳到另一只手的位置。
    • 改成位置就近关联为主、分类器为辅:新增 _assign_hand_roles,每帧检测到的手先按手腕/bbox 位置去匹配"这个 role(left_hand/right_hand)上一次出现的位置"(HAND_POSITION_JUMP_FACTOR=6 倍手掌尺寸内算同一只手),位置连续就沿用原 role,不管这帧的 class_id 说了什么;只有全新出现、附近没有已知位置的手才用平滑后的分类器标签去认领空着的 role。
    • class_id 投票也加了置信度门槛 HAND_LABEL_MIN_CONFIDENCE=0.6,score 太接近 0.5 的帧直接不计入投票窗口。

怎么验证的

  • WS payload 实测:开关 stick-figure 模式,确认 shoulder_position/elbow_position 已从 payload 消失,hand_landmarks 字段仍正常。
  • Playwright 无头加载 /3d(开 stick-figure 模式)无 console 报错,截图确认场景只剩手部关键点骨架(21 点 MediaPipe 连线)和角色圆点,肩肘线已不在。
  • 新增单元测试 ~/workspaces/insight_capture_tests/test_hand_role_assignment_20260722.py,直接调 _assign_hand_roles(不需要 ROS):模拟双手同时出现且 hand_index+class_id 同时互换的场景,确认 role 分配跟着物理位置走不翻转;另覆盖低置信度单手不硬吃、track 死亡后按标签重新认领两个边界情况。三项全过。
  • 网上搜索确认了诊断方向(非本仓库自证):google/mediapipe#3047(单手判别近 0.5 边界不稳定)、#3902(双手同时判成同一标签)、#4785(一般性误判报告),见对话记录。

已知问题 / 待办

  • 本次未用真实双手同时入镜的画面验证——只做到了合成场景单元测试和无头页面加载检查。上一次这条 pipeline 的 chirality 问题就是靠用户拿真手测试才最终确认过,这次同样需要真人测试确认闪烁是否真的消失,尤其是两只手距离很近、可能超出 HAND_POSITION_JUMP_FACTOR 阈值的场景。
  • HAND_CLASS_TO_ROLE 那个 hand_left→left_hand 的映射方向本身仍然"未针对真实数据验证过"(代码注释原话),如果真人测试发现左右手整体性反了(不是闪烁,是稳定地反),去 scripts/hand_overlay.py 里翻这个映射,而不是本次改的位置关联逻辑。

2026-07-22 · WebRTC 拆独立进程,消除 3D+三路视频同屏的 GIL 争抢;pose_publish_hz 配置化

提交:b72068d(main,功能本体)、8833c68(main,app.js 缓存版本号补丁,见下)· scripts/webrtc_worker.py(新增)、scripts/webrtc_stream.py、scripts/multi_camera_dashboard_web.py、web_dashboard/src/app.js、config/devices/*/cameras.json

踩坑记录:b72068d 改了 app.js 但漏改了 6 个页面 <script src="app.js?v=..."> 的缓存版本号(停留在 2026-07-15 那次改动留下的值)——已经打开过页面、浏览器缓存过这个确切 URL 的 kiosk 不会去重新拉取新版 app.js,会继续拿旧版本连已经被这次改动移除的主进程 /ws/webrtc 路由,表现就是"部署了但没有任何变化"。本次会话验证时全程用无缓存的 Playwright headless 实例,没有复现出这个问题,是用户实际打开页面反馈"没有改善"才发现的——之后这类改动要记得检查缓存版本号是不是也一起改了,或者干脆用构建产物 hash 代替手写版本号。已在 8833c68 里补上(?v=20260722-webrtc-split)并重新 build。

为什么

用户反馈:三路相机 + 3D 视图同屏时,即使已经在用 WebRTC,三路视频都只剩十几帧,怀疑是 GIL 被占满。

做了什么

  • 先定位:不经浏览器,纯后端复现(~/workspaces/insight_capture_tests/test_gil_3d_video_20260722.py)——单独开 3D 的 /ws 姿态广播、或单独开三路 /ws/webrtc,掉帧都很轻微;只有两者同时活跃(对应真实的"三图+3D同屏")三路才一起明显掉帧。同时采样 /proc/stat 发现该场景下 6 核总占用只有 ~30%,没有一个核跑满——是 GIL 争抢的特征,不是算力不够。
  • 拆进程:把 webrtc_stream.py 的 GStreamer/webrtcbin session 管理整体挪到新的独立进程 scripts/webrtc_worker.py(subprocess.Popen 启动,不用 multiprocessing.Process/fork,避免 fork 一个已经多线程+已初始化 GStreamer 状态的进程)。主进程与 worker 用 multiprocessing.connection(AF_UNIX + authkey 鉴权)做双向 IPC:主进程→worker 发已解析好的帧字节,worker→主进程回传每路相机"有没有人在看"的会话状态——帧的构造/发送必须先查这个状态,没人看直接跳过,不然会把现在"没人看=零成本"的特性废掉。worker 挂掉不影响主进程:PR_SET_PDEATHSIG 保证父进程以任何方式退出 worker 都会跟着退出,主进程侧则通过定时探测 worker 的 /healthz 来决定要不要告诉前端"WebRTC 可用",worker 没起来/挂了就自动报不可用,前端照旧退回轮询。前端相应地把 WebRtc 信令连接换成打 worker 自己的端口(camera.webrtc_port,从相机状态 JSON 里带过来)。
  • 顺带:pose_publish_hz 从写死的 15 改成可以在 dashboard.trajectory.pose_publish_hz(config/devices/<device>/cameras.json)里配置,三个 device profile 都设成了 50(用户要求)。

怎么验证的

  • 原问题确认修复(都在 pose_publish_hz=15 这个此前验证过的良好状态下测的,隔离拆进程本身的效果):

    camera 拆前 baseline 拆前 ws+webrtc 同开 拆后 baseline 拆后 webrtc 单开 拆后 ws 单开
    insight3_a 19.8 17.6 20.0 20.0 19.7
    insight3_b 19.9 17.3 20.1 20.0 19.6
    insight9_a 24.2 17.3 29.6 29.3 27.2

    拆进程后 webrtc 单开幅本几乎不掉帧(跟 baseline 几乎一样),说明 GStreamer session 管理已经不再挤占主进程的 GIL。

  • 崩溃隔离(意外验证,不是刻意设计的用例):测试过程中 worker 进程真的崩溃了一次(原生库 libnvmmlite_video.so 缺符号 video_parser_flush,发生在同时关闭多路 pipeline 的时候)——主进程完全没受影响,相机继续以满帧率(19.9/20.0/29.9)出帧,/api/cameras 的 webrtc_available 在下一个 5s 探测周期内正确翻成 false,前端会自动退回轮询。

  • 零成本门控:多次测试里"没人连 /ws/webrtc"的 baseline 轮次,帧率始终维持在相机原生速率,没有观察到任何因为门控逻辑本身引入的开销。

已知问题 / 待办

  • libnvmmlite_video.so 原生崩溃是预先存在的问题,这次只是把它的影响范围从"整个后端"缩小到"WebRTC 功能",没有修复它本身——发生在多路 pipeline 同时 teardown 时,触碰到的代码(GStreamer pipeline 构建/析构)这次完全没改动,很可能和 wiki 里之前记录的"WebRTC 联调期间两次不明原因重启"是同一个根因。worker 目前没有自动重启逻辑(挂了要手动 docker restart 或手动重新拉起),是有意保持简单的取舍,不是遗漏。
  • pose_publish_hz 改成 50 之后,只要 3D 页面打开(/ws 有客户端),会比之前 15 目标值掉更多相机帧——实测同样"只开 /ws"这一个条件下,15 目标值时是 19.7/19.6/27.2,50 目标值时掉到只剩 15.2/15.3/15.1,均匀地砸在三路相机上,跟本次 WebRTC 拆进程无关。原因:广播 loop 每 tick 的实际构建+序列化耗时(300 点轨迹 ×3 pose 取整+JSON)本来就到不了 50Hz(实测封顶在 ~13-15Hz),但把目标从 15 提到 50 之后循环里留给 sleep 的余量被压没了,变成几乎连轴转,白白多吃 GIL 时间却没换来更高的实际广播频率。如果要真正受益于更高的 pose 刷新率,需要先优化 build_pose_payload/_build_pose_broadcast_json 本身的开销(更小的 trace payload、增量更新而非每 tick 全量重发),否则现在这个配置是"3D 打开时多付出相机帧率,但 3D 轨迹本身刷新率并没有真正提高"。
  • 本次会话没有用真实 Firefox kiosk 复验三路视频+3D 同屏画面(只在纯后端层面复现和验证了修复),下次接触这块代码前建议先跑一遍。
  • 诊断用的 SIGUSR1 活体全线程 dump hook(multi_camera_dashboard_web.py 里 faulthandler.register)保留下来了,对以后排查这类问题应该还有用。

2026-07-15 · 3D 手部骨架防闪烁(track 生命周期)+ Spatial 视图渲染降负

提交:f9ddf62(main)· scripts/hand_overlay.py、web_dashboard/src/app.js、web_dashboard/src/*.html

做了什么

  • 3D 手部骨架 track 生命周期:HandEngine 即使手稳定在视野内,单帧检出率也远低于 100%,且漏检帧发布的是"空检测数组"而不是"没有消息"——此前 3D 骨架只渲染"有新检测"的帧,导致每秒闪烁数次。新增按 teleop role(left_hand/right_hand)独立维护的 HandRoleTrack 生命周期:birth(连续 3 次命中、允许 0.3s 间隔容错才让骨架出现,过滤单帧误检)+ coast(出现后断检最长保持 0.5s 上一姿态,而不是立即消失)。2D 图像叠加层不受影响,本来就能容忍漏检帧不可见,不需要这套逻辑。
  • Spatial 视图(/3d,已合并实时画面)渲染降负:3D 场景渲染循环从 30fps 降到 20fps(匹配 pose 广播的 15Hz,此前一半帧在重绘同一份姿态数据);BabylonJS 硬件缩放从 1.15 提到 1.4(WebGL 缓冲实际渲染像素减少约 1/3)。目的是给 Firefox 合成线程减负——该线程同时要合成三路 WebRTC <video> 元素,此前实测被跑满整核。

怎么验证的

  • 手部骨架:~/workspaces/insight_capture_tests/test_hand_track_lifecycle_20260715.py,合成 30s@15Hz、15% 逐帧漏检序列跑 track 生命周期,11 项断言全过;关键对比——同一份漏检序列下,旧逻辑("这帧有检测就渲染")可见性翻转 110 次(3.7 次/秒,肉眼即闪烁),新逻辑仅 1 次。另有 test_hand_track_e2e_ws_20260715.py 接真实 WebSocket 广播复验状态转移次数落在预期区间(3-6 次:出生/coast 死亡/重生,而非逐漏检帧翻转)。
  • 渲染降负:本次会话未重新量化 Firefox 合成线程占用/显示帧率,build_release.sh 检查清单里的"关键页面无 console 报错"这一项也未重跑;下次接触这块代码时应先用 test_3d_camera_fps_20260715.py(Playwright 起 /3d 读 fps 标签)复测一遍再改动。

2026-07-15 · 视频流+3D 并存帧率优化第二轮:WebRTC 硬件降采样 + 广播降频

提交:b76930a(main)· scripts/webrtc_stream.py、scripts/multi_camera_dashboard_web.py

做了什么

  • WebRTC 流编码前 VIC 硬件降采样:此前编码管线按相机原始分辨率 1088×1920@30fps 输出,而相机墙缩略图只显示 ~300px 宽——kiosk 浏览器(Firefox RDD 进程)软件解码这三路流实测吃掉整整一个核,解码像素的 93% 被缩小丢弃。现在利用管线里已有的 nvvidconv(Jetson VIC 硬件缩放,零 CPU)在编码前把宽度钳到 STREAM_MAX_WIDTH=540(保持宽高比、NV12 偶数对齐)。实测日志确认:1088x1920 -> 540x952 at 2313 kbps(原 9400 kbps,像素量 1/7.2)。代价:maximize 放大面板时画面从 540p 拉伸、偏软。
  • pose 广播目标频率 20→15Hz:单次 payload 构建+序列化实测仅 ~4ms,广播间隔拖到 ~82ms 的真凶是 GIL 竞争(300Hz 位姿回调 + 3 路 JPEG worker + GStreamer 线程)。降频后 tick 相对排期的超时从 +32ms 缩到 +15ms,p95 抖动 113→100ms,轨迹视觉无差别。trace 四舍五入同时改为一次 np.round(替代 300 次迭代的 Python 循环,实测 2 倍快)。

怎么验证的

  • 编码端:真实浏览器会话触发新管线,后端日志确认降采样分辨率与码率;
  • 广播:WS 采样 45 帧测间隔(mean 81.8ms/p95 100.4ms vs 改前 82.6/112.9,注意目标周期已从 50ms 变为 66.7ms,超时余量明显收窄);
  • 待确认:kiosk 端解码 CPU(RDD 进程)与相机墙显示帧率——验证时 kiosk 未运行,等重新打开后观察(预期 RDD 从 ~106% 降到 ~15%,显示帧率回到 25-30fps)。

2026-07-15 · 暖纸主题字号整体上调

提交:07deb95(main)· web_dashboard/src/styles.css

用户反馈主题改版后字体偏小(等宽标题字在相同像素下视觉上也比 Inter 更小,叠加放大了问题)。可读性覆盖层整体上调 1-2px(正文 14→15px、按钮 12→13px、输入框 13→14px、日志 10→11px 等),大标题从 clamp(26-40px) 恢复到 clamp(32-48px),h2/统计数字同步放大,≥1800px 视口档位同步上移。Playwright 验证 /recording、/3d 零错误并截图复核。


2026-07-15 · 暖纸主题微调:3D 场景与日志区改浅、去掉刻度线

提交:3ced616(main)· web_dashboard/src/app.js(仅视觉常量)、web_dashboard/src/styles.css

做了什么

  • 用户反馈暖纸主题下残留的深色块突兀(3D 轨迹场景、录制页 Recorder log、优化页 Pipeline activity),且标题下的刻度线横条看着奇怪:
    • Babylon 主场景 clearColor 改暖纸色、网格线改暖灰(app.js createScene 里的纯视觉常量);优化页轨迹图同样改暖纸底,VIO/COLMAP 线色对齐 CSS 图例色(--red/--green);
    • Recorder log / Pipeline activity 日志区改为浅暖纸面 + 暖墨文字;
    • 页面标题和 3D 相机墙标题下的刻度线全部移除,只留一条普通细线;
    • "屏幕保持深色"的例外现在只剩相机视频框体(视频周围亮底确实眩光)。

怎么验证的

  • Playwright 无头加载 /3d、/recording、/optimization:零 console/页面错误,逐页截图复核(3D 场景呈绘图纸效果,轨迹绿/蓝轴线在浅底上清晰)。

2026-07-15 · Daylight Telemetry 暖纸主题:应用户要求由深色改为浅色暖色

提交:a6fe134(main)· web_dashboard/src/styles.css、各页 HTML(仅缓存版本号)

做了什么

  • 用户反馈不喜欢深色主题,要求浅色暖色。经 theme-factory 流程确认(定制 Daylight Telemetry vs Golden Hour / Desert Rose 预设,用户选定制方案)后重做配色层,保留上一版的仪表设计语言(等宽标题、刻度线 signature、琥珀主强调):
    • 组织隐喻:"控制台是纸,屏幕是暗的"——页面 chrome(背景 #F6F0E4 暖米纸、面板 #FFFCF5、侧栏 #EFE8D8)全部浅暖;固有的"屏幕"内容(相机视频框体、3D 画布、终端日志、轨迹图)保持深色,避免视频四周亮底眩光;
    • 琥珀 #FFB020 填充 + 赭石 #B87700 描边/文字(纸面上 AA 对比度),数据值深青 #0E7490,状态绿/红转暖调。
  • 仍是纯视觉层:零 JS 改动,选择器/布局属性/状态类全部不动,HTML 只更新样式表缓存版本号。

怎么验证的

  • Playwright 无头加载 /3d、/recording、/bags、/settings:零 console/页面错误,逐页截图人工复核;
  • Settings stick-figure 开关往返冒烟正常,确认无功能回归。

2026-07-15 · Telemetry Ink 深色仪表主题:全页面视觉重设计

提交:fbeed6c(main)· web_dashboard/src/styles.css、各页 HTML(仅缓存版本号)

做了什么

  • 按 Anthropic 官方 frontend-design skill 的设计流程 + theme-factory 的定制主题流程(用户从 Telemetry Ink 定制方案 / Tech Innovation / Midnight Galaxy 三个候选中选定前者),把原本"暖白工作台 + 暗色 3D"的双主题统一为一套深色仪表主题:
    • 基底深空蓝黑 #0A0F1C(刻意不用纯黑),面板 #101624/#131B2C;
    • 主强调信号琥珀 #FFB020(示波器荧光/录制语义),用于主按钮、导航激活态、焦点环、fps 读数;数据值用遥测青 #5EC8E5;live 绿/错误红语义保留;
    • 标题/导航/按钮/标签统一用系统等宽字体栈(遥测终端感),正文保持已内嵌的 Inter Variable;
    • Signature 元素:页面大标题与 3D 相机墙标题下的"示波器刻度线"条,fps 徽章改为仪表读数样式。
  • 纯视觉层改动:全部选择器、布局属性、id/class/data 钩子、JS 切换的状态类(minimized/maximized/is-stale/system-load-* 等)一律未动,零 JS 改动,HTML 只更新了样式表缓存版本号;响应式断点与 prefers-reduced-motion 支持原样保留。

为什么

用户要求参照 frontend-design 原则重做视觉并用 theme-factory 出一个适合"多路视频流 + 3D pose 可视化"监控场景的深色科技风主题。操作员在暗环境长时间盯屏,暖白页面与暗色 3D 页之间切换刺眼;统一深色也让相机画面和 3D 场景成为视觉主体。

怎么验证的

  • 两个 skill 通过 claude plugin marketplace add anthropics/skills + plugin install example-skills 安装,按 SKILL.md 流程执行(主题经用户显式确认后才应用)。
  • Playwright 无头加载 /3d、/recording、/bags、/scoring、/settings 五个页面:零 console/页面错误,逐页截图人工复核视觉统一性。
  • 交互冒烟:Settings 页 stick-figure 开关点击往返正常(状态文本双向正确),确认零 JS 改动无功能回归。

2026-07-15 · 火柴人骨架首次实测反馈修复:开关门控、圆点尺寸、左右手闪烁

提交:9b4fae5(main)· scripts/hand_overlay.py、web_dashboard/src/app.js

做了什么

  • 骨架层(手臂骨骼 + 手部骨架)改为完全受 Stick-figure 开关门控:开关关闭时 3D 场景与该功能上线前完全一致(只有 GLB 模型),不再有骨架线叠加在相机模型上。
  • 圆点直径减半:头 12cm→6cm、手 9cm→4.5cm。
  • 修复单手实测时骨架在两个腕节点间来回闪烁的问题:根因是 HandEngine 对同一只手的左右分类逐帧跳变(hand_left/hand_right 交替)。现在按 (相机, hand_index) 对原始标签做滑动窗口多数投票 + 滞回平滑:翻转需要挑战者标签占近期投票 ≥70% 且绝对票数 ≥8(绝对票数下限防止窗口未填满时被丢包造成的同标签连跑误翻转)。真实换手约 0.5 秒收敛,50/50 抖动则永远钉在现任标签上。

为什么

用户首次真手实测反馈的三个问题。其中闪烁问题的实测结果同时说明:HandEngine 的左右分类本身不可靠,之前遗留的"左右手映射待验证"问题的答案是——映射方向无法单靠 class_id 稳定确定,平滑后钉住的是"第一个建立多数的标签",方向仍可能与物理左右相反,如需精确对应还要结合其他信号(如手在头部相机画面中的位置)。

怎么验证的

  • 平滑函数单测 6/6:严格交替永不翻转、一致标签立即采纳、真实切换恰好翻转一次且不振荡、60/40 噪声保持现任。
  • 闪烁 e2e:100Hz 注入逐帧交替标签的单手数据,采样 60 帧 /ws,51 帧带骨架全部钉在同一 role、0 次切换、从未同时出现在双腕。
  • 门控 e2e 8/8:开关关→0 条骨架可见、GLB 完整;开→骨架启用、3 个圆点本地包围盒 0.06/0.045(注意测尺寸要用本地包围盒——世界 AABB 会被节点旋转撑大产生假象)。全程零页面错误。
  • 测试脚本:~/workspaces/insight_capture_tests/test_label_smoothing_20260715.py、test_flicker_e2e_20260715.py、test_rig_gating_20260715.py。

2026-07-15 · 3D 火柴人骨架:IK 手臂 + 21 点手部骨架 + 圆点显示模式

提交:0357c86、4bd2388(main)· scripts/hand_overlay.py、scripts/multi_camera_dashboard_web.py、scripts/post_processing.py、web_dashboard/src/app.js、web_dashboard/src/settings.html

做了什么

  • 3D 场景新增火柴人骨架层:每只手一条"肩→肘→腕"两段手臂骨骼(肩部锚定在头部位姿的本地坐标系随头转动,肘部由固定臂长的余弦定理双骨 IK 解算合成——是"编"出来的合理近似,不是追踪数据),以及锚定在腕部位姿上的 MediaPipe 21 点手部骨架(按手指分组着色,与 2D 图像叠加层同拓扑同配色)。
  • 手部形状数据来自 HandEngine 的 hand/hand_keypoints 检测(纯 2D 像素坐标,无深度):以腕点为原点、腕到中指根距离归一为 1 做尺度归一化,再加固定的合成 z 偏移防止手完全平面化。手的 3D 位置完全由既有的腕部 VIO 位姿提供,不做深度估计。
  • hand/hand_keypoints 加入默认录制 topic 集合,并补齐 /bagplay 影子订阅与 ros2 bag play remap 条目:回放 bag 现在能同时驱动 2D 图像手部叠加层(原本回放时直接失效)和新的 3D 骨架。回调门控从"回放时早退"改为与图像/位姿一致的 is_live 双向门控。
  • Settings 页新增全局"Stick-figure mode"开关(POST /api/settings/stick-figure,内存态):开启后 3D 场景不加载 GLB 相机模型,改用三个角色配色的大圆点(头 12cm、手 9cm)表示位姿,骨架线成为画面主体。开关状态随 pose 广播下发,翻转后下一个 tick 双向自动重建视觉,无需刷新页面。

为什么

回放分析时需要直观看到操作者的身体姿态(头、双手、手臂弯曲、手指开合),此前 3D 场景只有三个孤立的 GLB 模型,手部检测数据也只画在 2D 图像上且回放时完全不可用。圆点模式让骨架线不被相机模型遮挡,接近参考效果图的观感。

怎么验证的

  • IK/归一化数学单测 23/23 通过(骨长精确到 1e-3、越界钳制、退化姿态无 NaN、归一化原点/尺度/轴向正确),测试脚本在 ~/workspaces/insight_capture_tests/test_arm_ik_hand_norm_20260715.py。
  • 端到端:合成 HandEngine 消息注入真实后端,/ws payload 两只手均带 shoulder_position/elbow_position,右手 21 点 hand_landmarks 归一化正确、未发布的左手正确为 null;Playwright 加载 /3d 零 console 错误,arm-rig(3 顶点)/hand-rig(42 顶点、挂在腕节点下)mesh 全部启用。
  • 圆点开关 e2e 9/9 通过:关→10 个 GLB mesh 无圆点;开→3 个圆点、GLB 全部释放、手臂骨骼不受影响;关→GLB 恢复;Settings 页 checkbox 双向驱动同一端点。
  • 性能无回归:广播间隔 77.9ms(改动前 74ms),hand_parse 实测占 0.0–0.1% 单核。
  • 遗留待验证:HAND_CLASS_TO_ROLE(hand_overlay.py)中检测 class_id 与物理左右手的映射未经真实数据核实——insight9_a 的 HandEngine 实际在跑(15Hz,视野无手时发空检测),在头部相机前挥动单只手即可肉眼核对 3D 骨架挂载的腕节点是否正确,镜像则交换该映射表两值。

2026-07-15 · Dashboard 工作台视觉重构、3D 视图整合与相机流性能优化

提交:5329a22 + 328ec77(main,原单条提交 148528f 已拆分为前端重构与后端广播修复两条并 force-push 重写)· web_dashboard/src/、web_dashboard/dist/、web_dashboard/build.js、scripts/multi_camera_dashboard_web.py

做了什么

  • 重构 Dashboard 的整体信息架构:非 3D 页面统一使用侧边导航、顶部状态栏和暖白/低饱和绿色工作台配色;引入本地 Inter Variable 字体,并针对普通电脑与宽屏显示器增加响应式字号,改善跨屏幕可读性。
  • 将相机画面和 rosbag 回放入口合并到 3D Spatial View,/images 与首页改为跳转 /3d。3D 顶部删除冗余标题区,将回放、对齐校准、相机状态和轨迹控制上移,减少画布遮挡和页面跳转。
  • 3D 地面由三角形线框改为规则方格,同时按最终需求恢复 Git 原始深蓝黑场景背景及网格颜色,保留新的布局和交互结构。
  • 优化 3D 与相机流共存时的渲染开销:关闭 Babylon.js preserveDrawingBuffer,设置适度硬件缩放并把 3D 渲染限制为 30 FPS;位姿 payload 的构建与 JSON 序列化移到 executor,避免阻塞 aiohttp 事件循环和相机帧 HTTP 响应。
  • 精简 Rosbags 页面:删除顶部三个概览框和每条 bag 的完整路径,改为更清晰的序号、时长、大小、消息数、topic 数及状态布局。
  • 精简 Recording 页面:删除每个 ROS topic 下重复显示的完整 topic 名称;Recorder Log 容器高度改为适配实际日志内容,避免出现过长的黑色空白区域。
  • Scoring、Optimization、Settings 等页面同步到统一的工作台视觉系统;构建脚本增加本地字体复制,dist 已重新生成。

为什么

原界面存在三个相互关联的问题:信息分散导致实时操作需要在 Images 和 3D 页面之间切换;3D 与视频流并行时事件循环和 GPU 渲染开销使相机显示帧率明显下降;旧页面字号偏小、颜色和组件结构不统一,在电脑显示器上可读性不足。此次改动把实时监看集中到一个工作区,同时降低渲染和序列化对图像响应的影响,并统一整个产品的视觉层级。

怎么验证的

  • 在 web_dashboard/ 执行 npm run build,成功生成全部 HTML、JavaScript、CSS 和字体资源到 dist/。
  • 执行 node --check dist/static/app.js,JavaScript 语法检查通过。
  • 执行 git diff --check,未发现空白符或补丁格式错误。
  • 核对路由与资源:/images 和首页均指向 /3d,3D 页面包含 camera dock、rosbag playback、alignment、camera status/trail 控件;Inter 字体同时存在于 src/fonts/ 和 dist/static/fonts/。
  • 核对最终场景颜色与 Git 原始值一致:Babylon.js clear color 为 (0.03, 0.08, 0.11, 1.0),网格颜色为 (0.07, 0.14, 0.18)、透明度 0.55。

2026-07-14 · 合并阶段裁剪启动错位 + 丢帧统计如实化 + VIO 打分本机重新校准

提交:a15fc87(main)scripts/post_processing.py、scripts/check_bag.py · 4d07c89(main)scripts/traj_score.py

背景:接续同日早些时候的 netdev_max_backlog 修复(见下一条),用 dashboard 真实录制链路复测时,/api/integrity/run(Scoring 页实际调用的接口)仍然偶发误报 FAIL——查到是另一个独立问题:每台相机的 IMU/VIO/camera_info 走 ros2 bag record 子进程录制,子进程各自的 DDS 发现耗时不同(实测 0.1-1.8 秒),子进程晚开始的那几百毫秒里没有数据,快速模式的 loss 计算把这段"还没开始录"误判成"丢了"。

1. 合并阶段自动裁剪对齐(a15fc87,post_processing.py)

做了什么:RecordingManager._merge_worker 在 _convert_merge 合并出单个 bag 之后,新增 _trim_startup_skew:找每个"帧类"话题(check_bag.nominal_for() 有标称频率的,即 IMU/VIO/camera_info/image)自己最早一条消息的时间,取所有话题里最晚的作为对齐点,把对齐点之前的帧类话题数据整体删掉——保留下来的每个话题要么从自己真正的起点完整,要么整段不在,不会再出现"起了但缺开头几条"。同步重写 metadata.yaml 的 starting_time/duration/各话题 message_count。裁剪上限 2 秒:超过这个值不再往下裁,剩余差距保留为真实缺口并在合并日志报警——超过 2 秒说明相机真的卡顿,不该被静默吞掉。tf_static 等 latched 话题完全不参与裁剪和对齐点计算,避免丢真实静态数据、也避免早发的 latched 样本把对齐时间拉回原点。

怎么验证的:合成单元测试(不需要真相机)验证对齐、2 秒上限、已对齐时不动手三种情形,见 ~/workspaces/insight_capture_tests/test_trim_startup_skew_20260714.py;151 真机录了 60 秒真实数据,合并日志正确打出 Trimmed 1.50s...,裁剪后所有帧话题 start_lag 落在 0.000-0.038 秒(噪声级);ros2 bag info/ros2 bag play 确认改写后的 metadata.yaml 仍是标准工具可读的合法格式。

2. check_bag.py 精度调整(a15fc87)

  • DEFAULT_WARMUP_S:2.0 → 0。裁剪已经从源头解决了启动错位问题,不再需要这个"一刀切扣除固定额度"的宽容——它之前既能遮住启动抖动,也同样能遮住录制中段的真实丢帧(例如 IMU 均值 395-398Hz 时报出 0% loss 就是这个宽容额度导致的)。
  • vio_100hz 标称频率:100.0 → 99.0。排查另一次误报时发现:完全裸的 ros2 topic hz(零录制代码介入)测三台相机的 vio_100hz,稳定在 99.19-99.31Hz(标准差仅 ~0.0025s,不是偶发丢包,是 VIO 解算本身的周期就做不到严格 10ms)。标称值定 100 会把一个健康话题误判成丢帧。

怎么验证的:用 Playwright 直接操作 151 上的真实网页(不是模拟 curl):打开 Recording 页 → 填包名 → Start → 等待 → Stop → 等合并完成 → 跳转 Scoring 页 → 选包 → 点 Scoring 按钮 → 等结果。两轮真实验证(修复前后对比 + 换新数据线之后的完整复测,含相机重连)均显示 Scoring 页绿色 "Complete — no frame loss above 0.5% on any topic"、Bags 页徽章 complete、浏览器控制台无报错。第二轮复测顺带确认 VIO 打分功能本身也正常出结果(/api/scoring/run 异步任务)。

3. VIO 打分基准从本机(192.168.19.222)历史数据重新校准(4d07c89,traj_score.py)

做了什么:原基准是 2026-07-10 的 fleet 历史校准(33 组 bag×相机样本,中位数 median_L=-3.743 映射到约 75 分)。改为只用本机自己的录制历史:本机 rosbags/ 下全部历史录制,排除 <5 秒的冒烟测试片段后剩 53 组样本(每段录制 × 每台相机的 vio_image_cov),取 L=(log10(p50)+log10(p90))/2 的中位数 -3.9195(trace ≈ 1.2e-4),把这个"本机相对 fair"的水平映射到 80 分(而不是原来对应的 ~75)。sigma(分布展开度)保留原值 0.412 不变——53 个样本重新估计展开度样本量偏少,只挪锚点已经满足这次的诉求。

怎么验证的:数值验证 base_score(median_L) == 80.00;用本机和 151 上各一段真实 bag 分别验证打分结果符合新公式换算(如 insight3_b 某段从旧基准下的 87.6 变为新基准下的 84.9,与本机数据更贴近的尺度一致)。

2026-07-14 · IMU 零散丢帧根因修正:不是存储层,是主机网卡 NAPI backlog 队列溢出

提交:953cd94(main) · scripts/host_setup.sh、docs/USAGE.md;151 上手动同步部署机副本并持久化 sysctl

起因:check_bag.py 默认快速模式(metadata 计数,见 28d9370)对同一份录制报 400Hz IMU 0% loss,但 avg_hz 显示只有 395-398Hz——快速模式的 warmup 宽限(默认 2 秒 × 标称频率)把这部分真实丢帧悄悄吸收掉了;--deep(逐消息时间戳分析)在同一份 bag 上如实报出 0.47%-0.94% 丢帧,缺口分散在整个录制过程中,不集中在开头。

排查过程(按尝试顺序,全部在 151 上实测):

  1. 先怀疑录制架构本身:把 IMU/VIO 从 ros2 bag record 子进程迁移成进程内常驻订阅(意图消除子进程冷启动的 DDS 发现延迟)。隔离单元测试完美(0% loss,tf_static latched 缓存正确重放),但接入真实相机后严重退化:图像流从 20Hz 掉到 10-11Hz,IMU 丢帧不降反升到 59.7%+。定位到 400Hz 原始消息在与 dashboard 主进程共享的 GIL 上产生了争用——ros2 bag record 子进程方案的真正价值不只是"避免同一话题挂两个订阅者"(inprocess_bag_writer.py 里写的原因),还有"独立进程=独立 GIL",这一点在 400Hz 才会显现,在图像/VIO 的 20-100Hz 不会。已完整回滚,不建议再尝试。
  2. 转向存储层:依次测试订阅端 QoS 历史深度(10→200)、WAL checkpoint 阈值(默认→10 倍)、彻底不用 WAL 换 journal_mode=MEMORY——四种配置丢帧率都在 0.6%-0.9% 的同一噪声带里,无一有效。
  3. 决定性排除实验:完全不录制,只挂一个裸的 ros2 topic hz 订阅,同样测得 ~397Hz——证明丢帧发生在录制代码接触消息之前,跟存储层、录制架构完全无关。
  4. 转向主机网络层:ip -s link show <相机对应网口> 发现三个接口本身有累计丢包(1.44%/0.37%/0.55%);cat /proc/interrupts | grep xhci 显示三台相机的 USB-以太网中断全部落在同一个 CPU 核上;/proc/net/softnet_stat 确认该核的 NAPI backlog 队列(默认 1000 条)在丢包,且 time_squeeze 恒为 0(不是 CPU 处理不过来,纯粹是队列太浅,突发流量瞬间填满)。

修复:net.core.netdev_max_backlog 从默认 1000 调到 8192,写入 /etc/sysctl.d/99-dds-rx-buffers.conf(与已有的 rmem/wmem 调优同一个文件,scripts/host_setup.sh 负责写入+持久化)。

怎么验证的:151 上前后各测一轮:

状态 IMU 丢帧(3 台相机) 备注
默认 backlog=1000(基线) 0.47% / 0.51% / 0.59% check_bag --deep --warmup 0
各种存储层改动(4 种配置) 0.6%-0.94%(均无改善) QoS 深度、WAL checkpoint、去 WAL
backlog=8192(修复后,纯 IMU 子进程测试) 0.0% / 0.0% / 0.0% 400.0Hz,0 missing,接口 dropped 计数在录制窗口内完全没有增长
backlog=8192(修复后,完整 100 秒生产录制,15 话题) 0.0%(IMU),VIO_100hz 0.01-0.04% 通过真实 /api/recording/start,全部话题 OK

注意:scripts/host_setup.sh 不随镜像升级自动同步,151 上是手动拷贝部署目录副本 + 直接写 /etc/sysctl.d/99-dds-rx-buffers.conf 持久化的(脚本自身的幂等检查只看当前生效的 sysctl 值,如果之前已经用 sysctl -w 临时改过,重跑脚本会误判"已配置"而跳过写文件——这次就踩了一次,最后是手动确认文件内容补上的)。build_release.sh 打的正式发布包目前还没带上这个改动,下次正式发布前需要重新打包验证。其他设备(249、lite/lite-779 profile)如果也表现出"高频小消息话题分散丢帧、image 正常"的模式,应独立评估应用同样的修复。

2026-07-13 · v1.0.6:完整性检查秒级返回(metadata 计数模式)+ run_dashboard 等全部相机上线;发布并升级 151

提交:28d9370 224e917 d083a4b b107956(main,发布为 v1.0.6) · scripts/check_bag.py、scripts/run_dashboard.sh、scripts/multi_camera_dashboard_web.py、web_dashboard/src+dist/app.js、docs/USAGE.md、.dockerignore

1. check_bag / Scoring 页完整性检查:默认秒出(28d9370)

做了什么:默认模式不再逐条扫 sqlite 全表,直接读 metadata.yaml 的每话题消息计数和录制时长,算平均帧率和估算丢帧率(预期 = 时长 × 标称频率,减去起始 warmup 宽限)。原来的逐消息时间戳分析保留为 --deep(能区分"相机没发出"和"到达但突发",输出每次断流的时刻),没有可读 metadata.yaml 的 bag(如断电孤儿恢复前)自动回落深扫。深扫本身也提速了:SQL 用 substr(data,1,12) 只取 12 字节时间戳而不搬整个图像 blob(3GB bag 冷读 11.6s → 8.2s)。报告新增 method 字段(metadata_counts / deep_scan)。

为什么:旧实现每录制 1GB 约需 10 秒,随录制时长线性增长——客户录完长片段后点"Verify Integrity"要等很久,而完整性徽章只需要每话题计数级别的丢帧率,用不着逐消息扫描。

怎么验证的:开发机 60 秒三相机 bag:CLI 快速模式 0.12s vs --deep 4.3s,两种模式结论一致(全部话题 OK);后端 /api/integrity/run 实测 87ms 返回 method=metadata_counts、15 话题全通过。升级后在 151 上对真实 bag(insight_record_20260713_212525)容器内实测 0.105s 全部 OK。

2. run_dashboard 启动等待:等全部相机 + 输出缺谁(224e917 b107956)

做了什么:启动时的"等相机出数据"从"任意一路有数据就放行"改为等 config/cameras.json 里全部相机 live;超时且相机链路存在时自动重启后端重建 DDS 参与者(最多 INSIGHT_ALL_LIVE_MAX_RESTARTS=3 次)再等。等待期间每 5 秒打印仍然 stale 的相机名,最终放弃的警告里给出排查提示(相机 HTTP 活着但接口零流量 → 重启相机)。

为什么:insight3_b 静默事件的教训——Fast DDS 只在参与者创建时枚举网卡,某路相机 USB 链路晚于容器起来时那路永远收不到,旧逻辑只等到"任一相机"就放行,部分掉线被静默容忍。另外当天实际使用中用户对着 "Waiting for all cameras..." 干等 30 秒不知道缺哪路、只能盲目 Ctrl-C——现在每 5 秒报 stale 名单。

怎么验证的:bash -n 语法检查;本机实战一次:启动卡在等待时按新逻辑排查,发现 insight9_a HTTP 正常但接口 0 KB/s(相机侧推流僵死,与 insight3_b 事件同模式),curl -X POST http://169.254.30.1/api/reboot 后 30 秒恢复 9.2MB/s、三路全 live——正是脚本新提示指向的处置路径。

3. 镜像内容修正(d083a4b)

.dockerignore 补排除 CLAUDE.md(未跟踪的开发须知,此前会被 COPY . 烤进客户镜像)和 config/alignment/live_alignment_state.json(本机运行时对齐解,部署机挂载自己的 config/ 卷)。

4. 发布与部署

build_release.sh v1.0.6(jetson-nx profile 校验通过)→ scp 镜像 tar(1.43GB,约 6.5 分钟)到 151 → update.sh 升级:22:06:42 起容器、22:06:48 healthz 通过,v1.0.5 保留可回滚。host 侧 run_dashboard.sh 手动同步(host 脚本不随镜像升级走,md5 与 main 最新版比对一致)。本次无 config 变更,无需像 v1.0.5 那样手改部署机 config。

2026-07-13 · insight9_a 零散丢帧根因修正与彻底修复:不是 CycloneDDS,是 best_effort 单包丢弃整帧;发布 v1.0.5 并部署 249

提交:1c983e8(main,发布为 v1.0.5) · scripts/multi_camera_dashboard_web.py、config/devices/jetson-nx/cameras.json

备注:这次修复恰逢仓库从"每设备一个分支"迁移到"单 main 分支 + config/devices/ profile"(c89e2a6)。修复最初在本机旧 deploy/jetson-nx 工作目录上验证(本地提交 aeb9ec1),随后在 main 的临时 worktree 里按新结构重新提交为 1c983e8 推送;v1.0.5 镜像从 main(jetson-nx profile 选中态)打包。

先纠正一个错误:上一条 changelog(同日更早)把这个问题的后续方向写成"调大 CycloneDDS 的 Internal/DefragUnreliableMaxSamples"——这个项目实际跑的是 Fast DDS(rmw_fastrtps_cpp,ros2 doctor 与已装包核实,容器里根本没装 CycloneDDS),那条建议无法生效,特此更正。

重新排查:本机(开发机)当时恰好三路相机都接着,直接复现,不用等 151。沿用同一套"三观察者对比法"(录制 + 两个独立裸订阅者,比对缺帧集合):

  1. 先测的假设(怀疑 depth=5 的订阅在 MultiThreadedExecutor 下被其他高频话题挤到执行延迟、导致 KEEP_LAST 历史被覆盖):把这个订阅的 depth 从 5 调到 20,两轮实测前后"录制单独漏、两个裸订阅者都收到"的帧数几乎不变(7、7、5,量级没有下降)——这个方向被数据推翻,不是真正原因(保留了这个改动作为免费的防御性余量,但没指望它解决问题)。
  2. InProcessBagWriter 的有界队列(maxsize=128,满了会打日志)在所有测试轮次里从没打印过一次 "queue full" 警告——排除应用层队列溢出。
  3. 翻出 config/cameras.json 里 image_qos_reliability: "best_effort"——这才是真正原因:BEST_EFFORT 下任何一个 UDP 分片丢了,Fast DDS 不会重传,整帧图像就永久丢失,且内核这层完全干净(不是缓冲区溢出,就是真实的、少量的、单包级别的丢包,符合"零散"、"内核 0 错包"的全部症状)。把它改成 "reliable",连续两轮 35 秒录制丢帧从 3.51%/3.84%/0.94% 直接归零(0/1267、0/1283),同一时间裸订阅者(仍是 best_effort)继续各自独立丢帧,形成清晰对照。

风险排查:inprocess_bag_writer.py 文档过"同一话题挂两个 RELIABLE 订阅者会触发发布端背压卡死"——本项目录制走 in-process 订阅复用(display 订阅一份,不额外开 ros2 bag record 的第二个订阅),且对齐逻辑已有"话题相同就复用回调、不重复建订阅"的保护,所以切换成 RELIABLE 不会撞到这个已知坑,实测也确认 insight3_a/b 的原始图像流同样 0.0% 丢帧、CPU 占用无变化。

验证数据(本机开发环境,三路相机实测,35-42 秒窗口 ×2 轮):

状态 insight9_a 压缩图丢帧 录制独漏(两裸订阅者都收到)
best_effort(基线) 3.51% / 3.84% 7 / 7
best_effort + depth 5→20 3.84% / 0.94% 7 / 5
reliable(最终修复) 0.0% / 0.0% 0 / 0

注意:这次改的是 config/devices/jetson-nx/cameras.json(jetson-nx profile),只影响 jetson-nx 机群;如果 lite/lite-779 profile 的设备也观察到类似的零散丢帧,需要单独评估是否在各自 profile 里做同样修复(相机/网络条件可能不同)。

附:v1.0.5 升级 151 并实测 QoS 修复生效

151 从 v1.0.4 升级到 v1.0.5(v1.0.4 保留可回滚)。关键操作:151 是已装机设备,config/ 首装播种后不随镜像升级——镜像里烤好的 reliable 不会自动生效,必须手动把宿主机 /home/nvidia/insight-dashboard-deploy/config/cameras.json 的 image_qos_reliability 从 best_effort 改成 reliable(升级前已改,容器重建直接带上)。给所有存量设备升级 v1.0.5 时都要记得这一步。

验证(相机机群此时已移到 151,三路 20/20/30fps):连录两包 35s,insight9_a 压缩图两包均 0.0% 丢帧(升级前该机上为 0~2.4% 零散丢帧);第一包 insight9_a IMU 有 0.53% 瞬态小缺口(刚重启后未稳定),第二包全部话题 OK。

附:v1.0.5 部署到全新设备 192.168.19.249

第二台全新 Orin NX 首装,走部署与发布手册的使用者路径,全程远程完成:

  • 前置:设备出厂无 docker——装 docker.io + docker-compose-v2 + nvidia-container-toolkit,nvidia-ctk runtime configure 注册 runtime;发现出厂在 15W 低功耗档(nproc=4),nvpmodel -m 0 切 MAXN(注意:该命令要求交互确认 YES 且立即触发重启,SSH 掉线属预期);重启后 nproc=6,docker run --runtime=nvidia --gpus all ubuntu nvidia-smi 验证 GPU 直通成功。
  • 安装:scp 部署包 + v1.0.5 镜像(无线网约 3.8MB/s,1.4G 约 6 分钟),./update.sh 首装(自动播种 config/),再跑 ./scripts/host_setup.sh(UDP 缓冲 sysctl + 开机相机重启 unit,全部成功)。
  • 验证:healthz OK,7 个页面 Playwright 无头加载 6 个全净;/3d 页在这台设备的无头环境报 "WebGL not supported"(同一测试在开发机上干净,怀疑是该机 headless 软件渲染环境差异,未证实影响真实浏览器——待接显示器或用 kiosk Firefox 实机复核)。设备尚未接相机,录制/图像链路的功能验证等相机到位后补做。

2026-07-13 · v1.0.4:新设备录制掉帧根因(相机漏重启 + NTP 跳表)修复;Scoring 单按钮;设置页锁定模型

提交:f8630d2 + 1e93986(deploy/jetson-nx,发布为 v1.0.4) · scripts/reboot_cameras.sh、scripts/systemd/insight-camera-reboot.service、web_dashboard/

1. 新设备(192.168.19.151)录制掉帧根因排查与修复

现象:新设备跑 v1.0.3,今早三段录制里 insight3_b 掉帧 48-56%,且是所有话题(图像/IMU/camera_info)同步出现 9-12 秒整段空洞,最后一段里 IMU/VIO 干脆 0 条。

排查:不是 UDP 缓冲问题(sysctl 已配好 64MB)、不是磁盘(NVMe)、不是链路(0 错包、ping 0% 丢失)。关键证据:insight3_b 网卡上仍有 ~10.7 MB/s 数据进来、HTTP API 正常,但容器内 ros2 topic hz 收到 0 条——相机在往陈旧的 DDS locator 单播,新订阅永远匹配不上(开机相机重启服务本该修掉的问题)。

根因(journalctl -u insight-camera-reboot.service -b 两条线索):

  1. 发现循环见到 ≥1 台相机就退出,不校验应有数量。开机时 USB 链路逐个枚举,insight3_b 的链路起得晚,从头到尾没被重启,带着陈旧 DDS participant 一路烂到彻底断流。
  2. 超时全部用 date +%s 墙钟。Jetson 无 RTC 电池,开机时钟停在上次关机时间,NTP 同步后瞬间前跳约 20 小时(日志里 13:21 直接跳到次日 09:42),把所有发现/等待窗口瞬间打穿,服务误报 FAILURE。

修复:发现循环改为等齐 config/cameras.json 里的相机数(等不齐则大声告警后继续处理已见到的);全部 deadline 改用 bash SECONDS(脚本启动起单调计时,不受跳表影响);unit 发现窗口 120s→180s(现在要等最慢的链路而非第一条)。注意:reboot_cameras.sh 属宿主机脚本,不随镜像升级走,已手动同步到 151 的部署目录并 daemon-reload;其他已部署设备下次首装/重装才会带上,或手动同步。

验证:

  • 应急处置当场生效:curl -X POST http://169.254.20.1/api/reboot 重启 insight3_b 后录 35 秒测试包,check_bag 全部 16 话题 OK(此前 FAIL 5 个话题),最大丢帧 0.18%(此前 48-56%)。
  • 新脚本在 151 真机实测:发现阶段秒级找齐 3/3 台(expecting 3 per config/cameras.json),无相机的开发机上正确走 ERROR 退出路径。
  • 相机 IP 速查:169.254.10.1=insight3_a、20.1=insight3_b、30.1=insight9_a(curl http://<ip>/api/camera-config 可确认身份)。

1.5 残余问题:insight9_a 彩色图间歇性 ~1% 零散丢帧的归因(未修,已定位到层)

2026-07-13 更正:本节"接收进程用户态 CycloneDDS 分片重组"的归因和"调大 CYCLONEDDS_URI"的建议都是错的——这个项目实际跑 Fast DDS,容器里没装 CycloneDDS。真实根因和已验证的修复见本页顶部同日新条目。以下原文保留供排查过程参考,结论以顶部新条目为准。

修完 insight3_b 后,151 上多次 35s 录制显示 insight9_a 压缩图偶发 0~2.4% 的零散单帧丢失(67-100ms 间隙,与 3_b 的整段空洞完全不同模式)。用"三观察者对比法"定位丢在哪一层:录制的同时跑两个独立的裸订阅者(只记时间戳),三方比对缺帧集合,并采集内核计数器。

实测结论(35s 窗口,union 1049 帧):

  • 相机没发的帧只有 2 帧(0.19%)——三个观察者谁都没见过的帧几乎不存在,排除"相机采集/发布就少了"。
  • recorder 漏的 21 帧,两个裸订阅者全都收到了——帧确实到了这台主机,丢的是 recorder 自己那份拷贝(DDS unicast 下每个订阅者一份独立拷贝)。
  • 内核路径干净:网卡 0 错包、UDP RcvbufErrors=0、IP 分片重组失败仅 3 个——到达主机的数据几乎全部完好进入 socket。
  • 不对称性:忙着写盘的 recorder 丢 21 帧,空转的裸订阅者只丢 5/2 帧——如果是相机发送端随机丢拷贝,三者应该丢得差不多。指向接收进程用户态(CycloneDDS 大消息分片重组在接收线程被写盘耽搁时超出 defrag 限额丢样本)为主,相机端多拷贝扇出发送压力为辅(测试中多挂 2 个订阅者,丢帧率从 ~1% 升到 2.37%;恢复 2 个订阅者后最后一包 0.0%)。
  • 后续若要压这个残余丢帧,方向是 CYCLONEDDS_URI 调大 Internal/DefragUnreliableMaxSamples(recorder 容器内),而不是继续调内核缓冲(内核侧已证明无丢)。

2. Scoring 页合并为单按钮

"Run Scoring" 和 "Verify Integrity" 两个按钮合并为一个 Scoring 按钮:点击后先跑完整性校验(几秒,报告保留显示),无论结果如何接着启动打分任务。验证:容器内 Playwright 无头点击实测——API 调用顺序 integrity/run → scoring/run,完整性表格先渲染,打分到达终态后按钮恢复可用,无 console error。

3. 设置页暂时锁定 Avatar 模型更换

下拉框禁用并显示 "(locked)" 提示,后端 API 保持可用;恢复入口:app.js 里 AVATAR_MODEL_SWITCHING_LOCKED 改为 false 后重新构建 dist。验证:Playwright 确认 3 个相机卡片的模型下拉框全部 disabled。

4. v1.0.4 发布并升级 151

build_release.sh v1.0.4 打包(镜像 1.4G),scp 到 151 后 ./update.sh insight-dashboard-v1.0.4.tar.gz 升级,v1.0.3 保留可回滚。升级后在 151 实测:healthz 通过;容器内 Playwright 跑同一套 UI 检查全部 PASS;dashboard 三路相机接收 20/20/30 fps 满额;末次 35s 录制 check_bag RESULT: OK(全部话题 ≤0.43%,insight9_a 0.0%)。宿主机侧 reboot_cameras.sh + systemd unit(不随镜像走)已手动同步并 daemon-reload,新发现逻辑真机验证:秒级找齐 3/3 台相机。


2026-07-12 · bag 回放只有 4-8fps:发送端 UDP 缓冲区没调 + 看门狗误触发,两项一并修复

提交:b0106d5(deploy/jetson-nx) · scripts/host_setup.sh、scripts/multi_camera_dashboard_web.py

背景:在开发机验证 WebRTC 时发现(相机机群已移到第二台设备,开发机用 bag 回放测试):实时流 20/30fps 正常,回放却只有 4-8fps。

问题 1 —— 回放慢的根因:逐层测量,ros2 topic hz 显示 bagplay 话题发布端就只有 4.6/7.3Hz(bag 里录的是足额 20/30Hz),与 kiosk/WebRTC 负载无关(关掉 kiosk 数字不变),ros2 bag play 进程 CPU 也不高。真正原因:07-07 修录制丢帧时只调了接收端 rmem(相机→dashboard 方向),发送端 wmem 还是内核默认 208KB——而回放的发布方是本机的 ros2 bag play,510KB best-effort 大样本突发直接在发送 socket 上被静默丢弃。实时流从来不受影响,因为实时帧的发送方是相机自己的内核,不经过本机的 wmem——这个不对称正是问题藏了这么久的原因。sysctl -w net.core.wmem_max/default=64MB 后当场从 4.6/7.3Hz 跳到精准 20.0/30.0Hz。已写入 host_setup.sh 的 sysctl 文件(幂等检查条件同步加上 wmem,否则已部署设备重跑脚本会误判"已配置"跳过),两台设备均已持久化落地。

问题 2 —— 看门狗误触发:今天上午加的相机掉线看门狗,在"没接相机的机器上放 bag"场景下误触发——回放帧填充了 camera_frame_times,bag 播完帧一停,15 秒后看门狗把整个后端重启(实测两次,还顺带杀了 kiosk)。修复:掉线重启新增前置条件"相机 USB 链路(169.254)物理存在"——没有链路,"帧停了"就不是可恢复的链路掉落,重启无意义。修复后连续两轮"播完→帧停"零重启。

验证:开发机 kiosk 回放 31 秒 bag,屏幕徽章 19.8 / 19.1 / 28.3 fps(修复前 4-5.5/8),全链路(bag 读取→DDS→dashboard→NVENC→WebRTC→Firefox)满帧。开发机的 WebRTC 也确认打通(镜像重建烤入 kiosk pref + mDNS 解析,ICE 秒连)。

注意:第二台设备当前跑的 v1.0.3 镜像还没带看门狗的链路门控修复(它有相机在接,该 bug 触发不了,不紧急),随下个版本自然带上;它的 wmem sysctl 已经通过重跑 host_setup.sh 落地,不依赖镜像版本。


2026-07-12 · WebRTC 其实从未连通过:"15fps 问题"根因是浏览器 mDNS ICE 候选无法解析

提交:d52c090 + c680ddf(deploy/jetson-nx,发布为 v1.0.3) · scripts/webrtc_stream.py、scripts/firefox-kiosk-user.js

问题:"用了 WebRTC 但帧率只有 15fps 而不是 20fps",且不开 kiosk、用普通浏览器看时更低。

排查过程(全程在第二台设备实测):

  1. 后端 /api/cameras 显示 ROS 接收侧满额(19.9/19.9/30fps)→ 排除接收端。
  2. 给 GStreamer 管线加三点探针(appsrc 入/出、编码器出)+ ICE 状态日志:ICE 永远卡在 checking,从未 connected;没连上时 webrtcbin 不消费,appsrc 疯狂漏帧(push 20/s、enc-out 2/s)。前端 8 秒首帧超时把会话拆掉重试,页面从头到尾都在 JPEG 轮询回退路径上——14.7fps 正是 50ms 元数据轮询 + 每帧 HTTP GET 的路径极限。也就是说 15fps 从来不是"WebRTC 慢",而是"WebRTC 根本没通"。(07-10 那条"kiosk 换 Firefox 后 WebRTC 实测可用"的验证结论有误——截图上轮询和 WebRTC 渲染效果无法区分,当时没有核对 fps 徽章/ICE 状态。)
  3. 把对端 ICE 候选打出来:全部是 <uuid>.local mDNS 混淆地址(Chrome/Firefox 的隐私默认行为),容器里没有 avahi/nss-mdns,libnice 一个都解析不了 → 所有候选对不可用。

修复(两部分):

  • webrtc_stream.py:后端自己解析 mDNS 候选再交给 webrtcbin——裸 socket 以组播组成员身份绑 5353(SO_REUSEADDR 与宿主 avahi/浏览器 responder 共存,host 网络模式下同一命名空间)发标准组播 A 查询,零新增依赖。QU 单播查询实测 Firefox 不回,必须走组播组接收。第一版在 aiohttp 事件循环里同步解析,一个慢观看端的 2 秒超时能把整个后端信令拖死(v1.0.2 实测翻车:kiosk 也被饿回轮询),c680ddf 改为后台线程解析后迟到补交候选(ICE trickle 允许),0.7s×3 次短重试代替单次 2s 长等待。
  • firefox-kiosk-user.js:kiosk Firefox 直接关掉候选混淆(media.peerconnection.ice.obfuscate_host_addresses=false)——它看的是自己本机的后端,该隐私措施无意义,关掉后即使组播被过滤 kiosk 也稳连。

验证(v1.0.3 干净构建,升级到第二台设备后):

  • kiosk 本机观看:14.7/14.6/14.6 → 20.0 / 20.0 / 30.0 fps(原生满帧),稳态 60 秒零会话重建。
  • 开发机 Firefox 经真实 WiFi 远程观看(默认 mDNS 混淆,走后端解析路径):→ 19.9 / 19.4 / 29.9 fps,mDNS 毫秒级解析。
  • 双观看端同时在线不互相拖累(v1.0.2 的事件循环阻塞问题专门用这个场景复测)。
  • 编码器全链路探针确认 NVENC 从来不是瓶颈(三路同编 20/20/30 无损)。

遗留小瑕疵(记录不阻塞):kiosk Firefox 被 kill/容器重建后重启,页面顶部会出现"Open previous tabs?"会话恢复提示条,不影响显示,点掉即可;后续可加 pref 压掉。


2026-07-12 · 部署包改为解压到不带版本号的常驻目录 + 第二台设备全功能实测通过

提交:e75eee2(deploy/jetson-nx) · scripts/build_release.sh、docs/DEPLOYMENT.md、deploy/README.md

问题:部署包解压出来的目录名带版本号(insight-dashboard-deploy-v1.0.0),但这个目录并不随升级更换——它是设备的常驻安装根目录,.env、config/、rosbags/、outputs/ 都在里面越积越多。常驻目录用版本化名字,意味着每台新装设备落在不同路径(systemd unit 的 ExecStart、文档、肌肉记忆全都跟着错位),还会误导人以为目录名反映当前运行版本(其实 .env 才是)。

修复:build_release.sh 打包的顶层目录改为固定的 insight-dashboard-deploy/,版本号只保留在 tar 文件名和镜像 tag 上;deploy/README.md 首装说明补上之前漏掉的 host_setup.sh 一步。已在第二台设备上把在用安装目录实际改名并重跑 host_setup.sh(systemd unit 路径自动改写跟随),确认无缝。

第二台设备(192.168.19.151)全功能实测记录(相机机群已物理迁移到该设备,三路 USB 链路 169.254.10/20/30 全部在线):

  • run_dashboard.sh --jetson --logs 拉起 kiosk:Firefox 以非 root kiosk 用户运行,无沙箱警告条,3D 页 WebGL 正常渲染,三路位姿全部跟踪(截图确认,升级重建容器后 kiosk 需要重新拉起——docker exec 的进程随容器消亡,这是预期行为)。
  • 全部 7 个页面(3d/images/recording/bags/scoring/optimization/settings)Playwright 无头加载零 console 错误(/images 因持续拉流不能用 networkidle 等待条件,属正常)。
  • /images 页三路 WebRTC <video> 元素全部激活(各 405px 宽正常出流)。
  • 真实录制 12 秒(16 个默认话题含 vio_image_cov):合并完成,check_bag.py 全部话题丢失率 <0.5%(图像流足额 20/30Hz,IMU 400Hz 零丢失)——宿主机 sysctl 调优生效的直接证据。
  • 改名后目录内再次完整走 --rollback v1.0.0 → 升级回 v1.0.1:均健康,数据目录不受影响。升级/回滚机制确认可用。
  • 后端 /api/cameras 实测帧率:insight3_a/b ≈ 19.9fps(原生 20)、insight9_a = 30fps——编码前的接收侧满额,为 WebRTC 15fps 问题的排查划定了范围(瓶颈在编码/网络/浏览器一侧,不在 ROS 接收)。
  • 注意事项:切到 MAXN_SUPER 满血模式后设备温度 ~75°C(风扇工作中,距 ~95°C 节流线尚有余量),桌面会弹 NVIDIA 标配的"表面高温勿触摸"提示,属正常现象;封闭环境长时间高负载运行时注意散热。
  • 已知隐患(未处理,记录备查):两台设备同在一个 WiFi 局域网且同用 ROS_DOMAIN_ID=20,两个 dashboard 的 DDS participant 互相可见(ros2 node list 出现两个同名节点)。相机数据本身走点对点 USB 链路不会串,但 bag 回放(/bagplay/...)会跨设备串流——在一台上回放,另一台的影子订阅也会收到。后续要么给设备分配不同 DOMAIN_ID(需相机侧配合),要么给 FastDDS 配接口白名单。

2026-07-12 · 第二台 jetson-nx 实机部署:把使用者路径走通、补上一路踩到的坑

提交:6b374ef(deploy/jetson-nx) · scripts/build_release.sh(新增,之前一直没提交)、scripts/host_setup.sh(新增)、scripts/setup_host.sh、scripts/systemd/insight-camera-reboot.service、deploy/docker-compose.yml、deploy/update.sh(新增,之前一直没提交)、deploy/README.md(新增,之前一直没提交)、docs/DEPLOYMENT.md、.gitignore

背景:第一次真正端到端走一遍"使用者路径"(拿部署包 + 镜像 tar,机器上没有源码)部署一台全新 jetson-nx(192.168.19.151),之前这条路径只是照着文档走过纸面流程,没有真机验证过。

发现的问题:

  1. scripts/build_release.sh、deploy/update.sh、deploy/README.md 这三个部署工作流核心文件居然从来没有提交过,一直是 untracked 状态。
  2. 部署包完全没打包 scripts/host_setup.sh(原来叫这部分逻辑内嵌在 setup_host.sh 里)、reboot_cameras.sh、systemd unit、looper_cli/——使用者路径完全没有办法应用宿主机层的两项调优(sysctl DDS 接收缓冲区、开机自动重启相机),这两项此前只有开发者路径的 setup_host.sh 能做。
  3. deploy/docker-compose.yml 写死 cpus: '5.8',假设 Orin NX 满 6 核在线——这台新机器当时是 nvpmodel 15W 功耗档,只开了 4/6 核,容器直接创建失败("range of CPUs is from 0.01 to 4.00"),不是跑得慢而是根本起不来。
  4. docs/DEPLOYMENT.md 里 docker compose v2 插件的包名写错了(写的是 docker-compose-plugin,这个包名在 Ubuntu jammy 源里不存在,实际包名是 docker-compose-v2),而且 nvidia-container-toolkit 装完之后还漏了一步 nvidia-ctk runtime configure --runtime=docker,不做这步 docker 不会真正注册上 nvidia runtime。

修复:

  • 把三个从未提交的核心部署文件补进仓库。
  • 把 sysctl + systemd unit 这两步从 setup_host.sh 抽成独立的 scripts/host_setup.sh,两条路径共用;build_release.sh 现在把它连同 reboot_cameras.sh/systemd unit/looper_cli/ 一起打进部署包。systemd unit 的 WorkingDirectory/ExecStart 改成安装时用 sed 动态改写成实际路径,不再硬编码 /home/nvidia/insight-capture-dashboard(开发者路径克隆目录名、部署包解压目录名本来就不一样)。
  • host_setup.sh 新增 CPU 满核检测,检测到功耗模式导致核数不足会提示怎么用 nvpmodel -m 0 切满血模式。
  • docs/DEPLOYMENT.md 前置条件命令改成 docker-compose-v2,补上 nvidia-ctk runtime configure 步骤和一条 nvidia-smi 透传验证命令。
  • 补充说明:deploy/jetson-nx 分支的机群配置(insight3_a/insight3_b/insight9_a + Jetson Orin NX)从现在起是标准化配置,不是单台设备的私有配置——新设备只需要重新做 board_calibration.json 标定,cameras.json 不用改。
  • .gitignore 补上 release/、rosbags/、outputs/、runs/、data/、对齐运行时状态文件——这几个目录之前完全没被 ignore,只是没人手滑 git add -A。

怎么验证的:全程实机操作这台新设备——JetPack 6.2.1 确认、装 docker.io + nvidia-container-toolkit + nvidia-ctk runtime configure、nvidia-smi 透传通过、nvpmodel -m 0 切到满核(原来 15W 只有 4 核)、打包 v1.0.0 传过去、update.sh 首次安装成功、/healthz 正常、手动应用 sysctl + systemd unit 并验证生效。


2026-07-12 · kiosk Firefox 改成非 root 用户,去掉"沙箱已禁用"警告条

提交:a8627be(deploy/jetson-nx) · Dockerfile、scripts/open_web_3d_right.sh、scripts/run_dashboard.sh

问题:设备屏幕的 kiosk Firefox 顶部一直有一行"security sandbox is disabled, unsupported and less secure"。这是 Firefox 内置的、无法通过任何 pref/policy 关闭的警告,专门针对"以 root 身份运行"这一情况(Mozilla 认为给 root 加内容沙箱没有意义——root 本来就能逃逸任何非特权沙箱——所以故意做成不可隐藏来劝退这种用法)。之前 kiosk 容器里 Firefox 以 root 跑,靠 MOZ_DISABLE_CONTENT_SANDBOX=1 绕开 Firefox 对 root 的拒绝,这行警告正是这个绕过动作的副作用。

修复:Dockerfile 新增非特权用户 kiosk(uid 1000,跟宿主机桌面用户 nvidia 的 uid 保持一致——这套 docker 没有配置 userns-remap,容器里的 uid 1000 在内核层面就是宿主机的 uid 1000),把 Firefox profile 目录 chown 给它。open_web_3d_right.sh 改成 su 进这个用户再起 Firefox,去掉 MOZ_DISABLE_CONTENT_SANDBOX。run_dashboard.sh 在原有给 root 的 xhost +SI:localuser:root 旁边,加一条给这个 uid 的同等 X11 授权。

验证:镜像重新 build、容器重建,确认 kiosk 里的 Firefox 进程确实以 kiosk 用户在跑(不是 root),在宿主机真实的 :0 显示器上截图确认——警告条消失,Babylon.js 3D 场景正常渲染,kiosk 日志里没有新增报错(CanCreateUserNamespace() clone() failure: EPERM 这条本来就有,是容器 seccomp 挡了嵌套 user namespace,跟这条警告是两回事,不影响功能)。


2026-07-12 · 相机掉线看门狗:从"只管开机"扩到"运行中也管"

提交:ef251df(deploy/jetson-nx) · scripts/multi_camera_dashboard_web.py

问题:dashboard 起来之后,把某路相机的 USB 拔掉再插回去,画面不会自动恢复。原有的 _stale_participant_watchdog_loop 只处理"容器开机比相机链路先起来,participant 从没收到过任何数据"这一种情况,而且只要收到过一次数据就永久退休(return)——所以只要有任意一路相机在开机时正常连上,这个看门狗立刻退休,之后任何一路相机中途掉线都不会被发现。根因跟原来那条一样:Fast DDS 只在 participant 创建时枚举网卡,链路断了再回来不会自愈,唯一修复办法是重建 participant(即重启进程)。

修复:看门狗改成永不退休,每 5 秒轮询一次(沿用原有间隔),新增第二条检查:逐个相机看 camera_frame_times(已有的每相机最近出图时间戳),如果之前出过图、现在连续 15 秒没有新帧,就判定该相机掉线,退出进程让 docker-compose.yml 的 restart: unless-stopped 重建 participant。15 秒是刻意比 UI 的"无信号"阈值(2 秒)宽松很多,避免瞬时抖动误触发。加了录制态保护:正在录制时不触发整体重启,避免为了恢复一路相机把其他正在录的相机一起打断。

验证:重启容器后放着跑,约 1 分钟后自然实测到一次 insight3_b 真实掉帧(image_encode_hw 性能日志里 insight3_b 从统计中消失了一个 10 秒窗口),看门狗按预期打日志退出,容器自动重启后两路相机都恢复正常出图,之后没有再触发(RestartCount 保持 1,非重启循环)。15 秒阈值目前未做成可配置项,如果误触发频率不理想(比如设备负载高时的瞬时卡顿也被当成掉线)可以调大。


2026-07-12 · 使用者部署前置条件补上可直接抄的命令

提交:1f96edb(deploy/jetson-nx) · docs/DEPLOYMENT.md

问题:docs/DEPLOYMENT.md §3.2(使用者路径,拿到部署包但没有源码)之前只有一句"前提:JetPack 6.x、已装 docker 与 nvidia-container-runtime、当前用户在 docker 组里",然后让对方自己去翻 JetPack/NVIDIA 官方文档——这几项其实都是一行命令能查/能装的事。

修复:直接加了 4 条命令:cat /etc/nv_tegra_release 核对 JetPack 版本(R36.x = JetPack 6.x)、command -v docker 缺失就装 docker.io、docker info | grep Runtimes 核对 nvidia runtime 缺失就装 nvidia-container-toolkit 并重启 docker、groups | grep docker 缺失就 usermod -aG docker。命令直接取自 scripts/setup_host.sh 开发者路径已有的同一套检查逻辑,保证跟脚本行为不会各说各话。

顺带清理:这份内容之前先临时放过一份独立 wiki 专题页(Jetson主机环境准备),后来决定还是应该跟 docs/DEPLOYMENT.md 主体保持在同一处——两份内容一旦分开容易慢慢对不上,删掉了那份 wiki 页,改成完全并入仓库文档。

怎么验证的:文档改动,人工核对命令跟 setup_host.sh 里的检查逻辑一致;未在设备上做"真的缺 docker/runtime"的实机复现(这台设备本来就装好了)。


2026-07-10 · kiosk 浏览器正式换成 Firefox,写进 Dockerfile

提交:b4dd4ba(deploy/jetson-nx) · Dockerfile、scripts/open_web_3d_right.sh、scripts/firefox-kiosk-user.js(新增)

问题:设备屏幕的 kiosk 一直用 vendored Playwright Chromium,它完全没有 H.264 解码能力(RTCRtpReceiver.getCapabilities 实测只有 VP8/VP9/AV1),导致 WebRTC 相机流永远显示不出来,kiosk 卡死在 JPEG 轮询这条回退路径上——不管后端硬件编码做得多好,设备屏幕都吃不到。

排查:一一确认了 arm64 桌面 Linux 生态里几乎没有厂商发 H.264 版本——Microsoft Edge、Brave、Vivaldi、xtradeb PPA 全部没有 arm64 构建(查各自仓库源都是空包列表/404)。Mozilla 是唯一还在维护 arm64 官方版的厂商,而且 Firefox 专门为 WebRTC 场景内置了 Cisco 出钱买专利的 OpenH264 插件,正好是这个用途。

修复:Dockerfile 拉取固定版本的 Firefox 官方发布包(152.0.5,钉版本号保证可复现构建)到 /opt/firefox,装齐运行时依赖(libgtk-3-0/libx11-xcb1/libasound2),烤进一份 kiosk profile(压掉首次运行的各种弹窗——kiosk 没人在键盘前点"继续")。open_web_3d_right.sh 改成直接起 Firefox --kiosk,去掉了之前临时验证阶段的"有 Firefox 就用,没有就退回 Chromium"双路径逻辑——现在 Firefox 就是唯一的 kiosk 浏览器,Chromium(Playwright 那份)只保留给无头页面验证用(截图、查 console error)。

验证:全新镜像重新构建、容器整个重建(不是复用旧容器),走真实的生产脚本路径在宿主机真实的 :0 显示器上拉起——Babylon.js GPU 场景正常渲染,无缺依赖报错,无 sandbox 崩溃。


2026-07-10 · 手部覆盖层补上同步窗口门控,对齐 PC 参考工具行为

提交:399e8c1(deploy/jetson-nx) · scripts/hand_overlay.py、scripts/multi_camera_dashboard_web.py

问题:PC 端参考工具 visualize_hand_landmarks.py(仓库外的独立工具,注意不在 scripts/ 下——之前几处注释写得像仓库内文件,容易误导去找,这次一并改正)画骨架前会检查图像帧和关键点帧的时间戳差距,超过 --max-sync-ms(默认 120ms)就不画、改显示"kpt out of sync"。dashboard 这边的 compose_hand_overlay_jpeg 只有一个宽松得多的 2 秒"话题是否还活着"判断(HAND_DATA_TIMEOUT_SEC),两者用途不同——后者用于 Settings 页 API 的 stale 标记,从没检查过关键点帧和它将要画上去的具体图像帧是否时间对齐,手动得快时会看到明显错位的骨架。

修复:compose_hand_overlay_jpeg 新增 image_stamp_ns 参数,超过 MAX_SYNC_NS=120ms(与参考工具默认值一致)直接跳过绘制(该帧走普通直通 JPEG),不再画错位骨架。2 秒的宽松判断保持不变,服务于它原本的用途。

验证:独立测试(不需要真实相机)覆盖 4 种情况——0ms/119ms 差距正常画、121ms 差距跳过、关键点快照超过 2 秒陈旧独立判定跳过,全部符合预期。


2026-07-10 · 回放彻底隔离:回放中不再与在线相机同题交错

提交:f2a25ab(deploy/jetson-nx) · scripts/multi_camera_dashboard_web.py、scripts/post_processing.py、scripts/hand_overlay.py

问题:上一条回放黑屏修复(ac3b635)代价是"实时相机与回放同时推流时两路会交错显示"——相机时间戳是开机相对时钟,无法从时间戳判断消息来源,之前干脆放行一切。用户明确这不可接受:回放必须只显示录制内容,与是否物理拔了相机无关。

修复:来源判断从"猜时间戳"改成"看话题"——PlaybackManager.start() 用 ros2 bag play 自带的 --remap(rosbag2 自己的话题改名,不是通用 ROS remap,录制话题是否绝对路径都适用)把每个要显示的话题(相机图像 + 位姿)在回放时改名到 /bagplay/...;dashboard 对每个话题额外常驻订阅一份 /bagplay/... 的"影子"订阅,复用同一个回调,回调头部一句 is_live == self._playback_mode 就把"错误来源"的消息(回放中的实时帧、或回放结束后残留的影子消息)挡在显示之外。camera_info、在线标定、手部覆盖层三个次要通道不单独开影子话题,回放期间直接静默,避免过度工程。原来基于时间戳范围的 _in_bag_range/_bag_time_range 整体删除——话题身份是确定性的,不再需要启发式。

验证:容器内起一个假"实时"发布者,在真实 bag 回放期间持续往 insight9_a 原始话题灌可区分的帧(尺寸异常小),15 秒内轮询 /api/cameras 48 次,零泄漏;纯回放(无并发实时发布者)回归三路相机帧率正常(20/20/30fps)。


2026-07-10 · 断电录制恢复:WAL 写入 + 开机自动回收 _staging 孤儿

提交:8f05756(deploy/jetson-nx) · config/rosbag_storage_sqlite3.yaml、scripts/post_processing.py、scripts/inprocess_bag_writer.py、scripts/multi_camera_dashboard_web.py、Dockerfile

问题:录制中断电,一次录制会丢两遍——rosbag2 sqlite 默认 journal_mode=MEMORY, synchronous=OFF,断电后 .db3 直接 "database disk image is malformed";且录制只有在优雅停止时才会从 rosbags/_staging/<名字>/ merge 成正式 bag,断电后 staging 目录永远躺在那里,bag 列表看不到(用户上午的三次断电测试共 13GB 数据全部处于这个状态,这也是"录制失败"观感的来源——录制启动本身没有 bug,空 bag 名走默认命名经 API 和 Playwright UI 双路验证均正常)。

修复:

  • 所有录制写入端(ros2 bag record 子进程 + 进程内图像写入器)统一走 config/rosbag_storage_sqlite3.yaml:journal_mode=WAL, synchronous=NORMAL,断电最多丢最后未同步的事务;
  • 后端启动时后台线程回收 staging 孤儿:只缺 metadata.yaml 的部分 ros2 bag reindex;数据库损坏的用 sqlite3 .recover 流式重建(进程管道直连,不在内存里攒 GB 级 SQL);救回的部分统一 merge 成正式 bag;merge 段错误时逐部分探测、剔除毒部分(实测遇到 1 例);救不回的数据留在 staging 供取证,绝不删除。

验证(全部用真实断电产物):三个孤儿——全损坏组救回 53.4 万条消息且 check_bag RESULT OK;元数据完好组 7/7 直接 merge OK;混合组 6/7 恢复(毒部分被隔离,含全部 VIO/IMU 话题)。实弹两轮:录制中 docker kill(等效断电)→ 重启后自动 7/7 恢复、无一损坏。

调参记录(重要):先试的 synchronous=NORMAL 被实测否决——checkpoint fsync 在名义负载下把图像写入线程卡出 32-54% 丢帧(最长 12.3s 空洞,双相机同刻);改为 synchronous=OFF(提交 945033c)后干净录制 0 丢帧。WAL 帧自带校验和,断电后撕裂的尾部会被自动丢弃,崩溃一致性不依赖 fsync;代价是断电额外丢 OS 未刷盘的最后几秒。sync=OFF 下断电重测:7/7 恢复,check_bag 仅 0.37-0.61% 丢帧且全在录制开头订阅暖机段,切断点无丢失。

后续修正(bac6134):部分恢复后残留的 staging 目录会被每次开机重复回收、每次多铸一个 _recovered_ 重复包——现在部分恢复后目录改名 <名字>.leftover,扫描跳过。另实测发现触发部分隔离的 ros2 bag convert 段错误是非确定性的(同一输入时崩时好,像 rosbag2 关闭竞态而非数据损坏),且崩溃的 convert 可能留下带 metadata.yaml 的"看似完整"输出——与 staging 并存的 convert 产物不可信。


2026-07-10 · 回放全程黑屏修复:开机相对时间戳永远不落在 bag 的纪元时间范围内

提交:ac3b635(deploy/jetson-nx) · scripts/multi_camera_dashboard_web.py

问题:回放期间相机面板全程冻结(fps=0,一帧都不显示)。Insight 相机的消息头时间戳来自设备开机时钟(开机后几分钟,非纪元时间),而回放门控 _in_bag_range 拿它与 bag 元数据的纪元时间范围(录制当天的真实日期)比较——所有回放帧和位姿永远不在范围内,全部被丢弃。

修复:时间戳早于 2000 年的判定为开机相对时钟(不携带日期信息、无法做范围判断),直接放行;bag 时间范围读不出来时同理。取舍:实时相机与回放同时推流时两路会在同一话题上交错显示——但比什么都不显示强。

验证:回放 insight_record_20260709_143315,物理断开的 insight3_b 从全程 0 fps 变为 20-30 fps 纯回放帧;playback_mode 与状态迁移不受影响。


2026-07-10 · WebRTC 硬件 H.264 显示链路(相机面板不再靠 HTTP 轮询逐帧拉图)

提交:68bf182(deploy/jetson-nx) · scripts/webrtc_stream.py(新增)、scripts/multi_camera_dashboard_web.py、web_dashboard/src/{app.js,styles.css}、Dockerfile

问题:显示链路(50ms JSON 轮询 + 每帧一次 HTTP GET + 浏览器 CPU 软解 JPEG)把面板帧率钉死在轮询频率上且负载下抖动;NVJPEG 只加速了后端编码这一半,浏览器端从未受益。这也是"性能更强的 Orin NX 看起来反而比 nano 卡"的根因——nano 100ms 轮询节奏均匀,NX 50ms 轮询在软渲染 kiosk 里节奏抖动,人眼对帧间隔抖动比对平均帧率敏感。

修复:裸流相机走 appsrc → nvvidconv → nvv4l2h264enc(硬编)→ webrtcbin,压缩相机先过 nvjpegdec(硬解);信令走 /ws/webrtc;前端每面板 RTCPeerConnection + <video>,出帧才切换,连接失败/断线自动回退轮询并退避重试。每观看者独立会话,无人观看零开销。码率按分辨率自适应(insight3 ≈1.6Mbps,insight9 ≈9.4Mbps)。

验证:容器内 GStreamer 接收探针对回放流完成完整 offer/ICE/DTLS 握手(offer 含真实 SPS/PPS);后端扛住 10+ 会话含 3 路并发、握手中途 SIGKILL 客户端、回放反复起停;vendored Chromium 加载页面无 JS 错误并按设计留在轮询。

已知缺口:Playwright vendored Chromium 无 H.264 WebRTC 解码(仅 VP8/VP9/AV1,getCapabilities 实测),设备屏幕 kiosk 暂仍走轮询——需换带编解码器的 arm64 Chromium(如 xtradeb 构建)并重建镜像;笔记本浏览器立即生效。AV1 硬编可用但 GStreamer 1.20 无 AV1 RTP payloader,H.264 是唯一可行编码。另:后端加了 faulthandler 取证(原生崩溃/SIGTERM 线程栈 → outputs/backend_crash.log),因 bring-up 期间出现过两次未解释的后端重启(未再复现)。


2026-07-10 · 运维:根分区被 27.5GB 的 git tmp_pack 塞满(含 docker build cache 修剪)

现象:磁盘 100%(116G 用 110G),Claude Code/后端各种 ENOSPC。定位:.git/objects/pack/ 里三个 tmp_pack_*(11G+11G+5.5G),时间戳为当日 merge/commit 之后——git gc --auto 并发触发 repack、赶上磁盘紧张被杀,反复重试越积越大(仓库真实对象库仅 1.32G)。

处理:确认无 git 进程占用后删除 tmp_pack(.git 29G→2.4G),git fsck 通过;另 docker builder prune --keep-storage 8GB 回收 13.8G 陈旧 build cache(保住 COLMAP 编译层)。最终磁盘 100%→70%(34G 可用)。留意:rosbags/ 已 19G,宿主机同步已开启,攒多可清。


2026-07-10 · Bags 列表加载慢:metadata.yaml 换 libyaml C 解析器(10 倍提速)

提交:cedd77e(deploy/lite)/ dbeb440(deploy/lite-779) · scripts/post_processing.py

问题:/bags 页面随包数量增多线性变慢。profile 定位:/api/rosbags 97% 耗时在 yaml.safe_load 的纯 Python 解析器(Jetson 上每个 metadata.yaml ~34ms,13 个小文件 93 万次函数调用);目录大小递归、状态检查均为微秒级。注意:页面上的 msgs 统计是 metadata.yaml 里的现成字段,去掉它不会变快。

修复:_read_bag_metadata 改用 yaml.CSafeLoader(libyaml,容器内可用),无 libyaml 的 PyYAML 回退 SafeLoader。

验证:容器内 13 包实测 list_rosbags 360ms→33ms(每文件 34ms→3.2ms);输出字段逐字节一致;重启后端后 /api/rosbags 端到端 ~80ms。

待办(可选):若包数量继续增长,可再加 mtime 缓存(录完的包 metadata 永不变),把重复访问降到近零。


2026-07-10 · 轨迹评分重构:机群定标 log 域主分 + 尖峰分档罚分(面向训练数据筛选)

提交:b40c51d(deploy/lite)/ 202f556(deploy/lite-779) · scripts/traj_score.py、scripts/multi_camera_dashboard_web.py、web_dashboard/src/{app.js,scoring.html}

问题:评分两极分化——不是满分就是很低。旧公式 score = min(100, ref_cov / max_trace × 100) 有三个叠加缺陷:整包分数由最差一帧决定(极值统计);1/x 双曲映射 + 100 封顶(好包全挤在 100,差一点就急坠);ref_cov 手动填写(历史结果里同一包出现过 66.72 和 100.0 两个分,因为两次填了不同 ref)。在机群 33 个 bag×相机 历史样本上:29 个 ≥90,其余 ≤50。

新模型(定位是训练数据筛选,episode 整段使用):

  • 主分:整体协方差水平 L = (log10 p50 + log10 p90)/2,经机群定标的 z 分数过 logistic——定标锚取自本机群 bag 历史(2026-07-10,33 样本:median_L=-3.743≈1.8e-4,稳健 σ=0.412 个数量级),机群中位包落 ~75 分,分数自然呈钟形分布。定标值写死在脚本常量里(机群/环境大变时需重定标,跨定标的分数不可比)。
  • 尖峰按时间结构分档(阈值 = 自身中位数 ×8,按连续段分组):瞬时(≤2 帧)每段扣 1.5、封顶 5——孤立毛刺不毁好包;成段(≥3 帧)每占录制时长 1% 扣 12、不设上限——时间相关的位姿污染会毒化 BC 训练标签(UMI 系管线对此直接弃用数据);任何 >1s 的坏段额外把总分封顶 40(episode 级作废语义)。
  • 分项透明:JSON/报告/前端卡片输出 base_score、两类罚分、坏段数/时长/最长段、usable_fraction、定标参数;scoring 页删除 ref_cov 输入框及其 API 链路。

验证(全量 33 样本回归 + 端到端):

  • 分布:20 段:1 / 30 段:3 / 40 段:4 / 50 段:4 / 60 段:5 / 70 段:3 / 80 段:8 / 90 段:5(旧公式 29 个挤在 90+)。
  • 关键案例:孤立 2 帧毛刺 88→86.5(旧公式砍到 46.5);两段共 0.79s(2.3% 时长)持续劣化 86.6→58.9 落"需复查"档(该案例实测坏段结构:0.52s 峰值 47× 中位 + 0.50s 峰值 16× 中位)。
  • 端到端:重启后端 → /api/scoring/run 真实跑分 → Playwright 验证卡片渲染分项、0 console error。测试脚本 test_score_formula_ab_20260710.py、test_traj_score_regression_20260710.py。

后续:若训练管线改为切片(clip)使用,可放宽成段罚分/episode 封顶,改为输出坏段时间戳供切片避开(usable_fraction 字段已就位)。

修订(同日,058e46d/0b9e24f):成段罚分从"每占时长 1% 扣 12"改为绝对时长口径"每秒坏时长扣 35"——百分比口径下同样 0.5s 的坏段在 20s 短包扣 21 分、在 2 分钟长包只扣 3.5 分,但坏段对训练数据的伤害是绝对的,不随包变长而稀释。系数 35 使基准案例(34s 包 0.79s 坏段 → −27.7 → 58.9)行为不变;全量回归分布逐字节一致,仅 0.18s 短段案例 −5.2 → −6.3。


2026-07-10 · 小修集:Recording 页自身导航标签、删除 Sync-to-Host 与标定板参数编辑、kiosk 启动报错降噪

提交:0b4a27d + a1872dc(deploy/lite)/ ded4f92 + fdbbe35(deploy/lite-779)

  • Recording 页导航缺自身标签:七个页面里唯独 recording.html 的导航没有自己的 Recording 项,补齐(带 aria-current)。后续修订(87cf9e4/0503717、fcb3562/96cae6b):recording 页原有专属导航类 .recording-page-links(无 font-size,字体 16px 偏大、高亮规则也吃不到),最终改为直接复用与其他页面完全相同的 page-nav 类并删除专属 CSS;Playwright 验证 6 个页面 font-size 14px、当前页高亮色全部一致。
  • 删除 Sync Latest To Host:Recording 页按钮/状态行、Settings 页 rosbag remote sync 面板、app.js 相关函数与 CSS 全部移除(功能不需要;后端 API 路由保留未动,sync_rosbag_to_host 配置本就是 false)。
  • 删除 Settings 标定板参数编辑面板:该表单读写的 max_translation_std_m/max_rotation_std_deg 是精度重构已废弃的旧键,留着只会把改对的配置写坏。
  • kiosk 启动报错降噪:容器内无 D-Bus/Google 账号,Chromium 每次启动刷屏 UPower/GCM/组件更新/设备端模型下载错误。加 --disable-background-networking --disable-component-update --disable-sync --disable-features=OptimizationGuideOnDeviceModel 从源头关闭,残余 stderr 重定向到 /tmp/insight-kiosk-chrome.log(INSIGHT_KIOSK_LOG 可覆盖)。
  • 验证:Playwright 全 7 页 0 console error、删除元素不在 DOM;kiosk 用伪 DISPLAY 触发失败路径确认终端干净、错误进日志。

2026-07-09 · 校准精度重构定稿:逐样本锚定 + RANSAC/IPPE 板位姿 + 板几何修正 + 质量门控

本条汇总当天三轮迭代(精度重构 → 上线后板几何报错排查 → revert → 复验后重新应用 + 新增 IPPE 消歧)的当时最终版本。旧专题页《校准原理与参数》已随 AprilTag 在线对齐功能整体删除;本条仅作为历史记录,不代表当前架构。

最终提交:5a78060+b87cadc+3bc867c+e6511b2(deploy/lite)/ 755c581+ab185ba+b4f934c(deploy/lite-779) · scripts/live_alignment.py、config/board_calibration.json(中间夹了一次 revert 36a2f6f/c456562,后确认 revert 理由不成立而恢复,见下)

起因:校准完三个模型相对位置有可见偏差,且现场反馈"依旧很不精确、可用视角苛刻"。定位到三个独立问题,全部已解决:

  1. 锚定只用单帧 VIO 噪声:旧代码对 12 个板位姿样本取平均,但锚定时只配对最后一帧检测时刻的单个 VIO 插值位姿,把那一瞬间的 VIO 噪声原封不动烙进整个会话。→ 改为逐样本锚定:每帧检测各自配对该帧时间戳插值出的 VIO 位姿,独立生成锚候选,MAD 剔除离群后取最近 N 个内点平均;副作用是锚在相机运动中理论为常量,校准不再要求静止。
  2. 板几何配置与实物不符:marker_separation_m 填的是 Kalibr 规格页的白色通道尺寸(16.5mm),但 OpenCV GridBoard 要的是检测黑框之间的距离(16.5 + 两侧各 6.9mm 印刷黑边 ≈ 30.5mm);同时实物板(官方 Kalibr AprilGrid)行内 id 顺序与 OpenCV GridBoard 的标准假设相反(row-mirrored)。板和官方参数本身都没错,错在这两处语义换算——此前所有校准用的都是这个错误几何算出的垃圾位姿(重投影残差 ~110px),这才是"校准精度不够"的真正根源。中途曾因怀疑"改了官方板参数"而被 revert,之后用两种独立方法(逐 tag IPPE 重建实物几何;纯单应、不依赖内参的测量)在不同实拍帧上分别复验,结论一致,遂恢复该修复。
  3. 板位姿求解无质量指标 + 平面双解容易翻转:estimatePoseBoard 把所有角点一股脑丢 PnP,一块误检 tag 就能拉歪整个解,且无重投影误差可查;另外平面板 PnP 天生有两个解,斜视角下迭代解常年选错分支,且翻转解还能蒙混过重投影门控。→ 改为 matchImagePoints → solvePnPRansac 剔除误检 → solvePnPRefineLM 细化 → solvePnPGeneric(SOLVEPNP_IPPE) 在 RANSAC 内点上取双解按残差选正确分支(次优分支同样达标且转角差 >20° 时判定真歧义,整帧丢弃)→ 重投影 RMS 超限直接丢帧。
  4. 质量硬顶 与 数字化:锚候选相对平均锚的 RMS 偏差超过 anchor_max_translation_std_m / anchor_max_rotation_std_deg 直接拒绝出解(状态 unstable),不再用旧的、且被放宽到形同虚设的 max_translation_std_m / max_rotation_std_deg(已删除);每次出解在日志/结果文件里打印 anchor_rms=XXmm/X.XXdeg 供现场判断质量。

验证数据汇总:

  • 板几何 A/B(实拍帧全角点重投影 RMS,两帧独立):标准布局+16.5mm 120.2/115.4px → 镜像布局+30.5mm 2.4/2.2px;生产 RANSAC 内点路径 1.29/1.36px,稳定低于 2.0px 门控。
  • 精度蒙特卡洛(合成数据 200 轮/场景,4mm 检测噪声+4mm VIO 噪声,对照真值锚):静止中位误差 6.2mm→2.6mm,p95 12.1mm→4.7mm;举板移动 0.5m 时旧法 280mm(不可用)→新法 2.5mm;巨噪声/tag 污染场景 100/100 轮正确拒绝或抗污染出解。
  • 视角/翻转蒙特卡洛(60 轮/格,0.4px 噪声+5% 离群 tag,通过率/翻转率):40°/2m 93%/7%→100%/0%;55°/2m 42%/58%→93%/2%;65°/2m 2%/98%→90%/2%;75°/2m 0%/100%→90%/0%。可用视角约从 ±40° 扩展到 ±75°。
  • 端到端:实机校准 CALIBRATED insight7_b | anchor_rms=0.4~0.6mm/0.00deg,20/20 内点,稳定 tracking;重启后端 Playwright 无头验证页面 0 console error。

运维提醒:旧的 live_alignment_state.json 是错误几何算出的垃圾解,已备份为 *.bak-20260709-wrongboard,升级后需重新现场校准一次;deploy/lite(insight3 红外机群,另一块物理板/另一台相机)几何参数已按同样方法复验通过,deploy/lite-779(insight7/9 彩色机群)几何参数当天已用实测确认。


2026-07-09 · Qt 面板 NV12 红外流显示黑白

提交:fae3847(deploy/lite)/ d3747c6(deploy/lite-779) · scripts/multi_camera_dashboard_qt.py

问题:insight3 机群(红外 infra1 流,NV12 编码)下启动 open_monitor_dashboard.sh,Qt 面板画面是黑白的。根因:Qt 版的 _convert_ros_image 没有 nv12 分支,走到通用 step 兜底逻辑——它只读 msg.height 行数据,恰好就是 NV12 的 Y(亮度)平面,色度(UV 平面)被整个丢弃。Web 版同样的坑 7 月 7 日已在 8fc6556 修过(当时实测 insight3_b 有真实逐帧色度,不是无彩信号),但那次只改了 web 版,Qt 版漏了。

修复:移植 web 版逻辑到 Qt 版——从数据长度推导真实缓冲行数(部分驱动的 msg.height 报的是亮度高度而缓冲有 1.5 倍行数),COLOR_YUV2RGB_NV12 全彩解码(Qt 管线内部是 RGB,与 web 版的 BGR 不同)。

验证:合成已知色度的 NV12 帧精确还原(橙 [255,128,0]/蓝 [0,128,255],测试脚本 test_qt_nv12_decode_20260709.py);mono8 灰度直通不受影响;实机重启 Qt 面板截图确认 insight3_a/b 全彩显示。

教训:双端(web/Qt)共用的解码逻辑分裂在两处实现,修一处漏一处——第二次踩了。后续值得把图像解码统一抽到共享模块。


2026-07-09 · 3D 视图 WebSocket 假死自动重连

提交:d2294b9(deploy/lite-779)/ 6be7d67(deploy/lite) · web_dashboard/src/app.js

问题:后端容器重启后,浏览器端的 WebSocket 可能进入 TCP 半开状态——连接不再送数据,但也永远收不到 close 帧,ws.onclose 不触发。原有重连逻辑只挂在 onclose 上,于是 3D 视图永久卡死在最后一帧(三个模型全部不动),必须手动切到别的页面再切回来(= 整页刷新)才恢复。

修复:记录最近一次收到消息的时间,每秒检查一次:连接状态是 OPEN 但超过 4 秒没有任何消息(正常节奏约 20Hz),就主动 close() 强制走已有的 onclose 重连路径。

验证:容器内 Playwright 无头加载 /3d 页面,确认控制台零报错、模型与图例正常渲染。

教训(附带事故):第一版把 let activeWs 声明放在了顶层 connect() 调用之后,let 暂时性死区导致整个 module script 抛 ReferenceError 中断——app.js 是所有页面共享的单脚本,结果所有页面全白。改顶层代码务必声明在使用之前,改完用 Playwright 实测再宣布修复。


2026-07-09 · 轨迹显示卡顿:payload 坐标精度截断

提交:564b649(deploy/lite-779)/ 2a87eb5(deploy/lite) · scripts/multi_camera_dashboard_web.py

问题:轨迹明显卡顿不实时,但相机图像不卡。排查发现 WebSocket 每次广播(约 20Hz)都重发完整轨迹历史(最多 300 点 × 3 个位姿),坐标是未截断的 float64(约 17 位有效数字),单条消息约 60KB。服务端 JSON 序列化 + 客户端解析渲染这个体量、每秒 15~20 次,实测消息间隔最大 141ms(应为 50ms)。图像走独立 HTTP/JPEG 通道所以不受影响。

修复:位置/四元数保留 5 位小数(0.01mm 精度),轨迹点保留 4 位小数(0.1mm 精度)——远超显示所需精度。

验证(WebSocket 实测对比):

指标 修复前 修复后
平均消息大小 59.7KB 26.6KB
平均发送间隔 76.8ms 60.5ms
最大间隔(卡顿峰值) 140.9ms 75.3ms

2026-07-09 · Qt 监控窗口精确左半屏

提交:26d8d79(deploy/lite-779)/ e5cba5c(deploy/lite) · scripts/multi_camera_dashboard_qt.py

问题:open_monitor_dashboard.sh 打开的 Qt 监控窗口不是标准的半个屏幕。根因是宿主机桌面是 GNOME(顶栏 + 左侧 Dock),Mutter 把普通窗口钳制在 _NET_WORKAREA(可用工作区)内:代码请求 (0, 0, 960, 1080),实际落在 (70, 27, 960, 1053)。

修复:窗口本就是无边框 + 自绘标题栏拖动的 kiosk 风格,加 Qt.X11BypassWindowManagerHint 完全绕过窗口管理器布局。

验证:xwininfo 确认窗口精确落在 (0,0) 960×1080;截图确认与右侧 3D 仪表盘严丝合缝拼成整屏。


2026-07-09 · 镜像过期导致后端崩溃循环(运维记录,无代码改动)

问题:run_dashboard.sh --jetson 卡在等待健康检查。容器日志报 "rclpy is not available in this environment" 并无限重启。真实原因:git pull 拉到的新代码 import 了 vision_msgs(手部关键点覆盖层用),但本地镜像是 7 月 1 日构建的,没装 ros-humble-vision-msgs(7 月 6 日才加进 Dockerfile)。顶部的 try/except 把一切 import 失败都误报成 rclpy 缺失。

处理:docker compose build 重建镜像 + docker compose up -d 重建容器。

经验:拉代码后如果 Dockerfile 有变更(git log -- Dockerfile 检查),必须重建镜像;"rclpy is not available" 的报错要先怀疑是别的包缺失。

⚠️ **GitHub.com Fallback** ⚠️