A photomosaic is an image composed of many smaller images arranged in a grid. From a distance the mosaic appears to be a single target image. When viewed up close it reveals thousands of individual images carefully selected to match different regions of the target.
This illusion works because the human visual system blends colors and shapes at a distance allowing coarse color matching to recreate fine visual detail. Photomosaics are widely used in:
- Digital art and posters
- Marketing visuals
- Image processing research
- Educational projects for computer vision fundamentals
Types of Photomosaics
1. Simple (Average-Color-Based) Photomosaic
- Each target tile and library image is reduced to a single average RGB color and the tile is replaced by the library image with the closest color match.
- This method is fast, scalable and easy to implement but it cannot preserve fine textures or details.
2. Advanced (Pixel-by-Pixel Matching) Photomosaic
- Each pixel of a target tile is compared with the corresponding pixels of every library image and the image with the smallest total difference is selected.
- This produces much higher visual quality and detail but is computationally expensive and often requires GPU acceleration.
How to Create Photomosaics
The process of creating a photomosaic involves transforming a target image into a grid of tiles and replacing each tile with the most visually similar image from a collection of smaller images. The complete workflow consists of the following steps:
- Load tile (library) images that will be used to construct the mosaic.
- Read the target image and divide it into an M × N grid of equal-sized tiles.
- Analyze each target tile to extract its color characteristics.
- Match each target tile with the most similar tile image from the library.
- Assemble the final mosaic by arranging the selected tile images in their respective grid positions.
1. Splitting the Images into Tiles
The target image is divided into an M x N grid, where each tile represents a rectangular region of the image. For a tile at position (i, j), the coordinates are computed using the tile width (w) and height (h) to ensure uniform partitioning.

2. Averaging Color Values
Each image tile is represented by its average RGB color, calculated by averaging the red, green and blue values of all pixels in the tile. This reduces a complex image region to a single color vector, enabling fast and efficient comparison.
(r, g, b)_{avg} = \left( \frac{1}{N} \sum r_i, \frac{1}{N} \sum g_i, \frac{1}{N} \sum b_i \right)
where
(r, g, b)_{avg} : average color of an image or image tile in the RGB color spacer_{i},g_{i},b_{i} : The red, green and blue values of the i-th pixel in the image or tileN : The total number of pixels in the image or tile being analyzed
3. Matching Images
For each target tile the average RGB value is compared with the average RGB values of all library images using a distance metric. The tile image with the minimum color distance is selected as the best match.
D = \sqrt{(r_1 - r_2)^2 + (g_1 - g_2)^2 + (b_1 - b_2)^2}
where D Represents the Euclidean distance between two colors in RGB color space
Step By Step Implementation
Here we implement Photomosaic
Step 1: Install Dependencies
Here we Installs Pillow for image processing and NumPy for numerical operations.
!pip install pillow numpy
Step 2: Import Libraries
Here we import os, zipfile, numpy, Image, files and Matplotlib
import os
import zipfile
import numpy as np
from PIL import Image
from google.colab import files
import matplotlib.pyplot as plt
Step 3: Define Function to Compute Average RGB Color
- Converts an image to a NumPy array and calculates the mean red, green and blue values across all pixels.
- This single vector represents the color of the tile for comparison.
def average_color(image):
img = np.asarray(image)
return img.mean(axis=(0, 1))
Step 4: Upload Target Image
Here we upload taget image and converts the image to RGB mode for consistent processing.

uploaded_target = files.upload()
target_path = list(uploaded_target.keys())[0]
target_image = Image.open(target_path).convert("RGB")
Step 5: Upload Tile Images ZIP
- user upload a ZIP file containing tile images.
- Extracts the images into a tiles folder.
You can download a ZIP file of the color dataset from Kaggle
uploaded_zip = files.upload()
zip_path = list(uploaded_zip.keys())[0]
tile_folder = "tiles"
os.makedirs(tile_folder, exist_ok=True)
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(tile_folder)
Step 6: Load Tile Images and Precompute Colors
Loads each image from the folder, converts it to RGB and resizes it to match tile dimensions. Computes average RGB color for each tile.
def load_tile_images(folder, tile_size):
tiles = []
colors = []
for file in os.listdir(folder):
path = os.path.join(folder, file)
try:
img = Image.open(path).convert("RGB")
img = img.resize(tile_size)
tiles.append(img)
colors.append(average_color(img))
except:
pass
return tiles, np.array(colors)
Step 7: Find Best Matching Tile
Calculates Euclidean distance between the target tile color and all tile images. Returns the index of the closest matching tile.
def find_best_match(target_color, tile_colors):
distances = np.linalg.norm(tile_colors - target_color, axis=1)
return np.argmin(distances)
Step 8: Create Photomosaic
Divides the target image into a grid of MxN tiles. For each tile computes the average color, finds the best matching tile and pastes it into the mosaic.
def create_mosaic(target_image, tiles, tile_colors, grid_size):
W, H = target_image.size
M, N = grid_size
tile_w, tile_h = W // M, H // N
mosaic = Image.new("RGB", (W, H))
for i in range(M):
for j in range(N):
box = (i * tile_w, j * tile_h,
(i + 1) * tile_w, (j + 1) * tile_h)
tile = target_image.crop(box)
target_color = average_color(tile)
best_idx = find_best_match(target_color, tile_colors)
mosaic.paste(tiles[best_idx], box)
return mosaic
Step 9: User Input for Grid Size
Allows the user to choose the grid resolution. Higher grid numbers give more detailed mosaics, but require more tile images.
M = int(input("Enter number of tiles along width (e.g., 64): "))
N = int(input("Enter number of tiles along height (e.g., 64): "))
grid_size = (M, N)
Output:
Enter number of tiles along width (e.g., 64): 96
Enter number of tiles along height (e.g., 64): 96
Step 10: Handle Tile Folder Structure
- Handles cases where images are in a subfolder. Ensures the code finds the correct folder containing actual tile images.
- Loads all tile images and computes their average colors.
- Raises an error if no valid images are found, guiding the user to fix the ZIP content.
tile_w = target_image.size[0] // M
tile_h = target_image.size[1] // N
actual_tile_image_folder = tile_folder
extracted_items = os.listdir(tile_folder)
if len(extracted_items) == 1 and os.path.isdir(os.path.join(tile_folder, extracted_items[0])):
actual_tile_image_folder = os.path.join(tile_folder, extracted_items[0])
elif 'Images' in extracted_items and os.path.isdir(os.path.join(tile_folder, 'Images')):
actual_tile_image_folder = os.path.join(tile_folder, 'Images')
elif 'images' in extracted_items and os.path.isdir(os.path.join(tile_folder, 'images')):
actual_tile_image_folder = os.path.join(tile_folder, 'images')
tiles, tile_colors = load_tile_images(actual_tile_image_folder, (tile_w, tile_h))
if not tiles:
raise ValueError("No tile images were successfully loaded from the ZIP file.")
Step 11: Generate Mosaic
Here we creates the photomosaic using the target image, tile images and user-specified grid size
mosaic = create_mosaic(target_image, tiles, tile_colors, grid_size)
plt.figure(figsize=(10, 10))
plt.imshow(mosaic)
plt.axis("off")
mosaic.save("photomosaic_output.jpg")
files.download("photomosaic_output.jpg")
