Pixels2GenAI
Path i Foundations
M 01 · 1.1.1 · hands-on

1.1.1 Images as Arrays & RGB

Treat every digital image as a NumPy array of intensity values; build colour from scratch by writing red, green, and blue channels by hand.

Duration20–25 min
Levelbeginner
Load3 core concepts
PrereqsBasic Python

Overview

Digital images are arrays of numbers. A single 4K image contains 26 million pixels. Three integers per pixel, each specifying the intensity of the red, green, and blue channels. Manipulating these arrays directly opens every technique from vintage photo filters to training convolutional networks.

Learning objectives

  1. Understand that images are 2D arrays of numerical values (grayscale).
  2. Recognise the relationship between array values (0–255) and brightness.
  3. Extend to 3D NumPy arrays with RGB channels for colour images.
  4. Grasp the additive colour model and why computers use RGB.

Quick start — your first colourful image

Run this code to create a simple image:

python · quick_start.py
import numpy as np
from PIL import Image

# Create a 200x200 image with 3 color channels (RGB)
image = np.zeros((200, 200, 3), dtype=np.uint8)

# Top half: cyan
image[:100, :, 1] = 255  # Green channel
image[:100, :, 2] = 255  # Blue channel

# Bottom half: magenta
image[100:, :, 0] = 255  # Red channel
image[100:, :, 2] = 255  # Blue channel

# Convert to PIL and save
pil_image = Image.fromarray(image)
pil_image.save('cyan_magenta.png')

Core concepts

Concept 1 — Grayscale, the foundation

Before diving into colour, the simplest digital image: grayscale.

A grayscale image is a 2D grid of pixels, where each pixel has a brightness value:

  • 0 = black (no light)
  • 255 = white (maximum light)
  • 128 = medium gray (half intensity)
python · grayscale.py
import numpy as np
from PIL import Image

# Create a 200x200 array filled with medium gray
array = np.zeros((200, 200), dtype=np.uint8)
array += 128

# PIL interprets 2D arrays as grayscale automatically
image = Image.fromarray(array)
image.save('gray.png')

The Image.fromarray() function from Pillow converts NumPy arrays into displayable images.

The uint8 data type

The dtype=np.uint8 parameter is required for image arrays:

  • u = unsigned (no negative numbers)
  • int = integer (whole numbers only)
  • 8 = 8 bits per value (range: 0 to 255)
python
# Correct: uint8 for standard images
array = np.zeros((100, 100), dtype=np.uint8)

# Wrong: without dtype specification
array = np.zeros((100, 100))   # Defaults to float64 — PIL expects uint8 for 0-255 images

Array shape and coordinates

NumPy arrays use [row, column] indexing:

  • row = y-coordinate (vertical position, 0 at top)
  • column = x-coordinate (horizontal position, 0 at left)
  • Shape: (height, width) — height (rows) comes first.
python
array = np.zeros((200, 300), dtype=np.uint8)  # 200 tall, 300 wide

# Set pixel at row 50, column 100 to white
array[50, 100] = 255

# Set bottom-right corner
array[199, 299] = 128

Concept 2 — Understanding digital images

An RGB image is a 3D array of numbers. Each number represents the intensity of light for one colour channel at one pixel location. In NumPy, an RGB image has shape (height, width, 3). The three channels represent red, green, and blue intensities.

python
# RGB array: height × width × 3 channels
image = np.zeros((100, 150, 3), dtype=np.uint8)
# Shape: (height=100, width=150, channels=3)

# Access a specific pixel's RGB values
pixel = image[50, 75, :]  # Returns [R, G, B]

# Access just the red channel
red_channel = image[:, :, 0]

Concept 3 — The RGB colour model

RGB is an additive colour model: start with darkness (black) and add colored light.

  • Red (255, 0, 0) → pure red light
  • Green (0, 255, 0) → pure green light
  • Blue (0, 0, 255) → pure blue light
  • White (255, 255, 255) → all three at maximum
  • Black (0, 0, 0) → no light

Each channel stores values from 0 to 255 (8 bits = 256 possible values), giving 16,777,216 total colours (256³). This is called “24-bit true colour” and exceeds the ~10 million colours the human eye can discriminate.

Diagram showing RGB additive colour mixing — three overlapping circles producing cyan, magenta, yellow, and white
Fig. 1 RGB additive colour mixing: overlapping light creates secondary colours.

Common RGB colour patterns

  • Primary colours: one channel at 255, others at 0.
  • Secondary colours: two channels at 255, one at 0.
    • Cyan (0, 255, 255) = green + blue.
    • Magenta (255, 0, 255) = red + blue.
    • Yellow (255, 255, 0) = red + green.
  • Grayscale: all three channels equal (N, N, N).
  • Pastels: high values across all channels (light colours).
  • Dark colours: low values across all channels.

Exercises

Three progressively challenging exercises, each building on the previous using the Execute → Modify → Create approach.

EXECUTE I.

Run and predict

Run the following code and observe the output. Try to predict what colour you will see before running it.

python · exercise1.py
import numpy as np
from PIL import Image

# Create a 150x150 image
image = np.zeros((150, 150, 3), dtype=np.uint8)

# Set all pixels to the same color
image[:, :, 0] = 255  # Red channel
image[:, :, 1] = 128  # Green channel
image[:, :, 2] = 0    # Blue channel

# Save and inspect
Image.fromarray(image).save('exercise1_color.png')

Reflection questions

  • What colour appears? Why?
  • What would happen if you set all three channels to 255?
  • What would (0, 0, 0) look like?
MODIFY II.

Hit specific colours

Modify the code from Exercise 1 to produce each of these colours. Change only the three channel values.

CREATE III.

Build a gradient from scratch

Create something from scratch: a horizontal colour gradient that transitions smoothly from one colour to another.

Goal: a 200×200 image that transitions from pure red on the left to pure blue on the right.

python · exercise3_starter.py
import numpy as np
from PIL import Image

# Create image
height, width = 200, 200
image = np.zeros((height, width, 3), dtype=np.uint8)

# Your code here: fill the image with a gradient.
# Loop over columns and set red and blue channels.

Image.fromarray(image).save('gradient.png')

Downloads

rgb.py — combined script rgb_additive_mixing_diagram.py — diagram generator

Summary

In 15–20 minutes, three core concepts: grayscale, RGB, and array indexing.

Common pitfalls to avoid

  • Don’t confuse RGB (additive / light) with CMYK (subtractive / paint).
  • Remember: image[row, column] — not image[x, y].
  • Always use dtype=np.uint8 for standard 0–255 image data.
  • Different libraries may use BGR instead of RGB (OpenCV in particular).

References

  1. [1] Foley, J. D., van Dam, A., Feiner, S. K., and Hughes, J. F. (1990). Computer Graphics: Principles and Practice (2nd ed.). Addison-Wesley. ISBN 0-201-12110-7.
  2. [2] Gonzalez, R. C. and Woods, R. E. (2007). Digital Image Processing (3rd ed.). Pearson.
  3. [3] Hunt, R. W. G. (2004). The Reproduction of Colour (6th ed.). Wiley. ISBN 0-470-02425-9.
  4. [4] Mayer, R. E. (2020). Multimedia Learning (3rd ed.). Cambridge University Press.
  5. [5] Sweller, J. and Cooper, G. (1985). The use of worked examples as a substitute for problem solving in learning algebra. Cognition and Instruction, 2(1), 59–89.
  6. [6] Harris, C. R., et al. (2020). Array programming with NumPy. Nature, 585, 357–362. doi:10.1038/s41586-020-2649-2
  7. [7] Clark, A. (2015). Pillow (PIL Fork) Documentation. pillow.readthedocs.io