ripguy Claude Opus 4.8 (1M context) commited on
Commit
6dc2242
·
1 Parent(s): 9e4faa1

feat(extract): mirror VLM deskew preprocessing into the extract Space

Browse files

Scanned POs / spec sheets (the high-frequency input routed to hitech-extract) often
arrive tilted, which hurts NuExtract field extraction. Adds _preprocess_image
(Hough-line median-angle deskew, falls back to the raw image on any error) verbatim
from the VLM Space, wires it into _build_messages, and adds opencv-python-headless to
requirements. Closes the long-standing gap (VLM had deskew, extract didn't).

NOTE: takes effect only after a Space redeploy (scripts/sync_spaces.ps1) — HF git is
unreachable from the automated env, so the user runs it. py_compile clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files changed (2) hide show
  1. app.py +49 -1
  2. requirements.txt +1 -0
app.py CHANGED
@@ -30,7 +30,9 @@ import json
30
  import re
31
  from typing import Any
32
 
 
33
  import gradio as gr
 
34
  import spaces
35
  import torch
36
  from PIL import Image
@@ -94,6 +96,51 @@ model = AutoModelForImageTextToText.from_pretrained(
94
  ).eval()
95
 
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  def _clean_json(text: str) -> str:
98
  """Drop <think> blocks, code fences and prose; keep the JSON object.
99
 
@@ -121,7 +168,8 @@ def _clean_json(text: str) -> str:
121
  def _build_messages(prompt: str, image_path: str | None) -> list[dict]:
122
  content: list[dict] = []
123
  if image_path:
124
- content.append({"type": "image", "image": Image.open(image_path).convert("RGB")})
 
125
  content.append({"type": "text", "text": prompt})
126
  return [{"role": "user", "content": content}]
127
 
 
30
  import re
31
  from typing import Any
32
 
33
+ import cv2
34
  import gradio as gr
35
+ import numpy as np
36
  import spaces
37
  import torch
38
  from PIL import Image
 
96
  ).eval()
97
 
98
 
99
+ def _preprocess_image(pil_img: Image.Image) -> Image.Image:
100
+ """Deskew + auto-rotate a scanned document before extraction.
101
+
102
+ Scanned POs and spec sheets — the high-frequency input routed to this Space —
103
+ often arrive slightly tilted from a phone camera or flatbed scanner, which hurts
104
+ NuExtract's field extraction. Mirrors the VLM Space's preprocessing exactly:
105
+
106
+ 1. Convert to grayscale and threshold to isolate text/content.
107
+ 2. Find the dominant skew angle via Hough lines and rotate to correct it.
108
+ 3. Return as RGB PIL Image (unchanged if preprocessing fails for any reason).
109
+ """
110
+ try:
111
+ img = np.array(pil_img.convert("RGB"))
112
+ gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
113
+ _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
114
+
115
+ # Detect lines to estimate skew angle
116
+ lines = cv2.HoughLinesP(binary, 1, np.pi / 180, threshold=100, minLineLength=100, maxLineGap=10)
117
+ if lines is None or len(lines) == 0:
118
+ return pil_img
119
+
120
+ angles = []
121
+ for line in lines:
122
+ x1, y1, x2, y2 = line[0]
123
+ if x2 != x1:
124
+ angles.append(np.degrees(np.arctan2(y2 - y1, x2 - x1)))
125
+
126
+ if not angles:
127
+ return pil_img
128
+
129
+ # Median angle — robust against outlier lines
130
+ skew = float(np.median(angles))
131
+ # Only correct small skews (> 0.5° and < 45°) to avoid false rotations
132
+ if abs(skew) < 0.5 or abs(skew) > 45:
133
+ return pil_img
134
+
135
+ h, w = img.shape[:2]
136
+ center = (w / 2, h / 2)
137
+ M = cv2.getRotationMatrix2D(center, skew, 1.0)
138
+ rotated = cv2.warpAffine(img, M, (w, h), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REPLICATE)
139
+ return Image.fromarray(rotated)
140
+ except Exception:
141
+ return pil_img
142
+
143
+
144
  def _clean_json(text: str) -> str:
145
  """Drop <think> blocks, code fences and prose; keep the JSON object.
146
 
 
168
  def _build_messages(prompt: str, image_path: str | None) -> list[dict]:
169
  content: list[dict] = []
170
  if image_path:
171
+ img = _preprocess_image(Image.open(image_path).convert("RGB"))
172
+ content.append({"type": "image", "image": img})
173
  content.append({"type": "text", "text": prompt})
174
  return [{"role": "user", "content": content}]
175
 
requirements.txt CHANGED
@@ -10,3 +10,4 @@ transformers>=4.57.0
10
  accelerate>=1.0
11
  torchvision
12
  pillow
 
 
10
  accelerate>=1.0
11
  torchvision
12
  pillow
13
+ opencv-python-headless