Niranjan Sathish commited on
Commit
2724e1d
·
1 Parent(s): 9e5f32a

Adding all files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. Cache_Builder.py +234 -0
  2. ColourClassification.py +511 -0
  3. ImagePreprocessor.py +199 -0
  4. Performance_metrics.py +168 -0
  5. app.py +480 -0
  6. cache_16x16_bins8_rot.pkl +3 -0
  7. cache_32x32_bins8_rot.pkl +3 -0
  8. contextual_Mosaic_Builder.py +369 -0
  9. extracted_images/img_2001.png +0 -0
  10. extracted_images/img_2002.png +0 -0
  11. extracted_images/img_2003.png +0 -0
  12. extracted_images/img_2004.png +0 -0
  13. extracted_images/img_2005.png +0 -0
  14. extracted_images/img_2006.png +0 -0
  15. extracted_images/img_2007.png +0 -0
  16. extracted_images/img_2008.png +0 -0
  17. extracted_images/img_2009.png +0 -0
  18. extracted_images/img_2010.png +0 -0
  19. extracted_images/img_2011.png +0 -0
  20. extracted_images/img_2012.png +0 -0
  21. extracted_images/img_2013.png +0 -0
  22. extracted_images/img_2014.png +0 -0
  23. extracted_images/img_2015.png +0 -0
  24. extracted_images/img_2016.png +0 -0
  25. extracted_images/img_2017.png +0 -0
  26. extracted_images/img_2018.png +0 -0
  27. extracted_images/img_2019.png +0 -0
  28. extracted_images/img_2020.png +0 -0
  29. extracted_images/img_2021.png +0 -0
  30. extracted_images/img_2022.png +0 -0
  31. extracted_images/img_2023.png +0 -0
  32. extracted_images/img_2024.png +0 -0
  33. extracted_images/img_2025.png +0 -0
  34. extracted_images/img_2026.png +0 -0
  35. extracted_images/img_2027.png +0 -0
  36. extracted_images/img_2028.png +0 -0
  37. extracted_images/img_2029.png +0 -0
  38. extracted_images/img_2030.png +0 -0
  39. extracted_images/img_2031.png +0 -0
  40. extracted_images/img_2032.png +0 -0
  41. extracted_images/img_2033.png +0 -0
  42. extracted_images/img_2034.png +0 -0
  43. extracted_images/img_2035.png +0 -0
  44. extracted_images/img_2036.png +0 -0
  45. extracted_images/img_2037.png +0 -0
  46. extracted_images/img_2038.png +0 -0
  47. extracted_images/img_2039.png +0 -0
  48. extracted_images/img_2040.png +0 -0
  49. extracted_images/img_2041.png +0 -0
  50. extracted_images/img_2042.png +0 -0
Cache_Builder.py ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ cache_builder.py
3
+ Builds optimized tile caches for fast mosaic generation.
4
+ Run this once to create cache files, then use for instant tile loading.
5
+ """
6
+
7
+ import numpy as np
8
+ import cv2
9
+ from pathlib import Path
10
+ from typing import Tuple
11
+ from sklearn.cluster import MiniBatchKMeans
12
+ from sklearn.neighbors import NearestNeighbors
13
+ from sklearn.metrics.pairwise import euclidean_distances
14
+ from tqdm import tqdm
15
+ import pickle
16
+ import time
17
+ import warnings
18
+ from collections import defaultdict
19
+
20
+ class TileCacheBuilder:
21
+ """Builds optimized tile caches with rotation variants and color subgrouping."""
22
+
23
+ def __init__(self, tile_folder: str,
24
+ tile_size: Tuple[int, int] = (32, 32),
25
+ colour_bins: int = 8,
26
+ enable_rotation: bool = True):
27
+ """
28
+ Initialize cache builder.
29
+
30
+ Args:
31
+ tile_folder: Path to folder containing tile images
32
+ tile_size: Target tile size (width, height)
33
+ colour_bins: Number of colour categories for subgrouping
34
+ enable_rotation: Whether to create rotated variants
35
+ """
36
+ self.tile_folder = Path(tile_folder)
37
+ self.tile_size = tile_size
38
+ self.colour_bins = colour_bins
39
+ self.enable_rotation = enable_rotation
40
+ self.rotation_angles = [0, 90, 180, 270] if enable_rotation else [0]
41
+
42
+ # Data containers
43
+ self.tile_images = []
44
+ self.tile_colours = []
45
+ self.tile_names = []
46
+ self.colour_palette = None
47
+ self.colour_groups = defaultdict(list)
48
+ self.colour_indices = {}
49
+
50
+ print(f"Tile Cache Builder")
51
+ print(f"Folder: {tile_folder}")
52
+ print(f"Tile size: {tile_size[0]}x{tile_size[1]}")
53
+ print(f"Colour bins: {colour_bins}")
54
+ print(f"Rotation: {enable_rotation}")
55
+
56
+ def build_cache(self, output_file: str, force_rebuild: bool = False) -> bool:
57
+ """Build complete optimized tile cache."""
58
+ if Path(output_file).exists() and not force_rebuild:
59
+ print(f"Cache exists: {output_file} (use force_rebuild=True to rebuild)")
60
+ return False
61
+
62
+ print("Building comprehensive tile cache...")
63
+ total_start = time.time()
64
+
65
+ try:
66
+ self._load_base_tiles()
67
+ if self.enable_rotation:
68
+ self._create_rotated_variants()
69
+ self._analyze_tile_colors()
70
+ self._create_color_subgroups()
71
+ self._save_cache(output_file)
72
+
73
+ total_time = time.time() - total_start
74
+ print(f"Cache built in {total_time:.2f} seconds: {output_file}")
75
+ return True
76
+
77
+ except Exception as e:
78
+ print(f"Cache building failed: {e}")
79
+ return False
80
+
81
+ def _load_base_tiles(self):
82
+ """Load base tiles from folder."""
83
+ if not self.tile_folder.exists():
84
+ raise ValueError(f"Tile folder not found: {self.tile_folder}")
85
+
86
+ # Find all image files
87
+ extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff']
88
+ image_files = []
89
+ for ext in extensions:
90
+ image_files.extend(self.tile_folder.glob(f'*{ext}'))
91
+ image_files.extend(self.tile_folder.glob(f'*{ext.upper()}'))
92
+
93
+ if not image_files:
94
+ raise ValueError(f"No images found in {self.tile_folder}")
95
+
96
+ print(f"Loading {len(image_files)} base tiles...")
97
+
98
+ for img_path in tqdm(image_files, desc="Loading tiles"):
99
+ try:
100
+ img = cv2.imread(str(img_path))
101
+ if img is not None:
102
+ img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
103
+
104
+ # Resize to target size
105
+ if img_rgb.shape[:2] != (self.tile_size[1], self.tile_size[0]):
106
+ img_rgb = cv2.resize(img_rgb, self.tile_size, interpolation=cv2.INTER_AREA)
107
+
108
+ self.tile_images.append(img_rgb)
109
+ self.tile_names.append(img_path.name)
110
+
111
+ except Exception as e:
112
+ print(f"Error loading {img_path}: {e}")
113
+
114
+ print(f"Loaded {len(self.tile_images)} base tiles")
115
+
116
+ def _create_rotated_variants(self):
117
+ """Create 90, 180, 270 degree rotated variants."""
118
+ print("Creating rotated variants...")
119
+
120
+ base_count = len(self.tile_images)
121
+ rotated_images = []
122
+ rotated_names = []
123
+
124
+ for i in range(base_count):
125
+ base_image = self.tile_images[i]
126
+ base_name = self.tile_names[i]
127
+
128
+ # Create 3 rotated versions
129
+ for angle in [90, 180, 270]:
130
+ if angle == 90:
131
+ rotated = cv2.rotate(base_image, cv2.ROTATE_90_CLOCKWISE)
132
+ elif angle == 180:
133
+ rotated = cv2.rotate(base_image, cv2.ROTATE_180)
134
+ elif angle == 270:
135
+ rotated = cv2.rotate(base_image, cv2.ROTATE_90_COUNTERCLOCKWISE)
136
+
137
+ rotated_images.append(rotated)
138
+ rotated_names.append(f"{base_name}_rot{angle}")
139
+
140
+ # Add to main collections
141
+ self.tile_images.extend(rotated_images)
142
+ self.tile_names.extend(rotated_names)
143
+
144
+ print(f"Expanded to {len(self.tile_images)} tiles with rotation")
145
+
146
+ def _analyze_tile_colors(self):
147
+ """Calculate average colors for all tiles."""
148
+ print("Analyzing tile colors...")
149
+
150
+ for tile_image in tqdm(self.tile_images, desc="Color analysis"):
151
+ avg_colour = np.mean(tile_image, axis=(0, 1))
152
+ self.tile_colours.append(avg_colour)
153
+
154
+ self.tile_colours = np.array(self.tile_colours)
155
+ print(f"Color analysis complete: {len(self.tile_colours)} tiles")
156
+
157
+ def _create_color_subgroups(self):
158
+ """Create color palette and subgroup tiles for fast searching."""
159
+ print(f"Creating {self.colour_bins}-color palette...")
160
+
161
+ # Create color palette using Mini-Batch K-means
162
+ with warnings.catch_warnings():
163
+ warnings.filterwarnings("ignore", category=UserWarning)
164
+
165
+ batch_size = min(max(len(self.tile_colours) // 10, 100), 1000)
166
+ kmeans = MiniBatchKMeans(
167
+ n_clusters=self.colour_bins,
168
+ batch_size=batch_size,
169
+ random_state=42,
170
+ n_init=3
171
+ )
172
+ kmeans.fit(self.tile_colours)
173
+ self.colour_palette = kmeans.cluster_centers_
174
+
175
+ # Assign tiles to color bins
176
+ for i, tile_colour in enumerate(self.tile_colours):
177
+ distances = euclidean_distances(
178
+ tile_colour.reshape(1, -1),
179
+ self.colour_palette
180
+ )[0]
181
+ closest_bin = np.argmin(distances)
182
+ self.colour_groups[closest_bin].append(i)
183
+
184
+ # Create search indices for each group
185
+ for bin_id, tile_indices in self.colour_groups.items():
186
+ if len(tile_indices) > 0:
187
+ group_colours = self.tile_colours[tile_indices]
188
+
189
+ index = NearestNeighbors(
190
+ n_neighbors=min(10, len(tile_indices)),
191
+ metric='euclidean',
192
+ algorithm='kd_tree'
193
+ )
194
+ index.fit(group_colours)
195
+ self.colour_indices[bin_id] = (index, tile_indices)
196
+
197
+ print(f"Created {len(self.colour_groups)} color subgroups")
198
+
199
+ def _save_cache(self, output_file: str):
200
+ """Save processed data to cache file."""
201
+ cache_data = {
202
+ 'tile_images': np.array(self.tile_images),
203
+ 'tile_colours': self.tile_colours,
204
+ 'tile_names': self.tile_names,
205
+ 'colour_palette': self.colour_palette,
206
+ 'colour_groups': dict(self.colour_groups),
207
+ 'colour_indices': self.colour_indices,
208
+ 'tile_size': self.tile_size,
209
+ 'colour_bins': self.colour_bins,
210
+ 'enable_rotation': self.enable_rotation,
211
+ 'build_timestamp': time.time()
212
+ }
213
+
214
+ with open(output_file, 'wb') as f:
215
+ pickle.dump(cache_data, f)
216
+
217
+ cache_size_mb = Path(output_file).stat().st_size / 1024 / 1024
218
+ print(f"Cache saved: {cache_size_mb:.1f}MB")
219
+
220
+ if __name__ == "__main__":
221
+ # Build cache with your settings
222
+ builder = TileCacheBuilder(
223
+ tile_folder="extracted_images",
224
+ tile_size=(32, 32),
225
+ colour_bins=8,
226
+ enable_rotation=True
227
+ )
228
+
229
+ success = builder.build_cache("tiles_cache.pkl", force_rebuild=True)
230
+
231
+ if success:
232
+ print("Cache building completed! You can now run main.py")
233
+ else:
234
+ print("Cache building failed!")
ColourClassification.py ADDED
@@ -0,0 +1,511 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import cv2
3
+ from sklearn.cluster import KMeans
4
+ from sklearn.preprocessing import LabelEncoder
5
+ import matplotlib.pyplot as plt
6
+ from typing import Tuple, List, Dict, Optional
7
+ from dataclasses import dataclass
8
+ from enum import Enum
9
+ import seaborn as sns
10
+
11
+ class ColorClassificationMethod(Enum):
12
+ """Different methods for classifying cell colors."""
13
+ DOMINANT_COLOR = "dominant_color"
14
+ AVERAGE_COLOR = "average_color"
15
+ HISTOGRAM_BINS = "histogram_bins"
16
+ HSV_QUANTIZATION = "hsv_quantization"
17
+
18
+ @dataclass
19
+ class GridCell:
20
+ """Represents a single grid cell with its properties."""
21
+ row: int
22
+ col: int
23
+ average_color: np.ndarray
24
+ dominant_color: np.ndarray
25
+ brightness: float
26
+ saturation: float
27
+ hue: float
28
+ color_category: int
29
+ pixel_data: np.ndarray
30
+
31
+ class ImageGridAnalyzer:
32
+ """
33
+ Analyzes images by dividing them into grids and classifying each cell's color properties.
34
+ Uses vectorized NumPy operations for high performance.
35
+ """
36
+
37
+ def __init__(self, grid_size: Tuple[int, int] = (32, 32),
38
+ classification_method: ColorClassificationMethod = ColorClassificationMethod.DOMINANT_COLOR,
39
+ n_color_categories: int = 16):
40
+ """
41
+ Initialize the grid analyzer.
42
+
43
+ Args:
44
+ grid_size: (rows, cols) for the grid division
45
+ classification_method: Method to classify cell colors
46
+ n_color_categories: Number of color categories for classification
47
+ """
48
+ self.grid_size = grid_size
49
+ self.classification_method = classification_method
50
+ self.n_color_categories = n_color_categories
51
+ self.color_classifier = None
52
+ self.category_colors = None
53
+
54
+ def divide_image_into_grid(self, image: np.ndarray) -> Tuple[np.ndarray, Tuple[int, int]]:
55
+ """
56
+ Divide image into a grid using vectorized operations.
57
+
58
+ Args:
59
+ image: Input image (H, W, C)
60
+
61
+ Returns:
62
+ Grid of cells (grid_rows, grid_cols, tile_height, tile_width, channels)
63
+ Tuple of (tile_height, tile_width)
64
+ """
65
+ h, w, c = image.shape
66
+ grid_rows, grid_cols = self.grid_size
67
+
68
+ # Calculate tile dimensions
69
+ tile_h = h // grid_rows
70
+ tile_w = w // grid_cols
71
+
72
+ # Adjust image size to fit grid perfectly (crop if necessary)
73
+ adjusted_h = tile_h * grid_rows
74
+ adjusted_w = tile_w * grid_cols
75
+ image = image[:adjusted_h, :adjusted_w]
76
+
77
+ # Vectorized grid division using reshape and transpose
78
+ # This is much faster than nested loops
79
+ grid = image.reshape(grid_rows, tile_h, grid_cols, tile_w, c)
80
+ grid = grid.transpose(0, 2, 1, 3, 4) # (grid_rows, grid_cols, tile_h, tile_w, c)
81
+
82
+ return grid, (tile_h, tile_w)
83
+
84
+ def analyze_grid_colors_vectorized(self, grid: np.ndarray) -> Dict[str, np.ndarray]:
85
+ """
86
+ Analyze color properties of all grid cells using vectorized operations.
87
+
88
+ Args:
89
+ grid: Grid of cells (grid_rows, grid_cols, tile_h, tile_w, c)
90
+
91
+ Returns:
92
+ Dictionary containing vectorized analysis results
93
+ """
94
+ grid_rows, grid_cols, tile_h, tile_w, c = grid.shape
95
+
96
+ # Reshape for vectorized operations: (total_cells, pixels_per_cell, channels)
97
+ cells_flat = grid.reshape(grid_rows * grid_cols, tile_h * tile_w, c)
98
+
99
+ # Calculate average colors for all cells at once
100
+ average_colors = np.mean(cells_flat, axis=1) # (total_cells, c)
101
+
102
+ # Calculate dominant colors using vectorized approach
103
+ dominant_colors = self._calculate_dominant_colors_vectorized(cells_flat)
104
+
105
+ # Convert to HSV for additional analysis
106
+ hsv_averages = self._rgb_to_hsv_vectorized(average_colors)
107
+
108
+ # Calculate brightness (V in HSV)
109
+ brightness = hsv_averages[:, 2]
110
+
111
+ # Calculate saturation
112
+ saturation = hsv_averages[:, 1]
113
+
114
+ # Calculate hue
115
+ hue = hsv_averages[:, 0]
116
+
117
+ # Reshape results back to grid format
118
+ results = {
119
+ 'average_colors': average_colors.reshape(grid_rows, grid_cols, c),
120
+ 'dominant_colors': dominant_colors.reshape(grid_rows, grid_cols, c),
121
+ 'brightness': brightness.reshape(grid_rows, grid_cols),
122
+ 'saturation': saturation.reshape(grid_rows, grid_cols),
123
+ 'hue': hue.reshape(grid_rows, grid_cols),
124
+ 'cells_data': grid # Keep original cell data
125
+ }
126
+
127
+ return results
128
+
129
+ def _calculate_dominant_colors_vectorized(self, cells_flat: np.ndarray) -> np.ndarray:
130
+ """
131
+ Calculate dominant color for each cell using vectorized operations.
132
+
133
+ Args:
134
+ cells_flat: Flattened cells (total_cells, pixels_per_cell, channels)
135
+
136
+ Returns:
137
+ Dominant colors for all cells (total_cells, channels)
138
+ """
139
+ import warnings
140
+
141
+ total_cells, pixels_per_cell, c = cells_flat.shape
142
+ dominant_colors = np.zeros((total_cells, c))
143
+
144
+ # Process cells in batches for memory efficiency
145
+ batch_size = 100
146
+ for i in range(0, total_cells, batch_size):
147
+ end_idx = min(i + batch_size, total_cells)
148
+ batch = cells_flat[i:end_idx]
149
+
150
+ for j, cell_pixels in enumerate(batch):
151
+ # Check for color diversity first
152
+ unique_pixels = np.unique(cell_pixels, axis=0)
153
+
154
+ if len(unique_pixels) >= 3 and pixels_per_cell > 100:
155
+ # Use k-means for larger cells with sufficient color diversity
156
+ with warnings.catch_warnings():
157
+ warnings.filterwarnings("ignore", category=UserWarning)
158
+ warnings.filterwarnings("ignore", message=".*ConvergenceWarning.*")
159
+
160
+ kmeans = KMeans(n_clusters=min(3, len(unique_pixels)),
161
+ random_state=42, n_init=5)
162
+ labels = kmeans.fit_predict(cell_pixels)
163
+ # Get the most frequent cluster center
164
+ unique_labels, counts = np.unique(labels, return_counts=True)
165
+ dominant_idx = unique_labels[np.argmax(counts)]
166
+ dominant_colors[i + j] = kmeans.cluster_centers_[dominant_idx]
167
+ elif len(unique_pixels) >= 2:
168
+ # Use most frequent color for limited diversity
169
+ unique_colors, counts = np.unique(cell_pixels, axis=0, return_counts=True)
170
+ dominant_colors[i + j] = unique_colors[np.argmax(counts)]
171
+ else:
172
+ # Use simple average for uniform cells
173
+ dominant_colors[i + j] = np.mean(cell_pixels, axis=0)
174
+
175
+ return dominant_colors
176
+
177
+ def _rgb_to_hsv_vectorized(self, rgb_colors: np.ndarray) -> np.ndarray:
178
+ """
179
+ Convert RGB colors to HSV using vectorized operations.
180
+
181
+ Args:
182
+ rgb_colors: RGB colors (N, 3)
183
+
184
+ Returns:
185
+ HSV colors (N, 3)
186
+ """
187
+ # Normalize to 0-1 range
188
+ rgb_normalized = rgb_colors / 255.0
189
+
190
+ # Create a dummy image for cv2 conversion
191
+ dummy_img = rgb_normalized.reshape(-1, 1, 3).astype(np.float32)
192
+ hsv_img = cv2.cvtColor(dummy_img, cv2.COLOR_RGB2HSV)
193
+ hsv_colors = hsv_img.reshape(-1, 3)
194
+
195
+ return hsv_colors
196
+
197
+ def classify_colors(self, color_data: Dict[str, np.ndarray]) -> np.ndarray:
198
+ """
199
+ Classify each grid cell into color categories.
200
+
201
+ Args:
202
+ color_data: Dictionary containing color analysis results
203
+
204
+ Returns:
205
+ Color categories for each grid cell (grid_rows, grid_cols)
206
+ """
207
+ import warnings
208
+
209
+ if self.classification_method == ColorClassificationMethod.AVERAGE_COLOR:
210
+ features = color_data['average_colors']
211
+ elif self.classification_method == ColorClassificationMethod.DOMINANT_COLOR:
212
+ features = color_data['dominant_colors']
213
+ elif self.classification_method == ColorClassificationMethod.HSV_QUANTIZATION:
214
+ # Combine HSV features
215
+ h = color_data['hue']
216
+ s = color_data['saturation']
217
+ v = color_data['brightness']
218
+ features = np.stack([h, s, v], axis=-1)
219
+ else:
220
+ features = color_data['average_colors']
221
+
222
+ # Flatten for clustering
223
+ grid_rows, grid_cols = features.shape[:2]
224
+ features_flat = features.reshape(-1, features.shape[-1])
225
+
226
+ # Check for sufficient diversity before clustering
227
+ unique_features = np.unique(features_flat, axis=0)
228
+ effective_clusters = min(self.n_color_categories, len(unique_features))
229
+
230
+ if effective_clusters < 2:
231
+ # Handle case with very limited color diversity
232
+ print(f"Warning: Only {len(unique_features)} unique colors found. Using simple classification.")
233
+ categories = np.zeros(len(features_flat), dtype=int)
234
+ categories_grid = categories.reshape(grid_rows, grid_cols)
235
+ self.category_colors = unique_features[:1] if len(unique_features) > 0 else np.array([[128, 128, 128]])
236
+ return categories_grid
237
+
238
+ # Fit color classifier with warning suppression
239
+ with warnings.catch_warnings():
240
+ warnings.filterwarnings("ignore", category=UserWarning)
241
+ warnings.filterwarnings("ignore", message=".*ConvergenceWarning.*")
242
+
243
+ self.color_classifier = KMeans(n_clusters=effective_clusters,
244
+ random_state=42, n_init=10)
245
+ categories = self.color_classifier.fit_predict(features_flat)
246
+
247
+ # Store category representative colors
248
+ self.category_colors = self.color_classifier.cluster_centers_
249
+
250
+ # Reshape back to grid
251
+ categories_grid = categories.reshape(grid_rows, grid_cols)
252
+
253
+ return categories_grid
254
+
255
+ def apply_thresholding(self, color_data: Dict[str, np.ndarray],
256
+ brightness_threshold: float = 0.5,
257
+ saturation_threshold: float = 0.3) -> Dict[str, np.ndarray]:
258
+ """
259
+ Apply thresholding to create binary masks for different criteria.
260
+
261
+ Args:
262
+ color_data: Color analysis results
263
+ brightness_threshold: Threshold for bright/dark classification
264
+ saturation_threshold: Threshold for saturated/desaturated classification
265
+
266
+ Returns:
267
+ Dictionary containing various threshold masks
268
+ """
269
+ brightness = color_data['brightness']
270
+ saturation = color_data['saturation']
271
+
272
+ # Normalize brightness and saturation to 0-1 range
273
+ brightness_norm = brightness / 255.0 if brightness.max() > 1.0 else brightness
274
+ saturation_norm = saturation / 255.0 if saturation.max() > 1.0 else saturation
275
+
276
+ thresholds = {
277
+ 'bright_mask': brightness_norm > brightness_threshold,
278
+ 'dark_mask': brightness_norm <= brightness_threshold,
279
+ 'saturated_mask': saturation_norm > saturation_threshold,
280
+ 'desaturated_mask': saturation_norm <= saturation_threshold,
281
+ 'bright_saturated': (brightness_norm > brightness_threshold) &
282
+ (saturation_norm > saturation_threshold),
283
+ 'dark_saturated': (brightness_norm <= brightness_threshold) &
284
+ (saturation_norm > saturation_threshold)
285
+ }
286
+
287
+ return thresholds
288
+
289
+ def analyze_image_complete(self, image: np.ndarray) -> Dict:
290
+ """
291
+ Complete analysis pipeline for an image.
292
+
293
+ Args:
294
+ image: Input image (H, W, C)
295
+
296
+ Returns:
297
+ Complete analysis results
298
+ """
299
+ print(f"Analyzing image with {self.grid_size[0]}x{self.grid_size[1]} grid...")
300
+
301
+ # Step 1: Divide into grid
302
+ grid, tile_size = self.divide_image_into_grid(image)
303
+ print(f"Created grid with tile size: {tile_size}")
304
+
305
+ # Step 2: Analyze colors (vectorized)
306
+ color_data = self.analyze_grid_colors_vectorized(grid)
307
+ print("Completed color analysis")
308
+
309
+ # Step 3: Classify colors
310
+ color_categories = self.classify_colors(color_data)
311
+ print(f"Classified into {self.n_color_categories} color categories")
312
+
313
+ # Step 4: Apply thresholding
314
+ thresholds = self.apply_thresholding(color_data)
315
+ print("Applied thresholding")
316
+
317
+ # Combine all results
318
+ results = {
319
+ 'grid': grid,
320
+ 'tile_size': tile_size,
321
+ 'color_data': color_data,
322
+ 'color_categories': color_categories,
323
+ 'thresholds': thresholds,
324
+ 'category_colors': self.category_colors
325
+ }
326
+
327
+ return results
328
+
329
+ def visualize_analysis(self, results: Dict, original_image: np.ndarray):
330
+ """
331
+ Create comprehensive visualizations of the analysis results.
332
+
333
+ Args:
334
+ results: Analysis results from analyze_image_complete
335
+ original_image: Original input image
336
+ """
337
+ fig, axes = plt.subplots(2, 4, figsize=(20, 10))
338
+
339
+ # Original image
340
+ axes[0, 0].imshow(original_image)
341
+ axes[0, 0].set_title('Original Image')
342
+ axes[0, 0].axis('off')
343
+
344
+ # Average colors
345
+ avg_colors = results['color_data']['average_colors'].astype(np.uint8)
346
+ axes[0, 1].imshow(avg_colors)
347
+ axes[0, 1].set_title('Average Colors per Cell')
348
+ axes[0, 1].axis('off')
349
+
350
+ # Dominant colors
351
+ dom_colors = results['color_data']['dominant_colors'].astype(np.uint8)
352
+ axes[0, 2].imshow(dom_colors)
353
+ axes[0, 2].set_title('Dominant Colors per Cell')
354
+ axes[0, 2].axis('off')
355
+
356
+ # Color categories
357
+ categories = results['color_categories']
358
+ im_cat = axes[0, 3].imshow(categories, cmap='tab20')
359
+ axes[0, 3].set_title(f'Color Categories ({self.n_color_categories} classes)')
360
+ axes[0, 3].axis('off')
361
+ plt.colorbar(im_cat, ax=axes[0, 3])
362
+
363
+ # Brightness
364
+ brightness = results['color_data']['brightness']
365
+ im_bright = axes[1, 0].imshow(brightness, cmap='gray')
366
+ axes[1, 0].set_title('Brightness Values')
367
+ axes[1, 0].axis('off')
368
+ plt.colorbar(im_bright, ax=axes[1, 0])
369
+
370
+ # Saturation
371
+ saturation = results['color_data']['saturation']
372
+ im_sat = axes[1, 1].imshow(saturation, cmap='viridis')
373
+ axes[1, 1].set_title('Saturation Values')
374
+ axes[1, 1].axis('off')
375
+ plt.colorbar(im_sat, ax=axes[1, 1])
376
+
377
+ # Threshold: Bright areas
378
+ axes[1, 2].imshow(results['thresholds']['bright_mask'], cmap='gray')
379
+ axes[1, 2].set_title('Bright Areas (Threshold)')
380
+ axes[1, 2].axis('off')
381
+
382
+ # Threshold: Saturated areas
383
+ axes[1, 3].imshow(results['thresholds']['saturated_mask'], cmap='gray')
384
+ axes[1, 3].set_title('Saturated Areas (Threshold)')
385
+ axes[1, 3].axis('off')
386
+
387
+ plt.tight_layout()
388
+ plt.show()
389
+
390
+ # Show color category palette
391
+ self._visualize_color_palette(results['category_colors'])
392
+
393
+ def _visualize_color_palette(self, category_colors: np.ndarray):
394
+ """
395
+ Visualize the color category palette.
396
+
397
+ Args:
398
+ category_colors: Color palette (n_categories, channels)
399
+ """
400
+ if category_colors is None:
401
+ return
402
+
403
+ fig, ax = plt.subplots(1, 1, figsize=(12, 2))
404
+
405
+ # Normalize colors if needed
406
+ colors = category_colors.copy()
407
+ if colors.max() > 1.0:
408
+ colors = colors / 255.0
409
+
410
+ # Create color swatches
411
+ palette = colors.reshape(1, -1, 3)
412
+ ax.imshow(palette, aspect='auto')
413
+ ax.set_xlim(0, len(colors))
414
+ ax.set_ylim(0, 1)
415
+ ax.set_xticks(range(len(colors)))
416
+ ax.set_xticklabels([f'Cat {i}' for i in range(len(colors))])
417
+ ax.set_title(f'Color Category Palette ({len(colors)} categories)')
418
+ ax.set_ylabel('Color Categories')
419
+
420
+ plt.tight_layout()
421
+ plt.show()
422
+
423
+ def get_performance_stats(self, results: Dict) -> Dict:
424
+ """
425
+ Calculate performance and analysis statistics.
426
+
427
+ Args:
428
+ results: Analysis results
429
+
430
+ Returns:
431
+ Dictionary containing statistics
432
+ """
433
+ grid_shape = results['color_categories'].shape
434
+ total_cells = np.prod(grid_shape)
435
+
436
+ # Color diversity
437
+ unique_categories = len(np.unique(results['color_categories']))
438
+
439
+ # Brightness statistics
440
+ brightness = results['color_data']['brightness']
441
+
442
+ # Saturation statistics
443
+ saturation = results['color_data']['saturation']
444
+
445
+ stats = {
446
+ 'grid_size': f"{grid_shape[0]}x{grid_shape[1]}",
447
+ 'total_cells': total_cells,
448
+ 'unique_color_categories': unique_categories,
449
+ 'category_utilization': unique_categories / self.n_color_categories,
450
+ 'avg_brightness': np.mean(brightness),
451
+ 'brightness_std': np.std(brightness),
452
+ 'avg_saturation': np.mean(saturation),
453
+ 'saturation_std': np.std(saturation),
454
+ 'bright_cells_percent': np.mean(results['thresholds']['bright_mask']) * 100,
455
+ 'saturated_cells_percent': np.mean(results['thresholds']['saturated_mask']) * 100
456
+ }
457
+
458
+ return stats
459
+
460
+ # Example usage and testing
461
+ def main():
462
+ """
463
+ Example usage of the ImageGridAnalyzer.
464
+ """
465
+ # Create sample test image (or load your own)
466
+ def create_test_image():
467
+ # Create a colorful test image with gradients and patterns
468
+ img = np.zeros((256, 256, 3), dtype=np.uint8)
469
+
470
+ # Add gradients
471
+ for i in range(256):
472
+ for j in range(256):
473
+ img[i, j, 0] = i # Red gradient
474
+ img[i, j, 1] = j # Green gradient
475
+ img[i, j, 2] = (i + j) % 255 # Blue pattern
476
+
477
+ return img
478
+
479
+ # Initialize analyzer
480
+ analyzer = ImageGridAnalyzer(
481
+ grid_size=(32, 32), # 32x32 grid = 1024 cells
482
+ classification_method=ColorClassificationMethod.DOMINANT_COLOR,
483
+ n_color_categories=16
484
+ )
485
+
486
+ # Create or load test image
487
+ # test_image = create_test_image()
488
+
489
+ # Or load real image (uncomment and modify path):
490
+ test_image = cv2.imread('processed_quantized.jpg')
491
+ test_image = cv2.cvtColor(test_image, cv2.COLOR_BGR2RGB)
492
+
493
+ print("Starting analysis...")
494
+
495
+ # Analyze the image
496
+ results = analyzer.analyze_image_complete(test_image)
497
+
498
+ # Get performance statistics
499
+ stats = analyzer.get_performance_stats(results)
500
+
501
+ print("\n=== Analysis Statistics ===")
502
+ for key, value in stats.items():
503
+ print(f"{key}: {value}")
504
+
505
+ # Visualize results
506
+ # analyzer.visualize_analysis(results, test_image)
507
+
508
+ return results, analyzer
509
+
510
+ if __name__ == "__main__":
511
+ results, analyzer = main()
ImagePreprocessor.py ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ImagePreprocessor.py
3
+ Step 1: Image preprocessing for mosaic generation.
4
+ Handles image loading, resizing, cropping, and color quantization.
5
+ """
6
+
7
+ import cv2
8
+ import numpy as np
9
+ from PIL import Image
10
+ import os
11
+ from typing import Tuple, Optional
12
+ from sklearn.cluster import MiniBatchKMeans
13
+ import warnings
14
+
15
+ class ImagePreprocessor:
16
+ """
17
+ Handles image preprocessing for mosaic generation.
18
+ Features: Smart resizing, grid-perfect cropping, fast color quantization.
19
+ """
20
+
21
+ def __init__(self, target_resolution: Tuple[int, int] = (800, 600),
22
+ grid_size: Tuple[int, int] = (20, 15)):
23
+ """
24
+ Initialize the preprocessor.
25
+
26
+ Args:
27
+ target_resolution: Target (width, height) for processed images
28
+ grid_size: Grid dimensions (cols, rows) for mosaic
29
+ """
30
+ self.target_resolution = target_resolution
31
+ self.grid_size = grid_size
32
+
33
+ # Calculate tile size for perfect grid alignment
34
+ self.tile_width = target_resolution[0] // grid_size[0]
35
+ self.tile_height = target_resolution[1] // grid_size[1]
36
+
37
+ # Adjust target resolution to fit grid perfectly
38
+ self.adjusted_width = self.tile_width * grid_size[0]
39
+ self.adjusted_height = self.tile_height * grid_size[1]
40
+
41
+ print(f"Target resolution: {self.adjusted_width}x{self.adjusted_height}")
42
+ print(f"Grid size: {grid_size[0]}x{grid_size[1]}")
43
+ print(f"Tile size: {self.tile_width}x{self.tile_height}")
44
+
45
+ def load_and_preprocess_image(self, image_path: str,
46
+ apply_quantization: bool = False,
47
+ n_colors: int = 16) -> Optional[np.ndarray]:
48
+ """
49
+ Load and preprocess image from file path.
50
+
51
+ Args:
52
+ image_path: Path to the image file
53
+ apply_quantization: Whether to apply color quantization
54
+ n_colors: Number of colors for quantization
55
+
56
+ Returns:
57
+ Preprocessed image as numpy array (RGB) or None if failed
58
+ """
59
+ try:
60
+ # Load and convert image
61
+ image = cv2.imread(image_path)
62
+ if image is None:
63
+ raise ValueError(f"Could not load image: {image_path}")
64
+
65
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
66
+
67
+ # Resize and crop to fit grid
68
+ processed_image = self._resize_and_crop(image)
69
+
70
+ # Apply color quantization if requested
71
+ if apply_quantization:
72
+ processed_image = self._apply_color_quantization(processed_image, n_colors)
73
+
74
+ return processed_image
75
+
76
+ except Exception as e:
77
+ print(f"Error processing {image_path}: {str(e)}")
78
+ return None
79
+
80
+ def preprocess_numpy_image(self, image: np.ndarray,
81
+ apply_quantization: bool = False,
82
+ n_colors: int = 16) -> Optional[np.ndarray]:
83
+ """
84
+ Preprocess numpy image array (for Gradio integration).
85
+
86
+ Args:
87
+ image: Input image as numpy array
88
+ apply_quantization: Whether to apply color quantization
89
+ n_colors: Number of colors for quantization
90
+
91
+ Returns:
92
+ Preprocessed image as numpy array (RGB) or None if failed
93
+ """
94
+ try:
95
+ if len(image.shape) != 3 or image.shape[2] != 3:
96
+ raise ValueError("Image must be RGB format with shape (H, W, 3)")
97
+
98
+ processed_image = image.copy()
99
+ processed_image = self._resize_and_crop(processed_image)
100
+
101
+ if apply_quantization:
102
+ processed_image = self._apply_color_quantization(processed_image, n_colors)
103
+
104
+ return processed_image
105
+
106
+ except Exception as e:
107
+ print(f"Error processing numpy image: {str(e)}")
108
+ return None
109
+
110
+ def _resize_and_crop(self, image: np.ndarray) -> np.ndarray:
111
+ """
112
+ Resize and crop image to fit target resolution while maintaining aspect ratio.
113
+ """
114
+ h, w = image.shape[:2]
115
+ target_w, target_h = self.adjusted_width, self.adjusted_height
116
+
117
+ # Scale to fill target size
118
+ scale_w = target_w / w
119
+ scale_h = target_h / h
120
+ scale = max(scale_w, scale_h)
121
+
122
+ # Resize image
123
+ new_w = int(w * scale)
124
+ new_h = int(h * scale)
125
+ resized = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_AREA)
126
+
127
+ # Center crop to exact target size
128
+ start_x = (new_w - target_w) // 2
129
+ start_y = (new_h - target_h) // 2
130
+ cropped = resized[start_y:start_y + target_h, start_x:start_x + target_w]
131
+
132
+ return cropped
133
+
134
+ def _apply_color_quantization(self, image: np.ndarray, n_colors: int) -> np.ndarray:
135
+ """
136
+ Apply color quantization using Mini-Batch K-means for speed.
137
+ 3-4x faster than regular K-means with similar quality.
138
+ """
139
+ h, w, c = image.shape
140
+ pixels = image.reshape(-1, c)
141
+
142
+ # Adaptive batch size based on image size
143
+ total_pixels = len(pixels)
144
+ batch_size = min(max(total_pixels // 100, 1000), 10000)
145
+
146
+ print(f"Applying Mini-Batch K-means quantization:")
147
+ print(f" Total pixels: {total_pixels:,}")
148
+ print(f" Batch size: {batch_size:,}")
149
+ print(f" Target colors: {n_colors}")
150
+
151
+ with warnings.catch_warnings():
152
+ warnings.filterwarnings("ignore", category=UserWarning)
153
+ warnings.filterwarnings("ignore", message=".*ConvergenceWarning.*")
154
+
155
+ kmeans = MiniBatchKMeans(
156
+ n_clusters=n_colors,
157
+ batch_size=batch_size,
158
+ random_state=42,
159
+ n_init=3,
160
+ max_iter=100
161
+ )
162
+ labels = kmeans.fit_predict(pixels)
163
+
164
+ # Replace pixels with cluster centers
165
+ quantized_pixels = kmeans.cluster_centers_[labels]
166
+ quantized_image = quantized_pixels.reshape(h, w, c).astype(np.uint8)
167
+
168
+ return quantized_image
169
+
170
+ def save_preprocessed_image(self, image: np.ndarray, output_path: str):
171
+ """Save preprocessed image to disk."""
172
+ try:
173
+ pil_image = Image.fromarray(image)
174
+ pil_image.save(output_path, quality=95)
175
+ print(f"Saved preprocessed image to: {output_path}")
176
+ except Exception as e:
177
+ print(f"Error saving image: {str(e)}")
178
+
179
+ if __name__ == "__main__":
180
+ # Test preprocessing
181
+ preprocessor = ImagePreprocessor(
182
+ target_resolution=(1280, 1280),
183
+ grid_size=(32, 32)
184
+ )
185
+
186
+ test_image = "EmmaPotrait.jpg"
187
+
188
+ if os.path.exists(test_image):
189
+ processed = preprocessor.load_and_preprocess_image(
190
+ test_image,
191
+ apply_quantization=True,
192
+ n_colors=8
193
+ )
194
+
195
+ if processed is not None:
196
+ preprocessor.save_preprocessed_image(processed, "processed_quantized.jpg")
197
+ print("Image preprocessing completed!")
198
+ else:
199
+ print(f"Test image not found: {test_image}")
Performance_metrics.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ performance_metrics.py
3
+ Performance evaluation for mosaic quality assessment.
4
+ """
5
+
6
+ import numpy as np
7
+ import cv2
8
+ from typing import Dict
9
+ from skimage.metrics import structural_similarity as ssim
10
+ import matplotlib.pyplot as plt
11
+
12
+ class PerformanceEvaluator:
13
+ """Comprehensive mosaic quality evaluation."""
14
+
15
+ def __init__(self):
16
+ self.metrics = {}
17
+
18
+ def calculate_mse(self, original: np.ndarray, mosaic: np.ndarray) -> float:
19
+ """Calculate Mean Squared Error."""
20
+ if original.shape != mosaic.shape:
21
+ original_resized = cv2.resize(original, (mosaic.shape[1], mosaic.shape[0]))
22
+ else:
23
+ original_resized = original
24
+
25
+ orig_float = original_resized.astype(np.float64)
26
+ mosaic_float = mosaic.astype(np.float64)
27
+ mse = np.mean((orig_float - mosaic_float) ** 2)
28
+ return float(mse)
29
+
30
+ def calculate_psnr(self, original: np.ndarray, mosaic: np.ndarray) -> float:
31
+ """Calculate Peak Signal-to-Noise Ratio."""
32
+ mse = self.calculate_mse(original, mosaic)
33
+ if mse == 0:
34
+ return float('inf')
35
+ psnr = 20 * np.log10(255.0 / np.sqrt(mse))
36
+ return float(psnr)
37
+
38
+ def calculate_ssim(self, original: np.ndarray, mosaic: np.ndarray) -> float:
39
+ """Calculate Structural Similarity Index."""
40
+ if original.shape != mosaic.shape:
41
+ original_resized = cv2.resize(original, (mosaic.shape[1], mosaic.shape[0]))
42
+ else:
43
+ original_resized = original
44
+
45
+ if len(original_resized.shape) == 3:
46
+ ssim_values = []
47
+ for channel in range(original_resized.shape[2]):
48
+ channel_ssim = ssim(original_resized[:, :, channel], mosaic[:, :, channel], data_range=255)
49
+ ssim_values.append(channel_ssim)
50
+ return float(np.mean(ssim_values))
51
+ else:
52
+ return float(ssim(original_resized, mosaic, data_range=255))
53
+
54
+ def calculate_color_histogram_similarity(self, original: np.ndarray, mosaic: np.ndarray) -> float:
55
+ """Calculate color histogram similarity."""
56
+ if original.shape != mosaic.shape:
57
+ original_resized = cv2.resize(original, (mosaic.shape[1], mosaic.shape[0]))
58
+ else:
59
+ original_resized = original
60
+
61
+ correlations = []
62
+ for channel in range(3):
63
+ hist_orig = cv2.calcHist([original_resized], [channel], None, [256], [0, 256])
64
+ hist_mosaic = cv2.calcHist([mosaic], [channel], None, [256], [0, 256])
65
+ corr = cv2.compareHist(hist_orig, hist_mosaic, cv2.HISTCMP_CORREL)
66
+ correlations.append(corr)
67
+
68
+ return float(np.mean(correlations))
69
+
70
+ def evaluate_mosaic_quality(self, original: np.ndarray, mosaic: np.ndarray,
71
+ original_path: str = None) -> Dict[str, float]:
72
+ """Comprehensive quality evaluation."""
73
+ print("Calculating quality metrics...")
74
+
75
+ metrics = {
76
+ 'mse': self.calculate_mse(original, mosaic),
77
+ 'psnr': self.calculate_psnr(original, mosaic),
78
+ 'ssim': self.calculate_ssim(original, mosaic),
79
+ 'histogram_similarity': self.calculate_color_histogram_similarity(original, mosaic)
80
+ }
81
+
82
+ # Overall score
83
+ ssim_norm = (metrics['ssim'] + 1) / 2
84
+ psnr_norm = min(metrics['psnr'] / 50.0, 1.0)
85
+ hist_norm = metrics['histogram_similarity']
86
+ overall = (0.5 * ssim_norm + 0.3 * hist_norm + 0.2 * psnr_norm) * 100
87
+ metrics['overall_quality'] = float(overall)
88
+
89
+ self.metrics = metrics
90
+ self._print_report(metrics, original_path)
91
+ return metrics
92
+
93
+ def _print_report(self, metrics: Dict[str, float], original_path: str = None):
94
+ """Print quality assessment report."""
95
+ print(f"\n{'='*50}")
96
+ print("QUALITY ASSESSMENT")
97
+ print(f"{'='*50}")
98
+
99
+ if original_path:
100
+ print(f"Image: {original_path}")
101
+
102
+ print(f"MSE: {metrics['mse']:.2f} (lower = better)")
103
+ print(f"PSNR: {metrics['psnr']:.2f} dB (>30 = good)")
104
+ print(f"SSIM: {metrics['ssim']:.4f} (>0.8 = very good)")
105
+ print(f"Histogram: {metrics['histogram_similarity']:.4f}")
106
+ print(f"Overall Score: {metrics['overall_quality']:.1f}/100")
107
+
108
+ score = metrics['overall_quality']
109
+ if score >= 85:
110
+ quality = "Excellent"
111
+ elif score >= 75:
112
+ quality = "Very Good"
113
+ elif score >= 65:
114
+ quality = "Good"
115
+ else:
116
+ quality = "Fair"
117
+
118
+ print(f"Assessment: {quality}")
119
+ print(f"{'='*50}")
120
+
121
+ def visualize_quality_comparison(self, original: np.ndarray, mosaic: np.ndarray,
122
+ metrics: Dict[str, float] = None):
123
+ """Create visual comparison with metrics."""
124
+ if metrics is None:
125
+ metrics = self.metrics
126
+
127
+ print("Showing quality comparison...")
128
+
129
+ fig, axes = plt.subplots(2, 2, figsize=(16, 12))
130
+
131
+ # Original
132
+ axes[0, 0].imshow(original)
133
+ axes[0, 0].set_title(f'Original\n{original.shape[1]}×{original.shape[0]}')
134
+ axes[0, 0].axis('off')
135
+
136
+ # Mosaic
137
+ axes[0, 1].imshow(mosaic)
138
+ axes[0, 1].set_title(f'Mosaic\n{mosaic.shape[1]}×{mosaic.shape[0]}')
139
+ axes[0, 1].axis('off')
140
+
141
+ # Metrics
142
+ if metrics:
143
+ metrics_text = (f"Quality Metrics:\n\n"
144
+ f"MSE: {metrics['mse']:.1f}\n"
145
+ f"PSNR: {metrics['psnr']:.1f} dB\n"
146
+ f"SSIM: {metrics['ssim']:.4f}\n"
147
+ f"Histogram: {metrics['histogram_similarity']:.4f}\n\n"
148
+ f"Score: {metrics['overall_quality']:.1f}/100")
149
+
150
+ axes[1, 0].text(0.1, 0.5, metrics_text, fontsize=12, fontfamily='monospace', va='center')
151
+ axes[1, 0].set_title('Quality Assessment')
152
+ axes[1, 0].axis('off')
153
+
154
+ # Difference map
155
+ if original.shape == mosaic.shape:
156
+ diff_img = np.abs(original.astype(np.int16) - mosaic.astype(np.int16))
157
+ else:
158
+ original_resized = cv2.resize(original, (mosaic.shape[1], mosaic.shape[0]))
159
+ diff_img = np.abs(original_resized.astype(np.int16) - mosaic.astype(np.int16))
160
+
161
+ diff_enhanced = np.clip(diff_img * 3, 0, 255).astype(np.uint8)
162
+ axes[1, 1].imshow(diff_enhanced)
163
+ axes[1, 1].set_title('Difference Map')
164
+ axes[1, 1].axis('off')
165
+
166
+ plt.suptitle('Mosaic Quality Evaluation', fontsize=16)
167
+ plt.tight_layout()
168
+ plt.show()
app.py ADDED
@@ -0,0 +1,480 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ app.py
3
+ Deployment-only Gradio interface that NEVER builds caches during startup.
4
+ Uses only pre-built cache files for fast cloud deployment.
5
+ """
6
+
7
+ import gradio as gr
8
+ import numpy as np
9
+ import cv2
10
+ from pathlib import Path
11
+ import time
12
+ import pickle
13
+ import warnings
14
+ from typing import Tuple, Dict, Optional, List
15
+ from dataclasses import dataclass
16
+ from sklearn.cluster import MiniBatchKMeans
17
+ from sklearn.metrics.pairwise import euclidean_distances
18
+ from skimage.metrics import structural_similarity as ssim
19
+
20
+ # Deployment configuration
21
+ TILE_FOLDER = "extracted_images"
22
+
23
+ # Pre-built cache files that should be uploaded with the app
24
+ AVAILABLE_CACHES = {
25
+ 16: "cache_16x16_bins8_rot.pkl",
26
+ 32: "cache_32x32_bins8_rot.pkl",
27
+ 64: "cache_64x64_bins8_rot.pkl"
28
+ }
29
+
30
+ @dataclass
31
+ class ImageContext:
32
+ """Contextual analysis results."""
33
+ has_faces: bool
34
+ face_regions: List[Tuple[int, int, int, int]]
35
+ is_portrait: bool
36
+ is_landscape: bool
37
+ content_complexity: float
38
+
39
+ class DeploymentMosaicGenerator:
40
+ """Deployment-optimized mosaic generator that NEVER builds caches."""
41
+
42
+ def __init__(self, cache_file: str):
43
+ """Initialize with existing cache file only."""
44
+ self.cache_file = cache_file
45
+
46
+ # Load cache data
47
+ try:
48
+ with open(cache_file, 'rb') as f:
49
+ data = pickle.load(f)
50
+
51
+ self.tile_images = data['tile_images']
52
+ self.tile_colours = data['tile_colours']
53
+ self.tile_names = data['tile_names']
54
+ self.colour_palette = data['colour_palette']
55
+ self.colour_groups = data['colour_groups']
56
+ self.colour_indices = data['colour_indices']
57
+ self.tile_size = data['tile_size']
58
+
59
+ print(f"Loaded cache: {len(self.tile_images)} tiles, size {self.tile_size}")
60
+
61
+ except Exception as e:
62
+ raise RuntimeError(f"Failed to load cache {cache_file}: {e}")
63
+
64
+ # Face detection
65
+ try:
66
+ self.face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
67
+ except:
68
+ self.face_cascade = None
69
+
70
+ def analyze_context(self, image: np.ndarray) -> ImageContext:
71
+ """Basic context analysis for deployment."""
72
+ faces = []
73
+ has_faces = False
74
+
75
+ if self.face_cascade is not None:
76
+ try:
77
+ gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
78
+ detected_faces = self.face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(20, 20))
79
+ faces = [(x, y, w, h) for (x, y, w, h) in detected_faces]
80
+ has_faces = len(faces) > 0
81
+ except:
82
+ pass
83
+
84
+ # Basic scene classification
85
+ aspect_ratio = image.shape[1] / image.shape[0]
86
+ is_portrait = aspect_ratio < 1.2 and has_faces
87
+ is_landscape = aspect_ratio > 1.5
88
+
89
+ # Simple complexity measure
90
+ gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
91
+ edges = cv2.Canny(gray, 50, 150)
92
+ content_complexity = np.mean(edges) / 255.0
93
+
94
+ return ImageContext(
95
+ has_faces=has_faces,
96
+ face_regions=faces,
97
+ is_portrait=is_portrait,
98
+ is_landscape=is_landscape,
99
+ content_complexity=content_complexity
100
+ )
101
+
102
+ def find_best_tile(self, target_colour: np.ndarray) -> int:
103
+ """Find best matching tile using color subgroups."""
104
+ distances = euclidean_distances(target_colour.reshape(1, -1), self.colour_palette)[0]
105
+ target_bin = np.argmin(distances)
106
+
107
+ if target_bin in self.colour_indices:
108
+ index, tile_indices = self.colour_indices[target_bin]
109
+ _, indices = index.kneighbors(target_colour.reshape(1, -1), n_neighbors=1)
110
+ return tile_indices[indices[0][0]]
111
+
112
+ return 0
113
+
114
+ def create_mosaic(self, image: np.ndarray, grid_size: int, diversity_factor: float = 0.1) -> Tuple[np.ndarray, ImageContext]:
115
+ """Create mosaic with deployment-optimized processing."""
116
+ print(f"Creating {grid_size}x{grid_size} mosaic...")
117
+
118
+ # Analyze context
119
+ context = self.analyze_context(image)
120
+
121
+ # Preprocess image to fit grid
122
+ target_size = grid_size * self.tile_size[0]
123
+ h, w = image.shape[:2]
124
+ scale = max(target_size / w, target_size / h)
125
+ new_w, new_h = int(w * scale), int(h * scale)
126
+ resized = cv2.resize(image, (new_w, new_h))
127
+
128
+ # Center crop
129
+ start_x = (new_w - target_size) // 2
130
+ start_y = (new_h - target_size) // 2
131
+ processed_image = resized[start_y:start_y + target_size, start_x:start_x + target_size]
132
+
133
+ # Create mosaic
134
+ cell_size = target_size // grid_size
135
+ mosaic = np.zeros((grid_size * self.tile_size[1], grid_size * self.tile_size[0], 3), dtype=np.uint8)
136
+
137
+ usage_count = {} if diversity_factor > 0 else None
138
+
139
+ for i in range(grid_size):
140
+ for j in range(grid_size):
141
+ # Get cell color
142
+ start_y = i * cell_size
143
+ end_y = start_y + cell_size
144
+ start_x = j * cell_size
145
+ end_x = start_x + cell_size
146
+
147
+ cell = processed_image[start_y:end_y, start_x:end_x]
148
+ target_colour = np.mean(cell, axis=(0, 1))
149
+
150
+ # Find best tile
151
+ best_tile_idx = self.find_best_tile(target_colour)
152
+
153
+ # Apply diversity if enabled
154
+ if usage_count is not None:
155
+ tile_name = self.tile_names[best_tile_idx]
156
+ usage_penalty = usage_count.get(tile_name, 0) * diversity_factor
157
+
158
+ # Try alternative tiles if this one is overused
159
+ if usage_penalty > 2.0: # Simple diversity check
160
+ distances = euclidean_distances(target_colour.reshape(1, -1), self.tile_colours)[0]
161
+ sorted_indices = np.argsort(distances)
162
+ for idx in sorted_indices[1:6]: # Try next 5 best matches
163
+ alt_tile_name = self.tile_names[idx]
164
+ if usage_count.get(alt_tile_name, 0) < usage_count[tile_name]:
165
+ best_tile_idx = idx
166
+ break
167
+
168
+ usage_count[self.tile_names[best_tile_idx]] = usage_count.get(self.tile_names[best_tile_idx], 0) + 1
169
+
170
+ # Place tile
171
+ tile_start_y = i * self.tile_size[1]
172
+ tile_end_y = tile_start_y + self.tile_size[1]
173
+ tile_start_x = j * self.tile_size[0]
174
+ tile_end_x = tile_start_x + self.tile_size[0]
175
+
176
+ mosaic[tile_start_y:tile_end_y, tile_start_x:tile_end_x] = self.tile_images[best_tile_idx]
177
+
178
+ return mosaic, context
179
+
180
+ def calculate_metrics(original: np.ndarray, mosaic: np.ndarray) -> Dict[str, float]:
181
+ """Calculate the 4 core quality metrics."""
182
+ # Resize for comparison
183
+ if original.shape != mosaic.shape:
184
+ original_resized = cv2.resize(original, (mosaic.shape[1], mosaic.shape[0]))
185
+ else:
186
+ original_resized = original
187
+
188
+ # MSE
189
+ orig_float = original_resized.astype(np.float64)
190
+ mosaic_float = mosaic.astype(np.float64)
191
+ mse = float(np.mean((orig_float - mosaic_float) ** 2))
192
+
193
+ # PSNR
194
+ psnr = float(20 * np.log10(255.0 / np.sqrt(mse))) if mse > 0 else float('inf')
195
+
196
+ # SSIM
197
+ ssim_values = []
198
+ for channel in range(3):
199
+ channel_ssim = ssim(original_resized[:, :, channel], mosaic[:, :, channel], data_range=255)
200
+ ssim_values.append(channel_ssim)
201
+ ssim_score = float(np.mean(ssim_values))
202
+
203
+ # Histogram similarity
204
+ correlations = []
205
+ for channel in range(3):
206
+ hist_orig = cv2.calcHist([original_resized], [channel], None, [256], [0, 256])
207
+ hist_mosaic = cv2.calcHist([mosaic], [channel], None, [256], [0, 256])
208
+ corr = cv2.compareHist(hist_orig, hist_mosaic, cv2.HISTCMP_CORREL)
209
+ correlations.append(corr)
210
+ histogram_similarity = float(np.mean(correlations))
211
+
212
+ # Overall score
213
+ ssim_norm = (ssim_score + 1) / 2
214
+ psnr_norm = min(psnr / 50.0, 1.0)
215
+ overall = (0.5 * ssim_norm + 0.3 * histogram_similarity + 0.2 * psnr_norm) * 100
216
+
217
+ return {
218
+ 'mse': mse,
219
+ 'psnr': psnr,
220
+ 'ssim': ssim_score,
221
+ 'histogram_similarity': histogram_similarity,
222
+ 'overall_quality': float(overall)
223
+ }
224
+
225
+ def get_best_available_cache(requested_tile_size: int) -> Optional[str]:
226
+ """Get the best available cache for requested tile size."""
227
+ # Check exact match first
228
+ if requested_tile_size in AVAILABLE_CACHES:
229
+ cache_file = AVAILABLE_CACHES[requested_tile_size]
230
+ if Path(cache_file).exists():
231
+ return cache_file
232
+
233
+ # Find closest available cache
234
+ available_sizes = []
235
+ for size, cache_file in AVAILABLE_CACHES.items():
236
+ if Path(cache_file).exists():
237
+ available_sizes.append(size)
238
+
239
+ if not available_sizes:
240
+ return None
241
+
242
+ # Return closest match
243
+ closest_size = min(available_sizes, key=lambda x: abs(x - requested_tile_size))
244
+ return AVAILABLE_CACHES[closest_size]
245
+
246
+ def create_mosaic_interface(image, grid_size, tile_size, diversity_factor, enable_rotation, apply_quantization, n_colors):
247
+ """Main interface function - deployment optimized."""
248
+ if image is None:
249
+ return None, None, "Please upload an image first.", "Please upload an image first."
250
+
251
+ try:
252
+ start_time = time.time()
253
+
254
+ # Get appropriate cache file
255
+ cache_file = get_best_available_cache(tile_size)
256
+ if cache_file is None:
257
+ error_msg = f"No cache available for tile size {tile_size}x{tile_size}"
258
+ return None, None, error_msg, error_msg
259
+
260
+ # Initialize generator with existing cache
261
+ generator = DeploymentMosaicGenerator(cache_file)
262
+
263
+ # Basic color quantization if requested
264
+ if apply_quantization:
265
+ pixels = image.reshape(-1, 3)
266
+ sample_size = min(5000, len(pixels))
267
+ sampled_pixels = pixels[np.random.choice(len(pixels), sample_size, replace=False)]
268
+
269
+ with warnings.catch_warnings():
270
+ warnings.filterwarnings("ignore")
271
+ kmeans = MiniBatchKMeans(n_clusters=n_colors, batch_size=500, random_state=42)
272
+ kmeans.fit(sampled_pixels)
273
+ labels = kmeans.predict(pixels)
274
+
275
+ quantized_pixels = kmeans.cluster_centers_[labels]
276
+ image = quantized_pixels.reshape(image.shape).astype(np.uint8)
277
+
278
+ # Create mosaic
279
+ mosaic, context = generator.create_mosaic(image, grid_size, diversity_factor)
280
+
281
+ # Calculate metrics
282
+ metrics = calculate_metrics(image, mosaic)
283
+
284
+ total_time = time.time() - start_time
285
+
286
+ # Create comparison
287
+ comparison = np.hstack([
288
+ cv2.resize(image, (mosaic.shape[1]//2, mosaic.shape[0])),
289
+ cv2.resize(mosaic, (mosaic.shape[1]//2, mosaic.shape[0]))
290
+ ])
291
+
292
+ # Metrics display - only 4 core metrics
293
+ metrics_text = f"""PERFORMANCE METRICS
294
+
295
+ Mean Squared Error (MSE): {metrics['mse']:.2f}
296
+
297
+ Peak Signal-to-Noise Ratio (PSNR): {metrics['psnr']:.2f} dB
298
+
299
+ Structural Similarity Index (SSIM): {metrics['ssim']:.4f}
300
+
301
+ Color Histogram Similarity: {metrics['histogram_similarity']:.4f}
302
+
303
+ Overall Quality Score: {metrics['overall_quality']:.1f}/100"""
304
+
305
+ # Status
306
+ status = f"""Generation Successful!
307
+
308
+ Grid: {grid_size}x{grid_size} = {grid_size**2} tiles
309
+ Tile Size: {tile_size}x{tile_size} pixels
310
+ Processing Time: {total_time:.2f} seconds
311
+ Cache Used: {cache_file}
312
+
313
+ Contextual Analysis:
314
+ • Faces Detected: {len(context.face_regions)}
315
+ • Scene Type: {'Portrait' if context.is_portrait else 'Landscape' if context.is_landscape else 'General'}
316
+ • Content Complexity: {context.content_complexity:.3f}"""
317
+
318
+ return mosaic, comparison, metrics_text, status
319
+
320
+ except Exception as e:
321
+ error_msg = f"Error: {str(e)}"
322
+ return None, None, error_msg, error_msg
323
+
324
+ def verify_deployment_setup():
325
+ """Check deployment setup without building anything."""
326
+ available_caches = {}
327
+ total_size_mb = 0
328
+
329
+ for size, cache_file in AVAILABLE_CACHES.items():
330
+ if Path(cache_file).exists():
331
+ size_mb = Path(cache_file).stat().st_size / 1024 / 1024
332
+ available_caches[size] = size_mb
333
+ total_size_mb += size_mb
334
+
335
+ setup_msg = f"Found {len(available_caches)} cache files ({total_size_mb:.1f}MB total)"
336
+
337
+ return len(available_caches) > 0, setup_msg, available_caches
338
+
339
+ def get_system_status():
340
+ """System status for deployment."""
341
+ setup_ok, setup_msg, available_caches = verify_deployment_setup()
342
+
343
+ cache_list = ""
344
+ for size, size_mb in available_caches.items():
345
+ cache_list += f" {size}x{size}: {size_mb:.1f}MB\n"
346
+
347
+ status = f"""DEPLOYMENT STATUS
348
+ {'='*30}
349
+
350
+ Cache System: {'✅' if setup_ok else '❌'}
351
+ {setup_msg}
352
+
353
+ Available Caches:
354
+ {cache_list if cache_list else " None found"}
355
+
356
+ Smart Selection: System automatically uses the best
357
+ available cache for your chosen tile size.
358
+
359
+ INNOVATIONS INCLUDED
360
+ {'='*30}
361
+
362
+ 🧠 Contextual Awareness: Face detection, scene classification
363
+ 🔄 Multi-Orientation: Rotation variants in cache files
364
+ ⚡ Performance: Color subgrouping, Mini-Batch K-means
365
+ 📊 Quality Metrics: MSE, PSNR, SSIM, Histogram similarity
366
+
367
+ DEPLOYMENT OPTIMIZED
368
+ {'='*30}
369
+
370
+ • No cache building during startup
371
+ • Fast initialization with pre-built caches
372
+ • Lightweight processing for cloud deployment
373
+ • Maintains all core innovations"""
374
+
375
+ return status
376
+
377
+ def create_interface():
378
+ """Create deployment-optimized Gradio interface."""
379
+
380
+ css = """
381
+ .gradio-container { max-width: 100% !important; padding: 0 20px; }
382
+ .left-panel {
383
+ flex: 0 0 350px; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
384
+ padding: 25px; border-radius: 15px; box-shadow: 0 4px 15px rgba(0,0,0,0.1);
385
+ }
386
+ .right-panel {
387
+ flex: 1; background: white; padding: 25px; border-radius: 15px;
388
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
389
+ }
390
+ .metrics-display {
391
+ background: linear-gradient(145deg, #667eea 0%, #764ba2 100%);
392
+ color: white; padding: 20px; border-radius: 10px;
393
+ font-family: 'Courier New', monospace; font-size: 14px; line-height: 1.6;
394
+ }
395
+ """
396
+
397
+ with gr.Blocks(css=css, title="Advanced Mosaic Generator") as demo:
398
+
399
+ gr.Markdown("# Advanced Contextual Mosaic Generator")
400
+ gr.Markdown("AI-powered mosaic creation with contextual awareness and performance metrics.")
401
+
402
+ with gr.Accordion("System Status", open=False):
403
+ status_display = gr.Textbox(value=get_system_status(), lines=20, show_label=False)
404
+ gr.Button("Refresh Status").click(fn=get_system_status, outputs=status_display)
405
+
406
+ with gr.Row():
407
+ # Left Panel: Controls
408
+ with gr.Column(scale=0, min_width=350, elem_classes=["left-panel"]):
409
+ gr.Markdown("## Configuration")
410
+
411
+ # Generate button at top
412
+ generate_btn = gr.Button("Generate Advanced Mosaic", variant="primary", size="lg")
413
+
414
+ gr.Markdown("---")
415
+
416
+ # Image upload
417
+ input_image = gr.Image(type="numpy", label="Upload Image", height=200)
418
+
419
+ # Controls
420
+ grid_size = gr.Slider(16, 96, 32, step=8, label="Grid Size", info="Number of tiles per side")
421
+ tile_size = gr.Slider(16, 64, 32, step=16, label="Tile Size", info="Must match available cache")
422
+ diversity_factor = gr.Slider(0.0, 0.5, 0.15, step=0.05, label="Diversity", info="Tile variety")
423
+ enable_rotation = gr.Checkbox(label="Enable Rotation", value=False, info="Uses rotation variants if available")
424
+ apply_quantization = gr.Checkbox(label="Color Quantization", value=True)
425
+ n_colors = gr.Slider(4, 24, 12, step=2, label="Colors")
426
+
427
+ # Presets
428
+ gr.Markdown("### Quick Presets")
429
+ with gr.Row():
430
+ preset_fast = gr.Button("Fast", size="sm")
431
+ preset_quality = gr.Button("Quality", size="sm")
432
+
433
+ # Right Panel: Results
434
+ with gr.Column(scale=2, elem_classes=["right-panel"]):
435
+ gr.Markdown("## Results & Analysis")
436
+
437
+ with gr.Row():
438
+ with gr.Column():
439
+ gr.Markdown("### Generated Mosaic")
440
+ mosaic_output = gr.Image(height=300, show_label=False)
441
+
442
+ with gr.Column():
443
+ gr.Markdown("### Comparison")
444
+ comparison_output = gr.Image(height=300, show_label=False)
445
+
446
+ gr.Markdown("### Performance Metrics")
447
+ metrics_output = gr.Textbox(lines=8, elem_classes=["metrics-display"], show_label=False)
448
+
449
+ status_output = gr.Textbox(label="Status", lines=6)
450
+
451
+ # Connect functions
452
+ def fast_preset():
453
+ return 24, 32, 0.1, False, True, 8
454
+
455
+ def quality_preset():
456
+ return 48, 32, 0.15, False, True, 12
457
+
458
+ generate_btn.click(
459
+ fn=create_mosaic_interface,
460
+ inputs=[input_image, grid_size, tile_size, diversity_factor, enable_rotation, apply_quantization, n_colors],
461
+ outputs=[mosaic_output, comparison_output, metrics_output, status_output]
462
+ )
463
+
464
+ preset_fast.click(fn=fast_preset, outputs=[grid_size, tile_size, diversity_factor, enable_rotation, apply_quantization, n_colors])
465
+ preset_quality.click(fn=quality_preset, outputs=[grid_size, tile_size, diversity_factor, enable_rotation, apply_quantization, n_colors])
466
+
467
+ return demo
468
+
469
+ if __name__ == "__main__":
470
+ print("Advanced Mosaic Generator - Deployment Version")
471
+ print("Checking deployment setup...")
472
+
473
+ setup_ok, setup_msg, caches = verify_deployment_setup()
474
+ if setup_ok:
475
+ print(f"Deployment ready: {setup_msg}")
476
+ demo = create_interface()
477
+ demo.launch(server_name="0.0.0.0", server_port=7860, share=True)
478
+ else:
479
+ print(f"Deployment not ready: {setup_msg}")
480
+ print("Please upload the required cache files")
cache_16x16_bins8_rot.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f471f90618f8a5c431fa4e066f7b9c02ea698e74d21a2c1082c98a76a75d63fa
3
+ size 57528829
cache_32x32_bins8_rot.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c8a246f56373475cec1642fea61a1cb2ee0b1c5d9106869d380c9d464fc2ac94
3
+ size 213371389
contextual_Mosaic_Builder.py ADDED
@@ -0,0 +1,369 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ contextual_mosaic.py
3
+ Advanced contextual mosaic generator with face detection and rotation.
4
+ """
5
+
6
+ import numpy as np
7
+ import cv2
8
+ from pathlib import Path
9
+ from typing import Dict, Tuple, List, Optional
10
+ from dataclasses import dataclass
11
+ from sklearn.metrics.pairwise import euclidean_distances
12
+ from sklearn.cluster import MiniBatchKMeans
13
+ from tqdm import tqdm
14
+ import pickle
15
+ import warnings
16
+ import matplotlib.pyplot as plt
17
+
18
+ @dataclass
19
+ class ImageContext:
20
+ """Stores contextual analysis results."""
21
+ has_faces: bool
22
+ face_regions: List[Tuple[int, int, int, int]]
23
+ is_portrait: bool
24
+ is_landscape: bool
25
+ dominant_colors: np.ndarray
26
+ brightness_map: np.ndarray
27
+ edge_density_map: np.ndarray
28
+ content_complexity: float
29
+
30
+ class ContextualMosaicGenerator:
31
+ """Advanced mosaic generator with contextual awareness."""
32
+
33
+ def __init__(self, cache_file: str, tile_folder: str = "extracted_images",
34
+ tile_size: Tuple[int, int] = (32, 32),
35
+ colour_bins: int = 8,
36
+ enable_rotation: bool = True):
37
+ self.cache_file = cache_file
38
+ self.tile_folder = tile_folder
39
+ self.target_tile_size = tile_size
40
+ self.target_colour_bins = colour_bins
41
+ self.target_enable_rotation = enable_rotation
42
+
43
+ # Data containers
44
+ self.tile_images = None
45
+ self.tile_colours = None
46
+ self.tile_names = None
47
+ self.colour_palette = None
48
+ self.colour_groups = None
49
+ self.colour_indices = None
50
+ self.tile_size = None
51
+ self.enable_rotation = False
52
+
53
+ # Setup face detection
54
+ try:
55
+ self.face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
56
+ except:
57
+ print("Warning: Face detection not available")
58
+ self.face_cascade = None
59
+
60
+ self._load_or_build_cache()
61
+
62
+ def _load_or_build_cache(self):
63
+ """Load existing cache or build new one."""
64
+ if Path(self.cache_file).exists():
65
+ try:
66
+ if self._load_and_validate_cache():
67
+ print("Cache loaded successfully")
68
+ return
69
+ except Exception as e:
70
+ print(f"Cache error: {e}")
71
+
72
+ print("Building new cache...")
73
+ self._build_cache_automatically()
74
+
75
+ def _load_and_validate_cache(self) -> bool:
76
+ """Load and validate cache parameters."""
77
+ with open(self.cache_file, 'rb') as f:
78
+ data = pickle.load(f)
79
+
80
+ required_keys = ['tile_images', 'tile_colours', 'tile_names', 'colour_palette', 'colour_groups', 'colour_indices']
81
+ if not all(key in data for key in required_keys):
82
+ return False
83
+
84
+ # Validate parameters
85
+ if (data.get('tile_size') != self.target_tile_size or
86
+ data.get('colour_bins') != self.target_colour_bins or
87
+ data.get('enable_rotation') != self.target_enable_rotation):
88
+ print("Cache parameters don't match")
89
+ return False
90
+
91
+ # Load data
92
+ self.tile_images = data['tile_images']
93
+ self.tile_colours = data['tile_colours']
94
+ self.tile_names = data['tile_names']
95
+ self.colour_palette = data['colour_palette']
96
+ self.colour_groups = data['colour_groups']
97
+ self.colour_indices = data['colour_indices']
98
+ self.tile_size = data['tile_size']
99
+ self.enable_rotation = data.get('enable_rotation', False)
100
+
101
+ print(f"Loaded: {len(self.tile_images)} tiles")
102
+ return True
103
+
104
+ def _build_cache_automatically(self):
105
+ """Build cache using cache builder."""
106
+ try:
107
+ from Cache_Builder import TileCacheBuilder
108
+
109
+ print("Auto-building cache...")
110
+ builder = TileCacheBuilder(
111
+ tile_folder=self.tile_folder,
112
+ tile_size=self.target_tile_size,
113
+ colour_bins=self.target_colour_bins,
114
+ enable_rotation=self.target_enable_rotation
115
+ )
116
+
117
+ success = builder.build_cache(self.cache_file, force_rebuild=True)
118
+ if not success:
119
+ raise RuntimeError("Cache builder failed")
120
+
121
+ if not self._load_and_validate_cache():
122
+ raise RuntimeError("Failed to load new cache")
123
+
124
+ print("Cache built successfully!")
125
+
126
+ except Exception as e:
127
+ raise RuntimeError(f"Auto-cache failed: {e}")
128
+
129
+ def analyze_image_context(self, image: np.ndarray) -> ImageContext:
130
+ """Analyze image content for contextual tile selection."""
131
+ print("Analyzing context...")
132
+
133
+ # Face detection
134
+ faces = []
135
+ has_faces = False
136
+ if self.face_cascade is not None:
137
+ gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
138
+ detected_faces = self.face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30))
139
+ faces = [(x, y, w, h) for (x, y, w, h) in detected_faces]
140
+ has_faces = len(faces) > 0
141
+
142
+ # Scene classification
143
+ aspect_ratio = image.shape[1] / image.shape[0]
144
+ is_portrait = aspect_ratio < 1.2 and has_faces
145
+ is_landscape = aspect_ratio > 1.5
146
+
147
+ # Dominant colors
148
+ pixels = image.reshape(-1, 3)
149
+ sample_size = min(10000, len(pixels))
150
+ sampled_pixels = pixels[np.random.choice(len(pixels), sample_size, replace=False)]
151
+
152
+ with warnings.catch_warnings():
153
+ warnings.filterwarnings("ignore")
154
+ kmeans = MiniBatchKMeans(n_clusters=5, random_state=42, batch_size=1000)
155
+ kmeans.fit(sampled_pixels)
156
+ dominant_colors = kmeans.cluster_centers_
157
+
158
+ # Analysis maps
159
+ gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
160
+ brightness_map = cv2.resize(gray, (32, 32)) / 255.0
161
+ edges = cv2.Canny(gray, 50, 150)
162
+ edge_density_map = cv2.resize(edges, (32, 32)) / 255.0
163
+ content_complexity = np.mean(edge_density_map)
164
+
165
+ context = ImageContext(
166
+ has_faces=has_faces,
167
+ face_regions=faces,
168
+ is_portrait=is_portrait,
169
+ is_landscape=is_landscape,
170
+ dominant_colors=dominant_colors,
171
+ brightness_map=brightness_map,
172
+ edge_density_map=edge_density_map,
173
+ content_complexity=content_complexity
174
+ )
175
+
176
+ print(f"Context: {len(faces)} faces, {'Portrait' if is_portrait else 'Landscape' if is_landscape else 'General'}")
177
+ return context
178
+
179
+ def get_adaptive_tile_strategy(self, context: ImageContext, grid_pos: Tuple[int, int], grid_size: Tuple[int, int]) -> Dict:
180
+ """Get adaptive strategy based on content and position."""
181
+ row, col = grid_pos
182
+ grid_rows, grid_cols = grid_size
183
+
184
+ strategy = {
185
+ 'precision_factor': 1.0,
186
+ 'diversity_factor': 0.1,
187
+ 'allow_rotation': self.enable_rotation,
188
+ 'search_candidates': 5
189
+ }
190
+
191
+ # Face area adjustments
192
+ if context.has_faces:
193
+ for face_x, face_y, face_w, face_h in context.face_regions:
194
+ face_grid_x = int((face_x / 1024) * grid_cols)
195
+ face_grid_y = int((face_y / 1024) * grid_rows)
196
+ face_grid_w = max(1, int((face_w / 1024) * grid_cols))
197
+ face_grid_h = max(1, int((face_h / 1024) * grid_rows))
198
+
199
+ if (face_grid_x <= col <= face_grid_x + face_grid_w and
200
+ face_grid_y <= row <= face_grid_y + face_grid_h):
201
+ strategy['precision_factor'] = 2.0
202
+ strategy['diversity_factor'] = 0.05
203
+ strategy['allow_rotation'] = False
204
+ strategy['search_candidates'] = 10
205
+ break
206
+
207
+ # Edge adjustments
208
+ edge_row = min(int((row / grid_rows) * 32), 31)
209
+ edge_col = min(int((col / grid_cols) * 32), 31)
210
+ edge_density = context.edge_density_map[edge_row, edge_col]
211
+
212
+ if edge_density > 0.3:
213
+ strategy['precision_factor'] *= 1.5
214
+ strategy['search_candidates'] = min(strategy['search_candidates'] * 2, 15)
215
+
216
+ return strategy
217
+
218
+ def find_contextual_best_tile(self, target_colour: np.ndarray, context: ImageContext,
219
+ grid_pos: Tuple[int, int], grid_size: Tuple[int, int],
220
+ usage_count: Dict = None) -> int:
221
+ """Find best tile using contextual analysis."""
222
+ strategy = self.get_adaptive_tile_strategy(context, grid_pos, grid_size)
223
+
224
+ # Find color bin
225
+ distances = euclidean_distances(target_colour.reshape(1, -1), self.colour_palette)[0]
226
+ target_bin = np.argmin(distances)
227
+
228
+ if target_bin in self.colour_indices:
229
+ index, tile_indices = self.colour_indices[target_bin]
230
+
231
+ n_candidates = min(strategy['search_candidates'], len(tile_indices))
232
+ _, indices = index.kneighbors(target_colour.reshape(1, -1), n_neighbors=n_candidates)
233
+
234
+ best_score = float('inf')
235
+ best_tile_idx = tile_indices[indices[0][0]]
236
+
237
+ for local_idx in indices[0]:
238
+ global_tile_idx = tile_indices[local_idx]
239
+ tile_name = self.tile_names[global_tile_idx]
240
+
241
+ # Skip rotation in face areas
242
+ if not strategy['allow_rotation'] and '_rot' in tile_name:
243
+ continue
244
+
245
+ # Calculate score
246
+ tile_colour = self.tile_colours[global_tile_idx]
247
+ color_distance = np.linalg.norm(target_colour - tile_colour) * strategy['precision_factor']
248
+
249
+ usage_penalty = 0
250
+ if usage_count is not None:
251
+ usage_penalty = usage_count.get(tile_name, 0) * strategy['diversity_factor']
252
+
253
+ total_score = color_distance + usage_penalty
254
+
255
+ if total_score < best_score:
256
+ best_score = total_score
257
+ best_tile_idx = global_tile_idx
258
+
259
+ return best_tile_idx
260
+
261
+ return 0
262
+
263
+ def create_contextual_mosaic(self, image: np.ndarray, grid_size: Tuple[int, int],
264
+ diversity_factor: float = 0.1) -> Tuple[np.ndarray, ImageContext]:
265
+ """Create mosaic with contextual awareness."""
266
+ print("Creating contextual mosaic...")
267
+
268
+ # Analyze context
269
+ context = self.analyze_image_context(image)
270
+
271
+ # Setup grid
272
+ h, w = image.shape[:2]
273
+ grid_rows, grid_cols = grid_size
274
+ cell_h = h // grid_rows
275
+ cell_w = w // grid_cols
276
+
277
+ # Initialize mosaic
278
+ tile_h, tile_w = self.tile_size[1], self.tile_size[0]
279
+ mosaic = np.zeros((grid_rows * tile_h, grid_cols * tile_w, 3), dtype=np.uint8)
280
+
281
+ usage_count = {} if diversity_factor > 0 else None
282
+
283
+ print(f"Building {grid_rows}x{grid_cols} mosaic...")
284
+
285
+ for i in tqdm(range(grid_rows), desc="Creating mosaic"):
286
+ for j in range(grid_cols):
287
+ # Get cell color
288
+ start_y = i * cell_h
289
+ end_y = min(start_y + cell_h, h)
290
+ start_x = j * cell_w
291
+ end_x = min(start_x + cell_w, w)
292
+
293
+ cell = image[start_y:end_y, start_x:end_x]
294
+ target_colour = np.mean(cell, axis=(0, 1))
295
+
296
+ # Find best tile
297
+ best_tile_idx = self.find_contextual_best_tile(
298
+ target_colour, context, (i, j), (grid_rows, grid_cols), usage_count
299
+ )
300
+
301
+ # Place tile
302
+ tile_start_row = i * tile_h
303
+ tile_end_row = tile_start_row + tile_h
304
+ tile_start_col = j * tile_w
305
+ tile_end_col = tile_start_col + tile_w
306
+
307
+ mosaic[tile_start_row:tile_end_row, tile_start_col:tile_end_col] = self.tile_images[best_tile_idx]
308
+
309
+ # Update usage
310
+ if usage_count is not None:
311
+ tile_name = self.tile_names[best_tile_idx]
312
+ usage_count[tile_name] = usage_count.get(tile_name, 0) + 1
313
+
314
+ print("Contextual mosaic completed")
315
+ return mosaic, context
316
+
317
+ def visualize_context_analysis(self, image: np.ndarray, context: ImageContext):
318
+ """Display contextual analysis visualization."""
319
+ print("Showing context analysis...")
320
+
321
+ fig, axes = plt.subplots(2, 3, figsize=(18, 12))
322
+
323
+ # Original with face overlay
324
+ img_with_faces = image.copy()
325
+ for (x, y, w, h) in context.face_regions:
326
+ cv2.rectangle(img_with_faces, (x, y), (x+w, y+h), (255, 0, 0), 3)
327
+
328
+ axes[0, 0].imshow(img_with_faces)
329
+ axes[0, 0].set_title(f'Face Detection\n{len(context.face_regions)} faces')
330
+ axes[0, 0].axis('off')
331
+
332
+ # Brightness map
333
+ axes[0, 1].imshow(context.brightness_map, cmap='gray')
334
+ axes[0, 1].set_title('Brightness Map')
335
+ axes[0, 1].axis('off')
336
+
337
+ # Edge density
338
+ axes[0, 2].imshow(context.edge_density_map, cmap='hot')
339
+ axes[0, 2].set_title(f'Edge Density\nComplexity: {context.content_complexity:.3f}')
340
+ axes[0, 2].axis('off')
341
+
342
+ # Dominant colors
343
+ colors_display = context.dominant_colors.reshape(1, -1, 3).astype(np.uint8)
344
+ axes[1, 0].imshow(colors_display)
345
+ axes[1, 0].set_title('Dominant Colors')
346
+ axes[1, 0].axis('off')
347
+
348
+ # Scene info
349
+ scene_text = (f"Scene Analysis:\n\n"
350
+ f"Portrait: {context.is_portrait}\n"
351
+ f"Landscape: {context.is_landscape}\n"
352
+ f"Has Faces: {context.has_faces}\n"
353
+ f"Complexity: {context.content_complexity:.3f}")
354
+
355
+ axes[1, 1].text(0.5, 0.5, scene_text, ha='center', va='center',
356
+ fontsize=14, transform=axes[1, 1].transAxes)
357
+ axes[1, 1].set_title('Scene Classification')
358
+ axes[1, 1].axis('off')
359
+
360
+ # Color palette
361
+ if self.colour_palette is not None:
362
+ palette_display = self.colour_palette.reshape(1, -1, 3).astype(np.uint8)
363
+ axes[1, 2].imshow(palette_display)
364
+ axes[1, 2].set_title('Color Palette')
365
+ axes[1, 2].axis('off')
366
+
367
+ plt.suptitle('Contextual Analysis for Intelligent Tile Selection', fontsize=16)
368
+ plt.tight_layout()
369
+ plt.show()
extracted_images/img_2001.png ADDED
extracted_images/img_2002.png ADDED
extracted_images/img_2003.png ADDED
extracted_images/img_2004.png ADDED
extracted_images/img_2005.png ADDED
extracted_images/img_2006.png ADDED
extracted_images/img_2007.png ADDED
extracted_images/img_2008.png ADDED
extracted_images/img_2009.png ADDED
extracted_images/img_2010.png ADDED
extracted_images/img_2011.png ADDED
extracted_images/img_2012.png ADDED
extracted_images/img_2013.png ADDED
extracted_images/img_2014.png ADDED
extracted_images/img_2015.png ADDED
extracted_images/img_2016.png ADDED
extracted_images/img_2017.png ADDED
extracted_images/img_2018.png ADDED
extracted_images/img_2019.png ADDED
extracted_images/img_2020.png ADDED
extracted_images/img_2021.png ADDED
extracted_images/img_2022.png ADDED
extracted_images/img_2023.png ADDED
extracted_images/img_2024.png ADDED
extracted_images/img_2025.png ADDED
extracted_images/img_2026.png ADDED
extracted_images/img_2027.png ADDED
extracted_images/img_2028.png ADDED
extracted_images/img_2029.png ADDED
extracted_images/img_2030.png ADDED
extracted_images/img_2031.png ADDED
extracted_images/img_2032.png ADDED
extracted_images/img_2033.png ADDED
extracted_images/img_2034.png ADDED
extracted_images/img_2035.png ADDED
extracted_images/img_2036.png ADDED
extracted_images/img_2037.png ADDED
extracted_images/img_2038.png ADDED
extracted_images/img_2039.png ADDED
extracted_images/img_2040.png ADDED
extracted_images/img_2041.png ADDED
extracted_images/img_2042.png ADDED