新增 HHD 场加权标定模型与加权空载基线

field_gain/1 用坐标增益图对无旋向量场做 L1 场加权,以连续 30 帧加权中位数替代旧 raw 零点,实时入口暂不支持。目标测试 102 项与 lint_gate 通过;契约见 orisys-sdk/docs/field-gain-model.zh.md。
This commit is contained in:
ykcheungab
2026-10-19 19:49:25 +08:00
parent c78f7a304f
commit 0a042c7fbf
11 changed files with 1355 additions and 70 deletions
+110
View File
@@ -345,3 +345,113 @@ def test_bad_zero_reference_is_rejected_without_reset(reference):
with pytest.raises(ValueError):
sensor.set_calibration_reference(reference, reference_id="bad")
assert sensor._calibration_reference == {"fn": 10.}
class FieldRecordingModel:
schema_version = 3
channels = {"fn": "N"}
input_profile = "hhd_normal_l1_field/1"
def __init__(self):
self.calls = []
self.resets = []
self.begin_calls = []
self.coordinate_map = np.zeros((2, 2, 2), dtype=np.float32)
self.coordinate_valid_mask = np.ones((2, 2), dtype=bool)
def reset(self, reference_id):
self.resets.append(reference_id)
def begin_reference(self, reference_id):
self.begin_calls.append(reference_id)
def validate_for_context(self, context):
if context != {"signature": "actual"}:
raise CalibrationError("pipeline mismatch")
def predict(self, **kwargs):
self.calls.append(kwargs)
features = kwargs["features"]
if "field" not in features:
return SimpleNamespace(values_N={"fn": None}, status={"fn": "invalid_input"},
diagnostics={})
return SimpleNamespace(values_N={"fn": 1.0}, status={"fn": "ok"}, diagnostics={})
def test_hhd_field_model_infers_from_same_frame_field_without_contact():
sensor = make_sensor()
sensor.calibration = FieldRecordingModel()
sensor._calibration_context = {"signature": "actual"}
sensor.cuda = sensor.verbose = False
sensor.valid_mask = sensor.valid_mask_hhd = None
sensor._hhd_downsample = 1
sensor._hhd_grid_shape = sensor._hhd_ref_shape = (2, 2)
sensor._model_frame_id = 0
field = np.zeros((2, 2, 2), dtype=np.float32)
calls = []
def get_field(valid_mask=None):
calls.append(valid_mask)
return {"d": field, "valid_mask": np.ones((2, 2), dtype=bool), "area_scale": 1.0,
"grid_shape": (2, 2), "reference_shape": (2, 2)}
sensor._Sensor__hhd = SimpleNamespace(
get_force=lambda *args, **kwargs: (None, (10., 0., 0., 0.)),
get_normal=lambda: field, get_shear=lambda: field, get_field=get_field,
)
sensor.compute_hhd(np.zeros((2, 2, 2), dtype=np.float32))
features = sensor.calibration.calls[-1]["features"]
assert features["field"]["grid_shape"] == (2, 2)
assert features["output_valid_mask"].shape == (2, 2)
assert "contact_found" not in features
assert sensor.calibration_status == {"fn": "ok"}
assert sensor.fnormal == pytest.approx(1.0)
assert calls == [None]
def test_weighted_unloaded_reference_is_declared_not_a_scalar_zero():
sensor = make_sensor()
sensor.calibration = FieldRecordingModel()
sensor._calibration_context = {"signature": "actual"}
sensor.declare_unloaded_reference(reference_id="video-unloaded-100")
assert sensor.calibration.begin_calls == ["video-unloaded-100"]
assert sensor._calibration_reference_id == "video-unloaded-100"
with pytest.raises(CalibrationError, match="scalar zero"):
sensor.set_calibration_reference({"fn": 0.}, reference_id="bad")
with pytest.raises(CalibrationError, match="hhd_normal_l1_field"):
make_sensor().declare_unloaded_reference(reference_id="scalar-model")
def test_finger_runtime_binds_field_identity_and_forwards_the_window(monkeypatch, tmp_path):
import orisys.calibration_context as context_mod
from orisys.field_calibration import FIELD_IDENTITY
from orisys.finger.runtime import FingerRuntime
sensor = make_sensor()
sensor.calibration = FieldRecordingModel()
sensor._calibration_context = {"signature": "actual"}
sensor.device_id = None
sensor.mirror = False
runtime = FingerRuntime.__new__(FingerRuntime)
runtime.tactile = SimpleNamespace(raw_sensor=sensor)
mesh = tmp_path / "mesh.npz"
mesh.write_bytes(b"actual geometry")
runtime.geometry = SimpleNamespace(config={}, export_paths=SimpleNamespace(mesh=mesh),
source_scale=.42, out_height=2, out_width=2)
runtime.domain = object()
runtime.frame_processor = SimpleNamespace(flip_source_vertical=True)
monkeypatch.setattr(context_mod, "domain_fingerprint", lambda domain: "domain")
contexts = []
def context(config, filters, **kwargs):
contexts.append(kwargs)
return {"signature": "actual"}
monkeypatch.setattr(context_mod, "build_context", context)
selected = FieldRecordingModel()
selected.pipeline = {"coordinates": {"frame": "autocal_flat"}}
runtime.set_calibration(selected)
assert contexts[0]["field"] == FIELD_IDENTITY
runtime.declare_unloaded_reference("runtime-window")
assert sensor.calibration.begin_calls == ["runtime-window"]