Doctr Tutorial: Deep Learning OCR Library for Document Understanding
Doctr (Document Text Recognition) is an open-source library developed by Mindee for performing Optical Character Recognition (OCR) using deep learning models. Unlike traditional OCR libraries such as Tesseract that rely on classical approaches, Doctr is built entirely on modern neural network architectures, resulting in significantly higher accuracy, especially on documents with complex layouts.
The library supports two major deep learning backends: PyTorch and TensorFlow, giving developers the flexibility to choose the framework that best fits their ecosystem. Doctr is specifically designed to process business documents such as invoices, receipts, forms, contracts, and various types of structured documents.
In this tutorial, we will learn how to use Doctr from installation, basic text extraction, to advanced techniques like model fine-tuning and integration into production pipelines.
Why Choose Doctr?
Before diving into implementation, it's important to understand what makes Doctr stand out compared to other OCR solutions:
Two-Stage Pipeline Architecture: Doctr uses a two-stage approach consisting of text detection (finding text locations in images) and text recognition (reading characters from detected areas). Each stage uses independently optimized deep learning models. Pre-trained Models: The library provides various pre-trained models already trained on large-scale document datasets, so they can be used directly without additional training for most use cases. Structured Output: OCR results from Doctr are returned in a hierarchical structured format (Document > Page > Block > Line > Word), making it easy to perform layout analysis and structured information extraction. Multi-Format Support: Doctr can process various input formats including images (PNG, JPG, TIFF) and PDF documents natively.Installation
Basic Installation with PyTorch Backend
pip install "python-doctr[torch]"
Basic Installation with TensorFlow Backend
pip install "python-doctr[tf]"
Full Installation (All Features)
pip install "python-doctr[torch,viz,html]"
Additional options:
viz: For detection and recognition result visualizationhtml: For exporting results to HTML formatcontrib: For access to community-contributed models
Verify Installation
import doctr
print(f"Doctr version: {doctr.version}")
from doctr.models import ocrpredictor
model = ocrpredictor(pretrained=True)
print("Model loaded successfully!")
System Dependencies
To process PDF files, Doctr requires additional system libraries:
# Ubuntu/Debian
sudo apt-get install poppler-utils
macOS
brew install poppler
Windows (via conda)
conda install -c conda-forge poppler
Basic Usage
Loading Documents from Files
The first step is loading the document you want to process. Doctr supports both images and PDFs.
from doctr.io import DocumentFile
Load a PDF file
pdfdoc = DocumentFile.frompdf("sampledocument.pdf")
print(f"Number of pages: {len(pdfdoc)}")
Load an image file
imgdoc = DocumentFile.fromimages("invoice.png")
print(f"Number of images: {len(imgdoc)}")
Load multiple images at once
multidoc = DocumentFile.fromimages([
"page1.png",
"page2.png",
"page3.jpg"
])
print(f"Total images: {len(multidoc)}")
Running the OCR Predictor
The OCR predictor is the main component that combines text detection and text recognition.
from doctr.models import ocrpredictor
Initialize model with pre-trained weights
model = ocrpredictor(pretrained=True)
Run OCR on the document
result = model(pdfdoc)
Display results in JSON format
import json
output = result.export()
print(json.dumps(output, indent=2))
Extracting Text from OCR Results
OCR results are returned in a hierarchical structure. Here's how to access text at each level:
from doctr.models import ocrpredictor
from doctr.io import DocumentFile
model = ocr
predictor(pretrained=True)
doc = DocumentFile.frompdf("invoice.pdf")
result = model(doc)
Extract all text from the document
fulltext = result.render()
print("=== Full Text ===")
print(fulltext)
Access per page
for pageidx, page in enumerate(result.pages):
print(f"\n--- Page {pageidx + 1} ---")
for block in page.blocks:
for line in block.lines:
linetext = " ".join([word.value for word in line.words])
print(f" Line: {linetext}")
Access per word with confidence score
for page in result.pages:
for block in page.blocks:
for line in block.lines:
for word in line.words:
print(f"Word: '{word.value}' | "
f"Confidence: {word.confidence:.4f} | "
f"Bounding Box: {word.geometry}")
Understanding Bounding Boxes
Each detected element has geometry information in the form of a bounding box. Coordinates are normalized between 0 and 1 relative to the page dimensions.
for page in result.pages:
pagedims = page.dimensions # (height, width) in pixels
print(f"Page dimensions: {pagedims}")
for block in page.blocks:
# Geometry format: ((xmin, ymin), (xmax, ymax))
bbox = block.geometry
xmin, ymin = bbox[0]
xmax, ymax = bbox[1]
# Convert to pixels
pxxmin = int(xmin pagedims[1])
pxymin = int(ymin pagedims[0])
pxxmax = int(xmax pagedims[1])
pxymax = int(ymax pagedims[0])
print(f"Block bbox (normalized): {bbox}")
print(f"Block bbox (pixels): ({pxxmin}, {pxymin}) -> ({pxxmax}, {pxymax})")
Model Configuration
Choosing Detection and Recognition Architectures
Doctr provides several model architecture options for detection and recognition. You can choose based on your needs between accuracy and speed.
from doctr.models import ocrpredictor
Default model (balanced accuracy and speed)
modeldefault = ocrpredictor(pretrained=True)
Model with specific architecture
modelcustom = ocrpredictor(
detarch='dbresnet50', # Detection: DBNet with ResNet50
recoarch='crnnvgg16bn', # Recognition: CRNN with VGG16
pretrained=True
)
Lightweight model for speed
modelfast = ocrpredictor(
detarch='dbmobilenetv3large',
recoarch='crnnmobilenetv3small',
pretrained=True
)
Available Detection Architectures
| Architecture | Backbone | Speed | Accuracy |
|-------------|----------|-------|----------|
| dbresnet50 | ResNet-50 | Medium | High |
| dbmobilenetv3large | MobileNetV3-L | Fast | Medium |
| linknetresnet18 | ResNet-18 | Fast | Medium |
| linknetresnet50 | ResNet-50 | Medium | High |
Available Recognition Architectures
| Architecture | Backbone | Speed | Accuracy |
|-------------|----------|-------|----------|
| crnnvgg16bn | VGG-16 BN | Medium | High |
| crnnmobilenetv3small | MobileNetV3-S | Fast | Medium |
| crnnmobilenetv3large | MobileNetV3-L | Medium | High |
| sarresnet31 | ResNet-31 | Slow | Very High |
| master | - | Slow | Very High |
| vitstrsmall | ViT-Small | Medium | High |
Additional Parameter Configuration
model = ocrpredictor(
pretrained=True,
assumestraightpages=True, # Assume pages are already straight (faster)
straightenpages=False, # Disable orientation correction
preserveaspectratio=True, # Preserve aspect ratio during resize
symmetricpad=True, # Symmetric padding
exportasstraightboxes=True, # Export bboxes as straight rectangles
)
Visualizing Results
Visualization with Matplotlib
Doctr provides built-in utilities for visualizing detection results.
from doctr.io import DocumentFile
from doctr.models import ocrpredictor
from doctr.utils.visualization import visualizepage
import matplotlib.pyplot as plt
model = ocrpredictor(pretrained=True)
doc = DocumentFile.fromimages("document.png")
result = model(doc)
Visualize first page
pageresult = result.pages[0]
fig, ax = plt.subplots(1, 1, figsize=(12, 16))
visualizepage(pageresult.export(), doc[0], ax=ax, interactive=False)
plt.tightlayout()
plt.savefig("ocrresult.png", dpi=150)
plt.show()
Drawing Bounding Boxes Manually
import cv2
import numpy as np
from doctr.io import DocumentFile
from doctr.models import ocrpredictor
model = ocrpredictor(pretrained=True)
doc = DocumentFile.fromimages("invoice.png")
result = model(doc)
Load original image
image = cv2.imread("invoice.png")
h, w = image.shape[:2]
for page in result.pages:
for block in page.blocks:
for line in block.lines:
for word in line.words:
# Convert normalized coordinates to pixels
(x1, y1), (x2, y2) = word.geometry
pt1 = (int(x1 w), int(y1 h))
pt2 = (int(x2 w), int(y2 h))
# Draw rectangle
cv2.rectangle(image, pt1, pt2, (0, 255, 0), 2)
# Add text label
cv2.putText(image, word.value,
(pt1[0], pt1[1] - 5),
cv2.FONTHERSHEYSIMPLEX, 0.4,
(0, 0, 255), 1)
cv2.imwrite("annotatedresult.png", image)
print("Result saved to annotatedresult.png")
Export to HTML Format
from doctr.io import DocumentFile
from doctr.models import ocrpredictor
model = ocrpredictor(pretrained=True)
doc = DocumentFile.frompdf("document.pdf")
result = model(doc)
Reconstruct page as synthetic image
syntheticpages = result.synthesize()
for idx, pageimg in enumerate(syntheticpages):
import matplotlib.pyplot as plt
plt.imsave(f"syntheticpage{idx}.png", pageimg)
print(f"Synthetic page {idx} saved")
Advanced Usage
Batch Processing Multiple Documents
When you need to process many documents at once, use batch processing for efficiency.
import os
import json
from doctr.io import DocumentFile
from doctr.models import ocrpredictor
model = ocrpredictor(pretrained=True)
def processdirectory(inputdir, outputdir):
os.makedirs(outputdir, existok=True)
supportedformats = ('.png', '.jpg', '.jpeg', '.tiff', '.pdf')
files = [f for f in os.listdir(inputdir)
if f.lower().endswith(supportedformats)]
results = {}
for filename in files:
filepath = os.path.join(inputdir, filename)
print(f"Processing: {filename}")
if filename.lower().endswith('.pdf'):
doc = DocumentFile.frompdf(filepath)
else:
doc = DocumentFile.fromimages(filepath)
result = model(doc)
# Save full text
text = result.render()
textpath = os.path.join(outputdir, f"{filename}.txt")
with open(textpath, 'w', encoding='utf-8') as f:
f.write(text)
# Save structured results
jsonpath = os.path.join(outputdir, f"{filename}.json")
with open(jsonpath, 'w', encoding='utf-8') as f:
json.dump(result.export(), f, indent=2)
results[filename] = {
'pages': len(result.pages),
'textlength': len(text),
'outputtext': textpath,
'outputjson': jsonpath
}
print(f" -> {len(result.pages)} pages, {len(text)} characters")
return results
Run
summary = processdirectory("./documents", "./ocrresults")
print(f"\nTotal documents processed: {len(summary)}")
Extracting Information from Invoices
A practical example of extracting key fields from invoices using Doctr and simple post-processing.
import re
from doctr.io import DocumentFile
from doctr.models import ocrpredictor
model = ocrpredictor(pretrained=True)
def extractinvoicedata(pdfpath):
doc = DocumentFile.frompdf(pdfpath)
result = model(doc)
fulltext = result.render()
lines = fulltext.split('\n')
invoicedata = {
'invoicenumber': None,
'date': None,
'total': None,
'items': []
}
for line in lines:
line = line.strip()
# Find invoice number
invmatch = re.search(r'(?:Invoice|INV|No)[.\s#:](\S+)', line, re.IGNORECASE)
if invmatch and not invoicedata['invoicenumber']:
invoicedata['invoicenumber'] = invmatch.group(1)
# Find date
datematch = re.search(
r'(\d{1,2}[/-]\d{1,2}[/-]\d{2,4}|\d{1,2}\s+\w+\s+\d{4})',
line
)
if datematch and not invoicedata['date']:
invoicedata['date'] = datematch.group(1)
# Find total amount
totalmatch = re.search(
r'(?:Total|Grand Total|Amount Due)[:\s](?:\$|€|£)?\s([\d.,]+)',
line, re.IGNORECASE
)
if totalmatch:
invoicedata['total'] = totalmatch.group(1)
# Extract structured data per page
for page in result.pages:
for block in page.blocks:
blocktext = ""
for line in block.lines:
linetext = " ".join([w.value for w in line.words])
blocktext += linetext + "\n"
if any(keyword in blocktext.lower()
for keyword in ['item', 'description', 'qty', 'quantity']):
invoicedata['items'].append(blocktext.strip())
return invoicedata
Usage
data = extractinvoicedata("sampleinvoice.pdf")
print(f"Invoice Number: {data['invoicenumber']}")
print(f"Date: {data['date']}")
print(f"Total: {data['total']}")
print(f"Items found: {len(data['items'])}")
Using GPU for Acceleration
import torch
from doctr.models import ocrpredictor
Check GPU availability
device = torch.device("cuda" if torch.cuda.isavailable() else "cpu")
print(f"Using device: {device}")
Model automatically uses GPU if available
model = ocrpredictor(pretrained=True).to(device)
For TensorFlow backend
import tensorflow as tf
gpus = tf.config.listphysicaldevices('GPU')
print(f"GPUs available: {len(gpus)}")
Image Preprocessing for Better Results
import cv2
import numpy as np
from doctr.io import DocumentFile
from doctr.models import ocrpredictor
def preprocessimage(imagepath):
img = cv2.imread(imagepath)
# Convert to grayscale
gray = cv2.cvtColor(img, cv2.COLORBGR2GRAY)
# Adaptive thresholding to handle uneven lighting
thresh = cv2.adaptiveThreshold(
gray, 255, cv2.ADAPTIVETHRESHGAUSSIANC,
cv2.THRESHBINARY, 11, 2
)
# Noise removal
kernel = np.ones((1, 1), np.uint8)
cleaned = cv2.morphologyEx(thresh, cv2.MORPHCLOSE, kernel)
# Deskew (correct skew)
coords = np.columnstack(np.where(cleaned > 0))
angle = cv2.minAreaRect(coords)[-1]
if angle < -45:
angle = -(90 + angle)
else:
angle = -angle
(h, w) = cleaned.shape[:2]
center = (w // 2, h // 2)
M = cv2.getRotationMatrix2D(center, angle, 1.0)
rotated = cv2.warpAffine(
cleaned, M, (w, h),
flags=cv2.INTERCUBIC,
borderMode=cv2.BORDERREPLICATE
)
# Save preprocessed result
outputpath = "preprocessed" + imagepath.split("/")[-1]
cv2.imwrite(outputpath, rotated)
return outputpath
Use preprocessed image
processedpath = preprocessimage("scanneddocument.png")
model = ocrpredictor(pretrained=True)
doc = DocumentFile.fromimages(processedpath)
result = model(doc)
print(result.render())
Integration with FastAPI
Building a REST API for OCR services using FastAPI and Doctr.
from fastapi import FastAPI, UploadFile, File
from fastapi.responses import JSONResponse
import tempfile
import os
from doctr.io import DocumentFile
from doctr.models import ocrpredictor
app = FastAPI(title="Doctr OCR API")
model = ocrpredictor(pretrained=True)
@app.post("/ocr/image")
async def ocrimage(file: UploadFile = File(...)):
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp:
content = await file.read()
tmp.write(content)
tmppath = tmp.name
try:
doc = DocumentFile.fromimages(tmppath)
result = model(doc)
output = {
"filename": file.filename,
"text": result.render(),
"pages": []
}
for pageidx, page in enumerate(result.pages):
pagedata = {
"pagenumber": pageidx + 1,
"dimensions": page.dimensions,
"words": []
}
for block in page.blocks:
for line in block.lines:
for word in line.words:
pagedata["words"].append({
"text": word.value,
"confidence": round(word.confidence, 4),
"bbox": word.geometry
})
output["pages"].append(pagedata)
return JSONResponse(content=output)
finally:
os.unlink(tmppath)
@app.post("/ocr/pdf")
async def ocrpdf(file: UploadFile = File(...)):
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
content = await file.read()
tmp.write(content)
tmppath = tmp.name
try:
doc = DocumentFile.frompdf(tmppath)
result = model(doc)
return JSONResponse(content={
"filename": file.filename,
"text": result.render(),
"totalpages": len(result.pages),
"structured": result.export()
})
finally:
os.unlink(tmppath)
Run the server:
uvicorn ocrapi:app --host 0.0.0.0 --port 8000
Test the endpoint:
curl -X POST "http://localhost:8000/ocr/image" \
-F "file=@invoice.png" | python -m json.tool
Fine-tuning the Recognition Model
If the pre-trained model doesn't provide sufficient accuracy for specific document types, you can fine-tune it.
import torch
from doctr.datasets import CORD
from doctr.models import recognition
from doctr.transforms import Resize
from torch.utils.data import DataLoader
Prepare dataset
traindataset = CORD(
train=True,
download=True,
recognitiontask=True,
imgtransforms=Resize((32, 128))
)
valdataset = CORD(
train=False,
download=True,
recognitiontask=True,
imgtransforms=Resize((32, 128))
)
trainloader = DataLoader(traindataset, batchsize=32, shuffle=True)
valloader = DataLoader(valdataset, batchsize=32, shuffle=False)
Initialize model
model = recognition.crnnvgg16bn(pretrained=True)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
Training loop
device = torch.device("cuda" if torch.cuda.isavailable() else "cpu")
model = model.to(device)
numepochs = 10
for epoch in range(numepochs):
model.train()
totalloss = 0
for batchidx, (images, targets) in enumerate(trainloader):
images = images.to(device)
output = model(images, targets)
loss = output["loss"]
optimizer.zerograd()
loss.backward()
optimizer.step()
totalloss += loss.item()
if batchidx % 50 == 0:
print(f"Epoch [{epoch+1}/{numepochs}], "
f"Batch [{batchidx}/{len(trainloader)}], "
f"Loss: {loss.item():.4f}")
avgloss = totalloss / len(trainloader)
print(f"Epoch [{epoch+1}/{numepochs}], Average Loss: {avgloss:.4f}")
Save model
torch.save(model.statedict(), "doctrfinetuned.pth")
print("Model saved!")
Using Custom Models
from doctr.models import ocrpredictor, recognition
Load fine-tuned recognition model
customreco = recognition.crnnvgg16bn(pretrained=False)
customreco.loadstatedict(torch.load("doctrfinetuned.pth"))
Use in OCR pipeline
model = ocrpredictor(
detarch='dbresnet50',
recoarch=customreco,
pretrained=False
)
Best Practices
1. Optimize Input Quality
Input image quality significantly affects OCR accuracy. Here are some tips:
- Ensure minimum resolution of 150 DPI, ideally 300 DPI
- Avoid images that are too dark or too bright
- Correct document skew before OCR
- Remove noise and artifacts from the scanning process
2. Choose the Right Model
- For standard documents with horizontal text, use
assumestraightpages=Trueto speed up inference - For documents with varying orientations, use
straightenpages=True - For deployment on edge devices, choose MobileNet-based architectures
- For maximum accuracy, use
sarresnet31ormasteras the recognition model
3. Memory Management
import torch
from doctr.models import ocrpredictor
model = ocrpredictor(pretrained=True)
Use torch.nograd() during inference
with torch.nograd():
result = model(doc)
Clear GPU cache after processing large batches
if torch.cuda.isavailable():
torch.cuda.emptycache()
4. Post-processing OCR Results
Apply post-processing to improve output quality:
import re
def cleanocrtext(text):
# Join words split by line breaks
text = re.sub(r'(\w)-\n(\w)', r'\1\2', text)
# Normalize whitespace
text = re.sub(r'\s+', ' ', text)
# Common OCR error corrections
corrections = {
'0': 'O', # Contextual: at word start
'l': '1', # Contextual: between digits
'rn': 'm', # Common error
}
# Remove invalid characters
text = re.sub(r'[^\w\s.,;:!?@#$%&()\-+=\[\]{}\'"/\\]', '', text)
return text.strip()
Usage
rawtext = result.render()
cleantext = cleanocrtext(rawtext)
print(cleantext)
5. Confidence Filtering
Filter words with low confidence to improve reliability:
def filterlowconfidence(result, threshold=0.5):
filteredwords = []
lowconfidencewords = []
for page in result.pages:
for block in page.blocks:
for line in block.lines:
for word in line.words:
if word.confidence >= threshold:
filteredwords.append(word.value)
else:
lowconfidencewords.append(
(word.value, word.confidence)
)
print(f"Words with confidence >= {threshold}: {len(filteredwords)}")
print(f"Words with confidence < {threshold}: {len(lowconfidencewords)}")
if lowconfidencewords:
print("\nLow confidence words:")
for word, conf in lowconfidencewords:
print(f" '{word}' -> {conf:.4f}")
return " ".join(filteredwords)
highqualitytext = filterlowconfidence(result, threshold=0.7)
Comparison with Other OCR Libraries
| Feature | Doctr | Tesseract | EasyOCR | PaddleOCR |
|---------|-------|-----------|---------|-----------|
| Backend | PyTorch/TF | C++ | PyTorch | PaddlePaddle |
| Deep Learning | Yes | Partial | Yes | Yes |
| Structured Output | Yes | Limited | Limited | Yes |
| PDF Support | Native | Via wrapper | No | Yes |
| Pre-trained Models | Many | Limited | Many | Many |
| Fine-tuning | Easy | Difficult | Medium | Medium |
| GPU Speed | Fast | N/A | Medium | Fast |
Conclusion
Doctr is an excellent choice for OCR projects that require high accuracy on business documents. With its modern two-stage pipeline architecture, multi-backend support (PyTorch and TensorFlow), and structured output that is easy to process, Doctr offers a good balance between ease of use and flexibility.
Key takeaways to remember:
- Use pre-trained models to get started quickly
- Choose model architecture according to your needs: MobileNet for speed, ResNet/ViT for accuracy
- Good image preprocessing significantly impacts final results
- Leverage the structured output (Document > Page > Block > Line > Word) for layout analysis
- Fine-tune when default accuracy is insufficient for your specific document domain
- Confidence scores are very useful for filtering and quality control
By following the guidelines and best practices in this tutorial, you have a solid foundation for building deep learning-based OCR systems using Doctr, whether for experimentation or production deployment.