接入 Autocal 实时相机及帧时间显示

视频和相机共用 Finger worker,并保留采集时间、源帧编号及媒体时间。通过界面、采集链和真实视频验证。
This commit is contained in:
ykcheungab
2026-10-11 12:50:52 +08:00
parent 12c2fb1c4b
commit 32dcecb270
13 changed files with 280 additions and 91 deletions
@@ -211,6 +211,8 @@ def default_preferences() -> dict[str, Any]:
"finger_bundle_path": DEFAULT_FINGER_BUNDLE,
"finger_video_path": "",
"finger_config_ref": "",
"finger_source_kind": "video",
"finger_camera_index": 0,
#: The UV-space texture for the flat picker. Preferred over the derived
#: path when it exists; empty falls back to the layout map.
"finger_texture_path": DEFAULT_FINGER_TEXTURE,
@@ -1,7 +1,8 @@
"""Finger video input and preview for the AutoCal sensor workspace."""
"""Finger video or live camera input for the AutoCal sensor workspace."""
from __future__ import annotations
from datetime import datetime
from pathlib import Path
import numpy as np
@@ -11,18 +12,20 @@ from PySide6.QtCore import Qt, QTimer, Signal
from PySide6.QtGui import QImage, QPixmap
from PySide6.QtWidgets import (
QFileDialog,
QComboBox,
QGridLayout,
QHBoxLayout,
QLabel,
QLineEdit,
QPushButton,
QSpinBox,
QVBoxLayout,
QWidget,
)
class FingerSourcePanel(QWidget):
"""Run the shared finger processing path from a video file.
"""Run the shared finger processing path from a video or camera.
Worker callbacks originate on its acquisition thread; the GUI timer alone
reads packets and changes widgets. The same worker also accepts a camera ID.
@@ -35,6 +38,9 @@ class FingerSourcePanel(QWidget):
video_path: str = "",
config_ref: str = "",
parent: QWidget | None = None,
*,
source_kind: str = "video",
camera_index: int = 0,
) -> None:
super().__init__(parent)
self.worker = FingerAcquisitionWorker(on_error=self.errorReported.emit)
@@ -46,20 +52,41 @@ class FingerSourcePanel(QWidget):
layout = QVBoxLayout(self)
fields = QGridLayout()
self.source_label = QLabel(self)
self.source_kind = QComboBox(self)
self.source_kind.setObjectName("fingerSourceKind")
self.source_kind.setMaximumWidth(220)
self.source_kind.addItem("", "video")
self.source_kind.addItem("", "camera")
self.source_kind.setCurrentIndex(1 if source_kind == "camera" else 0)
self.video_label = QLabel(self)
self.video_path = QLineEdit(video_path, self)
self.video_path.setObjectName("fingerVideoPath")
self.video_browse = QPushButton(self)
self.camera_label = QLabel(self)
self.camera_index = QSpinBox(self)
self.camera_index.setObjectName("fingerCameraIndex")
self.camera_index.setRange(0, 31)
try:
remembered_camera = int(camera_index)
except (TypeError, ValueError):
remembered_camera = 0
self.camera_index.setValue(max(0, min(31, remembered_camera)))
self.camera_index.setMaximumWidth(100)
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)
fields.addWidget(self.source_label, 0, 0)
fields.addWidget(self.source_kind, 0, 1)
fields.addWidget(self.video_label, 1, 0)
fields.addWidget(self.video_path, 1, 1)
fields.addWidget(self.video_browse, 1, 2)
fields.addWidget(self.camera_label, 2, 0)
fields.addWidget(self.camera_index, 2, 1)
fields.addWidget(self.config_label, 3, 0)
fields.addWidget(self.config_ref, 3, 1)
fields.addWidget(self.config_browse, 3, 2)
layout.addLayout(fields)
controls = QHBoxLayout()
@@ -74,6 +101,9 @@ class FingerSourcePanel(QWidget):
controls.addWidget(self.stop_button)
controls.addWidget(self.status_label, 1)
layout.addLayout(controls)
self.preview_hint = QLabel(self)
self.preview_hint.setWordWrap(True)
layout.addWidget(self.preview_hint)
self.preview = QLabel(self)
self.preview.setObjectName("fingerVideoPreview")
@@ -91,15 +121,22 @@ class FingerSourcePanel(QWidget):
self.config_browse.clicked.connect(self._choose_config)
self.start_button.clicked.connect(self.start)
self.stop_button.clicked.connect(self.stop)
self.source_kind.currentIndexChanged.connect(self._on_source_changed)
self._on_source_changed()
self.retranslate()
def retranslate(self) -> None:
self.source_label.setText(t("source.kind"))
self.source_kind.setItemText(0, t("source.kind_video"))
self.source_kind.setItemText(1, t("source.kind_camera"))
self.video_label.setText(t("source.video"))
self.camera_label.setText(t("source.camera_index"))
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.preview_hint.setText(t("source.preview_only"))
self._set_status(self._status_key)
if self._last_image is None:
self.preview.setText(t("source.no_frame"))
@@ -121,12 +158,21 @@ class FingerSourcePanel(QWidget):
if path:
self.config_ref.setText(path)
def _on_source_changed(self) -> None:
video = self.source_kind.currentData() == "video"
for widget in (self.video_label, self.video_path, self.video_browse):
widget.setVisible(video)
self.camera_label.setVisible(not video)
self.camera_index.setVisible(not video)
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():
is_video = self.source_kind.currentData() == "video"
if not config.is_file() or (is_video and not video.is_file()):
self._show_error(t("source.missing_input"))
return
source = str(video) if is_video else self.camera_index.value()
self._error = ""
self._last_frame_id = None
self._last_image = None
@@ -134,7 +180,7 @@ class FingerSourcePanel(QWidget):
self.preview.setText(t("source.no_frame"))
self.frame_label.clear()
try:
self.worker.start(str(video), config_ref=str(config))
self.worker.start(source, config_ref=str(config))
except Exception as exc: # noqa: BLE001 - show source/config errors to operator
self._show_error(str(exc))
return
@@ -155,6 +201,8 @@ class FingerSourcePanel(QWidget):
def _set_running_controls(self, running: bool) -> None:
self.start_button.setEnabled(not running)
self.stop_button.setEnabled(running)
self.source_kind.setEnabled(not running)
self.camera_index.setEnabled(not running)
for widget in (
self.video_path,
self.video_browse,
@@ -181,9 +229,21 @@ class FingerSourcePanel(QWidget):
if identity != self._last_frame_id:
self._last_frame_id = identity
self._show_frame(packet.processed_frame)
acquired = datetime.fromtimestamp(packet.timestamp).strftime(
"%H:%M:%S.%f"
)[:-3]
basis_key = {
"host_replay": "source.basis_replay",
"host_camera": "source.basis_camera",
}.get(getattr(packet, "time_basis", ""), "source.basis_unknown")
basis = t(basis_key)
self.frame_label.setText(
t("source.frame").format(
frame=packet.frame_id, time=packet.timestamp
frame=packet.frame_id,
source_frame=getattr(packet, "source_frame_id", None) or "–",
time=acquired,
basis=basis,
dropped=getattr(packet, "capture_dropped", 0),
)
)
if not self._error:
@@ -194,9 +254,12 @@ class FingerSourcePanel(QWidget):
self._active = False
self._set_running_controls(False)
if not self._error:
self._set_status(
"source.finished" if reason == "ended" else "source.stopped"
)
key = {
"ended": "source.finished",
"disconnected": "source.disconnected",
"error": "source.failed",
}.get(reason, "source.stopped")
self._set_status(key)
def _show_frame(self, frame: np.ndarray) -> None:
if frame.ndim != 3 or frame.shape[2] != 3:
@@ -292,6 +292,8 @@ class AutoCalMainWindow(QMainWindow):
video_path=str(self.prefs.get("finger_video_path") or ""),
config_ref=str(self.prefs.get("finger_config_ref") or ""),
parent=body,
source_kind=str(self.prefs.get("finger_source_kind") or "video"),
camera_index=self.prefs.get("finger_camera_index") or 0,
)
self.workspace = WorkspaceTabs(
self.axis_panel,
@@ -1370,6 +1372,8 @@ class AutoCalMainWindow(QMainWindow):
self.prefs["finger_config_ref"] = (
self.finger_source_panel.config_ref.text().strip()
)
self.prefs["finger_source_kind"] = self.finger_source_panel.source_kind.currentData()
self.prefs["finger_camera_index"] = self.finger_source_panel.camera_index.value()
try:
save_preferences(self.prefs)
except OSError: