Refractor/config package #14

Merged
zhangyijian merged 3 commits from refractor/config_package into dev 2026-08-23 12:25:29 +08:00
34 changed files with 688 additions and 98 deletions
Showing only changes of commit 1ceb0d9ca4 - Show all commits
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+5 -1
View File
@@ -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,
+35
View File
@@ -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
View File
@@ -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
},
+7 -5
View File
@@ -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"
}
+121 -48
View File
@@ -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:
+113
View File
@@ -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
+73 -25
View File
@@ -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
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
{
"normal_force": {
"model": "linear",
"slope": 3.92685554668795e-05,
"offset": 0.0217596967278535,
"unit": "N"
}
}
+46
View File
@@ -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.
@@ -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.
+6
View File
@@ -0,0 +1,6 @@
{
"name": "finger",
"sensor_json": "sensor.json",
"assets_dir": "assets/30fps-0703",
"viz_pose": "assets/visualization/world_to_cam.npy"
}
+35
View File
@@ -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
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"normal_force": {
"model": "linear",
"slope": 3.92685554668795e-05,
"offset": 0.0217596967278535,
"unit": "N"
}
}
+46
View File
@@ -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
View File
@@ -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"):
+9 -4
View File
@@ -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] = []
+22 -2
View File
@@ -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: