update examples to fix path issues
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"video_source": "D:/repo/orisys-sdk/data/dd02-0703/tune.mp4",
|
||||
"sensor_bundle_dir": "sensors/finger",
|
||||
"video_source": "D:/repo/orisys-sdk/data/dd02-0703/1.mp4",
|
||||
"config_path": "finger",
|
||||
"finger_assets_dir": "sensors/finger/assets/30fps-0703",
|
||||
"csv_save_dir": "./outputs/csv",
|
||||
@@ -8,5 +7,5 @@
|
||||
"show_flow_arrows": false,
|
||||
"arrow_background": "unrolled",
|
||||
"free_rotate": true,
|
||||
"log_display_mode": "hidden"
|
||||
"sensor_bundle_dir": "sensors/finger"
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ from typing import Any, Optional
|
||||
import cv2
|
||||
import numpy as np
|
||||
import orisys
|
||||
from orisys.signal_processing import calibrate_normal_force
|
||||
|
||||
from .roi_geometry import (
|
||||
build_roi_warp,
|
||||
@@ -23,6 +22,10 @@ from .roi_geometry import (
|
||||
)
|
||||
|
||||
|
||||
NORMAL_FORCE_CAL_SLOPE = 3.92685554668795e-05
|
||||
NORMAL_FORCE_CAL_OFFSET = 0.0217596967278535
|
||||
|
||||
|
||||
@dataclass
|
||||
class FrameSnapshot:
|
||||
"""Normalized per-frame payload consumed by the Qt viewer."""
|
||||
@@ -46,6 +49,11 @@ class FrameSnapshot:
|
||||
record_frame: Optional[np.ndarray] = None
|
||||
|
||||
|
||||
def calibrate_normal_force(raw_value: float) -> float:
|
||||
"""Convert raw normal-force reading to Newtons using y = mx + c."""
|
||||
return NORMAL_FORCE_CAL_SLOPE * float(raw_value) + NORMAL_FORCE_CAL_OFFSET
|
||||
|
||||
|
||||
class SensorAdapter(ABC):
|
||||
"""Common interface between Qt and concrete sensor backends."""
|
||||
|
||||
@@ -76,37 +84,11 @@ def _bgr_from_gray_or_bgr(img: np.ndarray) -> np.ndarray:
|
||||
return img
|
||||
|
||||
|
||||
def _resolve_calibration_sensor_name(config_name) -> str | None:
|
||||
"""Map a config path/name to a sensors/<name> bundle when one exists."""
|
||||
if config_name is None:
|
||||
return None
|
||||
from orisys.configs import resolve_sensor_bundle
|
||||
|
||||
text = str(config_name).strip()
|
||||
if not text:
|
||||
return None
|
||||
|
||||
path = Path(text)
|
||||
parts = path.parts
|
||||
if "sensors" in parts:
|
||||
idx = parts.index("sensors")
|
||||
if idx + 1 < len(parts):
|
||||
candidate = parts[idx + 1]
|
||||
if resolve_sensor_bundle(candidate) is not None:
|
||||
return candidate
|
||||
|
||||
stem = path.stem if path.suffix else text
|
||||
if resolve_sensor_bundle(stem) is not None:
|
||||
return stem
|
||||
return None
|
||||
|
||||
|
||||
def _snapshot_from_sensor(
|
||||
sensor,
|
||||
*,
|
||||
raw_image=None,
|
||||
processed_image=None,
|
||||
sensor_name: str | None = None,
|
||||
) -> Optional[FrameSnapshot]:
|
||||
"""Build a FrameSnapshot with HHD/forces refreshed every frame."""
|
||||
if sensor is None or sensor.img is None:
|
||||
@@ -150,9 +132,6 @@ def _snapshot_from_sensor(
|
||||
below_threshold_color=(160, 160, 160),
|
||||
)
|
||||
|
||||
cal_name = sensor_name or _resolve_calibration_sensor_name(
|
||||
getattr(sensor, "config_name", None)
|
||||
)
|
||||
fn_raw = float(fnormal)
|
||||
return FrameSnapshot(
|
||||
image=image_bgr,
|
||||
@@ -163,7 +142,7 @@ def _snapshot_from_sensor(
|
||||
normal_force=fn_raw,
|
||||
shear_x=float(fshearx),
|
||||
shear_y=float(fsheary),
|
||||
normal_force_calibrated=calibrate_normal_force(fn_raw, sensor_name=cal_name),
|
||||
normal_force_calibrated=calibrate_normal_force(fn_raw),
|
||||
fps=float(fps),
|
||||
timestamp=time.time(),
|
||||
raw_image=raw_image,
|
||||
@@ -227,7 +206,6 @@ class OrisysSensorAdapter(SensorAdapter):
|
||||
if self.sensor.frame is not None
|
||||
else None,
|
||||
processed_image=_bgr_from_gray_or_bgr(self.sensor.img),
|
||||
sensor_name=_resolve_calibration_sensor_name(self.config_name),
|
||||
)
|
||||
|
||||
def reset(self) -> None:
|
||||
@@ -304,7 +282,6 @@ class Dd06RoiAdapter(SensorAdapter):
|
||||
self.sensor,
|
||||
raw_image=frame,
|
||||
processed_image=rectified,
|
||||
sensor_name=_resolve_calibration_sensor_name(self.config_name),
|
||||
)
|
||||
|
||||
def reset(self) -> None:
|
||||
|
||||
@@ -13,7 +13,7 @@ from typing import Literal, Optional, Sequence, Tuple
|
||||
import cv2
|
||||
import numpy as np
|
||||
import orisys
|
||||
from orisys.signal_processing import lowpass_filter
|
||||
from scipy.signal import butter, filtfilt
|
||||
|
||||
FORCE_SAMPLE_RATE_HZ = 30.0
|
||||
PLOT_BUFFER_POINTS = 120
|
||||
@@ -61,6 +61,32 @@ class ForcePlotState:
|
||||
y_ranges: Tuple[Tuple[float, float], Tuple[float, float], Tuple[float, float]]
|
||||
|
||||
|
||||
def lowpass_filter(
|
||||
data,
|
||||
cutoff_hz: float,
|
||||
sample_rate_hz: float = FORCE_SAMPLE_RATE_HZ,
|
||||
order: int = 2,
|
||||
) -> np.ndarray:
|
||||
"""Zero-phase Butterworth low-pass for uniformly sampled 1-D data."""
|
||||
y = np.asarray(data, dtype=np.float64)
|
||||
if cutoff_hz == 0:
|
||||
return y.copy()
|
||||
if y.size == 0:
|
||||
return y
|
||||
|
||||
nyquist = 0.5 * sample_rate_hz
|
||||
if cutoff_hz < 0 or cutoff_hz >= nyquist:
|
||||
raise ValueError(
|
||||
f"cutoff_hz must be in (0, {nyquist}) for sample_rate_hz={sample_rate_hz}"
|
||||
)
|
||||
|
||||
b, a = butter(order, cutoff_hz / nyquist, btype="low")
|
||||
padlen = 3 * (max(len(b), len(a)) - 1)
|
||||
if y.size <= padlen:
|
||||
return y.copy()
|
||||
return filtfilt(b, a, y)
|
||||
|
||||
|
||||
def ensure_bgr(img: Optional[np.ndarray]) -> Optional[np.ndarray]:
|
||||
if img is None:
|
||||
return None
|
||||
|
||||
@@ -1,22 +1,139 @@
|
||||
"""Discover USB cameras for the dashboard.
|
||||
|
||||
Thin compatibility shim over ``orisys.capture`` discovery helpers.
|
||||
"""
|
||||
"""Discover USB cameras for the dashboard (Windows DSHOW/MSMF, Linux V4L2)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from orisys.capture import (
|
||||
DEFAULT_MAX_CAMERA_INDEX,
|
||||
CameraFilter,
|
||||
CameraInfo,
|
||||
list_cameras,
|
||||
pick_default_camera,
|
||||
)
|
||||
import contextlib
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_MAX_CAMERA_INDEX",
|
||||
"CameraFilter",
|
||||
"CameraInfo",
|
||||
"list_cameras",
|
||||
"pick_default_camera",
|
||||
]
|
||||
import cv2
|
||||
|
||||
DEFAULT_MAX_CAMERA_INDEX = 10
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CameraInfo:
|
||||
index: int
|
||||
backend_name: str
|
||||
backend_api: int
|
||||
width: int
|
||||
height: int
|
||||
|
||||
@property
|
||||
def source(self) -> str:
|
||||
return str(self.index)
|
||||
|
||||
def label(self) -> str:
|
||||
return f"{self.index} ({self.width}x{self.height}) [{self.backend_name}]"
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _suppress_opencv_noise():
|
||||
"""Hide OpenCV VideoCapture probe warnings (stderr + OpenCV log)."""
|
||||
prev_level = None
|
||||
set_level = None
|
||||
try:
|
||||
logging = getattr(getattr(cv2, "utils", None), "logging", None)
|
||||
if logging is not None:
|
||||
set_level = getattr(logging, "setLogLevel", None)
|
||||
get_level = getattr(logging, "getLogLevel", None)
|
||||
silent = getattr(logging, "LOG_LEVEL_SILENT", 0)
|
||||
if set_level is not None:
|
||||
if get_level is not None:
|
||||
prev_level = get_level()
|
||||
set_level(silent)
|
||||
except Exception:
|
||||
prev_level = None
|
||||
|
||||
devnull = open(os.devnull, "w")
|
||||
try:
|
||||
with contextlib.redirect_stderr(devnull):
|
||||
stderr_fd = sys.stderr.fileno()
|
||||
saved_fd = os.dup(stderr_fd)
|
||||
try:
|
||||
os.dup2(devnull.fileno(), stderr_fd)
|
||||
yield
|
||||
finally:
|
||||
os.dup2(saved_fd, stderr_fd)
|
||||
os.close(saved_fd)
|
||||
finally:
|
||||
devnull.close()
|
||||
if set_level is not None and prev_level is not None:
|
||||
try:
|
||||
set_level(prev_level)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _backend_candidates() -> list[tuple[str, int]]:
|
||||
candidates: list[tuple[str, int]] = []
|
||||
if sys.platform == "win32":
|
||||
if hasattr(cv2, "CAP_DSHOW"):
|
||||
candidates.append(("DSHOW", cv2.CAP_DSHOW))
|
||||
if hasattr(cv2, "CAP_MSMF"):
|
||||
candidates.append(("MSMF", cv2.CAP_MSMF))
|
||||
elif sys.platform.startswith("linux"):
|
||||
# Prefer v4l2 for USB camera indices on Linux.
|
||||
if hasattr(cv2, "CAP_V4L2"):
|
||||
candidates.append(("V4L2", cv2.CAP_V4L2))
|
||||
if not candidates:
|
||||
candidates.append(("ANY", cv2.CAP_ANY))
|
||||
return candidates
|
||||
|
||||
|
||||
def _try_open(index: int, backend_name: str, backend_api: int) -> CameraInfo | None:
|
||||
cap = cv2.VideoCapture(index, backend_api)
|
||||
if not cap.isOpened():
|
||||
cap.release()
|
||||
return None
|
||||
ok, frame = cap.read()
|
||||
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH) or 0)
|
||||
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT) or 0)
|
||||
if not ok or frame is None:
|
||||
if width <= 0 or height <= 0:
|
||||
cap.release()
|
||||
return None
|
||||
else:
|
||||
h, w = frame.shape[:2]
|
||||
width = width or int(w)
|
||||
height = height or int(h)
|
||||
cap.release()
|
||||
return CameraInfo(
|
||||
index=index,
|
||||
backend_name=backend_name,
|
||||
backend_api=backend_api,
|
||||
width=width,
|
||||
height=height,
|
||||
)
|
||||
|
||||
|
||||
def list_cameras(max_index: int = DEFAULT_MAX_CAMERA_INDEX) -> list[CameraInfo]:
|
||||
"""Probe indices ``0 .. max_index`` and return working cameras."""
|
||||
found: list[CameraInfo] = []
|
||||
seen_indices: set[int] = set()
|
||||
backends = _backend_candidates()
|
||||
|
||||
with _suppress_opencv_noise():
|
||||
for index in range(0, max_index + 1):
|
||||
for backend_name, backend_api in backends:
|
||||
info = _try_open(index, backend_name, backend_api)
|
||||
if info is None:
|
||||
continue
|
||||
if index in seen_indices:
|
||||
break
|
||||
found.append(info)
|
||||
seen_indices.add(index)
|
||||
break
|
||||
return found
|
||||
|
||||
|
||||
def pick_default_camera(cameras: list[CameraInfo], preferred: str | None = None) -> str | None:
|
||||
if not cameras:
|
||||
return None
|
||||
preferred = (preferred or "").strip()
|
||||
if preferred:
|
||||
for cam in cameras:
|
||||
if cam.source == preferred:
|
||||
return cam.source
|
||||
return cameras[0].source
|
||||
|
||||
@@ -2,19 +2,14 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5.QtCore import Qt, QTimer
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import (
|
||||
QAction,
|
||||
QActionGroup,
|
||||
QApplication,
|
||||
QDockWidget,
|
||||
QFileDialog,
|
||||
QMainWindow,
|
||||
QSplitter,
|
||||
@@ -66,7 +61,6 @@ class FingerDashboardWindow(QMainWindow):
|
||||
self._frame_index = 0
|
||||
self._wall_fps = 0.0
|
||||
self._wall_fps_alpha = 0.15
|
||||
self._log_display_mode = "hidden"
|
||||
|
||||
self.setWindowTitle(layout.window_title)
|
||||
self.setStyleSheet(STYLE)
|
||||
@@ -90,7 +84,7 @@ class FingerDashboardWindow(QMainWindow):
|
||||
root_layout.setSpacing(layout.root_spacing_px)
|
||||
|
||||
setup = layout.panel_setup
|
||||
self._log_panel_visible = False
|
||||
self._log_panel_visible = setup.log_panel.enabled
|
||||
self.mesh_state = MeshViewState.from_layout(layout)
|
||||
self.mesh_viewport = MeshViewportWidget(layout=layout, mesh_state=self.mesh_state)
|
||||
arrows_slot = layout.slot("arrows")
|
||||
@@ -117,6 +111,7 @@ class FingerDashboardWindow(QMainWindow):
|
||||
|
||||
self._preferences = load_preferences()
|
||||
self.settings_panel.apply_preference_state(self._preferences)
|
||||
self._log_panel_visible = bool(self.settings_panel.show_log_check.isChecked())
|
||||
self.mesh_viewport.set_free_rotation(bool(self.settings_panel.free_rotate_check.isChecked()))
|
||||
if self.arrow_panel is not None:
|
||||
self.arrow_panel.set_background(self.settings_panel.arrow_background())
|
||||
@@ -157,6 +152,11 @@ class FingerDashboardWindow(QMainWindow):
|
||||
|
||||
self.charts_column = QSplitter(Qt.Vertical)
|
||||
self.charts_column.addWidget(self.force_charts.widget)
|
||||
if setup.log_panel.enabled:
|
||||
self.charts_column.addWidget(self.log_widget)
|
||||
self.log_widget.setVisible(self._log_panel_visible)
|
||||
self.charts_column.setStretchFactor(0, setup.charts_log_stretch)
|
||||
self.charts_column.setStretchFactor(1, setup.log_stretch)
|
||||
self.charts_column.setChildrenCollapsible(False)
|
||||
|
||||
h_splitter = QSplitter(Qt.Horizontal)
|
||||
@@ -172,104 +172,18 @@ class FingerDashboardWindow(QMainWindow):
|
||||
self.h_splitter = h_splitter
|
||||
|
||||
root_layout.addWidget(h_splitter)
|
||||
self._setup_log_dock()
|
||||
self._apply_log_display_mode(self._preferences.get("log_display_mode", "hidden"))
|
||||
|
||||
self.resize(*self._target_window_size())
|
||||
QTimer.singleShot(0, self._apply_panel_layout)
|
||||
|
||||
def _set_log_panel_visible(self, visible: bool) -> None:
|
||||
self._log_panel_visible = bool(visible)
|
||||
if hasattr(self, "log_dock"):
|
||||
self.log_dock.setVisible(self._log_panel_visible)
|
||||
self.log_widget.setVisible(self._log_panel_visible)
|
||||
QTimer.singleShot(0, self._apply_panel_layout)
|
||||
|
||||
def _setup_log_dock(self) -> None:
|
||||
self.log_dock = QDockWidget("Log", self)
|
||||
self.log_dock.setObjectName("logDock")
|
||||
self.log_dock.setWidget(self.log_widget)
|
||||
self.log_dock.setAllowedAreas(Qt.BottomDockWidgetArea | Qt.RightDockWidgetArea)
|
||||
self.addDockWidget(Qt.BottomDockWidgetArea, self.log_dock)
|
||||
self.log_dock.hide()
|
||||
self.log_dock.visibilityChanged.connect(self._on_log_dock_visibility_changed)
|
||||
|
||||
view_menu = self.menuBar().addMenu("View")
|
||||
self.log_toggle_action = QAction("Log Panel", self)
|
||||
self.log_toggle_action.setCheckable(True)
|
||||
self.log_toggle_action.triggered.connect(self._on_log_toggle_action)
|
||||
view_menu.addAction(self.log_toggle_action)
|
||||
|
||||
log_content_menu = view_menu.addMenu("Log Content")
|
||||
self.log_content_actions = QActionGroup(self)
|
||||
self.log_content_actions.setExclusive(False)
|
||||
for label, mode in (
|
||||
("Runtime log", "runtime"),
|
||||
("Bundle info", "bundle"),
|
||||
("Performance/profile", "profile"),
|
||||
):
|
||||
action = QAction(label, self)
|
||||
action.setCheckable(True)
|
||||
action.setData(mode)
|
||||
action.triggered.connect(lambda _checked=False, m=mode: self._apply_log_display_mode(m, save=True))
|
||||
self.log_content_actions.addAction(action)
|
||||
log_content_menu.addAction(action)
|
||||
|
||||
def _on_log_toggle_action(self, checked: bool) -> None:
|
||||
if checked:
|
||||
mode = self._log_display_mode if self._log_display_mode != "hidden" else "runtime"
|
||||
self._apply_log_display_mode(mode, save=True)
|
||||
else:
|
||||
self._apply_log_display_mode("hidden", save=True)
|
||||
|
||||
def _on_log_dock_visibility_changed(self, visible: bool) -> None:
|
||||
self._log_panel_visible = bool(visible)
|
||||
if hasattr(self, "log_toggle_action"):
|
||||
self.log_toggle_action.blockSignals(True)
|
||||
self.log_toggle_action.setChecked(visible)
|
||||
self.log_toggle_action.blockSignals(False)
|
||||
if not visible and self._log_display_mode != "hidden":
|
||||
self._log_display_mode = "hidden"
|
||||
if hasattr(self, "log_content_actions"):
|
||||
for action in self.log_content_actions.actions():
|
||||
action.blockSignals(True)
|
||||
action.setChecked(False)
|
||||
action.blockSignals(False)
|
||||
self._save_preferences()
|
||||
|
||||
def _apply_log_display_mode(self, mode: str | None, *, save: bool = False) -> None:
|
||||
mode = mode or "hidden"
|
||||
self._log_display_mode = mode
|
||||
if hasattr(self, "log_toggle_action"):
|
||||
self.log_toggle_action.blockSignals(True)
|
||||
self.log_toggle_action.setChecked(mode != "hidden")
|
||||
self.log_toggle_action.blockSignals(False)
|
||||
if hasattr(self, "log_content_actions"):
|
||||
for action in self.log_content_actions.actions():
|
||||
action.blockSignals(True)
|
||||
action.setChecked(action.data() == mode)
|
||||
action.blockSignals(False)
|
||||
if mode == "hidden":
|
||||
self._set_log_panel_visible(False)
|
||||
if save:
|
||||
self._save_preferences()
|
||||
return
|
||||
if mode == "bundle":
|
||||
self.log_widget.setPlainText(
|
||||
"Sensor bundle\n"
|
||||
f"Config: {self.settings_panel.config_path()}\n"
|
||||
f"Assets: {self.settings_panel.finger_export_dir()}\n"
|
||||
)
|
||||
elif mode == "profile":
|
||||
self.log_widget.setPlainText(
|
||||
"Performance/profile\n"
|
||||
"Run with --profile to print per-stage timings in the runtime log.\n"
|
||||
)
|
||||
self._set_log_panel_visible(True)
|
||||
if save:
|
||||
self._save_preferences()
|
||||
|
||||
def _on_log_visibility_changed(self, visible: bool) -> None:
|
||||
self._apply_log_display_mode("runtime" if visible else "hidden", save=True)
|
||||
self._set_log_panel_visible(visible)
|
||||
self._save_preferences()
|
||||
|
||||
def _on_free_rotation_changed(self, enabled: bool) -> None:
|
||||
self.mesh_viewport.set_free_rotation(enabled)
|
||||
@@ -282,7 +196,6 @@ class FingerDashboardWindow(QMainWindow):
|
||||
|
||||
def _save_preferences(self) -> None:
|
||||
prefs = self.settings_panel.preference_state()
|
||||
prefs["log_display_mode"] = self._log_display_mode
|
||||
self._preferences = prefs
|
||||
save_preferences(prefs)
|
||||
# Keep in-memory pipeline config aligned with panel values.
|
||||
@@ -304,6 +217,7 @@ class FingerDashboardWindow(QMainWindow):
|
||||
scale = self._layout_scale_factor()
|
||||
column_height = self._scaled_top_row_height(scale)
|
||||
charts_slot = self.layout.top_slot("charts")
|
||||
log_slot = setup.log_panel
|
||||
|
||||
for slot in setup.top_row.slots:
|
||||
if not slot.enabled:
|
||||
@@ -322,11 +236,27 @@ class FingerDashboardWindow(QMainWindow):
|
||||
settings_height,
|
||||
)
|
||||
|
||||
self.force_charts.set_panel_size(
|
||||
self._scaled_px(charts_slot.width_px, scale),
|
||||
column_height,
|
||||
)
|
||||
self.charts_column.setSizes([column_height])
|
||||
if (
|
||||
self._log_panel_visible
|
||||
and log_slot.enabled
|
||||
and log_slot.height_px > 0
|
||||
):
|
||||
self.log_widget.show()
|
||||
log_height = self._scaled_px(log_slot.height_px, scale)
|
||||
charts_height = self._scaled_px(charts_slot.height_px, scale)
|
||||
self.log_widget.setFixedHeight(log_height)
|
||||
self.force_charts.set_panel_size(
|
||||
self._scaled_px(charts_slot.width_px, scale),
|
||||
charts_height,
|
||||
)
|
||||
self.charts_column.setSizes([charts_height, log_height])
|
||||
else:
|
||||
self.log_widget.hide()
|
||||
self.force_charts.set_panel_size(
|
||||
self._scaled_px(charts_slot.width_px, scale),
|
||||
column_height,
|
||||
)
|
||||
self.charts_column.setSizes([column_height, 0])
|
||||
|
||||
self.h_splitter.setSizes(
|
||||
[self._scaled_px(slot.width_px, scale) for slot in setup.top_row.slots if slot.enabled]
|
||||
@@ -349,6 +279,12 @@ class FingerDashboardWindow(QMainWindow):
|
||||
width += margins.left() + margins.right()
|
||||
|
||||
height = layout.charts_column_height_px()
|
||||
if (
|
||||
self._log_panel_visible
|
||||
and layout.panel_setup.log_panel.enabled
|
||||
and layout.panel_setup.log_panel.height_px > 0
|
||||
):
|
||||
height += self.charts_column.handleWidth()
|
||||
height += margins.top() + margins.bottom()
|
||||
|
||||
return max(layout.window_width, width), max(layout.window_height, height)
|
||||
@@ -380,6 +316,12 @@ class FingerDashboardWindow(QMainWindow):
|
||||
base_width += max(len(enabled_slots) - 1, 0) * self.h_splitter.handleWidth()
|
||||
|
||||
base_height = self.layout.charts_column_height_px()
|
||||
if (
|
||||
self._log_panel_visible
|
||||
and self.layout.panel_setup.log_panel.enabled
|
||||
and self.layout.panel_setup.log_panel.height_px > 0
|
||||
):
|
||||
base_height += self.charts_column.handleWidth()
|
||||
if base_width <= 0 or base_height <= 0:
|
||||
return 1.0
|
||||
return min(available_width / base_width, available_height / base_height)
|
||||
@@ -669,41 +611,27 @@ class FingerDashboardWindow(QMainWindow):
|
||||
def _select_finger_assets(self) -> None:
|
||||
from .preferences import (
|
||||
default_finger_assets_dir,
|
||||
default_finger_config,
|
||||
normalize_assets_preference,
|
||||
normalize_config_preference,
|
||||
packaged_finger_bundle_root,
|
||||
packaged_finger_assets_root,
|
||||
)
|
||||
|
||||
start = packaged_finger_bundle_root()
|
||||
start = packaged_finger_assets_root()
|
||||
start_dir = (
|
||||
str(start)
|
||||
if start is not None
|
||||
else (self.settings_panel.finger_export_dir() or default_finger_assets_dir())
|
||||
)
|
||||
path = QFileDialog.getExistingDirectory(
|
||||
self,
|
||||
"选择传感器 bundle 目录",
|
||||
str(start) if start is not None else (
|
||||
self.settings_panel.sensor_bundle_dir() or "sensors/finger"
|
||||
),
|
||||
"选择 finger 资产目录",
|
||||
start_dir,
|
||||
)
|
||||
if path:
|
||||
bundle_dir = Path(path)
|
||||
config_path = bundle_dir / "sensor.json"
|
||||
manifest_path = bundle_dir / "manifest.json"
|
||||
assets_dir = bundle_dir / "assets" / "30fps-0703"
|
||||
if manifest_path.is_file():
|
||||
try:
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
manifest = {}
|
||||
rel_assets = manifest.get("assets_dir")
|
||||
if isinstance(rel_assets, str) and rel_assets.strip():
|
||||
assets_dir = bundle_dir / rel_assets
|
||||
if config_path.is_file():
|
||||
config = normalize_config_preference(str(config_path)) or default_finger_config()
|
||||
self.settings_panel.set_config_path(config)
|
||||
self.config.config = config
|
||||
assets = normalize_assets_preference(str(assets_dir)) or default_finger_assets_dir()
|
||||
assets = normalize_assets_preference(path)
|
||||
self.settings_panel.set_finger_export_dir(assets)
|
||||
self.config.export_dir = assets
|
||||
self._save_preferences()
|
||||
|
||||
def _select_csv_dir(self) -> None:
|
||||
path = QFileDialog.getExistingDirectory(
|
||||
self,
|
||||
|
||||
@@ -103,10 +103,12 @@ def normalize_assets_preference(value: str | None) -> str:
|
||||
return default
|
||||
except OSError:
|
||||
pass
|
||||
# Keep a user-selected custom directory that still exists.
|
||||
if "sensors/finger" in posix or path.name == "30fps-0703":
|
||||
return text if path.is_absolute() else default
|
||||
return text
|
||||
|
||||
# Missing old absolute/repo paths (assets/finger, ./config era) -> packaged default.
|
||||
return default
|
||||
|
||||
|
||||
|
||||
@@ -15,16 +15,11 @@ from PyQt5.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from viz.render_2d import ArrowBackground
|
||||
from orisys.finger import DEFAULT_FINGER_EXPORT_DIR
|
||||
from viz.render_2d import ArrowBackground
|
||||
|
||||
from ..layout import DashboardLayout, LAYOUT
|
||||
from ..mesh_view_state import MeshViewState
|
||||
from ..presets import (
|
||||
SensorBundlePreset,
|
||||
discover_sensor_bundle_presets,
|
||||
sensor_bundle_preset_index,
|
||||
)
|
||||
from ..transport_icons import reset_icon, transport_icon
|
||||
|
||||
_PLAY_PAUSE_BUTTON_NAME = "btnPlayPause"
|
||||
@@ -91,7 +86,7 @@ class SettingsPanelWidget(QWidget):
|
||||
self.show_log_check = QCheckBox("Show log panel")
|
||||
self.show_log_check.setChecked(show_log)
|
||||
self.show_log_check.toggled.connect(self.log_visibility_changed.emit)
|
||||
self.show_log_check.hide()
|
||||
root.addWidget(self.show_log_check)
|
||||
|
||||
self.show_flow_arrows_check = QCheckBox("Show displacement arrows")
|
||||
self.show_flow_arrows_check.setChecked(self._mesh_state.flow_arrows_enabled)
|
||||
@@ -139,38 +134,36 @@ class SettingsPanelWidget(QWidget):
|
||||
self._video_file_path = ""
|
||||
self._updating_camera_options = False
|
||||
|
||||
root.addWidget(self._caption("Sensor bundle"))
|
||||
bundle_row = QHBoxLayout()
|
||||
bundle_row.setSpacing(6)
|
||||
self.sensor_bundle_combo = QComboBox()
|
||||
self.sensor_bundle_combo.setToolTip("Select a finger sensor bundle")
|
||||
self._bundle_presets = discover_sensor_bundle_presets()
|
||||
for preset in self._bundle_presets:
|
||||
self.sensor_bundle_combo.addItem(preset.label, preset)
|
||||
self.sensor_bundle_combo.addItem("Custom…", None)
|
||||
self._updating_bundle_combo = False
|
||||
self.sensor_bundle_combo.currentIndexChanged.connect(self._on_bundle_combo_changed)
|
||||
bundle_row.addWidget(self.sensor_bundle_combo, 1)
|
||||
self.btn_browse_finger_assets = QPushButton("…")
|
||||
self.btn_browse_finger_assets.setFixedWidth(32)
|
||||
self.btn_browse_finger_assets.setToolTip("Browse sensor bundle folder")
|
||||
self.btn_browse_finger_assets.clicked.connect(self.browse_finger_assets_requested.emit)
|
||||
bundle_row.addWidget(self.btn_browse_finger_assets)
|
||||
root.addLayout(bundle_row)
|
||||
|
||||
root.addWidget(self._caption("Config"))
|
||||
config_row = QHBoxLayout()
|
||||
config_row.setSpacing(6)
|
||||
self.config_edit = QLineEdit("finger")
|
||||
self.config_edit.hide()
|
||||
config_row.addWidget(self.config_edit, 1)
|
||||
self.btn_browse_config = QPushButton("…")
|
||||
self.btn_browse_config.hide()
|
||||
self.btn_browse_config.setFixedWidth(32)
|
||||
self.btn_browse_config.setToolTip("Browse config JSON")
|
||||
self.btn_browse_config.clicked.connect(self.browse_config_requested.emit)
|
||||
config_row.addWidget(self.btn_browse_config)
|
||||
root.addLayout(config_row)
|
||||
|
||||
# Optional calibration path is kept for future re-enablement but hidden for now.
|
||||
self.cal_edit = QLineEdit("")
|
||||
self.cal_edit.setPlaceholderText("Optional calibration path")
|
||||
self.cal_edit.hide()
|
||||
|
||||
root.addWidget(self._caption("Finger assets"))
|
||||
finger_assets_row = QHBoxLayout()
|
||||
finger_assets_row.setSpacing(6)
|
||||
self.finger_assets_edit = QLineEdit(DEFAULT_FINGER_EXPORT_DIR)
|
||||
self.finger_assets_edit.hide()
|
||||
self._apply_bundle_preset(self._bundle_presets[0])
|
||||
self.finger_assets_edit.setPlaceholderText("Blender export bundle directory")
|
||||
finger_assets_row.addWidget(self.finger_assets_edit, 1)
|
||||
self.btn_browse_finger_assets = QPushButton("…")
|
||||
self.btn_browse_finger_assets.setFixedWidth(32)
|
||||
self.btn_browse_finger_assets.setToolTip("Browse finger asset directory")
|
||||
self.btn_browse_finger_assets.clicked.connect(self.browse_finger_assets_requested.emit)
|
||||
finger_assets_row.addWidget(self.btn_browse_finger_assets)
|
||||
root.addLayout(finger_assets_row)
|
||||
|
||||
root.addWidget(self._caption("CSV save directory"))
|
||||
csv_dir_row = QHBoxLayout()
|
||||
csv_dir_row.setSpacing(6)
|
||||
@@ -344,18 +337,16 @@ class SettingsPanelWidget(QWidget):
|
||||
|
||||
def set_config_path(self, value: str) -> None:
|
||||
self.config_edit.setText(value)
|
||||
self._sync_bundle_combo_from_paths()
|
||||
|
||||
def set_calibration_path(self, value: str) -> None:
|
||||
self.cal_edit.setText(value or "")
|
||||
|
||||
def set_finger_export_dir(self, value: str) -> None:
|
||||
self.finger_assets_edit.setText(value)
|
||||
self._sync_bundle_combo_from_paths()
|
||||
|
||||
def set_finger_export_dir_enabled(self, enabled: bool) -> None:
|
||||
self.finger_assets_edit.setEnabled(enabled)
|
||||
self.btn_browse_finger_assets.setEnabled(enabled)
|
||||
self.sensor_bundle_combo.setEnabled(enabled)
|
||||
|
||||
def set_csv_save_dir(self, value: str) -> None:
|
||||
self.csv_dir_edit.setText(value)
|
||||
@@ -396,33 +387,6 @@ class SettingsPanelWidget(QWidget):
|
||||
return
|
||||
self.csv_record_toggled.emit(not self._csv_recording)
|
||||
|
||||
def _apply_bundle_preset(self, preset: SensorBundlePreset) -> None:
|
||||
self.config_edit.setText(preset.config)
|
||||
self.finger_assets_edit.setText(preset.export_dir)
|
||||
|
||||
def _on_bundle_combo_changed(self, _index: int) -> None:
|
||||
if self._updating_bundle_combo:
|
||||
return
|
||||
preset = self.sensor_bundle_combo.currentData()
|
||||
if isinstance(preset, SensorBundlePreset):
|
||||
self._apply_bundle_preset(preset)
|
||||
|
||||
def _sync_bundle_combo_from_paths(self) -> None:
|
||||
idx = sensor_bundle_preset_index(
|
||||
self.config_edit.text(),
|
||||
self.finger_assets_edit.text(),
|
||||
self._bundle_presets,
|
||||
)
|
||||
if idx < 0:
|
||||
idx = self.sensor_bundle_combo.findData(None)
|
||||
if idx < 0:
|
||||
return
|
||||
self._updating_bundle_combo = True
|
||||
self.sensor_bundle_combo.blockSignals(True)
|
||||
self.sensor_bundle_combo.setCurrentIndex(idx)
|
||||
self.sensor_bundle_combo.blockSignals(False)
|
||||
self._updating_bundle_combo = False
|
||||
|
||||
def _emit_arrow_background_changed(self, _index: int) -> None:
|
||||
value = self.arrow_background_combo.currentData()
|
||||
if value:
|
||||
@@ -449,15 +413,6 @@ class SettingsPanelWidget(QWidget):
|
||||
def finger_export_dir(self) -> str:
|
||||
return self.finger_assets_edit.text().strip()
|
||||
|
||||
def sensor_bundle_dir(self) -> str:
|
||||
preset = self.sensor_bundle_combo.currentData()
|
||||
if isinstance(preset, SensorBundlePreset):
|
||||
return preset.bundle_dir
|
||||
config = self.config_path()
|
||||
if config.endswith("/sensor.json") or config.endswith("\\sensor.json"):
|
||||
return config.rsplit("/", 1)[0].rsplit("\\", 1)[0]
|
||||
return ""
|
||||
|
||||
def csv_save_dir(self) -> str:
|
||||
return self.csv_dir_edit.text().strip()
|
||||
|
||||
@@ -465,7 +420,6 @@ class SettingsPanelWidget(QWidget):
|
||||
"""Serialize user-facing settings suitable for preference.json."""
|
||||
return {
|
||||
"video_source": self.video_source(),
|
||||
"sensor_bundle_dir": self.sensor_bundle_dir(),
|
||||
"config_path": self.config_path(),
|
||||
"finger_assets_dir": self.finger_export_dir(),
|
||||
"csv_save_dir": self.csv_save_dir(),
|
||||
@@ -511,8 +465,6 @@ class SettingsPanelWidget(QWidget):
|
||||
if isinstance(assets, str) and assets.strip():
|
||||
self.set_finger_export_dir(assets.strip())
|
||||
|
||||
self._sync_bundle_combo_from_paths()
|
||||
|
||||
csv_dir = state.get("csv_save_dir")
|
||||
if isinstance(csv_dir, str) and csv_dir.strip():
|
||||
self.set_csv_save_dir(csv_dir.strip())
|
||||
|
||||
Reference in New Issue
Block a user