Article
Extract text from an image with Python (OCR that actually works)
Tesseract plus a few lines of OpenCV preprocessing turns blurry photos into clean text. Learn the cleanup steps that take accuracy from unusable to reliable.
On this page0%
- What you need
- Step 1 — Confirm Tesseract is reachable
- Step 2 — Read text the naive way
- Step 3 — Preprocess: the part that matters
- Step 4 — Straighten the page
- Step 5 — Tell Tesseract what shape the text is
- Step 6 — Get positions, not just text
- Step 7 — A reusable function
- Common problems (and fixes)
- FAQ
- How do I extract text from an image in Python?
- Why is my Tesseract OCR accuracy so bad?
- What does the psm option do in Tesseract?
- How do I fix TesseractNotFoundError in Python?
- Can I get the position of each word from Tesseract?
- Next reading on Sythra Articles
Most OCR tutorials show you three lines, get poor results, and stop. The three lines are real, but the accuracy comes from what you do to the image before those lines run.
OCROptical Character Recognition — reading text out of a picture engines want black text on a flat white background, straight and reasonably large. Photos are none of those things. This guide covers the cleanup that turns garbage output into something you can use.
What you need
Tesseract is a separate program, not just a Python package. Install both.
Windows — download the installer from UB Mannheim's build, then note the install path.
macOS
brew install tesseract
Linux
sudo apt install tesseract-ocr
Then the Python side:
python -m pip install pytesseract opencv-python pillow
Step 1 — Confirm Tesseract is reachable
import pytesseract
# Windows only — point at the executable you installed
# pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"
print(pytesseract.get_tesseract_version())
TesseractNotFoundError means the program is installed but Python cannot find it. Uncomment that line and set your real path. This is the single most common setup failure.
Step 2 — Read text the naive way
import cv2
import pytesseract
image = cv2.imread("receipt.jpg")
if image is None:
raise FileNotFoundError("Check the path — imread returned None")
text = pytesseract.image_to_string(image)
print(text)
That is None guard is not optional. cv2.imread returns None for a bad path without raising anything — the trap explained in NoneType is not subscriptable.
On a clean scan this already works. On a phone photo the output is usually a mess. Fix that next.
Step 3 — Preprocess: the part that matters
Five steps, in this order:
import cv2
def prepare(path):
image = cv2.imread(path)
if image is None:
raise FileNotFoundError(path)
# 1. Grayscale — colour carries no information for OCR
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 2. Upscale — Tesseract wants characters roughly 30 px tall
gray = cv2.resize(gray, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC)
# 3. Denoise while keeping edges sharp
gray = cv2.bilateralFilter(gray, 9, 75, 75)
# 4. Adaptive threshold — handles uneven lighting
binary = cv2.adaptiveThreshold(
gray, 255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY,
blockSize=31, C=15,
)
return binary
Why each step earns its place:
- Grayscale — Tesseract ignores colour anyway
- Upscale 2× — small text is the number one cause of bad OCR
- Bilateral filter — removes grain without softening letter edges the way a blur would
- Adaptive threshold — computes a separate cutoff per region, so a shadow across half the page stops mattering
Plain cv2.threshold with a fixed value works on flat scans and fails on anything photographed by hand. Prefer the adaptive version.
Step 4 — Straighten the page
Even five degrees of rotation hurts accuracy noticeably. Tesseract can measure the skew for you:
import cv2
import numpy as np
import pytesseract
def deskew(binary):
osd = pytesseract.image_to_osd(binary, output_type=pytesseract.Output.DICT)
angle = osd.get("rotate", 0)
if angle == 0:
return binary
h, w = binary.shape[:2]
matrix = cv2.getRotationMatrix2D((w // 2, h // 2), -angle, 1.0)
return cv2.warpAffine(
binary, matrix, (w, h),
flags=cv2.INTER_CUBIC,
borderMode=cv2.BORDER_REPLICATE,
)
image_to_osd reports orientation and script detection. It occasionally throws on images with very little text, so wrap it in try/except and fall back to the unrotated image.
Step 5 — Tell Tesseract what shape the text is
The --psm (page segmentation mode) flag changes results dramatically. Using the right one is often worth more than any preprocessing:
| Mode | Use for |
|---|---|
--psm 3 | Default — a full page with mixed layout |
--psm 4 | A single column, like a receipt |
--psm 6 | A uniform block of text |
--psm 7 | One single line |
--psm 8 | One single word |
--psm 11 | Sparse text scattered anywhere |
config = "--oem 3 --psm 6"
text = pytesseract.image_to_string(binary, config=config)
Restrict the character set when you know the format — this alone eliminates most of the classic O versus 0 confusion:
digits_only = "--psm 7 -c tessedit_char_whitelist=0123456789"
plate = pytesseract.image_to_string(binary, config=digits_only)
Step 6 — Get positions, not just text
image_to_data returns every word with its bounding box and a confidence score, which lets you filter out junk:
import pytesseract
from pytesseract import Output
data = pytesseract.image_to_data(binary, output_type=Output.DICT)
for i, word in enumerate(data["text"]):
confidence = int(data["conf"][i])
if word.strip() and confidence > 60:
x, y = data["left"][i], data["top"][i]
print(f"{word!r} at ({x}, {y}) confidence {confidence}")
Dropping everything under 60 confidence removes most nonsense output in one line. You can also draw the boxes on the original image to see exactly what Tesseract read and where.
Step 7 — A reusable function
import cv2
import pytesseract
def image_to_text(path, psm=6, whitelist=None):
image = cv2.imread(path)
if image is None:
raise FileNotFoundError(path)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray = cv2.resize(gray, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC)
gray = cv2.bilateralFilter(gray, 9, 75, 75)
binary = cv2.adaptiveThreshold(
gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 31, 15,
)
config = f"--oem 3 --psm {psm}"
if whitelist:
config += f" -c tessedit_char_whitelist={whitelist}"
return pytesseract.image_to_string(binary, config=config).strip()
if __name__ == "__main__":
print(image_to_text("receipt.jpg", psm=4))
Pair it with the folder-walking pattern from our file renaming guide and you can OCR an entire directory of scans in one run.
Common problems (and fixes)
| Problem | Fix |
|---|---|
TesseractNotFoundError | Set tesseract_cmd to the real executable path |
| Output is gibberish | Upscale 2–3× and switch to adaptive threshold |
| Numbers read as letters | Add tessedit_char_whitelist=0123456789 |
| Columns merge together | Try --psm 4 or crop each column separately |
| Blank output | The image may be inverted — try cv2.bitwise_not |
| Slow on large scans | Resize down to about 2000 px on the long edge |
FAQ
How do I extract text from an image in Python?
Install Tesseract and pytesseract, load the image with OpenCV, convert it to grayscale, upscale it, apply an adaptive threshold, then call pytesseract.image_to_string on the cleaned image.
Why is my Tesseract OCR accuracy so bad?
Almost always the input image. Characters should be roughly 30 pixels tall, the background flat white, and the page straight. Upscaling 2× and using adaptive thresholding fixes most cases.
What does the psm option do in Tesseract?
Page segmentation mode tells Tesseract what layout to expect. Use 6 for a block of text, 4 for a single column such as a receipt, and 7 for a single line.
How do I fix TesseractNotFoundError in Python?
The Tesseract program is not on your PATH. Set pytesseract.pytesseract.tesseract_cmd to the full path of tesseract.exe on Windows, or install it with Homebrew or apt on macOS and Linux.
Can I get the position of each word from Tesseract?
Yes. pytesseract.image_to_data returns each word with its bounding box coordinates and a confidence score, so you can filter low-confidence results and draw boxes on the image.