Use this Skill to transcribe historical documents: Tesseract 5 OCR with preprocessing, Kraken for historical fonts, confidence filtering, and post-OCR correction with symspellpy.
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Um comando direto ignora o prompt de revisão. Verifique a origem antes de executá-lo.
Instruções da origem · Visualização somente leitura
name
historical-ocr
description
Use this Skill to transcribe historical documents: Tesseract 5 OCR with preprocessing, Kraken for historical fonts, confidence filtering, and post-OCR correction with symspellpy.
TL;DR — Transcribe printed and handwritten historical documents using Tesseract 5 LSTM
with OpenCV preprocessing, Kraken for historical typefaces, confidence filtering, and
symspellpy post-correction to reduce word error rate on archaic vocabulary.
When to Use
Use this Skill when you need to:
Digitize printed historical documents (early modern newspapers, books, pamphlets)
Transcribe manuscripts with non-standard letterforms (long-s, ligatures, blackletter)
Process large batches of archival page scans and generate quality reports
Apply post-OCR correction to reduce errors from unusual historical spelling
Extract word-level bounding boxes and confidence scores for downstream NLP
Do not use this Skill for:
Modern documents with clean typography (use a simple pytesseract.image_to_string call)
Real-time camera OCR on mobile (latency budget too low)
HTML with word bounding boxes, baselines, and per-word confidence
Language packs
eng, lat, deu, fra, spa; install with apt install tesseract-ocr-[lang]
Long-s / ligatures
Characters unique to early modern printing; require historical traineddata
Confidence score
Per-word integer 0–100 from image_to_data(); filter below 60 as uncertain
Kraken is an alternative OCR engine trained explicitly on historical typefaces and
handwriting. It uses a segmentation-first workflow: kraken.pageseg.segment() detects
text lines, then a trained model reads each line.
Post-OCR correction with symspellpy applies edit-distance lookup against a historical
vocabulary dictionary. Words with confidence below threshold are replaced by the closest
dictionary entry, reducing character error rate by 15–30% on 17th-century German texts.
Raw archival scans typically require noise removal, binarization, and deskew before OCR.
The pipeline below uses OpenCV and converts to a PIL Image for pytesseract.
import cv2
import numpy as np
from PIL import Image
import pytesseract
defpreprocess_historical_image(
image_path: str,
deskew: bool = True,
method: str = "otsu",
) -> tuple[np.ndarray, Image.Image]:
"""
Preprocess a historical document scan for Tesseract OCR.
Steps:
1. Load in colour, convert to grayscale.
2. Denoise with a Gaussian blur (light despeckling).
3. Binarize with Otsu or adaptive thresholding.
4. Optionally deskew using the Hough line transform.
Args:
image_path: Absolute path to the input image (JPG, PNG, TIFF).
deskew: Whether to correct rotational skew.
method: Binarization method: "otsu" | "adaptive".
Returns:
Tuple of (binary numpy array, PIL Image ready for pytesseract).
"""
img_bgr = cv2.imread(image_path)
if img_bgr isNone:
raise FileNotFoundError(f"Cannot open image: {image_path}")
gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
# Despeckling: Gaussian blur removes salt-and-pepper noise
denoised = cv2.GaussianBlur(gray, (3, 3), 0)
# Binarizationif method == "otsu":
_, binary = cv2.threshold(denoised, 0, 255,
cv2.THRESH_BINARY + cv2.THRESH_OTSU)
elif method == "adaptive":
binary = cv2.adaptiveThreshold(
denoised, 255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY,
blockSize=31,
C=10,
)
else:
raise ValueError(f"Unknown method '{method}'. Use 'otsu' or 'adaptive'.")
# Morphological closing to reconnect broken character strokes
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2))
binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
# Deskewif deskew:
binary = _deskew(binary)
pil_image = Image.fromarray(binary)
return binary, pil_image
def_deskew(binary: np.ndarray) -> np.ndarray:
"""
Correct rotational skew using minAreaRect on the foreground text pixels.
Args:
binary: Binarized image as numpy array (white text on black OR inverted).
Returns:
Deskewed binary image.
"""# Invert so text pixels are white (foreground)
inverted = cv2.bitwise_not(binary)
coords = np.column_stack(np.where(inverted > 0))
iflen(coords) < 10:
return binary # not enough foreground pixels
angle = cv2.minAreaRect(coords)[-1]
if angle < -45:
angle = 90 + angle
(h, w) = binary.shape
center = (w // 2, h // 2)
M = cv2.getRotationMatrix2D(center, angle, 1.0)
deskewed = cv2.warpAffine(
binary, M, (w, h),
flags=cv2.INTER_CUBIC,
borderMode=cv2.BORDER_REPLICATE,
)
return deskewed
defrun_tesseract_with_confidence(
pil_image: Image.Image,
lang: str = "eng",
confidence_threshold: int = 60,
psm: int = 6,
) -> dict:
"""
Run Tesseract OCR and return full text plus confidence-filtered word list.
Args:
pil_image: Preprocessed PIL Image.
lang: Tesseract language code(s), e.g. "eng+lat".
confidence_threshold: Discard words with confidence below this value.
psm: Page segmentation mode (6 = uniform text block).
Returns:
Dict with keys: full_text (str), words (list of dicts with
keys text/conf/left/top/width/height), low_conf_words (list).
"""
config = f"--oem 1 --psm {psm}"
full_text = pytesseract.image_to_string(pil_image, lang=lang, config=config)
data = pytesseract.image_to_data(
pil_image, lang=lang, config=config,
output_type=pytesseract.Output.DICT,
)
words = []
low_conf_words = []
for i, word_text inenumerate(data["text"]):
word_text = word_text.strip()
ifnot word_text:
continue
conf = int(data["conf"][i])
entry = {
"text": word_text,
"conf": conf,
"left": data["left"][i],
"top": data["top"][i],
"width": data["width"][i],
"height": data["height"][i],
}
words.append(entry)
if conf < confidence_threshold:
low_conf_words.append(entry)
return {
"full_text": full_text,
"words": words,
"low_conf_words": low_conf_words,
"mean_confidence": float(np.mean([w["conf"] for w in words])) if words else0.0,
}
Step 2 — Batch Folder OCR with Quality Report
import os
import pandas as pd
from pathlib import Path
defbatch_ocr_folder(
input_dir: str,
output_dir: str,
lang: str = "deu",
extensions: tuple = (".jpg", ".jpeg", ".tif", ".tiff", ".png"),
confidence_threshold: int = 60,
) -> pd.DataFrame:
"""
Run OCR on every image in a folder and produce a quality report CSV.
Args:
input_dir: Directory containing page scans.
output_dir: Directory to write .txt transcript files.
lang: Tesseract language code(s).
extensions: Accepted image file extensions.
confidence_threshold: Confidence threshold for quality flagging.
Returns:
DataFrame with one row per image: filename, mean_conf, low_conf_pct,
word_count, flagged (bool).
"""
os.makedirs(output_dir, exist_ok=True)
image_paths = sorted(
p for p in Path(input_dir).iterdir()
if p.suffix.lower() in extensions
)
ifnot image_paths:
raise ValueError(f"No images found in {input_dir}")
records = []
for img_path in image_paths:
try:
_, pil_img = preprocess_historical_image(str(img_path))
result = run_tesseract_with_confidence(
pil_img, lang=lang,
confidence_threshold=confidence_threshold,
)
except Exception as exc:
print(f"[WARN] Failed on {img_path.name}: {exc}")
records.append({
"filename": img_path.name,
"mean_conf": 0.0,
"low_conf_pct": 100.0,
"word_count": 0,
"flagged": True,
"error": str(exc),
})
continue# Write transcript
out_path = Path(output_dir) / (img_path.stem + ".txt")
out_path.write_text(result["full_text"], encoding="utf-8")
word_count = len(result["words"])
low_conf_pct = (
100.0 * len(result["low_conf_words"]) / word_count
if word_count > 0else100.0
)
records.append({
"filename": img_path.name,
"mean_conf": round(result["mean_confidence"], 1),
"low_conf_pct": round(low_conf_pct, 1),
"word_count": word_count,
"flagged": result["mean_confidence"] < confidence_threshold,
"error": "",
})
print(f" {img_path.name}: mean_conf={result['mean_confidence']:.1f} "f"words={word_count} low_conf={low_conf_pct:.1f}%")
report_df = pd.DataFrame(records)
report_path = Path(output_dir) / "quality_report.csv"
report_df.to_csv(report_path, index=False)
print(f"\nQuality report saved to {report_path}")
print(report_df.describe())
return report_df
Step 3 — symspellpy Post-OCR Correction
from symspellpy import SymSpell, Verbosity
defbuild_symspell(
dictionary_path: str,
max_edit_distance: int = 2,
) -> SymSpell:
"""
Load a frequency dictionary into SymSpell for post-OCR correction.
Args:
dictionary_path: Path to a SymSpell-format frequency dictionary
(word SPACE frequency, one per line).
max_edit_distance: Maximum edit distance for lookup (2 recommended).
Returns:
Loaded SymSpell instance.
"""
sym_spell = SymSpell(max_dictionary_edit_distance=max_edit_distance)
loaded = sym_spell.load_dictionary(
dictionary_path, term_index=0, count_index=1, encoding="utf-8"
)
ifnot loaded:
raise ValueError(f"Failed to load dictionary from {dictionary_path}")
return sym_spell
defcorrect_ocr_text(
words: list[dict],
sym_spell: SymSpell,
confidence_threshold: int = 60,
max_edit_distance: int = 2,
) -> str:
"""
Apply symspellpy correction to low-confidence OCR words.
Only words below confidence_threshold are looked up; high-confidence
words are kept as-is to avoid over-correction.
Args:
words: List of word dicts from run_tesseract_with_confidence().
sym_spell: Loaded SymSpell instance.
confidence_threshold: Words below this confidence are candidates for correction.
max_edit_distance: Maximum edit distance for the lookup.
Returns:
Corrected full text string.
"""
corrected_tokens = []
for w in words:
token = w["text"]
if w["conf"] < confidence_threshold and token.isalpha():
suggestions = sym_spell.lookup(
token.lower(),
Verbosity.CLOSEST,
max_edit_distance=max_edit_distance,
)
if suggestions:
candidate = suggestions[0].term
# Preserve capitalisation heuristicif token[0].isupper():
candidate = candidate.capitalize()
corrected_tokens.append(candidate)
else:
corrected_tokens.append(token)
else:
corrected_tokens.append(token)
return" ".join(corrected_tokens)
defcalculate_wer(reference: str, hypothesis: str) -> float:
"""
Calculate Word Error Rate (WER) = (S + D + I) / N.
Args:
reference: Ground-truth transcription string.
hypothesis: OCR output string.
Returns:
WER as a float between 0.0 and 1.0.
"""
ref_words = reference.lower().split()
hyp_words = hypothesis.lower().split()
n = len(ref_words)
if n == 0:
return0.0# Dynamic programming edit distance on word sequences
dp = list(range(len(hyp_words) + 1))
for i, rw inenumerate(ref_words):
new_dp = [i + 1]
for j, hw inenumerate(hyp_words):
if rw == hw:
new_dp.append(dp[j])
else:
new_dp.append(1 + min(dp[j], dp[j + 1], new_dp[-1]))
dp = new_dp
return dp[len(hyp_words)] / n
Advanced Usage
hOCR Output with Coordinates
defextract_hocr(
pil_image: Image.Image,
lang: str = "eng",
output_path: str = None,
) -> str:
"""
Generate hOCR (HTML with bounding boxes) from a preprocessed image.
The hOCR format encodes word coordinates as:
title="bbox left top right bottom; x_wconf NN"
which is useful for downstream alignment with page facsimiles.
Args:
pil_image: Preprocessed PIL Image.
lang: Tesseract language code.
output_path: If given, write the hOCR HTML to this file.
Returns:
hOCR HTML string.
"""
config = "--oem 1 --psm 6"
hocr = pytesseract.image_to_pdf_or_hocr(
pil_image, lang=lang, config=config, extension="hocr"
)
hocr_str = hocr.decode("utf-8")
if output_path:
withopen(output_path, "w", encoding="utf-8") as fh:
fh.write(hocr_str)
print(f"hOCR saved to {output_path}")
return hocr_str
Kraken Segmentation for Historical Fonts
# Install Kraken and download a historical Latin model
pip install kraken
kraken get 10.5281/zenodo.6657808
# Binarize input image
kraken -i page_001.jpg page_001_bin.png binarize
# Segment text lines
kraken -i page_001_bin.png page_001_seg.json segment
# Transcribe with a historical model
kraken -i page_001_bin.png page_001.txt ocr -m historical_latin.mlmodel
Troubleshooting
Problem
Cause
Fix
TesseractNotFoundError
Tesseract binary not on PATH
Set pytesseract.pytesseract.tesseract_cmd = '/usr/bin/tesseract'
Empty output on good image
Wrong PSM mode
Try --psm 3 (auto) or --psm 11 (sparse text)
Very low confidence (<30) across page
Binarization threshold too aggressive
Switch from Otsu to adaptive (method="adaptive")
Long-s (ſ) misread as f
No historical traineddata
Download enm (Middle English) or custom traineddata
SymSpell: dictionary not loaded
Wrong path or encoding
Ensure UTF-8; check term/count column indices
Deskew makes image worse
Very curved binding warps
Set deskew=False; use manual GCP-based correction
cv2.error on TIFF
Unsupported TIFF compression
Open with Pillow first: pil_img = Image.open(path).convert("RGB"); convert via numpy