Image Merger Grid
Description
Image Merger Grid is a simple Python desktop application that lets you batch‑merge images into customizable grid layouts (e.g., 2×2, 4×4, or any rows×columns) with adjustable margins and background colors. Built with Tkinter and Pillow, it automatically processes all images in a folder, pads incomplete grids with blanks, and shows real‑time progress.

Usage
- Install dependencies:
pip install pillow
- Launch the application:
python grid.py
- In the GUI:
- Browse: Select the folder containing your images.
- Rows / Columns: Enter the number of rows and columns for each merged image.
- Margin (px): Set the padding between images.
- Background: Pick a background color for the canvas and padding.
- Merge Images: Click to start. The app will display how many output images will be created and show a progress bar.
- Find the merged output files (
merged_<rows>x<cols>_<batch>.png
) in the selected folder.
Python
Image Merger Grid (0 downloads )
import os
import glob
import math
import tkinter as tk
from tkinter import filedialog, messagebox, colorchooser
from tkinter import ttk
from PIL import Image
# Handle compatibility for Pillow versions that removed Image.ANTIALIAS
try:
resample_filter = Image.Resampling.LANCZOS
except AttributeError:
resample_filter = Image.LANCZOS # older Pillow
class ImageMergerApp:
def __init__(self, master):
self.master = master
master.title("Image Merger Grid")
# Variables
self.folder_path = tk.StringVar()
self.rows_var = tk.StringVar(value="2")
self.cols_var = tk.StringVar(value="2")
self.margin_var = tk.StringVar(value="0")
self.bgcolor = "#FFFFFF"
self.info_text = tk.StringVar(value="No folder selected")
# Layout
frm = ttk.Frame(master, padding=10)
frm.grid(row=0, column=0, sticky="nsew")
master.rowconfigure(0, weight=1)
master.columnconfigure(0, weight=1)
# Row 0: Folder
ttk.Label(frm, text="Source Folder:").grid(row=0, column=0, sticky="e")
ttk.Entry(frm, textvariable=self.folder_path, width=40).grid(row=0, column=1, sticky="we", padx=5)
ttk.Button(frm, text="Browse…", command=self.browse_folder).grid(row=0, column=2)
# Row 1: Rows/Cols
ttk.Label(frm, text="Rows:").grid(row=1, column=0, sticky="e", pady=5)
ttk.Entry(frm, textvariable=self.rows_var, width=5).grid(row=1, column=1, sticky="w")
ttk.Label(frm, text="Columns:").grid(row=1, column=1, sticky="e", padx=(60,0))
ttk.Entry(frm, textvariable=self.cols_var, width=5).grid(row=1, column=2, sticky="w")
# Row 2: Margin
ttk.Label(frm, text="Margin (px):").grid(row=2, column=0, sticky="e", pady=5)
ttk.Entry(frm, textvariable=self.margin_var, width=5).grid(row=2, column=1, sticky="w")
# Row 3: Background color
ttk.Label(frm, text="Background:").grid(row=3, column=0, sticky="e", pady=5)
self.color_btn = ttk.Button(frm, text="Choose Color", command=self.choose_color)
self.color_btn.grid(row=3, column=1, sticky="w")
# Row 4: Info label
self.info_label = ttk.Label(frm, textvariable=self.info_text, foreground="blue")
self.info_label.grid(row=4, column=0, columnspan=3, pady=(10,5))
# Row 5: Merge button
ttk.Button(frm, text="Merge Images", command=self.merge_images).grid(
row=5, column=0, columnspan=3, pady=(5,10)
)
# Row 6: Progress bar
self.progress = ttk.Progressbar(frm, mode="determinate")
self.progress.grid(row=6, column=0, columnspan=3, sticky="we")
# Trace changes
self.rows_var.trace_add("write", lambda *a: self.update_info())
self.cols_var.trace_add("write", lambda *a: self.update_info())
self.margin_var.trace_add("write", lambda *a: self.update_info())
def browse_folder(self):
folder = filedialog.askdirectory()
if folder:
self.folder_path.set(folder)
self.update_info()
def choose_color(self):
c = colorchooser.askcolor(title="Select background color", color=self.bgcolor)
if c and c[1]:
self.bgcolor = c[1]
self.color_btn.config(text=self.bgcolor)
def update_info(self):
folder = self.folder_path.get().strip()
if not folder or not os.path.isdir(folder):
self.info_text.set("No folder selected")
return
try:
rows = int(self.rows_var.get())
cols = int(self.cols_var.get())
margin = int(self.margin_var.get())
except ValueError:
self.info_text.set("Rows, Columns, Margin must be integers")
return
patterns = ("*.png", "*.jpg", "*.jpeg", "*.bmp", "*.gif")
files = []
for pat in patterns:
files.extend(glob.glob(os.path.join(folder, pat)))
files.sort()
total = len(files)
needed = rows * cols
batches = math.ceil(total / needed) if needed > 0 else 0
self.info_text.set(
f"Found {total} image(s), grid {rows}×{cols}, margin {margin}px → {batches} output image(s)"
)
def merge_images(self):
folder = self.folder_path.get().strip()
try:
rows = int(self.rows_var.get())
cols = int(self.cols_var.get())
margin = int(self.margin_var.get())
except ValueError:
messagebox.showerror("Invalid input", "Rows, Columns, and Margin must be integers.")
return
if not folder or not os.path.isdir(folder):
messagebox.showerror("Folder error", "Please select a valid source folder.")
return
# Gather images
patterns = ("*.png", "*.jpg", "*.jpeg", "*.bmp", "*.gif")
files = []
for pat in patterns:
files.extend(glob.glob(os.path.join(folder, pat)))
files.sort()
total = len(files)
if total == 0:
messagebox.showerror("No images", "No images found in the folder.")
return
needed = rows * cols
batch_count = math.ceil(total / needed)
# Configure progress bar
self.progress['maximum'] = batch_count
self.progress['value'] = 0
for batch_idx in range(batch_count):
batch = files[batch_idx*needed : (batch_idx+1)*needed]
imgs = [Image.open(fp) for fp in batch]
# Determine target tile size
widths, heights = zip(*(im.size for im in imgs)) if imgs else ((100,), (100,))
tw, th = min(widths), min(heights)
resized = [im.resize((tw, th), resample_filter) for im in imgs]
# Pad last batch with blanks
missing = needed - len(resized)
if missing > 0:
blank = Image.new('RGB', (tw, th), color=self.bgcolor)
resized.extend([blank]*missing)
# Canvas size: account for margins
mw = cols*tw + (cols+1)*margin
mh = rows*th + (rows+1)*margin
merged = Image.new('RGB', (mw, mh), color=self.bgcolor)
# Paste tiles
idx = 0
for r in range(rows):
for c in range(cols):
x = margin + c*(tw + margin)
y = margin + r*(th + margin)
merged.paste(resized[idx], (x, y))
idx += 1
# Save
out = os.path.join(folder, f"merged_{rows}x{cols}_{batch_idx+1}.png")
merged.save(out)
# Update progress bar
self.progress['value'] += 1
self.master.update_idletasks()
messagebox.showinfo("Done", f"Created {batch_count} merged image(s) in:\n{folder}")
def main():
root = tk.Tk()
ImageMergerApp(root)
root.mainloop()
if __name__ == "__main__":
main()