import argparse import re from pathlib import Path from PIL import Image, ImageDraw, ImageFont # --- Configuration --- PADDING = 40 FONT_SIZE = 48 FONT_COLOR = "black" ARROW_COLOR = "black" BACKGROUND_COLOR = "white" ARROW_WIDTH_RATIO = 0.3 ARROW_HEIGHT_RATIO = 0.1 def parse_filename(filepath: Path): """Extracts the step number and name from a filename.""" # Pattern for standard steps like '..._02_log_exposure_RGB.jpg' match = re.search(r'_(\d+)_([a-zA-Z0-9_]+?)_RGB\.', filepath.name) if match: step_name = match.group(2).replace('_', ' ').title() return int(match.group(1)), step_name # Fallback pattern for the first input image like '..._input_linear_sRGB.jpg' match_input = re.search(r'_input_([a-zA-Z0-9_]+?)_sRGB\.', filepath.name) if match_input: step_name = f"Input {match_input.group(1).replace('_', ' ').title()}" return 0, step_name # If no pattern matches, return a generic name return 999, filepath.stem.replace('_', ' ').title() def create_arrow(width: int, height: int, color: str) -> Image.Image: """Creates a right-pointing arrow image with a transparent background.""" arrow_img = Image.new("RGBA", (width, height), (0, 0, 0, 0)) draw = ImageDraw.Draw(arrow_img) shaft_width = width * 0.7 rect_start_y = (height // 2) - (height // 10) rect_height = max(1, height // 5) draw.rectangle([(0, rect_start_y), (shaft_width, rect_start_y + rect_height)], fill=color) draw.polygon([(shaft_width, 0), (width, height // 2), (shaft_width, height)], fill=color) return arrow_img def main(): parser = argparse.ArgumentParser( description="Create a visual pipeline of image processing steps with diffs.", formatter_class=argparse.RawTextHelpFormatter ) parser.add_argument("input_dir", type=str, help="Directory containing the input and diff images.") parser.add_argument("output_file", type=str, help="Path for the final combined image.") parser.add_argument( "--scale", type=float, default=0.25, help="Scale factor for the main pipeline images (e.g., 0.25 for 25%%). Default is 0.25." ) parser.add_argument( "--diff-scale", type=float, default=0.15, help="Scale factor for the smaller diff images. Default is 0.15." ) args = parser.parse_args() input_path = Path(args.input_dir) if not input_path.is_dir(): print(f"Error: Input directory '{args.input_dir}' not found.") return # 1. Find and sort all images # --- THIS IS THE FIX --- # Use a more general glob to find all .jpg files for the pipeline pipeline_images_raw = list(input_path.glob("*.jpg")) diff_images_raw = list(input_path.glob("diff_*_RGB.png")) if not pipeline_images_raw: print("Error: No pipeline images (*.jpg) found in the directory.") return # Sort files alphabetically by their full name, which mimics 'ls -1' behavior. pipeline_images_sorted = sorted(pipeline_images_raw) diff_images_sorted = sorted(diff_images_raw) print("Found and sorted the following pipeline images:") for p in pipeline_images_sorted: print(f" - {p.name}") # 2. Prepare images and assets with Image.open(pipeline_images_sorted[0]) as img: orig_w, orig_h = img.size img_w, img_h = int(orig_w * args.scale), int(orig_h * args.scale) diff_w, diff_h = int(orig_w * args.diff_scale), int(orig_h * args.scale) pipeline_images = [Image.open(p).resize((img_w, img_h), Image.Resampling.LANCZOS) for p in pipeline_images_sorted] diff_images = [Image.open(p).resize((diff_w, diff_h), Image.Resampling.LANCZOS) for p in diff_images_sorted] arrow_w, arrow_h = int(img_w * ARROW_WIDTH_RATIO), int(img_h * ARROW_HEIGHT_RATIO) arrow = create_arrow(arrow_w, arrow_h, ARROW_COLOR) try: font = ImageFont.truetype("arial.ttf", FONT_SIZE) except IOError: print("Arial font not found, using default font.") font = ImageFont.load_default() # 3. Calculate canvas size num_steps = len(pipeline_images) gap_width = max(arrow_w, diff_w) + PADDING total_width = (num_steps * img_w) + ((num_steps - 1) * gap_width) + (2 * PADDING) total_height = PADDING + FONT_SIZE + PADDING + img_h + PADDING + diff_h + PADDING # 4. Create the final canvas and draw everything canvas = Image.new("RGB", (total_width, total_height), BACKGROUND_COLOR) draw = ImageDraw.Draw(canvas) y_text = PADDING y_pipeline = y_text + FONT_SIZE + PADDING y_arrow = y_pipeline + (img_h // 2) - (arrow_h // 2) y_diff = y_pipeline + img_h + PADDING current_x = PADDING for i, p_img in enumerate(pipeline_images): canvas.paste(p_img, (current_x, y_pipeline)) _, step_name = parse_filename(pipeline_images_sorted[i]) if step_name: text_bbox = draw.textbbox((0, 0), step_name, font=font) text_w = text_bbox[2] - text_bbox[0] draw.text((current_x + (img_w - text_w) // 2, y_text), step_name, font=font, fill=FONT_COLOR) if i < num_steps - 1: gap_start_x = current_x + img_w arrow_x = gap_start_x + (gap_width - arrow_w) // 2 canvas.paste(arrow, (arrow_x, y_arrow), mask=arrow) if i < len(diff_images): diff_x = gap_start_x + (gap_width - diff_w) // 2 canvas.paste(diff_images[i], (diff_x, y_diff)) current_x += img_w + gap_width # 5. Save the final image canvas.save(args.output_file, quality=95) print(f"\nPipeline image successfully created at '{args.output_file}'") if __name__ == "__main__": main()