added ruff code reivew
This commit is contained in:
@@ -20,7 +20,10 @@ from common.verify import ( # noqa: E402
|
|||||||
verify_wheel_assets,
|
verify_wheel_assets,
|
||||||
verify_wheel_runtime,
|
verify_wheel_runtime,
|
||||||
)
|
)
|
||||||
from common.verify.wheel import DEFAULT_RUNTIME_CONFIG, default_verification_video # noqa: E402
|
from common.verify.wheel import ( # noqa: E402
|
||||||
|
DEFAULT_RUNTIME_CONFIG,
|
||||||
|
default_verification_video,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main(argv: list[str] | None = None) -> int:
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
|||||||
@@ -31,11 +31,16 @@ _BUILD_ROOT = Path(__file__).resolve().parents[1]
|
|||||||
if str(_BUILD_ROOT) not in sys.path:
|
if str(_BUILD_ROOT) not in sys.path:
|
||||||
sys.path.insert(0, str(_BUILD_ROOT))
|
sys.path.insert(0, str(_BUILD_ROOT))
|
||||||
|
|
||||||
from common.paths import REPO_ROOT # noqa: E402
|
|
||||||
from common.pyproject_utils import bundle_items, extras_require, install_requires, project_metadata # noqa: E402
|
|
||||||
from wheel.layout import resolve_bundle_source_files # noqa: E402
|
from wheel.layout import resolve_bundle_source_files # noqa: E402
|
||||||
from wheel.package import is_compliant_manylinux_wheel # noqa: E402
|
from wheel.package import is_compliant_manylinux_wheel # noqa: E402
|
||||||
|
|
||||||
|
from common.paths import REPO_ROOT # noqa: E402
|
||||||
|
from common.pyproject_utils import ( # noqa: E402
|
||||||
|
bundle_items,
|
||||||
|
extras_require,
|
||||||
|
install_requires,
|
||||||
|
)
|
||||||
|
|
||||||
# PyPI's default per-project file limit is 100 MB; keep headroom.
|
# PyPI's default per-project file limit is 100 MB; keep headroom.
|
||||||
MAX_WHEEL_BYTES = 90 * 1024 * 1024
|
MAX_WHEEL_BYTES = 90 * 1024 * 1024
|
||||||
|
|
||||||
@@ -339,11 +344,7 @@ def _check_forbidden_entries(result: WheelCheckResult, names: list[str]) -> None
|
|||||||
offenders = []
|
offenders = []
|
||||||
for name in names:
|
for name in names:
|
||||||
lowered = name.lower()
|
lowered = name.lower()
|
||||||
if lowered.startswith(_FORBIDDEN_PREFIXES):
|
if lowered.startswith(_FORBIDDEN_PREFIXES) or lowered.endswith(_FORBIDDEN_SUFFIXES) or "__pycache__" in lowered or ".egg-info" in lowered:
|
||||||
offenders.append(name)
|
|
||||||
elif lowered.endswith(_FORBIDDEN_SUFFIXES):
|
|
||||||
offenders.append(name)
|
|
||||||
elif "__pycache__" in lowered or ".egg-info" in lowered:
|
|
||||||
offenders.append(name)
|
offenders.append(name)
|
||||||
result.add(
|
result.add(
|
||||||
"no-forbidden-entries",
|
"no-forbidden-entries",
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
_BUILD_ROOT = Path(__file__).resolve().parents[2]
|
_BUILD_ROOT = Path(__file__).resolve().parents[2]
|
||||||
if str(_BUILD_ROOT) not in sys.path:
|
if str(_BUILD_ROOT) not in sys.path:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ if str(_BUILD_ROOT) not in sys.path:
|
|||||||
sys.path.insert(0, str(_BUILD_ROOT))
|
sys.path.insert(0, str(_BUILD_ROOT))
|
||||||
|
|
||||||
from common.paths import REPO_ROOT
|
from common.paths import REPO_ROOT
|
||||||
|
|
||||||
from gui.common.pyinstaller_driver import GuiBuildConfig
|
from gui.common.pyinstaller_driver import GuiBuildConfig
|
||||||
|
|
||||||
ENTRY_SCRIPT = REPO_ROOT / "examples" / "basic_pipeline.py"
|
ENTRY_SCRIPT = REPO_ROOT / "examples" / "basic_pipeline.py"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ if str(_BUILD_ROOT) not in sys.path:
|
|||||||
sys.path.insert(0, str(_BUILD_ROOT))
|
sys.path.insert(0, str(_BUILD_ROOT))
|
||||||
|
|
||||||
from common.verify.gui import verify_gui_dist
|
from common.verify.gui import verify_gui_dist
|
||||||
|
|
||||||
from gui.common.pyinstaller_driver import build_with_pyinstaller
|
from gui.common.pyinstaller_driver import build_with_pyinstaller
|
||||||
from gui.ory_finger.config import make_config
|
from gui.ory_finger.config import make_config
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ if str(_BUILD_ROOT) not in sys.path:
|
|||||||
sys.path.insert(0, str(_BUILD_ROOT))
|
sys.path.insert(0, str(_BUILD_ROOT))
|
||||||
|
|
||||||
from common.paths import REPO_ROOT
|
from common.paths import REPO_ROOT
|
||||||
|
|
||||||
from gui.common.pyinstaller_driver import GuiBuildConfig
|
from gui.common.pyinstaller_driver import GuiBuildConfig
|
||||||
|
|
||||||
ENTRY_SCRIPT = REPO_ROOT / "examples" / "finger_pipeline_3dview.py"
|
ENTRY_SCRIPT = REPO_ROOT / "examples" / "finger_pipeline_3dview.py"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ if str(_BUILD_ROOT) not in sys.path:
|
|||||||
sys.path.insert(0, str(_BUILD_ROOT))
|
sys.path.insert(0, str(_BUILD_ROOT))
|
||||||
|
|
||||||
from common.paths import REPO_ROOT
|
from common.paths import REPO_ROOT
|
||||||
|
|
||||||
from gui.common.pyinstaller_driver import GuiBuildConfig
|
from gui.common.pyinstaller_driver import GuiBuildConfig
|
||||||
|
|
||||||
ENTRY_SCRIPT = REPO_ROOT / "examples" / "mini_pipeline.py"
|
ENTRY_SCRIPT = REPO_ROOT / "examples" / "mini_pipeline.py"
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from common.verify.wheel import ( # noqa: E402
|
|||||||
verify_wheel_runtime,
|
verify_wheel_runtime,
|
||||||
)
|
)
|
||||||
from common.verify.wheel_contents import inspect_wheel # noqa: E402
|
from common.verify.wheel_contents import inspect_wheel # noqa: E402
|
||||||
|
|
||||||
from release import auth, github, pypi # noqa: E402
|
from release import auth, github, pypi # noqa: E402
|
||||||
from release.preflight import PreflightOptions, run_preflight # noqa: E402
|
from release.preflight import PreflightOptions, run_preflight # noqa: E402
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ if str(_BUILD_ROOT) not in sys.path:
|
|||||||
sys.path.insert(0, str(_BUILD_ROOT))
|
sys.path.insert(0, str(_BUILD_ROOT))
|
||||||
|
|
||||||
from common.paths import REPO_ROOT # noqa: E402
|
from common.paths import REPO_ROOT # noqa: E402
|
||||||
|
|
||||||
from release.publish import GateResult, PublishGateError, load_gate # noqa: E402
|
from release.publish import GateResult, PublishGateError, load_gate # noqa: E402
|
||||||
|
|
||||||
MANIFEST_PATH = REPO_ROOT / "tools" / "downstream_sync_manifest.json"
|
MANIFEST_PATH = REPO_ROOT / "tools" / "downstream_sync_manifest.json"
|
||||||
|
|||||||
@@ -45,9 +45,13 @@ from common.verify.wheel import ( # noqa: E402
|
|||||||
verify_wheel,
|
verify_wheel,
|
||||||
)
|
)
|
||||||
from common.verify.wheel_contents import inspect_wheel # noqa: E402
|
from common.verify.wheel_contents import inspect_wheel # noqa: E402
|
||||||
|
from wheel.layout import ( # noqa: E402
|
||||||
|
resolve_bundle_source_files,
|
||||||
|
verify_required_assets,
|
||||||
|
)
|
||||||
|
|
||||||
from release import report as report_mod # noqa: E402
|
from release import report as report_mod # noqa: E402
|
||||||
from release import version as version_mod # noqa: E402
|
from release import version as version_mod # noqa: E402
|
||||||
from wheel.layout import resolve_bundle_source_files, verify_required_assets # noqa: E402
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ probe, and ``--apply`` being required even to reach TestPyPI.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import shutil
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -33,6 +32,7 @@ if str(_BUILD_ROOT) not in sys.path:
|
|||||||
sys.path.insert(0, str(_BUILD_ROOT))
|
sys.path.insert(0, str(_BUILD_ROOT))
|
||||||
|
|
||||||
from common.paths import REPO_ROOT # noqa: E402
|
from common.paths import REPO_ROOT # noqa: E402
|
||||||
|
|
||||||
from release import auth # noqa: E402
|
from release import auth # noqa: E402
|
||||||
from release.publish import GateResult, PublishGateError, load_gate # noqa: E402
|
from release.publish import GateResult, PublishGateError, load_gate # noqa: E402
|
||||||
|
|
||||||
@@ -452,7 +452,7 @@ def publish(options: Options) -> int:
|
|||||||
|
|
||||||
print()
|
print()
|
||||||
if options.pypi_only:
|
if options.pypi_only:
|
||||||
print(f"=== release pypi PASSED (PyPI only, no TestPyPI rehearsal) ===")
|
print("=== release pypi PASSED (PyPI only, no TestPyPI rehearsal) ===")
|
||||||
else:
|
else:
|
||||||
print("=== release pypi PASSED (TestPyPI + PyPI) ===")
|
print("=== release pypi PASSED (TestPyPI + PyPI) ===")
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -29,7 +29,13 @@ _BUILD_ROOT = Path(__file__).resolve().parents[1]
|
|||||||
if str(_BUILD_ROOT) not in sys.path:
|
if str(_BUILD_ROOT) not in sys.path:
|
||||||
sys.path.insert(0, str(_BUILD_ROOT))
|
sys.path.insert(0, str(_BUILD_ROOT))
|
||||||
|
|
||||||
from common.paths import NUITKA_COMPILE_DIR, NUITKA_WHEEL_DIR, REPO_ROOT, repaired_wheel_dir, repo_path
|
from common.paths import (
|
||||||
|
NUITKA_COMPILE_DIR,
|
||||||
|
NUITKA_WHEEL_DIR,
|
||||||
|
REPO_ROOT,
|
||||||
|
repaired_wheel_dir,
|
||||||
|
repo_path,
|
||||||
|
)
|
||||||
from common.pyproject_utils import (
|
from common.pyproject_utils import (
|
||||||
bundle_items,
|
bundle_items,
|
||||||
extras_require,
|
extras_require,
|
||||||
@@ -38,6 +44,7 @@ from common.pyproject_utils import (
|
|||||||
project_metadata,
|
project_metadata,
|
||||||
)
|
)
|
||||||
from common.verify.wheel import verify_built_wheel
|
from common.verify.wheel import verify_built_wheel
|
||||||
|
|
||||||
from wheel.compile import compile_orisys, copy_compiled_orisys, install_nuitka
|
from wheel.compile import compile_orisys, copy_compiled_orisys, install_nuitka
|
||||||
from wheel.layout import (
|
from wheel.layout import (
|
||||||
copy_bundle_items,
|
copy_bundle_items,
|
||||||
@@ -199,9 +206,7 @@ def main(argv: list[str] | None = None) -> None:
|
|||||||
)
|
)
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
verify: bool | None
|
verify: bool | None
|
||||||
if args.target == "pypi-linux":
|
if args.target == "pypi-linux" or args.verify:
|
||||||
verify = True
|
|
||||||
elif args.verify:
|
|
||||||
verify = True
|
verify = True
|
||||||
elif args.skip_verify:
|
elif args.skip_verify:
|
||||||
verify = False
|
verify = False
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# Code review: the automated part
|
||||||
|
|
||||||
|
Four gates existed before this document (the test suites, `apps/studio/tools/ui_check.py`, the
|
||||||
|
real-GL smoke tests, the binding-purity test) and none of them is a linter. This adds the missing
|
||||||
|
*static* layer, deliberately small.
|
||||||
|
|
||||||
|
## Why it exists, in one measured fact
|
||||||
|
|
||||||
|
Until now the three repos had **no Ruff configuration at all**. `ruff check .` therefore ran
|
||||||
|
whatever the installed version enables by default — Ruff 0.16 enables a very broad set
|
||||||
|
(`UP`/`RUF`/`PLR`/`S`/`TRY`/`PERF`/`FURB`/`YTT`/`ASYNC`…) — so the same tree produced **1038 /
|
||||||
|
438 / 117** findings in `orisys-app` / `orisys-sdk` / `orisys-sensor-design`, and a different
|
||||||
|
Ruff version produced different ones. A gate built on that is not a gate. `ruff.toml` now pins it.
|
||||||
|
|
||||||
|
## What is gated, and what is deliberately not
|
||||||
|
|
||||||
|
| rule | meaning | gated? | why |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `BLE001` | broad `except Exception` | **no** | A broad catch is the *right* choice around device I/O, GL teardown and camera probing. This codebase already records that choice inline as `# noqa: BLE001 - <reason>`. Gating it would produce ~100 findings whose only correct fix is "add a reasoned suppression". |
|
||||||
|
| `S110`, `S112` | handler is only `pass` / `continue` | **yes** | The complaint is not the breadth of the catch, it is that the failure becomes **invisible**. Silence is acceptable only with a reasoned suppression or a handler that reports/re-raises. |
|
||||||
|
| `E722` | bare `except:` | **yes** | Strictly worse than `except Exception:` — it also swallows `KeyboardInterrupt`/`SystemExit`. |
|
||||||
|
| `F` | unused import, redefinition, undefined name, unused variable | **yes** | Real defects: `F821`/`F822` are latent `NameError`s, `F811` can be a shadowed definition. |
|
||||||
|
| `B023` | closure over a loop variable | **yes** | Late binding silently produces wrong geometry in the headless Blender scripts. |
|
||||||
|
| `PLW1510` | `subprocess.run` without `check` | **yes** | A swallowed tool failure. |
|
||||||
|
| `RUF100` | unused suppression | reported, **never auto-fixed** | The `- reason` in a `noqa` comment is documentation. A machine must not delete it. Triage by hand. |
|
||||||
|
| `UP`, `I`, `PIE`, `RET501`, `PLR1711`, `SIM114`, `RUF022`, `RUF023` | mechanical modernisation | in the lint config, **not** in the gate | Applied once by `--fix`; gating them would only churn AI-authored diffs. |
|
||||||
|
|
||||||
|
`E402` (module import not at top of file) is **selected but not gated**. `import` after
|
||||||
|
`os.environ[...]` is deliberate in this codebase — the environment must be set before Qt/cv2/the SDK
|
||||||
|
load — so gating it would be pure noise. But it is the only rule that would catch an *accidental*
|
||||||
|
import moved above that setup, so it stays visible. When you touch one of those files, record the
|
||||||
|
intent as `# noqa: E402 - <reason>`; `E701`/`E702` are style, which `ruff format` owns. A gate that
|
||||||
|
cries wolf gets ignored, which is worse than no gate.
|
||||||
|
|
||||||
|
## The three commands
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 1. The agent loop, after every edit — mechanical, no review needed.
|
||||||
|
ruff check <changed files> --fix # never --unsafe-fixes
|
||||||
|
ruff check <changed files> --output-format json # what remains, machine-readable
|
||||||
|
|
||||||
|
# 2. The gate before a change lands. Changed files only, machine-readable, non-zero on findings.
|
||||||
|
python tools/lint_gate.py # vs HEAD
|
||||||
|
python tools/lint_gate.py --base origin/main
|
||||||
|
python tools/lint_gate.py --all # gate rules over the whole repo
|
||||||
|
python tools/lint_gate.py --json
|
||||||
|
|
||||||
|
# 3. Formatting, changed files only — never a whole-tree rewrite mid-diff.
|
||||||
|
ruff format <changed files>
|
||||||
|
```
|
||||||
|
|
||||||
|
`$RUFF` may point at a specific ruff binary; otherwise `tools/lint_gate.py` uses `PATH` or the one
|
||||||
|
beside the interpreter. **Pin the version**: the gate prints the version it ran, and findings differ
|
||||||
|
between versions, so a runner must install the same one the developers use (currently 0.16.9).
|
||||||
|
|
||||||
|
## What this found when it was introduced
|
||||||
|
|
||||||
|
Filtering to rules that can indicate a defect produced ~18 candidates rather than style noise —
|
||||||
|
which is the argument for the narrow set:
|
||||||
|
|
||||||
|
- six latent `NameError`s (`np`, `sys`, `Callable`, `FrameIdentity`, `tessellate_polygon`, and an
|
||||||
|
`__all__` entry naming a function that does not exist) — none caught by any test;
|
||||||
|
- a **duplicated** function definition that silently shadowed the first;
|
||||||
|
- `lower == lower` / `upper == upper` in a range check, and the same pattern inside a **test
|
||||||
|
assertion that could never fail**;
|
||||||
|
- 13 closures over loop variables in the headless Blender/patch scripts;
|
||||||
|
- two `subprocess.run` calls whose failure was silently ignored.
|
||||||
|
|
||||||
|
## Follow-up deliberately not done yet
|
||||||
|
|
||||||
|
`PLR0124` ("name compared with itself") is not in `select`. Eleven of its sites are the deliberate
|
||||||
|
`value != value` NaN idiom and must become `math.isnan(...)` (or carry a reasoned suppression)
|
||||||
|
before the rule can be gated — otherwise it is noise. Once those are converted, add `PLR0124` to
|
||||||
|
`select` and to `GATE_SELECT` in `tools/lint_gate.py`.
|
||||||
@@ -9,12 +9,16 @@ ORY-BOX:单传感器基础流程
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||||
import cv2
|
|
||||||
import orisys
|
|
||||||
import numpy as np
|
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
import orisys
|
||||||
|
|
||||||
|
|
||||||
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="视频源:摄像头编号或视频文件路径")
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import sys
|
|||||||
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
|
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
|
|
||||||
import orisys
|
import orisys
|
||||||
from orisys.finger import FingerRuntime
|
from orisys.finger import FingerRuntime
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ Acquisition adapters (no widgets) live in ``gui.sensors``.
|
|||||||
Shared bootstrap: ``gui.app``, ``gui.app_paths``, ``gui.log_config``, ``gui.widgets``.
|
Shared bootstrap: ``gui.app``, ``gui.app_paths``, ``gui.log_config``, ``gui.widgets``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from viz.contact_arrow import ContactArrowStyle
|
||||||
|
|
||||||
from .log_config import LOG_CONFIG, LogConfig
|
from .log_config import LOG_CONFIG, LogConfig
|
||||||
from .viewer3d import (
|
from .viewer3d import (
|
||||||
FingerDashboardWindow,
|
|
||||||
LAYOUT,
|
LAYOUT,
|
||||||
LOG_PANEL,
|
LOG_PANEL,
|
||||||
MESH_CONTACT_ARROW,
|
MESH_CONTACT_ARROW,
|
||||||
@@ -19,33 +20,33 @@ from .viewer3d import (
|
|||||||
PANEL_SETUP,
|
PANEL_SETUP,
|
||||||
TOP_ROW,
|
TOP_ROW,
|
||||||
DashboardLayout,
|
DashboardLayout,
|
||||||
|
FingerDashboardWindow,
|
||||||
MeshLighting,
|
MeshLighting,
|
||||||
PanelRow,
|
PanelRow,
|
||||||
PanelSetup,
|
PanelSetup,
|
||||||
PanelSlot,
|
PanelSlot,
|
||||||
run_dashboard,
|
run_dashboard,
|
||||||
top_row_splitter_sizes,
|
|
||||||
top_row_slot_index,
|
top_row_slot_index,
|
||||||
|
top_row_splitter_sizes,
|
||||||
)
|
)
|
||||||
from viz.contact_arrow import ContactArrowStyle
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"DashboardLayout",
|
|
||||||
"FingerDashboardWindow",
|
|
||||||
"LAYOUT",
|
"LAYOUT",
|
||||||
"LOG_CONFIG",
|
"LOG_CONFIG",
|
||||||
"LOG_PANEL",
|
"LOG_PANEL",
|
||||||
"LogConfig",
|
|
||||||
"MESH_CONTACT_ARROW",
|
"MESH_CONTACT_ARROW",
|
||||||
"MESH_LIGHTING",
|
"MESH_LIGHTING",
|
||||||
"ContactArrowStyle",
|
|
||||||
"MeshLighting",
|
|
||||||
"PANEL_SETUP",
|
"PANEL_SETUP",
|
||||||
"TOP_ROW",
|
"TOP_ROW",
|
||||||
|
"ContactArrowStyle",
|
||||||
|
"DashboardLayout",
|
||||||
|
"FingerDashboardWindow",
|
||||||
|
"LogConfig",
|
||||||
|
"MeshLighting",
|
||||||
"PanelRow",
|
"PanelRow",
|
||||||
"PanelSetup",
|
"PanelSetup",
|
||||||
"PanelSlot",
|
"PanelSlot",
|
||||||
"run_dashboard",
|
"run_dashboard",
|
||||||
"top_row_splitter_sizes",
|
|
||||||
"top_row_slot_index",
|
"top_row_slot_index",
|
||||||
|
"top_row_splitter_sizes",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ def default_config_path() -> str:
|
|||||||
def require_gui_extras() -> None:
|
def require_gui_extras() -> None:
|
||||||
try:
|
try:
|
||||||
import open3d # noqa: F401
|
import open3d # noqa: F401
|
||||||
from PyQt5 import QtWidgets # noqa: F401
|
|
||||||
import pyqtgraph # noqa: F401
|
import pyqtgraph # noqa: F401
|
||||||
|
from PyQt5 import QtWidgets # noqa: F401
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
raise SystemExit('GUI example requires: pip install "orisys[gui]"') from exc
|
raise SystemExit('GUI example requires: pip install "orisys[gui]"') from exc
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
"""Sensor acquisition adapters for Qt / multi-sensor viewers (no Qt widgets)."""
|
"""Sensor acquisition adapters for Qt / multi-sensor viewers (no Qt widgets)."""
|
||||||
|
|
||||||
|
from . import roi_geometry
|
||||||
from .adapter import (
|
from .adapter import (
|
||||||
Dd06RoiAdapter,
|
Dd06RoiAdapter,
|
||||||
FrameSnapshot,
|
FrameSnapshot,
|
||||||
OrisysSensorAdapter,
|
OrisysSensorAdapter,
|
||||||
SensorAdapter,
|
SensorAdapter,
|
||||||
|
_snapshot_from_sensor,
|
||||||
calibrate_normal_force,
|
calibrate_normal_force,
|
||||||
create_sensor_adapter,
|
create_sensor_adapter,
|
||||||
_snapshot_from_sensor,
|
|
||||||
)
|
)
|
||||||
from . import roi_geometry
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Dd06RoiAdapter",
|
"Dd06RoiAdapter",
|
||||||
"FrameSnapshot",
|
"FrameSnapshot",
|
||||||
"OrisysSensorAdapter",
|
"OrisysSensorAdapter",
|
||||||
"SensorAdapter",
|
"SensorAdapter",
|
||||||
|
"_snapshot_from_sensor",
|
||||||
"calibrate_normal_force",
|
"calibrate_normal_force",
|
||||||
"create_sensor_adapter",
|
"create_sensor_adapter",
|
||||||
"roi_geometry",
|
"roi_geometry",
|
||||||
"_snapshot_from_sensor",
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ from __future__ import annotations
|
|||||||
import time
|
import time
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from typing import Any
|
||||||
from typing import Any, Optional
|
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
import orisys
|
import orisys
|
||||||
|
|
||||||
from .roi_geometry import (
|
from .roi_geometry import (
|
||||||
@@ -21,7 +21,6 @@ from .roi_geometry import (
|
|||||||
warp_roi_to_rectangle,
|
warp_roi_to_rectangle,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
NORMAL_FORCE_CAL_SLOPE = 3.92685554668795e-05
|
NORMAL_FORCE_CAL_SLOPE = 3.92685554668795e-05
|
||||||
NORMAL_FORCE_CAL_OFFSET = 0.0217596967278535
|
NORMAL_FORCE_CAL_OFFSET = 0.0217596967278535
|
||||||
|
|
||||||
@@ -30,10 +29,10 @@ NORMAL_FORCE_CAL_OFFSET = 0.0217596967278535
|
|||||||
class FrameSnapshot:
|
class FrameSnapshot:
|
||||||
"""Normalized per-frame payload consumed by the Qt viewer."""
|
"""Normalized per-frame payload consumed by the Qt viewer."""
|
||||||
|
|
||||||
image: Optional[np.ndarray] = None
|
image: np.ndarray | None = None
|
||||||
flow: Optional[np.ndarray] = None
|
flow: np.ndarray | None = None
|
||||||
depth: Optional[np.ndarray] = None
|
depth: np.ndarray | None = None
|
||||||
contour: Optional[np.ndarray] = None
|
contour: np.ndarray | None = None
|
||||||
centroid: Any = None
|
centroid: Any = None
|
||||||
# Raw SDK force channels used for curves (same units for FN/FX/FY).
|
# Raw SDK force channels used for curves (same units for FN/FX/FY).
|
||||||
normal_force: float = 0.0
|
normal_force: float = 0.0
|
||||||
@@ -43,10 +42,10 @@ class FrameSnapshot:
|
|||||||
normal_force_calibrated: float = 0.0
|
normal_force_calibrated: float = 0.0
|
||||||
fps: float = 0.0
|
fps: float = 0.0
|
||||||
timestamp: float = 0.0
|
timestamp: float = 0.0
|
||||||
raw_image: Optional[np.ndarray] = None
|
raw_image: np.ndarray | None = None
|
||||||
processed_image: Optional[np.ndarray] = None
|
processed_image: np.ndarray | None = None
|
||||||
flow_overlay: Optional[np.ndarray] = None
|
flow_overlay: np.ndarray | None = None
|
||||||
record_frame: Optional[np.ndarray] = None
|
record_frame: np.ndarray | None = None
|
||||||
|
|
||||||
|
|
||||||
def calibrate_normal_force(raw_value: float) -> float:
|
def calibrate_normal_force(raw_value: float) -> float:
|
||||||
@@ -62,7 +61,7 @@ class SensorAdapter(ABC):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def read(self) -> Optional[FrameSnapshot]:
|
def read(self) -> FrameSnapshot | None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@@ -89,7 +88,7 @@ def _snapshot_from_sensor(
|
|||||||
*,
|
*,
|
||||||
raw_image=None,
|
raw_image=None,
|
||||||
processed_image=None,
|
processed_image=None,
|
||||||
) -> Optional[FrameSnapshot]:
|
) -> FrameSnapshot | None:
|
||||||
"""Build a FrameSnapshot with HHD/forces refreshed every frame."""
|
"""Build a FrameSnapshot with HHD/forces refreshed every frame."""
|
||||||
if sensor is None or sensor.img is None:
|
if sensor is None or sensor.img is None:
|
||||||
return None
|
return None
|
||||||
@@ -186,7 +185,7 @@ class OrisysSensorAdapter(SensorAdapter):
|
|||||||
verbose=self.verbose,
|
verbose=self.verbose,
|
||||||
)
|
)
|
||||||
|
|
||||||
def read(self) -> Optional[FrameSnapshot]:
|
def read(self) -> FrameSnapshot | None:
|
||||||
if self.sensor is None:
|
if self.sensor is None:
|
||||||
return None
|
return None
|
||||||
img = self.sensor.get_img()
|
img = self.sensor.get_img()
|
||||||
@@ -255,7 +254,7 @@ class Dd06RoiAdapter(SensorAdapter):
|
|||||||
f"video={self._source}"
|
f"video={self._source}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def read(self) -> Optional[FrameSnapshot]:
|
def read(self) -> FrameSnapshot | None:
|
||||||
if self.sensor is None or self.cap is None:
|
if self.sensor is None or self.cap is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ Does not import camera, ROI, or `orisys.Sensor`.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Literal, Optional, Sequence, Tuple
|
from typing import Literal
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import orisys
|
|
||||||
from scipy.signal import butter, filtfilt
|
from scipy.signal import butter, filtfilt
|
||||||
|
|
||||||
|
import orisys
|
||||||
|
|
||||||
FORCE_SAMPLE_RATE_HZ = 30.0
|
FORCE_SAMPLE_RATE_HZ = 30.0
|
||||||
PLOT_BUFFER_POINTS = 120
|
PLOT_BUFFER_POINTS = 120
|
||||||
PLOT_WINDOW_SEC = PLOT_BUFFER_POINTS / FORCE_SAMPLE_RATE_HZ
|
PLOT_WINDOW_SEC = PLOT_BUFFER_POINTS / FORCE_SAMPLE_RATE_HZ
|
||||||
@@ -40,14 +41,14 @@ class ViewerFrame:
|
|||||||
image_height: int
|
image_height: int
|
||||||
image_width: int
|
image_width: int
|
||||||
aspect_ratio: float # width / height
|
aspect_ratio: float # width / height
|
||||||
depth: Optional[np.ndarray]
|
depth: np.ndarray | None
|
||||||
normal_force: float
|
normal_force: float
|
||||||
shear_x: float
|
shear_x: float
|
||||||
shear_y: float
|
shear_y: float
|
||||||
normal_force_calibrated: float
|
normal_force_calibrated: float
|
||||||
fps: float
|
fps: float
|
||||||
timestamp: float
|
timestamp: float
|
||||||
record_frame: Optional[np.ndarray]
|
record_frame: np.ndarray | None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -57,8 +58,8 @@ class ForcePlotState:
|
|||||||
times: np.ndarray # shape (n,)
|
times: np.ndarray # shape (n,)
|
||||||
values: np.ndarray # shape (n, 3) FN, FX, FY
|
values: np.ndarray # shape (n, 3) FN, FX, FY
|
||||||
sample_count: int
|
sample_count: int
|
||||||
x_range: Tuple[float, float]
|
x_range: tuple[float, float]
|
||||||
y_ranges: Tuple[Tuple[float, float], Tuple[float, float], Tuple[float, float]]
|
y_ranges: tuple[tuple[float, float], tuple[float, float], tuple[float, float]]
|
||||||
|
|
||||||
|
|
||||||
def lowpass_filter(
|
def lowpass_filter(
|
||||||
@@ -87,7 +88,7 @@ def lowpass_filter(
|
|||||||
return filtfilt(b, a, y)
|
return filtfilt(b, a, y)
|
||||||
|
|
||||||
|
|
||||||
def ensure_bgr(img: Optional[np.ndarray]) -> Optional[np.ndarray]:
|
def ensure_bgr(img: np.ndarray | None) -> np.ndarray | None:
|
||||||
if img is None:
|
if img is None:
|
||||||
return None
|
return None
|
||||||
if img.ndim == 2:
|
if img.ndim == 2:
|
||||||
@@ -97,14 +98,14 @@ def ensure_bgr(img: Optional[np.ndarray]) -> Optional[np.ndarray]:
|
|||||||
return img
|
return img
|
||||||
|
|
||||||
|
|
||||||
def image_shape_meta(img: np.ndarray) -> Tuple[int, int, float]:
|
def image_shape_meta(img: np.ndarray) -> tuple[int, int, float]:
|
||||||
"""Return (height, width, width/height) from a display image."""
|
"""Return (height, width, width/height) from a display image."""
|
||||||
h, w = int(img.shape[0]), int(img.shape[1])
|
h, w = int(img.shape[0]), int(img.shape[1])
|
||||||
aspect = float(w) / float(h) if h > 0 else 1.0
|
aspect = float(w) / float(h) if h > 0 else 1.0
|
||||||
return h, w, aspect
|
return h, w, aspect
|
||||||
|
|
||||||
|
|
||||||
def _base_bgr(snapshot) -> Optional[np.ndarray]:
|
def _base_bgr(snapshot) -> np.ndarray | None:
|
||||||
for attr in ("image", "processed_image", "raw_image"):
|
for attr in ("image", "processed_image", "raw_image"):
|
||||||
out = ensure_bgr(getattr(snapshot, attr, None))
|
out = ensure_bgr(getattr(snapshot, attr, None))
|
||||||
if out is not None:
|
if out is not None:
|
||||||
@@ -152,7 +153,7 @@ def build_display_image(
|
|||||||
return select_display_image(snapshot)
|
return select_display_image(snapshot)
|
||||||
|
|
||||||
|
|
||||||
def bgr_to_rgb_bytes(img_bgr: np.ndarray) -> Tuple[np.ndarray, int, int]:
|
def bgr_to_rgb_bytes(img_bgr: np.ndarray) -> tuple[np.ndarray, int, int]:
|
||||||
"""Convert BGR uint8 image to contiguous RGB for QImage."""
|
"""Convert BGR uint8 image to contiguous RGB for QImage."""
|
||||||
rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
|
rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
|
||||||
if not rgb.flags["C_CONTIGUOUS"]:
|
if not rgb.flags["C_CONTIGUOUS"]:
|
||||||
@@ -231,7 +232,7 @@ class ForceHistory:
|
|||||||
self.data[-1, 0, :] = t
|
self.data[-1, 0, :] = t
|
||||||
self.data[-1, 1, :] = values
|
self.data[-1, 1, :] = values
|
||||||
|
|
||||||
def _smooth_y_range(self, channel: int, ymin: float, ymax: float) -> Tuple[float, float]:
|
def _smooth_y_range(self, channel: int, ymin: float, ymax: float) -> tuple[float, float]:
|
||||||
if self._y_range_smooth[channel] is None:
|
if self._y_range_smooth[channel] is None:
|
||||||
smoothed = (ymin, ymax)
|
smoothed = (ymin, ymax)
|
||||||
else:
|
else:
|
||||||
@@ -251,7 +252,7 @@ class ForceHistory:
|
|||||||
pad = max((smoothed[1] - smoothed[0]) * 0.12, 0.01)
|
pad = max((smoothed[1] - smoothed[0]) * 0.12, 0.01)
|
||||||
return smoothed[0] - pad, smoothed[1] + pad
|
return smoothed[0] - pad, smoothed[1] + pad
|
||||||
|
|
||||||
def plot_state(self) -> Optional[ForcePlotState]:
|
def plot_state(self) -> ForcePlotState | None:
|
||||||
n = self.sample_count
|
n = self.sample_count
|
||||||
if n == 0:
|
if n == 0:
|
||||||
return None
|
return None
|
||||||
@@ -305,8 +306,8 @@ class QtViewerBackend:
|
|||||||
lowpass_cutoff_hz=lowpass_cutoff_hz,
|
lowpass_cutoff_hz=lowpass_cutoff_hz,
|
||||||
y_axis_min_span=y_axis_min_span,
|
y_axis_min_span=y_axis_min_span,
|
||||||
)
|
)
|
||||||
self._start_time: Optional[float] = None
|
self._start_time: float | None = None
|
||||||
self.last_viewer_frame: Optional[ViewerFrame] = None
|
self.last_viewer_frame: ViewerFrame | None = None
|
||||||
self.arrow_background: ArrowBackground = arrow_background
|
self.arrow_background: ArrowBackground = arrow_background
|
||||||
|
|
||||||
def set_arrow_background(self, background: ArrowBackground) -> None:
|
def set_arrow_background(self, background: ArrowBackground) -> None:
|
||||||
@@ -357,7 +358,7 @@ class QtViewerBackend:
|
|||||||
self._start_time = None
|
self._start_time = None
|
||||||
self.last_viewer_frame = None
|
self.last_viewer_frame = None
|
||||||
|
|
||||||
def ingest(self, snapshot, *, now: Optional[float] = None) -> ViewerFrame:
|
def ingest(self, snapshot, *, now: float | None = None) -> ViewerFrame:
|
||||||
"""Convert a FrameSnapshot into ViewerFrame and update force history."""
|
"""Convert a FrameSnapshot into ViewerFrame and update force history."""
|
||||||
display = build_display_image(snapshot, self.arrow_background)
|
display = build_display_image(snapshot, self.arrow_background)
|
||||||
h, w, aspect = image_shape_meta(display)
|
h, w, aspect = image_shape_meta(display)
|
||||||
@@ -396,5 +397,5 @@ class QtViewerBackend:
|
|||||||
self.last_viewer_frame = frame
|
self.last_viewer_frame = frame
|
||||||
return frame
|
return frame
|
||||||
|
|
||||||
def plot_state(self) -> Optional[ForcePlotState]:
|
def plot_state(self) -> ForcePlotState | None:
|
||||||
return self.force_history.plot_state()
|
return self.force_history.plot_state()
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
from .dashboard import FingerDashboardWindow, run_dashboard
|
from .dashboard import FingerDashboardWindow, run_dashboard
|
||||||
from .layout import (
|
from .layout import (
|
||||||
LOG_PANEL,
|
|
||||||
LAYOUT,
|
LAYOUT,
|
||||||
|
LOG_PANEL,
|
||||||
MESH_CONTACT_ARROW,
|
MESH_CONTACT_ARROW,
|
||||||
MESH_LIGHTING,
|
MESH_LIGHTING,
|
||||||
PANEL_SETUP,
|
PANEL_SETUP,
|
||||||
@@ -13,24 +13,24 @@ from .layout import (
|
|||||||
PanelRow,
|
PanelRow,
|
||||||
PanelSetup,
|
PanelSetup,
|
||||||
PanelSlot,
|
PanelSlot,
|
||||||
top_row_splitter_sizes,
|
|
||||||
top_row_slot_index,
|
top_row_slot_index,
|
||||||
|
top_row_splitter_sizes,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"DashboardLayout",
|
|
||||||
"FingerDashboardWindow",
|
|
||||||
"LAYOUT",
|
"LAYOUT",
|
||||||
"LOG_PANEL",
|
"LOG_PANEL",
|
||||||
"MESH_CONTACT_ARROW",
|
"MESH_CONTACT_ARROW",
|
||||||
"MESH_LIGHTING",
|
"MESH_LIGHTING",
|
||||||
"MeshLighting",
|
|
||||||
"PANEL_SETUP",
|
"PANEL_SETUP",
|
||||||
"TOP_ROW",
|
"TOP_ROW",
|
||||||
|
"DashboardLayout",
|
||||||
|
"FingerDashboardWindow",
|
||||||
|
"MeshLighting",
|
||||||
"PanelRow",
|
"PanelRow",
|
||||||
"PanelSetup",
|
"PanelSetup",
|
||||||
"PanelSlot",
|
"PanelSlot",
|
||||||
"run_dashboard",
|
"run_dashboard",
|
||||||
"top_row_splitter_sizes",
|
|
||||||
"top_row_slot_index",
|
"top_row_slot_index",
|
||||||
|
"top_row_splitter_sizes",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -11,6 +12,8 @@ import cv2
|
|||||||
|
|
||||||
DEFAULT_MAX_CAMERA_INDEX = 10
|
DEFAULT_MAX_CAMERA_INDEX = 10
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class CameraInfo:
|
class CameraInfo:
|
||||||
@@ -62,8 +65,14 @@ def _suppress_opencv_noise():
|
|||||||
if set_level is not None and prev_level is not None:
|
if set_level is not None and prev_level is not None:
|
||||||
try:
|
try:
|
||||||
set_level(prev_level)
|
set_level(prev_level)
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
pass
|
# Best-effort: a failure only leaves OpenCV probe noise suppressed,
|
||||||
|
# but report it so a stuck log level is diagnosable.
|
||||||
|
logger.warning(
|
||||||
|
"camera discovery: could not restore OpenCV log level %r: %s",
|
||||||
|
prev_level,
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _backend_candidates() -> list[tuple[str, int]]:
|
def _backend_candidates() -> list[tuple[str, int]]:
|
||||||
@@ -115,9 +124,21 @@ def list_cameras(max_index: int = DEFAULT_MAX_CAMERA_INDEX) -> list[CameraInfo]:
|
|||||||
backends = _backend_candidates()
|
backends = _backend_candidates()
|
||||||
|
|
||||||
with _suppress_opencv_noise():
|
with _suppress_opencv_noise():
|
||||||
for index in range(0, max_index + 1):
|
for index in range(max_index + 1):
|
||||||
for backend_name, backend_api in backends:
|
for backend_name, backend_api in backends:
|
||||||
info = _try_open(index, backend_name, backend_api)
|
try:
|
||||||
|
info = _try_open(index, backend_name, backend_api)
|
||||||
|
except Exception as exc:
|
||||||
|
# Same hazard as the SDK enumerator: an escaping error for one
|
||||||
|
# backend would abort the whole probe and hide every other
|
||||||
|
# camera. Skip that candidate and keep going.
|
||||||
|
logger.warning(
|
||||||
|
"camera discovery: probing index %d via %s failed: %s",
|
||||||
|
index,
|
||||||
|
backend_name,
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
continue
|
||||||
if info is None:
|
if info is None:
|
||||||
continue
|
continue
|
||||||
if index in seen_indices:
|
if index in seen_indices:
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import math
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from pipeline.config import PipelineConfig
|
||||||
|
from pipeline.csv_recorder import CsvRecorder
|
||||||
|
from pipeline.service import FingerPipeline
|
||||||
from PyQt5.QtCore import Qt, QTimer
|
from PyQt5.QtCore import Qt, QTimer
|
||||||
from PyQt5.QtGui import QIcon
|
from PyQt5.QtGui import QIcon
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
@@ -17,23 +20,17 @@ from PyQt5.QtWidgets import (
|
|||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
from pipeline.config import PipelineConfig
|
|
||||||
from pipeline.csv_recorder import CsvRecorder
|
|
||||||
from pipeline.service import FingerPipeline
|
|
||||||
|
|
||||||
from gui.app_paths import resource_path
|
from gui.app_paths import resource_path
|
||||||
|
from gui.widgets import TeeStream, WidgetLogHandler, make_log_widget
|
||||||
|
|
||||||
from .camera_discovery import list_cameras, pick_default_camera
|
from .camera_discovery import list_cameras, pick_default_camera
|
||||||
from .preferences import get_preferred_video_source, load_preferences, save_preferences
|
|
||||||
from .layout import (
|
from .layout import (
|
||||||
DashboardLayout,
|
|
||||||
LAYOUT,
|
LAYOUT,
|
||||||
PANEL_SETUP,
|
DashboardLayout,
|
||||||
PanelSetup,
|
|
||||||
top_row_splitter_sizes,
|
|
||||||
)
|
)
|
||||||
from .mesh_view_state import MeshViewState
|
from .mesh_view_state import MeshViewState
|
||||||
|
from .preferences import get_preferred_video_source, load_preferences, save_preferences
|
||||||
from .styles import STYLE
|
from .styles import STYLE
|
||||||
from gui.widgets import TeeStream, WidgetLogHandler, make_log_widget
|
|
||||||
from .widgets import (
|
from .widgets import (
|
||||||
ArrowPanelWidget,
|
ArrowPanelWidget,
|
||||||
ForceChartWidget,
|
ForceChartWidget,
|
||||||
|
|||||||
@@ -47,11 +47,12 @@ import math
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from gui.log_config import LOG_CONFIG, LogConfig
|
from viz.contact_arrow import ContactArrowStyle
|
||||||
from viz.contact_arrow import ContactArrowStyle, DEFAULT_CONTACT_ARROW_STYLE
|
|
||||||
from viz.flow_arrow import FlowArrowStyle
|
from viz.flow_arrow import FlowArrowStyle
|
||||||
from viz.render_2d import ArrowBackground, ArrowCanvasColors
|
from viz.render_2d import ArrowBackground, ArrowCanvasColors
|
||||||
|
|
||||||
|
from gui.log_config import LOG_CONFIG, LogConfig
|
||||||
|
|
||||||
PanelId = Literal["mesh", "arrows", "charts", "log", "settings"]
|
PanelId = Literal["mesh", "arrows", "charts", "log", "settings"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtSignal
|
from PyQt5.QtCore import QObject, pyqtSignal
|
||||||
|
|
||||||
from .layout import DashboardLayout, LAYOUT
|
from .layout import LAYOUT, DashboardLayout
|
||||||
|
|
||||||
|
|
||||||
class MeshViewState(QObject):
|
class MeshViewState(QObject):
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
|
|
||||||
from pipeline.frame import FingerFrameView
|
from pipeline.frame import FingerFrameView
|
||||||
from viz.render_2d import ArrowBackground, ArrowCanvasColors, render_uv_arrows
|
from viz.render_2d import ArrowBackground, ArrowCanvasColors, render_uv_arrows
|
||||||
|
|
||||||
from ..layout import DashboardLayout, LAYOUT
|
|
||||||
from .image_panel import ImagePanel
|
|
||||||
from gui.widgets.qt_pixmap import ndarray_to_pixmap
|
from gui.widgets.qt_pixmap import ndarray_to_pixmap
|
||||||
|
|
||||||
|
from ..layout import LAYOUT, DashboardLayout
|
||||||
|
from .image_panel import ImagePanel
|
||||||
|
|
||||||
|
|
||||||
class ArrowPanelWidget(ImagePanel):
|
class ArrowPanelWidget(ImagePanel):
|
||||||
"""UV unrolled flow-arrow panel."""
|
"""UV unrolled flow-arrow panel."""
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from pipeline.frame import FingerFrameView
|
||||||
from PyQt5.QtGui import QFont
|
from PyQt5.QtGui import QFont
|
||||||
from PyQt5.QtWidgets import QSizePolicy
|
from PyQt5.QtWidgets import QSizePolicy
|
||||||
|
|
||||||
from ..layout import DashboardLayout, LAYOUT
|
|
||||||
from ..styles import LOG_FONT_PRIMARY, UI_FONT_PRIMARY
|
|
||||||
from pipeline.frame import FingerFrameView
|
|
||||||
from viz.signal_filters import lowpass_filter
|
from viz.signal_filters import lowpass_filter
|
||||||
|
|
||||||
|
from ..layout import LAYOUT, DashboardLayout
|
||||||
|
from ..styles import LOG_FONT_PRIMARY, UI_FONT_PRIMARY
|
||||||
|
|
||||||
|
|
||||||
def _require_pyqtgraph():
|
def _require_pyqtgraph():
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pipeline.frame import FingerFrameView
|
||||||
from PyQt5.QtCore import QEvent, Qt
|
from PyQt5.QtCore import QEvent, Qt
|
||||||
from PyQt5.QtGui import QMouseEvent
|
from PyQt5.QtGui import QMouseEvent
|
||||||
|
|
||||||
from pipeline.frame import FingerFrameView
|
|
||||||
from viz.offscreen_mesh import OffscreenFingerMeshRenderer
|
from viz.offscreen_mesh import OffscreenFingerMeshRenderer
|
||||||
|
|
||||||
from ..layout import DashboardLayout, LAYOUT
|
|
||||||
from ..mesh_view_state import MeshViewState
|
|
||||||
from ..mesh_render_worker import MeshRenderWorker
|
|
||||||
from gui.widgets.qt_pixmap import ndarray_to_pixmap
|
from gui.widgets.qt_pixmap import ndarray_to_pixmap
|
||||||
|
|
||||||
|
from ..layout import LAYOUT, DashboardLayout
|
||||||
|
from ..mesh_render_worker import MeshRenderWorker
|
||||||
|
from ..mesh_view_state import MeshViewState
|
||||||
from .image_panel import ImagePanel
|
from .image_panel import ImagePanel
|
||||||
|
|
||||||
_VIEW_ROTATE_SENSITIVITY = 0.35
|
_VIEW_ROTATE_SENSITIVITY = 0.35
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from PyQt5.QtCore import QSize, Qt, pyqtSignal
|
from PyQt5.QtCore import QSize, pyqtSignal
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QCheckBox,
|
QCheckBox,
|
||||||
QComboBox,
|
QComboBox,
|
||||||
@@ -14,11 +14,11 @@ from PyQt5.QtWidgets import (
|
|||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
from orisys.finger import DEFAULT_FINGER_EXPORT_DIR
|
|
||||||
from viz.render_2d import ArrowBackground
|
from viz.render_2d import ArrowBackground
|
||||||
|
|
||||||
from ..layout import DashboardLayout, LAYOUT
|
from orisys.finger import DEFAULT_FINGER_EXPORT_DIR
|
||||||
|
|
||||||
|
from ..layout import LAYOUT, DashboardLayout
|
||||||
from ..mesh_view_state import MeshViewState
|
from ..mesh_view_state import MeshViewState
|
||||||
from ..transport_icons import reset_icon, transport_icon
|
from ..transport_icons import reset_icon, transport_icon
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,16 @@ ORY-MINI:单传感器基础流程
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||||
import cv2
|
|
||||||
import orisys
|
|
||||||
import numpy as np
|
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
import orisys
|
||||||
|
|
||||||
|
|
||||||
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="视频源:摄像头编号或视频文件路径")
|
||||||
@@ -29,11 +33,13 @@ def main():
|
|||||||
# 输入既可以是摄像头编号,也可以是视频文件路径
|
# 输入既可以是摄像头编号,也可以是视频文件路径
|
||||||
try:
|
try:
|
||||||
input = int(args.video)
|
input = int(args.video)
|
||||||
is_camera = True
|
except ValueError:
|
||||||
sensor = orisys.Sensor(input, config_name=args.config, verbose=args.verbose)
|
# Not a camera index: treat the argument as a video-file path. Falling back
|
||||||
except:
|
# only on the conversion (not on Sensor construction) keeps a real
|
||||||
is_camera = False
|
# constructor failure visible instead of retrying it as a file path.
|
||||||
sensor = orisys.Sensor(args.video, config_name=args.config, verbose=args.verbose)
|
sensor = orisys.Sensor(args.video, config_name=args.config, verbose=args.verbose)
|
||||||
|
else:
|
||||||
|
sensor = orisys.Sensor(input, config_name=args.config, verbose=args.verbose)
|
||||||
|
|
||||||
print("\n按键说明:'q' 退出程序,'r' 重置追踪器。")
|
print("\n按键说明:'q' 退出程序,'r' 重置追踪器。")
|
||||||
|
|
||||||
|
|||||||
@@ -245,7 +245,10 @@ def main():
|
|||||||
try:
|
try:
|
||||||
with open(path) as fh:
|
with open(path) as fh:
|
||||||
rows.append(json.load(fh))
|
rows.append(json.load(fh))
|
||||||
except Exception:
|
except Exception: # noqa: S110 - a stream that wrote no result is reported below
|
||||||
|
# A stream that crashed or produced no JSON output is expected here;
|
||||||
|
# the empty-``rows`` check below reports that case explicitly, so this
|
||||||
|
# is not a hidden failure.
|
||||||
pass
|
pass
|
||||||
if not rows:
|
if not rows:
|
||||||
print("no stream produced results", file=sys.stderr)
|
print("no stream produced results", file=sys.stderr)
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from orisys.finger import DEFAULT_FINGER_EXPORT_DIR
|
|
||||||
from viz.mesh_camera import DEFAULT_FINGER_VIZ_POSE
|
from viz.mesh_camera import DEFAULT_FINGER_VIZ_POSE
|
||||||
|
|
||||||
|
from orisys.finger import DEFAULT_FINGER_EXPORT_DIR
|
||||||
|
|
||||||
|
|
||||||
def mesh_threshold(mesh_map: str, threshold: float | None) -> float:
|
def mesh_threshold(mesh_map: str, threshold: float | None) -> float:
|
||||||
if threshold is not None:
|
if threshold is not None:
|
||||||
@@ -33,7 +34,7 @@ class PipelineConfig:
|
|||||||
profile_every: int = 30
|
profile_every: int = 30
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_args(cls, args) -> "PipelineConfig":
|
def from_args(cls, args) -> PipelineConfig:
|
||||||
return cls(
|
return cls(
|
||||||
video=args.video,
|
video=args.video,
|
||||||
config=args.config,
|
config=args.config,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from dataclasses import dataclass
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
CSV_COLUMNS = (
|
CSV_COLUMNS = (
|
||||||
"frame_index",
|
"frame_index",
|
||||||
"timestamp_s",
|
"timestamp_s",
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import time
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from orisys.finger import FingerRuntime, compute_uv_surface_frames
|
|
||||||
|
|
||||||
from viz import FrameRecorder, build_uv_surface_mesh, resolve_viz_pose_path
|
from viz import FrameRecorder, build_uv_surface_mesh, resolve_viz_pose_path
|
||||||
|
|
||||||
|
from orisys.finger import FingerRuntime, compute_uv_surface_frames
|
||||||
|
|
||||||
from .config import PipelineConfig
|
from .config import PipelineConfig
|
||||||
from .csv_recorder import CsvMeasurement
|
from .csv_recorder import CsvMeasurement
|
||||||
from .frame import FingerFrameView
|
from .frame import FingerFrameView
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
|||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import orisys
|
|
||||||
|
|
||||||
from viz import ForceChartWindow, FrameRecorder
|
from viz import ForceChartWindow, FrameRecorder
|
||||||
|
|
||||||
|
import orisys
|
||||||
|
|
||||||
|
|
||||||
def open_sensor(video: str, config: str, cal: str, verbose: bool):
|
def open_sensor(video: str, config: str, cal: str, verbose: bool):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -20,14 +20,17 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||||
import cv2
|
|
||||||
import orisys
|
|
||||||
import numpy as np
|
|
||||||
import multiprocessing as mp
|
|
||||||
from multiprocessing import Queue, Process
|
|
||||||
import time
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import multiprocessing as mp
|
||||||
|
import queue
|
||||||
|
import time
|
||||||
|
from multiprocessing import Process, Queue
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
|
||||||
|
import orisys
|
||||||
|
|
||||||
|
|
||||||
def sensor_worker(sensor_id, vid_src, config_name, result_queue, stop_event, target_fps=None):
|
def sensor_worker(sensor_id, vid_src, config_name, result_queue, stop_event, target_fps=None):
|
||||||
@@ -55,6 +58,7 @@ def sensor_worker(sensor_id, vid_src, config_name, result_queue, stop_event, tar
|
|||||||
print(f"传感器 {sensor_id} 已启动" + (f"(目标帧率:{target_fps} FPS)" if target_fps else ""))
|
print(f"传感器 {sensor_id} 已启动" + (f"(目标帧率:{target_fps} FPS)" if target_fps else ""))
|
||||||
|
|
||||||
frame_interval = 1.0 / target_fps if target_fps and target_fps > 0 else 0.0
|
frame_interval = 1.0 / target_fps if target_fps and target_fps > 0 else 0.0
|
||||||
|
dropped_frames = 0
|
||||||
|
|
||||||
while not stop_event.is_set():
|
while not stop_event.is_set():
|
||||||
t_start = time.perf_counter()
|
t_start = time.perf_counter()
|
||||||
@@ -95,9 +99,10 @@ def sensor_worker(sensor_id, vid_src, config_name, result_queue, stop_event, tar
|
|||||||
# 步骤 6:将结果发送到主进程(非阻塞)
|
# 步骤 6:将结果发送到主进程(非阻塞)
|
||||||
try:
|
try:
|
||||||
result_queue.put_nowait(data)
|
result_queue.put_nowait(data)
|
||||||
except Exception:
|
except queue.Full:
|
||||||
# 如果队列已满,则丢弃当前帧,避免阻塞工作进程
|
# 队列已满:丢弃当前帧,避免阻塞工作进程。这是有意的背压策略,
|
||||||
pass
|
# 但丢帧必须可观测,否则主进程看到的低帧率无从解释。
|
||||||
|
dropped_frames += 1
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"传感器 {sensor_id} 处理失败:{e}")
|
print(f"传感器 {sensor_id} 处理失败:{e}")
|
||||||
@@ -112,7 +117,13 @@ def sensor_worker(sensor_id, vid_src, config_name, result_queue, stop_event, tar
|
|||||||
|
|
||||||
# 释放资源
|
# 释放资源
|
||||||
sensor.disconnect()
|
sensor.disconnect()
|
||||||
print(f"传感器 {sensor_id} 已停止。")
|
if dropped_frames:
|
||||||
|
print(
|
||||||
|
f"传感器 {sensor_id} 已停止(因结果队列已满丢弃 {dropped_frames} 帧,"
|
||||||
|
"主进程处理慢于采集)。"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(f"传感器 {sensor_id} 已停止。")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"传感器 {sensor_id} 初始化失败:{e}")
|
print(f"传感器 {sensor_id} 初始化失败:{e}")
|
||||||
@@ -193,7 +204,8 @@ def main():
|
|||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
data = queue.get_nowait()
|
data = queue.get_nowait()
|
||||||
except:
|
except queue.Empty:
|
||||||
|
# 队列已排空:正常结束内层循环,不是错误。
|
||||||
break
|
break
|
||||||
|
|
||||||
if data is not None:
|
if data is not None:
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"""Shared visualization helpers for Orisys example scripts."""
|
"""Shared visualization helpers for Orisys example scripts."""
|
||||||
|
|
||||||
|
from .finger_mesh_viewer import FingerMeshViewer
|
||||||
|
from .flow_arrow import DEFAULT_FLOW_ARROW_STYLE, FlowArrowOverlay, FlowArrowStyle
|
||||||
from .force_charts import ForceChartWindow
|
from .force_charts import ForceChartWindow
|
||||||
from .mesh_camera import (
|
from .mesh_camera import (
|
||||||
DEFAULT_FINGER_VIZ_POSE,
|
DEFAULT_FINGER_VIZ_POSE,
|
||||||
@@ -8,33 +10,32 @@ from .mesh_camera import (
|
|||||||
camera_look_at_from_world_to_cam,
|
camera_look_at_from_world_to_cam,
|
||||||
resolve_viz_pose_path,
|
resolve_viz_pose_path,
|
||||||
)
|
)
|
||||||
|
from .mesh_viewer import UVMeshViewer, build_uv_surface_mesh
|
||||||
from .offscreen_mesh import OffscreenFingerMeshRenderer
|
from .offscreen_mesh import OffscreenFingerMeshRenderer
|
||||||
|
from .recording import FrameRecorder
|
||||||
from .render_2d import (
|
from .render_2d import (
|
||||||
|
DEFAULT_ARROW_CANVAS_COLORS,
|
||||||
ArrowBackground,
|
ArrowBackground,
|
||||||
ArrowCanvasColors,
|
ArrowCanvasColors,
|
||||||
DEFAULT_ARROW_CANVAS_COLORS,
|
|
||||||
depth_colormap,
|
depth_colormap,
|
||||||
frame_to_bgr,
|
frame_to_bgr,
|
||||||
render_contact_panel,
|
render_contact_panel,
|
||||||
render_depth_panel,
|
render_depth_panel,
|
||||||
render_uv_arrows,
|
render_uv_arrows,
|
||||||
)
|
)
|
||||||
from .flow_arrow import DEFAULT_FLOW_ARROW_STYLE, FlowArrowOverlay, FlowArrowStyle
|
|
||||||
from .finger_mesh_viewer import FingerMeshViewer
|
|
||||||
from .mesh_viewer import UVMeshViewer, build_uv_surface_mesh
|
|
||||||
from .recording import FrameRecorder
|
|
||||||
from .signal_filters import lowpass_filter
|
from .signal_filters import lowpass_filter
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"ArrowBackground",
|
|
||||||
"ArrowCanvasColors",
|
|
||||||
"DEFAULT_ARROW_CANVAS_COLORS",
|
"DEFAULT_ARROW_CANVAS_COLORS",
|
||||||
"DEFAULT_FLOW_ARROW_STYLE",
|
"DEFAULT_FLOW_ARROW_STYLE",
|
||||||
|
"ArrowBackground",
|
||||||
|
"ArrowCanvasColors",
|
||||||
|
"FingerMeshViewer",
|
||||||
"FlowArrowOverlay",
|
"FlowArrowOverlay",
|
||||||
"FlowArrowStyle",
|
"FlowArrowStyle",
|
||||||
"FingerMeshViewer",
|
|
||||||
"ForceChartWindow",
|
"ForceChartWindow",
|
||||||
"FrameRecorder",
|
"FrameRecorder",
|
||||||
|
"OffscreenFingerMeshRenderer",
|
||||||
"UVMeshViewer",
|
"UVMeshViewer",
|
||||||
"apply_mesh_lighting",
|
"apply_mesh_lighting",
|
||||||
"apply_view_from_world_to_cam",
|
"apply_view_from_world_to_cam",
|
||||||
@@ -43,7 +44,6 @@ __all__ = [
|
|||||||
"depth_colormap",
|
"depth_colormap",
|
||||||
"frame_to_bgr",
|
"frame_to_bgr",
|
||||||
"lowpass_filter",
|
"lowpass_filter",
|
||||||
"OffscreenFingerMeshRenderer",
|
|
||||||
"render_contact_panel",
|
"render_contact_panel",
|
||||||
"render_depth_panel",
|
"render_depth_panel",
|
||||||
"render_uv_arrows",
|
"render_uv_arrows",
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ import numpy as np
|
|||||||
|
|
||||||
from orisys.finger.viewer import UVMeshViewer
|
from orisys.finger.viewer import UVMeshViewer
|
||||||
|
|
||||||
from .contact_arrow import ContactArrowOverlay, ContactArrowStyle, DEFAULT_CONTACT_ARROW_STYLE
|
from .contact_arrow import (
|
||||||
|
DEFAULT_CONTACT_ARROW_STYLE,
|
||||||
|
ContactArrowOverlay,
|
||||||
|
ContactArrowStyle,
|
||||||
|
)
|
||||||
from .flow_arrow import DEFAULT_FLOW_ARROW_STYLE, FlowArrowOverlay, FlowArrowStyle
|
from .flow_arrow import DEFAULT_FLOW_ARROW_STYLE, FlowArrowOverlay, FlowArrowStyle
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Callable, Literal
|
from typing import Literal
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
"""Re-exports for finger 3D mesh examples; core viewer lives in orisys.finger."""
|
"""Re-exports for finger 3D mesh examples; core viewer lives in orisys.finger."""
|
||||||
|
|
||||||
from orisys.finger.mesh import * # noqa: F403
|
from orisys.finger.mesh import * # noqa: F403
|
||||||
from orisys.finger.viewer import UVMeshViewer, init_uv_mesh_viewer, update_uv_mesh_viewer # noqa: F401
|
from orisys.finger.viewer import ( # noqa: F401
|
||||||
|
UVMeshViewer,
|
||||||
|
init_uv_mesh_viewer,
|
||||||
|
update_uv_mesh_viewer,
|
||||||
|
)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Literal
|
|||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
import orisys
|
import orisys
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from scipy.signal import butter, filtfilt
|
from scipy.signal import butter, filtfilt
|
||||||
|
|
||||||
|
|
||||||
def lowpass_filter(data, cutoff_hz=5, sample_rate_hz=30, order=2) -> np.ndarray:
|
def lowpass_filter(data, cutoff_hz=5, sample_rate_hz=30, order=2) -> np.ndarray:
|
||||||
"""Zero-phase Butterworth low-pass filter for uniformly sampled 1-D data."""
|
"""Zero-phase Butterworth low-pass filter for uniformly sampled 1-D data."""
|
||||||
y = np.asarray(data, dtype=np.float64)
|
y = np.asarray(data, dtype=np.float64)
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Ruff configuration — pinned deliberately.
|
||||||
|
#
|
||||||
|
# Before this file existed the repo had no Ruff config, so `ruff check .` ran whatever the
|
||||||
|
# installed Ruff version enables by default (Ruff 0.16 turns on a very broad set: UP/RUF/PLR/
|
||||||
|
# S/TRY/PERF/FURB/YTT/ASYNC...). The same tree therefore produced different findings on
|
||||||
|
# different machines and between a developer and a runner. Never rely on tool defaults.
|
||||||
|
#
|
||||||
|
# Policy, decided from the measured findings rather than from taste:
|
||||||
|
#
|
||||||
|
# * `select` is explicit. High-signal correctness rules first, then only the mechanical
|
||||||
|
# modernisation rules we are willing to apply without human review.
|
||||||
|
# * BLE001 is selected so existing `# noqa: BLE001 - <reason>` suppressions stay *used* and
|
||||||
|
# RUF100 can never delete their rationale — a broad `except Exception` is a legitimate
|
||||||
|
# choice around device I/O, capture probes and teardown. It is NOT part of the gate.
|
||||||
|
# * S110 / S112 / E722 ARE part of the gate: a silent handler hides a failure. This matters
|
||||||
|
# most in the live capture path, where a swallowed read error kills a source silently.
|
||||||
|
# * RUF100 reports unused suppressions but is `unfixable`: a machine must not remove a
|
||||||
|
# suppression whose comment records why it exists. Triage those by hand.
|
||||||
|
# * The mechanical sweep never uses `--unsafe-fixes`.
|
||||||
|
#
|
||||||
|
# The gate selects the narrow set explicitly, so BLE001 cannot fail a build, and only ever
|
||||||
|
# touches changed files:
|
||||||
|
#
|
||||||
|
# tools/lint_gate.py # the gate: changed files, machine-readable
|
||||||
|
|
||||||
|
target-version = "py310"
|
||||||
|
line-length = 88
|
||||||
|
|
||||||
|
[lint]
|
||||||
|
select = [
|
||||||
|
"E9", "E722", # syntax/IO errors, and bare `except:`
|
||||||
|
"E402", # module import not at top of file — SELECTED for visibility, NOT gated.
|
||||||
|
# `import` after `os.environ[...]` is deliberate in examples/ and scripts/
|
||||||
|
# (the environment must be set before Qt/cv2/orisys load). E402 is the only
|
||||||
|
# rule that would catch an accidental import moved above that setup, so it
|
||||||
|
# stays reported, without failing a build. An intentional site belongs in a
|
||||||
|
# `# noqa: E402 - <reason>` when it is touched.
|
||||||
|
"F", # pyflakes: unused import, redefinition, undefined name
|
||||||
|
"S110", "S112", # try-except-pass / try-except-continue — an invisible failure
|
||||||
|
"B023", # function definition does not bind loop variable
|
||||||
|
"PLW1510", # subprocess.run without an explicit check
|
||||||
|
"RUF100", # unused noqa — reported for triage, never auto-removed
|
||||||
|
"UP", # pyupgrade
|
||||||
|
"I", # import sorting
|
||||||
|
"RUF022", "RUF023", # __all__ / __slots__ ordering
|
||||||
|
"PIE", # flake8-pie
|
||||||
|
"RET501", "PLR1711",
|
||||||
|
"SIM114",
|
||||||
|
"BLE001", # selected for suppression hygiene only — excluded from the gate
|
||||||
|
]
|
||||||
|
unfixable = ["RUF100"]
|
||||||
|
|
||||||
|
[format]
|
||||||
|
quote-style = "double"
|
||||||
|
indent-style = "space"
|
||||||
+11
-7
@@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||||
import cv2
|
import cv2
|
||||||
|
|
||||||
@@ -9,10 +10,11 @@ _local_src = os.path.join(os.path.dirname(__file__), "src")
|
|||||||
if _local_src not in sys.path:
|
if _local_src not in sys.path:
|
||||||
sys.path.insert(0, _local_src)
|
sys.path.insert(0, _local_src)
|
||||||
|
|
||||||
import orisys
|
|
||||||
import numpy as np
|
|
||||||
import time
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import time
|
||||||
|
|
||||||
|
import orisys
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
@@ -30,11 +32,13 @@ def main():
|
|||||||
# 方式1: 从视频文件读取
|
# 方式1: 从视频文件读取
|
||||||
try:
|
try:
|
||||||
input = int(args.video)
|
input = int(args.video)
|
||||||
is_camera = True
|
except ValueError:
|
||||||
sensor = orisys.Sensor(input, config_name=args.config, cal_path= args.cal, verbose=args.verbose, debug=args.debug)
|
# Not a camera index: treat the argument as a video-file path. Only the
|
||||||
except:
|
# conversion is guarded, so a genuine Sensor construction error is not
|
||||||
is_camera = False
|
# swallowed and retried as a file path.
|
||||||
sensor = orisys.Sensor(args.video, config_name=args.config, cal_path= args.cal, verbose=args.verbose, debug=args.debug)
|
sensor = orisys.Sensor(args.video, config_name=args.config, cal_path= args.cal, verbose=args.verbose, debug=args.debug)
|
||||||
|
else:
|
||||||
|
sensor = orisys.Sensor(input, config_name=args.config, cal_path= args.cal, verbose=args.verbose, debug=args.debug)
|
||||||
# sensor = orisys.Sensor(2, config_name="./config/ddjx01_q6a.json", cal_path= "./config/ddjx01.npy", isstitch=True, cuda=False, verbose=True, backend='cpu')
|
# sensor = orisys.Sensor(2, config_name="./config/ddjx01_q6a.json", cal_path= "./config/ddjx01.npy", isstitch=True, cuda=False, verbose=True, backend='cpu')
|
||||||
|
|
||||||
# 方式2: 从摄像头读取(取消注释使用)
|
# 方式2: 从摄像头读取(取消注释使用)
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||||
import cv2
|
|
||||||
import orisys
|
|
||||||
import numpy as np
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
|
|
||||||
|
import orisys
|
||||||
|
|
||||||
|
|
||||||
def create_plot():
|
def create_plot():
|
||||||
p1 = win.addPlot(title="法相力")
|
p1 = win.addPlot(title="法相力")
|
||||||
p1.setLabel('bottom', '时间', units='s')
|
p1.setLabel('bottom', '时间', units='s')
|
||||||
@@ -149,7 +152,7 @@ def main():
|
|||||||
print("\n 停止程序")
|
print("\n 停止程序")
|
||||||
break
|
break
|
||||||
if key == ord("s"):
|
if key == ord("s"):
|
||||||
save_path = f"./data/pulse_test.mp4"
|
save_path = "./data/pulse_test.mp4"
|
||||||
# Ensure directory exists
|
# Ensure directory exists
|
||||||
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
||||||
# Get frame dimensions from the actual frame
|
# Get frame dimensions from the actual frame
|
||||||
|
|||||||
+16
-9
@@ -46,7 +46,7 @@ import os
|
|||||||
import shlex
|
import shlex
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Union
|
from typing import Union
|
||||||
|
|
||||||
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
|
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
|
||||||
|
|
||||||
@@ -60,13 +60,14 @@ except ImportError as exc: # pragma: no cover
|
|||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
|
|
||||||
from orisys.capture import is_live_camera, list_cameras
|
from orisys.capture import is_live_camera, list_cameras
|
||||||
from orisys.finger import FingerRuntime
|
from orisys.finger import FingerRuntime
|
||||||
|
|
||||||
Source = Union[int, str]
|
Source = Union[int, str]
|
||||||
|
|
||||||
|
|
||||||
def _parse_source(token: str) -> Optional[Source]:
|
def _parse_source(token: str) -> Source | None:
|
||||||
"""Accept camera index int, numeric string, V4L path, or filesystem path."""
|
"""Accept camera index int, numeric string, V4L path, or filesystem path."""
|
||||||
text = str(token).strip().strip('"').strip("'")
|
text = str(token).strip().strip('"').strip("'")
|
||||||
if not text:
|
if not text:
|
||||||
@@ -116,9 +117,9 @@ class UartFingerController:
|
|||||||
self.show = show
|
self.show = show
|
||||||
self.verbose = verbose
|
self.verbose = verbose
|
||||||
|
|
||||||
self.runtime: Optional[FingerRuntime] = None
|
self.runtime: FingerRuntime | None = None
|
||||||
self._connected_source: Optional[Source] = None
|
self._connected_source: Source | None = None
|
||||||
self._last_search_source: Optional[Source] = None
|
self._last_search_source: Source | None = None
|
||||||
self._streaming = False
|
self._streaming = False
|
||||||
self._should_quit = False
|
self._should_quit = False
|
||||||
|
|
||||||
@@ -134,14 +135,18 @@ class UartFingerController:
|
|||||||
if self.runtime is not None:
|
if self.runtime is not None:
|
||||||
try:
|
try:
|
||||||
self.runtime.close()
|
self.runtime.close()
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001, S110 - teardown must not raise
|
||||||
|
# Best-effort release: a close failure here has no recovery path in
|
||||||
|
# a shutdown request handler, and raising would skip the reset below.
|
||||||
pass
|
pass
|
||||||
self.runtime = None
|
self.runtime = None
|
||||||
self._connected_source = None
|
self._connected_source = None
|
||||||
if self.show:
|
if self.show:
|
||||||
try:
|
try:
|
||||||
cv2.destroyAllWindows()
|
cv2.destroyAllWindows()
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001, S110 - no GUI window to destroy
|
||||||
|
# Expected when no window was ever created (headless/--no-show run):
|
||||||
|
# this is a normal no-op, not a hidden failure.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _resolve_connect_source(self, args: list[str]) -> Source:
|
def _resolve_connect_source(self, args: list[str]) -> Source:
|
||||||
@@ -220,7 +225,7 @@ class UartFingerController:
|
|||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _poll_command_line(self, *, blocking: bool = True) -> Optional[str]:
|
def _poll_command_line(self, *, blocking: bool = True) -> str | None:
|
||||||
"""Read one command line if available; return None on timeout/empty."""
|
"""Read one command line if available; return None on timeout/empty."""
|
||||||
if not blocking and self.ser.in_waiting <= 0:
|
if not blocking and self.ser.in_waiting <= 0:
|
||||||
return None
|
return None
|
||||||
@@ -447,7 +452,9 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
print("\nInterrupted.", flush=True)
|
print("\nInterrupted.", flush=True)
|
||||||
try:
|
try:
|
||||||
controller.reply("OK quit")
|
controller.reply("OK quit")
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001, S110 - peer may already be gone
|
||||||
|
# We are already shutting down on Ctrl-C; failing to send the final
|
||||||
|
# acknowledgement (peer closed the port) must not mask the exit path.
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
controller.close_runtime()
|
controller.close_runtime()
|
||||||
|
|||||||
@@ -36,10 +36,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import csv
|
import csv
|
||||||
import statistics
|
|
||||||
import sys
|
import sys
|
||||||
|
from collections.abc import Sequence
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Sequence
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -79,7 +78,7 @@ def _window(
|
|||||||
valid = _column(rows, VALID_COLUMN)
|
valid = _column(rows, VALID_COLUMN)
|
||||||
mask = np.ones(times.shape, dtype=bool)
|
mask = np.ones(times.shape, dtype=bool)
|
||||||
if times.size:
|
if times.size:
|
||||||
mask &= times == times # drop NaN timestamps
|
mask &= ~np.isnan(times) # drop NaN timestamps (unparsable TIME cells)
|
||||||
if start is not None:
|
if start is not None:
|
||||||
mask &= times >= float(start)
|
mask &= times >= float(start)
|
||||||
if stop is not None:
|
if stop is not None:
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||||
import cv2
|
|
||||||
import orisys
|
|
||||||
import numpy as np
|
|
||||||
import time
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import time
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from PyQt5.QtGui import QImage, QFont
|
from PyQt5.QtGui import QFont, QImage
|
||||||
from pyqtgraph.Qt import QtGui
|
from pyqtgraph.Qt import QtGui
|
||||||
|
|
||||||
|
import orisys
|
||||||
|
|
||||||
|
|
||||||
def make_gradient_brush(color_hex, max_alpha=150):
|
def make_gradient_brush(color_hex, max_alpha=150):
|
||||||
"""
|
"""
|
||||||
创建一个从上到下渐变的笔刷 (兼容 PyQt5 和 PyQt6)
|
创建一个从上到下渐变的笔刷 (兼容 PyQt5 和 PyQt6)
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ Historical import path ``from orisys.Stitcher import Stitcher`` remains supporte
|
|||||||
|
|
||||||
from .stitch import Stitcher
|
from .stitch import Stitcher
|
||||||
from .stitch.backends.numpy_backend import blend_kernel_numpy as _blend_kernel_numpy
|
from .stitch.backends.numpy_backend import blend_kernel_numpy as _blend_kernel_numpy
|
||||||
from .stitch.config import BLEND_MODES as _BLEND_MODES
|
from .stitch.config import numba_blend_available
|
||||||
from .stitch.config import DEFAULT_BLEND_MODE as _DEFAULT_BLEND_MODE
|
|
||||||
from .stitch.config import numba_blend_available, resolve_blend_mode as _resolve_blend_mode
|
|
||||||
|
|
||||||
# Backward-compatible aliases used by older call sites / tests.
|
# Backward-compatible aliases used by older call sites / tests.
|
||||||
_blend_kernel = _blend_kernel_numpy
|
_blend_kernel = _blend_kernel_numpy
|
||||||
|
|||||||
@@ -8,16 +8,11 @@ including image stitching, optical flow tracking, and deformation decomposition.
|
|||||||
__version__ = "0.5.3"
|
__version__ = "0.5.3"
|
||||||
__author__ = "Orisys"
|
__author__ = "Orisys"
|
||||||
|
|
||||||
from .sensor import Sensor
|
from . import calibration, capture, configs, finger, signal_processing, util
|
||||||
from . import util
|
|
||||||
from . import configs
|
|
||||||
from . import calibration
|
|
||||||
from . import finger
|
|
||||||
from . import signal_processing
|
|
||||||
from . import capture
|
|
||||||
from .pipeline import FrameSnapshot, PipelineStats, StreamPipeline
|
|
||||||
from .export import StreamRecorder, mosaic_videos
|
from .export import StreamRecorder, mosaic_videos
|
||||||
from .frame_processors import FFCFrameProcessor
|
from .frame_processors import FFCFrameProcessor
|
||||||
|
from .pipeline import FrameSnapshot, PipelineStats, StreamPipeline
|
||||||
|
from .sensor import Sensor
|
||||||
|
|
||||||
CompoundEyeSensor = Sensor
|
CompoundEyeSensor = Sensor
|
||||||
CameraSensor = Sensor
|
CameraSensor = Sensor
|
||||||
|
|||||||
@@ -63,8 +63,8 @@ except ImportError:
|
|||||||
try:
|
try:
|
||||||
import pyopencl as cl
|
import pyopencl as cl
|
||||||
import pyopencl.array as cla
|
import pyopencl.array as cla
|
||||||
from pyvkfft.fft import rfftn as cl_rfftn
|
|
||||||
from pyvkfft.fft import irfftn as cl_irfftn
|
from pyvkfft.fft import irfftn as cl_irfftn
|
||||||
|
from pyvkfft.fft import rfftn as cl_rfftn
|
||||||
_opencl_cl = cl
|
_opencl_cl = cl
|
||||||
_opencl_cla = cla
|
_opencl_cla = cla
|
||||||
_opencl_rfftn = cl_rfftn
|
_opencl_rfftn = cl_rfftn
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import warnings
|
import warnings
|
||||||
|
from collections.abc import Iterable, Mapping
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from dataclasses import asdict, dataclass, field
|
from dataclasses import asdict, dataclass, field
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Iterable, Mapping
|
from typing import Any
|
||||||
|
|
||||||
CURRENT_SCHEMA = 2
|
CURRENT_SCHEMA = 2
|
||||||
SUPPORTED_SCHEMAS = (1, 2)
|
SUPPORTED_SCHEMAS = (1, 2)
|
||||||
|
|||||||
@@ -25,21 +25,21 @@ from .source import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"MAX_SIMULTANEOUS_CAPTURES",
|
|
||||||
"DEFAULT_MAX_CAMERA_INDEX",
|
|
||||||
"DEFAULT_LAPTOP_WEBCAM_PATTERNS",
|
"DEFAULT_LAPTOP_WEBCAM_PATTERNS",
|
||||||
|
"DEFAULT_MAX_CAMERA_INDEX",
|
||||||
|
"MAX_SIMULTANEOUS_CAPTURES",
|
||||||
|
"CameraFilter",
|
||||||
|
"CameraInfo",
|
||||||
"CaptureConfig",
|
"CaptureConfig",
|
||||||
"CaptureHealth",
|
"CaptureHealth",
|
||||||
"CaptureStatus",
|
|
||||||
"ReconnectPolicy",
|
|
||||||
"CaptureSource",
|
"CaptureSource",
|
||||||
|
"CaptureStatus",
|
||||||
"CaptureWorker",
|
"CaptureWorker",
|
||||||
"CameraInfo",
|
"ReconnectPolicy",
|
||||||
"CameraFilter",
|
|
||||||
"list_cameras",
|
|
||||||
"pick_default_camera",
|
|
||||||
"normalize_source",
|
|
||||||
"is_live_camera",
|
"is_live_camera",
|
||||||
"open_opencv_capture",
|
"list_cameras",
|
||||||
|
"normalize_source",
|
||||||
"open_capture",
|
"open_capture",
|
||||||
|
"open_opencv_capture",
|
||||||
|
"pick_default_camera",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Optional, Sequence, Tuple, Union
|
from typing import Any, Union
|
||||||
|
|
||||||
MAX_SIMULTANEOUS_CAPTURES = 10
|
MAX_SIMULTANEOUS_CAPTURES = 10
|
||||||
|
|
||||||
@@ -44,11 +45,11 @@ class ReconnectPolicy:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
enabled: bool = False
|
enabled: bool = False
|
||||||
max_attempts: Optional[int] = None
|
max_attempts: int | None = None
|
||||||
initial_backoff_s: float = 0.25
|
initial_backoff_s: float = 0.25
|
||||||
max_backoff_s: float = 5.0
|
max_backoff_s: float = 5.0
|
||||||
fail_threshold: int = 5
|
fail_threshold: int = 5
|
||||||
stale_timeout_s: Optional[float] = 2.0
|
stale_timeout_s: float | None = 2.0
|
||||||
|
|
||||||
def next_backoff(self, attempt: int) -> float:
|
def next_backoff(self, attempt: int) -> float:
|
||||||
"""Exponential backoff capped at ``max_backoff_s`` (attempt is 0-based)."""
|
"""Exponential backoff capped at ``max_backoff_s`` (attempt is 0-based)."""
|
||||||
@@ -62,19 +63,19 @@ class CaptureConfig:
|
|||||||
|
|
||||||
source: SourceType
|
source: SourceType
|
||||||
backend: str = "auto"
|
backend: str = "auto"
|
||||||
width: Optional[int] = None
|
width: int | None = None
|
||||||
height: Optional[int] = None
|
height: int | None = None
|
||||||
fps: Optional[float] = None
|
fps: float | None = None
|
||||||
fourcc: Optional[str] = "MJPG"
|
fourcc: str | None = "MJPG"
|
||||||
background: bool = True
|
background: bool = True
|
||||||
reconnect: ReconnectPolicy = field(default_factory=ReconnectPolicy)
|
reconnect: ReconnectPolicy = field(default_factory=ReconnectPolicy)
|
||||||
verbose: bool = False
|
verbose: bool = False
|
||||||
# Optional discovery filters (used by list/open helpers, not open itself).
|
# Optional discovery filters (used by list/open helpers, not open itself).
|
||||||
name_include: Optional[Sequence[str]] = None
|
name_include: Sequence[str] | None = None
|
||||||
name_exclude: Optional[Sequence[str]] = None
|
name_exclude: Sequence[str] | None = None
|
||||||
vid_pid_allowlist: Optional[Sequence[Tuple[int, int]]] = None
|
vid_pid_allowlist: Sequence[tuple[int, int]] | None = None
|
||||||
min_width: Optional[int] = None
|
min_width: int | None = None
|
||||||
min_height: Optional[int] = None
|
min_height: int | None = None
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if not isinstance(self.reconnect, ReconnectPolicy):
|
if not isinstance(self.reconnect, ReconnectPolicy):
|
||||||
@@ -86,10 +87,10 @@ class CaptureConfig:
|
|||||||
source: SourceType,
|
source: SourceType,
|
||||||
camera: dict[str, Any],
|
camera: dict[str, Any],
|
||||||
*,
|
*,
|
||||||
reconnect: Optional[ReconnectPolicy] = None,
|
reconnect: ReconnectPolicy | None = None,
|
||||||
verbose: bool = False,
|
verbose: bool = False,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> "CaptureConfig":
|
) -> CaptureConfig:
|
||||||
"""Build config from a sensor JSON ``camera`` section."""
|
"""Build config from a sensor JSON ``camera`` section."""
|
||||||
# Accept deprecated queue/worker kwargs from older callers without error.
|
# Accept deprecated queue/worker kwargs from older callers without error.
|
||||||
kwargs.pop("mode", None)
|
kwargs.pop("mode", None)
|
||||||
@@ -117,9 +118,9 @@ class CaptureStatus:
|
|||||||
"""Snapshot of capture health for diagnostics / multi-camera UIs."""
|
"""Snapshot of capture health for diagnostics / multi-camera UIs."""
|
||||||
|
|
||||||
health: CaptureHealth = CaptureHealth.CLOSED
|
health: CaptureHealth = CaptureHealth.CLOSED
|
||||||
source: Optional[SourceType] = None
|
source: SourceType | None = None
|
||||||
last_error: Optional[str] = None
|
last_error: str | None = None
|
||||||
last_frame_time: Optional[float] = None
|
last_frame_time: float | None = None
|
||||||
last_seq: int = 0
|
last_seq: int = 0
|
||||||
dropped_frames: int = 0
|
dropped_frames: int = 0
|
||||||
reconnect_attempts: int = 0
|
reconnect_attempts: int = 0
|
||||||
|
|||||||
@@ -3,21 +3,24 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
from collections.abc import Iterable, Sequence
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Iterable, Optional, Sequence, Tuple
|
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
|
|
||||||
from .config import MAX_SIMULTANEOUS_CAPTURES
|
from .config import MAX_SIMULTANEOUS_CAPTURES
|
||||||
from .source import _backend_candidates
|
from .source import _backend_candidates
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_MAX_CAMERA_INDEX = MAX_SIMULTANEOUS_CAPTURES
|
DEFAULT_MAX_CAMERA_INDEX = MAX_SIMULTANEOUS_CAPTURES
|
||||||
|
|
||||||
# Common built-in laptop webcam name fragments (case-insensitive).
|
# Common built-in laptop webcam name fragments (case-insensitive).
|
||||||
DEFAULT_LAPTOP_WEBCAM_PATTERNS: Tuple[str, ...] = (
|
DEFAULT_LAPTOP_WEBCAM_PATTERNS: tuple[str, ...] = (
|
||||||
r"integrated",
|
r"integrated",
|
||||||
r"built[- ]?in",
|
r"built[- ]?in",
|
||||||
r"laptop",
|
r"laptop",
|
||||||
@@ -49,8 +52,8 @@ class CameraInfo:
|
|||||||
height: int
|
height: int
|
||||||
name: str = ""
|
name: str = ""
|
||||||
path: str = ""
|
path: str = ""
|
||||||
vendor_id: Optional[int] = None
|
vendor_id: int | None = None
|
||||||
product_id: Optional[int] = None
|
product_id: int | None = None
|
||||||
bus: str = ""
|
bus: str = ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -64,7 +67,7 @@ class CameraInfo:
|
|||||||
return f"{self.index} ({self.width}x{self.height}) [{self.backend_name}]"
|
return f"{self.index} ({self.width}x{self.height}) [{self.backend_name}]"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def vid_pid(self) -> Optional[Tuple[int, int]]:
|
def vid_pid(self) -> tuple[int, int] | None:
|
||||||
if self.vendor_id is None or self.product_id is None:
|
if self.vendor_id is None or self.product_id is None:
|
||||||
return None
|
return None
|
||||||
return (int(self.vendor_id), int(self.product_id))
|
return (int(self.vendor_id), int(self.product_id))
|
||||||
@@ -74,13 +77,13 @@ class CameraInfo:
|
|||||||
class CameraFilter:
|
class CameraFilter:
|
||||||
"""Optional filters for ``list_cameras`` / ``pick_default_camera``."""
|
"""Optional filters for ``list_cameras`` / ``pick_default_camera``."""
|
||||||
|
|
||||||
name_include: Optional[Sequence[str]] = None
|
name_include: Sequence[str] | None = None
|
||||||
name_exclude: Optional[Sequence[str]] = None
|
name_exclude: Sequence[str] | None = None
|
||||||
exclude_laptop_webcams: bool = False
|
exclude_laptop_webcams: bool = False
|
||||||
laptop_patterns: Sequence[str] = field(default_factory=lambda: DEFAULT_LAPTOP_WEBCAM_PATTERNS)
|
laptop_patterns: Sequence[str] = field(default_factory=lambda: DEFAULT_LAPTOP_WEBCAM_PATTERNS)
|
||||||
vid_pid_allowlist: Optional[Sequence[Tuple[int, int]]] = None
|
vid_pid_allowlist: Sequence[tuple[int, int]] | None = None
|
||||||
min_width: Optional[int] = None
|
min_width: int | None = None
|
||||||
min_height: Optional[int] = None
|
min_height: int | None = None
|
||||||
prefer_named: bool = True
|
prefer_named: bool = True
|
||||||
|
|
||||||
def matches(self, cam: CameraInfo) -> bool:
|
def matches(self, cam: CameraInfo) -> bool:
|
||||||
@@ -143,8 +146,15 @@ def _suppress_opencv_noise():
|
|||||||
if set_level is not None and prev_level is not None:
|
if set_level is not None and prev_level is not None:
|
||||||
try:
|
try:
|
||||||
set_level(prev_level)
|
set_level(prev_level)
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
pass
|
# Restoring OpenCV's log level is best-effort, but a silent failure
|
||||||
|
# here leaves probing noise suppressed for the rest of the process,
|
||||||
|
# which hides exactly the diagnostics a failed enumeration needs.
|
||||||
|
logger.warning(
|
||||||
|
"capture discovery: could not restore OpenCV log level %r: %s",
|
||||||
|
prev_level,
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _linux_device_metadata(index: int) -> dict:
|
def _linux_device_metadata(index: int) -> dict:
|
||||||
@@ -152,7 +162,7 @@ def _linux_device_metadata(index: int) -> dict:
|
|||||||
meta = {"name": "", "path": f"/dev/video{index}", "vendor_id": None, "product_id": None, "bus": ""}
|
meta = {"name": "", "path": f"/dev/video{index}", "vendor_id": None, "product_id": None, "bus": ""}
|
||||||
name_path = f"/sys/class/video4linux/video{index}/name"
|
name_path = f"/sys/class/video4linux/video{index}/name"
|
||||||
try:
|
try:
|
||||||
with open(name_path, "r", encoding="utf-8", errors="ignore") as fh:
|
with open(name_path, encoding="utf-8", errors="ignore") as fh:
|
||||||
meta["name"] = fh.read().strip()
|
meta["name"] = fh.read().strip()
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
@@ -165,9 +175,9 @@ def _linux_device_metadata(index: int) -> dict:
|
|||||||
vend = os.path.join(cur, "idVendor")
|
vend = os.path.join(cur, "idVendor")
|
||||||
prod = os.path.join(cur, "idProduct")
|
prod = os.path.join(cur, "idProduct")
|
||||||
if os.path.isfile(vend) and os.path.isfile(prod):
|
if os.path.isfile(vend) and os.path.isfile(prod):
|
||||||
with open(vend, "r", encoding="utf-8") as fh:
|
with open(vend, encoding="utf-8") as fh:
|
||||||
meta["vendor_id"] = int(fh.read().strip(), 16)
|
meta["vendor_id"] = int(fh.read().strip(), 16)
|
||||||
with open(prod, "r", encoding="utf-8") as fh:
|
with open(prod, encoding="utf-8") as fh:
|
||||||
meta["product_id"] = int(fh.read().strip(), 16)
|
meta["product_id"] = int(fh.read().strip(), 16)
|
||||||
meta["bus"] = "usb"
|
meta["bus"] = "usb"
|
||||||
break
|
break
|
||||||
@@ -229,7 +239,7 @@ def _windows_device_names() -> dict[int, dict]:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def _device_metadata(index: int, windows_names: Optional[dict[int, dict]] = None) -> dict:
|
def _device_metadata(index: int, windows_names: dict[int, dict] | None = None) -> dict:
|
||||||
if sys.platform.startswith("linux"):
|
if sys.platform.startswith("linux"):
|
||||||
return _linux_device_metadata(index)
|
return _linux_device_metadata(index)
|
||||||
if sys.platform == "win32" and windows_names is not None:
|
if sys.platform == "win32" and windows_names is not None:
|
||||||
@@ -238,7 +248,7 @@ def _device_metadata(index: int, windows_names: Optional[dict[int, dict]] = None
|
|||||||
return {"name": "", "path": "", "vendor_id": None, "product_id": None, "bus": ""}
|
return {"name": "", "path": "", "vendor_id": None, "product_id": None, "bus": ""}
|
||||||
|
|
||||||
|
|
||||||
def _try_open(index: int, backend_name: str, backend_api: int, meta: dict) -> Optional[CameraInfo]:
|
def _try_open(index: int, backend_name: str, backend_api: int, meta: dict) -> CameraInfo | None:
|
||||||
cap = cv2.VideoCapture(index, backend_api)
|
cap = cv2.VideoCapture(index, backend_api)
|
||||||
if not cap.isOpened():
|
if not cap.isOpened():
|
||||||
cap.release()
|
cap.release()
|
||||||
@@ -272,13 +282,13 @@ def _try_open(index: int, backend_name: str, backend_api: int, meta: dict) -> Op
|
|||||||
def list_cameras(
|
def list_cameras(
|
||||||
max_index: int = DEFAULT_MAX_CAMERA_INDEX,
|
max_index: int = DEFAULT_MAX_CAMERA_INDEX,
|
||||||
*,
|
*,
|
||||||
camera_filter: Optional[CameraFilter] = None,
|
camera_filter: CameraFilter | None = None,
|
||||||
exclude_laptop_webcams: bool = False,
|
exclude_laptop_webcams: bool = False,
|
||||||
name_include: Optional[Sequence[str]] = None,
|
name_include: Sequence[str] | None = None,
|
||||||
name_exclude: Optional[Sequence[str]] = None,
|
name_exclude: Sequence[str] | None = None,
|
||||||
vid_pid_allowlist: Optional[Sequence[Tuple[int, int]]] = None,
|
vid_pid_allowlist: Sequence[tuple[int, int]] | None = None,
|
||||||
min_width: Optional[int] = None,
|
min_width: int | None = None,
|
||||||
min_height: Optional[int] = None,
|
min_height: int | None = None,
|
||||||
backend: str = "auto",
|
backend: str = "auto",
|
||||||
) -> list[CameraInfo]:
|
) -> list[CameraInfo]:
|
||||||
"""Probe indices ``0 .. max_index`` and return working cameras.
|
"""Probe indices ``0 .. max_index`` and return working cameras.
|
||||||
@@ -302,10 +312,22 @@ def list_cameras(
|
|||||||
windows_names = _windows_device_names() if sys.platform == "win32" else None
|
windows_names = _windows_device_names() if sys.platform == "win32" else None
|
||||||
|
|
||||||
with _suppress_opencv_noise():
|
with _suppress_opencv_noise():
|
||||||
for index in range(0, max_index + 1):
|
for index in range(max_index + 1):
|
||||||
meta = _device_metadata(index, windows_names)
|
meta = _device_metadata(index, windows_names)
|
||||||
for backend_name, backend_api in backends:
|
for backend_name, backend_api in backends:
|
||||||
info = _try_open(index, backend_name, backend_api, meta)
|
try:
|
||||||
|
info = _try_open(index, backend_name, backend_api, meta)
|
||||||
|
except Exception as exc:
|
||||||
|
# One bad backend/device must not abort the whole probe: an
|
||||||
|
# escaping error here made every *other* camera invisible, not
|
||||||
|
# just this index. Skip the candidate and keep enumerating.
|
||||||
|
logger.warning(
|
||||||
|
"capture discovery: probing index %d via %s failed: %s",
|
||||||
|
index,
|
||||||
|
backend_name,
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
continue
|
||||||
if info is None:
|
if info is None:
|
||||||
continue
|
continue
|
||||||
if index in seen_indices:
|
if index in seen_indices:
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import Any, Optional, Protocol, runtime_checkable
|
from typing import Any, Protocol, runtime_checkable
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -19,6 +20,8 @@ from .config import (
|
|||||||
SourceType,
|
SourceType,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
class VideoCaptureLike(Protocol):
|
class VideoCaptureLike(Protocol):
|
||||||
@@ -83,7 +86,7 @@ def _backend_candidates(preferred: str = "auto") -> list[tuple[str, int]]:
|
|||||||
return candidates
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
def _fourcc_code(fourcc: Optional[str]) -> Optional[int]:
|
def _fourcc_code(fourcc: str | None) -> int | None:
|
||||||
if not fourcc:
|
if not fourcc:
|
||||||
return None
|
return None
|
||||||
text = str(fourcc).strip()
|
text = str(fourcc).strip()
|
||||||
@@ -111,10 +114,10 @@ def _read_stream_meta(cap: VideoCaptureLike) -> tuple[int, int, float, str]:
|
|||||||
def _apply_stream_props(
|
def _apply_stream_props(
|
||||||
cap: VideoCaptureLike,
|
cap: VideoCaptureLike,
|
||||||
*,
|
*,
|
||||||
width: Optional[int],
|
width: int | None,
|
||||||
height: Optional[int],
|
height: int | None,
|
||||||
fps: Optional[float],
|
fps: float | None,
|
||||||
fourcc: Optional[str],
|
fourcc: str | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Apply live-camera stream properties (width/height before fourcc on DSHOW)."""
|
"""Apply live-camera stream properties (width/height before fourcc on DSHOW)."""
|
||||||
code = _fourcc_code(fourcc)
|
code = _fourcc_code(fourcc)
|
||||||
@@ -143,10 +146,10 @@ def open_opencv_capture(
|
|||||||
source: SourceType,
|
source: SourceType,
|
||||||
*,
|
*,
|
||||||
backend: str = "auto",
|
backend: str = "auto",
|
||||||
width: Optional[int] = None,
|
width: int | None = None,
|
||||||
height: Optional[int] = None,
|
height: int | None = None,
|
||||||
fps: Optional[float] = None,
|
fps: float | None = None,
|
||||||
fourcc: Optional[str] = "MJPG",
|
fourcc: str | None = "MJPG",
|
||||||
apply_stream_props: bool = True,
|
apply_stream_props: bool = True,
|
||||||
) -> tuple[VideoCaptureLike, str]:
|
) -> tuple[VideoCaptureLike, str]:
|
||||||
"""Open a ``cv2.VideoCapture`` trying preferred backends in order.
|
"""Open a ``cv2.VideoCapture`` trying preferred backends in order.
|
||||||
@@ -212,16 +215,16 @@ class CaptureSource:
|
|||||||
):
|
):
|
||||||
self.config = config
|
self.config = config
|
||||||
self._factory = factory or self._default_factory
|
self._factory = factory or self._default_factory
|
||||||
self._cap: Optional[VideoCaptureLike] = None
|
self._cap: VideoCaptureLike | None = None
|
||||||
self._backend_name = ""
|
self._backend_name = ""
|
||||||
self._status = CaptureStatus(source=normalize_source(config.source))
|
self._status = CaptureStatus(source=normalize_source(config.source))
|
||||||
self._reconnect_attempt = 0
|
self._reconnect_attempt = 0
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
self._frame_lock = threading.Lock()
|
self._frame_lock = threading.Lock()
|
||||||
self._frame_cond = threading.Condition(self._frame_lock)
|
self._frame_cond = threading.Condition(self._frame_lock)
|
||||||
self._latest_frame: Optional[np.ndarray] = None
|
self._latest_frame: np.ndarray | None = None
|
||||||
self._latest_seq = 0
|
self._latest_seq = 0
|
||||||
self._thread: Optional[threading.Thread] = None
|
self._thread: threading.Thread | None = None
|
||||||
self._background_running = False
|
self._background_running = False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -238,7 +241,7 @@ class CaptureSource:
|
|||||||
|
|
||||||
# Compatibility with older CaptureWorker API.
|
# Compatibility with older CaptureWorker API.
|
||||||
@property
|
@property
|
||||||
def source(self) -> "CaptureSource":
|
def source(self) -> CaptureSource:
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -265,7 +268,7 @@ class CaptureSource:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cap(self) -> Optional[VideoCaptureLike]:
|
def cap(self) -> VideoCaptureLike | None:
|
||||||
"""Raw capture handle (compat with Sensor.cap)."""
|
"""Raw capture handle (compat with Sensor.cap)."""
|
||||||
return self._cap
|
return self._cap
|
||||||
|
|
||||||
@@ -324,8 +327,14 @@ class CaptureSource:
|
|||||||
if self._cap is not None:
|
if self._cap is not None:
|
||||||
try:
|
try:
|
||||||
self._cap.release()
|
self._cap.release()
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
pass
|
# A failed release is not fatal, but it must not be invisible: the
|
||||||
|
# handle below is dropped either way, so a live-camera handle that
|
||||||
|
# OpenCV refused to free would otherwise leak silently until the
|
||||||
|
# process exits. Report it and keep tearing down.
|
||||||
|
logger.warning(
|
||||||
|
"capture: %r.release() failed: %s", self._backend_name or "capture", exc
|
||||||
|
)
|
||||||
self._cap = None
|
self._cap = None
|
||||||
with self._frame_cond:
|
with self._frame_cond:
|
||||||
self._latest_frame = None
|
self._latest_frame = None
|
||||||
@@ -341,8 +350,11 @@ class CaptureSource:
|
|||||||
self._status.height = height
|
self._status.height = height
|
||||||
self._status.fps = fps
|
self._status.fps = fps
|
||||||
self._status.fourcc = fourcc
|
self._status.fourcc = fourcc
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
pass
|
# Metadata is advisory: a backend that cannot answer these get() calls
|
||||||
|
# still delivers frames, so do not fail the open. Record why the
|
||||||
|
# reported stream geometry is left at its previous/default values.
|
||||||
|
logger.warning("capture: could not read stream metadata: %s", exc)
|
||||||
|
|
||||||
def warmup_frames(self, count: int = 3) -> None:
|
def warmup_frames(self, count: int = 3) -> None:
|
||||||
"""Discard frames so the driver/format stabilizes (sync reads only)."""
|
"""Discard frames so the driver/format stabilizes (sync reads only)."""
|
||||||
@@ -351,7 +363,7 @@ class CaptureSource:
|
|||||||
for _ in range(max(0, int(count))):
|
for _ in range(max(0, int(count))):
|
||||||
self.read_raw()
|
self.read_raw()
|
||||||
|
|
||||||
def read_raw(self) -> tuple[bool, Optional[np.ndarray]]:
|
def read_raw(self) -> tuple[bool, np.ndarray | None]:
|
||||||
"""Read one frame directly from the underlying capture (no reconnect)."""
|
"""Read one frame directly from the underlying capture (no reconnect)."""
|
||||||
cap = self._cap
|
cap = self._cap
|
||||||
if cap is None:
|
if cap is None:
|
||||||
@@ -379,7 +391,7 @@ class CaptureSource:
|
|||||||
*,
|
*,
|
||||||
copy: bool = True,
|
copy: bool = True,
|
||||||
wait_s: float = 0.0,
|
wait_s: float = 0.0,
|
||||||
) -> tuple[bool, Optional[np.ndarray]]:
|
) -> tuple[bool, np.ndarray | None]:
|
||||||
"""Return the newest frame from the background thread, or read sync."""
|
"""Return the newest frame from the background thread, or read sync."""
|
||||||
if not self._background_running:
|
if not self._background_running:
|
||||||
ok, frame = self.read_raw()
|
ok, frame = self.read_raw()
|
||||||
@@ -397,7 +409,7 @@ class CaptureSource:
|
|||||||
frame = self._latest_frame.copy() if copy else self._latest_frame
|
frame = self._latest_frame.copy() if copy else self._latest_frame
|
||||||
return True, frame
|
return True, frame
|
||||||
|
|
||||||
def read(self, *, copy: bool = True, wait_s: float = 1.0) -> tuple[bool, Optional[np.ndarray]]:
|
def read(self, *, copy: bool = True, wait_s: float = 1.0) -> tuple[bool, np.ndarray | None]:
|
||||||
"""Compat alias used by finger runtime and older worker callers."""
|
"""Compat alias used by finger runtime and older worker callers."""
|
||||||
wait = wait_s if wait_s > 0 else 1.0
|
wait = wait_s if wait_s > 0 else 1.0
|
||||||
return self.read_latest(copy=copy, wait_s=wait)
|
return self.read_latest(copy=copy, wait_s=wait)
|
||||||
@@ -531,8 +543,8 @@ def open_capture(
|
|||||||
config: CaptureConfig,
|
config: CaptureConfig,
|
||||||
*,
|
*,
|
||||||
factory=None,
|
factory=None,
|
||||||
start_background: Optional[bool] = None,
|
start_background: bool | None = None,
|
||||||
start_worker: Optional[bool] = None,
|
start_worker: bool | None = None,
|
||||||
) -> CaptureSource:
|
) -> CaptureSource:
|
||||||
"""Open a capture source and optionally start its background thread.
|
"""Open a capture source and optionally start its background thread.
|
||||||
|
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ def _config_files() -> list[Path]:
|
|||||||
|
|
||||||
|
|
||||||
def _load_json(path: Path) -> dict:
|
def _load_json(path: Path) -> dict:
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
with open(path, encoding="utf-8") as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
@@ -433,7 +433,7 @@ def load_config(config_name_or_path):
|
|||||||
for candidate in _config_files():
|
for candidate in _config_files():
|
||||||
try:
|
try:
|
||||||
data = _load_json(candidate)
|
data = _load_json(candidate)
|
||||||
except Exception:
|
except Exception: # noqa: S112 - skip unreadable config during alias scan
|
||||||
continue
|
continue
|
||||||
if data.get("name") in candidates:
|
if data.get("name") in candidates:
|
||||||
return data
|
return data
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ from __future__ import annotations
|
|||||||
import os
|
import os
|
||||||
import queue
|
import queue
|
||||||
import threading
|
import threading
|
||||||
import time
|
from collections.abc import Callable, Iterable, Sequence
|
||||||
from typing import Any, Callable, Iterable, Sequence
|
from typing import Any
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -243,7 +243,7 @@ class StreamRecorder:
|
|||||||
"fourcc": self.fourcc,
|
"fourcc": self.fourcc,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __enter__(self) -> "StreamRecorder":
|
def __enter__(self) -> StreamRecorder:
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc, tb) -> bool:
|
def __exit__(self, exc_type, exc, tb) -> bool:
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ from .mesh import (
|
|||||||
lift_uv_deformation_to_3d,
|
lift_uv_deformation_to_3d,
|
||||||
lift_uv_flow_to_3d,
|
lift_uv_flow_to_3d,
|
||||||
mesh_uv_to_world_scale,
|
mesh_uv_to_world_scale,
|
||||||
scalar_map_vertex_colors,
|
|
||||||
sample_field_at_mesh_vertices,
|
sample_field_at_mesh_vertices,
|
||||||
|
scalar_map_vertex_colors,
|
||||||
)
|
)
|
||||||
from .runtime import (
|
from .runtime import (
|
||||||
FingerGeometry,
|
FingerGeometry,
|
||||||
@@ -39,8 +39,8 @@ __all__ = [
|
|||||||
"FingerGeometry",
|
"FingerGeometry",
|
||||||
"FingerRuntime",
|
"FingerRuntime",
|
||||||
"TactileProcessor",
|
"TactileProcessor",
|
||||||
"UVSurfaceMesh",
|
|
||||||
"UVMeshViewer",
|
"UVMeshViewer",
|
||||||
|
"UVSurfaceMesh",
|
||||||
"build_uv_surface_mesh",
|
"build_uv_surface_mesh",
|
||||||
"compute_uv_surface_frames",
|
"compute_uv_surface_frames",
|
||||||
"deform_mesh_vertices",
|
"deform_mesh_vertices",
|
||||||
|
|||||||
@@ -12,15 +12,15 @@ class UVSurfaceMesh:
|
|||||||
"""Triangle mesh built from a UV flatten grid and world_xyz surface map."""
|
"""Triangle mesh built from a UV flatten grid and world_xyz surface map."""
|
||||||
|
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
"rest_vertices",
|
|
||||||
"triangles",
|
|
||||||
"vertex_ys",
|
|
||||||
"vertex_xs",
|
|
||||||
"index_map",
|
|
||||||
"valid_mask",
|
|
||||||
"shape",
|
|
||||||
"world_xyz",
|
|
||||||
"base_colors",
|
"base_colors",
|
||||||
|
"index_map",
|
||||||
|
"rest_vertices",
|
||||||
|
"shape",
|
||||||
|
"triangles",
|
||||||
|
"valid_mask",
|
||||||
|
"vertex_xs",
|
||||||
|
"vertex_ys",
|
||||||
|
"world_xyz",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ class FingerRuntime:
|
|||||||
mirror=False,
|
mirror=False,
|
||||||
auto_reconnect: bool = False,
|
auto_reconnect: bool = False,
|
||||||
capture_mode: str = "latest",
|
capture_mode: str = "latest",
|
||||||
) -> "FingerRuntime":
|
) -> FingerRuntime:
|
||||||
"""Open a live finger runtime from export bundle, config, and video source."""
|
"""Open a live finger runtime from export bundle, config, and video source."""
|
||||||
del capture_mode # deprecated; latest-frame capture only
|
del capture_mode # deprecated; latest-frame capture only
|
||||||
if camera_id is not None:
|
if camera_id is not None:
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from .unroll import UVRemap
|
from .unroll import UVRemap
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import numpy as np
|
|
||||||
import cv2
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
from .backend_availability import CUPY_AVAILABLE, get_cupy
|
from .backend_availability import CUPY_AVAILABLE, get_cupy
|
||||||
|
|
||||||
cp = get_cupy()
|
cp = get_cupy()
|
||||||
# 缓存形态学操作的核,避免重复创建
|
# 缓存形态学操作的核,避免重复创建
|
||||||
_morphology_kernels = {}
|
_morphology_kernels = {}
|
||||||
|
|||||||
@@ -20,4 +20,4 @@ from .grid import StructuredGrid
|
|||||||
from .poisson import PoissonSolver
|
from .poisson import PoissonSolver
|
||||||
from .timing import Timer
|
from .timing import Timer
|
||||||
|
|
||||||
__all__ = ["nHHD", "StructuredGrid", "PoissonSolver", "Timer"]
|
__all__ = ["PoissonSolver", "StructuredGrid", "Timer", "nHHD"]
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ import numpy
|
|||||||
|
|
||||||
from ..backend_availability import (
|
from ..backend_availability import (
|
||||||
CUPY_AVAILABLE,
|
CUPY_AVAILABLE,
|
||||||
OPENCL_AVAILABLE,
|
|
||||||
KERNEL_AVAILABLE,
|
KERNEL_AVAILABLE,
|
||||||
cp,
|
OPENCL_AVAILABLE,
|
||||||
_torque_kernel,
|
_torque_kernel,
|
||||||
|
cp,
|
||||||
)
|
)
|
||||||
from .grid import StructuredGrid
|
from .grid import StructuredGrid
|
||||||
from .poisson import PoissonSolver
|
from .poisson import PoissonSolver
|
||||||
|
|
||||||
|
|
||||||
class nHHD(object):
|
class nHHD:
|
||||||
|
|
||||||
# constructor
|
# constructor
|
||||||
def __init__(self, cuda=True, **kwargs):
|
def __init__(self, cuda=True, **kwargs):
|
||||||
@@ -25,9 +25,7 @@ class nHHD(object):
|
|||||||
backend = 'cupy' if CUPY_AVAILABLE else ('opencl' if OPENCL_AVAILABLE else 'cpu')
|
backend = 'cupy' if CUPY_AVAILABLE else ('opencl' if OPENCL_AVAILABLE else 'cpu')
|
||||||
|
|
||||||
# Validate backend availability and fallback to 'cpu' if needed
|
# Validate backend availability and fallback to 'cpu' if needed
|
||||||
if backend == 'cupy' and not CUPY_AVAILABLE:
|
if backend == 'cupy' and not CUPY_AVAILABLE or backend == 'opencl' and not OPENCL_AVAILABLE:
|
||||||
backend = 'cpu'
|
|
||||||
elif backend == 'opencl' and not OPENCL_AVAILABLE:
|
|
||||||
backend = 'cpu'
|
backend = 'cpu'
|
||||||
|
|
||||||
self.debug = kwargs.get('debug', False)
|
self.debug = kwargs.get('debug', False)
|
||||||
|
|||||||
+13
-11
@@ -56,7 +56,9 @@ def build_masked_factor(lap):
|
|||||||
options=dict(SymmetricMode=True),
|
options=dict(SymmetricMode=True),
|
||||||
)
|
)
|
||||||
return lu.solve, numpy.float32
|
return lu.solve, numpy.float32
|
||||||
except Exception:
|
except Exception: # noqa: S112 - try the next pivoting strategy
|
||||||
|
# Probe: try the next pivoting strategy; the float64 fallback below is
|
||||||
|
# returned if every candidate fails, so this failure is not lost.
|
||||||
continue
|
continue
|
||||||
# Last resort: the original pivoting LU on the float64 matrix.
|
# Last resort: the original pivoting LU on the float64 matrix.
|
||||||
return sparse_linalg.factorized(lap_csc), numpy.float64
|
return sparse_linalg.factorized(lap_csc), numpy.float64
|
||||||
@@ -82,20 +84,20 @@ class MaskedDomain:
|
|||||||
"""Reduced Laplacian system on active mask pixels (2D only)."""
|
"""Reduced Laplacian system on active mask pixels (2D only)."""
|
||||||
|
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
"shape",
|
|
||||||
"mask",
|
|
||||||
"index_map",
|
|
||||||
"factor",
|
|
||||||
"factor_dtype",
|
|
||||||
"lap",
|
|
||||||
"num_active",
|
|
||||||
"pinned",
|
|
||||||
"active_coords",
|
|
||||||
"active_linear_indices",
|
|
||||||
"_rhs_buffer",
|
"_rhs_buffer",
|
||||||
"_rhs_pair_buffer",
|
"_rhs_pair_buffer",
|
||||||
"_scatter_buffer",
|
"_scatter_buffer",
|
||||||
"_scatter_pair_buffer",
|
"_scatter_pair_buffer",
|
||||||
|
"active_coords",
|
||||||
|
"active_linear_indices",
|
||||||
|
"factor",
|
||||||
|
"factor_dtype",
|
||||||
|
"index_map",
|
||||||
|
"lap",
|
||||||
|
"mask",
|
||||||
|
"num_active",
|
||||||
|
"pinned",
|
||||||
|
"shape",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import numpy
|
|
||||||
import timeit
|
|
||||||
import sys
|
import sys
|
||||||
# import cv2
|
|
||||||
from scipy import signal, spatial
|
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
# import cv2
|
||||||
from ..backend_availability import (
|
from ..backend_availability import (
|
||||||
CUPY_AVAILABLE, OPENCL_AVAILABLE, KERNEL_AVAILABLE,
|
CUPY_AVAILABLE,
|
||||||
cp, cp_signal, _torque_kernel,
|
cp,
|
||||||
cl, cla, cl_rfftn, cl_irfftn
|
|
||||||
)
|
)
|
||||||
from .timing import Timer
|
from .timing import Timer
|
||||||
|
|
||||||
class StructuredGrid(object):
|
|
||||||
|
class StructuredGrid:
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ from .timing import Timer
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
class PoissonSolver(object):
|
class PoissonSolver:
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
args = kwargs.keys()
|
args = kwargs.keys()
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ from .capacitance import CapacitanceFFTSolver
|
|||||||
from .masked_sparse import MaskedSparseSolver
|
from .masked_sparse import MaskedSparseSolver
|
||||||
from .rectangular import RectangularFFTSolver
|
from .rectangular import RectangularFFTSolver
|
||||||
|
|
||||||
__all__ = ["RectangularFFTSolver", "MaskedSparseSolver", "CapacitanceFFTSolver"]
|
__all__ = ["CapacitanceFFTSolver", "MaskedSparseSolver", "RectangularFFTSolver"]
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import numpy
|
|
||||||
import timeit
|
import timeit
|
||||||
|
|
||||||
|
import numpy
|
||||||
from scipy import signal
|
from scipy import signal
|
||||||
|
|
||||||
from ...backend_availability import (
|
from ...backend_availability import (
|
||||||
CUPY_AVAILABLE,
|
CUPY_AVAILABLE,
|
||||||
OPENCL_AVAILABLE,
|
OPENCL_AVAILABLE,
|
||||||
|
cl,
|
||||||
|
cl_irfftn,
|
||||||
|
cl_rfftn,
|
||||||
|
cla,
|
||||||
cp,
|
cp,
|
||||||
cp_signal,
|
cp_signal,
|
||||||
cl,
|
|
||||||
cla,
|
|
||||||
cl_rfftn,
|
|
||||||
cl_irfftn,
|
|
||||||
)
|
)
|
||||||
from ..timing import Timer
|
from ..timing import Timer
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import numpy
|
|
||||||
import timeit
|
import timeit
|
||||||
|
|
||||||
class Timer(object):
|
import numpy
|
||||||
|
|
||||||
|
|
||||||
|
class Timer:
|
||||||
|
|
||||||
def display(self):
|
def display(self):
|
||||||
|
|
||||||
|
|||||||
@@ -66,9 +66,10 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
from collections.abc import Callable, Iterable, Sequence
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from queue import Empty, Full, Queue
|
from queue import Empty, Full, Queue
|
||||||
from typing import Any, Callable, Iterable, Sequence
|
from typing import Any
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -155,7 +156,7 @@ class StreamPipeline:
|
|||||||
contact: bool = False,
|
contact: bool = False,
|
||||||
depth: int = 2,
|
depth: int = 2,
|
||||||
include: Sequence[str] | Iterable[str] = ("image",),
|
include: Sequence[str] | Iterable[str] = ("image",),
|
||||||
on_frame: Callable[["FrameSnapshot"], None] | None = None,
|
on_frame: Callable[[FrameSnapshot], None] | None = None,
|
||||||
verbose: bool = False,
|
verbose: bool = False,
|
||||||
fps_window: int = 30,
|
fps_window: int = 30,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -189,7 +190,7 @@ class StreamPipeline:
|
|||||||
|
|
||||||
# ------------------------------------------------------------------ ctors
|
# ------------------------------------------------------------------ ctors
|
||||||
@classmethod
|
@classmethod
|
||||||
def for_finger_runtime(cls, runtime: Any, **kwargs: Any) -> "StreamPipeline":
|
def for_finger_runtime(cls, runtime: Any, **kwargs: Any) -> StreamPipeline:
|
||||||
"""Build a pipeline for a ``FingerRuntime`` (capture + UV unroll stage)."""
|
"""Build a pipeline for a ``FingerRuntime`` (capture + UV unroll stage)."""
|
||||||
|
|
||||||
def produce() -> np.ndarray | None:
|
def produce() -> np.ndarray | None:
|
||||||
@@ -199,7 +200,7 @@ class StreamPipeline:
|
|||||||
return cls(runtime.tactile.raw_sensor, produce, **kwargs)
|
return cls(runtime.tactile.raw_sensor, produce, **kwargs)
|
||||||
|
|
||||||
# --------------------------------------------------------------- lifecycle
|
# --------------------------------------------------------------- lifecycle
|
||||||
def start(self) -> "StreamPipeline":
|
def start(self) -> StreamPipeline:
|
||||||
if self._running:
|
if self._running:
|
||||||
return self
|
return self
|
||||||
self._running = True
|
self._running = True
|
||||||
@@ -232,7 +233,7 @@ class StreamPipeline:
|
|||||||
err, self._error = self._error, None
|
err, self._error = self._error, None
|
||||||
raise err
|
raise err
|
||||||
|
|
||||||
def __enter__(self) -> "StreamPipeline":
|
def __enter__(self) -> StreamPipeline:
|
||||||
return self.start()
|
return self.start()
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc, tb) -> bool:
|
def __exit__(self, exc_type, exc, tb) -> bool:
|
||||||
|
|||||||
+21
-19
@@ -7,23 +7,19 @@ and Helmholtz-Hodge decomposition.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# import os # only used by commented-out stitch_cal
|
# import os # only used by commented-out stitch_cal
|
||||||
import numpy as np
|
|
||||||
import cv2
|
|
||||||
import time
|
import time
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Tuple
|
|
||||||
# from threading import Thread, Event
|
|
||||||
# from queue import Queue, Empty
|
|
||||||
|
|
||||||
from .Stitcher import Stitcher
|
import cv2
|
||||||
from .tracker import Tracker
|
import numpy as np
|
||||||
from .hhd import nHHD
|
|
||||||
from . import calibration as cal_mod
|
from . import calibration as cal_mod
|
||||||
from . import configs
|
from . import configs
|
||||||
from .get_depth import Contact3D
|
from .backend_availability import check_cupy_available, check_opencl_available
|
||||||
from .backend_availability import (check_cupy_available, check_opencl_available)
|
|
||||||
from .frame_processors import create_frame_processor
|
from .frame_processors import create_frame_processor
|
||||||
|
from .get_depth import Contact3D
|
||||||
|
from .hhd import nHHD
|
||||||
from .signal_processing import (
|
from .signal_processing import (
|
||||||
DEFAULT_SAMPLE_RATE_HZ,
|
DEFAULT_SAMPLE_RATE_HZ,
|
||||||
CausalMedianFilter,
|
CausalMedianFilter,
|
||||||
@@ -31,6 +27,11 @@ from .signal_processing import (
|
|||||||
SampleRateEstimator,
|
SampleRateEstimator,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# from threading import Thread, Event
|
||||||
|
# from queue import Queue, Empty
|
||||||
|
from .Stitcher import Stitcher
|
||||||
|
from .tracker import Tracker
|
||||||
|
|
||||||
# Default signal-conditioning settings (config: top-level "filters" block).
|
# Default signal-conditioning settings (config: top-level "filters" block).
|
||||||
#
|
#
|
||||||
# A key a config does not state is **off**, not "whatever we thought was
|
# A key a config does not state is **off**, not "whatever we thought was
|
||||||
@@ -101,6 +102,7 @@ from .capture import (
|
|||||||
open_capture,
|
open_capture,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Sensor:
|
class Sensor:
|
||||||
"""
|
"""
|
||||||
Optical tactile sensor interface with deformation analysis.
|
Optical tactile sensor interface with deformation analysis.
|
||||||
@@ -137,7 +139,7 @@ class Sensor:
|
|||||||
|
|
||||||
def __init__(self, vid_src, cuda=True, isstitch=True, verbose=True, debug=False,
|
def __init__(self, vid_src, cuda=True, isstitch=True, verbose=True, debug=False,
|
||||||
config_name="prototype", cal_path=None, override=False, box=None, mirror=True,
|
config_name="prototype", cal_path=None, override=False, box=None, mirror=True,
|
||||||
backend='auto', capture_config: Optional[CaptureConfig] = None,
|
backend='auto', capture_config: CaptureConfig | None = None,
|
||||||
auto_reconnect: bool = False, capture_mode: str = "latest",
|
auto_reconnect: bool = False, capture_mode: str = "latest",
|
||||||
capture_queue_size: int = 1, capture_overflow: str = "drop_oldest",
|
capture_queue_size: int = 1, capture_overflow: str = "drop_oldest",
|
||||||
**_deprecated_capture_kwargs):
|
**_deprecated_capture_kwargs):
|
||||||
@@ -215,11 +217,11 @@ class Sensor:
|
|||||||
self.n_boxes = None
|
self.n_boxes = None
|
||||||
self.fnormal = self.fshearx = self.fsheary = self.ftor = 0.0
|
self.fnormal = self.fshearx = self.fsheary = self.ftor = 0.0
|
||||||
self._force_raw = (0.0, 0.0, 0.0)
|
self._force_raw = (0.0, 0.0, 0.0)
|
||||||
self.calibration: Optional[cal_mod.Calibration] = None
|
self.calibration: cal_mod.Calibration | None = None
|
||||||
self._force_cal_sensor_name: Optional[str] = None
|
self._force_cal_sensor_name: str | None = None
|
||||||
#: Why no calibration was applied, when a bundle existed but carried no
|
#: Why no calibration was applied, when a bundle existed but carried no
|
||||||
#: usable coefficients. ``None`` means the question never came up.
|
#: usable coefficients. ``None`` means the question never came up.
|
||||||
self._force_cal_attempts: Optional[list[dict]] = None
|
self._force_cal_attempts: list[dict] | None = None
|
||||||
self._init_force_calibration()
|
self._init_force_calibration()
|
||||||
|
|
||||||
# ---------------------------------------------------------------
|
# ---------------------------------------------------------------
|
||||||
@@ -413,7 +415,7 @@ class Sensor:
|
|||||||
# self._av_stream = None
|
# self._av_stream = None
|
||||||
# self._av_decoder = None
|
# self._av_decoder = None
|
||||||
self.cap = None # compat alias for _capture_worker.source.cap
|
self.cap = None # compat alias for _capture_worker.source.cap
|
||||||
self._capture_source: Optional[CaptureSource] = None
|
self._capture_source: CaptureSource | None = None
|
||||||
self._is_live_camera = (
|
self._is_live_camera = (
|
||||||
is_live_camera(self.vid_src) and not self._external_only
|
is_live_camera(self.vid_src) and not self._external_only
|
||||||
)
|
)
|
||||||
@@ -1013,7 +1015,7 @@ class Sensor:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def read_info(self, *args) -> Tuple:
|
def read_info(self, *args) -> tuple:
|
||||||
"""
|
"""
|
||||||
Get sensor data by info type.
|
Get sensor data by info type.
|
||||||
|
|
||||||
@@ -1323,7 +1325,7 @@ class Sensor:
|
|||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
attempts.append(configs.describe_bundle_resolution(name))
|
attempts.append(configs.describe_bundle_resolution(name))
|
||||||
except Exception: # noqa: BLE001 - diagnostics must never break loading
|
except Exception: # noqa: BLE001, S112 - diagnostics must never break loading
|
||||||
continue
|
continue
|
||||||
# Deliberately NOT setting ``_force_cal_sensor_name``: callers read a
|
# Deliberately NOT setting ``_force_cal_sensor_name``: callers read a
|
||||||
# non-None value there as "this sensor is calibrated", and a bundle
|
# non-None value there as "this sensor is calibrated", and a bundle
|
||||||
@@ -1700,6 +1702,6 @@ class Sensor:
|
|||||||
for k, v in timings.items():
|
for k, v in timings.items():
|
||||||
if k != 'total':
|
if k != 'total':
|
||||||
print(f" {k}: {v:7.2f} ms")
|
print(f" {k}: {v:7.2f} ms")
|
||||||
print(f" ─────────────────────────")
|
print(" ─────────────────────────")
|
||||||
print(f" TOTAL: {timings['total']:7.2f} ms ({1000/timings['total']:.1f} FPS)")
|
print(f" TOTAL: {timings['total']:7.2f} ms ({1000/timings['total']:.1f} FPS)")
|
||||||
|
|
||||||
|
|||||||
@@ -15,19 +15,19 @@ from .ops import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Stitcher",
|
|
||||||
"StitchStatus",
|
|
||||||
"BLEND_MODES",
|
"BLEND_MODES",
|
||||||
"resolve_blend_mode",
|
"StitchStatus",
|
||||||
"validate_box_grid",
|
"Stitcher",
|
||||||
"get_blend_backend",
|
|
||||||
"available_blend_backends",
|
"available_blend_backends",
|
||||||
"tile_weight_map",
|
|
||||||
"paste_tile_clipped",
|
|
||||||
"copy_into_tile_buffer",
|
|
||||||
"build_blend_coef",
|
"build_blend_coef",
|
||||||
"compute_final_crop_rect",
|
"compute_final_crop_rect",
|
||||||
|
"copy_into_tile_buffer",
|
||||||
|
"coverage_ratio",
|
||||||
|
"get_blend_backend",
|
||||||
"largest_fully_covered_rect",
|
"largest_fully_covered_rect",
|
||||||
"match_bgr_brightness",
|
"match_bgr_brightness",
|
||||||
"coverage_ratio",
|
"paste_tile_clipped",
|
||||||
|
"resolve_blend_mode",
|
||||||
|
"tile_weight_map",
|
||||||
|
"validate_box_grid",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
from . import boxes as boxes_mod
|
||||||
|
from . import feature as feature_mod
|
||||||
|
from . import tiles as tiles_mod
|
||||||
from .backends import get_blend_backend
|
from .backends import get_blend_backend
|
||||||
from .config import (
|
from .config import (
|
||||||
BLEND_MODES,
|
BLEND_MODES,
|
||||||
@@ -14,10 +16,7 @@ from .config import (
|
|||||||
numba_blend_available,
|
numba_blend_available,
|
||||||
resolve_blend_mode,
|
resolve_blend_mode,
|
||||||
)
|
)
|
||||||
from . import boxes as boxes_mod
|
|
||||||
from . import feature as feature_mod
|
|
||||||
from .ops import build_blend_coef
|
from .ops import build_blend_coef
|
||||||
from . import tiles as tiles_mod
|
|
||||||
|
|
||||||
|
|
||||||
class Stitcher:
|
class Stitcher:
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Callable, Optional
|
from collections.abc import Callable
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from ..config import numba_blend_available
|
||||||
from .base import BlendBackend
|
from .base import BlendBackend
|
||||||
from .numba_backend import NumbaBlendBackend
|
from .numba_backend import NumbaBlendBackend
|
||||||
from .numpy_backend import NumpyBlendBackend
|
from .numpy_backend import NumpyBlendBackend
|
||||||
from ..config import numba_blend_available
|
|
||||||
|
|
||||||
_BACKENDS = {
|
_BACKENDS = {
|
||||||
"numpy": NumpyBlendBackend,
|
"numpy": NumpyBlendBackend,
|
||||||
@@ -18,7 +19,7 @@ _BACKENDS = {
|
|||||||
def get_blend_backend(
|
def get_blend_backend(
|
||||||
name: str,
|
name: str,
|
||||||
*,
|
*,
|
||||||
on_numba_failure: Optional[Callable[[BaseException], None]] = None,
|
on_numba_failure: Callable[[BaseException], None] | None = None,
|
||||||
) -> BlendBackend:
|
) -> BlendBackend:
|
||||||
"""Return a blend backend instance by name.
|
"""Return a blend backend instance by name.
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import numpy as np
|
|||||||
from .numpy_backend import NumpyBlendBackend, blend_kernel_numpy
|
from .numpy_backend import NumpyBlendBackend, blend_kernel_numpy
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from orisys_blend.kernel import _NUMBA_AVAILABLE, blend_kernel as _blend_kernel_numba
|
from orisys_blend.kernel import _NUMBA_AVAILABLE
|
||||||
|
from orisys_blend.kernel import blend_kernel as _blend_kernel_numba
|
||||||
except Exception:
|
except Exception:
|
||||||
_NUMBA_AVAILABLE = False
|
_NUMBA_AVAILABLE = False
|
||||||
_blend_kernel_numba = None
|
_blend_kernel_numba = None
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from typing import List, Optional, Sequence, Tuple
|
from collections.abc import Sequence
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -38,13 +38,13 @@ def correct_edge_bloom(img, method: str = "divide", kernel_size=None, clip: bool
|
|||||||
|
|
||||||
|
|
||||||
def filter_boxes_by_size(
|
def filter_boxes_by_size(
|
||||||
boxes: Sequence[Tuple[int, int, int, int]],
|
boxes: Sequence[tuple[int, int, int, int]],
|
||||||
img_shape,
|
img_shape,
|
||||||
*,
|
*,
|
||||||
min_area_frac: float,
|
min_area_frac: float,
|
||||||
max_area_frac: float,
|
max_area_frac: float,
|
||||||
max_aspect_ratio: float,
|
max_aspect_ratio: float,
|
||||||
) -> List[Tuple[int, int, int, int]]:
|
) -> list[tuple[int, int, int, int]]:
|
||||||
h, w = img_shape
|
h, w = img_shape
|
||||||
img_area = h * w
|
img_area = h * w
|
||||||
min_area = img_area * float(min_area_frac)
|
min_area = img_area * float(min_area_frac)
|
||||||
@@ -72,7 +72,7 @@ def detect_nonblack_regions(
|
|||||||
show: bool = False,
|
show: bool = False,
|
||||||
row_tol: float = 0.4,
|
row_tol: float = 0.4,
|
||||||
save_preview_path: str = "./config/compound_eye.png",
|
save_preview_path: str = "./config/compound_eye.png",
|
||||||
) -> Tuple[int, List[Tuple[int, int, int, int]]]:
|
) -> tuple[int, list[tuple[int, int, int, int]]]:
|
||||||
"""Detect tile boxes; returns ``(n_boxes, boxes as x0,y0,x1,y1)``."""
|
"""Detect tile boxes; returns ``(n_boxes, boxes as x0,y0,x1,y1)``."""
|
||||||
if img is None:
|
if img is None:
|
||||||
raise ValueError("Input image is None.")
|
raise ValueError("Input image is None.")
|
||||||
@@ -80,7 +80,7 @@ def detect_nonblack_regions(
|
|||||||
h, w = img.shape[:2]
|
h, w = img.shape[:2]
|
||||||
n_boxes = 0
|
n_boxes = 0
|
||||||
bk_thresh = 0
|
bk_thresh = 0
|
||||||
boxes: List[Tuple[int, int, int, int]] = []
|
boxes: list[tuple[int, int, int, int]] = []
|
||||||
|
|
||||||
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
|
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
|
||||||
v = hsv[:, :, 2]
|
v = hsv[:, :, 2]
|
||||||
@@ -114,9 +114,7 @@ def detect_nonblack_regions(
|
|||||||
bk_thresh += bk_thresh_step
|
bk_thresh += bk_thresh_step
|
||||||
|
|
||||||
print(
|
print(
|
||||||
"Detected {} candidate boxes with bk_thresh={}".format(
|
f"Detected {n_boxes} candidate boxes with bk_thresh={bk_thresh - bk_thresh_step}"
|
||||||
n_boxes, bk_thresh - bk_thresh_step
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
if n_boxes == 0:
|
if n_boxes == 0:
|
||||||
return 0, []
|
return 0, []
|
||||||
@@ -194,7 +192,7 @@ def find_stitch_boxes(
|
|||||||
nrow: int,
|
nrow: int,
|
||||||
ncol: int,
|
ncol: int,
|
||||||
show: bool = False,
|
show: bool = False,
|
||||||
n_boxes_target: Optional[int] = None,
|
n_boxes_target: int | None = None,
|
||||||
):
|
):
|
||||||
"""Run bloom correction + detection; return ``(n_boxes, boxes, grid_ok)``.
|
"""Run bloom correction + detection; return ``(n_boxes, boxes, grid_ok)``.
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from collections.abc import Mapping
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Mapping, Optional
|
from typing import Any
|
||||||
|
|
||||||
# JSON ``stitcher.blend`` / ``blend_list``: none = fast paste; numpy/numba = feathered.
|
# JSON ``stitcher.blend`` / ``blend_list``: none = fast paste; numpy/numba = feathered.
|
||||||
BLEND_MODES = frozenset({"none", "numpy", "numba"})
|
BLEND_MODES = frozenset({"none", "numpy", "numba"})
|
||||||
@@ -19,7 +20,7 @@ def numba_blend_available() -> bool:
|
|||||||
return bool(_NUMBA_AVAILABLE and blend_kernel is not None)
|
return bool(_NUMBA_AVAILABLE and blend_kernel is not None)
|
||||||
|
|
||||||
|
|
||||||
def resolve_blend_mode(config_stitcher: Optional[Mapping[str, Any]] = None):
|
def resolve_blend_mode(config_stitcher: Mapping[str, Any] | None = None):
|
||||||
"""Resolve stitcher blend mode from env, then JSON ``blend``.
|
"""Resolve stitcher blend mode from env, then JSON ``blend``.
|
||||||
|
|
||||||
Returns ``(mode, fell_back_from_numba)`` where mode is one of
|
Returns ``(mode, fell_back_from_numba)`` where mode is one of
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Dict, Optional, Sequence
|
from collections.abc import Sequence
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -530,14 +531,14 @@ def stitch_feature_optimized(
|
|||||||
nrow: int,
|
nrow: int,
|
||||||
ncol: int,
|
ncol: int,
|
||||||
tile_list: Sequence,
|
tile_list: Sequence,
|
||||||
cache: Dict[str, Any],
|
cache: dict[str, Any],
|
||||||
blend_backend: BlendBackend,
|
blend_backend: BlendBackend,
|
||||||
blend: bool = True,
|
blend: bool = True,
|
||||||
match_brightness: bool = False,
|
match_brightness: bool = False,
|
||||||
pad: int = 5,
|
pad: int = 5,
|
||||||
print_every: int = 30,
|
print_every: int = 30,
|
||||||
verbose: bool = False,
|
verbose: bool = False,
|
||||||
frame_idx_holder: Optional[Dict[str, int]] = None,
|
frame_idx_holder: dict[str, int] | None = None,
|
||||||
):
|
):
|
||||||
"""Estimate transforms once and composite the current tile grid."""
|
"""Estimate transforms once and composite the current tile grid."""
|
||||||
plan = get_feature_stitch_plan(isstitch, index, nrow, ncol, tile_list)
|
plan = get_feature_stitch_plan(isstitch, index, nrow, ncol, tile_list)
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ instance state so future Torch/CuPy backends can reuse the same call shapes.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Optional, Tuple
|
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -74,7 +72,7 @@ def match_bgr_brightness(tile_bgr, ref_mean_gray: float):
|
|||||||
return cv2.LUT(tile_bgr, lut)
|
return cv2.LUT(tile_bgr, lut)
|
||||||
|
|
||||||
|
|
||||||
def largest_fully_covered_rect(cov) -> Optional[Tuple[int, int, int, int]]:
|
def largest_fully_covered_rect(cov) -> tuple[int, int, int, int] | None:
|
||||||
if cov.size == 0:
|
if cov.size == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -104,7 +102,7 @@ def largest_fully_covered_rect(cov) -> Optional[Tuple[int, int, int, int]]:
|
|||||||
return best_rect
|
return best_rect
|
||||||
|
|
||||||
|
|
||||||
def compute_final_crop_rect(cov) -> Optional[Tuple[int, int, int, int]]:
|
def compute_final_crop_rect(cov) -> tuple[int, int, int, int] | None:
|
||||||
valid = cov > 0
|
valid = cov > 0
|
||||||
if not np.any(valid):
|
if not np.any(valid):
|
||||||
return None
|
return None
|
||||||
@@ -126,7 +124,7 @@ def coverage_ratio(cov) -> float:
|
|||||||
return float(np.mean(cov > 0))
|
return float(np.mean(cov > 0))
|
||||||
|
|
||||||
|
|
||||||
def translation_from_H_to_ref(H) -> Tuple[float, float]:
|
def translation_from_H_to_ref(H) -> tuple[float, float]:
|
||||||
if H is None:
|
if H is None:
|
||||||
return 0.0, 0.0
|
return 0.0, 0.0
|
||||||
H = np.asarray(H, dtype=np.float64)
|
H = np.asarray(H, dtype=np.float64)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Iterable, List, Optional, Sequence, Tuple
|
from collections.abc import Iterable, Sequence
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -11,10 +11,10 @@ from .ops import copy_into_tile_buffer
|
|||||||
|
|
||||||
def extract_units_from_boxes(
|
def extract_units_from_boxes(
|
||||||
img,
|
img,
|
||||||
boxes: Sequence[Tuple[int, int, int, int]],
|
boxes: Sequence[tuple[int, int, int, int]],
|
||||||
*,
|
*,
|
||||||
selected_index: Iterable[int],
|
selected_index: Iterable[int],
|
||||||
units: Optional[list] = None,
|
units: list | None = None,
|
||||||
flip_x: bool = True,
|
flip_x: bool = True,
|
||||||
flip_y: bool = True,
|
flip_y: bool = True,
|
||||||
) -> list:
|
) -> list:
|
||||||
@@ -49,7 +49,7 @@ def extract_units_from_boxes(
|
|||||||
return flipped_units
|
return flipped_units
|
||||||
|
|
||||||
|
|
||||||
def get_tile_by_index(units: Sequence, idx) -> List:
|
def get_tile_by_index(units: Sequence, idx) -> list:
|
||||||
if isinstance(idx, int):
|
if isinstance(idx, int):
|
||||||
idx = [idx]
|
idx = [idx]
|
||||||
if len(idx) > len(units):
|
if len(idx) > len(units):
|
||||||
@@ -61,7 +61,7 @@ def get_tile_by_index(units: Sequence, idx) -> List:
|
|||||||
return tile_list
|
return tile_list
|
||||||
|
|
||||||
|
|
||||||
def get_tile_index_by_position(units: Sequence, x, y, tol: int = 20) -> Optional[int]:
|
def get_tile_index_by_position(units: Sequence, x, y, tol: int = 20) -> int | None:
|
||||||
i = 0
|
i = 0
|
||||||
for xi, yi, _tile in units:
|
for xi, yi, _tile in units:
|
||||||
if abs(int(x) - int(xi)) <= tol and abs(int(y) - int(yi)) <= tol:
|
if abs(int(x) - int(xi)) <= tol and abs(int(y) - int(yi)) <= tol:
|
||||||
@@ -70,7 +70,7 @@ def get_tile_index_by_position(units: Sequence, x, y, tol: int = 20) -> Optional
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_unit_pos(units: Sequence, idx) -> List[Tuple[int, int]]:
|
def get_unit_pos(units: Sequence, idx) -> list[tuple[int, int]]:
|
||||||
if isinstance(idx, int):
|
if isinstance(idx, int):
|
||||||
idx = [idx]
|
idx = [idx]
|
||||||
if len(idx) > len(units):
|
if len(idx) > len(units):
|
||||||
@@ -78,7 +78,7 @@ def get_unit_pos(units: Sequence, idx) -> List[Tuple[int, int]]:
|
|||||||
return [(units[i][0], units[i][1]) for i in idx]
|
return [(units[i][0], units[i][1]) for i in idx]
|
||||||
|
|
||||||
|
|
||||||
def normalize_index(index) -> Tuple[int, ...]:
|
def normalize_index(index) -> tuple[int, ...]:
|
||||||
if isinstance(index, int):
|
if isinstance(index, int):
|
||||||
return (index,)
|
return (index,)
|
||||||
if isinstance(index, np.ndarray):
|
if isinstance(index, np.ndarray):
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
class Tracker:
|
class Tracker:
|
||||||
@@ -255,7 +256,6 @@ class Tracker:
|
|||||||
"""
|
"""
|
||||||
Setup a simpler version using available models with performance optimizations
|
Setup a simpler version using available models with performance optimizations
|
||||||
"""
|
"""
|
||||||
import torchvision.transforms as T
|
|
||||||
from torchvision.models.optical_flow import raft_large
|
from torchvision.models.optical_flow import raft_large
|
||||||
# Load model and move to GPU
|
# Load model and move to GPU
|
||||||
model = raft_large(pretrained=True, progress=False).to(self.__raft_device)
|
model = raft_large(pretrained=True, progress=False).to(self.__raft_device)
|
||||||
|
|||||||
@@ -6,7 +6,17 @@ Runtime: live camera frame -> FrameUnwarper -> Sensor (with valid_mask)
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from .domain import UVDomain
|
from .domain import UVDomain
|
||||||
from .mesh_uv import align_frame_to_uv_map, barycentric_grid, face_uvs_from_loop_data, uv_triangle_to_image
|
from .geometry import (
|
||||||
|
resolve_uv_runtime_sizes,
|
||||||
|
scale_camera_geometry,
|
||||||
|
scaled_map_cache_path,
|
||||||
|
)
|
||||||
|
from .mesh_uv import (
|
||||||
|
align_frame_to_uv_map,
|
||||||
|
barycentric_grid,
|
||||||
|
face_uvs_from_loop_data,
|
||||||
|
uv_triangle_to_image,
|
||||||
|
)
|
||||||
from .precompute import (
|
from .precompute import (
|
||||||
build_or_load_domain,
|
build_or_load_domain,
|
||||||
build_uv_domain,
|
build_uv_domain,
|
||||||
@@ -15,7 +25,6 @@ from .precompute import (
|
|||||||
load_exported_data,
|
load_exported_data,
|
||||||
recommend_edge_expand,
|
recommend_edge_expand,
|
||||||
)
|
)
|
||||||
from .geometry import resolve_uv_runtime_sizes, scale_camera_geometry, scaled_map_cache_path
|
|
||||||
from .remap import UVRemap, flatten_frame, remap_valid_ratio
|
from .remap import UVRemap, flatten_frame, remap_valid_ratio
|
||||||
from .surface import UVSurfaceMap
|
from .surface import UVSurfaceMap
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ build_or_load_domain, tests, and 3D viewer tools.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -22,7 +21,7 @@ class UVDomain:
|
|||||||
"""Image-to-UV remap plus optional UV-to-world surface map."""
|
"""Image-to-UV remap plus optional UV-to-world surface map."""
|
||||||
|
|
||||||
remap: UVRemap
|
remap: UVRemap
|
||||||
surface: Optional[UVSurfaceMap] = None
|
surface: UVSurfaceMap | None = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict) -> "UVDomain":
|
def from_dict(cls, data: dict) -> "UVDomain":
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ def load_calib_json(json_path):
|
|||||||
|
|
||||||
returns: (K, dist_coeffs, (width, height))
|
returns: (K, dist_coeffs, (width, height))
|
||||||
"""
|
"""
|
||||||
with open(json_path, "r", encoding="utf-8") as f:
|
with open(json_path, encoding="utf-8") as f:
|
||||||
calib = json.load(f)
|
calib = json.load(f)
|
||||||
K = np.array(calib["camera_matrix"], dtype=np.float64)
|
K = np.array(calib["camera_matrix"], dtype=np.float64)
|
||||||
dist = np.array(calib["distortion"], dtype=np.float64).flatten()
|
dist = np.array(calib["distortion"], dtype=np.float64).flatten()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ convert source camera frames into the UV flatten domain.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any, Dict, Optional, Tuple
|
from typing import Any
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -18,9 +18,9 @@ class UVRemap:
|
|||||||
map_x: np.ndarray
|
map_x: np.ndarray
|
||||||
map_y: np.ndarray
|
map_y: np.ndarray
|
||||||
valid_mask: np.ndarray
|
valid_mask: np.ndarray
|
||||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
metadata: dict[str, Any] = field(default_factory=dict)
|
||||||
_map1_fixed: Optional[np.ndarray] = field(default=None, repr=False, compare=False)
|
_map1_fixed: np.ndarray | None = field(default=None, repr=False, compare=False)
|
||||||
_map2_fixed: Optional[np.ndarray] = field(default=None, repr=False, compare=False)
|
_map2_fixed: np.ndarray | None = field(default=None, repr=False, compare=False)
|
||||||
_border_only_invalid: bool = field(default=False, repr=False, compare=False)
|
_border_only_invalid: bool = field(default=False, repr=False, compare=False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -153,7 +153,7 @@ def _cache_matches(
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _remap_maps(remap) -> Tuple[np.ndarray, np.ndarray, np.ndarray, bool]:
|
def _remap_maps(remap) -> tuple[np.ndarray, np.ndarray, np.ndarray, bool]:
|
||||||
if isinstance(remap, UVRemap):
|
if isinstance(remap, UVRemap):
|
||||||
remap.ensure_fast_maps()
|
remap.ensure_fast_maps()
|
||||||
valid_mask = remap.valid_mask
|
valid_mask = remap.valid_mask
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -35,7 +34,6 @@ for path in (REPO_ROOT, SRC_ROOT):
|
|||||||
|
|
||||||
from orisys.tracker import Tracker # noqa: E402
|
from orisys.tracker import Tracker # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CONFIG = "box"
|
DEFAULT_CONFIG = "box"
|
||||||
|
|
||||||
|
|
||||||
@@ -125,7 +123,7 @@ class FixedRefDIS:
|
|||||||
preset = dis_preset_from_name(config_tracker.get("dis_preset", "medium"))
|
preset = dis_preset_from_name(config_tracker.get("dis_preset", "medium"))
|
||||||
self._dis = cv2.DISOpticalFlow_create(preset)
|
self._dis = cv2.DISOpticalFlow_create(preset)
|
||||||
self._dis.setFinestScale(int(config_tracker.get("dis_mode", 3)))
|
self._dis.setFinestScale(int(config_tracker.get("dis_mode", 3)))
|
||||||
self._ref: Optional[np.ndarray] = None
|
self._ref: np.ndarray | None = None
|
||||||
|
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
self._ref = None
|
self._ref = None
|
||||||
@@ -148,7 +146,7 @@ def make_tracker(config_tracker: dict, *, adaptive: bool) -> Tracker:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def read_gray_frame(cap: cv2.VideoCapture, static_image: Optional[np.ndarray]) -> tuple[bool, Optional[np.ndarray]]:
|
def read_gray_frame(cap: cv2.VideoCapture, static_image: np.ndarray | None) -> tuple[bool, np.ndarray | None]:
|
||||||
if static_image is not None:
|
if static_image is not None:
|
||||||
return True, static_image.copy()
|
return True, static_image.copy()
|
||||||
ok, frame = cap.read()
|
ok, frame = cap.read()
|
||||||
@@ -164,11 +162,11 @@ def run_comparison(
|
|||||||
*,
|
*,
|
||||||
config_tracker: dict,
|
config_tracker: dict,
|
||||||
max_frames: int,
|
max_frames: int,
|
||||||
reset_every: Optional[int],
|
reset_every: int | None,
|
||||||
show: bool,
|
show: bool,
|
||||||
) -> dict[str, ModeSeries]:
|
) -> dict[str, ModeSeries]:
|
||||||
path = Path(str(source)) if isinstance(source, str) else None
|
path = Path(str(source)) if isinstance(source, str) else None
|
||||||
static_image: Optional[np.ndarray] = None
|
static_image: np.ndarray | None = None
|
||||||
if isinstance(source, str) and path is not None and path.is_file():
|
if isinstance(source, str) and path is not None and path.is_file():
|
||||||
img = cv2.imread(str(path), cv2.IMREAD_GRAYSCALE)
|
img = cv2.imread(str(path), cv2.IMREAD_GRAYSCALE)
|
||||||
if img is not None:
|
if img is not None:
|
||||||
|
|||||||
+3
-1
@@ -114,7 +114,9 @@ def test_config_name() -> str:
|
|||||||
try:
|
try:
|
||||||
if _is_usable_config(name):
|
if _is_usable_config(name):
|
||||||
return name
|
return name
|
||||||
except Exception:
|
except Exception: # noqa: S112 - a failing config is simply not a candidate
|
||||||
|
# Probe: a config that cannot be loaded or lacks the key is simply not
|
||||||
|
# a candidate. Any such failure is reported if no config survives.
|
||||||
continue
|
continue
|
||||||
|
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
|||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
import orisys
|
import orisys
|
||||||
|
|
||||||
_REPO_ROOT = Path(__file__).resolve().parents[1]
|
_REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import pytest
|
|||||||
|
|
||||||
from orisys import Sensor
|
from orisys import Sensor
|
||||||
from orisys.calibration import (
|
from orisys.calibration import (
|
||||||
CalSample,
|
|
||||||
CalibrationError,
|
CalibrationError,
|
||||||
|
CalSample,
|
||||||
build,
|
build,
|
||||||
check,
|
check,
|
||||||
filter_mismatches,
|
filter_mismatches,
|
||||||
|
|||||||
@@ -22,9 +22,8 @@ from orisys.capture import (
|
|||||||
open_capture,
|
open_capture,
|
||||||
pick_default_camera,
|
pick_default_camera,
|
||||||
)
|
)
|
||||||
from orisys.capture.source import _apply_stream_props, _warmup_capture
|
|
||||||
from orisys.capture.discovery import DEFAULT_LAPTOP_WEBCAM_PATTERNS
|
from orisys.capture.discovery import DEFAULT_LAPTOP_WEBCAM_PATTERNS
|
||||||
|
from orisys.capture.source import _apply_stream_props, _warmup_capture
|
||||||
|
|
||||||
MJPG = cv2.VideoWriter_fourcc(*"MJPG")
|
MJPG = cv2.VideoWriter_fourcc(*"MJPG")
|
||||||
YUY2 = cv2.VideoWriter_fourcc(*"YUY2")
|
YUY2 = cv2.VideoWriter_fourcc(*"YUY2")
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ if str(_EXAMPLES) not in sys.path:
|
|||||||
sys.path.insert(0, str(_EXAMPLES))
|
sys.path.insert(0, str(_EXAMPLES))
|
||||||
|
|
||||||
from viz.contact_arrow import (
|
from viz.contact_arrow import (
|
||||||
ContactArrowStyle,
|
|
||||||
DEFAULT_CONTACT_ARROW_STYLE,
|
DEFAULT_CONTACT_ARROW_STYLE,
|
||||||
|
ContactArrowStyle,
|
||||||
force_visual_strength,
|
force_visual_strength,
|
||||||
length_from_force,
|
length_from_force,
|
||||||
thickness_scale_from_force,
|
thickness_scale_from_force,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
|
||||||
|
|
||||||
from orisys.util import _prepare_draw_mask, draw_arrows
|
from orisys.util import _prepare_draw_mask, draw_arrows
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import pytest
|
|||||||
|
|
||||||
from orisys import FrameSnapshot, StreamRecorder, mosaic_videos
|
from orisys import FrameSnapshot, StreamRecorder, mosaic_videos
|
||||||
|
|
||||||
|
|
||||||
_DEFAULT = object()
|
_DEFAULT = object()
|
||||||
|
|
||||||
|
|
||||||
@@ -156,7 +155,6 @@ def test_valid_mask_callable_resolved_per_frame(tmp_path):
|
|||||||
|
|
||||||
def mask():
|
def mask():
|
||||||
calls.append(1)
|
calls.append(1)
|
||||||
return None
|
|
||||||
|
|
||||||
path = str(tmp_path / "s0.mp4")
|
path = str(tmp_path / "s0.mp4")
|
||||||
rec = StreamRecorder(path, grid_spacing=8, overlay_values=False, valid_mask=mask)
|
rec = StreamRecorder(path, grid_spacing=8, overlay_values=False, valid_mask=mask)
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ from pathlib import Path
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from orisys import Sensor
|
from orisys import Sensor
|
||||||
|
from orisys import configs as orisys_configs
|
||||||
from orisys.calibration import filters_fingerprint
|
from orisys.calibration import filters_fingerprint
|
||||||
from orisys.sensor import FILTER_PRESETS, available_filter_presets
|
from orisys.sensor import FILTER_PRESETS, available_filter_presets
|
||||||
from orisys import configs as orisys_configs
|
|
||||||
|
|
||||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
EXAMPLES = REPO_ROOT / "examples" / "filters"
|
EXAMPLES = REPO_ROOT / "examples" / "filters"
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import numpy as np
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import orisys
|
import orisys
|
||||||
from orisys.finger import TactileProcessor, load_finger_export_bundle, resolve_export_paths
|
from orisys.finger import (
|
||||||
|
TactileProcessor,
|
||||||
|
load_finger_export_bundle,
|
||||||
|
resolve_export_paths,
|
||||||
|
)
|
||||||
from orisys.finger.exports import DEFAULT_FINGER_MAP_CACHE
|
from orisys.finger.exports import DEFAULT_FINGER_MAP_CACHE
|
||||||
from orisys.finger.runtime import resolve_finger_geometry
|
from orisys.finger.runtime import resolve_finger_geometry
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import pytest
|
|||||||
from orisys.backend_availability import CUPY_AVAILABLE
|
from orisys.backend_availability import CUPY_AVAILABLE
|
||||||
from orisys.hhd import PoissonSolver, nHHD
|
from orisys.hhd import PoissonSolver, nHHD
|
||||||
|
|
||||||
|
|
||||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ def packaging_utils():
|
|||||||
build_dir = REPO_ROOT / "build"
|
build_dir = REPO_ROOT / "build"
|
||||||
if str(build_dir) not in sys.path:
|
if str(build_dir) not in sys.path:
|
||||||
sys.path.insert(0, str(build_dir))
|
sys.path.insert(0, str(build_dir))
|
||||||
from common.pyproject_utils import extras_require, install_requires, project_metadata
|
from common.pyproject_utils import (
|
||||||
|
extras_require,
|
||||||
|
install_requires,
|
||||||
|
project_metadata,
|
||||||
|
)
|
||||||
|
|
||||||
return install_requires, extras_require, project_metadata
|
return install_requires, extras_require, project_metadata
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user