Fix clipped receipt margins, add time-of-day naming, speed up rotation detection

- vision/document.py: pad_quad() expands the detected quad outward
  before warping so a straight-line contour fit doesn't clip the first
  character of every line on a slightly curled/wrinkled receipt
- ocr/extract.py + llm/vision.py: extract transaction time (HH:MM) so
  same-day repeat visits to a vendor get distinct filenames
  (2023-03-20_1525_walmart.pdf vs. a later same-day trip)
- vision/orient.py: downscale before Tesseract OSD/confidence-sweep
  rotation detection — this was the dominant cost in the detection
  phase (7x speedup: 472s -> 67s on a 36s test video)
This commit is contained in:
2026-07-08 18:55:37 -04:00
parent a43ab20df6
commit 5be9d5935d
11 changed files with 160 additions and 12 deletions
+16 -2
View File
@@ -32,6 +32,19 @@ def apply_rotation(image_bgr: np.ndarray, rotation: int) -> np.ndarray:
return image_bgr if code is None else cv2.rotate(image_bgr, code)
def _downscale(image_bgr: np.ndarray, max_dim: int = 1000) -> np.ndarray:
"""Shrink before feeding Tesseract: orientation only needs word shapes,
not per-character resolution, and _confidence_sweep_rotation runs this
up to 4x per document — at full 4K-crop resolution that made rotation
detection by far the slowest part of the whole pipeline."""
h, w = image_bgr.shape[:2]
longest = max(h, w)
if longest <= max_dim:
return image_bgr
scale = max_dim / longest
return cv2.resize(image_bgr, (round(w * scale), round(h * scale)), interpolation=cv2.INTER_AREA)
def _osd_rotation(image_bgr: np.ndarray, lang: str) -> int | None:
"""Tesseract's orientation-and-script-detection: fast and purpose-built.
@@ -43,7 +56,7 @@ def _osd_rotation(image_bgr: np.ndarray, lang: str) -> int | None:
"""
try:
osd = pytesseract.image_to_osd(
image_bgr, lang=lang, output_type=pytesseract.Output.DICT
_downscale(image_bgr), lang=lang, output_type=pytesseract.Output.DICT
)
except pytesseract.TesseractError:
return None
@@ -72,10 +85,11 @@ def _confidence_sweep_rotation(
Requiring 3+ characters drops almost all of that noise while barely
touching real multi-character words.
"""
small = _downscale(image_bgr)
best_rotation = 0
best_score = -1.0
for rotation in (0, 90, 180, 270):
candidate = apply_rotation(image_bgr, rotation)
candidate = apply_rotation(small, rotation)
data = pytesseract.image_to_data(
candidate, lang=lang, config=f"--psm {psm}", output_type=pytesseract.Output.DICT
)