Python Document Scanner App Opencv

Written by

in

Scanning documents with a smartphone or webcam has become a daily routine for students, freelancers, and small businesses. While commercial apps offer polished interfaces, building your own Python document scanner app with OpenCV gives you full control over privacy, customization, and learning opportunities. In this guide we’ll walk through every step—from setting up the development environment to writing clean, production‑ready code—so you can create a fast, reliable scanner that automatically detects page edges, corrects perspective, and produces a crisp black‑and‑white PDF.

Why Choose OpenCV for a Document Scanner?

OpenCV (Open Source Computer Vision Library) is the de‑facto standard for image processing in Python. Its strengths for a document scanner app include:

  • Real‑time performance: Optimized C++ core with Python bindings makes processing frames from a webcam or mobile camera lightning fast.
  • Rich set of algorithms: Edge detection, contour finding, perspective transforms, and adaptive thresholding are all built‑in.
  • Cross‑platform support: Write once, run on Windows, macOS, Linux, or even Android via Kivy or PySide.
  • Active community: Thousands of tutorials, forums, and GitHub projects can help you troubleshoot quickly.

Prerequisites and Installation

Before diving into code, make sure you have the following installed on your development machine:

  1. Python 3.8+ (preferably from python.org)
  2. OpenCV‑Python package
  3. NumPy for array manipulation
  4. PyMuPDF or ReportLab for PDF generation (optional but recommended)

Run the commands below in your terminal to set up a virtual environment and install the required libraries:

python -m venv scanner-env
source scanner-env/bin/activate   # On Windows use `scanner-env\Scripts\activate`
pip install opencv-python numpy pymupdf

Understanding the Core Workflow

A robust document scanner follows a predictable pipeline. Breaking it down helps you debug each stage and swap out components later.

1. Capture the Image

Whether you read from a static file or a live camera feed, the first step is to obtain a high‑resolution color image. OpenCV’s cv2.VideoCapture class makes webcam integration trivial.

2. Detect the Document Contour

The goal is to locate the four corners of the page. The typical approach combines:

  • Grayscale conversion
  • Gaussian blur to reduce noise
  • Canny edge detection
  • Contour extraction and approximation

We then select the largest quadrilateral contour assuming it’s the document.

3. Apply Perspective Transform

Once we have the corner points, cv2.getPerspectiveTransform and cv2.warpPerspective map the skewed image to a flat, top‑down view. This step is crucial for readability.

4. Enhance Readability with Thresholding

Document images look best in black‑and‑white. Adaptive thresholding (or Otsu’s method) converts the warped color image into a clean binary image that mimics a scanned page.

5. Export to PDF (Optional)

After processing, you can save the result as a high‑resolution JPEG/PNG or embed it in a PDF using PyMuPDF. PDFs are ideal for sharing and archiving.

Full Code Walkthrough

Below is a complete, well‑commented Python script that implements the workflow described above. Feel free to copy, modify, and integrate it into a larger GUI or mobile app.

import cv2
import numpy as np
import fitz  # PyMuPDF for PDF creation

def order_points(pts):
    # Ensure points are in order: top‑left, top‑right, bottom‑right, bottom‑left
    rect = np.zeros((4, 2), dtype="float32")
    s = pts.sum(axis=1)
    rect[0] = pts[np.argmin(s)]      # top‑left
    rect[2] = pts[np.argmax(s)]      # bottom‑right
    diff = np.diff(pts, axis=1)
    rect[1] = pts[np.argmin(diff)]   # top‑right
    rect[3] = pts[np.argmax(diff)]   # bottom‑left
    return rect

def four_point_transform(image, pts):
    rect = order_points(pts)
    (tl, tr, br, bl) = rect

    # Compute width of new image
    widthA = np.linalg.norm(br - bl)
    widthB = np.linalg.norm(tr - tl)
    maxWidth = max(int(widthA), int(widthB))

    # Compute height of new image
    heightA = np.linalg.norm(tr - br)
    heightB = np.linalg.norm(tl - bl)
    maxHeight = max(int(heightA), int(heightB))

    # Destination points for "birds‑eye view"
    dst = np.array([
        [0, 0],
        [maxWidth - 1, 0],
        [maxWidth - 1, maxHeight - 1],
        [0, maxHeight - 1]], dtype="float32")

    # Perspective transform matrix and warped image
    M = cv2.getPerspectiveTransform(rect, dst)
    warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))
    return warped

def scan_document(image_path):
    # 1️⃣ Load image and resize for faster processing
    image = cv2.imread(image_path)
    ratio = image.shape[0] / 500.0
    orig = image.copy()
    image = cv2.resize(image, (int(image.shape[1] / ratio), 500))

    # 2️⃣ Convert to grayscale, blur, and detect edges
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    gray = cv2.GaussianBlur(gray, (5, 5), 0)
    edged = cv2.Canny(gray, 75, 200)

    # 3️⃣ Find contours and keep the largest quadrilateral
    cnts, _ = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
    cnts = sorted(cnts, key=cv2.contourArea, reverse=True)[:5]

    screenCnt = None
    for c in cnts:
        peri = cv2.arcLength(c, True)
        approx = cv2.approxPolyDP(c, 0.02 * peri, True)

        if len(approx) == 4:
            screenCnt = approx
            break

    if screenCnt is None:
        raise ValueError("Could not find document contour.")

    # 4️⃣ Apply perspective transform on the original high‑resolution image
    warped = four_point_transform(orig, screenCnt.reshape(4, 2) * ratio)

    # 5️⃣ Convert to grayscale and apply adaptive threshold
    warped_gray = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY)
    scanned = cv2.adaptiveThreshold(
        warped_gray, 255,
        cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
        cv2.THRESH_BINARY,
        11, 2)

    return scanned

def save_as_pdf(image, pdf_path):
    # Convert NumPy image to PDF page
    height, width = image.shape
    pdf_doc = fitz.open()
    pdf_page = pdf_doc.new_page(width=width, height=height)
    # PyMuPDF expects PNG bytes
    png_bytes = cv2.imencode('.png', image)[1].tobytes()
    pdf_page.insert_image(pdf_page.rect, stream=png_bytes)
    pdf_doc.save(pdf_path)
    pdf_doc.close()

if __name__ == "__main__":
    import argparse, os
    parser = argparse.ArgumentParser(description="Simple Python document scanner using OpenCV")
    parser.add_argument("input", help="Path to input image (photo of a document)")
    parser.add_argument("-o", "--output", default="scanned.pdf", help="Output PDF filename")
    args = parser.parse_args()

    scanned_img = scan_document(args.input)
    # Save intermediate JPEG for debugging
    cv2.imwrite("scanned.jpg", scanned_img)

    save_as_pdf(scanned_img, args.output)
    print(f"✅ Scanned PDF saved to {os.path.abspath(args.output)}")

Key takeaways from the script:

  • Resizing for speed: The image is downscaled to 500 px height for contour detection, then the original resolution is used for the final transform.
  • Robust corner ordering: The order_points helper guarantees a consistent point order, preventing upside‑down results.
  • Adaptive thresholding: Works well under varying lighting conditions, producing a clean black‑and‑white output.
  • PDF generation: Using PyMuPDF keeps the workflow pure Python without external binaries.

Enhancing the Scanner: Features You Can Add

Once the basic pipeline works, consider these extensions to turn a simple script

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *