Code readingTroubleshooting

OCR character regions not recognized: what reaches the classifier

When OCR characters are not recognized, the region list reaching the classifier is usually at fault, not the font. Count regions, overlay them, fix the handoff.

LumaSpec Vision12 min read Share on LinkedIn

On this page 10 sections
  1. 01Why does trained OCR read most of a string but drop the last characters?
  2. 02What do you check first: the region count or the classifier?
  3. 03Why does a second region filter throw away good characters?
  4. 04Does the classifier expect an inverted image, and which image object do you pass?
  5. 05How do you trace the handoff step by step?
  6. 06How many pixels does a character need before OCR is reliable?
    1. A worked pixel budget: 3 mm digits at 400 mm
  7. 07Why does the camera drop frames while the OCR pipeline runs?
  8. 08When is it really the font training, not the regions?
  9. 09What checks catch dropped characters before shipment?
  10. 10Questions

You trained the font, tuned the threshold, and the reader still returns every character except the last two digits. In a threshold, connect, filter, sort, classify pipeline the classifier is usually innocent: the region list that reaches it is not the list you inspected on screen, so the missing glyphs were never classified at all. This troubleshooting guide walks that handoff stage by stage, sets the pixel budget that decides whether a glyph is readable, and covers the capture path for the frame drops that appear once OCR runs beside a live camera. Work it with one frozen test image and you will find the failing stage in under ten minutes.

Why does trained OCR read most of a string but drop the last characters?

Because those glyphs never reach the classifier: a pair merges into one region that the width filter rejects, or a duplicate filter silently re-filters the set, so the classify step receives fewer regions than the string contains. The standard pipeline has six stages — threshold the gray image, split it into connected components, drop non-glyph regions by size, sort the survivors into reading order, match image polarity to the training set, classify one region at a time.

The failure sits at the final handoff. The overlay on your screen is drawn from one region list while the classify call receives another — an unsorted copy, a twice-filtered copy, a dilated copy. When the dropped digits sit adjacent and repeat, merged blobs lead the suspect list, because identical glyphs printed at the same pitch touch first. For the split itself, read how to separate touching digits before OCR.

What do you check first: the region count or the classifier?

The region count. Print the number of regions after every stage and compare it with the number of glyphs printed on the part; the first stage where the count falls below the glyph count is your fault location, and nothing downstream can recover it.

What you see Most likely cause 30-second test Fix
Count short by exactly the missing glyphs Merged pair rejected by the width limit Overlay the filtered regions; look where the digits vanished Split touching pairs or admit pair-width regions
Count right, same classes wrong in every image Polarity or font mismatch against the training set Push one stored training image through the live code path Match polarity or retrain on live images
Count zero after filtering Second filter ran on the raw component list Print the count after each filter Chain filters output to input; delete the duplicate
Count drifts frame to frame Motion blur or uneven light at the string end Measure stroke width in pixels across the string Shorten exposure, add light, or raise magnification

Count short — work the segmentation. Count right but classes wrong — polarity or training. Count drifting — imaging. That decision covers every row in the table above.

Why does a second region filter throw away good characters?

Because the second filter usually reads the wrong input — the raw connected-component list instead of the already-filtered list — and its bounds belong to a different scale, so it discards exactly the regions the first filter kept. Filters chain output to input; any stage that re-reads an earlier stage's output erases the work between them.

Numbers from one debug make it concrete. The first filter kept regions 50 to 70 px tall and 15 to 45 px wide. Two adjacent digits 25 px wide with a 2 px gap merge into a blob 52 px wide — 25 + 2 + 25 — and the 45 px width limit rejects it: both digits vanish, together, in every image. That is the signature of a merged pair, not of bad training.

The fix has three parts. Chain every filter so its output feeds the next stage, delete the duplicate stage outright, and let merged pairs through on width — up to twice the single-glyph limit plus the gap — then split them before the sort. Keep the search region tight as well, so neighboring print never enters the component list; the ROI discipline in seven segment display OCR applies to any font.

Does the classifier expect an inverted image, and which image object do you pass?

Pass the image whose polarity matches the training set and the same image object the regions were measured on, because a polarity or domain mismatch fails the whole string rather than two digits — which is what separates it from a segmentation drop. When the entire string fails, invert the live frame once and classify again; when training used dark digits on a light background, a light-on-dark part must be inverted before the classify call.

Regions are coordinates; the classifier samples gray values from the image argument you hand it. Pass a smoothed copy, a cropped copy, or the next camera frame, and every sample shifts. Bind the region list to one image variable from extraction to classification — the failure mode in ROI image source binding is the same class of bug in a wider pipeline.

How do you trace the handoff step by step?

Freeze one representative failure image, walk it through the pipeline with a count and an overlay after every stage, and stop at the first stage that loses a glyph; the whole trace runs in about ten minutes on a saved file.

  1. Save one image where digits are missing and work offline, so every run sees identical pixels.
  2. Threshold it, print the region count, and overlay the mask; the count runs high — every glyph plus noise blobs.
  3. Split into connected components and print the count again.
  4. Apply one size filter on height and width; print the count and each region's bounding box in pixels; overlay.
  5. Sort into reading order and overlay the index number at each region.
  6. Match polarity: invert the image if the training set was inverted, and confirm glyphs read dark-on-light on screen.
  7. Classify the sorted region object against that exact image object.
  8. Assert the region count equals the glyph count on the part and archive the labeled overlay.
# Pseudocode: names are ours, not any library's.
img: Frame = load_saved_failure()                  # one frozen image, same file every run
mask: Mask = threshold(img, low=55, high=202)      # example levels for this image
blobs: list[Region] = connected_components(mask)
glyphs: list[Region] = [r for r in blobs
                        if 15 <= r.width_px <= 45 and 50 <= r.height_px <= 70]
glyphs = sort_reading_order(glyphs)                # row bands first, then left to right
assert len(glyphs) == 10                           # glyph count printed on the part
frame: Frame = invert_if_training_was_inverted(img)
classes: list[str] = classify(glyphs, frame)       # same list, same image, every run

How many pixels does a character need before OCR is reliable?

Plan for at least 20 pixels of glyph height, treat 15 pixels as marginal, and expect failures below about 10 pixels: the classifier reads stroke geometry, and sparse sampling cannot tell an 8 from a B or a 5 from a 6. The floor is about sampling density, not software tuning — selecting a valid character image size walks the reasoning for integrated readers, and why small text fails at minimum font sizes traces what happens downstream.

scale = field of view / sensor pixels across the width m = sensor width / field of view f = WD × m / (1 + m) t(max) = 0.5 × scale / part speed DOF ≈ 2 × c × N × (1 + m) / m²

A worked pixel budget: 3 mm digits at 400 mm

Example inputs: digits 3.0 mm tall, a target of 24 pixels per glyph, a 1600 × 1200 sensor with 4.8 µm pixels, working distance 400 mm, part speed 100 mm/s.

  • Scale for the target: 3.0 mm ÷ 24 px = 0.125 mm/px.
  • Field of view: 1600 px × 0.125 mm/px = 200 mm across the track.
  • Sensor width: 1600 × 4.8 µm = 7.68 mm, so magnification m = 7.68 ÷ 200 = 0.0384.
  • Focal length: f = 400 mm × 0.0384 ÷ 1.0384 = 14.8 mm, so fit a 16 mm lens.
  • At 16 mm: m = 16 ÷ (400 − 16) = 0.0417, field of view = 7.68 ÷ 0.0417 = 184 mm, scale = 0.115 mm/px, glyph height = 3.0 ÷ 0.115 = 26 px — six pixels of margin over the floor.
How sensor width, working distance and field of view set the focal length Rays from the edges of the sensor pass through the lens and spread to the edges of the field of view. The ratio of sensor width to field of view is the magnification m.CameraField of view (FOV)Working distance (WD)≈ fsensor width slensm = s / FOVf = WD × m / (1 + m)
How sensor width, working distance and field of view set the focal length. Rays from the edges of the sensor pass through the lens and spread to the edges of the field of view. The ratio of sensor width to field of view is the magnification m.

Motion sets exposure. Hold blur under half a pixel: t = 0.5 × 0.115 mm ÷ 100 mm/s = 0.00058 s, so 575 µs — set 500 µs and recover brightness with a strobe. Depth of field at f/8, with c set to the 4.8 µm pixel: DOF ≈ 2 × 0.0048 mm × 8 × 1.0417 ÷ 0.0417² ≈ 46 mm total, ±23 mm about focus — a thin-lens estimate with one-pixel blur, comfortable for a fixtured part.

The PC side of this budget is served by area scan cameras: the MV-CU020-90GC is a 2.5 MP GigE camera with a global shutter and a C-mount, a clean match for a fixtured OCR station.

Area scan camerasHikrobot MV-CU020-90GC 2.5 MP Area Scan Camera, GigE, GMAX4002, ColorMV-CU020-90GC
Sensor format
1/1.7"
Lens mount
C-mount
Interface
GigE
Resolution
Up to 3 MP
$309.00View partBuild a system with it

Moving the whole pipeline on-device deletes the handoff bug class: segmentation and classification run in one firmware path, and the setup view shows the regions the camera will classify. That side is smart cameras — the LS-SMC-0017 is an 8.8 MP global-shutter model with a C-mount on an x86 platform, and the MV-SC5060XM-08M-WBN pairs a 6 MP mono sensor with an 8 mm M12 lens, its rolling shutter suited to stationary parts.

Smart cameras & vision sensorsSC / SC smart camera (X86 platform), 8.8 MP, USB3, Color/Mono, global shutter (LS-SMC-0017)LS-SMC-0017
Interface
USB 2.0
Resolution
6-10 MP
$1,359.00View part
Smart cameras & vision sensorsHikrobot MV-SC5060XM-08M-WBN SC5000X series Smart Camera, 6 MP, Mono, 8 mm focal length, M12-mountMV-SC5060XM-08M-WBN
Interface
GigE
Resolution
6-10 MP
$1,629.00View part

Run your own part through the lens focal length calculator and the camera resolution calculator.

Lens focal length, field of view and working distance
Units
Solve for
Focal length
– mm
Field of view
– mm
Working distance
– mm
Magnification
– x
Angle of view
– °
Pixel on the part
– mm

Thin-lens model: m = sensor width / field of view, f = WD × m / (1 + m). Accurate to a few percent above about 150 mm; check the lens's minimum object distance.

Find lenses with this focal length Open the full calculator

Why does the camera drop frames while the OCR pipeline runs?

Because acquisition and processing fight for one execution path. Grab through the camera's vision-standard transport — a GigE Vision or USB3 Vision device served by its own acquisition driver or GenTL producer — and hand each completed buffer to the OCR code in a worker thread; a preview window loop is not an acquisition path, and it drops frames whenever classification stalls it.

Two numbers settle the link. A 1 Gb/s link carries about 100 MB/s of usable payload, and a 2.5 MP mono frame at 8 bits is about 2.5 MB, so that link tops out near 40 fps — above that, move to 2.5G, 5G or 10G Ethernet or USB3. Set the network card's frame size to the camera's packet size, 9000 bytes as the common setting, and size the buffer queue so a 40 ms classification on a 20 ms frame period drains by design: finish under 20 ms or inspect every second frame deliberately. When packets arrive but no image shows, GVSP packets arriving while the player shows no image walks the stream debug; a camera that works in setup but goes grey at runtime covers the runtime handoff; cable and bandwidth limits for multigig links sit in 5GBASE-T camera interface: bandwidth, cables and distance.

When is it really the font training, not the regions?

When the region count is correct in every image and the same character class fails wherever it appears: that is a training or polarity fault, and region debugging will not touch it. Confusion pairs give it away — 0 against O, 8 against B, 5 against 6, 2 against Z. Collect at least 20 fresh samples of the failing class across the real print variation — dot gain, worn tooling, ink density — retrain, then prove the read on images held out of the training set. Training a custom OCR font covers the capture, and building OCR training data from cropped character images turns existing good reads into samples. Once the pipeline is sound, multi-capture majority voting smooths read-to-read flicker at the line.

What checks catch dropped characters before shipment?

Three checks on five parts catch nearly every drop: assert the glyph count in code, archive the labeled overlay, and measure the smallest glyph at 20 pixels or more. Run them at every release and after any optics change.

  • Assert the glyph count before the classify call — a short list must stop the part, not ship a wrong string.
  • Archive one labeled overlay per shift — it settles any later dispute in minutes.
  • Measure the smallest glyph on the golden image — under 15 pixels, add magnification before adding software.
  • Re-check exposure against line speed at every speed change — the half-pixel budget moves directly with speed.
  • Keep one inverted-polarity training image in the test set — it fails loudly if the inversion step is ever removed.
  • Review the filter chain for duplicates at every code change — the second filter is a classic regression.
  • Run the golden image through a clean build at every release.

Take the pixel budget to the camera configurator, compare parts in the camera finder, read wider in code reading, or send the print sample and target string through the contact page.

Questions

Should you dilate character regions before classification?

Only before your visual check, never after it. A dilation of 1 to 3 pixels can give a classifier the margin it wants around each stroke, but it also grows regions toward their neighbors; on tightly spaced print, two dilated regions merge or bite into the next glyph. If you dilate, dilate the sorted set, recount, overlay again, and pass that exact object to the classifier. The rule holds regardless: what you classify must be what you verified on screen.

How do you sort regions when the text runs on two lines?

Group first, sort second. Put regions into row bands — two regions whose vertical extents overlap by more than half a glyph height belong to one line — then sort each band left to right and concatenate the bands top to bottom. A missing sort shows up as a scrambled but complete string; wrong grouping shows up as line two read before line one. Confirm with an overlay of index numbers, never with the classified output.

Can a smart camera do this OCR without a PC pipeline?

Yes, and it removes this bug class. An on-device reader runs segmentation and classification inside one firmware path, so the region set you tune in its setup view is the set it classifies. The imaging budget is unchanged: at least 20 pixels of glyph height and blur under half a pixel. Integrated models also display the measured glyph height in the setup view, which turns the pixel check into a ten-second glance instead of a calculation.

Do you have to retrain after changing the lens or working distance?

Retrain when pixels per glyph drift far from the training set. The classifier reads stroke geometry in pixels, so hold glyph height within roughly 25 percent of the training images: at 26 pixels per glyph you can drift between about 20 and 32 pixels without new samples. Outside that band, capture fresh training images at the new scale — rescaling old images hides focus and contrast differences that the classifier then trips over.

What exposure keeps moving print sharp enough for OCR?

Hold motion blur under half a pixel: t = 0.5 × scale ÷ part speed. At a scale of 0.115 mm per pixel and a part moving 100 mm per second, t = 0.5 × 0.115 ÷ 100 = 0.00058 s, about 575 µs, so set 500 µs and add strobe to keep gray levels up. Stationary fixtured parts have no blur budget to respect; expose for contrast and depth of field instead.