#!/usr/bin/env python3 """Tile labeled images into a contact sheet for side-by-side comparison. Usage: montage.py --out sheet.png --cols 3 [--scale 0.5] path:label path:label ... """ import argparse from PIL import Image, ImageDraw, ImageFont def load_font(size): for p in ("/System/Library/Fonts/Supplemental/Arial.ttf", "/System/Library/Fonts/Helvetica.ttc", "/Library/Fonts/Arial.ttf"): try: return ImageFont.truetype(p, size) except OSError: continue return ImageFont.load_default() def main(): ap = argparse.ArgumentParser() ap.add_argument("--out", required=True) ap.add_argument("--cols", type=int, default=3) ap.add_argument("--scale", type=float, default=1.0) ap.add_argument("--pad", type=int, default=8) ap.add_argument("--label-h", type=int, default=24, dest="label_h") ap.add_argument("items", nargs="+", help="each 'path:label'") a = ap.parse_args() imgs, labels = [], [] for it in a.items: path, _, lab = it.partition(":") imgs.append(Image.open(path).convert("RGB")) labels.append(lab) cw = int(max(i.width for i in imgs) * a.scale) ch = int(max(i.height for i in imgs) * a.scale) font = load_font(max(12, int(a.label_h * 0.7))) cols = a.cols rows = (len(imgs) + cols - 1) // cols cellw, cellh = cw + a.pad, ch + a.label_h + a.pad sheet = Image.new("RGB", (cols * cellw + a.pad, rows * cellh + a.pad), (18, 18, 20)) d = ImageDraw.Draw(sheet) for k, (im, lab) in enumerate(zip(imgs, labels)): r, c = divmod(k, cols) x, y = a.pad + c * cellw, a.pad + r * cellh d.text((x + 2, y + 4), lab, font=font, fill=(232, 232, 238)) sheet.paste(im.resize((cw, ch), Image.LANCZOS), (x, y + a.label_h)) sheet.save(a.out) print(f"wrote {a.out} ({sheet.width}x{sheet.height})") if __name__ == "__main__": main()