167 lines
8.8 KiB
Python
167 lines
8.8 KiB
Python
import argparse
|
|
import sys
|
|
from PIL import Image, ImageDraw
|
|
|
|
def create_comparison_poster(input_path1: str, input_path2: str, output_path: str):
|
|
"""
|
|
Generates a comparison poster from two high-resolution source images.
|
|
|
|
The poster features 50% resolution versions of both main images side-by-side at the top.
|
|
Below, it shows a complete 3x3 grid of "rule of thirds" patches. Each grid cell
|
|
contains a side-by-side comparison of the 250% zoomed patch from both images.
|
|
"""
|
|
try:
|
|
# Open the two high-resolution source images
|
|
with Image.open(input_path1) as original_image1, Image.open(input_path2) as original_image2:
|
|
original_image1 = original_image1.convert("RGB")
|
|
original_image2 = original_image2.convert("RGB")
|
|
|
|
# --- Ensure images are the same size for consistent cropping ---
|
|
orig_width, orig_height = original_image1.size
|
|
if original_image1.size != original_image2.size:
|
|
print(f"Warning: Image sizes differ. Resizing second image from {original_image2.size} to {original_image1.size}.")
|
|
original_image2 = original_image2.resize(original_image1.size, Image.Resampling.LANCZOS)
|
|
|
|
print(f"Loaded input image 1: {input_path1} ({orig_width}x{orig_height})")
|
|
print(f"Loaded input image 2: {input_path2} ({orig_width}x{orig_height})")
|
|
|
|
# --- 1. Define Sizes ---
|
|
|
|
# Main images are 50% of original
|
|
main_image_width = orig_width // 2
|
|
main_image_height = orig_height // 2
|
|
|
|
# Crop area for patches is 1/6th of original width
|
|
patch_crop_width = orig_width // 6
|
|
patch_crop_height = int(patch_crop_width * (orig_height / orig_width))
|
|
|
|
# Patches are zoomed to 250% of their cropped size
|
|
zoom_factor = 2.5
|
|
zoomed_patch_width = int(patch_crop_width * zoom_factor)
|
|
zoomed_patch_height = int(patch_crop_height * zoom_factor)
|
|
|
|
# Define padding values
|
|
comparison_padding = 10 # Padding between the two patches in a pair
|
|
patch_padding = 25 # Padding between patch pairs in the grid
|
|
section_padding = 50 # Padding between main images and the grid
|
|
canvas_padding = 50 # Padding around the entire content
|
|
|
|
# --- 2. Calculate Layout & Create Canvas ---
|
|
|
|
# Calculate the dimensions of the top section (two main images side-by-side)
|
|
top_section_width = (main_image_width * 2) + section_padding
|
|
|
|
# Calculate the dimensions of a single side-by-side comparison patch pair
|
|
comparison_pair_width = (zoomed_patch_width * 2) + comparison_padding
|
|
|
|
# Calculate the dimensions of the full 3x3 patch grid
|
|
grid_width = (comparison_pair_width * 3) + (patch_padding * 2)
|
|
grid_height = (zoomed_patch_height * 3) + (patch_padding * 2)
|
|
|
|
# Calculate total canvas dimensions
|
|
canvas_width = max(top_section_width, grid_width) + (canvas_padding * 2)
|
|
canvas_height = main_image_height + grid_height + section_padding + (canvas_padding * 2)
|
|
|
|
# Create the blank white canvas
|
|
canvas = Image.new('RGB', (canvas_width, canvas_height), 'white')
|
|
draw = ImageDraw.Draw(canvas)
|
|
print(f"Created poster canvas of size: {canvas_width}x{canvas_height}")
|
|
|
|
# --- 3. Place Main Images & Draw Highlights ---
|
|
|
|
# Create the 50% resolution main images
|
|
main_image1 = original_image1.resize((main_image_width, main_image_height), Image.Resampling.LANCZOS)
|
|
main_image2 = original_image2.resize((main_image_width, main_image_height), Image.Resampling.LANCZOS)
|
|
|
|
# Position and paste the main images in the top section
|
|
top_section_x_start = (canvas_width - top_section_width) // 2
|
|
main_image_y = canvas_padding
|
|
main_image1_x = top_section_x_start
|
|
main_image2_x = top_section_x_start + main_image_width + section_padding
|
|
|
|
canvas.paste(main_image1, (main_image1_x, main_image_y))
|
|
canvas.paste(main_image2, (main_image2_x, main_image_y))
|
|
|
|
# Define the center points for the Rule of Thirds grid on the ORIGINAL image
|
|
thirds_points = [
|
|
(orig_width // 6, orig_height // 6), (orig_width // 2, orig_height // 6), (5 * orig_width // 6, orig_height // 6),
|
|
(orig_width // 6, orig_height // 2), (orig_width // 2, orig_height // 2), (5 * orig_width // 6, orig_height // 2),
|
|
(orig_width // 6, 5 * orig_height // 6), (orig_width // 2, 5 * orig_height // 6), (5 * orig_width // 6, 5 * orig_height // 6),
|
|
]
|
|
|
|
# Draw all 9 highlight boxes on BOTH main images
|
|
print("Drawing highlight boxes on main images...")
|
|
for main_img_x_offset in [main_image1_x, main_image2_x]:
|
|
for center_x, center_y in thirds_points:
|
|
crop_left = center_x - (patch_crop_width // 2)
|
|
crop_top = center_y - (patch_crop_height // 2)
|
|
# Scale coordinates to the 50% main image and add its offset
|
|
hl_x1 = main_img_x_offset + int(crop_left * 0.5)
|
|
hl_y1 = main_image_y + int(crop_top * 0.5)
|
|
hl_x2 = hl_x1 + int(patch_crop_width * 0.5)
|
|
hl_y2 = hl_y1 + int(patch_crop_height * 0.5)
|
|
draw.rectangle((hl_x1, hl_y1, hl_x2, hl_y2), outline="red", width=4)
|
|
|
|
# --- 4. Create and Place all 9 Comparison Patch Pairs in a Grid ---
|
|
|
|
print("Generating and placing 9 zoomed patch pairs in a grid...")
|
|
grid_origin_x = (canvas_width - grid_width) // 2
|
|
grid_origin_y = main_image_y + main_image_height + section_padding
|
|
|
|
for i, (center_x, center_y) in enumerate(thirds_points):
|
|
# Define the crop box on the ORIGINAL images (it's the same for both)
|
|
crop_box = (
|
|
center_x - patch_crop_width // 2, center_y - patch_crop_height // 2,
|
|
center_x + patch_crop_width // 2, center_y + patch_crop_height // 2
|
|
)
|
|
|
|
# Crop the patch from each image and zoom it
|
|
patch1 = original_image1.crop(crop_box)
|
|
zoomed_patch1 = patch1.resize((zoomed_patch_width, zoomed_patch_height), Image.Resampling.LANCZOS)
|
|
|
|
patch2 = original_image2.crop(crop_box)
|
|
zoomed_patch2 = patch2.resize((zoomed_patch_width, zoomed_patch_height), Image.Resampling.LANCZOS)
|
|
|
|
# Determine the patch pair's position in the 3x3 grid
|
|
row, col = divmod(i, 3)
|
|
pair_x_start = grid_origin_x + col * (comparison_pair_width + patch_padding)
|
|
patch_y = grid_origin_y + row * (zoomed_patch_height + patch_padding)
|
|
|
|
# Calculate individual patch coordinates within the pair
|
|
patch1_x = pair_x_start
|
|
patch2_x = pair_x_start + zoomed_patch_width + comparison_padding
|
|
|
|
# Paste the patches and draw borders
|
|
canvas.paste(zoomed_patch1, (patch1_x, patch_y))
|
|
draw.rectangle((patch1_x, patch_y, patch1_x + zoomed_patch_width, patch_y + zoomed_patch_height), outline="black", width=2)
|
|
|
|
canvas.paste(zoomed_patch2, (patch2_x, patch_y))
|
|
draw.rectangle((patch2_x, patch_y, patch2_x + zoomed_patch_width, patch_y + zoomed_patch_height), outline="black", width=2)
|
|
|
|
# --- 5. Save the Final Poster ---
|
|
canvas.save(output_path, quality=95)
|
|
print(f"\nSuccess! Comparison poster saved to: {output_path}")
|
|
|
|
except FileNotFoundError as e:
|
|
print(f"Error: An input file was not found. Details: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
print(f"An unexpected error occurred: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(
|
|
description="Create a side-by-side comparison poster from two images, featuring main views and a 3x3 grid of zoomed-in 'rule of thirds' patches.",
|
|
formatter_class=argparse.RawTextHelpFormatter,
|
|
epilog="Example:\n python create_comparison_poster.py image_A.jpg image_B.jpg comparison_result.png"
|
|
)
|
|
parser.add_argument("input1", help="Path to the first high-resolution input image (Image A).")
|
|
parser.add_argument("input2", help="Path to the second high-resolution input image (Image B).")
|
|
parser.add_argument("output", help="Path for the generated comparison poster image.")
|
|
|
|
if len(sys.argv) < 4:
|
|
parser.print_help(sys.stderr)
|
|
sys.exit(1)
|
|
|
|
args = parser.parse_args()
|
|
create_comparison_poster(args.input1, args.input2, args.output) |