Capstone: Image as an Array

Reviewed & published by Brayan K

An image is simply a 3D NumPy array of shape (height, width, 3) holding uint8 values from 0 to 255 — one number per color channel (red, green, blue) for every pixel — which means every editing trick is really just array indexing, slicing, broadcasting, and aggregation. In this final capstone you'll build a synthetic image, set its channels, convert it to grayscale, flip and crop with slicing, invert its colors, and brighten it with clip — tying together everything from the whole course.

Part of the free NumPy course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

1 An Image Is a 3D Array

A color image is a grid of pixels, and each pixel is three numbers — red, green, blue. NumPy stores that as a 3D array with shape (height, width, 3) and dtype uint8 (0–255). Create a blank black image with np.zeros.

The three axes are: axis 0 = rows (height), axis 1 = columns (width), axis 2 = color channels (R, G, B).

import numpy as np

# A 4 x 6 black image: 4 rows, 6 cols, 3 color channels
img = np.zeros((4, 6, 3), dtype=np.uint8)
print(img.shape)   # (4, 6, 3)
print(img.dtype)   # uint8
print(img[0, 0])   # [0 0 0]  (every pixel starts black)

# ✅ Expected output:
# (4, 6, 3)
# uint8
# [0 0 0]

Expected output: shape (4, 6, 3), dtype uint8, and the first pixel [0 0 0] (pure black).

2 Setting Color Channels

The last axis selects a channel: index 0 is red, 1 is green, 2 is blue. Assigning to a channel slice uses broadcasting to fill many pixels at once. Color cheat-sheet: [255, 0, 0] is red, [0, 255, 0] is green, [0, 0, 255] is blue, and [255, 255, 255] is white.

import numpy as np

img = np.zeros((4, 6, 3), dtype=np.uint8)

# Set the RED channel (index 0) of every pixel to full
img[:, :, 0] = 255
print(img[0, 0])   # [255   0   0]  -> red

# Paint a green square in the lower-left 2x2 corner via broadcasting
img[2:4, 0:2] = [0, 255, 0]
print(img[3, 0])   # [  0 255   0]  -> green

# ✅ Expected output:
# [255   0   0]
# [  0 255   0]

Expected output: a red pixel [255 0 0] and a green corner pixel [0 255 0].

3 Converting to Grayscale

Grayscale collapses the 3 channels into a single brightness value per pixel using a weighted sum across the channel axis. The standard luminosity weights are 0.2989·R + 0.5870·G + 0.1140·B, which match how the human eye perceives color. np.dot applies them across the last axis.

import numpy as np

# A tiny 2 x 2 image: red, green / blue, white
img = np.array([
    [[255,   0,   0], [  0, 255,   0]],
    [[  0,   0, 255], [255, 255, 255]],
], dtype=np.uint8)
print(img.shape)   # (2, 2, 3)

# Weighted sum across the channel axis -> a 2D (height, width) array
weights = np.array([0.2989, 0.5870, 0.1140])   # luminosity weights R, G, B
gray = np.dot(img[..., :3], weights)

print(gray.shape)        # (2, 2)
print(gray.round(1))     # [[ 76.2 149.7]
                         #  [ 29.1 255. ]]

# ✅ Expected output:
# (2, 2, 3)
# (2, 2)
# [[ 76.2 149.7]
#  [ 29.1 255. ]]

Expected output: a 2D (2, 2) array of brightness values about 76.2 (red), 149.7 (green), 29.1 (blue), 255.0 (white).

4 Flipping, Cropping, Inverting & Brightening

Geometry operations are pure slicing. img[::-1] reverses the row axis to flip top-to-bottom; img[:, ::-1] flips left-to-right. Cropping is a slice of the grid: img[r0:r1, c0:c1] keeps a rectangle of pixels (with all channels).

import numpy as np

# Label rows 0..3 by their red value so flips are easy to see
img = np.zeros((4, 4, 3), dtype=np.uint8)
for r in range(4):
    img[r, :, 0] = r * 10   # red channel = 0, 10, 20, 30 by row
print(img[:, 0, 0])         # [ 0 10 20 30]  (top -> bottom)

# Flip top-to-bottom (reverse rows), then crop the top-left 2x2 block
flipped = img[::-1]
print(flipped[:, 0, 0])     # [30 20 10  0]
print(img[0:2, 0:2].shape)  # (2, 2, 3)

# ✅ Expected output:
# [ 0 10 20 30]
# [30 20 10  0]
# (2, 2, 3)

Expected output: red values [0 10 20 30] top-to-bottom, reversed to [30 20 10 0] after the flip, and a crop of shape (2, 2, 3).

Inverting colors is broadcasting a scalar across the whole array: 255 - img turns black to white and red to cyan. Brightening adds a value but must stay in range — use np.clip(..., 0, 255) so nothing overflows past 255.

import numpy as np

img = np.array([[[ 10,  20,  30],
                 [200, 250, 100]]], dtype=np.uint8)

# Invert: broadcast 255 - img across every value
inverted = 255 - img
print(inverted[0, 0])   # [245 235 225]
print(inverted[0, 1])   # [ 55   5 155]

# Brighten by 40, clipping so values saturate at 255 (no uint8 wrap-around)
brighter = np.clip(img.astype(np.int16) + 40, 0, 255).astype(np.uint8)
print(brighter[0, 1])   # [240 255 140]  (250 + 40 clips to 255)

# ✅ Expected output:
# [245 235 225]
# [ 55   5 155]
# [240 255 140]

Expected output: inverted pixels [245 235 225] and [55 5 155]; the brightened pixel [240 255 140] — note 250 + 40 clips to 255, not a wrap-around.

🎯 YOUR TURN: Fill in the Blanks

Replace each ___ so the program builds a small blue image, then inverts it to yellow.

import numpy as np

# A 3 x 3 black image: rows, cols, and 3 color channels
img = np.zeros((3, 3, ___), dtype=np.uint8)   # fill in the channel count
img[:, :, ___] = 255                          # fill in the BLUE channel index
print(img[0, 0])      # [  0   0 255]  -> blue

# Invert by broadcasting 255 minus the image
inverted = ___ - img                          # fill in the value to subtract from
print(inverted[0, 0]) # [255 255   0]  -> yellow

Expected output: [0 0 255] then [255 255 0]. (Answers: 3, 2, 255.)

⚠️ Common Errors & Quick Reference

With uint8, 250 + 40 wraps to 34 instead of saturating, giving weird colors.

✅ Fix: compute in a wider type, then np.clip(result, 0, 255).astype(np.uint8).

❌ Mixing up axis order (width vs height)

The shape is (height, width, 3): rows come first, columns second.

✅ Fix: index as img[row, col], and remember axis 2 is the color channel.

TaskCode
Blank imagenp.zeros((h, w, 3), np.uint8)
Set red channelimg[:, :, 0] = 255
Flip / cropimg[::-1] · img[r0:r1, c0:c1]
Invert255 - img
Brighten safelynp.clip(img + 40, 0, 255)

🏆 Mini Challenge: A Full Edit Pipeline

Build a gradient image, brighten it with clip, flip it, then report its average brightness — tying together shape, dtype, slicing, broadcasting, clip, and aggregation.

import numpy as np

# A 4-pixel-wide gray gradient, 2 rows tall, 3 channels
row = np.array([50, 100, 150, 200], dtype=np.uint8)
img = np.stack([np.stack([row] * 3, axis=1)] * 2, axis=0)
print(img.shape)            # (2, 4, 3)

# Brighten by 60, clipping so we saturate at 255
bright = np.clip(img.astype(np.int16) + 60, 0, 255).astype(np.uint8)
print(bright[0, :, 0])      # [110 160 210 255]

# Flip left-to-right (reverse columns), then average brightness
flipped = bright[:, ::-1]
print(flipped[0, :, 0])     # [255 210 160 110]
print(round(float(flipped.mean()), 1))   # 183.8

# ✅ Expected output:
# (2, 4, 3)
# [110 160 210 255]
# [255 210 160 110]
# 183.8

❓ Frequently Asked Questions

🎉 Capstone complete — you've finished the NumPy course!

You treated an image as a 3D array and used every core NumPy skill — shape and dtype, indexing and slicing, broadcasting, aggregation, and clip — to create, recolor, grayscale, flip, crop, invert, and brighten it. That is real image processing, powered entirely by arrays.

🚀 Congratulations! From your first np.array to a full image-editing pipeline, you now have a working command of NumPy. Go build something amazing with it.

Practice quiz

What shape does a color image have as a NumPy array?

  • (width, height)
  • (channels, height, width)
  • (height, width, channels)
  • (height, width)

Answer: (height, width, channels). Color images are 3D arrays of shape (height, width, channels).

Which dtype is used for standard 0-255 image pixels?

  • uint8
  • float64
  • int32
  • bool

Answer: uint8. uint8 holds exactly the 0-255 range in a single byte per value.

What does np.zeros((4, 6, 3), dtype=np.uint8) create?

  • A white image
  • A 4x6 black RGB image
  • A 6x4 grayscale image
  • A 1D array

Answer: A 4x6 black RGB image. All-zero RGB pixels are black, with shape (4, 6, 3).

In an RGB image, which index selects the blue channel?

  • 0
  • 3
  • 1
  • 2

Answer: 2. Channel order is R=0, G=1, B=2, so blue is index 2.

What does img[:, :, 0] = 255 do?

  • Sets every pixel to white
  • Deletes the red channel
  • Sets the red channel of every pixel to full
  • Sets only the first pixel

Answer: Sets the red channel of every pixel to full. It assigns 255 to the red channel (index 0) of all pixels.

How is an RGB image converted to grayscale?

  • A weighted sum across the channel axis
  • By dropping the red channel
  • By summing all axes
  • By transposing the array

Answer: A weighted sum across the channel axis. Grayscale is a luminosity-weighted sum: 0.2989R + 0.5870G + 0.1140B.

What does img[::-1] do to an image?

  • Inverts the colors
  • Flips it top-to-bottom (reverses rows)
  • Crops the image
  • Flips it left-to-right

Answer: Flips it top-to-bottom (reverses rows). Slicing the row axis with ::-1 reverses the rows, a vertical flip.

Which expression inverts an image's colors?

  • img * -1
  • img.T
  • np.flip(img)
  • 255 - img

Answer: 255 - img. Broadcasting 255 - img turns each value into its complement.

Why use np.clip when brightening uint8 pixels?

  • To make the image larger
  • To prevent overflow wrap-around past 255
  • To convert to float
  • To flip the image

Answer: To prevent overflow wrap-around past 255. Clipping to 0-255 stops values from wrapping around in uint8.

Cropping a rectangle of an image is done with...

  • slicing like img[r0:r1, c0:c1]
  • np.crop(img)
  • img.resize()
  • img.reshape()

Answer: slicing like img[r0:r1, c0:c1]. Cropping is just slicing the row and column axes.

Continue this course

Frequently asked questions

How is a color image represented as a NumPy array?

A color image is a 3D array with shape (height, width, channels). The first two axes are the pixel grid and the last axis holds the color channels — usually 3 for red, green, and blue. Each value is a uint8 integer from 0 to 255, where 0 is no intensity and 255 is full intensity.

Why are image pixels stored as uint8?

uint8 is an unsigned 8-bit integer that holds exactly the 0-255 range each color channel needs, using just one byte per value. That keeps images compact in memory and matches how virtually every image format and display expects color data.

How do you convert, flip, or crop an image in NumPy?

Grayscale is a weighted sum across the channel axis: np.dot(img[..., :3], [0.2989, 0.5870, 0.1140]) collapses the channels into a 2D brightness array. Flipping and cropping are just slicing: img[::-1] reverses the rows (top-to-bottom flip), img[:, ::-1] flips left-to-right, and img[10:50, 20:80] crops a rectangle of pixels.