05 — NumPy for Computer Vision

Interactive NumPy refresher focused on arrays, images, vectorization, matrix operations, and data types.

NumPy for Computer Vision

NumPy is the core numerical array library used throughout Python computer vision. Images, coordinates, filters, features, and many geometric computations are naturally represented as NumPy arrays.

This notebook reviews the NumPy concepts that are especially useful in this course.

Before you begin

Open this notebook in VS Code and select the cv Conda environment as the notebook kernel.

The notebook is designed to be run from top to bottom. If variables become inconsistent because cells were executed out of order, restart the kernel and use Run All.

import numpy as np
import matplotlib.pyplot as plt

print("NumPy version:", np.__version__)

1. Arrays

A NumPy array stores values of a common data type in one or more dimensions.

a = np.array([1, 2, 3, 4, 5])
print(a)
print("shape:", a.shape)
print("dtype:", a.dtype)
print("ndim:", a.ndim)

Creating arrays

Some of the most common constructors are:

  • np.array(...)
  • np.zeros(...)
  • np.ones(...)
  • np.full(...)
  • np.arange(...)
  • np.linspace(...)
print(np.zeros((2, 3)))
print(np.ones((2, 3)))
print(np.full((2, 3), 7))
print(np.arange(0, 10, 2))
print(np.linspace(0, 1, 5))

2. Images are arrays

A grayscale image is commonly represented as:

height × width

A color image is commonly represented as:

height × width × channels

For an RGB image, the channel dimension usually has size 3.

image = np.zeros((300, 400, 3), dtype=np.float32)

# First 100 columns: half-strength red
image[:, 0:100, 0] = 0.5

# Next 100 columns: full red
image[:, 100:200, 0] = 1.0

# Add a blue region in the top half
image[0:150, :, 2] += 0.35

plt.figure(figsize=(8, 5))
plt.imshow(np.clip(image, 0, 1))
plt.axis("off")
plt.show()

print("shape:", image.shape)
print("dtype:", image.dtype)

3. Indexing and slicing

NumPy indexing is similar to Python list indexing, but it works across multiple dimensions.

For a color image:

image[row, column, channel]

Useful slicing examples:

image[:, :, 0]       # red channel
image[0:100, :, :]   # first 100 rows
image[:, 50:150, :]  # columns 50 through 149
image[::2, ::2, :]   # every second row and column
a = np.arange(1, 11)

print("array:", a)
print("first:", a[0])
print("last:", a[-1])
print("elements 2 through 5:", a[1:5])
print("every other element:", a[::2])

Boolean indexing

A Boolean condition can select all elements that satisfy a criterion.

a = np.array([4, 17, 2, 25, 8, 31])

mask = a > 10

print("mask:", mask)
print("values > 10:", a[mask])

a[a > 10] = 100
print("modified:", a)

Boolean indexing is extremely useful in image processing. For example, it can threshold every pixel without writing nested loops.

gray = np.linspace(0, 1, 400).reshape(20, 20)

binary = gray > 0.5

plt.figure(figsize=(4, 4))
plt.imshow(binary, cmap="gray")
plt.axis("off")
plt.show()

4. Shape, transpose, reshape, and dimensions

a = np.array([
    [1, 2, 3, 4],
    [5, 6, 7, 8]
])

print("a:")
print(a)
print("shape:", a.shape)

print("\na.T:")
print(a.T)
print("shape:", a.T.shape)
a = np.arange(12)

print("original:", a.shape)

b = a.reshape(3, 4)
print("reshaped:")
print(b)
print("shape:", b.shape)

c = np.expand_dims(a, axis=0)
print("expanded shape:", c.shape)

A dimension of size 1 is sometimes important because it controls how arrays interact during broadcasting or with machine-learning libraries.

5. Arithmetic and broadcasting

Arithmetic operators such as +, -, *, and / operate element-by-element.

a = np.array([1, 2, 3])
b = np.array([10, 20, 30])

print("a + b:", a + b)
print("a * b:", a * b)
print("a * 5:", a * 5)

Broadcasting

Broadcasting lets NumPy combine arrays with compatible shapes without manually repeating data.

image = np.ones((4, 5, 3))
channel_scale = np.array([1.0, 0.5, 0.25])

scaled = image * channel_scale

print("image shape:", image.shape)
print("scale shape:", channel_scale.shape)
print("result shape:", scaled.shape)
print("pixel:", scaled[0, 0])

6. Reductions and the axis argument

Functions such as sum, mean, min, and max can operate over the entire array or along a selected axis.

a = np.array([
    [1, 2, 3],
    [4, 5, 6]
])

print("sum all:", np.sum(a))
print("sum axis=0:", np.sum(a, axis=0))
print("sum axis=1:", np.sum(a, axis=1))

print("mean all:", np.mean(a))
print("mean axis=0:", np.mean(a, axis=0))

For an image with shape (H, W, 3), averaging over axis=2 combines the color channels and produces an array with shape (H, W).

7. Matrix multiplication vs. elementwise multiplication

For arrays A and B:

A * B

performs elementwise multiplication when the shapes are compatible.

A @ B

or

np.matmul(A, B)

performs matrix multiplication.

A = np.array([
    [1, 2],
    [3, 4]
])

B = np.array([
    [5, 6],
    [7, 8]
])

print("Elementwise A * B:")
print(A * B)

print("\nMatrix multiplication A @ B:")
print(A @ B)

np.dot has different behavior depending on the dimensionality of its inputs. For straightforward matrix multiplication in this course, @ is often the clearest notation.

u = np.array([1, 2, 3])
v = np.array([4, 5, 6])

print("vector dot product:", np.dot(u, v))
print("equivalently:", u @ v)

8. Concatenating and stacking arrays

a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])

print("concatenate rows:")
print(np.concatenate([a, b], axis=0))

print("\nconcatenate columns:")
print(np.concatenate([a, b], axis=1))

print("\nstack new axis:")
print(np.stack([a, b], axis=0).shape)

9. Vectorization: avoid unnecessary Python loops

NumPy operations are implemented efficiently in compiled code. Whenever possible, operate on entire arrays instead of iterating over individual pixels in Python.

import time

rng = np.random.default_rng(0)
A = rng.integers(0, 256, size=(800, 800), dtype=np.uint8)

# Python-loop version
loop_result = A.copy()

start = time.perf_counter()
for i in range(loop_result.shape[0]):
    for j in range(loop_result.shape[1]):
        if loop_result[i, j] > 100:
            loop_result[i, j] = 255
loop_time = time.perf_counter() - start

# Vectorized version
vector_result = A.copy()

start = time.perf_counter()
vector_result[vector_result > 100] = 255
vector_time = time.perf_counter() - start

print(f"Loop:       {loop_time:.4f} s")
print(f"Vectorized: {vector_time:.6f} s")
print("Same result:", np.array_equal(loop_result, vector_result))

The exact timing depends on your computer, but the vectorized approach should normally be much faster and is also easier to read.

10. Preallocation

Repeatedly resizing an array is inefficient because data may need to be copied each time.

Prefer creating the final-size array first when the size is known.

# Preferred when the required size is known
a = np.empty(10, dtype=np.float32)

for i in range(10):
    a[i] = i * 0.5

print(a)

11. Views and copies

Slicing often creates a view into the original array rather than an independent copy. Changing the slice can therefore change the original array.

a = np.array([1, 2, 3, 4, 5])

view = a[1:4]
view[0] = 100

print("view:", view)
print("original:", a)

Create an independent array with .copy() when you do not want later modifications to affect the original.

a = np.array([1, 2, 3, 4, 5])

independent = a[1:4].copy()
independent[0] = 100

print("copy:", independent)
print("original:", a)

12. Data types and pixel ranges

NumPy arrays have an explicit dtype.

Common image types include:

  • uint8: integer values from 0 to 255;
  • float32 or float64: often normalized to the range 0 to 1.

Data type and value range both matter when performing image arithmetic or displaying images.

uint_image = np.array([0, 64, 128, 255], dtype=np.uint8)
float_image = uint_image.astype(np.float32) / 255.0

print(uint_image, uint_image.dtype)
print(float_image, float_image.dtype)

Be careful with uint8 arithmetic

Unsigned 8-bit values cannot represent numbers below 0 or above 255. Arithmetic may wrap around.

x = np.array([250], dtype=np.uint8)

print("uint8:", x + np.array([10], dtype=np.uint8))
print("float:", x.astype(np.float32) + 10)

For many image-processing calculations, convert to floating point before arithmetic and convert back only when needed.

13. NumPy and OpenCV

OpenCV images are NumPy arrays.

One important convention is that cv2.imread() loads color images in BGR channel order, while Matplotlib expects RGB.

import cv2

# Example for an image file:
#
# image_bgr = cv2.imread("image.jpg")
# image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
# plt.imshow(image_rgb)
# plt.axis("off")
# plt.show()
#
# print(image_bgr.shape)
# print(image_bgr.dtype)

print("OpenCV version:", cv2.__version__)

14. Useful NumPy functions

A compact reference:

Function Purpose
np.array Create an array
np.zeros, np.ones, np.full Create initialized arrays
np.arange, np.linspace Create numerical sequences
arr.shape Array dimensions
arr.dtype Data type
arr.reshape(...) Change shape without changing data
arr.T, np.transpose Reorder axes
np.expand_dims Insert a dimension
np.squeeze Remove dimensions of size 1
np.concatenate Join arrays along an existing axis
np.stack Join arrays along a new axis
np.sum, np.mean Reductions
np.min, np.max Extrema
np.argmin, np.argmax Index of extrema
np.where Conditional selection
np.clip Limit values to a range
np.linalg.norm Vector/matrix norm
@, np.matmul Matrix multiplication

Practice Exercises

Try to solve these using NumPy operations. Prefer vectorized solutions when possible.

1. Mean

Calculate the mean value of all elements in arr.

arr = np.array([
    [1, 2, 3],
    [7, 8, 9],
    [2, 7, 3]
])

# Your code here

2. Row sums

Create an array containing the sum of each row.

arr = np.array([
    [5, 6, 2, 5],
    [4, 5, 2, 5],
    [6, 7, 2, 5],
    [1, 2, 8, 5],
    [9, 1, 3, 2]
])

# Your code here

3. Boolean indexing

Replace every even number in arr with 0.

arr = np.array([1, 4, 7, 8, 2, 5, 8, 1, 5, 7, 8, 9])

# Your code here

4. Vectorize a loop

Replace the loop below with a NumPy expression.

arr = np.array([
    [8, 1, 4],
    [6, 2, 8]
])

result = np.zeros_like(arr)

for i in range(arr.shape[0]):
    for j in range(arr.shape[1]):
        result[i, j] = arr[i, j] ** 2

# Rewrite without explicit loops

5. Combine columns

Create one array where each row contains [age, height, hours].

age = np.array([[14], [52], [24], [31]])
height = np.array([[65], [70], [68], [72]])
hours = np.array([[12], [40], [25], [35]])

# Your code here

6. Thresholding

Set every element greater than 5 to 255 and every other element to 0, without loops.

arr = np.array([
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
])

# Your code here

7. Elementwise and matrix multiplication

Compute elementwise multiplication between m1 and m2.T, then compute matrix multiplication between m1 and m2.

m1 = np.array([
    [4, 5, 2, 1],
    [6, 8, 2, 1],
    [1, 7, 9, 12]
])

m2 = np.array([
    [2, 1, 3],
    [4, 2, 5],
    [6, 3, 1],
    [2, 8, 4]
])

# Your code here

8. Highest total hours

Find the person who worked the highest total number of hours.

people = np.array(["alice", "bob", "cindy", "david", "elyse"])

hours = np.array([
    [8, 3, 6, 6, 7],
    [5, 8, 8, 4, 6],
    [9, 9, 5, 7, 8],
    [8, 7, 6, 5, 4],
    [6, 6, 7, 7, 6]
])

# Your code here

9. Highest earnings

Using the arrays below, assume the pay rate is:

  • day 1: $11/hour
  • day 2: $12/hour
  • day 3: $13/hour
  • day 4: $14/hour
  • day 5: $15/hour

Find the person who earned the most money.

people = np.array(["alice", "bob", "cindy", "david", "elyse"])

hours = np.array([
    [8, 3, 6, 6, 7],
    [5, 8, 8, 4, 6],
    [9, 9, 5, 7, 8],
    [8, 7, 6, 5, 4],
    [6, 6, 7, 7, 6]
])

rates = np.array([11, 12, 13, 14, 15])

# Your code here

10. Array pattern

Use m1 and m2 to create the following array without hard-coding the final values:

[0, 3, 1, 0, 0, 3, 1, 0, 0, 3, 1, 0]

Explore np.stack, np.column_stack, np.ravel, or reshaping.

m1 = np.array([0, 1, 0, 1, 0, 1])
m2 = np.array([3, 0, 3, 0, 3, 0])

# Your code here

Additional reference

NumPy documentation:

https://numpy.org/doc/stable/

When working with unfamiliar NumPy functions, reading the function documentation and checking the expected array shapes is often the fastest way to debug a problem.