hung-k-nguyen commited on
Commit
da75fa3
·
0 Parent(s):

form-field-v1

Browse files
.gitattributes ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ examples/city-of-mesquite-front-carport-application.jpg filter=lfs diff=lfs merge=lfs -text
37
+ examples/global-electronics-employee-information-form.png filter=lfs diff=lfs merge=lfs -text
38
+ examples/great-west-life-notice-of-claim.jpg filter=lfs diff=lfs merge=lfs -text
39
+ examples/us-direct-deposit-sign-up-form-1199a.jpg filter=lfs diff=lfs merge=lfs -text
40
+ examples/acme-credit-card-application.png filter=lfs diff=lfs merge=lfs -text
41
+ examples/filled-service-tiers.jpg filter=lfs diff=lfs merge=lfs -text
42
+ examples/filled-substitution-request.jpg filter=lfs diff=lfs merge=lfs -text
43
+ examples/handwritten-diploma-order.jpg filter=lfs diff=lfs merge=lfs -text
44
+ examples/handwritten-nursing-licensure.jpg filter=lfs diff=lfs merge=lfs -text
45
+ examples/handwritten-police-records.jpg filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: form-field-v1 detector demo
3
+ emoji: 🖼️
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: gradio
7
+ sdk_version: 6.20.0
8
+ app_file: app.py
9
+ pinned: false
10
+ license: other
11
+ short_description: Detect Text/Choice/Signature fields on a form page
12
+ ---
13
+
14
+ # form-field-v1 · detector demo
15
+
16
+ Upload a form page and detect its **Text**, **Choice** (checkbox/radio) and **Signature** fields with the
17
+ `form-field-v1` detectors — choose **nano** (open), **small**, or **medium** (best). Runs the verified ONNX via
18
+ ONNX Runtime on ZeroGPU.
19
+
20
+ - 🏆 [Leaderboard](https://huggingface.co/spaces/nutrientdocs/form-field-v1-leaderboard)
21
+ - 📊 [Benchmark](https://huggingface.co/datasets/nutrientdocs/form-field-v1-benchmark)
22
+
23
+ The **nano** model is open (Apache-2.0). The **small** and **medium** models are commercial; their weights are
24
+ served from a private repository and are not included in this Space.
25
+
26
+ ## About the author
27
+
28
+ <a href="https://nutrient.io/"><img src="https://avatars2.githubusercontent.com/u/1527679?v=3&s=200" height="80" /></a>
29
+
30
+ This project is maintained and funded by [Nutrient](https://nutrient.io/) - The deterministic document infrastructure enterprises run their highest-stakes workflows on: replayable output, clear exceptions, and full audit trails on the messy, regulated documents where AI alone breaks.
app.py ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """form-field-v1 detector demo — nano/small/medium run as verified ONNX via ONNX Runtime. Field STATE
2
+ (filled/empty, checked/unchecked, signed/blank) comes from the medium 6-class head when available, and an
3
+ ink-presence check that rescues filled fields the head misses (and supplies state for the 3-class models).
4
+ ZeroGPU (CUDA) with CPU fallback."""
5
+ import os, json
6
+ import numpy as np
7
+ import cv2
8
+ from PIL import Image, ImageDraw, ImageFont
9
+ from huggingface_hub import hf_hub_download
10
+ import onnxruntime as ort
11
+ import gradio as gr
12
+ try:
13
+ import spaces
14
+ except Exception: # local dev without the spaces shim
15
+ class _S:
16
+ def GPU(self, *a, **k):
17
+ def deco(f): return f
18
+ return deco
19
+ spaces = _S()
20
+
21
+ TOKEN = os.environ.get("HF_TOKEN")
22
+ CLASSES = ["Text", "ChoiceButton", "Signature"]
23
+ COLORS = {"Text": (29, 95, 168), "ChoiceButton": (22, 121, 79), "Signature": (176, 84, 20)}
24
+ # medium is a 6-class coarse3_state head: raw index -> (coarse type, state). nano/small are 3-class (no state).
25
+ RF_STATE = {0: ("ChoiceButton", "checked"), 1: ("ChoiceButton", "unchecked"),
26
+ 2: ("Signature", "blank"), 3: ("Signature", "signed"),
27
+ 4: ("Text", "empty"), 5: ("Text", "filled")}
28
+ FILLED_OF = {"Text": "filled", "ChoiceButton": "checked", "Signature": "signed"}
29
+ EMPTY_OF = {"Text": "empty", "ChoiceButton": "unchecked", "Signature": "blank"}
30
+ POS = {"filled", "checked", "signed"}
31
+ MODELS = {
32
+ "nano · open · 0.9M": {"repo": "nutrientdocs/form-field-v1-nano", "kind": "yolox", "size": 640},
33
+ "small · commercial · 8.9M": {"repo": "nutrientdocs/form-field-v1-small-private", "kind": "yolox", "size": 896},
34
+ "medium · commercial · 34M": {"repo": "nutrientdocs/form-field-v1-medium-private", "kind": "rfdetr", "size": 1216},
35
+ }
36
+ _SESS = {}
37
+ _MEAN = np.array([0.485, 0.456, 0.406], np.float32); _STD = np.array([0.229, 0.224, 0.225], np.float32)
38
+
39
+
40
+ def has_ink(pil_crop):
41
+ """Training-free ink presence: local-adaptive contrast (any polarity -> white/colored/dark/gradient fills) +
42
+ morphological removal of the box frame / underline (so a mark touching the border survives) + component filter."""
43
+ a = np.asarray(pil_crop.convert("RGB"), np.uint8); H, W = a.shape[:2]
44
+ if H < 6 or W < 6: return False
45
+ gray = cv2.cvtColor(a, cv2.COLOR_RGB2GRAY).astype(np.int16)
46
+ blk = max(9, (min(H, W)) | 1)
47
+ mean = cv2.blur(gray.astype(np.float32), (blk, blk))
48
+ mask = (np.abs(gray - mean) > 25).astype(np.uint8)
49
+ if mask.sum() == 0: return False
50
+ hk = cv2.getStructuringElement(cv2.MORPH_RECT, (max(8, int(0.55 * W)), 1))
51
+ vk = cv2.getStructuringElement(cv2.MORPH_RECT, (1, max(8, int(0.55 * H))))
52
+ lines = cv2.morphologyEx(mask, cv2.MORPH_OPEN, hk) | cv2.morphologyEx(mask, cv2.MORPH_OPEN, vk)
53
+ mask = cv2.bitwise_and(mask, cv2.bitwise_not(lines))
54
+ n, _lab, stats, _c = cv2.connectedComponentsWithStats(mask, connectivity=8)
55
+ content = 0; kept = 0; floor = max(6, int(0.0008 * H * W))
56
+ for i in range(1, n):
57
+ if stats[i, 4] >= floor: content += stats[i, 4]; kept += 1
58
+ return (content / (H * W)) >= 0.004 and kept >= 1
59
+
60
+
61
+ def _cb_marked(pil_crop, inset=0.22, thr=0.06):
62
+ """Checkbox/radio state: measure ink strictly INSIDE the box frame (inset past the border) vs the paper. An
63
+ empty box's interior is blank; a checked box's mark lives in the interior. Excluding the frame by construction
64
+ means an empty box can't be mistaken for checked (frame ink never counts)."""
65
+ a = np.asarray(pil_crop.convert("RGB"), np.uint8); H, W = a.shape[:2]
66
+ if H < 6 or W < 6: return False
67
+ g = cv2.cvtColor(a, cv2.COLOR_RGB2GRAY)
68
+ bg = np.percentile(g, 90) # paper / fill color
69
+ mx = max(1, int(W * inset)); my = max(1, int(H * inset))
70
+ core = g[my:H - my, mx:W - mx]
71
+ if core.size < 4: return False
72
+ return float((core.astype(np.int16) < bg - 50).mean()) >= thr
73
+
74
+
75
+ def _resolve_state(det, pil):
76
+ """Hybrid: trust the head's positive (filled/checked/signed); otherwise let the ink check rescue strays and
77
+ supply state for the 3-class models. One-directional — the heuristic never downgrades a positive.
78
+ Checkboxes/radios use the interior-fill method; text/signature use the frame-removal component method."""
79
+ t, hs = det["type"], det["state"]
80
+ if hs in POS:
81
+ return hs
82
+ x, y, w, h = det["box"]
83
+ crop = pil.crop((int(x), int(y), int(x + w), int(y + h)))
84
+ ink = _cb_marked(crop) if t == "ChoiceButton" else has_ink(crop)
85
+ return FILLED_OF[t] if ink else EMPTY_OF[t]
86
+
87
+
88
+ def _session(name):
89
+ if name not in _SESS:
90
+ cfg = MODELS[name]
91
+ path = hf_hub_download(cfg["repo"], "model.onnx", token=TOKEN)
92
+ provs = [("CUDAExecutionProvider", {"use_tf32": 0}), "CPUExecutionProvider"]
93
+ _SESS[name] = (ort.InferenceSession(path, providers=provs), cfg)
94
+ return _SESS[name]
95
+
96
+
97
+ def _nms(boxes, scores, iou=0.6):
98
+ if not boxes: return []
99
+ b = np.array(boxes); x1, y1, x2, y2 = b[:, 0], b[:, 1], b[:, 0] + b[:, 2], b[:, 1] + b[:, 3]
100
+ a = (x2 - x1) * (y2 - y1); order = np.array(scores).argsort()[::-1]; keep = []
101
+ while order.size:
102
+ i = order[0]; keep.append(i)
103
+ xx1 = np.maximum(x1[i], x1[order[1:]]); yy1 = np.maximum(y1[i], y1[order[1:]])
104
+ xx2 = np.minimum(x2[i], x2[order[1:]]); yy2 = np.minimum(y2[i], y2[order[1:]])
105
+ w = np.maximum(0, xx2 - xx1); h = np.maximum(0, yy2 - yy1); inter = w * h
106
+ ov = inter / (a[i] + a[order[1:]] - inter + 1e-9); order = order[1:][ov <= iou]
107
+ return keep
108
+
109
+
110
+ def _infer(name, pil, thr):
111
+ sess, cfg = _session(name); W, H = pil.size; S = cfg["size"]
112
+ dets = []
113
+ iname = sess.get_inputs()[0].name
114
+ if cfg["kind"] == "rfdetr":
115
+ x = np.asarray(pil.resize((S, S), Image.BILINEAR), np.float32) / 255.0
116
+ x = ((x - _MEAN) / _STD).transpose(2, 0, 1)[None]
117
+ logits, boxes = sess.run(None, {iname: x}); logits, boxes = logits[0], boxes[0]
118
+ prob = 1 / (1 + np.exp(-logits)); lab = prob.argmax(1); sc = prob.max(1)
119
+ for q in range(len(lab)):
120
+ if sc[q] < thr: continue
121
+ typ, state = RF_STATE[int(lab[q])] # 6-class head: keep the state (filled/empty, checked/…)
122
+ cx, cy, bw, bh = boxes[q]
123
+ dets.append({"type": typ, "state": state, "score": float(sc[q]),
124
+ "box": [(cx-bw/2)*W, (cy-bh/2)*H, bw*W, bh*H]})
125
+ else: # yolox: letterbox, decoded output [N,8] (3-class, no state head)
126
+ r = min(S/W, S/H); nw, nh = int(W*r), int(H*r)
127
+ canvas = np.full((S, S, 3), 114, np.float32)
128
+ canvas[:nh, :nw] = np.asarray(pil.resize((nw, nh), Image.BILINEAR), np.float32)
129
+ out = sess.run(None, {iname: canvas.transpose(2, 0, 1)[None]})[0][0]
130
+ obj = out[:, 4]; cls = out[:, 5:8]; clab = cls.argmax(1); score = obj * cls.max(1)
131
+ for c in (0, 1, 2):
132
+ idx = [i for i in range(len(clab)) if clab[i] == c and score[i] >= thr]
133
+ kb = [[float((out[i, 0]-out[i, 2]/2)/r), float((out[i, 1]-out[i, 3]/2)/r),
134
+ float(out[i, 2]/r), float(out[i, 3]/r)] for i in idx]
135
+ ks = [float(score[i]) for i in idx]
136
+ for j in _nms(kb, ks):
137
+ dets.append({"type": CLASSES[c], "state": None, "score": ks[j], "box": kb[j]})
138
+ return dets
139
+
140
+
141
+ @spaces.GPU(duration=60)
142
+ def detect(image, model_name, threshold):
143
+ if image is None:
144
+ return None, "Upload a form page to detect its fields."
145
+ pil = image.convert("RGB")
146
+ try:
147
+ dets = _infer(model_name, pil, float(threshold))
148
+ except Exception as e:
149
+ return None, f"⚠️ Model unavailable — this Space needs access to the model weights. ({str(e)[:120]})"
150
+ out = pil.copy(); d = ImageDraw.Draw(out)
151
+ try: font = ImageFont.load_default()
152
+ except Exception: font = None
153
+ rows = []
154
+ for det in sorted(dets, key=lambda t: -t["score"]):
155
+ cls, sc = det["type"], det["score"]; x, y, w, h = det["box"]
156
+ state = _resolve_state(det, pil) # hybrid: 6-class head + ink-presence stray-catch
157
+ col = COLORS[cls]; d.rectangle([x, y, x+w, y+h], outline=col, width=3)
158
+ d.text((x+2, max(0, y-11)), f"{cls}-{state} {sc:.2f}", fill=col, font=font)
159
+ rows.append({"box": [round(x), round(y), round(w), round(h)], "type": cls,
160
+ "state": state, "score": round(sc, 3)})
161
+ return out, json.dumps(rows, indent=2)
162
+
163
+
164
+ INTRO = """# form-field-v1 · detector demo
165
+ Detect **Text**, **Choice** (checkbox/radio) and **Signature** fields on empty, filled, and handwritten form pages —
166
+ and read each field's **state** (filled/empty, checked/unchecked, signed/blank). State comes from the model on
167
+ `medium`, plus an ink check that catches filled fields the model misses. Just upload a page.
168
+
169
+ - 🏆 [Leaderboard](https://huggingface.co/spaces/nutrientdocs/form-field-v1-leaderboard) ·
170
+ 📊 [Benchmark](https://huggingface.co/datasets/nutrientdocs/form-field-v1-benchmark) ·
171
+ Models: [nano](https://huggingface.co/nutrientdocs/form-field-v1-nano) (open) ·
172
+ [small](https://huggingface.co/nutrientdocs/form-field-v1-small) ·
173
+ [medium](https://huggingface.co/nutrientdocs/form-field-v1-medium) (commercial)
174
+ """
175
+ ABOUT = """## About the author
176
+ <a href="https://nutrient.io/"><img src="https://avatars2.githubusercontent.com/u/1527679?v=3&s=200" height="80" /></a>
177
+
178
+ This project is maintained and funded by [Nutrient](https://nutrient.io/) - The deterministic document infrastructure enterprises run their highest-stakes workflows on: replayable output, clear exceptions, and full audit trails on the messy, regulated documents where AI alone breaks.
179
+ """
180
+
181
+ with gr.Blocks(title="form-field-v1 detector") as demo:
182
+ gr.Markdown(INTRO)
183
+ with gr.Row():
184
+ with gr.Column():
185
+ inp = gr.Image(type="pil", label="Form page")
186
+ model = gr.Dropdown(list(MODELS), value="medium · commercial · 34M", label="Model")
187
+ thr = gr.Slider(0.05, 0.9, value=0.35, step=0.05, label="Confidence threshold")
188
+ btn = gr.Button("Detect fields", variant="primary")
189
+ with gr.Column():
190
+ outimg = gr.Image(type="pil", label="Detections")
191
+ outjson = gr.Code(language="json", label="Fields", lines=8, max_lines=24)
192
+ ex = [[os.path.join("examples", x)] for x in sorted(os.listdir("examples"))] if os.path.isdir("examples") else []
193
+ if ex: gr.Examples(ex, inputs=[inp], run_on_click=False)
194
+ btn.click(detect, [inp, model, thr], [outimg, outjson])
195
+ gr.Markdown(ABOUT)
196
+
197
+ if __name__ == "__main__":
198
+ demo.queue().launch(theme=gr.themes.Soft())
examples/acme-credit-card-application.png ADDED

Git LFS Details

  • SHA256: 44f4552755aeec37c85e48ca4973f729ef64561c08e06b38bf991a9ce8f1aef9
  • Pointer size: 131 Bytes
  • Size of remote file: 548 kB
examples/great-west-life-notice-of-claim.jpg ADDED

Git LFS Details

  • SHA256: d7594847e3650f572e826a6cb2ce0de4799b7e442e43f765ac5d15ea68a27cb6
  • Pointer size: 131 Bytes
  • Size of remote file: 136 kB
examples/handwritten-nursing-licensure.jpg ADDED

Git LFS Details

  • SHA256: 7a12d50a8f778461ac47d8dc0fae5052bd15dadf5c7fe85080c58461aef7be11
  • Pointer size: 131 Bytes
  • Size of remote file: 956 kB
examples/handwritten-police-records.jpg ADDED

Git LFS Details

  • SHA256: 9baabd2a3a160a825f17c46c7b11c3aa6c81f91cd0388626176bae6d6db5be24
  • Pointer size: 131 Bytes
  • Size of remote file: 663 kB
examples/us-direct-deposit-sign-up-form-1199a.jpg ADDED

Git LFS Details

  • SHA256: d4d71b417c4a4e17f14dc239c825dfd097c7c929188edf743b724ca6f1c61503
  • Pointer size: 131 Bytes
  • Size of remote file: 192 kB
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ onnxruntime-gpu==1.20.2
2
+ spaces
3
+ pillow
4
+ numpy
5
+ opencv-python-headless
6
+ huggingface_hub