PaddleOCR: High Accuracy Text Extraction from Images and Documents

# PaddleOCR: Ekstraksi Teks dari Gambar dan Dokumen dengan Akurasi Tinggi Halo temen-temen, kali ini kita bahas salah satu library OCR open source terbaik yang pernah ada, namanya PaddleOCR. Kalau ka...

By Ruby Abdullah · · tutorial
paddleocrocrdocument-aicomputer-visionpython

PaddleOCR: High Accuracy Text Extraction from Images and Documents

Hey everyone, today we look at one of the best open source OCR libraries available: PaddleOCR. If you have ever had to pull text out of an ID card photo, a receipt, an invoice, a form, or a screenshot, you know OCR looks simple and turns out to be full of traps. PaddleOCR is one of the most mature solutions to that problem, and pleasantly, it is free and runs fine on CPU.

What makes PaddleOCR stand out against the older Tesseract is that it is fully deep learning based, supports more than 80 languages, ships very lightweight models (the mobile variants are only a few MB), and most importantly handles slanted text, curved text, and messy backgrounds. Those are exactly the conditions where Tesseract gives up.

We will cover the OCR pipeline architecture, installation, your first extraction, understanding the output format, document layout and table analysis, preprocessing for hard cases, and finally building an invoice data extraction pipeline.

Introduction

How Modern OCR Works

I want you to understand the pipeline first, because it helps enormously when debugging.

Modern OCR is not one model but three sequential stages.

The first stage is text detection. This model finds where text exists in the image and draws boxes (or polygons) around it. Nothing is read yet, only located.

The second stage is angle classification. Each detected crop is checked for orientation, whether it is upright or rotated 180 degrees, and rotated if needed. This stage is optional but very helpful for casually captured photos.

The third stage is text recognition. Each crop containing a single line of text goes into a recognition model that turns pixels into characters.

Why does this matter? Because when your OCR output is bad, the cause can be at any stage. Text that is completely missing usually means a detection failure, not a recognition one. Text that appears with wrong characters is a recognition problem. Diagnosing the right stage saves a lot of time.

Installation

python -m venv venv

source venv/bin/activate # Linux / Mac

venv\Scripts\activate # Windows

CPU

pip install paddlepaddle

pip install paddleocr

Or GPU (match your CUDA version)

pip install paddlepaddle-gpu

Some workflows also need extra image dependencies:

pip install opencv-python pillow

Models download automatically on first use and are cached, so later calls start much faster.

First Extraction

from paddleocr import PaddleOCR

lang="en" for English, "ch" for Chinese, "japan", "korean",

"arabic", "latin" for most Latin-script languages, and more

ocr = PaddleOCR(useanglecls=True, lang="en")

result = ocr.ocr("receipt.jpg", cls=True)

for line in result[0]:

box, (text, score) = line

print(f"{score:.3f} {text}")

The output is a list per image, and each element carries two things: polygon coordinates (four x,y points) and a tuple with the recognized text and its confidence score.

Here is the structure in more detail:

result = ocr.ocr("document.jpg", cls=True)

for line in result[0]:

box = line[0] # [[x1,y1],[x2,y2],[x3,y3],[x4,y4]]

text = line[1][0] # the recognized string

score = line[1][1] # 0.0 to 1.0

left = min(p[0] for p in box)

top = min(p[1] for p in box)

print(f"position ({left:.0f}, {top:.0f}) score {score:.2f} '{text}'")

Those coordinates are not decoration, they are the key to many useful things. With coordinates you can sort text into reading order, group text that lives in the same column, or find the value sitting to the right of a label.

Sorting Results Into Reading Order

This is a practical problem that comes up constantly. PaddleOCR returns results in detection order, which is not always human reading order.

def readingorder(ocrresult, ytolerance=10):

"""Sort OCR results top to bottom, then left to right."""

items = []

for line in ocrresult:

box, (text, score) = line

y = sum(p[1] for p in box) / 4

x = min(p[0] for p in box)

items.append({"x": x, "y": y, "text": text, "score": score})

items.sort(key=lambda i: i["y"])

rows = []

current = []

refy = None

for it in items:

if refy is None or abs(it["y"] - refy) <= ytolerance:

current.append(it)

refy = it["y"] if refy is None else refy

else:

rows.append(sorted(current, key=lambda i: i["x"]))

current = [it]

refy = it["y"]

if current:

rows.append(sorted(current, key=lambda i: i["x"]))

return rows

result = ocr.ocr("invoice.jpg", cls=True)

for row in readingorder(result[0]):

print(" | ".join(i["text"] for i in row))

This groups text with similar vertical position into one row, then orders each row left to right. Tune ytolerance to your image resolution. Larger images need a larger value.

Document Layout and Table Analysis

This is the feature that separates PaddleOCR from ordinary OCR. The PP-Structure module recognizes document layout: which block is a title, a paragraph, a table, a figure. Most usefully, it can reconstruct tables into HTML.

from paddleocr import PPStructure, savestructureres

import cv2

engine = PPStructure(table=True, ocr=True, showlog=False, lang="en")

img = cv2.imread("financialreport.jpg")

result = engine(img)

for block in result:

print("type:", block["type"]) # text, title, table, figure, list

print("bbox:", block["bbox"])

if block["type"] == "table":

print("table HTML:")

print(block["res"]["html"])

savestructureres(result, "output", "report")

To turn that HTML table into a pandas DataFrame:

import pandas as pd

for block in result:

if block["type"] == "table":

table = pd.readhtml(block["res"]["html"])[0]

print(table)

table.toexcel("extractedtable.xlsx", index=False)

This is extremely useful for financial reports, lab results, or any document dominated by tables. I use it often for clients with thousands of scanned PDFs who want the data in a database.

Handling PDFs

PaddleOCR works on images, so PDFs must be rendered first.

import fitz  # pip install pymupdf

from paddleocr import PaddleOCR

import numpy as np

ocr = PaddleOCR(useanglecls=True, lang="en")

doc = fitz.open("document.pdf")

alltext = []

for pagenumber, page in enumerate(doc, start=1):

# Render at high resolution, this matters a lot for accuracy

pix = page.getpixmap(dpi=300)

img = np.frombuffer(pix.samples, dtype=np.uint8).reshape(pix.height, pix.width, pix.n)

img = img[:, :, :3] # drop alpha if present

result = ocr.ocr(img, cls=True)

if result and result[0]:

pagetext = "\n".join(b[1][0] for b in result[0])

alltext.append(f"=== Page {pagenumber} ===\n{pagetext}")

with open("output.txt", "w", encoding="utf-8") as f:

f.write("\n\n".join(alltext))

Note the dpi=300. This is one of the highest impact settings. Rendering a PDF at 72 dpi produces a small blurry image that OCR struggles with. 300 dpi is the sweet spot for most documents. For very small print, try 400.

Preprocessing for Hard Cases

When OCR output is poor, the biggest improvement usually comes from fixing the image rather than swapping models.

import cv2

import numpy as np

def preprocess(path):

img = cv2.imread(path)

# 1. Upscale if the resolution is low

h, w = img.shape[:2]

if max(h, w) < 1000:

scale = 1500 / max(h, w)

img = cv2.resize(img, None, fx=scale, fy=scale, interpolation=cv2.INTERCUBIC)

# 2. Convert to grayscale

gray = cv2.cvtColor(img, cv2.COLORBGR2GRAY)

# 3. Reduce noise while preserving character edges

gray = cv2.bilateralFilter(gray, 9, 75, 75)

# 4. Improve contrast with CLAHE

clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))

gray = clahe.apply(gray)

return cv2.cvtColor(gray, cv2.COLORGRAY2BGR)

clean = preprocess("darkreceiptphoto.jpg")

result = ocr.ocr(clean, cls=True)

For skewed document photos, deskewing helps a lot:

def deskew(img):

gray = cv2.cvtColor(img, cv2.COLORBGR2GRAY)

edges = cv2.Canny(gray, 50, 150, apertureSize=3)

lines = cv2.HoughLinesP(edges, 1, np.pi / 180, 100, minLineLength=100, maxLineGap=10)

if lines is None:

return img

angles = []

for x1, y1, x2, y2 in lines[:, 0]:

a = np.degrees(np.arctan2(y2 - y1, x2 - x1))

if abs(a) < 45:

angles.append(a)

if not angles:

return img

median = np.median(angles)

h, w = img.shape[:2]

M = cv2.getRotationMatrix2D((w // 2, h // 2), median, 1.0)

return cv2.warpAffine(img, M, (w, h), flags=cv2.INTERCUBIC,

borderMode=cv2.BORDERREPLICATE)

One important tip: do not immediately binarize the image (convert to pure black and white) the way people commonly recommend for Tesseract. PaddleOCR's deep learning models actually perform better on grayscale or color images that still have gradation. Aggressive binarization throws away information the model needs.

Case Study: Invoice Data Extraction

Let us combine everything into something useful. Here is a simple extractor that pulls specific fields from an invoice.

import re

from paddleocr import PaddleOCR

ocr = PaddleOCR(useanglecls=True, lang="en")

def extractinvoice(path):

result = ocr.ocr(path, cls=True)

if not result or not result[0]:

return {}

items = []

for line in result[0]:

box, (text, score) = line

items.append({

"text": text,

"score": score,

"x": min(p[0] for p in box),

"y": sum(p[1] for p in box) / 4,

})

joined = " ".join(i["text"] for i in items)

data = {}

# Invoice number

m = re.search(r"invoice[\s#:]*([A-Z0-9\-/]{4,})", joined, re.I)

if m:

data["invoicenumber"] = m.group(1)

# Date

m = re.search(r"(\d{1,2}[/\-]\d{1,2}[/\-]\d{2,4})", joined)

if m:

data["date"] = m.group(1)

# Total: find numeric values near the word total

candidates = []

for it in items:

if re.search(r"total|amount due|grand", it["text"], re.I):

for other in items:

if abs(other["y"] - it["y"]) < 15 and other["x"] > it["x"]:

digits = re.sub(r"[^\d]", "", other["text"])

if digits:

candidates.append(int(digits))

if candidates:

data["total"] = max(candidates)

# Average confidence, used as a quality gate

data["avgscore"] = sum(i["score"] for i in items) / len(items)

return data

print(extractinvoice("invoice.jpg"))

Notice the total lookup. I do not only regex over joined text, I use coordinates to find numbers sitting on the same line as the word "total". That is far more accurate than pure regex, because real invoices contain several similar looking numbers.

Also notice avgscore. In production you need a quality gate. If average confidence falls below a threshold, say 0.85, route that document to a human review queue instead of writing possibly wrong data into your system.

Tips and Best Practices

Resolution is king. Most OCR failures I encounter come from input images that are simply too small. Aim for a character height of at least 20 to 30 pixels.

Do not binarize aggressively. Deep learning OCR differs from classic OCR in this respect.

Enable useanglecls=True for user-submitted photos, since many arrive rotated or upside down.

Store confidence scores and coordinates in your database, not just the text. That data is gold for debugging and for building quality gates.

For fixed-format documents such as ID cards or standard forms, consider a template based approach: define a region of interest per field and run OCR only inside it. Accuracy is far higher than searching patterns across the whole page.

If you have many documents with an unusual font or style, PaddleOCR supports fine-tuning the recognition model on your own data. It is non-trivial work but can dramatically improve accuracy in a narrow domain.

For deployment, instantiate the PaddleOCR object once at module level rather than per request, because model initialization takes several seconds.

Conclusion

PaddleOCR is a very strong choice for nearly any OCR need, and it runs well even on CPU. The key takeaways:

The OCR pipeline is detection, angle classification, and recognition, and knowing which stage failed speeds up debugging enormously.

The output carries coordinates and confidence scores, both as valuable as the text itself.

The PP-Structure module reconstructs tables into HTML that converts directly into a DataFrame, which is a huge win for report documents.

Input image quality matters far more than model choice. Render PDFs at 300 dpi, upscale small images, fix contrast, and deskew crooked pages.

For field extraction, combine regex with coordinate based spatial reasoning, and always add a confidence based quality gate.

Take ten receipt or invoice photos you have on hand, run the pipeline above, and see what percentage extracts correctly. That number will tell you honestly how much preprocessing you still need before the system is production ready. Happy extracting.

Related Articles

Surya OCR Tutorial: Document AI for Text Detection, OCR, and Layout Analysis

Tutorial Surya OCR: Document AI untuk Text Detection, OCR, dan Layout Analysis Surya adalah library Document AI open-sou...

timm: A Warehouse of 1000+ Ready-to-Use Computer Vision Models in PyTorch

timm: Gudang 1000+ Model Computer Vision Siap Pakai di PyTorch Halo temen-temen, di tutorial kali ini aku mau ngajak kal...

DSPy: Stop Hand-Tuning Prompts, Let the Compiler Optimize Them

DSPy: Berhenti Ngoprek Prompt Manual, Biarkan Compiler yang Optimasi Halo temen-temen, kali ini aku mau ngenalin satu li...

Depth Anything V2: Predicting Depth from a Single Image with Python

Depth Anything V2: Prediksi Kedalaman dari Satu Gambar dengan Python Halo temen-temen, di tutorial kali ini aku mau ngaj...