Skip to main content

ocr-cleaner

An OCR engine reads what you hand it. Hand it a page that is two degrees off horizontal, speckled, and darker at one edge than the other, and it will read that. This straightens the page, takes the grain off it and binarises it against the lighting it actually has - and tells you which of those it did, which it skipped, and why.

Install

pip install ocr-cleaner

Quickstart

import numpy as np, ocr_cleaner

page = np.full((1100, 850), 246, dtype=np.uint8)      # a sheet of paper
for top in range(120, 1000, 34):                      # rows of text on it
    page[top:top + 11, 90:760] = 50

result = ocr_cleaner.clean(page)
print(result.summary())
ocr-cleaner: <array 850x1100>
  page        document, 26 line-shaped bands of text, 79% of the page bare paper
  size        850 x 1100 in, 850 x 1100 out
  text        lines about 11 px tall
  skew        +0.00 degrees measured, not corrected
  result      black and white
  steps
    grayscale skipped already 8-bit greyscale
    deskew    skipped the page is already straight at +0.00 degrees, under the 0.05 degree
                     floor worth an interpolation pass
    border    skipped no scanner edge or black margin found; every side of the page is
                     already paper
    denoise   skipped the page is already clean: the paper measures 0.0 grey levels of grain
                     and 0.00% specks, under the 1.5 level floor - filtering it would only
                     soften the text
    threshold applied local mean over a 33 x 33 window (3x the 11 px text height), ink is 24
                     levels below its surroundings
    upscale   skipped no upscale_to_dpi was asked for, so the page keeps its own resolution

result.image is the cleaned page. result.steps is the list above, and it is the point: a page that comes back looking much as it went in tells you which steps stood down rather than leaving you to guess.

For a file in and a file out, one line:

result = ocr_cleaner.clean_file("scan.tif", "clean.png", dpi=200, upscale_to_dpi=300)

What it does

Six steps, in this order, each one reported in result.steps whether it ran or not:

  • grayscale - luminance, from any Pillow mode: colour, palette, 16-bit, 1-bit, or transparency composited onto white. An EXIF orientation tag is honoured first, so a page photographed sideways is not reported as ninety degrees of skew.
  • deskew - the angle of the text, to a fraction of a degree, then a rotation back to horizontal with the new corners filled in the page's own paper colour rather than white. A page already straight is left alone and says so.
  • border - the black band a scanner leaves down the side of a page smaller than its glass. A band is solid black across its whole width, where even heavy display type leaves paper between the words, so the two are told apart by counting rather than guessing.
  • denoise - a median filter, sized so its window stays well inside the width of a stroke: 3 x 3 for ordinary body text, 5 x 5 only for text large enough to survive it. Grain is measured on the paper, where paper is supposed to be flat, so a crisp page is not softened for nothing.
  • threshold - "adaptive" compares every pixel to the mean of its own neighbourhood, with a window taken from the height of the text. That is the default because a page lit unevenly is the normal case, and one global cut has to choose between losing the text at the dark end and flooding the bright one. "otsu" is that single cut, for a page lit evenly end to end. "none" leaves the page in greyscale.
  • upscale - to upscale_to_dpi, and only when dpi says what the page is now. OCR engines do better at 300 dpi, and enlarging blind makes a page worse rather than better, so with only one of the two numbers this step does nothing and says so.

And three pages that are not put through any of it:

  • A blank sheet is reported blank and handed back untouched. Thresholding blank paper turns its grain into a field of speckles that an OCR engine reads as text, which is worse than doing nothing.
  • A photograph is reported as a photograph. It has no paper level, no lines and no skew, and binarising one destroys it.
  • A page that is already clean passes through with only the threshold applied, and the other four steps each say what they measured and why they stood down.

Pure numpy and Pillow. No OpenCV, no OCR engine, no model download, no network, and the same page always gives the same result. A 4000 x 3000 scan cleans in a couple of seconds, and your image is never modified.

This is the companion to document-quality, which decides whether a page is worth OCRing. This one improves it.

API

Call What you get
clean(image, *, deskew=True, denoise=True, threshold="adaptive", border=True, upscale_to_dpi=None, dpi=None) a CleanResult
clean_file(src, dst, **kw) the same, written to dst
estimate_skew(image) degrees counter-clockwise off horizontal, as a float

image is a path, a PIL.Image.Image, or a numpy array shaped (h, w), (h, w, 1), (h, w, 3) or (h, w, 4). Greyscale and colour are both fine.

CleanResult

Attribute Meaning
.image the cleaned page, a PIL.Image.Image in mode L
.steps list[Step], one per stage, in order
.skew_corrected_degrees how far the page was actually turned
.estimated_text_height_px line height in source pixels, ascender top to descender foot, or None
.page_kind "document", "blank" or "photograph"
.estimated_skew_degrees what was measured, corrected or not
.applied / .skipped step names, as two lists
.is_blank / .is_photograph / .binary the one-word answers
.summary() the report above, as plain ASCII text
.to_dict() / .to_json() the same, JSON-safe, minus the pixels
.save(path) write .image, creating parent directories

Step has .name, .applied and .detail - and .detail is never empty, including when .applied is False.

estimate_skew is positive counter-clockwise, matching PIL.Image.rotate, so image.rotate(-ocr_cleaner.estimate_skew(image)) straightens a page by hand.

CLI

ocr-cleaner scan.png                                  # report, writes nothing
ocr-cleaner scan.png --output clean.png
ocr-cleaner scans/ --out-dir cleaned/ --suffix -clean
ocr-cleaner scan.tif --dpi 200 --upscale-to-dpi 300 --output big.tif
ocr-cleaner scan.png --threshold otsu --no-denoise
ocr-cleaner scans/ --recursive --quiet
ocr-cleaner scan.png --json

Nothing is written unless you ask with --output or --out-dir. --require-document exits 2 when a page turns out to be blank or not a document, which is the flag a batch job wants. ocr-cleaner --help lists the rest.

License

MIT

Metadata

Release files for ocr-cleaner 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ocr-cleaner 0.1.0
File Size Uploaded
ocr_cleaner-0.1.0.tar.gz 54.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ocr-cleaner 0.1.0
File Interpreter ABI Platform
ocr_cleaner-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 95.8 kB

Release files / ocr_cleaner-0.1.0.tar.gz

Download URL ocr_cleaner-0.1.0.tar.gz
Size 54.4 kB
Tags Source
SHA-256 checksum
How to use checksums
9dea5dd3425572023958182d0353e8c66da67e625c985259a7c2182c8d9da1fd
BLAKE2b-256 checksum
How to use checksums
e343c9666b248659fa2acf8d206ce646d3efe59e5dca0ac403e4c465a9c961fa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / ocr_cleaner-0.1.0-py3-none-any.whl

Download URL ocr_cleaner-0.1.0-py3-none-any.whl
Size 41.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5c2f083f4037d755da9e1dede55a176067bf81916cfbf0199b98476346c24b68
BLAKE2b-256 checksum
How to use checksums
fe83e62a6288aaf11e9c0286ac39dab62279e725ed035be4bf144c0a4d38748e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page