Add page-marker grouping and party-aware naming
Consecutive captures whose printed PAGE X OF Y markers advance under the same total are grouped into one multi-page PDF, with duplicate pages keeping the better read. Naming gains doc-title/party components and a scan-date fallback so nothing lands in UNSORTED. Vision LLM default switches to minicpm-v (faster, fewer false blanks on dense mono pages).
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
from paperpod.capture.video import Frame
|
||||
from paperpod.vision.capture_events import find_document_events
|
||||
from paperpod.vision.document import DocumentDetection
|
||||
import numpy as np
|
||||
|
||||
from paperpod.capture.video import Frame
|
||||
from paperpod.vision.capture_events import find_document_events, skin_fraction_in_quad
|
||||
from paperpod.vision.document import DocumentDetection
|
||||
|
||||
_SKIN_BGR = (80, 120, 180) # inside the YCrCb skin range used by refine
|
||||
|
||||
|
||||
def _frame(t: float, idx: int = 0) -> Frame:
|
||||
return Frame(index=idx, t=t, image=np.zeros((100, 100, 3), dtype=np.uint8))
|
||||
@@ -66,3 +69,113 @@ def test_area_change_splits_documents():
|
||||
assert len(events) == 2
|
||||
assert events[0].detection.area_ratio == 0.25
|
||||
assert events[1].detection.area_ratio == 0.65
|
||||
|
||||
|
||||
def _paper_frame(t: float, idx: int, with_finger: bool, sharper: bool) -> Frame:
|
||||
"""White page frame; optionally with a skin-colored blob and extra texture."""
|
||||
rng = np.random.default_rng(idx)
|
||||
image = np.full((100, 100, 3), 245, dtype=np.uint8)
|
||||
if sharper:
|
||||
# High-frequency noise inflates Laplacian variance (the old
|
||||
# sharpness-only criterion would always pick this frame).
|
||||
noise = rng.integers(0, 60, size=(100, 100, 1), dtype=np.uint8)
|
||||
image = np.clip(image.astype(int) - noise, 0, 255).astype(np.uint8)
|
||||
if with_finger:
|
||||
image[30:70, 40:75] = _SKIN_BGR
|
||||
return Frame(index=idx, t=t, image=image)
|
||||
|
||||
|
||||
def _full_quad() -> DocumentDetection:
|
||||
quad = np.array([[0, 0], [100, 0], [100, 100], [0, 100]], dtype=np.float32)
|
||||
return DocumentDetection(quad=quad, area_ratio=0.3, method="quad", source="edges", confidence=0.8)
|
||||
|
||||
|
||||
def test_skin_fraction_detects_finger():
|
||||
clean = _paper_frame(1.0, 0, with_finger=False, sharper=False)
|
||||
finger = _paper_frame(1.2, 1, with_finger=True, sharper=False)
|
||||
quad = _full_quad().quad
|
||||
assert skin_fraction_in_quad(clean.image, quad) < 0.003
|
||||
assert skin_fraction_in_quad(finger.image, quad) > 0.05
|
||||
|
||||
|
||||
def test_hands_free_frame_beats_equally_sharp_finger_frame():
|
||||
# When sharpness is similar, prefer the clean frame.
|
||||
samples = [
|
||||
(_paper_frame(1.0, 0, with_finger=True, sharper=True), _full_quad()),
|
||||
(_paper_frame(1.2, 1, with_finger=False, sharper=True), _full_quad()),
|
||||
(_paper_frame(1.4, 2, with_finger=True, sharper=True), _full_quad()),
|
||||
]
|
||||
events = find_document_events(samples, gap_s=2.0, min_samples=2)
|
||||
assert len(events) == 1
|
||||
assert events[0].best_frame_index == 1
|
||||
assert events[0].skin_fraction < 0.003
|
||||
|
||||
|
||||
def test_much_sharper_touched_frame_beats_blurry_clean_frame():
|
||||
# Mid-flip blur with no fingers must not beat a sharp frame that has a
|
||||
# fingertip on the margin — that's how page 7 of a statement got lost.
|
||||
samples = [
|
||||
(_paper_frame(1.0, 0, with_finger=False, sharper=False), _full_quad()),
|
||||
(_paper_frame(1.2, 1, with_finger=True, sharper=True), _full_quad()),
|
||||
(_paper_frame(1.4, 2, with_finger=False, sharper=False), _full_quad()),
|
||||
]
|
||||
events = find_document_events(samples, gap_s=2.0, min_samples=2)
|
||||
assert len(events) == 1
|
||||
assert events[0].best_frame_index == 1
|
||||
|
||||
|
||||
def test_sharpest_wins_among_equally_clean_frames():
|
||||
samples = [
|
||||
(_paper_frame(1.0, 0, with_finger=False, sharper=False), _full_quad()),
|
||||
(_paper_frame(1.2, 1, with_finger=False, sharper=True), _full_quad()),
|
||||
]
|
||||
events = find_document_events(samples, gap_s=2.0, min_samples=2)
|
||||
assert len(events) == 1
|
||||
assert events[0].best_frame_index == 1
|
||||
|
||||
|
||||
def test_long_event_splits_on_page_flip_motion():
|
||||
"""Quiet dwells inside a long placement become separate events.
|
||||
|
||||
Synthetic: quiet page A, motion/hands, quiet page B. Contour is the
|
||||
same throughout (page flip under a fixed outline), so without dwell
|
||||
splitting only page A would be kept.
|
||||
"""
|
||||
|
||||
def page_frame(t: float, idx: int, pattern: int) -> Frame:
|
||||
# Nearly identical consecutive frames so inter-sample motion is quiet.
|
||||
img = np.full((180, 320, 3), 240, dtype=np.uint8)
|
||||
img[40:140, 40:280] = 200 if pattern == 1 else 160
|
||||
# Seeded by pattern only — same noise every page-A frame.
|
||||
noise = np.random.default_rng(pattern).integers(0, 3, size=img.shape, dtype=np.uint8)
|
||||
img = np.clip(img.astype(int) + noise, 0, 255).astype(np.uint8)
|
||||
return Frame(index=idx, t=t, image=img)
|
||||
|
||||
samples = []
|
||||
# Quiet page 1 dwell: t=0.0 .. 3.0
|
||||
for i in range(0, 13):
|
||||
samples.append((page_frame(i * 0.25, i, pattern=1), _full_quad()))
|
||||
# Motion / hands between pages
|
||||
for i, shade in enumerate((40, 80, 120, 60)):
|
||||
img = np.full((180, 320, 3), shade, dtype=np.uint8)
|
||||
samples.append((Frame(index=13 + i, t=3.25 + i * 0.25, image=img), _full_quad()))
|
||||
# Quiet page 2 dwell: t=4.25 .. 5.25
|
||||
for i in range(0, 5):
|
||||
samples.append((page_frame(4.25 + i * 0.25, 20 + i, pattern=2), _full_quad()))
|
||||
|
||||
events = find_document_events(samples, gap_s=2.0, min_samples=2)
|
||||
assert len(events) >= 2
|
||||
assert events[0].t_end <= 3.5
|
||||
assert events[1].t_start >= 3.5
|
||||
|
||||
|
||||
|
||||
def test_short_event_not_resegmented():
|
||||
samples = [
|
||||
(_paper_frame(1.0, 0, with_finger=False, sharper=True), _full_quad()),
|
||||
(_paper_frame(1.2, 1, with_finger=False, sharper=True), _full_quad()),
|
||||
(_paper_frame(1.4, 2, with_finger=False, sharper=True), _full_quad()),
|
||||
]
|
||||
events = find_document_events(samples, gap_s=2.0, min_samples=2)
|
||||
assert len(events) == 1
|
||||
|
||||
|
||||
Reference in New Issue
Block a user