Dev #19
@@ -8,7 +8,7 @@ and several runnable examples for camera and video-based workflows.
|
||||
|
||||
- `src/orisys/`: installable SDK package
|
||||
- `config/`: default and lab/runtime JSON configurations
|
||||
- `examples/`: user-facing example scripts
|
||||
- `examples/`: product demos (`products/ory_box`, `ory_mini`, `ory_finger`) plus shared `gui/`, `pipeline/`, `viz/`
|
||||
- `scripts/`: maintenance, calibration, packaging, and demo scripts
|
||||
- `tools/`: non-package tooling such as patch-generation utilities
|
||||
- `tests/`: automated pytest suite
|
||||
@@ -49,18 +49,36 @@ pytest
|
||||
|
||||
### 4. Run an example
|
||||
|
||||
ORY-BOX:
|
||||
|
||||
```bash
|
||||
python examples/basic_pipeline.py -v 0 -c ./config/ddjx01.json
|
||||
python examples/products/ory_box/basic_pipeline.py -v 0 -c ddjx01
|
||||
```
|
||||
|
||||
ORY-MINI:
|
||||
|
||||
```bash
|
||||
python examples/products/ory_mini/basic_pipeline.py -v 0 -c mini
|
||||
```
|
||||
|
||||
ORY-FINGER:
|
||||
|
||||
```bash
|
||||
python examples/products/ory_finger/finger_pipeline.py --camera_id 0 --config dd02
|
||||
```
|
||||
|
||||
Compatibility wrappers remain at `examples/basic_pipeline.py` and
|
||||
`examples/finger_pipeline*.py`.
|
||||
|
||||
## Configuration
|
||||
|
||||
The canonical configuration files now live in the repo-level `config/` folder.
|
||||
The package loader `orisys.configs` resolves configs from:
|
||||
|
||||
1. an explicit JSON file path
|
||||
2. a config file stem under `config/`
|
||||
3. a backward-compatible JSON `"name"` match, such as `"prototype"`
|
||||
2. a config file stem under `config/` (for example `ddjx01`, `mini`, `dd02`)
|
||||
3. a product alias such as `dd02` -> `dd02-ov`
|
||||
4. a backward-compatible JSON `"name"` match, such as `"prototype"`
|
||||
|
||||
Examples:
|
||||
|
||||
@@ -76,11 +94,24 @@ cfg = orisys.export_config("ddjx01", "./config/exported_config.json")
|
||||
- Example entry points: `examples/README.md`
|
||||
- User documentation: `docs/get_started.md`
|
||||
- Packaging helper: `build/build_nuitka.py` (compiled SDK wheel)
|
||||
- Manylinux / PyPI binary wheel (Docker Desktop): `docs/publishing.md`
|
||||
- GUI standalone folder: `build/build_gui_win.bat` → `dist/OrisysDashboard/`
|
||||
- Specialized utilities: `tools/`
|
||||
|
||||
## Publishing (Linux manylinux)
|
||||
|
||||
To build PyPI-compliant CPython 3.10/3.11/3.12 Linux wheels from Windows Docker Desktop:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\build\build_manylinux_docker.ps1
|
||||
```
|
||||
|
||||
Upload candidates land in `dist/repaired/`. Full TestPyPI → PyPI steps:
|
||||
`docs/publishing.md`.
|
||||
|
||||
## Notes for Collaborators
|
||||
|
||||
- Generated build outputs such as `build/`, `dist/`, and `SDK/` are intentionally ignored.
|
||||
- Large or generated tooling outputs should stay under ignored output folders rather than be committed.
|
||||
- If you add new images or arrays that should be versioned, review `.gitignore` first.
|
||||
- Never commit PyPI / TestPyPI API tokens.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,26 @@
|
||||
# manylinux_2_28: modern glibc baseline with binary wheels for numpy/scipy/opencv.
|
||||
# Prefer this over manylinux2014 — recent NumPy requires GCC >= 10.3 and will try
|
||||
# (and fail) to build from source on the older image.
|
||||
FROM quay.io/pypa/manylinux_2_28_x86_64:latest
|
||||
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
||||
PIP_ROOT_USER_ACTION=ignore
|
||||
|
||||
# Only install compile/repair tooling here. Runtime deps (numpy/scipy/opencv/numba)
|
||||
# are declared in the wheel metadata and installed by pip when users install orisys.
|
||||
# Nuitka is configured with --nofollow-import-to for those packages.
|
||||
RUN for PYBIN in \
|
||||
/opt/python/cp310-cp310/bin \
|
||||
/opt/python/cp311-cp311/bin \
|
||||
/opt/python/cp312-cp312/bin; do \
|
||||
echo "=== Installing build tooling into ${PYBIN} ===" && \
|
||||
"${PYBIN}/python" -m pip install --upgrade pip setuptools wheel && \
|
||||
"${PYBIN}/python" -m pip install \
|
||||
"nuitka>=2.0" \
|
||||
ordered-set \
|
||||
zstandard \
|
||||
auditwheel; \
|
||||
done
|
||||
|
||||
WORKDIR /src
|
||||
ENTRYPOINT ["/bin/bash", "/src/build/manylinux_entrypoint.sh"]
|
||||
@@ -0,0 +1,2 @@
|
||||
powershell -ExecutionPolicy Bypass -File .\build\build_manylinux_docker.ps1
|
||||
powershell -ExecutionPolicy Bypass -File .\build\build_manylinux_docker.ps1 -PythonVersions 3.10,3.12
|
||||
@@ -18,7 +18,7 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
# ---------- Configuration ----------
|
||||
ENTRY_SCRIPT = Path("examples/finger_pipeline_3dview.py").resolve()
|
||||
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
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
# Build PyPI-compliant manylinux wheels for CPython 3.10/3.11/3.12 using Docker Desktop.
|
||||
#
|
||||
# Usage (from repo root, PowerShell):
|
||||
# .\build\build_manylinux_docker.ps1
|
||||
# .\build\build_manylinux_docker.ps1 -PythonVersions 3.10,3.11,3.12
|
||||
# .\build\build_manylinux_docker.ps1 -PythonVersions 3.11
|
||||
# .\build\build_manylinux_docker.ps1 -SkipImageBuild
|
||||
#
|
||||
# From cmd.exe:
|
||||
# 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(
|
||||
[ValidateSet("pypi-linux", "internal")]
|
||||
[string]$Target = "pypi-linux",
|
||||
[string]$PythonVersions = "3.10,3.11,3.12",
|
||||
[switch]$SkipImageBuild,
|
||||
[string]$ImageName = "orisys-manylinux"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Convert-ToManylinuxTag {
|
||||
param([string]$Version)
|
||||
$v = $Version.Trim()
|
||||
switch ($v) {
|
||||
"3.10" { return "cp310-cp310" }
|
||||
"3.11" { return "cp311-cp311" }
|
||||
"3.12" { return "cp312-cp312" }
|
||||
"cp310-cp310" { return "cp310-cp310" }
|
||||
"cp311-cp311" { return "cp311-cp311" }
|
||||
"cp312-cp312" { return "cp312-cp312" }
|
||||
default {
|
||||
throw "Unsupported Python version '$Version'. Use 3.10, 3.11, and/or 3.12."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||
Set-Location $RepoRoot
|
||||
|
||||
$tags = @()
|
||||
foreach ($part in ($PythonVersions -split ",")) {
|
||||
if ([string]::IsNullOrWhiteSpace($part)) { continue }
|
||||
$tags += (Convert-ToManylinuxTag $part)
|
||||
}
|
||||
if ($tags.Count -eq 0) {
|
||||
throw "No Python versions specified."
|
||||
}
|
||||
$pythonTags = [string]::Join(",", $tags)
|
||||
|
||||
Write-Host "=== Orisys manylinux Docker build ==="
|
||||
Write-Host "Repo: $RepoRoot"
|
||||
Write-Host "Target: $Target"
|
||||
Write-Host "Image: $ImageName"
|
||||
Write-Host "Python tags: $pythonTags"
|
||||
|
||||
# Docker Desktop must be in Linux container mode for manylinux.
|
||||
$dockerInfo = docker version --format "{{.Server.Os}}" 2>$null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Docker is not available. Start Docker Desktop and retry."
|
||||
}
|
||||
if ($dockerInfo -ne "linux") {
|
||||
throw "Docker server OS is '$dockerInfo'. Switch Docker Desktop to Linux containers."
|
||||
}
|
||||
|
||||
$entrypoint = Join-Path $RepoRoot "build\manylinux_entrypoint.sh"
|
||||
# Normalize CRLF -> LF so bash in the container can execute the script.
|
||||
$raw = [System.IO.File]::ReadAllText($entrypoint)
|
||||
$normalized = $raw -replace "`r`n", "`n" -replace "`r", "`n"
|
||||
[System.IO.File]::WriteAllText($entrypoint, $normalized)
|
||||
|
||||
if (-not $SkipImageBuild) {
|
||||
Write-Host "`n=== Building Docker image ==="
|
||||
docker build -f build/Dockerfile.manylinux -t $ImageName .
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "docker build failed"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "`n=== Running manylinux container ==="
|
||||
# Mount the repo so artifacts land under dist/repaired and SDK/<version>/ on the host.
|
||||
docker run --rm `
|
||||
-e "ORISYS_BUILD_TARGET=$Target" `
|
||||
-e "ORISYS_PYTHON_TAGS=$pythonTags" `
|
||||
-v "${RepoRoot}:/src" `
|
||||
$ImageName
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "manylinux container build failed"
|
||||
}
|
||||
|
||||
Write-Host "`n=== Host artifacts ==="
|
||||
$repaired = Join-Path $RepoRoot "dist\repaired"
|
||||
if (Test-Path $repaired) {
|
||||
Get-ChildItem $repaired -Filter *.whl | ForEach-Object { Write-Host $_.FullName }
|
||||
} else {
|
||||
Write-Warning "No dist/repaired directory found."
|
||||
}
|
||||
|
||||
Write-Host "`nNext steps: see docs/publishing.md (TestPyPI then PyPI)."
|
||||
+123
-22
@@ -1,36 +1,80 @@
|
||||
"""Build a Nuitka-compiled orisys wheel and publish it to SDK/{version}/.
|
||||
|
||||
Pipeline:
|
||||
1. Compile src/orisys → .pyd
|
||||
1. Compile src/orisys → extension module (.pyd / .so)
|
||||
2. Stage wheel tree (compiled module + bundled files from pyproject.toml)
|
||||
3. Run bdist_wheel and move output to SDK/{version}/
|
||||
4. Smoke-test imports in a temporary venv
|
||||
3. Run bdist_wheel; on Linux, auditwheel-repair to a manylinux tag
|
||||
4. Smoke-test imports in a temporary venv (required for --target pypi-linux)
|
||||
|
||||
Run from repo root: python build/build_nuitka.py
|
||||
Targets:
|
||||
internal – local/SDK delivery (default). Linux still repairs with auditwheel.
|
||||
pypi-linux – PyPI binary wheel; requires Linux + CPython 3.10/3.11/3.12 + compliant tag.
|
||||
|
||||
Run from repo root:
|
||||
python build/build_nuitka.py
|
||||
python build/build_nuitka.py --target pypi-linux
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from nuitka_compile import compile_orisys, copy_compiled_orisys, install_nuitka
|
||||
from paths import NUITKA_COMPILE_DIR, NUITKA_WHEEL_DIR, REPO_ROOT, repo_path
|
||||
from paths import NUITKA_COMPILE_DIR, NUITKA_WHEEL_DIR, REPO_ROOT, repaired_wheel_dir, repo_path
|
||||
from pyproject_utils import bundle_items, extras_require, get_version, install_requires
|
||||
from verify import verify_built_wheel
|
||||
from wheel_build import publish_wheel_to_sdk, run_bdist_wheel, write_wheel_setup
|
||||
from wheel_build import (
|
||||
is_compliant_manylinux_wheel,
|
||||
publish_wheel_to_sdk,
|
||||
repair_linux_wheel,
|
||||
run_bdist_wheel,
|
||||
write_wheel_setup,
|
||||
)
|
||||
from wheel_layout import copy_bundle_items, copy_orisys_blend, package_data_from_bundle
|
||||
|
||||
VALID_TARGETS = ("internal", "pypi-linux")
|
||||
# ABI tags that manylinux images ship under /opt/python/cpXYY-cpXYY.
|
||||
PYPI_LINUX_PYTHON_VERSIONS = ((3, 10), (3, 11), (3, 12))
|
||||
|
||||
def _reset_dir(path) -> None:
|
||||
|
||||
def _reset_dir(path: Path) -> None:
|
||||
"""Remove and recreate a build output directory."""
|
||||
if path.exists():
|
||||
shutil.rmtree(path)
|
||||
path.mkdir(parents=True)
|
||||
|
||||
|
||||
def build() -> None:
|
||||
def _validate_pypi_linux_environment() -> None:
|
||||
if sys.platform != "linux":
|
||||
raise SystemExit(
|
||||
"[ERROR] --target pypi-linux must run inside a Linux manylinux container "
|
||||
"(for example quay.io/pypa/manylinux_2_28_x86_64 via Docker Desktop)."
|
||||
)
|
||||
if sys.version_info[:2] not in PYPI_LINUX_PYTHON_VERSIONS:
|
||||
supported = ", ".join(f"{major}.{minor}" for major, minor in PYPI_LINUX_PYTHON_VERSIONS)
|
||||
raise SystemExit(
|
||||
f"[ERROR] --target pypi-linux requires CPython {supported}, got "
|
||||
f"{sys.version_info.major}.{sys.version_info.minor}. "
|
||||
"Use /opt/python/cp310-cp310 (or cp311/cp312) in the manylinux_2_28 image."
|
||||
)
|
||||
|
||||
|
||||
def build(*, target: str = "internal", verify: bool | None = None) -> Path:
|
||||
if target not in VALID_TARGETS:
|
||||
raise SystemExit(f"[ERROR] Unknown target {target!r}; choose from {VALID_TARGETS}")
|
||||
|
||||
if target == "pypi-linux":
|
||||
_validate_pypi_linux_environment()
|
||||
|
||||
# pypi-linux always verifies; internal opts in with --verify.
|
||||
if verify is None:
|
||||
verify = target == "pypi-linux"
|
||||
|
||||
# --- 1. Compile ---
|
||||
install_nuitka()
|
||||
|
||||
@@ -38,7 +82,7 @@ def build() -> None:
|
||||
_reset_dir(NUITKA_COMPILE_DIR)
|
||||
_reset_dir(NUITKA_WHEEL_DIR)
|
||||
|
||||
print("\n=== Compiling with Nuitka ===")
|
||||
print(f"\n=== Compiling with Nuitka (target={target}, version={version}) ===")
|
||||
env = os.environ.copy()
|
||||
src_dir = repo_path("src")
|
||||
env["PYTHONPATH"] = src_dir.as_posix() + os.pathsep + env.get("PYTHONPATH", "")
|
||||
@@ -71,23 +115,80 @@ def build() -> None:
|
||||
extras_require=extras_require(REPO_ROOT),
|
||||
)
|
||||
|
||||
# --- 3. Build wheel and publish to SDK/ ---
|
||||
# --- 3. Build wheel, repair on Linux, publish to SDK/ ---
|
||||
wheel_path = run_bdist_wheel(NUITKA_WHEEL_DIR)
|
||||
if wheel_path is None:
|
||||
print("\n[ERROR] Wheel not found.")
|
||||
return
|
||||
raise SystemExit("\n[ERROR] Wheel not found after bdist_wheel.")
|
||||
|
||||
final_wheel = publish_wheel_to_sdk(wheel_path, version)
|
||||
print(f"\n[SUCCESS] Wheel created and moved to: {final_wheel}")
|
||||
repair_dir = repaired_wheel_dir()
|
||||
final_candidate = repair_linux_wheel(wheel_path, repair_dir)
|
||||
|
||||
# # --- 4. Verify (isolated venv; does not touch the active environment) ---
|
||||
# print("\n=== Verifying finger mesh/viewer imports ===")
|
||||
# try:
|
||||
# verify_built_wheel(final_wheel)
|
||||
# except subprocess.CalledProcessError as exc:
|
||||
# print(f"\n[WARNING] Wheel verify failed: {exc}")
|
||||
# print("The wheel was built successfully; verification runs in an isolated temp venv.")
|
||||
if target == "pypi-linux" and not is_compliant_manylinux_wheel(final_candidate):
|
||||
raise SystemExit(
|
||||
f"[ERROR] Wheel is not PyPI-compliant: {final_candidate.name}. "
|
||||
"Expected a manylinux_2_17_x86_64 (or newer) platform tag."
|
||||
)
|
||||
|
||||
# Keep a durable copy under dist/repaired for upload tooling.
|
||||
dist_repaired = REPO_ROOT / "dist" / "repaired"
|
||||
dist_repaired.mkdir(parents=True, exist_ok=True)
|
||||
dist_copy = dist_repaired / final_candidate.name
|
||||
if dist_copy.resolve() != final_candidate.resolve():
|
||||
shutil.copy2(final_candidate, dist_copy)
|
||||
|
||||
final_wheel = publish_wheel_to_sdk(final_candidate, version)
|
||||
print(f"\n[SUCCESS] Wheel created and copied to: {final_wheel}")
|
||||
print(f"[SUCCESS] Upload candidate also at: {dist_copy}")
|
||||
|
||||
# --- 4. Verify (isolated venv; does not touch the active environment) ---
|
||||
if verify:
|
||||
print("\n=== Verifying wheel imports in isolated venv ===")
|
||||
try:
|
||||
verify_built_wheel(final_wheel)
|
||||
print("[SUCCESS] Wheel smoke test passed.")
|
||||
except subprocess.CalledProcessError as exc:
|
||||
if target == "pypi-linux":
|
||||
raise SystemExit(
|
||||
f"[ERROR] Wheel verify failed (required for pypi-linux): {exc}"
|
||||
) from exc
|
||||
print(f"\n[WARNING] Wheel verify failed: {exc}")
|
||||
print("The wheel was built successfully; verification runs in an isolated temp venv.")
|
||||
else:
|
||||
print("\n[INFO] Skipping wheel verify (pass --verify to enable).")
|
||||
|
||||
return final_wheel
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> None:
|
||||
parser = argparse.ArgumentParser(description="Build a Nuitka-compiled orisys wheel.")
|
||||
parser.add_argument(
|
||||
"--target",
|
||||
choices=VALID_TARGETS,
|
||||
default="internal",
|
||||
help="internal: SDK delivery; pypi-linux: manylinux PyPI binary wheel",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--verify",
|
||||
action="store_true",
|
||||
help="Run isolated venv smoke test (always on for --target pypi-linux)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-verify",
|
||||
action="store_true",
|
||||
help="Skip smoke test when using --target internal",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
verify: bool | None
|
||||
if args.target == "pypi-linux":
|
||||
verify = True
|
||||
elif args.verify:
|
||||
verify = True
|
||||
elif args.skip_verify:
|
||||
verify = False
|
||||
else:
|
||||
verify = None
|
||||
build(target=args.target, verify=verify)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build()
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs inside quay.io/pypa/manylinux_2_28_x86_64 with the repo mounted at /src.
|
||||
# Builds one Nuitka wheel per CPython ABI listed in ORISYS_PYTHON_TAGS.
|
||||
set -euo pipefail
|
||||
|
||||
TARGET="${ORISYS_BUILD_TARGET:-pypi-linux}"
|
||||
# Comma-separated manylinux tags, e.g. "cp310-cp310,cp311-cp311,cp312-cp312"
|
||||
PYTHON_TAGS="${ORISYS_PYTHON_TAGS:-cp310-cp310,cp311-cp311,cp312-cp312}"
|
||||
|
||||
cd /src
|
||||
|
||||
echo "=== manylinux entrypoint ==="
|
||||
echo "Target: ${TARGET}"
|
||||
echo "Python tags: ${PYTHON_TAGS}"
|
||||
uname -a
|
||||
|
||||
IFS=',' read -r -a TAG_ARRAY <<< "${PYTHON_TAGS}"
|
||||
FAILED=0
|
||||
BUILT=()
|
||||
|
||||
for tag in "${TAG_ARRAY[@]}"; do
|
||||
tag="$(echo "${tag}" | tr -d '[:space:]')"
|
||||
if [[ -z "${tag}" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
PYBIN="/opt/python/${tag}/bin"
|
||||
if [[ ! -x "${PYBIN}/python" ]]; then
|
||||
echo "[ERROR] Python not found for tag ${tag}: ${PYBIN}/python" >&2
|
||||
FAILED=1
|
||||
continue
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "Building orisys wheel: ${tag}"
|
||||
echo "Python: $("${PYBIN}/python" -V)"
|
||||
echo "========================================"
|
||||
|
||||
export PATH="${PYBIN}:${PATH}"
|
||||
export PYTHONPATH="/src/src${PYTHONPATH:+:${PYTHONPATH}}"
|
||||
|
||||
"${PYBIN}/python" -m pip install -q --upgrade pip setuptools wheel auditwheel
|
||||
|
||||
if "${PYBIN}/python" build/build_nuitka.py --target "${TARGET}"; then
|
||||
BUILT+=("${tag}")
|
||||
else
|
||||
echo "[ERROR] Build failed for ${tag}" >&2
|
||||
FAILED=1
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Build artifacts ==="
|
||||
ls -la /src/dist/repaired/ || true
|
||||
ls -la /src/SDK/*/ || true
|
||||
|
||||
if [[ ${#BUILT[@]} -gt 0 ]]; then
|
||||
echo "Built tags: ${BUILT[*]}"
|
||||
fi
|
||||
|
||||
if [[ "${FAILED}" -ne 0 ]]; then
|
||||
echo "[ERROR] One or more Python ABI builds failed." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[SUCCESS] All requested manylinux wheels built."
|
||||
@@ -19,11 +19,18 @@ NUITKA_INCLUDE_PACKAGES = (
|
||||
)
|
||||
|
||||
NUITKA_NOFOLLOW_IMPORTS = (
|
||||
# Keep as runtime deps (shipped via wheel install_requires / extras).
|
||||
"numpy",
|
||||
"scipy",
|
||||
"cv2",
|
||||
"numba",
|
||||
"orisys_blend",
|
||||
"cupy",
|
||||
"cupyx",
|
||||
"pyopencl",
|
||||
"open3d",
|
||||
"PyQt5",
|
||||
"pyqtgraph",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -18,3 +18,8 @@ def repo_path(*parts: str) -> Path:
|
||||
|
||||
def sdk_dir(version: str) -> Path:
|
||||
return REPO_ROOT / "SDK" / version
|
||||
|
||||
|
||||
def repaired_wheel_dir() -> Path:
|
||||
"""Directory for auditwheel-repaired wheels under the Nuitka wheel build tree."""
|
||||
return NUITKA_WHEEL_DIR / "repaired"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import ast
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -41,17 +41,29 @@ def extras_require(repo_root: Path) -> dict[str, list[str]]:
|
||||
|
||||
def _resolve_version_attr(repo_root: Path, attr: str) -> str:
|
||||
"""Resolve a setuptools dynamic version attr (e.g. orisys.__version__)."""
|
||||
src_dir = str(repo_root / "src")
|
||||
if src_dir not in sys.path:
|
||||
sys.path.insert(0, src_dir)
|
||||
module_name, _, attr_name = attr.partition(".")
|
||||
if not attr_name:
|
||||
raise RuntimeError(f"Invalid dynamic version attr: {attr!r}")
|
||||
module = importlib.import_module(module_name)
|
||||
value = module
|
||||
for name in attr_name.split("."):
|
||||
value = getattr(value, name)
|
||||
return str(value)
|
||||
|
||||
# Avoid importing package modules here: __init__ may import heavy runtime deps
|
||||
# (e.g., cv2/numpy) that are not required just to read __version__.
|
||||
rel_module = Path(*module_name.split("."))
|
||||
module_file = repo_root / "src" / rel_module.with_suffix(".py")
|
||||
if not module_file.is_file():
|
||||
module_file = repo_root / "src" / rel_module / "__init__.py"
|
||||
if not module_file.is_file():
|
||||
raise RuntimeError(f"Cannot resolve module file for dynamic version attr: {attr!r}")
|
||||
|
||||
tree = ast.parse(module_file.read_text(encoding="utf-8"), filename=str(module_file))
|
||||
target_name = attr_name.split(".")[0]
|
||||
for node in tree.body:
|
||||
if isinstance(node, ast.Assign):
|
||||
for target in node.targets:
|
||||
if isinstance(target, ast.Name) and target.id == target_name:
|
||||
value = ast.literal_eval(node.value)
|
||||
return str(value)
|
||||
|
||||
raise RuntimeError(f"Attribute {target_name!r} not found in {module_file}")
|
||||
|
||||
|
||||
def get_version(repo_root: Path) -> str:
|
||||
|
||||
+30
-12
@@ -11,6 +11,32 @@ from pathlib import Path
|
||||
|
||||
from paths import GUI_DIST_DIR
|
||||
|
||||
# Public SDK surface that must import from a released wheel.
|
||||
_SMOKE_IMPORT_SCRIPT = """
|
||||
import orisys
|
||||
from orisys import Sensor, list_config, export_config, util, finger
|
||||
from orisys.finger import UVMeshViewer, build_uv_surface_mesh, FingerRuntime
|
||||
from orisys.finger.mesh import UVSurfaceMesh
|
||||
from orisys.finger.viewer import init_uv_mesh_viewer
|
||||
from orisys.hhd import nHHD
|
||||
from orisys.tracker import Tracker
|
||||
|
||||
assert orisys.__version__
|
||||
assert Sensor is not None
|
||||
assert callable(list_config)
|
||||
assert callable(export_config)
|
||||
assert util is not None
|
||||
assert finger is not None
|
||||
assert UVMeshViewer is not None
|
||||
assert build_uv_surface_mesh is not None
|
||||
assert FingerRuntime is not None
|
||||
assert UVSurfaceMesh is not None
|
||||
assert init_uv_mesh_viewer is not None
|
||||
assert nHHD is not None
|
||||
assert Tracker is not None
|
||||
print(f"orisys {orisys.__version__} public API imports OK")
|
||||
"""
|
||||
|
||||
|
||||
def _venv_python(venv_dir: Path) -> Path:
|
||||
if sys.platform == "win32":
|
||||
@@ -19,7 +45,7 @@ def _venv_python(venv_dir: Path) -> Path:
|
||||
|
||||
|
||||
def verify_built_wheel(wheel_path: Path) -> None:
|
||||
"""Install the wheel in a temporary venv and smoke-test finger imports."""
|
||||
"""Install the wheel in a temporary venv and smoke-test public imports."""
|
||||
wheel_path = wheel_path.resolve()
|
||||
with tempfile.TemporaryDirectory(prefix="orisys-wheel-verify-") as tmp:
|
||||
venv_dir = Path(tmp) / "venv"
|
||||
@@ -27,20 +53,12 @@ def verify_built_wheel(wheel_path: Path) -> None:
|
||||
python = _venv_python(venv_dir)
|
||||
|
||||
subprocess.check_call(
|
||||
[python, "-m", "pip", "install", "-q", str(wheel_path)],
|
||||
[python, "-m", "pip", "install", "-q", "--upgrade", "pip"],
|
||||
)
|
||||
subprocess.check_call(
|
||||
[
|
||||
python,
|
||||
"-c",
|
||||
(
|
||||
"from orisys.finger import UVMeshViewer, build_uv_surface_mesh, FingerRuntime; "
|
||||
"from orisys.finger.mesh import UVSurfaceMesh; "
|
||||
"from orisys.finger.viewer import init_uv_mesh_viewer; "
|
||||
"print('finger mesh/viewer imports OK')"
|
||||
),
|
||||
],
|
||||
[python, "-m", "pip", "install", "-q", str(wheel_path)],
|
||||
)
|
||||
subprocess.check_call([python, "-c", _SMOKE_IMPORT_SCRIPT])
|
||||
|
||||
|
||||
def verify_gui_dist(dist_dir: Path | None = None) -> None:
|
||||
|
||||
+74
-1
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -9,6 +10,11 @@ from pathlib import Path
|
||||
|
||||
from paths import sdk_dir
|
||||
|
||||
# PyPI rejects bare tags like "manylinux_x86_64"; require a glibc ABI version.
|
||||
_MANYLINUX_TAG_RE = re.compile(
|
||||
r"(?:manylinux2014|manylinux_2_(?:17|24|27|28|31|34|35))_x86_64"
|
||||
)
|
||||
|
||||
|
||||
def write_wheel_setup(
|
||||
wheel_dir: Path,
|
||||
@@ -52,11 +58,78 @@ def run_bdist_wheel(wheel_dir: Path) -> Path | None:
|
||||
return wheels[0] if wheels else None
|
||||
|
||||
|
||||
def _ensure_auditwheel() -> None:
|
||||
try:
|
||||
subprocess.check_call(
|
||||
[sys.executable, "-m", "auditwheel", "--version"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
print("Installing auditwheel...")
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "auditwheel", "-q"])
|
||||
|
||||
|
||||
def is_compliant_manylinux_wheel(wheel_path: Path) -> bool:
|
||||
"""True if the wheel filename uses a PyPI-accepted manylinux x86_64 tag."""
|
||||
return bool(_MANYLINUX_TAG_RE.search(wheel_path.name))
|
||||
|
||||
|
||||
def repair_linux_wheel(wheel_path: Path, repair_dir: Path) -> Path:
|
||||
"""Run auditwheel show/repair and return a PyPI-compliant manylinux wheel.
|
||||
|
||||
Raises RuntimeError if repair fails or the result still lacks a valid tag.
|
||||
On non-Linux platforms the input wheel is returned unchanged.
|
||||
"""
|
||||
if sys.platform != "linux":
|
||||
print("Skipping auditwheel repair (not running on Linux).")
|
||||
return wheel_path
|
||||
|
||||
_ensure_auditwheel()
|
||||
repair_dir = repair_dir.resolve()
|
||||
if repair_dir.exists():
|
||||
shutil.rmtree(repair_dir)
|
||||
repair_dir.mkdir(parents=True)
|
||||
|
||||
print("\n=== auditwheel show ===")
|
||||
subprocess.check_call([sys.executable, "-m", "auditwheel", "show", str(wheel_path)])
|
||||
|
||||
print("\n=== auditwheel repair ===")
|
||||
subprocess.check_call(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"auditwheel",
|
||||
"repair",
|
||||
str(wheel_path),
|
||||
"--wheel-dir",
|
||||
str(repair_dir),
|
||||
]
|
||||
)
|
||||
|
||||
repaired = sorted(repair_dir.glob("*.whl"))
|
||||
if not repaired:
|
||||
raise RuntimeError(f"auditwheel repair produced no wheels in {repair_dir}")
|
||||
|
||||
compliant = [path for path in repaired if is_compliant_manylinux_wheel(path)]
|
||||
if not compliant:
|
||||
names = ", ".join(path.name for path in repaired)
|
||||
raise RuntimeError(
|
||||
"auditwheel repair did not produce a PyPI-compliant manylinux tag. "
|
||||
f"Got: {names}. Rebuild inside quay.io/pypa/manylinux_2_28_x86_64 "
|
||||
"(or manylinux2014_x86_64) instead of a generic Linux host."
|
||||
)
|
||||
|
||||
final = compliant[0]
|
||||
print(f"Repaired wheel: {final.name}")
|
||||
return final
|
||||
|
||||
|
||||
def publish_wheel_to_sdk(wheel_path: Path, version: str) -> Path:
|
||||
sdk_version_dir = sdk_dir(version)
|
||||
sdk_version_dir.mkdir(parents=True, exist_ok=True)
|
||||
dest_wheel = sdk_version_dir / wheel_path.name
|
||||
if dest_wheel.exists():
|
||||
dest_wheel.unlink()
|
||||
shutil.move(str(wheel_path), str(dest_wheel))
|
||||
shutil.copy2(str(wheel_path), str(dest_wheel))
|
||||
return dest_wheel
|
||||
|
||||
@@ -2,24 +2,24 @@
|
||||
"name": "prototype",
|
||||
"version": "1.0",
|
||||
"camera": {
|
||||
"resolution_raw": [1280, 720],
|
||||
"fps": 15,
|
||||
"resolution_raw": [1920, 1080],
|
||||
"fps": 30,
|
||||
"units": 15,
|
||||
"units_used": 9,
|
||||
"resolution_stitch": [263, 263],
|
||||
"resolution_stitch": [365, 365],
|
||||
"stitch_res_list": [365,263,123,63]
|
||||
},
|
||||
"stitcher": {
|
||||
"min_area_frac": 0.01,
|
||||
"max_area_frac": 0.04,
|
||||
"max_aspect_ratio": 25,
|
||||
"max_area_frac": 0.05,
|
||||
"max_aspect_ratio": 50,
|
||||
"index": [1,2,3,6,7,8,11,12,13],
|
||||
"margin": 10,
|
||||
"nominal_ov_pos": 67,
|
||||
"nominal_ov_pos": 100,
|
||||
"n": [3,3],
|
||||
"row_tol": 0.6,
|
||||
"box_size": [180, 180],
|
||||
"blend":"numpy",
|
||||
"box_size": [250, 250],
|
||||
"blend":"numba",
|
||||
"blend_list": ["numpy", "numba","none"]
|
||||
},
|
||||
"tracker": {
|
||||
@@ -31,16 +31,15 @@
|
||||
"dis_preset": "medium",
|
||||
"dis_mode": 3,
|
||||
"algorithm_list": ["raft", "dis"],
|
||||
"hhd_downsample": 3,
|
||||
"backend": "cpu"
|
||||
"hhd_downsample": 3
|
||||
},
|
||||
"contact": {
|
||||
"min_contour_area": 3000,
|
||||
"flow_threshold":1,
|
||||
"div_threshold":0.02,
|
||||
"flow_threshold":5,
|
||||
"div_threshold":0.05,
|
||||
"expansion_threshold": 0.01,
|
||||
"kernel_size": [5,5],
|
||||
"denoise_sigma": 10
|
||||
"denoise_sigma": 12
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "dd02-ov",
|
||||
"type": "single_camera",
|
||||
"version": "1.0",
|
||||
"camera": {
|
||||
"resolution_raw": [1280, 720],
|
||||
"fps": 30,
|
||||
"resolution_stitch": [420, 450],
|
||||
"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": 5
|
||||
},
|
||||
"contact": {
|
||||
"min_contour_area": 3000,
|
||||
"flow_threshold": 0.2,
|
||||
"div_threshold": 0.022,
|
||||
"expansion_threshold": 0.01,
|
||||
"kernel_size": [5, 5],
|
||||
"denoise_sigma": 12
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "prototype",
|
||||
"version": "1.0",
|
||||
"camera": {
|
||||
"resolution_raw": [1280, 720],
|
||||
"fps": 30,
|
||||
"units": 15,
|
||||
"units_used": 9,
|
||||
"resolution_stitch": [263, 263],
|
||||
"stitch_res_list": [365,263,123,63]
|
||||
},
|
||||
"stitcher": {
|
||||
"min_area_frac": 0.01,
|
||||
"max_area_frac": 0.04,
|
||||
"max_aspect_ratio": 25,
|
||||
"index": [1,2,3,6,7,8,11,12,13],
|
||||
"margin": 10,
|
||||
"nominal_ov_pos": 67,
|
||||
"n": [3,3],
|
||||
"row_tol": 0.6,
|
||||
"box_size": [180, 180],
|
||||
"blend":"numpy",
|
||||
"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":1,
|
||||
"div_threshold":0.02,
|
||||
"expansion_threshold": 0.01,
|
||||
"kernel_size": [5,5],
|
||||
"denoise_sigma": 10
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
+10
-10
@@ -238,7 +238,7 @@ flow = sensor.read_info(sensor.info.VRAW)
|
||||
|
||||
## `orisys.finger` 手指 UV pipeline
|
||||
|
||||
`examples/finger_pipeline.py` 展示了一个更适合手指/单相机场景的高层 API:
|
||||
`examples/products/ory_finger/finger_pipeline.py` 展示了一个更适合手指/单相机场景的高层 API:
|
||||
|
||||
1. 使用 `FingerRuntime.open()` 打开视频源、加载 Blender 导出的资产,并构建 UV 展开映射。
|
||||
2. 用 `grab_and_process()` 或 `read_source() + process_frame()` 获取一帧展开后的触觉图像。
|
||||
@@ -262,7 +262,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`。
|
||||
* **config** (`str`,可选): Orisys 配置文件路径,默认 `./config/dd02-ov.json`。
|
||||
* **config** (`str`,可选): Orisys 配置文件名称或路径,默认 `dd02`(兼容解析到 `dd02-ov`)。
|
||||
* **map_cache** (`str`,可选): UV 展开缓存文件路径,默认值来自 `orisys.finger.DEFAULT_FINGER_MAP_CACHE`。
|
||||
* **source_scale** (`float`,可选): 输入相机分辨率缩放比例;未提供时根据配置自动推断。
|
||||
* **calib** (`str | None`,可选): 外部相机标定文件路径;未提供时优先使用导出资产中的内参。
|
||||
@@ -282,7 +282,7 @@ flow = sensor.read_info(sensor.info.VRAW)
|
||||
from orisys.finger import FingerRuntime
|
||||
|
||||
runtime = FingerRuntime.open(
|
||||
config="./config/dd02-ov.json",
|
||||
config="dd02",
|
||||
video=0,
|
||||
verbose=True,
|
||||
)
|
||||
@@ -312,7 +312,7 @@ runtime = FingerRuntime.open(
|
||||
|
||||
#### 描述
|
||||
|
||||
对一帧原始输入执行源图预处理、UV 展开,并把展开后的结果送入内部 `TactileProcessor`。这是 `examples/finger_pipeline.py` 中 profile 模式使用的方法。
|
||||
对一帧原始输入执行源图预处理、UV 展开,并把展开后的结果送入内部 `TactileProcessor`。这是 `examples/products/ory_finger/finger_pipeline.py` 中 profile 模式使用的方法。
|
||||
|
||||
#### 输入参数
|
||||
|
||||
@@ -327,7 +327,7 @@ runtime = FingerRuntime.open(
|
||||
|
||||
#### 描述
|
||||
|
||||
先读取一帧视频源,再调用 `process_frame()` 完成 UV 展开并推入内部触觉处理器。非 profile 模式下,`examples/finger_pipeline.py` 使用该方法作为主循环入口。
|
||||
先读取一帧视频源,再调用 `process_frame()` 完成 UV 展开并推入内部触觉处理器。非 profile 模式下,`examples/products/ory_finger/finger_pipeline.py` 使用该方法作为主循环入口。
|
||||
|
||||
#### 输入参数
|
||||
|
||||
@@ -357,7 +357,7 @@ runtime = FingerRuntime.open(
|
||||
|
||||
#### 描述
|
||||
|
||||
先执行一次采集与 UV 展开,然后调用 `runtime.tactile.warmup()` 预热内部光流/HHD 相关缓存。`examples/finger_pipeline.py` 在进入主循环前会先执行一次 warmup。
|
||||
先执行一次采集与 UV 展开,然后调用 `runtime.tactile.warmup()` 预热内部光流/HHD 相关缓存。`examples/products/ory_finger/finger_pipeline.py` 在进入主循环前会先执行一次 warmup。
|
||||
|
||||
#### 输入参数
|
||||
|
||||
@@ -386,7 +386,7 @@ runtime = FingerRuntime.open(
|
||||
* **raw_sensor**: 底层 `Sensor` 对象。
|
||||
* **info**: 与 `Sensor.info` 相同的枚举入口。
|
||||
* **img / flow / vnormal / vshear / depth_map**: 当前帧处理结果。
|
||||
* **mask_stats**: 有效 UV 区域统计信息;`examples/finger_pipeline.py` 会读取 `mask_stats["coverage"]` 输出当前掩码覆盖率。
|
||||
* **mask_stats**: 有效 UV 区域统计信息;`examples/products/ory_finger/finger_pipeline.py` 会读取 `mask_stats["coverage"]` 输出当前掩码覆盖率。
|
||||
* **valid_mask**: 当前有效区域掩码。
|
||||
|
||||
#### 说明
|
||||
@@ -395,7 +395,7 @@ runtime = FingerRuntime.open(
|
||||
|
||||
### 10. 最小手指 pipeline 示例
|
||||
|
||||
下面的流程与 `examples/finger_pipeline.py` 一致:
|
||||
下面的流程与 `examples/products/ory_finger/finger_pipeline.py` 一致:
|
||||
|
||||
```python
|
||||
import time
|
||||
@@ -404,7 +404,7 @@ import orisys
|
||||
from orisys.finger import FingerRuntime
|
||||
|
||||
runtime = FingerRuntime.open(
|
||||
config="./config/dd02-ov.json",
|
||||
config="dd02",
|
||||
video=0,
|
||||
verbose=True,
|
||||
)
|
||||
@@ -462,7 +462,7 @@ finally:
|
||||
|
||||
### 11. 关于 profile 模式
|
||||
|
||||
`examples/finger_pipeline.py` 中的 `--profile` / `--profile-every` 是示例脚本参数,不属于 `orisys` SDK 核心 API。本质上它只是把主循环拆成:
|
||||
`examples/products/ory_finger/finger_pipeline.py` 中的 `--profile` / `--profile-every` 是示例脚本参数,不属于 `orisys` SDK 核心 API。本质上它只是把主循环拆成:
|
||||
|
||||
1. `runtime.read_source()`
|
||||
2. `runtime.process_frame(...)`
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+17
-14
@@ -112,13 +112,16 @@ print(orisys.__version__)
|
||||
|
||||
# 使用方法
|
||||
## 运行示例程序
|
||||
套件里面包含了多个例子,建议都在仓库根目录运行(即 `config/`、`examples/`、`assets/` 同级目录)。常见入口分为两类:
|
||||
套件里面包含了多个例子,建议都在仓库根目录运行(即 `config/`、`examples/`、`assets/` 同级目录)。按产品入口如下:
|
||||
|
||||
1. `examples/basic_pipeline.py`:复眼/拼接传感器基础 OpenCV 示例
|
||||
2. `examples/finger_pipeline.py`:手指传感器2维显示最小流程示例
|
||||
3. `examples/finger_pipeline_3dview.py`:手指传感器 Qt 3D 可视化界面
|
||||
1. `examples/products/ory_box/basic_pipeline.py`:ORY-BOX 复眼/拼接传感器基础 OpenCV 示例(默认配置 `ddjx01`)
|
||||
2. `examples/products/ory_mini/basic_pipeline.py`:ORY-MINI 基础 OpenCV 示例(默认配置 `mini`)
|
||||
3. `examples/products/ory_finger/finger_pipeline.py`:ORY-FINGER 2D UV 最小流程(默认配置 `dd02`)
|
||||
4. `examples/products/ory_finger/finger_pipeline_3dview.py`:ORY-FINGER Qt 3D 可视化界面(默认配置 `dd02`)
|
||||
|
||||
下面先介绍最基础的 `basic_pipeline.py` 运行方式。
|
||||
`dd02` 是当前 ORY-FINGER 的稳定别名,实际会解析到 `dd02-ov`。旧路径 `examples/basic_pipeline.py` / `examples/finger_pipeline*.py` 仍可作为兼容入口。
|
||||
|
||||
下面先介绍最基础的 ORY-BOX 运行方式。
|
||||
|
||||
### Windows:
|
||||
1. 检查是否有其他正在连接的USB摄像头设备:打开powershell, 运行命令`Get-PnpDevice -PresentOnly | Where-Object { $_.Class -match '^Camera' }`,如果没有的话不会有输出。
|
||||
@@ -126,7 +129,7 @@ print(orisys.__version__)
|
||||
3. 如果第一步没有输出,传感器的ID就是“0”,如果第一步有一个设备,传感器的ID就是“1”,如此类推。
|
||||
4. 在确定了传感器的ID(下方例子假设为“0”)后,打开Anaconda Prompt, 切换到工作路径,然后在命令行中输入:
|
||||
``` bash
|
||||
python examples/basic_pipeline.py -v 0
|
||||
python examples/products/ory_box/basic_pipeline.py -v 0 -c ddjx01
|
||||
```
|
||||
5. 程序会运行,并自动打开可视化窗口和打印资料,在窗口上按键盘”q”键可以停止运行,按”r”键可以重设数据
|
||||
|
||||
@@ -136,22 +139,22 @@ print(orisys.__version__)
|
||||
3. 根据新增的输出,例如”/dev/video0”,确定传感器的ID为“0”, 如果是”/dev/video3”的话ID为“3” ,如此类推。
|
||||
4. 在确定了传感器的ID(下方例子假设为“0”)后,打开终端, 激活相应的python环境,切换到工作路径,然后在命令行中输入:
|
||||
``` bash
|
||||
python examples/basic_pipeline.py -v 0
|
||||
python examples/products/ory_box/basic_pipeline.py -v 0 -c ddjx01
|
||||
```
|
||||
5. 程序会运行,并自动打开可视化窗口和打印资料,在窗口上按键盘”q”键可以停止运行,按”r”键可以重设数据
|
||||
|
||||
### 手指传感器 UV 示例
|
||||
### ORY-FINGER UV 示例
|
||||
|
||||
如果您使用的是手指/单相机 UV 展开流程,可运行:
|
||||
如果您使用的是 ORY-FINGER(当前配置别名 `dd02`),可运行:
|
||||
|
||||
``` bash
|
||||
python examples/finger_pipeline.py --camera_id 0 --config ./config/dd02-ov.json
|
||||
python examples/products/ory_finger/finger_pipeline.py --camera_id 0 --config dd02
|
||||
```
|
||||
|
||||
如果输入来自视频文件,也可以改用:
|
||||
|
||||
``` bash
|
||||
python examples/finger_pipeline.py --video ./data/sample.mp4 --config ./config/dd02-ov.json
|
||||
python examples/products/ory_finger/finger_pipeline.py --video ./data/sample.mp4 --config dd02
|
||||
```
|
||||
|
||||
该示例会:
|
||||
@@ -168,10 +171,10 @@ python examples/finger_pipeline.py --video ./data/sample.mp4 --config ./config/d
|
||||
例如:
|
||||
|
||||
``` bash
|
||||
python examples/finger_pipeline.py --camera_id 0 --config ./config/dd02-ov.json
|
||||
python examples/products/ory_finger/finger_pipeline.py --camera_id 0 --config dd02
|
||||
```
|
||||
|
||||
### 手指 3D 可视化界面
|
||||
### ORY-FINGER 3D 可视化界面
|
||||
|
||||
如果要使用 GUI ,安装GUI依赖:
|
||||
``` bash
|
||||
@@ -180,7 +183,7 @@ pip install pyqt5 pyqtgraph
|
||||
```
|
||||
然后运行:
|
||||
``` bash
|
||||
python examples/finger_pipeline_3dview.py --video 0 --config ./config/dd02-ov.json
|
||||
python examples/products/ory_finger/finger_pipeline_3dview.py --video 0 --config dd02
|
||||
```
|
||||
|
||||
该界面会提供:
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,167 @@
|
||||
# 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.
|
||||
+52
-38
@@ -1,54 +1,66 @@
|
||||
# Examples
|
||||
|
||||
Top-level demo scripts under `examples/`. Run them from the repository root.
|
||||
Product-facing demos live under `examples/products/`. Shared helpers stay in
|
||||
`pipeline/`, `viz/`, and `gui/`. Run commands from the repository root.
|
||||
|
||||
## Top-Level Files
|
||||
## Product Entry Points
|
||||
|
||||
- `README.md` — this overview of the root-level example entry points
|
||||
- `basic_pipeline.py` — basic single-sensor OpenCV demo for the compound-eye pipeline
|
||||
- `plot_data.py` — single-sensor demo with OpenCV views plus PyQtGraph force charts
|
||||
- `two_sensor_pipeline.py` — two-sensor multiprocessing demo for parallel acquisition / display
|
||||
- `finger_pipeline.py` — minimal finger UV-unroll pipeline with optional profiling
|
||||
- `finger_pipeline_3dview.py` — main finger dashboard entry point with the unified Qt UI
|
||||
| Product | Entry | Default config |
|
||||
|---------|-------|----------------|
|
||||
| ORY-BOX | `products/ory_box/basic_pipeline.py` | `ddjx01` |
|
||||
| ORY-MINI | `products/ory_mini/basic_pipeline.py` | `mini` |
|
||||
| ORY-FINGER (2D) | `products/ory_finger/finger_pipeline.py` | `dd02` |
|
||||
| ORY-FINGER (3D Qt) | `products/ory_finger/finger_pipeline_3dview.py` | `dd02` |
|
||||
|
||||
`dd02` is the stable ORY-FINGER alias and currently resolves to `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
|
||||
|
||||
For the Qt dashboard:
|
||||
ORY-BOX:
|
||||
|
||||
```bash
|
||||
python examples/products/ory_box/basic_pipeline.py -v 0 -c ddjx01
|
||||
```
|
||||
|
||||
ORY-MINI:
|
||||
|
||||
```bash
|
||||
python examples/products/ory_mini/basic_pipeline.py -v 0 -c mini
|
||||
```
|
||||
|
||||
ORY-FINGER 2D:
|
||||
|
||||
```bash
|
||||
python examples/products/ory_finger/finger_pipeline.py --camera_id 0 --config dd02
|
||||
```
|
||||
|
||||
ORY-FINGER video file:
|
||||
|
||||
```bash
|
||||
python examples/products/ory_finger/finger_pipeline.py --video ./data/sample.mp4 --config dd02
|
||||
```
|
||||
|
||||
ORY-FINGER 3D Qt dashboard:
|
||||
|
||||
```bash
|
||||
pip install -e ".[gui]"
|
||||
python examples/finger_pipeline_3dview.py --video 0 --config ./config/dd02-ov.json
|
||||
python examples/products/ory_finger/finger_pipeline_3dview.py --video 0 --config dd02
|
||||
```
|
||||
|
||||
For the minimal finger pipeline:
|
||||
## Other Root Examples
|
||||
|
||||
```bash
|
||||
python examples/finger_pipeline.py --camera_id 0 --config ./config/dd02-ov.json
|
||||
```
|
||||
|
||||
For the minimal finger pipeline with profiling:
|
||||
|
||||
```bash
|
||||
python examples/finger_pipeline.py --camera_id 0 --config ./config/dd02-ov.json --profile --profile-every 30
|
||||
```
|
||||
|
||||
For a video-file finger run:
|
||||
|
||||
```bash
|
||||
python examples/finger_pipeline.py --video ./data/sample.mp4 --config ./config/dd02-ov.json
|
||||
```
|
||||
|
||||
For the basic compound-eye pipeline:
|
||||
|
||||
```bash
|
||||
python examples/basic_pipeline.py -v 0 -c ./config/ddjx01.json
|
||||
```
|
||||
- `plot_data.py` — single-sensor OpenCV views plus PyQtGraph force charts
|
||||
- `two_sensor_pipeline.py` — two-sensor multiprocessing demo
|
||||
|
||||
## Requirements
|
||||
|
||||
- SDK installed from the repository root, for example `pip install -e .`
|
||||
- camera index or test video path
|
||||
- matching config files under `./config/`
|
||||
- matching config names under `./config/` (or packaged `orisys/configs/`)
|
||||
|
||||
Qt / PyQtGraph examples additionally require:
|
||||
|
||||
@@ -58,8 +70,10 @@ pip install -e ".[gui]"
|
||||
|
||||
## Notes
|
||||
|
||||
- `finger_pipeline_3dview.py` is the main interactive demo. It uses `gui/app_paths.py` so the same entry point works in editable and packaged runs.
|
||||
- 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, `S` starts video recording, `E` stops video recording, and `Esc` toggles fullscreen.
|
||||
- `finger_pipeline.py` is the leanest finger example if you want to validate UV unroll + force/contact processing without the full dashboard.
|
||||
- `finger_pipeline.py` uses `orisys.finger.FingerRuntime` as the high-level entry point, then runs tactile analysis through `runtime.tactile`.
|
||||
- In `--profile` mode, the example splits the loop into capture, UV unroll, deformation, contact, and display stages so you can measure each step separately.
|
||||
- `products/ory_finger/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.
|
||||
- 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,
|
||||
`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
|
||||
validate UV unroll + force/contact processing without the full dashboard.
|
||||
|
||||
+13
-107
@@ -1,115 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compatibility wrapper for ORY-BOX.
|
||||
|
||||
Prefer:
|
||||
python examples/products/ory_box/basic_pipeline.py -v 0 -c ddjx01
|
||||
"""
|
||||
示例 1:单传感器基础流程
|
||||
|
||||
本示例演示 Orisys SDK 的标准处理流程:
|
||||
1. 初始化传感器(摄像头或视频文件)
|
||||
2. 获取图像并计算形变
|
||||
3. 按需计算接触区域
|
||||
4. 读取并可视化结果
|
||||
from __future__ import annotations
|
||||
|
||||
SDK version: 0.3.1
|
||||
"""
|
||||
import os
|
||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||
import cv2
|
||||
import orisys
|
||||
import numpy as np
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
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("--verbose","-verbose", type=bool, default=True, help="是否输出详细日志")
|
||||
args = parser.parse_args()
|
||||
_EXAMPLES_DIR = Path(__file__).resolve().parent
|
||||
if str(_EXAMPLES_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(_EXAMPLES_DIR))
|
||||
|
||||
# 步骤 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("形变矢量场", arrows)
|
||||
|
||||
# 步骤 3.2:显示接触区域和质心
|
||||
image_with_foe = orisys.util.draw_contact(img, contour, centroid)
|
||||
cv2.imshow("接触区域与质心", 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("深度图", 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() # 释放视频源及相关资源
|
||||
from products.ory_box.basic_pipeline import main
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
+10
-98
@@ -1,108 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Minimum UV-unroll finger tactile pipeline.
|
||||
"""Compatibility wrapper for ORY-FINGER 2D pipeline.
|
||||
|
||||
Offline: load Blender export -> build_or_load_domain -> unroll_map_uv.npz
|
||||
Runtime: camera frame -> FrameUnwarper -> TactileProcessor -> forces/contact
|
||||
|
||||
For the full demo (UV debug panels, dashboard, benchmarks) see:
|
||||
tools/3d_patch_generation_blender/unroll_realtime.py
|
||||
Prefer:
|
||||
python examples/products/ory_finger/finger_pipeline.py --camera_id 0 --config dd02
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
|
||||
_EXAMPLES_DIR = Path(__file__).resolve().parent
|
||||
if str(_EXAMPLES_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(_EXAMPLES_DIR))
|
||||
|
||||
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="./config/dd02-ov.json", help="Orisys SDK config")
|
||||
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()
|
||||
from products.ory_finger.finger_pipeline import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,75 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
UV-unroll finger tactile pipeline with unified Qt dashboard.
|
||||
"""Compatibility wrapper for ORY-FINGER 3D Qt dashboard.
|
||||
|
||||
Where to customize the GUI:
|
||||
Layout (sizes, proportions) -> examples/gui/layout.py <-- edit here
|
||||
Colors / QSS -> examples/gui/styles.py
|
||||
Widget rendering logic -> examples/gui/widgets/
|
||||
Window behavior -> examples/gui/dashboard.py
|
||||
Prefer:
|
||||
python examples/products/ory_finger/finger_pipeline_3dview.py --video 0 --config dd02
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
|
||||
_EXAMPLES_DIR = Path(__file__).resolve().parent
|
||||
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))
|
||||
from products.ory_finger.finger_pipeline_3dview import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -27,7 +27,8 @@ def resource_path(*parts: str) -> Path:
|
||||
|
||||
|
||||
def default_config_path() -> str:
|
||||
return str(resource_path("config", "dd02-ov.json"))
|
||||
"""Default ORY-FINGER config name (resolves to dd02-ov via alias)."""
|
||||
return "dd02"
|
||||
|
||||
|
||||
def configure_frozen_runtime() -> None:
|
||||
|
||||
@@ -137,7 +137,7 @@ class SettingsPanelWidget(QWidget):
|
||||
root.addWidget(self._caption("Config"))
|
||||
config_row = QHBoxLayout()
|
||||
config_row.setSpacing(6)
|
||||
self.config_edit = QLineEdit("./config/dd02-ov.json")
|
||||
self.config_edit = QLineEdit("dd02")
|
||||
config_row.addWidget(self.config_edit, 1)
|
||||
self.btn_browse_config = QPushButton("…")
|
||||
self.btn_browse_config.setFixedWidth(32)
|
||||
|
||||
@@ -17,7 +17,7 @@ def mesh_threshold(mesh_map: str, threshold: float | None) -> float:
|
||||
@dataclass
|
||||
class PipelineConfig:
|
||||
video: str = "0"
|
||||
config: str = "./config/dd02-ov.json"
|
||||
config: str = "dd02"
|
||||
calib: str | None = None
|
||||
export_dir: str = DEFAULT_FINGER_EXPORT_DIR
|
||||
csv_save_dir: str = "./outputs/csv"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Product-facing example entry points for ORY-BOX / ORY-MINI / ORY-FINGER."""
|
||||
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
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()
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/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()
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/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()
|
||||
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
ORY-MINI:单传感器基础流程
|
||||
|
||||
本示例演示 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="mini", 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("arrows", 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()
|
||||
@@ -18,7 +18,7 @@
|
||||
- Windows 下必须在 if __name__ == '__main__' 中调用 mp.freeze_support()
|
||||
- 每个传感器进程都会占用独立的 GPU 显存
|
||||
|
||||
SDK version: 0.3.1
|
||||
SDK version: 0.4.2
|
||||
"""
|
||||
import os
|
||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||
|
||||
+33
-6
@@ -4,25 +4,52 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "orisys"
|
||||
description = "Orisys - Vision Based Tactile Sensor SDK"
|
||||
description = "SDK for Orisys Technology Visuotactile Sensors"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
authors = [{ name = "Orisys" }]
|
||||
license = { text = "Proprietary" }
|
||||
authors = [{ name = "Orisys-Tech" }]
|
||||
maintainers = [
|
||||
{name = "Yijian Zhang", email = "zhangyijian@orisys-tech.cn"}
|
||||
]
|
||||
dynamic = ["version"]
|
||||
dependencies = [
|
||||
"numpy>=1.24.4",
|
||||
"opencv-python>=4.10.0.84",
|
||||
"scipy>=1.10.1",
|
||||
"open3d>=0.17.0",
|
||||
"numba>=0.59.0",
|
||||
]
|
||||
classifiers = [
|
||||
# How mature is this project? Common values are
|
||||
# 3 - Alpha
|
||||
# 4 - Beta
|
||||
# 5 - Production/Stable
|
||||
"Development Status :: 4 - Beta",
|
||||
|
||||
# Indicate who your project is intended for
|
||||
"Intended Audience :: Developers",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
"License :: Other/Proprietary License",
|
||||
"Operating System :: Microsoft :: Windows",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
|
||||
# Specify the Python versions you support here.
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://www.orisys-tech.cn"
|
||||
Documentation = "https://github.com/Orisys-Tech/orisys-sdk"
|
||||
Issues = "https://github.com/Orisys-Tech/orisys-sdk/issues"
|
||||
|
||||
[project.optional-dependencies]
|
||||
cuda12 = ["cupy-cuda12x>=12.3.0"]
|
||||
cuda13 = ["cupy-cuda13x>=13.0.0"]
|
||||
gui = ["open3d>=0.17.0","pyqt5>=5.15", "pyqtgraph>=0.13"]
|
||||
# Backward compatibility: 'cuda' defaults to CUDA 12.x
|
||||
cuda = ["cupy-cuda12x>=12.3.0"]
|
||||
gui = ["pyqt5>=5.15", "pyqtgraph>=0.13"]
|
||||
numba = ["numba>=0.59.0"]
|
||||
|
||||
[tool.setuptools]
|
||||
package-dir = { "" = "src" }
|
||||
@@ -52,7 +79,7 @@ dev_install = true
|
||||
name = "examples"
|
||||
source = "examples"
|
||||
target = "examples"
|
||||
patterns = ["*.py", "README.md", "viz/**/*.py", "pipeline/**/*.py", "gui/**/*.py"]
|
||||
patterns = ["*.py", "README.md", "products/**/*.py", "viz/**/*.py", "pipeline/**/*.py", "gui/**/*.py"]
|
||||
preserve_tree = true
|
||||
|
||||
[[tool.orisys.bundle.items]]
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# CuPy - Install the appropriate version based on your CUDA version:
|
||||
# For CUDA 12.x: pip install cupy-cuda12x>=12.3.0
|
||||
# For CUDA 13.x: pip install cupy-cuda13x>=13.0.0
|
||||
# Or use extras: pip install orisys[cuda12] or pip install orisys[cuda13]
|
||||
|
||||
numpy>=1.24.4
|
||||
opencv_python>=4.10.0.84
|
||||
scipy==1.13
|
||||
setuptools>=45.2.0
|
||||
av
|
||||
numba
|
||||
pyqt5
|
||||
pyqtgraph
|
||||
nuitka
|
||||
@@ -22,7 +22,7 @@ def main():
|
||||
|
||||
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="./config/dd02-ov.json")
|
||||
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)
|
||||
|
||||
+18
-5
@@ -1,6 +1,12 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
# Product-facing names that map to on-disk config stems.
|
||||
# ORY-FINGER currently ships as dd02-ov.json; keep "dd02" as the stable alias.
|
||||
CONFIG_ALIASES = {
|
||||
"dd02": "dd02-ov",
|
||||
}
|
||||
|
||||
|
||||
def _candidate_config_dirs() -> list[Path]:
|
||||
"""
|
||||
@@ -62,9 +68,16 @@ def load_config(config_name_or_path):
|
||||
if config_path.is_absolute():
|
||||
raise FileNotFoundError(f"Config file not found at absolute path: {config_path}")
|
||||
|
||||
direct = CONFIG_DIR / f"{config_name_or_path}.json"
|
||||
if direct.exists():
|
||||
return _load_json(direct)
|
||||
requested = str(config_name_or_path)
|
||||
candidates = [requested]
|
||||
alias = CONFIG_ALIASES.get(requested)
|
||||
if alias and alias not in candidates:
|
||||
candidates.append(alias)
|
||||
|
||||
for name in candidates:
|
||||
direct = CONFIG_DIR / f"{name}.json"
|
||||
if direct.exists():
|
||||
return _load_json(direct)
|
||||
|
||||
# Backward-compatible alias resolution: support old names via JSON "name" field.
|
||||
for candidate in _config_files():
|
||||
@@ -72,7 +85,7 @@ def load_config(config_name_or_path):
|
||||
data = _load_json(candidate)
|
||||
except Exception:
|
||||
continue
|
||||
if data.get("name") == config_name_or_path:
|
||||
if data.get("name") in candidates:
|
||||
return data
|
||||
|
||||
available = [f.stem for f in _config_files()]
|
||||
@@ -80,7 +93,7 @@ def load_config(config_name_or_path):
|
||||
raise FileNotFoundError(
|
||||
f"Input config file not found with name or path: {config_name_or_path}\n"
|
||||
f" Tried as file path: {config_path} (not found)\n"
|
||||
f" Tried as config file: {direct} (not found)\n"
|
||||
f" Tried as config files: {', '.join(candidates)} (not found)\n"
|
||||
f" Available config files: {available_str}"
|
||||
)
|
||||
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ Flow:
|
||||
4. Feed the rectified frame into Sensor.get_img(..., already_processed=True)
|
||||
5. Compute deformation/contact and visualize
|
||||
|
||||
SDK version: 0.3.1
|
||||
SDK version: 0.4.2
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def wheel_build():
|
||||
import sys
|
||||
|
||||
build_dir = REPO_ROOT / "build"
|
||||
if str(build_dir) not in sys.path:
|
||||
sys.path.insert(0, str(build_dir))
|
||||
import wheel_build
|
||||
|
||||
return wheel_build
|
||||
|
||||
|
||||
def test_compliant_manylinux_tags(wheel_build):
|
||||
assert wheel_build.is_compliant_manylinux_wheel(
|
||||
Path("orisys-0.4.2-cp310-cp310-manylinux_2_17_x86_64.whl")
|
||||
)
|
||||
assert wheel_build.is_compliant_manylinux_wheel(
|
||||
Path("orisys-0.4.2-cp310-cp310-manylinux2014_x86_64.whl")
|
||||
)
|
||||
assert wheel_build.is_compliant_manylinux_wheel(
|
||||
Path("orisys-0.4.2-cp310-cp310-manylinux_2_28_x86_64.whl")
|
||||
)
|
||||
|
||||
|
||||
def test_rejects_illegal_manylinux_tags(wheel_build):
|
||||
assert not wheel_build.is_compliant_manylinux_wheel(
|
||||
Path("orisys-0.4.2-cp310-cp310-manylinux_x86_64.whl")
|
||||
)
|
||||
assert not wheel_build.is_compliant_manylinux_wheel(
|
||||
Path("orisys-0.4.2-cp310-cp310-linux_x86_64.whl")
|
||||
)
|
||||
assert not wheel_build.is_compliant_manylinux_wheel(
|
||||
Path("orisys-0.4.2-cp310-cp310-win_amd64.whl")
|
||||
)
|
||||
|
||||
|
||||
def test_pyproject_no_private_classifier():
|
||||
import sys
|
||||
|
||||
build_dir = REPO_ROOT / "build"
|
||||
if str(build_dir) not in sys.path:
|
||||
sys.path.insert(0, str(build_dir))
|
||||
from pyproject_utils import project_metadata
|
||||
|
||||
classifiers = project_metadata(REPO_ROOT).get("classifiers", [])
|
||||
assert "Private :: Do Not Upload" not in classifiers
|
||||
assert "License :: Other/Proprietary License" in classifiers
|
||||
Reference in New Issue
Block a user