62 lines
2.5 KiB
Python
62 lines
2.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Measure how legible each page of the photocopied manual is.
|
|
|
|
check_legibility.py copied.pdf [crisp.pdf]
|
|
|
|
OCRs every page (tesseract) and scores it against the PDF's own invisible text
|
|
layer, which is the known-correct text: the score is the share of true words
|
|
that OCR recovers. If a crisp render is given, its score is the baseline (OCR
|
|
is imperfect on monospace tables), and a page is judged by how much the copying
|
|
cost it. Needs tesseract and poppler's pdftotext/pdftoppm. Development tool;
|
|
the build does not run it.
|
|
"""
|
|
import difflib
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from pathlib import Path
|
|
|
|
|
|
def words(text):
|
|
return re.findall(r"[A-Za-z0-9][A-Za-z0-9'.\-]*", text)
|
|
|
|
|
|
def ocr_scores(pdf, dpi=130):
|
|
"""Return {page: (recall, truth_word_count)} for each page of pdf."""
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
tmp = Path(tmp)
|
|
subprocess.run(["pdftoppm", "-r", str(dpi), "-gray", "-png", pdf, str(tmp / "p")], check=True)
|
|
images = sorted(tmp.glob("p-*.png"))
|
|
|
|
def score(img):
|
|
n = int(img.stem.split("-")[1])
|
|
truth = words(subprocess.run(["pdftotext", "-f", str(n), "-l", str(n), pdf, "-"],
|
|
capture_output=True, text=True, check=True).stdout)
|
|
got = words(subprocess.run(["tesseract", str(img), "-", "--psm", "6"],
|
|
capture_output=True, text=True, env={**os.environ, "OMP_THREAD_LIMIT": "1"}).stdout)
|
|
m = difflib.SequenceMatcher(None, truth, got, autojunk=False)
|
|
hit = sum(b.size for b in m.get_matching_blocks())
|
|
return n, (hit / max(1, len(truth)), len(truth))
|
|
|
|
with ThreadPoolExecutor(4) as pool:
|
|
return dict(pool.map(score, images))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
copied = ocr_scores(sys.argv[1])
|
|
crisp = ocr_scores(sys.argv[2]) if len(sys.argv) > 2 else {}
|
|
print("page words crisp copied loss")
|
|
losses = []
|
|
for n in sorted(copied):
|
|
c, w = copied[n]
|
|
b = crisp.get(n, (1.0, w))[0]
|
|
losses.append(b - c)
|
|
flag = " <-- check" if b - c > 0.10 else ""
|
|
print(f"{n:4d} {w:6d} {b:5.0%} {c:5.0%} {b - c:+5.0%}{flag}")
|
|
tot = sum(w for _, w in copied.values())
|
|
print(f"\nmean copied recall {sum(c * w for c, w in copied.values()) / tot:.1%}"
|
|
+ (f", mean crisp {sum(c * w for c, w in crisp.values()) / tot:.1%}" if crisp else ""))
|