|
|
"""
|
|
|
cache_builder.py
|
|
|
Builds optimized tile caches for fast mosaic generation.
|
|
|
Run this once to create cache files, then use for instant tile loading.
|
|
|
"""
|
|
|
|
|
|
import numpy as np
|
|
|
import cv2
|
|
|
from pathlib import Path
|
|
|
from typing import Tuple
|
|
|
from sklearn.cluster import MiniBatchKMeans
|
|
|
from sklearn.neighbors import NearestNeighbors
|
|
|
from sklearn.metrics.pairwise import euclidean_distances
|
|
|
from tqdm import tqdm
|
|
|
import pickle
|
|
|
import time
|
|
|
import warnings
|
|
|
from collections import defaultdict
|
|
|
|
|
|
class TileCacheBuilder:
|
|
|
"""Builds optimized tile caches with rotation variants and color subgrouping."""
|
|
|
|
|
|
def __init__(self, tile_folder: str,
|
|
|
tile_size: Tuple[int, int] = (32, 32),
|
|
|
colour_bins: int = 8,
|
|
|
enable_rotation: bool = True):
|
|
|
"""
|
|
|
Initialize cache builder.
|
|
|
|
|
|
Args:
|
|
|
tile_folder: Path to folder containing tile images
|
|
|
tile_size: Target tile size (width, height)
|
|
|
colour_bins: Number of colour categories for subgrouping
|
|
|
enable_rotation: Whether to create rotated variants
|
|
|
"""
|
|
|
self.tile_folder = Path(tile_folder)
|
|
|
self.tile_size = tile_size
|
|
|
self.colour_bins = colour_bins
|
|
|
self.enable_rotation = enable_rotation
|
|
|
self.rotation_angles = [0, 90, 180, 270] if enable_rotation else [0]
|
|
|
|
|
|
|
|
|
self.tile_images = []
|
|
|
self.tile_colours = []
|
|
|
self.tile_names = []
|
|
|
self.colour_palette = None
|
|
|
self.colour_groups = defaultdict(list)
|
|
|
self.colour_indices = {}
|
|
|
|
|
|
print(f"Tile Cache Builder")
|
|
|
print(f"Folder: {tile_folder}")
|
|
|
print(f"Tile size: {tile_size[0]}x{tile_size[1]}")
|
|
|
print(f"Colour bins: {colour_bins}")
|
|
|
print(f"Rotation: {enable_rotation}")
|
|
|
|
|
|
def build_cache(self, output_file: str, force_rebuild: bool = False) -> bool:
|
|
|
"""Build complete optimized tile cache."""
|
|
|
if Path(output_file).exists() and not force_rebuild:
|
|
|
print(f"Cache exists: {output_file} (use force_rebuild=True to rebuild)")
|
|
|
return False
|
|
|
|
|
|
print("Building comprehensive tile cache...")
|
|
|
total_start = time.time()
|
|
|
|
|
|
try:
|
|
|
self._load_base_tiles()
|
|
|
if self.enable_rotation:
|
|
|
self._create_rotated_variants()
|
|
|
self._analyze_tile_colors()
|
|
|
self._create_color_subgroups()
|
|
|
self._save_cache(output_file)
|
|
|
|
|
|
total_time = time.time() - total_start
|
|
|
print(f"Cache built in {total_time:.2f} seconds: {output_file}")
|
|
|
return True
|
|
|
|
|
|
except Exception as e:
|
|
|
print(f"Cache building failed: {e}")
|
|
|
return False
|
|
|
|
|
|
def _load_base_tiles(self):
|
|
|
"""Load base tiles from folder."""
|
|
|
if not self.tile_folder.exists():
|
|
|
raise ValueError(f"Tile folder not found: {self.tile_folder}")
|
|
|
|
|
|
|
|
|
extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff']
|
|
|
image_files = []
|
|
|
for ext in extensions:
|
|
|
image_files.extend(self.tile_folder.glob(f'*{ext}'))
|
|
|
image_files.extend(self.tile_folder.glob(f'*{ext.upper()}'))
|
|
|
|
|
|
if not image_files:
|
|
|
raise ValueError(f"No images found in {self.tile_folder}")
|
|
|
|
|
|
print(f"Loading {len(image_files)} base tiles...")
|
|
|
|
|
|
for img_path in tqdm(image_files, desc="Loading tiles"):
|
|
|
try:
|
|
|
img = cv2.imread(str(img_path))
|
|
|
if img is not None:
|
|
|
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
|
|
|
|
|
|
|
|
if img_rgb.shape[:2] != (self.tile_size[1], self.tile_size[0]):
|
|
|
img_rgb = cv2.resize(img_rgb, self.tile_size, interpolation=cv2.INTER_AREA)
|
|
|
|
|
|
self.tile_images.append(img_rgb)
|
|
|
self.tile_names.append(img_path.name)
|
|
|
|
|
|
except Exception as e:
|
|
|
print(f"Error loading {img_path}: {e}")
|
|
|
|
|
|
print(f"Loaded {len(self.tile_images)} base tiles")
|
|
|
|
|
|
def _create_rotated_variants(self):
|
|
|
"""Create 90, 180, 270 degree rotated variants."""
|
|
|
print("Creating rotated variants...")
|
|
|
|
|
|
base_count = len(self.tile_images)
|
|
|
rotated_images = []
|
|
|
rotated_names = []
|
|
|
|
|
|
for i in range(base_count):
|
|
|
base_image = self.tile_images[i]
|
|
|
base_name = self.tile_names[i]
|
|
|
|
|
|
|
|
|
for angle in [90, 180, 270]:
|
|
|
if angle == 90:
|
|
|
rotated = cv2.rotate(base_image, cv2.ROTATE_90_CLOCKWISE)
|
|
|
elif angle == 180:
|
|
|
rotated = cv2.rotate(base_image, cv2.ROTATE_180)
|
|
|
elif angle == 270:
|
|
|
rotated = cv2.rotate(base_image, cv2.ROTATE_90_COUNTERCLOCKWISE)
|
|
|
|
|
|
rotated_images.append(rotated)
|
|
|
rotated_names.append(f"{base_name}_rot{angle}")
|
|
|
|
|
|
|
|
|
self.tile_images.extend(rotated_images)
|
|
|
self.tile_names.extend(rotated_names)
|
|
|
|
|
|
print(f"Expanded to {len(self.tile_images)} tiles with rotation")
|
|
|
|
|
|
def _analyze_tile_colors(self):
|
|
|
"""Calculate average colors for all tiles."""
|
|
|
print("Analyzing tile colors...")
|
|
|
|
|
|
for tile_image in tqdm(self.tile_images, desc="Color analysis"):
|
|
|
avg_colour = np.mean(tile_image, axis=(0, 1))
|
|
|
self.tile_colours.append(avg_colour)
|
|
|
|
|
|
self.tile_colours = np.array(self.tile_colours)
|
|
|
print(f"Color analysis complete: {len(self.tile_colours)} tiles")
|
|
|
|
|
|
def _create_color_subgroups(self):
|
|
|
"""Create color palette and subgroup tiles for fast searching."""
|
|
|
print(f"Creating {self.colour_bins}-color palette...")
|
|
|
|
|
|
|
|
|
with warnings.catch_warnings():
|
|
|
warnings.filterwarnings("ignore", category=UserWarning)
|
|
|
|
|
|
batch_size = min(max(len(self.tile_colours) // 10, 100), 1000)
|
|
|
kmeans = MiniBatchKMeans(
|
|
|
n_clusters=self.colour_bins,
|
|
|
batch_size=batch_size,
|
|
|
random_state=42,
|
|
|
n_init=3
|
|
|
)
|
|
|
kmeans.fit(self.tile_colours)
|
|
|
self.colour_palette = kmeans.cluster_centers_
|
|
|
|
|
|
|
|
|
for i, tile_colour in enumerate(self.tile_colours):
|
|
|
distances = euclidean_distances(
|
|
|
tile_colour.reshape(1, -1),
|
|
|
self.colour_palette
|
|
|
)[0]
|
|
|
closest_bin = np.argmin(distances)
|
|
|
self.colour_groups[closest_bin].append(i)
|
|
|
|
|
|
|
|
|
for bin_id, tile_indices in self.colour_groups.items():
|
|
|
if len(tile_indices) > 0:
|
|
|
group_colours = self.tile_colours[tile_indices]
|
|
|
|
|
|
index = NearestNeighbors(
|
|
|
n_neighbors=min(10, len(tile_indices)),
|
|
|
metric='euclidean',
|
|
|
algorithm='kd_tree'
|
|
|
)
|
|
|
index.fit(group_colours)
|
|
|
self.colour_indices[bin_id] = (index, tile_indices)
|
|
|
|
|
|
print(f"Created {len(self.colour_groups)} color subgroups")
|
|
|
|
|
|
def _save_cache(self, output_file: str):
|
|
|
"""Save processed data to cache file."""
|
|
|
cache_data = {
|
|
|
'tile_images': np.array(self.tile_images),
|
|
|
'tile_colours': self.tile_colours,
|
|
|
'tile_names': self.tile_names,
|
|
|
'colour_palette': self.colour_palette,
|
|
|
'colour_groups': dict(self.colour_groups),
|
|
|
'colour_indices': self.colour_indices,
|
|
|
'tile_size': self.tile_size,
|
|
|
'colour_bins': self.colour_bins,
|
|
|
'enable_rotation': self.enable_rotation,
|
|
|
'build_timestamp': time.time()
|
|
|
}
|
|
|
|
|
|
with open(output_file, 'wb') as f:
|
|
|
pickle.dump(cache_data, f)
|
|
|
|
|
|
cache_size_mb = Path(output_file).stat().st_size / 1024 / 1024
|
|
|
print(f"Cache saved: {cache_size_mb:.1f}MB")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
builder = TileCacheBuilder(
|
|
|
tile_folder="extracted_images",
|
|
|
tile_size=(32, 32),
|
|
|
colour_bins=8,
|
|
|
enable_rotation=True
|
|
|
)
|
|
|
|
|
|
success = builder.build_cache("tiles_cache.pkl", force_rebuild=True)
|
|
|
|
|
|
if success:
|
|
|
print("Cache building completed! You can now run main.py")
|
|
|
else:
|
|
|
print("Cache building failed!") |