114 lines
3.9 KiB
Python
114 lines
3.9 KiB
Python
#!/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()
|