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:
@@ -0,0 +1,77 @@
|
||||
"""End-to-end orientation-detection tests against synthetic text pages.
|
||||
|
||||
Skipped automatically if the tesseract binary isn't installed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import pytest
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
from paperpod.vision.orient import apply_rotation, auto_rotate
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
shutil.which("tesseract") is None, reason="tesseract binary not installed"
|
||||
)
|
||||
|
||||
_FONT_CANDIDATES = [
|
||||
"/System/Library/Fonts/Helvetica.ttc",
|
||||
"/System/Library/Fonts/Supplemental/Arial.ttf",
|
||||
]
|
||||
|
||||
|
||||
def _font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
|
||||
for path in _FONT_CANDIDATES:
|
||||
try:
|
||||
return ImageFont.truetype(path, size)
|
||||
except OSError:
|
||||
continue
|
||||
return ImageFont.load_default()
|
||||
|
||||
|
||||
def _text_page() -> np.ndarray:
|
||||
"""A tall white page with a few lines of real words (upright, 0 degrees).
|
||||
|
||||
Rendered with a real TrueType font (not cv2.putText's blocky stroke
|
||||
font), which produces normal glyph shapes; cv2.putText text fragments
|
||||
into dozens of spurious single-character "words" when rotated sideways,
|
||||
which isn't representative of real documents/photos.
|
||||
"""
|
||||
img = Image.new("RGB", (400, 600), (255, 255, 255))
|
||||
draw = ImageDraw.Draw(img)
|
||||
font = _font(22)
|
||||
lines = [
|
||||
"INVOICE FROM ACME CORP",
|
||||
"Date: January 5, 2024",
|
||||
"Description of services rendered",
|
||||
"Thank you for your business",
|
||||
"Please remit payment within 30 days",
|
||||
]
|
||||
for i, line in enumerate(lines):
|
||||
draw.text((20, 80 + i * 60), line, fill=(0, 0, 0), font=font)
|
||||
return cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("applied_rotation", [0, 90, 180, 270])
|
||||
def test_auto_rotate_corrects_known_rotation(applied_rotation):
|
||||
"""Rotating upright text by N clockwise needs (360-N) clockwise to undo."""
|
||||
upright = _text_page()
|
||||
rotated_input = apply_rotation(upright, applied_rotation)
|
||||
corrected, result = auto_rotate(rotated_input)
|
||||
assert result.rotation == (360 - applied_rotation) % 360
|
||||
assert corrected.shape == upright.shape
|
||||
|
||||
|
||||
def test_apply_rotation_identity_at_zero():
|
||||
page = _text_page()
|
||||
assert np.array_equal(apply_rotation(page, 0), page)
|
||||
|
||||
|
||||
def test_apply_rotation_180_is_involutive():
|
||||
page = _text_page()
|
||||
twice = apply_rotation(apply_rotation(page, 180), 180)
|
||||
assert np.array_equal(twice, page)
|
||||
Reference in New Issue
Block a user