AI & Computer Vision, lesson 3 of 52

Python & NumPy Crash Course: An Image Is an Array

beginner18 min read

Introduction

In lesson 2 you printed frame.shape and got (480, 640, 3). That frame is a NumPy array: a grid of numbers Python can change all at once. Today you learn to handle it. You'll crop a photo, flip it, pull out one colour, and brighten it, using nothing but array code.

By the end of this lesson you'll be able to:

  • Use lists, loops and functions to process a small set of values
  • Create NumPy arrays and read their shape, dtype and size
  • Explain why a greyscale photo is a 2-D array and a colour photo is a 3-D array
  • Crop and flip an image using slicing
  • Explain what goes wrong when a uint8 value goes past 255, and brighten an image safely
  • Explain why array code is much faster than a Python loop over pixels

Working principle

Python refresher: lists, loops, functions

You may know this already. If so, skim it.

scores = [12, 7, 15, 9]          # a list
 
total = 0
for s in scores:                 # a loop
    total = total + s
 
def average(values):             # a function
    return sum(values) / len(values)
 
print(total / len(scores), average(scores))

NumPy arrays

A NumPy array holds numbers of one type in a fixed-size grid. It does maths on the whole grid at once.

IdeaExampleMeaning
Createnp.array([12, 7, 15, 9])A 1-D array
shape(4,) or (480, 640, 3)Size in each direction
dtypeuint8The type of each number. uint8 means whole numbers 0 to 255.
size921600How many numbers in total
Whole-array mathsa * 2Every number is doubled, with no loop

An image is an array

  • A greyscale image has shape (height, width). One number per pixel.
  • A colour image has shape (height, width, 3). Three numbers per pixel, in the order B, G, R in OpenCV.
  • Indexing is img[row, column], which means img[y, x]. Row first. This is the number one mistake.

Slicing

GoalCodeNotes
Cropimg[y1:y2, x1:x2]Rows first, then columns
Flip left to rightimg[:, ::-1]::-1 means "backwards"
Flip upside downimg[::-1, :]
One colour channelimg[:, :, 0]0 is blue, 1 is green, 2 is red
Paint a regionimg[50:100, 50:100] = (0, 0, 255)Writes red into a block

Slices are views, not copies. crop = img[0:100, 0:100] shares memory with img. If you change crop, the original changes too. Use .copy() when you want an independent picture.

The uint8 trap

uint8 can only hold 0 to 255. Go past it and the number wraps round silently: 250 plus 10 is 4, not 255. A bright patch turns dark. The fix is to do the maths in a bigger type, clip, and convert back.

250 + 10 = 260, but uint8 only goes to 255, so it rolls round like a car's odometer to 4. A bright pixel turns dark! Clipping stops at 255.

What you'll need

  • Your cv_course virtual environment from lesson 2, with Jupyter installed
  • A football photo called football_pitch.jpg (any wide photo works). No photo? The notebook makes a pretend pitch for you.

Build it

Each cell is a step. Run the cells in order and answer the question under each one. No football photo? Cell 3 makes a pretend pitch.

Cell 1: Python warm-up

scores = [12, 7, 15, 9]
 
total = 0
for s in scores:
    total = total + s
print("Mean by loop:", total / len(scores))
 
 
def average(values):
    return sum(values) / len(values)
 
 
print("Mean by function:", average(scores))

Question: how would you change average so it ignores zeros?

Cell 2: NumPy basics

import numpy as np
 
a = np.array([12, 7, 15, 9])
print(a.shape, a.dtype, a.size)
print(a * 2)
print(a.mean())
 
grid = np.array([[0, 128, 255],
                 [128, 255, 128],
                 [255, 128, 0]], dtype=np.uint8)
print(grid.shape)       # (3, 3)
print(grid[1, 2])       # row 1, column 2
print(grid[0, :])       # the whole top row
print(grid[:, 0])       # the whole left column

Question: what does grid[1, 2] print, and which row and column is that?

Cell 3: load an image (or make one)

import cv2
import matplotlib.pyplot as plt
 
img = cv2.imread("football_pitch.jpg")
 
if img is None:                      # no photo yet? make a fake pitch
    img = np.zeros((300, 400, 3), dtype=np.uint8)
    img[:] = (40, 140, 40)           # green grass (B, G, R)
    img[140:160, :] = (255, 255, 255)   # white line
    img[100:200, 180:220] = (0, 0, 220)  # a red "player"
 
print("shape:", img.shape)
print("dtype:", img.dtype)
print("size :", img.size)
 
 
def show(image, title=""):
    if image.ndim == 3:
        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
        plt.imshow(image)
    else:
        plt.imshow(image, cmap="gray")
    plt.title(title)
    plt.axis("off")
    plt.show()
 
 
show(img, "Original")

Question: what are the three numbers in img.shape? Which is the height?

Cell 4: crop, flip, channel

h, w = img.shape[:2]
 
crop = img[h // 4: 3 * h // 4, w // 4: 3 * w // 4].copy()
flipped = img[:, ::-1]
upside_down = img[::-1, :]
blue_only = img[:, :, 0]
 
print(crop.shape, flipped.shape, blue_only.shape)
 
show(crop, "Crop (middle half)")
show(flipped, "Flipped left to right")
show(upside_down, "Upside down")
show(blue_only, "Blue channel only")

Question: why is blue_only a 2-D array? Why does the green grass look dark in it?

Cell 5: brighten, the wrong way and the right way

a = np.array([250, 100], dtype=np.uint8)
print("wrong:", a + 10)          # 250 + 10 wraps round to 4
 
safe = np.clip(a.astype(np.int16) + 10, 0, 255).astype(np.uint8)
print("right:", safe)
 
bright = np.clip(img.astype(np.int16) + 60, 0, 255).astype(np.uint8)
dark = np.clip(img.astype(np.int16) - 60, 0, 255).astype(np.uint8)
 
show(bright, "Brighter (+60)")
show(dark, "Darker (-60)")

Question: predict what a + 10 prints before you run it. Why does the white line stay white in bright?

Cell 6: the speed test

import time
 
big = np.random.randint(0, 256, (480, 640), dtype=np.uint8)
 
start = time.time()
total = 0
for row in big:
    for pixel in row:
        total += int(pixel)
loop_time = time.time() - start
 
start = time.time()
total2 = int(big.sum())
numpy_time = time.time() - start
 
print(f"Loop : {loop_time:.4f} s")
print(f"NumPy: {numpy_time:.6f} s")
print("Same answer:", total == total2)
print(f"NumPy is about {loop_time / max(numpy_time, 1e-9):.0f} times faster")

Question: at 30 frames per second, how long can one frame take? Could the loop version keep up?

Cell 7: combine and save

result = np.hstack([img, bright])       # two images side by side
cv2.imwrite("s3_result.jpg", result)
print("Saved", result.shape)

If it goes wrong

ErrorCauseFix
IndexError: index 400 is out of boundsx and y swappedRows (y) first, columns (x) second: img[y, x]
img is None and img.shape failsThe path is wrong, so imread found nothingCheck the filename and folder. Use the fake pitch if needed.
Colours look wrong in MatplotlibOpenCV is BGR, Matplotlib expects RGBConvert with cv2.cvtColor(img, cv2.COLOR_BGR2RGB) before plt.imshow
Changing the crop changed the originalA slice is a viewUse .copy()
Bright image has black patchesuint8 wrapped past 255Convert to int16, add, np.clip, convert back
ValueError: operands could not be broadcastShapes don't matchPrint both shapes and check which is which
np.hstack failsThe two images have different heightsResize one first, or only combine same-size images

Try it

  • Mirror only the right half of the image, so the left half stays as it is
  • Make a "photo negative" with 255 - img and explain why it never overflows
  • Draw a 10-pixel black border by slicing the four edges

Check what you learnt

  1. A colour photo 640 pixels wide and 480 tall. What is its shape?

  2. You want rows 100 to 200 and columns 50 to 150. Which slice?

  3. What does img[:, ::-1] do?

  4. In uint8, what is 250 + 10?

  5. You change crop[:] = 0 and the original photo goes black too. Why?

Conclusion

A photo is a NumPy array: (height, width, 3), rows first. Slicing crops and flips it, whole-array maths changes every pixel at once (hundreds of times faster than a loop), and uint8 wraps round past 255, so brighten with int16 and np.clip.

What next?

Next: Images, pixels & colour spaces. You'll find out what those three numbers per pixel really mean, and use colour to pick out the pitch.