让 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
+5 -1
View File
@@ -2732,7 +2732,9 @@ class StudioMainWindow(QMainWindow):
session = RecordingReader(Path(recording)).session()
synthetic = dict(session.get("synthetic") or {})
chain = chain_spec_for(
session, calibration=synthetic.get("calibration") or None
session,
calibration=synthetic.get("calibration") or None,
device_id=synthetic.get("device_id") or None,
)
reference = synthetic.get("reference_frame")
if reference is None:
@@ -2755,6 +2757,7 @@ class StudioMainWindow(QMainWindow):
initial_calibration: Path | None = None,
prefer_no_calibration: bool = False,
initial_reference_frame: int | None = None,
initial_device_id: str | None = None,
) -> Path | None:
"""Ask how to analyse a bare video, then generate the workspace to do it in.
@@ -2786,6 +2789,7 @@ class StudioMainWindow(QMainWindow):
initial_calibration=initial_calibration,
prefer_no_calibration=prefer_no_calibration,
initial_reference_frame=initial_reference_frame,
initial_device_id=initial_device_id,
theme=self._theme,
)
if dialog.exec() != VideoAnalysisDialog.DialogCode.Accepted:
@@ -439,6 +439,7 @@ class ReplayController(QObject):
initial_calibration=None if prefer_none else Path(cal),
prefer_no_calibration=prefer_none,
initial_reference_frame=block.get("reference_frame"),
initial_device_id=str(block.get("device_id") or "") or None,
)
if workspace is None:
return
@@ -21,6 +21,7 @@ reason it cannot be used is one line, both in the language the catalog already s
from __future__ import annotations
import json
from pathlib import Path
from orisys_app_shared.analysis.bare_video import (
@@ -40,6 +41,7 @@ from PySide6.QtWidgets import (
QFormLayout,
QHBoxLayout,
QLabel,
QLineEdit,
QPushButton,
QSpinBox,
QVBoxLayout,
@@ -54,6 +56,25 @@ ROLE_NONE = "none"
ROLE_FILE = "file"
def calibration_identity(path: Path) -> tuple[str, str]:
"""``(sensor_type, sn)`` from a force-calibration file, or empty strings.
The SDK reads the same two places (``identity``, then ``meta``) when it checks
that a chosen file belongs to the device the operator named.
"""
try:
data = json.loads(Path(path).read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError):
return "", ""
if not isinstance(data, dict):
return "", ""
meta = data.get("meta") if isinstance(data.get("meta"), dict) else {}
identity = data.get("identity") if isinstance(data.get("identity"), dict) else {}
sensor_type = str(identity.get("sensor_type") or meta.get("sensor_type") or "")
sn = str(identity.get("sn") or meta.get("sn") or "")
return sensor_type, sn
class VideoAnalysisDialog(QDialog):
"""Ask how one bare video should be analysed, and hand back the three answers."""
@@ -68,6 +89,7 @@ class VideoAnalysisDialog(QDialog):
initial_calibration: Path | None = None,
prefer_no_calibration: bool = False,
initial_reference_frame: int | None = None,
initial_device_id: str | None = None,
lock_profile: bool = False,
theme: str = "dark",
) -> None:
@@ -87,6 +109,8 @@ class VideoAnalysisDialog(QDialog):
self._restore_reference = (
1 if initial_reference_frame is None else int(initial_reference_frame)
)
self._restore_device = str(initial_device_id or "")
self._device_source: Path | None = None
self._lock_profile = bool(lock_profile)
self.setObjectName("videoAnalysisDialog")
self.setModal(True)
@@ -145,6 +169,13 @@ class VideoAnalysisDialog(QDialog):
calibration_row.addWidget(self.btn_browse)
calibration_form.addRow(self.calibration_label, calibration_row)
self.device_label = QLabel()
self.device_label.setObjectName("fieldLabel")
self.device_edit = QLineEdit()
self.device_edit.setObjectName("deviceEdit")
calibration_form.addRow(self.device_label, self.device_edit)
self._calibration_form = calibration_form
self.reference_label = QLabel()
self.reference_label.setObjectName("fieldLabel")
self.reference_spin = QSpinBox()
@@ -170,7 +201,7 @@ class VideoAnalysisDialog(QDialog):
self.buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
self.buttons.accepted.connect(self.accept)
self.buttons.accepted.connect(self._on_accept)
self.buttons.rejected.connect(self.reject)
root.addWidget(self.buttons)
@@ -185,6 +216,9 @@ class VideoAnalysisDialog(QDialog):
elif self._restore_calibration is not None:
self.select_calibration(self._restore_calibration)
self.reference_spin.setValue(max(1, int(self._restore_reference)))
if self._restore_device:
self.device_edit.setText(self._restore_device)
self._sync_device_row()
# ------------------------------------------------------------------ selection
@property
@@ -231,6 +265,10 @@ class VideoAnalysisDialog(QDialog):
self.note_label.setText(t("video_analysis.blocked", reason=blocked))
return None
reference = int(self.reference_spin.value())
problem = self._device_problem()
if problem:
self.note_label.setText(problem)
return None
return BareVideoSetup(
profile_ref=entry.ref,
calibration=self._chosen_calibration(),
@@ -239,6 +277,7 @@ class VideoAnalysisDialog(QDialog):
# Saying so as ``None`` keeps a reset the pass cannot apply from being recorded
# as one that happened.
reference_frame=None if reference == 1 else reference,
device_id=self._confirmed_device_id(),
)
# ------------------------------------------------------------------ content
@@ -261,6 +300,7 @@ class VideoAnalysisDialog(QDialog):
)
self.buttons.button(QDialogButtonBox.StandardButton.Ok).setEnabled(not blocked)
self._refresh_time_label()
self._sync_device_row()
def _refresh_calibrations(self, *, custom: Path | None) -> None:
"""The calibration field: the profile's default, raw force, or a chosen file."""
@@ -286,6 +326,7 @@ class VideoAnalysisDialog(QDialog):
)
self.calibration_combo.setCurrentIndex(self.calibration_combo.count() - 1)
self.calibration_combo.blockSignals(False)
self._sync_device_row()
def _chosen_calibration(self) -> Path | None:
role = str(self.calibration_combo.currentData() or "")
@@ -308,6 +349,64 @@ class VideoAnalysisDialog(QDialog):
if chosen:
self._refresh_calibrations(custom=Path(chosen))
def _is_finger(self) -> bool:
entry = self.entry()
if entry is None:
return False
return str(entry.product_family or "").strip().lower() == "finger"
def _chosen_file(self) -> Path | None:
"""A file the operator browsed. The profile default is not one of these."""
if str(self.calibration_combo.currentData() or "") != ROLE_FILE:
return None
return self._custom_calibration
def _sync_device_row(self) -> None:
"""Show the device serial only when a Finger pass will load a browsed file."""
path = self._chosen_file() if self._is_finger() else None
visible = path is not None
self.device_label.setVisible(visible)
self.device_edit.setVisible(visible)
form = getattr(self, "_calibration_form", None)
if form is not None and hasattr(form, "setRowVisible"):
form.setRowVisible(self.device_label, visible)
if not visible:
self._device_source = None
return
if path != self._device_source:
self._device_source = path
_sensor_type, sn = calibration_identity(path)
if sn:
self.device_edit.setText(sn)
def _confirmed_device_id(self) -> str | None:
if self._chosen_file() is None or not self._is_finger():
return None
serial = self.device_edit.text().strip()
return serial or None
def _device_problem(self) -> str:
"""Why this Finger file cannot be applied, or "" when the serial matches."""
path = self._chosen_file()
if not self._is_finger() or path is None:
return ""
serial = self.device_edit.text().strip()
if not serial:
return t("video_analysis.device_required")
sensor_type, sn = calibration_identity(path)
if sensor_type and sensor_type != "finger":
return t("video_analysis.device_family", family=sensor_type)
if not sn:
return t("video_analysis.device_missing_sn")
if sn != serial:
return t("video_analysis.device_mismatch", sn=sn)
return ""
def _on_accept(self) -> None:
if self.choice() is None:
return
self.accept()
def _refresh_time_label(self) -> None:
fps = float(self._facts.fps) or 0.0
seconds = (int(self.reference_spin.value()) - 1) / fps if fps > 0 else 0.0
@@ -329,6 +428,8 @@ class VideoAnalysisDialog(QDialog):
)
self.profile_label.setText(t("video_analysis.profile"))
self.calibration_label.setText(t("video_analysis.calibration"))
self.device_label.setText(t("video_analysis.device"))
self.device_edit.setPlaceholderText(t("video_analysis.device_tip"))
self.reference_label.setText(t("video_analysis.reference"))
self.btn_browse.setText(t("video_analysis.calibration_browse"))
self.buttons.button(QDialogButtonBox.StandardButton.Ok).setText(t("dialog.ok"))