AI Hints
Home›Computer Vision Tutorial›Vision Projects One
Computer Vision · Lesson 78 of 111

A Document Scanner

Build the classic phone app.

⏱ 2 min read▶ Video at 2:02:37🎓 Free
Watch this lesson in the Computer Vision courseWatch on YouTube ↗Full Computer Vision course

The farm's old paper delivery notes need to be digitised. Staff photograph them at odd angles, on a messy desk. Scanner apps straighten pages automatically, and you've already learned every step that you need. Let's put them together.

A Document Scanner
From the video: A Document Scanner
  • Old paper delivery notes
  • Photographed at odd angles
  • You know every step already

From photo to flat page

First, we simulate a photo of a page, at an angle. Then the real work begins. We blur it and threshold it, so that the white page stands out. We find the largest outline. Approx poly simplifies that outline into just a few corners, and we expect four. Then we have to sort those corners into a consistent order. That's a neat trick. Top left has the smallest x plus y, and bottom right has the largest. Finally, we warp the page flat. Run it. Four corners found, and the flattened page is very close to the original. Filters, thresholds, contours, and homography, all together. Each step is simple, and the combination feels like magic.

scanner.pypython
import cv2
import numpy as np

page = np.full((500, 360, 3), 245, np.uint8)
for y in range(50, 460, 40):
    cv2.line(page, (40, y), (320, y), (40, 40, 40), 4)
photo = np.full((600, 800, 3), 90, np.uint8)
corners = np.float32([[200, 60], [600, 110], [560, 560], [150, 500]])
M = cv2.getPerspectiveTransform(np.float32([[0, 0], [360, 0], [360, 500], [0, 500]]), corners)
photo = cv2.warpPerspective(page, M, (800, 600), dst=photo, borderMode=cv2.BORDER_TRANSPARENT)

gray = cv2.GaussianBlur(cv2.cvtColor(photo, cv2.COLOR_BGR2GRAY), (5, 5), 0)
_, mask = cv2.threshold(gray, 170, 255, cv2.THRESH_BINARY)
biggest = max(cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0], key=cv2.contourArea)
box = cv2.approxPolyDP(biggest, 0.02 * cv2.arcLength(biggest, True), True).reshape(-1, 2).astype(np.float32)
print("Corners found:", len(box))

s, d = box.sum(1), np.diff(box, axis=1).ravel()
ordered = np.float32([box[s.argmin()], box[d.argmin()], box[s.argmax()], box[d.argmax()]])
flat = cv2.warpPerspective(photo, cv2.getPerspectiveTransform(ordered, np.float32([[0, 0], [360, 0], [360, 500], [0, 500]])), (360, 500))
cv2.imwrite("scan.png", np.hstack([cv2.resize(photo, (667, 500)), flat]))
print("Difference from the original:", round(cv2.absdiff(flat, page).mean(), 1))
Output
Corners found: 4
Difference from the original: 4.8

Making it robust

A real scanner needs more. If it can't find four corners, it should ask the user to adjust them. Shadows need adaptive thresholding. And a desk that's the same colour as the paper will defeat the whole method, which is why modern apps also use small neural networks to find the page. Classic steps do the geometry, and a network handles the mess.

Fallbacklet users drag corners
Shadowsadaptive threshold
Busy desksa small network
Hybridclassic geometry, neural detection

Summary

Five classic steps make a useful product. Next, how to find copies of an image.

Key takeaways
  • Find the largest outline
  • Sort corners consistently
  • Warp it flat
Next up: Finding Duplicate Images — perceptual hashing