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:
+28
-10
@@ -1,4 +1,12 @@
|
||||
"""Optional image enhancement for OCR-readiness."""
|
||||
"""Scan-style image enhancement, for OCR-readiness and optionally the saved crop.
|
||||
|
||||
`normalize_illumination` is the "flatten it, make it easier to read" trick
|
||||
scanner apps like CamScanner use: divide the image by a heavily-blurred copy
|
||||
of itself (an estimate of the local lighting/shadow), which cancels out
|
||||
gradients, glare, and wood-grain/shadow texture far better than a plain
|
||||
contrast boost. Confirmed on a real low-contrast receipt-on-wood-grain photo:
|
||||
raw OCR read almost nothing, normalized OCR recovered full lines of text.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -6,15 +14,25 @@ import cv2
|
||||
import numpy as np
|
||||
|
||||
|
||||
def enhance_for_ocr(image_bgr: np.ndarray) -> np.ndarray:
|
||||
"""Boost local contrast (CLAHE on the luminance channel).
|
||||
def normalize_illumination(image_bgr: np.ndarray, blur_fraction: float = 1 / 15) -> np.ndarray:
|
||||
"""Cancel out uneven lighting/shadow/background texture. Returns grayscale."""
|
||||
gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
|
||||
sigma = max(image_bgr.shape[1] * blur_fraction, 10.0)
|
||||
background = cv2.GaussianBlur(gray, (0, 0), sigmaX=sigma)
|
||||
# Background approaches white; dividing pulls the true page background
|
||||
# up to ~255 wherever lighting made it dim, while text (locally much
|
||||
# darker than its surroundings) stays dark.
|
||||
return cv2.divide(gray, background, scale=255)
|
||||
|
||||
Deliberately conservative: no binarization, so the crop stays pleasant to
|
||||
read in the PDF while giving OCR more to work with. Thresholding can be
|
||||
added behind a config flag later if OCR needs it.
|
||||
|
||||
def enhance_for_ocr(image_bgr: np.ndarray) -> np.ndarray:
|
||||
"""Illumination-normalize + local contrast boost. Returns a BGR image.
|
||||
|
||||
Always grayscale-looking (3 identical channels) since normalization
|
||||
operates on luminance. Intended primarily as an OCR preprocessing step;
|
||||
only baked into the saved crop/PDF when document.enhance is enabled.
|
||||
"""
|
||||
lab = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2LAB)
|
||||
l_chan, a_chan, b_chan = cv2.split(lab)
|
||||
normalized = normalize_illumination(image_bgr)
|
||||
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
|
||||
lab = cv2.merge((clahe.apply(l_chan), a_chan, b_chan))
|
||||
return cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)
|
||||
boosted = clahe.apply(normalized)
|
||||
return cv2.cvtColor(boosted, cv2.COLOR_GRAY2BGR)
|
||||
|
||||
Reference in New Issue
Block a user