- 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
78 lines
2.4 KiB
Python
78 lines
2.4 KiB
Python
"""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)
|