fix path errors
This commit is contained in:
@@ -17,6 +17,8 @@ from orisys.finger.mesh import UVSurfaceMesh
|
|||||||
from orisys.finger.viewer import init_uv_mesh_viewer
|
from orisys.finger.viewer import init_uv_mesh_viewer
|
||||||
from orisys.hhd import nHHD
|
from orisys.hhd import nHHD
|
||||||
from orisys.tracker import Tracker
|
from orisys.tracker import Tracker
|
||||||
|
from orisys import capture
|
||||||
|
from orisys.Stitcher import Stitcher
|
||||||
|
|
||||||
assert orisys.__version__
|
assert orisys.__version__
|
||||||
assert Sensor is not None
|
assert Sensor is not None
|
||||||
@@ -31,6 +33,8 @@ assert UVSurfaceMesh is not None
|
|||||||
assert init_uv_mesh_viewer is not None
|
assert init_uv_mesh_viewer is not None
|
||||||
assert nHHD is not None
|
assert nHHD is not None
|
||||||
assert Tracker is not None
|
assert Tracker is not None
|
||||||
|
assert capture is not None
|
||||||
|
assert Stitcher is not None
|
||||||
print(f"orisys {orisys.__version__} public API imports OK")
|
print(f"orisys {orisys.__version__} public API imports OK")
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ EXCLUDE_MODULES = [
|
|||||||
|
|
||||||
DATA_DIRS = [
|
DATA_DIRS = [
|
||||||
("config", "config"),
|
("config", "config"),
|
||||||
("assets", "assets"),
|
("sensors", "sensors"),
|
||||||
]
|
]
|
||||||
|
|
||||||
README_LINES = [
|
README_LINES = [
|
||||||
@@ -56,7 +56,7 @@ README_LINES = [
|
|||||||
"",
|
"",
|
||||||
"Bundled folders:",
|
"Bundled folders:",
|
||||||
" config/ sensor JSON configs",
|
" config/ sensor JSON configs",
|
||||||
" assets/ finger mesh and visualization data",
|
" sensors/ per-sensor bundles (finger mesh and visualization data)",
|
||||||
"",
|
"",
|
||||||
"CPU-only build (no CUDA/CuPy).",
|
"CPU-only build (no CUDA/CuPy).",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ def build(*, target: str = "internal", verify: bool | None = None) -> Path:
|
|||||||
print(f"Copied {copied_counts['configs']} JSON config files")
|
print(f"Copied {copied_counts['configs']} JSON config files")
|
||||||
print(f"Copied {copied_counts['examples']} example files to orisys/examples/")
|
print(f"Copied {copied_counts['examples']} example files to orisys/examples/")
|
||||||
print(f"Copied {copied_counts['docs']} doc files to orisys/docs/")
|
print(f"Copied {copied_counts['docs']} doc files to orisys/docs/")
|
||||||
print(f"Copied {copied_counts['assets']} finger asset files to orisys/assets/")
|
print(f"Copied {copied_counts['sensors']} sensor bundle files to orisys/sensors/")
|
||||||
|
|
||||||
write_wheel_setup(
|
write_wheel_setup(
|
||||||
NUITKA_WHEEL_DIR,
|
NUITKA_WHEEL_DIR,
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ from common.paths import repo_path
|
|||||||
|
|
||||||
NUITKA_INCLUDE_PACKAGES = (
|
NUITKA_INCLUDE_PACKAGES = (
|
||||||
"orisys",
|
"orisys",
|
||||||
|
"orisys.capture",
|
||||||
"orisys.finger",
|
"orisys.finger",
|
||||||
|
"orisys.stitch",
|
||||||
|
"orisys.stitch.backends",
|
||||||
"orisys.unroll",
|
"orisys.unroll",
|
||||||
"orisys.hhd",
|
"orisys.hhd",
|
||||||
"orisys.hhd.solvers",
|
"orisys.hhd.solvers",
|
||||||
|
|||||||
+5
-5
@@ -5,7 +5,7 @@ description: 安装并运行 Orisys SDK 的基础步骤
|
|||||||
|
|
||||||
# 快速开始
|
# 快速开始
|
||||||
|
|
||||||
版本:0.4.3
|
版本:0.4.4
|
||||||
|
|
||||||
## 简介
|
## 简介
|
||||||
本文件介绍如何上手和运行叠动视触觉传感器配套的开发套件。本套件是配合叠动视触觉传感器进行图像处理,计算形变、力以及接触信息。它可以从摄像头或视频文件中读取图像,处理后打印和展示处理后信息。
|
本文件介绍如何上手和运行叠动视触觉传感器配套的开发套件。本套件是配合叠动视触觉传感器进行图像处理,计算形变、力以及接触信息。它可以从摄像头或视频文件中读取图像,处理后打印和展示处理后信息。
|
||||||
@@ -84,9 +84,9 @@ pip install --upgrade orisys
|
|||||||
### 使用 .whl 文件安装(离线或指定版本)
|
### 使用 .whl 文件安装(离线或指定版本)
|
||||||
从官方渠道获取 .whl 文件,文件名格式通常为:
|
从官方渠道获取 .whl 文件,文件名格式通常为:
|
||||||
orisys-{版本}-{Python版本}-{Python版本}-{操作系统}_{硬件架构}.whl
|
orisys-{版本}-{Python版本}-{Python版本}-{操作系统}_{硬件架构}.whl
|
||||||
例如:orisys-0.4.3-cp310-cp310-win_amd64.whl 表示:
|
例如:orisys-0.4.4-cp310-cp310-win_amd64.whl 表示:
|
||||||
|
|
||||||
版本:0.4.3
|
版本:0.4.4
|
||||||
Python 版本:3.10(cp310)
|
Python 版本:3.10(cp310)
|
||||||
操作系统:Windows(win)
|
操作系统:Windows(win)
|
||||||
硬件架构:64位(amd64)
|
硬件架构:64位(amd64)
|
||||||
@@ -97,7 +97,7 @@ Python 版本:3.10(cp310)
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 请将文件名替换为您实际下载的文件名
|
# 请将文件名替换为您实际下载的文件名
|
||||||
pip install orisys-0.4.3-cp310-cp310-win_amd64.whl
|
pip install orisys-0.4.4-cp310-cp310-win_amd64.whl
|
||||||
```
|
```
|
||||||
|
|
||||||
### 安装 CuPy(可选)
|
### 安装 CuPy(可选)
|
||||||
@@ -139,7 +139,7 @@ python
|
|||||||
import orisys
|
import orisys
|
||||||
print(orisys.__version__)
|
print(orisys.__version__)
|
||||||
```
|
```
|
||||||
如果输出版本号(例如 0.4.3),则说明安装成功。
|
如果输出版本号(例如 0.4.4),则说明安装成功。
|
||||||
|
|
||||||
## 使用方法
|
## 使用方法
|
||||||
### 运行示例程序
|
### 运行示例程序
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ ORY-BOX:单传感器基础流程
|
|||||||
3. 按需计算接触区域
|
3. 按需计算接触区域
|
||||||
4. 读取并可视化结果
|
4. 读取并可视化结果
|
||||||
|
|
||||||
SDK version: 0.4.3
|
SDK version: 0.4.4
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"video_source": "D:/repo/orisys-sdk/data/dd02-0703/1.mp4",
|
"video_source": "D:/repo/orisys-sdk/data/dd02-0703/tune.mp4",
|
||||||
"sensor_bundle_dir": "sensors/finger",
|
"sensor_bundle_dir": "sensors/finger",
|
||||||
"config_path": "sensors/finger/sensor.json",
|
"config_path": "finger",
|
||||||
"finger_assets_dir": "sensors/finger/assets/30fps-0703",
|
"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"
|
"log_display_mode": "hidden"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,11 +76,37 @@ def _bgr_from_gray_or_bgr(img: np.ndarray) -> np.ndarray:
|
|||||||
return img
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_calibration_sensor_name(config_name) -> str | None:
|
||||||
|
"""Map a config path/name to a sensors/<name> bundle when one exists."""
|
||||||
|
if config_name is None:
|
||||||
|
return None
|
||||||
|
from orisys.configs import resolve_sensor_bundle
|
||||||
|
|
||||||
|
text = str(config_name).strip()
|
||||||
|
if not text:
|
||||||
|
return None
|
||||||
|
|
||||||
|
path = Path(text)
|
||||||
|
parts = path.parts
|
||||||
|
if "sensors" in parts:
|
||||||
|
idx = parts.index("sensors")
|
||||||
|
if idx + 1 < len(parts):
|
||||||
|
candidate = parts[idx + 1]
|
||||||
|
if resolve_sensor_bundle(candidate) is not None:
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
stem = path.stem if path.suffix else text
|
||||||
|
if resolve_sensor_bundle(stem) is not None:
|
||||||
|
return stem
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _snapshot_from_sensor(
|
def _snapshot_from_sensor(
|
||||||
sensor,
|
sensor,
|
||||||
*,
|
*,
|
||||||
raw_image=None,
|
raw_image=None,
|
||||||
processed_image=None,
|
processed_image=None,
|
||||||
|
sensor_name: str | None = None,
|
||||||
) -> Optional[FrameSnapshot]:
|
) -> Optional[FrameSnapshot]:
|
||||||
"""Build a FrameSnapshot with HHD/forces refreshed every frame."""
|
"""Build a FrameSnapshot with HHD/forces refreshed every frame."""
|
||||||
if sensor is None or sensor.img is None:
|
if sensor is None or sensor.img is None:
|
||||||
@@ -124,6 +150,9 @@ def _snapshot_from_sensor(
|
|||||||
below_threshold_color=(160, 160, 160),
|
below_threshold_color=(160, 160, 160),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cal_name = sensor_name or _resolve_calibration_sensor_name(
|
||||||
|
getattr(sensor, "config_name", None)
|
||||||
|
)
|
||||||
fn_raw = float(fnormal)
|
fn_raw = float(fnormal)
|
||||||
return FrameSnapshot(
|
return FrameSnapshot(
|
||||||
image=image_bgr,
|
image=image_bgr,
|
||||||
@@ -134,7 +163,7 @@ def _snapshot_from_sensor(
|
|||||||
normal_force=fn_raw,
|
normal_force=fn_raw,
|
||||||
shear_x=float(fshearx),
|
shear_x=float(fshearx),
|
||||||
shear_y=float(fsheary),
|
shear_y=float(fsheary),
|
||||||
normal_force_calibrated=calibrate_normal_force(fn_raw),
|
normal_force_calibrated=calibrate_normal_force(fn_raw, sensor_name=cal_name),
|
||||||
fps=float(fps),
|
fps=float(fps),
|
||||||
timestamp=time.time(),
|
timestamp=time.time(),
|
||||||
raw_image=raw_image,
|
raw_image=raw_image,
|
||||||
@@ -198,6 +227,7 @@ class OrisysSensorAdapter(SensorAdapter):
|
|||||||
if self.sensor.frame is not None
|
if self.sensor.frame is not None
|
||||||
else None,
|
else None,
|
||||||
processed_image=_bgr_from_gray_or_bgr(self.sensor.img),
|
processed_image=_bgr_from_gray_or_bgr(self.sensor.img),
|
||||||
|
sensor_name=_resolve_calibration_sensor_name(self.config_name),
|
||||||
)
|
)
|
||||||
|
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
@@ -274,6 +304,7 @@ class Dd06RoiAdapter(SensorAdapter):
|
|||||||
self.sensor,
|
self.sensor,
|
||||||
raw_image=frame,
|
raw_image=frame,
|
||||||
processed_image=rectified,
|
processed_image=rectified,
|
||||||
|
sensor_name=_resolve_calibration_sensor_name(self.config_name),
|
||||||
)
|
)
|
||||||
|
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
|
|||||||
@@ -646,22 +646,42 @@ class FingerDashboardWindow(QMainWindow):
|
|||||||
self._update_play_enabled_for_source()
|
self._update_play_enabled_for_source()
|
||||||
|
|
||||||
def _select_config(self) -> None:
|
def _select_config(self) -> None:
|
||||||
|
from .preferences import (
|
||||||
|
default_finger_config,
|
||||||
|
normalize_config_preference,
|
||||||
|
packaged_finger_bundle_root,
|
||||||
|
)
|
||||||
|
|
||||||
|
start = packaged_finger_bundle_root()
|
||||||
|
start_dir = str(start) if start is not None else "sensors/finger"
|
||||||
path, _ = QFileDialog.getOpenFileName(
|
path, _ = QFileDialog.getOpenFileName(
|
||||||
self,
|
self,
|
||||||
"选择配置文件",
|
"选择配置文件",
|
||||||
"config",
|
start_dir,
|
||||||
"JSON (*.json);;所有文件 (*)",
|
"JSON (*.json);;所有文件 (*)",
|
||||||
)
|
)
|
||||||
if path:
|
if path:
|
||||||
self.settings_panel.set_config_path(path)
|
config = normalize_config_preference(path) or default_finger_config()
|
||||||
self.config.config = path
|
self.settings_panel.set_config_path(config)
|
||||||
|
self.config.config = config
|
||||||
self._save_preferences()
|
self._save_preferences()
|
||||||
|
|
||||||
def _select_finger_assets(self) -> None:
|
def _select_finger_assets(self) -> None:
|
||||||
|
from .preferences import (
|
||||||
|
default_finger_assets_dir,
|
||||||
|
default_finger_config,
|
||||||
|
normalize_assets_preference,
|
||||||
|
normalize_config_preference,
|
||||||
|
packaged_finger_bundle_root,
|
||||||
|
)
|
||||||
|
|
||||||
|
start = packaged_finger_bundle_root()
|
||||||
path = QFileDialog.getExistingDirectory(
|
path = QFileDialog.getExistingDirectory(
|
||||||
self,
|
self,
|
||||||
"选择传感器 bundle 目录",
|
"选择传感器 bundle 目录",
|
||||||
self.settings_panel.sensor_bundle_dir() or "sensors/finger",
|
str(start) if start is not None else (
|
||||||
|
self.settings_panel.sensor_bundle_dir() or "sensors/finger"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
if path:
|
if path:
|
||||||
bundle_dir = Path(path)
|
bundle_dir = Path(path)
|
||||||
@@ -677,12 +697,13 @@ class FingerDashboardWindow(QMainWindow):
|
|||||||
if isinstance(rel_assets, str) and rel_assets.strip():
|
if isinstance(rel_assets, str) and rel_assets.strip():
|
||||||
assets_dir = bundle_dir / rel_assets
|
assets_dir = bundle_dir / rel_assets
|
||||||
if config_path.is_file():
|
if config_path.is_file():
|
||||||
self.settings_panel.set_config_path(str(config_path))
|
config = normalize_config_preference(str(config_path)) or default_finger_config()
|
||||||
self.config.config = str(config_path)
|
self.settings_panel.set_config_path(config)
|
||||||
self.settings_panel.set_finger_export_dir(str(assets_dir))
|
self.config.config = config
|
||||||
self.config.export_dir = str(assets_dir)
|
assets = normalize_assets_preference(str(assets_dir)) or default_finger_assets_dir()
|
||||||
|
self.settings_panel.set_finger_export_dir(assets)
|
||||||
|
self.config.export_dir = assets
|
||||||
self._save_preferences()
|
self._save_preferences()
|
||||||
|
|
||||||
def _select_csv_dir(self) -> None:
|
def _select_csv_dir(self) -> None:
|
||||||
path = QFileDialog.getExistingDirectory(
|
path = QFileDialog.getExistingDirectory(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -13,6 +13,17 @@ LEGACY_CAMERA_SELECTION_FILE = "camera_selection.json"
|
|||||||
# Keep preference.json at examples/gui/ for backward compatibility.
|
# Keep preference.json at examples/gui/ for backward compatibility.
|
||||||
_GUI_DIR = Path(__file__).resolve().parents[1]
|
_GUI_DIR = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
# Old lab / repo-relative paths that predate packaged sensors/<name>/.
|
||||||
|
_LEGACY_CONFIG_PATHS = {
|
||||||
|
"dd02",
|
||||||
|
"dd02-ov",
|
||||||
|
"config/dd02-ov.json",
|
||||||
|
"./config/dd02-ov.json",
|
||||||
|
"config/finger.json",
|
||||||
|
"./config/finger.json",
|
||||||
|
"sensors/finger/sensor.json",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def preference_path() -> Path:
|
def preference_path() -> Path:
|
||||||
# Keep preference file at examples/gui/preference.json
|
# Keep preference file at examples/gui/preference.json
|
||||||
@@ -23,6 +34,92 @@ def legacy_camera_selection_path() -> Path:
|
|||||||
return resource_path(LEGACY_CAMERA_SELECTION_FILE)
|
return resource_path(LEGACY_CAMERA_SELECTION_FILE)
|
||||||
|
|
||||||
|
|
||||||
|
def default_finger_config() -> str:
|
||||||
|
"""Public product config stem resolved via packaged ``sensors/finger``."""
|
||||||
|
return "finger"
|
||||||
|
|
||||||
|
|
||||||
|
def default_finger_assets_dir() -> str:
|
||||||
|
try:
|
||||||
|
from orisys.finger import DEFAULT_FINGER_EXPORT_DIR
|
||||||
|
|
||||||
|
return DEFAULT_FINGER_EXPORT_DIR
|
||||||
|
except ImportError:
|
||||||
|
return "sensors/finger/assets/30fps-0703"
|
||||||
|
|
||||||
|
|
||||||
|
def packaged_finger_bundle_root() -> Path | None:
|
||||||
|
"""Filesystem root of the installed ``sensors/finger`` bundle when present."""
|
||||||
|
try:
|
||||||
|
from orisys.configs import resolve_sensor_bundle
|
||||||
|
except ImportError:
|
||||||
|
return None
|
||||||
|
bundle = resolve_sensor_bundle("finger")
|
||||||
|
return bundle.root if bundle is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
def packaged_finger_assets_root() -> Path | None:
|
||||||
|
try:
|
||||||
|
from orisys.configs import resolve_sensor_asset_dir
|
||||||
|
except ImportError:
|
||||||
|
return None
|
||||||
|
return resolve_sensor_asset_dir("finger")
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_config_preference(value: str | None) -> str:
|
||||||
|
"""Map legacy config paths to the packaged ``finger`` sensor name."""
|
||||||
|
if value is None or not str(value).strip():
|
||||||
|
return default_finger_config()
|
||||||
|
text = str(value).strip().replace("\\", "/")
|
||||||
|
if text in _LEGACY_CONFIG_PATHS or text.endswith("/dd02-ov.json"):
|
||||||
|
return default_finger_config()
|
||||||
|
|
||||||
|
root = packaged_finger_bundle_root()
|
||||||
|
if root is not None:
|
||||||
|
try:
|
||||||
|
if Path(value).resolve() == (root / "sensor.json").resolve():
|
||||||
|
return default_finger_config()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_assets_preference(value: str | None) -> str:
|
||||||
|
"""Prefer packaged ``sensors/finger/assets/...`` when saved paths are stale."""
|
||||||
|
default = default_finger_assets_dir()
|
||||||
|
if value is None or not str(value).strip():
|
||||||
|
return default
|
||||||
|
text = str(value).strip()
|
||||||
|
posix = text.replace("\\", "/")
|
||||||
|
if posix == default or posix.endswith("/sensors/finger/assets/30fps-0703"):
|
||||||
|
return default
|
||||||
|
|
||||||
|
path = Path(text)
|
||||||
|
if path.is_dir():
|
||||||
|
packaged = packaged_finger_assets_root()
|
||||||
|
if packaged is not None:
|
||||||
|
try:
|
||||||
|
if path.resolve() == packaged.resolve():
|
||||||
|
return default
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
if "sensors/finger" in posix or path.name == "30fps-0703":
|
||||||
|
return text if path.is_absolute() else default
|
||||||
|
return text
|
||||||
|
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_preferences(prefs: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
prefs = dict(prefs)
|
||||||
|
if "config_path" in prefs or prefs:
|
||||||
|
prefs["config_path"] = normalize_config_preference(prefs.get("config_path"))
|
||||||
|
if "finger_assets_dir" in prefs or prefs:
|
||||||
|
prefs["finger_assets_dir"] = normalize_assets_preference(prefs.get("finger_assets_dir"))
|
||||||
|
prefs.setdefault("sensor_bundle_dir", "sensors/finger")
|
||||||
|
return prefs
|
||||||
|
|
||||||
|
|
||||||
def _read_json(path: Path) -> dict[str, Any]:
|
def _read_json(path: Path) -> dict[str, Any]:
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
return {}
|
return {}
|
||||||
@@ -34,14 +131,14 @@ def _read_json(path: Path) -> dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def load_preferences() -> dict[str, Any]:
|
def load_preferences() -> dict[str, Any]:
|
||||||
"""Load preferences, migrating camera_id from legacy file when needed."""
|
"""Load preferences, migrating camera_id / legacy config paths when needed."""
|
||||||
prefs = _read_json(preference_path())
|
prefs = _read_json(preference_path())
|
||||||
if "video_source" not in prefs:
|
if "video_source" not in prefs:
|
||||||
legacy = _read_json(legacy_camera_selection_path())
|
legacy = _read_json(legacy_camera_selection_path())
|
||||||
camera_id = legacy.get("camera_id")
|
camera_id = legacy.get("camera_id")
|
||||||
if camera_id is not None and str(camera_id).strip():
|
if camera_id is not None and str(camera_id).strip():
|
||||||
prefs["video_source"] = str(camera_id).strip()
|
prefs["video_source"] = str(camera_id).strip()
|
||||||
return prefs
|
return _normalize_preferences(prefs)
|
||||||
|
|
||||||
|
|
||||||
def save_preferences(prefs: dict[str, Any]) -> None:
|
def save_preferences(prefs: dict[str, Any]) -> None:
|
||||||
@@ -49,9 +146,10 @@ def save_preferences(prefs: dict[str, Any]) -> None:
|
|||||||
if not isinstance(prefs, dict):
|
if not isinstance(prefs, dict):
|
||||||
return
|
return
|
||||||
path = preference_path()
|
path = preference_path()
|
||||||
|
normalized = _normalize_preferences(prefs)
|
||||||
try:
|
try:
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
path.write_text(json.dumps(prefs, indent=2) + "\n", encoding="utf-8")
|
path.write_text(json.dumps(normalized, indent=2) + "\n", encoding="utf-8")
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,23 @@ class SensorBundlePreset:
|
|||||||
export_dir: str
|
export_dir: str
|
||||||
|
|
||||||
|
|
||||||
|
def _packaged_finger_preset() -> SensorBundlePreset | None:
|
||||||
|
"""Use logical names that resolve through the installed wheel sensors/ layout."""
|
||||||
|
try:
|
||||||
|
from orisys.configs import resolve_sensor_bundle
|
||||||
|
except ImportError:
|
||||||
|
return None
|
||||||
|
bundle = resolve_sensor_bundle("finger")
|
||||||
|
if bundle is None:
|
||||||
|
return None
|
||||||
|
return SensorBundlePreset(
|
||||||
|
label="ORY-FINGER",
|
||||||
|
bundle_dir="sensors/finger",
|
||||||
|
config="finger",
|
||||||
|
export_dir=DEFAULT_FINGER_EXPORT_DIR,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _load_manifest(bundle_root: Path) -> dict:
|
def _load_manifest(bundle_root: Path) -> dict:
|
||||||
path = bundle_root / "manifest.json"
|
path = bundle_root / "manifest.json"
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
@@ -50,6 +67,14 @@ def _bundle_from_root(bundle_root: Path, *, label: str | None = None) -> SensorB
|
|||||||
break
|
break
|
||||||
except ValueError:
|
except ValueError:
|
||||||
continue
|
continue
|
||||||
|
# Packaged / checkout sensors/finger: keep resolvable product names, not absolute paths.
|
||||||
|
if rel_bundle.replace("\\", "/").endswith("sensors/finger") or bundle_root.name == "finger":
|
||||||
|
return SensorBundlePreset(
|
||||||
|
label=label or "ORY-FINGER",
|
||||||
|
bundle_dir="sensors/finger",
|
||||||
|
config="finger",
|
||||||
|
export_dir=DEFAULT_FINGER_EXPORT_DIR,
|
||||||
|
)
|
||||||
return SensorBundlePreset(
|
return SensorBundlePreset(
|
||||||
label=label or "ORY-FINGER",
|
label=label or "ORY-FINGER",
|
||||||
bundle_dir=rel_bundle,
|
bundle_dir=rel_bundle,
|
||||||
@@ -59,6 +84,9 @@ def _bundle_from_root(bundle_root: Path, *, label: str | None = None) -> SensorB
|
|||||||
|
|
||||||
|
|
||||||
def default_finger_bundle() -> SensorBundlePreset:
|
def default_finger_bundle() -> SensorBundlePreset:
|
||||||
|
packaged = _packaged_finger_preset()
|
||||||
|
if packaged is not None:
|
||||||
|
return packaged
|
||||||
for root in _repo_root_candidates():
|
for root in _repo_root_candidates():
|
||||||
preset = _bundle_from_root(root / "sensors" / "finger", label="ORY-FINGER")
|
preset = _bundle_from_root(root / "sensors" / "finger", label="ORY-FINGER")
|
||||||
if preset is not None:
|
if preset is not None:
|
||||||
@@ -66,7 +94,7 @@ def default_finger_bundle() -> SensorBundlePreset:
|
|||||||
return SensorBundlePreset(
|
return SensorBundlePreset(
|
||||||
label="ORY-FINGER",
|
label="ORY-FINGER",
|
||||||
bundle_dir="sensors/finger",
|
bundle_dir="sensors/finger",
|
||||||
config="sensors/finger/sensor.json",
|
config="finger",
|
||||||
export_dir=DEFAULT_FINGER_EXPORT_DIR,
|
export_dir=DEFAULT_FINGER_EXPORT_DIR,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -108,6 +136,8 @@ def sensor_bundle_preset_index(
|
|||||||
for idx, preset in enumerate(presets):
|
for idx, preset in enumerate(presets):
|
||||||
if config == preset.config and export_dir == preset.export_dir:
|
if config == preset.config and export_dir == preset.export_dir:
|
||||||
return idx
|
return idx
|
||||||
|
if config in ("finger", "dd02", "dd02-ov") and preset.config == "finger":
|
||||||
|
return idx
|
||||||
if config.endswith("/sensor.json") and export_dir.endswith("/" + Path(preset.export_dir).name):
|
if config.endswith("/sensor.json") and export_dir.endswith("/" + Path(preset.export_dir).name):
|
||||||
return idx
|
return idx
|
||||||
return -1
|
return -1
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from PyQt5.QtWidgets import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from viz.render_2d import ArrowBackground
|
from viz.render_2d import ArrowBackground
|
||||||
|
from orisys.finger import DEFAULT_FINGER_EXPORT_DIR
|
||||||
|
|
||||||
from ..layout import DashboardLayout, LAYOUT
|
from ..layout import DashboardLayout, LAYOUT
|
||||||
from ..mesh_view_state import MeshViewState
|
from ..mesh_view_state import MeshViewState
|
||||||
@@ -157,7 +158,7 @@ class SettingsPanelWidget(QWidget):
|
|||||||
bundle_row.addWidget(self.btn_browse_finger_assets)
|
bundle_row.addWidget(self.btn_browse_finger_assets)
|
||||||
root.addLayout(bundle_row)
|
root.addLayout(bundle_row)
|
||||||
|
|
||||||
self.config_edit = QLineEdit("sensors/finger/sensor.json")
|
self.config_edit = QLineEdit("finger")
|
||||||
self.config_edit.hide()
|
self.config_edit.hide()
|
||||||
self.btn_browse_config = QPushButton("…")
|
self.btn_browse_config = QPushButton("…")
|
||||||
self.btn_browse_config.hide()
|
self.btn_browse_config.hide()
|
||||||
@@ -167,10 +168,9 @@ class SettingsPanelWidget(QWidget):
|
|||||||
self.cal_edit.setPlaceholderText("Optional calibration path")
|
self.cal_edit.setPlaceholderText("Optional calibration path")
|
||||||
self.cal_edit.hide()
|
self.cal_edit.hide()
|
||||||
|
|
||||||
self.finger_assets_edit = QLineEdit("sensors/finger/assets/30fps-0703")
|
self.finger_assets_edit = QLineEdit(DEFAULT_FINGER_EXPORT_DIR)
|
||||||
self.finger_assets_edit.hide()
|
self.finger_assets_edit.hide()
|
||||||
self._apply_bundle_preset(self._bundle_presets[0])
|
self._apply_bundle_preset(self._bundle_presets[0])
|
||||||
|
|
||||||
root.addWidget(self._caption("CSV save directory"))
|
root.addWidget(self._caption("CSV save directory"))
|
||||||
csv_dir_row = QHBoxLayout()
|
csv_dir_row = QHBoxLayout()
|
||||||
csv_dir_row.setSpacing(6)
|
csv_dir_row.setSpacing(6)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ ORY-MINI:单传感器基础流程
|
|||||||
3. 按需计算接触区域
|
3. 按需计算接触区域
|
||||||
4. 读取并可视化结果
|
4. 读取并可视化结果
|
||||||
|
|
||||||
SDK version: 0.4.3
|
SDK version: 0.4.4
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
- Windows 下必须在 if __name__ == '__main__' 中调用 mp.freeze_support()
|
- Windows 下必须在 if __name__ == '__main__' 中调用 mp.freeze_support()
|
||||||
- 每个传感器进程都会占用独立的 GPU 显存
|
- 每个传感器进程都会占用独立的 GPU 显存
|
||||||
|
|
||||||
SDK version: 0.4.3
|
SDK version: 0.4.4
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ from pathlib import Path
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
DEFAULT_FINGER_VIZ_POSE = "sensors/finger/assets/visualization/world_to_cam.npy"
|
DEFAULT_FINGER_VIZ_POSE = "sensors/finger/assets/visualization/world_to_cam.npy"
|
||||||
|
_LEGACY_FINGER_VIZ_POSES = {
|
||||||
|
"assets/finger/visualization/world_to_cam.npy",
|
||||||
|
DEFAULT_FINGER_VIZ_POSE,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _finger_bundle_viz_pose_candidates() -> list[Path]:
|
def _finger_bundle_viz_pose_candidates() -> list[Path]:
|
||||||
@@ -63,8 +67,9 @@ def resolve_viz_pose_path(path: str | Path | None = None) -> Path:
|
|||||||
here = Path(__file__).resolve()
|
here = Path(__file__).resolve()
|
||||||
candidates: list[Path] = []
|
candidates: list[Path] = []
|
||||||
|
|
||||||
# Prefer the sensors/finger bundle for the default (and matching relative) path.
|
# Prefer the sensors/finger bundle for default / legacy relative paths.
|
||||||
if path is None or raw.as_posix() == DEFAULT_FINGER_VIZ_POSE or not raw.is_absolute():
|
posix = raw.as_posix()
|
||||||
|
if path is None or posix in _LEGACY_FINGER_VIZ_POSES or not raw.is_absolute():
|
||||||
candidates.extend(_finger_bundle_viz_pose_candidates())
|
candidates.extend(_finger_bundle_viz_pose_candidates())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,8 +1,30 @@
|
|||||||
{
|
{
|
||||||
"normal_force": {
|
"schema_version": 1,
|
||||||
"model": "linear",
|
"unit": "N",
|
||||||
"slope": 3.92685554668795e-05,
|
"channels": {
|
||||||
"offset": 0.0217596967278535,
|
"normal": {
|
||||||
"unit": "N"
|
"method": "piecewise_linear",
|
||||||
|
"raw_key": "fnormal",
|
||||||
|
"extrapolate": true,
|
||||||
|
"clamp_output_min": 0.0,
|
||||||
|
"points": [
|
||||||
|
[1000.0, 0.0],
|
||||||
|
[10000.0, 0.377],
|
||||||
|
[30000.0, 1.377],
|
||||||
|
[62000.0, 2.377]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"shear": {
|
||||||
|
"method": "piecewise_linear",
|
||||||
|
"input": "components",
|
||||||
|
"signed": true,
|
||||||
|
"extrapolate": true,
|
||||||
|
"clamp_output_min": 0.0,
|
||||||
|
"points": [
|
||||||
|
[1000.0, 0.0],
|
||||||
|
[36000.0, 1.19],
|
||||||
|
[68000.0, 2.06]
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
"dis_preset": "medium",
|
"dis_preset": "medium",
|
||||||
"dis_mode": 1,
|
"dis_mode": 1,
|
||||||
"algorithm_list": ["raft", "dis"],
|
"algorithm_list": ["raft", "dis"],
|
||||||
"hhd_downsample": 5
|
"hhd_downsample": 1
|
||||||
},
|
},
|
||||||
"contact": {
|
"contact": {
|
||||||
"min_contour_area": 3000,
|
"min_contour_area": 3000,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ A Python library for processing and analyzing optical tactile sensor data,
|
|||||||
including image stitching, optical flow tracking, and deformation decomposition.
|
including image stitching, optical flow tracking, and deformation decomposition.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "0.4.3"
|
__version__ = "0.4.4"
|
||||||
__author__ = "Orisys"
|
__author__ = "Orisys"
|
||||||
|
|
||||||
from .sensor import Sensor
|
from .sensor import Sensor
|
||||||
|
|||||||
+238
-18
@@ -1,11 +1,18 @@
|
|||||||
"""Signal helpers for force filtering and normal-force calibration.
|
"""Signal helpers for force filtering and force calibration.
|
||||||
|
|
||||||
Batch ``lowpass_filter`` is the zero-phase Butterworth used by example charts.
|
Batch ``lowpass_filter`` is the zero-phase Butterworth used by example charts.
|
||||||
``LowpassFilter`` is a causal, stateful filter for per-frame ``Sensor`` outputs.
|
``LowpassFilter`` is a causal, stateful filter for per-frame ``Sensor`` outputs.
|
||||||
|
|
||||||
|
Force calibration resolution order (when register support arrives):
|
||||||
|
1. Sensor register / EEPROM calibration blob
|
||||||
|
2. ``sensors/<name>/force_calibration.json``
|
||||||
|
3. Hardcoded linear defaults below
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from scipy.signal import butter, filtfilt, sosfilt, sosfilt_zi
|
from scipy.signal import butter, filtfilt, sosfilt, sosfilt_zi
|
||||||
|
|
||||||
@@ -18,28 +25,241 @@ DEFAULT_SAMPLE_RATE_HZ = 30.0
|
|||||||
DEFAULT_LOWPASS_ORDER = 2
|
DEFAULT_LOWPASS_ORDER = 2
|
||||||
|
|
||||||
|
|
||||||
def _load_normal_force_coeffs(sensor_name: str | None = None) -> tuple[float, float]:
|
def _parse_calibration_point(point) -> tuple[float, float]:
|
||||||
if sensor_name:
|
if isinstance(point, dict):
|
||||||
try:
|
raw = point.get("fnormal", point.get("raw", point.get("x")))
|
||||||
from .configs import load_force_calibration
|
force_n = point.get("force_n", point.get("n", point.get("y")))
|
||||||
|
if raw is None or force_n is None:
|
||||||
|
raise ValueError(f"invalid calibration point: {point!r}")
|
||||||
|
return float(raw), float(force_n)
|
||||||
|
if isinstance(point, (list, tuple)) and len(point) >= 2:
|
||||||
|
return float(point[0]), float(point[1])
|
||||||
|
raise ValueError(f"invalid calibration point: {point!r}")
|
||||||
|
|
||||||
data = load_force_calibration(sensor_name)
|
|
||||||
except Exception:
|
@dataclass(frozen=True)
|
||||||
data = None
|
class ForceChannelCalibration:
|
||||||
if isinstance(data, dict):
|
"""One force channel: linear coeffs and/or piecewise-linear breakpoints."""
|
||||||
block = data.get("normal_force", data)
|
|
||||||
if isinstance(block, dict):
|
method: str = "linear"
|
||||||
slope = block.get("slope")
|
slope: float | None = None
|
||||||
offset = block.get("offset")
|
offset: float | None = None
|
||||||
if slope is not None and offset is not None:
|
points: tuple[tuple[float, float], ...] = ()
|
||||||
return float(slope), float(offset)
|
extrapolate: bool = True
|
||||||
|
clamp_output_min: float | None = 0.0
|
||||||
|
input_mode: str = "magnitude"
|
||||||
|
signed: bool = False
|
||||||
|
unit: str = "N"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def linear(cls, slope: float, offset: float, *, unit: str = "N") -> ForceChannelCalibration:
|
||||||
|
return cls(method="linear", slope=float(slope), offset=float(offset), unit=unit)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_channel_dict(
|
||||||
|
cls,
|
||||||
|
block: dict,
|
||||||
|
*,
|
||||||
|
unit: str = "N",
|
||||||
|
default_method: str = "piecewise_linear",
|
||||||
|
) -> ForceChannelCalibration | None:
|
||||||
|
if not isinstance(block, dict):
|
||||||
|
return None
|
||||||
|
|
||||||
|
method = str(block.get("method", block.get("model", default_method))).lower()
|
||||||
|
if method in ("linear", "affine"):
|
||||||
|
slope = block.get("slope")
|
||||||
|
offset = block.get("offset")
|
||||||
|
if slope is None or offset is None:
|
||||||
|
return None
|
||||||
|
return cls.linear(float(slope), float(offset), unit=str(block.get("unit", unit)))
|
||||||
|
|
||||||
|
if method != "piecewise_linear":
|
||||||
|
return None
|
||||||
|
|
||||||
|
raw_points = block.get("points", [])
|
||||||
|
if not isinstance(raw_points, (list, tuple)) or len(raw_points) < 2:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
points = tuple(sorted(_parse_calibration_point(p) for p in raw_points))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
if len(points) < 2:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
method="piecewise_linear",
|
||||||
|
points=points,
|
||||||
|
extrapolate=bool(block.get("extrapolate", True)),
|
||||||
|
clamp_output_min=block.get("clamp_output_min", 0.0),
|
||||||
|
input_mode=str(block.get("input", "magnitude")).lower(),
|
||||||
|
signed=bool(block.get("signed", False)),
|
||||||
|
unit=str(block.get("unit", unit)),
|
||||||
|
)
|
||||||
|
|
||||||
|
def convert(self, raw: float) -> float:
|
||||||
|
raw = float(raw)
|
||||||
|
if self.method == "linear":
|
||||||
|
slope = NORMAL_FORCE_CAL_SLOPE if self.slope is None else float(self.slope)
|
||||||
|
offset = NORMAL_FORCE_CAL_OFFSET if self.offset is None else float(self.offset)
|
||||||
|
return slope * raw + offset
|
||||||
|
|
||||||
|
points = self.points
|
||||||
|
if len(points) < 2:
|
||||||
|
return raw
|
||||||
|
|
||||||
|
if raw <= points[0][0]:
|
||||||
|
segment = (points[0], points[1])
|
||||||
|
if not self.extrapolate:
|
||||||
|
return self._clamp(points[0][1])
|
||||||
|
elif raw >= points[-1][0]:
|
||||||
|
segment = (points[-2], points[-1])
|
||||||
|
if not self.extrapolate:
|
||||||
|
return self._clamp(points[-1][1])
|
||||||
|
else:
|
||||||
|
segment = None
|
||||||
|
for left, right in zip(points, points[1:]):
|
||||||
|
if left[0] <= raw <= right[0]:
|
||||||
|
segment = (left, right)
|
||||||
|
break
|
||||||
|
if segment is None:
|
||||||
|
return self._clamp(points[-1][1])
|
||||||
|
|
||||||
|
(raw0, n0), (raw1, n1) = segment
|
||||||
|
if raw1 == raw0:
|
||||||
|
value = n0
|
||||||
|
else:
|
||||||
|
ratio = (raw - raw0) / (raw1 - raw0)
|
||||||
|
value = n0 + ratio * (n1 - n0)
|
||||||
|
return self._clamp(value)
|
||||||
|
|
||||||
|
def convert_component(self, raw: float) -> float:
|
||||||
|
if not self.signed:
|
||||||
|
return self.convert(raw)
|
||||||
|
raw = float(raw)
|
||||||
|
sign = -1.0 if raw < 0.0 else 1.0
|
||||||
|
return sign * self.convert(abs(raw))
|
||||||
|
|
||||||
|
def _clamp(self, value: float) -> float:
|
||||||
|
if self.clamp_output_min is not None:
|
||||||
|
return max(float(self.clamp_output_min), float(value))
|
||||||
|
return float(value)
|
||||||
|
|
||||||
|
|
||||||
|
_DEFAULT_NORMAL = ForceChannelCalibration.linear(
|
||||||
|
NORMAL_FORCE_CAL_SLOPE,
|
||||||
|
NORMAL_FORCE_CAL_OFFSET,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_force_calibration_data(sensor_name: str | None) -> dict | None:
|
||||||
|
if not sensor_name:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
from .configs import load_force_calibration
|
||||||
|
|
||||||
|
data = load_force_calibration(sensor_name)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return data if isinstance(data, dict) else None
|
||||||
|
|
||||||
|
|
||||||
|
def _channel_from_data(data: dict | None, channel: str) -> ForceChannelCalibration | None:
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return None
|
||||||
|
|
||||||
|
unit = str(data.get("unit", "N"))
|
||||||
|
channels = data.get("channels")
|
||||||
|
if isinstance(channels, dict):
|
||||||
|
block = channels.get(channel)
|
||||||
|
cal = ForceChannelCalibration.from_channel_dict(block, unit=unit)
|
||||||
|
if cal is not None:
|
||||||
|
return cal
|
||||||
|
|
||||||
|
# Legacy single-channel file: {"normal_force": {"model": "linear", ...}}
|
||||||
|
if channel == "normal":
|
||||||
|
block = data.get("normal_force", data)
|
||||||
|
cal = ForceChannelCalibration.from_channel_dict(
|
||||||
|
block, unit=unit, default_method="linear"
|
||||||
|
)
|
||||||
|
if cal is not None:
|
||||||
|
return cal
|
||||||
|
if isinstance(block, dict):
|
||||||
|
slope = block.get("slope")
|
||||||
|
offset = block.get("offset")
|
||||||
|
if slope is not None and offset is not None:
|
||||||
|
return ForceChannelCalibration.linear(
|
||||||
|
float(slope),
|
||||||
|
float(offset),
|
||||||
|
unit=str(block.get("unit", unit)),
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _normal_calibration(sensor_name: str | None = None) -> ForceChannelCalibration:
|
||||||
|
cal = _channel_from_data(_load_force_calibration_data(sensor_name), "normal")
|
||||||
|
return cal if cal is not None else _DEFAULT_NORMAL
|
||||||
|
|
||||||
|
|
||||||
|
def _shear_calibration(sensor_name: str | None = None) -> ForceChannelCalibration | None:
|
||||||
|
return _channel_from_data(_load_force_calibration_data(sensor_name), "shear")
|
||||||
|
|
||||||
|
|
||||||
|
def _load_normal_force_coeffs(sensor_name: str | None = None) -> tuple[float, float]:
|
||||||
|
"""Legacy helper: linear slope/offset only (piecewise returns hardcoded defaults)."""
|
||||||
|
cal = _normal_calibration(sensor_name)
|
||||||
|
if cal.method == "linear" and cal.slope is not None and cal.offset is not None:
|
||||||
|
return float(cal.slope), float(cal.offset)
|
||||||
return NORMAL_FORCE_CAL_SLOPE, NORMAL_FORCE_CAL_OFFSET
|
return NORMAL_FORCE_CAL_SLOPE, NORMAL_FORCE_CAL_OFFSET
|
||||||
|
|
||||||
|
|
||||||
def calibrate_normal_force(raw_value: float, *, sensor_name: str | None = None) -> float:
|
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.
|
||||||
slope, offset = _load_normal_force_coeffs(sensor_name)
|
|
||||||
return slope * float(raw_value) + offset
|
Uses ``sensors/<name>/force_calibration.json`` when present (piecewise or linear).
|
||||||
|
Falls back to hardcoded linear defaults when missing.
|
||||||
|
"""
|
||||||
|
return _normal_calibration(sensor_name).convert(raw_value)
|
||||||
|
|
||||||
|
|
||||||
|
def calibrate_shear_force(raw_value: float, *, sensor_name: str | None = None) -> float:
|
||||||
|
"""Convert a raw shear magnitude to Newtons using the shear channel curve.
|
||||||
|
|
||||||
|
Returns ``float(raw_value)`` unchanged when no shear calibration is available.
|
||||||
|
"""
|
||||||
|
cal = _shear_calibration(sensor_name)
|
||||||
|
if cal is None:
|
||||||
|
return float(raw_value)
|
||||||
|
return cal.convert(raw_value)
|
||||||
|
|
||||||
|
|
||||||
|
def calibrate_shear_components(
|
||||||
|
shear_x: float,
|
||||||
|
shear_y: float,
|
||||||
|
*,
|
||||||
|
sensor_name: str | None = None,
|
||||||
|
) -> tuple[float, float]:
|
||||||
|
"""Convert raw shear components to Newtons.
|
||||||
|
|
||||||
|
When the shear channel uses ``input: components`` and ``signed: true``, each
|
||||||
|
axis is calibrated on absolute magnitude then re-signed. Otherwise the
|
||||||
|
magnitude is calibrated and components are scaled proportionally.
|
||||||
|
"""
|
||||||
|
sx = float(shear_x)
|
||||||
|
sy = float(shear_y)
|
||||||
|
cal = _shear_calibration(sensor_name)
|
||||||
|
if cal is None:
|
||||||
|
return sx, sy
|
||||||
|
|
||||||
|
if cal.input_mode == "components":
|
||||||
|
return cal.convert_component(sx), cal.convert_component(sy)
|
||||||
|
|
||||||
|
magnitude = (sx * sx + sy * sy) ** 0.5
|
||||||
|
if magnitude <= 0.0:
|
||||||
|
return 0.0, 0.0
|
||||||
|
calibrated = cal.convert(magnitude)
|
||||||
|
scale = calibrated / magnitude
|
||||||
|
return sx * scale, sy * scale
|
||||||
|
|
||||||
|
|
||||||
def _nyquist_hz(sample_rate_hz: float) -> float:
|
def _nyquist_hz(sample_rate_hz: float) -> float:
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ Flow:
|
|||||||
4. Feed the rectified frame into Sensor.get_img(..., already_processed=True)
|
4. Feed the rectified frame into Sensor.get_img(..., already_processed=True)
|
||||||
5. Compute deformation/contact and visualize
|
5. Compute deformation/contact and visualize
|
||||||
|
|
||||||
SDK version: 0.4.3
|
SDK version: 0.4.4
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ def test_pyproject_dynamic_version(packaging_utils):
|
|||||||
sys.path.insert(0, str(build_dir))
|
sys.path.insert(0, str(build_dir))
|
||||||
from common.pyproject_utils import get_version
|
from common.pyproject_utils import get_version
|
||||||
|
|
||||||
assert get_version(REPO_ROOT) == "0.4.3"
|
assert get_version(REPO_ROOT) == "0.4.4"
|
||||||
|
|
||||||
|
|
||||||
def test_pyproject_bundle_manifest(packaging_utils):
|
def test_pyproject_bundle_manifest(packaging_utils):
|
||||||
@@ -48,7 +48,7 @@ def test_pyproject_bundle_manifest(packaging_utils):
|
|||||||
|
|
||||||
items = bundle_items(REPO_ROOT)
|
items = bundle_items(REPO_ROOT)
|
||||||
names = {item["name"] for item in items}
|
names = {item["name"] for item in items}
|
||||||
assert names == {"configs", "examples", "docs", "assets"}
|
assert names == {"configs", "examples", "docs", "sensors"}
|
||||||
|
|
||||||
data_files = dict(bundle_data_files(REPO_ROOT))
|
data_files = dict(bundle_data_files(REPO_ROOT))
|
||||||
assert "orisys/configs" in data_files
|
assert "orisys/configs" in data_files
|
||||||
@@ -57,8 +57,9 @@ def test_pyproject_bundle_manifest(packaging_utils):
|
|||||||
assert all("/" in path and "*" not in path for path in config_files)
|
assert all("/" in path and "*" not in path for path in config_files)
|
||||||
assert any(path.endswith(".json") for path in config_files)
|
assert any(path.endswith(".json") for path in config_files)
|
||||||
|
|
||||||
asset_dirs = [dest for dest in data_files if dest.startswith("orisys/assets/")]
|
sensor_dirs = [dest for dest in data_files if dest.startswith("orisys/sensors/")]
|
||||||
assert asset_dirs
|
assert sensor_dirs
|
||||||
|
assert any("finger/assets" in dest for dest in sensor_dirs)
|
||||||
assert all("*" not in path for files in data_files.values() for path in files)
|
assert all("*" not in path for files in data_files.values() for path in files)
|
||||||
assert "examples" not in data_files
|
assert "examples" not in data_files
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ from orisys.signal_processing import (
|
|||||||
NORMAL_FORCE_CAL_SLOPE,
|
NORMAL_FORCE_CAL_SLOPE,
|
||||||
LowpassFilter,
|
LowpassFilter,
|
||||||
calibrate_normal_force,
|
calibrate_normal_force,
|
||||||
|
calibrate_shear_components,
|
||||||
|
calibrate_shear_force,
|
||||||
lowpass_filter,
|
lowpass_filter,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -73,6 +75,46 @@ def test_calibrate_normal_force_preserves_linear_curve():
|
|||||||
assert calibrate_normal_force(0.0) == pytest.approx(NORMAL_FORCE_CAL_OFFSET)
|
assert calibrate_normal_force(0.0) == pytest.approx(NORMAL_FORCE_CAL_OFFSET)
|
||||||
|
|
||||||
|
|
||||||
|
def test_calibrate_normal_force_mini_uses_legacy_linear_file():
|
||||||
|
raw = 10000.0
|
||||||
|
expected = NORMAL_FORCE_CAL_SLOPE * raw + NORMAL_FORCE_CAL_OFFSET
|
||||||
|
assert calibrate_normal_force(raw, sensor_name="mini") == pytest.approx(expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_calibrate_normal_force_box_uses_piecewise_curve():
|
||||||
|
assert calibrate_normal_force(10000.0, sensor_name="box") == pytest.approx(0.377)
|
||||||
|
assert calibrate_normal_force(30000.0, sensor_name="box") == pytest.approx(1.377)
|
||||||
|
# Mid-segment interpolation between 10000→0.377 and 30000→1.377
|
||||||
|
mid = calibrate_normal_force(20000.0, sensor_name="box")
|
||||||
|
assert mid == pytest.approx(0.877)
|
||||||
|
# Below first breakpoint with extrapolate=true
|
||||||
|
assert calibrate_normal_force(0.0, sensor_name="box") == pytest.approx(0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_calibrate_shear_force_box_piecewise():
|
||||||
|
assert calibrate_shear_force(1000.0, sensor_name="box") == pytest.approx(0.0)
|
||||||
|
assert calibrate_shear_force(36000.0, sensor_name="box") == pytest.approx(1.19)
|
||||||
|
assert calibrate_shear_force(68000.0, sensor_name="box") == pytest.approx(2.06)
|
||||||
|
|
||||||
|
|
||||||
|
def test_calibrate_shear_components_box_signed():
|
||||||
|
sx, sy = calibrate_shear_components(36000.0, -36000.0, sensor_name="box")
|
||||||
|
assert sx == pytest.approx(1.19)
|
||||||
|
assert sy == pytest.approx(-1.19)
|
||||||
|
|
||||||
|
|
||||||
|
def test_calibrate_shear_missing_file_returns_raw():
|
||||||
|
assert calibrate_shear_force(1234.5, sensor_name="no-such-sensor") == pytest.approx(1234.5)
|
||||||
|
sx, sy = calibrate_shear_components(1.0, -2.0, sensor_name="no-such-sensor")
|
||||||
|
assert (sx, sy) == pytest.approx((1.0, -2.0))
|
||||||
|
|
||||||
|
|
||||||
|
def test_calibrate_normal_force_unknown_sensor_falls_back_to_defaults():
|
||||||
|
raw = 5000.0
|
||||||
|
expected = NORMAL_FORCE_CAL_SLOPE * raw + NORMAL_FORCE_CAL_OFFSET
|
||||||
|
assert calibrate_normal_force(raw, sensor_name="no-such-sensor") == pytest.approx(expected)
|
||||||
|
|
||||||
|
|
||||||
def _external_sensor():
|
def _external_sensor():
|
||||||
return Sensor("external", config_name="box", verbose=False, backend="cpu")
|
return Sensor("external", config_name="box", verbose=False, backend="cpu")
|
||||||
|
|
||||||
|
|||||||
@@ -19,25 +19,25 @@ def wheel_package():
|
|||||||
|
|
||||||
def test_compliant_manylinux_tags(wheel_package):
|
def test_compliant_manylinux_tags(wheel_package):
|
||||||
assert wheel_package.is_compliant_manylinux_wheel(
|
assert wheel_package.is_compliant_manylinux_wheel(
|
||||||
Path("orisys-0.4.3-cp310-cp310-manylinux_2_17_x86_64.whl")
|
Path("orisys-0.4.4-cp310-cp310-manylinux_2_17_x86_64.whl")
|
||||||
)
|
)
|
||||||
assert wheel_package.is_compliant_manylinux_wheel(
|
assert wheel_package.is_compliant_manylinux_wheel(
|
||||||
Path("orisys-0.4.3-cp310-cp310-manylinux2014_x86_64.whl")
|
Path("orisys-0.4.4-cp310-cp310-manylinux2014_x86_64.whl")
|
||||||
)
|
)
|
||||||
assert wheel_package.is_compliant_manylinux_wheel(
|
assert wheel_package.is_compliant_manylinux_wheel(
|
||||||
Path("orisys-0.4.3-cp310-cp310-manylinux_2_28_x86_64.whl")
|
Path("orisys-0.4.4-cp310-cp310-manylinux_2_28_x86_64.whl")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_rejects_illegal_manylinux_tags(wheel_package):
|
def test_rejects_illegal_manylinux_tags(wheel_package):
|
||||||
assert not wheel_package.is_compliant_manylinux_wheel(
|
assert not wheel_package.is_compliant_manylinux_wheel(
|
||||||
Path("orisys-0.4.3-cp310-cp310-manylinux_x86_64.whl")
|
Path("orisys-0.4.4-cp310-cp310-manylinux_x86_64.whl")
|
||||||
)
|
)
|
||||||
assert not wheel_package.is_compliant_manylinux_wheel(
|
assert not wheel_package.is_compliant_manylinux_wheel(
|
||||||
Path("orisys-0.4.3-cp310-cp310-linux_x86_64.whl")
|
Path("orisys-0.4.4-cp310-cp310-linux_x86_64.whl")
|
||||||
)
|
)
|
||||||
assert not wheel_package.is_compliant_manylinux_wheel(
|
assert not wheel_package.is_compliant_manylinux_wheel(
|
||||||
Path("orisys-0.4.3-cp310-cp310-win_amd64.whl")
|
Path("orisys-0.4.4-cp310-cp310-win_amd64.whl")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user