Serve replay playback from a cached analysis result
Replay re-ran the SDK chain on every seek; Part A's pass stores the flow samples once. Opening a recording now reuses a compatible result or runs the pass behind a progress/cancel surface, and one cursor with two modes (recorded CSV, analysed) feeds the charts, the 2D canvas and the 3D pane by frame id. A threshold, colour or idle-dot change redraws from the stored frames and writes nothing; a cancelled pass is reported and never played. Verified: apps/studio/tests 440 items / 0 failed / 18 skipped (15 new, no existing assertion changed); shared 417 / 0 / 1; autocal 358.
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
# 回放分析结果(Part B:从结果回放)
|
||||
|
||||
本文是 [replay-analysis.md](replay-analysis.md) 的续篇:那一篇定义结果本身,这一篇定义播放端
|
||||
怎么读它。打开录像即取用或跑出结果,之后播放、拖动、单步与跳转**全部是读文件**。实现分布在
|
||||
`replay_analysis.py`(结果 → 时间轴与画面)、`analysis_worker.py`(pass 的线程边界与取消)、
|
||||
`ui/analysis_progress.py`(进度/取消面板)、`replay.py` 与 `replay_controller.py`(一个
|
||||
游标、两个模式),以及 `app_window.py` 的接线(第 12 节)。各节编号承接 Part A。
|
||||
|
||||
## 9. 一个时间轴,两个模式
|
||||
|
||||
游标只有一个:`ReplayEngine.position()` 的 `t_rel_s`。模式说明**画面从哪里读**:
|
||||
|
||||
| 模式 | 画面来源 | 说明 |
|
||||
|---|---|---|
|
||||
| `recording` | 录像自己的 `raw_*.mp4`,按需解码 | 只有原始相机图像:没有光流,也没有深度 |
|
||||
| `analysis` | 存下来的分析结果,按帧读 | 有 pass 记录下来的光流样本与候选网格 |
|
||||
|
||||
图表、二维画布、三维面板都读这一个游标,模式由 `ReplayEngine.mode()` 回答;「从视频重算」
|
||||
因此不再是第三种来源,而是**显式重跑**这一遍的开关(第 10 节)。时间基准是**信号时间**
|
||||
(Part A 第 5 节),进度、位置、单步与跳转都说它。
|
||||
|
||||
**帧身份只来自录像自己的计数器。** 证据录像 id 是 20…52 而非 0…32,所以
|
||||
`AnalysisSession.frame(frame_id)` 对结果里没有的 id 报 `AnalysisNotFoundError`:把索引当 id
|
||||
传进去不会被"顺手改成能读的那一帧",那会返回一张看起来完全正确的图。按时间取帧走
|
||||
`frame_id_at(t)`,取**该时刻或之前**的那一帧 —— 与顺序播放经过的是同一帧。
|
||||
|
||||
## 10. 打开即分析
|
||||
|
||||
打开录像后固定做一件事:**有兼容结果就用,没有就跑**。`stored_result()` 命中是一次文件读,
|
||||
面板显示"已有结果,无需重算";没有命中就在**工作线程**上跑 pass,`Progress` 按 0.1 s 合流后
|
||||
经 Qt 信号回到 GUI 线程,面板显示 `已写/预计`、分段号与已用时间。pass 期间时间轴留在
|
||||
`recording` 模式,完成后才切到 `analysis` —— 半截结果永远不上屏。面板的取消按钮置一次取消
|
||||
事件,服务在检查点停下,目录带 `INCOMPLETE` 标记留下,**不提供回放**(第 13 节)。「从视频
|
||||
重算」语义不变,与打开路径走同一个 `on_analysis_requested()`,因此不可能出现两条并行模式。
|
||||
|
||||
分析的四条终态消息**都不写状态栏**,只更新进度面板:pass 是异步的,可能在操作员做了别的事
|
||||
几秒后才结束,一条活得比原因久的状态栏消息会把无关操作说成失败。原因留在面板上,也由
|
||||
`analysis_error()` 提供。
|
||||
|
||||
**并发与工作目录。** 开链路会往当前目录写 2 MB 预览图(Part A 第 6 节),所以 pass 跑在**本
|
||||
进程**的一个线程里,启动前要求没有采集会话、没有预览、也没有第二遍分析,并且**本模块从不改
|
||||
工作目录**。锁只覆盖开链路那一小段(`_working_directory` 在 `_OpenChain.open()` 内进出),
|
||||
逐帧处理在锁之外,GUI 线程这期间正常刷新。关窗时先取消并等待 pass。
|
||||
|
||||
## 11. 三视图如何被同一个位置喂
|
||||
|
||||
| 视图 | 读什么 | 怎么读 |
|
||||
|---|---|---|
|
||||
| 图表 | `forces.csv`(记录值) | `charts.set_cursor(t)` 用同一游标;结果里不存力 |
|
||||
| 二维画布 | 结果的 `flow` 与 `valid_mask` | `ImageFrame(source="analysis")` 走窗口唯一的 2D 出口 `_show_image_frame`,箭头、暗点、配色与域边界与实时/重算**同一套代码** |
|
||||
| 三维面板 | 同一帧的 `SdkPacket` | `session.packet(frame)` → `Scene3DView.submit_packet`,走同一个 adapter、域掩码 latch 与有效性求交 |
|
||||
|
||||
**画面是重建的,并且是精确重建。** 二维需要的 `[H,W,2]` 流场是把存下来的 `dx`/`dy` 写回
|
||||
**记录下来的候选位置**、其余格子填 `NaN`(选区器把非有限值当作非法,邻居格子因此不可能贡献
|
||||
出 pass 没记录过的样本)。写回要除以
|
||||
`DISPLACEMENT_PX_PER_FLOW × flow_arrow_scale × flow_length_margin`。
|
||||
|
||||
`dx`/`dy` 是**画出来的**位移,已经带上长度余量那一步的缩放:只用前两个因子会让每根箭头短
|
||||
20%(finger 余量 0.8),330 个样本里 319 个不同、活动集也不同 —— 实测的,不是推导的。余量补
|
||||
上后,重建流场重新过一遍共享选区器,样本集与存下来的**逐元素相等**(33 帧全覆盖)。三维面板
|
||||
直接拿存下来的 `FlowSamples`,所以"两张图是同一个测量"由构造决定。
|
||||
|
||||
## 12. 显示设置是读
|
||||
|
||||
阈值、配色范围、暗点开关都不在失效键里(Part A 第 3 节),所以改动它们必须**只重画**:阈值
|
||||
经 `AnalysisReader.samples(policy=…)` 作用在存下来的 `magnitude` 上,配色范围与暗点开关只
|
||||
存在于绘制调用里。走线是 `Scene3DPanel.settingsChanged` → `on_display_settings()` →
|
||||
`session.serve_policy()` → `ReplayEngine.redraw()`,重画不移动游标、不写文件。
|
||||
|
||||
**栅格类设置被拒绝**:`flow_grid_spacing`、`flow_edge_margin`、`max_arrows`、
|
||||
`flow_length_margin`、`flow_arrow_scale` 决定候选位置**在哪里**,而记录下来的位置是 pass 当时
|
||||
测的那些。`serve_policy()` 返回 `False` 并提示哪一项动了,画面继续按结果自己的栅格绘制 —— 画
|
||||
一份"看起来一样、其实采在别处"的箭头才是真正错误的选项。
|
||||
|
||||
`app_window.py` 的接线只有三处,供 Part C 使用:`_analysis_service(recording, cancelled, *,
|
||||
on_progress)`(`chain_spec_for` 从录像自己的 `session.json` 取链路,因此结果可复现)、
|
||||
`_show_analysis_frame(packet)`、`_connect_analysis_display(mesh_panel)`(把
|
||||
`Scene3DPanel.settingsChanged` 接到控制器;三维视图首次使用时才建,用有界的 40 × 250 ms 重试
|
||||
等它出现)。工具栏、复位与 transport 控件、`display_prefs.py`、`_on_reset_view`/F8 未改。
|
||||
|
||||
## 13. 未完成与取消
|
||||
|
||||
`AnalysisSession.__init__` 在 `reader.complete` 为假时抛 `ResultPolicyError`;
|
||||
`ResultLibrary.open(..., allow_incomplete=True)` 也拒绝 ——「可查看」与「可回放」是两个不同的
|
||||
断言。面板的 `cancelled` 与 `failed` 是两个独立状态,都不会显示成完成;这两种情况下时间轴留
|
||||
在 `recording` 模式。
|
||||
|
||||
## 14. 验收证据
|
||||
|
||||
`apps/studio/tests/test_replay_analysis.py`(15 项,真实素材):
|
||||
|
||||
* **跳转等于顺序播放**:顺序走过全部 33 帧记下每帧,再按 `time_of(frame_id)` 直接跳到 5 个
|
||||
采样位置。容差是零:帧 id、`rows/cols/dx/dy/active` 逐元素相等,流场按位相等(非候选格按
|
||||
构造是 `NaN`,用 `equal_nan` 比较)。跳转精确的原因是给它的时间就是那一帧自己的 `t_rel_s`。
|
||||
* **显示设置不写结果**:`scan_results(cache)` 的目录集合在改阈值(0.2 与 1e9)、改显示增益、
|
||||
重复同一栅格前后完全相同。
|
||||
* **取消不被当完整**:pass 在第一帧后取消 → 面板报 `cancelled`、`analysis_active()` 为假、
|
||||
引擎停在 `recording` 模式、目录里 `complete=false` 且默认读端抛
|
||||
`AnalysisIncompleteError`。
|
||||
* **回归**:`apps/studio/tests` 440 项 / 0 失败 / 18 跳过(原 425 项:新增 15 项,既有断言一条
|
||||
未改)。把分析终态写进状态栏时,
|
||||
`test_the_replay_reset_is_the_display_reset_and_never_the_device` 失败过 —— 合成录像的配置
|
||||
缺失让 pass 在断言之前结束并覆盖了 `复位显示` 的提示;修的是消息落在哪里。
|
||||
|
||||
真实 GL 快照(`QT_QPA_PLATFORM=windows` + `QT_OPENGL=desktop`,真实窗口路径、pass 写入
|
||||
scratch cache、游标停在存下来的帧 id 上):
|
||||
|
||||
| 文件 | 内容 |
|
||||
|---|---|
|
||||
| `.scratch/replay-analysis-b/snapshots/analysis-frame-21-2d.png` | 帧 21(t=0.0641 s)二维画布,308×331 |
|
||||
| `.scratch/replay-analysis-b/snapshots/analysis-frame-21-3d.png` | 同一帧三维面板,676×610 |
|
||||
| `.scratch/replay-analysis-b/snapshots/analysis-frame-36-2d.png` | 帧 36(t=0.9601 s)二维画布,308×331 |
|
||||
| `.scratch/replay-analysis-b/snapshots/analysis-frame-36-3d.png` | 同一帧三维面板,676×594 |
|
||||
|
||||
二维画面是候选网格上的暗点阵(该帧活动样本很少),三维是手指岛面加同一套样本点;"同一个样本
|
||||
集"由上面的逐元素断言保证,快照证明这条链路真的走到了 GL。
|
||||
|
||||
## 15. 已知限制
|
||||
|
||||
* 结果里没有力,也没有深度:力曲线始终读记录的 `forces.csv`,深度视图在回放模式下不可用(没有
|
||||
可显示的数据,面板按空处理而不是画一张假的)。
|
||||
* 结果覆盖的帧是 pass 走过的那些:录像比结果长时游标可以离开结果范围(面板说"这个分析结果里
|
||||
没有可显示的帧"),不会静默重复最后一帧。
|
||||
* 栅格类显示设置不生效(第 12 节);换栅格就是重跑一遍,那是**另一个键**。
|
||||
* 一个进程同时只有一遍分析在跑;采集会话运行期间不启动分析。
|
||||
Reference in New Issue
Block a user