Compare commits
22 Commits
4acdce4456
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 75fa98eff0 | |||
| bc059e3539 | |||
| 5be2d9759a | |||
| 32823a7666 | |||
| 26168852ef | |||
| 36e0c22de6 | |||
| a387b8b693 | |||
| 8a4be5f8f2 | |||
| 68910e63c1 | |||
| 577e3fe528 | |||
| 32d4164471 | |||
| d59e1cb7fb | |||
| 2cb152b76e | |||
| ab97efe039 | |||
| 135857b6de | |||
| 8a7760f12b | |||
| 4583dc3a40 | |||
| 1ceb0d9ca4 | |||
| 44bcc19426 | |||
| 2e09c9360e | |||
| 93b4517ebf | |||
| 9a69f01278 |
+5
-1
@@ -5,8 +5,12 @@ __pycache__/
|
|||||||
|
|
||||||
# Packaging / build outputs
|
# Packaging / build outputs
|
||||||
/build/nuitka/
|
/build/nuitka/
|
||||||
/build/lib/orisys/
|
/build/build/
|
||||||
|
/build/bdist.*/
|
||||||
|
/build/dist/
|
||||||
|
/build/lib/
|
||||||
/build/pyinstaller_work/
|
/build/pyinstaller_work/
|
||||||
|
/build/*.spec
|
||||||
/dist/
|
/dist/
|
||||||
/dist_obfuscated/
|
/dist_obfuscated/
|
||||||
/dist_pyd/
|
/dist_pyd/
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Non-Python files needed for sdist -> wheel builds from an unpacked sdist.
|
||||||
|
include pyproject.toml
|
||||||
|
include setup.py
|
||||||
|
include README.md
|
||||||
|
recursive-include config *.json
|
||||||
|
recursive-include sensors *
|
||||||
|
recursive-include build/common *.py
|
||||||
|
recursive-include build/wheel *.py
|
||||||
@@ -4,11 +4,13 @@ Orisys SDK is a Python toolkit for optical tactile sensing. It provides image ca
|
|||||||
stitching, dense optical flow, Helmholtz-Hodge decomposition, contact/depth estimation,
|
stitching, dense optical flow, Helmholtz-Hodge decomposition, contact/depth estimation,
|
||||||
and several runnable examples for camera and video-based workflows.
|
and several runnable examples for camera and video-based workflows.
|
||||||
|
|
||||||
|
PyPI: [https://pypi.org/project/orisys/](https://pypi.org/project/orisys/)
|
||||||
|
|
||||||
## Repository Layout
|
## Repository Layout
|
||||||
|
|
||||||
- `src/orisys/`: installable SDK package
|
- `src/orisys/`: installable SDK package
|
||||||
- `config/`: default and lab/runtime JSON configurations
|
- `config/`: default and lab/runtime JSON configurations
|
||||||
- `examples/`: product demos (`products/ory_box`, `ory_mini`, `ory_finger`) plus shared `gui/`, `pipeline/`, `viz/`
|
- `examples/`: runnable demos (`basic_pipeline.py`, `mini_pipeline.py`, `finger_pipeline*.py`) plus shared `gui/`, `pipeline/`, `viz/`
|
||||||
- `scripts/`: maintenance, calibration, packaging, and demo scripts
|
- `scripts/`: maintenance, calibration, packaging, and demo scripts
|
||||||
- `tools/`: non-package tooling such as patch-generation utilities
|
- `tools/`: non-package tooling such as patch-generation utilities
|
||||||
- `tests/`: automated pytest suite
|
- `tests/`: automated pytest suite
|
||||||
@@ -52,23 +54,22 @@ pytest
|
|||||||
ORY-BOX:
|
ORY-BOX:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python examples/products/ory_box/basic_pipeline.py -v 0 -c ddjx01
|
python examples/basic_pipeline.py -v 0 -c box
|
||||||
```
|
```
|
||||||
|
|
||||||
ORY-MINI:
|
ORY-MINI:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python examples/products/ory_mini/basic_pipeline.py -v 0 -c mini
|
python examples/mini_pipeline.py -v 0 -c mini
|
||||||
```
|
```
|
||||||
|
|
||||||
ORY-FINGER:
|
ORY-FINGER:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python examples/products/ory_finger/finger_pipeline.py --camera_id 0 --config dd02
|
python examples/finger_pipeline.py --camera_id 0 --config finger
|
||||||
```
|
```
|
||||||
|
|
||||||
Compatibility wrappers remain at `examples/basic_pipeline.py` and
|
Run commands from the repository root.
|
||||||
`examples/finger_pipeline*.py`.
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@@ -76,7 +77,7 @@ The canonical configuration files now live in the repo-level `config/` folder.
|
|||||||
The package loader `orisys.configs` resolves configs from:
|
The package loader `orisys.configs` resolves configs from:
|
||||||
|
|
||||||
1. an explicit JSON file path
|
1. an explicit JSON file path
|
||||||
2. a config file stem under `config/` (for example `ddjx01`, `mini`, `dd02`)
|
2. a config file stem under `config/` (for example `box`, `mini`, `finger`)
|
||||||
3. a product alias such as `dd02` -> `dd02-ov`
|
3. a product alias such as `dd02` -> `dd02-ov`
|
||||||
4. a backward-compatible JSON `"name"` match, such as `"prototype"`
|
4. a backward-compatible JSON `"name"` match, such as `"prototype"`
|
||||||
|
|
||||||
@@ -85,17 +86,16 @@ Examples:
|
|||||||
```python
|
```python
|
||||||
import orisys
|
import orisys
|
||||||
|
|
||||||
sensor = orisys.Sensor(0, config_name="ddjx01")
|
sensor = orisys.Sensor(0, config_name="box")
|
||||||
cfg = orisys.export_config("ddjx01", "./config/exported_config.json")
|
cfg = orisys.export_config("box", "./config/exported_config.json")
|
||||||
```
|
```
|
||||||
|
|
||||||
## Examples and Tools
|
## Examples and Tools
|
||||||
|
|
||||||
- Example entry points: `examples/README.md`
|
- Example entry points: `examples/README.md`
|
||||||
- User documentation: `docs/get_started.md`
|
- User documentation: `docs/get_started.md`
|
||||||
- Packaging helper: `build/build_nuitka.py` (compiled SDK wheel)
|
- Build launcher: `python build/run.py` (see `build/README.md`)
|
||||||
- Manylinux / PyPI binary wheel (Docker Desktop): `docs/publishing.md`
|
- Manylinux / PyPI binary wheel: `docs/publishing.md`
|
||||||
- GUI standalone folder: `build/build_gui_win.bat` → `dist/OrisysDashboard/`
|
|
||||||
- Specialized utilities: `tools/`
|
- Specialized utilities: `tools/`
|
||||||
|
|
||||||
## Publishing (Linux manylinux)
|
## Publishing (Linux manylinux)
|
||||||
@@ -103,7 +103,7 @@ cfg = orisys.export_config("ddjx01", "./config/exported_config.json")
|
|||||||
To build PyPI-compliant CPython 3.10/3.11/3.12 Linux wheels from Windows Docker Desktop:
|
To build PyPI-compliant CPython 3.10/3.11/3.12 Linux wheels from Windows Docker Desktop:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell -ExecutionPolicy Bypass -File .\build\build_manylinux_docker.ps1
|
python build/run.py manylinux
|
||||||
```
|
```
|
||||||
|
|
||||||
Upload candidates land in `dist/repaired/`. Full TestPyPI → PyPI steps:
|
Upload candidates land in `dist/repaired/`. Full TestPyPI → PyPI steps:
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
@@ -1,44 +0,0 @@
|
|||||||
# -*- mode: python ; coding: utf-8 -*-
|
|
||||||
|
|
||||||
|
|
||||||
a = Analysis(
|
|
||||||
['D:\\repo\\orisys-sdk\\examples\\finger_pipeline_3dview.py'],
|
|
||||||
pathex=[],
|
|
||||||
binaries=[],
|
|
||||||
datas=[('D:\\repo\\orisys-sdk\\config', 'config'), ('D:\\repo\\orisys-sdk\\assets', 'assets')],
|
|
||||||
hiddenimports=['PyQt5', 'pyqtgraph', 'open3d', 'cv2', 'orisys', 'gui', 'pipeline', 'viz', 'gui.app_paths', 'plotly', '_plotly_utils', 'open3d.cpu.pybind'],
|
|
||||||
hookspath=[],
|
|
||||||
hooksconfig={},
|
|
||||||
runtime_hooks=[],
|
|
||||||
excludes=['cupy', 'cupyx', 'pyopencl', 'tkinter', 'pip', 'wheel', 'matplotlib'],
|
|
||||||
noarchive=False,
|
|
||||||
optimize=0,
|
|
||||||
)
|
|
||||||
pyz = PYZ(a.pure)
|
|
||||||
|
|
||||||
exe = EXE(
|
|
||||||
pyz,
|
|
||||||
a.scripts,
|
|
||||||
[],
|
|
||||||
exclude_binaries=True,
|
|
||||||
name='OrisysDashboard',
|
|
||||||
debug=False,
|
|
||||||
bootloader_ignore_signals=False,
|
|
||||||
strip=False,
|
|
||||||
upx=True,
|
|
||||||
console=False,
|
|
||||||
disable_windowed_traceback=False,
|
|
||||||
argv_emulation=False,
|
|
||||||
target_arch=None,
|
|
||||||
codesign_identity=None,
|
|
||||||
entitlements_file=None,
|
|
||||||
)
|
|
||||||
coll = COLLECT(
|
|
||||||
exe,
|
|
||||||
a.binaries,
|
|
||||||
a.datas,
|
|
||||||
strip=False,
|
|
||||||
upx=True,
|
|
||||||
upx_exclude=[],
|
|
||||||
name='OrisysDashboard',
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# Build tooling
|
||||||
|
|
||||||
|
Use one launcher from the repo root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python build/run.py <command>
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
build/
|
||||||
|
run.py << single entrypoint
|
||||||
|
README.md
|
||||||
|
common/ shared helpers (paths, pyproject, verify)
|
||||||
|
wheel/ SDK .whl (Nuitka + manylinux Docker)
|
||||||
|
gui/
|
||||||
|
common/ shared PyInstaller driver
|
||||||
|
ory_finger/ ORY-FINGER dashboard
|
||||||
|
ory_mini/ ORY-MINI internal GUI (scaffold)
|
||||||
|
ory_box/ ORY-BOX internal GUI (scaffold)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### SDK wheel
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python build/run.py wheel-win
|
||||||
|
python build/run.py wheel-manylinux
|
||||||
|
|
||||||
|
# Single Version
|
||||||
|
python build/run.py wheel
|
||||||
|
# Inside Docker
|
||||||
|
python build/run.py wheel --target pypi-linux --verify
|
||||||
|
```
|
||||||
|
|
||||||
|
Direct module paths (same behavior):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python build/wheel/build.py
|
||||||
|
build\wheel\win.bat
|
||||||
|
.\build\wheel\docker\run.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
### GUI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python build/run.py gui finger
|
||||||
|
python build/run.py gui mini # scaffold / NotImplemented
|
||||||
|
python build/run.py gui box # scaffold / NotImplemented
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python build/run.py verify path\to\orisys.whl
|
||||||
|
python build/run.py verify --gui-dist --product ory_finger
|
||||||
|
```
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
- Wheels: `SDK/<version>/`, `dist/repaired/`
|
||||||
|
- Finger GUI: `dist/OrisysDashboard/`
|
||||||
|
- Future mini/box: `dist/OryMini/`, `dist/OryBox/`
|
||||||
|
|
||||||
|
Generated work files under `build/nuitka/` and `build/pyinstaller_work/` are gitignored.
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
powershell -ExecutionPolicy Bypass -File .\build\build_manylinux_docker.ps1
|
|
||||||
powershell -ExecutionPolicy Bypass -File .\build\build_manylinux_docker.ps1 -PythonVersions 3.10,3.12
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
"""Build a CPU-only standalone Windows folder for Orisys 3D dashboard using PyInstaller.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
conda activate py310 # or a clean virtualenv with only required packages
|
|
||||||
pip install pyinstaller
|
|
||||||
python build/build_gui_win_pyinstaller.py
|
|
||||||
|
|
||||||
Output:
|
|
||||||
dist/OrisysDashboard/OrisysDashboard.exe (+ bundled Python, DLLs, config/, assets/)
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# ---------- Configuration ----------
|
|
||||||
ENTRY_SCRIPT = Path("examples/products/ory_finger/finger_pipeline_3dview.py").resolve()
|
|
||||||
EXE_NAME = "OrisysDashboard.exe"
|
|
||||||
DIST_NAME = "OrisysDashboard" # final folder name
|
|
||||||
repo_root = Path(__file__).parent.parent
|
|
||||||
|
|
||||||
# Packages you want to force-include (PyInstaller often auto-detects them, but explicit avoids misses)
|
|
||||||
HIDDEN_IMPORTS = [
|
|
||||||
"PyQt5",
|
|
||||||
"pyqtgraph",
|
|
||||||
"open3d",
|
|
||||||
"cv2",
|
|
||||||
"orisys",
|
|
||||||
"gui",
|
|
||||||
"pipeline",
|
|
||||||
"viz",
|
|
||||||
"gui.app_paths",
|
|
||||||
"plotly",
|
|
||||||
"_plotly_utils",
|
|
||||||
# sometimes needed for open3d:
|
|
||||||
"open3d.cpu.pybind", # adjust to actual module if needed
|
|
||||||
]
|
|
||||||
|
|
||||||
# Modules to completely exclude (they won't be imported anyway if absent in venv)
|
|
||||||
EXCLUDE_MODULES = [
|
|
||||||
"cupy",
|
|
||||||
"cupyx",
|
|
||||||
"pyopencl",
|
|
||||||
"tkinter",
|
|
||||||
# "unittest",
|
|
||||||
# "email",
|
|
||||||
# "http",
|
|
||||||
# "xmlrpc",
|
|
||||||
# "pkg_resources",
|
|
||||||
# "setuptools",
|
|
||||||
"pip",
|
|
||||||
"wheel",
|
|
||||||
"matplotlib", # unless you use it
|
|
||||||
# "scipy", # unless you use it
|
|
||||||
]
|
|
||||||
|
|
||||||
# Data directories: (source relative to repo root, destination in bundle)
|
|
||||||
DATA_DIRS = [
|
|
||||||
("config", "config"),
|
|
||||||
("assets", "assets"),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Optional: UPX directory (download from https://upx.github.io)
|
|
||||||
UPX_DIR = r"C:\tools\upx" # set to None if not available
|
|
||||||
# ---------- End Configuration ----------
|
|
||||||
|
|
||||||
|
|
||||||
def clean_dist(dist_path: Path) -> None:
|
|
||||||
if dist_path.exists():
|
|
||||||
shutil.rmtree(dist_path)
|
|
||||||
dist_path.mkdir(parents=True)
|
|
||||||
|
|
||||||
|
|
||||||
def build_with_pyinstaller():
|
|
||||||
# Ensure we are in a clean, minimal venv
|
|
||||||
print("Using Python:", sys.executable)
|
|
||||||
|
|
||||||
# Output folder for PyInstaller work files
|
|
||||||
work_dir = Path("build/pyinstaller_work")
|
|
||||||
dist_dir = Path("dist") / DIST_NAME
|
|
||||||
clean_dist(work_dir)
|
|
||||||
clean_dist(dist_dir)
|
|
||||||
|
|
||||||
# Build PyInstaller command
|
|
||||||
cmd = [
|
|
||||||
sys.executable, "-m", "PyInstaller",
|
|
||||||
"--onedir", # produce a folder, not a single exe
|
|
||||||
"--name", DIST_NAME,
|
|
||||||
f"--distpath={dist_dir.parent}",
|
|
||||||
f"--workpath={work_dir}",
|
|
||||||
f"--specpath=build",
|
|
||||||
"--noconfirm",
|
|
||||||
"--clean",
|
|
||||||
"--windowed", # or "--console" if you need terminal
|
|
||||||
]
|
|
||||||
|
|
||||||
# Add data files
|
|
||||||
for src_rel, dest_rel in DATA_DIRS:
|
|
||||||
src = Path(src_rel).resolve()
|
|
||||||
if not src.is_dir():
|
|
||||||
raise FileNotFoundError(f"Data directory missing: {src}")
|
|
||||||
cmd.append(f"--add-data={src};{dest_rel}")
|
|
||||||
|
|
||||||
# Logo file if exists
|
|
||||||
logo = Path("logo.png")
|
|
||||||
if logo.exists():
|
|
||||||
cmd.append(f"--add-data={logo.resolve()};.")
|
|
||||||
|
|
||||||
# Hidden imports
|
|
||||||
for mod in HIDDEN_IMPORTS:
|
|
||||||
cmd.append(f"--hidden-import={mod}")
|
|
||||||
|
|
||||||
# Exclude modules
|
|
||||||
for mod in EXCLUDE_MODULES:
|
|
||||||
cmd.append(f"--exclude-module={mod}")
|
|
||||||
|
|
||||||
# UPX if available
|
|
||||||
if UPX_DIR and Path(UPX_DIR).is_dir():
|
|
||||||
cmd.append(f"--upx-dir={UPX_DIR}")
|
|
||||||
|
|
||||||
# Entry point
|
|
||||||
cmd.append(str(ENTRY_SCRIPT))
|
|
||||||
|
|
||||||
# Environment (same as Nuitka script)
|
|
||||||
env = os.environ.copy()
|
|
||||||
env["PYTHONPATH"] = str(Path("src").resolve()) + os.pathsep + str(Path("examples").resolve()) + os.pathsep + env.get("PYTHONPATH", "")
|
|
||||||
|
|
||||||
print("\n=== Running PyInstaller ===")
|
|
||||||
print("Command:", " ".join(cmd))
|
|
||||||
subprocess.check_call(cmd, env=env)
|
|
||||||
|
|
||||||
# After build, copy data directories explicitly
|
|
||||||
# PyInstaller output folder
|
|
||||||
dist_dir = Path("dist") / DIST_NAME
|
|
||||||
final_exe = dist_dir / EXE_NAME
|
|
||||||
if not final_exe.is_file():
|
|
||||||
raise RuntimeError(f"Executable not found at {final_exe}")
|
|
||||||
print("\n=== Copying data directories ===")
|
|
||||||
for src_rel, dest_rel in DATA_DIRS:
|
|
||||||
src = repo_root / src_rel # use an absolute reference to repo root
|
|
||||||
dest = dist_dir / dest_rel
|
|
||||||
if not src.is_dir():
|
|
||||||
raise FileNotFoundError(f"Data directory not found: {src}")
|
|
||||||
if dest.exists():
|
|
||||||
shutil.rmtree(dest)
|
|
||||||
shutil.copytree(src, dest, dirs_exist_ok=True) # Python 3.8+ use dirs_exist_ok=True
|
|
||||||
print(f" {src} -> {dest}")
|
|
||||||
|
|
||||||
# Copy logo
|
|
||||||
logo_src = repo_root / "logo.png"
|
|
||||||
if logo_src.is_file():
|
|
||||||
shutil.copy2(logo_src, dist_dir / "logo.png")
|
|
||||||
print(f" Logo copied")
|
|
||||||
|
|
||||||
# Write README (same as Nuitka script)
|
|
||||||
readme = dist_dir / "README.txt"
|
|
||||||
readme.write_text(
|
|
||||||
"\n".join([
|
|
||||||
"Orisys Finger Dashboard (standalone)",
|
|
||||||
"",
|
|
||||||
"Run:",
|
|
||||||
f" {EXE_NAME}",
|
|
||||||
f" {EXE_NAME} --video path\\to\\video.mp4",
|
|
||||||
f" {EXE_NAME} --config config\\dd02-ov.json",
|
|
||||||
"",
|
|
||||||
"Bundled folders:",
|
|
||||||
" config/ sensor JSON configs",
|
|
||||||
" assets/ finger mesh and visualization data",
|
|
||||||
"",
|
|
||||||
"CPU-only build (no CUDA/CuPy).",
|
|
||||||
]),
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f"\n[SUCCESS] Standalone GUI folder: {dist_dir}")
|
|
||||||
print(f"Executable: {final_exe}")
|
|
||||||
|
|
||||||
# Optional smoke test (same as Nuitka script)
|
|
||||||
try:
|
|
||||||
from verify import verify_gui_dist
|
|
||||||
print("\n=== Smoke test ===")
|
|
||||||
verify_gui_dist(dist_dir)
|
|
||||||
except Exception as exc:
|
|
||||||
print(f"\n[WARNING] Smoke test failed: {exc}")
|
|
||||||
|
|
||||||
return dist_dir
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
build_with_pyinstaller()
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Shared build utilities used by wheel and GUI packaging targets."""
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
"""Ensure ``build/`` is importable as the root for ``common``, ``wheel``, and ``gui``."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_BUILD_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_build_root_on_path() -> Path:
|
||||||
|
root = str(_BUILD_ROOT)
|
||||||
|
if root not in sys.path:
|
||||||
|
sys.path.insert(0, root)
|
||||||
|
return _BUILD_ROOT
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
"""Repository and Nuitka build output paths."""
|
"""Repository and packaging output paths."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
# build/common/paths.py → parents: common, build, repo
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
NUITKA_COMPILE_DIR = REPO_ROOT / "build" / "nuitka" / "compile"
|
NUITKA_COMPILE_DIR = REPO_ROOT / "build" / "nuitka" / "compile"
|
||||||
NUITKA_WHEEL_DIR = REPO_ROOT / "build" / "nuitka" / "wheel"
|
NUITKA_WHEEL_DIR = REPO_ROOT / "build" / "nuitka" / "wheel"
|
||||||
NUITKA_GUI_DIR = REPO_ROOT / "build" / "nuitka" / "gui"
|
NUITKA_GUI_DIR = REPO_ROOT / "build" / "nuitka" / "gui"
|
||||||
GUI_DIST_DIR = REPO_ROOT / "dist" / "OrisysDashboard"
|
GUI_DIST_DIR = REPO_ROOT / "dist" / "OrisysDashboard"
|
||||||
|
PYINSTALLER_WORK_DIR = REPO_ROOT / "build" / "pyinstaller_work"
|
||||||
|
|
||||||
|
|
||||||
def repo_path(*parts: str) -> Path:
|
def repo_path(*parts: str) -> Path:
|
||||||
@@ -23,3 +25,7 @@ def sdk_dir(version: str) -> Path:
|
|||||||
def repaired_wheel_dir() -> Path:
|
def repaired_wheel_dir() -> Path:
|
||||||
"""Directory for auditwheel-repaired wheels under the Nuitka wheel build tree."""
|
"""Directory for auditwheel-repaired wheels under the Nuitka wheel build tree."""
|
||||||
return NUITKA_WHEEL_DIR / "repaired"
|
return NUITKA_WHEEL_DIR / "repaired"
|
||||||
|
|
||||||
|
|
||||||
|
def gui_dist_dir(dist_name: str) -> Path:
|
||||||
|
return REPO_ROOT / "dist" / dist_name
|
||||||
@@ -3,10 +3,24 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import ast
|
import ast
|
||||||
|
import importlib.util
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
_BUILD_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def _load_wheel_layout_module():
|
||||||
|
layout_path = _BUILD_ROOT / "wheel" / "layout.py"
|
||||||
|
spec = importlib.util.spec_from_file_location("_orisys_build_wheel_layout", layout_path)
|
||||||
|
if spec is None or spec.loader is None:
|
||||||
|
raise ImportError(f"Cannot load wheel layout helper from {layout_path}")
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
def _load_toml(text: str) -> dict:
|
def _load_toml(text: str) -> dict:
|
||||||
if sys.version_info >= (3, 11):
|
if sys.version_info >= (3, 11):
|
||||||
@@ -89,8 +103,12 @@ def bundle_items(repo_root: Path) -> list[dict]:
|
|||||||
|
|
||||||
|
|
||||||
def bundle_data_files(repo_root: Path) -> list[tuple[str, list[str]]]:
|
def bundle_data_files(repo_root: Path) -> list[tuple[str, list[str]]]:
|
||||||
"""Map bundle items marked dev_install=true to setuptools data_files entries."""
|
"""Map bundle items marked ``dev_install=true`` to install destinations.
|
||||||
from wheel_layout import resolve_bundle_source_files
|
|
||||||
|
Kept for tests / tooling. Prefer :func:`stage_dev_bundle_into` for pip
|
||||||
|
installs so files land inside ``site-packages/orisys/``.
|
||||||
|
"""
|
||||||
|
resolve_bundle_source_files = _load_wheel_layout_module().resolve_bundle_source_files
|
||||||
|
|
||||||
grouped: dict[str, list[str]] = {}
|
grouped: dict[str, list[str]] = {}
|
||||||
for item in bundle_items(repo_root):
|
for item in bundle_items(repo_root):
|
||||||
@@ -110,3 +128,33 @@ def bundle_data_files(repo_root: Path) -> list[tuple[str, list[str]]]:
|
|||||||
grouped.setdefault(dest, []).append(rel_src)
|
grouped.setdefault(dest, []).append(rel_src)
|
||||||
|
|
||||||
return [(dest, sorted(files)) for dest, files in sorted(grouped.items())]
|
return [(dest, sorted(files)) for dest, files in sorted(grouped.items())]
|
||||||
|
|
||||||
|
|
||||||
|
def stage_dev_bundle_into(repo_root: Path, pkg_dir: Path) -> list[str]:
|
||||||
|
"""Copy ``dev_install`` bundle files under ``pkg_dir`` (the orisys package).
|
||||||
|
|
||||||
|
Returns package-relative paths that were written (e.g. ``sensors/box/...``).
|
||||||
|
"""
|
||||||
|
resolve_bundle_source_files = _load_wheel_layout_module().resolve_bundle_source_files
|
||||||
|
packaged: list[str] = []
|
||||||
|
|
||||||
|
for item in bundle_items(repo_root):
|
||||||
|
if not item.get("dev_install", False):
|
||||||
|
continue
|
||||||
|
|
||||||
|
src_dir = repo_root / item["source"]
|
||||||
|
dst_dir = pkg_dir / item["target"]
|
||||||
|
dst_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
for src_file in resolve_bundle_source_files(repo_root, item):
|
||||||
|
if item.get("preserve_tree", False):
|
||||||
|
rel = src_file.relative_to(src_dir)
|
||||||
|
dst_file = dst_dir / rel
|
||||||
|
else:
|
||||||
|
rel = Path(src_file.name)
|
||||||
|
dst_file = dst_dir / src_file.name
|
||||||
|
dst_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy2(src_file, dst_file)
|
||||||
|
packaged.append(f"{item['target']}/{rel.as_posix()}")
|
||||||
|
|
||||||
|
return packaged
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
"""Post-build smoke tests for wheels and product GUI folders."""
|
||||||
|
|
||||||
|
from common.verify.gui import GUI_PRODUCTS, verify_gui_dist
|
||||||
|
from common.verify.wheel import verify_built_wheel
|
||||||
|
|
||||||
|
__all__ = ["GUI_PRODUCTS", "verify_built_wheel", "verify_gui_dist"]
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""CLI for packaged artifact verification."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from common.verify import GUI_PRODUCTS, verify_built_wheel, verify_gui_dist
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Verify packaged Orisys artifacts.")
|
||||||
|
parser.add_argument("--gui-dist", action="store_true", help="Verify a GUI dist folder")
|
||||||
|
parser.add_argument(
|
||||||
|
"--product",
|
||||||
|
choices=sorted(GUI_PRODUCTS),
|
||||||
|
default="ory_finger",
|
||||||
|
help="GUI product profile (default: ory_finger)",
|
||||||
|
)
|
||||||
|
parser.add_argument("--dist-dir", type=Path, default=None, help="Override GUI dist folder")
|
||||||
|
parser.add_argument("wheel", nargs="?", type=Path, help="Wheel path for import smoke test")
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
if args.gui_dist:
|
||||||
|
verify_gui_dist(args.dist_dir, product=args.product)
|
||||||
|
return 0
|
||||||
|
if args.wheel is None:
|
||||||
|
parser.error("Provide a wheel path or pass --gui-dist")
|
||||||
|
verify_built_wheel(args.wheel)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"""Smoke tests for standalone GUI product folders."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_BUILD_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
if str(_BUILD_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(_BUILD_ROOT))
|
||||||
|
|
||||||
|
from common.paths import GUI_DIST_DIR, gui_dist_dir
|
||||||
|
|
||||||
|
# Product profiles for GUI packaging verification.
|
||||||
|
GUI_PRODUCTS: dict[str, dict] = {
|
||||||
|
"ory_finger": {
|
||||||
|
"dist_name": "OrisysDashboard",
|
||||||
|
"exe_name": "OrisysDashboard.exe",
|
||||||
|
"required_relpaths": [
|
||||||
|
"config/dd02-ov.json",
|
||||||
|
"sensors/finger/assets/30fps-0703/mesh.npz",
|
||||||
|
"sensors/finger/assets/30fps-0703/world_to_cam.npy",
|
||||||
|
"sensors/finger/assets/visualization/world_to_cam.npy",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"ory_mini": {
|
||||||
|
"dist_name": "OryMini",
|
||||||
|
"exe_name": "OryMini.exe",
|
||||||
|
"required_relpaths": [
|
||||||
|
"config/mini.json",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"ory_box": {
|
||||||
|
"dist_name": "OryBox",
|
||||||
|
"exe_name": "OryBox.exe",
|
||||||
|
"required_relpaths": [
|
||||||
|
"config/ddjx01.json",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def verify_gui_dist(
|
||||||
|
dist_dir: Path | None = None,
|
||||||
|
*,
|
||||||
|
product: str = "ory_finger",
|
||||||
|
) -> None:
|
||||||
|
"""Smoke-test a standalone GUI folder for the given product."""
|
||||||
|
if product not in GUI_PRODUCTS:
|
||||||
|
raise ValueError(f"Unknown GUI product {product!r}; choose from {sorted(GUI_PRODUCTS)}")
|
||||||
|
|
||||||
|
profile = GUI_PRODUCTS[product]
|
||||||
|
if dist_dir is None:
|
||||||
|
dist_dir = gui_dist_dir(profile["dist_name"]) if product != "ory_finger" else GUI_DIST_DIR
|
||||||
|
dist_dir = dist_dir.resolve()
|
||||||
|
|
||||||
|
exe = dist_dir / profile["exe_name"]
|
||||||
|
if not exe.is_file():
|
||||||
|
raise FileNotFoundError(f"GUI executable not found: {exe}")
|
||||||
|
|
||||||
|
required = [dist_dir / rel for rel in profile["required_relpaths"]]
|
||||||
|
missing = [str(path.relative_to(dist_dir)) for path in required if not path.is_file()]
|
||||||
|
if missing:
|
||||||
|
raise FileNotFoundError(f"Missing bundled files in {dist_dir}: {', '.join(missing)}")
|
||||||
|
|
||||||
|
subprocess.check_call([str(exe), "--help"], cwd=dist_dir)
|
||||||
|
print(f"GUI dist smoke test OK ({product}): {exe}")
|
||||||
@@ -1,16 +1,13 @@
|
|||||||
"""Post-build smoke tests for packaged wheels and standalone GUI builds."""
|
"""Isolated venv smoke tests for packaged orisys wheels."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import venv
|
import venv
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from paths import GUI_DIST_DIR
|
|
||||||
|
|
||||||
# Public SDK surface that must import from a released wheel.
|
# Public SDK surface that must import from a released wheel.
|
||||||
_SMOKE_IMPORT_SCRIPT = """
|
_SMOKE_IMPORT_SCRIPT = """
|
||||||
import orisys
|
import orisys
|
||||||
@@ -20,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
|
||||||
@@ -34,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")
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -59,42 +60,3 @@ def verify_built_wheel(wheel_path: Path) -> None:
|
|||||||
[python, "-m", "pip", "install", "-q", str(wheel_path)],
|
[python, "-m", "pip", "install", "-q", str(wheel_path)],
|
||||||
)
|
)
|
||||||
subprocess.check_call([python, "-c", _SMOKE_IMPORT_SCRIPT])
|
subprocess.check_call([python, "-c", _SMOKE_IMPORT_SCRIPT])
|
||||||
|
|
||||||
|
|
||||||
def verify_gui_dist(dist_dir: Path | None = None) -> None:
|
|
||||||
"""Smoke-test a standalone GUI folder produced by build_gui_win.py."""
|
|
||||||
dist_dir = (dist_dir or GUI_DIST_DIR).resolve()
|
|
||||||
exe = dist_dir / "OrisysDashboard.exe"
|
|
||||||
if not exe.is_file():
|
|
||||||
raise FileNotFoundError(f"GUI executable not found: {exe}")
|
|
||||||
|
|
||||||
required = [
|
|
||||||
dist_dir / "config" / "dd02-ov.json",
|
|
||||||
dist_dir / "assets" / "finger" / "visualization" / "world_to_cam.npy",
|
|
||||||
dist_dir / "assets" / "finger" / "30fps-0703" / "mesh.npz",
|
|
||||||
]
|
|
||||||
missing = [str(path.relative_to(dist_dir)) for path in required if not path.is_file()]
|
|
||||||
if missing:
|
|
||||||
raise FileNotFoundError(f"Missing bundled files in {dist_dir}: {', '.join(missing)}")
|
|
||||||
|
|
||||||
subprocess.check_call([str(exe), "--help"], cwd=dist_dir)
|
|
||||||
print(f"GUI dist smoke test OK: {exe}")
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
parser = argparse.ArgumentParser(description="Verify packaged Orisys artifacts.")
|
|
||||||
parser.add_argument("--gui-dist", action="store_true", help="Verify dist/OrisysDashboard/")
|
|
||||||
parser.add_argument("--dist-dir", type=Path, default=None, help="Override GUI dist folder")
|
|
||||||
parser.add_argument("wheel", nargs="?", type=Path, help="Wheel path for import smoke test")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if args.gui_dist:
|
|
||||||
verify_gui_dist(args.dist_dir)
|
|
||||||
return
|
|
||||||
if args.wheel is None:
|
|
||||||
parser.error("Provide a wheel path or pass --gui-dist")
|
|
||||||
verify_built_wheel(args.wheel)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Standalone GUI packaging targets (PyInstaller)."""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Shared helpers for product GUI builds."""
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
"""Shared PyInstaller driver for product GUI standalone folders."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
_BUILD_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
if str(_BUILD_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(_BUILD_ROOT))
|
||||||
|
|
||||||
|
from common.paths import PYINSTALLER_WORK_DIR, REPO_ROOT, gui_dist_dir
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GuiBuildConfig:
|
||||||
|
product: str
|
||||||
|
entry_script: Path
|
||||||
|
exe_name: str
|
||||||
|
dist_name: str
|
||||||
|
hidden_imports: list[str] = field(default_factory=list)
|
||||||
|
exclude_modules: list[str] = field(default_factory=list)
|
||||||
|
data_dirs: list[tuple[str, str]] = field(default_factory=list)
|
||||||
|
readme_lines: list[str] = field(default_factory=list)
|
||||||
|
windowed: bool = True
|
||||||
|
upx_dir: str | None = r"C:\tools\upx"
|
||||||
|
logo_name: str = "logo.png"
|
||||||
|
work_dir: Path = PYINSTALLER_WORK_DIR
|
||||||
|
spec_dir: Path = REPO_ROOT / "build" / "pyinstaller_work"
|
||||||
|
|
||||||
|
|
||||||
|
def clean_dir(path: Path) -> None:
|
||||||
|
if path.exists():
|
||||||
|
shutil.rmtree(path)
|
||||||
|
path.mkdir(parents=True)
|
||||||
|
|
||||||
|
|
||||||
|
def build_with_pyinstaller(
|
||||||
|
config: GuiBuildConfig,
|
||||||
|
*,
|
||||||
|
verify_fn: Callable[[Path], None] | None = None,
|
||||||
|
) -> Path:
|
||||||
|
"""Run PyInstaller for one GUI product and copy bundled data folders."""
|
||||||
|
print("Using Python:", sys.executable)
|
||||||
|
print(f"Building GUI product: {config.product}")
|
||||||
|
|
||||||
|
dist_dir = gui_dist_dir(config.dist_name)
|
||||||
|
clean_dir(config.work_dir)
|
||||||
|
clean_dir(dist_dir)
|
||||||
|
config.spec_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
entry = config.entry_script
|
||||||
|
if not entry.is_file():
|
||||||
|
raise FileNotFoundError(f"GUI entry script not found: {entry}")
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
sys.executable,
|
||||||
|
"-m",
|
||||||
|
"PyInstaller",
|
||||||
|
"--onedir",
|
||||||
|
"--name",
|
||||||
|
config.dist_name,
|
||||||
|
f"--distpath={dist_dir.parent}",
|
||||||
|
f"--workpath={config.work_dir}",
|
||||||
|
f"--specpath={config.spec_dir}",
|
||||||
|
"--noconfirm",
|
||||||
|
"--clean",
|
||||||
|
"--windowed" if config.windowed else "--console",
|
||||||
|
]
|
||||||
|
|
||||||
|
for src_rel, dest_rel in config.data_dirs:
|
||||||
|
src = (REPO_ROOT / src_rel).resolve()
|
||||||
|
if not src.is_dir():
|
||||||
|
raise FileNotFoundError(f"Data directory missing: {src}")
|
||||||
|
cmd.append(f"--add-data={src};{dest_rel}")
|
||||||
|
|
||||||
|
logo = REPO_ROOT / config.logo_name
|
||||||
|
if logo.is_file():
|
||||||
|
cmd.append(f"--add-data={logo.resolve()};.")
|
||||||
|
|
||||||
|
for mod in config.hidden_imports:
|
||||||
|
cmd.append(f"--hidden-import={mod}")
|
||||||
|
for mod in config.exclude_modules:
|
||||||
|
cmd.append(f"--exclude-module={mod}")
|
||||||
|
|
||||||
|
if config.upx_dir and Path(config.upx_dir).is_dir():
|
||||||
|
cmd.append(f"--upx-dir={config.upx_dir}")
|
||||||
|
|
||||||
|
cmd.append(str(entry.resolve()))
|
||||||
|
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["PYTHONPATH"] = (
|
||||||
|
str((REPO_ROOT / "src").resolve())
|
||||||
|
+ os.pathsep
|
||||||
|
+ str((REPO_ROOT / "examples").resolve())
|
||||||
|
+ os.pathsep
|
||||||
|
+ env.get("PYTHONPATH", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\n=== Running PyInstaller ===")
|
||||||
|
print("Command:", " ".join(cmd))
|
||||||
|
subprocess.check_call(cmd, env=env, cwd=REPO_ROOT)
|
||||||
|
|
||||||
|
final_exe = dist_dir / config.exe_name
|
||||||
|
if not final_exe.is_file():
|
||||||
|
raise RuntimeError(f"Executable not found at {final_exe}")
|
||||||
|
|
||||||
|
print("\n=== Copying data directories ===")
|
||||||
|
for src_rel, dest_rel in config.data_dirs:
|
||||||
|
src = REPO_ROOT / src_rel
|
||||||
|
dest = dist_dir / dest_rel
|
||||||
|
if not src.is_dir():
|
||||||
|
raise FileNotFoundError(f"Data directory not found: {src}")
|
||||||
|
if dest.exists():
|
||||||
|
shutil.rmtree(dest)
|
||||||
|
shutil.copytree(src, dest, dirs_exist_ok=True)
|
||||||
|
print(f" {src} -> {dest}")
|
||||||
|
|
||||||
|
if logo.is_file():
|
||||||
|
shutil.copy2(logo, dist_dir / config.logo_name)
|
||||||
|
print(" Logo copied")
|
||||||
|
|
||||||
|
if config.readme_lines:
|
||||||
|
(dist_dir / "README.txt").write_text("\n".join(config.readme_lines) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
print(f"\n[SUCCESS] Standalone GUI folder: {dist_dir}")
|
||||||
|
print(f"Executable: {final_exe}")
|
||||||
|
|
||||||
|
if verify_fn is not None:
|
||||||
|
try:
|
||||||
|
print("\n=== Smoke test ===")
|
||||||
|
verify_fn(dist_dir)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"\n[WARNING] Smoke test failed: {exc}")
|
||||||
|
|
||||||
|
return dist_dir
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""ORY-BOX internal calibration/testing GUI packaging (scaffold)."""
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""Scaffold for ORY-BOX internal GUI packaging.
|
||||||
|
|
||||||
|
Not implemented yet. When ready, wire a dedicated calibration/testing entry
|
||||||
|
script and call ``gui.common.pyinstaller_driver.build_with_pyinstaller``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_BUILD_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
if str(_BUILD_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(_BUILD_ROOT))
|
||||||
|
|
||||||
|
from gui.ory_box.config import DIST_NAME, ENTRY_SCRIPT, make_config
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
config = make_config()
|
||||||
|
raise NotImplementedError(
|
||||||
|
"ORY-BOX GUI packaging is a scaffold only. "
|
||||||
|
f"Planned dist={DIST_NAME!r}, entry={ENTRY_SCRIPT}. "
|
||||||
|
"Implement the internal calibration/testing UI entry, then call "
|
||||||
|
"gui.common.pyinstaller_driver.build_with_pyinstaller(config)."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
"""Product configuration scaffold for the ORY-BOX internal GUI."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_BUILD_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
if str(_BUILD_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(_BUILD_ROOT))
|
||||||
|
|
||||||
|
from common.paths import REPO_ROOT
|
||||||
|
from gui.common.pyinstaller_driver import GuiBuildConfig
|
||||||
|
|
||||||
|
ENTRY_SCRIPT = REPO_ROOT / "examples" / "basic_pipeline.py"
|
||||||
|
EXE_NAME = "OryBox.exe"
|
||||||
|
DIST_NAME = "OryBox"
|
||||||
|
|
||||||
|
HIDDEN_IMPORTS = [
|
||||||
|
"PyQt5",
|
||||||
|
"pyqtgraph",
|
||||||
|
"cv2",
|
||||||
|
"orisys",
|
||||||
|
"gui",
|
||||||
|
"pipeline",
|
||||||
|
"viz",
|
||||||
|
]
|
||||||
|
|
||||||
|
EXCLUDE_MODULES = [
|
||||||
|
"cupy",
|
||||||
|
"cupyx",
|
||||||
|
"pyopencl",
|
||||||
|
"open3d",
|
||||||
|
"tkinter",
|
||||||
|
"pip",
|
||||||
|
"wheel",
|
||||||
|
]
|
||||||
|
|
||||||
|
DATA_DIRS = [
|
||||||
|
("config", "config"),
|
||||||
|
]
|
||||||
|
|
||||||
|
README_LINES = [
|
||||||
|
"ORY-BOX internal calibration/testing GUI (scaffold)",
|
||||||
|
"",
|
||||||
|
"This product packaging target is reserved for a future internal tool.",
|
||||||
|
"Entry point placeholder:",
|
||||||
|
f" {ENTRY_SCRIPT.as_posix()}",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def make_config() -> GuiBuildConfig:
|
||||||
|
return GuiBuildConfig(
|
||||||
|
product="ory_box",
|
||||||
|
entry_script=ENTRY_SCRIPT,
|
||||||
|
exe_name=EXE_NAME,
|
||||||
|
dist_name=DIST_NAME,
|
||||||
|
hidden_imports=list(HIDDEN_IMPORTS),
|
||||||
|
exclude_modules=list(EXCLUDE_MODULES),
|
||||||
|
data_dirs=list(DATA_DIRS),
|
||||||
|
readme_lines=list(README_LINES),
|
||||||
|
windowed=False,
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""ORY-FINGER standalone GUI packaging."""
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"""Build the ORY-FINGER standalone Windows GUI folder with PyInstaller.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python build/run.py gui finger
|
||||||
|
python build/gui/ory_finger/build.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_BUILD_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
if str(_BUILD_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(_BUILD_ROOT))
|
||||||
|
|
||||||
|
from common.verify.gui import verify_gui_dist
|
||||||
|
from gui.common.pyinstaller_driver import build_with_pyinstaller
|
||||||
|
from gui.ory_finger.config import make_config
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
config = make_config()
|
||||||
|
build_with_pyinstaller(
|
||||||
|
config,
|
||||||
|
verify_fn=lambda dist_dir: verify_gui_dist(dist_dir, product="ory_finger"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
"""Product configuration for the ORY-FINGER Windows dashboard."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_BUILD_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
if str(_BUILD_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(_BUILD_ROOT))
|
||||||
|
|
||||||
|
from common.paths import REPO_ROOT
|
||||||
|
from gui.common.pyinstaller_driver import GuiBuildConfig
|
||||||
|
|
||||||
|
ENTRY_SCRIPT = REPO_ROOT / "examples" / "finger_pipeline_3dview.py"
|
||||||
|
EXE_NAME = "OrisysDashboard.exe"
|
||||||
|
DIST_NAME = "OrisysDashboard"
|
||||||
|
|
||||||
|
HIDDEN_IMPORTS = [
|
||||||
|
"PyQt5",
|
||||||
|
"pyqtgraph",
|
||||||
|
"open3d",
|
||||||
|
"cv2",
|
||||||
|
"orisys",
|
||||||
|
"gui",
|
||||||
|
"pipeline",
|
||||||
|
"viz",
|
||||||
|
"gui.app_paths",
|
||||||
|
"plotly",
|
||||||
|
"_plotly_utils",
|
||||||
|
"open3d.cpu.pybind",
|
||||||
|
]
|
||||||
|
|
||||||
|
EXCLUDE_MODULES = [
|
||||||
|
"cupy",
|
||||||
|
"cupyx",
|
||||||
|
"pyopencl",
|
||||||
|
"tkinter",
|
||||||
|
"pip",
|
||||||
|
"wheel",
|
||||||
|
"matplotlib",
|
||||||
|
]
|
||||||
|
|
||||||
|
DATA_DIRS = [
|
||||||
|
("config", "config"),
|
||||||
|
("sensors", "sensors"),
|
||||||
|
]
|
||||||
|
|
||||||
|
README_LINES = [
|
||||||
|
"Orisys Finger Dashboard (standalone)",
|
||||||
|
"",
|
||||||
|
"Run:",
|
||||||
|
f" {EXE_NAME}",
|
||||||
|
f" {EXE_NAME} --video path\\to\\video.mp4",
|
||||||
|
f" {EXE_NAME} --config config\\dd02-ov.json",
|
||||||
|
"",
|
||||||
|
"Bundled folders:",
|
||||||
|
" config/ sensor JSON configs",
|
||||||
|
" sensors/ per-sensor bundles (finger mesh and visualization data)",
|
||||||
|
"",
|
||||||
|
"CPU-only build (no CUDA/CuPy).",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def make_config() -> GuiBuildConfig:
|
||||||
|
return GuiBuildConfig(
|
||||||
|
product="ory_finger",
|
||||||
|
entry_script=ENTRY_SCRIPT,
|
||||||
|
exe_name=EXE_NAME,
|
||||||
|
dist_name=DIST_NAME,
|
||||||
|
hidden_imports=list(HIDDEN_IMPORTS),
|
||||||
|
exclude_modules=list(EXCLUDE_MODULES),
|
||||||
|
data_dirs=list(DATA_DIRS),
|
||||||
|
readme_lines=list(README_LINES),
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""ORY-MINI internal calibration/testing GUI packaging (scaffold)."""
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""Scaffold for ORY-MINI internal GUI packaging.
|
||||||
|
|
||||||
|
Not implemented yet. When ready, wire a dedicated calibration/testing entry
|
||||||
|
script and call ``gui.common.pyinstaller_driver.build_with_pyinstaller``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_BUILD_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
if str(_BUILD_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(_BUILD_ROOT))
|
||||||
|
|
||||||
|
from gui.ory_mini.config import DIST_NAME, ENTRY_SCRIPT, make_config
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
config = make_config()
|
||||||
|
raise NotImplementedError(
|
||||||
|
"ORY-MINI GUI packaging is a scaffold only. "
|
||||||
|
f"Planned dist={DIST_NAME!r}, entry={ENTRY_SCRIPT}. "
|
||||||
|
"Implement the internal calibration/testing UI entry, then call "
|
||||||
|
"gui.common.pyinstaller_driver.build_with_pyinstaller(config)."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
"""Product configuration scaffold for the ORY-MINI internal GUI."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_BUILD_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
if str(_BUILD_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(_BUILD_ROOT))
|
||||||
|
|
||||||
|
from common.paths import REPO_ROOT
|
||||||
|
from gui.common.pyinstaller_driver import GuiBuildConfig
|
||||||
|
|
||||||
|
ENTRY_SCRIPT = REPO_ROOT / "examples" / "mini_pipeline.py"
|
||||||
|
EXE_NAME = "OryMini.exe"
|
||||||
|
DIST_NAME = "OryMini"
|
||||||
|
|
||||||
|
HIDDEN_IMPORTS = [
|
||||||
|
"PyQt5",
|
||||||
|
"pyqtgraph",
|
||||||
|
"cv2",
|
||||||
|
"orisys",
|
||||||
|
"gui",
|
||||||
|
"pipeline",
|
||||||
|
"viz",
|
||||||
|
]
|
||||||
|
|
||||||
|
EXCLUDE_MODULES = [
|
||||||
|
"cupy",
|
||||||
|
"cupyx",
|
||||||
|
"pyopencl",
|
||||||
|
"open3d",
|
||||||
|
"tkinter",
|
||||||
|
"pip",
|
||||||
|
"wheel",
|
||||||
|
]
|
||||||
|
|
||||||
|
DATA_DIRS = [
|
||||||
|
("config", "config"),
|
||||||
|
]
|
||||||
|
|
||||||
|
README_LINES = [
|
||||||
|
"ORY-MINI internal calibration/testing GUI (scaffold)",
|
||||||
|
"",
|
||||||
|
"This product packaging target is reserved for a future internal tool.",
|
||||||
|
"Entry point placeholder:",
|
||||||
|
f" {ENTRY_SCRIPT.as_posix()}",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def make_config() -> GuiBuildConfig:
|
||||||
|
return GuiBuildConfig(
|
||||||
|
product="ory_mini",
|
||||||
|
entry_script=ENTRY_SCRIPT,
|
||||||
|
exe_name=EXE_NAME,
|
||||||
|
dist_name=DIST_NAME,
|
||||||
|
hidden_imports=list(HIDDEN_IMPORTS),
|
||||||
|
exclude_modules=list(EXCLUDE_MODULES),
|
||||||
|
data_dirs=list(DATA_DIRS),
|
||||||
|
readme_lines=list(README_LINES),
|
||||||
|
windowed=False,
|
||||||
|
)
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
"""Unified Orisys packaging entrypoint.
|
||||||
|
|
||||||
|
Usage (from repo root):
|
||||||
|
|
||||||
|
python build/run.py wheel [--target internal|pypi-linux] [--verify]
|
||||||
|
python build/run.py wheel-win
|
||||||
|
python build/run.py wheel-manylinux [PowerShell args...]
|
||||||
|
python build/run.py gui finger|mini|box
|
||||||
|
python build/run.py verify [wheel.whl]
|
||||||
|
python build/run.py verify --gui-dist [--product ory_finger]
|
||||||
|
|
||||||
|
See build/README.md for details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_BUILD_ROOT = Path(__file__).resolve().parent
|
||||||
|
_REPO_ROOT = _BUILD_ROOT.parent
|
||||||
|
if str(_BUILD_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(_BUILD_ROOT))
|
||||||
|
|
||||||
|
|
||||||
|
def _run_python(script: Path, argv: list[str]) -> int:
|
||||||
|
cmd = [sys.executable, str(script), *argv]
|
||||||
|
return subprocess.call(cmd, cwd=_REPO_ROOT)
|
||||||
|
|
||||||
|
|
||||||
|
def _cmd_wheel(argv: list[str]) -> int:
|
||||||
|
return _run_python(_BUILD_ROOT / "wheel" / "build.py", argv)
|
||||||
|
|
||||||
|
|
||||||
|
def _cmd_wheel_win(argv: list[str]) -> int:
|
||||||
|
bat = _BUILD_ROOT / "wheel" / "win.bat"
|
||||||
|
return subprocess.call(["cmd", "/c", str(bat), *argv], cwd=_REPO_ROOT)
|
||||||
|
|
||||||
|
|
||||||
|
def _cmd_manylinux(argv: list[str]) -> int:
|
||||||
|
ps1 = _BUILD_ROOT / "wheel" / "docker" / "run.ps1"
|
||||||
|
cmd = [
|
||||||
|
"powershell",
|
||||||
|
"-ExecutionPolicy",
|
||||||
|
"Bypass",
|
||||||
|
"-File",
|
||||||
|
str(ps1),
|
||||||
|
*argv,
|
||||||
|
]
|
||||||
|
return subprocess.call(cmd, cwd=_REPO_ROOT)
|
||||||
|
|
||||||
|
|
||||||
|
def _cmd_gui(argv: list[str]) -> int:
|
||||||
|
parser = argparse.ArgumentParser(prog="build/run.py gui")
|
||||||
|
parser.add_argument(
|
||||||
|
"product",
|
||||||
|
choices=("finger", "mini", "box", "ory_finger", "ory_mini", "ory_box"),
|
||||||
|
help="GUI product to package",
|
||||||
|
)
|
||||||
|
args, rest = parser.parse_known_args(argv)
|
||||||
|
product = {
|
||||||
|
"finger": "ory_finger",
|
||||||
|
"mini": "ory_mini",
|
||||||
|
"box": "ory_box",
|
||||||
|
}.get(args.product, args.product)
|
||||||
|
return _run_python(_BUILD_ROOT / "gui" / product / "build.py", rest)
|
||||||
|
|
||||||
|
|
||||||
|
def _cmd_verify(argv: list[str]) -> int:
|
||||||
|
from common.verify.cli import main as verify_main
|
||||||
|
|
||||||
|
return verify_main(argv)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
if not argv or argv[0] in ("-h", "--help"):
|
||||||
|
print(__doc__)
|
||||||
|
print("Commands: wheel | wheel-win | manylinux | gui | verify")
|
||||||
|
print("Examples:")
|
||||||
|
print(" python build/run.py wheel --help")
|
||||||
|
print(" python build/run.py gui finger")
|
||||||
|
print(" python build/run.py manylinux -PythonVersions 3.10")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
command, rest = argv[0], argv[1:]
|
||||||
|
dispatch = {
|
||||||
|
"wheel": _cmd_wheel,
|
||||||
|
"wheel-win": _cmd_wheel_win,
|
||||||
|
"wheel-manylinux": _cmd_manylinux,
|
||||||
|
"gui": _cmd_gui,
|
||||||
|
"verify": _cmd_verify,
|
||||||
|
}
|
||||||
|
if command not in dispatch:
|
||||||
|
print(f"Unknown command: {command!r}", file=sys.stderr)
|
||||||
|
print("Choose from: wheel, wheel-win, manylinux, gui, verify", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
return dispatch[command](rest)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Nuitka-compiled SDK wheel packaging."""
|
||||||
@@ -11,8 +11,9 @@ Targets:
|
|||||||
pypi-linux – PyPI binary wheel; requires Linux + CPython 3.10/3.11/3.12 + compliant tag.
|
pypi-linux – PyPI binary wheel; requires Linux + CPython 3.10/3.11/3.12 + compliant tag.
|
||||||
|
|
||||||
Run from repo root:
|
Run from repo root:
|
||||||
python build/build_nuitka.py
|
python build/run.py wheel
|
||||||
python build/build_nuitka.py --target pypi-linux
|
python build/run.py wheel --target pypi-linux
|
||||||
|
python build/wheel/build.py --target pypi-linux
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -24,18 +25,26 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from nuitka_compile import compile_orisys, copy_compiled_orisys, install_nuitka
|
_BUILD_ROOT = Path(__file__).resolve().parents[1]
|
||||||
from paths import NUITKA_COMPILE_DIR, NUITKA_WHEEL_DIR, REPO_ROOT, repaired_wheel_dir, repo_path
|
if str(_BUILD_ROOT) not in sys.path:
|
||||||
from pyproject_utils import bundle_items, extras_require, get_version, install_requires
|
sys.path.insert(0, str(_BUILD_ROOT))
|
||||||
from verify import verify_built_wheel
|
|
||||||
from wheel_build import (
|
from common.paths import NUITKA_COMPILE_DIR, NUITKA_WHEEL_DIR, REPO_ROOT, repaired_wheel_dir, repo_path
|
||||||
|
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.package import (
|
||||||
is_compliant_manylinux_wheel,
|
is_compliant_manylinux_wheel,
|
||||||
publish_wheel_to_sdk,
|
publish_wheel_to_sdk,
|
||||||
repair_linux_wheel,
|
repair_linux_wheel,
|
||||||
run_bdist_wheel,
|
run_bdist_wheel,
|
||||||
write_wheel_setup,
|
write_wheel_setup,
|
||||||
)
|
)
|
||||||
from wheel_layout import copy_bundle_items, copy_orisys_blend, package_data_from_bundle
|
|
||||||
|
|
||||||
VALID_TARGETS = ("internal", "pypi-linux")
|
VALID_TARGETS = ("internal", "pypi-linux")
|
||||||
# ABI tags that manylinux images ship under /opt/python/cpXYY-cpXYY.
|
# ABI tags that manylinux images ship under /opt/python/cpXYY-cpXYY.
|
||||||
@@ -105,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,
|
||||||
@@ -8,11 +8,18 @@ import sys
|
|||||||
from importlib.machinery import EXTENSION_SUFFIXES
|
from importlib.machinery import EXTENSION_SUFFIXES
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from paths import repo_path
|
_BUILD_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
if str(_BUILD_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(_BUILD_ROOT))
|
||||||
|
|
||||||
|
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",
|
||||||
@@ -23,4 +23,4 @@ RUN for PYBIN in \
|
|||||||
done
|
done
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
ENTRYPOINT ["/bin/bash", "/src/build/manylinux_entrypoint.sh"]
|
ENTRYPOINT ["/bin/bash", "/src/build/wheel/docker/entrypoint.sh"]
|
||||||
@@ -42,7 +42,7 @@ for tag in "${TAG_ARRAY[@]}"; do
|
|||||||
|
|
||||||
"${PYBIN}/python" -m pip install -q --upgrade pip setuptools wheel auditwheel
|
"${PYBIN}/python" -m pip install -q --upgrade pip setuptools wheel auditwheel
|
||||||
|
|
||||||
if "${PYBIN}/python" build/build_nuitka.py --target "${TARGET}"; then
|
if "${PYBIN}/python" build/wheel/build.py --target "${TARGET}"; then
|
||||||
BUILT+=("${tag}")
|
BUILT+=("${tag}")
|
||||||
else
|
else
|
||||||
echo "[ERROR] Build failed for ${tag}" >&2
|
echo "[ERROR] Build failed for ${tag}" >&2
|
||||||
@@ -1,17 +1,11 @@
|
|||||||
# Build PyPI-compliant manylinux wheels for CPython 3.10/3.11/3.12 using Docker Desktop.
|
# Build PyPI-compliant manylinux wheels for CPython 3.10/3.11/3.12 using Docker Desktop.
|
||||||
#
|
#
|
||||||
# Usage (from repo root, PowerShell):
|
# Usage (from repo root, PowerShell):
|
||||||
# .\build\build_manylinux_docker.ps1
|
# .\build\wheel\docker\run.ps1
|
||||||
# .\build\build_manylinux_docker.ps1 -PythonVersions 3.10,3.11,3.12
|
# .\build\build_manylinux_docker.ps1 # compatibility wrapper
|
||||||
# .\build\build_manylinux_docker.ps1 -PythonVersions 3.11
|
|
||||||
# .\build\build_manylinux_docker.ps1 -SkipImageBuild
|
|
||||||
#
|
#
|
||||||
# From cmd.exe:
|
# From cmd.exe:
|
||||||
# powershell -ExecutionPolicy Bypass -File .\build\build_manylinux_docker.ps1
|
# powershell -ExecutionPolicy Bypass -File .\build\build_manylinux_docker.ps1
|
||||||
#
|
|
||||||
# Prerequisites:
|
|
||||||
# - Docker Desktop running in Linux container mode
|
|
||||||
# - Enough disk for the manylinux image (~2GB+)
|
|
||||||
|
|
||||||
param(
|
param(
|
||||||
[ValidateSet("pypi-linux", "internal")]
|
[ValidateSet("pypi-linux", "internal")]
|
||||||
@@ -39,7 +33,7 @@ function Convert-ToManylinuxTag {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..\..")).Path
|
||||||
Set-Location $RepoRoot
|
Set-Location $RepoRoot
|
||||||
|
|
||||||
$tags = @()
|
$tags = @()
|
||||||
@@ -67,15 +61,17 @@ if ($dockerInfo -ne "linux") {
|
|||||||
throw "Docker server OS is '$dockerInfo'. Switch Docker Desktop to Linux containers."
|
throw "Docker server OS is '$dockerInfo'. Switch Docker Desktop to Linux containers."
|
||||||
}
|
}
|
||||||
|
|
||||||
$entrypoint = Join-Path $RepoRoot "build\manylinux_entrypoint.sh"
|
$entrypoint = Join-Path $RepoRoot "build\wheel\docker\entrypoint.sh"
|
||||||
# Normalize CRLF -> LF so bash in the container can execute the script.
|
# Normalize CRLF -> LF so bash in the container can execute the script.
|
||||||
$raw = [System.IO.File]::ReadAllText($entrypoint)
|
$raw = [System.IO.File]::ReadAllText($entrypoint)
|
||||||
$normalized = $raw -replace "`r`n", "`n" -replace "`r", "`n"
|
$normalized = $raw -replace "`r`n", "`n" -replace "`r", "`n"
|
||||||
[System.IO.File]::WriteAllText($entrypoint, $normalized)
|
[System.IO.File]::WriteAllText($entrypoint, $normalized)
|
||||||
|
|
||||||
|
$dockerfile = "build/wheel/docker/Dockerfile.manylinux"
|
||||||
|
|
||||||
if (-not $SkipImageBuild) {
|
if (-not $SkipImageBuild) {
|
||||||
Write-Host "`n=== Building Docker image ==="
|
Write-Host "`n=== Building Docker image ==="
|
||||||
docker build -f build/Dockerfile.manylinux -t $ImageName .
|
docker build -f $dockerfile -t $ImageName .
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
throw "docker build failed"
|
throw "docker build failed"
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,21 @@ from __future__ import annotations
|
|||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Required files that must ship with the wheel for ORY-FINGER runtime/GUI.
|
||||||
|
# Accept both the flat layout and a nested assets/finger/ checkout tree.
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
REQUIRED_ASSET_RELPATH_ALTERNATES = {
|
||||||
|
"finger/assets/30fps-0703/mesh.npz": "finger/assets/finger/30fps-0703/mesh.npz",
|
||||||
|
"finger/assets/30fps-0703/camera_intrinsics.npz": "finger/assets/finger/30fps-0703/camera_intrinsics.npz",
|
||||||
|
"finger/assets/30fps-0703/world_to_cam.npy": "finger/assets/finger/30fps-0703/world_to_cam.npy",
|
||||||
|
"finger/assets/visualization/world_to_cam.npy": "finger/assets/finger/visualization/world_to_cam.npy",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def resolve_bundle_source_files(repo_root: Path, item: dict) -> list[Path]:
|
def resolve_bundle_source_files(repo_root: Path, item: dict) -> list[Path]:
|
||||||
"""Resolve explicit files and/or glob patterns for one bundle item."""
|
"""Resolve explicit files and/or glob patterns for one bundle item."""
|
||||||
@@ -37,6 +52,36 @@ def resolve_bundle_source_files(repo_root: Path, item: dict) -> list[Path]:
|
|||||||
return resolved
|
return resolved
|
||||||
|
|
||||||
|
|
||||||
|
def verify_required_assets(repo_root: Path, items: list[dict]) -> None:
|
||||||
|
"""Fail early when required finger assets are missing from the bundle."""
|
||||||
|
asset_items = [item for item in items if item.get("name") == "sensors"]
|
||||||
|
if not asset_items:
|
||||||
|
return
|
||||||
|
|
||||||
|
resolved: set[str] = set()
|
||||||
|
for item in asset_items:
|
||||||
|
src_dir = (repo_root / item["source"]).resolve()
|
||||||
|
for path in resolve_bundle_source_files(repo_root, item):
|
||||||
|
try:
|
||||||
|
rel = path.resolve().relative_to(src_dir).as_posix()
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
resolved.add(rel)
|
||||||
|
|
||||||
|
missing = []
|
||||||
|
for rel in REQUIRED_ASSET_RELPATHS:
|
||||||
|
alt = REQUIRED_ASSET_RELPATH_ALTERNATES.get(rel)
|
||||||
|
if rel not in resolved and (alt is None or alt not in resolved):
|
||||||
|
missing.append(rel)
|
||||||
|
if missing:
|
||||||
|
joined = ", ".join(missing)
|
||||||
|
raise FileNotFoundError(
|
||||||
|
"Required finger assets are missing from the wheel bundle. "
|
||||||
|
f"Missing: {joined}. Update [[tool.orisys.bundle.items]] patterns "
|
||||||
|
"or restore assets under sensors/finger/assets/ before building."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def package_data_paths_for_item(item: dict) -> list[str]:
|
def package_data_paths_for_item(item: dict) -> list[str]:
|
||||||
"""Return setuptools package_data paths relative to the orisys package."""
|
"""Return setuptools package_data paths relative to the orisys package."""
|
||||||
target = str(item["target"]).replace("\\", "/")
|
target = str(item["target"]).replace("\\", "/")
|
||||||
@@ -54,6 +99,7 @@ def data_file_entries_for_item(item: dict) -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
def copy_bundle_items(repo_root: Path, pkg_dir: Path, items: list[dict]) -> dict[str, int]:
|
def copy_bundle_items(repo_root: Path, pkg_dir: Path, items: list[dict]) -> dict[str, int]:
|
||||||
|
verify_required_assets(repo_root, items)
|
||||||
copied_counts: dict[str, int] = {}
|
copied_counts: dict[str, int] = {}
|
||||||
for item in items:
|
for item in items:
|
||||||
name = item["name"]
|
name = item["name"]
|
||||||
@@ -8,7 +8,11 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from paths import sdk_dir
|
_BUILD_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
if str(_BUILD_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(_BUILD_ROOT))
|
||||||
|
|
||||||
|
from common.paths import sdk_dir
|
||||||
|
|
||||||
# PyPI rejects bare tags like "manylinux_x86_64"; require a glibc ABI version.
|
# PyPI rejects bare tags like "manylinux_x86_64"; require a glibc ABI version.
|
||||||
_MANYLINUX_TAG_RE = re.compile(
|
_MANYLINUX_TAG_RE = re.compile(
|
||||||
@@ -3,7 +3,7 @@ REM Build Nuitka wheels for Python 3.10, 3.11, and 3.12.
|
|||||||
REM Expects conda envs named py310, py311, py312 (or set CONDA_ENVS_ROOT).
|
REM Expects conda envs named py310, py311, py312 (or set CONDA_ENVS_ROOT).
|
||||||
setlocal enabledelayedexpansion
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
cd /d "%~dp0\.."
|
cd /d "%~dp0\..\.."
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo ERROR: Could not change to repository root.
|
echo ERROR: Could not change to repository root.
|
||||||
exit /b 1
|
exit /b 1
|
||||||
@@ -32,7 +32,7 @@ for %%E in (py310 py311 py312) do (
|
|||||||
|
|
||||||
set "OK=0"
|
set "OK=0"
|
||||||
if !HAS_CONDA! equ 1 (
|
if !HAS_CONDA! equ 1 (
|
||||||
call conda run --no-capture-output -n %%E python build\build_nuitka.py
|
call conda run --no-capture-output -n %%E python build\wheel\build.py
|
||||||
if not errorlevel 1 set "OK=1"
|
if not errorlevel 1 set "OK=1"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ for %%E in (py310 py311 py312) do (
|
|||||||
if not exist "!PYEXE!" (
|
if not exist "!PYEXE!" (
|
||||||
echo ERROR: Python not found: !PYEXE!
|
echo ERROR: Python not found: !PYEXE!
|
||||||
) else (
|
) else (
|
||||||
"!PYEXE!" build\build_nuitka.py
|
"!PYEXE!" build\wheel\build.py
|
||||||
if not errorlevel 1 set "OK=1"
|
if not errorlevel 1 set "OK=1"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
{
|
||||||
|
"name": "dd06-ov",
|
||||||
|
"type": "single_camera",
|
||||||
|
"version": "1.0",
|
||||||
|
"camera": {
|
||||||
|
"resolution_raw": [
|
||||||
|
1280,
|
||||||
|
720
|
||||||
|
],
|
||||||
|
"fps": 30,
|
||||||
|
"resolution_stitch": [
|
||||||
|
195,
|
||||||
|
275
|
||||||
|
],
|
||||||
|
"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": 3,
|
||||||
|
"algorithm_list": [
|
||||||
|
"raft",
|
||||||
|
"dis"
|
||||||
|
],
|
||||||
|
"hhd_downsample": 3
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"min_contour_area": 3000,
|
||||||
|
"flow_threshold": 0.2,
|
||||||
|
"div_threshold": 0.022,
|
||||||
|
"expansion_threshold": 0.01,
|
||||||
|
"kernel_size": [
|
||||||
|
5,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"denoise_sigma": 12
|
||||||
|
},
|
||||||
|
"roi": {
|
||||||
|
"type": "polygon",
|
||||||
|
"image_size": [
|
||||||
|
1280,
|
||||||
|
720
|
||||||
|
],
|
||||||
|
"created_at": "2026-07-26T11:56:15",
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
242,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
[
|
||||||
|
402,
|
||||||
|
597
|
||||||
|
],
|
||||||
|
[
|
||||||
|
799,
|
||||||
|
606
|
||||||
|
],
|
||||||
|
[
|
||||||
|
976,
|
||||||
|
2
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"mask_path": "dd06-ov_mask.png",
|
||||||
|
"metadata": {
|
||||||
|
"source": "./data/dd06/0726.mp4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
{
|
||||||
|
"name": "dd06-ov",
|
||||||
|
"type": "single_camera",
|
||||||
|
"version": "1.0",
|
||||||
|
"camera": {
|
||||||
|
"resolution_raw": [
|
||||||
|
1280,
|
||||||
|
720
|
||||||
|
],
|
||||||
|
"fps": 30,
|
||||||
|
"resolution_stitch": [
|
||||||
|
195,
|
||||||
|
275
|
||||||
|
],
|
||||||
|
"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": 3,
|
||||||
|
"algorithm_list": [
|
||||||
|
"raft",
|
||||||
|
"dis"
|
||||||
|
],
|
||||||
|
"hhd_downsample": 3
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"min_contour_area": 3000,
|
||||||
|
"flow_threshold": 0.2,
|
||||||
|
"div_threshold": 0.022,
|
||||||
|
"expansion_threshold": 0.01,
|
||||||
|
"kernel_size": [
|
||||||
|
5,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"denoise_sigma": 12
|
||||||
|
},
|
||||||
|
"roi": {
|
||||||
|
"type": "polygon",
|
||||||
|
"image_size": [
|
||||||
|
1280,
|
||||||
|
720
|
||||||
|
],
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
628,
|
||||||
|
532
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1145,
|
||||||
|
694
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1161,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
630,
|
||||||
|
126
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"created_at": "2026-08-10T14:26:31",
|
||||||
|
"mask_path": "dd06-ov-0810_mask.png",
|
||||||
|
"metadata": {
|
||||||
|
"source": ".\\data\\dd06\\2026-08-10-142316.webm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -31,7 +31,7 @@
|
|||||||
"dis_preset": "medium",
|
"dis_preset": "medium",
|
||||||
"dis_mode": 3,
|
"dis_mode": 3,
|
||||||
"algorithm_list": ["raft", "dis"],
|
"algorithm_list": ["raft", "dis"],
|
||||||
"hhd_downsample": 3
|
"hhd_downsample": 1
|
||||||
},
|
},
|
||||||
"contact": {
|
"contact": {
|
||||||
"min_contour_area": 3000,
|
"min_contour_area": 3000,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
"dis_preset": "medium",
|
"dis_preset": "medium",
|
||||||
"dis_mode": 3,
|
"dis_mode": 3,
|
||||||
"algorithm_list": ["raft", "dis"],
|
"algorithm_list": ["raft", "dis"],
|
||||||
"hhd_downsample": 3,
|
"hhd_downsample": 1,
|
||||||
"backend": "cpu"
|
"backend": "cpu"
|
||||||
},
|
},
|
||||||
"contact": {
|
"contact": {
|
||||||
|
|||||||
+2
-2
@@ -5,7 +5,7 @@
|
|||||||
"camera": {
|
"camera": {
|
||||||
"resolution_raw": [1280, 720],
|
"resolution_raw": [1280, 720],
|
||||||
"fps": 30,
|
"fps": 30,
|
||||||
"resolution_stitch": [420, 450],
|
"resolution_stitch": [280, 300],
|
||||||
"source_scale": 0.4,
|
"source_scale": 0.4,
|
||||||
"old":[672, 728, 1,0.5],
|
"old":[672, 728, 1,0.5],
|
||||||
"work": [504, 546,1],
|
"work": [504, 546,1],
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
"loop_closure_interval": 1,
|
"loop_closure_interval": 1,
|
||||||
"algorithm": "dis",
|
"algorithm": "dis",
|
||||||
"dis_preset": "medium",
|
"dis_preset": "medium",
|
||||||
"dis_mode": 3,
|
"dis_mode": 1,
|
||||||
"algorithm_list": ["raft", "dis"],
|
"algorithm_list": ["raft", "dis"],
|
||||||
"hhd_downsample": 5
|
"hhd_downsample": 5
|
||||||
},
|
},
|
||||||
|
|||||||
Binary file not shown.
+187
-151
@@ -1,77 +1,30 @@
|
|||||||
# orisys API 文档
|
---
|
||||||
|
title: API Reference
|
||||||
|
description: Orisys SDK Python API 中文参考
|
||||||
|
---
|
||||||
|
|
||||||
本文件提供了 `orisys` 套件的中文文档
|
# API Reference
|
||||||
|
|
||||||
## 准备 Python 开发环境
|
本文件提供 `orisys` 套件的 Python API 中文参考。安装、CUDA、CuPy 与运行示例的基础步骤请参考 [快速开始](./get_started.md)。
|
||||||
推荐下载 Anaconda,并使用Python 3.10
|
|
||||||
```
|
|
||||||
# 创建并激活虚拟环境
|
|
||||||
conda create -n orisys python=3.10
|
|
||||||
conda activate orisys
|
|
||||||
```
|
|
||||||
## 安装 CUDA Toolkit 和 cuDNN
|
|
||||||
在官方链接下载和安装:https://developer.nvidia.com/cuda/toolkit
|
|
||||||
或
|
|
||||||
在Anaconda环境中安装(例子使用cuda12.9):
|
|
||||||
https://anaconda.org/channels/nvidia/packages/cuda/overview
|
|
||||||
```
|
|
||||||
conda install nvidia/label/cuda-12.9.0::cuda-toolkit nvidia::cudnn
|
|
||||||
```
|
|
||||||
## SDK安装方法
|
|
||||||
1. 根据系统(win / linux),平台(amd64, aarch64), python(如p38、p39、p310) 版本下载对应的`.whl`文件
|
|
||||||
2. 运行pip命令安装:
|
|
||||||
```bash
|
|
||||||
pip install orisys-{version}-{python-version}-{python-version}-{os}_{machine}.whl
|
|
||||||
```
|
|
||||||
如
|
|
||||||
```bash
|
|
||||||
pip install orisys-0.4.2-cp310-cp310-win_amd64.whl
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **安装 CuPy(根据您的 CUDA 版本选择):**
|
## Sensor API
|
||||||
|
|
||||||
**方法 1:使用 extras(推荐)**
|
|
||||||
```bash
|
|
||||||
# 对于 CUDA 12.x
|
|
||||||
pip install orisys[cuda12]
|
|
||||||
|
|
||||||
# 对于 CUDA 13.x
|
|
||||||
pip install orisys[cuda13]
|
|
||||||
```
|
|
||||||
|
|
||||||
**方法 2:手动安装**
|
|
||||||
```bash
|
|
||||||
# 对于 CUDA 12.x
|
|
||||||
pip install cupy-cuda12x>=12.3.0
|
|
||||||
|
|
||||||
# 对于 CUDA 13.x
|
|
||||||
pip install cupy-cuda13x>=13.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
4. 测试导入:
|
### `Sensor` 类别
|
||||||
```python
|
#### 描述
|
||||||
import orisys
|
|
||||||
print(orisys.__version__)
|
|
||||||
```
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
## `Sensor` 类别
|
|
||||||
### 描述
|
|
||||||
`orisys`套件的核心类,用于从视频文件或实时摄像头中读取传感器图像,创建一个具体的传感器对象(实例)。并完成形变、光流及力信息的计算与管理。
|
`orisys`套件的核心类,用于从视频文件或实时摄像头中读取传感器图像,创建一个具体的传感器对象(实例)。并完成形变、光流及力信息的计算与管理。
|
||||||
|
|
||||||
### 输入参数
|
#### 输入参数
|
||||||
|
|
||||||
* **vid_src** (`int | str`, 必填): 传感器 ID、相机编号或视频路径。默认为 `0`。
|
* **vid_src** (`int | str`, 必填): 传感器 ID、相机编号或视频路径。默认为 `0`。
|
||||||
* **config_name** (`str`,可选): 传感器配置文件名称或 `json` 文件路径,用于加载对应的参数配置;例如 `ddjx01` 或 `./config/ddjx01.json`。
|
* **config_name** (`str`,可选): 传感器配置文件名称或 `json` 文件路径,用于加载对应的参数配置;例如 `box` 或 `./config/box.json`。
|
||||||
* **cal_path** (`str`,可选,已弃用): 该参数仅为兼容旧代码保留,当前版本中不再使用,传入后也会被忽略。
|
* **cal_path** (`str`,可选,已弃用): 该参数仅为兼容旧代码保留,当前版本中不再使用,传入后也会被忽略。
|
||||||
* **backend** (`str`,可选): 光流场解耦计算后端,默认为 `auto`,可选`cupy`,`opencl`,`cpu`。
|
* **backend** (`str`,可选): 光流场解耦计算后端,默认为 `auto`,可选`cupy`,`opencl`,`cpu`。
|
||||||
* **verbose** (`bool`,可选): 是否输出初始化及运行过程中的详细日志信息,默认为 `False`。
|
* **verbose** (`bool`,可选): 是否输出初始化及运行过程中的详细日志信息,默认为 `False`。
|
||||||
|
|
||||||
|
|
||||||
### 返回
|
#### 返回
|
||||||
* `Sensor` 实例,用于后续图像采集、形变计算和数据读取操作。
|
* `Sensor` 实例,用于后续图像采集、形变计算和数据读取操作。
|
||||||
### 示例
|
#### 示例
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Example 1: 用相机编号创建
|
# Example 1: 用相机编号创建
|
||||||
@@ -82,63 +35,62 @@ sensor = Sensor(0)
|
|||||||
sensor = Sensor("/data/data.mp4")
|
sensor = Sensor("/data/data.mp4")
|
||||||
```
|
```
|
||||||
|
|
||||||
***
|
### `Sensor.get_img` 方法
|
||||||
|
|
||||||
## 2. `Sensor.get_img` 方法
|
#### 描述
|
||||||
|
|
||||||
### 描述
|
|
||||||
|
|
||||||
采集并预处理一帧图像并存到`Sensor` 类别中。
|
采集并预处理一帧图像并存到`Sensor` 类别中。
|
||||||
|
|
||||||
|
|
||||||
### 返回
|
#### 返回
|
||||||
* `bool`:是否成功读取图像。
|
* `bool`:是否成功读取图像。
|
||||||
|
|
||||||
### 状态更新
|
#### 状态更新
|
||||||
* 成功时更新`sensor`类里面的`sensor.img`。处理后的BGR格式图像(`uint8`,尺寸为json文件中`resolution_stitch`对应分辨率)。
|
* 成功时更新`sensor`类里面的`sensor.img`。处理后的BGR格式图像(`uint8`,尺寸为json文件中`resolution_stitch`对应分辨率)。
|
||||||
### 示例
|
#### 示例
|
||||||
|
|
||||||
```python
|
```python
|
||||||
sensor.get_img()
|
sensor.get_img()
|
||||||
```
|
```
|
||||||
***
|
### `Sensor.compute_deformation` 方法
|
||||||
|
|
||||||
## 3. `Sensor.compute_deformation` 方法
|
#### 描述
|
||||||
|
|
||||||
### 描述
|
|
||||||
|
|
||||||
基于当前帧图像计算光流/形变向量场,并通过 HHD 分解将形变场分解为法向/切向分量,同时估计法向力、切向力与扭矩等标量输出。该方法会更新
|
基于当前帧图像计算光流/形变向量场,并通过 HHD 分解将形变场分解为法向/切向分量,同时估计法向力、切向力与扭矩等标量输出。该方法会更新
|
||||||
`sensor`的形变场与力相关状态,并返回当前帧的光流场。
|
`sensor`的形变场与力相关状态,并返回当前帧的光流场。
|
||||||
调用前置条件:需要先通过`get_img()`成功更新图像。
|
调用前置条件:需要先通过`get_img()`成功更新图像。
|
||||||
|
|
||||||
### 输入参数
|
#### 输入参数
|
||||||
* **check_motion**(`bool`,可选): 为 `True` 时先根据光流判断是否有运动,有运动才做 HHD 分解;为 `False` 时每帧都做 HHD。默认 `True`。
|
* **decompose** (`bool`,可选): 是否在计算光流后继续执行 HHD 分解,默认 `True`。
|
||||||
* **threshold**(`float`,可选): **check_motion** 功能的阈值,默认为`6`。
|
* **check_motion** (`bool`,可选): 是否启用运动检测门控。当前不推荐依赖该功能;新代码建议显式传入 `False`,让每帧都执行 HHD 分解。为兼容旧代码,函数默认值仍为 `True`。
|
||||||
* **tail_frames** (`int`,可选): 光流变化低于阈值后继续解算多少帧,确保传感器回复,,默认为 `10`.
|
|
||||||
|
|
||||||
### 返回
|
#### 兼容参数(暂不推荐)
|
||||||
|
|
||||||
* `bool`:是否接触到物体,如果**check_motion**为`False`时返回`None`。
|
以下参数只在 `check_motion=True` 时生效。由于运动检测门控目前不稳定,建议在新代码中暂时不要使用:
|
||||||
|
|
||||||
### 示例
|
* **threshold** (`float`,可选): 运动检测阈值,默认 `6`。
|
||||||
|
* **tail_frames** (`int`,可选): 运动低于阈值后继续解算多少帧,默认 `10`。
|
||||||
|
|
||||||
|
#### 返回
|
||||||
|
|
||||||
|
* `None`: 当 `check_motion=False` 时返回。推荐用法下不要依赖该返回值判断接触状态。
|
||||||
|
* `bool`: 仅当 `check_motion=True` 时返回运动检测结果;该模式目前暂不推荐。
|
||||||
|
|
||||||
|
#### 示例
|
||||||
|
|
||||||
```python
|
```python
|
||||||
sensor.get_img()
|
sensor.get_img()
|
||||||
|
sensor.compute_deformation(check_motion=False)
|
||||||
is_touched = sensor.compute_deformation(check_motion=True, threshold=6)
|
sensor.compute_contact()
|
||||||
# 步骤 2.3: 计算触碰点
|
|
||||||
if is_touched:
|
|
||||||
sensor.compute_contact()
|
|
||||||
```
|
```
|
||||||
***
|
### `Sensor.compute_contact` 方法
|
||||||
## 4. `Sensor.compute_contact` 方法
|
|
||||||
|
|
||||||
### 描述
|
#### 描述
|
||||||
基于已计算的法向形变场进行接触区域分析,输出接触质心与轮廓等信息,并生成接触深度图,默认配置在`json`文件中,可按需调节。
|
基于已计算的法向形变场进行接触区域分析,输出接触质心与轮廓等信息,并生成接触深度图,默认配置在`json`文件中,可按需调节。
|
||||||
|
|
||||||
调用前置条件:需先执行`compute_deformation()`。
|
调用前置条件:需先执行`compute_deformation()`。
|
||||||
|
|
||||||
### 返回
|
#### 返回
|
||||||
`dict`为接触分析结果字典,至少包含以下键:
|
`dict`为接触分析结果字典,至少包含以下键:
|
||||||
|
|
||||||
* **expansion_mask**: 扩张区域掩码(`np.ndarray`)。
|
* **expansion_mask**: 扩张区域掩码(`np.ndarray`)。
|
||||||
@@ -148,12 +100,12 @@ if is_touched:
|
|||||||
* **centroid_found**: 是否检测到接触区域(`bool`)。
|
* **centroid_found**: 是否检测到接触区域(`bool`)。
|
||||||
* **contour**: 最大轮廓或轮廓列表(可选字段,存在时返回)。
|
* **contour**: 最大轮廓或轮廓列表(可选字段,存在时返回)。
|
||||||
|
|
||||||
### 状态更新
|
#### 状态更新
|
||||||
|
|
||||||
* **sensor.centroid**: 接触质心(单点为 `(x, y)`,多点为列表)。
|
* **sensor.centroid**: 接触质心(单点为 `(x, y)`,多点为列表)。
|
||||||
* **sensor.centroid_found**: 是否找到质心(`bool`)。
|
* **sensor.centroid_found**: 是否找到质心(`bool`)。
|
||||||
|
|
||||||
### 示例
|
#### 示例
|
||||||
```python
|
```python
|
||||||
sensor.compute_contact()
|
sensor.compute_contact()
|
||||||
centroid, contour, depth = sensor.read_info(
|
centroid, contour, depth = sensor.read_info(
|
||||||
@@ -162,19 +114,18 @@ centroid, contour, depth = sensor.read_info(
|
|||||||
sensor.info.DEPTH,
|
sensor.info.DEPTH,
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
***
|
### `Sensor.read_info` 方法
|
||||||
## 4. `Sensor.read_info` 方法
|
|
||||||
|
|
||||||
### 描述
|
#### 描述
|
||||||
|
|
||||||
按指定的信息类型(`sensor.info`枚举值)批量读取传感器状态数据,并按传入顺序返回对应值的元组。
|
按指定的信息类型(`sensor.info`枚举值)批量读取传感器状态数据,并按传入顺序返回对应值的元组。
|
||||||
该方法通常用于在完成形变计算后,读取光流场、分量场、力、帧率等信息。
|
该方法通常用于在完成形变计算后,读取光流场、分量场、力、帧率等信息。
|
||||||
|
|
||||||
### 输入参数
|
#### 输入参数
|
||||||
|
|
||||||
* **args**: 任意数量的 `Sensor.info` 枚举,用于指定需要获取的数据类型。支持的枚举值及对应数据如下:
|
* **args**: 任意数量的 `Sensor.info` 枚举,用于指定需要获取的数据类型。支持的枚举值及对应数据如下:
|
||||||
|
|
||||||
### 支持的枚 info keys:
|
#### 支持的 info keys
|
||||||
|
|
||||||
向量/图像类
|
向量/图像类
|
||||||
|
|
||||||
@@ -188,6 +139,9 @@ centroid, contour, depth = sensor.read_info(
|
|||||||
* FNORMAL: 法向力,`float`。
|
* FNORMAL: 法向力,`float`。
|
||||||
* FSHEARX: X切向力,`float`。
|
* FSHEARX: X切向力,`float`。
|
||||||
* FSHEARY: Y切向力,`float`。
|
* FSHEARY: Y切向力,`float`。
|
||||||
|
* FNORMAL_RAW: 未经过低通滤波的原始法向力,`float`。
|
||||||
|
* FSHEARX_RAW: 未经过低通滤波的原始 X 切向力,`float`。
|
||||||
|
* FSHEARY_RAW: 未经过低通滤波的原始 Y 切向力,`float`。
|
||||||
* FPS: 输出帧率,`float`。
|
* FPS: 输出帧率,`float`。
|
||||||
* CENTROID: 触碰点的坐标,(x, y)。
|
* CENTROID: 触碰点的坐标,(x, y)。
|
||||||
* CONTOUR: 触碰区域轮廓,`np.ndarray`, shape=(H, W), 未生成时返回`np.empty`
|
* CONTOUR: 触碰区域轮廓,`np.ndarray`, shape=(H, W), 未生成时返回`np.empty`
|
||||||
@@ -198,7 +152,7 @@ centroid, contour, depth = sensor.read_info(
|
|||||||
|
|
||||||
注:H/W 由当前`json`文件中的`resolution_stitch`配置分辨率决定。
|
注:H/W 由当前`json`文件中的`resolution_stitch`配置分辨率决定。
|
||||||
|
|
||||||
### 返回
|
#### 返回
|
||||||
|
|
||||||
`Tuple`
|
`Tuple`
|
||||||
|
|
||||||
@@ -210,7 +164,7 @@ centroid, contour, depth = sensor.read_info(
|
|||||||
- 标量类数据:`float`
|
- 标量类数据:`float`
|
||||||
- 时间戳:`str`(`TIMESTAMP`,格式为 `HH:MM:SS`)
|
- 时间戳:`str`(`TIMESTAMP`,格式为 `HH:MM:SS`)
|
||||||
|
|
||||||
### 示例
|
#### 示例
|
||||||
```python
|
```python
|
||||||
# Example 1:读取标量数据
|
# Example 1:读取标量数据
|
||||||
fps, fn, fx, fy = sensor.read_info(
|
fps, fn, fx, fy = sensor.read_info(
|
||||||
@@ -223,22 +177,114 @@ fps, fn, fx, fy = sensor.read_info(
|
|||||||
# Example 2: 读取单个向量场(注意解包方式)
|
# Example 2: 读取单个向量场(注意解包方式)
|
||||||
flow = sensor.read_info(sensor.info.VRAW)
|
flow = sensor.read_info(sensor.info.VRAW)
|
||||||
```
|
```
|
||||||
***
|
### `Sensor.disconnect` 方法
|
||||||
|
|
||||||
## 5. `Sensor.disconnect` 方法
|
#### 描述
|
||||||
|
|
||||||
### 描述
|
|
||||||
|
|
||||||
释放传感器相关资源并停止后台异步线程。 调用后将关闭视频/摄像头输入源,并销毁所有 OpenCV 窗口。
|
释放传感器相关资源并停止后台异步线程。 调用后将关闭视频/摄像头输入源,并销毁所有 OpenCV 窗口。
|
||||||
|
|
||||||
建议在程序退出前调用以确保资源被正确释放。
|
建议在程序退出前调用以确保资源被正确释放。
|
||||||
|
|
||||||
***
|
### `Sensor.configure` 低通滤波设置
|
||||||
***
|
|
||||||
|
|
||||||
## `orisys.finger` 手指 UV pipeline
|
#### 描述
|
||||||
|
|
||||||
`examples/products/ory_finger/finger_pipeline.py` 展示了一个更适合手指/单相机场景的高层 API:
|
运行时更新传感器设置,无需重新连接相机。常用于开启力信号低通滤波,降低 `FNORMAL`、`FSHEARX`、`FSHEARY` 的高频噪声。
|
||||||
|
|
||||||
|
默认采样率按 30 fps 计算,推荐低通截止频率为 5 Hz:
|
||||||
|
|
||||||
|
```python
|
||||||
|
sensor.configure(lowpass_cutoff_hz=5.0, lowpass_sample_rate_hz=30.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:`lowpass_cutoff_hz` 必须小于 Nyquist 频率,即 `lowpass_sample_rate_hz / 2`。例如 30 fps 时,截止频率必须小于 15 Hz。
|
||||||
|
|
||||||
|
#### 输入参数
|
||||||
|
|
||||||
|
* **lowpass_cutoff_hz** (`float`,可选): 力信号低通截止频率,单位 Hz。`0` 表示关闭滤波;正数默认开启滤波。
|
||||||
|
* **lowpass_sample_rate_hz** (`float`,可选): 力信号采样率,默认 `30.0`。
|
||||||
|
* **lowpass_order** (`int`,可选): Butterworth 滤波器阶数,默认 `2`。
|
||||||
|
* **lowpass_enabled** (`bool`,可选): 显式开启或关闭低通滤波。
|
||||||
|
* **hhd_downsample** (`int`,可选): HHD 解算下采样倍数,需为 `>= 1` 的整数,配置默认 `3`。
|
||||||
|
|
||||||
|
#### 读取结果
|
||||||
|
|
||||||
|
开启低通滤波后,`read_info()` 读取的力输出会被滤波:
|
||||||
|
|
||||||
|
```python
|
||||||
|
fn, fx, fy = sensor.read_info(
|
||||||
|
sensor.info.FNORMAL,
|
||||||
|
sensor.info.FSHEARX,
|
||||||
|
sensor.info.FSHEARY,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
原始力输出仍可通过 `FNORMAL_RAW`、`FSHEARX_RAW`、`FSHEARY_RAW` 读取,便于调试或对比。
|
||||||
|
|
||||||
|
## Signal Processing API
|
||||||
|
|
||||||
|
### `orisys.signal_processing.lowpass_filter` 函数
|
||||||
|
|
||||||
|
#### 描述
|
||||||
|
|
||||||
|
对一维、等间隔采样数据执行零相位 Butterworth 低通滤波,适合示例程序或图表中对历史数据做平滑处理。
|
||||||
|
|
||||||
|
默认参数为 `cutoff_hz=5.0`、`sample_rate_hz=30.0`、`order=2`。这表示在 30 fps 数据上使用 5 Hz 低通滤波。
|
||||||
|
|
||||||
|
#### 输入参数
|
||||||
|
|
||||||
|
* **data**: 一维数据序列。
|
||||||
|
* **cutoff_hz** (`float`,可选): 截止频率,默认 `5.0`。传入 `0` 时不做滤波并返回数据副本。
|
||||||
|
* **sample_rate_hz** (`float`,可选): 采样率,默认 `30.0`。
|
||||||
|
* **order** (`int`,可选): Butterworth 滤波器阶数,默认 `2`。
|
||||||
|
|
||||||
|
#### 返回
|
||||||
|
|
||||||
|
* `np.ndarray`: 滤波后的 `float64` 数组。
|
||||||
|
|
||||||
|
#### 示例
|
||||||
|
|
||||||
|
```python
|
||||||
|
from orisys.signal_processing import lowpass_filter
|
||||||
|
|
||||||
|
filtered_force = lowpass_filter(force_values, cutoff_hz=5.0, sample_rate_hz=30.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `orisys.signal_processing.LowpassFilter` 类别
|
||||||
|
|
||||||
|
#### 描述
|
||||||
|
|
||||||
|
有状态的因果 Butterworth 低通滤波器,适合逐帧处理实时数据。它会保留上一帧的滤波状态,并通过 `process()` 一次处理一个多通道样本。
|
||||||
|
|
||||||
|
`Sensor.configure()` 内部使用该类来滤波力信号。构造时默认 `cutoff_hz=0.0`,表示关闭滤波;若要按 30 fps 使用推荐滤波,设置 `cutoff_hz=5.0`、`sample_rate_hz=30.0`。
|
||||||
|
|
||||||
|
#### 输入参数
|
||||||
|
|
||||||
|
* **cutoff_hz** (`float`,可选): 截止频率,默认 `0.0`,表示关闭滤波。
|
||||||
|
* **sample_rate_hz** (`float`,可选): 采样率,默认 `30.0`。
|
||||||
|
* **order** (`int`,可选): Butterworth 滤波器阶数,默认 `2`。
|
||||||
|
* **n_channels** (`int`,可选): 每个样本的通道数,默认 `3`。
|
||||||
|
|
||||||
|
#### 常用方法
|
||||||
|
|
||||||
|
* **configure(...)**: 运行时更新 `cutoff_hz`、`sample_rate_hz` 或 `order`。
|
||||||
|
* **reset(x0=None)**: 清除滤波器状态;传入 `x0` 时会用该样本初始化稳态。
|
||||||
|
* **process(values)**: 滤波一个多通道样本,返回 shape 为 `(n_channels,)` 的 `np.ndarray`。
|
||||||
|
|
||||||
|
#### 示例
|
||||||
|
|
||||||
|
```python
|
||||||
|
from orisys.signal_processing import LowpassFilter
|
||||||
|
|
||||||
|
filt = LowpassFilter(cutoff_hz=5.0, sample_rate_hz=30.0, n_channels=3)
|
||||||
|
filtered = filt.process([fnormal, fshearx, fsheary])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Finger Pipeline API
|
||||||
|
|
||||||
|
### `orisys.finger` 手指 UV pipeline
|
||||||
|
|
||||||
|
`examples/finger_pipeline.py` 展示了一个更适合手指/单相机场景的高层 API:
|
||||||
|
|
||||||
1. 使用 `FingerRuntime.open()` 打开视频源、加载 Blender 导出的资产,并构建 UV 展开映射。
|
1. 使用 `FingerRuntime.open()` 打开视频源、加载 Blender 导出的资产,并构建 UV 展开映射。
|
||||||
2. 用 `grab_and_process()` 或 `read_source() + process_frame()` 获取一帧展开后的触觉图像。
|
2. 用 `grab_and_process()` 或 `read_source() + process_frame()` 获取一帧展开后的触觉图像。
|
||||||
@@ -247,7 +293,7 @@ flow = sensor.read_info(sensor.info.VRAW)
|
|||||||
|
|
||||||
相比直接使用 `Sensor`,`FingerRuntime` 已封装了视频读取、相机尺寸设置、UV 展开和有效区域掩码传递,更适合作为手指 2D/3D 示例与上层应用的入口。
|
相比直接使用 `Sensor`,`FingerRuntime` 已封装了视频读取、相机尺寸设置、UV 展开和有效区域掩码传递,更适合作为手指 2D/3D 示例与上层应用的入口。
|
||||||
|
|
||||||
### 1. `FingerRuntime.open` 方法
|
### `FingerRuntime.open` 方法
|
||||||
|
|
||||||
#### 描述
|
#### 描述
|
||||||
|
|
||||||
@@ -262,7 +308,7 @@ flow = sensor.read_info(sensor.info.VRAW)
|
|||||||
#### 输入参数
|
#### 输入参数
|
||||||
|
|
||||||
* **export_dir** (`str`,可选): 手指导出资产目录,默认值来自 `orisys.finger.DEFAULT_FINGER_EXPORT_DIR`。目录下通常需要包含 `mesh.npz`、`camera_intrinsics.npz`、`world_to_cam.npy`。
|
* **export_dir** (`str`,可选): 手指导出资产目录,默认值来自 `orisys.finger.DEFAULT_FINGER_EXPORT_DIR`。目录下通常需要包含 `mesh.npz`、`camera_intrinsics.npz`、`world_to_cam.npy`。
|
||||||
* **config** (`str`,可选): Orisys 配置文件名称或路径,默认 `dd02`(兼容解析到 `dd02-ov`)。
|
* **config** (`str`,可选): Orisys 配置文件名称或路径,默认 `finger`。
|
||||||
* **map_cache** (`str`,可选): UV 展开缓存文件路径,默认值来自 `orisys.finger.DEFAULT_FINGER_MAP_CACHE`。
|
* **map_cache** (`str`,可选): UV 展开缓存文件路径,默认值来自 `orisys.finger.DEFAULT_FINGER_MAP_CACHE`。
|
||||||
* **source_scale** (`float`,可选): 输入相机分辨率缩放比例;未提供时根据配置自动推断。
|
* **source_scale** (`float`,可选): 输入相机分辨率缩放比例;未提供时根据配置自动推断。
|
||||||
* **calib** (`str | None`,可选): 外部相机标定文件路径;未提供时优先使用导出资产中的内参。
|
* **calib** (`str | None`,可选): 外部相机标定文件路径;未提供时优先使用导出资产中的内参。
|
||||||
@@ -282,13 +328,13 @@ flow = sensor.read_info(sensor.info.VRAW)
|
|||||||
from orisys.finger import FingerRuntime
|
from orisys.finger import FingerRuntime
|
||||||
|
|
||||||
runtime = FingerRuntime.open(
|
runtime = FingerRuntime.open(
|
||||||
config="dd02",
|
config="finger",
|
||||||
video=0,
|
video=0,
|
||||||
verbose=True,
|
verbose=True,
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. `FingerRuntime` 常用属性
|
### `FingerRuntime` 常用属性
|
||||||
|
|
||||||
* **runtime.geometry**: `FingerGeometry`,包含输入/输出分辨率、导出资产路径、相机内参、缓存路径等一次性初始化结果。
|
* **runtime.geometry**: `FingerGeometry`,包含输入/输出分辨率、导出资产路径、相机内参、缓存路径等一次性初始化结果。
|
||||||
* **runtime.domain**: UV 展开域对象,内部包含 remap、valid mask 以及可选 surface 数据。
|
* **runtime.domain**: UV 展开域对象,内部包含 remap、valid mask 以及可选 surface 数据。
|
||||||
@@ -296,7 +342,7 @@ runtime = FingerRuntime.open(
|
|||||||
* **runtime.capture**: OpenCV `VideoCapture` 对象。
|
* **runtime.capture**: OpenCV `VideoCapture` 对象。
|
||||||
* **runtime.valid_mask**: 当前 UV 域有效像素掩码。
|
* **runtime.valid_mask**: 当前 UV 域有效像素掩码。
|
||||||
|
|
||||||
### 3. `FingerRuntime.read_source` 方法
|
### `FingerRuntime.read_source` 方法
|
||||||
|
|
||||||
#### 描述
|
#### 描述
|
||||||
|
|
||||||
@@ -308,11 +354,11 @@ runtime = FingerRuntime.open(
|
|||||||
* **ok** (`bool`): 是否成功读取。
|
* **ok** (`bool`): 是否成功读取。
|
||||||
* **frame** (`np.ndarray | None`): 原始 BGR 帧。
|
* **frame** (`np.ndarray | None`): 原始 BGR 帧。
|
||||||
|
|
||||||
### 4. `FingerRuntime.process_frame` 方法
|
### `FingerRuntime.process_frame` 方法
|
||||||
|
|
||||||
#### 描述
|
#### 描述
|
||||||
|
|
||||||
对一帧原始输入执行源图预处理、UV 展开,并把展开后的结果送入内部 `TactileProcessor`。这是 `examples/products/ory_finger/finger_pipeline.py` 中 profile 模式使用的方法。
|
对一帧原始输入执行源图预处理、UV 展开,并把展开后的结果送入内部 `TactileProcessor`。这是 `examples/finger_pipeline.py` 中 profile 模式使用的方法。
|
||||||
|
|
||||||
#### 输入参数
|
#### 输入参数
|
||||||
|
|
||||||
@@ -323,11 +369,11 @@ runtime = FingerRuntime.open(
|
|||||||
|
|
||||||
* `np.ndarray`: 展开后的触觉图像。`color=False` 时通常为单通道灰度图;`color=True` 时为 BGR 图。
|
* `np.ndarray`: 展开后的触觉图像。`color=False` 时通常为单通道灰度图;`color=True` 时为 BGR 图。
|
||||||
|
|
||||||
### 5. `FingerRuntime.grab_and_process` 方法
|
### `FingerRuntime.grab_and_process` 方法
|
||||||
|
|
||||||
#### 描述
|
#### 描述
|
||||||
|
|
||||||
先读取一帧视频源,再调用 `process_frame()` 完成 UV 展开并推入内部触觉处理器。非 profile 模式下,`examples/products/ory_finger/finger_pipeline.py` 使用该方法作为主循环入口。
|
先读取一帧视频源,再调用 `process_frame()` 完成 UV 展开并推入内部触觉处理器。非 profile 模式下,`examples/finger_pipeline.py` 使用该方法作为主循环入口。
|
||||||
|
|
||||||
#### 输入参数
|
#### 输入参数
|
||||||
|
|
||||||
@@ -339,7 +385,7 @@ runtime = FingerRuntime.open(
|
|||||||
* **ok** (`bool`): 是否成功读取并处理。
|
* **ok** (`bool`): 是否成功读取并处理。
|
||||||
* **unrolled** (`np.ndarray | None`): 展开后的触觉图像。
|
* **unrolled** (`np.ndarray | None`): 展开后的触觉图像。
|
||||||
|
|
||||||
### 6. `FingerRuntime.get_img` 方法
|
### `FingerRuntime.get_img` 方法
|
||||||
|
|
||||||
#### 描述
|
#### 描述
|
||||||
|
|
||||||
@@ -353,11 +399,11 @@ runtime = FingerRuntime.open(
|
|||||||
|
|
||||||
* `np.ndarray | None`
|
* `np.ndarray | None`
|
||||||
|
|
||||||
### 7. `FingerRuntime.warmup` 方法
|
### `FingerRuntime.warmup` 方法
|
||||||
|
|
||||||
#### 描述
|
#### 描述
|
||||||
|
|
||||||
先执行一次采集与 UV 展开,然后调用 `runtime.tactile.warmup()` 预热内部光流/HHD 相关缓存。`examples/products/ory_finger/finger_pipeline.py` 在进入主循环前会先执行一次 warmup。
|
先执行一次采集与 UV 展开,然后调用 `runtime.tactile.warmup()` 预热内部光流/HHD 相关缓存。`examples/finger_pipeline.py` 在进入主循环前会先执行一次 warmup。
|
||||||
|
|
||||||
#### 输入参数
|
#### 输入参数
|
||||||
|
|
||||||
@@ -369,13 +415,13 @@ runtime = FingerRuntime.open(
|
|||||||
* **ok** (`bool`): 是否成功完成 warmup 前的采集。
|
* **ok** (`bool`): 是否成功完成 warmup 前的采集。
|
||||||
* **unrolled** (`np.ndarray | None`): warmup 时得到的展开图。
|
* **unrolled** (`np.ndarray | None`): warmup 时得到的展开图。
|
||||||
|
|
||||||
### 8. `FingerRuntime.close` 方法
|
### `FingerRuntime.close` 方法
|
||||||
|
|
||||||
#### 描述
|
#### 描述
|
||||||
|
|
||||||
释放内部 `VideoCapture` 并调用 `runtime.tactile.disconnect()` 清理 `Sensor` 资源。程序退出前应调用。
|
释放内部 `VideoCapture` 并调用 `runtime.tactile.disconnect()` 清理 `Sensor` 资源。程序退出前应调用。
|
||||||
|
|
||||||
### 9. `TactileProcessor` 类别
|
### `TactileProcessor` 类别
|
||||||
|
|
||||||
#### 描述
|
#### 描述
|
||||||
|
|
||||||
@@ -386,16 +432,16 @@ runtime = FingerRuntime.open(
|
|||||||
* **raw_sensor**: 底层 `Sensor` 对象。
|
* **raw_sensor**: 底层 `Sensor` 对象。
|
||||||
* **info**: 与 `Sensor.info` 相同的枚举入口。
|
* **info**: 与 `Sensor.info` 相同的枚举入口。
|
||||||
* **img / flow / vnormal / vshear / depth_map**: 当前帧处理结果。
|
* **img / flow / vnormal / vshear / depth_map**: 当前帧处理结果。
|
||||||
* **mask_stats**: 有效 UV 区域统计信息;`examples/products/ory_finger/finger_pipeline.py` 会读取 `mask_stats["coverage"]` 输出当前掩码覆盖率。
|
* **mask_stats**: 有效 UV 区域统计信息;`examples/finger_pipeline.py` 会读取 `mask_stats["coverage"]` 输出当前掩码覆盖率。
|
||||||
* **valid_mask**: 当前有效区域掩码。
|
* **valid_mask**: 当前有效区域掩码。
|
||||||
|
|
||||||
#### 说明
|
#### 说明
|
||||||
|
|
||||||
除 `push_frame()` / `warmup()` 之外,`TactileProcessor` 的 `compute_deformation()`、`compute_contact()`、`read_info()`、`reset()` 调用方式与 `Sensor` 基本一致,因此可直接复用前文 `Sensor` 章节中的参数说明。
|
除 `push_frame()` / `warmup()` 之外,`TactileProcessor` 的 `compute_deformation()`、`compute_contact()`、`read_info()`、`reset()` 调用方式与 `Sensor` 基本一致,因此可直接复用前文 `Sensor` 章节中的参数说明。
|
||||||
|
|
||||||
### 10. 最小手指 pipeline 示例
|
### 最小手指 pipeline 示例
|
||||||
|
|
||||||
下面的流程与 `examples/products/ory_finger/finger_pipeline.py` 一致:
|
下面的流程与 `examples/finger_pipeline.py` 一致:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import time
|
import time
|
||||||
@@ -404,7 +450,7 @@ import orisys
|
|||||||
from orisys.finger import FingerRuntime
|
from orisys.finger import FingerRuntime
|
||||||
|
|
||||||
runtime = FingerRuntime.open(
|
runtime = FingerRuntime.open(
|
||||||
config="dd02",
|
config="finger",
|
||||||
video=0,
|
video=0,
|
||||||
verbose=True,
|
verbose=True,
|
||||||
)
|
)
|
||||||
@@ -460,9 +506,9 @@ finally:
|
|||||||
cv2.destroyAllWindows()
|
cv2.destroyAllWindows()
|
||||||
```
|
```
|
||||||
|
|
||||||
### 11. 关于 profile 模式
|
### 关于 profile 模式
|
||||||
|
|
||||||
`examples/products/ory_finger/finger_pipeline.py` 中的 `--profile` / `--profile-every` 是示例脚本参数,不属于 `orisys` SDK 核心 API。本质上它只是把主循环拆成:
|
`examples/finger_pipeline.py` 中的 `--profile` / `--profile-every` 是示例脚本参数,不属于 `orisys` SDK 核心 API。本质上它只是把主循环拆成:
|
||||||
|
|
||||||
1. `runtime.read_source()`
|
1. `runtime.read_source()`
|
||||||
2. `runtime.process_frame(...)`
|
2. `runtime.process_frame(...)`
|
||||||
@@ -472,11 +518,11 @@ finally:
|
|||||||
|
|
||||||
并对各阶段做耗时统计。因此在业务代码中,也可以按同样方式自行插入计时逻辑。
|
并对各阶段做耗时统计。因此在业务代码中,也可以按同样方式自行插入计时逻辑。
|
||||||
|
|
||||||
# 可视化相关
|
## 可视化相关
|
||||||
|
|
||||||
## 1. `util.draw_arrows` 方法
|
### `util.draw_arrows` 方法
|
||||||
|
|
||||||
### 输入参数
|
#### 输入参数
|
||||||
|
|
||||||
* **frame**:背景图像
|
* **frame**:背景图像
|
||||||
* **optical_flow**:光流场
|
* **optical_flow**:光流场
|
||||||
@@ -488,12 +534,12 @@ finally:
|
|||||||
* **below_threshold_color**:RGB格式的`uint8`颜色,如(0, 0, 255)
|
* **below_threshold_color**:RGB格式的`uint8`颜色,如(0, 0, 255)
|
||||||
* **edge_margin**: 不显示箭头边距(pixel)
|
* **edge_margin**: 不显示箭头边距(pixel)
|
||||||
|
|
||||||
### 返回
|
#### 返回
|
||||||
`np.ndarray` :箭头光流可视化图像
|
`np.ndarray` :箭头光流可视化图像
|
||||||
|
|
||||||
## 2. `util.draw_contact` 方法
|
### `util.draw_contact` 方法
|
||||||
|
|
||||||
### 输入参数
|
#### 输入参数
|
||||||
|
|
||||||
* **image**:背景图像
|
* **image**:背景图像
|
||||||
* **contour**:轮廓
|
* **contour**:轮廓
|
||||||
@@ -501,42 +547,36 @@ finally:
|
|||||||
* **point_color**:触碰点的颜色 (BGR)
|
* **point_color**:触碰点的颜色 (BGR)
|
||||||
* **region_color**:轮廓的颜色 (BGR)
|
* **region_color**:轮廓的颜色 (BGR)
|
||||||
|
|
||||||
### 返回
|
#### 返回
|
||||||
`np.ndarray` :箭头光流可视化图像
|
`np.ndarray` :箭头光流可视化图像
|
||||||
|
|
||||||
***
|
## 配置标定相关
|
||||||
|
|
||||||
# 配置标定相关
|
### `list_config` 方法
|
||||||
|
|
||||||
## 1. `list_config` 方法
|
#### 描述
|
||||||
|
|
||||||
### 描述
|
|
||||||
列出内置传感器配置参数字典。
|
列出内置传感器配置参数字典。
|
||||||
|
|
||||||
### 返回
|
#### 返回
|
||||||
|
|
||||||
* `list`: 内置传感器配置参数字典的名字列表。
|
* `list`: 内置传感器配置参数字典的名字列表。
|
||||||
***
|
### `export_config` 方法
|
||||||
## 2. `export_config` 方法
|
|
||||||
|
|
||||||
### 描述
|
#### 描述
|
||||||
导出当前传感器实例使用的配置参数字典。
|
导出当前传感器实例使用的配置参数字典。
|
||||||
|
|
||||||
### 输入参数
|
#### 输入参数
|
||||||
* **config_name**: 需要导出的配置参数字典名字,`str`
|
* **config_name**: 需要导出的配置参数字典名字,`str`
|
||||||
* **path**: 导出的路径,默认`"./config/exported_config.json"`
|
* **path**: 导出的路径,默认`"./config/exported_config.json"`
|
||||||
|
|
||||||
### 返回
|
#### 返回
|
||||||
|
|
||||||
* `dict`: 导出的配置参数字典。
|
* `dict`: 导出的配置参数字典。
|
||||||
|
|
||||||
### 保存
|
#### 保存
|
||||||
|
|
||||||
* 导出内置的`config_name`配置参数字典至`path`
|
* 导出内置的`config_name`配置参数字典至`path`
|
||||||
|
|
||||||
***
|
|
||||||
***
|
|
||||||
|
|
||||||
## 常见问题解答 (FAQ)
|
## 常见问题解答 (FAQ)
|
||||||
### Q1:为什么必须先调用`get_img()`,再调用`compute_deformation()`?
|
### Q1:为什么必须先调用`get_img()`,再调用`compute_deformation()`?
|
||||||
A:`compute_deformation()`依赖最近一次获取并拼接的图像数据进行形变与力计算。
|
A:`compute_deformation()`依赖最近一次获取并拼接的图像数据进行形变与力计算。
|
||||||
@@ -556,9 +596,5 @@ A:当需要接触相关信息(如接触质心、轮廓或深度/散度图)
|
|||||||
A:`read_info()`按传入的`sensor.info`参数顺序返回一个元组。
|
A:`read_info()`按传入的`sensor.info`参数顺序返回一个元组。
|
||||||
返回值顺序与参数顺序一一对应,与内部存储顺序无关。
|
返回值顺序与参数顺序一一对应,与内部存储顺序无关。
|
||||||
|
|
||||||
### Q5:如果没有标定文件,或拼接结果不正确,会发生什么?
|
### Q5: 运行报错:“AttributeError: 'Stitcher' object has no attribute 'pos_default'”
|
||||||
A:若未指定或未找到标定文件,系统会在需要时创建新的拼接标定数据并保存到默认路径。
|
|
||||||
拼接结果异常通常与标定数据、拼接配置或输入图像质量有关,建议重新生成拼接标定并检查配置参数。
|
|
||||||
|
|
||||||
### Q6: 运行报错:“AttributeError: 'Stitcher' object has no attribute 'pos_default'”
|
|
||||||
A: 请检查视频源的编号是否正确,以及是否能正常打开视频源。在linux 使用`v4l2-ctl --list-devices` 或 Windows使用`Get-PnpDevice -PresentOnly | Where-Object { $_.Class -match '^Camera' }` 列出摄像头设备。
|
A: 请检查视频源的编号是否正确,以及是否能正常打开视频源。在linux 使用`v4l2-ctl --list-devices` 或 Windows使用`Get-PnpDevice -PresentOnly | Where-Object { $_.Class -match '^Camera' }` 列出摄像头设备。
|
||||||
Binary file not shown.
Binary file not shown.
+113
-83
@@ -1,80 +1,106 @@
|
|||||||
叠动科技软件使用手册
|
---
|
||||||
版本: 0.4.2
|
title: 快速开始
|
||||||
更新日期: 2026年7月13日
|
description: 安装并运行 Orisys SDK 的基础步骤
|
||||||
|
---
|
||||||
|
|
||||||
# 简介
|
# 快速开始
|
||||||
|
|
||||||
|
版本:0.5.0
|
||||||
|
|
||||||
|
## 简介
|
||||||
本文件介绍如何上手和运行叠动视触觉传感器配套的开发套件。本套件是配合叠动视触觉传感器进行图像处理,计算形变、力以及接触信息。它可以从摄像头或视频文件中读取图像,处理后打印和展示处理后信息。
|
本文件介绍如何上手和运行叠动视触觉传感器配套的开发套件。本套件是配合叠动视触觉传感器进行图像处理,计算形变、力以及接触信息。它可以从摄像头或视频文件中读取图像,处理后打印和展示处理后信息。
|
||||||
支持操作系统:Windows、Ubuntu
|
支持操作系统:Windows、Ubuntu
|
||||||
建议Python 版本: 3.10, 3.11, 3.12
|
建议Python 版本: 3.10, 3.11, 3.12
|
||||||
|
|
||||||
# 基础环境设置
|
项目链接:
|
||||||
## 安装 Python 与 Anaconda
|
|
||||||
1. 访问 [Anaconda 官网](https://www.anaconda.com/download)下载适合您操作系统的安装包(Windows、Linux)。
|
|
||||||
2. 按照提示完成安装。
|
|
||||||
3. 安装完成后,打开命令行终端(Windows 用户可在”Anaconda Navigator”打开“Anaconda Prompt”),输入 `conda info` ,若能显示 conda 的信息,则说明安装成功。
|
|
||||||
|
|
||||||
## 创建conda虚拟环境
|
- PyPI 安装页:[https://pypi.org/project/orisys/](https://pypi.org/project/orisys/)
|
||||||
|
- 项目文档/代码:[https://github.com/Orisys-Tech/orisys-sdk](https://github.com/Orisys-Tech/orisys-sdk)
|
||||||
|
|
||||||
|
## 基础环境设置
|
||||||
|
### 安装 Python 与 Anaconda
|
||||||
|
1. 访问 [Anaconda 官网](https://www.anaconda.com/download) 下载适合您操作系统的安装包(Windows、Linux)。
|
||||||
|
2. 按照提示完成安装。
|
||||||
|
3. 安装完成后,打开命令行终端(Windows 用户可在 “Anaconda Navigator” 打开 “Anaconda Prompt”),输入 `conda info`,若能显示 conda 的信息,则说明安装成功。
|
||||||
|
|
||||||
|
### 创建 conda 虚拟环境
|
||||||
虚拟环境相当于一个独立的 Python 工作空间,可以避免不同项目所需的库版本冲突。
|
虚拟环境相当于一个独立的 Python 工作空间,可以避免不同项目所需的库版本冲突。
|
||||||
打开命令行终端(或Anaconda Prompt),依次输入以下命令(以 python 3.10 为例):
|
打开命令行终端(或 Anaconda Prompt),依次输入以下命令(以 Python 3.10 为例):
|
||||||
``` bash
|
```bash
|
||||||
conda create -n orisys python=3.10
|
conda create -n orisys python=3.10
|
||||||
conda activate orisys
|
conda activate orisys
|
||||||
```
|
```
|
||||||
|
|
||||||
激活后,命令行提示符前会出现 `(orisys)` 字样,表示当前已进入虚拟环境。
|
激活后,命令行提示符前会出现 `(orisys)` 字样,表示当前已进入虚拟环境。
|
||||||
|
|
||||||
## 创建工作区文件夹
|
### 创建工作区文件夹
|
||||||
“Anaconda Prompt”启动后的默认工作路径在用户根文件夹中,建议创建工作区文件夹方便管理,可以手动创建或者运行`mkdir <path>`命令:, 例如
|
“Anaconda Prompt” 启动后的默认工作路径在用户根文件夹中,建议创建工作区文件夹方便管理。可以手动创建文件夹,也可以运行 `mkdir <path>` 命令。例如:
|
||||||
|
|
||||||
``` bash
|
```bash
|
||||||
mkdir orisys_ws
|
mkdir orisys_ws
|
||||||
```
|
```
|
||||||
|
|
||||||
创建后如果要更换工作区,使用 `cd <path>`命令,例如:
|
创建后如果要更换工作区,使用 `cd <path>` 命令,例如:
|
||||||
|
|
||||||
``` bash
|
```bash
|
||||||
cd "C:\Users\orisys\orisys_ws"
|
cd "C:\Users\orisys\orisys_ws"
|
||||||
```
|
```
|
||||||
<div style="page-break-after: always;"></div>
|
### 安装 CUDA 和 cuDNN(可选)
|
||||||
|
|
||||||
## 安装 CUDA 和 cuDNN(可选)
|
|
||||||
如果您拥有 NVIDIA 显卡,并希望利用 GPU 加速计算,则需要安装 CUDA 工具包和 cuDNN。
|
如果您拥有 NVIDIA 显卡,并希望利用 GPU 加速计算,则需要安装 CUDA 工具包和 cuDNN。
|
||||||
|
|
||||||
1. 方法一(官网下载):
|
1. 方法一(官网下载):
|
||||||
访问 [NVIDIA CUDA Toolkit官网](https://developer.nvidia.com/cuda/toolkit)下载对应系统的 CUDA Toolkit 安装包,并按照提示安装, 注意12.X 和 13.X 的版本对应不同的 CuPy 库版本。
|
访问 [NVIDIA CUDA Toolkit 官网](https://developer.nvidia.com/cuda/toolkit) 下载对应系统的 CUDA Toolkit 安装包,并按照提示安装。注意 12.x 和 13.x 的版本对应不同的 CuPy 库版本。
|
||||||
|
|
||||||
2. 方法二(使用 Anaconda 安装,推荐):
|
2. 方法二(使用 Anaconda 安装,推荐):
|
||||||
在已激活的 orisys 环境中直接运行以下命令(以 CUDA 12.9 为例):
|
在已激活的 orisys 环境中直接运行以下命令(以 CUDA 12.9 为例):
|
||||||
`conda install nvidia/label/cuda-12.9.0::cuda-toolkit nvidia::cudnn`
|
|
||||||
|
```bash
|
||||||
|
conda install nvidia/label/cuda-12.9.0::cuda-toolkit nvidia::cudnn
|
||||||
|
```
|
||||||
|
|
||||||
该方法会自动配置好 CUDA 和 cuDNN,无需手动下载。
|
该方法会自动配置好 CUDA 和 cuDNN,无需手动下载。
|
||||||
|
|
||||||
注意:如果您的计算机没有 NVIDIA 显卡,或者不想使用 GPU 加速,可以跳过此步骤。套件也支持纯 CPU 运行(但速度可能较慢)。
|
注意:如果您的计算机没有 NVIDIA 显卡,或者不想使用 GPU 加速,可以跳过此步骤。套件也支持纯 CPU 运行(但速度可能较慢)。
|
||||||
|
|
||||||
# 安装 orisys SDK
|
## 安装 orisys SDK
|
||||||
|
|
||||||
orisys SDK 以 .whl 文件形式提供,您需要根据操作系统、Python 版本和硬件平台选择正确的文件。
|
推荐优先从 PyPI 安装。PyPI 会自动选择适合当前 Python 版本和操作系统的安装包,适合大多数用户。
|
||||||
|
|
||||||
## 下载正确的 .whl 文件
|
### 使用 PyPI 安装(推荐)
|
||||||
|
确认已经激活 `orisys` 虚拟环境后,运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install orisys
|
||||||
|
```
|
||||||
|
|
||||||
|
如果需要升级到最新版本,运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install --upgrade orisys
|
||||||
|
```
|
||||||
|
|
||||||
|
如果当前网络无法访问 PyPI,或需要安装客服提供的指定版本,可以使用下方 `.whl` 文件安装方式。
|
||||||
|
|
||||||
|
### 使用 .whl 文件安装(离线或指定版本)
|
||||||
从官方渠道获取 .whl 文件,文件名格式通常为:
|
从官方渠道获取 .whl 文件,文件名格式通常为:
|
||||||
orisys-{版本}-{Python版本}-{Python版本}-{操作系统}_{硬件架构}.whl
|
orisys-{版本}-{Python版本}-{Python版本}-{操作系统}_{硬件架构}.whl
|
||||||
例如:orisys-0.4.2-cp310-cp310-win_amd64.whl 表示:
|
例如:orisys-0.5.0-cp310-cp310-win_amd64.whl 表示:
|
||||||
|
|
||||||
版本:0.4.2
|
版本:0.5.0
|
||||||
Python 版本:3.10(cp310)
|
Python 版本:3.10(cp310)
|
||||||
操作系统:Windows(win)
|
操作系统:Windows(win)
|
||||||
硬件架构:64位(amd64)
|
硬件架构:64位(amd64)
|
||||||
|
|
||||||
请根据您的系统选择对应的文件,或者咨询客服索取:
|
请根据您的系统选择对应的文件,或者咨询客服索取。
|
||||||
|
|
||||||
## 安装 .whl 文件
|
将下载好的 `.whl` 文件放在一个文件夹中(如上述 `orisys_ws` 文件夹),打开命令行(确保已激活 `orisys` 环境或使用 “Anaconda Prompt”),使用 `cd` 命令切换到该文件夹,然后运行:
|
||||||
将下载好的 .whl 文件放在一个文件夹中(如上述`orisys_ws`文件夹),打开命令行(确保已激活 orisys 环境或使用"Anaconda Prompt"),使用 `cd` 命令切换到该文件夹,然后运行:
|
|
||||||
|
|
||||||
``` bash
|
```bash
|
||||||
# 请将文件名替换为您实际下载的文件名
|
# 请将文件名替换为您实际下载的文件名
|
||||||
pip install orisys-0.4.2-cp310-cp310-win_amd64.whl
|
pip install orisys-0.5.0-cp310-cp310-win_amd64.whl
|
||||||
```
|
```
|
||||||
|
|
||||||
## 安装 CuPy(可选)
|
### 安装 CuPy(可选)
|
||||||
注意:如果您没有安装 CUDA,或者使用 CPU 运行,可以跳过这一步。
|
注意:如果您没有安装 CUDA,或者使用 CPU 运行,可以跳过这一步。
|
||||||
|
|
||||||
CuPy 是一个与 NumPy 类似但支持 GPU 加速的库,您需要根据您安装的 CUDA 版本选择对应的 CuPy 版本。
|
CuPy 是一个与 NumPy 类似但支持 GPU 加速的库,您需要根据您安装的 CUDA 版本选择对应的 CuPy 版本。
|
||||||
@@ -84,83 +110,88 @@ CuPy 是一个与 NumPy 类似但支持 GPU 加速的库,您需要根据您安
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 如果您的 CUDA 版本是 12.x
|
# 如果您的 CUDA 版本是 12.x
|
||||||
pip install orisys[cuda12]
|
pip install "orisys[cuda12]"
|
||||||
|
|
||||||
# 如果您的 CUDA 版本是 13.x
|
# 如果您的 CUDA 版本是 13.x
|
||||||
pip install orisys[cuda13]
|
pip install "orisys[cuda13]"
|
||||||
```
|
```
|
||||||
如果之前已经安装了 orisys,可以单独安装 CuPy:
|
如果之前已经安装了 orisys,可以单独安装 CuPy:
|
||||||
|
|
||||||
方法二(手动安装):
|
方法二(手动安装):
|
||||||
|
|
||||||
``` bash
|
```bash
|
||||||
# CUDA 12.x
|
# CUDA 12.x
|
||||||
pip install cupy-cuda12x>=12.3.0
|
pip install "cupy-cuda12x>=12.3.0"
|
||||||
|
|
||||||
# CUDA 13.x
|
# CUDA 13.x
|
||||||
pip install cupy-cuda13x>=13.0.0
|
pip install "cupy-cuda13x>=13.0.0"
|
||||||
```
|
```
|
||||||
|
|
||||||
## 测试安装
|
### 测试安装
|
||||||
安装完成后,在命令行输入” python“命令进入 Python 环境, 并导入 orisys 库并查看版本号,验证是否成功:
|
安装完成后,在命令行输入 `python` 命令进入 Python 环境,并导入 `orisys` 库查看版本号,验证是否成功:
|
||||||
``` bash
|
```bash
|
||||||
python
|
python
|
||||||
|
```
|
||||||
|
|
||||||
|
进入 Python 后继续输入:
|
||||||
|
|
||||||
|
```python
|
||||||
import orisys
|
import orisys
|
||||||
print(orisys.__version__)
|
print(orisys.__version__)
|
||||||
```
|
```
|
||||||
如果输出版本号(例如 0.4.2),则说明安装成功。
|
如果输出版本号(例如 0.5.0),则说明安装成功。
|
||||||
|
|
||||||
# 使用方法
|
## 使用方法
|
||||||
## 运行示例程序
|
### 运行示例程序
|
||||||
套件里面包含了多个例子,建议都在仓库根目录运行(即 `config/`、`examples/`、`assets/` 同级目录)。按产品入口如下:
|
套件里面包含了多个例子。运行示例前,请先进入 SDK 示例所在的文件夹,建议在仓库根目录运行(即 `config/`、`examples/`、`assets/` 同级目录)。如果您是通过 PyPI 安装并且本地没有示例文件,可以从上方项目链接下载示例。按产品入口如下:
|
||||||
|
|
||||||
1. `examples/products/ory_box/basic_pipeline.py`:ORY-BOX 复眼/拼接传感器基础 OpenCV 示例(默认配置 `ddjx01`)
|
1. `examples/basic_pipeline.py`:ORY-BOX 复眼/拼接传感器基础 OpenCV 示例(默认配置 `box`)
|
||||||
2. `examples/products/ory_mini/basic_pipeline.py`:ORY-MINI 基础 OpenCV 示例(默认配置 `mini`)
|
2. `examples/mini_pipeline.py`:ORY-MINI 基础 OpenCV 示例(默认配置 `mini`)
|
||||||
3. `examples/products/ory_finger/finger_pipeline.py`:ORY-FINGER 2D UV 最小流程(默认配置 `dd02`)
|
3. `examples/finger_pipeline.py`:ORY-FINGER 2D 显示最小流程(默认配置 `finger`)
|
||||||
4. `examples/products/ory_finger/finger_pipeline_3dview.py`:ORY-FINGER Qt 3D 可视化界面(默认配置 `dd02`)
|
4. `examples/finger_pipeline_3dview.py`:ORY-FINGER Qt 3D 可视化界面(默认配置 `finger`)
|
||||||
|
|
||||||
`dd02` 是当前 ORY-FINGER 的稳定别名,实际会解析到 `dd02-ov`。旧路径 `examples/basic_pipeline.py` / `examples/finger_pipeline*.py` 仍可作为兼容入口。
|
`finger` 是当前 ORY-FINGER 的产品配置名。
|
||||||
|
|
||||||
下面先介绍最基础的 ORY-BOX 运行方式。
|
下面先介绍最基础的 ORY-BOX 运行方式。
|
||||||
|
|
||||||
### Windows:
|
#### Windows
|
||||||
1. 检查是否有其他正在连接的USB摄像头设备:打开powershell, 运行命令`Get-PnpDevice -PresentOnly | Where-Object { $_.Class -match '^Camera' }`,如果没有的话不会有输出。
|
1. 检查是否有其他正在连接的 USB 摄像头设备:打开 PowerShell,运行命令 `Get-PnpDevice -PresentOnly | Where-Object { $_.Class -match '^Camera' }`,如果没有的话不会有输出。
|
||||||
2. 把传感器的USB头与电脑连接,再次运行步骤一的命令,会新增一行输出
|
2. 把传感器的 USB 头与电脑连接,再次运行步骤一的命令,会新增一行输出。
|
||||||
3. 如果第一步没有输出,传感器的ID就是“0”,如果第一步有一个设备,传感器的ID就是“1”,如此类推。
|
3. 如果第一步没有输出,传感器的 ID 就是 `0`;如果第一步有一个设备,传感器的 ID 就是 `1`,如此类推。
|
||||||
4. 在确定了传感器的ID(下方例子假设为“0”)后,打开Anaconda Prompt, 切换到工作路径,然后在命令行中输入:
|
4. 在确定了传感器的 ID(下方例子假设为 `0`)后,打开 Anaconda Prompt,切换到工作路径,然后在命令行中输入:
|
||||||
``` bash
|
```bash
|
||||||
python examples/products/ory_box/basic_pipeline.py -v 0 -c ddjx01
|
python examples/basic_pipeline.py -v 0 -c box
|
||||||
```
|
```
|
||||||
5. 程序会运行,并自动打开可视化窗口和打印资料,在窗口上按键盘”q”键可以停止运行,按”r”键可以重设数据
|
5. 程序会运行,并自动打开可视化窗口和打印资料。在窗口上按键盘 `q` 键可以停止运行,按 `r` 键可以重设数据。
|
||||||
|
|
||||||
### Ubuntu:
|
#### Ubuntu
|
||||||
1. 检查是否有其他正在连接的USB摄像头设备:打开终端, 运行命令`v4l2 –list-devices`,如果没有的话不会有输出。
|
1. 检查是否有其他正在连接的 USB 摄像头设备:打开终端,运行命令 `v4l2 --list-devices`,如果没有的话不会有输出。
|
||||||
2. 把传感器的USB头与电脑连接,再次运行步骤一的命令,会新增输出
|
2. 把传感器的 USB 头与电脑连接,再次运行步骤一的命令,会新增输出。
|
||||||
3. 根据新增的输出,例如”/dev/video0”,确定传感器的ID为“0”, 如果是”/dev/video3”的话ID为“3” ,如此类推。
|
3. 根据新增的输出,例如 `/dev/video0`,确定传感器的 ID 为 `0`;如果是 `/dev/video3`,ID 为 `3`,如此类推。
|
||||||
4. 在确定了传感器的ID(下方例子假设为“0”)后,打开终端, 激活相应的python环境,切换到工作路径,然后在命令行中输入:
|
4. 在确定了传感器的 ID(下方例子假设为 `0`)后,打开终端,激活相应的 Python 环境,切换到工作路径,然后在命令行中输入:
|
||||||
``` bash
|
```bash
|
||||||
python examples/products/ory_box/basic_pipeline.py -v 0 -c ddjx01
|
python examples/basic_pipeline.py -v 0 -c box
|
||||||
```
|
```
|
||||||
5. 程序会运行,并自动打开可视化窗口和打印资料,在窗口上按键盘”q”键可以停止运行,按”r”键可以重设数据
|
5. 程序会运行,并自动打开可视化窗口和打印资料。在窗口上按键盘 `q` 键可以停止运行,按 `r` 键可以重设数据。
|
||||||
|
|
||||||
### ORY-FINGER UV 示例
|
#### ORY-FINGER 示例
|
||||||
|
|
||||||
如果您使用的是 ORY-FINGER(当前配置别名 `dd02`),可运行:
|
如果您使用的是 ORY-FINGER(配置名 `finger`),可运行:
|
||||||
|
|
||||||
``` bash
|
```bash
|
||||||
python examples/products/ory_finger/finger_pipeline.py --camera_id 0 --config dd02
|
python examples/finger_pipeline.py --camera_id 0 --config finger
|
||||||
```
|
```
|
||||||
|
|
||||||
如果输入来自视频文件,也可以改用:
|
如果输入来自视频文件,也可以改用:
|
||||||
|
|
||||||
``` bash
|
```bash
|
||||||
python examples/products/ory_finger/finger_pipeline.py --video ./data/sample.mp4 --config dd02
|
python examples/finger_pipeline.py --video ./data/sample.mp4 --config finger
|
||||||
```
|
```
|
||||||
|
|
||||||
该示例会:
|
该示例会:
|
||||||
|
|
||||||
- 读取相机或视频源
|
- 读取相机或视频源
|
||||||
- 将图像展开到 UV 平面
|
- 将图像展开到平面
|
||||||
- 计算光流、法向/切向力和接触区域
|
- 计算光流、法向/切向力和接触区域
|
||||||
- 在 OpenCV 窗口中显示展开后的光流箭头图
|
- 在 OpenCV 窗口中显示展开后的光流箭头图
|
||||||
|
|
||||||
@@ -170,20 +201,19 @@ python examples/products/ory_finger/finger_pipeline.py --video ./data/sample.mp4
|
|||||||
|
|
||||||
例如:
|
例如:
|
||||||
|
|
||||||
``` bash
|
```bash
|
||||||
python examples/products/ory_finger/finger_pipeline.py --camera_id 0 --config dd02
|
python examples/finger_pipeline.py --camera_id 0 --config finger
|
||||||
```
|
```
|
||||||
|
|
||||||
### ORY-FINGER 3D 可视化界面
|
#### ORY-FINGER 3D 可视化界面
|
||||||
|
|
||||||
如果要使用 GUI ,安装GUI依赖:
|
如果要使用 GUI,先安装 GUI 依赖:
|
||||||
``` bash
|
```bash
|
||||||
pip install open3d
|
pip install "orisys[gui]"
|
||||||
pip install pyqt5 pyqtgraph
|
|
||||||
```
|
```
|
||||||
然后运行:
|
然后运行:
|
||||||
``` bash
|
```bash
|
||||||
python examples/products/ory_finger/finger_pipeline_3dview.py --video 0 --config dd02
|
python examples/finger_pipeline_3dview.py --video 0 --config finger
|
||||||
```
|
```
|
||||||
|
|
||||||
该界面会提供:
|
该界面会提供:
|
||||||
@@ -192,5 +222,5 @@ python examples/products/ory_finger/finger_pipeline_3dview.py --video 0 --config
|
|||||||
- 力曲线
|
- 力曲线
|
||||||
- 3D 视图
|
- 3D 视图
|
||||||
|
|
||||||
## 开发套件使用方法
|
### 开发套件使用方法
|
||||||
参考开发套件附带的开发文档。
|
参考开发套件附带的开发文档。
|
||||||
Binary file not shown.
@@ -1,167 +0,0 @@
|
|||||||
# Publishing Orisys to PyPI
|
|
||||||
|
|
||||||
This document covers the **Linux manylinux binary wheel** path (Nuitka-compiled
|
|
||||||
`orisys` for CPython 3.10 / 3.11 / 3.12). Windows wheels are still built on
|
|
||||||
Windows hosts; they cannot be produced inside the manylinux container.
|
|
||||||
|
|
||||||
## Why Docker?
|
|
||||||
|
|
||||||
PyPI rejects wheels tagged `manylinux_x86_64` (missing glibc ABI version). A
|
|
||||||
compliant tag looks like:
|
|
||||||
|
|
||||||
```text
|
|
||||||
orisys-0.4.2-cp310-cp310-manylinux_2_28_x86_64.whl
|
|
||||||
orisys-0.4.2-cp311-cp311-manylinux_2_28_x86_64.whl
|
|
||||||
orisys-0.4.2-cp312-cp312-manylinux_2_28_x86_64.whl
|
|
||||||
```
|
|
||||||
|
|
||||||
Those tags are produced by building inside the official manylinux image and
|
|
||||||
running `auditwheel repair`. On Windows, use **Docker Desktop in Linux container
|
|
||||||
mode**.
|
|
||||||
|
|
||||||
> Note: we use `manylinux_2_28_x86_64` (not 2014). Recent NumPy/SciPy no longer
|
|
||||||
> provide manylinux2014 wheels and fail to build from source on that older GCC.
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
1. Docker Desktop installed and running (**Linux containers**, not Windows containers).
|
|
||||||
2. Repository checked out locally (this tree is bind-mounted into the container).
|
|
||||||
3. Version bumped in `src/orisys/__init__.py` (`__version__`) before every upload.
|
|
||||||
PyPI never allows re-uploading the same filename/version.
|
|
||||||
4. PyPI / TestPyPI project-scoped API tokens (never commit tokens or `.pypirc` with secrets).
|
|
||||||
|
|
||||||
Quick Docker check:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
docker version --format "{{.Server.Os}}"
|
|
||||||
# expect: linux
|
|
||||||
docker run --rm hello-world
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build manylinux wheels (3.10 / 3.11 / 3.12)
|
|
||||||
|
|
||||||
From the repository root. Prefer PowerShell, or from **cmd.exe**:
|
|
||||||
|
|
||||||
```bat
|
|
||||||
powershell -ExecutionPolicy Bypass -File .\build\build_manylinux_docker.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
PowerShell:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
.\build\build_manylinux_docker.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
Default builds **all three** ABIs. To build a subset:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
.\build\build_manylinux_docker.ps1 -PythonVersions 3.10,3.12
|
|
||||||
.\build\build_manylinux_docker.ps1 -PythonVersions 3.11
|
|
||||||
```
|
|
||||||
|
|
||||||
What this does:
|
|
||||||
|
|
||||||
1. Builds `orisys-manylinux` from [`build/Dockerfile.manylinux`](../build/Dockerfile.manylinux)
|
|
||||||
(`quay.io/pypa/manylinux_2_28_x86_64` with Nuitka/auditwheel in cp310/cp311/cp312).
|
|
||||||
2. Runs [`build/manylinux_entrypoint.sh`](../build/manylinux_entrypoint.sh), which loops:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
/opt/python/cp310-cp310/bin/python build/build_nuitka.py --target pypi-linux
|
|
||||||
/opt/python/cp311-cp311/bin/python build/build_nuitka.py --target pypi-linux
|
|
||||||
/opt/python/cp312-cp312/bin/python build/build_nuitka.py --target pypi-linux
|
|
||||||
```
|
|
||||||
|
|
||||||
3. For each ABI: Nuitka compile → wheel → `auditwheel repair` → isolated smoke test.
|
|
||||||
4. Artifacts land in:
|
|
||||||
|
|
||||||
- `dist/repaired/orisys-*-manylinux_2_28_x86_64.whl` (upload candidates)
|
|
||||||
- `SDK/<version>/` (same compliant wheels)
|
|
||||||
|
|
||||||
Expect roughly **3×** the wall time of a single-ABI build.
|
|
||||||
|
|
||||||
Rebuild without refreshing the image:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
.\build\build_manylinux_docker.ps1 -SkipImageBuild
|
|
||||||
```
|
|
||||||
|
|
||||||
Equivalent manual `docker` invocation:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
docker build -f build/Dockerfile.manylinux -t orisys-manylinux .
|
|
||||||
docker run --rm `
|
|
||||||
-e ORISYS_BUILD_TARGET=pypi-linux `
|
|
||||||
-e ORISYS_PYTHON_TAGS=cp310-cp310,cp311-cp311,cp312-cp312 `
|
|
||||||
-v "${PWD}:/src" `
|
|
||||||
orisys-manylinux
|
|
||||||
```
|
|
||||||
|
|
||||||
### Acceptance checks before upload
|
|
||||||
|
|
||||||
- Each filename contains `cp310` / `cp311` / `cp312` and `manylinux_2_28_x86_64`
|
|
||||||
(or another accepted manylinux tag such as `manylinux_2_17_x86_64`).
|
|
||||||
- Isolated smoke test printed `public API imports OK` for every ABI.
|
|
||||||
- Do **not** upload any wheel still tagged `linux_x86_64` or bare `manylinux_x86_64`.
|
|
||||||
|
|
||||||
Optional re-verify on a Linux machine:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python build/verify.py dist/repaired/orisys-*-cp310-cp310-manylinux_*.whl
|
|
||||||
```
|
|
||||||
|
|
||||||
## Promote: TestPyPI then PyPI
|
|
||||||
|
|
||||||
Install upload tooling in a clean environment:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
python -m pip install --upgrade twine
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1. Upload to TestPyPI
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$env:TWINE_USERNAME = "__token__"
|
|
||||||
$env:TWINE_PASSWORD = "<TestPyPI API token>"
|
|
||||||
|
|
||||||
python -m twine check dist/repaired/*.whl
|
|
||||||
python -m twine upload --repository testpypi dist/repaired/*.whl
|
|
||||||
```
|
|
||||||
|
|
||||||
Install from TestPyPI in a fresh venv (pull runtime deps from real PyPI), testing
|
|
||||||
each Python you care about:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
py -3.10 -m venv .venv-testpypi310
|
|
||||||
.\.venv-testpypi310\Scripts\Activate.ps1
|
|
||||||
python -m pip install --index-url https://test.pypi.org/simple/ `
|
|
||||||
--extra-index-url https://pypi.org/simple `
|
|
||||||
"orisys==<version>"
|
|
||||||
python -c "import orisys; print(orisys.__version__)"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Upload the same artifacts to PyPI
|
|
||||||
|
|
||||||
Only after TestPyPI install/import succeeds:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$env:TWINE_USERNAME = "__token__"
|
|
||||||
$env:TWINE_PASSWORD = "<PyPI API token>"
|
|
||||||
|
|
||||||
python -m twine upload dist/repaired/*.whl
|
|
||||||
```
|
|
||||||
|
|
||||||
Prefer CI **Trusted Publishing** (OIDC) for production uploads so long-lived tokens
|
|
||||||
are not stored on developer machines.
|
|
||||||
|
|
||||||
## Related local targets
|
|
||||||
|
|
||||||
| Command | Purpose |
|
|
||||||
|---------|---------|
|
|
||||||
| `python build/build_nuitka.py` | Internal SDK wheel on the current host (Windows `.pyd` or Linux `.so`) |
|
|
||||||
| `python build/build_nuitka.py --target pypi-linux` | Linux-only; requires manylinux + CPython 3.10–3.12 |
|
|
||||||
| `.\build\build_nuitka_win.bat` | Windows wheels for py310/py311/py312 conda envs |
|
|
||||||
| `.\build\build_manylinux_docker.ps1` | Linux manylinux wheels for 3.10/3.11/3.12 via Docker |
|
|
||||||
| `python -m build` | Pure-Python / sdist path from `pyproject.toml` (no Nuitka) |
|
|
||||||
|
|
||||||
Windows binary wheels and Linux manylinux wheels must be built on their respective
|
|
||||||
platforms. Never rename a platform tag to bypass PyPI validation.
|
|
||||||
+13
-17
@@ -1,54 +1,50 @@
|
|||||||
# Examples
|
# Examples
|
||||||
|
|
||||||
Product-facing demos live under `examples/products/`. Shared helpers stay in
|
Runnable demos live at the `examples/` root. Shared helpers stay in
|
||||||
`pipeline/`, `viz/`, and `gui/`. Run commands from the repository root.
|
`pipeline/`, `viz/`, and `gui/`. Run commands from the repository root.
|
||||||
|
|
||||||
## Product Entry Points
|
## Product Entry Points
|
||||||
|
|
||||||
| Product | Entry | Default config |
|
| Product | Entry | Default config |
|
||||||
|---------|-------|----------------|
|
|---------|-------|----------------|
|
||||||
| ORY-BOX | `products/ory_box/basic_pipeline.py` | `ddjx01` |
|
| ORY-BOX | `basic_pipeline.py` | `box` |
|
||||||
| ORY-MINI | `products/ory_mini/basic_pipeline.py` | `mini` |
|
| ORY-MINI | `mini_pipeline.py` | `mini` |
|
||||||
| ORY-FINGER (2D) | `products/ory_finger/finger_pipeline.py` | `dd02` |
|
| ORY-FINGER (2D) | `finger_pipeline.py` | `finger` |
|
||||||
| ORY-FINGER (3D Qt) | `products/ory_finger/finger_pipeline_3dview.py` | `dd02` |
|
| ORY-FINGER (3D Qt) | `finger_pipeline_3dview.py` | `finger` |
|
||||||
|
|
||||||
`dd02` is the stable ORY-FINGER alias and currently resolves to `dd02-ov`.
|
`finger` is the public ORY-FINGER config stem. `dd02` remains a compatibility alias for `dd02-ov`.
|
||||||
|
|
||||||
Compatibility wrappers still exist at the `examples/` root
|
|
||||||
(`basic_pipeline.py`, `finger_pipeline.py`, `finger_pipeline_3dview.py`) and
|
|
||||||
forward to the product entries above.
|
|
||||||
|
|
||||||
## Typical Usage
|
## Typical Usage
|
||||||
|
|
||||||
ORY-BOX:
|
ORY-BOX:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python examples/products/ory_box/basic_pipeline.py -v 0 -c ddjx01
|
python examples/basic_pipeline.py -v 0 -c box
|
||||||
```
|
```
|
||||||
|
|
||||||
ORY-MINI:
|
ORY-MINI:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python examples/products/ory_mini/basic_pipeline.py -v 0 -c mini
|
python examples/mini_pipeline.py -v 0 -c mini
|
||||||
```
|
```
|
||||||
|
|
||||||
ORY-FINGER 2D:
|
ORY-FINGER 2D:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python examples/products/ory_finger/finger_pipeline.py --camera_id 0 --config dd02
|
python examples/finger_pipeline.py --camera_id 0 --config finger
|
||||||
```
|
```
|
||||||
|
|
||||||
ORY-FINGER video file:
|
ORY-FINGER video file:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python examples/products/ory_finger/finger_pipeline.py --video ./data/sample.mp4 --config dd02
|
python examples/finger_pipeline.py --video ./data/sample.mp4 --config finger
|
||||||
```
|
```
|
||||||
|
|
||||||
ORY-FINGER 3D Qt dashboard:
|
ORY-FINGER 3D Qt dashboard:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install -e ".[gui]"
|
pip install -e ".[gui]"
|
||||||
python examples/products/ory_finger/finger_pipeline_3dview.py --video 0 --config dd02
|
python examples/finger_pipeline_3dview.py --video 0 --config finger
|
||||||
```
|
```
|
||||||
|
|
||||||
## Other Root Examples
|
## Other Root Examples
|
||||||
@@ -70,10 +66,10 @@ pip install -e ".[gui]"
|
|||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- `products/ory_finger/finger_pipeline_3dview.py` is the main interactive finger demo.
|
- `finger_pipeline_3dview.py` is the main interactive finger demo.
|
||||||
It uses `gui/app_paths.py` so the same entry works in editable and packaged runs.
|
It uses `gui/app_paths.py` so the same entry works in editable and packaged runs.
|
||||||
- Dashboard controls: click **启动** to start acquisition; use **Record CSV / Stop CSV**
|
- Dashboard controls: click **启动** to start acquisition; use **Record CSV / Stop CSV**
|
||||||
and the CSV save-directory browser to log contact/force data; `Q` quits, `R` resets,
|
and the CSV save-directory browser to log contact/force data; `Q` quits, `R` resets,
|
||||||
`S` starts video recording, `E` stops video recording, and `Esc` toggles fullscreen.
|
`S` starts video recording, `E` stops video recording, and `Esc` toggles fullscreen.
|
||||||
- `products/ory_finger/finger_pipeline.py` is the leanest finger example if you want to
|
- `finger_pipeline.py` is the leanest finger example if you want to
|
||||||
validate UV unroll + force/contact processing without the full dashboard.
|
validate UV unroll + force/contact processing without the full dashboard.
|
||||||
|
|||||||
+125
-13
@@ -1,21 +1,133 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Compatibility wrapper for ORY-BOX.
|
|
||||||
|
|
||||||
Prefer:
|
|
||||||
python examples/products/ory_box/basic_pipeline.py -v 0 -c ddjx01
|
|
||||||
"""
|
"""
|
||||||
|
ORY-BOX:单传感器基础流程
|
||||||
|
|
||||||
from __future__ import annotations
|
本示例演示 Orisys SDK 的标准处理流程:
|
||||||
|
1. 初始化传感器(摄像头或视频文件)
|
||||||
|
2. 获取图像并计算形变
|
||||||
|
3. 按需计算接触区域
|
||||||
|
4. 读取并可视化结果
|
||||||
|
|
||||||
import sys
|
SDK version: 0.5.0
|
||||||
from pathlib import Path
|
"""
|
||||||
|
import os
|
||||||
|
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||||
|
import cv2
|
||||||
|
import orisys
|
||||||
|
import numpy as np
|
||||||
|
import argparse
|
||||||
|
|
||||||
_EXAMPLES_DIR = Path(__file__).resolve().parent
|
def main():
|
||||||
if str(_EXAMPLES_DIR) not in sys.path:
|
parser = argparse.ArgumentParser()
|
||||||
sys.path.insert(0, str(_EXAMPLES_DIR))
|
parser.add_argument("--video","-v", type=str, default="0", help="视频源:摄像头编号或视频文件路径")
|
||||||
|
parser.add_argument("--config","-c", type=str, default="box", help="配置文件名称或路径")
|
||||||
|
parser.add_argument("--verbose","-verbose", type=bool, default=True, help="是否输出详细日志")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
from products.ory_box.basic_pipeline import main
|
# 步骤 1:创建传感器对象(必需)
|
||||||
|
# config_name 可以是内置配置名称,也可以是自定义配置文件路径
|
||||||
|
# cal_path 为标定文件路径;每台设备建议使用独立的标定文件
|
||||||
|
|
||||||
|
# 输入既可以是摄像头编号,也可以是视频文件路径
|
||||||
|
if args.video.isdigit():
|
||||||
|
sensor = orisys.Sensor(int(args.video), config_name=args.config, verbose=args.verbose)
|
||||||
|
else:
|
||||||
|
sensor = orisys.Sensor(args.video, config_name=args.config, verbose=args.verbose)
|
||||||
|
|
||||||
|
print("\n按键说明:'q' 退出程序,'r' 重置追踪器。")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# 步骤 2:数据处理
|
||||||
|
# 步骤 2.1:获取并拼接图像(必需)
|
||||||
|
sensor.get_img()
|
||||||
|
# 步骤 2.2:计算形变场(必需);返回值表示当前帧是否检测到接触/运动
|
||||||
|
is_contact_now = sensor.compute_deformation()
|
||||||
|
# 步骤 2.3:仅在当前帧检测到接触/运动时计算接触区域
|
||||||
|
# if is_contact_now:
|
||||||
|
sensor.compute_contact()
|
||||||
|
|
||||||
|
|
||||||
|
# 步骤 2.4:读取结果(FNORMAL/FSHEAR* 在有 sensors/box/force_calibration.json 时为标定后牛顿力;
|
||||||
|
# *_RAW 始终为原始积分量)
|
||||||
|
(
|
||||||
|
fps,
|
||||||
|
flow,
|
||||||
|
vnormal,
|
||||||
|
img,
|
||||||
|
contour,
|
||||||
|
centroid,
|
||||||
|
depth_map,
|
||||||
|
fnormal,
|
||||||
|
fshearx,
|
||||||
|
fsheary,
|
||||||
|
fnormal_raw,
|
||||||
|
fshearx_raw,
|
||||||
|
fsheary_raw,
|
||||||
|
) = sensor.read_info(
|
||||||
|
sensor.info.FPS,
|
||||||
|
sensor.info.VRAW,
|
||||||
|
sensor.info.VNORMAL,
|
||||||
|
sensor.info.IMG,
|
||||||
|
sensor.info.CONTOUR,
|
||||||
|
sensor.info.CENTROID,
|
||||||
|
sensor.info.DEPTH,
|
||||||
|
sensor.info.FNORMAL,
|
||||||
|
sensor.info.FSHEARX,
|
||||||
|
sensor.info.FSHEARY,
|
||||||
|
sensor.info.FNORMAL_RAW,
|
||||||
|
sensor.info.FSHEARX_RAW,
|
||||||
|
sensor.info.FSHEARY_RAW,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 步骤 3:可视化显示
|
||||||
|
# 步骤 3.1:显示形变矢量场
|
||||||
|
arrows = orisys.util.draw_arrows(
|
||||||
|
img,
|
||||||
|
flow,
|
||||||
|
threshold=2,
|
||||||
|
grid_spacing=20,
|
||||||
|
arrow_scale=1.0
|
||||||
|
)
|
||||||
|
cv2.imshow("arrow", arrows)
|
||||||
|
|
||||||
|
# 步骤 3.2:显示接触区域和质心
|
||||||
|
image_with_foe = orisys.util.draw_contact(img, contour, centroid)
|
||||||
|
cv2.imshow("contact", image_with_foe)
|
||||||
|
|
||||||
|
# 步骤 3.3:显示深度图
|
||||||
|
# 将深度值归一化到 0-255 范围
|
||||||
|
div_abs = depth_map
|
||||||
|
if div_abs.max() > div_abs.min():
|
||||||
|
div_normalized = ((div_abs - div_abs.min()) / (div_abs.max() - div_abs.min()) * 255).astype(np.uint8)
|
||||||
|
else:
|
||||||
|
div_normalized = np.zeros_like(div_abs, dtype=np.uint8)
|
||||||
|
|
||||||
|
# 应用伪彩色映射
|
||||||
|
cv2.imshow("depth", cv2.applyColorMap(div_normalized, cv2.COLORMAP_JET))
|
||||||
|
|
||||||
|
# 步骤 3.4:打印关键结果(标定力 + 原始力)
|
||||||
|
print(
|
||||||
|
f"FPS={fps:.2f}, "
|
||||||
|
f"法向力={fnormal:.4f} N (raw={fnormal_raw:.4f}), "
|
||||||
|
f"切向力X={fshearx:.4f} N (raw={fshearx_raw:.4f}), "
|
||||||
|
f"切向力Y={fsheary:.4f} N (raw={fsheary_raw:.4f}), "
|
||||||
|
f"深度图尺寸={depth_map.shape}, 原始分辨率={sensor.img_size_raw}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 步骤 3.5:键盘控制
|
||||||
|
key = cv2.waitKey(1) & 0xFF
|
||||||
|
if key == ord("q"):
|
||||||
|
print("\n正在退出示例程序...")
|
||||||
|
break
|
||||||
|
if key == ord("r"):
|
||||||
|
# 当光流追踪出现漂移时,可手动重置追踪器
|
||||||
|
sensor.reset()
|
||||||
|
print("追踪器已重置。")
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# 4. 释放资源
|
||||||
|
# =========================================================================
|
||||||
|
sensor.disconnect() # 释放视频源及相关资源
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
+101
-10
@@ -1,20 +1,111 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Compatibility wrapper for ORY-FINGER 2D pipeline.
|
"""
|
||||||
|
ORY-FINGER:Minimum UV-unroll finger tactile pipeline.
|
||||||
|
|
||||||
Prefer:
|
Offline: load Blender export -> build_or_load_domain -> unroll_map_uv.npz
|
||||||
python examples/products/ory_finger/finger_pipeline.py --camera_id 0 --config dd02
|
Runtime: camera frame -> FrameUnwarper -> TactileProcessor -> forces/contact
|
||||||
|
|
||||||
|
Recommended entry:
|
||||||
|
python examples/finger_pipeline.py --camera_id 0 --config finger
|
||||||
|
|
||||||
|
Current product config: ``finger`` (``dd02`` still resolves to ``dd02-ov``).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
import argparse
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
_EXAMPLES_DIR = Path(__file__).resolve().parent
|
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
|
||||||
if str(_EXAMPLES_DIR) not in sys.path:
|
|
||||||
sys.path.insert(0, str(_EXAMPLES_DIR))
|
|
||||||
|
|
||||||
from products.ory_finger.finger_pipeline import main
|
import cv2
|
||||||
|
import orisys
|
||||||
|
from orisys.finger import FingerRuntime
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Minimum UV-unroll finger tactile pipeline")
|
||||||
|
parser.add_argument("--camera_id", type=int, default=0)
|
||||||
|
parser.add_argument("--video","-v", help="Video file path (overrides camera)")
|
||||||
|
parser.add_argument("--config", "-c", default="finger", help="Orisys SDK config name or path")
|
||||||
|
parser.add_argument("--show", action="store_true", default=True, help="Show unrolled image window")
|
||||||
|
parser.add_argument("--verbose", action="store_true", help="SDK verbose logs")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
video = args.camera_id
|
||||||
|
if args.video:
|
||||||
|
video = args.video
|
||||||
|
runtime = FingerRuntime.open(
|
||||||
|
config=args.config,
|
||||||
|
video=video,
|
||||||
|
verbose=args.verbose,
|
||||||
|
)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
print(str(exc), file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if args.verbose:
|
||||||
|
geometry = runtime.geometry
|
||||||
|
print(
|
||||||
|
f"UV geometry: native={geometry.native_width}x{geometry.native_height}, "
|
||||||
|
f"source={geometry.source_width}x{geometry.source_height} "
|
||||||
|
f"output={geometry.out_width}x{geometry.out_height} "
|
||||||
|
)
|
||||||
|
|
||||||
|
runtime.warmup(color=False)
|
||||||
|
tactile = runtime.tactile
|
||||||
|
|
||||||
|
print("Press 'q' to quit, 'r' to reset tracker.")
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
ok, unrolled = runtime.grab_and_process(color=False)
|
||||||
|
if not ok:
|
||||||
|
break
|
||||||
|
|
||||||
|
tactile.compute_deformation()
|
||||||
|
tactile.compute_contact()
|
||||||
|
|
||||||
|
fps, flow, vnormal, img, contour, centroid, depth_map, fnormal, fshearx, fsheary = tactile.read_info(
|
||||||
|
tactile.info.FPS,
|
||||||
|
tactile.info.VRAW,
|
||||||
|
tactile.info.VNORMAL,
|
||||||
|
tactile.info.IMG,
|
||||||
|
tactile.info.CONTOUR,
|
||||||
|
tactile.info.CENTROID,
|
||||||
|
tactile.info.DEPTH,
|
||||||
|
tactile.info.FNORMAL,
|
||||||
|
tactile.info.FSHEARX,
|
||||||
|
tactile.info.FSHEARY,
|
||||||
|
)
|
||||||
|
|
||||||
|
status = (
|
||||||
|
f"FPS={fps:.2f} normal={fnormal:.2f} shear=({fshearx:.2f}, {fsheary:.2f}) "
|
||||||
|
f"mask={tactile.mask_stats['coverage']:.2f}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if args.show:
|
||||||
|
arrows = orisys.util.draw_arrows(
|
||||||
|
unrolled,
|
||||||
|
flow,
|
||||||
|
threshold=0.4,
|
||||||
|
grid_spacing=10,
|
||||||
|
arrow_scale=1.0,
|
||||||
|
valid_mask=tactile.valid_mask,
|
||||||
|
)
|
||||||
|
cv2.imshow("UV Unrolled", arrows)
|
||||||
|
|
||||||
|
print(status, end="\r")
|
||||||
|
|
||||||
|
key = cv2.waitKey(1) & 0xFF
|
||||||
|
if key == ord("q"):
|
||||||
|
print("\nExiting.")
|
||||||
|
break
|
||||||
|
if key == ord("r"):
|
||||||
|
tactile.reset()
|
||||||
|
print("\nTracker reset.")
|
||||||
|
finally:
|
||||||
|
runtime.close()
|
||||||
|
cv2.destroyAllWindows()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1,20 +1,90 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Compatibility wrapper for ORY-FINGER 3D Qt dashboard.
|
"""
|
||||||
|
ORY-FINGER:UV-unroll finger tactile pipeline with unified Qt dashboard.
|
||||||
|
|
||||||
Prefer:
|
Where to customize the GUI:
|
||||||
python examples/products/ory_finger/finger_pipeline_3dview.py --video 0 --config dd02
|
Layout (sizes, proportions) -> examples/gui/viewer3d/layout.py
|
||||||
|
Colors / QSS -> examples/gui/viewer3d/styles.py
|
||||||
|
Widget rendering logic -> examples/gui/viewer3d/widgets/
|
||||||
|
Window behavior -> examples/gui/viewer3d/dashboard.py
|
||||||
|
|
||||||
|
Recommended entry:
|
||||||
|
python examples/finger_pipeline_3dview.py --video 0 --config finger
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
import argparse
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
|
||||||
|
|
||||||
|
# Make shared packages importable when launched as a file path.
|
||||||
_EXAMPLES_DIR = Path(__file__).resolve().parent
|
_EXAMPLES_DIR = Path(__file__).resolve().parent
|
||||||
if str(_EXAMPLES_DIR) not in sys.path:
|
if str(_EXAMPLES_DIR) not in sys.path:
|
||||||
sys.path.insert(0, str(_EXAMPLES_DIR))
|
sys.path.insert(0, str(_EXAMPLES_DIR))
|
||||||
|
|
||||||
from products.ory_finger.finger_pipeline_3dview import main
|
from gui.app_paths import (
|
||||||
|
configure_frozen_runtime,
|
||||||
|
default_config_path,
|
||||||
|
ensure_examples_import_path,
|
||||||
|
require_gui_extras,
|
||||||
|
)
|
||||||
|
|
||||||
|
configure_frozen_runtime()
|
||||||
|
ensure_examples_import_path()
|
||||||
|
require_gui_extras()
|
||||||
|
|
||||||
|
from gui import run_dashboard
|
||||||
|
from pipeline import PipelineConfig
|
||||||
|
from viz import DEFAULT_FINGER_VIZ_POSE
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_args():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="UV-unroll finger tactile pipeline with unified Qt dashboard"
|
||||||
|
)
|
||||||
|
parser.add_argument("--video", "-v", default="0", help="Camera index or video file path")
|
||||||
|
parser.add_argument("--config", "-c", default=default_config_path(), help="Orisys SDK config")
|
||||||
|
parser.add_argument("--cal", default="", help="Optional calibration file path")
|
||||||
|
parser.add_argument("--verbose", action="store_true", help="SDK verbose logs")
|
||||||
|
parser.add_argument(
|
||||||
|
"--mesh-map",
|
||||||
|
choices=["flow", "depth"],
|
||||||
|
default="depth",
|
||||||
|
help="Color the 3D mesh from flow magnitude or contact depth.",
|
||||||
|
)
|
||||||
|
parser.add_argument("--threshold", type=float, default=None, help="Mesh/arrow threshold")
|
||||||
|
parser.add_argument("--no-3d", action="store_true", help="Disable 3D mesh view")
|
||||||
|
parser.add_argument(
|
||||||
|
"--viz-pose",
|
||||||
|
default=DEFAULT_FINGER_VIZ_POSE,
|
||||||
|
help="world_to_cam.npy for the default view; pass 'none' to skip",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--cutoff",
|
||||||
|
type=float,
|
||||||
|
default=5.0,
|
||||||
|
help="Low-pass cutoff for force curves (Hz)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--profile",
|
||||||
|
action="store_true",
|
||||||
|
help="Print per-stage timing to the log panel / console",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--profile-every",
|
||||||
|
type=int,
|
||||||
|
default=30,
|
||||||
|
help="When --profile is set, print rolling averages every N frames",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = _parse_args()
|
||||||
|
config = PipelineConfig.from_args(args)
|
||||||
|
sys.exit(run_dashboard(config))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -27,8 +27,17 @@ def resource_path(*parts: str) -> Path:
|
|||||||
|
|
||||||
|
|
||||||
def default_config_path() -> str:
|
def default_config_path() -> str:
|
||||||
"""Default ORY-FINGER config name (resolves to dd02-ov via alias)."""
|
"""Default ORY-FINGER config name (packaged stem ``finger``)."""
|
||||||
return "dd02"
|
return "finger"
|
||||||
|
|
||||||
|
|
||||||
|
def require_gui_extras() -> None:
|
||||||
|
try:
|
||||||
|
import open3d # noqa: F401
|
||||||
|
from PyQt5 import QtWidgets # noqa: F401
|
||||||
|
import pyqtgraph # noqa: F401
|
||||||
|
except ImportError as exc:
|
||||||
|
raise SystemExit('GUI example requires: pip install "orisys[gui]"') from exc
|
||||||
|
|
||||||
|
|
||||||
def configure_frozen_runtime() -> None:
|
def configure_frozen_runtime() -> None:
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"video_source": "D:/repo/orisys-sdk/data/dd02/0607_1.mp4",
|
"video_source": "D:/repo/orisys-sdk/data/dd02-0703/1.mp4",
|
||||||
"config_path": "./config/dd02-ov.json",
|
"config_path": "finger",
|
||||||
"finger_assets_dir": "D:/repo/orisys-sdk/assets/finger/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,
|
||||||
|
"sensor_bundle_dir": "sensors/finger"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ class QtViewerWindow(QMainWindow):
|
|||||||
self.btn_config.setObjectName("btnConfig")
|
self.btn_config.setObjectName("btnConfig")
|
||||||
self.btn_config.clicked.connect(self._select_config)
|
self.btn_config.clicked.connect(self._select_config)
|
||||||
bar.addWidget(self.btn_config)
|
bar.addWidget(self.btn_config)
|
||||||
self.config_edit = QLineEdit("./config/ddjx01.json")
|
self.config_edit = QLineEdit("box")
|
||||||
bar.addWidget(self.config_edit, 1)
|
bar.addWidget(self.config_edit, 1)
|
||||||
|
|
||||||
self.btn_reset = QPushButton("重置")
|
self.btn_reset = QPushButton("重置")
|
||||||
|
|||||||
@@ -570,6 +570,8 @@ class FingerDashboardWindow(QMainWindow):
|
|||||||
camera_id = str(camera_id).strip()
|
camera_id = str(camera_id).strip()
|
||||||
if not camera_id:
|
if not camera_id:
|
||||||
return
|
return
|
||||||
|
# Explicit camera pick clears sticky file mode and persists the live source.
|
||||||
|
self.settings_panel.set_video_source(camera_id)
|
||||||
self.config.video = camera_id
|
self.config.video = camera_id
|
||||||
self._save_preferences()
|
self._save_preferences()
|
||||||
self._update_play_enabled_for_source()
|
self._update_play_enabled_for_source()
|
||||||
@@ -588,26 +590,48 @@ 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,
|
||||||
|
normalize_assets_preference,
|
||||||
|
packaged_finger_assets_root,
|
||||||
|
)
|
||||||
|
|
||||||
|
start = packaged_finger_assets_root()
|
||||||
|
start_dir = (
|
||||||
|
str(start)
|
||||||
|
if start is not None
|
||||||
|
else (self.settings_panel.finger_export_dir() or default_finger_assets_dir())
|
||||||
|
)
|
||||||
path = QFileDialog.getExistingDirectory(
|
path = QFileDialog.getExistingDirectory(
|
||||||
self,
|
self,
|
||||||
"选择 finger 资产目录",
|
"选择 finger 资产目录",
|
||||||
self.settings_panel.finger_export_dir() or "assets/finger",
|
start_dir,
|
||||||
)
|
)
|
||||||
if path:
|
if path:
|
||||||
self.settings_panel.set_finger_export_dir(path)
|
assets = normalize_assets_preference(path)
|
||||||
self.config.export_dir = path
|
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:
|
||||||
|
|||||||
@@ -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,94 @@ 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
|
||||||
|
# Keep a user-selected custom directory that still exists.
|
||||||
|
if "sensors/finger" in posix or path.name == "30fps-0703":
|
||||||
|
return text if path.is_absolute() else default
|
||||||
|
return text
|
||||||
|
|
||||||
|
# Missing old absolute/repo paths (assets/finger, ./config era) -> packaged default.
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
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 +133,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 +148,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
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
"""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 _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:
|
||||||
|
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
|
||||||
|
# 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(
|
||||||
|
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:
|
||||||
|
packaged = _packaged_finger_preset()
|
||||||
|
if packaged is not None:
|
||||||
|
return packaged
|
||||||
|
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="finger",
|
||||||
|
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 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):
|
||||||
|
return idx
|
||||||
|
return -1
|
||||||
@@ -110,6 +110,8 @@ class SettingsPanelWidget(QWidget):
|
|||||||
self.camera_combo = QComboBox()
|
self.camera_combo = QComboBox()
|
||||||
self.camera_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
self.camera_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||||
self.camera_combo.setToolTip("Select an available camera")
|
self.camera_combo.setToolTip("Select an available camera")
|
||||||
|
# activated fires on user pick even when the index is unchanged (file → live override).
|
||||||
|
self.camera_combo.activated.connect(self._on_camera_combo_changed)
|
||||||
self.camera_combo.currentIndexChanged.connect(self._on_camera_combo_changed)
|
self.camera_combo.currentIndexChanged.connect(self._on_camera_combo_changed)
|
||||||
video_row.addWidget(self.camera_combo, 1)
|
video_row.addWidget(self.camera_combo, 1)
|
||||||
self.btn_refresh_cameras = QPushButton("Refresh")
|
self.btn_refresh_cameras = QPushButton("Refresh")
|
||||||
@@ -137,7 +139,7 @@ class SettingsPanelWidget(QWidget):
|
|||||||
root.addWidget(self._caption("Config"))
|
root.addWidget(self._caption("Config"))
|
||||||
config_row = QHBoxLayout()
|
config_row = QHBoxLayout()
|
||||||
config_row.setSpacing(6)
|
config_row.setSpacing(6)
|
||||||
self.config_edit = QLineEdit("dd02")
|
self.config_edit = QLineEdit("finger")
|
||||||
config_row.addWidget(self.config_edit, 1)
|
config_row.addWidget(self.config_edit, 1)
|
||||||
self.btn_browse_config = QPushButton("…")
|
self.btn_browse_config = QPushButton("…")
|
||||||
self.btn_browse_config.setFixedWidth(32)
|
self.btn_browse_config.setFixedWidth(32)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ ORY-MINI:单传感器基础流程
|
|||||||
3. 按需计算接触区域
|
3. 按需计算接触区域
|
||||||
4. 读取并可视化结果
|
4. 读取并可视化结果
|
||||||
|
|
||||||
SDK version: 0.4.2
|
SDK version: 0.5.0
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||||
@@ -43,7 +43,7 @@ def main():
|
|||||||
# 步骤 2.1:获取并拼接图像(必需)
|
# 步骤 2.1:获取并拼接图像(必需)
|
||||||
sensor.get_img()
|
sensor.get_img()
|
||||||
# 步骤 2.2:计算形变场(必需);返回值表示当前帧是否检测到接触/运动
|
# 步骤 2.2:计算形变场(必需);返回值表示当前帧是否检测到接触/运动
|
||||||
is_contact_now = sensor.compute_deformation(check_motion=True, threshold=0)
|
is_contact_now = sensor.compute_deformation()
|
||||||
# 步骤 2.3:仅在当前帧检测到接触/运动时计算接触区域
|
# 步骤 2.3:仅在当前帧检测到接触/运动时计算接触区域
|
||||||
# if is_contact_now:
|
# if is_contact_now:
|
||||||
sensor.compute_contact()
|
sensor.compute_contact()
|
||||||
@@ -17,7 +17,7 @@ def mesh_threshold(mesh_map: str, threshold: float | None) -> float:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class PipelineConfig:
|
class PipelineConfig:
|
||||||
video: str = "0"
|
video: str = "0"
|
||||||
config: str = "dd02"
|
config: str = "finger"
|
||||||
calib: str | None = None
|
calib: str | None = None
|
||||||
export_dir: str = DEFAULT_FINGER_EXPORT_DIR
|
export_dir: str = DEFAULT_FINGER_EXPORT_DIR
|
||||||
csv_save_dir: str = "./outputs/csv"
|
csv_save_dir: str = "./outputs/csv"
|
||||||
|
|||||||
+25
-24
@@ -1,29 +1,24 @@
|
|||||||
"""
|
"""
|
||||||
示例 3:实时曲线与幅值图显示
|
示例 3:实时曲线与深度图显示
|
||||||
|
|
||||||
本示例演示如何在读取传感器结果的同时:
|
本示例演示如何在读取传感器结果的同时:
|
||||||
1. 显示形变矢量场
|
1. 显示形变矢量场
|
||||||
2. 显示流场幅值热力图
|
2. 显示接触深度图
|
||||||
3. 使用 PyQtGraph 实时绘制法向力与切向力曲线
|
3. 使用 PyQtGraph 实时绘制法向力与切向力曲线
|
||||||
4. 按键控制录像开始与结束
|
4. 按键控制录像开始与结束
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
|
import numpy as np
|
||||||
import orisys
|
import orisys
|
||||||
|
|
||||||
_EXAMPLES_DIR = Path(__file__).resolve().parent
|
from viz import ForceChartWindow, FrameRecorder
|
||||||
if str(_EXAMPLES_DIR) not in sys.path:
|
|
||||||
sys.path.insert(0, str(_EXAMPLES_DIR))
|
|
||||||
|
|
||||||
from viz import ForceChartWindow, FrameRecorder, draw_magnitude_map, lowpass_filter
|
|
||||||
|
|
||||||
|
|
||||||
def open_sensor(video: str, config: str, cal: str, verbose: bool):
|
def open_sensor(video: str, config: str, cal: str, verbose: bool):
|
||||||
@@ -37,14 +32,21 @@ def open_sensor(video: str, config: str, cal: str, verbose: bool):
|
|||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--video", "-v", type=str, default="0", help="视频源:摄像头编号或视频文件路径")
|
parser.add_argument("--video", "-v", type=str, default="0", help="视频源:摄像头编号或视频文件路径")
|
||||||
parser.add_argument("--config", "-c", type=str, default="./config/ddjx01.json", help="配置文件名称或路径")
|
parser.add_argument("--config", "-c", type=str, default="box", help="配置文件名称或路径")
|
||||||
parser.add_argument("--cal", "-cal", type=str, default="./config/ddjx01.npy", help="标定文件路径")
|
parser.add_argument("--cal", "-cal", type=str, default="", help="标定文件路径")
|
||||||
|
parser.add_argument(
|
||||||
|
"--cutoff",
|
||||||
|
type=float,
|
||||||
|
default=5.0,
|
||||||
|
help="力曲线低通滤波截止频率(Hz),30 Hz 采样时必须小于 15;设为 0 可关闭滤波",
|
||||||
|
)
|
||||||
parser.add_argument("--verbose", "-verbose", type=bool, default=True, help="是否输出详细日志")
|
parser.add_argument("--verbose", "-verbose", type=bool, default=True, help="是否输出详细日志")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
charts = ForceChartWindow()
|
charts = ForceChartWindow()
|
||||||
recorder = FrameRecorder()
|
recorder = FrameRecorder()
|
||||||
sensor = open_sensor(args.video, args.config, args.cal, args.verbose)
|
sensor = open_sensor(args.video, args.config, args.cal, args.verbose)
|
||||||
|
sensor.configure(lowpass_cutoff_hz=args.cutoff, lowpass_sample_rate_hz=30.0)
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
print("\n按键说明:'q' 退出程序,'s' 开始录像,'e' 结束录像。")
|
print("\n按键说明:'q' 退出程序,'s' 开始录像,'e' 结束录像。")
|
||||||
@@ -53,31 +55,30 @@ def main():
|
|||||||
sensor.get_img()
|
sensor.get_img()
|
||||||
recorder.write_if_recording(sensor.frame)
|
recorder.write_if_recording(sensor.frame)
|
||||||
|
|
||||||
sensor.compute_deformation(check_motion=True, threshold=0)
|
sensor.compute_deformation()
|
||||||
fps, fn, fx, fy, flow, vnormal, vshear = sensor.read_info(
|
sensor.compute_contact()
|
||||||
|
fps, fn, fx, fy, flow, depth_map = sensor.read_info(
|
||||||
sensor.info.FPS,
|
sensor.info.FPS,
|
||||||
sensor.info.FNORMAL,
|
sensor.info.FNORMAL,
|
||||||
sensor.info.FSHEARX,
|
sensor.info.FSHEARX,
|
||||||
sensor.info.FSHEARY,
|
sensor.info.FSHEARY,
|
||||||
sensor.info.VRAW,
|
sensor.info.VRAW,
|
||||||
sensor.info.VNORMAL,
|
sensor.info.DEPTH,
|
||||||
sensor.info.VSHEAR,
|
|
||||||
)
|
)
|
||||||
print(f"FPS={fps:.2f}, 法向力={fn:.4f}, 切向力X={fx:.4f}, 切向力Y={fy:.4f}")
|
print(f"FPS={fps:.2f}, 法向力={fn:.4f}, 切向力X={fx:.4f}, 切向力Y={fy:.4f}")
|
||||||
|
|
||||||
arrows = orisys.util.draw_arrows(sensor.img, flow, threshold=5, grid_spacing=10, arrow_scale=1.0)
|
arrows = orisys.util.draw_arrows(sensor.img, flow, threshold=5, grid_spacing=10, arrow_scale=1.0)
|
||||||
cv2.imshow("形变矢量场", arrows)
|
cv2.imshow("形变矢量场", arrows)
|
||||||
cv2.imshow("流场幅值图", draw_magnitude_map(flow))
|
if depth_map.max() > depth_map.min():
|
||||||
|
depth_normalized = (
|
||||||
|
(depth_map - depth_map.min()) / (depth_map.max() - depth_map.min()) * 255
|
||||||
|
).astype(np.uint8)
|
||||||
|
else:
|
||||||
|
depth_normalized = np.zeros_like(depth_map, dtype=np.uint8)
|
||||||
|
cv2.imshow("depth", cv2.applyColorMap(depth_normalized, cv2.COLORMAP_JET))
|
||||||
|
|
||||||
charts.push(time.time() - start_time, fn, fx, fy)
|
charts.push(time.time() - start_time, fn, fx, fy)
|
||||||
display = charts.data.copy()
|
charts.refresh()
|
||||||
for channel in range(3):
|
|
||||||
display[:, 1, channel] = lowpass_filter(
|
|
||||||
charts.data[:, 1, channel],
|
|
||||||
cutoff_hz=5,
|
|
||||||
sample_rate_hz=30,
|
|
||||||
)
|
|
||||||
charts.refresh(display)
|
|
||||||
|
|
||||||
key = cv2.waitKey(1)
|
key = cv2.waitKey(1)
|
||||||
if key & 0xFF == ord("q"):
|
if key & 0xFF == ord("q"):
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
"""Product-facing example entry points for ORY-BOX / ORY-MINI / ORY-FINGER."""
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
"""
|
|
||||||
ORY-BOX:单传感器基础流程
|
|
||||||
|
|
||||||
本示例演示 Orisys SDK 的标准处理流程:
|
|
||||||
1. 初始化传感器(摄像头或视频文件)
|
|
||||||
2. 获取图像并计算形变
|
|
||||||
3. 按需计算接触区域
|
|
||||||
4. 读取并可视化结果
|
|
||||||
|
|
||||||
SDK version: 0.4.2
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
|
||||||
import cv2
|
|
||||||
import orisys
|
|
||||||
import numpy as np
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("--video","-v", type=str, default="0", help="视频源:摄像头编号或视频文件路径")
|
|
||||||
parser.add_argument("--config","-c", type=str, default="ddjx01", help="配置文件名称或路径")
|
|
||||||
parser.add_argument("--verbose","-verbose", type=bool, default=True, help="是否输出详细日志")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# 步骤 1:创建传感器对象(必需)
|
|
||||||
# config_name 可以是内置配置名称,也可以是自定义配置文件路径
|
|
||||||
# cal_path 为标定文件路径;每台设备建议使用独立的标定文件
|
|
||||||
|
|
||||||
# 输入既可以是摄像头编号,也可以是视频文件路径
|
|
||||||
try:
|
|
||||||
input = int(args.video)
|
|
||||||
is_camera = True
|
|
||||||
sensor = orisys.Sensor(input, config_name=args.config, verbose=args.verbose)
|
|
||||||
except:
|
|
||||||
is_camera = False
|
|
||||||
sensor = orisys.Sensor(args.video, config_name=args.config, verbose=args.verbose)
|
|
||||||
|
|
||||||
print("\n按键说明:'q' 退出程序,'r' 重置追踪器。")
|
|
||||||
|
|
||||||
while True:
|
|
||||||
# 步骤 2:数据处理
|
|
||||||
# 步骤 2.1:获取并拼接图像(必需)
|
|
||||||
sensor.get_img()
|
|
||||||
# 步骤 2.2:计算形变场(必需);返回值表示当前帧是否检测到接触/运动
|
|
||||||
is_contact_now = sensor.compute_deformation(check_motion=True, threshold=0)
|
|
||||||
# 步骤 2.3:仅在当前帧检测到接触/运动时计算接触区域
|
|
||||||
# if is_contact_now:
|
|
||||||
sensor.compute_contact()
|
|
||||||
|
|
||||||
|
|
||||||
# 步骤 2.4:读取结果
|
|
||||||
fps, flow, vnormal, img, contour, centroid, depth_map, fnormal, fshearx, fsheary = sensor.read_info(
|
|
||||||
sensor.info.FPS,
|
|
||||||
sensor.info.VRAW,
|
|
||||||
sensor.info.VNORMAL,
|
|
||||||
sensor.info.IMG,
|
|
||||||
sensor.info.CONTOUR,
|
|
||||||
sensor.info.CENTROID,
|
|
||||||
sensor.info.DEPTH,
|
|
||||||
sensor.info.FNORMAL,
|
|
||||||
sensor.info.FSHEARX,
|
|
||||||
sensor.info.FSHEARY
|
|
||||||
)
|
|
||||||
|
|
||||||
# 步骤 3:可视化显示
|
|
||||||
# 步骤 3.1:显示形变矢量场
|
|
||||||
arrows = orisys.util.draw_arrows(
|
|
||||||
img,
|
|
||||||
flow,
|
|
||||||
threshold=2,
|
|
||||||
grid_spacing=20,
|
|
||||||
arrow_scale=1.0
|
|
||||||
)
|
|
||||||
cv2.imshow("arrow", arrows)
|
|
||||||
|
|
||||||
# 步骤 3.2:显示接触区域和质心
|
|
||||||
image_with_foe = orisys.util.draw_contact(img, contour, centroid)
|
|
||||||
cv2.imshow("contact", image_with_foe)
|
|
||||||
|
|
||||||
# 步骤 3.3:显示深度图
|
|
||||||
# 将深度值归一化到 0-255 范围
|
|
||||||
div_abs = depth_map
|
|
||||||
if div_abs.max() > div_abs.min():
|
|
||||||
div_normalized = ((div_abs - div_abs.min()) / (div_abs.max() - div_abs.min()) * 255).astype(np.uint8)
|
|
||||||
else:
|
|
||||||
div_normalized = np.zeros_like(div_abs, dtype=np.uint8)
|
|
||||||
|
|
||||||
# 应用伪彩色映射
|
|
||||||
cv2.imshow("depth", cv2.applyColorMap(div_normalized, cv2.COLORMAP_JET))
|
|
||||||
|
|
||||||
# 步骤 3.4:打印关键结果
|
|
||||||
print(
|
|
||||||
f"FPS={fps:.2f}, 法向力={fnormal:.4f}, 切向力X={fshearx:.4f}, "
|
|
||||||
f"切向力Y={fsheary:.4f}, 深度图尺寸={depth_map.shape}, 原始分辨率={sensor.img_size_raw}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 步骤 3.5:键盘控制
|
|
||||||
key = cv2.waitKey(1) & 0xFF
|
|
||||||
if key == ord("q"):
|
|
||||||
print("\n正在退出示例程序...")
|
|
||||||
break
|
|
||||||
if key == ord("r"):
|
|
||||||
# 当光流追踪出现漂移时,可手动重置追踪器
|
|
||||||
sensor.reset()
|
|
||||||
print("追踪器已重置。")
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
# 4. 释放资源
|
|
||||||
# =========================================================================
|
|
||||||
sensor.disconnect() # 释放视频源及相关资源
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
ORY-FINGER:Minimum UV-unroll finger tactile pipeline.
|
|
||||||
|
|
||||||
Offline: load Blender export -> build_or_load_domain -> unroll_map_uv.npz
|
|
||||||
Runtime: camera frame -> FrameUnwarper -> TactileProcessor -> forces/contact
|
|
||||||
|
|
||||||
Recommended entry:
|
|
||||||
python examples/products/ory_finger/finger_pipeline.py --camera_id 0 --config dd02
|
|
||||||
|
|
||||||
Current config alias: ``dd02`` resolves to ``dd02-ov`` for compatibility.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
|
|
||||||
|
|
||||||
import cv2
|
|
||||||
import orisys
|
|
||||||
from orisys.finger import FingerRuntime
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(description="Minimum UV-unroll finger tactile pipeline")
|
|
||||||
parser.add_argument("--camera_id", type=int, default=0)
|
|
||||||
parser.add_argument("--video", help="Video file path (overrides camera)")
|
|
||||||
parser.add_argument("--config", "-c", default="dd02", help="Orisys SDK config name or path")
|
|
||||||
parser.add_argument("--show", action="store_true", default=True, help="Show unrolled image window")
|
|
||||||
parser.add_argument("--verbose", action="store_true", help="SDK verbose logs")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
try:
|
|
||||||
video = args.camera_id
|
|
||||||
if args.video:
|
|
||||||
video = args.video
|
|
||||||
runtime = FingerRuntime.open(
|
|
||||||
config=args.config,
|
|
||||||
video=video,
|
|
||||||
verbose=args.verbose,
|
|
||||||
)
|
|
||||||
except RuntimeError as exc:
|
|
||||||
print(str(exc), file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if args.verbose:
|
|
||||||
geometry = runtime.geometry
|
|
||||||
print(
|
|
||||||
f"UV geometry: native={geometry.native_width}x{geometry.native_height}, "
|
|
||||||
f"source={geometry.source_width}x{geometry.source_height} "
|
|
||||||
f"output={geometry.out_width}x{geometry.out_height} "
|
|
||||||
)
|
|
||||||
|
|
||||||
runtime.warmup(color=False)
|
|
||||||
tactile = runtime.tactile
|
|
||||||
|
|
||||||
print("Press 'q' to quit, 'r' to reset tracker.")
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
ok, unrolled = runtime.grab_and_process(color=False)
|
|
||||||
if not ok:
|
|
||||||
break
|
|
||||||
|
|
||||||
tactile.compute_deformation(check_motion=True, threshold=0)
|
|
||||||
tactile.compute_contact()
|
|
||||||
|
|
||||||
fps, flow, vnormal, img, contour, centroid, depth_map, fnormal, fshearx, fsheary = tactile.read_info(
|
|
||||||
tactile.info.FPS,
|
|
||||||
tactile.info.VRAW,
|
|
||||||
tactile.info.VNORMAL,
|
|
||||||
tactile.info.IMG,
|
|
||||||
tactile.info.CONTOUR,
|
|
||||||
tactile.info.CENTROID,
|
|
||||||
tactile.info.DEPTH,
|
|
||||||
tactile.info.FNORMAL,
|
|
||||||
tactile.info.FSHEARX,
|
|
||||||
tactile.info.FSHEARY,
|
|
||||||
)
|
|
||||||
|
|
||||||
status = (
|
|
||||||
f"FPS={fps:.2f} normal={fnormal:.2f} shear=({fshearx:.2f}, {fsheary:.2f}) "
|
|
||||||
f"mask={tactile.mask_stats['coverage']:.2f}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if args.show:
|
|
||||||
arrows = orisys.util.draw_arrows(
|
|
||||||
unrolled,
|
|
||||||
flow,
|
|
||||||
threshold=0.4,
|
|
||||||
grid_spacing=10,
|
|
||||||
arrow_scale=1.0,
|
|
||||||
)
|
|
||||||
cv2.imshow("UV Unrolled", arrows)
|
|
||||||
|
|
||||||
print(status, end="\r")
|
|
||||||
|
|
||||||
key = cv2.waitKey(1) & 0xFF
|
|
||||||
if key == ord("q"):
|
|
||||||
print("\nExiting.")
|
|
||||||
break
|
|
||||||
if key == ord("r"):
|
|
||||||
tactile.reset()
|
|
||||||
print("\nTracker reset.")
|
|
||||||
finally:
|
|
||||||
runtime.close()
|
|
||||||
cv2.destroyAllWindows()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
ORY-FINGER:UV-unroll finger tactile pipeline with unified Qt dashboard.
|
|
||||||
|
|
||||||
Where to customize the GUI:
|
|
||||||
Layout (sizes, proportions) -> examples/gui/viewer3d/layout.py
|
|
||||||
Colors / QSS -> examples/gui/viewer3d/styles.py
|
|
||||||
Widget rendering logic -> examples/gui/viewer3d/widgets/
|
|
||||||
Window behavior -> examples/gui/viewer3d/dashboard.py
|
|
||||||
|
|
||||||
Recommended entry:
|
|
||||||
python examples/products/ory_finger/finger_pipeline_3dview.py --video 0 --config dd02
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
|
|
||||||
|
|
||||||
# Make shared packages importable when launched as a file path.
|
|
||||||
_EXAMPLES_DIR = Path(__file__).resolve().parents[2]
|
|
||||||
if str(_EXAMPLES_DIR) not in sys.path:
|
|
||||||
sys.path.insert(0, str(_EXAMPLES_DIR))
|
|
||||||
|
|
||||||
from gui.app_paths import configure_frozen_runtime, default_config_path, ensure_examples_import_path
|
|
||||||
|
|
||||||
configure_frozen_runtime()
|
|
||||||
ensure_examples_import_path()
|
|
||||||
|
|
||||||
from gui import run_dashboard
|
|
||||||
from pipeline import PipelineConfig
|
|
||||||
from viz import DEFAULT_FINGER_VIZ_POSE
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_args():
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="UV-unroll finger tactile pipeline with unified Qt dashboard"
|
|
||||||
)
|
|
||||||
parser.add_argument("--video", "-v", default="0", help="Camera index or video file path")
|
|
||||||
parser.add_argument("--config", "-c", default=default_config_path(), help="Orisys SDK config")
|
|
||||||
parser.add_argument("--cal", default="", help="Optional calibration file path")
|
|
||||||
parser.add_argument("--verbose", action="store_true", help="SDK verbose logs")
|
|
||||||
parser.add_argument(
|
|
||||||
"--mesh-map",
|
|
||||||
choices=["flow", "depth"],
|
|
||||||
default="depth",
|
|
||||||
help="Color the 3D mesh from flow magnitude or contact depth.",
|
|
||||||
)
|
|
||||||
parser.add_argument("--threshold", type=float, default=None, help="Mesh/arrow threshold")
|
|
||||||
parser.add_argument("--no-3d", action="store_true", help="Disable 3D mesh view")
|
|
||||||
parser.add_argument(
|
|
||||||
"--viz-pose",
|
|
||||||
default=DEFAULT_FINGER_VIZ_POSE,
|
|
||||||
help="world_to_cam.npy for the default view; pass 'none' to skip",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--cutoff",
|
|
||||||
type=float,
|
|
||||||
default=5.0,
|
|
||||||
help="Low-pass cutoff for force curves (Hz)",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--profile",
|
|
||||||
action="store_true",
|
|
||||||
help="Print per-stage timing to the log panel / console",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--profile-every",
|
|
||||||
type=int,
|
|
||||||
default=30,
|
|
||||||
help="When --profile is set, print rolling averages every N frames",
|
|
||||||
)
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
args = _parse_args()
|
|
||||||
config = PipelineConfig.from_args(args)
|
|
||||||
sys.exit(run_dashboard(config))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
- Windows 下必须在 if __name__ == '__main__' 中调用 mp.freeze_support()
|
- Windows 下必须在 if __name__ == '__main__' 中调用 mp.freeze_support()
|
||||||
- 每个传感器进程都会占用独立的 GPU 显存
|
- 每个传感器进程都会占用独立的 GPU 显存
|
||||||
|
|
||||||
SDK version: 0.4.2
|
SDK version: 0.5.0
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||||
@@ -138,12 +138,12 @@ def main():
|
|||||||
{
|
{
|
||||||
'sensor_id': 0,
|
'sensor_id': 0,
|
||||||
'vid_src': args.video1,
|
'vid_src': args.video1,
|
||||||
'config_name': './config/ddjx01.json'
|
'config_name': 'box'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'sensor_id': 1,
|
'sensor_id': 1,
|
||||||
'vid_src': args.video2, # 可替换为其他视频源或摄像头索引
|
'vid_src': args.video2, # 可替换为其他视频源或摄像头索引
|
||||||
'config_name': './config/ddjx01.json'
|
'config_name': 'box'
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+58
-11
@@ -6,7 +6,38 @@ from pathlib import Path
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
DEFAULT_FINGER_VIZ_POSE = "assets/finger/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]:
|
||||||
|
"""Resolve viz pose paths from the packaged sensors/finger bundle when present."""
|
||||||
|
try:
|
||||||
|
from orisys.configs import resolve_sensor_bundle, resolve_sensor_resource
|
||||||
|
except ImportError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
found: list[Path] = []
|
||||||
|
bundle = resolve_sensor_bundle("finger")
|
||||||
|
if bundle is not None and bundle.manifest_json is not None:
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
|
||||||
|
manifest = json.loads(bundle.manifest_json.read_text(encoding="utf-8"))
|
||||||
|
except Exception:
|
||||||
|
manifest = {}
|
||||||
|
rel = manifest.get("viz_pose")
|
||||||
|
if isinstance(rel, str) and rel.strip():
|
||||||
|
found.append((bundle.root / rel).resolve())
|
||||||
|
|
||||||
|
packaged = resolve_sensor_resource("finger", "assets", "visualization", "world_to_cam.npy")
|
||||||
|
if packaged is not None:
|
||||||
|
found.append(packaged.resolve())
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
_CAMERA_X_ROT_90 = np.array(
|
_CAMERA_X_ROT_90 = np.array(
|
||||||
[
|
[
|
||||||
@@ -28,27 +59,43 @@ _CAMERA_Z_ROT_180 = np.array(
|
|||||||
|
|
||||||
|
|
||||||
def resolve_viz_pose_path(path: str | Path | None = None) -> Path:
|
def resolve_viz_pose_path(path: str | Path | None = None) -> Path:
|
||||||
"""Resolve world_to_cam.npy from cwd or packaged asset layout."""
|
"""Resolve world_to_cam.npy from sensor bundle, cwd, or packaged layout."""
|
||||||
raw = Path(path or DEFAULT_FINGER_VIZ_POSE).expanduser()
|
raw = Path(path or DEFAULT_FINGER_VIZ_POSE).expanduser()
|
||||||
if raw.is_file():
|
if raw.is_file():
|
||||||
return raw.resolve()
|
return raw.resolve()
|
||||||
|
|
||||||
here = Path(__file__).resolve()
|
here = Path(__file__).resolve()
|
||||||
candidates = [
|
candidates: list[Path] = []
|
||||||
Path.cwd() / raw,
|
|
||||||
here.parents[2] / raw,
|
# Prefer the sensors/finger bundle for default / legacy relative paths.
|
||||||
]
|
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())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from gui.app_paths import app_root
|
from gui.app_paths import app_root
|
||||||
|
|
||||||
candidates.insert(0, app_root() / raw)
|
candidates.append(app_root() / raw)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
for candidate in candidates:
|
candidates.extend(
|
||||||
if candidate.is_file():
|
[
|
||||||
return candidate.resolve()
|
Path.cwd() / raw,
|
||||||
|
here.parents[2] / raw,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
checked = ", ".join(str(p) for p in candidates)
|
unique: list[Path] = []
|
||||||
|
for candidate in candidates:
|
||||||
|
resolved = candidate.resolve()
|
||||||
|
if resolved not in unique:
|
||||||
|
unique.append(resolved)
|
||||||
|
|
||||||
|
for candidate in unique:
|
||||||
|
if candidate.is_file():
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
checked = ", ".join(str(p) for p in unique)
|
||||||
raise FileNotFoundError(f"Visualization pose file not found: {raw}. Checked: {checked}")
|
raise FileNotFoundError(f"Visualization pose file not found: {raw}. Checked: {checked}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+20
-11
@@ -1,5 +1,5 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=61.0", "wheel"]
|
requires = ["setuptools>=61.0,<80", "wheel==0.45.1"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
@@ -63,7 +63,7 @@ version = { attr = "orisys.__version__" }
|
|||||||
|
|
||||||
# Single source of truth for bundled non-code files.
|
# Single source of truth for bundled non-code files.
|
||||||
# - dev_install = true -> pip install / editable install (via setup.py)
|
# - dev_install = true -> pip install / editable install (via setup.py)
|
||||||
# - all items -> Nuitka wheel (build/wheel_layout.py)
|
# - all items -> Nuitka wheel (build/wheel/layout.py)
|
||||||
[tool.orisys.bundle]
|
[tool.orisys.bundle]
|
||||||
|
|
||||||
# Use patterns (globs) and/or files (explicit names under source/).
|
# Use patterns (globs) and/or files (explicit names under source/).
|
||||||
@@ -75,11 +75,28 @@ patterns = ["*.json"]
|
|||||||
# files = ["dd02-ov.json", "ddjx01.json"]
|
# files = ["dd02-ov.json", "ddjx01.json"]
|
||||||
dev_install = true
|
dev_install = true
|
||||||
|
|
||||||
|
[[tool.orisys.bundle.items]]
|
||||||
|
name = "sensors"
|
||||||
|
source = "sensors"
|
||||||
|
target = "sensors"
|
||||||
|
# JSON bundles (sensor.json, force_calibration.json) plus finger mesh/viz assets.
|
||||||
|
# Prefer the flat layout sensors/finger/assets/{30fps-*,visualization}/;
|
||||||
|
# also accept a nested assets/finger/ tree if present in a checkout.
|
||||||
|
patterns = [
|
||||||
|
"**/*.json",
|
||||||
|
"finger/assets/30fps*/*",
|
||||||
|
"finger/assets/visualization/*",
|
||||||
|
"finger/assets/finger/30fps*/*",
|
||||||
|
"finger/assets/finger/visualization/*",
|
||||||
|
]
|
||||||
|
preserve_tree = true
|
||||||
|
dev_install = true
|
||||||
|
|
||||||
[[tool.orisys.bundle.items]]
|
[[tool.orisys.bundle.items]]
|
||||||
name = "examples"
|
name = "examples"
|
||||||
source = "examples"
|
source = "examples"
|
||||||
target = "examples"
|
target = "examples"
|
||||||
patterns = ["*.py", "README.md", "products/**/*.py", "viz/**/*.py", "pipeline/**/*.py", "gui/**/*.py"]
|
patterns = ["*.py", "README.md", "viz/**/*.py", "pipeline/**/*.py", "gui/**/*.py"]
|
||||||
preserve_tree = true
|
preserve_tree = true
|
||||||
|
|
||||||
[[tool.orisys.bundle.items]]
|
[[tool.orisys.bundle.items]]
|
||||||
@@ -88,11 +105,3 @@ source = "docs"
|
|||||||
target = "docs"
|
target = "docs"
|
||||||
patterns = ["*.md", "*.pdf"]
|
patterns = ["*.md", "*.pdf"]
|
||||||
|
|
||||||
[[tool.orisys.bundle.items]]
|
|
||||||
name = "assets"
|
|
||||||
source = "assets"
|
|
||||||
target = "assets"
|
|
||||||
patterns = ["finger/30fps/*", "finger/visualization/*"]
|
|
||||||
preserve_tree = true
|
|
||||||
dev_install = true
|
|
||||||
|
|
||||||
|
|||||||
+25
-11
@@ -1,13 +1,27 @@
|
|||||||
# CuPy - Install the appropriate version based on your CUDA version:
|
# Runtime dependencies for editable / source installs.
|
||||||
# For CUDA 12.x: pip install cupy-cuda12x>=12.3.0
|
# Prefer extras from pyproject.toml:
|
||||||
# For CUDA 13.x: pip install cupy-cuda13x>=13.0.0
|
# pip install -e .
|
||||||
# Or use extras: pip install orisys[cuda12] or pip install orisys[cuda13]
|
# pip install -e ".[gui]"
|
||||||
|
# pip install -e ".[cuda12]"
|
||||||
|
# pip install -e ".[cuda13]"
|
||||||
|
# pip install -e ".[gui,cuda12]"
|
||||||
|
# PyPI users: pip install orisys / "orisys[gui]" / "orisys[cuda12]"
|
||||||
|
|
||||||
|
# Core (same as [project.dependencies])
|
||||||
numpy>=1.24.4
|
numpy>=1.24.4
|
||||||
opencv_python>=4.10.0.84
|
opencv-python>=4.10.0.84
|
||||||
scipy==1.13
|
scipy>=1.10.1
|
||||||
setuptools>=45.2.0
|
numba>=0.59.0
|
||||||
av
|
|
||||||
numba
|
# GUI extras: pip install -e ".[gui]"
|
||||||
pyqt5
|
# open3d>=0.17.0
|
||||||
pyqtgraph
|
# pyqt5>=5.15
|
||||||
|
# pyqtgraph>=0.13
|
||||||
|
|
||||||
|
# CUDA extras (pick one CUDA major version)
|
||||||
|
# CUDA 12.x: pip install -e ".[cuda12]" -> cupy-cuda12x>=12.3.0
|
||||||
|
# CUDA 13.x: pip install -e ".[cuda13]" -> cupy-cuda13x>=13.0.0
|
||||||
|
# Alias: pip install -e ".[cuda]" -> same as cuda12
|
||||||
|
|
||||||
|
# Optional video tooling (not an orisys extra)
|
||||||
|
# av
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ def main():
|
|||||||
|
|
||||||
writer.write(save_img)
|
writer.write(save_img)
|
||||||
|
|
||||||
sensor1.compute_deformation(check_motion=False) # 计算形变和分解
|
sensor1.compute_deformation() # 计算形变和分解
|
||||||
# 提取信息:
|
# 提取信息:
|
||||||
# 向量场: VRAW, VNORMAL, VSHEAR: 原始,法向,切向
|
# 向量场: VRAW, VNORMAL, VSHEAR: 原始,法向,切向
|
||||||
# 帧率,时间戳: FPS, TIMESTAMP
|
# 帧率,时间戳: FPS, TIMESTAMP
|
||||||
|
|||||||
@@ -1,961 +0,0 @@
|
|||||||
import os
|
|
||||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
|
||||||
# Force pyqtgraph to bind to PyQt5 when multiple Qt bindings are installed.
|
|
||||||
os.environ.setdefault("PYQTGRAPH_QT_LIB", "PyQt5")
|
|
||||||
import cv2
|
|
||||||
import orisys
|
|
||||||
import numpy as np
|
|
||||||
import time
|
|
||||||
import argparse
|
|
||||||
from datetime import datetime
|
|
||||||
import pyqtgraph as pg
|
|
||||||
from PyQt5.QtCore import Qt, QSize, QEvent
|
|
||||||
from PyQt5.QtGui import QImage, QFont, QPixmap, QKeySequence, QCursor
|
|
||||||
from PyQt5.QtWidgets import QLabel, QHBoxLayout, QVBoxLayout, QWidget, QSizePolicy, QShortcut, QApplication
|
|
||||||
from pyqtgraph.Qt import QtGui
|
|
||||||
|
|
||||||
|
|
||||||
# Final desired appearance on the mirrored 1872x1560 (6:5) display.
|
|
||||||
# The desktop is first shown on a 1920x1080 (16:9) screen, then stretched by
|
|
||||||
# the OS onto the 6:5 mirror. We therefore pre-distort the source layout so
|
|
||||||
# the mirrored output looks correct.
|
|
||||||
TARGET_SINGLE_PANEL_W = 936
|
|
||||||
TARGET_ASPECT_W = TARGET_SINGLE_PANEL_W * 2
|
|
||||||
TARGET_VIDEO_FRAME_H = 1053
|
|
||||||
TARGET_BOTTOM_PANEL_H = 507
|
|
||||||
TARGET_ASPECT_H_TOTAL = TARGET_VIDEO_FRAME_H + TARGET_BOTTOM_PANEL_H
|
|
||||||
TARGET_MIRROR_W = 1872
|
|
||||||
TARGET_MIRROR_H = 1560
|
|
||||||
|
|
||||||
SINGLE_PANEL_W = TARGET_SINGLE_PANEL_W
|
|
||||||
ASPECT_W = TARGET_ASPECT_W
|
|
||||||
BOTTOM_PANEL_H = TARGET_BOTTOM_PANEL_H
|
|
||||||
VIDEO_FRAME_W = TARGET_ASPECT_W
|
|
||||||
VIDEO_FRAME_H = TARGET_VIDEO_FRAME_H
|
|
||||||
ASPECT_H_TOTAL = TARGET_ASPECT_H_TOTAL
|
|
||||||
UI_SCALE = 1.0
|
|
||||||
ARROW_STRETCH = 507
|
|
||||||
PLOT_STRETCH = 429
|
|
||||||
VIDEO_LOOP_PATH = "./叠动机械爪_成片.m4v"
|
|
||||||
npoints = 120 # 图表数据长度
|
|
||||||
dispaly_angle = -90 # 箭头图显示角度
|
|
||||||
touch_threshold = 5 # 触碰反应阈值
|
|
||||||
frame_to_recover = 10 # 触碰后等待回复帧数
|
|
||||||
|
|
||||||
# Opneing Hour for keep reading image
|
|
||||||
open_hour = 9
|
|
||||||
close_hour = 17
|
|
||||||
manual_open_enable = True # 控制是否可以手动切换营业状态 (按键G)
|
|
||||||
is_open = True
|
|
||||||
|
|
||||||
# Match arrow panel / Qt style (not pure black)
|
|
||||||
PLOT_THEME_BG = "#0B1016"
|
|
||||||
PLOT_THEME_FG = "#A0A0A0"
|
|
||||||
|
|
||||||
# OS title bar cannot be tinted to #0B1016; frameless removes it (use Alt+drag to move).
|
|
||||||
USE_FRAMELESS_WINDOW = False
|
|
||||||
|
|
||||||
|
|
||||||
def scale_ui_px(px, floor=1):
|
|
||||||
return max(floor, int(round(px * UI_SCALE)))
|
|
||||||
|
|
||||||
|
|
||||||
def compute_source_layout(screen_w, screen_h):
|
|
||||||
"""Pre-distort the app on the source screen so the mirrored monitor is correct."""
|
|
||||||
target_video_ratio = TARGET_VIDEO_FRAME_H / float(TARGET_ASPECT_H_TOTAL)
|
|
||||||
target_bottom_ratio = TARGET_BOTTOM_PANEL_H / float(TARGET_ASPECT_H_TOTAL)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"window_w": max(320, int(screen_w)),
|
|
||||||
"window_h": max(240, int(screen_h)),
|
|
||||||
"video_w": max(320, int(screen_w)),
|
|
||||||
"video_h": max(1, int(round(screen_h * target_video_ratio))),
|
|
||||||
"bottom_h": max(1, int(round(screen_h * target_bottom_ratio))),
|
|
||||||
"single_panel_w": max(1, int(round(screen_w / 2.0))),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class CombinedTactileWindow(QWidget):
|
|
||||||
"""
|
|
||||||
Resizable window that keeps width:height = ASPECT_W : ASPECT_H_TOTAL.
|
|
||||||
Dragging the width or height edge updates the other dimension accordingly.
|
|
||||||
"""
|
|
||||||
|
|
||||||
RESIZE_MARGIN = 8
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self._aspect_hw = ASPECT_H_TOTAL / float(ASPECT_W)
|
|
||||||
self._resize_guard = False
|
|
||||||
self.user_closed = False
|
|
||||||
self._drag_anchor = None
|
|
||||||
self._frameless_resize_edge = None
|
|
||||||
self._frameless_resize_start_global = None
|
|
||||||
self._frameless_resize_start_geom = None
|
|
||||||
|
|
||||||
def _min_client_size(self):
|
|
||||||
w = max(320, self.minimumSizeHint().width())
|
|
||||||
h = int(round(w * self._aspect_hw))
|
|
||||||
return w, h
|
|
||||||
|
|
||||||
def _hit_test_resize(self, global_pos):
|
|
||||||
if not USE_FRAMELESS_WINDOW:
|
|
||||||
return None
|
|
||||||
p = self.mapFromGlobal(global_pos)
|
|
||||||
w, h = self.width(), self.height()
|
|
||||||
m = self.RESIZE_MARGIN
|
|
||||||
left = p.x() <= m
|
|
||||||
right = p.x() >= w - m
|
|
||||||
top = p.y() <= m
|
|
||||||
bottom = p.y() >= h - m
|
|
||||||
if top and left:
|
|
||||||
return "tl"
|
|
||||||
if top and right:
|
|
||||||
return "tr"
|
|
||||||
if bottom and left:
|
|
||||||
return "bl"
|
|
||||||
if bottom and right:
|
|
||||||
return "br"
|
|
||||||
if top:
|
|
||||||
return "t"
|
|
||||||
if bottom:
|
|
||||||
return "b"
|
|
||||||
if left:
|
|
||||||
return "l"
|
|
||||||
if right:
|
|
||||||
return "r"
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _cursor_for_edge(self, edge):
|
|
||||||
return {
|
|
||||||
"tl": Qt.SizeFDiagCursor,
|
|
||||||
"br": Qt.SizeFDiagCursor,
|
|
||||||
"tr": Qt.SizeBDiagCursor,
|
|
||||||
"bl": Qt.SizeBDiagCursor,
|
|
||||||
"l": Qt.SizeHorCursor,
|
|
||||||
"r": Qt.SizeHorCursor,
|
|
||||||
"t": Qt.SizeVerCursor,
|
|
||||||
"b": Qt.SizeVerCursor,
|
|
||||||
}.get(edge, Qt.ArrowCursor)
|
|
||||||
|
|
||||||
def _apply_frameless_resize(self, dg):
|
|
||||||
edge = self._frameless_resize_edge
|
|
||||||
g0 = self._frameless_resize_start_geom
|
|
||||||
min_w, min_h = self._min_client_size()
|
|
||||||
dx, dy = dg.x(), dg.y()
|
|
||||||
x0, y0, w0, h0 = g0.x(), g0.y(), g0.width(), g0.height()
|
|
||||||
|
|
||||||
if edge == "br":
|
|
||||||
nw = max(min_w, w0 + dx)
|
|
||||||
nh = int(round(nw * self._aspect_hw))
|
|
||||||
self.setGeometry(x0, y0, nw, nh)
|
|
||||||
elif edge == "r":
|
|
||||||
nw = max(min_w, w0 + dx)
|
|
||||||
nh = int(round(nw * self._aspect_hw))
|
|
||||||
self.setGeometry(x0, y0, nw, nh)
|
|
||||||
elif edge == "b":
|
|
||||||
nh = max(min_h, h0 + dy)
|
|
||||||
nw = int(round(nh / self._aspect_hw))
|
|
||||||
self.setGeometry(x0, y0, nw, nh)
|
|
||||||
elif edge == "l":
|
|
||||||
nw = max(min_w, w0 - dx)
|
|
||||||
nh = int(round(nw * self._aspect_hw))
|
|
||||||
nx = g0.right() - nw
|
|
||||||
self.setGeometry(nx, y0, nw, nh)
|
|
||||||
elif edge == "t":
|
|
||||||
nh = max(min_h, h0 - dy)
|
|
||||||
nw = int(round(nh / self._aspect_hw))
|
|
||||||
ny = g0.bottom() - nh
|
|
||||||
self.setGeometry(x0, ny, nw, nh)
|
|
||||||
elif edge == "tr":
|
|
||||||
nw = max(min_w, w0 + dx)
|
|
||||||
nh = int(round(nw * self._aspect_hw))
|
|
||||||
ny = g0.bottom() - nh
|
|
||||||
self.setGeometry(x0, ny, nw, nh)
|
|
||||||
elif edge == "bl":
|
|
||||||
nw = max(min_w, w0 - dx)
|
|
||||||
nh = int(round(nw * self._aspect_hw))
|
|
||||||
nx = g0.right() - nw
|
|
||||||
self.setGeometry(nx, y0, nw, nh)
|
|
||||||
elif edge == "tl":
|
|
||||||
nw = max(min_w, w0 - dx)
|
|
||||||
nh = int(round(nw * self._aspect_hw))
|
|
||||||
nx = g0.right() - nw
|
|
||||||
ny = g0.bottom() - nh
|
|
||||||
self.setGeometry(nx, ny, nw, nh)
|
|
||||||
|
|
||||||
def install_drag_filters(self):
|
|
||||||
"""Alt+left-drag to move when frameless; grabs go to this widget."""
|
|
||||||
for w in self.findChildren(QWidget):
|
|
||||||
if w is not self:
|
|
||||||
w.installEventFilter(self)
|
|
||||||
|
|
||||||
def eventFilter(self, watched, event):
|
|
||||||
if not USE_FRAMELESS_WINDOW:
|
|
||||||
return super().eventFilter(watched, event)
|
|
||||||
et = event.type()
|
|
||||||
if et == QEvent.MouseButtonPress and event.button() == Qt.LeftButton:
|
|
||||||
edge = self._hit_test_resize(event.globalPos())
|
|
||||||
if edge is not None:
|
|
||||||
self._frameless_resize_edge = edge
|
|
||||||
self._frameless_resize_start_global = event.globalPos()
|
|
||||||
self._frameless_resize_start_geom = self.geometry()
|
|
||||||
self.grabMouse()
|
|
||||||
return True
|
|
||||||
if QApplication.keyboardModifiers() & Qt.AltModifier:
|
|
||||||
self._drag_anchor = event.globalPos() - self.frameGeometry().topLeft()
|
|
||||||
self.grabMouse()
|
|
||||||
return True
|
|
||||||
if et == QEvent.MouseMove and not event.buttons():
|
|
||||||
edge = self._hit_test_resize(event.globalPos())
|
|
||||||
watched.setCursor(QCursor(self._cursor_for_edge(edge)))
|
|
||||||
return super().eventFilter(watched, event)
|
|
||||||
|
|
||||||
def mouseMoveEvent(self, event):
|
|
||||||
if USE_FRAMELESS_WINDOW and self._frameless_resize_edge and event.buttons() & Qt.LeftButton:
|
|
||||||
dg = event.globalPos() - self._frameless_resize_start_global
|
|
||||||
self._apply_frameless_resize(dg)
|
|
||||||
event.accept()
|
|
||||||
return
|
|
||||||
if (
|
|
||||||
USE_FRAMELESS_WINDOW
|
|
||||||
and self._drag_anchor is not None
|
|
||||||
and event.buttons() & Qt.LeftButton
|
|
||||||
):
|
|
||||||
self.move(event.globalPos() - self._drag_anchor)
|
|
||||||
event.accept()
|
|
||||||
return
|
|
||||||
super().mouseMoveEvent(event)
|
|
||||||
|
|
||||||
def mouseReleaseEvent(self, event):
|
|
||||||
if USE_FRAMELESS_WINDOW and self._frameless_resize_edge and event.button() == Qt.LeftButton:
|
|
||||||
self.releaseMouse()
|
|
||||||
self._frameless_resize_edge = None
|
|
||||||
self._frameless_resize_start_global = None
|
|
||||||
self._frameless_resize_start_geom = None
|
|
||||||
event.accept()
|
|
||||||
return
|
|
||||||
if USE_FRAMELESS_WINDOW and self._drag_anchor is not None and event.button() == Qt.LeftButton:
|
|
||||||
self.releaseMouse()
|
|
||||||
self._drag_anchor = None
|
|
||||||
event.accept()
|
|
||||||
return
|
|
||||||
super().mouseReleaseEvent(event)
|
|
||||||
|
|
||||||
def closeEvent(self, event):
|
|
||||||
self.user_closed = True
|
|
||||||
event.accept()
|
|
||||||
|
|
||||||
def minimumSizeHint(self):
|
|
||||||
w = 320
|
|
||||||
return QSize(w, int(round(w * self._aspect_hw)))
|
|
||||||
|
|
||||||
def resizeEvent(self, event):
|
|
||||||
if self._resize_guard:
|
|
||||||
super().resizeEvent(event)
|
|
||||||
return
|
|
||||||
super().resizeEvent(event)
|
|
||||||
w, h = self.width(), self.height()
|
|
||||||
if w < 8 or h < 8:
|
|
||||||
return
|
|
||||||
old = event.oldSize()
|
|
||||||
self._resize_guard = True
|
|
||||||
try:
|
|
||||||
if old.isValid() and old.width() > 0 and old.height() > 0:
|
|
||||||
dw = abs(w - old.width())
|
|
||||||
dh = abs(h - old.height())
|
|
||||||
if dw >= dh:
|
|
||||||
new_w, new_h = w, int(round(w * self._aspect_hw))
|
|
||||||
else:
|
|
||||||
new_w, new_h = int(round(h / self._aspect_hw)), h
|
|
||||||
else:
|
|
||||||
new_w, new_h = w, int(round(w * self._aspect_hw))
|
|
||||||
if new_w != w or new_h != h:
|
|
||||||
self.resize(new_w, new_h)
|
|
||||||
finally:
|
|
||||||
self._resize_guard = False
|
|
||||||
|
|
||||||
|
|
||||||
def compute_arrow_canvas_side(panel_widget):
|
|
||||||
"""
|
|
||||||
Pixel size of the square arrow canvas from the window client area and the
|
|
||||||
fixed horizontal split ARROW_STRETCH : PLOT_STRETCH (matches QHBoxLayout stretch).
|
|
||||||
"""
|
|
||||||
w = panel_widget.width()
|
|
||||||
h = panel_widget.height()
|
|
||||||
if w < 16 or h < 16:
|
|
||||||
return max(32, ASPECT_H_TOTAL)
|
|
||||||
w_arrow = int(round(w * ARROW_STRETCH / float(ARROW_STRETCH + PLOT_STRETCH)))
|
|
||||||
# Keep the rendered arrow image square inside the left panel.
|
|
||||||
return max(32, min(h, w_arrow))
|
|
||||||
|
|
||||||
|
|
||||||
def draw_arrows_wuxi(
|
|
||||||
frame,
|
|
||||||
optical_flow,
|
|
||||||
fps=True,
|
|
||||||
threshold=5,
|
|
||||||
grid_spacing=40,
|
|
||||||
arrow_scale=1.0,
|
|
||||||
margin=0.8,
|
|
||||||
colormap=cv2.COLORMAP_PARULA,
|
|
||||||
below_threshold_color=(0, 0, 255),
|
|
||||||
edge_margin=15,
|
|
||||||
target_size=936,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Draw colored optical flow arrows on the image by magnitude (JET colormap).
|
|
||||||
Resizes frame and flow to target_size x target_size; scales flow vectors and
|
|
||||||
grid_spacing / margins / threshold so arrow density matches the original resolution.
|
|
||||||
"""
|
|
||||||
image = frame.copy()
|
|
||||||
if len(image.shape) == 2 or image.shape[2] == 1:
|
|
||||||
image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
|
|
||||||
|
|
||||||
h0, w0 = image.shape[:2]
|
|
||||||
if h0 != target_size or w0 != target_size:
|
|
||||||
sx = target_size / float(w0)
|
|
||||||
sy = target_size / float(h0)
|
|
||||||
image = cv2.resize(image, (target_size, target_size), interpolation=cv2.INTER_LINEAR)
|
|
||||||
u = cv2.resize(optical_flow[:, :, 0], (target_size, target_size), interpolation=cv2.INTER_LINEAR) * sx
|
|
||||||
v = cv2.resize(optical_flow[:, :, 1], (target_size, target_size), interpolation=cv2.INTER_LINEAR) * sy
|
|
||||||
optical_flow = np.stack([u, v], axis=2)
|
|
||||||
scale = max(sx, sy)
|
|
||||||
grid_spacing = max(4, int(round(grid_spacing * scale)))
|
|
||||||
edge_margin = max(1, int(round(edge_margin * scale)))
|
|
||||||
threshold = float(threshold) * scale
|
|
||||||
|
|
||||||
h, w = image.shape[:2]
|
|
||||||
# Arrow grid in pixel coordinates (assume frame and flow same size)
|
|
||||||
rr = np.arange(grid_spacing // 2, h, grid_spacing)
|
|
||||||
cc = np.arange(grid_spacing // 2, w, grid_spacing)
|
|
||||||
rr, cc = np.meshgrid(rr, cc, indexing="ij")
|
|
||||||
rr, cc = rr.reshape(-1), cc.reshape(-1)
|
|
||||||
|
|
||||||
# Displacement at each grid point (flow stored as half-displacement)
|
|
||||||
disp = 2.0 * optical_flow[rr, cc, :]
|
|
||||||
mag = np.linalg.norm(disp, axis=1)
|
|
||||||
dx = disp[:, 0] / arrow_scale
|
|
||||||
dy = disp[:, 1] / arrow_scale
|
|
||||||
length = np.sqrt(dx * dx + dy * dy)
|
|
||||||
# Cap arrow length at grid_spacing
|
|
||||||
scale = np.where(length > grid_spacing*margin, grid_spacing / np.maximum(length, 1e-9), 1.0)*margin
|
|
||||||
dx, dy = dx * scale, dy * scale
|
|
||||||
end_x = (cc + dx).astype(np.int32)
|
|
||||||
end_y = (rr + dy).astype(np.int32)
|
|
||||||
|
|
||||||
valid = (
|
|
||||||
(cc >= edge_margin) & (cc < w - edge_margin) & (rr >= edge_margin) & (rr < h - edge_margin)
|
|
||||||
& (end_x >= 0) & (end_x < w) & (end_y >= 0) & (end_y < h)
|
|
||||||
)
|
|
||||||
mag_range = mag.max() - mag.min()
|
|
||||||
norm_mag = ((mag - mag.min()) / mag_range * 255).astype(np.uint8) if mag_range > 0 else np.zeros_like(mag, dtype=np.uint8)
|
|
||||||
norm_mag[mag < threshold] = 0
|
|
||||||
colors = cv2.applyColorMap(norm_mag.reshape(-1, 1), colormap)
|
|
||||||
|
|
||||||
for i in np.where(valid)[0]:
|
|
||||||
pt1, pt2 = (int(cc[i]), int(rr[i])), (int(end_x[i]), int(end_y[i]))
|
|
||||||
color = tuple(int(c) for c in colors[i, 0])
|
|
||||||
if mag[i] < threshold:
|
|
||||||
cv2.circle(image, pt1, radius=1, color=below_threshold_color, thickness=-1)
|
|
||||||
else:
|
|
||||||
# thickness = max(1, int(2 * mag[i] / mag.max())) if mag.max() > 0 else 2
|
|
||||||
thickness = 1
|
|
||||||
cv2.arrowedLine(image, pt1, pt2, color, thickness=thickness, tipLength=0.2)
|
|
||||||
return image
|
|
||||||
|
|
||||||
|
|
||||||
def set_arrow_label_bgr(label, bgr_image):
|
|
||||||
"""Show a BGR OpenCV image in a QLabel using source-screen pre-distortion."""
|
|
||||||
rgb = cv2.cvtColor(bgr_image, cv2.COLOR_BGR2RGB)
|
|
||||||
rgb = np.ascontiguousarray(rgb)
|
|
||||||
h, w, ch = rgb.shape
|
|
||||||
qimg = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888).copy()
|
|
||||||
pm = QPixmap.fromImage(qimg)
|
|
||||||
lw, lh = label.width(), label.height()
|
|
||||||
if lw > 0 and lh > 0 and (pm.width() != lw or pm.height() != lh):
|
|
||||||
pm = pm.scaled(lw, lh, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
|
|
||||||
label.setPixmap(pm)
|
|
||||||
|
|
||||||
def set_video_label_bgr(label, bgr_image):
|
|
||||||
"""Show loop video in the top frame using the fitted display size."""
|
|
||||||
rgb = cv2.cvtColor(bgr_image, cv2.COLOR_BGR2RGB)
|
|
||||||
rgb = np.ascontiguousarray(rgb)
|
|
||||||
h, w, ch = rgb.shape
|
|
||||||
qimg = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888).copy()
|
|
||||||
pm = QPixmap.fromImage(qimg)
|
|
||||||
lw, lh = label.width(), label.height()
|
|
||||||
if lw > 0 and lh > 0 and (pm.width() != lw or pm.height() != lh):
|
|
||||||
pm = pm.scaled(lw, lh, Qt.IgnoreAspectRatio, Qt.FastTransformation)
|
|
||||||
label.setPixmap(pm)
|
|
||||||
|
|
||||||
|
|
||||||
def read_loop_video_frame_realtime(video_cap, video_clock):
|
|
||||||
"""Read loop-video frame paced to wall clock; skip frames when main loop is slow."""
|
|
||||||
if video_cap is None or not video_cap.isOpened():
|
|
||||||
return False, None
|
|
||||||
|
|
||||||
now = time.perf_counter()
|
|
||||||
fps = max(1.0, float(video_clock.get("fps", 30.0)))
|
|
||||||
frame_dt = 1.0 / fps
|
|
||||||
next_ts = video_clock.get("next_ts")
|
|
||||||
if next_ts is None:
|
|
||||||
next_ts = now
|
|
||||||
|
|
||||||
frames_due = 1
|
|
||||||
if now > next_ts + frame_dt:
|
|
||||||
frames_due += int((now - next_ts) / frame_dt)
|
|
||||||
# Cap skip burst to avoid long catch-up loops after pauses.
|
|
||||||
frames_due = min(frames_due, int(max(2.0, fps)))
|
|
||||||
|
|
||||||
for _ in range(frames_due - 1):
|
|
||||||
if not video_cap.grab():
|
|
||||||
video_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
|
||||||
if not video_cap.grab():
|
|
||||||
return False, None
|
|
||||||
|
|
||||||
vret, vframe = video_cap.read()
|
|
||||||
if not vret:
|
|
||||||
video_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
|
||||||
vret, vframe = video_cap.read()
|
|
||||||
if not vret:
|
|
||||||
return False, None
|
|
||||||
|
|
||||||
next_ts = next_ts + frames_due * frame_dt
|
|
||||||
if next_ts < now - 2.0 * frame_dt:
|
|
||||||
next_ts = now
|
|
||||||
video_clock["next_ts"] = next_ts
|
|
||||||
return True, vframe
|
|
||||||
|
|
||||||
|
|
||||||
def make_gradient_brush(color_hex, max_alpha=150):
|
|
||||||
"""
|
|
||||||
创建一个从上到下渐变的笔刷 (兼容 PyQt5 和 PyQt6)
|
|
||||||
"""
|
|
||||||
c_top = QtGui.QColor(color_hex)
|
|
||||||
c_top.setAlpha(max_alpha) # 顶部半透明
|
|
||||||
|
|
||||||
c_bot = QtGui.QColor(color_hex)
|
|
||||||
c_bot.setAlpha(0) # 底部全透明
|
|
||||||
|
|
||||||
# 创建线性渐变:(0,0)到(0,1) 代表从控件顶部到底部
|
|
||||||
grad = QtGui.QLinearGradient(0, 0, 0, 1)
|
|
||||||
|
|
||||||
if hasattr(QtGui.QGradient, 'CoordinateMode'):
|
|
||||||
grad.setCoordinateMode(QtGui.QGradient.CoordinateMode.ObjectBoundingMode)
|
|
||||||
else:
|
|
||||||
grad.setCoordinateMode(QtGui.QGradient.ObjectBoundingMode)
|
|
||||||
# ==============================================
|
|
||||||
|
|
||||||
grad.setColorAt(0, c_top) # 0.0 (顶端) 颜色
|
|
||||||
grad.setColorAt(1, c_bot) # 1.0 (底端) 颜色
|
|
||||||
|
|
||||||
return QtGui.QBrush(grad)
|
|
||||||
|
|
||||||
def create_plot(plot_container):
|
|
||||||
pg.setConfigOption("background", PLOT_THEME_BG)
|
|
||||||
pg.setConfigOption("foreground", PLOT_THEME_FG)
|
|
||||||
pg.setConfigOptions(antialias=True) # 抗锯齿
|
|
||||||
theme_color = '#00E5FF' # '#00E5FF'
|
|
||||||
line_pen = pg.mkPen(color=theme_color, width=2)
|
|
||||||
grad_brush = make_gradient_brush(theme_color, max_alpha=120)
|
|
||||||
|
|
||||||
axis_font = QFont("Segoe UI", scale_ui_px(11, floor=8))
|
|
||||||
p1 = plot_container.addPlot()
|
|
||||||
p1.setLabel('left', 'N')
|
|
||||||
p1.getAxis('left').label.setFont(axis_font)
|
|
||||||
p1.getAxis('left').label.setRotation(dispaly_angle)
|
|
||||||
p1.showGrid(x=True, y=True, alpha=0.3)
|
|
||||||
plot_container.nextRow()
|
|
||||||
p2 = plot_container.addPlot()
|
|
||||||
p2.setLabel('left', 'X')
|
|
||||||
p2.getAxis('left').label.setFont(axis_font)
|
|
||||||
p2.getAxis('left').label.setRotation(dispaly_angle)
|
|
||||||
p2.showGrid(x=True, y=True, alpha=0.3)
|
|
||||||
plot_container.nextRow()
|
|
||||||
p3 = plot_container.addPlot()
|
|
||||||
p3.setLabel('left', 'Y')
|
|
||||||
|
|
||||||
# p3.setLabel('bottom', 'Time', units='s')
|
|
||||||
p3.getAxis('left').label.setFont(axis_font)
|
|
||||||
p3.getAxis('left').label.setRotation(dispaly_angle)
|
|
||||||
p3.getAxis('bottom').label.setFont(axis_font)
|
|
||||||
p3.showGrid(x=True, y=True, alpha=0.3)
|
|
||||||
_plot_bg = pg.mkColor(PLOT_THEME_BG)
|
|
||||||
for p in (p1, p2, p3):
|
|
||||||
p.getAxis("bottom").setStyle(showValues=False)
|
|
||||||
p.getAxis("left").setStyle(showValues=False)
|
|
||||||
# Keep title as "N" / "X" / "Y" — no PyQtGraph "(x0.001)" suffix on small ranges.
|
|
||||||
p.getAxis("left").enableAutoSIPrefix(False)
|
|
||||||
# PlotItem.setBackground exists only in newer pyqtgraph; ViewBox works on older installs.
|
|
||||||
p.vb.setBackgroundColor(_plot_bg)
|
|
||||||
plot_af = p1.plot(pen=line_pen, fillLevel=0, fillBrush=grad_brush)
|
|
||||||
plot_tfx = p2.plot(pen=line_pen, fillLevel=0, fillBrush=grad_brush)
|
|
||||||
plot_tfy = p3.plot(pen=line_pen, fillLevel=0, fillBrush=grad_brush)
|
|
||||||
return p1, p2, p3, plot_af, plot_tfx, plot_tfy
|
|
||||||
|
|
||||||
def _plot_y_range(y):
|
|
||||||
"""Avoid degenerate Y span; explicit x/y in setData so older PyQtGraph never plots time as y."""
|
|
||||||
mn = float(np.min(y))
|
|
||||||
mx = float(np.max(y))
|
|
||||||
if not (np.isfinite(mn) and np.isfinite(mx)):
|
|
||||||
return 0.0, 1.0
|
|
||||||
if mx - mn <= 1e-15:
|
|
||||||
mid = 0.5 * (mn + mx)
|
|
||||||
return mid - 0.5, mid + 0.5
|
|
||||||
pad = (mx - mn) * 0.05
|
|
||||||
return mn - pad, mx + pad
|
|
||||||
|
|
||||||
|
|
||||||
def update(plot_af, plot_tfx, plot_tfy, data, p1, p2, p3):
|
|
||||||
# global plot_af, plot_tfx, plot_tfy, data, p1, p2, p3
|
|
||||||
plot_af.setData(data[:, 0, 0], data[:, 1, 0])
|
|
||||||
p1.setYRange(*_plot_y_range(data[:, 1, 0]))
|
|
||||||
plot_tfx.setData(data[:, 0, 1], data[:, 1, 1])
|
|
||||||
p2.setYRange(*_plot_y_range(data[:, 1, 1]))
|
|
||||||
plot_tfy.setData(data[:, 0, 2], data[:, 1, 2])
|
|
||||||
p3.setYRange(*_plot_y_range(data[:, 1, 2]))
|
|
||||||
|
|
||||||
|
|
||||||
def build_sensor_panel(parent_layout):
|
|
||||||
panel = QWidget()
|
|
||||||
panel_layout = QHBoxLayout(panel)
|
|
||||||
panel_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
panel_layout.setSpacing(0)
|
|
||||||
|
|
||||||
# Ignored: do not let pixmap/content drive QLabel size — layout stretch sets arrow vs plot widths.
|
|
||||||
_policy_layout = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
|
|
||||||
|
|
||||||
arrow_label = QLabel()
|
|
||||||
arrow_label.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
|
|
||||||
arrow_label.setStyleSheet(
|
|
||||||
"background-color: #0B1016; margin: 0; padding: 0; border: none;"
|
|
||||||
)
|
|
||||||
arrow_label.setSizePolicy(_policy_layout)
|
|
||||||
arrow_label.setMinimumSize(0, 0)
|
|
||||||
arrow_label.setScaledContents(False)
|
|
||||||
panel_layout.addWidget(arrow_label, stretch=ARROW_STRETCH)
|
|
||||||
|
|
||||||
plot_widget = pg.GraphicsLayoutWidget()
|
|
||||||
plot_widget.setBackground(PLOT_THEME_BG)
|
|
||||||
plot_widget.setSizePolicy(_policy_layout)
|
|
||||||
plot_widget.setMinimumSize(0, 0)
|
|
||||||
panel_layout.addWidget(plot_widget, stretch=PLOT_STRETCH)
|
|
||||||
|
|
||||||
disconnected_label = QLabel("已断开")
|
|
||||||
disconnected_label.setAlignment(Qt.AlignCenter)
|
|
||||||
disconnected_label.setStyleSheet(
|
|
||||||
f"background-color: #0B1016; color: #FF4D4D; font-size: {scale_ui_px(54, floor=24)}px; "
|
|
||||||
"font-weight: bold; letter-spacing: 2px; margin: 0; padding: 0; border: none;"
|
|
||||||
)
|
|
||||||
disconnected_label.hide()
|
|
||||||
panel_layout.addWidget(disconnected_label, stretch=SINGLE_PANEL_W)
|
|
||||||
|
|
||||||
parent_layout.addWidget(panel, stretch=1)
|
|
||||||
|
|
||||||
p1, p2, p3, plot_af, plot_tfx, plot_tfy = create_plot(plot_widget)
|
|
||||||
data = np.zeros([npoints, 2, 3])
|
|
||||||
for i in range(3):
|
|
||||||
data[:, 0, i] = np.linspace(0.0, npoints / 30.0, npoints)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"panel": panel,
|
|
||||||
"arrow_label": arrow_label,
|
|
||||||
"plot_widget": plot_widget,
|
|
||||||
"disconnected_label": disconnected_label,
|
|
||||||
"p1": p1,
|
|
||||||
"p2": p2,
|
|
||||||
"p3": p3,
|
|
||||||
"plot_af": plot_af,
|
|
||||||
"plot_tfx": plot_tfx,
|
|
||||||
"plot_tfy": plot_tfy,
|
|
||||||
"data": data,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 实时图表:单窗口,两套并排(左 sensor1,右 sensor2)
|
|
||||||
pg.setConfigOption("background", PLOT_THEME_BG)
|
|
||||||
pg.setConfigOption("foreground", PLOT_THEME_FG)
|
|
||||||
app = pg.mkQApp("Wuxi Demo")
|
|
||||||
screen = app.primaryScreen()
|
|
||||||
screen_geometry = screen.availableGeometry() if screen is not None else None
|
|
||||||
screen_w = screen_geometry.width() if screen_geometry is not None else 1920
|
|
||||||
screen_h = screen_geometry.height() if screen_geometry is not None else 1080
|
|
||||||
source_layout = compute_source_layout(screen_w, screen_h)
|
|
||||||
ASPECT_W = source_layout["window_w"]
|
|
||||||
ASPECT_H_TOTAL = source_layout["window_h"]
|
|
||||||
VIDEO_FRAME_W = source_layout["video_w"]
|
|
||||||
VIDEO_FRAME_H = source_layout["video_h"]
|
|
||||||
BOTTOM_PANEL_H = source_layout["bottom_h"]
|
|
||||||
SINGLE_PANEL_W = source_layout["single_panel_w"]
|
|
||||||
UI_SCALE = max(0.1, screen_h / float(TARGET_ASPECT_H_TOTAL))
|
|
||||||
|
|
||||||
combined_win = CombinedTactileWindow()
|
|
||||||
combined_win.setWindowTitle("Wuxi tactile — dual flow + forces")
|
|
||||||
main_layout = QVBoxLayout(combined_win)
|
|
||||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
main_layout.setSpacing(0)
|
|
||||||
|
|
||||||
video_label = QLabel()
|
|
||||||
video_label.setAlignment(Qt.AlignCenter)
|
|
||||||
video_label.setStyleSheet("background-color: #000000; margin: 0; padding: 0; border: none;")
|
|
||||||
video_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
||||||
video_label.setMinimumSize(0, 0)
|
|
||||||
main_layout.addWidget(video_label, stretch=VIDEO_FRAME_H)
|
|
||||||
|
|
||||||
bottom_container = QWidget()
|
|
||||||
bottom_layout = QHBoxLayout(bottom_container)
|
|
||||||
bottom_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
bottom_layout.setSpacing(0)
|
|
||||||
main_layout.addWidget(bottom_container, stretch=BOTTOM_PANEL_H)
|
|
||||||
|
|
||||||
panel1_ui = build_sensor_panel(bottom_layout)
|
|
||||||
panel2_ui = build_sensor_panel(bottom_layout)
|
|
||||||
|
|
||||||
if USE_FRAMELESS_WINDOW:
|
|
||||||
combined_win.setWindowFlags(Qt.Window | Qt.FramelessWindowHint)
|
|
||||||
# Client area matches theme (no bright border flash on frameless)
|
|
||||||
combined_win.setStyleSheet(f"background-color: {PLOT_THEME_BG};")
|
|
||||||
combined_win.resize(ASPECT_W, ASPECT_H_TOTAL)
|
|
||||||
combined_win.install_drag_filters()
|
|
||||||
combined_win.showFullScreen()
|
|
||||||
closed_hour_overlay = QLabel(combined_win)
|
|
||||||
closed_hour_overlay.setAlignment(Qt.AlignCenter)
|
|
||||||
closed_hour_overlay.setStyleSheet(
|
|
||||||
f"background-color: {PLOT_THEME_BG}; color: #A0A0A0; "
|
|
||||||
f"font-size: {scale_ui_px(72, floor=32)}px; font-weight: bold; "
|
|
||||||
"margin: 0; padding: 0; border: none;"
|
|
||||||
)
|
|
||||||
closed_hour_overlay.setText("Closed hour\n非营业时间")
|
|
||||||
closed_hour_overlay.hide()
|
|
||||||
start_time = None # Will be set when sensor starts
|
|
||||||
fps = 30.0
|
|
||||||
|
|
||||||
|
|
||||||
def outside_opening_hours(now):
|
|
||||||
return now.hour < open_hour or now.hour >= close_hour
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
global start_time
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("--video1","-v1", type=str, default="0", help="视频文件路径")
|
|
||||||
parser.add_argument("--video2","-v2", type=str, default="0", help="视频文件路径")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
session_start = datetime.now()
|
|
||||||
print(
|
|
||||||
f"[session] START {session_start:%Y-%m-%d %H:%M:%S} (long-run / crash log marker)",
|
|
||||||
flush=True,
|
|
||||||
)
|
|
||||||
if USE_FRAMELESS_WINDOW:
|
|
||||||
print(
|
|
||||||
"[window] Frameless: resize = drag window edges/corners; move = Alt+left-drag; quit = Q.",
|
|
||||||
flush=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
sensor1 = None
|
|
||||||
sensor2 = None
|
|
||||||
video_cap = None
|
|
||||||
try:
|
|
||||||
input1 = int(args.video1)
|
|
||||||
input2 = int(args.video2)
|
|
||||||
is_camera = True
|
|
||||||
except:
|
|
||||||
is_camera = False
|
|
||||||
try:
|
|
||||||
if is_camera:
|
|
||||||
sensor1 = orisys.Sensor(
|
|
||||||
input1,
|
|
||||||
config_name="./config/ddjx01_wuxi.json",
|
|
||||||
cal_path="./config/dd01a_0306.npy",
|
|
||||||
isstitch=True,
|
|
||||||
cuda=False,
|
|
||||||
verbose=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
sensor2 = orisys.Sensor(
|
|
||||||
input2,
|
|
||||||
config_name="./config/ddjx01_wuxi.json",
|
|
||||||
cal_path="./config/dd01a_0306.npy",
|
|
||||||
isstitch=True,
|
|
||||||
cuda=False,
|
|
||||||
verbose=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
sensor1 = orisys.Sensor(
|
|
||||||
args.video1,
|
|
||||||
config_name="./config/ddjx01_wuxi.json",
|
|
||||||
cal_path="./config/dd01a_0306.npy",
|
|
||||||
isstitch=True,
|
|
||||||
cuda=False,
|
|
||||||
verbose=False,
|
|
||||||
)
|
|
||||||
sensor2 = orisys.Sensor(
|
|
||||||
args.video2,
|
|
||||||
config_name="./config/ddjx01_wuxi.json",
|
|
||||||
cal_path="./config/dd01a_0306.npy",
|
|
||||||
isstitch=True,
|
|
||||||
cuda=False,
|
|
||||||
verbose=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _shortcut_reset_tracker():
|
|
||||||
sensor1.reset()
|
|
||||||
sensor2.reset()
|
|
||||||
print("传感器已重置 (快捷键 R)", flush=True)
|
|
||||||
|
|
||||||
def _shortcut_quit():
|
|
||||||
combined_win.close()
|
|
||||||
def _shortcut_toggle_open():
|
|
||||||
global is_open
|
|
||||||
is_open = not is_open
|
|
||||||
print(f"营业状态: {'营业' if is_open else '非营业'}", flush=True)
|
|
||||||
|
|
||||||
_sc_r = QShortcut(QKeySequence(Qt.Key_R), combined_win)
|
|
||||||
_sc_r.activated.connect(_shortcut_reset_tracker)
|
|
||||||
if manual_open_enable:
|
|
||||||
_sc_g = QShortcut(QKeySequence(Qt.Key_G), combined_win)
|
|
||||||
_sc_g.activated.connect(_shortcut_toggle_open)
|
|
||||||
_sc_q = QShortcut(QKeySequence(Qt.Key_Q), combined_win)
|
|
||||||
_sc_q.activated.connect(_shortcut_quit)
|
|
||||||
|
|
||||||
video_cap = cv2.VideoCapture(VIDEO_LOOP_PATH)
|
|
||||||
video_clock = {"fps": 30.0, "next_ts": None}
|
|
||||||
if not video_cap.isOpened():
|
|
||||||
print(f"[video] Failed to open loop video: {VIDEO_LOOP_PATH}", flush=True)
|
|
||||||
else:
|
|
||||||
src_fps = float(video_cap.get(cv2.CAP_PROP_FPS) or 0.0)
|
|
||||||
if np.isfinite(src_fps) and src_fps >= 1.0:
|
|
||||||
video_clock["fps"] = src_fps
|
|
||||||
|
|
||||||
start_time = time.time()
|
|
||||||
is_save = False
|
|
||||||
frame_count = 0
|
|
||||||
writer = None
|
|
||||||
sensor1_active = True
|
|
||||||
sensor2_active = True
|
|
||||||
# Local to main(): assignments inside the loop make this local; must exist
|
|
||||||
# before the first iteration that skips the closed branch.
|
|
||||||
start_open = False
|
|
||||||
while True:
|
|
||||||
now = datetime.now()
|
|
||||||
if outside_opening_hours(now) or not is_open:
|
|
||||||
start_open = False
|
|
||||||
closed_hour_overlay.setGeometry(0, 0, combined_win.width(), combined_win.height())
|
|
||||||
closed_hour_overlay.raise_()
|
|
||||||
closed_hour_overlay.show()
|
|
||||||
app.processEvents()
|
|
||||||
if combined_win.user_closed or not combined_win.isVisible():
|
|
||||||
print("\n窗口已关闭,退出程序")
|
|
||||||
if writer is not None:
|
|
||||||
writer.release()
|
|
||||||
writer = None
|
|
||||||
break
|
|
||||||
time.sleep(1)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not start_open:
|
|
||||||
start_open = True
|
|
||||||
sensor1.reset()
|
|
||||||
sensor2.reset()
|
|
||||||
print(f"传感器已重置: {datetime.now():%Y-%m-%d %H:%M:%S}", flush=True)
|
|
||||||
closed_hour_overlay.hide()
|
|
||||||
|
|
||||||
if video_cap is not None and video_cap.isOpened():
|
|
||||||
vret, vframe = read_loop_video_frame_realtime(video_cap, video_clock)
|
|
||||||
if vret:
|
|
||||||
if vframe.shape[1] != VIDEO_FRAME_W or vframe.shape[0] != VIDEO_FRAME_H:
|
|
||||||
vframe = cv2.resize(vframe, (VIDEO_FRAME_W, VIDEO_FRAME_H), interpolation=cv2.INTER_LINEAR)
|
|
||||||
set_video_label_bgr(video_label, vframe)
|
|
||||||
|
|
||||||
if sensor1_active:
|
|
||||||
img1_valid = sensor1.get_img()
|
|
||||||
if img1_valid is None:
|
|
||||||
panel1_ui["arrow_label"].hide()
|
|
||||||
panel1_ui["plot_widget"].hide()
|
|
||||||
panel1_ui["disconnected_label"].show()
|
|
||||||
sensor1_active = False
|
|
||||||
print(f"Sensor1 disconnected. Showing DISCONNECTED screen. {datetime.now():%Y-%m-%d %H:%M:%S}", flush=True)
|
|
||||||
else:
|
|
||||||
sensor1.compute_deformation(check_motion=True, tail_frames=frame_to_recover, threshold=touch_threshold)
|
|
||||||
_, fn, fx, fy, flow = sensor1.read_info(
|
|
||||||
sensor1.info.FPS,
|
|
||||||
sensor1.info.FNORMAL,
|
|
||||||
sensor1.info.FSHEARX,
|
|
||||||
sensor1.info.FSHEARY,
|
|
||||||
sensor1.info.VRAW,
|
|
||||||
)
|
|
||||||
_bg_bgr = (22, 16, 11)
|
|
||||||
_img = sensor1.img
|
|
||||||
_h, _w = _img.shape[:2]
|
|
||||||
black_img = np.empty((_h, _w, 3), dtype=_img.dtype)
|
|
||||||
black_img[:, :, 0] = _bg_bgr[0]
|
|
||||||
black_img[:, :, 1] = _bg_bgr[1]
|
|
||||||
black_img[:, :, 2] = _bg_bgr[2]
|
|
||||||
lw, lh = panel1_ui["arrow_label"].width(), panel1_ui["arrow_label"].height()
|
|
||||||
if lw > 16 and lh > 16:
|
|
||||||
side = max(32, min(lw, lh))
|
|
||||||
else:
|
|
||||||
side = compute_arrow_canvas_side(panel1_ui["panel"])
|
|
||||||
arrows = draw_arrows_wuxi(
|
|
||||||
black_img,
|
|
||||||
flow,
|
|
||||||
threshold=2,
|
|
||||||
grid_spacing=20,
|
|
||||||
arrow_scale=0.5,
|
|
||||||
below_threshold_color=(160, 160, 160),
|
|
||||||
target_size=int(side),
|
|
||||||
)
|
|
||||||
set_arrow_label_bgr(panel1_ui["arrow_label"], arrows)
|
|
||||||
data1 = panel1_ui["data"]
|
|
||||||
for i in range(3):
|
|
||||||
data1[:-1, 0, i] = data1[1:, 0, i]
|
|
||||||
data1[:-1, 1, i] = data1[1:, 1, i]
|
|
||||||
data1[-1, 0, :] = frame_count / 15.0
|
|
||||||
data1[-1, 1, 0] = fn
|
|
||||||
data1[-1, 1, 1] = abs(fx)
|
|
||||||
data1[-1, 1, 2] = abs(fy)
|
|
||||||
update(
|
|
||||||
panel1_ui["plot_af"],
|
|
||||||
panel1_ui["plot_tfx"],
|
|
||||||
panel1_ui["plot_tfy"],
|
|
||||||
data1,
|
|
||||||
panel1_ui["p1"],
|
|
||||||
panel1_ui["p2"],
|
|
||||||
panel1_ui["p3"],
|
|
||||||
)
|
|
||||||
|
|
||||||
if sensor2_active:
|
|
||||||
img2_valid = sensor2.get_img()
|
|
||||||
if img2_valid is None:
|
|
||||||
panel2_ui["arrow_label"].hide()
|
|
||||||
panel2_ui["plot_widget"].hide()
|
|
||||||
panel2_ui["disconnected_label"].show()
|
|
||||||
sensor2_active = False
|
|
||||||
print(f"Sensor2 disconnected. Showing DISCONNECTED screen. {datetime.now():%Y-%m-%d %H:%M:%S}", flush=True)
|
|
||||||
else:
|
|
||||||
sensor2.compute_deformation(check_motion=True, tail_frames=frame_to_recover, threshold=touch_threshold)
|
|
||||||
_, fn2, fx2, fy2, flow2 = sensor2.read_info(
|
|
||||||
sensor2.info.FPS,
|
|
||||||
sensor2.info.FNORMAL,
|
|
||||||
sensor2.info.FSHEARX,
|
|
||||||
sensor2.info.FSHEARY,
|
|
||||||
sensor2.info.VRAW,
|
|
||||||
)
|
|
||||||
_bg_bgr2 = (22, 16, 11)
|
|
||||||
_img2 = sensor2.img
|
|
||||||
_h2, _w2 = _img2.shape[:2]
|
|
||||||
black_img2 = np.empty((_h2, _w2, 3), dtype=_img2.dtype)
|
|
||||||
black_img2[:, :, 0] = _bg_bgr2[0]
|
|
||||||
black_img2[:, :, 1] = _bg_bgr2[1]
|
|
||||||
black_img2[:, :, 2] = _bg_bgr2[2]
|
|
||||||
lw2, lh2 = panel2_ui["arrow_label"].width(), panel2_ui["arrow_label"].height()
|
|
||||||
if lw2 > 16 and lh2 > 16:
|
|
||||||
side2 = max(32, min(lw2, lh2))
|
|
||||||
else:
|
|
||||||
side2 = compute_arrow_canvas_side(panel2_ui["panel"])
|
|
||||||
arrows2 = draw_arrows_wuxi(
|
|
||||||
black_img2,
|
|
||||||
flow2,
|
|
||||||
threshold=2,
|
|
||||||
grid_spacing=20,
|
|
||||||
arrow_scale=0.5,
|
|
||||||
below_threshold_color=(160, 160, 160),
|
|
||||||
target_size=int(side2),
|
|
||||||
)
|
|
||||||
set_arrow_label_bgr(panel2_ui["arrow_label"], arrows2)
|
|
||||||
data2 = panel2_ui["data"]
|
|
||||||
for i in range(3):
|
|
||||||
data2[:-1, 0, i] = data2[1:, 0, i]
|
|
||||||
data2[:-1, 1, i] = data2[1:, 1, i]
|
|
||||||
data2[-1, 0, :] = frame_count / 15.0
|
|
||||||
data2[-1, 1, 0] = fn2
|
|
||||||
data2[-1, 1, 1] = abs(fx2)
|
|
||||||
data2[-1, 1, 2] = abs(fy2)
|
|
||||||
update(
|
|
||||||
panel2_ui["plot_af"],
|
|
||||||
panel2_ui["plot_tfx"],
|
|
||||||
panel2_ui["plot_tfy"],
|
|
||||||
data2,
|
|
||||||
panel2_ui["p1"],
|
|
||||||
panel2_ui["p2"],
|
|
||||||
panel2_ui["p3"],
|
|
||||||
)
|
|
||||||
|
|
||||||
app.processEvents()
|
|
||||||
now = datetime.now()
|
|
||||||
if now.hour == 9 and now.minute == 0 and now.second == 0:
|
|
||||||
if sensor1_active:
|
|
||||||
sensor1.reset()
|
|
||||||
if sensor2_active:
|
|
||||||
sensor2.reset()
|
|
||||||
print(
|
|
||||||
f"[session] 传感器已重置: {now:%Y-%m-%d %H:%M:%S} ",
|
|
||||||
flush=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not sensor1_active and not sensor2_active:
|
|
||||||
while not combined_win.user_closed and combined_win.isVisible():
|
|
||||||
app.processEvents()
|
|
||||||
time.sleep(0.05)
|
|
||||||
break
|
|
||||||
|
|
||||||
frame_count += 1
|
|
||||||
app.processEvents()
|
|
||||||
|
|
||||||
if combined_win.user_closed or not combined_win.isVisible():
|
|
||||||
print("\n窗口已关闭,退出程序")
|
|
||||||
if writer is not None:
|
|
||||||
writer.release()
|
|
||||||
writer = None
|
|
||||||
break
|
|
||||||
|
|
||||||
# Convert combined window to image (arrows + curves)
|
|
||||||
pixmap = combined_win.grab()
|
|
||||||
# Convert QPixmap to QImage
|
|
||||||
qimg = pixmap.toImage()
|
|
||||||
# Convert QImage to numpy array (RGB format)
|
|
||||||
qimg = qimg.convertToFormat(QImage.Format_RGB888)
|
|
||||||
width = qimg.width()
|
|
||||||
height = qimg.height()
|
|
||||||
# Convert QImage to numpy array
|
|
||||||
ptr = qimg.bits()
|
|
||||||
ptr.setsize(height * width * 3)
|
|
||||||
arr = np.array(ptr, dtype=np.uint8).reshape((height, width, 3))
|
|
||||||
|
|
||||||
except BaseException as exc:
|
|
||||||
session_abnormal = datetime.now()
|
|
||||||
print(
|
|
||||||
f"[session] ABNORMAL at {session_abnormal:%Y-%m-%d %H:%M:%S}: "
|
|
||||||
f"{type(exc).__name__}: {exc}",
|
|
||||||
flush=True,
|
|
||||||
)
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
session_end = datetime.now()
|
|
||||||
elapsed = session_end - session_start
|
|
||||||
print(
|
|
||||||
f"[session] END {session_end:%Y-%m-%d %H:%M:%S} "
|
|
||||||
f"(elapsed {elapsed})",
|
|
||||||
flush=True,
|
|
||||||
)
|
|
||||||
if sensor1 is not None:
|
|
||||||
try:
|
|
||||||
sensor1.disconnect()
|
|
||||||
except Exception as disconnect_err:
|
|
||||||
print(f"[session] disconnect error: {disconnect_err} {datetime.now():%Y-%m-%d %H:%M:%S}", flush=True)
|
|
||||||
if 'sensor2' in locals() and sensor2 is not None:
|
|
||||||
try:
|
|
||||||
sensor2.disconnect()
|
|
||||||
except Exception as disconnect_err:
|
|
||||||
print(f"[session] disconnect error (sensor2): {disconnect_err} {datetime.now():%Y-%m-%d %H:%M:%S}", flush=True)
|
|
||||||
if video_cap is not None:
|
|
||||||
video_cap.release()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -0,0 +1,461 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""UART finger controller for Radxa / Dragon Q6A (sensor-side receiver).
|
||||||
|
|
||||||
|
Combines ORY-FINGER processing (same path as ``examples/finger_pipeline.py``)
|
||||||
|
with a line-based UART command interface. A host (or ``uart_send.py``-style
|
||||||
|
transmitter) sends commands; this script replies and streams forces.
|
||||||
|
|
||||||
|
Dependencies (beyond orisys):
|
||||||
|
pip install pyserial
|
||||||
|
|
||||||
|
Default UART (Dragon Q6A):
|
||||||
|
port=/dev/ttyHS1 baud=115200 8N1
|
||||||
|
|
||||||
|
Commands (one line, case-insensitive):
|
||||||
|
search List cameras (/dev/video*) via orisys.capture.list_cameras
|
||||||
|
connect [source] Open FingerRuntime (camera index, /dev/videoN, or video file)
|
||||||
|
run Stream FNORMAL / FSHEARX / FSHEARY until stop (or EOF)
|
||||||
|
stop Stop force streaming; keep source connected
|
||||||
|
quit | exit Close runtime and serial, then exit
|
||||||
|
|
||||||
|
Replies:
|
||||||
|
camera,<index>,<path>,<WxH>
|
||||||
|
OK search <N>
|
||||||
|
OK connect <source>
|
||||||
|
OK run
|
||||||
|
force,<fnormal>,<fshearx>,<fsheary>
|
||||||
|
OK eof Video file ended (when --no-loop-video)
|
||||||
|
OK stop
|
||||||
|
OK quit
|
||||||
|
ERR <context> <message>
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Live camera over UART
|
||||||
|
python scripts/example_uart.py --port /dev/ttyHS1 --config finger
|
||||||
|
|
||||||
|
# Offline debug from a recorded file (no camera needed)
|
||||||
|
python scripts/example_uart.py --port /dev/ttyHS1 --video /path/to/clip.mp4
|
||||||
|
# then UART: connect (uses --video) or connect /path/to/clip.mp4
|
||||||
|
# then UART: run
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import serial
|
||||||
|
except ImportError as exc: # pragma: no cover
|
||||||
|
raise SystemExit(
|
||||||
|
"pyserial is required for this script.\n"
|
||||||
|
" pip install pyserial\n"
|
||||||
|
f"Import error: {exc}"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
from orisys.capture import is_live_camera, list_cameras
|
||||||
|
from orisys.finger import FingerRuntime
|
||||||
|
|
||||||
|
Source = Union[int, str]
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_source(token: str) -> Optional[Source]:
|
||||||
|
"""Accept camera index int, numeric string, V4L path, or filesystem path."""
|
||||||
|
text = str(token).strip().strip('"').strip("'")
|
||||||
|
if not text:
|
||||||
|
return None
|
||||||
|
if text.isdigit() or (text.startswith("-") and text[1:].isdigit()):
|
||||||
|
return int(text)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def _is_video_file(source: Source) -> bool:
|
||||||
|
"""True when source is a filesystem video path (not a live camera)."""
|
||||||
|
if is_live_camera(source):
|
||||||
|
return False
|
||||||
|
if isinstance(source, int):
|
||||||
|
return False
|
||||||
|
path = Path(str(source))
|
||||||
|
return path.is_file() or path.suffix.lower() in {
|
||||||
|
".mp4",
|
||||||
|
".avi",
|
||||||
|
".mkv",
|
||||||
|
".mov",
|
||||||
|
".m4v",
|
||||||
|
".wmv",
|
||||||
|
".webm",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class UartFingerController:
|
||||||
|
"""Serial command dispatcher around a single FingerRuntime."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
ser: serial.Serial,
|
||||||
|
*,
|
||||||
|
config: str = "finger",
|
||||||
|
default_source: Source = 0,
|
||||||
|
max_camera_index: int = 10,
|
||||||
|
loop_video: bool = True,
|
||||||
|
show: bool = False,
|
||||||
|
verbose: bool = False,
|
||||||
|
):
|
||||||
|
self.ser = ser
|
||||||
|
self.config = config
|
||||||
|
self.default_source = default_source
|
||||||
|
self.max_camera_index = max_camera_index
|
||||||
|
self.loop_video = loop_video
|
||||||
|
self.show = show
|
||||||
|
self.verbose = verbose
|
||||||
|
|
||||||
|
self.runtime: Optional[FingerRuntime] = None
|
||||||
|
self._connected_source: Optional[Source] = None
|
||||||
|
self._last_search_source: Optional[Source] = None
|
||||||
|
self._streaming = False
|
||||||
|
self._should_quit = False
|
||||||
|
|
||||||
|
def reply(self, line: str) -> None:
|
||||||
|
text = line.rstrip("\r\n") + "\n"
|
||||||
|
self.ser.write(text.encode("utf-8"))
|
||||||
|
self.ser.flush()
|
||||||
|
if self.verbose:
|
||||||
|
print(f"<< {text.rstrip()}", flush=True)
|
||||||
|
|
||||||
|
def close_runtime(self) -> None:
|
||||||
|
self._streaming = False
|
||||||
|
if self.runtime is not None:
|
||||||
|
try:
|
||||||
|
self.runtime.close()
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
self.runtime = None
|
||||||
|
self._connected_source = None
|
||||||
|
if self.show:
|
||||||
|
try:
|
||||||
|
cv2.destroyAllWindows()
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _resolve_connect_source(self, args: list[str]) -> Source:
|
||||||
|
if args:
|
||||||
|
# Join remaining tokens so unquoted paths with spaces still work.
|
||||||
|
joined = " ".join(args).strip()
|
||||||
|
parsed = _parse_source(joined)
|
||||||
|
if parsed is None:
|
||||||
|
raise ValueError("empty connect source")
|
||||||
|
return parsed
|
||||||
|
# Prefer --video default over last search so offline debug stays on file.
|
||||||
|
if isinstance(self.default_source, str) and not is_live_camera(self.default_source):
|
||||||
|
return self.default_source
|
||||||
|
if self._last_search_source is not None:
|
||||||
|
return self._last_search_source
|
||||||
|
return self.default_source
|
||||||
|
|
||||||
|
def cmd_search(self, _args: list[str]) -> None:
|
||||||
|
try:
|
||||||
|
cameras = list_cameras(max_index=self.max_camera_index)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
self.reply(f"ERR search {exc}")
|
||||||
|
return
|
||||||
|
|
||||||
|
self._last_search_source = None
|
||||||
|
for cam in cameras:
|
||||||
|
path = (cam.path or f"/dev/video{cam.index}").strip() or f"/dev/video{cam.index}"
|
||||||
|
size = f"{cam.width}x{cam.height}"
|
||||||
|
self.reply(f"camera,{cam.index},{path},{size}")
|
||||||
|
if self._last_search_source is None:
|
||||||
|
self._last_search_source = cam.index
|
||||||
|
self.reply(f"OK search {len(cameras)}")
|
||||||
|
|
||||||
|
def cmd_connect(self, args: list[str]) -> None:
|
||||||
|
try:
|
||||||
|
source = self._resolve_connect_source(args)
|
||||||
|
except ValueError as exc:
|
||||||
|
self.reply(f"ERR connect {exc}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if isinstance(source, str) and not is_live_camera(source):
|
||||||
|
path = Path(source)
|
||||||
|
if not path.is_file():
|
||||||
|
self.reply(f"ERR connect file not found: {source}")
|
||||||
|
return
|
||||||
|
|
||||||
|
self._streaming = False
|
||||||
|
self.close_runtime()
|
||||||
|
try:
|
||||||
|
runtime = FingerRuntime.open(
|
||||||
|
config=self.config,
|
||||||
|
video=source,
|
||||||
|
verbose=self.verbose,
|
||||||
|
)
|
||||||
|
runtime.warmup(color=False)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
self.reply(f"ERR connect {exc}")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.runtime = runtime
|
||||||
|
self._connected_source = source
|
||||||
|
kind = "file" if _is_video_file(source) else "camera"
|
||||||
|
self.reply(f"OK connect {source} ({kind})")
|
||||||
|
|
||||||
|
def _rewind_video(self) -> bool:
|
||||||
|
"""Seek connected file source back to frame 0. Return True on success."""
|
||||||
|
if self.runtime is None or self._connected_source is None:
|
||||||
|
return False
|
||||||
|
if not _is_video_file(self._connected_source):
|
||||||
|
return False
|
||||||
|
cap = self.runtime.capture
|
||||||
|
if cap is None:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return bool(cap.set(cv2.CAP_PROP_POS_FRAMES, 0))
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _poll_command_line(self, *, blocking: bool = True) -> Optional[str]:
|
||||||
|
"""Read one command line if available; return None on timeout/empty."""
|
||||||
|
if not blocking and self.ser.in_waiting <= 0:
|
||||||
|
return None
|
||||||
|
raw = self.ser.readline()
|
||||||
|
if not raw:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
line = raw.decode("utf-8", errors="replace").strip()
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
return None
|
||||||
|
return line or None
|
||||||
|
|
||||||
|
def cmd_run(self, _args: list[str]) -> None:
|
||||||
|
if self.runtime is None:
|
||||||
|
self.reply("ERR run not connected")
|
||||||
|
return
|
||||||
|
|
||||||
|
self._streaming = True
|
||||||
|
self.reply("OK run")
|
||||||
|
tactile = self.runtime.tactile
|
||||||
|
is_file = self._connected_source is not None and _is_video_file(self._connected_source)
|
||||||
|
|
||||||
|
while self._streaming and not self._should_quit:
|
||||||
|
ok, unrolled = self.runtime.grab_and_process(color=False)
|
||||||
|
if not ok:
|
||||||
|
if is_file and self.loop_video and self._rewind_video():
|
||||||
|
if self.verbose:
|
||||||
|
print("video EOF: looping", flush=True)
|
||||||
|
continue
|
||||||
|
if is_file:
|
||||||
|
self.reply("OK eof")
|
||||||
|
else:
|
||||||
|
self.reply("ERR run grab failed")
|
||||||
|
self._streaming = False
|
||||||
|
break
|
||||||
|
|
||||||
|
tactile.compute_deformation()
|
||||||
|
tactile.compute_contact()
|
||||||
|
fnormal, fshearx, fsheary = tactile.read_info(
|
||||||
|
tactile.info.FNORMAL,
|
||||||
|
tactile.info.FSHEARX,
|
||||||
|
tactile.info.FSHEARY,
|
||||||
|
)
|
||||||
|
self.reply(f"force,{float(fnormal):.6f},{float(fshearx):.6f},{float(fsheary):.6f}")
|
||||||
|
|
||||||
|
if self.show:
|
||||||
|
try:
|
||||||
|
import orisys
|
||||||
|
|
||||||
|
arrows = orisys.util.draw_arrows(
|
||||||
|
unrolled,
|
||||||
|
tactile.flow,
|
||||||
|
threshold=0.4,
|
||||||
|
grid_spacing=10,
|
||||||
|
arrow_scale=1.0,
|
||||||
|
valid_mask=tactile.valid_mask,
|
||||||
|
)
|
||||||
|
cv2.imshow("UV Unrolled", arrows)
|
||||||
|
key = cv2.waitKey(1) & 0xFF
|
||||||
|
if key == ord("q"):
|
||||||
|
self._should_quit = True
|
||||||
|
break
|
||||||
|
if key == ord("r"):
|
||||||
|
tactile.reset()
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
if self.verbose:
|
||||||
|
print(f"show error: {exc}", file=sys.stderr)
|
||||||
|
|
||||||
|
# Non-blocking command poll so force streaming is not delayed by UART timeout.
|
||||||
|
pending = self._poll_command_line(blocking=False)
|
||||||
|
if pending is not None:
|
||||||
|
if self.verbose:
|
||||||
|
print(f">> {pending}", flush=True)
|
||||||
|
if not self._dispatch(pending, allow_run=False):
|
||||||
|
break
|
||||||
|
|
||||||
|
def cmd_stop(self, _args: list[str]) -> None:
|
||||||
|
self._streaming = False
|
||||||
|
self.reply("OK stop")
|
||||||
|
|
||||||
|
def cmd_quit(self, _args: list[str]) -> None:
|
||||||
|
self._streaming = False
|
||||||
|
self._should_quit = True
|
||||||
|
self.reply("OK quit")
|
||||||
|
|
||||||
|
def _dispatch(self, line: str, *, allow_run: bool = True) -> bool:
|
||||||
|
"""Dispatch one command line. Return False if the main loop should exit."""
|
||||||
|
try:
|
||||||
|
parts = shlex.split(line)
|
||||||
|
except ValueError:
|
||||||
|
parts = line.split()
|
||||||
|
if not parts:
|
||||||
|
return True
|
||||||
|
cmd = parts[0].lower()
|
||||||
|
args = parts[1:]
|
||||||
|
|
||||||
|
if cmd == "search":
|
||||||
|
self.cmd_search(args)
|
||||||
|
elif cmd == "connect":
|
||||||
|
self.cmd_connect(args)
|
||||||
|
elif cmd == "run":
|
||||||
|
if not allow_run:
|
||||||
|
self.reply("ERR run already running")
|
||||||
|
else:
|
||||||
|
self.cmd_run(args)
|
||||||
|
elif cmd == "stop":
|
||||||
|
self.cmd_stop(args)
|
||||||
|
elif cmd in ("quit", "exit"):
|
||||||
|
self.cmd_quit(args)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
self.reply(f"ERR unknown {cmd}")
|
||||||
|
return not self._should_quit
|
||||||
|
|
||||||
|
def serve(self) -> None:
|
||||||
|
self.reply("OK ready")
|
||||||
|
while not self._should_quit:
|
||||||
|
line = self._poll_command_line()
|
||||||
|
if line is None:
|
||||||
|
continue
|
||||||
|
if self.verbose:
|
||||||
|
print(f">> {line}", flush=True)
|
||||||
|
if not self._dispatch(line):
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def open_serial(port: str, baud: int, timeout: float) -> serial.Serial:
|
||||||
|
try:
|
||||||
|
return serial.Serial(
|
||||||
|
port=port,
|
||||||
|
baudrate=baud,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
parity=serial.PARITY_NONE,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
except serial.SerialException as exc:
|
||||||
|
print(f"Serial error: {exc}", file=sys.stderr)
|
||||||
|
print("Check:", file=sys.stderr)
|
||||||
|
print(f" 1. Device node {port} exists", file=sys.stderr)
|
||||||
|
print(" 2. User has access (sudo or dialout group)", file=sys.stderr)
|
||||||
|
print(" 3. Wiring / USB-TTL connection", file=sys.stderr)
|
||||||
|
raise SystemExit(1) from exc
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="UART finger controller (search/connect/run/stop force stream)"
|
||||||
|
)
|
||||||
|
parser.add_argument("--port", default="/dev/ttyHS1", help="Serial device (default: /dev/ttyHS1)")
|
||||||
|
parser.add_argument("--baud", type=int, default=115200, help="Baud rate (default: 115200)")
|
||||||
|
parser.add_argument(
|
||||||
|
"--timeout",
|
||||||
|
type=float,
|
||||||
|
default=0.1,
|
||||||
|
help="Serial readline timeout seconds (default: 0.1)",
|
||||||
|
)
|
||||||
|
parser.add_argument("--config", "-c", default="finger", help="Orisys config name or path")
|
||||||
|
parser.add_argument(
|
||||||
|
"--camera-id",
|
||||||
|
type=int,
|
||||||
|
default=0,
|
||||||
|
help="Default camera index when connect has no argument (ignored if --video is set)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--video",
|
||||||
|
default=None,
|
||||||
|
help="Default video file for connect (offline debug without a camera)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--max-camera-index",
|
||||||
|
type=int,
|
||||||
|
default=10,
|
||||||
|
help="Highest index probed by search",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--loop-video",
|
||||||
|
dest="loop_video",
|
||||||
|
action="store_true",
|
||||||
|
default=True,
|
||||||
|
help="Loop video file on EOF during run (default: on)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-loop-video",
|
||||||
|
dest="loop_video",
|
||||||
|
action="store_false",
|
||||||
|
help="Stop run with OK eof when video file ends",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--show",
|
||||||
|
action="store_true",
|
||||||
|
help="Show UV unrolled OpenCV window (local debug only)",
|
||||||
|
)
|
||||||
|
parser.add_argument("--verbose", action="store_true", help="Echo UART traffic and SDK logs")
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
if args.video:
|
||||||
|
video_path = Path(args.video)
|
||||||
|
if not video_path.is_file():
|
||||||
|
print(f"Video file not found: {args.video}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
default_source: Source = str(video_path.resolve())
|
||||||
|
else:
|
||||||
|
default_source = args.camera_id
|
||||||
|
|
||||||
|
ser = open_serial(args.port, args.baud, args.timeout)
|
||||||
|
controller = UartFingerController(
|
||||||
|
ser,
|
||||||
|
config=args.config,
|
||||||
|
default_source=default_source,
|
||||||
|
max_camera_index=args.max_camera_index,
|
||||||
|
loop_video=args.loop_video,
|
||||||
|
show=args.show,
|
||||||
|
verbose=args.verbose,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"UART finger controller on {args.port} @ {args.baud} bps", flush=True)
|
||||||
|
print(f"Default source: {default_source}", flush=True)
|
||||||
|
print("Commands: search | connect [source|file] | run | stop | quit", flush=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
controller.serve()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nInterrupted.", flush=True)
|
||||||
|
try:
|
||||||
|
controller.reply("OK quit")
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
controller.close_runtime()
|
||||||
|
if ser.is_open:
|
||||||
|
ser.close()
|
||||||
|
print("Serial closed.", flush=True)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Shim for the Orisys finger Qt dashboard (see examples/gui/)."""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
|
|
||||||
|
|
||||||
_EXAMPLES_DIR = Path(__file__).resolve().parents[1] / "examples"
|
|
||||||
if str(_EXAMPLES_DIR) not in sys.path:
|
|
||||||
sys.path.insert(0, str(_EXAMPLES_DIR))
|
|
||||||
|
|
||||||
from argparse import Namespace
|
|
||||||
|
|
||||||
from gui import run_dashboard
|
|
||||||
from pipeline import PipelineConfig
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="Orisys finger Qt dashboard")
|
|
||||||
parser.add_argument("--video", "-v", type=str, default="0")
|
|
||||||
parser.add_argument("--config", "-c", type=str, default="dd02")
|
|
||||||
parser.add_argument("--cal", type=str, default="")
|
|
||||||
parser.add_argument("--verbose", action="store_true")
|
|
||||||
parser.add_argument("--cutoff", type=float, default=10.0)
|
|
||||||
parser.add_argument("--no-3d", action="store_true")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
config = PipelineConfig.from_args(
|
|
||||||
Namespace(
|
|
||||||
video=args.video,
|
|
||||||
config=args.config,
|
|
||||||
cal=args.cal,
|
|
||||||
verbose=args.verbose,
|
|
||||||
mesh_map="depth",
|
|
||||||
threshold=None,
|
|
||||||
viz_pose="assets/finger/visualization/world_to_cam.npy",
|
|
||||||
no_3d=args.no_3d,
|
|
||||||
cutoff=args.cutoff,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
sys.exit(run_dashboard(config))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
import argparse
|
|
||||||
import av
|
|
||||||
import cv2
|
|
||||||
import platform
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
TARGET_FPS = 30
|
|
||||||
WIDTH = 1920
|
|
||||||
HEIGHT = 1080
|
|
||||||
|
|
||||||
|
|
||||||
def list_windows_dshow_devices():
|
|
||||||
"""
|
|
||||||
Print DirectShow devices using ffmpeg, if available.
|
|
||||||
"""
|
|
||||||
cmd = ["ffmpeg", "-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "dummy"]
|
|
||||||
print("Listing Windows DirectShow devices via ffmpeg...")
|
|
||||||
try:
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
|
||||||
except FileNotFoundError as exc:
|
|
||||||
raise RuntimeError("ffmpeg was not found in PATH; cannot list DirectShow devices.") from exc
|
|
||||||
|
|
||||||
output = (result.stderr or "") + (result.stdout or "")
|
|
||||||
print(output.strip())
|
|
||||||
|
|
||||||
|
|
||||||
def open_pyav_capture(system_name, device, pixel_format):
|
|
||||||
"""
|
|
||||||
Open a camera device via PyAV on Linux or Windows.
|
|
||||||
"""
|
|
||||||
if system_name == "linux":
|
|
||||||
options = {
|
|
||||||
"video_size": f"{WIDTH}x{HEIGHT}",
|
|
||||||
"framerate": str(TARGET_FPS),
|
|
||||||
"input_format": pixel_format or "mjpeg",
|
|
||||||
}
|
|
||||||
container = av.open(device or "/dev/video4", format="v4l2", options=options)
|
|
||||||
elif system_name == "windows":
|
|
||||||
if not device:
|
|
||||||
raise RuntimeError(
|
|
||||||
"On Windows, PyAV/dshow requires a camera name, for example "
|
|
||||||
'--device "USB Camera". Use --list-devices first.'
|
|
||||||
)
|
|
||||||
options = {
|
|
||||||
"video_size": f"{WIDTH}x{HEIGHT}",
|
|
||||||
"framerate": str(TARGET_FPS),
|
|
||||||
}
|
|
||||||
if pixel_format:
|
|
||||||
options["pixel_format"] = pixel_format
|
|
||||||
container = av.open(f"video={device}", format="dshow", options=options)
|
|
||||||
else:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Unsupported OS: {platform.system()}. Only Linux and Windows are supported."
|
|
||||||
)
|
|
||||||
|
|
||||||
stream = container.streams.video[0]
|
|
||||||
return container, stream
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Capture from camera via PyAV on Linux or Windows, optionally save to MP4."
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-d",
|
|
||||||
"--device",
|
|
||||||
type=str,
|
|
||||||
default="",
|
|
||||||
help=(
|
|
||||||
"Linux: V4L2 device path like /dev/video4. "
|
|
||||||
"Windows: DirectShow camera name like 'USB Camera'. "
|
|
||||||
"If empty, Linux uses /dev/video4; Windows requires --device."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--pixel-format",
|
|
||||||
type=str,
|
|
||||||
default="",
|
|
||||||
help=(
|
|
||||||
"Optional input pixel format / format hint. "
|
|
||||||
"Examples: Linux 'mjpeg'; Windows may accept values such as 'mjpeg' if supported."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--list-devices",
|
|
||||||
action="store_true",
|
|
||||||
help="On Windows, list DirectShow device names via ffmpeg and exit.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-o",
|
|
||||||
"--output",
|
|
||||||
type=str,
|
|
||||||
default="",
|
|
||||||
help="Output MP4 path (e.g. capture.mp4). If empty, only preview, no file.",
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
system_name = platform.system().lower()
|
|
||||||
if args.list_devices:
|
|
||||||
if system_name != "windows":
|
|
||||||
raise RuntimeError("--list-devices is currently only implemented for Windows.")
|
|
||||||
list_windows_dshow_devices()
|
|
||||||
return
|
|
||||||
|
|
||||||
container, stream = open_pyav_capture(system_name, args.device, args.pixel_format)
|
|
||||||
print(
|
|
||||||
f"Using PyAV backend on {platform.system()} "
|
|
||||||
f"with device={args.device or '/dev/video4'}"
|
|
||||||
)
|
|
||||||
|
|
||||||
frame_count = 0
|
|
||||||
t_start = time.perf_counter()
|
|
||||||
|
|
||||||
writer = None
|
|
||||||
if args.output:
|
|
||||||
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
|
||||||
writer = cv2.VideoWriter(
|
|
||||||
args.output, fourcc, float(TARGET_FPS), (WIDTH, HEIGHT)
|
|
||||||
)
|
|
||||||
if not writer.isOpened():
|
|
||||||
container.close()
|
|
||||||
raise RuntimeError(f"Could not open VideoWriter for {args.output}")
|
|
||||||
print(f"Recording to {args.output} @ {TARGET_FPS} fps, {WIDTH}x{HEIGHT}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
for frame in container.decode(stream):
|
|
||||||
t0 = time.perf_counter()
|
|
||||||
img = frame.to_ndarray(format="bgr24") # OpenCV-compatible
|
|
||||||
t1 = time.perf_counter()
|
|
||||||
|
|
||||||
if writer is not None:
|
|
||||||
writer.write(img)
|
|
||||||
|
|
||||||
cv2.imshow("pyav_cam", img)
|
|
||||||
key = cv2.waitKey(1) & 0xFF
|
|
||||||
t2 = time.perf_counter()
|
|
||||||
|
|
||||||
frame_count += 1
|
|
||||||
if frame_count % 30 == 0:
|
|
||||||
elapsed = t2 - t_start
|
|
||||||
print(
|
|
||||||
f"Frame {frame_count}: convert={1000*(t1-t0):.1f}ms, "
|
|
||||||
f"imshow+waitKey={1000*(t2-t1):.1f}ms, avg_fps={frame_count/elapsed:.1f}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if key == ord("q"):
|
|
||||||
break
|
|
||||||
finally:
|
|
||||||
container.close()
|
|
||||||
if writer is not None:
|
|
||||||
writer.release()
|
|
||||||
print(f"Saved {frame_count} frames to {args.output}")
|
|
||||||
cv2.destroyAllWindows()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"unit": "N",
|
||||||
|
"channels": {
|
||||||
|
"normal": {
|
||||||
|
"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]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
@@ -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.
|
||||||
@@ -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": 1
|
||||||
|
},
|
||||||
|
"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,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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,13 +1,42 @@
|
|||||||
"""Setuptools entry point; bundle rules are read from pyproject.toml."""
|
"""Setuptools entry point; bundle rules are read from pyproject.toml.
|
||||||
|
|
||||||
|
``dev_install`` bundle items (configs/sensors) are staged into the ``orisys``
|
||||||
|
package during build so ``pip install .`` places them at
|
||||||
|
``site-packages/orisys/...`` (importlib / configs resolvers), not under
|
||||||
|
``sys.prefix/orisys`` via deprecated ``data_files``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
import sys
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
from setuptools.command.build_py import build_py as _build_py
|
||||||
|
|
||||||
REPO_ROOT = Path(__file__).resolve().parent
|
REPO_ROOT = Path(__file__).resolve().parent
|
||||||
sys.path.insert(0, str(REPO_ROOT / "build"))
|
sys.path.insert(0, str(REPO_ROOT / "build" / "common"))
|
||||||
|
|
||||||
from pyproject_utils import bundle_data_files
|
from pyproject_utils import stage_dev_bundle_into # noqa: E402
|
||||||
|
|
||||||
setup(data_files=bundle_data_files(REPO_ROOT))
|
|
||||||
|
class build_py(_build_py):
|
||||||
|
def run(self) -> None:
|
||||||
|
super().run()
|
||||||
|
# After pure modules are copied, stage configs/sensors into build/lib/orisys.
|
||||||
|
pkg_dir = Path(self.build_lib) / "orisys"
|
||||||
|
pkg_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
stage_dev_bundle_into(REPO_ROOT, pkg_dir)
|
||||||
|
|
||||||
|
|
||||||
|
setup(
|
||||||
|
cmdclass={"build_py": build_py},
|
||||||
|
# Declared so wheels record the staged files; actual copy happens in build_py.
|
||||||
|
package_data={
|
||||||
|
"orisys": [
|
||||||
|
"configs/*.json",
|
||||||
|
"sensors/**/*",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
include_package_data=True,
|
||||||
|
)
|
||||||
|
|||||||
+16
-1534
File diff suppressed because it is too large
Load Diff
+10
-2
@@ -5,13 +5,15 @@ 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.2"
|
__version__ = "0.5.0"
|
||||||
__author__ = "Orisys"
|
__author__ = "Orisys"
|
||||||
|
|
||||||
from .sensor import Sensor
|
from .sensor import Sensor
|
||||||
from . import util
|
from . import util
|
||||||
from . import configs
|
from . import configs
|
||||||
from . import finger
|
from . import finger
|
||||||
|
from . import signal_processing
|
||||||
|
from . import capture
|
||||||
from .frame_processors import FFCFrameProcessor
|
from .frame_processors import FFCFrameProcessor
|
||||||
|
|
||||||
CompoundEyeSensor = Sensor
|
CompoundEyeSensor = Sensor
|
||||||
@@ -33,9 +35,15 @@ __all__ = [
|
|||||||
# finger runtime helpers
|
# finger runtime helpers
|
||||||
'finger',
|
'finger',
|
||||||
|
|
||||||
|
# signal helpers
|
||||||
|
'signal_processing',
|
||||||
|
|
||||||
|
# capture helpers
|
||||||
|
'capture',
|
||||||
|
|
||||||
# configs utilities functions
|
# configs utilities functions
|
||||||
'list_config',
|
'list_config',
|
||||||
'export_config'
|
'export_config',
|
||||||
|
|
||||||
# test
|
# test
|
||||||
'FFCFrameProcessor'
|
'FFCFrameProcessor'
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""Capture subsystem: OpenCV source, optional background thread, discovery."""
|
||||||
|
|
||||||
|
from .config import (
|
||||||
|
MAX_SIMULTANEOUS_CAPTURES,
|
||||||
|
CaptureConfig,
|
||||||
|
CaptureHealth,
|
||||||
|
CaptureStatus,
|
||||||
|
ReconnectPolicy,
|
||||||
|
)
|
||||||
|
from .discovery import (
|
||||||
|
DEFAULT_LAPTOP_WEBCAM_PATTERNS,
|
||||||
|
DEFAULT_MAX_CAMERA_INDEX,
|
||||||
|
CameraFilter,
|
||||||
|
CameraInfo,
|
||||||
|
list_cameras,
|
||||||
|
pick_default_camera,
|
||||||
|
)
|
||||||
|
from .source import (
|
||||||
|
CaptureSource,
|
||||||
|
CaptureWorker,
|
||||||
|
is_live_camera,
|
||||||
|
normalize_source,
|
||||||
|
open_capture,
|
||||||
|
open_opencv_capture,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"MAX_SIMULTANEOUS_CAPTURES",
|
||||||
|
"DEFAULT_MAX_CAMERA_INDEX",
|
||||||
|
"DEFAULT_LAPTOP_WEBCAM_PATTERNS",
|
||||||
|
"CaptureConfig",
|
||||||
|
"CaptureHealth",
|
||||||
|
"CaptureStatus",
|
||||||
|
"ReconnectPolicy",
|
||||||
|
"CaptureSource",
|
||||||
|
"CaptureWorker",
|
||||||
|
"CameraInfo",
|
||||||
|
"CameraFilter",
|
||||||
|
"list_cameras",
|
||||||
|
"pick_default_camera",
|
||||||
|
"normalize_source",
|
||||||
|
"is_live_camera",
|
||||||
|
"open_opencv_capture",
|
||||||
|
"open_capture",
|
||||||
|
]
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user