Spaces:
Sleeping
Sleeping
Download mosaic_generator/utils.py from lowwhit/image-mosaic-generator: direct link, hf CLI and curl.
- Browser
- Download file 11.6 kB
-
https://huggingface.co/spaces/lowwhit/image-mosaic-generator/resolve/main/mosaic_generator/utils.py
- Command line
-
hf download hf://spaces/lowwhit/image-mosaic-generator/mosaic_generator/utils.py
-
curl -L -o utils.py https://huggingface.co/spaces/lowwhit/image-mosaic-generator/resolve/main/mosaic_generator/utils.py
11.6 kB
| """ | |
| Helper utility functions for the mosaic generator. | |
| This module contains utility functions used across the mosaic generation pipeline, | |
| including asset generation and image cropping utilities. | |
| """ | |
| import os | |
| from typing import List, Tuple | |
| import numpy as np | |
| from PIL import Image | |
| from skimage import data | |
| from mosaic_generator.config import ASSETS_DIR | |
| class ValidationError(Exception): | |
| """Custom exception for validation errors with informative messages.""" | |
| pass | |
| def _save_skimage_samples() -> List[str]: | |
| """ | |
| Save sample images from skimage data module to assets directory. | |
| Returns: | |
| List[str]: List of file paths to the saved images. | |
| """ | |
| samples = [ | |
| (data.astronaut(), "astronaut.png"), | |
| (data.chelsea(), "chelsea_cat.png"), | |
| (data.coffee(), "coffee.png"), | |
| (data.rocket(), "rocket.png"), | |
| (data.camera(), "camera.png"), | |
| (data.text(), "text.png"), | |
| ] | |
| paths: List[str] = [] | |
| for arr, name in samples: | |
| img = Image.fromarray(arr) | |
| path = os.path.join(ASSETS_DIR, name) | |
| if not os.path.exists(path): | |
| img.save(path) | |
| paths.append(path) | |
| return paths | |
| def _make_mario_like_sprite(scale: int = 8) -> str: | |
| """ | |
| Create a Mario-like pixel sprite and save it. | |
| Uses a color palette and grid pattern to create a simple sprite image. | |
| Args: | |
| scale: Scaling factor for the sprite (default 8). | |
| Returns: | |
| str: Path to the saved sprite image. | |
| """ | |
| palette = { | |
| 0: (255, 255, 255), # white | |
| 1: (255, 205, 148), # skin | |
| 2: (200, 30, 30), # red | |
| 3: (40, 80, 200), # blue | |
| 4: (120, 70, 30), # brown | |
| 5: (10, 10, 10), # black | |
| 6: (240, 200, 60), # yellow | |
| } | |
| grid = np.array([ | |
| [0,0,0,0,0,2,2,2,2,0,0,0,0,0,0,0], | |
| [0,0,0,0,2,2,2,2,2,2,0,0,0,0,0,0], | |
| [0,0,0,4,4,1,1,1,1,4,4,0,0,0,0,0], | |
| [0,0,4,1,1,1,1,1,1,1,1,4,0,0,0,0], | |
| [0,0,4,1,5,1,1,1,1,5,1,4,0,0,0,0], | |
| [0,0,4,1,1,1,1,1,1,1,1,4,0,0,0,0], | |
| [0,0,0,4,4,1,1,1,1,4,4,0,0,0,0,0], | |
| [0,0,0,0,3,3,3,3,3,3,0,0,0,0,0,0], | |
| [0,0,0,3,3,3,3,3,3,3,3,0,0,0,0,0], | |
| [0,0,4,4,3,4,3,3,3,4,4,4,0,0,0,0], | |
| [0,4,4,4,4,4,4,4,4,4,4,4,4,0,0,0], | |
| [0,0,0,2,2,0,0,0,0,2,2,0,0,0,0,0], | |
| [0,0,2,2,2,0,0,0,0,2,2,2,0,0,0,0], | |
| [0,2,2,2,2,2,2,0,2,2,2,2,2,0,0,0], | |
| [0,2,2,0,0,2,2,2,2,0,0,2,2,0,0,0], | |
| [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], | |
| ], dtype=np.uint8) | |
| h, w = grid.shape | |
| rgb = np.zeros((h, w, 3), dtype=np.uint8) | |
| for k, col in palette.items(): | |
| rgb[grid == k] = col | |
| img = Image.fromarray(rgb).resize((w*scale, h*scale), resample=Image.NEAREST) | |
| path = os.path.join(ASSETS_DIR, "mario_like.png") | |
| if not os.path.exists(path): | |
| img.save(path) | |
| return path | |
| def _multi_crops(img: Image.Image, how_many: int = 4) -> List[Image.Image]: | |
| """ | |
| Extract multiple square crops from an image to create tile diversity. | |
| Crops from center and corners to get varied content. | |
| Args: | |
| img: Input PIL Image. | |
| how_many: Number of crops to return (max 4). | |
| Returns: | |
| List[Image.Image]: List of cropped PIL Images. | |
| """ | |
| w, h = img.size | |
| s = min(w, h) # Size of square crop | |
| return [ | |
| img.crop(((w-s)//2, (h-s)//2, (w+s)//2, (h+s)//2)), # center crop | |
| img.crop((0, 0, s, s)), # top-left corner | |
| img.crop((w-s, 0, w, s)), # top-right corner | |
| img.crop((0, h-s, s, h)), # bottom-left corner | |
| ][:how_many] | |
| def generate_assets() -> None: | |
| """ | |
| Generate all required assets (sample images and sprite). | |
| Creates the assets directory if it doesn't exist and generates | |
| all sample images and the Mario-like sprite. | |
| """ | |
| os.makedirs(ASSETS_DIR, exist_ok=True) | |
| _save_skimage_samples() | |
| _make_mario_like_sprite(scale=8) | |
| def validate_grid_cells(grid_cells: int, min_cells: int = 4, max_cells: int = 128) -> None: | |
| """ | |
| Validate grid cells parameter. | |
| Args: | |
| grid_cells: Number of grid cells per side. | |
| min_cells: Minimum allowed grid cells (default: 4). | |
| max_cells: Maximum allowed grid cells (default: 128). | |
| Raises: | |
| ValidationError: If grid_cells is invalid. | |
| """ | |
| if not isinstance(grid_cells, (int, float)): | |
| raise ValidationError( | |
| f"Grid cells must be a number, got {type(grid_cells).__name__}: {grid_cells}" | |
| ) | |
| grid_cells = int(grid_cells) | |
| if grid_cells < min_cells: | |
| raise ValidationError( | |
| f"Grid cells ({grid_cells}) must be at least {min_cells}. " | |
| f"Smaller values would create too few cells for a meaningful mosaic." | |
| ) | |
| if grid_cells > max_cells: | |
| raise ValidationError( | |
| f"Grid cells ({grid_cells}) exceeds maximum of {max_cells}. " | |
| f"Larger values may cause performance issues or memory errors." | |
| ) | |
| if grid_cells <= 0: | |
| raise ValidationError( | |
| f"Grid cells must be positive, got: {grid_cells}" | |
| ) | |
| def validate_tile_size(tile_size: int, min_size: int = 4, max_size: int = 256) -> None: | |
| """ | |
| Validate tile size parameter. | |
| Args: | |
| tile_size: Size of each tile in pixels. | |
| min_size: Minimum allowed tile size (default: 4). | |
| max_size: Maximum allowed tile size (default: 256). | |
| Raises: | |
| ValidationError: If tile_size is invalid. | |
| """ | |
| if not isinstance(tile_size, (int, float)): | |
| raise ValidationError( | |
| f"Tile size must be a number, got {type(tile_size).__name__}: {tile_size}" | |
| ) | |
| tile_size = int(tile_size) | |
| if tile_size < min_size: | |
| raise ValidationError( | |
| f"Tile size ({tile_size}px) must be at least {min_size}px. " | |
| f"Smaller tiles would be too small to display properly." | |
| ) | |
| if tile_size > max_size: | |
| raise ValidationError( | |
| f"Tile size ({tile_size}px) exceeds maximum of {max_size}px. " | |
| f"Larger tiles may cause performance issues or memory errors." | |
| ) | |
| if tile_size <= 0: | |
| raise ValidationError( | |
| f"Tile size must be positive, got: {tile_size}px" | |
| ) | |
| def validate_image_size(img: Image.Image, min_dimension: int = 32, max_dimension: int = 4096) -> Tuple[int, int]: | |
| """ | |
| Validate image dimensions. | |
| Args: | |
| img: PIL Image to validate. | |
| min_dimension: Minimum dimension (width or height) in pixels (default: 32). | |
| max_dimension: Maximum dimension (width or height) in pixels (default: 4096). | |
| Returns: | |
| Tuple[int, int]: Image width and height. | |
| Raises: | |
| ValidationError: If image dimensions are invalid. | |
| """ | |
| if img is None: | |
| raise ValidationError("Image cannot be None. Please provide a valid image.") | |
| if not isinstance(img, Image.Image): | |
| raise ValidationError( | |
| f"Expected PIL Image, got {type(img).__name__}" | |
| ) | |
| width, height = img.size | |
| if width <= 0 or height <= 0: | |
| raise ValidationError( | |
| f"Image dimensions must be positive, got: {width}x{height}px" | |
| ) | |
| min_side = min(width, height) | |
| max_side = max(width, height) | |
| if min_side < min_dimension: | |
| raise ValidationError( | |
| f"Image is too small: {width}x{height}px. " | |
| f"Minimum dimension must be at least {min_dimension}px. " | |
| f"Current smallest dimension: {min_side}px." | |
| ) | |
| if max_side > max_dimension: | |
| raise ValidationError( | |
| f"Image is too large: {width}x{height}px. " | |
| f"Maximum dimension must be at most {max_dimension}px. " | |
| f"Current largest dimension: {max_side}px. " | |
| f"Consider resizing the image before processing." | |
| ) | |
| return width, height | |
| def validate_image_grid_compatibility(img: Image.Image, grid_cells: int) -> None: | |
| """ | |
| Validate that image dimensions are compatible with grid size. | |
| Checks that image can be divided into grid cells without excessive cropping. | |
| Args: | |
| img: PIL Image to validate. | |
| grid_cells: Number of grid cells per side. | |
| Raises: | |
| ValidationError: If image and grid are incompatible. | |
| """ | |
| width, height = img.size | |
| # After preprocessing, image will be cropped to multiples of grid_cells | |
| # Check if the resulting size would be too small | |
| w_crop = (width // grid_cells) * grid_cells | |
| h_crop = (height // grid_cells) * grid_cells | |
| if w_crop < grid_cells or h_crop < grid_cells: | |
| raise ValidationError( | |
| f"Image dimensions ({width}x{height}px) are too small for grid size {grid_cells}x{grid_cells}. " | |
| f"After cropping to grid multiples, the image would be {w_crop}x{h_crop}px, " | |
| f"which is insufficient. Please use a larger image or smaller grid size." | |
| ) | |
| # Warn if too much of the image would be cropped | |
| crop_ratio_w = w_crop / width if width > 0 else 0 | |
| crop_ratio_h = h_crop / height if height > 0 else 0 | |
| if crop_ratio_w < 0.5 or crop_ratio_h < 0.5: | |
| raise ValidationError( | |
| f"Image dimensions ({width}x{height}px) are poorly aligned with grid size {grid_cells}x{grid_cells}. " | |
| f"More than 50% of the image would be cropped. " | |
| f"Consider adjusting the grid size or using a differently sized image." | |
| ) | |
| def validate_tile_paths(image_paths: List[str]) -> Tuple[List[str], List[str]]: | |
| """ | |
| Validate that tile image paths exist and return valid/missing paths. | |
| Args: | |
| image_paths: List of paths to tile images. | |
| Returns: | |
| Tuple[List[str], List[str]]: Tuple of (valid_paths, missing_paths). | |
| Raises: | |
| ValidationError: If no valid tile paths are found. | |
| """ | |
| if not image_paths: | |
| raise ValidationError( | |
| "No tile image paths provided. Cannot build mosaic without tiles." | |
| ) | |
| valid_paths = [] | |
| missing_paths = [] | |
| for path in image_paths: | |
| if not isinstance(path, str): | |
| raise ValidationError( | |
| f"Tile path must be a string, got {type(path).__name__}: {path}" | |
| ) | |
| if os.path.exists(path): | |
| # Verify it's actually an image file | |
| try: | |
| with Image.open(path) as test_img: | |
| test_img.verify() | |
| valid_paths.append(path) | |
| except Exception as e: | |
| raise ValidationError( | |
| f"Tile path exists but is not a valid image file: {path}. " | |
| f"Error: {str(e)}" | |
| ) | |
| else: | |
| missing_paths.append(path) | |
| if not valid_paths: | |
| raise ValidationError( | |
| f"None of the provided tile paths exist or are valid images. " | |
| f"Missing paths: {', '.join(missing_paths)}. " | |
| f"Please ensure tile images are available at the specified paths." | |
| ) | |
| if missing_paths: | |
| # Log warning but don't fail - we can work with partial tile set | |
| print(f"Warning: {len(missing_paths)} tile image(s) not found: {', '.join(missing_paths)}") | |
| print(f"Using {len(valid_paths)} available tile image(s).") | |
| return valid_paths, missing_paths | |