#!/usr/bin/env python3 from __future__ import annotations import argparse from pathlib import Path from PIL import Image, ImageDraw, ImageFont def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Build side-by-side ROI prediction comparisons") parser.add_argument("--left-dir", required=True, help="Directory containing left prediction images") parser.add_argument("--right-dir", required=True, help="Directory containing right prediction images") parser.add_argument("--output-dir", required=True, help="Directory to write comparison images") parser.add_argument("--left-title", default="Left", help="Title shown above left image") parser.add_argument("--right-title", default="Right", help="Title shown above right image") return parser.parse_args() def load_font() -> ImageFont.ImageFont: for candidate in ("arial.ttf", "C:/Windows/Fonts/arial.ttf", "C:/Windows/Fonts/msyh.ttc"): try: return ImageFont.truetype(candidate, 22) except OSError: continue return ImageFont.load_default() def fit_height(image: Image.Image, target_height: int) -> Image.Image: if image.height == target_height: return image scale = target_height / image.height target_width = max(1, int(round(image.width * scale))) return image.resize((target_width, target_height), Image.Resampling.LANCZOS) def build_pair(left_path: Path, right_path: Path, output_path: Path, left_title: str, right_title: str, font: ImageFont.ImageFont) -> None: with Image.open(left_path) as left_image, Image.open(right_path) as right_image: left_rgb = left_image.convert("RGB") right_rgb = right_image.convert("RGB") target_height = max(left_rgb.height, right_rgb.height) left_rgb = fit_height(left_rgb, target_height) right_rgb = fit_height(right_rgb, target_height) gap = 20 padding = 16 title_band = 48 canvas_width = left_rgb.width + right_rgb.width + gap + (padding * 2) canvas_height = target_height + title_band + (padding * 2) canvas = Image.new("RGB", (canvas_width, canvas_height), color=(245, 243, 238)) draw = ImageDraw.Draw(canvas) left_x = padding right_x = padding + left_rgb.width + gap image_y = padding + title_band draw.text((left_x, padding), left_title, fill=(32, 32, 32), font=font) draw.text((right_x, padding), right_title, fill=(32, 32, 32), font=font) canvas.paste(left_rgb, (left_x, image_y)) canvas.paste(right_rgb, (right_x, image_y)) draw.rectangle( (left_x - 2, image_y - 2, left_x + left_rgb.width + 1, image_y + left_rgb.height + 1), outline=(160, 160, 160), width=2, ) draw.rectangle( (right_x - 2, image_y - 2, right_x + right_rgb.width + 1, image_y + right_rgb.height + 1), outline=(160, 160, 160), width=2, ) output_path.parent.mkdir(parents=True, exist_ok=True) canvas.save(output_path, quality=95) def main() -> None: args = parse_args() left_dir = Path(args.left_dir) right_dir = Path(args.right_dir) output_dir = Path(args.output_dir) output_dir.mkdir(parents=True, exist_ok=True) font = load_font() left_images = {path.name: path for path in left_dir.glob("*.jpg")} right_images = {path.name: path for path in right_dir.glob("*.jpg")} shared_names = sorted(set(left_images) & set(right_images)) for name in shared_names: build_pair( left_images[name], right_images[name], output_dir / name, args.left_title, args.right_title, font, ) summary_lines = [ f"left_dir={left_dir}", f"right_dir={right_dir}", f"pairs={len(shared_names)}", "", ] summary_lines.extend(shared_names) (output_dir / "summary.txt").write_text("\n".join(summary_lines) + "\n", encoding="utf-8") if __name__ == "__main__": main()