Choosing a Python OCR Library and When to Use an API Instead

Posted on | Last updated on
Choosing a Python OCR Library and When to Use an API Instead

Every Python OCR library does the same two things: find text in an image, and guess what it says. What separates them is how much of your machine they need, how they handle a page that is not a clean scan, and who maintains the model when accuracy drifts.

This walks the three that people actually ship with, then the point where running OCR yourself stops being the cheaper option.

Key takeaways

  • Check for a text layer first, because extracting is exact and recognising is a guess.
  • Tesseract is fastest and cheapest, but most of the work is preprocessing you write.
  • EasyOCR and PaddleOCR read angled photos well, at the cost of size and cold start.
  • Build an evaluation set from your real documents; published rankings will not match.
  • The reason to buy an API is operations, not accuracy.

What you need before any of this works

If the Python side of a Filestack integration is new to you, the Filestack Python client setup guide covers what this article assumes.

OCR operates on images. A PDF that already carries a text layer is not an OCR problem, and recognising it discards text the file already holds. Check first:

import fitz  # pymupdf

doc = fitz.open("invoice.pdf")
text = "".join(page.get_text() for page in doc)
print(f"{len(text)} characters already in the file")

If that prints anything substantial, the text is already there and you should extract it rather than recognise it. OCR is for scans, photographs and PDFs that are wrapped images.

Tesseract, by way of pytesseract

Tesseract is the default answer and has been since Google open-sourced it. pytesseract is a thin wrapper around the binary, so you install both:

brew install tesseract        # or: apt-get install tesseract-ocr
pip install pytesseract pillow
import pytesseract
from PIL import Image

text = pytesseract.image_to_string(Image.open("receipt.png"))
print(text)

That is the whole integration, which is why it is everywhere. What the two-line example hides is that Tesseract is extremely sensitive to input quality. A photograph taken at an angle, a receipt with a fold, or a scan at 150 DPI will produce output that looks like text and is not.

Most production Tesseract code is preprocessing, which pulls in one more dependency:

pip install opencv-python
import cv2

img = cv2.imread("receipt.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
text = pytesseract.image_to_string(gray)

Deskewing, denoising and upscaling to 300 DPI each add another block. The library is free; the engineering around it is where the cost lands.

image_to_data is the call worth knowing, because it returns per-word confidence and lets you reject bad reads rather than passing them downstream:

data = pytesseract.image_to_data(gray, output_type=pytesseract.Output.DICT)
low = [w for w, c in zip(data["text"], data["conf"]) if w.strip() and int(c) < 60]

EasyOCR

A PyTorch model rather than a C++ binary, which changes the trade completely.

pip install easyocr
import easyocr

reader = easyocr.Reader(["en"])          # downloads model weights on first run
for box, text, confidence in reader.readtext("receipt.png"):
    print(f"{confidence:.2f}  {text}")

It handles angled and curved text far better than Tesseract with no preprocessing, and it returns bounding boxes and confidence by default. The costs are size and speed. The model weights are hundreds of megabytes, the first run downloads them, and on CPU a single page takes seconds rather than milliseconds. In a container that has to start quickly, that weight is the whole problem.

It supports around eighty languages, and unlike Tesseract you do not install a separate language pack for each.

PaddleOCR

Separate detection and recognition models, with variants trained for dense, rotated and non-Latin text, which is where this one separates from the other two.

pip install paddlepaddle paddleocr
from paddleocr import PaddleOCR

ocr = PaddleOCR(use_textline_orientation=True, lang="en")
for res in ocr.predict("receipt.png"):
    for text, score in zip(res.json["rec_texts"], res.json["rec_scores"]):
        print(f"{score:.2f}  {text}")

use_textline_orientation is the flag that matters, because it detects and corrects rotated text, which a phone photograph of a document usually contains.

The trade is the dependency tree. PaddlePaddle is a full deep learning framework, the install is large, and version conflicts with an existing PyTorch environment are common enough to plan for. It is the strongest option when accuracy is the requirement and the deployment is yours to control.

How they compare

Tesseract EasyOCR PaddleOCR
Install system binary plus wrapper pip, large weights pip, full framework
Clean scans good good good
Photos and angles poor without preprocessing good best
Speed on CPU fastest slow slow
Bounding boxes via image_to_data default default
Cold start negligible model download model download
Who maintains accuracy you upstream upstream

That last row is the operational difference. All three are libraries you deploy, and when a new document format starts reading badly, improving it is your work.

Where a hosted API changes the arithmetic

A Python OCR library is the right answer when documents are predictable, volume is steady, and you control the machine. It stops being the right answer at a specific point, and the point is usually not accuracy. It is operations.

Running OCR yourself means a container large enough to hold the model, a queue so a burst of uploads does not exhaust memory, retries for the pages that time out, and a person who owns accuracy when the input changes. A hosted ocr api moves all of that behind one request.

With Filestack, recognition is a task in the delivery URL applied to a file you have already stored. The upload half is an ordinary POST, and the key belongs in the environment rather than in the script:

import base64
import hashlib
import hmac
import json
import os
import time

import requests

API_KEY = os.environ["FILESTACK_API_KEY"]
APP_SECRET = os.environ["FILESTACK_APP_SECRET"]

with open("receipt.png", "rb") as f:
    stored = requests.post(
        "https://www.filestackapi.com/api/store/S3",
        params={"key": API_KEY},
        headers={"Content-Type": "image/png"},
        data=f.read(),
    )
stored.raise_for_status()
handle = stored.json()["url"].rsplit("/", 1)[-1]

Recognition is where this stops looking like the plain delivery URLs further down. ocr is a secured task, and a request carrying only a handle returns HTTP 403. It needs a short policy document signed with your app secret, which stays on your server and never reaches the browser:

def security(call, seconds=3600):
    policy = {"expiry": int(time.time()) + seconds, "call": call}
    encoded = base64.urlsafe_b64encode(json.dumps(policy).encode()).decode()
    signature = hmac.new(APP_SECRET.encode(), encoded.encode(), hashlib.sha256).hexdigest()
    return f"security=policy:{encoded},signature:{signature}"


url = f"https://cdn.filestackcontent.com/{API_KEY}/{security(['read', 'convert'])}/ocr/{handle}"
response = requests.get(url)
response.raise_for_status()
result = response.json()

print(result["text"])

result["text"] is the whole document as plain text. Underneath it, result["document"]["text_areas"] repeats the same words with a bounding box for every line and every word, which is what you want when a value’s position on the page is part of its meaning, as it is on an invoice. One thing the response does not carry is a per-word confidence score, so the “flag anything under 0.80 for a human” queue that image_to_data and EasyOCR make possible is not available here. A signed request still returns 403 when the task is not included in your plan, and the document capture and data extraction page lists which plans include it.

The path that needs no recognition at all

If your documents are PDFs that already carry a text layer, none of the above applies. No signature, no key, no recognition step, only the handle and a task:

import requests

handle = "jLoemkNMSiyxhKHxo4G0"
text = requests.get(f"https://cdn.filestackcontent.com/output=format:txt/{handle}").text

That returns the text a PDF already contains, exactly, with no recognition step and no accuracy question. Check for a text layer before reaching for OCR. Extracted text is what the file declares; recognized text is a reading of pixels.

Join the Filestack developer community on Discord

Language support and what it costs you

The three libraries handle multilingual documents very differently, and this is where a comparison based on English-only benchmarks misleads.

Tesseract needs a language pack installed per language, and you tell it which one to use. Getting that wrong produces confident nonsense rather than an error, because the engine reads French as though it were English. Detecting the language first, or passing several at once with lang="eng+fra", costs accuracy in both.

EasyOCR loads a model per language group and accepts several at once in the Reader constructor. The practical limit is memory: each additional group is more weight resident in the process, which matters in a container sized to a budget.

PaddleOCR ships separate detection and recognition models per language and switches between them with the lang argument. Its Chinese, Japanese and Korean models are the strongest of the three.

A hosted API sidesteps the question, since language selection happens server side and adding a language is not a deployment. How much that matters depends on how many languages the product actually sees.

Running any of this at volume

Whichever you choose, the shape of the production system is similar.

Do the work off the request path. OCR takes seconds, and a web request that waits for it is a request that times out under load. The upload returns immediately, recognition happens in a worker, and the result arrives by webhook or polling.

Bound the concurrency. Each worker running a model holds its weights in memory. Four workers on a machine sized for one is how an OCR service dies, and it dies at exactly the moment traffic justifies it.

Store the raw text, not just the parsed fields. Extraction rules change more often than documents do, and re-running recognition to recover a field you did not think to keep is far more expensive than storing a few kilobytes of text.

Keep the original. Whatever you extracted, someone will eventually need to see the page it came from, usually during a dispute.

Choosing between them

Three questions decide it, and none of them is about which library scores best.

Do the documents look the same every time? Fixed-format scans from one scanner suit a local library. Whatever a customer uploads from a phone does not.

Does a burst of a thousand pages have to finish today? Local OCR is bounded by your own compute, and sizing for the peak means paying for the peak all year.

Who fixes it when accuracy drops? With a library, that is your team reading papers about preprocessing. With a service, the improvement arrives in a version somebody else shipped.

For a batch job over a controlled corpus, Tesseract remains hard to beat on cost. For user uploads at unpredictable volume, the operational surface is what you are really buying.

What people build with this

OCR is usually one step in a pipeline rather than the product. Text extraction feeds a search index, as in this OCR for document archiving walkthrough. Extracted fields feed a ledger. Seasonal document intake arrives in a six-week window and disappears, which is the shape that argues hardest against sizing your own hardware.

In each of those, recognition is a small part. Getting the file, storing it, keeping it available and handing text to the next step is most of the work.

Measuring accuracy honestly

Build a small evaluation set before choosing.

Take fifty documents that look like the ones you will actually receive. Not clean samples, the real distribution: the phone photograph at an angle, the fax, the third-generation photocopy, the one with a coffee ring. Type out the correct text for each by hand once.

Then measure character error rate rather than eyeballing the output:

def cer(truth: str, got: str) -> float:
    if not truth:
        return 0.0 if not got else 1.0
    previous = list(range(len(got) + 1))
    for i, t_char in enumerate(truth, start=1):
        current = [i] + [0] * len(got)
        for j, g_char in enumerate(got, start=1):
            cost = 0 if t_char == g_char else 1
            current[j] = min(
                previous[j] + 1,
                current[j - 1] + 1,
                previous[j - 1] + cost,
            )
        previous = current
    return previous[-1] / len(truth)

That is edit distance divided by the length of the reference text, the standard definition, not a stand-in for it.

Run all three libraries over the set and compare. Two things usually surface. The ranking on your documents rarely matches the published ranking, because your documents are not the benchmark corpus. And the spread between preprocessing settings on one library is often wider than the spread between libraries, which tells you where to spend the next week.

Keep that evaluation set. It is what turns a later change from a guess into a measurement, and it is the only way to tell whether a model upgrade helped or quietly regressed a document type you rely on.

Questions people ask

Which Python OCR library is most accurate?

On clean scans all three are close. On photographs and angled text, PaddleOCR and EasyOCR read what Tesseract needs preprocessing to reach. The gap between libraries is usually smaller than the gap preprocessing makes to any one of them, which is why the evaluation set above settles it rather than a benchmark.

Can I run OCR without installing anything?

Yes, through an API. The trade is a request instead of a dependency tree.

Does OCR work on PDFs?

Only on PDFs that are images. If the PDF has a text layer, extract it directly rather than recognising it, which is both faster and exact.

How do I improve bad results?

In order of impact: increase resolution to at least 300 DPI, convert to greyscale, threshold to pure black and white, then deskew. Model choice comes after all four.

Should I use a large language model for this instead?

For reading a page, no. Vision models are far more expensive per page and slower than a purpose-built recogniser, and they will invent plausible text where a recogniser returns a low confidence score you can act on. Where they genuinely help is the step after recognition, turning extracted text into structured fields, which is a different problem from reading the pixels.

How much does self-hosting actually cost?

Compute is rarely the expensive part. A machine large enough to hold a model and process a steady trickle of pages is cheap. The cost is the engineer who owns preprocessing, the queue, the retry logic and the accuracy regressions, and that cost does not scale down when volume does.

 

 

Read More →