A Document Scanner
Build the classic phone app.
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.

- 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.
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))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.
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