EthicsComputerCourses
Join training +91 90532 10052

Resizing, Cropping & Rotating

Computer Vision · Lesson 5 of 28 Intermediate 6 min read Updated

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.

PythonRun it yourself
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)
Output
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.

PythonRun it yourself
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))
Output
[[ 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.

PythonRun it yourself
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()
Output
[[  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).

Python
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)
Output
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?

JobFlag
Shrinking a photocv2.INTER_AREA (averages pixels, avoids jagged patterns)
Enlarging a photocv2.INTER_LINEAR (fast) or cv2.INTER_CUBIC (smoother)
Resizing a mask or label imagecv2.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.

PythonRun it yourself
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()
Output
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_NEAREST or INTER_LINEAR. Use INTER_AREA for downscaling photos.

Key points

  • Cropping is slicing; flips are reversed slices; np.rot90 turns 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 with INTER_LINEAR or INTER_CUBIC, masks with INTER_NEAREST.
  • Any-angle rotation uses a matrix and inverse mapping (cv2.getRotationMatrix2D with cv2.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
Python
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?

2. Which interpolation keeps class numbers in a segmentation mask exact?

3. What is the best OpenCV flag for shrinking a photo?

4. Why does rotation code loop over output pixels (inverse mapping)?

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