feat: 将图像块拆分为可独立运行的 ImagePipeline

光流、HHD 和场从 Sensor 中抽到 ImageSettings/ImagePipeline,Sensor 与 FingerRuntime 改为委托,公开接口与数值不变(222 个基线数组逐位一致,含运行时 flow_ema_alpha 场景)。参考版本只有一个来源:StreamPipeline 在 flow 线程领取的版本逐帧交给图像块。对外数组为只读副本;输出带 stream-contract 的 MeasurementFrame 头。回归 357 项、lint 通过。
This commit is contained in:
ykcheungab
2026-10-21 13:35:02 +08:00
parent 474dbf5a64
commit e3b60f3d6d
6 changed files with 1306 additions and 58 deletions
+43 -4
View File
@@ -182,7 +182,7 @@ assert measurement.stamp == stamp
- 每个 Sensor 实例独立绑定,没有模块级共享缓存;创建或使用另一个 Sensor 不会改变本实例的上下文。
- 显式 `Sensor.set_calibration()` 换标定时原子替换上下文:SN/配置/资产绑定保持不变,标定路径与指纹更新;`set_calibration(None)` 后 `calibration_path` 立即为 `None`、指纹为 `"none"`,不保留构造时请求过的旧路径。
- 绑定异常只写入 `diagnostics`,不把原本可构造的 Sensor 变成构造失败。
- `assets`/`manifest_digest` 是 **manifest 声明的绑定**,不是运行时实际加载的路径:Finger 的导出目录由 `finger/exports.py` 在 cwd、打包目录、可编辑安装目录等候选中另行查找,实际加载路径由 T05 的图像块绑定;需要复现实验时以 T05/记录的身份为准。
- `assets`/`manifest_digest` 是 **manifest 声明的绑定**,不是运行时实际加载的路径:Finger 的导出目录由 `finger/exports.py` 在 cwd、打包目录、可编辑安装目录等候选中另行查找;T05 的 ImagePipeline 消费已准备帧,不重新选择资产,需要复现实验时以记录的身份为准。
- 资产取应用包内相对路径,不复制、不上传、不按顺序推断;配置指纹取实际加载后的配置内容,而不是文件名。
- 本任务(T04)不改变 Sensor 的数值与既有资产包解析行为,也不把上下文接入流入口;T05/T08 消费该上下文。
@@ -203,14 +203,53 @@ identity: FrameIdentity = context.frame_identity(
assert identity.sensor_id == "BOX-001"
```
## ImagePipeline(T05)
图像块(ImageSettings → 光流 / HHD / 场)已从 `Sensor` 抽成可独立运行的对象;`Sensor`、`FingerRuntime` 与 `StreamPipeline` 委托同一实现(Finger 的 UV 展开与采集仍留在运行时侧,`runtime.image_pipeline` 暴露该对象)。
| 组成 | 说明 |
|---|---|
| `ImageSettings` | 一次图像参数的声明式身份:tracker 配置、HHD 下采样/后端与 masked-Poisson 参数、光流 EMA 初值、cuda;`from_config(sensor.json)` 读取与 Sensor 相同的键。 |
| `ImagePipeline` | 拥有 tracker、nHHD、EMA 与 scratch 状态;`compute_flow()`、`decompose()`、`current_field()`;光流 EMA 系数与历史由传入的 `ForceReadout` 单一持有(独立使用时自建一个)。 |
| `ImageMeasurement` | T03 `MeasurementFrame` 头 + 光流/HHD/场/force 的只读副本;`frame.reference_generation` 由计算该帧的图像线程写入。 |
规则:
- 输入是**已准备**的帧(采集层完成 stitch/unroll/resize/mirror);独立实验直接喂灰度帧即可,不必构造 Sensor:`SensorContext` 给 SN/身份,`ImageSettings` 给图像参数。
- `include=` 选择要复制的数组(image/flow/vnormal/vshear/field/valid_mask);返回数组是独立副本且 `writeable=False`,发布后不会被后续帧覆写(K11.3)。
- 参考版本只有一个来源:驱动它的 `StreamPipeline` 领取版本后经 `set_stream_reference_generation()` 交给图像块(逐帧、计算前);独立实验把版本直接传给 `measure(reference_generation=...)`。`ImagePipeline` 不自己计数,`reset_image_state()`/`reset_tracker()` 只重置状态。
- 光流 EMA 的系数与历史属于 `ForceReadout`,因此已发布的 `Sensor.configure(flow_ema_alpha=...)` 从下一帧起生效(与拆分前一致);运行时修改后 `ImageSettings.flow_ema_alpha` 仍只表示构造时声明。
- 帧身份复用 T03 `FrameIdentity`/`FrameClock`,`measure()` 构造 `MeasurementFrame`;`Sensor` 的公开方法与数值不变(流入口 T08 才接 MeasurementFrame)。
```python
import numpy as np
from orisys import configs
from orisys.image_pipeline import ImagePipeline, ImageSettings, sensor_clock
from orisys.sensors import build_sensor_context
context = build_sensor_context(config_name="box", config=configs.load_config("box"),
device_id="BOX-001", bundle_candidates=["box"])
pipeline = ImagePipeline(ImageSettings.from_config(configs.load_config("box")), context=context)
prepared_grey_frame = np.full((365, 365), 120, dtype=np.uint8)
measurement = pipeline.measure(
prepared_grey_frame,
identity=context.frame_identity(session_id="run-1", frame_id=0, source_frame_id=0),
clock=sensor_clock(1_000_000, clock_id="host"),
include=("flow", "field"),
)
assert measurement.frame.reference_generation == 0
```
## 不包含什么
- 不实现 SensorStream 入口、实时最新选帧、完整回放和背压(T08)。
- 不实现图像块拆分、测量负载、ForceSession 重做、零点与输出滤波(T05–T07)。
- 不实现测量负载、ForceSession 重做、零点与输出滤波(T06–T07)。
- 不实现批次、配组、多流调度(后续多流计划)。
- 不实现录制/回放格式、Studio 迁移和离线重新滤波(T11、T13–T15)。
- 不修改现有 `Sensor`、`StreamPipeline` 和当前 `ForceSession`;现有 `ForceSession.FrameStamp` 在 T06 重做时被本合同的帧头替换。
- T05 只把 flow/HHD/场移到 `ImagePipeline`,不改变 `Sensor` 的公开接口与数值;当前 `ForceSession` 在 T06 重做时改用本合同的帧头。
## 验证
`tests/test_stream_contract.py` 覆盖 T03 的四项验收:逐路身份与时钟、组字段可缺但不半组、同 PTS 帧可区分、`source_dt_s` 与事件 JSON 往返;同时覆盖 frozen 校验、严格版本/未知字段拒绝(政策 A)、`SUPPORTED_CONTRACT_VERSIONS`、事件可哈希、`kind` 的 lower_snake 格式和非法负载拒绝。`tests/test_sensor_context.py` 覆盖 T04 的只读 SensorContext:显式 SN/配置/资产/标定路径绑定、不猜 SN、实例隔离、换标定原子替换、绑定失败只报 diagnostics,以及 config source/资产包绑定。类型与上下文本身不启动传感器、流水线或会话。
`tests/test_stream_contract.py` 覆盖 T03 的四项验收:逐路身份与时钟、组字段可缺但不半组、同 PTS 帧可区分、`source_dt_s` 与事件 JSON 往返;同时覆盖 frozen 校验、严格版本/未知字段拒绝(政策 A)、`SUPPORTED_CONTRACT_VERSIONS`、事件可哈希、`kind` 的 lower_snake 格式和非法负载拒绝。`tests/test_sensor_context.py` 覆盖 T04 的只读 SensorContext:显式 SN/配置/资产/标定路径绑定、不猜 SN、实例隔离、换标定原子替换、绑定失败只报 diagnostics,以及 config source/资产包绑定。
`tests/test_image_pipeline.py` 覆盖 T05:独立流水线与 Sensor 委托路径逐帧逐位一致、只读副本不被后续帧覆写、`include` 选择、`Sensor.configure(flow_ema_alpha=...)` 的运行时生效与 EMA 单属主、由 StreamPipeline 驱动时图像块版本逐帧等于快照 `tracker_reset_seq`(含复用 Sensor、直接 `reset()` 后),以及独立 `measure(reference_generation=...)` 的在途版本保留。