added ruff code reivew

This commit is contained in:
ykcheungab
2026-09-27 19:21:55 +08:00
parent 07da372204
commit e2ac36269c
110 changed files with 890 additions and 426 deletions
+1 -3
View File
@@ -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
+3 -8
View File
@@ -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
+1 -1
View File
@@ -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
+2 -1
View File
@@ -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)
+10 -10
View File
@@ -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",
]
+18 -17
View File
@@ -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
+47 -25
View File
@@ -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:
+36 -24
View File
@@ -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.
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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:
+2 -2
View File
@@ -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",
+8 -8
View File
@@ -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__(
+1 -1
View File
@@ -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 -1
View File
@@ -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
+2 -1
View File
@@ -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 = {}
+1 -1
View File
@@ -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"]
+4 -6
View File
@@ -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
View File
@@ -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__(
+7 -8
View File
@@ -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:
# --------------------------------------------------------------------------
# --------------------------------------------------------------------------
+1 -1
View File
@@ -50,7 +50,7 @@ from .timing import Timer
'''
class PoissonSolver(object):
class PoissonSolver:
def __init__(self, **kwargs):
args = kwargs.keys()
+1 -1
View File
@@ -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"]
+6 -5
View File
@@ -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
+4 -2
View File
@@ -1,7 +1,9 @@
import numpy
import timeit
class Timer(object):
import numpy
class Timer:
def display(self):
+6 -5
View File
@@ -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:
+21 -19
View File
@@ -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 . import configs
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)")
+9 -9
View File
@@ -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 -4
View File
@@ -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:
+4 -3
View File
@@ -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.
+2 -1
View File
@@ -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
+7 -9
View File
@@ -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 -2
View File
@@ -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
+4 -3
View File
@@ -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)
+3 -5
View File
@@ -6,8 +6,6 @@ instance state so future Torch/CuPy backends can reuse the same call shapes.
from __future__ import annotations
from 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)
+7 -7
View File
@@ -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):
+2 -2
View File
@@ -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)
+11 -2
View File
@@ -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
+1 -2
View File
@@ -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":
+1 -1
View File
@@ -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 -5
View File
@@ -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