接入 Autocal Finger 视频预览
复用 shared FingerAcquisitionWorker 显示处理后画面,并保存视频与配置路径。通过针对性测试和真实视频验证。
This commit is contained in:
@@ -232,6 +232,26 @@
|
|||||||
"reserved_surface": "Multi-point calibration sensitivity will be drawn as a colour map on the sensor's 3D surface here: each point's sensitivity interpolated and coloured, with a legend and range.",
|
"reserved_surface": "Multi-point calibration sensitivity will be drawn as a colour map on the sensor's 3D surface here: each point's sensitivity interpolated and coloured, with a legend and range.",
|
||||||
"reserved_points": "The calibration point table and capture controls will appear here: platform coordinates, reference load, sampling statistics and the resulting sensitivity."
|
"reserved_points": "The calibration point table and capture controls will appear here: platform coordinates, reference load, sampling statistics and the resulting sensitivity."
|
||||||
},
|
},
|
||||||
|
"source": {
|
||||||
|
"video": "Finger video",
|
||||||
|
"config": "Sensor config",
|
||||||
|
"browse": "Browse…",
|
||||||
|
"start": "Play video",
|
||||||
|
"stop": "Stop",
|
||||||
|
"idle": "Choose a video and sensor.json",
|
||||||
|
"no_frame": "Waiting for processed frames",
|
||||||
|
"choose_video": "Choose Finger video",
|
||||||
|
"choose_config": "Choose sensor.json",
|
||||||
|
"video_filter": "Video files (*.mp4 *.avi *.mov *.mkv);;All files (*)",
|
||||||
|
"missing_input": "Choose an existing video file and sensor.json",
|
||||||
|
"opening": "Opening video…",
|
||||||
|
"running": "Processing video",
|
||||||
|
"stopping": "Stopping…",
|
||||||
|
"stopped": "Stopped",
|
||||||
|
"finished": "Video finished",
|
||||||
|
"error": "Video processing failed: {detail}",
|
||||||
|
"frame": "Frame {frame} · acquired at {time:.3f}"
|
||||||
|
},
|
||||||
"finger": {
|
"finger": {
|
||||||
"title": "Finger calibration-point debug",
|
"title": "Finger calibration-point debug",
|
||||||
"bundle_path": "Export bundle",
|
"bundle_path": "Export bundle",
|
||||||
|
|||||||
@@ -232,6 +232,26 @@
|
|||||||
"reserved_surface": "多点标定的灵敏度将在此以色图叠加在传感器三维表面上:每个标定点的灵敏度经插值着色,并给出色标与范围。",
|
"reserved_surface": "多点标定的灵敏度将在此以色图叠加在传感器三维表面上:每个标定点的灵敏度经插值着色,并给出色标与范围。",
|
||||||
"reserved_points": "标定点列表与采集控制将在此显示:平台坐标、参考载荷、采样统计与灵敏度结果。"
|
"reserved_points": "标定点列表与采集控制将在此显示:平台坐标、参考载荷、采样统计与灵敏度结果。"
|
||||||
},
|
},
|
||||||
|
"source": {
|
||||||
|
"video": "Finger 视频",
|
||||||
|
"config": "传感器配置",
|
||||||
|
"browse": "选择…",
|
||||||
|
"start": "开始播放",
|
||||||
|
"stop": "停止",
|
||||||
|
"idle": "请选择视频和 sensor.json",
|
||||||
|
"no_frame": "等待处理后的画面",
|
||||||
|
"choose_video": "选择 Finger 视频",
|
||||||
|
"choose_config": "选择 sensor.json",
|
||||||
|
"video_filter": "视频文件 (*.mp4 *.avi *.mov *.mkv);;所有文件 (*)",
|
||||||
|
"missing_input": "请选择存在的视频文件和 sensor.json",
|
||||||
|
"opening": "正在打开视频…",
|
||||||
|
"running": "正在处理视频",
|
||||||
|
"stopping": "正在停止…",
|
||||||
|
"stopped": "已停止",
|
||||||
|
"finished": "视频播放完毕",
|
||||||
|
"error": "视频处理失败:{detail}",
|
||||||
|
"frame": "帧 {frame} · 采集时间 {time:.3f}"
|
||||||
|
},
|
||||||
"finger": {
|
"finger": {
|
||||||
"title": "手指标定点调试",
|
"title": "手指标定点调试",
|
||||||
"bundle_path": "导出包",
|
"bundle_path": "导出包",
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
"sdo_last": { "node": 0, "index": 24704, "subindex": 0, "type": 0 },
|
"sdo_last": { "node": 0, "index": 24704, "subindex": 0, "type": 0 },
|
||||||
"rtex_last": { "axis": 0, "parameter": 0 },
|
"rtex_last": { "axis": 0, "parameter": 0 },
|
||||||
"show_diagnostics": false,
|
"show_diagnostics": false,
|
||||||
|
"finger_video_path": "",
|
||||||
|
"finger_config_ref": "",
|
||||||
"safety_limits": {
|
"safety_limits": {
|
||||||
"max_travel_mm": 100.0,
|
"max_travel_mm": 100.0,
|
||||||
"max_descent_mm": 10.0,
|
"max_descent_mm": 10.0,
|
||||||
|
|||||||
@@ -209,6 +209,8 @@ def default_preferences() -> dict[str, Any]:
|
|||||||
#: The export bundle the debug screen loads. Empty means "ask", and the
|
#: The export bundle the debug screen loads. Empty means "ask", and the
|
||||||
#: default below points at the revision the bench confirmed.
|
#: default below points at the revision the bench confirmed.
|
||||||
"finger_bundle_path": DEFAULT_FINGER_BUNDLE,
|
"finger_bundle_path": DEFAULT_FINGER_BUNDLE,
|
||||||
|
"finger_video_path": "",
|
||||||
|
"finger_config_ref": "",
|
||||||
#: The UV-space texture for the flat picker. Preferred over the derived
|
#: The UV-space texture for the flat picker. Preferred over the derived
|
||||||
#: path when it exists; empty falls back to the layout map.
|
#: path when it exists; empty falls back to the layout map.
|
||||||
"finger_texture_path": DEFAULT_FINGER_TEXTURE,
|
"finger_texture_path": DEFAULT_FINGER_TEXTURE,
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
"""Finger video input and preview for the AutoCal sensor workspace."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from orisys_app_shared.acquisition.finger_worker import FingerAcquisitionWorker
|
||||||
|
from orisys_app_shared.i18n import t
|
||||||
|
from PySide6.QtCore import Qt, QTimer, Signal
|
||||||
|
from PySide6.QtGui import QImage, QPixmap
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QFileDialog,
|
||||||
|
QGridLayout,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QLineEdit,
|
||||||
|
QPushButton,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FingerSourcePanel(QWidget):
|
||||||
|
"""Run the shared finger processing path from a video file.
|
||||||
|
|
||||||
|
Worker callbacks originate on its acquisition thread; the GUI timer alone
|
||||||
|
reads packets and changes widgets. The same worker also accepts a camera ID.
|
||||||
|
"""
|
||||||
|
|
||||||
|
errorReported = Signal(str)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
video_path: str = "",
|
||||||
|
config_ref: str = "",
|
||||||
|
parent: QWidget | None = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.worker = FingerAcquisitionWorker(on_error=self.errorReported.emit)
|
||||||
|
self._last_frame_id: tuple[int, int] | None = None
|
||||||
|
self._last_image: QImage | None = None
|
||||||
|
self._active = False
|
||||||
|
self._error = ""
|
||||||
|
self._status_key = "source.idle"
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
fields = QGridLayout()
|
||||||
|
self.video_label = QLabel(self)
|
||||||
|
self.video_path = QLineEdit(video_path, self)
|
||||||
|
self.video_path.setObjectName("fingerVideoPath")
|
||||||
|
self.video_browse = QPushButton(self)
|
||||||
|
self.config_label = QLabel(self)
|
||||||
|
self.config_ref = QLineEdit(config_ref, self)
|
||||||
|
self.config_ref.setObjectName("fingerConfigRef")
|
||||||
|
self.config_browse = QPushButton(self)
|
||||||
|
fields.addWidget(self.video_label, 0, 0)
|
||||||
|
fields.addWidget(self.video_path, 0, 1)
|
||||||
|
fields.addWidget(self.video_browse, 0, 2)
|
||||||
|
fields.addWidget(self.config_label, 1, 0)
|
||||||
|
fields.addWidget(self.config_ref, 1, 1)
|
||||||
|
fields.addWidget(self.config_browse, 1, 2)
|
||||||
|
layout.addLayout(fields)
|
||||||
|
|
||||||
|
controls = QHBoxLayout()
|
||||||
|
self.start_button = QPushButton(self)
|
||||||
|
self.start_button.setObjectName("fingerVideoStart")
|
||||||
|
self.stop_button = QPushButton(self)
|
||||||
|
self.stop_button.setObjectName("fingerVideoStop")
|
||||||
|
self.stop_button.setEnabled(False)
|
||||||
|
self.status_label = QLabel(self)
|
||||||
|
self.status_label.setObjectName("fingerVideoStatus")
|
||||||
|
controls.addWidget(self.start_button)
|
||||||
|
controls.addWidget(self.stop_button)
|
||||||
|
controls.addWidget(self.status_label, 1)
|
||||||
|
layout.addLayout(controls)
|
||||||
|
|
||||||
|
self.preview = QLabel(self)
|
||||||
|
self.preview.setObjectName("fingerVideoPreview")
|
||||||
|
self.preview.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.preview.setMinimumSize(320, 260)
|
||||||
|
layout.addWidget(self.preview, 1)
|
||||||
|
self.frame_label = QLabel(self)
|
||||||
|
layout.addWidget(self.frame_label)
|
||||||
|
|
||||||
|
self._timer = QTimer(self)
|
||||||
|
self._timer.setInterval(33)
|
||||||
|
self._timer.timeout.connect(self._poll)
|
||||||
|
self.errorReported.connect(self._on_error)
|
||||||
|
self.video_browse.clicked.connect(self._choose_video)
|
||||||
|
self.config_browse.clicked.connect(self._choose_config)
|
||||||
|
self.start_button.clicked.connect(self.start)
|
||||||
|
self.stop_button.clicked.connect(self.stop)
|
||||||
|
self.retranslate()
|
||||||
|
|
||||||
|
def retranslate(self) -> None:
|
||||||
|
self.video_label.setText(t("source.video"))
|
||||||
|
self.config_label.setText(t("source.config"))
|
||||||
|
self.video_browse.setText(t("source.browse"))
|
||||||
|
self.config_browse.setText(t("source.browse"))
|
||||||
|
self.start_button.setText(t("source.start"))
|
||||||
|
self.stop_button.setText(t("source.stop"))
|
||||||
|
self._set_status(self._status_key)
|
||||||
|
if self._last_image is None:
|
||||||
|
self.preview.setText(t("source.no_frame"))
|
||||||
|
|
||||||
|
def _choose_video(self) -> None:
|
||||||
|
path, _ = QFileDialog.getOpenFileName(
|
||||||
|
self,
|
||||||
|
t("source.choose_video"),
|
||||||
|
self.video_path.text(),
|
||||||
|
t("source.video_filter"),
|
||||||
|
)
|
||||||
|
if path:
|
||||||
|
self.video_path.setText(path)
|
||||||
|
|
||||||
|
def _choose_config(self) -> None:
|
||||||
|
path, _ = QFileDialog.getOpenFileName(
|
||||||
|
self, t("source.choose_config"), self.config_ref.text(), "JSON (*.json)"
|
||||||
|
)
|
||||||
|
if path:
|
||||||
|
self.config_ref.setText(path)
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
video = Path(self.video_path.text().strip())
|
||||||
|
config = Path(self.config_ref.text().strip())
|
||||||
|
if not video.is_file() or not config.is_file():
|
||||||
|
self._show_error(t("source.missing_input"))
|
||||||
|
return
|
||||||
|
self._error = ""
|
||||||
|
self._last_frame_id = None
|
||||||
|
self._last_image = None
|
||||||
|
self.preview.clear()
|
||||||
|
self.preview.setText(t("source.no_frame"))
|
||||||
|
self.frame_label.clear()
|
||||||
|
try:
|
||||||
|
self.worker.start(str(video), config_ref=str(config))
|
||||||
|
except Exception as exc: # noqa: BLE001 - show source/config errors to operator
|
||||||
|
self._show_error(str(exc))
|
||||||
|
return
|
||||||
|
self._active = True
|
||||||
|
self._set_running_controls(True)
|
||||||
|
self._set_status("source.opening")
|
||||||
|
self._timer.start()
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
if self._active:
|
||||||
|
self.worker.stop()
|
||||||
|
self._set_status("source.stopping")
|
||||||
|
|
||||||
|
def shutdown(self) -> None:
|
||||||
|
self._timer.stop()
|
||||||
|
self.worker.stop(wait=True)
|
||||||
|
|
||||||
|
def _set_running_controls(self, running: bool) -> None:
|
||||||
|
self.start_button.setEnabled(not running)
|
||||||
|
self.stop_button.setEnabled(running)
|
||||||
|
for widget in (
|
||||||
|
self.video_path,
|
||||||
|
self.video_browse,
|
||||||
|
self.config_ref,
|
||||||
|
self.config_browse,
|
||||||
|
):
|
||||||
|
widget.setEnabled(not running)
|
||||||
|
|
||||||
|
def _show_error(self, message: str) -> None:
|
||||||
|
self._error = message
|
||||||
|
self._set_status("source.error")
|
||||||
|
|
||||||
|
def _set_status(self, key: str) -> None:
|
||||||
|
self._status_key = key
|
||||||
|
self.status_label.setText(t(key).format(detail=self._error))
|
||||||
|
|
||||||
|
def _on_error(self, message: str) -> None:
|
||||||
|
self._show_error(message)
|
||||||
|
|
||||||
|
def _poll(self) -> None:
|
||||||
|
packet = self.worker.latest()
|
||||||
|
if packet is not None:
|
||||||
|
identity = (packet.session_id, packet.frame_id)
|
||||||
|
if identity != self._last_frame_id:
|
||||||
|
self._last_frame_id = identity
|
||||||
|
self._show_frame(packet.processed_frame)
|
||||||
|
self.frame_label.setText(
|
||||||
|
t("source.frame").format(
|
||||||
|
frame=packet.frame_id, time=packet.timestamp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not self._error:
|
||||||
|
self._set_status("source.running")
|
||||||
|
if not self.worker.is_running:
|
||||||
|
reason = self.worker.consume_end_reason()
|
||||||
|
self._timer.stop()
|
||||||
|
self._active = False
|
||||||
|
self._set_running_controls(False)
|
||||||
|
if not self._error:
|
||||||
|
self._set_status(
|
||||||
|
"source.finished" if reason == "ended" else "source.stopped"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _show_frame(self, frame: np.ndarray) -> None:
|
||||||
|
if frame.ndim != 3 or frame.shape[2] != 3:
|
||||||
|
return
|
||||||
|
height, width = frame.shape[:2]
|
||||||
|
self._last_image = QImage(
|
||||||
|
frame.data, width, height, frame.strides[0], QImage.Format.Format_BGR888
|
||||||
|
).copy()
|
||||||
|
self._scale_preview()
|
||||||
|
|
||||||
|
def _scale_preview(self) -> None:
|
||||||
|
if self._last_image is not None:
|
||||||
|
self.preview.setPixmap(
|
||||||
|
QPixmap.fromImage(self._last_image).scaled(
|
||||||
|
self.preview.size(),
|
||||||
|
Qt.AspectRatioMode.KeepAspectRatio,
|
||||||
|
Qt.TransformationMode.SmoothTransformation,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def resizeEvent(self, event) -> None: # noqa: N802 - Qt override
|
||||||
|
super().resizeEvent(event)
|
||||||
|
self._scale_preview()
|
||||||
@@ -65,6 +65,7 @@ from .brand_bar import BrandBar
|
|||||||
from .bus_data_panel import BusDataPanel
|
from .bus_data_panel import BusDataPanel
|
||||||
from .bus_panel import default_bas_dir
|
from .bus_panel import default_bas_dir
|
||||||
from .device_setup import DeviceSetupDialog
|
from .device_setup import DeviceSetupDialog
|
||||||
|
from .finger_source_panel import FingerSourcePanel
|
||||||
from .sensor_panel import FingerPanel
|
from .sensor_panel import FingerPanel
|
||||||
from .sequence_panel import SequencePanel
|
from .sequence_panel import SequencePanel
|
||||||
from .status_strip import RunBar, StatusStrip
|
from .status_strip import RunBar, StatusStrip
|
||||||
@@ -287,8 +288,17 @@ class AutoCalMainWindow(QMainWindow):
|
|||||||
tool_xy_mm=_triple(self.prefs.get("finger_tool_xy_mm"), 2),
|
tool_xy_mm=_triple(self.prefs.get("finger_tool_xy_mm"), 2),
|
||||||
parent=body,
|
parent=body,
|
||||||
)
|
)
|
||||||
|
self.finger_source_panel = FingerSourcePanel(
|
||||||
|
video_path=str(self.prefs.get("finger_video_path") or ""),
|
||||||
|
config_ref=str(self.prefs.get("finger_config_ref") or ""),
|
||||||
|
parent=body,
|
||||||
|
)
|
||||||
self.workspace = WorkspaceTabs(
|
self.workspace = WorkspaceTabs(
|
||||||
self.axis_panel, self.sequence_panel, self.finger_panel, body
|
self.axis_panel,
|
||||||
|
self.sequence_panel,
|
||||||
|
self.finger_panel,
|
||||||
|
body,
|
||||||
|
sensor_panel=self.finger_source_panel,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.main_splitter = QSplitter(Qt.Orientation.Horizontal, body)
|
self.main_splitter = QSplitter(Qt.Orientation.Horizontal, body)
|
||||||
@@ -1273,6 +1283,7 @@ class AutoCalMainWindow(QMainWindow):
|
|||||||
self.axis_readout.retranslate()
|
self.axis_readout.retranslate()
|
||||||
self.sequence_panel.retranslate()
|
self.sequence_panel.retranslate()
|
||||||
self.finger_panel.retranslate()
|
self.finger_panel.retranslate()
|
||||||
|
self.finger_source_panel.retranslate()
|
||||||
self.workspace.retranslate()
|
self.workspace.retranslate()
|
||||||
|
|
||||||
self.menu_file.setTitle(t("menu.file"))
|
self.menu_file.setTitle(t("menu.file"))
|
||||||
@@ -1353,6 +1364,12 @@ class AutoCalMainWindow(QMainWindow):
|
|||||||
# The sequence profile is the P5 design artefact: whatever is on screen is
|
# The sequence profile is the P5 design artefact: whatever is on screen is
|
||||||
# what a later run would use, so it is persisted with everything else.
|
# what a later run would use, so it is persisted with everything else.
|
||||||
self.prefs["sequence_profile"] = self.sequence_panel.profile()
|
self.prefs["sequence_profile"] = self.sequence_panel.profile()
|
||||||
|
self.prefs["finger_video_path"] = (
|
||||||
|
self.finger_source_panel.video_path.text().strip()
|
||||||
|
)
|
||||||
|
self.prefs["finger_config_ref"] = (
|
||||||
|
self.finger_source_panel.config_ref.text().strip()
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
save_preferences(self.prefs)
|
save_preferences(self.prefs)
|
||||||
except OSError:
|
except OSError:
|
||||||
@@ -1360,6 +1377,7 @@ class AutoCalMainWindow(QMainWindow):
|
|||||||
|
|
||||||
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802 - Qt override
|
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802 - Qt override
|
||||||
"""Stop the platform, close the controller, then persist and leave."""
|
"""Stop the platform, close the controller, then persist and leave."""
|
||||||
|
self.finger_source_panel.shutdown()
|
||||||
logger.info("Closing: stopping axes and shutting the poller down")
|
logger.info("Closing: stopping axes and shutting the poller down")
|
||||||
try:
|
try:
|
||||||
self.poller.submit(lambda service: service.stop_all())
|
self.poller.submit(lambda service: service.stop_all())
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ class WorkspaceTabs(QTabWidget):
|
|||||||
sequence_panel: SequencePanel,
|
sequence_panel: SequencePanel,
|
||||||
finger_panel: QWidget | None = None,
|
finger_panel: QWidget | None = None,
|
||||||
parent: QWidget | None = None,
|
parent: QWidget | None = None,
|
||||||
|
sensor_panel: QWidget | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setObjectName("workspaceTabs")
|
self.setObjectName("workspaceTabs")
|
||||||
@@ -90,7 +91,11 @@ class WorkspaceTabs(QTabWidget):
|
|||||||
self._pages["sequence"] = sequence_panel
|
self._pages["sequence"] = sequence_panel
|
||||||
if finger_panel is not None:
|
if finger_panel is not None:
|
||||||
self._pages["finger"] = finger_panel
|
self._pages["finger"] = finger_panel
|
||||||
|
if sensor_panel is not None:
|
||||||
|
self._pages["sensor"] = sensor_panel
|
||||||
for key, locale_key in _PLACEHOLDERS.items():
|
for key, locale_key in _PLACEHOLDERS.items():
|
||||||
|
if key in self._pages:
|
||||||
|
continue
|
||||||
page = PlaceholderPage(locale_key, self)
|
page = PlaceholderPage(locale_key, self)
|
||||||
self._pages[key] = page
|
self._pages[key] = page
|
||||||
self._placeholder_keys[key] = locale_key
|
self._placeholder_keys[key] = locale_key
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
"""The Autocal sensor tab drives the shared finger acquisition interface."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
class FakeFingerWorker:
|
||||||
|
def __init__(self, *, on_error):
|
||||||
|
self.on_error = on_error
|
||||||
|
self.is_running = False
|
||||||
|
self.started = None
|
||||||
|
self.stopped = []
|
||||||
|
self.packet = None
|
||||||
|
|
||||||
|
def start(self, video, *, config_ref):
|
||||||
|
self.started = (video, config_ref)
|
||||||
|
self.is_running = True
|
||||||
|
|
||||||
|
def stop(self, *, wait=False):
|
||||||
|
self.stopped.append(wait)
|
||||||
|
self.is_running = False
|
||||||
|
|
||||||
|
def latest(self):
|
||||||
|
return self.packet
|
||||||
|
|
||||||
|
def consume_end_reason(self):
|
||||||
|
return "stopped"
|
||||||
|
|
||||||
|
|
||||||
|
def test_video_controls_use_shared_worker_and_show_processed_frame(
|
||||||
|
qt_app, tmp_dir, monkeypatch
|
||||||
|
):
|
||||||
|
from orisys_autocal.ui import finger_source_panel
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
finger_source_panel, "FingerAcquisitionWorker", FakeFingerWorker
|
||||||
|
)
|
||||||
|
video = tmp_dir / "input.mp4"
|
||||||
|
config = tmp_dir / "sensor.json"
|
||||||
|
video.write_bytes(b"video")
|
||||||
|
config.write_text("{}", encoding="utf-8")
|
||||||
|
|
||||||
|
panel = finger_source_panel.FingerSourcePanel(str(video), str(config))
|
||||||
|
panel.start_button.click()
|
||||||
|
assert panel.worker.started == (str(video), str(config))
|
||||||
|
assert not panel.start_button.isEnabled()
|
||||||
|
|
||||||
|
panel.worker.packet = SimpleNamespace(
|
||||||
|
session_id=1,
|
||||||
|
frame_id=7,
|
||||||
|
timestamp=123.5,
|
||||||
|
processed_frame=np.full((20, 30, 3), (10, 20, 30), dtype=np.uint8),
|
||||||
|
)
|
||||||
|
panel._poll()
|
||||||
|
assert panel._last_image.size().width() == 30
|
||||||
|
assert panel.frame_label.text().find("7") >= 0
|
||||||
|
assert not panel.preview.pixmap().isNull()
|
||||||
|
|
||||||
|
panel.stop_button.click()
|
||||||
|
panel._poll()
|
||||||
|
assert panel.worker.stopped == [False]
|
||||||
|
assert panel.start_button.isEnabled()
|
||||||
|
panel.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
def test_sensor_tab_is_live_panel_and_paths_are_saved(make_window, tmp_dir):
|
||||||
|
window = make_window()
|
||||||
|
assert window.workspace.page("sensor") is window.finger_source_panel
|
||||||
|
window.finger_source_panel.video_path.setText(str(tmp_dir / "input.mp4"))
|
||||||
|
window.finger_source_panel.config_ref.setText(str(tmp_dir / "sensor.json"))
|
||||||
|
window._persist_prefs()
|
||||||
|
assert window.prefs["finger_video_path"].endswith("input.mp4")
|
||||||
|
assert window.prefs["finger_config_ref"].endswith("sensor.json")
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_input_is_visible_and_does_not_start_worker(qt_app, monkeypatch):
|
||||||
|
from orisys_autocal.ui import finger_source_panel
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
finger_source_panel, "FingerAcquisitionWorker", FakeFingerWorker
|
||||||
|
)
|
||||||
|
panel = finger_source_panel.FingerSourcePanel()
|
||||||
|
panel.start_button.click()
|
||||||
|
assert panel.worker.started is None
|
||||||
|
assert panel.start_button.isEnabled()
|
||||||
|
assert panel._error
|
||||||
|
assert panel._error in panel.status_label.text()
|
||||||
|
panel.shutdown()
|
||||||
Reference in New Issue
Block a user