修复回放标定选择与曲线显示数据保留
让标定选择进入处理和缓存,避免显示设置清空曲线。102 项针对性检查通过,全量门禁仍有停止时序、模型尺寸与原生崩溃问题。
This commit is contained in:
@@ -63,6 +63,7 @@ __all__ = [
|
||||
"SyntheticWorkspace",
|
||||
"VideoFacts",
|
||||
"build_workspace",
|
||||
"calibration_identity",
|
||||
"is_synthetic",
|
||||
"profiles_for_video",
|
||||
"read_synthetic",
|
||||
@@ -325,7 +326,7 @@ def _session_id(identity: str) -> int:
|
||||
return int(identity[:12], 16) % (2**31)
|
||||
|
||||
|
||||
def _calibration_identity(path: Path | None) -> str:
|
||||
def calibration_identity(path: Path | None) -> str:
|
||||
"""A calibration's key component: its name plus a digest of its own content.
|
||||
|
||||
The *content* is what decides the numbers the chain computes, so a coefficient file
|
||||
@@ -384,7 +385,7 @@ def session_document(
|
||||
"files": {"video": [VIDEO_FILENAME]},
|
||||
"preroll_frames": 0,
|
||||
"preroll_segment": "",
|
||||
"calibration_id": _calibration_identity(calibration),
|
||||
"calibration_id": calibration_identity(calibration),
|
||||
"depth": {"stored": True, "replay": "analysis"},
|
||||
"synthetic": {
|
||||
"generator": GENERATOR,
|
||||
|
||||
@@ -168,21 +168,21 @@ class ChainSpec:
|
||||
flip_source_vertical: bool = True
|
||||
mirror: bool = False
|
||||
#: 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.
|
||||
#: own bundle resolves. The chosen content belongs to the calibration half of
|
||||
#: the result key, separate from the recording and sensor configuration identity.
|
||||
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
|
||||
#: Explicit raw-force selection, distinct from resolving the bundle default.
|
||||
calibration_disabled: bool = False
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.sensor_type not in ("finger", "box"):
|
||||
raise ValueError(f"unsupported sensor type {self.sensor_type!r}")
|
||||
if self.calibration_disabled and self.calibration is not None:
|
||||
raise ValueError("a disabled calibration cannot name a file")
|
||||
|
||||
@property
|
||||
def config_sha256(self) -> str:
|
||||
@@ -215,6 +215,7 @@ def chain_spec_for(
|
||||
export_dir: str | Path | None = None,
|
||||
calibration: str | Path | None = None,
|
||||
device_id: str | None = None,
|
||||
calibration_disabled: bool = False,
|
||||
) -> ChainSpec:
|
||||
"""The chain a recording was made with, unless the caller names a different one.
|
||||
|
||||
@@ -222,8 +223,8 @@ def chain_spec_for(
|
||||
recorded ``device.config_ref`` is the configuration the footage was taken under,
|
||||
not whatever happens to be installed now. ``calibration`` is the one fact a *recording*
|
||||
cannot supply — it names the curve in force at record time, not the file it lived in —
|
||||
so a caller that knows which file it means (the generated workspace for a bare video)
|
||||
passes it, and everything else keeps the bundle's own resolution.
|
||||
so an explicit setup passes that file. Otherwise the bundle resolves its default,
|
||||
unless the caller explicitly disables calibration.
|
||||
"""
|
||||
device = dict(session.get("device") or {})
|
||||
kind = str(sensor_type or device.get("sensor_type") or "")
|
||||
@@ -246,6 +247,7 @@ def chain_spec_for(
|
||||
export_dir=bundle,
|
||||
calibration=None if calibration is None else Path(str(calibration)),
|
||||
device_id=serial,
|
||||
calibration_disabled=calibration_disabled,
|
||||
)
|
||||
|
||||
|
||||
@@ -321,6 +323,8 @@ class _OpenChain:
|
||||
if self.spec.device_id:
|
||||
kwargs["device_id"] = str(self.spec.device_id)
|
||||
self._runtime = FingerRuntime.open(**kwargs)
|
||||
if self.spec.calibration_disabled:
|
||||
self._runtime.set_calibration(None)
|
||||
self.sensor = self._runtime.tactile
|
||||
self._read = self._runtime.read_source
|
||||
self._feed = self._runtime.process_frame
|
||||
@@ -341,7 +345,10 @@ class _OpenChain:
|
||||
),
|
||||
verbose=False,
|
||||
backend="auto",
|
||||
device_id=self.spec.device_id if self.spec.calibration is not None else None,
|
||||
)
|
||||
if self.spec.calibration_disabled:
|
||||
sensor.set_calibration(None)
|
||||
self.sensor = sensor
|
||||
raw_read = getattr(sensor, "_read_frame", None)
|
||||
if not callable(raw_read):
|
||||
@@ -524,6 +531,7 @@ class AnalysisService:
|
||||
max_bytes: int = DEFAULT_MAX_BYTES,
|
||||
working_directory: str | Path | None = None,
|
||||
store_force: bool = False,
|
||||
unloaded_reference_frames: int | None = None,
|
||||
on_progress: Callable[[Progress], None] | None = None,
|
||||
cancelled: threading.Event | None = None,
|
||||
) -> None:
|
||||
@@ -546,11 +554,10 @@ class AnalysisService:
|
||||
self.working_directory = (
|
||||
None if working_directory is None else Path(working_directory)
|
||||
)
|
||||
#: Whether each frame also stores the chain's own force sample. Off for a recording,
|
||||
#: whose force is a *measurement in its own file* (``forces.csv``) and must not be
|
||||
#: overwritten in the reader's mind by a recomputation of it; on for a bare video,
|
||||
#: where the chain's force is the only force there is (see ``bare_video``).
|
||||
#: Store a distinct recomputed trace for a bare video or an explicitly
|
||||
#: reconfigured recording; never rewrite the recording's forces.csv.
|
||||
self.store_force = bool(store_force)
|
||||
self.unloaded_reference_frames = unloaded_reference_frames
|
||||
self.on_progress = on_progress
|
||||
self.cancelled = cancelled
|
||||
self._reader: RecordingReader | None = None
|
||||
@@ -618,12 +625,20 @@ 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")
|
||||
if self.chain.calibration_disabled:
|
||||
calibration = ""
|
||||
elif self.chain.calibration is not None:
|
||||
from orisys_app_shared.analysis.bare_video import calibration_identity
|
||||
|
||||
calibration = calibration_identity(self.chain.calibration)
|
||||
zero_frames = self._zero_reference_frames()
|
||||
key = result_key(
|
||||
recording=identity,
|
||||
sensor_config=self.chain.identity() + (";unloaded-reference=30" if zero_frames else ""),
|
||||
sensor_config=(
|
||||
self.chain.identity()
|
||||
+ (";unloaded-reference=30" if zero_frames else "")
|
||||
+ (";force=recomputed" if self.store_force else "")
|
||||
),
|
||||
calibration=calibration,
|
||||
reference_frame=self.reference_frame,
|
||||
)
|
||||
@@ -813,7 +828,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))
|
||||
zero_frames = self._zero_reference_frames()
|
||||
for position, number in enumerate(state.segments):
|
||||
if state.reasons or self._stop_requested():
|
||||
if self._stop_requested():
|
||||
@@ -934,6 +949,15 @@ class AnalysisService:
|
||||
raise ValueError("video ended before 30 unloaded reference frames were available")
|
||||
|
||||
# ------------------------------------------------------------------- internals
|
||||
def _zero_reference_frames(self) -> int:
|
||||
value = self.unloaded_reference_frames
|
||||
if value is None:
|
||||
value = (self.reader().session().get("synthetic") or {}).get("unloaded_reference_frames", 0)
|
||||
frames = int(value)
|
||||
if frames not in (0, 30):
|
||||
raise ValueError("unloaded reference must contain exactly 30 frames")
|
||||
return frames
|
||||
|
||||
def _read_force(self, chain: _OpenChain) -> ForceSample | None:
|
||||
"""This frame's force sample, or ``None`` when the pass was not asked for one.
|
||||
|
||||
@@ -1169,10 +1193,8 @@ class AnalysisService:
|
||||
sources={
|
||||
"flow": "recomputed",
|
||||
"depth": "recomputed",
|
||||
# The one channel whose answer depends on what the input *is*. Beside a
|
||||
# recording, ``forces.csv`` is the measurement and the result stores none of
|
||||
# it; over a bare video there is no CSV, so the chain's own recomputation is
|
||||
# what a chart can draw — and it says so.
|
||||
# Bare videos and explicitly reconfigured recordings store a separate
|
||||
# recomputed trace. Other passes leave force display to forces.csv.
|
||||
"forces": "recomputed"
|
||||
if self.store_force
|
||||
else "recorded",
|
||||
|
||||
@@ -63,6 +63,7 @@ class MetricHistory:
|
||||
lowpass_cutoff_hz: float = 0.0,
|
||||
y_axis_min_span: float = 0.5,
|
||||
max_fps: float = DEFAULT_MAX_FPS,
|
||||
time_origin: float | None = None,
|
||||
):
|
||||
self.channels = int(channels)
|
||||
self.window_sec = float(window_sec)
|
||||
@@ -80,6 +81,8 @@ class MetricHistory:
|
||||
self.lowpass_cutoff_hz = float(lowpass_cutoff_hz)
|
||||
self.y_axis_min_span = float(y_axis_min_span)
|
||||
self._capacity = self.max_points
|
||||
# None uses the first sample; sources already in session seconds pass zero.
|
||||
self._time_origin = time_origin
|
||||
self.reset()
|
||||
|
||||
def set_window_sec(self, window_sec: float) -> None:
|
||||
@@ -96,7 +99,7 @@ class MetricHistory:
|
||||
(self._capacity, 1 + self.channels), np.nan, dtype=np.float64
|
||||
)
|
||||
self.sample_count = 0
|
||||
self._t0: float | None = None
|
||||
self._t0: float | None = self._time_origin
|
||||
self._y_range_smooth: list[tuple[float, float] | None] = [
|
||||
None for _ in range(self.channels)
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user