OCR
OpenCV
Python
Digit Recognition
Image Processing

Simple Digit Recognition OCR in OpenCV-Python

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

Introduction

A simple digit OCR system does not need a deep neural network to be useful. For clean images, a classical pipeline of preprocessing, contour extraction, resizing, and a lightweight classifier can work well enough to prove the concept and solve small tasks.

A Practical OCR Pipeline

For digits, the pipeline is usually:

  • read the image in grayscale
  • threshold it so digits stand out from the background
  • find connected components or contours
  • crop each digit region
  • resize each crop to the format expected by the classifier
  • predict one label per region

OpenCV handles the image work, and a small classifier can handle recognition.

Train a Lightweight Baseline Classifier

The built-in digits dataset from scikit-learn is convenient for a runnable baseline. It uses 8x8 grayscale images labeled 0 through 9.

python
1import numpy as np
2from sklearn.datasets import load_digits
3from sklearn.model_selection import train_test_split
4from sklearn.neighbors import KNeighborsClassifier
5from sklearn.metrics import accuracy_score
6
7X, y = load_digits(return_X_y=True)
8X = X.astype(np.float32)
9
10X_train, X_test, y_train, y_test = train_test_split(
11    X,
12    y,
13    test_size=0.2,
14    random_state=42,
15    stratify=y,
16)
17
18model = KNeighborsClassifier(n_neighbors=3)
19model.fit(X_train, y_train)
20
21pred = model.predict(X_test)
22print("validation accuracy:", accuracy_score(y_test, pred))

This is not a production OCR engine, but it is fast to train and good enough to demonstrate the rest of the pipeline.

Extract Digit Regions With OpenCV

The next step is segmenting the digits from an input image. The example below assumes dark digits on a light background and returns one feature vector per detected region.

python
1import cv2
2import numpy as np
3
4
5def extract_digit_vectors(image_path: str):
6    image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
7    if image is None:
8        raise FileNotFoundError(image_path)
9
10    blurred = cv2.GaussianBlur(image, (5, 5), 0)
11    _, thresh = cv2.threshold(
12        blurred,
13        0,
14        255,
15        cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU,
16    )
17
18    contours, _ = cv2.findContours(
19        thresh,
20        cv2.RETR_EXTERNAL,
21        cv2.CHAIN_APPROX_SIMPLE,
22    )
23
24    vectors = []
25    for contour in contours:
26        x, y, w, h = cv2.boundingRect(contour)
27        if w * h < 80:
28            continue
29
30        roi = thresh[y:y + h, x:x + w]
31        resized = cv2.resize(roi, (8, 8), interpolation=cv2.INTER_AREA)
32        scaled = (resized / 255.0) * 16.0
33        vectors.append((x, scaled.flatten().astype(np.float32)))
34
35    vectors.sort(key=lambda item: item[0])
36    return [vec for _, vec in vectors]

The sort step is important because contour discovery order is not guaranteed to match left-to-right reading order.

Run Recognition on an Image

Once the model is trained and the regions are extracted, prediction is straightforward.

python
1features = extract_digit_vectors("digits_line.png")
2
3if features:
4    labels = model.predict(np.vstack(features))
5    print("recognized digits:", "".join(map(str, labels)))
6else:
7    print("no digits found")

This works best when the image quality is controlled and the digits resemble the data used during training.

Match Preprocessing Between Training and Inference

One subtle but important rule is that the classifier only understands the representation it was trained on. If training data is normalized to a certain range or image size, inference must match that format.

In the example above, the scikit-learn digits dataset uses 8x8 inputs with values scaled roughly into a 0 to 16 range. That is why the extracted regions are resized to 8x8 and rescaled before prediction.

If you skip that alignment step, accuracy drops quickly even if the classifier itself is fine.

When This Baseline Stops Being Enough

A classical OCR pipeline is good for:

  • clean printed digits
  • screenshots or instrument panels
  • small automation tasks
  • proof-of-concept prototypes

It becomes weak when the images contain heavy noise, skewed handwriting, shadows, touching digits, or unusual fonts. At that point, you usually need stronger segmentation, more realistic training data, or a convolutional neural network.

Common Pitfalls

A common mistake is hard-coding a fixed threshold and expecting it to work under every lighting condition. Otsu or adaptive thresholding is safer.

Another mistake is forgetting to sort bounding boxes, which makes predictions appear in random order. Small contour noise can also create false digits, so area filtering matters.

Finally, if the model is trained on clean digits and inference uses noisy camera photos, the dataset mismatch will dominate the result.

Summary

  • A simple digit OCR system can be built with OpenCV preprocessing and a lightweight classifier.
  • The main steps are thresholding, contour extraction, resizing, and prediction.
  • Preprocessing at inference time must match the representation used during training.
  • Sorting detected regions is essential for correct reading order.
  • This baseline is useful for clean inputs, but harder cases usually need stronger models and better data.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

All Rights Reserved.