Four points in, a straight-on view out: scanning documents and looking at roads from above.

Problem: a photo of a document taken from an angle.
Solution: a perspective transform to a frontal view.
Result: a rectangular, readable document.
Find the document's 4 corners.
Define where they should go.
Compute the 3×3 matrix.
Resample to the frontal view.
import cv2
import numpy as np
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# threshold to binary
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
# find contours
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # ans:
# find largest contour
largest = max(contours, key=cv2.contourArea) # ans:
# approximate to polygon
epsilon = 0.02 * cv2.arcLength(largest, True)
approx = cv2.approxPolyDP(largest, epsilon, True) # ans:
# ans: if len(approx) == 4, it's a rectangle (the document)
# corners may be in any order: we need TL, TR, BR, BL
def order_points(pts):
rect = np.zeros((4, 2), dtype='float32')
# sum: TL has the smallest, BR the largest
s = pts.sum(axis=1)
rect[0] = pts[np.argmin(s)] # top-left
rect[2] = pts[np.argmax(s)] # bottom-right
# diff (y - x): TR has the smallest, BL the largest
diff = np.diff(pts, axis=1)
rect[1] = pts[np.argmin(diff)] # top-right
rect[3] = pts[np.argmax(diff)] # bottom-left
return rect
ordered_pts = order_points(approx.reshape(4, 2)) # ans:
# compute width and height of output
(tl, tr, br, bl) = ordered_pts # ans:
# width = max of top and bottom widths
widthA = np.sqrt((br[0] - bl[0])**2 + (br[1] - bl[1])**2) # ans:
widthB = np.sqrt((tr[0] - tl[0])**2 + (tr[1] - tl[1])**2) # ans:
maxWidth = int(max(widthA, widthB)) # ans:
# height = max of left and right heights
heightA = np.sqrt((tr[0] - br[0])**2 + (tr[1] - br[1])**2) # ans:
heightB = np.sqrt((tl[0] - bl[0])**2 + (tl[1] - bl[1])**2) # ans:
maxHeight = int(max(heightA, heightB)) # ans:
# source points (detected corners)
src = ordered_pts
# destination points (rectangle)
dst = np.array([[0, 0], # ans:
[maxWidth - 1, 0], # ans:
[maxWidth - 1, maxHeight - 1], # ans:
[0, maxHeight - 1]], dtype='float32') # ans:
# compute perspective transform
M = cv2.getPerspectiveTransform(src, dst) # ans:
# apply transform
warped = cv2.warpPerspective(img, M, (maxWidth, maxHeight)) # ans:
Drag the pink corners. Labels come from order_points, so the order you pick them in does not matter.
Purpose: a top-down view of a scene.
# road region (trapezoid)
h, w = img.shape[:2]
src = np.float32([[w*0.45, h*0.6], # ans:
[w*0.55, h*0.6], # ans:
[w*0.9, h], # ans:
[w*0.1, h]]) # ans:
# bird's eye view (rectangle)
dst = np.float32([[0, 0], # ans:
[w, 0], # ans:
[w, h], # ans:
[0, h]]) # ans:
# transform
M = cv2.getPerspectiveTransform(src, dst)
birds_eye = cv2.warpPerspective(img, M, (w, h)) # ans:
The points keep the code's order, src[0] to src[3]. The fitted trapezoid's sides point at the vanishing point, so the lane lines come out parallel; the code's own trapezoid sits almost on this photo's horizon, so it magnifies a tiny patch. Drag a corner to see what a bad trapezoid does. (order_points would mislabel this wide trapezoid: its bottom-left corner has the smallest x + y.)
Purpose: map the bird's eye view back to the original perspective, using the inverse of the homography.
# forward transform
M = cv2.getPerspectiveTransform(src, dst)
birds_eye = cv2.warpPerspective(img, M, (w, h))
# inverse transform
M_inv = cv2.getPerspectiveTransform(dst, src) # ans:
original = cv2.warpPerspective(birds_eye, M_inv, (w, h)) # ans:
# or use numpy inverse
M_inv = np.linalg.inv(M) # ans:
Anything the forward warp threw away (the sky, the verges) stays black: the inverse only undoes the geometry.
Answer first, then press → to check.
What is a perspective transform used for?
Document scanning, bird's eye views, rectification.
How many points are needed?
4 point pairs (source and destination).
What is a homography?
A 3×3 perspective transformation matrix.
Why order the corner points?
To ensure the correct TL, TR, BR, BL correspondence.
What is a bird's eye view?
A top-down perspective of the scene.
How do you compute the output rectangle size?
Measure the distances in the source and take the max width and height.
What is inverse perspective?
Transforming back from the warped view to the original.
How do you get the inverse homography?
cv2.getPerspectiveTransform(dst, src) or np.linalg.inv(M).
The document scan steps?
Detect corners, order the points, compute the homography, warp.
# perspective transform with 4 points
pts1 = np.float32([[56, 65], [368, 52], [28, 387], [389, 390]])
pts2 = np.float32([[0, 0], [300, 0], [0, 300], [300, 300]])
M = cv2.getPerspectiveTransform(pts1, pts2) # ans:
result = cv2.warpPerspective(img, M, (300, 300)) # ans:
Here the points are listed TL, TR, BL, BR: any order works as long as both lists use the same one.
# order points function
def order_points(pts):
rect = np.zeros((4, 2), dtype='float32')
s = pts.sum(axis=1)
rect[0] = pts[np.argmin(s)] # ans:
rect[2] = pts[np.argmax(s)] # ans:
diff = np.diff(pts, axis=1)
rect[1] = pts[np.argmin(diff)] # ans:
rect[3] = pts[np.argmax(diff)] # ans:
return rect
# bird's eye view transform
h, w = img.shape[:2]
src = np.float32([[w*0.45, h*0.6], [w*0.55, h*0.6], [w*0.9, h], [w*0.1, h]])
dst = np.float32([[0, 0], [w, 0], [w, h], [0, h]])
M = cv2.getPerspectiveTransform(src, dst) # ans:
birds_eye = cv2.warpPerspective(img, M, (w, h)) # ans:
# inverse transform
M_inv = cv2.getPerspectiveTransform(dst, src) # ans:
original = cv2.warpPerspective(birds_eye, M_inv, (w, h)) # ans: