Geometric transformations
Resize and interpolation, rotation without cropping, affine transforms, perspective warping for document scans, and border handling.
Resize, rotate and flip
import cv2
import numpy as np
image = cv2.imread("page.jpg")
h, w = image.shape[:2]
# shrinking: INTER_AREA is the correct choice, it averages pixels
small = cv2.resize(image, (w // 2, h // 2), interpolation=cv2.INTER_AREA)
# enlarging: INTER_CUBIC or INTER_LINEAR; INTER_AREA is wrong here
large = cv2.resize(image, (w * 2, h * 2), interpolation=cv2.INTER_CUBIC)
# aspect-ratio-preserving resize to a target height
target_h = 480
scale = target_h / h
resized = cv2.resize(image, (int(w * scale), target_h), interpolation=cv2.INTER_AREA)
# rotation about the centre without cropping the corners
angle = 30
centre = (w / 2, h / 2)
matrix = cv2.getRotationMatrix2D(centre, angle, 1.0)
cos, sin = abs(matrix[0, 0]), abs(matrix[0, 1])
new_w = int(h * sin + w * cos)
new_h = int(h * cos + w * sin)
matrix[0, 2] += new_w / 2 - centre[0]
matrix[1, 2] += new_h / 2 - centre[1]
rotated = cv2.warpAffine(image, matrix, (new_w, new_h))
print(image.shape, rotated.shape)
flipped = cv2.flip(image, 1) # 1 = horizontal, 0 = vertical, -1 = both| Interpolation | Best for | Cost |
|---|---|---|
INTER_NEAREST | Masks and label images | Cheapest, blocky |
INTER_LINEAR | Default, and general use | Fast, slightly soft |
INTER_CUBIC | Enlarging photographs | Slower, sharper, can ring |
INTER_AREA | Shrinking | Best quality for downscaling |
INTER_LANCZOS4 | High-quality enlargement | Slowest |
- Use
INTER_AREAwhen shrinking and anything else when enlarging. Getting this backwards produces either aliasing or an unnecessarily blurry result. cv2.resizetakes(width, height)while shape reports(height, width). Swapping them is the most common bug in this lesson.- Never resize a mask or a label image with a smooth interpolator: it creates intermediate values that are not valid class ids. Use
INTER_NEAREST. - Rotation about the image centre crops the corners. Compute the bounding size of the rotated rectangle first, then translate the centre.
Affine transforms from three points
# affine: three source points map to three destination points
src = np.float32([[50, 50], [200, 50], [50, 200]])
dst = np.float32([[10, 100], [200, 50], [100, 250]])
matrix = cv2.getAffineTransform(src, dst)
warped = cv2.warpAffine(image, matrix, (w, h), flags=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT, borderValue=(255, 255, 255))
print(matrix)
# the general case: build the matrix yourself
# [ a b tx ]
# [ c d ty ]
shear = np.float32([[1, 0.3, 0],
[0, 1.0, 0]])
sheared = cv2.warpAffine(image, shear, (int(w * 1.3), h))
# scaling about a point other than the origin requires the translation terms
scale = 1.5
about = (w / 2, h / 2)
s = np.float32([[scale, 0, about[0] * (1 - scale)],
[0, scale, about[1] * (1 - scale)]])
scaled = cv2.warpAffine(image, s, (int(w * scale), int(h * scale)))getAffineTransformneeds exactly three point pairs; it cannot solve a general homography.- Affine preserves parallel lines but not angles. For a document photographed at an angle you need the perspective transform instead.
borderModedecides what fills the new area: constant (a chosen colour), replicate (edge pixels), reflect, or wrap. Constant white is usually right for document work.- Do not chain resize and rotate operations when one affine transform can do both. Each resampling step loses a little sharpness.
Perspective warping
def order_corners(points):
"""Order four points as top-left, top-right, bottom-right, bottom-left."""
points = np.array(points, dtype="float32").reshape(4, 2)
ordered = np.zeros((4, 2), dtype="float32")
total = points.sum(axis=1)
ordered[0] = points[np.argmin(total)] # top-left has the smallest sum
ordered[2] = points[np.argmax(total)] # bottom-right has the largest
diff = np.diff(points, axis=1).ravel()
ordered[1] = points[np.argmin(diff)] # top-right has the smallest difference
ordered[3] = points[np.argmax(diff)]
return ordered
# find the document in the frame, then warp it flat
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
edges = cv2.Canny(blurred, 75, 200)
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
largest = max(contours, key=cv2.contourArea)
perimeter = cv2.arcLength(largest, True)
approx = cv2.approxPolyDP(largest, 0.02 * perimeter, True)
print("corners found:", len(approx))
if len(approx) == 4:
src = order_corners(approx)
top_left, top_right, bottom_right, bottom_left = src
width = int(max(np.linalg.norm(bottom_right - bottom_left),
np.linalg.norm(top_right - top_left)))
height = int(max(np.linalg.norm(top_right - bottom_right),
np.linalg.norm(top_left - bottom_left)))
dst = np.float32([[0, 0], [width - 1, 0], [width - 1, height - 1], [0, height - 1]])
matrix = cv2.getPerspectiveTransform(src, dst)
flat = cv2.warpPerspective(image, matrix, (width, height))
cv2.imwrite("flat.png", flat)⚠️
A correct corner order is the whole ball game. Feed
getPerspectiveTransform a rotated corner list and you get a mirrored or twisted output that still looks plausible at a glance. Draw the ordered points on the source image and check them visually before trusting the warp.FAQ
Why is my resized image blurry?
You are probably enlarging with
INTER_AREA, which is designed for downscaling, or shrinking with INTER_NEAREST, which drops pixels. Match the interpolator to the direction.How do I warp a document when contour detection fails?
Provide the four corners manually from a click tool, or use a detector trained for document boundaries. A manual corner step for rare failures is cheaper than tuning an edge pipeline that must work on every input.
Related
Reading, writing and inspecting images Contours and object detection basics
Last refreshed 2026-09-18.