Resizing, Cropping & Rotating
By the end of this lesson you will be able to crop, flip, resize and rotate images, both by writing the maths yourself in NumPy and with OpenCV's one-line functions. You will also learn which resizing method to choose so your images stay sharp.
Cropping and flipping
Cropping keeps only part of an image. In NumPy, a crop is just a slice. Flipping mirrors the image. A left-right flip is what your phone's selfie camera does.
To see what is happening, we need an image whose direction is obvious. An "L" shape works well, because a flipped L looks like a J or upside down.
Step 1: draw an L. Step 2: crop the top-left area. Step 3: flip it three ways.
import numpy as np
import matplotlib.pyplot as plt
# Step 1: a white L on black, 40x30
img = np.zeros((40, 30), dtype=np.uint8)
img[5:35, 5:11] = 255 # the tall stroke
img[29:35, 5:25] = 255 # the foot
# Step 2: crop rows 0-19 and columns 0-14
crop = img[0:20, 0:15]
print("original:", img.shape, " crop:", crop.shape)
# Step 3: flips
views = {"original": img, "crop": crop,
"left-right": img[:, ::-1], "up-down": img[::-1, :],
"rotate 90": np.rot90(img)}
fig, axes = plt.subplots(1, 5, figsize=(12, 3))
for ax, (name, v) in zip(axes, views.items()):
ax.imshow(v, cmap="gray", vmin=0, vmax=255)
ax.set_title(name)
ax.axis("off")
plt.show()
print("rot90 shape:", np.rot90(img).shape)original: (40, 30) crop: (20, 15) rot90 shape: (30, 40)
img[:, ::-1] reads the columns backwards, which is a left-right flip. np.rot90 turns the image 90 degrees anticlockwise, which swaps height and width: a 40 by 30 image becomes 30 by 40. These are all exact operations: no pixel value changes, pixels only move.
Resizing: where do new pixels come from?
Resizing is harder. If you make an image twice as big, you need four times as many pixels. Where do their values come from? This filling-in is called interpolation.
Imagine a class photo of 40 students that must be printed on a poster with 160 slots. The lazy way is to repeat each face four times. That is nearest-neighbour interpolation: every new pixel copies the closest old pixel. It is fast and keeps hard edges, but looks blocky.
Here is nearest-neighbour written by hand. For each output row and column we work out which input row and column it came from.
import numpy as np
def resize_nearest(img, new_h, new_w):
h, w = img.shape
rows = (np.arange(new_h) * h / new_h).astype(int) # which old row each new row copies
cols = (np.arange(new_w) * w / new_w).astype(int)
return img[rows[:, None], cols[None, :]]
img = np.array([[ 10, 20],
[200, 250]], dtype=np.uint8)
print(resize_nearest(img, 4, 4))
print(resize_nearest(img, 3, 5))[[ 10 10 20 20] [ 10 10 20 20] [200 200 250 250] [200 200 250 250]] [[ 10 10 10 20 20] [ 10 10 10 20 20] [200 200 200 250 250]]
rows[:, None] and cols[None, :] use broadcasting to build a full grid of (row, column) pairs in one step, with no Python loops. That is why NumPy code is fast.
Bilinear interpolation: smoother results
Bilinear interpolation looks at the four nearest old pixels and blends them, giving more weight to the closer ones. It is like estimating the temperature at Rohtak from the readings at four nearby weather stations: a closer station counts more.
import numpy as np
import matplotlib.pyplot as plt
def resize_bilinear(img, new_h, new_w):
h, w = img.shape
img = img.astype(np.float64)
# position of each new pixel centre in old-pixel coordinates
y = (np.arange(new_h) + 0.5) * h / new_h - 0.5
x = (np.arange(new_w) + 0.5) * w / new_w - 0.5
y = np.clip(y, 0, h - 1)
x = np.clip(x, 0, w - 1)
y0 = np.floor(y).astype(int); y1 = np.minimum(y0 + 1, h - 1)
x0 = np.floor(x).astype(int); x1 = np.minimum(x0 + 1, w - 1)
wy = (y - y0)[:, None]; wx = (x - x0)[None, :]
top = img[y0][:, x0] * (1 - wx) + img[y0][:, x1] * wx
bottom = img[y1][:, x0] * (1 - wx) + img[y1][:, x1] * wx
return np.round(top * (1 - wy) + bottom * wy).astype(np.uint8)
small = np.array([[ 0, 0, 255],
[ 0, 255, 0],
[255, 0, 0]], dtype=np.uint8)
big = resize_bilinear(small, 6, 6)
print(big)
near = small.repeat(2, axis=0).repeat(2, axis=1) # nearest neighbour, 2x
fig, axes = plt.subplots(1, 2, figsize=(7, 3.5))
for ax, im, t in zip(axes, [near, big], ["nearest", "bilinear"]):
ax.imshow(im, cmap="gray", vmin=0, vmax=255)
ax.set_title(t)
plt.show()[[ 0 0 0 64 191 255] [ 0 16 48 96 159 191] [ 0 48 143 159 96 64] [ 64 96 159 143 48 0] [191 159 96 48 16 0] [255 191 64 0 0 0]]
The bilinear result has in-between greys where the nearest result jumps straight from 0 to 255. For photos that looks natural. For a mask or a label image, where every value means a class number, those in-between values are wrong, so masks must always be resized with nearest-neighbour.
Resizing and rotating with OpenCV
OpenCV does all of this in one call. Watch the order of the size argument: cv2.resize wants (width, height), while NumPy shapes are (height, width).
import cv2
import numpy as np
img = np.zeros((300, 400, 3), dtype=np.uint8) # 300 rows tall, 400 columns wide
small = cv2.resize(img, (200, 150), interpolation=cv2.INTER_AREA) # (width, height)!
big = cv2.resize(img, None, fx=2.5, fy=2.5, interpolation=cv2.INTER_CUBIC)
print("small:", small.shape, " big:", big.shape)
turned = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
mirror = cv2.flip(img, 1) # 1 = left-right, 0 = up-down, -1 = both
print("turned:", turned.shape, " mirror:", mirror.shape)
h, w = img.shape[:2]
M = cv2.getRotationMatrix2D((w / 2, h / 2), 30, 1.0) # centre, angle in degrees, scale
rotated = cv2.warpAffine(img, M, (w, h), flags=cv2.INTER_LINEAR, borderValue=(255, 255, 255))
print("matrix shape:", M.shape)
print(np.round(M, 3))
print("rotated:", rotated.shape)small: (150, 200, 3) big: (750, 1000, 3) turned: (400, 300, 3) mirror: (300, 400, 3) matrix shape: (2, 3) [[ 0.866 0.5 -48.205] [ -0.5 0.866 120.096]] rotated: (300, 400, 3)
Which interpolation should you choose?
| Job | Flag |
|---|---|
| Shrinking a photo | cv2.INTER_AREA (averages pixels, avoids jagged patterns) |
| Enlarging a photo | cv2.INTER_LINEAR (fast) or cv2.INTER_CUBIC (smoother) |
| Resizing a mask or label image | cv2.INTER_NEAREST (keeps class numbers exact) |
For any angle other than 90, OpenCV builds a 2 by 3 rotation matrix and warpAffine applies it. In OpenCV a positive angle turns the image anticlockwise. Corners that fall outside get cut off, and empty areas are filled with borderValue. You will learn how these matrices work in Affine & Perspective Transforms.
Advanced: rotating by any angle in NumPy
Rotation by hand uses a clever trick called inverse mapping. Instead of pushing each input pixel to the output (which leaves holes), we go through each output pixel and ask: "which input pixel lands here?" Then we copy it.
import numpy as np
import matplotlib.pyplot as plt
def rotate(img, degrees, fill=0):
h, w = img.shape
t = np.deg2rad(degrees)
cy, cx = (h - 1) / 2, (w - 1) / 2
rr, cc = np.mgrid[0:h, 0:w] # every output pixel
# rotate output coordinates backwards to find the source pixel
x = (cc - cx) * np.cos(t) - (rr - cy) * np.sin(t) + cx
y = (cc - cx) * np.sin(t) + (rr - cy) * np.cos(t) + cy
xs, ys = np.round(x).astype(int), np.round(y).astype(int)
inside = (xs >= 0) & (xs < w) & (ys >= 0) & (ys < h)
out = np.full_like(img, fill)
out[inside] = img[ys[inside], xs[inside]]
return out
img = np.zeros((60, 60), dtype=np.uint8)
img[25:35, 10:50] = 255 # a horizontal bar
out = rotate(img, 30)
print("white pixels before:", (img == 255).sum(), " after:", (out == 255).sum())
plt.imshow(np.hstack([img, out]), cmap="gray")
plt.title("Before and after a 30 degree turn")
plt.show()white pixels before: 400 after: 400
The bar keeps its 400 white pixels, but look closely at its edges: they are jagged "staircases", because every source position is rounded to the nearest whole pixel. That roughness is why professional code uses bilinear interpolation for rotation too.
Common mistakes
- Passing (height, width) to
cv2.resize. It wants (width, height). - Stretching the image. Keep the aspect ratio: compute the new height as
new_w * h / w. - Resizing masks with bilinear. Use nearest-neighbour so labels stay exact.
- Shrinking with
INTER_NEARESTorINTER_LINEAR. UseINTER_AREAfor downscaling photos.
Key points
- Cropping is slicing; flips are reversed slices;
np.rot90turns by 90 degrees. - Resizing needs interpolation: nearest copies, bilinear blends the four closest pixels.
cv2.resize(img, (width, height)): note the order.- Shrink with
INTER_AREA, enlarge withINTER_LINEARorINTER_CUBIC, masks withINTER_NEAREST. - Any-angle rotation uses a matrix and inverse mapping (
cv2.getRotationMatrix2Dwithcv2.warpAffine).
Exercise
Write a function fit_width(img, new_w) that resizes a grey image to a new width while keeping the aspect ratio, using resize_nearest from this lesson. Test it on a 30 by 90 image and a target width of 45, and print the result's shape.
Show solution
import numpy as np
def resize_nearest(img, new_h, new_w):
h, w = img.shape
rows = (np.arange(new_h) * h / new_h).astype(int)
cols = (np.arange(new_w) * w / new_w).astype(int)
return img[rows[:, None], cols[None, :]]
def fit_width(img, new_w):
h, w = img.shape
new_h = round(new_w * h / w)
return resize_nearest(img, new_h, new_w)
print(fit_width(np.zeros((30, 90), dtype=np.uint8), 45).shape) # (15, 45)The height is scaled by the same factor as the width (45 / 90 = 0.5), so 30 becomes 15 and the picture is not stretched.
Quick Quiz
1. What does cv2.resize(img, (640, 480)) produce?
OpenCV's dsize is (width, height), so the NumPy shape becomes (480, 640, ...).
2. Which interpolation keeps class numbers in a segmentation mask exact?
Nearest-neighbour only copies existing values, so it never creates in-between labels.
3. What is the best OpenCV flag for shrinking a photo?
INTER_AREA averages the pixels that fall into each output pixel, which avoids jagged patterns.
4. Why does rotation code loop over output pixels (inverse mapping)?
Every output pixel gets exactly one value when you look back to find its source.
Learn Computer Vision with a mentor
Free lessons get you started. Our Data Science & Machine Learning training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
