feat: add stable-centered auto-detect capture strategy
This commit is contained in:
parent
aaac91f03c
commit
d5f5b47cef
2 changed files with 131 additions and 0 deletions
78
bookmark/capture_strategies/stable_centered.py
Normal file
78
bookmark/capture_strategies/stable_centered.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from .base import CaptureStrategy
|
||||
|
||||
|
||||
class StableCenteredStrategy(CaptureStrategy):
|
||||
"""
|
||||
Fires when a book-sized region is detected filling the guide box and
|
||||
its center position holds steady for `stable_frames` consecutive
|
||||
observe() calls.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reference_frame: np.ndarray,
|
||||
guide_box: tuple[int, int, int, int],
|
||||
diff_threshold: int = 30,
|
||||
min_fill_ratio: float = 0.5,
|
||||
center_tolerance: float = 10.0,
|
||||
stable_frames: int = 5,
|
||||
) -> None:
|
||||
self.reference_frame = reference_frame
|
||||
self.guide_box = guide_box
|
||||
self.diff_threshold = diff_threshold
|
||||
self.min_fill_ratio = min_fill_ratio
|
||||
self.center_tolerance = center_tolerance
|
||||
self.stable_frames = stable_frames
|
||||
self._previous_center: tuple[float, float] | None = None
|
||||
self._stable_count = 0
|
||||
self._fired = False
|
||||
|
||||
def reset(self) -> None:
|
||||
self._previous_center = None
|
||||
self._stable_count = 0
|
||||
self._fired = False
|
||||
|
||||
def _detect(self, frame: np.ndarray) -> tuple[tuple[float, float], float] | None:
|
||||
diff = cv2.absdiff(frame, self.reference_frame)
|
||||
gray = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY)
|
||||
_, mask = cv2.threshold(gray, self.diff_threshold, 255, cv2.THRESH_BINARY)
|
||||
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||
if not contours:
|
||||
return None
|
||||
largest = max(contours, key=cv2.contourArea)
|
||||
x, y, w, h = cv2.boundingRect(largest)
|
||||
_, _, gw, gh = self.guide_box
|
||||
fill_ratio = (w * h) / float(gw * gh)
|
||||
center = (x + w / 2.0, y + h / 2.0)
|
||||
return center, fill_ratio
|
||||
|
||||
def observe(self, frame: np.ndarray) -> bool:
|
||||
if self._fired:
|
||||
return False
|
||||
|
||||
detection = self._detect(frame)
|
||||
if detection is None:
|
||||
self._stable_count = 0
|
||||
self._previous_center = None
|
||||
return False
|
||||
|
||||
center, fill_ratio = detection
|
||||
if fill_ratio < self.min_fill_ratio:
|
||||
self._stable_count = 0
|
||||
self._previous_center = None
|
||||
return False
|
||||
|
||||
if self._previous_center is not None:
|
||||
dx = center[0] - self._previous_center[0]
|
||||
dy = center[1] - self._previous_center[1]
|
||||
distance = (dx**2 + dy**2) ** 0.5
|
||||
self._stable_count = self._stable_count + 1 if distance <= self.center_tolerance else 0
|
||||
self._previous_center = center
|
||||
|
||||
if self._stable_count >= self.stable_frames:
|
||||
self._fired = True
|
||||
return True
|
||||
return False
|
||||
53
tests/test_stable_centered_strategy.py
Normal file
53
tests/test_stable_centered_strategy.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
from bookmark.capture_strategies.stable_centered import StableCenteredStrategy
|
||||
import numpy as np
|
||||
|
||||
|
||||
def make_reference(size=100, color=200):
|
||||
return np.full((size, size, 3), color, dtype=np.uint8)
|
||||
|
||||
|
||||
def make_book_frame(size=100, ref_color=200, book_color=50, box=(20, 20, 60, 60)):
|
||||
frame = np.full((size, size, 3), ref_color, dtype=np.uint8)
|
||||
x, y, w, h = box
|
||||
frame[y:y + h, x:x + w] = book_color
|
||||
return frame
|
||||
|
||||
|
||||
def test_no_fire_on_empty_reference_frame():
|
||||
reference = make_reference()
|
||||
strategy = StableCenteredStrategy(reference_frame=reference, guide_box=(20, 20, 60, 60), stable_frames=3)
|
||||
assert strategy.observe(reference) is False
|
||||
|
||||
|
||||
def test_no_fire_when_book_too_small():
|
||||
reference = make_reference()
|
||||
small_book = make_book_frame(box=(40, 40, 10, 10))
|
||||
strategy = StableCenteredStrategy(
|
||||
reference_frame=reference, guide_box=(20, 20, 60, 60), min_fill_ratio=0.5, stable_frames=3
|
||||
)
|
||||
for _ in range(5):
|
||||
assert strategy.observe(small_book) is False
|
||||
|
||||
|
||||
def test_fires_after_stable_frames_with_book_filling_guide_box():
|
||||
reference = make_reference()
|
||||
book_frame = make_book_frame(box=(20, 20, 60, 60))
|
||||
strategy = StableCenteredStrategy(
|
||||
reference_frame=reference, guide_box=(20, 20, 60, 60), min_fill_ratio=0.5, stable_frames=3
|
||||
)
|
||||
assert strategy.observe(book_frame) is False
|
||||
assert strategy.observe(book_frame) is False
|
||||
assert strategy.observe(book_frame) is False
|
||||
assert strategy.observe(book_frame) is True
|
||||
|
||||
|
||||
def test_fires_only_once():
|
||||
reference = make_reference()
|
||||
book_frame = make_book_frame(box=(20, 20, 60, 60))
|
||||
strategy = StableCenteredStrategy(
|
||||
reference_frame=reference, guide_box=(20, 20, 60, 60), min_fill_ratio=0.5, stable_frames=2
|
||||
)
|
||||
strategy.observe(book_frame)
|
||||
strategy.observe(book_frame)
|
||||
assert strategy.observe(book_frame) is True
|
||||
assert strategy.observe(book_frame) is False
|
||||
Loading…
Reference in a new issue