diff --git a/bookmark/image_processing.py b/bookmark/image_processing.py new file mode 100644 index 0000000..87f4f71 --- /dev/null +++ b/bookmark/image_processing.py @@ -0,0 +1,28 @@ +import cv2 +import numpy as np + + +def crop_to_book(frame: np.ndarray, reference_frame: np.ndarray, margin: int = 20) -> np.ndarray: + """ + Crops `frame` down to the bounding box of the largest region that + differs from `reference_frame` (the book), padded by `margin` pixels + and clamped to the frame bounds. Raises ValueError if no difference + is found. + """ + diff = cv2.absdiff(frame, reference_frame) + gray = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY) + _, mask = cv2.threshold(gray, 30, 255, cv2.THRESH_BINARY) + contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + if not contours: + raise ValueError("No book detected in frame (no difference from reference)") + + largest = max(contours, key=cv2.contourArea) + x, y, w, h = cv2.boundingRect(largest) + frame_h, frame_w = frame.shape[:2] + + x0 = max(0, x - margin) + y0 = max(0, y - margin) + x1 = min(frame_w, x + w + margin) + y1 = min(frame_h, y + h + margin) + + return frame[y0:y1, x0:x1] diff --git a/tests/test_image_processing.py b/tests/test_image_processing.py new file mode 100644 index 0000000..27afef1 --- /dev/null +++ b/tests/test_image_processing.py @@ -0,0 +1,44 @@ +import numpy as np +import pytest + +from bookmark.image_processing import crop_to_book + + +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=(30, 30, 20, 20)): + 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_crop_raises_when_no_difference_from_reference(): + reference = make_reference() + with pytest.raises(ValueError): + crop_to_book(reference.copy(), reference) + + +def test_crop_returns_region_around_book_with_margin(): + reference = make_reference() + frame = make_book_frame(box=(30, 30, 20, 20)) + + cropped = crop_to_book(frame, reference, margin=10) + + # book occupies x:[30,50) y:[30,50), with margin=10 expected crop is + # roughly x:[20,60) y:[20,60) -> 40x40 + assert cropped.shape[0] == pytest.approx(40, abs=2) + assert cropped.shape[1] == pytest.approx(40, abs=2) + + +def test_crop_clamps_margin_to_frame_bounds(): + reference = make_reference() + frame = make_book_frame(box=(0, 0, 20, 20)) + + cropped = crop_to_book(frame, reference, margin=10) + + # book touches top-left corner, margin should clamp instead of going negative + assert cropped.shape[0] <= 30 + assert cropped.shape[1] <= 30