organize settings to dedicated sensors folder, json + assets
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -33,7 +33,11 @@ from common.paths import NUITKA_COMPILE_DIR, NUITKA_WHEEL_DIR, REPO_ROOT, repair
|
|||||||
from common.pyproject_utils import bundle_items, extras_require, get_version, install_requires
|
from common.pyproject_utils import bundle_items, extras_require, get_version, install_requires
|
||||||
from common.verify.wheel import verify_built_wheel
|
from common.verify.wheel import verify_built_wheel
|
||||||
from wheel.compile import compile_orisys, copy_compiled_orisys, install_nuitka
|
from wheel.compile import compile_orisys, copy_compiled_orisys, install_nuitka
|
||||||
from wheel.layout import copy_bundle_items, copy_orisys_blend, package_data_from_bundle
|
from wheel.layout import (
|
||||||
|
copy_bundle_items,
|
||||||
|
copy_orisys_blend,
|
||||||
|
package_data_from_bundle,
|
||||||
|
)
|
||||||
from wheel.package import (
|
from wheel.package import (
|
||||||
is_compliant_manylinux_wheel,
|
is_compliant_manylinux_wheel,
|
||||||
publish_wheel_to_sdk,
|
publish_wheel_to_sdk,
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ from __future__ import annotations
|
|||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Required files that must ship with the wheel for ORY-FINGER runtime/GUI.
|
||||||
|
REQUIRED_ASSET_RELPATHS = (
|
||||||
|
"finger/assets/30fps-0703/mesh.npz",
|
||||||
|
"finger/assets/30fps-0703/camera_intrinsics.npz",
|
||||||
|
"finger/assets/30fps-0703/world_to_cam.npy",
|
||||||
|
"finger/assets/visualization/world_to_cam.npy",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def resolve_bundle_source_files(repo_root: Path, item: dict) -> list[Path]:
|
def resolve_bundle_source_files(repo_root: Path, item: dict) -> list[Path]:
|
||||||
"""Resolve explicit files and/or glob patterns for one bundle item."""
|
"""Resolve explicit files and/or glob patterns for one bundle item."""
|
||||||
@@ -37,6 +45,32 @@ def resolve_bundle_source_files(repo_root: Path, item: dict) -> list[Path]:
|
|||||||
return resolved
|
return resolved
|
||||||
|
|
||||||
|
|
||||||
|
def verify_required_assets(repo_root: Path, items: list[dict]) -> None:
|
||||||
|
"""Fail early when required finger assets are missing from the bundle."""
|
||||||
|
asset_items = [item for item in items if item.get("name") == "sensors"]
|
||||||
|
if not asset_items:
|
||||||
|
return
|
||||||
|
|
||||||
|
resolved: set[str] = set()
|
||||||
|
for item in asset_items:
|
||||||
|
src_dir = (repo_root / item["source"]).resolve()
|
||||||
|
for path in resolve_bundle_source_files(repo_root, item):
|
||||||
|
try:
|
||||||
|
rel = path.resolve().relative_to(src_dir).as_posix()
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
resolved.add(rel)
|
||||||
|
|
||||||
|
missing = [rel for rel in REQUIRED_ASSET_RELPATHS if rel not in resolved]
|
||||||
|
if missing:
|
||||||
|
joined = ", ".join(missing)
|
||||||
|
raise FileNotFoundError(
|
||||||
|
"Required finger assets are missing from the wheel bundle. "
|
||||||
|
f"Missing: {joined}. Update [[tool.orisys.bundle.items]] patterns "
|
||||||
|
"or restore assets under sensors/finger/assets/ before building."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def package_data_paths_for_item(item: dict) -> list[str]:
|
def package_data_paths_for_item(item: dict) -> list[str]:
|
||||||
"""Return setuptools package_data paths relative to the orisys package."""
|
"""Return setuptools package_data paths relative to the orisys package."""
|
||||||
target = str(item["target"]).replace("\\", "/")
|
target = str(item["target"]).replace("\\", "/")
|
||||||
@@ -54,6 +88,7 @@ def data_file_entries_for_item(item: dict) -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
def copy_bundle_items(repo_root: Path, pkg_dir: Path, items: list[dict]) -> dict[str, int]:
|
def copy_bundle_items(repo_root: Path, pkg_dir: Path, items: list[dict]) -> dict[str, int]:
|
||||||
|
verify_required_assets(repo_root, items)
|
||||||
copied_counts: dict[str, int] = {}
|
copied_counts: dict[str, int] = {}
|
||||||
for item in items:
|
for item in items:
|
||||||
name = item["name"]
|
name = item["name"]
|
||||||
|
|||||||
+2
-2
@@ -5,7 +5,7 @@
|
|||||||
"camera": {
|
"camera": {
|
||||||
"resolution_raw": [1280, 720],
|
"resolution_raw": [1280, 720],
|
||||||
"fps": 30,
|
"fps": 30,
|
||||||
"resolution_stitch": [420, 450],
|
"resolution_stitch": [280, 300],
|
||||||
"source_scale": 0.4,
|
"source_scale": 0.4,
|
||||||
"old":[672, 728, 1,0.5],
|
"old":[672, 728, 1,0.5],
|
||||||
"work": [504, 546,1],
|
"work": [504, 546,1],
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
"loop_closure_interval": 1,
|
"loop_closure_interval": 1,
|
||||||
"algorithm": "dis",
|
"algorithm": "dis",
|
||||||
"dis_preset": "medium",
|
"dis_preset": "medium",
|
||||||
"dis_mode": 3,
|
"dis_mode": 1,
|
||||||
"algorithm_list": ["raft", "dis"],
|
"algorithm_list": ["raft", "dis"],
|
||||||
"hhd_downsample": 5
|
"hhd_downsample": 5
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
{
|
{
|
||||||
"video_source": "D:/repo/orisys-sdk/data/dd02/0607_1.mp4",
|
"video_source": "D:/repo/orisys-sdk/data/dd02-0703/1.mp4",
|
||||||
"config_path": "./config/dd02-ov.json",
|
"sensor_bundle_dir": "sensors/finger",
|
||||||
"finger_assets_dir": "D:/repo/orisys-sdk/assets/finger/30fps-0703",
|
"config_path": "sensors/finger/sensor.json",
|
||||||
|
"finger_assets_dir": "sensors/finger/assets/30fps-0703",
|
||||||
"csv_save_dir": "./outputs/csv",
|
"csv_save_dir": "./outputs/csv",
|
||||||
"show_log": true,
|
"show_log": true,
|
||||||
"show_flow_arrows": false,
|
"show_flow_arrows": false,
|
||||||
"arrow_background": "pure_color",
|
"arrow_background": "unrolled",
|
||||||
"free_rotate": true
|
"free_rotate": true,
|
||||||
|
"log_display_mode": "hidden"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,19 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
import math
|
import math
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, QTimer
|
from PyQt5.QtCore import Qt, QTimer
|
||||||
from PyQt5.QtGui import QIcon
|
from PyQt5.QtGui import QIcon
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
|
QAction,
|
||||||
|
QActionGroup,
|
||||||
QApplication,
|
QApplication,
|
||||||
|
QDockWidget,
|
||||||
QFileDialog,
|
QFileDialog,
|
||||||
QMainWindow,
|
QMainWindow,
|
||||||
QSplitter,
|
QSplitter,
|
||||||
@@ -61,6 +66,7 @@ class FingerDashboardWindow(QMainWindow):
|
|||||||
self._frame_index = 0
|
self._frame_index = 0
|
||||||
self._wall_fps = 0.0
|
self._wall_fps = 0.0
|
||||||
self._wall_fps_alpha = 0.15
|
self._wall_fps_alpha = 0.15
|
||||||
|
self._log_display_mode = "hidden"
|
||||||
|
|
||||||
self.setWindowTitle(layout.window_title)
|
self.setWindowTitle(layout.window_title)
|
||||||
self.setStyleSheet(STYLE)
|
self.setStyleSheet(STYLE)
|
||||||
@@ -84,7 +90,7 @@ class FingerDashboardWindow(QMainWindow):
|
|||||||
root_layout.setSpacing(layout.root_spacing_px)
|
root_layout.setSpacing(layout.root_spacing_px)
|
||||||
|
|
||||||
setup = layout.panel_setup
|
setup = layout.panel_setup
|
||||||
self._log_panel_visible = setup.log_panel.enabled
|
self._log_panel_visible = False
|
||||||
self.mesh_state = MeshViewState.from_layout(layout)
|
self.mesh_state = MeshViewState.from_layout(layout)
|
||||||
self.mesh_viewport = MeshViewportWidget(layout=layout, mesh_state=self.mesh_state)
|
self.mesh_viewport = MeshViewportWidget(layout=layout, mesh_state=self.mesh_state)
|
||||||
arrows_slot = layout.slot("arrows")
|
arrows_slot = layout.slot("arrows")
|
||||||
@@ -111,7 +117,6 @@ class FingerDashboardWindow(QMainWindow):
|
|||||||
|
|
||||||
self._preferences = load_preferences()
|
self._preferences = load_preferences()
|
||||||
self.settings_panel.apply_preference_state(self._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()))
|
self.mesh_viewport.set_free_rotation(bool(self.settings_panel.free_rotate_check.isChecked()))
|
||||||
if self.arrow_panel is not None:
|
if self.arrow_panel is not None:
|
||||||
self.arrow_panel.set_background(self.settings_panel.arrow_background())
|
self.arrow_panel.set_background(self.settings_panel.arrow_background())
|
||||||
@@ -152,11 +157,6 @@ class FingerDashboardWindow(QMainWindow):
|
|||||||
|
|
||||||
self.charts_column = QSplitter(Qt.Vertical)
|
self.charts_column = QSplitter(Qt.Vertical)
|
||||||
self.charts_column.addWidget(self.force_charts.widget)
|
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)
|
self.charts_column.setChildrenCollapsible(False)
|
||||||
|
|
||||||
h_splitter = QSplitter(Qt.Horizontal)
|
h_splitter = QSplitter(Qt.Horizontal)
|
||||||
@@ -172,19 +172,105 @@ class FingerDashboardWindow(QMainWindow):
|
|||||||
self.h_splitter = h_splitter
|
self.h_splitter = h_splitter
|
||||||
|
|
||||||
root_layout.addWidget(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())
|
self.resize(*self._target_window_size())
|
||||||
QTimer.singleShot(0, self._apply_panel_layout)
|
QTimer.singleShot(0, self._apply_panel_layout)
|
||||||
|
|
||||||
def _set_log_panel_visible(self, visible: bool) -> None:
|
def _set_log_panel_visible(self, visible: bool) -> None:
|
||||||
self._log_panel_visible = bool(visible)
|
self._log_panel_visible = bool(visible)
|
||||||
self.log_widget.setVisible(self._log_panel_visible)
|
if hasattr(self, "log_dock"):
|
||||||
|
self.log_dock.setVisible(self._log_panel_visible)
|
||||||
QTimer.singleShot(0, self._apply_panel_layout)
|
QTimer.singleShot(0, self._apply_panel_layout)
|
||||||
|
|
||||||
def _on_log_visibility_changed(self, visible: bool) -> None:
|
def _setup_log_dock(self) -> None:
|
||||||
self._set_log_panel_visible(visible)
|
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()
|
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)
|
||||||
|
|
||||||
def _on_free_rotation_changed(self, enabled: bool) -> None:
|
def _on_free_rotation_changed(self, enabled: bool) -> None:
|
||||||
self.mesh_viewport.set_free_rotation(enabled)
|
self.mesh_viewport.set_free_rotation(enabled)
|
||||||
self._save_preferences()
|
self._save_preferences()
|
||||||
@@ -196,6 +282,7 @@ class FingerDashboardWindow(QMainWindow):
|
|||||||
|
|
||||||
def _save_preferences(self) -> None:
|
def _save_preferences(self) -> None:
|
||||||
prefs = self.settings_panel.preference_state()
|
prefs = self.settings_panel.preference_state()
|
||||||
|
prefs["log_display_mode"] = self._log_display_mode
|
||||||
self._preferences = prefs
|
self._preferences = prefs
|
||||||
save_preferences(prefs)
|
save_preferences(prefs)
|
||||||
# Keep in-memory pipeline config aligned with panel values.
|
# Keep in-memory pipeline config aligned with panel values.
|
||||||
@@ -217,7 +304,6 @@ class FingerDashboardWindow(QMainWindow):
|
|||||||
scale = self._layout_scale_factor()
|
scale = self._layout_scale_factor()
|
||||||
column_height = self._scaled_top_row_height(scale)
|
column_height = self._scaled_top_row_height(scale)
|
||||||
charts_slot = self.layout.top_slot("charts")
|
charts_slot = self.layout.top_slot("charts")
|
||||||
log_slot = setup.log_panel
|
|
||||||
|
|
||||||
for slot in setup.top_row.slots:
|
for slot in setup.top_row.slots:
|
||||||
if not slot.enabled:
|
if not slot.enabled:
|
||||||
@@ -236,27 +322,11 @@ class FingerDashboardWindow(QMainWindow):
|
|||||||
settings_height,
|
settings_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.force_charts.set_panel_size(
|
||||||
self._scaled_px(charts_slot.width_px, scale),
|
self._scaled_px(charts_slot.width_px, scale),
|
||||||
column_height,
|
column_height,
|
||||||
)
|
)
|
||||||
self.charts_column.setSizes([column_height, 0])
|
self.charts_column.setSizes([column_height])
|
||||||
|
|
||||||
self.h_splitter.setSizes(
|
self.h_splitter.setSizes(
|
||||||
[self._scaled_px(slot.width_px, scale) for slot in setup.top_row.slots if slot.enabled]
|
[self._scaled_px(slot.width_px, scale) for slot in setup.top_row.slots if slot.enabled]
|
||||||
@@ -279,12 +349,6 @@ class FingerDashboardWindow(QMainWindow):
|
|||||||
width += margins.left() + margins.right()
|
width += margins.left() + margins.right()
|
||||||
|
|
||||||
height = layout.charts_column_height_px()
|
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()
|
height += margins.top() + margins.bottom()
|
||||||
|
|
||||||
return max(layout.window_width, width), max(layout.window_height, height)
|
return max(layout.window_width, width), max(layout.window_height, height)
|
||||||
@@ -316,12 +380,6 @@ class FingerDashboardWindow(QMainWindow):
|
|||||||
base_width += max(len(enabled_slots) - 1, 0) * self.h_splitter.handleWidth()
|
base_width += max(len(enabled_slots) - 1, 0) * self.h_splitter.handleWidth()
|
||||||
|
|
||||||
base_height = self.layout.charts_column_height_px()
|
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:
|
if base_width <= 0 or base_height <= 0:
|
||||||
return 1.0
|
return 1.0
|
||||||
return min(available_width / base_width, available_height / base_height)
|
return min(available_width / base_width, available_height / base_height)
|
||||||
@@ -602,12 +660,27 @@ class FingerDashboardWindow(QMainWindow):
|
|||||||
def _select_finger_assets(self) -> None:
|
def _select_finger_assets(self) -> None:
|
||||||
path = QFileDialog.getExistingDirectory(
|
path = QFileDialog.getExistingDirectory(
|
||||||
self,
|
self,
|
||||||
"选择 finger 资产目录",
|
"选择传感器 bundle 目录",
|
||||||
self.settings_panel.finger_export_dir() or "assets/finger",
|
self.settings_panel.sensor_bundle_dir() or "sensors/finger",
|
||||||
)
|
)
|
||||||
if path:
|
if path:
|
||||||
self.settings_panel.set_finger_export_dir(path)
|
bundle_dir = Path(path)
|
||||||
self.config.export_dir = 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():
|
||||||
|
self.settings_panel.set_config_path(str(config_path))
|
||||||
|
self.config.config = str(config_path)
|
||||||
|
self.settings_panel.set_finger_export_dir(str(assets_dir))
|
||||||
|
self.config.export_dir = str(assets_dir)
|
||||||
self._save_preferences()
|
self._save_preferences()
|
||||||
|
|
||||||
def _select_csv_dir(self) -> None:
|
def _select_csv_dir(self) -> None:
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
"""Finger-compatible sensor-bundle presets for the 3D dashboard."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from orisys.finger import DEFAULT_FINGER_EXPORT_DIR
|
||||||
|
|
||||||
|
|
||||||
|
def _repo_root_candidates() -> list[Path]:
|
||||||
|
here = Path(__file__).resolve()
|
||||||
|
return [
|
||||||
|
Path.cwd(),
|
||||||
|
here.parents[3], # examples/gui/viewer3d -> repo root
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SensorBundlePreset:
|
||||||
|
label: str
|
||||||
|
bundle_dir: str
|
||||||
|
config: str
|
||||||
|
export_dir: str
|
||||||
|
|
||||||
|
|
||||||
|
def _load_manifest(bundle_root: Path) -> dict:
|
||||||
|
path = bundle_root / "manifest.json"
|
||||||
|
if not path.is_file():
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _bundle_from_root(bundle_root: Path, *, label: str | None = None) -> SensorBundlePreset | None:
|
||||||
|
sensor_json = bundle_root / "sensor.json"
|
||||||
|
if not sensor_json.is_file():
|
||||||
|
return None
|
||||||
|
manifest = _load_manifest(bundle_root)
|
||||||
|
assets_dir = manifest.get("assets_dir", "assets/30fps-0703")
|
||||||
|
if not isinstance(assets_dir, str) or not assets_dir.strip():
|
||||||
|
assets_dir = "assets/30fps-0703"
|
||||||
|
rel_bundle = bundle_root.as_posix()
|
||||||
|
for root in _repo_root_candidates():
|
||||||
|
try:
|
||||||
|
rel_bundle = bundle_root.relative_to(root).as_posix()
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
return SensorBundlePreset(
|
||||||
|
label=label or "ORY-FINGER",
|
||||||
|
bundle_dir=rel_bundle,
|
||||||
|
config=f"{rel_bundle}/sensor.json",
|
||||||
|
export_dir=f"{rel_bundle}/{assets_dir}".replace("\\", "/"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def default_finger_bundle() -> SensorBundlePreset:
|
||||||
|
for root in _repo_root_candidates():
|
||||||
|
preset = _bundle_from_root(root / "sensors" / "finger", label="ORY-FINGER")
|
||||||
|
if preset is not None:
|
||||||
|
return preset
|
||||||
|
return SensorBundlePreset(
|
||||||
|
label="ORY-FINGER",
|
||||||
|
bundle_dir="sensors/finger",
|
||||||
|
config="sensors/finger/sensor.json",
|
||||||
|
export_dir=DEFAULT_FINGER_EXPORT_DIR,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def discover_sensor_bundle_presets() -> list[SensorBundlePreset]:
|
||||||
|
"""Return finger-compatible sensor bundle presets."""
|
||||||
|
presets = [default_finger_bundle()]
|
||||||
|
known_dirs = {presets[0].bundle_dir.replace("\\", "/")}
|
||||||
|
|
||||||
|
for root in _repo_root_candidates():
|
||||||
|
sensors_root = root / "sensors"
|
||||||
|
if not sensors_root.is_dir():
|
||||||
|
continue
|
||||||
|
for child in sorted(sensors_root.iterdir()):
|
||||||
|
if not child.is_dir():
|
||||||
|
continue
|
||||||
|
if child.name != "finger":
|
||||||
|
continue
|
||||||
|
preset = _bundle_from_root(child, label="ORY-FINGER")
|
||||||
|
if preset is None:
|
||||||
|
continue
|
||||||
|
key = preset.bundle_dir.replace("\\", "/")
|
||||||
|
if key in known_dirs:
|
||||||
|
continue
|
||||||
|
presets.append(preset)
|
||||||
|
known_dirs.add(key)
|
||||||
|
break
|
||||||
|
|
||||||
|
return presets
|
||||||
|
|
||||||
|
|
||||||
|
def sensor_bundle_preset_index(
|
||||||
|
config: str,
|
||||||
|
export_dir: str,
|
||||||
|
presets: list[SensorBundlePreset],
|
||||||
|
) -> int:
|
||||||
|
config = (config or "").strip().replace("\\", "/")
|
||||||
|
export_dir = (export_dir or "").strip().replace("\\", "/")
|
||||||
|
for idx, preset in enumerate(presets):
|
||||||
|
if config == preset.config and export_dir == preset.export_dir:
|
||||||
|
return idx
|
||||||
|
if config.endswith("/sensor.json") and export_dir.endswith("/" + Path(preset.export_dir).name):
|
||||||
|
return idx
|
||||||
|
return -1
|
||||||
@@ -15,11 +15,15 @@ from PyQt5.QtWidgets import (
|
|||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
from orisys.finger import DEFAULT_FINGER_EXPORT_DIR
|
|
||||||
from viz.render_2d import ArrowBackground
|
from viz.render_2d import ArrowBackground
|
||||||
|
|
||||||
from ..layout import DashboardLayout, LAYOUT
|
from ..layout import DashboardLayout, LAYOUT
|
||||||
from ..mesh_view_state import MeshViewState
|
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
|
from ..transport_icons import reset_icon, transport_icon
|
||||||
|
|
||||||
_PLAY_PAUSE_BUTTON_NAME = "btnPlayPause"
|
_PLAY_PAUSE_BUTTON_NAME = "btnPlayPause"
|
||||||
@@ -86,7 +90,7 @@ class SettingsPanelWidget(QWidget):
|
|||||||
self.show_log_check = QCheckBox("Show log panel")
|
self.show_log_check = QCheckBox("Show log panel")
|
||||||
self.show_log_check.setChecked(show_log)
|
self.show_log_check.setChecked(show_log)
|
||||||
self.show_log_check.toggled.connect(self.log_visibility_changed.emit)
|
self.show_log_check.toggled.connect(self.log_visibility_changed.emit)
|
||||||
root.addWidget(self.show_log_check)
|
self.show_log_check.hide()
|
||||||
|
|
||||||
self.show_flow_arrows_check = QCheckBox("Show displacement arrows")
|
self.show_flow_arrows_check = QCheckBox("Show displacement arrows")
|
||||||
self.show_flow_arrows_check.setChecked(self._mesh_state.flow_arrows_enabled)
|
self.show_flow_arrows_check.setChecked(self._mesh_state.flow_arrows_enabled)
|
||||||
@@ -134,35 +138,38 @@ class SettingsPanelWidget(QWidget):
|
|||||||
self._video_file_path = ""
|
self._video_file_path = ""
|
||||||
self._updating_camera_options = False
|
self._updating_camera_options = False
|
||||||
|
|
||||||
root.addWidget(self._caption("Config"))
|
root.addWidget(self._caption("Sensor bundle"))
|
||||||
config_row = QHBoxLayout()
|
bundle_row = QHBoxLayout()
|
||||||
config_row.setSpacing(6)
|
bundle_row.setSpacing(6)
|
||||||
self.config_edit = QLineEdit("finger")
|
self.sensor_bundle_combo = QComboBox()
|
||||||
config_row.addWidget(self.config_edit, 1)
|
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)
|
||||||
|
|
||||||
|
self.config_edit = QLineEdit("sensors/finger/sensor.json")
|
||||||
|
self.config_edit.hide()
|
||||||
self.btn_browse_config = QPushButton("…")
|
self.btn_browse_config = QPushButton("…")
|
||||||
self.btn_browse_config.setFixedWidth(32)
|
self.btn_browse_config.hide()
|
||||||
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.
|
# Optional calibration path is kept for future re-enablement but hidden for now.
|
||||||
self.cal_edit = QLineEdit("")
|
self.cal_edit = QLineEdit("")
|
||||||
self.cal_edit.setPlaceholderText("Optional calibration path")
|
self.cal_edit.setPlaceholderText("Optional calibration path")
|
||||||
self.cal_edit.hide()
|
self.cal_edit.hide()
|
||||||
|
|
||||||
root.addWidget(self._caption("Finger assets"))
|
self.finger_assets_edit = QLineEdit("sensors/finger/assets/30fps-0703")
|
||||||
finger_assets_row = QHBoxLayout()
|
self.finger_assets_edit.hide()
|
||||||
finger_assets_row.setSpacing(6)
|
self._apply_bundle_preset(self._bundle_presets[0])
|
||||||
self.finger_assets_edit = QLineEdit(DEFAULT_FINGER_EXPORT_DIR)
|
|
||||||
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"))
|
root.addWidget(self._caption("CSV save directory"))
|
||||||
csv_dir_row = QHBoxLayout()
|
csv_dir_row = QHBoxLayout()
|
||||||
@@ -337,16 +344,18 @@ class SettingsPanelWidget(QWidget):
|
|||||||
|
|
||||||
def set_config_path(self, value: str) -> None:
|
def set_config_path(self, value: str) -> None:
|
||||||
self.config_edit.setText(value)
|
self.config_edit.setText(value)
|
||||||
|
self._sync_bundle_combo_from_paths()
|
||||||
|
|
||||||
def set_calibration_path(self, value: str) -> None:
|
def set_calibration_path(self, value: str) -> None:
|
||||||
self.cal_edit.setText(value or "")
|
self.cal_edit.setText(value or "")
|
||||||
|
|
||||||
def set_finger_export_dir(self, value: str) -> None:
|
def set_finger_export_dir(self, value: str) -> None:
|
||||||
self.finger_assets_edit.setText(value)
|
self.finger_assets_edit.setText(value)
|
||||||
|
self._sync_bundle_combo_from_paths()
|
||||||
|
|
||||||
def set_finger_export_dir_enabled(self, enabled: bool) -> None:
|
def set_finger_export_dir_enabled(self, enabled: bool) -> None:
|
||||||
self.finger_assets_edit.setEnabled(enabled)
|
|
||||||
self.btn_browse_finger_assets.setEnabled(enabled)
|
self.btn_browse_finger_assets.setEnabled(enabled)
|
||||||
|
self.sensor_bundle_combo.setEnabled(enabled)
|
||||||
|
|
||||||
def set_csv_save_dir(self, value: str) -> None:
|
def set_csv_save_dir(self, value: str) -> None:
|
||||||
self.csv_dir_edit.setText(value)
|
self.csv_dir_edit.setText(value)
|
||||||
@@ -387,6 +396,33 @@ class SettingsPanelWidget(QWidget):
|
|||||||
return
|
return
|
||||||
self.csv_record_toggled.emit(not self._csv_recording)
|
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:
|
def _emit_arrow_background_changed(self, _index: int) -> None:
|
||||||
value = self.arrow_background_combo.currentData()
|
value = self.arrow_background_combo.currentData()
|
||||||
if value:
|
if value:
|
||||||
@@ -413,6 +449,15 @@ class SettingsPanelWidget(QWidget):
|
|||||||
def finger_export_dir(self) -> str:
|
def finger_export_dir(self) -> str:
|
||||||
return self.finger_assets_edit.text().strip()
|
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:
|
def csv_save_dir(self) -> str:
|
||||||
return self.csv_dir_edit.text().strip()
|
return self.csv_dir_edit.text().strip()
|
||||||
|
|
||||||
@@ -420,6 +465,7 @@ class SettingsPanelWidget(QWidget):
|
|||||||
"""Serialize user-facing settings suitable for preference.json."""
|
"""Serialize user-facing settings suitable for preference.json."""
|
||||||
return {
|
return {
|
||||||
"video_source": self.video_source(),
|
"video_source": self.video_source(),
|
||||||
|
"sensor_bundle_dir": self.sensor_bundle_dir(),
|
||||||
"config_path": self.config_path(),
|
"config_path": self.config_path(),
|
||||||
"finger_assets_dir": self.finger_export_dir(),
|
"finger_assets_dir": self.finger_export_dir(),
|
||||||
"csv_save_dir": self.csv_save_dir(),
|
"csv_save_dir": self.csv_save_dir(),
|
||||||
@@ -465,6 +511,8 @@ class SettingsPanelWidget(QWidget):
|
|||||||
if isinstance(assets, str) and assets.strip():
|
if isinstance(assets, str) and assets.strip():
|
||||||
self.set_finger_export_dir(assets.strip())
|
self.set_finger_export_dir(assets.strip())
|
||||||
|
|
||||||
|
self._sync_bundle_combo_from_paths()
|
||||||
|
|
||||||
csv_dir = state.get("csv_save_dir")
|
csv_dir = state.get("csv_save_dir")
|
||||||
if isinstance(csv_dir, str) and csv_dir.strip():
|
if isinstance(csv_dir, str) and csv_dir.strip():
|
||||||
self.set_csv_save_dir(csv_dir.strip())
|
self.set_csv_save_dir(csv_dir.strip())
|
||||||
|
|||||||
+8
-8
@@ -75,6 +75,14 @@ patterns = ["*.json"]
|
|||||||
# files = ["dd02-ov.json", "ddjx01.json"]
|
# files = ["dd02-ov.json", "ddjx01.json"]
|
||||||
dev_install = true
|
dev_install = true
|
||||||
|
|
||||||
|
[[tool.orisys.bundle.items]]
|
||||||
|
name = "sensors"
|
||||||
|
source = "sensors"
|
||||||
|
target = "sensors"
|
||||||
|
patterns = ["**/*.json", "finger/assets/30fps*/*", "finger/assets/visualization/*"]
|
||||||
|
preserve_tree = true
|
||||||
|
dev_install = true
|
||||||
|
|
||||||
[[tool.orisys.bundle.items]]
|
[[tool.orisys.bundle.items]]
|
||||||
name = "examples"
|
name = "examples"
|
||||||
source = "examples"
|
source = "examples"
|
||||||
@@ -88,11 +96,3 @@ source = "docs"
|
|||||||
target = "docs"
|
target = "docs"
|
||||||
patterns = ["*.md", "*.pdf"]
|
patterns = ["*.md", "*.pdf"]
|
||||||
|
|
||||||
[[tool.orisys.bundle.items]]
|
|
||||||
name = "assets"
|
|
||||||
source = "assets"
|
|
||||||
target = "assets"
|
|
||||||
patterns = ["finger/30fps/*", "finger/visualization/*"]
|
|
||||||
preserve_tree = true
|
|
||||||
dev_install = true
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"normal_force": {
|
||||||
|
"model": "linear",
|
||||||
|
"slope": 3.92685554668795e-05,
|
||||||
|
"offset": 0.0217596967278535,
|
||||||
|
"unit": "N"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"name": "prototype",
|
||||||
|
"version": "1.0",
|
||||||
|
"camera": {
|
||||||
|
"resolution_raw": [1920, 1080],
|
||||||
|
"fps": 30,
|
||||||
|
"units": 15,
|
||||||
|
"units_used": 9,
|
||||||
|
"resolution_stitch": [365, 365],
|
||||||
|
"stitch_res_list": [365,263,123,63]
|
||||||
|
},
|
||||||
|
"stitcher": {
|
||||||
|
"min_area_frac": 0.01,
|
||||||
|
"max_area_frac": 0.05,
|
||||||
|
"max_aspect_ratio": 50,
|
||||||
|
"index": [1,2,3,6,7,8,11,12,13],
|
||||||
|
"margin": 10,
|
||||||
|
"nominal_ov_pos": 100,
|
||||||
|
"n": [3,3],
|
||||||
|
"row_tol": 0.6,
|
||||||
|
"box_size": [250, 250],
|
||||||
|
"blend":"numba",
|
||||||
|
"blend_list": ["numpy", "numba","none"]
|
||||||
|
},
|
||||||
|
"tracker": {
|
||||||
|
"adaptive": true,
|
||||||
|
"lower_threshold": 12.0,
|
||||||
|
"upper_threshold": 18.0,
|
||||||
|
"loop_closure_interval": 1,
|
||||||
|
"algorithm": "dis",
|
||||||
|
"dis_preset": "medium",
|
||||||
|
"dis_mode": 3,
|
||||||
|
"algorithm_list": ["raft", "dis"],
|
||||||
|
"hhd_downsample": 3
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"min_contour_area": 3000,
|
||||||
|
"flow_threshold":5,
|
||||||
|
"div_threshold":0.05,
|
||||||
|
"expansion_threshold": 0.01,
|
||||||
|
"kernel_size": [5,5],
|
||||||
|
"denoise_sigma": 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
Place the ORY-FINGER runtime asset bundle files in this directory before
|
||||||
|
building release wheels:
|
||||||
|
|
||||||
|
- `mesh.npz`
|
||||||
|
- `camera_intrinsics.npz`
|
||||||
|
- `world_to_cam.npy`
|
||||||
|
|
||||||
|
These binary assets are validated by the wheel build and are intentionally not
|
||||||
|
generated by the package build step.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,6 @@
|
|||||||
|
Place visualization camera assets in this directory before building release
|
||||||
|
wheels:
|
||||||
|
|
||||||
|
- `world_to_cam.npy`
|
||||||
|
|
||||||
|
The 3D dashboard uses this pose as the default view preset when available.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "finger",
|
||||||
|
"sensor_json": "sensor.json",
|
||||||
|
"assets_dir": "assets/30fps-0703",
|
||||||
|
"viz_pose": "assets/visualization/world_to_cam.npy"
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "dd02-ov",
|
||||||
|
"type": "single_camera",
|
||||||
|
"version": "1.0",
|
||||||
|
"camera": {
|
||||||
|
"resolution_raw": [1280, 720],
|
||||||
|
"fps": 30,
|
||||||
|
"resolution_stitch": [280, 300],
|
||||||
|
"source_scale": 0.4,
|
||||||
|
"old":[672, 728, 1,0.5],
|
||||||
|
"work": [504, 546,1],
|
||||||
|
"work2": [420, 450,0.8],
|
||||||
|
"work3": [350, 375,4,0.7],
|
||||||
|
"work4": [280, 300,4,0.4,0.2]
|
||||||
|
},
|
||||||
|
"tracker": {
|
||||||
|
"adaptive": true,
|
||||||
|
"lower_threshold": 12.0,
|
||||||
|
"upper_threshold": 18.0,
|
||||||
|
"loop_closure_interval": 1,
|
||||||
|
"algorithm": "dis",
|
||||||
|
"dis_preset": "medium",
|
||||||
|
"dis_mode": 1,
|
||||||
|
"algorithm_list": ["raft", "dis"],
|
||||||
|
"hhd_downsample": 5
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"min_contour_area": 3000,
|
||||||
|
"flow_threshold": 0.2,
|
||||||
|
"div_threshold": 0.022,
|
||||||
|
"expansion_threshold": 0.01,
|
||||||
|
"kernel_size": [5, 5],
|
||||||
|
"denoise_sigma": 12
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"normal_force": {
|
||||||
|
"model": "linear",
|
||||||
|
"slope": 3.92685554668795e-05,
|
||||||
|
"offset": 0.0217596967278535,
|
||||||
|
"unit": "N"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"name": "prototype",
|
||||||
|
"version": "1.0",
|
||||||
|
"camera": {
|
||||||
|
"resolution_raw": [1920, 1080],
|
||||||
|
"fps": 30,
|
||||||
|
"units": 15,
|
||||||
|
"units_used": 9,
|
||||||
|
"resolution_stitch": [365, 365],
|
||||||
|
"stitch_res_list": [365,263,123,63]
|
||||||
|
},
|
||||||
|
"stitcher": {
|
||||||
|
"min_area_frac": 0.01,
|
||||||
|
"max_area_frac": 0.05,
|
||||||
|
"max_aspect_ratio": 50,
|
||||||
|
"index": [1,2,3,6,7,8,11,12,13],
|
||||||
|
"margin": 10,
|
||||||
|
"nominal_ov_pos": 100,
|
||||||
|
"n": [3,3],
|
||||||
|
"row_tol": 0.6,
|
||||||
|
"box_size": [250, 250],
|
||||||
|
"blend":"numba",
|
||||||
|
"blend_list": ["numpy", "numba","none"]
|
||||||
|
},
|
||||||
|
"tracker": {
|
||||||
|
"adaptive": true,
|
||||||
|
"lower_threshold": 12.0,
|
||||||
|
"upper_threshold": 18.0,
|
||||||
|
"loop_closure_interval": 1,
|
||||||
|
"algorithm": "dis",
|
||||||
|
"dis_preset": "medium",
|
||||||
|
"dis_mode": 3,
|
||||||
|
"algorithm_list": ["raft", "dis"],
|
||||||
|
"hhd_downsample": 3
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"min_contour_area": 3000,
|
||||||
|
"flow_threshold":5,
|
||||||
|
"div_threshold":0.05,
|
||||||
|
"expansion_threshold": 0.01,
|
||||||
|
"kernel_size": [5,5],
|
||||||
|
"denoise_sigma": 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
+129
-3
@@ -1,4 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
|
from contextlib import ExitStack
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from importlib.resources import as_file, files
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Product-facing names that map to on-disk config stems.
|
# Product-facing names that map to on-disk config stems.
|
||||||
@@ -8,6 +11,18 @@ CONFIG_ALIASES = {
|
|||||||
"dd02": "dd02-ov",
|
"dd02": "dd02-ov",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PRODUCT_BUNDLES = ("box", "mini", "finger")
|
||||||
|
_RESOURCE_STACK = ExitStack()
|
||||||
|
|
||||||
|
|
||||||
|
def _resource_path(*parts: str) -> Path | None:
|
||||||
|
"""Resolve a package resource to a filesystem path for wheel-safe loaders."""
|
||||||
|
try:
|
||||||
|
resource = files("orisys").joinpath(*parts)
|
||||||
|
return _RESOURCE_STACK.enter_context(as_file(resource))
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _candidate_config_dirs() -> list[Path]:
|
def _candidate_config_dirs() -> list[Path]:
|
||||||
"""
|
"""
|
||||||
@@ -28,6 +43,26 @@ def _candidate_config_dirs() -> list[Path]:
|
|||||||
return unique
|
return unique
|
||||||
|
|
||||||
|
|
||||||
|
def _candidate_sensor_roots() -> list[Path]:
|
||||||
|
"""Candidate roots that contain per-sensor bundle folders."""
|
||||||
|
here = Path(__file__).resolve().parent
|
||||||
|
candidates = []
|
||||||
|
packaged = _resource_path("sensors")
|
||||||
|
if packaged is not None:
|
||||||
|
candidates.append(packaged) # packaged wheel layout: orisys/sensors/
|
||||||
|
candidates.extend([
|
||||||
|
here / "sensors",
|
||||||
|
here.parent.parent / "sensors", # editable install: repo_root/sensors/
|
||||||
|
here.parent / "sensors",
|
||||||
|
Path.cwd() / "sensors",
|
||||||
|
])
|
||||||
|
unique: list[Path] = []
|
||||||
|
for path in candidates:
|
||||||
|
if path not in unique:
|
||||||
|
unique.append(path)
|
||||||
|
return unique
|
||||||
|
|
||||||
|
|
||||||
def _get_config_dir() -> Path:
|
def _get_config_dir() -> Path:
|
||||||
for candidate in _candidate_config_dirs():
|
for candidate in _candidate_config_dirs():
|
||||||
if candidate.exists() and any(candidate.glob("*.json")):
|
if candidate.exists() and any(candidate.glob("*.json")):
|
||||||
@@ -50,14 +85,96 @@ def _load_json(path: Path) -> dict:
|
|||||||
return json.load(f)
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SensorBundle:
|
||||||
|
"""Resolved paths for one additive per-sensor package folder."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
root: Path
|
||||||
|
sensor_json: Path
|
||||||
|
force_calibration_json: Path | None = None
|
||||||
|
manifest_json: Path | None = None
|
||||||
|
assets_dir: Path | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_sensor_bundle(name: str) -> SensorBundle | None:
|
||||||
|
"""Resolve ``sensors/<name>/`` when present; otherwise return ``None``."""
|
||||||
|
requested = CONFIG_ALIASES.get(str(name), str(name))
|
||||||
|
for root in _candidate_sensor_roots():
|
||||||
|
bundle_root = root / requested
|
||||||
|
sensor_json = bundle_root / "sensor.json"
|
||||||
|
if not sensor_json.is_file():
|
||||||
|
continue
|
||||||
|
force_path = bundle_root / "force_calibration.json"
|
||||||
|
manifest_path = bundle_root / "manifest.json"
|
||||||
|
assets_dir = None
|
||||||
|
if manifest_path.is_file():
|
||||||
|
try:
|
||||||
|
manifest = _load_json(manifest_path)
|
||||||
|
except Exception:
|
||||||
|
manifest = {}
|
||||||
|
rel_assets = manifest.get("assets_dir")
|
||||||
|
if isinstance(rel_assets, str) and rel_assets.strip():
|
||||||
|
candidate = (bundle_root / rel_assets).resolve()
|
||||||
|
if candidate.is_dir():
|
||||||
|
assets_dir = candidate
|
||||||
|
return SensorBundle(
|
||||||
|
name=requested,
|
||||||
|
root=bundle_root.resolve(),
|
||||||
|
sensor_json=sensor_json.resolve(),
|
||||||
|
force_calibration_json=force_path.resolve() if force_path.is_file() else None,
|
||||||
|
manifest_json=manifest_path.resolve() if manifest_path.is_file() else None,
|
||||||
|
assets_dir=assets_dir,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_sensor_config_path(name: str) -> Path | None:
|
||||||
|
"""Return ``sensor.json`` for a resolved sensor bundle."""
|
||||||
|
bundle = resolve_sensor_bundle(name)
|
||||||
|
return bundle.sensor_json if bundle is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_sensor_asset_dir(name: str) -> Path | None:
|
||||||
|
"""Return the default asset directory for a resolved sensor bundle."""
|
||||||
|
bundle = resolve_sensor_bundle(name)
|
||||||
|
return bundle.assets_dir if bundle is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_sensor_resource(name: str, *relative_parts: str) -> Path | None:
|
||||||
|
"""Return a path inside one sensor bundle when present."""
|
||||||
|
bundle = resolve_sensor_bundle(name)
|
||||||
|
if bundle is None:
|
||||||
|
return None
|
||||||
|
return bundle.root.joinpath(*relative_parts)
|
||||||
|
|
||||||
|
|
||||||
|
def list_sensor_bundles() -> list[str]:
|
||||||
|
"""Return product names that have an additive ``sensors/<name>/`` folder."""
|
||||||
|
found: list[str] = []
|
||||||
|
for name in PRODUCT_BUNDLES:
|
||||||
|
if resolve_sensor_bundle(name) is not None:
|
||||||
|
found.append(name)
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def load_force_calibration(name: str) -> dict | None:
|
||||||
|
"""Load ``force_calibration.json`` from a sensor bundle when available."""
|
||||||
|
bundle = resolve_sensor_bundle(name)
|
||||||
|
if bundle is None or bundle.force_calibration_json is None:
|
||||||
|
return None
|
||||||
|
return _load_json(bundle.force_calibration_json)
|
||||||
|
|
||||||
|
|
||||||
def load_config(config_name_or_path):
|
def load_config(config_name_or_path):
|
||||||
"""
|
"""
|
||||||
Load sensor configuration from a JSON file path or config name.
|
Load sensor configuration from a JSON file path or config name.
|
||||||
|
|
||||||
Config lookup order:
|
Config lookup order:
|
||||||
1. Exact file path (absolute or relative)
|
1. Exact file path (absolute or relative)
|
||||||
2. File stem under the config directory (e.g. ``box`` -> ``config/box.json``)
|
2. Additive sensor bundle ``sensors/<name>/sensor.json``
|
||||||
3. JSON field ``"name"`` matching the requested name (keeps ``prototype`` compatible)
|
3. File stem under the config directory (e.g. ``box`` -> ``config/box.json``)
|
||||||
|
4. JSON field ``"name"`` matching the requested name (keeps ``prototype`` compatible)
|
||||||
"""
|
"""
|
||||||
config_path = Path(config_name_or_path)
|
config_path = Path(config_name_or_path)
|
||||||
|
|
||||||
@@ -75,6 +192,11 @@ def load_config(config_name_or_path):
|
|||||||
if alias and alias not in candidates:
|
if alias and alias not in candidates:
|
||||||
candidates.append(alias)
|
candidates.append(alias)
|
||||||
|
|
||||||
|
for name in candidates:
|
||||||
|
bundle = resolve_sensor_bundle(name)
|
||||||
|
if bundle is not None:
|
||||||
|
return _load_json(bundle.sensor_json)
|
||||||
|
|
||||||
for name in candidates:
|
for name in candidates:
|
||||||
direct = CONFIG_DIR / f"{name}.json"
|
direct = CONFIG_DIR / f"{name}.json"
|
||||||
if direct.exists():
|
if direct.exists():
|
||||||
@@ -103,7 +225,11 @@ def list_config():
|
|||||||
"""
|
"""
|
||||||
List all available JSON configuration files by file stem.
|
List all available JSON configuration files by file stem.
|
||||||
"""
|
"""
|
||||||
return [f.stem for f in _config_files()]
|
stems = [f.stem for f in _config_files()]
|
||||||
|
for name in list_sensor_bundles():
|
||||||
|
if name not in stems:
|
||||||
|
stems.append(name)
|
||||||
|
return sorted(stems)
|
||||||
|
|
||||||
|
|
||||||
def export_config(config_name=None, path="./config/exported_config.json"):
|
def export_config(config_name=None, path="./config/exported_config.json"):
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import sys
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ..configs import resolve_sensor_asset_dir
|
||||||
from ..unroll import (
|
from ..unroll import (
|
||||||
compute_uv_distortion_metrics,
|
compute_uv_distortion_metrics,
|
||||||
load_exported_data,
|
load_exported_data,
|
||||||
@@ -11,7 +12,7 @@ from ..unroll import (
|
|||||||
scaled_map_cache_path,
|
scaled_map_cache_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
DEFAULT_FINGER_EXPORT_DIR = "assets/finger/30fps-0703"
|
DEFAULT_FINGER_EXPORT_DIR = "sensors/finger/assets/30fps-0703"
|
||||||
DEFAULT_FINGER_MAP_CACHE = f"{DEFAULT_FINGER_EXPORT_DIR}/unroll_map_uv.npz"
|
DEFAULT_FINGER_MAP_CACHE = f"{DEFAULT_FINGER_EXPORT_DIR}/unroll_map_uv.npz"
|
||||||
|
|
||||||
|
|
||||||
@@ -44,11 +45,15 @@ def _candidate_export_dirs(export_dir) -> list[Path]:
|
|||||||
return [raw]
|
return [raw]
|
||||||
|
|
||||||
here = Path(__file__).resolve()
|
here = Path(__file__).resolve()
|
||||||
candidates = [
|
candidates = []
|
||||||
|
bundled_assets = resolve_sensor_asset_dir("finger")
|
||||||
|
if bundled_assets is not None and raw.as_posix() == DEFAULT_FINGER_EXPORT_DIR:
|
||||||
|
candidates.append(bundled_assets)
|
||||||
|
candidates.extend([
|
||||||
Path.cwd() / raw,
|
Path.cwd() / raw,
|
||||||
here.parents[1] / raw, # packaged wheel: orisys/assets/...
|
here.parents[1] / raw, # packaged wheel filesystem layout
|
||||||
here.parents[3] / raw, # editable install: repo/assets/...
|
here.parents[3] / raw, # editable install: repo/assets/...
|
||||||
]
|
])
|
||||||
if getattr(sys, "frozen", False):
|
if getattr(sys, "frozen", False):
|
||||||
candidates.insert(0, Path(sys.executable).resolve().parent / raw)
|
candidates.insert(0, Path(sys.executable).resolve().parent / raw)
|
||||||
unique: list[Path] = []
|
unique: list[Path] = []
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import numpy as np
|
|||||||
from scipy.signal import butter, filtfilt, sosfilt, sosfilt_zi
|
from scipy.signal import butter, filtfilt, sosfilt, sosfilt_zi
|
||||||
|
|
||||||
# Linear calibration from raw normal-force units to Newtons (ORY-BOX / ORY-MINI).
|
# Linear calibration from raw normal-force units to Newtons (ORY-BOX / ORY-MINI).
|
||||||
|
# Prefer sensors/<name>/force_calibration.json when available.
|
||||||
NORMAL_FORCE_CAL_SLOPE = 3.92685554668795e-05
|
NORMAL_FORCE_CAL_SLOPE = 3.92685554668795e-05
|
||||||
NORMAL_FORCE_CAL_OFFSET = 0.0217596967278535
|
NORMAL_FORCE_CAL_OFFSET = 0.0217596967278535
|
||||||
|
|
||||||
@@ -17,9 +18,28 @@ DEFAULT_SAMPLE_RATE_HZ = 30.0
|
|||||||
DEFAULT_LOWPASS_ORDER = 2
|
DEFAULT_LOWPASS_ORDER = 2
|
||||||
|
|
||||||
|
|
||||||
def calibrate_normal_force(raw_value: float) -> float:
|
def _load_normal_force_coeffs(sensor_name: str | None = None) -> tuple[float, float]:
|
||||||
|
if sensor_name:
|
||||||
|
try:
|
||||||
|
from .configs import load_force_calibration
|
||||||
|
|
||||||
|
data = load_force_calibration(sensor_name)
|
||||||
|
except Exception:
|
||||||
|
data = None
|
||||||
|
if isinstance(data, dict):
|
||||||
|
block = data.get("normal_force", data)
|
||||||
|
if isinstance(block, dict):
|
||||||
|
slope = block.get("slope")
|
||||||
|
offset = block.get("offset")
|
||||||
|
if slope is not None and offset is not None:
|
||||||
|
return float(slope), float(offset)
|
||||||
|
return NORMAL_FORCE_CAL_SLOPE, NORMAL_FORCE_CAL_OFFSET
|
||||||
|
|
||||||
|
|
||||||
|
def calibrate_normal_force(raw_value: float, *, sensor_name: str | None = None) -> float:
|
||||||
"""Convert a raw normal-force reading to Newtons using y = mx + c."""
|
"""Convert a raw normal-force reading to Newtons using y = mx + c."""
|
||||||
return NORMAL_FORCE_CAL_SLOPE * float(raw_value) + NORMAL_FORCE_CAL_OFFSET
|
slope, offset = _load_normal_force_coeffs(sensor_name)
|
||||||
|
return slope * float(raw_value) + offset
|
||||||
|
|
||||||
|
|
||||||
def _nyquist_hz(sample_rate_hz: float) -> float:
|
def _nyquist_hz(sample_rate_hz: float) -> float:
|
||||||
|
|||||||
Reference in New Issue
Block a user