Fix the phase-1/2 review findings in recording, calibration and depth replay
What - record: one packet->row conversion (record/sample.py). A failed compute chain records no force value at all, a newton value is written only for an axis that really has a calibration curve, and mono_t (not the wall clock) orders history with a per-row calibration hash. - record lifecycle: explicit starting/recording/stopping/stopped/failed states owned by the writer thread; stop() reports completed=False on timeout and does not write ended_at; consecutive write failures stop acceptance immediately. - record capacity: byte budget for the video queue (512 MiB default); a full queue sheds the *frame* and parks the tiny data row, so a video overflow no longer deletes CSV rows. Lost rows leave a dropped=1 index row and are counted as dropped_gaps / unrecorded_gaps. - calibration identity: sha256 content hash instead of a path, minted from the calibration the sensor actually loaded (reported before the first frame) and confirmed by the acquisition thread; events are timestamped when they happen. - calibration diagnostics: the fit summary, residual and validation error now come from the model that export/apply would use (piecewise_linear); validation points are held out of the build; repeated reference loads are merged with an n-weighted mean and pooled std. - calibration record: every point keeps its full three-axis sample series (frame id, time, raw, newtons, valid) plus diagnostics, written to an independent calibration_session_*.json that can be reopened for inspection; save record / export coefficients / apply to device are three separate actions. - depth replay: the cursor's frame is bound to the depth result by request sequence and frame id, so a moved cursor never shows another frame's map; the SDK is stepped from its current position instead of re-reading from the start; the live chain (box deformation+contact, finger unroll+tactile) is replayed with the exact resolved config path; algorithm state is rebuilt from a reset marker or from the recorded pre-roll, and says so when it cannot be. Why The review (D:/orisys_ws/_review_phase12/review.md) showed the app writing raw counts into the newton columns, recording a stale value after a failed compute, losing CSV rows when the video queue filled, reporting success while a writer was still running, diagnosing a model nobody applies, applying a calibration before the device confirmed it, dropping the samples behind a calibration point, showing a stale depth map under a moved cursor and recomputing depth of the wrong frame. Verification - D:/orisys_ws/_review_phase12/reproduce.py: all ten acceptance lines flipped (log saved as _review_phase12/reproduce_after.log). Including 25/25 rows kept with 8 video frames shed, frames [2]->[3], depth 8.0/8.0, fit 2/2 points. - apps/studio/tools/ui_check.py: 249 checks, 0 failures (twice). - shared suites: 91 tests, 0 failures (new: test_record_semantics, test_record_lifecycle, test_calibration_model, test_calibration_record, test_depth_replay). - apps/studio/tests/test_ui_smoke.py: 23/30; the 7 remaining are harness artifacts of the sandbox (4 tmp_path PermissionError, 3 parametrized tests the shim cannot drive), unchanged from before this change. - compileall clean; locale parity zh/en + t() key coverage: 0 failures. Not in this change - P2#10 disk-based browsing of a long recording and range CSV export (next round, per your choice). - Stability-triggered calibration sampling, replay fixed colour scale, PyOpenGL three-dimensional view. - Hardware verification: no camera attached, so the SDK's real processing chain and the pre-roll's effect on a live tracker were exercised with fakes only.
This commit is contained in:
@@ -166,6 +166,83 @@ outputs/recordings/<UTC 时间戳>_session<NNNN>/
|
||||
|
||||
---
|
||||
|
||||
## 7. 第一、二阶段评审 P1/P2 的整改(数据语义、终态、标定身份、深度重算)
|
||||
|
||||
评审脚本 `D:/orisys_ws/_review_phase12/reproduce.py` 的十条复现条件已全部翻转,改后
|
||||
输出见同目录 `reproduce_after.log`。以下按发现逐条说明改法。
|
||||
|
||||
### 7.1 记录语义(P1-1)
|
||||
|
||||
唯一转换入口 `shared/.../record/sample.py::to_record_sample`。规则:
|
||||
|
||||
1. 计算链失败(`valid=False` 或 `stale_forces=True`)的帧,六个力值**全部**记为不可用。
|
||||
SDK 的 `info.FNORMAL` 在计算失败时仍返回上一帧的数值,记录它等于伪造测量值。
|
||||
2. 只有**该轴确实加载了标定曲线**才写 N 值(`calibrated_axes` 按轴判断,而不是
|
||||
`force_calibrated` 这个传感器级标志)。未标定的轴留空。
|
||||
3. `mono_t` 是排序基准,墙钟只进元数据。`SdkPacket.mono_t/wall_t` 默认 `None` 而不是
|
||||
`0.0`:默认 0 会让所有帧的时间相同。
|
||||
4. 每行 `calibration_id` 取 **packet 自带版本**(采集线程在采集瞬间盖的哈希),
|
||||
会话级版本只作回退;一份 CSV 中途换标定也说得清。
|
||||
|
||||
### 7.2 记录终态与队列损失(P1-5、P1-6)
|
||||
|
||||
- 生命周期显式化为 `starting/recording/stopping/stopped/failed`;**写线程拥有终态与最终
|
||||
报告**,文件打开、写入、flush、close 全在异常边界内。连续写失败会立刻停止接收
|
||||
(`rejected_after_failure` 计数),`is_recording` 随之变假 —— 不会出现 failed=True 却
|
||||
仍在录制。`starting` 也算“录制中”,否则开始录制后立刻点停止会失效。
|
||||
- `stop(wait=True, timeout=…)` 超时返回 `completed=False` 的报告,**不写 `ended_at`**、
|
||||
不丢弃线程引用;未结束的 recorder 不允许再次 `start()`。
|
||||
- 队列按**字节预算**(`max_queue_bytes`,默认 512 MiB)而不是仅按条数:1920×1080 BGR
|
||||
排 2048 条副本约 11.9 GiB。预算用尽时**丢视频保数据行**;连条目队列都满时把数据行放进
|
||||
独立的 `_row_backlog`(默认 8192 行),因此视频溢出不再连带丢掉 CSV。
|
||||
- 真的丢掉的数据行会在 `frames.csv` 留下 `dropped=1` 的索引行,回放据此显示“该帧未录制”,
|
||||
而不是拿上一帧冒充。统计区分 `dropped_data`、`dropped_video`、`dropped_gaps`
|
||||
(已标记的缺口)与 `unrecorded_gaps`(连标记都放不下的)。
|
||||
|
||||
### 7.3 标定身份与生效确认(P1-7)
|
||||
|
||||
- 标定身份是**内容哈希**(`calibration_digest`,sha256),不是路径:同一路径可以重新导出
|
||||
不同系数。`Sensor` 打开时自带 bundle 标定,因此采集线程在**第一帧之前**就把实际加载的
|
||||
标定哈希报出来(`on_calibration_applied(path, version, frame_id)`),事件与每行版本都
|
||||
来自这次确认,而不是“已下发”的意图。
|
||||
- 事件在**发生时**打时间戳(`log_event(..., mono_t=…, frame_id=…)`),不再在写线程出队时
|
||||
重新计时。
|
||||
- 面板拆成三个独立动作:**保存标定记录 / 导出系数 / 应用到设备**。导出只写文件,
|
||||
应用需要再次确认并等设备确认后才记 `calibration_applied`。
|
||||
|
||||
### 7.4 标定诊断与实验明细(P1-2、P1-8)
|
||||
|
||||
- `CalibrationRecorder.fit` 用**将要导出的那个模型**(`build_export_model` + SDK 的
|
||||
`Calibration.convert`)计算残差与验证误差。分段线性模型会精确穿过它自己的标定点,
|
||||
所以训练残差恒为 0 —— 真正有意义的诊断是**留出验证点**的偏差。界面按“导出模型:分段线性
|
||||
(N 点 / M 个折点,最大残差)…”显示。
|
||||
- 验证点**不进入**构建(`CalPanel._rebuild_store` 跳过 `is_validation`),重复参考载荷按
|
||||
n 加权合并(`merge_samples`),不再“最后一个覆盖前面的”。
|
||||
- `CalibrationPoint.samples` 保存该点**全部**三轴采样(帧号、相对时间、三轴 raw、三轴 N、
|
||||
valid),连同排除/验证状态、方向、备注与实际时长一起写入独立的标定会话文件
|
||||
(`calibration_session_*.json`,`force/calibration_session.py`),并可用「打开标定记录」
|
||||
重新载入检查。界面上的降采样 trace 只是预览,不是审计数据。
|
||||
|
||||
### 7.5 深度重算与游标同步(P1-3、P1-4)
|
||||
|
||||
- **不再重开回放位置**:`DepthRecomputer` 记录当前已读到的段内帧号,请求第 2 帧再请求第 3
|
||||
帧只多读 1 帧(旧实现每次都从头读,2 和 3 会落到 5);往回退才重开视频。
|
||||
- 每帧都执行**实时同一条链路**:盒子 `compute_deformation + compute_contact`,手指再加上
|
||||
`FingerRuntime` 的展开与有效区域(`_BoxReplay` / `_FingerReplay`)。
|
||||
- `config_name` 用**完整解析后的配置路径**(`load_fixed_mode`),不再缩减成父目录名或
|
||||
stem —— 那可能加载同名但不同的配置,而哈希校验还在比对原路径。
|
||||
- **算法参考状态**:SDK 的跟踪器/滤波是有状态的,离线复现需要历史。两条可用依据:
|
||||
1. 操作归零(`zeroing` 事件,带帧号)会清空跟踪器,因此从该帧重放是**精确**的;
|
||||
2. 录制开始前采集线程保留 `PREROLL_FRAMES`(30 帧,且不超过 192 MiB)的预处理帧,
|
||||
录制开始时写入 `preroll_0001.mp4`,`session.json` 记 `preroll_frames` /
|
||||
`preroll_segment`,回放先把它们喂给 SDK 再进入首帧。
|
||||
都不满足时(跨分段、无归零、无预卷)仍然重算,但结果带
|
||||
`state_approximate` / `state_across_segment` 提示,**不假装与当时一致**。
|
||||
- 回放游标与深度结果按**请求序号 + 帧号**绑定:等待期间移动游标会作废旧请求,结果回来
|
||||
只渲染属于当前帧的那一张;否则面板显示「当前游标位置尚未重算深度」,绝不复用旧图。
|
||||
|
||||
---
|
||||
|
||||
## 7. 同步回放与深度重算
|
||||
|
||||
**决定:不保存深度。** 回放时按录像与配置**离线重算**。
|
||||
|
||||
Reference in New Issue
Block a user