Files
orisys-sdk/examples/gui/dashboard.py
T
2026-06-18 22:18:19 +08:00

334 lines
11 KiB
Python

"""Unified Qt dashboard for the finger tactile example."""
from __future__ import annotations
import sys
from pathlib import Path
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import (
QApplication,
QFileDialog,
QHBoxLayout,
QLineEdit,
QMainWindow,
QPushButton,
QSplitter,
QVBoxLayout,
QWidget,
)
from pipeline.config import PipelineConfig
from pipeline.service import FingerPipeline
from .layout import (
DashboardLayout,
LAYOUT,
PANEL_SETUP,
PanelSetup,
top_row_splitter_sizes,
)
from .styles import STYLE
from .widgets import (
ArrowPanelWidget,
ForceChartWidget,
MeshViewportWidget,
TeeStream,
WidgetLogHandler,
make_log_widget,
)
class FingerDashboardWindow(QMainWindow):
"""Single-window finger demo: 3D mesh | UV arrows | force charts."""
def __init__(
self,
config: PipelineConfig,
*,
layout: DashboardLayout = LAYOUT,
log_file=None,
):
super().__init__()
self.config = config
self.layout = layout
self.pipeline = FingerPipeline(config)
self._running = False
self.setWindowTitle(layout.window_title)
self.setStyleSheet(STYLE)
logo_path = Path(__file__).resolve().parents[2] / "logo.png"
if logo_path.is_file():
self.setWindowIcon(QIcon(str(logo_path)))
self._setup_ui()
self._wire_logging(log_file)
self.timer = QTimer()
self.timer.timeout.connect(self._on_timer)
self.camera_edit.setText(str(config.video))
self.config_edit.setText(config.config)
self.cal_edit.setText(config.calib or "")
def _setup_ui(self) -> None:
layout = self.layout
central = QWidget()
self.setCentralWidget(central)
root_layout = QVBoxLayout(central)
root_layout.setContentsMargins(layout.root_margin_px, layout.root_margin_px, layout.root_margin_px, layout.root_margin_px)
root_layout.setSpacing(layout.root_spacing_px)
bar = QHBoxLayout()
bar.setSpacing(layout.toolbar_spacing_px)
self.btn_camera = QPushButton("摄像机")
self.btn_camera.clicked.connect(self._select_camera)
bar.addWidget(self.btn_camera)
self.camera_edit = QLineEdit("0")
self.camera_edit.setPlaceholderText("摄像头编号 或 视频路径")
bar.addWidget(self.camera_edit, 1)
self.btn_config = QPushButton("配置")
self.btn_config.clicked.connect(self._select_config)
bar.addWidget(self.btn_config)
self.config_edit = QLineEdit("./config/dd02-ov.json")
bar.addWidget(self.config_edit, 1)
self.cal_edit = QLineEdit("")
bar.addWidget(self.cal_edit, 1)
self.btn_reset = QPushButton("重置")
self.btn_reset.setEnabled(False)
self.btn_reset.clicked.connect(self._on_reset)
bar.addWidget(self.btn_reset)
self.btn_start = QPushButton("启动")
self.btn_start.setObjectName("btnStart")
self.btn_start.clicked.connect(self._on_start)
bar.addWidget(self.btn_start)
self.btn_stop = QPushButton("暂停")
self.btn_stop.setObjectName("btnStop")
self.btn_stop.setEnabled(False)
self.btn_stop.clicked.connect(self._on_stop)
bar.addWidget(self.btn_stop)
root_layout.addLayout(bar)
setup = layout.panel_setup
self._panel_widgets: dict[str, QWidget] = {}
self.mesh_viewport = MeshViewportWidget(layout=layout)
self.arrow_panel = ArrowPanelWidget(flow_threshold=layout.arrow_flow_threshold, layout=layout)
self.force_charts = ForceChartWidget(layout=layout, lowpass_cutoff_hz=self.config.lowpass_cutoff_hz)
self.log_widget = make_log_widget()
self._panel_widgets["mesh"] = self.mesh_viewport
self._panel_widgets["arrows"] = self.arrow_panel
self._panel_widgets["charts"] = self.force_charts.widget
self._panel_widgets["log"] = self.log_widget
# Top row: mesh | arrows | charts (order from layout.PANEL_SETUP.top_row)
h_splitter = QSplitter(Qt.Horizontal)
for slot in setup.top_row.slots:
if not slot.enabled:
continue
h_splitter.addWidget(self._panel_widgets[slot.id])
h_splitter.setStretchFactor(h_splitter.count() - 1, slot.stretch)
h_splitter.setChildrenCollapsible(setup.top_row.collapsible)
self.h_splitter = h_splitter
# Vertical: top panel | log panel
v_splitter = QSplitter(Qt.Vertical)
v_splitter.addWidget(h_splitter)
if setup.log_panel.enabled:
v_splitter.addWidget(self.log_widget)
v_splitter.setStretchFactor(0, setup.top_stretch)
v_splitter.setStretchFactor(1, setup.log_stretch)
else:
v_splitter.setStretchFactor(0, 1)
self.v_splitter = v_splitter
root_layout.addWidget(v_splitter)
self.resize(layout.window_width, layout.window_height)
QTimer.singleShot(0, self._apply_panel_layout)
def _apply_panel_layout(self) -> None:
setup = self.layout.panel_setup
for slot in setup.top_row.slots:
if not slot.enabled:
continue
if slot.id in ("mesh", "arrows"):
self._panel_widgets[slot.id].set_fixed_size(slot.width_px, slot.height_px)
elif slot.id == "charts":
self.force_charts.set_panel_size(slot.width_px, slot.height_px)
if setup.log_panel.enabled and setup.log_panel.height_px > 0:
self.log_widget.setFixedHeight(setup.log_panel.height_px)
self.h_splitter.setSizes(list(top_row_splitter_sizes(self.layout)))
def resizeEvent(self, event):
super().resizeEvent(event)
QTimer.singleShot(0, self._apply_panel_layout)
def showEvent(self, event):
super().showEvent(event)
QTimer.singleShot(0, self._apply_panel_layout)
def _wire_logging(self, log_file) -> None:
streams = []
if log_file is not None:
streams.append(log_file)
self.log_handler = WidgetLogHandler(self.log_widget)
streams.append(self.log_handler)
sys.stdout = TeeStream(*streams)
sys.stderr = sys.stdout
def _on_start(self) -> None:
if self._running:
return
self.btn_start.setText("启动中...")
self.btn_start.setEnabled(False)
QApplication.processEvents()
self.config.video = self.camera_edit.text().strip()
self.config.config = self.config_edit.text().strip()
cal = self.cal_edit.text().strip()
self.config.calib = cal or None
try:
self.pipeline = FingerPipeline(self.config)
self.pipeline.open()
self.mesh_viewport.configure(self.pipeline)
self.force_charts.reset()
QTimer.singleShot(0, self._apply_panel_layout)
except Exception as exc:
self.mesh_viewport.close_renderer()
self.btn_start.setText("启动")
self.btn_start.setEnabled(True)
print(f"启动失败: {exc}")
return
self._running = True
self.timer.start(self.layout.timer_interval_ms)
self.btn_start.setText("启动")
self.btn_stop.setEnabled(True)
self.btn_reset.setEnabled(True)
print(
f"▶ 启动 — 摄像机={self.config.video}, 配置={self.config.config}, "
f"标定={self.config.calib or '(none)'}"
)
def _on_stop(self) -> None:
if not self._running:
return
self._running = False
self.timer.stop()
self.btn_stop.setText("停止中...")
self.btn_stop.setEnabled(False)
QApplication.processEvents()
self.mesh_viewport.close_renderer()
self.pipeline.close()
self.btn_start.setEnabled(True)
self.btn_stop.setText("暂停")
self.btn_reset.setEnabled(False)
print("⏸ 已暂停")
def _on_reset(self) -> None:
if not self._running:
print("请先启动传感器后再重置追踪器。")
return
self.pipeline.reset()
print("追踪器已重置。")
def _on_timer(self) -> None:
if not self._running:
return
frame = self.pipeline.tick()
if frame is None:
print("采集结束或读取失败。")
self._on_stop()
return
flow_mag = 0.0
if frame.flow.size:
flow_mag = float((frame.flow[:, :, 0] ** 2 + frame.flow[:, :, 1] ** 2).mean() ** 0.5)
print(
f"FPS={frame.fps:.2f} flow_rms={flow_mag:.2f} "
f"mask={frame.mask_coverage:.2f} normal={frame.fnormal:.2f} "
f"shear=({frame.fshearx:.2f}, {frame.fsheary:.2f})",
end="\r",
)
self.mesh_viewport.update_frame(frame)
self.arrow_panel.update_frame(frame)
self.force_charts.update_frame(frame)
def _select_camera(self) -> None:
path, _ = QFileDialog.getOpenFileName(
self,
"选择视频文件",
"data",
"视频 (*.mp4 *.avi *.mov);;所有文件 (*)",
)
if path:
self.camera_edit.setText(path)
def _select_config(self) -> None:
path, _ = QFileDialog.getOpenFileName(
self,
"选择配置文件",
"config",
"JSON (*.json);;所有文件 (*)",
)
if path:
self.config_edit.setText(path)
def keyPressEvent(self, event):
key = event.key()
if key == Qt.Key_Escape:
if self.isFullScreen():
self.showNormal()
else:
self.showFullScreen()
elif key == Qt.Key_Q:
print("\n正在退出...")
self.close()
elif key == Qt.Key_S:
if self.pipeline.start_recording():
print("开始录像。")
elif key == Qt.Key_E:
self.pipeline.stop_recording()
print("录像结束。")
elif key == Qt.Key_R:
self._on_reset()
else:
super().keyPressEvent(event)
def closeEvent(self, event):
self.timer.stop()
self.mesh_viewport.close_renderer()
self.pipeline.close()
event.accept()
def run_dashboard(config: PipelineConfig, *, layout: DashboardLayout = LAYOUT) -> int:
"""Launch the unified finger dashboard and block until the window closes."""
from .app import get_or_create_qapp
print("\n按键说明:Q 退出 | R 重置追踪器 | S 开始录像 | E 结束录像 | ESC 全屏")
app = get_or_create_qapp()
window = FingerDashboardWindow(config, layout=layout)
window.show()
QTimer.singleShot(0, window._apply_panel_layout)
return app.exec_()