← All posts

A compact CRNN for UT measurements: 640k parameters and empty cells

Field notes: a local CRNN on thickness crops, CTC, 640k parameters, the empty-cell conflict, and why a Transformer is not the first step.

A compact CRNN for UT measurements: 640k parameters and empty cells
Contents

On an ultrasonic thickness (UT) draft form, wall thickness is not “text in general.” It is a short handwritten number in an already cropped cell: 12.3, 8.0, or empty. An error in the tenths lands in the plant’s enterprise system (ERP). In the pilot we ask how far a small convolutional-recurrent net can go on those crops — no cloud, no universal transformer, no “ready for the shop floor” claim.

This is a sequel, not a replacement. The whole-sheet loop — geometry, a vision model, confidence, operator review — is in why one model is not enough. Digit-versus-sequence mechanics and when TrOCR is justified are in from convolution to Transformer. Here the subject is the pilot’s MeasurementCrnn head: input 1 × 48 × 160, alphabet 0123456789,, about 640 thousand trainable parameters, and the lesson the net learned on empty cells.

The repository is private; the public outline is on the portfolio page. No customer forms below. Run numbers are pilot measurements, not an acceptance spec.

Key takeaways

This is not universal OCR. The template is known, the cell is already cropped, the alphabet is narrow. A general model on the full page is the wrong first step.

CRNN is not a rival to convolution; it is convolution plus sequence. A CNN lifts strokes, a bidirectional LSTM keeps digit order, CTC removes the need to box every character.

About 640 thousand parameters are enough for a working baseline. Most of the weights sit in the recurrent part, not in a giant encoder.

Exact-match is stricter than character accuracy. Four correct digits out of five still produce a wrong thickness.

An empty cell is a separate task. One head that jointly answers “is there a number?” and “which number?” cuts false alarms at the cost of filled-cell quality.

You do not need a model per field. Split by visual contract: measurement, printed date, printed text, handwritten comment, empty versus filled.

Bring in a Transformer as a fallback after the compact limit is measured — not as the starting stack.

Known geometry is not universal OCR

In the pilot the form is already taken apart by local computer vision (CV): perspective, table, cell map, crops. By measurement time we usually look not at a page photo but at a small rectangle that should hold a number or nothing.

That changes the contract. Universal optical character recognition (OCR) must find text anywhere. We already know the field coordinates. So we can build a narrow loop: crop → empty or filled → read contents → check a thickness range → a structured field. One network is not asked to find the table, read a surname, and guess tenths at once.

flowchart TB
  form[UT_form]
  form --> layout[Layout_and_crop]
  layout --> empty[Empty_or_value]
  empty -->|empty| skip[No_OCR]
  empty -->|value| crnn[Measurement_CRNN]
  crnn --> rules[Range_and_format]
  rules --> json[Structured_field]

Printed columns (zone, diameter, element type) and the header are different visual jobs. They belong to printed OCR and ordinary rules, not to a wider measurement alphabet. The same “geometry first, model second” idea is in the system article; here it shrinks to one measurement cell.

Why a compact model before a Transformer

The pilot already has cloud vision models on cropped fragments. They are a strong, paid loop. A compact CRNN answers a different question: can we read a measurement inside the plant, on a CPU, without sending the form to an external API, with a predictable artifact size and our own weight version.

We do not claim a convolutional-recurrent net is “better than a transformer in general.” The claim is narrower: first measure a simple specialized option. Grow the architecture when the baseline hits a ceiling, not because “that is what 2026 does.” For a narrow alphabet and a short string that is more honest than renting a GPU to fine-tune TrOCR on day one.

A local loop is maintainable: the checkpoint sits next to the code, the alphabet is frozen, the input size is known. There is no per-request fee. The cost is the data and experimental discipline. That is closer to dataset engineering than to picking “the most modern” net.

Convolution, bidirectional memory, and CTC in one head

A convolutional net (CNN) looks for visual features: lines and strokes, then bends, then digit-like shapes. This is not a “Compact Neural Network”: compact here is about model size; CNN is about convolution.

Image classification is not enough. A measurement is a sequence: 5 → 8 → 3 → 2 → 1, often with a comma. The net must remember what it already read on the left and not scramble order. That is LSTM (Long Short-Term Memory): a recurrent layer that carries previous steps into the present. In code bidirectional=True, so memory looks both ways: a later digit helps disambiguate an earlier one when the handwriting is doubtful.

CRNN (Convolutional Recurrent Neural Network) in the pilot is exactly that chain:

crop
  → CNN (features)
  → BiLSTM (order)
  → Linear (12 classes)
  → CTC
  → "12,3"

CTC (Connectionist Temporal Classification) means we do not label every digit’s edges. A pair “cell image → string” is enough. At each time step the net outputs a distribution over symbols plus a service blank. The decoder collapses repeats and drops empty steps: 5 5 blank 8 8 blank 3 becomes 583. In code BLANK_INDEX = 0, the loss is CTCLoss. On a small set the net otherwise learns to predict only blanks: the blank class bias is shifted negative (-2.0) so digits have a chance in the first epochs.

The tutorial CTC walkthrough and the TrOCR comparison live in the sibling digit article. Here the point is that the chain is wired to a measurement cell, not to an abstract benchmark.

MeasurementCrnn: 48×160 and six hundred thousand parameters

The pilot net takes a single-channel tensor 1 × 48 × 160. Four blocks of convolution → normalization → ReLU → pooling. The last pool collapses height to one pixel and leaves 20 steps along width — the time axis for the LSTM. Then two bidirectional layers, hidden_size = 96, dropout = 0.2. A linear head: 12 classes — 11 symbols of 0123456789, plus blank.

A decimal point in the source becomes a comma: normalize_label(" 7.3 ") yields 7,3. That is a thickness-domain rule, not “one more class.”

Trainable parameters are on the order of 640 thousand. Roughly: convolution ≈ 241k, bidirectional LSTM ≈ 396k, linear layer ≈ 2k. Most weights sit in the sequence, and the model is still tiny next to a vision transformer.

Parameters are not stored pictures and not a lookup of ready answers. They are numeric coefficients fitted on “crop → string” pairs. After training, measurement-crnn.pt stores weights, alphabet, hidden_size, and input size. The architecture loads on CPU and runs new cells without retraining. Training is periodic; inference runs on every new form.

How we prepare a crop and why we augment

A raw cell crop is not fed as-is. The code path: grayscale, autocontrast, scale with margins, a 48 × 160 canvas with centering, brightness inversion (the stroke becomes light on dark), tensor. Training adds light augmentation: ±3° rotation, a few pixels of shift, contrast and brightness in a narrow band.

Handwriting, pen thickness, phone shadow, and the grid line change from sheet to sheet. If the net memorizes two hundred crops with no variation, the third handwriting style surprises it. Augmentation does not replace form diversity — it stops the model from treating one scanner as the only true world.

The loader splits with a bind to the source sheet and field, not a random pile of files. Similar cells from one form should not sit quietly in both train and validation — the same discipline as in the dataset-leakage article.

First run: exact-match versus character accuracy

The first compact run in the pilot: about a thousand numeric values, training around 19 minutes, 70.9% full-string match and 89% character accuracy. That is not “the model is 89% shop-ready.” Those are two contracts.

Reference 58321, prediction 58327: four of five characters are right, the measurement as a whole is wrong. Thickness in millimetres lives as a whole value. The operator and the ERP need the string, not the average share of guessed signs.

A rough illustration, not a forecast of our net: if each character were independent at 0.89, five in a row would give about 0.89⁵ ≈ 56% exact match. In practice characters correlate, lengths vary, the comma is critical — but the direction is right: exact-match is always stricter than a per-character score. For industrial OCR you watch it separately, with errors by length and by digit.

Later, on a wider set of filled cells, exact match rose (in one digits-only run — 85.4%). Those are still pilot measurements on our crops, not a promise to someone else’s archive.

An empty cell breaks the recognizer

An older head trained almost only on filled measurements produced 100% false positives on empty cells. The ghosts are familiar: 8,0, 8,9, 5,0, 11,1. A grid line, dust, a pen trace from the next cell look like “almost a digit” to OCR.

The pipeline already has a content heuristic (ink, ditto, strike-through, text). It reduces how many empty crops reach OCR at all. But if the recognizer still looks at an empty field, it must say something — and it says a plausible thickness.

An experiment with negatives: 75 reviewed sheets; train 10,960 crops, val 1,977; among training 1,912 numbers and 191 empty cells (default empty ratio in the loader is 0.10). Pilot comparison:

Run Digits, exact match Empty-cell false positives
Baseline, almost no empties 85.4% 100%
From scratch, with empties 78.6% 0.1%
Fine-tune on empties 83.2% 0.5%

Negatives smash the ghosts and take quality on filled cells. One CRNN starts solving two conflicting tasks: “is there a value here?” and “which value?” Weights that learn to ignore the grid read a thin tail of a nine worse.

The next step, still open: a tiny EMPTY / VALUE classifier before the measurement head. Empty — do not feed OCR. Filled — you can restore a more “numeric” checkpoint (measurement-crnn.prev.pt) without a compromise inside one head. Same lesson as hard negatives on radiographs: they are their own queue, not “one more class inside the same net.” How we already classify empty, ditto, and strike-through in CV is the follow-up note.

We keep experimental weights apart: previous best, from-scratch run, fine-tune. Otherwise in two weeks you cannot say honestly what the new set broke.

One model per task type, not per field

The industrial-form temptation is a separate net for the date, the surname, each measurement column. That is four deploys and the same confusion in four copies. Cut by visual contract.

Task type In the pilot
Handwritten measurement MeasurementCrnn + thickness range check
Empty / filled ink heuristic now; a tiny dedicated classifier is next
Printed date printed OCR + day 1–31, month 1–12, a sane year
Printed header and meta shared printed OCR
Handwritten comment / name another task, another alphabet; do not widen the measurement head
Tick, ditto, strike-through rules / a small classifier

The network reads what you cannot reliably compute. Ordinary code checks what is deterministic. A date 12.09.2026 does not need a second CRNN: digits and rules are enough. A handwritten surname, conversely, must not ride in an alphabet of ten digits and a comma.

A transformer is warranted when the compact limit is measured and still short: mixed document types, weak layout predictability, a wide alphabet, enough data. The practical scheme is not “heavy OCR first,” but a specialized head → confidence → on low confidence a heavy loop or a human.

Checkpoint, training, and plant metrics

Training in the pilot: AdamW, learning rate 1e-3, weight decay 1e-4, batch of 16, gradient clipping, ReduceLROnPlateau, early stopping. Boring and enough. An epoch is a full pass over train; the best snapshot is written when validation exact-match grows.

Inference (ocr_crnn.py) loads the checkpoint on CPU, greedy-decodes, normalizes thickness, and may return empty on low confidence. The handwriting engine switches by environment: PaddleOCR / EasyOCR by default, CRNN as a separate mode. That honesty matters: the compact net is not yet the only path in the pilot. The sheet loop still holds CV, cloud passes, and the operator report.

Metrics that matter on the floor are wider than accuracy: exact match, character score, empty false positives, misses on filled cells, share of manual review, confidence, quality by string length, CPU time. The cost of error is the same as in testing economics: a missed thickness is more expensive than an extra “look with eyes.”

The dataset outranks head size. Hands, scanners and phones, nasty real crops, empty cells, hard negatives, split by sheet not by file. Growing the model does not fix a holey set — already true for golden records in general.

What to try today

If you have similar forms with a fixed grid, do not start by choosing “CRNN or TrOCR.”

Crop 200–500 measurement cells with labels from the system of record, including empties. Split by sheet, not by random files.

Build the narrowest head you can explain: a fixed-size canvas, digits plus a separator, CTC. Save the checkpoint with alphabet and input size.

Count exact match, character accuracy, and empty false positives separately. If empties “draw” a thickness — do not fine-tune the same head forever: put an empty/value gate before OCR.

FAQ

Why a CRNN if we already have a vision model on fragments?

The cloud model covers the header, rare text, and doubtful cells at the cost of a request and a data perimeter. The compact head covers bulk measurements locally. In the pilot those are different loops, not mutually exclusive religions.

Why not train TrOCR immediately?

Because you first need a measurable limit of the narrow task. TrOCR is justified when alphabet and layout stop being narrow. The sibling article is about that threshold; here the threshold is still open because of empty cells and handwriting variety.

Is 70.9% exact match low or normal?

For shop-floor acceptance — low. For a first compact run on a thousand values — an honest baseline that shows where errors go: last digit, comma, empty fields.

Can one net read both a measurement and a surname?

Technically yes; practically that is another alphabet and another length. A measurement head with a comma should not “just a bit” become universal OCR. A separate handwriting loop waits for a separate set.

Why are empty false positives more important than another 2% on digits?

An empty cell turned into 8,0 looks like a confident measurement. The operator may not notice. An unread filled cell more often lands in “review.” A silent hallucination is more dangerous than an explicit miss.

Do we need a GPU to use this?

For inference in the pilot — no, the checkpoint loads on CPU. Training a thousand crops fitted in tens of minutes. Renting a large GPU makes sense later, for a general model, if the compact limit is exhausted.

Further reading

The form-level loop, a human in the loop, and why you must not feed the whole sheet to one model — UT field notes. CNN, CTC, and when to take TrOCRfrom convolution to Transformer. What to do with empty cells, dittos, and strike-through before OCR — the cell-content classifier. How not to mix train and eval — dataset engineering. Why to count the cost of a miss — testing economics. The experiment loop after the baseline run — quality does not grow from hitting train. A sibling industrial case of “small net, high cost of error” — AI on weld radiographs.

Conclusion

A fixed UT grid lets you skip universal OCR. A compact CRNN with CTC, a 48 × 160 input, and about 640 thousand parameters gives an honest local baseline on handwritten measurements. Quality then hits empty cells, handwriting variety, and a split of tasks: empty/value classifier, date rules, a printed loop, a human on the tail.

A practical step: take crops from one measurement column, forbid a universal model on that subset, and measure exact match and ghosts on empties. The list of confident mistakes will tell you more about your archive than an architecture table. In the pilot this is still an open loop: the measurement head lives as an engine mode, the empty classifier is ahead, ERP is phase two.