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).

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.

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support

Space using nutrientdocs/form-field-v1-state 1