接入力标定 schema 3 与多点空间增益模型

保留旧格式和 cal_path,校验 SN、特征身份及坐标附件,按帧返回逐轴状态。相关测试 263 项通过,已核对存量预测并实测 180 帧录像。
This commit is contained in:
ykcheungab
2026-10-18 12:56:40 +08:00
parent f7f4682ff9
commit ff7614a6a0
13 changed files with 1422 additions and 19 deletions
+120
View File
@@ -0,0 +1,120 @@
"""File selection, SN isolation and legacy compatibility at the public loader boundary."""
import json
import time
from types import SimpleNamespace
import numpy as np
import pytest
from orisys import calibration
from orisys.calibration_context import build_context
from orisys.sensors.calibration import resolve_calibration
def model_doc():
config = {"type": "single_camera", "camera": {"resolution_stitch": [64, 64]},
"tracker": {"dis_mode": 1, "hhd_downsample": 3}}
return {"schema_version": 3, "unit": "N", "identity": {"sensor_type": "finger", "sn": "A1"},
"pipeline": build_context(config), "model": {
"type": "global_gain/1", "input_profile": "force_delta/1", "output_frame": "sdk_force/1",
"outputs": ["fn"], "params": {"gains_N_per_raw": {"fn": .001}, "valid_force_range_N": [0, 3]}}}
def test_schema3_public_load_write_and_fn_only_sn(tmp_path):
path = tmp_path / "finger" / "A1.json"
path.parent.mkdir()
path.write_text(json.dumps(model_doc()))
model = calibration.load_for_device(tmp_path, "finger", "A1")
assert tuple(model.channels) == ("fn",)
assert calibration.check(model).ok
target = tmp_path / "copy.json"
calibration.write(model, target)
assert json.loads(target.read_text())["schema_version"] == 3
assert calibration.load(target).fingerprint == model.fingerprint
with pytest.raises(calibration.CalibrationError, match="identity mismatch"):
calibration.validate_for_device(model, "finger", "another-SN")
def test_missing_sn_does_not_pick_any_neighbor_or_default(tmp_path):
(tmp_path / "finger").mkdir()
(tmp_path / "finger" / "A1.json").write_text(json.dumps(model_doc()))
(tmp_path / "force_calibration.json").write_text(json.dumps(model_doc()))
assert calibration.load_for_device(tmp_path, "finger", "B2") is None
with pytest.raises(calibration.CalibrationError, match="directory"):
calibration.load_for_device(tmp_path / "missing", "finger", "B2")
@pytest.mark.parametrize("change", ["unknown_type", "unknown_schema", "malformed"])
def test_selected_bad_model_never_falls_back(tmp_path, change):
data = model_doc()
if change == "unknown_type":
data["model"]["type"] = "onnx/1"
elif change == "unknown_schema":
data["schema_version"] = 99
else:
data["model"]["params"]["gains_N_per_raw"] = {}
path = tmp_path / "selected.json"
path.write_text(json.dumps(data))
with pytest.raises(calibration.CalibrationError):
calibration.load(path, sensor_type="box")
def test_invalid_default_schema3_does_not_silently_become_raw(monkeypatch, tmp_path):
from orisys import configs
data = model_doc()
data["model"]["type"] = "onnx/1"
path = tmp_path / "force_calibration.json"
path.write_text(json.dumps(data))
monkeypatch.setattr(configs, "resolve_sensor_bundle", lambda name: SimpleNamespace(force_calibration_json=path))
with pytest.raises(calibration.CalibrationError, match="unsupported calibration model"):
calibration.load(sensor_type="finger")
def test_path_selection_keeps_explicit_and_sn_modes_separate(tmp_path):
with pytest.raises(calibration.CalibrationError, match="mutually exclusive"):
calibration.select_calibration_path(cal_path="file.json", calibration_dir=tmp_path)
with pytest.raises(calibration.CalibrationError, match="requires"):
calibration.select_calibration_path(device_id="A1")
assert calibration.select_calibration_path(cal_path=tmp_path / "A1.json") == tmp_path / "A1.json"
def test_profile_declares_only_available_axes_and_checks_family(tmp_path):
path = tmp_path / "selected.json"
path.write_text(json.dumps(model_doc()))
options = dict(bundle_root=tmp_path, declared_default=None, profile_id="test", selection=path)
resolved = resolve_calibration(product_family="finger", **options)
assert resolved.axes == ("fn",) and resolved.document is not None
assert resolved.fingerprint == resolved.document.fingerprint
wrong = resolve_calibration(product_family="box", **options)
assert wrong.document is None and wrong.issues[0].code == "calibration.model-family-mismatch"
def test_processor_applies_resolved_selection_including_none(monkeypatch):
from orisys.sensors import backends
monkeypatch.setattr(backends, "validate_profile", lambda profile: SimpleNamespace(usable=True))
selected = calibration.parse(model_doc())
calls = []
sensor = SimpleNamespace(set_calibration=calls.append)
for model in (selected, None):
profile = SimpleNamespace(processing_backend="single_camera", config={"camera": {}},
calibration=SimpleNamespace(document=model), profile_id="test")
session = backends.create_processor(profile, sensor=sensor)
assert session.sensor is sensor
assert calls == [selected, None]
def test_snapshot_keeps_per_frame_units_and_validity_copy():
from orisys.pipeline import StreamPipeline
sensor = SimpleNamespace(info=object(), force_unit="N", force_units=("N", "raw", "raw"),
calibration_status={"fn": "outside_coverage"}, fnormal=float("nan"),
fshearx=2., fsheary=3., _force_raw=(1., 2., 3.))
pipeline = StreamPipeline(sensor, lambda: None, include=())
snapshot = pipeline._build(np.zeros((2, 2)), None, 0., 0., 0., time.perf_counter())
assert snapshot.force_units == ("N", "raw", "raw")
assert snapshot.calibration_status == {"fn": "outside_coverage"}
sensor.calibration_status["fn"] = "ok"
assert snapshot.calibration_status["fn"] == "outside_coverage"