DetectionModelTraining/14_build_roi_compare_visuals.py
2026-03-17 22:20:31 +08:00

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()