Computer vision

Perspective Transform

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

Document scanning

A loan application form photographed at an angle

Problem: a photo of a document taken from an angle.

Solution: a perspective transform to a frontal view.

Result: a rectangular, readable document.

Detect corners

Find the document's 4 corners.

Output rectangle

Define where they should go.

Homography

Compute the 3×3 matrix.

Warp

Resample to the frontal view.

Finding document corners


        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)
      

Ordering corner points


          # 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:
        

Computing the output size


          # 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:
        

Complete document scan


        # 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:
      

Scan it, live

M =

Drag the pink corners. Labels come from order_points, so the order you pick them in does not matter.

Bird's eye view

Purpose: a top-down view of a scene.

Use cases

  • Lane detection
  • Parking assistance
  • Sports analysis

Method: the same as a document scan

  1. Define a region of interest: a trapezoid on the road
  2. Map it to a rectangle: the top-down view

Bird's eye view example


        # 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:
      

From the driver's seat to above

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

Inverse perspective

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.

Exercises

Answer first, then press → to check.

Exercises 1 · concepts

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.

Exercises 2 · concepts

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.

Exercises 3 · coding


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

Exercises 4 · coding


        # 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
      

Exercises 5 · mixed


        # 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: