From 0e34cbfcdb349d705a721bb52a3ec60244785199 Mon Sep 17 00:00:00 2001 From: ykcheungab Date: Sun, 11 Oct 2026 11:47:07 +0800 Subject: [PATCH] =?UTF-8?q?=E9=99=90=E5=AE=9A=20Autocal=20=E9=A6=96?= =?UTF-8?q?=E7=89=88=E4=B8=BA=20finger=20=E5=B9=B6=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E8=A7=86=E9=A2=91=E9=87=87=E9=9B=86=E8=B7=AF=E5=BE=84=20?= =?UTF-8?q?=E6=98=8E=E7=A1=AE=E8=A7=86=E9=A2=91=E4=B8=8E=E7=9B=B8=E6=9C=BA?= =?UTF-8?q?=E5=85=B1=E7=94=A8=E9=87=87=E9=9B=86=E6=8E=A5=E5=8F=A3=E3=80=81?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E5=92=8C=E6=97=B6=E9=97=B4=E5=9F=BA=E5=87=86?= =?UTF-8?q?=EF=BC=9B=E8=A1=A5=E5=85=85=E9=92=88=E5=AF=B9=E6=80=A7=E5=9B=9E?= =?UTF-8?q?=E5=BD=92=E6=B5=8B=E8=AF=95=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/calibration-data-format.zh.md | 57 ++++++++++++++++++- apps/autocal/docs/implementation-plan.zh.md | 18 +++--- .../tests/test_stream_pipeline_acquisition.py | 31 ++++++++++ 3 files changed, 94 insertions(+), 12 deletions(-) diff --git a/apps/autocal/docs/calibration-data-format.zh.md b/apps/autocal/docs/calibration-data-format.zh.md index 2b5c464..233ca48 100644 --- a/apps/autocal/docs/calibration-data-format.zh.md +++ b/apps/autocal/docs/calibration-data-format.zh.md @@ -1,15 +1,17 @@ # Autocal 标定操作与数据格式草稿 -状态:待评审,2026-10-10。用途:方案及操作指南。近期目标是跑通多测点法向采集、保存和离线重算;下列操作是首版目标流程,尚非全部可执行的软件说明。 +状态:待评审,2026-10-11。用途:方案及操作指南。近期目标只针对 finger 传感器跑通多测点法向采集、保存和离线重算;下列操作是首版目标流程,尚非全部可执行的软件说明。 ## 范围与软件现状 | 项目 | 当前约定 | |---|---| | 标定范围 | 多测点法向力,暂定 0–30 N;剪切及组合载荷后续再做 | +| 传感器 | 首版固定为 finger;其余类型须另行验证几何、采集和处理契约 | | 压头 | 直径 6 mm、球面半径 3 mm 的半球压头 | | 参考 | 砝码或 50 N/2 N·m FT sensor,首版先接通一种 | | 图像 | 一个完整测试事件一个连续视频,档位间不切段 | +| 调试输入 | 先用已有视频接通 finger 采集链;实时相机随后复用同一个 worker 和 `SdkPacket` 消费路径 | | 交付 | 单个 session 能重新打开、追溯参考载荷并离线重算 | [LF606 规格书](references/lf606-d40-h18-4x-spec.pdf)量程表包含 50 N/2 N·m 档位,但图签为 200 N/8 N·m;实机以铭牌及标定证书核对。 @@ -57,7 +59,7 @@ | 文件 | 主要字段/数据 | 写入时机 | |---|---|---| -| `session.json` | `format_version`、`session_id`、身份、配置快照、坐标定义、文件清单、sites、trials、状态 | 会话建立、事件完成及结束时 | +| `session.json` | `format_version`、`session_id`、`sensor_type`、`input_source`、身份、配置快照、坐标定义、文件清单、sites、trials、状态 | 会话建立、事件完成及结束时 | | `sequence.oryseq` | 实际使用的测点计划;遵循现有格式 | 开始采集前保存快照 | | `video/.` | 算法入口图像、完整循环及恢复 | 每 trial 开始至结束 | | `frames.csv` | `session_id`、`trial_id`、`frame_id`、`t_rel_s`、视频路径、段内帧号、丢帧标记 | 每帧 | @@ -71,6 +73,7 @@ |---|---|---| | session 身份 | `sensor_sn`、`gel_id`、`bench_id`、`indenter_id`、`reference_source` | 操作员填写或设备读取 | | session 采集 | 分辨率、像素格式、编码、版本、滤波与归零配置 | 软件快照;编码待实测确认 | +| 输入来源 | `input_source.kind` 为 `video` 或 `camera`;`value` 分别为视频路径或相机整数编号;视频另存文件哈希 | 传给同一 `FingerAcquisitionWorker.start`;来源文件与 trial 输出视频不可混淆 | | session 时间 | 各流 `clock_id`、时间戳含义、偏移及同步误差 | 软件记录;不能按行号配对 | | site | `site_id`、UV、三维位置、几何关联 | pixel/mm;软件映射 | | trial | `trial_id`、`site_id`、重复编号、姿态、`baseline_id`、视频、起止时间、状态 | 软件分配并记录实际执行 | @@ -79,6 +82,54 @@ | step 砝码参考 | 质量、重力加速度、传力方向、有效时段及 `reference_load_n` | kg/m/s²/N;仅在传力条件成立时使用 m×g | | 质量 | `valid`、`invalid_reason`、丢帧/中断状态 | 未知不补零;算法失败不等于图像不可用 | +视频输入的现有 `SdkPacket.mono_t` 由帧号和配置帧率推算;相机输入使用采集期间的单调时钟。已检查的 DD02 示例视频约 20 fps,而配置为 30 fps;因此现有视频时间不能直接用于物理保持时长或 FT 对齐。P1 须读取来源视频的实际时间基准,核对配置帧率,并在不一致时拒绝时间对齐或明确标记失效。 + +两种输入都输出 `frame_id` 和 `raw_frame`,但不能把不同来源的时间戳当成同一物理时钟。离线视频若无同步的 FT/运动记录,只能验证图像链和文件关联。manifest 须保留来源路径、哈希、配置身份、配置/实际帧率及时间基准;正式会话还应保存现场取得的设备身份。 + +P0 事件边界如下。`events.csv` 每行至少保存 `event_id`、`trial_id`、可为空的 `step_id`、`event_type`、`t_rel_s`、`clock_id` 和 `reason`。事件只记录已发生的事实,不以 UI 按钮时间冒充到档或写盘完成。P1/P2 用同一 `trial_id` 将视频帧、参考值和事件相连。 + +| 事件 | 触发与状态 | +|---|---| +| `trial_started` | 首帧可用且输出已打开后,trial 进入 `recording` | +| `step_reached` | 操作员确认到档或有效反馈确认;记录实际时刻和参考有效性 | +| `step_ended` | 本档保持及分析窗口结束,保留实际持续时间 | +| `trial_finished` | 视频与索引排空并通过完整性检查后,trial 进入 `complete` | +| `trial_aborted` | 输入、写入或操作异常;trial 进入 `incomplete`,保留已写数据及原因 | + +`session` 在所有 trial 完成并检查文件后才进入 `complete`;任何缺失必须保留 `incomplete` 与原因。每次重试使用新的 `trial_id`,不覆盖旧视频。 + +以下是目标 `session.json` 的最小示意,供 P1/P2 共用字段;`video/` 是本次 trial 的输出,不是 `input_source` 原文件。例子不代表写入器已经实现。 + +```json +{ + "format_version": "0.1-draft", + "session_id": "session-001", + "sensor_type": "finger", + "input_source": { + "kind": "video", + "value": "D:/samples/finger-input.mp4", + "sha256": "<待采集时计算>", + "clock_id": "video_frame_rate", + "configured_fps": 30.0, + "source_fps": 30.0 + }, + "config_ref": "config/sensors/finger/sensor.json", + "status": "incomplete", + "sites": [{"site_id": "site-001", "uv_xy": [120.5, -4.0], "position_sensor_mm": [1.2, 0.8, 3.4]}], + "trials": [{ + "trial_id": "trial-001", + "site_id": "site-001", + "baseline_id": "baseline-001", + "video": "video/trial-001.mp4", + "status": "incomplete", + "steps": [ + {"step_id": "step-001", "branch": "load", "target_load_n": 5.0}, + {"step_id": "step-002", "branch": "unload", "target_load_n": 0.0} + ] + }] +} +``` + ## 软件数据流与责任 ### 当前实际链路 @@ -96,7 +147,7 @@ |---|---|---| | 1 | 操作员设置 + `.oryseq` → sites/载荷路径 → session 快照 | Autocal 会话与事件控制 | | 2 | UV + 固定映射 → mesh 位置 + 平台姿态 → site/trial | 几何及现有姿态计算能力 | -| 3 | 相机 → SDK 采集包 → 视频、帧索引 | 复用 shared 采集与记录能力 | +| 3 | 视频路径优先、相机编号随后 → 同一 `FingerAcquisitionWorker` → `SdkPacket` → trial 视频、帧索引 | 复用 shared 采集能力;Autocal 不导入 Studio 的 UI 或控制器 | | 4 | FT/砝码 + 平台反馈 + 档位事件 → 带时间的数据流 | Autocal 参考输入及事件控制;与图像并行 | | 5 | 事件结束 → 完成写入 + 完整性检查 → trial 状态 | 文件记录器及会话控制 | | 6 | 视频解码 + 基准/配置 → SDK 特征重算;时间对齐参考 → 拟合/验证 | 离线处理,结果存 derived | diff --git a/apps/autocal/docs/implementation-plan.zh.md b/apps/autocal/docs/implementation-plan.zh.md index 5b0f737..a319d76 100644 --- a/apps/autocal/docs/implementation-plan.zh.md +++ b/apps/autocal/docs/implementation-plan.zh.md @@ -1,16 +1,16 @@ # Autocal 最小标定流程实施计划 -状态:待评审,2026-10-11。用途:方案。建议使用两个实施子代理,主代理负责接口、UI 和集成;本计划不启动代理、不实施设备动作。需求以[操作与数据格式草稿](calibration-data-format.zh.md)为准。 +状态:P0 契约和离线验证完成,2026-10-11;P1–P5 待实施。用途:方案。后续完整实施建议两个子代理;当前不启动代理或设备动作。需求以[操作与数据格式草稿](calibration-data-format.zh.md)为准。 ## 首版边界 | 交付 | 首版选择 | |---|---| -| 采集对象 | 先支持一种实际传感器配置及相机后端,不承诺所有型号 | +| 采集对象 | 固定为 finger;先以视频文件输入验证,再用实时相机输入验证同一路径 | | 测点 | 复用现有选择和姿态能力;UV pixel→3D mm,保存映射身份 | | 参考输入 | 人工确认砝码档位,记录质量、传力条件及有效时段;FT 自动采集另估 | | 载荷循环 | 操作员逐级加载卸载,软件记录到档、保持及恢复;暂不做闭环力控制 | -| 视频 | 每 trial 一个独立连续视频,含必要预录/基准;编码必须通过本机回放验证 | +| 视频 | 输入视频用于无设备调试;每 trial 的输出视频独立连续,含必要预录/基准;编码必须通过本机回放验证 | | 复核 | 重新打开 session,按 step 选择窗口,重算特征并对照参考载荷 | | 验证 | 独立 trial 的法向拟合误差及加载卸载差异;不承诺未经验证的实时精度 | @@ -24,7 +24,7 @@ |---|---|---| | `apps/autocal/src/orisys_autocal/sequence_io.py` | `.oryseq` 校验与保存 | trial/step 载荷路径 | | `sensor/export_bundle.py`、`sensor/uv_flat.py`、`motion/kinematics.py` | 几何、UV 与姿态能力;bundle 已有物理尺度处理 | 验证计算管线 UV 与预览映射、坐标尺度一致 | -| `shared/src/orisys_app_shared/acquisition/` | 采集 worker、帧号、时间、sink | Autocal 生命周期与最小预览接入 | +| `shared/src/orisys_app_shared/acquisition/` | `FingerAcquisitionWorker.start` 接受视频路径或相机整数编号,两者经同一 finger 处理链输出 `SdkPacket` | Autocal 生命周期与最小预览接入;视频与相机分别验证时间基准 | | `shared/.../record/experiment.py`、`video_writer.py` | 有界队列、后台写入、丢帧及错误处理 | 当前为滚动 MP4;事件视频、编码和新 manifest 不可直接视作已支持 | | `apps/studio/src/orisys_studio/recording.py`、`replay_analysis.py` | 已有记录组织和回放经验 | Autocal 不应直接依赖 Studio 私有模块;需要时仅提取共同核心到 shared | | Autocal `ui/window.py`、`ui/workspace.py` | 设备和页面装配 | 运行入口未实现,采集及标定点页占位 | @@ -38,14 +38,14 @@ Studio 从 `app_window._start_run` 读取选择、解析 config、设置滤波 | Studio 入口/现有位置 | Autocal 处理 | 原因及验证 | |---|---|---| | `shared/sensors` 的 `scan_sensor_catalog`、`CatalogEntry` | 直接调用,保存选择的 config 路径及身份 | 目录解析本来由 shared 拥有;拒绝不可运行的条目 | -| `shared/acquisition` 的 `FingerAcquisitionWorker`、`SdkAcquisitionWorker`、`SdkPacket`、`RecordSinkMixin` | 首版只实例化所选类型的 worker;使用公开 `start/stop/latest/add_sink/remove_sink` | 同一采集算法、帧号、时间和基准预录;验证首帧、断流、停止后 sink 解绑 | +| `shared/acquisition` 的 `FingerAcquisitionWorker`、`SdkPacket`、`RecordSinkMixin` | 首版只实例化 finger worker;视频路径或相机整数编号均调用公开 `start/stop/latest/add_sink/remove_sink` | 同一采集算法、帧号和基准预录;验证各自时间基准、首帧、断流、停止后 sink 解绑 | | `Studio/ui/sensor_picker.py` | 首版在 Autocal 用简短选择控件调用 shared catalog;不搬 Studio widget | widget 用 Studio 文案和状态栏布局;只有界面行为真正一致时再迁入共享 UI | | `Studio/ui/sensor_dialog.py` | 留在 Studio | 绑定 Studio window、校准文件热应用和面板;Autocal 只需本次原始采集的配置 | | `Studio/session_controller.py` | 留在 Studio;Autocal 写仅管理所选 worker 的薄协调器 | 类内有 `QTimer`、Studio 状态、双 worker、记录计数;整类移动会改变现有生命周期 | | `Studio/recording.py` | 复用底层 `shared/record`,新建 Autocal trial 协调;不移动整个类 | 现有类含 Studio session store、力阈值和标定采样器,语义不同 | | `Studio/session_metadata.py` 中 `capture_metadata`、`filter_metadata`、`video_fps` | 两端都需要时提取到 shared;Studio 保留兼容导入并验证输出相同 | 通用的设备事实可共享;Studio 偏好设置、显示和文案仍留本地 | -接线顺序:先确认选中 config 能打开对应 worker;再接一个只入队的 packet sink,UI 从 `latest()` 取预览;获得首帧后允许创建 trial,开始视频前取预录;结束时先停试验写入并等待完成报告,再停采集,关闭窗口前解绑 sink。若设备打开失败或写入失败,状态为失败,不能产生“已完成” trial。运动服务继续独立管理平台,采集协调器不下达运动命令。 +接线顺序:先用视频路径和 finger config 打开 worker;再接一个只入队的 packet sink,UI 从 `latest()` 取预览。相机接入时只替换输入值,不另建处理或记录分支。获得首帧后允许创建 trial,开始输出视频前取预录;结束时先停试验写入并等待完成报告,再停采集,关闭窗口前解绑 sink。若输入打开失败或写入失败,状态为失败,不能产生“已完成” trial。运动服务继续独立管理平台,采集协调器不下达运动命令。 只有当 Autocal 和 Studio 实际出现相同的 worker 生命周期代码时,才将这部分无 widget 逻辑抽到 `shared/acquisition`,两端改为调用同一实现;不为本次仅一个调用方预建框架。抽取须保留 Studio 的非阻塞停止、首帧才进入 running、reset 生效事件和 capture drop 计数。配对运行 Studio session/sensor catalog/recording 测试与 Autocal 生命周期测试。 @@ -55,8 +55,8 @@ Studio 从 `app_window._start_run` 读取选择、解析 config、设置滤波 | 包 | 内容及可观察交付 | 负责人/写入范围 | 依赖 | 人时 | |---|---|---|---|---:| -| P0 | 固定 schema、传感器选择/worker 接口、事件 API、时间原点、状态及示例 session;检查编码 | 主代理:契约、共享模型声明 | 无 | 4–6 | -| P1 | 单 worker 生命周期、基准/预录、trial 视频、CSV/manifest 写入、排空与失败状态;必要的元数据辅助函数迁移 | A:shared acquisition/record、Autocal `sensor/` 采集服务、Studio 兼容导入及测试;不改 UI | P0 | 13–19 | +| P0 | 固定 finger schema、视频/相机共用 worker 契约、事件 API、时间原点、状态及示例 session;验证输入路径并检查输出编码 | 主代理:契约及现有 worker 的针对性验证 | 无 | 4–6 | +| P1 | 视频输入先接通单 worker 生命周期、来源帧率核对、基准/预录、trial 输出视频、CSV/manifest 写入、排空与失败状态;再以相机输入跑同一路径;必要时迁移元数据辅助函数 | A:shared acquisition/record、Autocal `sensor/` 采集服务、Studio 兼容导入及测试;不改 UI | P0 | 13–19 | | P2 | session/site/trial/step 控制、人工载荷确认、阶段时间及状态约束 | B:Autocal 无 Qt 的标定控制及测试 | P0 | 8–12 | | P3 | 最小会话表单、测点表、预览、开始/到档/结束/中止、状态显示 | 主代理:Autocal UI、配置及集成入口 | P0 后可用桩;实接 P1/P2 | 8–12 | | P4 | 新格式读取、逐事件回放、基准恢复、窗口特征重算、最小拟合与误差表 | B:离线处理及测试;共享提取先协调 | P2;集成依赖 P1 样本 | 6–10 | @@ -113,4 +113,4 @@ A 独占新格式文件写入与 Autocal `sensor/` 采集服务,B 独占 Autoc | 回归 | Autocal 现有测试、shared 记录/生命周期/回放测试、Studio 相关消费者通过;GUI 与硬件证据分别报告 | | 完成 | 文档和 lint 门禁通过;实际编码吞吐、容量及误差有证据;缺失设备不报通过 | -首版通过 G1/G2 后停止扩展。实施开始前先确认所用 sensor_type、参考方式及设备可用性,决定是否把 FT 增量纳入关键路径。 +首版通过 G1/G2 后停止扩展。当前 `sensor_type` 固定为 finger。P0 已用假设备验证视频路径/相机编号经同一 worker 输出,并用现有 DD02 视频验证真实 SDK 处理链的首批帧;`mp4v` 在本机可写入及重新解码 30 帧。示例 session 与时间语义见数据格式草稿。该视频约 20 fps 而配置为 30 fps;P1 必须处理时间不一致,才可对齐参考力。尚未验证真实相机、长时写入及图像保真度;Autocal UI 与 trial 写入也尚未接通。参考方式及设备可用性决定是否把 FT 增量纳入后续关键路径。 diff --git a/shared/tests/test_stream_pipeline_acquisition.py b/shared/tests/test_stream_pipeline_acquisition.py index 4c6ef93..18dda04 100644 --- a/shared/tests/test_stream_pipeline_acquisition.py +++ b/shared/tests/test_stream_pipeline_acquisition.py @@ -250,6 +250,37 @@ def _config(kind: str) -> Path: return FINGER_CONFIG if kind == "finger" else BOX_CONFIG +@pytest.mark.parametrize("source", ["recording.mp4", 0]) +def test_finger_video_and_camera_use_the_same_packet_path(source, monkeypatch): + """Both source forms reach FingerRuntime and publish the same packet contract.""" + opened: list[str | int] = [] + + def open_runtime(cls, **kwargs): + opened.append(kwargs["video"]) + return cls() + + saved = _install() + monkeypatch.setattr(_Runtime, "open", classmethod(open_runtime)) + worker = _worker("finger") + packets: list = [] + try: + worker.add_sink(packets.append) + worker.start(source, config_ref=FINGER_CONFIG) + deadline = time.perf_counter() + 5.0 + while time.perf_counter() < deadline and not packets: + time.sleep(0.02) + assert opened == [source] + assert packets + packet = packets[0] + assert packet.raw_frame.shape == (8, 8, 3) + assert packet.processed_frame.shape == (8, 8, 3) + assert packet.session_id == 1 + assert packet.frame_id == 0 + finally: + worker.stop(wait=True) + _restore(saved) + + @pytest.mark.parametrize("kind", ["box", "finger"]) def test_a_stopped_session_leaves_no_pipeline_threads(kind): """``stop`` must join the three stages: they own the sensor's tracker state."""