Lohith Venkat Chamakura
Update app
0c3c4c4
Raw History Blame Contribute Delete
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