Files
orisys-sdk/docs/force-stream-session.zh.md
T
ykcheungab ad4c3ba6b9 feat: ForceStreamOutput 新增同幀未濾波量測欄位
App 需要濾波前的 raw 與 N 供錄製、分析與標定取樣;診斷幀保留 raw 特徵、N 為 NaN。已以帶標定、中值加低通的會話逐幀對照直通結果驗證。
2026-10-23 10:41:05 +08:00

12 KiB
Raw Blame History

新力侧会话与单路入口(T06a/T06b、T07 输出滤波、T08a/T08b)

本文件是单路 SensorStream 计划 T06a/T06b/T07 的 SDK 合同:新的力侧会话直接消费 T05 的 ImageMeasurement(T03 MeasurementFrame 头 + 同帧 raw/field),参考版本取帧上的 reference_generation。旧 ForceSession 仍为 App 保留到 T09/T10 迁移,本会话不提供 process_measurement、request_zero_external_reset、tracker_reset_seq 等旧外部复位接口。

接口

  • ForceStreamSession(sensor_family=..., calibration=None, settings=None):构造。T06a 支持 无标定与 schema2 标量曲线;T06b 补充 schema3 field 模型(hhd_normal_l1_field/1,需要 context 与同帧 field)和 FN-only 全局增益模型(outputs=["fn"]);settings 为 OutputSettings(None 用旧默认)。
  • measurement_requirements:(want_field, want_contact),告诉 T05 调用方是否需要把 field include 进 ImageMeasurement(field 模型为 (True, False),标量/FN-only 为 (False, False))。
  • request_zero(reference_id=..., declared_unloaded=True, expected_reference_generation=版本, max_gap_s=None, max_window_s=None): 为指定参考版本确认无载归零;可选窗口上限。
  • process(measurement) -> ForceStreamOutput:消费一个 T05 ImageMeasurement,返回逐帧结果 (frame 携带 T03 帧头与 reference_generation,以及 raw/newton/status/quality/zero)。 measurement_raw/measurement_newton 是同帧、未经输出滤波的测量三元组(T09c):接受帧取 ForceOutput.measurement 的未滤波 raw/N;诊断帧的 measurement_raw 等于该帧 force_raw、 measurement_newton 为 NaN。直通设置下它们与 raw/newton 逐位相同。

规则

  • 版本/会话变化即废旧零点(K8.3)。 reference_generation 或 session_id 一旦变化, resetting、collecting、ready 任一阶段的旧零点立即失败(zero.phase == "failed"), 必须对新的版本显式调用 request_zero(expected_reference_generation=新版本) 才能恢复有效 N。 正常顺序(会话在处理 v0 → 复位得到目标 v1 → v1 到达)中,等于请求目标的新版本会被采用 并开始/继续同版本窗口,不会作废这次请求;只有未请求的版本变化或版本超过目标才失败。 旧的 generation 在采样开始后不再检查的缺陷(阶段 0 P3-2)由本会话修复。
  • 基线只取请求版本的帧(K8.3/K8.5)。 归零请求绑定一个版本。目标到达前、仍属当前版本的 在途帧记为 zero_pending(不是迟到,也绝不进入窗口);比当前版本更旧的帧才是 late; 超过目标的版本使请求失败,而不是静默改绑。基线固定 30 帧无载输入; 可选 max_gap_s/max_window_s 超限时失败并把原因写进 zero.failure_reason。
  • 重复、迟到、同时间戳(K4.2)。 以 sensor_id + session_id + source_frame_id (无源编号时用 frame_id)去重,凡是送入引擎的帧都会登记身份。重复帧只产生诊断结果; 旧版本和同时间戳/乱序帧只标记 late/zero_pending,不推进零点或滤波,也不作废旧零点。 不同源帧时间戳相同不算重复。
  • 逐帧结果流(K6.1)。 每个输入帧都返回一个冻结 ForceStreamOutput,携带 T03 帧头与 reference_generation;quality 为 ok / uncalibrated / zero_pending / duplicate / late / session_changed / version_changed / window_failed / invalid。 迟到、无效和带质量标记的结果不会被丢弃;拒绝帧仍保留 raw 特征。
  • 数值等价与滤波。 零点/标定换算/读数滤波复用已发布算法;同一输入、无复位、同一标定时 与旧 ForceSession 逐帧相等(T06a 覆盖标量曲线、无标定、median+lowpass 滤波器;T06b 覆盖 field 模型与 FN-only 全局增益模型,三类标定都在同一平台同一次运行中对照旧会话)。
  • 三类标定与不匹配。 标量曲线、field、FN-only 各有有效测试;无标定会话保持 raw 与 uncalibrated 状态;field 模型缺 context 时构造报 CalibrationError,基线帧缺 field 或 模型拒绝帧时零点失败并给出 invalid_input/原因;FN-only 的 FX/FY 永远保持 NaN + uncalibrated,不冒充三轴标定。
  • 输出滤波(T07)。 设计帧率在构造时固定(design_sample_rate_hz),实测帧率漂移和长 间隔只作为 measured_sample_rate_hz/sample_rate_irregular 诊断,不重新设计链路、不清空 历史。request_output_settings(settings) 可从任意线程发出,只在力线程下一帧边界生效:只 重建滤波链并清空其历史,图像参考与已完成的零点不变;相同设置是空操作。每次实际切换发出 一个 output_filter_changed StreamEvent(规范化设置、指纹、算法版本、生效帧、设计帧率、 预热信息,可 JSON 往返),由 drain_events() 取出。预热帧数为 max(1, median_window) + 2×lowpass_order(低通开启时);结果行提供当前设置身份与预热计数。 全阶段关闭时输出与输入完全相同(含 NaN 与带符号的零)。已发布 ForceOutputFilter/旧会话 的自适应调频默认行为不变。
  • 线程与只读。 一个实例只属于一个力侧线程;输入数组只读,输出只含标量/状态/不可变进度。

输出滤波示例

from orisys.force_readout import OutputSettings
from orisys.force_stream_session import ForceStreamSession

session = ForceStreamSession(
    sensor_family="box",
    settings=OutputSettings(sample_rate_hz=30.0, median_window=3, lowpass_hz=5.0),
    design_sample_rate_hz=30.0,
)
new_settings = OutputSettings(sample_rate_hz=30.0, median_window=5, lowpass_hz=10.0)
session.request_output_settings(new_settings)
assert session.output_settings.median_window == 3, "switch waits for a frame boundary"
assert session.design_sample_rate_hz == 30.0

SensorStream 入口(T08a/T08b)

SensorStream 把 T04–T07 串成一个运行中的单路入口:

  • 所有权。 SensorStream.open(source, ...) 自己打开 Sensor 并在 stop() 关闭;调用方传入 已打开的 Sensor 或外部喂帧时只借用,stop() 不关闭借用源。运行中只有一个采集所有者; 流在 start() 启用 measurement session,stop()/启动失败时恢复借用 Sensor 原来的模式 (原本已启用的保持启用)。
  • 实时 latest(T08a)。 push_frame(...) 的未领取候选帧会被最新输入覆盖并计入 overwritten_frames;已经交给流水线的帧照常完成。wait_output() 是 latest-wins 结果流, drain_events() 取会话事件。
  • 文件/debug 完整(T08b)。 realtime=False 时逐帧处理每个被接受的输入:外部帧进入 有上限的 FIFO(depth),计算跟不上时 push_frame 阻塞并计入 backpressure_waits,不丢帧; end_input() 结束序列,wait_outputs(n)/drain_outputs() 按发布顺序返回结果。内置文件源由 SensorStream.open(path, realtime=False, nominal_fps=...) 打开,同样逐帧处理。文件模式下, 只有在所有已接受帧都已通过力阶段(published >= accepted)且零点仍未完成时, reset_and_zero 才以 SensorStreamError(“input ended before unloaded zero completed”) 结束;生产者刚读到结束标记、管线中还有已接受帧时不会提前判失败,足够的已接受帧仍会正常完成零点。
  • 线程/版本。 复用 StreamPipeline 的线程与原子 request_reset() 版本;力侧只在 HHD 线程的 on_frame 中调用 ForceStreamSession,因此 T07 的 drain_events 线程约束由门面满足, 没有第二套版本或复位记账。
  • 组合归零。 reset_and_zero(reference_id=...) 执行“图像复位确认 → 同版本 30 帧无载基线 → 力 ready”;用户不管理版本号。在途旧版本帧不会进入新基线(T06a 的规则)。每次调用只等待 本条命令的结果:力线程把该命令的 ZeroProgress/错误写回命令对象并通知,调用方线程不读取 session.zero,因此连续两次调用时第二次不会复用第一次的零点。超时后命令仍在力线程进行 (仍可能稍后 ready),调用方不应假设它被取消;在下一条命令完成前再次调用会被拒绝。
  • 时间来源(T08b)。 实时相机 capture_monotonic,内置采集用 CapturedFrame.host_mono_ns 和 sequence;Orisys 自己录像(文件内保存信号时间)recorded_signal;外部调用方提供可证明 的容器 PTS 才 video_pts(source_pts 原值保留);内置 OpenCV 普通视频用 estimated_source,capture_time_ns = source_frame_id ÷ nominal_fps,CAP_PROP_POS_MSEC 只作为 diagnostics 的 pos_msec,不填 source_pts。没有时间的输入按帧号推算并标 estimated_source。同一 PTS 的不同源帧是两帧;入口按源身份忽略重复/迟到输入并计数。
  • 诊断与 field(T08b)。 结果行的 diagnostics 含覆盖/重复/迟到计数、处理延迟、 long_gap、source_dt_s、source_time_estimated 和 pos_msec(有则);source_dt_s 取同路 上一条实际处理帧的源时间差。include=("field",) 让 StreamPipeline 在 HHD 线程 _build 中复制同帧 field;门面在标定模型需要 field 时自动启用。默认快照不含 field,已发布场不会被 后续帧改写。
import numpy as np
from orisys.sensor import Sensor
from orisys.sensor_stream import SensorStream

sensor = Sensor("external", config_name="box", verbose=False, backend="cpu")
stream = SensorStream(sensor, sensor_id="BOX-001", session_id="run-1")
stream.start()
try:
    stream.push_frame(
        np.full((365, 365), 100, dtype=np.uint8),
        source_frame_id=0,
        capture_time_ns=1_000_000_000,
    )
    output = stream.wait_output(timeout=5.0)
    assert output is not None and output.frame.stamp.sensor_id == "BOX-001"
finally:
    stream.stop()

示例

from orisys.force_stream_session import ForceStreamSession
from orisys.image_pipeline import ImageMeasurement
from orisys.stream_contract import FrameStamp, MeasurementFrame

session = ForceStreamSession(sensor_family="box")
session.request_zero(reference_id="run-1", declared_unloaded=True,
                     expected_reference_generation=0)
frame = MeasurementFrame(
    stamp=FrameStamp(sensor_id="BOX-001", session_id="run-1", frame_id=1,
                     source_frame_id=1, capture_time_ns=33_333_333,
                     clock_id="host", capture_time_basis="capture_monotonic"),
    source_dt_s=None, reference_generation=0,
)
row = session.process(ImageMeasurement(frame=frame, force_raw=(0.0, 0.0, 0.0),
                                       torque=0.0, flow=None))
assert row.frame.reference_generation == 0
assert row.zero.phase in ("resetting", "collecting")

后续

T06b 已交付标量/field/FN-only 三类标定与旧 ForceSession 的完整数值等价;T07 已交付固定 设计帧率、显式切换事件与历史/直通合同;T08a 已交付 SensorStream 实时入口、所有权与组合归零; T08b 已交付文件/debug 完整逐帧、有界背压、时间来源/PTS 标注、include="field" 与诊断。 spatial_gain/接触映射模型不在 T06/T07 验收范围,需要时另立任务。 下一步进入 T09/T10(App 迁移);删除旧 ForceSession 前先把零点、标定、读数引擎从旧类中 抽出来,避免新会话依赖待删除的类型。