added ruff code reivew

This commit is contained in:
ykcheungab
2026-09-27 19:21:55 +08:00
parent 07da372204
commit e2ac36269c
110 changed files with 890 additions and 426 deletions
+4 -1
View File
@@ -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:
+8 -7
View File
@@ -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",
+1 -1
View File
@@ -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:
+1
View File
@@ -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"
+1
View File
@@ -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
+1
View File
@@ -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"
+1
View File
@@ -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"
+1
View File
@@ -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
+1
View File
@@ -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"
+5 -1
View File
@@ -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
+2 -2
View File
@@ -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
+9 -4
View File
@@ -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
+74
View File
@@ -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`.
+7 -3
View File
@@ -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="视频源:摄像头编号或视频文件路径")
+1
View File
@@ -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
+10 -9
View File
@@ -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",
] ]
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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",
] ]
+14 -15
View File
@@ -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
+17 -16
View File
@@ -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()
+6 -6
View File
@@ -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",
] ]
+25 -4
View File
@@ -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:
+7 -10
View File
@@ -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,
+3 -2
View File
@@ -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"]
+1 -1
View File
@@ -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 -3
View File
@@ -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
+13 -7
View File
@@ -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' 重置追踪器。")
+4 -1
View File
@@ -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)
+3 -2
View File
@@ -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,
-1
View File
@@ -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",
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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:
+23 -11
View File
@@ -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:
+9 -9
View File
@@ -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",
+5 -1
View File
@@ -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 -1
View File
@@ -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
+5 -1
View File
@@ -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,
)
-1
View File
@@ -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
+1
View File
@@ -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:
+1
View File
@@ -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)
+55
View File
@@ -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
View File
@@ -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: 从摄像头读取(取消注释使用)
+7 -4
View File
@@ -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
View File
@@ -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()
+2 -3
View File
@@ -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:
+9 -5
View File
@@ -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)
+1 -3
View File
@@ -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
+3 -8
View File
@@ -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
+1 -1
View File
@@ -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
+2 -1
View File
@@ -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)
+10 -10
View File
@@ -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",
] ]
+18 -17
View File
@@ -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
+47 -25
View File
@@ -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:
+36 -24
View File
@@ -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.
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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:
+2 -2
View File
@@ -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",
+8 -8
View File
@@ -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__(
+1 -1
View File
@@ -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 -1
View File
@@ -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
+2 -1
View File
@@ -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 = {}
+1 -1
View File
@@ -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"]
+4 -6
View File
@@ -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
View File
@@ -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__(
+7 -8
View File
@@ -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:
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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"]
+6 -5
View File
@@ -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
+4 -2
View File
@@ -1,7 +1,9 @@
import numpy
import timeit import timeit
class Timer(object): import numpy
class Timer:
def display(self): def display(self):
+6 -5
View File
@@ -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
View File
@@ -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)")
+9 -9
View File
@@ -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 -4
View File
@@ -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:
+4 -3
View File
@@ -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.
+2 -1
View File
@@ -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
+7 -9
View File
@@ -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 -2
View File
@@ -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
+4 -3
View File
@@ -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)
+3 -5
View File
@@ -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)
+7 -7
View File
@@ -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):
+2 -2
View File
@@ -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)
+11 -2
View File
@@ -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
+1 -2
View File
@@ -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":
+1 -1
View File
@@ -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 -5
View File
@@ -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
+4 -6
View File
@@ -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
View File
@@ -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(
+1
View File
@@ -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]
+1 -1
View File
@@ -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,
+1 -2
View File
@@ -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")
+1 -1
View File
@@ -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,
-1
View File
@@ -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
-2
View File
@@ -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)
+1 -1
View File
@@ -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"
+5 -1
View File
@@ -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
-1
View File
@@ -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]
+5 -1
View File
@@ -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