The final exercise, "pimp my image", is where everything pays off. You write five color filters — invert, red, green, blue, grey — that keep the image shape identical and use only a restricted set of operators. Each constraint is a puzzle that teaches you how a channel is really manipulated.
The mental model
Remember from exercise 02: an RGB image is a (H, W, 3) array where the last axis holds [R, G, B]. A "filter" is a function that takes that array and returns a new one of the same shape. The trick is that channel selection is just slicing the last axis:
array[:, :, 0] # all the Red values
array[:, :, 1] # all the Green values
array[:, :, 2] # all the Blue values
ft_invert — operators: =, +, -, *
Inverting a color means reflecting each channel around the midpoint: a pixel value v becomes 255 - v. Black (0) becomes white (255) and vice-versa. With subtraction allowed it is a one-liner:
def ft_invert(array):
"""Invert the colors of the image received."""
return 255 - array
NumPy broadcasts the scalar 255 against every element. Because invert also permits + and *, you could write -1 * array + 255 — same result, showing the operators are interchangeable here.
ft_red — operators: =, *
The "red" filter keeps the red channel and zeroes the others. You are not allowed to subtract, only assign and multiply — so you multiply green and blue by 0:
def ft_red(array):
"""Keep only the red channel."""
result = array.copy()
result[:, :, 1] = result[:, :, 1] * 0 # green -> 0
result[:, :, 2] = result[:, :, 2] * 0 # blue -> 0
return result
Multiplying a channel by 0 wipes it out; multiplying by 1 leaves it untouched. That is why only = and * are needed.
ft_green — operators: =, -
Now you may only assign and subtract. To isolate green, subtract each unwanted channel from itself, which makes it zero:
def ft_green(array):
"""Keep only the green channel."""
result = array.copy()
result[:, :, 0] = result[:, :, 0] - result[:, :, 0] # red -> 0
result[:, :, 2] = result[:, :, 2] - result[:, :, 2] # blue -> 0
return result
The constraint forces a clever idea: x - x == 0. Same destination as the red filter, reached with a different tool.
ft_blue — operator: = only
The strictest one: assignment only, no arithmetic at all. So you simply assign a constant 0 to the channels you want gone:
def ft_blue(array):
"""Keep only the blue channel."""
result = array.copy()
result[:, :, 0] = 0 # red -> 0
result[:, :, 1] = 0 # green -> 0
return result
Assigning a scalar to a whole 2D slice sets every element of that channel — broadcasting again. With pure assignment you cannot compute, only overwrite, which is exactly enough here.
ft_grey — operators: =, /
Grayscale means every channel carries the same intensity, so R = G = B. The simple average uses division:
def ft_grey(array):
"""Convert the image to grayscale (average of channels)."""
result = array.copy()
grey = (result[:, :, 0] / 3) + (result[:, :, 1] / 3) + (result[:, :, 2] / 3)
result[:, :, 0] = grey
result[:, :, 1] = grey
result[:, :, 2] = grey
return result
Dividing each channel by 3 and summing gives the mean brightness; copying it back into all three channels produces grey. (If only = and / are allowed and not +, you can instead just replicate one channel, e.g. assign the red channel into green and blue — a cheaper "grey" that still satisfies R = G = B.)
Two recurring pitfalls
- Copy before you mutate. Every filter starts with
array.copy(). Without it you edit the caller's image in place, and the next filter sees corrupted data. - uint8 overflow. Pixels are 0–255 bytes.
200 + 100wraps around to 44, not 300.255 - arrayis safe because it stays in range, but if you add or average, cast to a wider type (array.astype(int)) and clip back, or you will get bright speckle artifacts.
Why the operator restrictions matter
Each filter reaches the same kind of result — zeroing or equalizing channels — but the allowed operators change. That is a deliberate lesson: there is rarely one way to manipulate array data. Multiply by zero, subtract from self, or assign a constant all silence a channel. Recognizing these equivalences is what turns array syntax into fluent thinking.
Takeaways
- A filter maps a
(H, W, 3)array to another of the same shape. - Select channels by slicing the last axis:
array[:, :, c]. - Invert =
255 - array; isolate a channel by zeroing the others (×0, x−x, or =0); grey = equal channels. - Always
.copy()first, and minduint8overflow when adding.