接入 Studio 多点标定的会话置零与状态记录
支持显式无载荷置零、处理线程内切换模型及完整标定包快照。shared、界面检查与兼容测试通过;Studio 全量仍有一项回放停止时序测试失败。
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user