refractor capture to a module

This commit is contained in:
ykcheungab
2026-08-23 15:53:02 +08:00
parent 4583dc3a40
commit 8a7760f12b
30 changed files with 1992 additions and 332 deletions
+58 -21
View File
@@ -2,9 +2,16 @@ from __future__ import annotations
from dataclasses import dataclass
import cv2
import numpy as np
from ..capture import (
CaptureConfig,
CaptureWorker,
ReconnectPolicy,
is_live_camera,
normalize_source,
open_capture,
)
from ..configs import load_config
from ..frame_processors import FrameUnwarper
from ..sensor import Sensor
@@ -24,25 +31,28 @@ from .exports import (
def normalize_video_source(video):
"""Accept camera index, numeric string, or filesystem video path."""
if video is None:
return 0
if isinstance(video, str):
stripped = video.strip().strip("/\\")
if not stripped:
return 0
if stripped.isdigit():
return int(stripped)
return stripped
return video
return normalize_source(video)
def open_source(video, config):
capture = cv2.VideoCapture(normalize_video_source(video))
capture.set(cv2.CAP_PROP_FRAME_WIDTH, config.get("camera", {}).get("resolution_raw")[0])
capture.set(cv2.CAP_PROP_FRAME_HEIGHT, config.get("camera", {}).get("resolution_raw")[1])
capture.set(cv2.CAP_PROP_FPS, config.get("camera", {}).get("fps", 30))
capture.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter.fourcc('M', 'J', 'P', 'G'))
return capture
def open_source(video, config, *, auto_reconnect: bool = False, verbose: bool = False):
"""Open a capture worker for finger pipelines (compat: returns worker.cap-like via ``.cap``).
Historically returned a bare ``cv2.VideoCapture``. Callers that need the raw
OpenCV handle can use ``worker.source.cap``; prefer ``worker.read()``.
"""
capture_config = CaptureConfig.from_camera_dict(
normalize_video_source(video),
config.get("camera", {}),
reconnect=ReconnectPolicy(enabled=auto_reconnect),
verbose=verbose,
start_worker=False,
)
worker = open_capture(capture_config, start_worker=False)
# Preserve prior attribute access: treat returned object like VideoCapture for
# simple .read()/.release()/.isOpened() callers, while exposing capture worker.
cap = worker.source.cap
cap._orisys_worker = worker # type: ignore[attr-defined]
return cap
class TactileProcessor:
@@ -210,6 +220,7 @@ class FingerRuntime:
tactile: TactileProcessor,
frame_processor: FrameUnwarper,
capture,
capture_worker: CaptureWorker | None = None,
):
self.geometry = geometry
self.domain = domain
@@ -217,6 +228,7 @@ class FingerRuntime:
self.sensor = tactile
self.frame_processor = frame_processor
self.capture = capture
self.capture_worker = capture_worker
@classmethod
def open(
@@ -232,6 +244,8 @@ class FingerRuntime:
flip_source_vertical=True,
verbose=False,
mirror=False,
auto_reconnect: bool = False,
capture_mode: str = "latest",
) -> "FingerRuntime":
"""Open a live finger runtime from export bundle, config, and video source."""
if camera_id is not None:
@@ -263,9 +277,22 @@ class FingerRuntime:
)
domain.remap.ensure_fast_maps()
capture = open_source(video, geometry.config)
if not capture.isOpened():
capture_config = CaptureConfig.from_camera_dict(
video,
geometry.config.get("camera", {}),
mode=capture_mode,
reconnect=ReconnectPolicy(enabled=auto_reconnect),
verbose=verbose,
start_worker=False,
)
capture_worker = open_capture(capture_config, start_worker=False)
if not capture_worker.source.is_open():
raise RuntimeError("Cannot open camera/video source")
# Start latest-frame worker for live cameras so processing speed does not
# stall the device buffer.
if is_live_camera(video):
capture_worker.start()
capture = capture_worker.source.cap
sensor = Sensor("external", config_name=config, verbose=verbose, mirror=mirror)
frame_processor = FrameUnwarper(
@@ -283,6 +310,7 @@ class FingerRuntime:
tactile=tactile,
frame_processor=frame_processor,
capture=capture,
capture_worker=capture_worker,
)
@property
@@ -290,6 +318,10 @@ class FingerRuntime:
return self.frame_processor.valid_mask
def read_source(self):
if self.capture_worker is not None and self.capture_worker.running:
return self.capture_worker.read(copy=True, wait_s=1.0)
if self.capture_worker is not None:
return self.capture_worker.source.read_raw()
return self.capture.read()
def process_frame(self, frame, *, color=False):
@@ -323,6 +355,11 @@ class FingerRuntime:
return ok, unrolled
def close(self):
if self.capture is not None:
if self.capture_worker is not None:
self.capture_worker.close()
self.capture_worker = None
self.capture = None
elif self.capture is not None:
self.capture.release()
self.capture = None
self.tactile.disconnect()