Running OCR#
ocr_predictor() builds the end-to-end pipeline: it detects the words of each page, reads them,
and assembles the result into a Document. This page walks through its options, from choosing
the models to fine-tuning the output.
Create a predictor#
from doctr.io import DocumentFile
from doctr.models import ocr_predictor
predictor = ocr_predictor(pretrained=True)
pages = DocumentFile.from_pdf("path/to/report.pdf")
result = predictor(pages)
Building a predictor loads the models (and downloads their weights the first time), so create it once and reuse it for all your documents. The predictor runs in inference mode and accepts any number of pages per call.
Choose the architectures#
By default, ocr_predictor uses fast_base for text detection and crnn_vgg16_bn for text recognition. Pass the
names of other architectures to trade speed for accuracy:
predictor = ocr_predictor(det_arch="db_resnet50", reco_arch="parseq", pretrained=True)
You want… |
Detection ( |
Recognition ( |
|---|---|---|
A good default |
|
|
Maximum speed (CPU, edge devices) |
|
|
Maximum accuracy |
|
|
The model zoo lists every architecture with its benchmark. Instead of a name, you can also pass a model instance, which is how you plug in your own trained models:
from doctr.models import ocr_predictor, fast_base, parseq
predictor = ocr_predictor(
det_arch=fast_base(pretrained=True),
reco_arch=parseq(pretrained=True),
)
Options at a glance#
All options are keyword arguments of ocr_predictor().
Option |
Default |
Effect |
|---|---|---|
|
|
Assume upright pages with horizontal text: fastest, returns straight boxes. Set to |
|
|
Estimate the page rotation and straighten the pages before running the models |
|
|
With |
|
|
Pad the pages instead of stretching them when resizing them for the models |
|
|
With |
See Rotated documents for a detailed guide.
Option |
Default |
Effect |
|---|---|---|
|
|
Estimate the general orientation of each page, available as |
|
|
Estimate the language of each page, available as |
|
|
Run a layout model and attach its regions to each page ( |
|
|
Layout architecture name or model instance |
|
|
Recognize the structure of tables ( |
|
|
Layout classes to mask out before OCR, e.g. |
Option |
Default |
Effect |
|---|---|---|
|
|
Group words into lines |
|
|
Group lines into blocks (by default, all the lines of a page form a single block) |
|
|
Minimum horizontal gap, relative to the page width, that splits two words into different lines |
|
|
Sort the blocks of every page in reading order |
Option |
Default |
Effect |
|---|---|---|
|
|
Batch size of the detection model (also used by the layout and table models) |
|
|
Batch size of the recognition model (number of word crops per batch) |
|
|
Skip the page orientation classifier (see rotated documents) |
|
|
Skip the word crop orientation classifier |
See Performance for GPU and half-precision inference.
Tune the text detection#
The detection models output a probability map that is binarized, then split into boxes. Two thresholds of the post-processor control this step:
bin_threshMinimum probability for a pixel to be considered as text. Lower it to catch faint text, raise it to separate words that are close to each other.
box_threshMinimum mean score of a box to be kept. Lower it to keep more (uncertain) words, raise it to drop noise.
predictor = ocr_predictor(pretrained=True)
predictor.det_predictor.model.postprocessor.bin_thresh = 0.3
predictor.det_predictor.model.postprocessor.box_thresh = 0.1
Confidence scores#
The recognition model predicts a probability for each character of a word. They are aggregated into
word.confidence with one of the following methods:
Method |
Behavior |
|---|---|
|
The least confident character (default for CRNN, VIPTR, SAR and MASTER) |
|
Average probability (default for ViTSTR and PARSeq) |
|
Means that penalize a single uncertain character more than the arithmetic mean |
|
Ignore most uncertain characters: they barely separate misread words from correct ones |
a callable |
Any function taking the 1D array of probabilities and returning a float |
For the probabilities [0.99, 0.98, 0.12, 0.99] (one doubtful character), the mean is 0.77, the geometric mean
0.58, the harmonic mean 0.35 and the minimum 0.12. Pick a method that makes uncertain words stand out if
you use the confidence to flag words for human review.
from doctr.models import ocr_predictor, parseq
# When building the predictor (modifies a model instance passed as `reco_arch`)
predictor = ocr_predictor(pretrained=True, confidence_aggregation="geometric_mean")
# Or afterwards, on the post-processor of the recognition model
predictor.reco_predictor.model.postprocessor.confidence_aggregation = "mean"
# Or on a model instance
predictor = ocr_predictor(reco_arch=parseq(pretrained=True, confidence_aggregation="min"), pretrained=True)
Details: which probabilities are aggregated?
Only the probabilities of the predicted characters: the CTC blank frames (CRNN, VIPTR) and the positions from the
end-of-sequence token onwards (SAR, MASTER) are ignored, and a CTC character has the highest probability of its
frames. An empty prediction has a confidence of 0.
Very wide crops are split into parts that are read separately; their confidences are combined with the
split_confidence_aggregation of the recognition predictor ("min" by default):
predictor.reco_predictor.split_confidence_aggregation = "mean"
Restrict the output characters#
If you know which languages to expect, restrict the recognition model to their characters with
add_whitelist(). Forbidden characters can no longer be predicted, which removes a whole
class of errors (e.g. ł read instead of l on an English document).
from doctr.datasets import VOCABS
from doctr.models import ocr_predictor
from doctr.models.utils import add_whitelist
predictor = ocr_predictor(pretrained=True)
# Only German and Polish characters can be predicted from now on
handle = add_whitelist(predictor, [VOCABS["german"], VOCABS["polish"]])
result = predictor(pages)
# Restore the original decoding
handle.remove()
The handle is also a context manager, which removes the whitelist when the block ends:
with add_whitelist(predictor, VOCABS["german"]):
result = predictor(pages)
By default, forbidden characters are dropped (strategy="mask") and the model falls back to its best allowed
character. With strategy="nearest", each forbidden character is first folded onto the closest allowed one,
which is handy to normalize accents onto a base alphabet:
# é -> e, ł -> l, ß -> ss...: an ASCII-only output
handle = add_whitelist(predictor, VOCABS["latin"], strategy="nearest")
The closest character is found by transliteration by default. Pass mapping="weights" to derive it from the
model’s own confusions instead, or a {"forbidden": "allowed"} dict to override specific characters.
Note
A whitelist can only restrict a model: characters it was not trained on are silently ignored. To read a new alphabet, use a model trained on a matching vocabulary.
Hooks#
Hooks let you edit the detected boxes before they are read. A hook is a callable that receives the location predictions right after text detection and returns them, possibly modified. The recognition model then reads the crops of the boxes the hook returned:
remove boxes to skip regions you don’t need (they are not read, which also saves time);
move or resize boxes, e.g. to pad crops that are too tight;
add boxes, e.g. for fields at a known position on a form.
The location predictions are a list with one NumPy array per page, in relative coordinates: (N, 4) straight boxes
as xmin, ymin, xmax, ymax, or (N, 4, 2) polygons with assume_straight_pages=False. A hook returns the same
structure, with any number of boxes per page.
import numpy as np
from doctr.models import ocr_predictor
class SkipMargins:
"""Ignore the words in the top and bottom bands of the page (headers, footers)"""
def __init__(self, band: float = 0.05):
self.band = band
def __call__(self, loc_preds: list[np.ndarray]) -> list[np.ndarray]:
kept = []
for boxes in loc_preds: # one array per page, relative coordinates
if boxes.ndim == 2: # straight boxes (N, 4): xmin, ymin, xmax, ymax
center_y = (boxes[:, 1] + boxes[:, 3]) / 2
else: # rotated boxes (N, 4, 2): polygons
center_y = boxes[:, :, 1].mean(axis=1)
keep = (center_y > self.band) & (center_y < 1 - self.band)
kept.append(boxes[keep])
return kept
predictor = ocr_predictor(pretrained=True)
predictor.add_hook(SkipMargins())
On the sample invoice, the footer line
(ACME Office Supplies GmbH - VAT ID ...) is no longer read.
import numpy as np
from doctr.models import ocr_predictor
def pad_boxes(loc_preds: list[np.ndarray], pad: float = 0.002) -> list[np.ndarray]:
"""Enlarge the straight boxes slightly, so that the crops keep the character edges"""
offsets = np.array([-pad, -pad, pad, pad])
return [np.clip(boxes + offsets.astype(boxes.dtype), 0, 1) for boxes in loc_preds]
predictor = ocr_predictor(pretrained=True)
predictor.add_hook(pad_boxes)
Good to know:
Hooks run in the order they were added, each one receiving the output of the previous one.
Each returned box keeps the
objectness_scoreof the closest detected box of its page; a box added on a page without any detection gets a score of1.With the KIE predictor, hooks receive and return a dict with one such list per class.
An output in the wrong format (not one array per page, or boxes of another shape) raises a
ValueError.
Process many documents#
Reuse the predictor: building it is the expensive part.
Batch your pages: one call on a list of pages is faster than one call per page, as the models process several pages and word crops at once (see
det_bsandreco_bsabove).Free memory between documents: the predictor returns plain Python objects, so drop the results you no longer need.
from pathlib import Path
from doctr.io import DocumentFile
from doctr.models import ocr_predictor
predictor = ocr_predictor(pretrained=True)
for path in sorted(Path("invoices").glob("*.pdf")):
result = predictor(DocumentFile.from_pdf(path))
path.with_suffix(".txt").write_text(result.render())
Next steps#
Navigate the Document, read words and boxes, and save the predictions.
Run on GPU, in half precision, and compile the models.