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