Constructing Photo Mosaics Using Python Image Processing
Loading Source Tiles
Begin by ingesting the candidate images that will serve as mosaic tiles. The following implementation scans a directory and loads valid image files into memory:
def load_tile_library(directory):
"""
Scan directory for image files and load them into memory.
"""
tile_collection = []
path_obj = Path(directory)
for file_entry in path_obj.iterdir():
if file_entry.is_file():
try:
with open(file_entry, "rb") as file_handle:
tile_img = Image.open(file_handle)
tile_img.load() # Force load before closing file
tile_collection.append(tile_img)
except (IOError, OSError):
continue
return tile_collection
Color Signature Extraction
Each tile and target cell requires a color fingerprint for matching. This implemantation calculates mean RGB values using NumPy operations:
def extract_mean_color(pil_image):
"""
Compute dominant color via mean RGB values.
"""
pixel_array = np.asarray(pil_image)
height, width, channels = pixel_array.shape
flattened = pixel_array.reshape(height * width, channels)
mean_values = np.mean(flattened, axis=0)
return tuple(mean_values.astype(int))
Grid Decomposition
Segment the target image into an M×N matrix of cells. Each cell will later be replaced by a matching tile:
def segment_into_cells(source_img, grid_dimensions):
"""
Divide source image into MxN segments.
"""
img_width, img_height = source_img.size
rows, cols = grid_dimensions
cell_width = img_width // cols
cell_height = img_height // rows
cell_list = []
for row_idx in range(rows):
for col_idx in range(cols):
left = col_idx * cell_width
upper = row_idx * cell_height
right = left + cell_width
lower = upper + cell_height
cell = source_img.crop((left, upper, right, lower))
cell_list.append(cell)
return cell_list
Optimal Tile Matching
Locate the tile with the closest color signature to each tarrget cell using Euclidean distance in RGB space:
def locate_optimal_tile(target_color, color_catalog):
"""
Find tile with minimum Euclidean distance in RGB space.
"""
best_idx = 0
min_distance = float('inf')
for idx, candidate_color in enumerate(color_catalog):
delta_r = candidate_color[0] - target_color[0]
delta_g = candidate_color[1] - target_color[1]
delta_b = candidate_color[2] - target_color[2]
distance = delta_r**2 + delta_g**2 + delta_b**2
if distance < min_distance:
min_distance = distance
best_idx = idx
return best_idx
Canvas Assembly
Arrange selected tiles into the final composite image grid:
def assemble_composite(tiles, layout):
"""
Arrange tiles into final mosaic canvas.
"""
rows, cols = layout
assert len(tiles) == rows * cols
tile_width = max(t.size[0] for t in tiles)
tile_height = max(t.size[1] for t in tiles)
canvas_width = cols * tile_width
canvas_height = rows * tile_height
canvas = Image.new('RGB', (canvas_width, canvas_height))
for pos, tile in enumerate(tiles):
row = pos // cols
col = pos % cols
x_offset = col * tile_width
y_offset = row * tile_height
canvas.paste(tile, (x_offset, y_offset))
return canvas
Mosaic Generation Pipeline
Orchestrate the complete workflow from input to output:
def generate_mosaic(target_path, tile_sources, subdivisions, allow_reuse=True):
"""
Main pipeline for mosaic generation.
"""
print("Decomposing target image...")
target_cells = segment_into_cells(target_path, subdivisions)
print("Analyzing color profiles...")
tile_signatures = [extract_mean_color(tile) for tile in tile_sources]
selected_tiles = []
total_cells = len(target_cells)
milestone = max(1, total_cells // 10)
for iteration, cell in enumerate(target_cells):
cell_signature = extract_mean_color(cell)
match_index = locate_optimal_tile(cell_signature, tile_signatures)
selected_tiles.append(tile_sources[match_index])
if not allow_reuse:
tile_sources.pop(match_index)
tile_signatures.pop(match_index)
if iteration % milestone == 0 and iteration > 0:
print(f"Progress: {iteration}/{total_cells}")
print("Compositing final image...")
return assemble_composite(selected_tiles, subdivisions)
Command Line Interface
Configure runtime parameters through command-line arguments:
argument_parser = argparse.ArgumentParser(description="Generate photo mosaics from image collections")
argument_parser.add_argument("--source", dest="target_image", required=True, help="Path to base image")
argument_parser.add_argument("--tiles", dest="tile_directory", required=True, help="Directory containing tile images")
argument_parser.add_argument("--grid", nargs=2, type=int, dest="grid_dims", required=True, help="Grid dimensions (rows cols)")
argument_parser.add_argument("--output", dest="output_path", default="mosaic.png", help="Output filename")
Dimension Normalization
Prevent output bloat by scaling tiles to match grid cell dimensions before processing:
print("Normalizing tile dimensions...")
grid_rows, grid_cols = grid_dimensions
target_width = target_img.size[0] // grid_cols
target_height = target_img.size[1] // grid_rows
max_tile_size = (target_width, target_height)
for tile in tile_collection:
tile.thumbnail(max_tile_size)