让 Finger 视频分析使用所选的力标定文件。

分析链不再在打开前拒绝标定,而是把文件作为 cal_path 交给 FingerRuntime,并用操作者确认的设备编号核对文件里的 SN。标定传递测试与对话框 SN 校验通过。
This commit is contained in:
ykcheungab
2026-10-08 15:12:08 +08:00
parent 7afd3169fd
commit 48624dec99
9 changed files with 246 additions and 16 deletions
@@ -139,6 +139,9 @@ class BareVideoSetup:
#: (the first frame it processes — which *is* a clean tracker, so the first frame is the
#: reference without a reset).
reference_frame: int | None = None
#: Serial the operator confirmed for ``calibration``. Empty when the pass uses the
#: bundle's own file or raw force.
device_id: str | None = None
@dataclass(frozen=True)
@@ -346,6 +349,7 @@ def session_document(
reference_frame: int | None,
identity: str,
source_facts: dict[str, Any],
device_id: str | None = None,
) -> dict[str, Any]:
"""The ``session.json`` a bare video is analysed under.
@@ -388,6 +392,7 @@ def session_document(
"profile_ref": str(profile.ref),
"profile_id": str(profile.profile_id or ""),
"calibration": "" if calibration is None else str(Path(calibration).resolve()),
"device_id": str(device_id or ""),
"reference_frame": None
if reference_frame is None
else int(reference_frame),
@@ -511,6 +516,7 @@ def build_workspace(
identity, source_facts = video_identity(resolved)
calibration = None if setup is None else setup.calibration
reference_frame = None if setup is None else setup.reference_frame
device_id = None if setup is None else setup.device_id
if calibration is not None and not Path(calibration).is_file():
raise BareVideoError(f"the chosen calibration {calibration} is not there")
frames = synthetic_frame_index(facts)
@@ -522,6 +528,7 @@ def build_workspace(
reference_frame=reference_frame,
identity=identity,
source_facts=source_facts,
device_id=device_id,
)
directory = workspace_directory(identity, root=root)
directory.mkdir(parents=True, exist_ok=True)
@@ -167,14 +167,18 @@ class ChainSpec:
export_dir: Path | None = None
flip_source_vertical: bool = True
mirror: bool = False
#: An explicit ``force_calibration.json`` for the box chain, or ``None`` for whatever
#: the configuration's own bundle resolves. It is *not* part of :meth:`identity`,
#: deliberately: a recording was taken under one calibration and the identity of *what
#: it is* must not move when a different curve is tried against it. The calibration
#: half of the invalidation key is the recording's own ``calibration_id`` (see
#: An explicit force-calibration file, or ``None`` for whatever the configuration's
#: own bundle resolves. It is *not* part of :meth:`identity`, deliberately: a recording
#: was taken under one calibration and the identity of *what it is* must not move when
#: a different curve is tried against it. The calibration half of the invalidation key
#: is the recording's own ``calibration_id`` (see
#: :meth:`AnalysisService.dependency_report`), which the generated workspace sets from
#: the chosen file's content.
calibration: Path | None = None
#: Device serial the operator confirmed for ``calibration``. Passed with ``cal_path``
#: so the SDK can reject a file whose ``sn`` is a different sensor. Not part of
#: :meth:`identity`: a mismatch fails the open, and a match does not change the file.
device_id: str | None = None
def __post_init__(self) -> None:
if self.sensor_type not in ("finger", "box"):
@@ -210,6 +214,7 @@ def chain_spec_for(
config_ref: str | Path | None = None,
export_dir: str | Path | None = None,
calibration: str | Path | None = None,
device_id: str | None = None,
) -> ChainSpec:
"""The chain a recording was made with, unless the caller names a different one.
@@ -234,11 +239,13 @@ def chain_spec_for(
bundle = Path(str(export_dir))
elif kind == "finger" and resolved is not None:
bundle = resolve_finger_export_dir(resolved)
serial = str(device_id or "").strip() or None
return ChainSpec(
sensor_type=kind,
config_ref=resolved,
export_dir=bundle,
calibration=None if calibration is None else Path(str(calibration)),
device_id=serial,
)
@@ -296,15 +303,6 @@ class _OpenChain:
if self.spec.sensor_type == "finger":
from orisys.finger import FingerRuntime
if self.spec.calibration is not None:
# The finger runtime opens its ``Sensor`` without a calibration path,
# so a chosen curve cannot be applied to this chain. Refusing is the
# only honest answer: a pass that ran anyway would store numbers from a
# different calibration than the one the caller asked for.
raise RuntimeError(
"the finger chain cannot take a chosen force calibration; it uses "
"the one the bundle resolves"
)
kwargs: dict[str, Any] = {
"video": str(self.video),
"verbose": False,
@@ -315,6 +313,13 @@ class _OpenChain:
kwargs["config"] = str(self.spec.config_ref)
if self.spec.export_dir is not None:
kwargs["export_dir"] = str(self.spec.export_dir)
if self.spec.calibration is not None:
# ``cal_path`` is the file the operator chose. ``device_id`` is the
# serial they confirmed for this video; the SDK rejects the file when
# its ``sn`` is a different sensor.
kwargs["cal_path"] = str(self.spec.calibration)
if self.spec.device_id:
kwargs["device_id"] = str(self.spec.device_id)
self._runtime = FingerRuntime.open(**kwargs)
self.sensor = self._runtime.tactile
self._read = self._runtime.read_source
@@ -0,0 +1,55 @@
"""A chosen Finger calibration is handed to the runtime, not refused before it opens."""
from __future__ import annotations
from orisys_app_shared.analysis.service import ChainSpec, _OpenChain
class _Runtime:
tactile = object()
capture = None
opened: dict = {}
@classmethod
def open(cls, **kwargs):
cls.opened = dict(kwargs)
return cls()
def read_source(self):
return False, None
def process_frame(self, frame):
return None
def test_a_finger_chain_passes_the_chosen_file_and_device(monkeypatch, tmp_path):
import orisys.finger as finger
monkeypatch.setattr(finger, "FingerRuntime", _Runtime)
_Runtime.opened = {}
calibration = tmp_path / "DD02-14.json"
calibration.write_text("{}", encoding="utf-8")
video = tmp_path / "clip.mp4"
video.write_bytes(b"")
spec = ChainSpec(
sensor_type="finger",
calibration=calibration,
device_id="DD02-14",
)
_OpenChain(spec, video, tmp_path).open()
assert _Runtime.opened["cal_path"] == str(calibration)
assert _Runtime.opened["device_id"] == "DD02-14"
assert _Runtime.opened["video"] == str(video)
def test_a_finger_chain_without_a_file_does_not_invent_one(monkeypatch, tmp_path):
import orisys.finger as finger
monkeypatch.setattr(finger, "FingerRuntime", _Runtime)
_Runtime.opened = {}
video = tmp_path / "clip.mp4"
video.write_bytes(b"")
spec = ChainSpec(sensor_type="finger")
_OpenChain(spec, video, tmp_path).open()
assert "cal_path" not in _Runtime.opened
assert "device_id" not in _Runtime.opened