| | import base64
|
| | from io import BytesIO
|
| | from PIL import Image, ImageChops
|
| | from PIL import ImageDraw
|
| | import math
|
| |
|
| | class ImageUtils:
|
| | def __init__(self):
|
| | pass
|
| |
|
| | @staticmethod
|
| | def crop_base64(base64_string, output_format='PNG') -> str:
|
| | """
|
| | Takes a base64 encoded image, crops it by removing uniform background,
|
| | and returns the cropped image as base64.
|
| |
|
| | Args:
|
| | base64_string (str or bytes): Base64 encoded image string or raw bytes
|
| | output_format (str): Output image format ('PNG', 'JPEG', etc.)
|
| |
|
| | Returns:
|
| | str: Base64 encoded cropped image, or empty string if cropping fails
|
| | """
|
| | try:
|
| |
|
| | if isinstance(base64_string, bytes):
|
| |
|
| | image_data = base64_string
|
| | else:
|
| |
|
| | image_data = base64.b64decode(base64_string)
|
| |
|
| | im = Image.open(BytesIO(image_data))
|
| |
|
| |
|
| | bg = Image.new(im.mode, im.size, im.getpixel((0,0)))
|
| | diff = ImageChops.difference(im, bg)
|
| | diff = ImageChops.add(diff, diff, 2.0, -100)
|
| | bbox = diff.getbbox()
|
| |
|
| | if bbox:
|
| | cropped_im = im.crop(bbox)
|
| | else:
|
| | cropped_im = im
|
| |
|
| |
|
| | buffer = BytesIO()
|
| | cropped_im.save(buffer, format=output_format)
|
| | cropped_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
| |
|
| | return cropped_base64
|
| |
|
| | except Exception as e:
|
| | print(f"Error processing image: {e}")
|
| | return ""
|
| |
|
| | @staticmethod
|
| | def crop_image(im: Image.Image) -> Image.Image:
|
| | """
|
| | Original trim function for PIL Image objects
|
| | """
|
| | try:
|
| | bg = Image.new(im.mode, im.size, im.getpixel((0,0)))
|
| | diff = ImageChops.difference(im, bg)
|
| | diff = ImageChops.add(diff, diff, 2.0, -100)
|
| | bbox = diff.getbbox()
|
| | if bbox:
|
| | return im.crop(bbox)
|
| | return im
|
| | except Exception as e:
|
| | print(f"Error cropping image: {e}")
|
| | return im
|
| |
|
| | @staticmethod
|
| | def draw_bounding_boxes(pil_image: Image.Image, boxes: list[tuple[int, int, int, int]], color: str = "red", width: int = 2) -> Image.Image:
|
| | """
|
| | Draw bounding boxes on a PIL image.
|
| |
|
| | Args:
|
| | pil_image: A PIL.Image instance.
|
| | boxes: A list of boxes, each specified as (x1, y1, x2, y2).
|
| | color: The color for the bounding box outline.
|
| | width: The width of the bounding box line.
|
| |
|
| | Returns:
|
| | The PIL.Image with drawn bounding boxes.
|
| | """
|
| | try:
|
| | draw = ImageDraw.Draw(pil_image)
|
| | for box in boxes:
|
| | draw.rectangle(box, outline=color, width=width)
|
| | return pil_image
|
| | except Exception as e:
|
| | print(f"Error drawing bounding boxes: {e}")
|
| | return pil_image
|
| |
|
| | @staticmethod
|
| | def standardize_image_size(image: Image.Image, target_size: tuple = (1200, 1600), maintain_aspect_ratio: bool = True) -> Image.Image:
|
| | """
|
| | Resize image to target size while optionally maintaining aspect ratio.
|
| |
|
| | Args:
|
| | image: PIL Image to resize
|
| | target_size: Target (width, height) in pixels
|
| | maintain_aspect_ratio: If True, fit within target size while maintaining aspect ratio
|
| |
|
| | Returns:
|
| | Resized PIL Image
|
| | """
|
| | if maintain_aspect_ratio:
|
| |
|
| | img_ratio = image.width / image.height
|
| | target_ratio = target_size[0] / target_size[1]
|
| |
|
| | if img_ratio > target_ratio:
|
| |
|
| | new_width = target_size[0]
|
| | new_height = int(target_size[0] / img_ratio)
|
| | else:
|
| |
|
| | new_height = target_size[1]
|
| | new_width = int(target_size[1] * img_ratio)
|
| |
|
| |
|
| | resized_image = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
| |
|
| |
|
| | final_image = Image.new('RGB', target_size, 'white')
|
| |
|
| |
|
| | x_offset = (target_size[0] - new_width) // 2
|
| | y_offset = (target_size[1] - new_height) // 2
|
| |
|
| |
|
| | final_image.paste(resized_image, (x_offset, y_offset))
|
| |
|
| | return final_image
|
| | else:
|
| |
|
| | return image.resize(target_size, Image.Resampling.LANCZOS)
|
| |
|
| | @staticmethod
|
| | def optimize_image_quality(image: Image.Image, max_size_bytes: int = 1024 * 1024, initial_quality: int = 95) -> tuple[Image.Image, int]:
|
| | """
|
| | Optimize image quality to fit within specified file size limit.
|
| |
|
| | Args:
|
| | image: PIL Image to optimize
|
| | max_size_bytes: Maximum file size in bytes (default 1MB)
|
| | initial_quality: Starting quality (1-100) - not used for PNG but kept for compatibility
|
| |
|
| | Returns:
|
| | Tuple of (optimized_image, final_quality)
|
| | """
|
| |
|
| |
|
| | compression_levels = [0, 1, 3, 5, 7, 9]
|
| |
|
| | for compression in compression_levels:
|
| |
|
| | buffer = BytesIO()
|
| | image.save(buffer, format='PNG', optimize=True, compress_level=compression)
|
| | current_size = buffer.tell()
|
| |
|
| |
|
| | if current_size <= max_size_bytes:
|
| |
|
| | buffer.seek(0)
|
| | optimized_image = Image.open(buffer)
|
| | return optimized_image, 95
|
| |
|
| |
|
| | buffer = BytesIO()
|
| | image.save(buffer, format='PNG', optimize=True, compress_level=9)
|
| | buffer.seek(0)
|
| | optimized_image = Image.open(buffer)
|
| | return optimized_image, 50
|
| |
|
| | @staticmethod
|
| | def process_image_for_comparison(image: Image.Image, target_size: tuple = (1200, 1600), max_size_bytes: int = 1024 * 1024) -> tuple[Image.Image, int, int]:
|
| | """
|
| | Process image for comparison: standardize size and optimize quality.
|
| |
|
| | Args:
|
| | image: PIL Image to process
|
| | target_size: Target size in pixels (width, height)
|
| | max_size_bytes: Maximum file size in bytes (default 1MB)
|
| |
|
| | Returns:
|
| | Tuple of (processed_image, final_quality, file_size_bytes)
|
| | """
|
| |
|
| | sized_image = ImageUtils.standardize_image_size(image, target_size, maintain_aspect_ratio=True)
|
| |
|
| |
|
| | optimized_image, quality = ImageUtils.optimize_image_quality(sized_image, max_size_bytes)
|
| |
|
| |
|
| | buffer = BytesIO()
|
| | optimized_image.save(buffer, format='PNG', optimize=True)
|
| | file_size = buffer.tell()
|
| |
|
| | return optimized_image, quality, file_size
|
| |
|
| | @staticmethod
|
| | def image_to_base64_optimized(image: Image.Image, target_size: tuple = (1200, 1600), max_size_bytes: int = 1024 * 1024) -> str:
|
| | """
|
| | Convert image to base64 with size and quality optimization.
|
| |
|
| | Args:
|
| | image: PIL Image to convert
|
| | target_size: Target size in pixels (width, height)
|
| | max_size_bytes: Maximum file size in bytes (default 1MB)
|
| |
|
| | Returns:
|
| | Base64 encoded string of the optimized image
|
| | """
|
| | processed_image, quality, file_size = ImageUtils.process_image_for_comparison(
|
| | image, target_size, max_size_bytes
|
| | )
|
| |
|
| |
|
| | buffer = BytesIO()
|
| | processed_image.save(buffer, format='PNG', optimize=True)
|
| | image_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
| |
|
| | return image_base64 |