接入 Studio 多点标定的会话置零与状态记录

支持显式无载荷置零、处理线程内切换模型及完整标定包快照。shared、界面检查与兼容测试通过;Studio 全量仍有一项回放停止时序测试失败。
This commit is contained in:
ykcheungab
2026-10-19 09:47:51 +08:00
parent bc5dfc035d
commit 31750f0f84
19 changed files with 514 additions and 32 deletions
@@ -142,6 +142,7 @@ class BareVideoSetup:
#: Serial the operator confirmed for ``calibration``. Empty when the pass uses the
#: bundle's own file or raw force.
device_id: str | None = None
unloaded_reference_frames: int = 0
@dataclass(frozen=True)
@@ -335,6 +336,9 @@ def _calibration_identity(path: Path | None) -> str:
return ""
try:
digest = hashlib.sha256(Path(path).read_bytes()).hexdigest()[:12]
if json.loads(Path(path).read_text(encoding="utf-8-sig")).get("schema_version") == 3:
from orisys.calibration import load
digest = load(path, strict=True).fingerprint[:12]
except OSError:
return Path(path).name
return f"{Path(path).name}@{digest}"
@@ -531,6 +535,7 @@ def build_workspace(
device_id=device_id,
)
directory = workspace_directory(identity, root=root)
document["synthetic"]["unloaded_reference_frames"] = 0 if setup is None else setup.unloaded_reference_frames
directory.mkdir(parents=True, exist_ok=True)
mode = _link_or_copy(resolved, directory / VIDEO_FILENAME)
document["synthetic"]["video_in_workspace"] = mode
@@ -542,14 +542,18 @@ class ForceSample:
def from_reading(cls, reading: Mapping[str, Any]) -> ForceSample:
"""One :func:`~orisys_app_shared.acquisition.sdk_worker.read_forces_from_sensor`
reading, as this record's own type."""
axes = reading.get("calibrated_axes")
def newton(axis, key):
return float(reading.get(key, _NAN)) if axes is None or axis in axes else _NAN
return cls(
normal=float(reading.get("normal_force", _NAN)),
shear_x=float(reading.get("shear_x", _NAN)),
shear_y=float(reading.get("shear_y", _NAN)),
normal=newton("fn", "normal_force"),
shear_x=newton("fx", "shear_x"),
shear_y=newton("fy", "shear_y"),
normal_raw=float(reading.get("normal_force_raw", _NAN)),
shear_x_raw=float(reading.get("shear_x_raw", _NAN)),
shear_y_raw=float(reading.get("shear_y_raw", _NAN)),
valid=bool(reading.get("valid", True)),
valid=bool(reading.get("valid", True)) and all(
value == "ok" for value in reading.get("calibration_status", {}).values()),
calibrated=bool(reading.get("force_calibrated", False)),
)
@@ -618,9 +618,12 @@ class AnalysisService:
or dict(session.get("calibration") or {}).get("version")
or ""
)
zero_frames = int((session.get("synthetic") or {}).get("unloaded_reference_frames", 0))
if zero_frames not in (0, 30):
raise ValueError("unloaded reference must contain exactly 30 frames")
key = result_key(
recording=identity,
sensor_config=self.chain.identity(),
sensor_config=self.chain.identity() + (";unloaded-reference=30" if zero_frames else ""),
calibration=calibration,
reference_frame=self.reference_frame,
)
@@ -810,6 +813,7 @@ class AnalysisService:
from orisys_app_shared.acquisition.sdk_worker import process_chain
provenance = state.provenance
zero_frames = int((self.reader().session().get("synthetic") or {}).get("unloaded_reference_frames", 0))
for position, number in enumerate(state.segments):
if state.reasons or self._stop_requested():
if self._stop_requested():
@@ -823,6 +827,7 @@ class AnalysisService:
if any(row.segment_frame != index + 1 for index, row in enumerate(rows)):
provenance.warnings.append(f"segment_frame_gap:{number}")
with _OpenChain(self.chain, path, state.working) as chain:
zero_samples = []
offset, measured = self._segment_offset(chain, number, state.offsets)
state.offsets[str(number)] = offset
state.sources.add("measured" if measured else "declared")
@@ -873,6 +878,22 @@ class AnalysisService:
provenance.warnings.append(f"chain:{error}")
state.reasons.append(f"chain_failed:{row.frame_id}")
return
if zero_frames and (self.reference_frame is None or row.frame_id >= self.reference_frame) and len(zero_samples) < zero_frames:
sensor = getattr(chain.sensor, "raw_sensor", chain.sensor)
model = getattr(sensor, "calibration", None)
if getattr(model, "schema_version", None) != 3:
raise ValueError("unloaded reference requires a schema 3 calibration")
keys = (sensor.info.FNORMAL_RAW, sensor.info.FSHEARX_RAW, sensor.info.FSHEARY_RAW)
values = sensor.read_info(*keys)
if getattr(sensor, "centroid_found", False) or not np.all(np.isfinite(values)):
raise ValueError("unloaded reference contains contact or invalid readings")
zero_samples.append(values)
if len(zero_samples) == zero_frames:
raw = {axis: float(np.median([value[index] for value in zero_samples]))
for index, axis in enumerate(("fn", "fx", "fy")) if axis in model.channels}
sensor.set_calibration_reference(raw, reference_id=f"video-unloaded-{row.frame_id}")
provenance.counts["unloaded_reference_frames"] = zero_frames
provenance.counts["unloaded_reference_fn_raw"] = raw.get("fn")
force = self._read_force(chain)
flow = getattr(chain.sensor, "flow", None)
if flow is None or not getattr(flow, "size", 0):
@@ -909,6 +930,8 @@ class AnalysisService:
depth=depth_array,
force=force,
)
if zero_frames and len(zero_samples) != zero_frames:
raise ValueError("video ended before 30 unloaded reference frames were available")
# ------------------------------------------------------------------- internals
def _read_force(self, chain: _OpenChain) -> ForceSample | None: