# 统一力标定模型契约 Purpose: reference. Scope: SDK 标定文件选择、SN 绑定和推断接口。 状态:2026-10-18 已实现 schema 3 的空间/全局增益;ML 后端仅保留契约,尚未实现。 保持 `cal_path` 与现有单点文件;新增空间模型和机器学习模型使用同一入口。 ## 现状与边界 现有实现见 [calibration.py](../src/orisys/calibration.py)、 [Sensor](../src/orisys/sensor.py)、[profile resolver](../src/orisys/sensors/calibration.py)。 当前读 schema 1/2,按 SN 加载要求三个通道,转换只接收三个力标量。 `ForceReadout.apply` 会分别转换滤波前后数据,不能直接挂接带状态的空间或 ML 模型。 | 所有者 | 责任 | |---|---| | 离线实验 / AutoCal | 采集、训练、验证、导出模型;将实验坐标转换为契约坐标 | | SDK | 选文件、校验身份与输入、准备特征、推断、输出逐轴有效性 | | 应用 | 提供 SN 与文件选择,展示状态;不自己套用第二次增益 | | 设计资产 | 几何与坐标定义;不提供每台设备的实测标定值 | SDK 不训练模型、不导入应用代码、不读取训练 CSV,不运行文件内指定的 Python。 实验 `finger_multical.experiment_model/1` 必须显式导出,不能直接当 SDK 文件加载。 ## 文件选择:保留路径,文件决定模型 不增加 `model_type` 构造参数或全局活动模型。`Sensor`、`FingerRuntime.open` 和 profile 创建处理器统一复用内部选择器;现有入口参数只映射为选择请求。 | 请求 | 文件与行为 | |---|---| | 非空 `cal_path` | 精确加载该文件;缺失、错误或不兼容时报错,不换文件 | | `calibration_dir` + `device_id` | 保持 `//.json`;缺文件保持 raw 并报告 missing,不回退产品模板 | | 两者都未提供 | 保持产品包默认解析;有 descriptor 时用 `default_calibration`,旧包保留既有候选顺序 | | profile 的 `CalibrationSelection.NONE` | 明确不标定;不赋予 `cal_path="none"` 新语义 | `cal_path` 与 `calibration_dir` 仍互斥。构造入口 `device_id` 对应文件 `identity.sn`, 不新造另一个 SN 字段。未指定设备的录像可显式选文件,但实际 SN 一致性标记为 unknown。 有 `device_id` 时严格匹配 `sensor_type`、`sn`;不根据相机编号猜 SN。 保留 `device_id` 单独传入时的参数错误;目录本身不存在也报错,不能当成 SN 文件缺失。 ```python # 沿用现有调用参数;cal_path 可以选择 schema 1/2/3 文件。 runtime = FingerRuntime.open( config="finger", video="test.mp4", device_id="DD02-GL-001", cal_path="calibrations/finger/DD02-GL-001.json", ) # 归零值来自本次无载窗口,不是模型内固定常量。 runtime.set_calibration_reference({"fn": measured_zero_raw}, reference_id="unloaded-1") # 自动按 SN 选择仍使用 calibration_dir="calibrations",不同时传 cal_path。 ``` 同一 SN 比较不同模型时,显式指定不同 JSON;默认 SN 文件只代表一个选定版本。 不扫描目录选“最新模型”,不在每帧重新加载。 ## 最小文件:schema 3 复用 `force_calibration.json` 文件角色,增加 schema 3;文件名不参与模型类型判断。 以下是三点结构示例,数值和 SHA 占位值不是设备标定结果。 ```json { "schema_version": 3, "unit": "N", "identity": {"sensor_type": "finger", "sn": "DD02-GL-001"}, "pipeline": { "stitch_resolution": [700, 750], "dis_mode": 4, "hhd_downsample": 3, "source_scale": 0.42, "filters": {"measurement": {"flow_ema_alpha": 1.0}}, "feature_signature_sha256": "0000000000000000000000000000000000000000000000000000000000000000", "geometry_sha256": "0000000000000000000000000000000000000000000000000000000000000000", "coordinates": { "frame": "autocal_flat", "unit": "px", "origin_xy": [511, 1021], "map_sha256": "0000000000000000000000000000000000000000000000000000000000000000" } }, "model": { "type": "spatial_gain/1", "input_profile": "fn_delta_autocal_uv/1", "output_frame": "sdk_force/1", "outputs": ["fn"], "params": { "points_uv": [[-145, 604], [145, 604], [5, 430]], "gains_N_per_raw": [0.00015, 0.00020, 0.00025], "triangles": [[0, 1, 2]], "valid_force_range_N": [0.0, 3.0] } } } ``` | 字段 | 规则 | |---|---| | `schema_version` | 整数 3;未知版本拒绝 | | `unit` | 固定 `N` | | `identity` | `sensor_type`、非空 `sn` 必填;文件位置不能替代身份 | | `pipeline` | 输入 profile 要求的实际处理参数、几何和坐标身份;缺少必需项或不匹配则拒绝 | | `model.type` | SDK 已注册的版本化标识;决定参数解析和推断,不是模块路径 | | `input_profile` | SDK 已实现的特征、单位、形状、预处理和时序契约 | | `output_frame` | `sdk_force/1` 延续对应处理 profile 的 FN/FX/FY 符号和基方向;不是世界坐标合力模长 | | `outputs` | 非空、无重复的 `fn/fx/fy` 子集;支持 FN-only,不冒充三轴标定 | | `params` | 模型类型私有参数;通用加载器不解释点数或网络层 | `provenance` 可选,保存数据摘要、训练版本和验证指标;不作为运行时输入。 其余未知语义字段拒绝,不能靠忽略字段误读新模型。新增模型类型不必升级外层 schema; 修改输入或算法语义必须升级相应 `/1` 标识。 `geometry_sha256` 是所用计算 mesh 文件字节的 SHA-256;`map_sha256` 是 SDK 坐标映射定义的 附件字节摘要,包含坐标转换结果,不能只比较图幅。pipeline 的必需键由输入 profile 固定, 覆盖该 backend 所有影响测量的设置;只检查示例中的部分配置不算兼容校验。 新增 profile 必须定义闭合的签名字段表。首个 profile 的 `feature_signature_sha256` 摘要 对象包含 `feature_version`、`processing_backend`、有效的图像预处理/光流/HHD 配置和计算资产哈希; 不含文件绝对路径、SN、显示资源或力读数滤波。以 UTF-8、键排序、无多余空格的 JSON 计算 SHA-256。 特征定义或单位改变须升级 `feature_version`,配置覆盖后重新计算,不能用配置文件名代替。 实现由 `calibration_context.build_context` 统一生成,特征版本为 `orisys_hhd_raw/1`。 ## 空间增益模型 `spatial_gain/1` 固定输出 `fn`,使用 `fn_delta_autocal_uv/1`: - 输入为同一帧的未经过力读数滤波的 FN raw、有效会话零点及接触 U/V。 上游光流滤波属于 pipeline 身份;不得混用不同特征链。 `delta_raw`、U、V 为有限浮点标量,前者单位 raw,后两者为上述有符号像素。 - SDK 会话准备 `delta_raw = fn_raw - zero_raw`,模型不再次扣零。 零点必须来自明确的归零操作或已标注窗口;模型文件不保存某段录像的启动零点。 - U 向右、V 向上,`x=origin_x+U`、`y=origin_y-V`。 SDK 提供并校验坐标转换;不能让模型偷偷按比例换算,不能运行时依赖 AutoCal。 - `points_uv` 为 N×2,`gains_N_per_raw` 长度 N;N≥3 且非共线、坐标唯一、增益有限且为正。 `triangles` 为任意数量的三索引,必须构成支持点凸包内完整、无重叠的非退化三角网。 保存连接关系以避免重新剖分引入变化;点数由数组决定,不固定为 9/15。 - 对三角形内查询,`g=Σw_i*g_i`,`fn_N=g*delta_raw`;不插值倒数,不外推。 未覆盖、位置失败、归零无效均返回无效,不能偷偷改用 M0。 - `valid_force_range_N` 必填。估计超范围时保留数值并标记 `out_of_range`,不截断。 FingerRuntime 使用 `artifacts` 中 `role="coordinate_map"` 的只读 NPZ: `uv_by_output_px` 为 H×W×2 有符号 UV,`valid_mask` 为 H×W bool;可附 `origin_xy` 与 `flat_mapping_sha256`。`pipeline.output_domain_sha256` 绑定实际展开的 remap、mask 和 world_xyz; 形状、几何或特征身份不同则在打开视频前拒绝。离线直接提供 UV 的模型可以没有映射附件。 单点/两点数据不自动切换算法;可导出独立的 `global_gain/1`,参数为各输出轴的增益和有效范围, 输入 profile 为 `force_delta/1`,由 SDK 按同一归零语义准备对应 raw 增量。 这不同于旧曲线的绝对 raw 输入;兼容适配器不得替旧文件额外扣零。 ## 机器学习扩展 同一 SN、路径和外层字段不变,只替换 `model`。下面只预留 `onnx/1` 示例, 不表示 SDK 已支持 ONNX;后端实现和输入 profile 验证通过前必须拒绝加载。 ```json { "type": "onnx/1", "input_profile": "fn_delta_autocal_uv/1", "output_frame": "sdk_force/1", "outputs": ["fn"], "params": { "input_tensor": "features", "output_tensor": "force_N", "feature_order": ["delta_raw", "u", "v"], "dtype": "float32", "shape": [1, 3], "valid_force_range_N": [0.0, 3.0] }, "artifacts": [ { "role": "weights", "path": "DD02-GL-001/model.onnx", "sha256": "0000000000000000000000000000000000000000000000000000000000000000" } ] } ``` 此标量 profile 的规范输入即上述顺序、类型、形状;输出 float32 `[1, len(outputs)]`, 轴序严格按 `outputs`。归一化及单位转换封装在已验证的模型图内,不由应用另做。 `fn_delta_autocal_uv/1` 只允许 `outputs=["fn"]`,不能凭 FN 特征宣称三轴已标定。 需要图像、完整光流或历史序列时新增输入 profile,明确 H/W、通道、掩码、单位和时间窗口; 没有对应特征的处理器拒绝加载,不能用零张量凑齐输入。 `artifacts` 只在有外部文件时必填;路径相对所选 JSON 的父目录,禁止绝对路径、URL、 `..` 或链接逃逸。校验 SHA-256、角色和文件完整性后才初始化受支持的后端。 不加载 pickle、任意 Python 或模型自带扩展库;缺推断依赖须明确报错,不自动安装。 文件与所有附件共同构成内容指纹,不能只哈希 JSON。 附件间依赖也须逐项声明和校验,未声明的外部权重不可加载。 ## 每帧推断与有效性 内部统一生命周期如下,`calibration.load` 分流到旧曲线或新模型: ```text load(selected_file, device_identity, pipeline) -> CalibrationModel model.predict(frame_id, timestamp_s, profile_features, reference_id) -> CalibrationResult model.reset(reference_id) CalibrationResult: values_N: {axis: finite_float | null} status: {axis: ok | no_contact | invalid_input | zero_unavailable | coordinate_unmapped | outside_coverage | out_of_range} ``` FN 空间模型输入仅允许单一接触;当前定位器只返回一个中心,无法证明不存在多接触, 不能宣称已标定总力。全局 `force_delta/1` 不要求定位,未提供接触状态不等同于无接触。 无接触、失败帧的校准输出为空,raw 另行保留;缺失值不是 0 N。 `out_of_range` 有数值但不在验证范围,其余失败状态无数值;未声明轴不可用、单位仍为 raw。 不把某轴 raw 填进 `values_N`,也不只靠全局 `force_unit` 判断三轴单位。 schema 3 的旧数值读数接口以 NaN 表示已声明轴的无效输出,并同步暴露上述状态; `force_calibrated` 表示已加载模型能力,不表示当前帧有效。 一个模型实例属于一个流;按帧顺序每帧推断一次,读数和录像复用结果。 清零、重连、倒播或更换模型时重置状态;不同 SN 不共享可变状态。 schema 3 先做上游测量与归零、模型推断,再做有效 N 读数的显示滤波;失败帧不进滤波历史。 schema 1/2 仍保留旧滤波/转换顺序。状态模型不能塞入会调用两次的旧 `convert` 回调。 加载诊断至少包含路径、来源、SN 校验状态、模型/输入 profile、轴集合、内容指纹和失败原因。 加载不匹配失败与逐帧暂时无效分开;替换时先完成加载校验,失败不得破坏原活动模型。 Sensor 提供 `bind_calibration_context`、`set_calibration_reference`、`apply_calibration_frame`; FingerRuntime 自动绑定真实上下文,并转发归零。`FrameSnapshot.force_units` 和 `calibration_status` 随帧保存,不能用稍后读取的 Sensor 状态解释旧帧。 归零与换模型须在计算线程或暂停处理时调用,不承诺并发修改安全。 ## 兼容、实现位置与验收 新读取器并存支持 schema 1/2/3;旧文件不重写,SN 校验的旧三通道要求保持。 schema 3 按声明轴校验,允许 FN-only;旧 SDK 遇 schema 3 必须拒绝,不能降级猜读。 回退通过恢复旧文件的 `cal_path`;SN 默认文件切换前保留旧副本,不覆盖唯一证据。 | 修改位置 | 最小工作 | |---|---| | `calibration.py` | schema 分流、身份和附件校验;旧曲线适配器 | | `sensors/calibration.py` | 统一选择器与指纹;支持 SN 请求 | | 新 `calibration_models.py` | 受控类型表、空间增益与模型生命周期;ML 后端另行实现 | | `sensor.py`、`finger/runtime.py`、处理器适配层 | 提供同帧位置/零点,缓存逐轴结果和状态 | | Sensor 与 `ForceReadout` | 复用读数滤波器;schema 3 在推断后逐轴滤波,旧路径不变 | | 离线导出器 | 实验模型到 schema 3;SDK 不依赖实验目录 | 当前导出入口为 workspace 的 `scripts/finger_multical/export_sdk.py`,输出已有文件时拒绝覆盖。 `data/dd02-gl/force_calibration.v3.json` 与同名 `.coordinates.npz` 是本次实验交付, `identity.sn="dd02-gl"` 为实验标识,未从硬件读取;原三边缘点与非单调点警告保留于 provenance。 它不是生产包默认文件,需显式 `cal_path` 选择。 实现验收必须覆盖:旧结果与单位不变;显式路径/SN/默认/关闭选择;错误 SN 不串用; 3/14/更多点、退化网格与凸包外;节点和插值预测;零点不重复扣除;缺坐标/无接触; FN-only 的 FX/FY 不冒充 N;未知模型、附件损坏和依赖缺失;状态模型每帧只调用一次及 reset。 另以独立录像验证物理准确度,契约或单元测试通过不代表标定合格。