OCR — reading text from a frame

The one thing to know

LYNX does OCR as detection: each character is a normal detection whose class is the glyph. predict(ocr=...) then groups those glyphs into readable strings and attaches a typed Document to the frame. You read text off frame.documentnot off the raw detections.

1import lynx
2from lynx import Ocr
3
4m = lynx.open("lynx-ocr-fleet")
5frame = m.predict("image.png", ocr=Ocr.AUTO) # AUTO reads both orientations
6
7doc = frame.document # a Document, or None if OCR didn't run
8print(doc.text) # the recognized text (horizontal reading)

What you get back — Document

frame.document is typed (autocompletes; a typo raises instead of silently missing):

attributetypewhat it is
.textstrthe horizontal reading, one string
.vertical_textlist[VerticalText]vertical (top-to-bottom) columns read left-to-right — e.g. an ID painted down the side of a trailer
.blockslist[OcrBlock]structured detail: block.lines[].words[].char / .box / .score
.text_allstr.text + all .vertical_text, joined — the "just give me every string" shortcut
1print(doc.text) # horizontal
2for v in doc.vertical_text: # vertical IDs
3 print(v.text, f"({v.score:.2f})")
4print(doc.text_all) # everything
5
6for block in doc.blocks: # per-glyph detail
7 for line in block.lines:
8 print(line.text, [w.char for w in line.words])

Orientation — the common gotcha

A number painted vertically (one digit above the next) is not in .text — the horizontal reader would turn it into one-character-per-line junk ("3\n1\n0\n9…"). It's in .vertical_text. Ocr.AUTO (the default) reads both orientations and keeps them separate — the vertical glyphs are claimed by the vertical pass and never pollute .text. So for a vertical ID:

1frame = m.predict(img, ocr=Ocr.AUTO)
2for v in frame.document.vertical_text:
3 print("ID:", v.text) # e.g. "3109044"

Ocr modes

modebehaviour
Ocr.OFF (or False)no OCR post-processing; frame.document is None
Ocr.AUTO (or None, the default)on iff the model declares TEXT; reads both orientations
Ocr.ON (or True)force on; reads both orientations
Ocr.HORIZONTALforce on; horizontal text only
Ocr.VERTICALforce on; vertical columns only

Reading both is cheap: one inference, then two groupings over the same detections — there's no second forward pass.

If you get nothing / garbage

OCR quality is the model's recognition, not the SDK grouping. If .text and .vertical_text are empty or wrong:

  • Lower the confidence. Glyph detections are often low-score; try predict(..., conf=0.05) and inspect len(frame.detections).
  • Check the model actually reads it. [m.classes(d.class_id).name for d in frame.detections] — are those real characters at plausible boxes? If every detection is sub-0.1 confidence, the model isn't recognizing the input (wrong model, or text too small — OCR models are often trained on tight crops, so a full frame may need cropping/upscaling first).
  • frame.document is None? OCR didn't run — pass ocr=Ocr.ON (the model may not declare TEXT, so AUTO stayed off).

Streaming

track() / tracker() take the same ocr= and attach a Document to every frame:

1for frame in m.track(camera_frames, ocr=Ocr.AUTO):
2 if frame.document:
3 print(frame.document.text)