#!/usr/bin/env python3 """Draw a polygon on an image using Pillow.""" import argparse import sys from pathlib import Path try: from PIL import Image, ImageDraw, ImageFilter except ImportError: print("Error: Pillow is required. Install with: pip install Pillow") sys.exit(1) COLOR_PALETTE = { "red": "#FF0000", "green": "#00FF00", "blue": "#0000FF", "yellow": "#FFFF00", "cyan": "#00FFFF", "magenta": "#FF00FF", "white": "#FFFFFF", "black": "#000000", "orange": "#FFA500", "purple": "#800080", "brown": "#A52A2A", "pink": "#FFC0CB", "gray": "#808080", "grey": "#808080", "lime": "#00FF00", "navy": "#000080", "teal": "#008080", "maroon": "#800000", } def parse_color(color_str: str) -> str: """Parse color string to hex.""" color_lower = color_str.lower() if color_lower in COLOR_PALETTE: return COLOR_PALETTE[color_lower] if color_str.startswith("#") and len(color_str) in [7, 9]: return color_str raise ValueError( f"Invalid color '{color_str}'. Use a name (e.g., red, blue) or hex code (e.g., #FF0000)." ) def parse_points(points_str: str) -> list[tuple[float, float]]: """Parse points string into list of (x, y) tuples.""" points = [] for point in points_str.split(): if "," not in point: raise ValueError(f"Invalid point format: '{point}'. Use 'x,y' format.") try: x, y = point.split(",") points.append((float(x.strip()), float(y.strip()))) except ValueError: raise ValueError(f"Invalid point format: '{point}'. Use 'x,y' format.") return points def convert_to_pixels( points: list[tuple[float, float]], width: int, height: int, absolute: bool ) -> list[tuple[int, int]]: """Convert points to pixel coordinates.""" if absolute: return [(int(x), int(y)) for x, y in points] else: return [ (int(x * width), int(y * height)) for x, y in points if 0 <= x <= 1 and 0 <= y <= 1 ] def draw_polygon_on_image( image_path: Path, points: list[tuple[float, float]], color: str, thickness: int, fill: bool, absolute: bool, save_path: Path | None, ) -> None: """Draw polygon on image.""" image = Image.open(image_path).convert("RGBA") width, height = image.size pixel_points = convert_to_pixels(points, width, height, absolute) if len(pixel_points) < 3: raise ValueError("Need at least 3 points to draw a polygon.") draw = ImageDraw.Draw(image) hex_color = parse_color(color) draw.polygon(pixel_points, fill=hex_color if fill else None, outline=hex_color, width=thickness) if fill: alpha = 128 fill_color = hex_color[:7] + f"{alpha:02X}" draw.polygon(pixel_points, fill=fill_color, outline=hex_color, width=thickness) if save_path: image.save(save_path) print(f"Saved: {save_path}") else: image.show() def main(): parser = argparse.ArgumentParser( description="Draw a polygon on an image using Pillow.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: # Draw a triangle using percentages (default) python draw_polygon.py scene.png "0.5,0.1 0.9,0.9 0.1,0.9" # Draw with pixel coordinates (absolute mode) python draw_polygon.py scene.png "100,50 400,50 250,300" --absolute # Save to file with blue color python draw_polygon.py scene.png "0.2,0.2 0.8,0.2 0.8,0.8 0.2,0.8" --color blue --save output.png # Fill polygon semi-transparently python draw_polygon.py scene.png "0.3,0.3 0.7,0.3 0.7,0.7 0.3,0.7" --fill --color red # Draw with custom hex color and thickness python draw_polygon.py scene.png "0.1,0.1 0.9,0.1 0.9,0.9 0.1,0.9" --color #00FF00 --thickness 5 Coordinate Formats: Percentage (default): 0.0 to 1.0, where 0.0,0.0 is top-left and 1.0,1.0 is bottom-right Absolute (with --absolute): Actual pixel coordinates from 0 to image width/height Color Formats: Named colors: red, green, blue, yellow, cyan, magenta, white, black, orange, purple, brown, pink, gray, grey, lime, navy, teal, maroon Hex codes: #FF0000, #00FF00FF, etc. """, ) parser.add_argument( "image", type=Path, help="Path to input image file", ) parser.add_argument( "points", type=str, help="Space-separated list of x,y coordinates (e.g., '0.1,0.1 0.9,0.1 0.5,0.9'). Use percentages (0.0-1.0) by default, or --absolute for pixel coordinates.", ) parser.add_argument( "--color", type=str, default="red", help="Polygon color (default: red). Use named colors (red, blue, green, etc.) or hex codes (#FF0000).", ) parser.add_argument( "--absolute", action="store_true", help="Use pixel coordinates instead of percentages", ) parser.add_argument( "--save", type=Path, default=None, help="Save output to file path instead of displaying", ) parser.add_argument( "--thickness", type=int, default=2, help="Line thickness in pixels (default: 2)", ) parser.add_argument( "--fill", action="store_true", help="Fill polygon with semi-transparent color", ) args = parser.parse_args() if not args.image.exists(): print(f"Error: Image not found: {args.image}", file=sys.stderr) sys.exit(1) try: points = parse_points(args.points) if len(points) < 3: print(f"Error: Need at least 3 points. Got {len(points)}.", file=sys.stderr) sys.exit(1) draw_polygon_on_image( image_path=args.image, points=points, color=args.color, thickness=args.thickness, fill=args.fill, absolute=args.absolute, save_path=args.save, ) except ValueError as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) except Exception as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) if __name__ == "__main__": main()