Add crop refinement, finger removal, and local LLM naming

- vision/refine.py: tighten crops to the paper band (removes mat
  margins and hands beside receipts) and inpaint border-connected
  skin regions so fingers disappear from output
- llm/vision.py: identify documents with a local Ollama vision model
  (qwen2.5vl); extracts vendor/date/total/form code and flags quality
  issues (fingers, blur, glare); falls back to Tesseract when down
- pipeline: drop blank pages, dedupe consecutive captures of the same
  document, record refine/LLM fields in report and export summary
This commit is contained in:
2026-07-08 18:23:50 -04:00
parent 2c28a623fb
commit a43ab20df6
22 changed files with 1497 additions and 111 deletions
+68
View File
@@ -0,0 +1,68 @@
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
def _frame(t: float, idx: int = 0) -> Frame:
return Frame(index=idx, t=t, image=np.zeros((100, 100, 3), dtype=np.uint8))
def _det() -> DocumentDetection:
quad = np.array([[0, 0], [1, 0], [1, 1], [0, 1]], dtype=np.float32)
return DocumentDetection(quad=quad, area_ratio=0.3, method="quad", source="edges", confidence=0.8)
def test_single_event_from_consecutive_detections():
samples = [(_frame(1.0, 0), _det()), (_frame(1.2, 1), _det()), (_frame(1.4, 2), _det())]
events = find_document_events(samples, gap_s=2.0, min_samples=2)
assert len(events) == 1
assert events[0].t_start == 1.0
assert events[0].sample_count == 3
def test_gap_splits_separate_documents():
samples = [
(_frame(1.0, 0), _det()),
(_frame(1.2, 1), _det()),
(_frame(5.0, 2), _det()),
(_frame(5.2, 3), _det()),
]
events = find_document_events(samples, gap_s=2.0, min_samples=2)
assert len(events) == 2
assert events[0].t_end == 1.2
assert events[1].t_start == 5.0
def test_min_samples_filters_single_frame_noise():
samples = [(_frame(1.0, 0), _det()), (_frame(5.0, 1), _det()), (_frame(5.2, 2), _det())]
events = find_document_events(samples, gap_s=2.0, min_samples=2)
assert len(events) == 1
assert events[0].t_start == 5.0
def test_area_change_splits_documents():
small = DocumentDetection(
quad=np.array([[0, 0], [1, 0], [1, 1], [0, 1]], dtype=np.float32),
area_ratio=0.25,
method="quad",
source="threshold",
confidence=0.8,
)
large = DocumentDetection(
quad=np.array([[0, 0], [1, 0], [1, 1], [0, 1]], dtype=np.float32),
area_ratio=0.65,
method="quad",
source="threshold",
confidence=0.8,
)
samples = [
(_frame(1.0, 0), small),
(_frame(1.2, 1), small),
(_frame(1.4, 2), large),
(_frame(1.6, 3), large),
]
events = find_document_events(samples, gap_s=2.0, min_samples=2)
assert len(events) == 2
assert events[0].detection.area_ratio == 0.25
assert events[1].detection.area_ratio == 0.65