form-field-v1-state β open-weight
A 96k-parameter filled/empty classifier for detected form fields. Given a cropped field, it reads whether
the field is filled (filled / checked / signed) or empty (empty / unchecked / blank) β a single shared,
detector-agnostic model that supplies state for any of the form-field-v1 detectors. ~0.4 MB, CPU-fast,
Apache-2.0, runs via ONNX Runtime with no custom code.
Part of the form-field-v1 family β pair it with -nano
(open) or -small /
-medium (commercial).
- π― Try it: form-field-v1-demo
- π Leaderboard: form-field-v1-leaderboard
- π Benchmark: form-field-v1-benchmark
What it does
Field detectors locate Text, Choice, and Signature widgets; this model answers the follow-up question
"is that field filled?" on the detected crop. It takes a grayscale field crop, letterboxes it to 40Γ160
(aspect-preserving β form fields are wide, median aspect ~3.3, so a square resize destroys thin printed text),
and outputs a binary empty (0) / filled (1) label. State is mapped per type: Text β filled/empty,
Choice β checked/unchecked, Signature β signed/blank.
Results
Binary field-state accuracy, one shared evaluation.
| Accuracy | |
|---|---|
| form-field-v1-state (this model) | 0.981 |
| Training-free ink heuristic | 0.915 |
On a held-out synthetic split it reaches 0.981 vs a training-free ink-presence heuristic's 0.915. On a real filled form (the ACME credit-card application, 25 fields) it matches the heuristic at 23/25.
What's in this repo
| File | |
|---|---|
model.onnx |
ONNX graph, input 1Γ1Γ40Γ160 grayscale, output logits [n,2] |
state_cnn.pt |
PyTorch state dict (depthwise-separable CNN, 96k params) |
Classes: 0 = empty, 1 = filled.
Usage (ONNX Runtime)
import numpy as np, onnxruntime as ort
from PIL import Image
sess = ort.InferenceSession("model.onnx", providers=["CPUExecutionProvider"])
def letterbox(crop, th=40, tw=160): # aspect-preserving, pad 255 (paper)
g = crop.convert("L"); w, h = g.size
s = min(tw / w, th / h); nw, nh = max(1, round(w * s)), max(1, round(h * s))
out = Image.new("L", (tw, th), 255); out.paste(g.resize((nw, nh), Image.BILINEAR), ((tw - nw) // 2, (th - nh) // 2))
return np.asarray(out, np.uint8)
crop = Image.open("field_crop.png") # a detected field, cropped from the page
x = (((letterbox(crop).astype(np.float32) / 255) - 0.5) / 0.5)[None, None] # normalize, shape [1,1,40,160]
filled = int(sess.run(None, {sess.get_inputs()[0].name: x})[0].argmax(1)[0]) == 1
License & data
Apache-2.0. Trained on synthetic form renders (empty + filled + handwritten field crops, with on-the-fly augmentation for scan robustness); no PII.
About the author
This project is maintained and funded by Nutrient - 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.