1.2.1: Random Pattern Generation#

Duration:

12-15 minutes

Level:

Beginner

Overview#

Random number generation is a fundamental building block of computational art and generative design. In this module, you’ll discover how simple random processes can create compelling visual patterns, from abstract color compositions to structured artistic grids inspired by pioneers like Gerhard Richter.

Learning Objectives

By completing this exercise, you will:

  • Understand how random number generators create visual patterns

  • Master the uniform distribution and its properties for image generation

  • Use Kronecker products to efficiently scale pixel patterns

  • Explore the relationship between randomness and artistic structure

  • Create your own variations of algorithmic art techniques

Quick Start: See Randomness In Action#

Let’s begin by creating something visually striking immediately. Run this code to generate a random color tile pattern:

Create random color tiles in seconds#
 1import numpy as np
 2from PIL import Image
 3
 4# Set seed for reproducible randomness
 5np.random.seed(42)
 6
 7# Create 10x10 grid of random RGB colors
 8random_colors = np.random.randint(0, 256, size=(10, 10, 3), dtype=np.uint8)
 9
10# Scale each color to a 20x20 pixel tile using Kronecker product
11scaled_image = np.kron(random_colors, np.ones((20, 20, 1), dtype=np.uint8))
12
13# Save the result
14result = Image.fromarray(scaled_image)
15result.save('quick_random_tiles.png')
10x10 grid of random colored tiles, each tile is 20x20 pixels

Random color tiles generated using uniform distribution#

Tip

Notice how each tile has a completely different color, yet the overall composition feels balanced. This is the power of uniform distribution. There is no color is favored over others.

Core Concepts#

Uniform Random Distribution#

The uniform distribution is the foundation of digital randomness. When we use np.random.randint(0, 256), every integer from 0 to 255 has an equal probability of being selected. This creates what we perceive as “pure randomness”.

# Every RGB value has equal 1/256 probability
random_rgb = np.random.randint(0, 256, size=(5, 5, 3), dtype=np.uint8)

# This gives us 256³ = 16,777,216 possible colors per pixel
total_colors = 256 ** 3
print(f"Total possible colors: {total_colors:,}")

For generative art, uniform distribution provides:

  • Unpredictability: No discernible patterns in individual elements

  • Visual balance: All colors represented equally over large samples

  • Infinite variety: Each generation produces unique results

Important

Setting np.random.seed() makes randomness reproducible. This is especially useful for debugging and comparing different algorithms on identical random inputs.

Kronecker Product for Scaling#

The Kronecker product (np.kron) is a mathematical operation that efficiently scales images by repeating each pixel into a larger block. Instead of using nested loops, we leverage this linear algebra operation for performance.

# Original: 2x2 array
small = np.array([[1, 2], [3, 4]])

# Scaling matrix: each element becomes 3x3 block
scale = np.ones((3, 3))

# Kronecker product result: 6x6 array
large = np.kron(small, scale)
# Result: each original value repeated in 3x3 blocks

For images, this transforms a small random grid into pixel-perfect tiles:

# Small random grid: 5x5x3 (75 total colors)
small_grid = np.random.randint(0, 256, size=(5, 5, 3), dtype=np.uint8)

# Scale each color to 40x40 pixel tile
tile_size = np.ones((40, 40, 1), dtype=np.uint8)
large_image = np.kron(small_grid, tile_size)
# Result: 200x200x3 image (40,000 pixels, but only 75 unique colors)

Note

The Kronecker product was named after Leopold Kronecker (1823-1891), though the operation itself appears in much earlier mathematical work. In computer graphics, it’s invaluable for creating pixel-perfect scaling without interpolation artifacts.

Color Space Considerations#

When generating random colors, the choice of color space dramatically affects the visual result:

  • RGB space: Uniform in red, green, blue channels independently.

  • Perceptual uniformity: RGB is NOT perceptually uniformed. Some random colors appear much brighter than others.

  • Gamut coverage: Random RGB covers the entire digital color palette, including colors that rarely appear in nature.

# These are all equally "random" in RGB space
color1 = [255, 255, 255]  # Bright white
color2 = [128, 128, 128]  # Medium gray
color3 = [255, 0, 0]      # Pure red
color4 = [1, 1, 1]        # Nearly black

# But they have very different perceptual brightness!

This creates visually interesting compositions because the eye perceives some tiles as “popping forward” (bright colors) while others decrease (dark colors) which adds implicit depth to a flat pattern.

Hands-On Exercises#

Now apply what you’ve learned with three progressively challenging exercises. Each builds on the previous one using the Execute → Modify → Re-code approach.

Exercise 1: Execute and explore#

Time estimate: 3-4 minutes

Run the following code exactly as written and observe the output. This creates a grid of random colored tiles.

Exercise 1 — Random color tiles#
 1import numpy as np
 2from PIL import Image
 3
 4# Create a 10x10 grid of random RGB colors
 5random_colors = np.random.randint(0, 256, size=(10, 10, 3), dtype=np.uint8)
 6
 7# Scale each color to a 20x20 pixel tile using Kronecker product
 8scaling_matrix = np.ones((20, 20, 1), dtype=np.uint8)
 9image_array = np.kron(random_colors, scaling_matrix)
10
11# Convert to image and save
12result_image = Image.fromarray(image_array)
13result_image.save('random_tiles.png')
14
15print(f"Image dimensions: {image_array.shape}")

Reflection questions:

  • What do you notice about the colors? Are any two tiles exactly the same?

  • Why does each tile appear as a solid block rather than individual pixels?

  • What role does the Kronecker product play in creating the final image?

Solution & Explanation

What happened:

  1. np.random.randint(0, 256, …) creates a 10×10×3 array where each RGB value is randomly chosen from 0-255

  2. np.kron() scales each color into a 20×20 pixel block, creating the tile effect

  3. The final image is 200×200 pixels (10 tiles × 20 pixels per tile)

Key insights:

  • Each tile is a different random color due to uniform distribution

  • The Kronecker product efficiently repeats each color value into larger blocks

  • We get 100 unique colors (10×10 tiles) from 16.7 million possible RGB combinations

Exercise 2: Modify parameters#

Time estimate: 3-4 minutes

Modify the code from Exercise 1 to achieve these different effects. Change only the specified parameters.

Goals:

  1. Subtle variations: Change the color range to create muted colors only

  2. Grayscale tiles: Make all tiles appear in shades of gray

  3. Larger tiles: Make each tile bigger for a bolder effect

Hints
  • For subtle colors, try limiting the range: np.random.randint(100, 180, …)

  • For grayscale, make all RGB channels the same value

  • For larger tiles, increase the scaling matrix size

Solutions

1. Subtle variations:

# Change this line:
random_colors = np.random.randint(100, 180, size=(10, 10, 3), dtype=np.uint8)
# Creates colors only in the 100-179 range (muted/pastel effect)

2. Grayscale tiles:

# Generate grayscale values and repeat across RGB channels
gray_values = np.random.randint(0, 256, size=(10, 10), dtype=np.uint8)
random_colors = np.stack([gray_values, gray_values, gray_values], axis=2)

3. Larger tiles:

# Change this line:
scaling_matrix = np.ones((40, 40, 1), dtype=np.uint8)
# Creates 40×40 pixel tiles instead of 20×20

Exercise 3: Re-code with discrete palette#

Time estimate: 5-6 minutes

Now create something new from scratch: a Gerhard Richter-inspired color grid using only specific color values.

Goal: Create random tiles that use only colors divisible by 32 (0, 32, 64, 96, 128, 160, 192, 224).

Requirements: * Use a 16×16 grid of tiles * Each tile should be 12×12 pixels * Only use the 8 discrete color values listed above

Exercise 3 starter code#
import numpy as np
from PIL import Image

# Create discrete color palette (values divisible by 32)
color_palette = np.array([0, 32, 64, 96, 128, 160, 192, 224])

# Your code here:
# 1. Create 16x16 grid of random indices into the palette
# 2. Map indices to actual color values
# 3. Scale up to 12x12 pixel tiles
# 4. Save as 'richter_style.png'
Complete Solution
Richter-inspired discrete color grid#
 1import numpy as np
 2from PIL import Image
 3
 4# Create discrete color palette (values divisible by 32)
 5color_palette = np.array([0, 32, 64, 96, 128, 160, 192, 224])
 6
 7# Generate random indices into the palette for 16x16 grid
 8random_indices = np.random.randint(0, len(color_palette), size=(16, 16, 3))
 9
10# Map indices to actual color values
11small_array = color_palette[random_indices].astype(np.uint8)
12
13# Scale each color to 12x12 pixel tiles
14scaling_matrix = np.ones((12, 12, 1), dtype=np.uint8)
15image_array = np.kron(small_array, scaling_matrix)
16
17# Save result
18result_image = Image.fromarray(image_array)
19result_image.save('richter_style.png')
20print(f"Created {image_array.shape} Richter-style grid!")

How it works:

  • Line 6-8: Creates a limited palette of only 8 color values per channel

  • Line 11: Randomly selects from palette indices (0-7) for each RGB channel

  • Line 12: Maps those indices to actual color values using array indexing

  • The result has the structured, limited-palette aesthetic of Richter’s color charts

Challenge extension: Try different palette sizes or create complementary color schemes!

Summary#

In this exercise, you learned fundamental techniques for generating random visual patterns:

Key takeaways:

  • Uniform distribution creates unbiased randomness - essential for fair color sampling

  • Kronecker products provide efficient pixel-perfect scaling without interpolation

  • Color space choice dramatically affects visual perception of randomness

  • Discrete palettes can make random compositions more aesthetically pleasing

  • Controlled randomness balances algorithmic generation with design principles

Remember: Random doesn’t mean arbitrary. The most successful generative art combines algorithmic unpredictability with careful aesthetic choices about constraints, parameters, and color relationships.

This foundation in random pattern generation prepares you for more sophisticated techniques like cellular automata, noise functions, and emergent behavior systems.

What’s Next#

Continue to ../../../1.2_pixel_manipulation_patterns/1.2.2_cellular_automata/README to learn how simple rules can create complex, evolving patterns through cellular automata.

References#

Additional Resources