Stock OCR training sets teach a classifier to read clean, upright, evenly spaced print. Your camera sees dot-peen die marks, laser-etched lot codes, inkjet spatter and a house font nobody else publishes. The fix is to build the set from the print itself: crop single characters out of your own images, squeeze every crop onto a fixed 16 × 8 binary grid, and write one tab-separated line per sample — label, metadata, then 128 pixel values. The sections below give the pixel maths that sizes the camera, the line format, the crop-to-file workflow, the sample counts, the augmentation and the checks that catch a broken file before you waste a training run.
Why build OCR training data instead of using a generic font set?
Build your own set when the print your camera must read is not a standard typeface: dot-peened die marks, laser-etched lot codes, thermal-transfer text at 2 mm, or a house font whose 6 and 8 differ from every standard glyph. A classifier trained on your crops learns exactly those shapes.
Generic sets render clean glyphs: even strokes, perfect spacing, no glare. Real print breaks all three. Dot-peen is a row of dots, laser etch changes width with focus and power, inkjet adds spatter a generic classifier has never seen. Tuning light and exposure recovers part of that gap, and which typefaces machines read reliably explains why some faces survive and others never will.
Set a numeric bar before you start. If the built-in OCR on a fixed code reader reads your print at 99 percent or better in bench trials, tune it and skip this job. Below 99 percent — more than one missed read in a hundred — the custom set pays for itself. Curved inner-cap text is the classic case, and training a custom OCR font for curved inner-cap text walks the same crop-and-train path for that geometry.
How many pixels does a character need before you crop it?
Give every character at least 25 px of height in the raw image, because below roughly 12 px the strokes of a 6, an 8 or a slash merge into blobs that no amount of training data can separate. For a 2 mm font that target is 12.5 px per mm of part.
The floor has arithmetic behind it. A medium-weight 2 mm face has strokes near 0.3 mm, so at 12.5 px per mm a stroke is 12.5 × 0.3 ≈ 4 px wide — enough for a threshold to hold it as one solid run. At 6 px per mm the same stroke is 2 px, and one gain step erodes it. Below 12 px of character height the problem is physics, not your dataset, and why small text fails OCR picks up from there.
Worked example: point the LS-CRD-0028 at the code strip, because it captures 2560 × 2048 px. Spread that frame over a 24 mm ROI height and you get 2048 / 24 ≈ 85 px per mm, so a 2.0 mm character fills 85 × 2.0 ≈ 171 px — almost 7 times the 25 px floor. The same 2048 rows reach the floor exactly at a 163.8 mm ROI (2048 / 12.5), which sets the widest strip this frame covers at 2 mm font height.
- Interface
- Fast Ethernet
- Resolution
- 5 MP
- Sensor format
- 1/1.7"
- Shutter
- Global
What goes into one line of the training file?
One training line is one character: a label field, five more metadata fields, then the 128 pixel values of a 16 × 8 grid — 134 tab-separated fields of plain ASCII that any text editor opens and your own script writes and reads.
| Field | Content | Example |
|---|---|---|
| 1 | label, the character itself | 7 |
| 2 | sample number within the class | 014 |
| 3 | source id: camera, session, print variant | L2 |
| 4 | grid width in pixels | 8 |
| 5 | grid height in pixels | 16 |
| 6 | rotation applied, degrees | 0 |
| 7 to 134 | 128 pixel values, row-major: 16 rows of 8 | 0 0 1 1 0 0 0 0 |
Fields 4 and 5 make the file self-describing: a validator reads them from line one and refuses any line whose value count disagrees. Field 3 saves a weekend later — when one class reads badly, you trace its weak samples back to the session and print variant that produced them.
The writing side is one join:
label = "7" # str: the character
sample = "014" # str: index within the class
source = "L2" # str: camera + session + print variant
rotation = "0" # str: degrees applied
grid_w, grid_h = "8", "16" # str: fixed grid, pixels
line = "\t".join([label, sample, source, grid_w, grid_h, rotation] + px_values)
# px_values: list[str] of 128 entries, each "0" or "1", row-major
No header, no binary container: one sample, one line, appended as you crop. A 3,000-line set runs about 270 bytes per line, or 0.8 MB of text (3,000 × 270 = 810,000 bytes).
How do you crop and grid each character?
Crop the tight bounding box around each glyph, then squeeze that crop onto the fixed grid with a nearest-neighbour resize to 16 rows and 8 columns, so every sample in the file, whatever its original size, becomes exactly 128 values.
- Collect production frames: at least 30 per character class, spread over two or more sessions so lamp warm-up, part batches and focus drift all land in the set.
- Binarise each frame with Otsu on lightness, or the fixed threshold you proved on the bench; automatic threshold segmentation with Otsu's method shows the stable version.
- Find each glyph with connected components, or with the production layout window if the text sits in a known strip; layout segmentation for single numeric readings covers that case.
- Tight-crop each glyph to its bounding box plus a 1 px margin.
- Resize to the fixed grid with nearest-neighbour interpolation so values stay 0 and 1.
- Append one line per glyph with the join shown above.
Pick the grid from your glyphs, not from this guide. Measure the mean bounding box across the crops, then:
Digits drawn two tall to one wide give 8 columns and 134 fields. A squarer alphanumeric face at a 0.62 width-to-height ratio gives 10 columns, 160 values, 166 fields. Decide once, before the first export.
How many samples per character are enough?
Start with 30 real crops per character class — ten digit classes make 300 lines — because a fixed-grid template or nearest-neighbour classifier only needs each class's spread covered, and 30 samples across two or three sessions covers it.
Scale with the classifier and the alphabet. Template matching and nearest neighbour over 128 binary features fit comfortably on tens of samples per class. A small network with 128 inputs wants 200 or more per class before it stops memorising. Confusable pairs — 5 against S, 6 against 8, 0 against O and D, 1 against 7 — deserve 100 each whatever the classifier, because they fill the misread log.
Totals to plan against: digits-only inspection, 10 × 30 = 300 real crops. Full alphanumeric, 36 × 30 = 1,080, call it 1,100. With the common symbols, 44 × 30 = 1,320. Those are real crops, before augmentation.
How do you stretch one crop session across the print variations?
Multiply each real crop into 20 to 50 variants — rotations of ±10° in 2° steps, one-pixel erode and dilate, threshold re-runs 10 percent up and down — so 300 real crops become 6,000 to 15,000 training lines covering what the line actually prints.
Each variant maps to a real cause. Rotation stands in for part skew in the fixture, erode and dilate for ink gain and a worn stylus, the threshold sweep for exposure drift between shifts. Rotations alone give 11 crops (−10 to +10 in steps of 2), and 11 rotations × 3 morphology states = 33 per real crop, so 300 real crops yield about 10,000 lines — roughly 2.7 MB of text at 270 bytes per line.
Synthetic variants never replace genuine bad print: crop real samples of the worst glyphs the line produces — first article after startup, faded ribbon, dirty window — and keep them in the set unchanged.
How do you check the training file before training on it?
Before any training run, split every line on tabs and check two things: each line yields exactly 134 fields, and the 128 values rendered eight per row as text show a character your eyes can read.
The render is the whole quality gate in one step — write the 128 values into 16 rows of 8 and look:
.######.
......#.
......#.
.....##.
.....#..
....##..
....#...
...##...
...#....
..##....
..#.....
.##.....
.#......
#.......
........
........
That is a 7 you can read, so the gridding, the row-major order and the tab join are all correct. When a file renders as noise, one of those three is broken.
Then the counts. A one-pass script splits each line, checks the field count and totals lines per label: every class at 30 or more, confusable pairs at 100 or more. Last, the holdout: set aside every fifth real crop — 24 train and 6 test per class of 30, an 80/20 split — and let each crop's variants follow it to the same side. Splitting variants of one crop across the two sides inflates the test score, because near-copies on both sides stop the test measuring anything.
Which camera takes the crops?
Take the crops with the same camera, lens, light and working distance that will read the characters in production, because the classifier must see the same blur, glare and stroke width it will meet on the line.
Two routes work. The reader route hangs a fixed code reader over the strip, tries its built-in OCR first, and exports crops from the very frames it inspects. The LS-CRD-0047 reads at 60-120 mm with its 1.2 MP sensor, and the MV-ID813M-05S-WBN-SR brings its own white light and a 4.9 mm lens at 1.3 MP; both sit close to tight print.
- Interface
- GigE
- Resolution
- Up to 3 MP
- Resolution
- Up to 3 MP
- Shutter
- Global
For a longer stand-off, the MV-ID3013PM-12M-WBN carries a 12 mm lens over a 1.3 MP sensor.
- Interface
- Fast Ethernet
- Resolution
- Up to 3 MP
- Shutter
- Global
- Lens mount
- M12
The camera route suits a classifier you write yourself. A smart camera such as the MV-SC5060XM-08M-WBN pairs a 6 MP mono sensor with an 8 mm lens on M12, and mono keeps the threshold honest.
- Interface
- GigE
- Resolution
- 6-10 MP
- Sensor format
- 1/2.4"
- Shutter
- Rolling
A rolling shutter sensor smears text moving fast across the frame, so pause the belt or strobe the light on those mounts. When the classifier must run on the device itself, an X86-platform model such as the LS-SMC-0017 combines an 8.8 MP sensor, a 1" format and a C-mount, so the production lens carries straight over.
The configurator matches a camera, lens, light, light controller and cables to each other, step by step, and lists only parts that fit what you already picked.
One rule decides the choice either way. Whatever you mount, hold the 12.5 px per mm from the earlier maths, and take the crops from frames that look exactly like production frames.
When should you skip building a custom set?
Skip the custom set when a standard font face or the reader's built-in OCR already decodes the print in bench trials — OCR-A and OCR-B faces, clean thermal transfer, anything a stock engine reads first time — because your cropped set adds work, not accuracy.
Two more skips matter. If the typeface is about to change with a rebrand or a new label template, build nothing until the artwork freezes, or you will rebuild the set within the month. And if the failures are placement rather than glyph shape — text drifting out of the inspection window, rows at the wrong height — the set will not help; fix the window, and OCR on a fixed code reader with several text strings in one image shows how readers handle multiple strings at once.
Once decoding works, the text usually feeds a database or a PLC, and splitting the decoded string into lot and part number fields is the next step downstream.
The decision: if the built-in OCR misses more than one read in a hundred, build the set — 30 real crops per class from the production camera, 20 to 50 augmented variants each, one 134-field line per sample, every fifth crop held out for testing. Pick the camera that delivers 12.5 px per mm across your code strip in the camera finder, or send a photo of your worst print through the contact page first — if a stock engine reads it, you have saved the entire job. The reading side continues on the code-reading topic page.
Questions
Can I store grayscale values instead of 0 and 1?
Yes. Replace each 0/1 with its 0-255 byte and keep the field count the same; the line roughly doubles in size, so a 3,000-line file grows from about 0.8 MB to about 1.6 MB. Binary grids suit template and nearest-neighbour classifiers; keep grayscale only when strokes fade into the background at the edges and the threshold decision is what you need the classifier to learn.
How do I handle curved or tilted parts?
Rotate each glyph upright before it reaches the grid: the 16 × 8 layout assumes an upright character, and a 20° tilt smears strokes across several grid columns. Augmentation covers ±10°, so anything beyond that needs a per-glyph deskew in the cropping script. For curved caps, rectify the text band to a flat strip first — the approach in curved inner-cap OCR font training does exactly this before training.
Can I add a character class after training?
Yes — the format takes appended lines with no rebuild. Crop at least 30 new samples of the missing glyph, add them with the same field count, and keep every class within about a factor of two of the others so the classifier does not drift toward the large classes. Nearest-neighbour and template classifiers take the new class immediately; a neural network needs a retrain.
Does a fixed code reader train on this file?
No. A reader's built-in OCR tunes its own font library through its own settings and never imports this file. The tab-separated grid feeds a classifier you build yourself — template matching, nearest neighbour or a small network — running on a PC or an X86-platform smart camera. Try the reader's tuning first; build the custom set only when its accuracy stalls.
Can I synthesise samples by rendering fonts instead of cropping real print?
Both, in useful proportion. Render your house font at two or three weights and sizes, threshold and grid them with the same script, and you get perfectly labelled clean shapes in minutes. Synthetic pixels cannot teach dot-peen depth, ink spatter or glare, so keep real crops as at least half of every class and let synthetic lines fill out the clean baseline.




