| | """
|
| | app.py
|
| | Deployment-only Gradio interface with Numba optimizations and grid visualization.
|
| | Uses only pre-built cache files for fast cloud deployment.
|
| | Metrics calculated AFTER images are displayed for faster user experience.
|
| | """
|
| |
|
| | import gradio as gr
|
| | import numpy as np
|
| | import cv2
|
| | from pathlib import Path
|
| | import time
|
| | import pickle
|
| | import warnings
|
| | import sys
|
| | from typing import Tuple, Dict, Optional, List
|
| | from dataclasses import dataclass
|
| | from sklearn.cluster import MiniBatchKMeans
|
| | from sklearn.metrics.pairwise import euclidean_distances
|
| |
|
| |
|
| | SCRIPT_DIR = Path(__file__).parent
|
| | NUMBA_DIR = SCRIPT_DIR / "Numba_Scripts"
|
| | if NUMBA_DIR.exists() and str(NUMBA_DIR) not in sys.path:
|
| | sys.path.insert(0, str(NUMBA_DIR))
|
| |
|
| |
|
| | try:
|
| | from numba_optimizations import (
|
| | NUMBA_AVAILABLE,
|
| | extract_cell_colors_numba,
|
| | compute_squared_distances_numba,
|
| | assemble_mosaic_numba,
|
| | warmup_numba_functions
|
| | )
|
| | print(f"✅ Numba optimizations loaded (Available: {NUMBA_AVAILABLE})")
|
| | except ImportError:
|
| | NUMBA_AVAILABLE = False
|
| | print("⚠️ Numba optimizations not available - using NumPy fallback")
|
| |
|
| |
|
| | TILE_FOLDER = "extracted_images"
|
| |
|
| |
|
| | AVAILABLE_CACHES = {
|
| | 16: "cache_16x16_bins8_rot.pkl",
|
| | 32: "cache_32x32_bins8_rot.pkl",
|
| | 64: "cache_64x64_bins8_rot.pkl"
|
| | }
|
| |
|
| |
|
| | _last_generation = {}
|
| |
|
| | @dataclass
|
| | class ImageContext:
|
| | """Contextual analysis results."""
|
| | has_faces: bool
|
| | face_regions: List[Tuple[int, int, int, int]]
|
| | is_portrait: bool
|
| | is_landscape: bool
|
| | dominant_colors: np.ndarray
|
| | brightness_map: np.ndarray
|
| | edge_density_map: np.ndarray
|
| | content_complexity: float
|
| |
|
| |
|
| | class DeploymentMosaicGenerator:
|
| | """Deployment-optimized mosaic generator with Numba support."""
|
| |
|
| | def __init__(self, cache_file: str, use_numba: bool = True):
|
| | """Initialize with existing cache file only."""
|
| | self.cache_file = cache_file
|
| | self.use_numba = use_numba and NUMBA_AVAILABLE
|
| |
|
| |
|
| | try:
|
| | with open(cache_file, 'rb') as f:
|
| | data = pickle.load(f)
|
| |
|
| | self.tile_images = data['tile_images']
|
| | if not isinstance(self.tile_images, np.ndarray):
|
| | self.tile_images = np.array(self.tile_images, dtype=np.uint8)
|
| |
|
| | self.tile_colours = data['tile_colours']
|
| | self.tile_names = data['tile_names']
|
| | self.colour_palette = data['colour_palette']
|
| | self.colour_groups = data['colour_groups']
|
| |
|
| |
|
| | self.colour_indices = {}
|
| | for bin_id, (index, tile_indices) in data['colour_indices'].items():
|
| | if not isinstance(tile_indices, np.ndarray):
|
| | tile_indices = np.array(tile_indices, dtype=np.int32)
|
| | self.colour_indices[bin_id] = (index, tile_indices)
|
| |
|
| | self.tile_size = data['tile_size']
|
| |
|
| | print(f"Loaded cache: {len(self.tile_images)} tiles, size {self.tile_size}")
|
| |
|
| |
|
| | if self.use_numba:
|
| | print("Warming up Numba JIT...")
|
| | success = warmup_numba_functions(
|
| | self.tile_images,
|
| | self.tile_size[1],
|
| | self.tile_size[0]
|
| | )
|
| | if success:
|
| | print("✅ Numba JIT compiled and ready")
|
| | else:
|
| | print("⚠️ Numba warmup failed, using NumPy")
|
| | self.use_numba = False
|
| |
|
| | except Exception as e:
|
| | raise RuntimeError(f"Failed to load cache {cache_file}: {e}")
|
| |
|
| | def analyze_context(self, image: np.ndarray) -> ImageContext:
|
| | """Fast context analysis for deployment - Face detection DISABLED."""
|
| |
|
| | faces = []
|
| | has_faces = False
|
| |
|
| |
|
| | aspect_ratio = image.shape[1] / image.shape[0]
|
| | is_portrait = aspect_ratio < 1.0
|
| | is_landscape = aspect_ratio > 1.5
|
| |
|
| |
|
| | pixels = image.reshape(-1, 3)
|
| | sample_size = min(5000, len(pixels))
|
| | sample_indices = np.random.randint(0, len(pixels), size=sample_size)
|
| | sampled_pixels = pixels[sample_indices]
|
| |
|
| | with warnings.catch_warnings():
|
| | warnings.filterwarnings("ignore")
|
| | kmeans = MiniBatchKMeans(
|
| | n_clusters=3, random_state=42, batch_size=500,
|
| | n_init=1, max_iter=30
|
| | )
|
| | kmeans.fit(sampled_pixels)
|
| | dominant_colors = kmeans.cluster_centers_
|
| |
|
| |
|
| | brightness_map = np.zeros((16, 16), dtype=np.float32)
|
| | edge_density_map = np.zeros((16, 16), dtype=np.float32)
|
| | content_complexity = 0.0
|
| |
|
| | return ImageContext(
|
| | has_faces=False,
|
| | face_regions=[],
|
| | is_portrait=is_portrait,
|
| | is_landscape=is_landscape,
|
| | dominant_colors=dominant_colors,
|
| | brightness_map=brightness_map,
|
| | edge_density_map=edge_density_map,
|
| | content_complexity=0.0
|
| | )
|
| |
|
| | def create_mosaic_with_preprocessing(self, image: np.ndarray, grid_size: int,
|
| | diversity_factor: float = 0.1) -> Tuple[np.ndarray, np.ndarray, ImageContext]:
|
| | """Create mosaic with Numba optimization and preprocessing visualization."""
|
| | print(f"Creating {grid_size}x{grid_size} mosaic (Numba: {self.use_numba})...")
|
| |
|
| |
|
| | context = self.analyze_context(image)
|
| |
|
| |
|
| | target_size = grid_size * self.tile_size[0]
|
| | h, w = image.shape[:2]
|
| | scale = max(target_size / w, target_size / h)
|
| | new_w, new_h = int(w * scale), int(h * scale)
|
| | resized = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_LINEAR)
|
| |
|
| |
|
| | start_x = (new_w - target_size) // 2
|
| | start_y = (new_h - target_size) // 2
|
| | processed_image = resized[start_y:start_y + target_size, start_x:start_x + target_size].astype(np.uint8)
|
| |
|
| |
|
| | grid_visualization = self.create_grid_overlay(processed_image, grid_size)
|
| |
|
| |
|
| | cell_size = target_size // grid_size
|
| | tile_h, tile_w = self.tile_size[1], self.tile_size[0]
|
| |
|
| |
|
| | if self.use_numba:
|
| | all_cell_colors = extract_cell_colors_numba(processed_image, grid_size, grid_size)
|
| | else:
|
| |
|
| | cells = processed_image.reshape(grid_size, cell_size, grid_size, cell_size, 3)
|
| | cells = cells.transpose(0, 2, 1, 3, 4)
|
| | all_cell_colors = cells.mean(axis=(2, 3))
|
| |
|
| | flat_colors = all_cell_colors.reshape(-1, 3)
|
| |
|
| |
|
| | if self.use_numba:
|
| | all_distances = compute_squared_distances_numba(flat_colors, self.colour_palette)
|
| | else:
|
| | all_distances = euclidean_distances(flat_colors, self.colour_palette)
|
| |
|
| | best_bins = np.argmin(all_distances, axis=1)
|
| | best_tiles = np.zeros(len(flat_colors), dtype=np.int32)
|
| |
|
| |
|
| | for bin_id in np.unique(best_bins):
|
| | bin_id = int(bin_id)
|
| |
|
| | if bin_id not in self.colour_indices:
|
| | continue
|
| |
|
| | mask = best_bins == bin_id
|
| | bin_colors = flat_colors[mask]
|
| |
|
| | if len(bin_colors) == 0:
|
| | continue
|
| |
|
| | index, tile_indices = self.colour_indices[bin_id]
|
| | n_neighbors = min(5, len(tile_indices))
|
| |
|
| | if n_neighbors > 0:
|
| | _, indices = index.kneighbors(bin_colors, n_neighbors=n_neighbors)
|
| | best_tiles[mask] = tile_indices[indices[:, 0]]
|
| |
|
| | best_tiles_grid = best_tiles.reshape(grid_size, grid_size)
|
| |
|
| |
|
| | if self.use_numba and grid_size * grid_size > 256:
|
| | mosaic = assemble_mosaic_numba(
|
| | self.tile_images, best_tiles_grid,
|
| | grid_size, grid_size, tile_h, tile_w
|
| | )
|
| | else:
|
| |
|
| | selected_tiles = self.tile_images[best_tiles_grid]
|
| | mosaic = selected_tiles.transpose(0, 2, 1, 3, 4).reshape(
|
| | grid_size * tile_h, grid_size * tile_w, 3
|
| | )
|
| |
|
| | return mosaic, grid_visualization, context
|
| |
|
| | def create_grid_overlay(self, image: np.ndarray, grid_size: int) -> np.ndarray:
|
| | """Create visualization showing grid segmentation with enhanced styling."""
|
| | h, w = image.shape[:2]
|
| | cell_h = h // grid_size
|
| | cell_w = w // grid_size
|
| |
|
| | grid_image = image.copy()
|
| |
|
| | line_color = (255, 255, 255)
|
| | shadow_color = (0, 0, 0)
|
| | line_thickness = max(1, min(w, h) // 500)
|
| |
|
| |
|
| | for i in range(1, grid_size):
|
| | x = i * cell_w
|
| | cv2.line(grid_image, (x-1, 0), (x-1, h), shadow_color, line_thickness)
|
| | cv2.line(grid_image, (x+1, 0), (x+1, h), shadow_color, line_thickness)
|
| | cv2.line(grid_image, (x, 0), (x, h), line_color, line_thickness)
|
| |
|
| |
|
| | for i in range(1, grid_size):
|
| | y = i * cell_h
|
| | cv2.line(grid_image, (0, y-1), (w, y-1), shadow_color, line_thickness)
|
| | cv2.line(grid_image, (0, y+1), (w, y+1), shadow_color, line_thickness)
|
| | cv2.line(grid_image, (0, y), (w, y), line_color, line_thickness)
|
| |
|
| |
|
| | border_thickness = max(2, line_thickness * 2)
|
| | cv2.rectangle(grid_image, (0, 0), (w-1, h-1), line_color, border_thickness)
|
| | cv2.rectangle(grid_image, (border_thickness, border_thickness),
|
| | (w-border_thickness-1, h-border_thickness-1), shadow_color, 1)
|
| |
|
| |
|
| | font = cv2.FONT_HERSHEY_SIMPLEX
|
| | font_scale = max(0.5, min(w, h) / 800)
|
| | thickness = max(1, int(font_scale * 2))
|
| |
|
| | text_main = f"Grid: {grid_size}x{grid_size}"
|
| | text_sub = f"{grid_size**2} cells total"
|
| | text_cell = f"Cell size: {cell_w}x{cell_h}px"
|
| |
|
| | (main_w, main_h), _ = cv2.getTextSize(text_main, font, font_scale, thickness)
|
| | (sub_w, sub_h), _ = cv2.getTextSize(text_sub, font, font_scale * 0.7, thickness)
|
| | (cell_w_text, cell_h_text), _ = cv2.getTextSize(text_cell, font, font_scale * 0.6, thickness)
|
| |
|
| | padding = 10
|
| | bg_width = max(main_w, sub_w, cell_w_text) + padding * 2
|
| | bg_height = main_h + sub_h + cell_h_text + padding * 4
|
| |
|
| | overlay = grid_image.copy()
|
| | cv2.rectangle(overlay, (10, 10), (10 + bg_width, 10 + bg_height), (0, 0, 0), -1)
|
| | cv2.addWeighted(overlay, 0.7, grid_image, 0.3, 0, grid_image)
|
| | cv2.rectangle(grid_image, (10, 10), (10 + bg_width, 10 + bg_height), line_color, 1)
|
| |
|
| | y_offset = 10 + padding + main_h
|
| | cv2.putText(grid_image, text_main, (10 + padding, y_offset),
|
| | font, font_scale, line_color, thickness)
|
| |
|
| | y_offset += sub_h + padding // 2
|
| | cv2.putText(grid_image, text_sub, (10 + padding, y_offset),
|
| | font, font_scale * 0.7, (200, 200, 200), thickness)
|
| |
|
| | y_offset += cell_h_text + padding // 2
|
| | cv2.putText(grid_image, text_cell, (10 + padding, y_offset),
|
| | font, font_scale * 0.6, (180, 180, 180), thickness)
|
| |
|
| |
|
| | if grid_size <= 16:
|
| | for i in range(min(3, grid_size)):
|
| | for j in range(min(3, grid_size)):
|
| | x = j * cell_w + 5
|
| | y = i * cell_h + 15
|
| | cell_num = i * grid_size + j + 1
|
| |
|
| | cv2.circle(grid_image, (x + 10, y), 12, (0, 0, 0), -1)
|
| | cv2.circle(grid_image, (x + 10, y), 12, line_color, 1)
|
| | cv2.putText(grid_image, str(cell_num), (x + 5, y + 4),
|
| | font, 0.4, line_color, 1)
|
| |
|
| | return grid_image
|
| |
|
| |
|
| | def calculate_global_ssim(original: np.ndarray, mosaic: np.ndarray) -> float:
|
| | """Calculate Global SSIM."""
|
| | if original.shape != mosaic.shape:
|
| | original_resized = cv2.resize(original, (mosaic.shape[1], mosaic.shape[0]))
|
| | else:
|
| | original_resized = original
|
| |
|
| | orig_float = original_resized.astype(np.float64)
|
| | mosaic_float = mosaic.astype(np.float64)
|
| |
|
| | C1 = (0.01 * 255) ** 2
|
| | C2 = (0.03 * 255) ** 2
|
| |
|
| | global_ssim_values = []
|
| |
|
| | for channel in range(3):
|
| | orig_channel = orig_float[:, :, channel]
|
| | mosaic_channel = mosaic_float[:, :, channel]
|
| |
|
| | mu1 = np.mean(orig_channel)
|
| | mu2 = np.mean(mosaic_channel)
|
| |
|
| | sigma1_sq = np.var(orig_channel)
|
| | sigma2_sq = np.var(mosaic_channel)
|
| | sigma12 = np.mean((orig_channel - mu1) * (mosaic_channel - mu2))
|
| |
|
| | numerator = (2 * mu1 * mu2 + C1) * (2 * sigma12 + C2)
|
| | denominator = (mu1**2 + mu2**2 + C1) * (sigma1_sq + sigma2_sq + C2)
|
| |
|
| | channel_ssim = numerator / denominator
|
| | global_ssim_values.append(channel_ssim)
|
| |
|
| | return float(np.mean(global_ssim_values))
|
| |
|
| |
|
| | def calculate_global_color_similarity(original: np.ndarray, mosaic: np.ndarray) -> float:
|
| | """Calculate global color distribution similarity."""
|
| | if original.shape != mosaic.shape:
|
| | original_resized = cv2.resize(original, (mosaic.shape[1], mosaic.shape[0]))
|
| | else:
|
| | original_resized = original
|
| |
|
| | orig_mean = np.mean(original_resized, axis=(0, 1))
|
| | mosaic_mean = np.mean(mosaic, axis=(0, 1))
|
| |
|
| | orig_std = np.std(original_resized, axis=(0, 1))
|
| | mosaic_std = np.std(mosaic, axis=(0, 1))
|
| |
|
| | color_mean_diff = np.linalg.norm(orig_mean - mosaic_mean)
|
| | color_mean_sim = 1.0 / (1.0 + color_mean_diff / 255.0)
|
| |
|
| | color_std_diff = np.linalg.norm(orig_std - mosaic_std)
|
| | color_std_sim = 1.0 / (1.0 + color_std_diff / 255.0)
|
| |
|
| | global_color_sim = 0.6 * color_mean_sim + 0.4 * color_std_sim
|
| |
|
| | return float(global_color_sim)
|
| |
|
| |
|
| | def calculate_metrics(original: np.ndarray, mosaic: np.ndarray) -> Dict[str, float]:
|
| | """Calculate enhanced quality metrics with global SSIM."""
|
| | if original.shape != mosaic.shape:
|
| | original_resized = cv2.resize(original, (mosaic.shape[1], mosaic.shape[0]))
|
| | else:
|
| | original_resized = original
|
| |
|
| | orig_float = original_resized.astype(np.float64)
|
| | mosaic_float = mosaic.astype(np.float64)
|
| | mse = float(np.mean((orig_float - mosaic_float) ** 2))
|
| |
|
| | psnr = float(20 * np.log10(255.0 / np.sqrt(mse))) if mse > 0 else float('inf')
|
| |
|
| | global_ssim = calculate_global_ssim(original, mosaic)
|
| | global_color_sim = calculate_global_color_similarity(original, mosaic)
|
| |
|
| | correlations = []
|
| | for channel in range(3):
|
| | hist_orig = cv2.calcHist([original_resized], [channel], None, [256], [0, 256])
|
| | hist_mosaic = cv2.calcHist([mosaic], [channel], None, [256], [0, 256])
|
| | corr = cv2.compareHist(hist_orig, hist_mosaic, cv2.HISTCMP_CORREL)
|
| | correlations.append(corr)
|
| | histogram_similarity = float(np.mean(correlations))
|
| |
|
| | ssim_norm = (global_ssim + 1) / 2
|
| | psnr_norm = min(psnr / 50.0, 1.0)
|
| |
|
| | overall = (
|
| | 0.4 * ssim_norm +
|
| | 0.25 * global_color_sim +
|
| | 0.2 * histogram_similarity +
|
| | 0.15 * psnr_norm
|
| | ) * 100
|
| |
|
| | return {
|
| | 'mse': mse,
|
| | 'psnr': psnr,
|
| | 'ssim': global_ssim,
|
| | 'global_color_similarity': global_color_sim,
|
| | 'histogram_similarity': histogram_similarity,
|
| | 'overall_quality': float(overall)
|
| | }
|
| |
|
| |
|
| | def get_best_available_cache(requested_tile_size: int) -> Optional[str]:
|
| | """Get the best available cache for requested tile size."""
|
| | if requested_tile_size in AVAILABLE_CACHES:
|
| | cache_file = AVAILABLE_CACHES[requested_tile_size]
|
| | if Path(cache_file).exists():
|
| | return cache_file
|
| |
|
| | available_sizes = []
|
| | for size, cache_file in AVAILABLE_CACHES.items():
|
| | if Path(cache_file).exists():
|
| | available_sizes.append(size)
|
| |
|
| | if not available_sizes:
|
| | return None
|
| |
|
| | closest_size = min(available_sizes, key=lambda x: abs(x - requested_tile_size))
|
| | return AVAILABLE_CACHES[closest_size]
|
| |
|
| |
|
| | def create_mosaic_interface(image, grid_size, tile_size, diversity_factor,
|
| | enable_rotation, apply_quantization, n_colors):
|
| | """Main interface function - Returns images immediately, metrics calculated after."""
|
| | global _last_generation
|
| |
|
| | if image is None:
|
| | return None, None, None, "⏳ Generating...", "Please upload an image first."
|
| |
|
| | try:
|
| | start_time = time.time()
|
| |
|
| | cache_file = get_best_available_cache(tile_size)
|
| | if cache_file is None:
|
| | error_msg = f"No cache available for tile size {tile_size}x{tile_size}"
|
| | return None, None, None, error_msg, error_msg
|
| |
|
| |
|
| | generator = DeploymentMosaicGenerator(cache_file, use_numba=True)
|
| |
|
| |
|
| | if apply_quantization:
|
| | pixels = image.reshape(-1, 3)
|
| | sample_size = min(5000, len(pixels))
|
| | sampled_pixels = pixels[np.random.choice(len(pixels), sample_size, replace=False)]
|
| |
|
| | with warnings.catch_warnings():
|
| | warnings.filterwarnings("ignore")
|
| | kmeans = MiniBatchKMeans(n_clusters=n_colors, batch_size=500, random_state=42)
|
| | kmeans.fit(sampled_pixels)
|
| | labels = kmeans.predict(pixels)
|
| |
|
| | quantized_pixels = kmeans.cluster_centers_[labels]
|
| | image = quantized_pixels.reshape(image.shape).astype(np.uint8)
|
| |
|
| |
|
| | mosaic, grid_viz, context = generator.create_mosaic_with_preprocessing(
|
| | image, grid_size, diversity_factor
|
| | )
|
| |
|
| | total_time = time.time() - start_time
|
| |
|
| |
|
| | MAX_DISPLAY_SIZE = 1024
|
| |
|
| | if max(mosaic.shape[:2]) > MAX_DISPLAY_SIZE:
|
| | scale = MAX_DISPLAY_SIZE / max(mosaic.shape[:2])
|
| | new_h = int(mosaic.shape[0] * scale)
|
| | new_w = int(mosaic.shape[1] * scale)
|
| | mosaic_display = cv2.resize(mosaic, (new_w, new_h), interpolation=cv2.INTER_AREA)
|
| | else:
|
| | mosaic_display = mosaic
|
| |
|
| | if max(grid_viz.shape[:2]) > MAX_DISPLAY_SIZE:
|
| | scale = MAX_DISPLAY_SIZE / max(grid_viz.shape[:2])
|
| | new_h = int(grid_viz.shape[0] * scale)
|
| | new_w = int(grid_viz.shape[1] * scale)
|
| | grid_viz_display = cv2.resize(grid_viz, (new_w, new_h), interpolation=cv2.INTER_AREA)
|
| | else:
|
| | grid_viz_display = grid_viz
|
| |
|
| | comparison_h = min(mosaic.shape[0], MAX_DISPLAY_SIZE)
|
| | comparison_w_half = min(mosaic.shape[1]//2, MAX_DISPLAY_SIZE//2)
|
| |
|
| | comparison = np.hstack([
|
| | cv2.resize(image, (comparison_w_half, comparison_h), interpolation=cv2.INTER_AREA),
|
| | cv2.resize(mosaic, (comparison_w_half, comparison_h), interpolation=cv2.INTER_AREA)
|
| | ])
|
| |
|
| |
|
| | metrics_text = "⏳ Calculating quality metrics..."
|
| |
|
| | status = f"""Generation Successful!
|
| |
|
| | Grid: {grid_size}x{grid_size} = {grid_size**2} tiles
|
| | Tile Size: {tile_size}x{tile_size} pixels
|
| | Processing Time: {total_time:.2f} seconds
|
| | Cache Used: {cache_file}
|
| | Numba Optimization: {'✅ ACTIVE' if generator.use_numba else '❌ Inactive'}
|
| |
|
| | Contextual Analysis:
|
| | - Faces Detected: {len(context.face_regions)}
|
| | - Scene Type: {'Portrait' if context.is_portrait else 'Landscape' if context.is_landscape else 'General'}"""
|
| |
|
| |
|
| | _last_generation = {
|
| | 'original': image,
|
| | 'mosaic': mosaic,
|
| | 'generated': True
|
| | }
|
| |
|
| | return mosaic_display, comparison, grid_viz_display, metrics_text, status
|
| |
|
| | except Exception as e:
|
| | import traceback
|
| | error_msg = f"Error: {str(e)}\n{traceback.format_exc()}"
|
| | return None, None, None, error_msg, error_msg
|
| |
|
| |
|
| | def calculate_and_display_metrics():
|
| | """Calculate metrics after images are displayed."""
|
| | global _last_generation
|
| |
|
| | if not _last_generation.get('generated'):
|
| | return "No mosaic generated yet. Please generate a mosaic first."
|
| |
|
| | try:
|
| | original = _last_generation['original']
|
| | mosaic = _last_generation['mosaic']
|
| |
|
| |
|
| | metrics = calculate_metrics(original, mosaic)
|
| |
|
| | metrics_text = f"""ENHANCED PERFORMANCE METRICS
|
| |
|
| | Mean Squared Error (MSE): {metrics['mse']:.2f}
|
| |
|
| | Peak Signal-to-Noise Ratio (PSNR): {metrics['psnr']:.2f} dB
|
| |
|
| | Global Structural Similarity (SSIM): {metrics['ssim']:.4f}
|
| |
|
| | Global Color Similarity: {metrics['global_color_similarity']:.4f}
|
| |
|
| | Color Histogram Similarity: {metrics['histogram_similarity']:.4f}
|
| |
|
| | Overall Quality Score: {metrics['overall_quality']:.1f}/100"""
|
| |
|
| | return metrics_text
|
| |
|
| | except Exception as e:
|
| | return f"Error calculating metrics: {str(e)}"
|
| |
|
| |
|
| | def verify_deployment_setup():
|
| | """Check deployment setup."""
|
| | available_caches = {}
|
| | total_size_mb = 0
|
| |
|
| | for size, cache_file in AVAILABLE_CACHES.items():
|
| | if Path(cache_file).exists():
|
| | size_mb = Path(cache_file).stat().st_size / 1024 / 1024
|
| | available_caches[size] = size_mb
|
| | total_size_mb += size_mb
|
| |
|
| | setup_msg = f"Found {len(available_caches)} cache files ({total_size_mb:.1f}MB total)"
|
| |
|
| | return len(available_caches) > 0, setup_msg, available_caches
|
| |
|
| |
|
| | def get_system_status():
|
| | """System status for deployment."""
|
| | setup_ok, setup_msg, available_caches = verify_deployment_setup()
|
| |
|
| | cache_list = ""
|
| | for size, size_mb in available_caches.items():
|
| | cache_list += f" {size}x{size}: {size_mb:.1f}MB\n"
|
| |
|
| | numba_status = "✅ ACTIVE" if NUMBA_AVAILABLE else "❌ Not Available (using NumPy)"
|
| |
|
| | status = f"""DEPLOYMENT STATUS
|
| | {'='*30}
|
| |
|
| | Cache System: {'✅' if setup_ok else '❌'}
|
| | {setup_msg}
|
| |
|
| | Available Caches:
|
| | {cache_list if cache_list else " None found"}
|
| |
|
| | Numba Acceleration: {numba_status}
|
| |
|
| | Smart Selection: System automatically uses the best
|
| | available cache for your chosen tile size.
|
| |
|
| | INNOVATIONS INCLUDED
|
| | {'='*30}
|
| |
|
| | Performance: Numba JIT compilation (3-15x speedup)
|
| | Contextual Awareness: Scene classification
|
| | Color Optimization: Subgrouping, Mini-Batch K-means
|
| | Enhanced Metrics: Global SSIM, Color similarity analysis
|
| | Grid Visualization: Shows preprocessing segmentation
|
| |
|
| | DEPLOYMENT OPTIMIZED
|
| | {'='*30}
|
| |
|
| | - No cache building during startup
|
| | - Fast initialization with pre-built caches
|
| | - Numba JIT for computational bottlenecks
|
| | - Lightweight processing for cloud deployment
|
| | - Maintains all core innovations
|
| | - Global quality assessment
|
| | - Metrics calculated after display for speed"""
|
| |
|
| | return status
|
| |
|
| |
|
| | def create_interface():
|
| | """Create Gradio interface with Numba support."""
|
| |
|
| | css = """
|
| | .gradio-container { max-width: 100% !important; padding: 0 20px; }
|
| | .left-panel {
|
| | flex: 0 0 350px; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
| | padding: 25px; border-radius: 15px; box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
| | }
|
| | .right-panel {
|
| | flex: 1; background: white; padding: 25px; border-radius: 15px;
|
| | box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
| | }
|
| | .metrics-display {
|
| | background: linear-gradient(145deg, #667eea 0%, #764ba2 100%);
|
| | color: white; padding: 20px; border-radius: 10px;
|
| | font-family: 'Courier New', monospace; font-size: 14px; line-height: 1.6;
|
| | }
|
| | """
|
| |
|
| | with gr.Blocks(css=css, title="Advanced Mosaic Generator with Numba") as demo:
|
| |
|
| | gr.Markdown("# Advanced Contextual Mosaic Generator (Numba-Optimized)")
|
| | gr.Markdown("AI-powered mosaic creation with Numba JIT compilation, contextual awareness, grid visualization, and performance metrics.")
|
| |
|
| | with gr.Accordion("System Status", open=False):
|
| | status_display = gr.Textbox(value=get_system_status(), lines=26, show_label=False)
|
| | gr.Button("Refresh Status").click(fn=get_system_status, outputs=status_display)
|
| |
|
| | with gr.Row():
|
| |
|
| | with gr.Column(scale=0, min_width=350, elem_classes=["left-panel"]):
|
| | gr.Markdown("## Configuration")
|
| |
|
| | generate_btn = gr.Button("🚀 Generate Mosaic", variant="primary", size="lg")
|
| |
|
| | gr.Markdown("---")
|
| |
|
| | input_image = gr.Image(type="numpy", label="Upload Image", height=200)
|
| |
|
| | grid_size = gr.Slider(16, 128, 32, step=8, label="Grid Size", info="Number of tiles per side")
|
| | tile_size = gr.Slider(16, 64, 32, step=16, label="Tile Size", info="Must match available cache")
|
| | diversity_factor = gr.Slider(0.0, 0.5, 0.15, step=0.05, label="Diversity", info="Tile variety")
|
| | enable_rotation = gr.Checkbox(label="Enable Rotation", value=False, info="Uses rotation variants if available")
|
| | apply_quantization = gr.Checkbox(label="Color Quantization", value=False)
|
| | n_colors = gr.Slider(4, 24, 12, step=2, label="Colors")
|
| |
|
| | gr.Markdown("### Or, try with an example:")
|
| | gr.Examples(
|
| | examples=[
|
| | ["Images/EmmaPotrait.jpg", 64, 32, 0.15, False, False, 12],
|
| | ["Images/Batman.jpg", 128, 16, 0.05, False, False, 16],
|
| | ["Images/Indian_Dog.jpg", 56, 32, 0.2, False, False, 16],
|
| | ],
|
| | inputs=[input_image, grid_size, tile_size, diversity_factor,
|
| | enable_rotation, apply_quantization, n_colors]
|
| | )
|
| |
|
| | gr.Markdown("### Quick Presets")
|
| | with gr.Row():
|
| | preset_fast = gr.Button("⚡ Fast", size="sm")
|
| | preset_quality = gr.Button("💎 Quality", size="sm")
|
| |
|
| |
|
| | with gr.Column(scale=2, elem_classes=["right-panel"]):
|
| | gr.Markdown("## Results & Analysis")
|
| |
|
| | with gr.Row():
|
| | with gr.Column():
|
| | gr.Markdown("### Generated Mosaic")
|
| | mosaic_output = gr.Image(height=300, show_label=False)
|
| |
|
| | with gr.Column():
|
| | gr.Markdown("### Comparison (Original | Mosaic)")
|
| | comparison_output = gr.Image(height=300, show_label=False)
|
| |
|
| | with gr.Row():
|
| | with gr.Column():
|
| | gr.Markdown("### Grid Segmentation Visualization")
|
| | grid_viz_output = gr.Image(height=300, show_label=False)
|
| | gr.Markdown("*Shows how the image is divided into cells for tile placement*")
|
| |
|
| | with gr.Column():
|
| | gr.Markdown("### Performance Metrics")
|
| | metrics_output = gr.Textbox(lines=8, elem_classes=["metrics-display"], show_label=False)
|
| |
|
| | status_output = gr.Textbox(label="Generation Status", lines=6)
|
| |
|
| |
|
| | def fast_preset():
|
| | return 24, 32, 0.1, False, False, 8
|
| |
|
| | def quality_preset():
|
| | return 128, 16, 0.0, False, False, 24
|
| |
|
| |
|
| |
|
| | generate_btn.click(
|
| | fn=create_mosaic_interface,
|
| | inputs=[input_image, grid_size, tile_size, diversity_factor,
|
| | enable_rotation, apply_quantization, n_colors],
|
| | outputs=[mosaic_output, comparison_output, grid_viz_output,
|
| | metrics_output, status_output]
|
| | ).then(
|
| | fn=calculate_and_display_metrics,
|
| | inputs=[],
|
| | outputs=[metrics_output]
|
| | )
|
| |
|
| | preset_fast.click(fn=fast_preset, outputs=[grid_size, tile_size, diversity_factor,
|
| | enable_rotation, apply_quantization, n_colors])
|
| | preset_quality.click(fn=quality_preset, outputs=[grid_size, tile_size, diversity_factor,
|
| | enable_rotation, apply_quantization, n_colors])
|
| |
|
| | return demo
|
| |
|
| |
|
| | if __name__ == "__main__":
|
| | print("="*80)
|
| | print("Advanced Mosaic Generator - Numba-Optimized Deployment")
|
| | print("="*80)
|
| | print("Checking deployment setup...")
|
| |
|
| | setup_ok, setup_msg, caches = verify_deployment_setup()
|
| | print(f"Cache Status: {setup_msg}")
|
| | print(f"Numba Status: {'✅ Available' if NUMBA_AVAILABLE else '⚠️ Not available (using NumPy)'}")
|
| |
|
| | if setup_ok:
|
| | print("\n✅ Deployment ready!")
|
| | demo = create_interface()
|
| | demo.launch(server_name="0.0.0.0", server_port=7860, share=True)
|
| | else:
|
| | print(f"\n❌ Deployment not ready: {setup_msg}")
|
| | print("Please upload the required cache files") |