OCR for Scanned Documents
Tesseract via pytesseract, image preprocessing, and accuracy improvement.
OCR for Scanned Documents is a free AI Agents lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the AI Agents learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
When OCR Is Necessary
Not all documents are digital-native PDFs. Scanned documents, photographs of text, handwritten notes, and image-based PDFs require Optical Character Recognition (OCR) to extract text.
OCR converts pixel images of text into machine-readable characters. Two leading Python libraries: pytesseract (Google Tesseract) and EasyOCR (deep learning, multilingual).
pytesseract Basics
pytesseract is a Python wrapper around Google's Tesseract OCR engine. Install Tesseract first (OS-level), then pip install pytesseract Pillow.
import pytesseract
from PIL import Image
# Simple text extraction
image = Image.open('scanned_document.png')
text = pytesseract.image_to_string(image)
print(text)
# Specify language (default: English)
text_fr = pytesseract.image_to_string(
Image.open('french_doc.png'),
lang='fra'
)
# Get detailed output with bounding boxes
data = pytesseract.image_to_data(
image,
output_type=pytesseract.Output.DICT
)
for i, word in enumerate(data['text']):
if word.strip():
conf = data['conf'][i]
print(f'Word: {word!r:20} Confidence: {conf}')Image Preprocessing: Grayscale
OCR accuracy depends heavily on image quality. The first preprocessing step is converting to grayscale — color adds noise without helping character recognition.
Use Pillow or OpenCV for preprocessing. Grayscale reduces noise and improves Tesseract's character boundary detection.
from PIL import Image, ImageOps
import numpy as np
def preprocess_grayscale(image_path):
img = Image.open(image_path)
# Convert to grayscale
img = img.convert('L') # 'L' = 8-bit grayscale
# Optionally resize for better OCR (Tesseract works best at ~300 DPI)
# Scale up small images
width, height = img.size
if width < 800:
scale = 800 / width
new_size = (int(width * scale), int(height * scale))
img = img.resize(new_size, Image.LANCZOS)
return img
img = preprocess_grayscale('scan.jpg')
text = pytesseract.image_to_string(img)
print(text[:300])Image Preprocessing: Thresholding
Thresholding converts a grayscale image to pure black-and-white. This removes gray shadows, uneven lighting, and background noise — making text stand out sharply for OCR.
from PIL import Image, ImageFilter
import numpy as np
def apply_threshold(img):
# Method 1: Simple fixed threshold
img_array = np.array(img)
threshold = 128
binary = np.where(img_array > threshold, 255, 0).astype(np.uint8)
return Image.fromarray(binary)
def adaptive_threshold(img):
# Method 2: Otsu's method via OpenCV (better for uneven lighting)
try:
import cv2
img_array = np.array(img)
_, binary = cv2.threshold(
img_array, 0, 255,
cv2.THRESH_BINARY + cv2.THRESH_OTSU
)
return Image.fromarray(binary)
except ImportError:
return apply_threshold(img)
img = preprocess_grayscale('uneven_scan.png')
img_clean = adaptive_threshold(img)
text = pytesseract.image_to_string(img_clean)
print(text[:300])Image Preprocessing: Deskewing
Scanned documents are often slightly rotated. Even a 2-degree tilt significantly reduces OCR accuracy. Deskewing detects and corrects the rotation angle before passing the image to the OCR engine.
import numpy as np
from PIL import Image
def deskew(img):
try:
import cv2
img_array = np.array(img)
# Find rotation angle using Hough line transform
edges = cv2.Canny(img_array, 50, 150, apertureSize=3)
lines = cv2.HoughLines(edges, 1, np.pi/180, threshold=100)
if lines is None:
return img # no lines detected, return unchanged
angles = []
for line in lines:
rho, theta = line[0]
angle = np.degrees(theta) - 90
if -45 < angle < 45:
angles.append(angle)
if not angles:
return img
median_angle = np.median(angles)
if abs(median_angle) > 0.5: # only deskew if significant tilt
print(f'Deskewing by {median_angle:.2f} degrees')
return img.rotate(-median_angle, expand=True, fillcolor=255)
return img
except ImportError:
return img # OpenCV not available — skip deskewTesseract Language Options
Tesseract supports 100+ languages. Download language packs with your OS package manager. For multi-language documents, specify multiple languages with a + separator.
import pytesseract
from PIL import Image
# List available languages
import subprocess
result = subprocess.run(['tesseract', '--list-langs'], capture_output=True, text=True)
print('Available languages:')
print(result.stdout)
# Single language
text_en = pytesseract.image_to_string(Image.open('doc.png'), lang='eng')
# Multiple languages (auto-detect best match)
text_multi = pytesseract.image_to_string(
Image.open('doc.png'),
lang='eng+fra+deu' # English + French + German
)
# Tesseract config options for better accuracy
custom_config = r'--oem 3 --psm 6' # OEM 3=LSTM, PSM 6=assume uniform block of text
text_tuned = pytesseract.image_to_string(
Image.open('doc.png'),
config=custom_config
)EasyOCR: Deep Learning Based
EasyOCR uses deep learning models and outperforms Tesseract on degraded images, curved text, and non-Latin scripts. It requires no OS-level installation.
Install with pip install easyocr. First run downloads model weights (~200MB).
import easyocr
# Initialize reader (downloads model on first run)
reader = easyocr.Reader(
['en', 'tr'], # list of languages
gpu=False # set True if CUDA available
)
# Read text from image
results = reader.readtext('scanned_page.png')
for (bbox, text, confidence) in results:
print(f'Text: {text!r:30} Confidence: {confidence:.2f}')
# bbox = [[x1,y1],[x2,y2],[x3,y3],[x4,y4]] (four corners)
# Get plain text only
text_only = ' '.join(result[1] for result in results)
print('Full text:', text_only[:300])EasyOCR for Multilingual Documents
EasyOCR handles mixed-language documents well — useful for international contracts, research papers with captions in multiple languages, or documents with technical terms in a different script.
import easyocr
# For Arabic-English mixed document
reader_ar = easyocr.Reader(['ar', 'en'], gpu=False)
results = reader_ar.readtext('arabic_contract.png')
# For CJK languages
reader_cjk = easyocr.Reader(['ch_sim', 'en'], gpu=False) # Simplified Chinese + English
results_cjk = reader_cjk.readtext('chinese_report.png')
# Filter by confidence threshold
HIGH_CONFIDENCE = 0.7
def filter_confident_results(results, threshold=HIGH_CONFIDENCE):
return [
(bbox, text, conf)
for bbox, text, conf in results
if conf >= threshold
]Full Preprocessing Pipeline
Chain all preprocessing steps — grayscale, threshold, deskew — into a single pipeline function. Run OCR on the cleaned image and return both the text and confidence metrics.
import pytesseract
from PIL import Image
def ocr_pipeline(image_path, lang='eng', engine='tesseract'):
print(f'Processing: {image_path}')
# 1. Load
img = Image.open(image_path)
# 2. Preprocess
img = img.convert('L') # grayscale
img = adaptive_threshold(img) # binarize
img = deskew(img) # correct rotation
# 3. OCR
if engine == 'easyocr':
reader = easyocr.Reader([lang], gpu=False)
results = reader.readtext(image_path)
text = ' '.join(r[1] for r in results if r[2] > 0.5)
else:
config = f'--oem 3 --psm 6 -l {lang}'
text = pytesseract.image_to_string(img, config=config)
# 4. Clean
text = '\n'.join(
line.strip() for line in text.splitlines()
if line.strip()
)
return text
text = ocr_pipeline('invoice_scan.jpg')
print(text[:500])Handling Image-Based PDFs
Some PDFs contain scanned pages stored as images inside the PDF container. Detect these 'image PDFs' and apply OCR page by page using PyMuPDF to extract the images first.
import fitz
from PIL import Image
import io
import pytesseract
def is_image_pdf(doc):
# Check if first page has very little extractable text
text = doc[0].get_text().strip()
return len(text) < 50
def ocr_pdf(filepath):
doc = fitz.open(filepath)
if not is_image_pdf(doc):
# Native text — no OCR needed
return '\n'.join(doc[i].get_text() for i in range(len(doc)))
print('Image PDF detected — running OCR')
all_text = []
for page_num in range(len(doc)):
page = doc[page_num]
# Render page as image at 300 DPI
mat = fitz.Matrix(300/72, 300/72) # scale to 300 DPI
pix = page.get_pixmap(matrix=mat)
img = Image.open(io.BytesIO(pix.tobytes('png')))
text = pytesseract.image_to_string(img)
all_text.append(f'--- Page {page_num+1} ---\n{text}')
doc.close()
return '\n'.join(all_text)Confidence Filtering and Post-Processing
OCR output contains errors, especially on degraded images. Post-process the text by filtering low-confidence words, correcting common OCR mistakes (0 vs O, 1 vs l), and normalizing whitespace.
import re
COMMON_OCR_ERRORS = {
r'\b0([a-z])\b': r'O\1',
r'\bl([0-9])\b': r'1\1',
r'\|': 'I',
r' +': ' ',
}
def post_process_ocr(text):
for pattern, replacement in COMMON_OCR_ERRORS.items():
text = re.sub(pattern, replacement, text)
lines = [line.strip() for line in text.splitlines()]
return '\n'.join(line for line in lines if line)
def high_confidence_ocr(words_with_conf, min_confidence=60):
words = [w for w, conf in words_with_conf if w.strip() and conf >= min_confidence]
return post_process_ocr(' '.join(words))
fake_ocr_output = [('0range', 90), ('|s', 40), ('l5', 85), ('good', 95)]
print(high_confidence_ocr(fake_ocr_output))Knowledge Check
What is the purpose of applying a threshold (binarization) to an image before OCR?
Recap: OCR for Scanned Documents
OCR converts scanned images to machine-readable text. pytesseract wraps Google Tesseract — fast and mature with 100+ language packs. EasyOCR uses deep learning and handles degraded images and multilingual text better.
Always preprocess: convert to grayscale, apply Otsu thresholding for binarization, and deskew to correct rotation. For image-based PDFs, render pages as high-DPI images with PyMuPDF then run OCR. Filter results by confidence threshold to exclude noise.
Frequently asked questions
Is the “OCR for Scanned Documents” lesson free?
Yes — the full text of “OCR for Scanned Documents” is free to read here on the web, and the AI Agents course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the AI Agents course, upgrade to CoddyKit PRO.
What will I learn in “OCR for Scanned Documents”?
Tesseract via pytesseract, image preprocessing, and accuracy improvement. You practise AI Agents with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start AI Agents?
No prior experience is required. AI Agents on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “OCR for Scanned Documents” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this AI Agents lesson?
Yes. Every AI Agents lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- PDF Parsing with PyMuPDF and pdfplumber
- OCR for Scanned Documents
- Multi-Document Q&A Agents
- Document Classification and Routing