Building a Document Scanner with OpenCV
Have you ever needed to quickly digitize a paper document, but didn‘t have a scanner handy? With a little bit of computer vision and image processing, it‘s actually quite easy to build your own document scanner using Python and OpenCV. In this article, we‘ll walk through the steps of detecting a document in an image and extracting it into a clean, scanned version suitable for saving or further processing.
The core idea is to use edge detection and contour extraction to identify the document region within the larger image. Then we can isolate the document, correct for any perspective distortion, and apply filters to clean up the scanned image. While there are certainly more advanced techniques for document scanning, this simple approach is a great way to get started with practical applications of computer vision.
Setting Up the Environment
To get started, you‘ll need a Python development environment with a few key libraries installed:
- Python 3.x
- OpenCV (cv2)
- NumPy
The easiest way to install OpenCV is using pip:
pip install opencv-python
NumPy can be installed the same way, if you don‘t already have it:
pip install numpy
OpenCV and NumPy are the only third-party dependencies we need for this project. OpenCV provides the core computer vision and image processing functions, while NumPy allows us to work efficiently with image data stored as arrays.
With the environment set up, let‘s dive into the code! Create a new Python file, e.g. doc_scanner.py, and we‘ll implement the document scanner step-by-step.
Loading and Preprocessing the Image
The first step is to load the input image containing the document we want to scan. OpenCV makes this easy with the cv2.imread() function:
import cv2image = cv2.imread(‘input.jpg‘)
This loads the image from the file input.jpg in the project directory. You can replace this with the path to your own input image.
Next, we‘ll do some preprocessing on the image to make it easier to detect the document edges. First, convert the image to grayscale using cv2.cvtColor():
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
This reduces the image to a single color channel, which is all we need for edge detection.
To further improve the results, we can apply blurring to reduce high frequency noise in the image. Gaussian blur is commonly used for this:
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
This applies a 5×5 Gaussian kernel to smooth out the image. The third argument, 0, is the standard deviation in the X and Y directions (we let them be computed from the kernel size).
Finally, we‘ll apply edge detection to identify the "discontinuities" in the image brightness that usually correspond with object boundaries. The Canny edge detection algorithm is a good choice:
edges = cv2.Canny(blurred, 50, 200)
The second and third arguments are the low and high thresholds used by the Canny algorithm. You can think of these as "how much" edge to include. A lower threshold will include fainter edges, while a higher value will only keep the strongest ones.
Finding the Document Contour
With the edges identified, we can now try to find the contour (outline) of the document within the image. OpenCV provides convenient functions for contour detection and manipulation.
First, find all the contours in the edge map:
contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
The cv2.RETR_LIST flag returns all contours without establishing any hierarchical relationships. cv2.CHAIN_APPROX_SIMPLE compresses horizontal, vertical, and diagonal segments into their end points only, saving memory.
Next, we‘ll assume that the document corresponds to the largest rectangular contour in the image. This is a reasonable assumption in most cases, but keep in mind it may fail if there are other large rectangular objects in the image background.
To find the largest rectangular contour, we can loop through all the detected contours, approximate each one with a polygon, and keep track of the largest contour with four vertices:
max_area = 0 doc_contour = Nonefor contour in contours: perimeter = cv2.arcLength(contour, True) approx = cv2.approxPolyDP(contour, 0.02 * perimeter, True)
if len(approx) == 4 and cv2.contourArea(approx) > max_area: max_area = cv2.contourArea(approx) doc_contour = approx</pre>The
cv2.arcLength()function computes the contour perimeter. We then use that withcv2.approxPolyDP()to get a polygonal approximation of the contour. The second argument specifies the approximation accuracy as a fraction of the perimeter - a higher value means a less accurate (but simpler) contour.We check if the approximated polygon has four vertices (like a rectangle) and if its area is the largest seen so far, updating
max_areaanddoc_contourif so.After the loop,
doc_contourwill hold the four vertices of the document region, in the order: top-left, top-right, bottom-right, bottom-left. If no rectangular contour was found, it will beNone.Extracting the Document
With the document contour identified, we can now isolate and "scan" the document by performing a perspective transform. This will give us a top-down view of the document, correcting for any skew or rotation in the original image.
First, let‘s order the contour vertices consistently and get the dimensions of the output image:
if doc_contour is None: print("Could not find document in image!") exit(0)pts1 = np.float32(doc_contour) pts2 = np.float32([[0, 0], [width, 0], [width, height], [0, height]])
matrix = cv2.getPerspectiveTransform(pts1, pts2) result = cv2.warpPerspective(image, matrix, (width, height))
Here we define the output image rectangle as
pts2, with dimensionswidthandheight(which are just the maximum dimensions of the document contour).The key step is using
cv2.getPerspectiveTransform()to compute the transformation matrix that maps the contour verticespts1to the output rectangle verticespts2. Thencv2.warpPerspective()performs the actual transformation, giving us the final "scanned" document imageresult!Keep in mind this assumes the original document is rectangular and viewed from a reasonable angle. If the document is extremely skewed or partially outside the image frame, the results may be distorted or incomplete. More advanced techniques like detecting the actual document edges or content area could help in those cases.
Postprocessing the Scanned Document
At this point we have a pretty good digital version of the original document, but the image might still be a bit noisy or unclear, especially if the original was wrinkled, dirty or had a complex background. We can apply a few final filtering steps to further clean up the scanned document image.
First, let‘s convert the image to pure black-and-white:
grayscale = cv2.cvtColor(result, cv2.COLOR_BGR2GRAY) thresh = cv2.threshold(grayscale, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]After converting to grayscale, we apply binary thresholding using Otsu‘s method, which automatically chooses an appropriate threshold value. This will help further isolate the actual document content (text and lines) from the background.
We can also apply morphological operations to further clean up small noise regions and fill in gaps in the thresholded image:
kernel = np.ones((5,5), np.uint8) opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=1)result = cv2.GaussianBlur(opening, (5,5), 0) result = cv2.addWeighted(result, 1.5, result, -0.5, 0)
Here we first perform an "opening" operation (erosion followed by dilation) to remove small white noise regions. Then, we reapply Gaussian blur and use
cv2.addWeighted()to sharpen the image by subtracting a blurred version from itself. This enhances the text edges and makes the content more readable.Saving the Scanned Document
Finally, let‘s save the scanned document image to disk:
cv2.imwrite("scanned.jpg", result)This will write the image to a file called
scanned.jpgin the project directory. You can change the filename and extension as needed - common options are PNG for lossless quality, or JPG if file size is a concern.Higher resolution scans can be obtained by saving the image with larger dimensions:
scale_factor = 2 enlarged = cv2.resize(result, None, fx=scale_factor, fy=scale_factor, interpolation=cv2.INTER_CUBIC) cv2.imwrite("scanned_enlarged.jpg", enlarged)This uses bilinear interpolation to smoothly upscale the image resolution by a factor of 2 (or whatever factor you choose) before saving. Keep in mind this doesn‘t add any actual detail to the image, but can be helpful if you want to print the scanned document or need a higher resolution for downstream processing.
Future Improvements
We‘ve built a functional document scanner that can isolate, extract and clean up document images pretty well! However, there are definitely some limitations and areas for improvement:
- Detecting documents with nonrectangular or nonconvex shapes, like book pages or crumpled paper
- Handling low contrast or unevenly lit images by using adaptive thresholding or illumination correction
- Automatically correcting the scanned document orientation (portrait vs. landscape)
- Better filtering and noise removal, e.g. removing stamps, stickers, coffee stains, etc.
- Detecting and extracting information from the scanned document, like the title, date, author, etc.
- Using deep learning to handle more challenging cases, like multiple documents per image, very noisy backgrounds, etc.
There are some great resources available if you want to dig deeper into these improvements, or document analysis in general:
- Image processing with OpenCV: Edge detection and contour recognition
- Building a Kick-Ass Mobile Document Scanner
- Document Image Processing for Scanning and Printing
Conclusion
In this article, we‘ve seen how to build a simple but effective document scanner using Python and OpenCV. The key steps are:
- Detect edges in the input image
- Find the contours of the document
- Extract and warp the document contour to obtain a scanned version
- Apply filtering and thresholding to clean up the final scanned image
While there are certainly more sophisticated approaches, this is a great starting point that you can adapt and build on for your own projects. Whether you‘re scanning receipts, digitizing your book collection, or building a mobile scanning app, the basic principles outlined here should serve you well!
Thanks for reading, and happy scanning!