File size: 9,123 Bytes
f647a80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
"""

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]
        
        # Data containers
        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}")
        
        # Find all image files
        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)
                    
                    # Resize to target size
                    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]
            
            # Create 3 rotated versions
            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}")
        
        # Add to main collections
        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...")
        
        # Create color palette using Mini-Batch K-means
        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_
        
        # Assign tiles to color bins
        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)
        
        # Create search indices for each group
        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__":
    # Build cache with your settings
    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!")