Refractor/config package #14
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.verify.wheel import verify_built_wheel
|
||||
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 (
|
||||
is_compliant_manylinux_wheel,
|
||||
publish_wheel_to_sdk,
|
||||
|
||||
@@ -5,6 +5,14 @@ from __future__ import annotations
|
||||
import shutil
|
||||
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]:
|
||||
"""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
|
||||
|
||||
|
||||
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]:
|
||||
"""Return setuptools package_data paths relative to the orisys package."""
|
||||
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]:
|
||||
verify_required_assets(repo_root, items)
|
||||
copied_counts: dict[str, int] = {}
|
||||
for item in items:
|
||||
name = item["name"]
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@
|
||||
"camera": {
|
||||
"resolution_raw": [1280, 720],
|
||||
"fps": 30,
|
||||
"resolution_stitch": [420, 450],
|
||||
"resolution_stitch": [280, 300],
|
||||
"source_scale": 0.4,
|
||||
"old":[672, 728, 1,0.5],
|
||||
"work": [504, 546,1],
|
||||
@@ -20,7 +20,7 @@
|
||||
"loop_closure_interval": 1,
|
||||
"algorithm": "dis",
|
||||
"dis_preset": "medium",
|
||||
"dis_mode": 3,
|
||||
"dis_mode": 1,
|
||||
"algorithm_list": ["raft", "dis"],
|
||||
"hhd_downsample": 5
|
||||
},
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
{
|
||||
"video_source": "D:/repo/orisys-sdk/data/dd02/0607_1.mp4",
|
||||
"config_path": "./config/dd02-ov.json",
|
||||
"finger_assets_dir": "D:/repo/orisys-sdk/assets/finger/30fps-0703",
|
||||
"video_source": "D:/repo/orisys-sdk/data/dd02-0703/1.mp4",
|
||||
"sensor_bundle_dir": "sensors/finger",
|
||||
"config_path": "sensors/finger/sensor.json",
|
||||
"finger_assets_dir": "sensors/finger/assets/30fps-0703",
|
||||
"csv_save_dir": "./outputs/csv",
|
||||
"show_log": true,
|
||||
"show_flow_arrows": false,
|
||||
"arrow_background": "pure_color",
|
||||
"free_rotate": true
|
||||
"arrow_background": "unrolled",
|
||||
"free_rotate": true,
|
||||
"log_display_mode": "hidden"
|
||||
}
|
||||
|
||||
@@ -2,14 +2,19 @@
|
||||
|
||||
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,
|
||||
@@ -61,6 +66,7 @@ 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)
|
||||
@@ -84,7 +90,7 @@ class FingerDashboardWindow(QMainWindow):
|
||||
root_layout.setSpacing(layout.root_spacing_px)
|
||||
|
||||
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_viewport = MeshViewportWidget(layout=layout, mesh_state=self.mesh_state)
|
||||
arrows_slot = layout.slot("arrows")
|
||||
@@ -111,7 +117,6 @@ 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())
|
||||
@@ -152,11 +157,6 @@ 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,18 +172,104 @@ 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)
|
||||
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)
|
||||
|
||||
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._set_log_panel_visible(visible)
|
||||
self._save_preferences()
|
||||
self._apply_log_display_mode("runtime" if visible else "hidden", save=True)
|
||||
|
||||
def _on_free_rotation_changed(self, enabled: bool) -> None:
|
||||
self.mesh_viewport.set_free_rotation(enabled)
|
||||
@@ -196,6 +282,7 @@ 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.
|
||||
@@ -217,7 +304,6 @@ 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:
|
||||
@@ -236,27 +322,11 @@ class FingerDashboardWindow(QMainWindow):
|
||||
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._scaled_px(charts_slot.width_px, scale),
|
||||
column_height,
|
||||
)
|
||||
self.charts_column.setSizes([column_height, 0])
|
||||
self.force_charts.set_panel_size(
|
||||
self._scaled_px(charts_slot.width_px, scale),
|
||||
column_height,
|
||||
)
|
||||
self.charts_column.setSizes([column_height])
|
||||
|
||||
self.h_splitter.setSizes(
|
||||
[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()
|
||||
|
||||
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)
|
||||
@@ -316,12 +380,6 @@ 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)
|
||||
@@ -602,12 +660,27 @@ class FingerDashboardWindow(QMainWindow):
|
||||
def _select_finger_assets(self) -> None:
|
||||
path = QFileDialog.getExistingDirectory(
|
||||
self,
|
||||
"选择 finger 资产目录",
|
||||
self.settings_panel.finger_export_dir() or "assets/finger",
|
||||
"选择传感器 bundle 目录",
|
||||
self.settings_panel.sensor_bundle_dir() or "sensors/finger",
|
||||
)
|
||||
if path:
|
||||
self.settings_panel.set_finger_export_dir(path)
|
||||
self.config.export_dir = 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():
|
||||
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()
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
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"
|
||||
@@ -86,7 +90,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)
|
||||
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.setChecked(self._mesh_state.flow_arrows_enabled)
|
||||
@@ -134,35 +138,38 @@ class SettingsPanelWidget(QWidget):
|
||||
self._video_file_path = ""
|
||||
self._updating_camera_options = False
|
||||
|
||||
root.addWidget(self._caption("Config"))
|
||||
config_row = QHBoxLayout()
|
||||
config_row.setSpacing(6)
|
||||
self.config_edit = QLineEdit("finger")
|
||||
config_row.addWidget(self.config_edit, 1)
|
||||
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)
|
||||
|
||||
self.config_edit = QLineEdit("sensors/finger/sensor.json")
|
||||
self.config_edit.hide()
|
||||
self.btn_browse_config = QPushButton("…")
|
||||
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)
|
||||
self.btn_browse_config.hide()
|
||||
|
||||
# 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.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)
|
||||
self.finger_assets_edit = QLineEdit("sensors/finger/assets/30fps-0703")
|
||||
self.finger_assets_edit.hide()
|
||||
self._apply_bundle_preset(self._bundle_presets[0])
|
||||
|
||||
root.addWidget(self._caption("CSV save directory"))
|
||||
csv_dir_row = QHBoxLayout()
|
||||
@@ -337,16 +344,18 @@ 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)
|
||||
@@ -387,6 +396,33 @@ 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:
|
||||
@@ -413,6 +449,15 @@ 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()
|
||||
|
||||
@@ -420,6 +465,7 @@ 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(),
|
||||
@@ -465,6 +511,8 @@ 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())
|
||||
|
||||
+8
-8
@@ -75,6 +75,14 @@ patterns = ["*.json"]
|
||||
# files = ["dd02-ov.json", "ddjx01.json"]
|
||||
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]]
|
||||
name = "examples"
|
||||
source = "examples"
|
||||
@@ -88,11 +96,3 @@ source = "docs"
|
||||
target = "docs"
|
||||
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
|
||||
from contextlib import ExitStack
|
||||
from dataclasses import dataclass
|
||||
from importlib.resources import as_file, files
|
||||
from pathlib import Path
|
||||
|
||||
# Product-facing names that map to on-disk config stems.
|
||||
@@ -8,6 +11,18 @@ CONFIG_ALIASES = {
|
||||
"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]:
|
||||
"""
|
||||
@@ -28,6 +43,26 @@ def _candidate_config_dirs() -> list[Path]:
|
||||
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:
|
||||
for candidate in _candidate_config_dirs():
|
||||
if candidate.exists() and any(candidate.glob("*.json")):
|
||||
@@ -50,14 +85,96 @@ def _load_json(path: Path) -> dict:
|
||||
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):
|
||||
"""
|
||||
Load sensor configuration from a JSON file path or config name.
|
||||
|
||||
Config lookup order:
|
||||
1. Exact file path (absolute or relative)
|
||||
2. File stem under the config directory (e.g. ``box`` -> ``config/box.json``)
|
||||
3. JSON field ``"name"`` matching the requested name (keeps ``prototype`` compatible)
|
||||
2. Additive sensor bundle ``sensors/<name>/sensor.json``
|
||||
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)
|
||||
|
||||
@@ -75,6 +192,11 @@ def load_config(config_name_or_path):
|
||||
if alias and alias not in candidates:
|
||||
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:
|
||||
direct = CONFIG_DIR / f"{name}.json"
|
||||
if direct.exists():
|
||||
@@ -103,7 +225,11 @@ def list_config():
|
||||
"""
|
||||
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"):
|
||||
|
||||
@@ -4,6 +4,7 @@ import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from ..configs import resolve_sensor_asset_dir
|
||||
from ..unroll import (
|
||||
compute_uv_distortion_metrics,
|
||||
load_exported_data,
|
||||
@@ -11,7 +12,7 @@ from ..unroll import (
|
||||
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"
|
||||
|
||||
|
||||
@@ -44,11 +45,15 @@ def _candidate_export_dirs(export_dir) -> list[Path]:
|
||||
return [raw]
|
||||
|
||||
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,
|
||||
here.parents[1] / raw, # packaged wheel: orisys/assets/...
|
||||
here.parents[1] / raw, # packaged wheel filesystem layout
|
||||
here.parents[3] / raw, # editable install: repo/assets/...
|
||||
]
|
||||
])
|
||||
if getattr(sys, "frozen", False):
|
||||
candidates.insert(0, Path(sys.executable).resolve().parent / raw)
|
||||
unique: list[Path] = []
|
||||
|
||||
@@ -10,6 +10,7 @@ import numpy as np
|
||||
from scipy.signal import butter, filtfilt, sosfilt, sosfilt_zi
|
||||
|
||||
# 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_OFFSET = 0.0217596967278535
|
||||
|
||||
@@ -17,9 +18,28 @@ DEFAULT_SAMPLE_RATE_HZ = 30.0
|
||||
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."""
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user