Store the depth channel in an analysis result and serve it to the 2D depth view
A replay could draw arrows but not depth, and the recording view said there was nothing to draw even when a result existed. Depth is now quantised per frame (14 B empty, 486 B at the worst frame seen), read from the cache by the same draw path live uses, and the raw-recording note appears only when no result exists. Format version 2; see apps/studio/docs/replay-analysis.md.
This commit is contained in:
@@ -12,8 +12,12 @@
|
||||
|
||||
| 模式 | 画面来源 | 说明 |
|
||||
|---|---|---|
|
||||
| `recording` | 录像自己的 `raw_*.mp4`,按需解码 | 只有原始相机图像:没有光流,也没有深度 |
|
||||
| `analysis` | 存下来的分析结果,按帧读 | 有 pass 记录下来的光流样本与候选网格 |
|
||||
| `recording` | 录像自己的 `raw_*.mp4`,按需解码 | 这个画面是原始相机图像:它自己没有光流也没有深度 |
|
||||
| `analysis` | 存下来的分析结果,按帧读 | 有 pass 记录下来的光流样本、候选网格与深度图 |
|
||||
|
||||
模式说的是**画面从哪里读**,而"有没有可画的东西"是另一件事:只要有结果,二维面板的模式
|
||||
选择器就提供 深度图/箭头(时间轴在录像上时连"原始图像"一起提供),选了派生模式的那些帧
|
||||
一律从结果读、按窗口唯一的 2D 出口画 —— 绝不会把原始图像当成箭头图交出去。
|
||||
|
||||
图表、二维画布、三维面板都读这一个游标,模式由 `ReplayEngine.mode()` 回答;「从视频重算」
|
||||
因此不再是第三种来源,而是**显式重跑**这一遍的开关(第 10 节)。时间基准是**信号时间**
|
||||
@@ -47,9 +51,16 @@
|
||||
| 视图 | 读什么 | 怎么读 |
|
||||
|---|---|---|
|
||||
| 图表 | `forces.csv`(记录值) | `charts.set_cursor(t)` 用同一游标;结果里不存力 |
|
||||
| 二维画布 | 结果的 `flow` 与 `valid_mask` | `ImageFrame(source="analysis")` 走窗口唯一的 2D 出口 `_show_image_frame`,箭头、暗点、配色与域边界与实时/重算**同一套代码** |
|
||||
| 二维画布 | 结果的 `flow`、`depth` 与 `valid_mask` | `ImageFrame(source="analysis")` 走窗口唯一的 2D 出口 `_show_image_frame`,箭头、暗点、配色、域边界与深度视图与实时/重算**同一套代码** |
|
||||
| 三维面板 | 同一帧的 `SdkPacket` | `session.packet(frame)` → `Scene3DView.submit_packet`,走同一个 adapter、域掩码 latch 与有效性求交 |
|
||||
|
||||
**深度视图也由结果喂。** `AnalysedFrame.depth` 就是第 1 节那幅量化平面重建出来的
|
||||
`float32` 图(空帧是形状正确的全零),`depth_valid` 因此是 `True` 而不像以前那样恒为
|
||||
`False`;深度着色、色标锁定、空/缺数据的区分全在窗口既有的那条路径上,回放没有第二条绘制
|
||||
代码。"这一帧深度是精确的零"由面板说出来(`panels.depth_empty`,带 `{pixels}`),与
|
||||
"这一帧根本没有深度通道"(`panels.depth_unavailable`)是两句不同的话,因为它们是两个不同的
|
||||
事实。
|
||||
|
||||
**画面是重建的,并且是精确重建。** 二维需要的 `[H,W,2]` 流场是把存下来的 `dx`/`dy` 写回
|
||||
**记录下来的候选位置**、其余格子填 `NaN`(选区器把非有限值当作非法,邻居格子因此不可能贡献
|
||||
出 pass 没记录过的样本)。写回要除以
|
||||
@@ -102,6 +113,22 @@ on_progress)`(`chain_spec_for` 从录像自己的 `session.json` 取链路,
|
||||
`test_the_replay_reset_is_the_display_reset_and_never_the_device` 失败过 —— 合成录像的配置
|
||||
缺失让 pass 在断言之前结束并覆盖了 `复位显示` 的提示;修的是消息落在哪里。
|
||||
|
||||
**深度那一片加的验收**(同一个文件再 +8 项,另加 `shared/tests/test_analysis_depth.py` 10 项):
|
||||
|
||||
* **深度往返**:另跑一遍链路,逐帧比 `depth_map`,误差上界是该帧自己的 `(max − min) / 510`,
|
||||
在 box 录像(33 帧,1 帧非空、76 px)和 264 帧的 `long_8x.mp4`(241 帧空、23 帧非空、
|
||||
最坏 336 px)上都是 0 失败;非零计数、min/max 与链路的完全相等。
|
||||
* **跳转等于顺序播放(深度模式)**:把上面那条测试扩到深度 —— 顺序播放记下每帧渲染出的深度
|
||||
图,再跳到 5 个位置,帧 id、`depth`、统计与**渲染出的字节**逐项相等。
|
||||
* **尺寸**:空帧 4,048 B(固定 4,042 + 6 B 载荷头),最坏非空帧 4,534 B(+486 B);
|
||||
按实测比例 4,090 B/帧,十分钟 70.2 MB(上限 81.6 MB)。断言写死了这三个数。
|
||||
* **模式切换不写结果**:四个模式各画一次,`scan_results(cache)` 的目录集合不变。
|
||||
* **"没什么可画"只在该说的时候说**:无结果时 `plan_image` 只给 `recorded` 并带
|
||||
`replay_raw_note`;有结果时该行消失、四个模式都在,默认仍是原始图像。两种语言各断言一次,
|
||||
且文案必须点出产生结果的动作。
|
||||
* **派生模式走结果**:时间轴在录像上、模式选深度时,送到 2D 出口的是**结果那一帧**(含深度),
|
||||
不是解码出来的视频帧;切回"原始图像"仍是录像自己的画面。
|
||||
|
||||
真实 GL 快照(`QT_QPA_PLATFORM=windows` + `QT_OPENGL=desktop`,真实窗口路径、pass 写入
|
||||
scratch cache、游标停在存下来的帧 id 上):
|
||||
|
||||
@@ -115,11 +142,27 @@ scratch cache、游标停在存下来的帧 id 上):
|
||||
二维画面是候选网格上的暗点阵(该帧活动样本很少),三维是手指岛面加同一套样本点;"同一个样本
|
||||
集"由上面的逐元素断言保证,快照证明这条链路真的走到了 GL。
|
||||
|
||||
**深度那一片的快照**(`.scratch/replay-analysis-a2/snapshots/`,同一个窗口、同一个游标、
|
||||
同一个模式选择器;时间轴停在录像上,所以"原始图像"是真的相机图,两个派生模式读结果):
|
||||
|
||||
| 文件 | 内容 |
|
||||
|---|---|
|
||||
| `analysis-finger-frame-33-2d-raw.png` / `-arrows.png` / `-depth.png` | finger 链路帧 33,深度非零 76 px(峰值 0.0242) |
|
||||
| `analysis-box-frame-27-2d-raw.png` / `-arrows.png` / `-depth.png` | box 链路帧 27,`depth_map` 365×365 **恰好全零**,面板显示"本帧深度全为 0(133225 px 中 0 个非零)" |
|
||||
|
||||
同一帧三张图的大小不同(raw 96.6 kB 是相机图,arrows 4.9 kB,depth 4.2 kB),这本身就是
|
||||
"三个模式画的是三样东西"的证据。
|
||||
|
||||
## 15. 已知限制
|
||||
|
||||
* 结果里没有力,也没有深度:力曲线始终读记录的 `forces.csv`,深度视图在回放模式下不可用(没有
|
||||
可显示的数据,面板按空处理而不是画一张假的)。
|
||||
* 结果里没有力:力曲线始终读记录的 `forces.csv`。深度有了(第 1、11 节),但**三维面板仍只
|
||||
拿光流**:`session.packet()` 的 `depth` 依旧是 `None`、`depth_valid` 依旧是 `False`,
|
||||
因为把深度接进三维会改变三维图形的构成,而那不在这一片里。二维深度视图是这一片交付的东西。
|
||||
* 结果覆盖的帧是 pass 走过的那些:录像比结果长时游标可以离开结果范围(面板说"这个分析结果里
|
||||
没有可显示的帧"),不会静默重复最后一帧。
|
||||
* 栅格类显示设置不生效(第 12 节);换栅格就是重跑一遍,那是**另一个键**。
|
||||
* 一个进程同时只有一遍分析在跑;采集会话运行期间不启动分析。
|
||||
* 时间轴与模式选择是两件事,而"有没有结果"是录像的属性、不是游标的属性:
|
||||
`ReplayController.available_result()` 查的是**本录像**在 `ResultLibrary` 里的那一份,所以
|
||||
即使 `ReplayEngine.use_recording()` 让引擎不再借用 session(目前 UI 没有第二条路会走到
|
||||
这个状态),二维面板照样能画箭头和深度,`replay_raw_note` 也不会重新出现。
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
回放过去等于每移动一次光标就重跑一遍 SDK 链路:**每个视频秒 0.48–0.51 秒墙钟**,十分钟录像
|
||||
一次约 4.8 分钟。Part A 把这一遍跑一次,只存回放真正要画的东西。
|
||||
|
||||
格式版本从 1 升到 2:帧记录里多了深度通道。旧结果不会被误读 —— 版本不符直接
|
||||
`AnalysisFormatError`,重跑一遍即可(结果本来就是可重算的)。
|
||||
|
||||
## 1. 结果模型
|
||||
|
||||
原始输出每帧 **1,176,000 B**(`flow` 672,000 + `depth_map` 336,000 + `valid_mask` 84,000 +
|
||||
@@ -15,19 +18,34 @@
|
||||
`select_flow_samples` 选出的样本。
|
||||
|
||||
**每帧记录**(`frame_id`、`t_rel_s`、网格 keyframe 编号、候选数、深度统计、`dx`、`dy`、
|
||||
`magnitude`、位打包 `active`、掩码摘要)在 shipped finger 网格下是 **4,034 B/帧**:
|
||||
`24 + 330×12 + ⌈330/8⌉ + 8`;十分钟 30 fps 约 70 MB。选这个编码而不是实测的另外两种
|
||||
(float16 的 2,022 B/帧、丢掉 `magnitude` 的 4,002 B/帧),是因为往返必须**逐位相等**,而
|
||||
`magnitude` 正是"同一份结果换个阈值也能用"的依据(第 3 节)。`dx`/`dy`/`magnitude` 存
|
||||
`magnitude`、位打包 `active`、掩码摘要、深度载荷)在 shipped finger 网格下是 **4,048 B/帧**
|
||||
(空深度):`32 + 330×12 + ⌈330/8⌉ + 8 + 6`;十分钟 30 fps 约 70 MB。选这个编码而不是实测的
|
||||
另外两种(float16 的 2,022 B/帧、丢掉 `magnitude` 的 4,002 B/帧),是因为往返必须**逐位相等**,
|
||||
而 `magnitude` 正是"同一份结果换个阈值也能用"的依据(第 3 节)。`dx`/`dy`/`magnitude` 存
|
||||
float32,与实时选择器比对时后者也要转成 float32 才是精确比较。
|
||||
|
||||
**不存的**:力(`forces.csv` 已是图表读的内容,180.9 B/帧);深度图只存每帧一个
|
||||
`depth_nonzero` 计数(4 B)加汇总统计 —— 这段素材上深度几乎全零(264 帧中 241 帧全零,最大
|
||||
336 px / 84,000),统计记下来"深度视图是空的"才有解释。
|
||||
**深度存下来,而且是量化后存的。** 单帧 `depth_map` 336,000 B,十分钟 6.0 GB —— 直接存不可行;
|
||||
实测这段素材上它绝大多数帧**恰好全零**(264 帧的八倍速片段里 241 帧,box 录像 33 帧里 32 帧),
|
||||
最坏的非空帧也只有 84,000 px 中的 336 px。
|
||||
|
||||
所以每帧固定记下**深度通道的统计**(是否有通道、
|
||||
非零计数、最小/最大值,共 8 B)并附一个深度载荷:空帧只有一个 6 B 的载荷头(声明形状,
|
||||
指明"这一帧的深度是精确的零"),非空帧是**按该帧自己的 [min, max] 量化的 uint8 平面**再
|
||||
zlib level 6。
|
||||
|
||||
实测代价:空帧 +14 B,最坏非空帧 486 B;能在往返中引入的最大误差是半个量化步长
|
||||
`(max − min) / 510`,实测 6.6e-5(该帧峰值 0.034)。这不到深度视图所用 256 级色标的一步,所以
|
||||
重建出来的帧**不可能被染成与原帧不同的颜色**。
|
||||
|
||||
稀疏存储实测过并否决:每像素 4 B 索引在最坏帧上要 910 B,而整幅量化平面只要 486 B —— 一片几乎
|
||||
全零的字节平面在 zlib 下几乎免费,像素索引不是。限制在岛内也实测过并否决:418 B 对 486 B,
|
||||
在 4,048 B 的记录里省 5.2 B/帧,代价是重建要依赖有效性掩码 keyframe(box 链路根本没有),
|
||||
而且一旦有深度值落在岛外就会**静默丢掉一个测量**。
|
||||
|
||||
`provenance` 随结果存:算法版本、传感器配置及其内容哈希、标定 id、单位、参考帧 id、首个输出
|
||||
帧 id、解码偏移、逐通道的"重算/记录"标记(`flow`、`depth` 重算,`forces` 记录)、`complete`
|
||||
标志与计数。
|
||||
标志与计数。`counts` 另记 `depth_frames` / `depth_nonzero_frames` / `depth_nonzero_max` /
|
||||
`depth_pixels` 与实测的 `frame_record_bytes`,进度面板和"这一帧为什么是空的"都读它。
|
||||
|
||||
## 2. Keyframe:候选位置与有效掩码
|
||||
|
||||
|
||||
Reference in New Issue
Block a user