record the full experiment: video, CSV and metadata
Phase 1 of the review asked for one directory per experiment, a force CSV, video of the pre-processing image, and metadata that ties them together. The record channel from the previous change already kept the whole session; this adds the on-disk format. What - Add record/experiment.py: ExperimentRecorder writes one directory per session containing session.json, forces.csv, frames.csv, events.csv, raw_NNNN.mp4 (segmented) and an optional lossless PNG sequence. - forces.csv uses the reviewed column order (session_id, frame_id, t_rel_s, FN_raw, FX_raw, FY_raw, FN_N, FX_N, FY_N, valid, calibration_id) and leaves a cell empty when a value is unavailable: an uncalibrated newton value or a failed readout is never written as 0. - frames.csv is the frame-index mapping: frame_id, t_rel_s, acquisition wall clock, segment, segment_frame, in_video. forces.csv and frames.csv share the packet frame_id and the same t_rel_s origin, so video and data can be aligned after dropped or segmented frames. - session.json carries format version, session id, start/end time in UTC, device and config (with a sha256 of the config file), app/shared/SDK versions, calibration reference, axis convention, filter parameters, video codec and the lossy-MP4 caveat, plus the event log and end-of-run statistics. It is written once at start so a crash still leaves identifiable metadata. - Add a record control to the toolbar: mode (video+data / data only) and a start/stop toggle that stays enabled while recording. - Log zeroing, calibration application and session end as timestamped events. Why - The recorder is fed from the worker sink list on the acquisition thread and only appends to a bounded queue, so disk I/O can never stall acquisition. - On_packet copies the raw frame instead of holding a reference: the packet frame may alias a buffer the SDK reuses between frames. - A queue overflow is counted and alarmed (rate limited), and a write or encode failure is counted, reported and after 5 consecutive errors stops the recording with failed=true rather than silently losing data. Verification - shared/tests/test_experiment_recorder.py: 12 cases covering the directory layout, the CSV contract, frame-id/time-base agreement, segment rolling, data-only and lossless modes, queue overflow accounting, encoder failure, metadata fields, crash-before-stop, and that on_packet never blocks. - tools/ui_check.py gains an "experiment record" section (169 checks, 0 failures overall), driving a recording through the UI and asserting the layout, the empty-cell rule and the event sequence. - apps/studio/tests/test_ui_smoke.py mirrors those invariants. - docs/record-channel.md documents the format and the remaining phase-2 work. Not in this change: CSV export of a selected time range, full-session curve browsing, calibration diagnostics (outlier exclusion, fit residuals) and PyOpenGL 3D.
This commit is contained in:
@@ -104,11 +104,14 @@ cd D:\orisys_ws\orisys-app\apps\studio
|
||||
|
||||
- `shared/tests/test_record_channel.py`:11 项,覆盖内存上界、双单位、无效隔离、
|
||||
跨线程 feed、截止时间结算、取消语义。
|
||||
- `shared/tests/test_experiment_recorder.py`:12 项,覆盖目录结构、CSV 列序与空值规则、
|
||||
帧号/时间基准一致、分段滚动与帧索引、仅数据模式、无损序列、队列溢出计数与报警、
|
||||
编码失败上报并停止录制、元数据字段、崩溃后仍留有元数据、`on_packet` 不阻塞。
|
||||
- `shared/tests/test_worker_cancellation.py`:4 项,覆盖非阻塞 stop、线程所有权、
|
||||
会话交接。
|
||||
- `tools/ui_check.py`:新增 `record channel` 段落,断言记录通道在**完全没有 UI 定时器
|
||||
触发**的情况下仍收满样本、隐藏标定面板仍能采样、录制按钮是可用取消入口、
|
||||
深度缺失不回退、色标锁帧。
|
||||
- `tools/ui_check.py`:新增 `record channel` 与 `experiment record` 两段,断言记录通道
|
||||
在**完全没有 UI 定时器触发**的情况下仍收满样本、隐藏标定面板仍能采样、录制按钮是可用
|
||||
取消入口、深度缺失不回退、色标锁帧,以及通过界面录制的目录结构与事件序列。
|
||||
- `apps/studio/tests/test_ui_smoke.py`:上述不变量各有对应 pytest 用例,复用
|
||||
`ui_check._StuckWorker` 与 `ui_check._fake_packet`,保持单一来源。
|
||||
|
||||
@@ -120,11 +123,52 @@ cd D:\orisys_ws\orisys-app\apps\studio
|
||||
|
||||
---
|
||||
|
||||
## 6. 尚未完成(第二阶段及以后)
|
||||
## 6. 实验录制(第一阶段剩余部分)
|
||||
|
||||
- MP4 分段录制、`frames.csv` 帧索引、`session.json` 元数据(评审第一阶段剩余部分)。
|
||||
- CSV 导出(`session_id/frame_id/t_rel_s/FN_raw/…/valid/calibration_id`)——数据模型
|
||||
`SessionSample` 已按这些字段设计,`SessionStore.iter_valid()` 已可直接供导出使用。
|
||||
`shared/src/orisys_app_shared/record/experiment.py` 的 `ExperimentRecorder` 落盘格式:
|
||||
|
||||
```
|
||||
outputs/recordings/<UTC 时间戳>_session<NNNN>/
|
||||
session.json 元数据 + 事件 + 结束统计
|
||||
forces.csv 每帧一行
|
||||
frames.csv 帧索引映射(帧号 ↔ 视频分段)
|
||||
events.csv 带时间戳的操作事件
|
||||
raw_0001.mp4 处理前相机图像,按需分段
|
||||
frames/ 可选无损 PNG 序列
|
||||
```
|
||||
|
||||
`forces.csv` 列序固定为
|
||||
`session_id, frame_id, t_rel_s, FN_raw, FX_raw, FY_raw, FN_N, FX_N, FY_N, valid, calibration_id`。
|
||||
|
||||
几条硬性约束:
|
||||
|
||||
- **一次实验一个目录**,`session.json` 在**开始时先写一次**,因此即使进程崩溃也留有可识别
|
||||
的元数据;停止时再写一次并补上 `ended_at`、事件与统计。
|
||||
- **不静默丢数据**:`on_packet` 只把标量(以及开启视频时的一帧拷贝)放进**有界队列**,
|
||||
队列满则计数并报警,绝不阻塞采集;连续写失败达到 `MAX_CONSECUTIVE_ERRORS` 就停止录制
|
||||
并把 `failed` 置位,而不是假装录完了。
|
||||
- **帧号与时间统一**:`forces.csv` 与 `frames.csv` 共用 packet 的 `frame_id`,`t_rel_s`
|
||||
同源于首帧;`frames.csv` 额外记录采集墙钟时间、落在哪个分段、以及是否进过视频,
|
||||
这就是丢帧/分段场景下的重建依据。
|
||||
- **未标定或读取失败的单元格留空**,不写 0:`FN_N` 等列在无标定时为空,`valid=0` 的行
|
||||
所有数值列都为空。精度用 `repr(float)`(最短往返表示),界面小数位不影响落盘。
|
||||
- **MP4 是有损封装**:`session.json` 的 `video.lossy_note` 明确记录这一点,需要算法复现时
|
||||
可开启 `lossless_frames` 输出 PNG 序列。
|
||||
- `raw_frame` 在入队时**必须拷贝**:packet 里的帧可能复用 SDK 的采集缓冲,直接持有引用
|
||||
会让录制内容随后续帧改变。
|
||||
|
||||
### 已知环境限制
|
||||
|
||||
本机未安装 pytest,且沙箱拒绝写入工作区之外的路径,`conftest.py` 的 `tmp_path` 因此
|
||||
写 prefs 失败。上述 pytest 用例是用等价驱动脚本执行的(参数化用例除外);`ui_check.py`
|
||||
覆盖同样的不变量且可完整运行。录制相关的测试把临时目录放在
|
||||
`shared/tests/_scratch/`(已加入 `.gitignore`)以避开该限制。
|
||||
|
||||
---
|
||||
|
||||
## 7. 尚未完成(第二阶段及以后)
|
||||
|
||||
- CSV 的「导出选中时间段」(整段导出已随录制落盘;区间导出待做)。
|
||||
- 全程曲线浏览(跟随/历史、框选缩放、降采样保峰值)。
|
||||
- 标定诊断扩展:稳定后采样、异常点排除/恢复、拟合残差、独立验证点。当前
|
||||
`CalibrationPoint` 已记录三轴均值/标准差/有效样本数、方向、轮次与实际采样时长。
|
||||
|
||||
Reference in New Issue
Block a user