ocrsieve
A sieve for document pages. For every page it answers which extraction path the page needs: what comes out for free, what needs OCR, and what needs a strong model.
| Class | Meaning | Extraction |
|---|---|---|
digital_text |
native digital text in the PDF layer | direct read, zero cost, no OCR |
blank |
no content | skip |
typed_clean |
printed or typewritten, no pen strokes at all | cheap OCR |
typed_signed |
printed plus signatures, initials or stamps only | cheap OCR |
typed_annotated |
printed plus handwriting that carries information | OCR that reads handwriting |
handwritten |
the body of the text is handwritten | strong model |
The five model classes sit on one axis - how much handwritten information the page carries - and the routing decision is a single number read off it:
score = p(typed_annotated) + p(handwritten)
Compare it with a threshold and you have the decision. Uncertainty that does not change the decision (clean versus signed) is marginalised away, and uncertainty that does change it pushes the score up, which is the safe side.
typed_clean and typed_signed route to the same place on purpose. Keeping
them apart splits one contradictory training signal - "a pen stroke is
irrelevant here, decisive next door" - into two learnable tasks: detecting that
a stroke exists, and judging whether it carries content.
digital_text is never a prediction: it is read from the file.
Install
pip install ocrsieve[cpu] # inference on CPU (onnxruntime)
pip install ocrsieve[gpu] # inference on CUDA (onnxruntime-gpu)
The base package pulls only pymupdf, pillow and numpy, and the extra
picks the ONNX Runtime build - the two runtimes conflict, so exactly one of
them has to be chosen. Without either, the deterministic stage still works and
the visual model raises a clear ImportError.
The model ships inside the wheel, at ocrsieve/artifacts/model.onnx (16 MB).
There is nothing to download at first run.
CLI
ocrsieve examples/1.jpg
ocrsieve examples/1.pdf
ocrsieve ./folder -o manifest.json # walks the folder, writes JSON
ocrsieve a.pdf b.png ./folder --json # JSON to stdout
ocrsieve ./folder --no-model # deterministic stage only
ocrsieve ./folder --inspect # per-page PDF signals, no model
ocrsieve ./folder --max-image-cover 0 # any image goes to the model
ocrsieve ./folder --threads 8
$ ocrsieve examples/1.jpg
examples/1.jpg
1 typed_clean 0.55 model model: typed_clean (score 0.0429)
1 pages in 1 files (0 mixed)
typed_clean 1 100.0%
decided by: {'model': 1}
Python
classify takes anything: a path to a PDF or an image, a directory entry, raw
bytes, an open file, a PIL.Image or a pymupdf.Document. Multi-page TIFFs
and PDFs come back as several pages.
from ocrsieve import PageClassifier
sieve = PageClassifier()
result = sieve.classify("document.pdf")
result = sieve.classify("scan.jpg")
result = sieve.classify(pdf_bytes, name="upload.pdf")
result = sieve.classify(Image.open("page.png"))
result = sieve.classify(open("document.pdf", "rb"))
for page in result.pages:
print(page.page, page.label, page.needs_ocr, page.decided_by, page.confidence)
result.mixed # pages of different classes in one file
result.pages_needing_ocr()
result.pages_needing_strong_ocr()
result.to_dict() # JSON-serializable
confidence, score and probabilities are filled in whenever the model
decided, and decided_by says who decided: stage1, model or fallback.
One-shot helpers, for when the classifier does not need to be kept around:
from ocrsieve import classify, inspect
classify("document.pdf")
inspect("document.pdf") # deterministic stage only, no model loaded
Deterministic stage only - per-page characteristics, no ONNX call:
from ocrsieve import inspect
for view in inspect("document.pdf"):
print(view.page, view.stage1, view.chars, view.max_image_cover,
view.has_native_text, view.has_invisible_text_layer, view.reason)
Model only - classify images directly, bypassing the PDF logic:
from ocrsieve import VisualClassifier
model = VisualClassifier()
model.predict([image])
# [{'label': 'typed_clean', 'confidence': 0.91, 'score': 0.04,
# 'needs_strong_ocr': False, 'probabilities': {...}}]
Options:
PageClassifier(
model_path=None, # a different .onnx
use_model=True, # False = deterministic stage only
max_image_cover=0.03, # 0 sends any page containing an image to the model
render_width=500, # must match how the model was trained
threads=None, # ONNX Runtime cores
providers=None, # ONNX Runtime providers, CUDA first when available
)
ocrsieve.available_providers() reports what the installed runtime offers, so
a [gpu] install can be checked in one line.
Tests
pip install -e ".[dev]"
pytest
Metadata
Release files for ocrsieve 1.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ocrsieve-1.0.1.tar.gz | 14.9 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ocrsieve-1.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 29.8 MB
Release files / ocrsieve-1.0.1.tar.gz
| Download URL | ocrsieve-1.0.1.tar.gz |
|---|---|
| Size | 14.9 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3db186b8173b3bdffb9314121d5a06a1570055f3e9b1e60e9676c4f59465eec3
|
|
BLAKE2b-256 checksum How to use checksums |
1b2ed666d2f6501878e669745edab629371b788aa21e3f3a36d93f56abfba9d0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.9
|
Release files / ocrsieve-1.0.1-py3-none-any.whl
| Download URL | ocrsieve-1.0.1-py3-none-any.whl |
|---|---|
| Size | 14.9 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a13ac08c8fe74b104a6e185aa45f4663e7bc82462f08034f601e117a367d010a
|
|
BLAKE2b-256 checksum How to use checksums |
87c6a7bb2c1a2189bf2155c672d45ba57c1fabfa382b10aa527991d60b20a434
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.9
|