Skip to main content...
Python + Classical CV — the Catalog Tool
25 min

Day 4: NumPy vectorization: broadcasting, indexing, why loops are slow

The single habit that matters most this week

A Python for loop over an array's elements pays Python's interpreter overhead — type checks, function call overhead, object allocation — on every single element. A vectorized operation hands the whole array to a compiled C loop once. On a million-element array, the difference isn't a rounding error; it's routinely 50-100x.

Loop vs vectorized — same result, very different cost
import numpy as np, time

a = np.random.rand(1_000_000)

# loop
start = time.perf_counter()
result = np.empty_like(a)
for i in range(len(a)):
    result[i] = a[i] * 2 + 1
print("loop:", time.perf_counter() - start)       # ~0.15-0.3s

# vectorized
start = time.perf_counter()
result = a * 2 + 1
print("vectorized:", time.perf_counter() - start)  # ~0.001-0.003s

Same computation, 1M elements — vectorized NumPy vs a plain Python loop (log scale)

The rule to internalize

If you catch yourself writing for i in range(len(array)), stop — there is almost always a vectorized equivalent. This one habit is the difference between code that processes a catalog in seconds and code that processes it in minutes.

Broadcasting

Broadcasting is the rule set that lets NumPy apply an operation between arrays of *different* shapes without you writing an explicit loop — e.g. adding a single RGB offset (3,) to every pixel of an entire image (H, W, 3). NumPy compares shapes from the right; dimensions match if they're equal or one of them is 1.

Broadcasting: a (3,) vector applied to every pixel of an (H, W, 3) image
image = np.random.randint(0, 255, (480, 640, 3), dtype=np.uint8)
brightness_offset = np.array([10, 10, 10])   # shape (3,)

# broadcasting stretches (3,) across every (H, W) pixel — no loop needed
brightened = np.clip(image.astype(int) + brightness_offset, 0, 255).astype(np.uint8)

Indexing: slicing, fancy indexing, boolean masks

Three ways to select data — all vectorized, no explicit loop
# slicing: every row, first 100 columns
cropped = image[:, :100]

# boolean mask: every pixel darker than 50 across all channels
dark_mask = image.mean(axis=2) < 50

# fancy indexing: select specific rows by an array of indices
selected_rows = image[[0, 5, 10]]

Key terms

Vectorization
Expressing a computation as whole-array operations so it runs inside compiled loops instead of the Python interpreter.
Broadcasting
NumPy's rule set for applying operations between differently-shaped arrays by virtually stretching smaller dimensions.
Boolean mask
An array of True/False values, the same shape as the data, used to select elements matching a condition.

Why is `image * 2` dramatically faster than looping over every pixel and multiplying by 2 in pure Python?

We use cookies

We use cookies to enhance your browsing experience, serve personalized content, and analyze our traffic. By clicking "Accept All", you consent to our use of cookies. Learn more

    Day 4: NumPy vectorization: broadcasting, indexing, why loops are slow | RBTechIconX