Receipt OCR in Python, tested on 99 receipts
84%
of receipt words found by RapidOCR, free
For printed scans: $0.006 a page
pytesseract missed 2 of every 3 words
A word counts when it shows up anywhere in the output. The photos are shop receipts from Indonesia, from the public CORD set.
How I tested it
- 99 receipt photos from CORD v2 by Clova AI (CC BY 4.0). Its truth has the item and total lines only, so shop names and dates are not scored.
- Our Actor's numbers come from its build 0.1.44 code in a local container, with the same scorer.
- The line helper comes from an earlier test on other pages. A simpler helper I tried first on these 99 kept 86% of the priced lines.
- With --psm 4, pytesseract found 34%.
- One Mac (Apple M5 Max): Tesseract 5.5.3, pytesseract 0.3.13, RapidOCR 3.10.0 and ONNX Runtime 1.31.1.
The usual snippet splits almost every price off
RapidOCR returns one box for each run of text. Join
result.txts with new lines, and most prices land on a
line of their own.
The script takes out the photo's tilt, then joins the boxes on each line from left to right. The helper comes from our four engine test.
What counts as a priced line?
A receipt line that ends in a price, totals included. It counts when one of its words and then its price come out close together on one line.
The script: 19 lines, no GPU
pip install rapidocr onnxruntime
import sys
from rapidocr import RapidOCR
from lines import to_lines
engine = RapidOCR()
def read_receipt(path):
"""Receipt photo to text, one receipt line per output line."""
result = engine(path)
if result.boxes is None: # no text found
return ""
return "\n".join(to_lines(result.boxes.tolist(), result.txts))
if __name__ == "__main__":
print(read_receipt(sys.argv[1]))
Save the helper below as lines.py in the same folder.
Then run python receipt_ocr.py receipt.jpg.
lines.py, the 27-line helper
import math
import statistics
def to_lines(boxes, texts):
"""Sort OCR boxes into lines of text, top to bottom, left to right."""
tilts = []
for b in boxes:
(x0, y0), (x1, y1) = b[0], b[1]
if abs(x1 - x0) > 3 * abs(b[3][1] - y0):
tilts.append(math.atan2(y1 - y0, x1 - x0))
tilt = statistics.median(tilts) if tilts else 0
cos, sin = math.cos(tilt), math.sin(tilt)
items = []
for b, text in zip(boxes, texts):
x, y = sum(p[0] for p in b) / 4, sum(p[1] for p in b) / 4
items.append((y * cos - x * sin, x * cos + y * sin, math.dist(b[0], b[3]), text))
height = statistics.median(i[2] for i in items)
rows = []
for item in sorted(items, key=lambda i: i[0]):
if rows and item[0] - rows[-1][-1][0] < height / 2:
rows[-1].append(item)
else:
rows.append([item])
return [" ".join(i[3] for i in sorted(r, key=lambda i: i[1])) for r in rows]
One receipt: 22 of 22 items right
The first 8 lines that the script printed:
Nasi Campur Bali 75,000
Bbk Bengil Nasi 125,000
MilkShake Starwb 37,000
Ice Lemon Tea 24,000
Nasi Ayam Dewata 70,000
Free Ice Tea 0
Organic Green Sa 65,000
Ice Tea 18,000
It dropped the faint quantity column (1 x, 3 x) and the word "Total" in "Grand Total".
What it still gets wrong
- It missed 16% of the words: 393 of 2,501.
- 4 priced lines came out split, on 3 receipts. On 1 of them, most lines split.
- Turn the photo upright first. Turned 90 or 180 degrees, our sample receipt came out scrambled.
- It gives you lines, not fields. To get the total or the date, you still parse the lines.
- We tested printed shop receipts only. We did not test faded thermal paper, handwriting or other scripts.
Where our Actor fits
On receipt photos, use the script above. Our Actor is built for printed scans and PDFs.
- Book scans: 96% of the words, on 60 pages, 5 handwritten.
- Handwritten notes: 52%, on 40 notes. It is weak at handwriting.
Two ways to skip the work
Run it yourself. Our Apify Actor turns scanned PDFs into Markdown, tables included.
Or send us the scan. Tack, our AI, does the job for $29, up to 50 pages. First page free.
Questions people ask
How often does pytesseract return nothing?
At the default page mode, 24 of the 99 receipts came back empty. With --psm 6, only one did.
Is RapidOCR free for business use?
Its code is Apache-2.0, and the notice for its model files says they are not restricted to non-commercial use. Read both notices before you ship.
Does it need a GPU?
No. It ran on the CPU through ONNX Runtime, in about 0.2 seconds a receipt (the median) on our Mac.