Spaces:
Sleeping
Sleeping
File size: 5,876 Bytes
8edf546 6d232bb e36ad66 388efcc e36ad66 6d232bb 388efcc 1933832 6d232bb 388efcc 8edf546 388efcc e36ad66 388efcc e36ad66 388efcc e36ad66 6d232bb e36ad66 388efcc e36ad66 8edf546 388efcc 8edf546 e36ad66 6d232bb e36ad66 6d232bb e36ad66 8edf546 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | import numpy as np
import os
from PIL import Image
import cv2
from typing import Dict, Tuple
from logic.validation import ValidationError, ensure_file_exists
_TILE_SOURCE_CACHE: Dict[str, list] = {}
_TILE_RESIZE_CACHE: Dict[Tuple[str, int], Tuple[np.ndarray, np.ndarray]] = {}
def calculate_average_color(image: np.ndarray) -> np.ndarray:
"""Calculate the average BGR color of an image.
Args:
image (np.ndarray): Tile image in BGR format.
Returns:
np.ndarray: Average color ``[b, g, r]``.
"""
return np.mean(image.reshape(-1, 3), axis=0)
def color_distance(color1: np.ndarray, color2: np.ndarray) -> float:
"""Return the Euclidean distance between two colors.
Args:
color1 (np.ndarray): First color vector.
color2 (np.ndarray): Second color vector.
Returns:
float: Euclidean distance.
"""
return float(np.linalg.norm(color1 - color2))
def _ensure_tile_sources(tile_dir: str):
"""Load base BGR tiles from disk once per directory.
Args:
tile_dir (str): Directory containing tile imagery.
Returns:
list[np.ndarray]: List of base-resolution BGR tiles.
"""
if tile_dir in _TILE_SOURCE_CACHE:
return _TILE_SOURCE_CACHE[tile_dir]
base_tiles = []
ensure_file_exists(tile_dir, "Tile directory")
for f in sorted(os.listdir(tile_dir)):
if not f.lower().endswith(('.png', '.jpg', '.jpeg', '.png', '.webp', '.bmp')):
continue
path = os.path.join(tile_dir, f)
try:
img = Image.open(path).convert('RGB')
tile_array = np.array(img)
tile_bgr = cv2.cvtColor(tile_array, cv2.COLOR_RGB2BGR)
base_tiles.append(tile_bgr)
except Exception as exc:
print(f"Error loading {path}: {exc}")
continue
_TILE_SOURCE_CACHE[tile_dir] = base_tiles
return base_tiles
def _ensure_resized_tiles(tile_dir: str, cell_size: int):
"""Resize cached tiles to a particular cell size.
Args:
tile_dir (str): Source directory.
cell_size (int): Desired tile size.
Returns:
tuple[np.ndarray, np.ndarray]: Resized tiles and their colors.
"""
key = (tile_dir, cell_size)
if key in _TILE_RESIZE_CACHE:
return _TILE_RESIZE_CACHE[key]
base_tiles = _ensure_tile_sources(tile_dir)
resized_tiles = []
tile_colors = []
for tile in base_tiles:
interpolation = cv2.INTER_AREA if tile.shape[0] >= cell_size else cv2.INTER_CUBIC
resized = cv2.resize(tile, (cell_size, cell_size), interpolation=interpolation)
resized_tiles.append(resized)
tile_colors.append(calculate_average_color(resized))
if not resized_tiles:
# fallback synthetic tile if directory empty
color = np.array([128, 128, 128], dtype=np.uint8)
dummy = np.full((cell_size, cell_size, 3), color, dtype=np.uint8)
resized_tiles.append(dummy)
tile_colors.append(color.astype(float))
cache_value = (np.stack(resized_tiles, axis=0), np.vstack(tile_colors))
_TILE_RESIZE_CACHE[key] = cache_value
return cache_value
def load_tile_images(tile_dir: str, n_colors: int, cell_size: int):
"""Load tiles, raising informative errors when assets are missing.
Args:
tile_dir (str): Directory containing tile images.
n_colors (int): Number of representative colors required.
cell_size (int): Target cell dimension in pixels.
Returns:
tuple[np.ndarray, np.ndarray]: Arrays of tiles and their average colors.
Raises:
ValidationError: If no tiles could be produced.
"""
tiles_arr, tile_colors = _ensure_resized_tiles(tile_dir, cell_size)
if len(tiles_arr) < n_colors:
deficit = n_colors - len(tiles_arr)
if deficit > 0:
random_tiles = []
random_colors = []
for _ in range(deficit):
color = np.random.randint(0, 255, 3, dtype=np.uint8)
tile = np.full((cell_size, cell_size, 3), color, dtype=np.uint8)
random_tiles.append(tile)
random_colors.append(color.astype(float))
tiles_arr = np.concatenate([tiles_arr, np.stack(random_tiles)], axis=0)
tile_colors = np.concatenate([tile_colors, np.vstack(random_colors)], axis=0)
return tiles_arr, tile_colors
def map_tiles_to_grid(
grid_labels: np.ndarray,
tiles: np.ndarray,
cell_size: int,
color_centers: np.ndarray | None = None,
tile_colors: np.ndarray | None = None,
) -> np.ndarray:
"""Assemble the mosaic image by replacing each cell with a tile.
Args:
grid_labels (np.ndarray): ``grid_size x grid_size`` indices.
tiles (np.ndarray): Stack of tile images.
cell_size (int): Tile dimension in pixels.
color_centers (np.ndarray | None): Optional color palette for matching.
tile_colors (np.ndarray | None): Average colors per tile.
Returns:
np.ndarray: Mosaic image in BGR format.
"""
grid_size = grid_labels.shape[0]
tiles_arr = np.asarray(tiles)
if color_centers is not None and tile_colors is not None:
color_centers = np.asarray(color_centers)
tile_colors = np.asarray(tile_colors)
distances = np.linalg.norm(
color_centers[:, np.newaxis, :] - tile_colors[np.newaxis, :, :], axis=2
)
color_to_tile = np.argmin(distances, axis=1)
tile_indices = color_to_tile[grid_labels]
else:
max_index = len(tiles_arr) - 1
tile_indices = np.clip(grid_labels, 0, max_index)
flat_tiles = tiles_arr[tile_indices.reshape(-1)]
tile_grid = flat_tiles.reshape(grid_size, grid_size, cell_size, cell_size, 3)
mosaic = tile_grid.transpose(0, 2, 1, 3, 4).reshape(grid_size * cell_size, grid_size * cell_size, 3)
return mosaic
|