import os import sqlite3 import random import numpy as np import gradio as gr from PIL import Image from pillow_heif import register_heif_opener from skimage.color import rgb2lab from scipy.spatial import cKDTree from huggingface_hub import hf_hub_download # Initialize specialized image support register_heif_opener() Image.MAX_IMAGE_PIXELS = None # ================= YOUR DATASET REPO ================= DATASET_REPO_ID = "Daksh17440/satellite_images_color_clustered" # ===================================================== print("šŸ“„ Downloading Brains from Dataset...") DB_PATH = hf_hub_download(repo_id=DATASET_REPO_ID, filename="mosaic_library/tiles.db", repo_type="dataset") CENTROIDS_FILE = hf_hub_download(repo_id=DATASET_REPO_ID, filename="mosaic_library/centroids.npy", repo_type="dataset") print("āš™ļø Booting Engine & Loading KD-Tree...") conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute("SELECT DISTINCT bucket_id FROM tiles") active_bucket_ids = [row[0] for row in cursor.fetchall()] bucket_inventory = {b_id: [] for b_id in active_bucket_ids} cursor.execute("SELECT bucket_id, filepath FROM tiles") for b_id, filepath in cursor.fetchall(): bucket_inventory[b_id].append(filepath) conn.close() all_centroids = np.load(CENTROIDS_FILE) active_centroids = all_centroids[active_bucket_ids] color_tree = cKDTree(active_centroids) print("āœ… Engine Ready!") def generate_mosaic(target_image_path, mode, print_w, print_dpi, tile_in, max_w, dig_tile, deep_patch, deep_out, progress=gr.Progress()): if target_image_path is None: return None, None, "āŒ Please upload an image." target_img = Image.open(target_image_path).convert('RGB') input_w, input_h = target_img.size # ================= THE DYNAMIC MATH ENGINE ================= if mode == "šŸ–Øļø Type 1: Printout": final_w_px = print_w * print_dpi OUTPUT_TILE_SIZE = round(tile_in * print_dpi) grid_columns = final_w_px / OUTPUT_TILE_SIZE PATCH_SIZE = max(1, round(input_w / grid_columns)) elif mode == "šŸ’» Type 2: Finer Digital": grid_columns = max_w // dig_tile OUTPUT_TILE_SIZE = int(dig_tile) PATCH_SIZE = max(2, round(input_w / grid_columns)) else: # šŸ”¬ Type 3: Deep Zoom PATCH_SIZE = int(deep_patch) OUTPUT_TILE_SIZE = int(deep_out) actual_cols = input_w // PATCH_SIZE actual_rows = input_h // PATCH_SIZE final_w = actual_cols * OUTPUT_TILE_SIZE final_h = actual_rows * OUTPUT_TILE_SIZE if final_w * final_h > 150_000_000: target_img.thumbnail((3000, 3000)) input_w, input_h = target_img.size actual_cols = input_w // PATCH_SIZE actual_rows = input_h // PATCH_SIZE final_w = actual_cols * OUTPUT_TILE_SIZE final_h = actual_rows * OUTPUT_TILE_SIZE final_canvas = Image.new('RGB', (final_w, final_h)) target_array = np.array(target_img) # Error Tracking Variables first_error = None failed_tiles_count = 0 # Error Tracking Variables first_error = None failed_tiles_count = 0 # NEW: In-Memory RAM Cache for lightning-fast pasting tile_cache = {} # ================= STITCHING LOOP ================= for row in range(actual_rows): progress(row / actual_rows, desc=f"Stitching Row {row + 1} of {actual_rows}...") for col in range(actual_cols): y1, y2 = row * PATCH_SIZE, (row + 1) * PATCH_SIZE x1, x2 = col * PATCH_SIZE, (col + 1) * PATCH_SIZE patch = target_array[y1:y2, x1:x2] mean_rgb = patch.mean(axis=(0, 1)).reshape(1, 1, 3).astype(np.uint8) mean_lab = rgb2lab(mean_rgb).reshape(3) _, active_index = color_tree.query(mean_lab) matched_bucket_id = active_bucket_ids[active_index] chosen_tile_path = random.choice(bucket_inventory[matched_bucket_id]) try: # If we have already downloaded and resized this exact tile, use it instantly! if chosen_tile_path in tile_cache: tile_img = tile_cache[chosen_tile_path] # Otherwise, fetch it, clean the path, resize it, and save it to the cache else: clean_path = chosen_tile_path if clean_path.startswith("./"): clean_path = clean_path[2:] if clean_path.startswith("mosaic_library/"): full_repo_path = clean_path else: full_repo_path = f"mosaic_library/{clean_path}" cached_tile_path = hf_hub_download( repo_id=DATASET_REPO_ID, filename=full_repo_path, repo_type="dataset" ) tile_img = Image.open(cached_tile_path).convert('RGB') tile_img = tile_img.resize((OUTPUT_TILE_SIZE, OUTPUT_TILE_SIZE), Image.Resampling.LANCZOS) # Store the finished image in RAM for next time tile_cache[chosen_tile_path] = tile_img # Paste it onto the canvas final_canvas.paste(tile_img, (col * OUTPUT_TILE_SIZE, row * OUTPUT_TILE_SIZE)) except Exception as e: failed_tiles_count += 1 if first_error is None: # Fallback so full_repo_path exists in the error message if it fails before assignment err_path = locals().get('full_repo_path', chosen_tile_path) first_error = f"{type(e).__name__}: {str(e)} | Attempted: {err_path}" # ================= FINAL EXPORT ================= progress(1.0, desc="Saving High-Res Output...") output_filename = "final_mosaic.jpg" try: final_canvas.save(output_filename, quality=95) except ValueError: output_filename = "final_mosaic.tiff" final_canvas.save(output_filename, format="TIFF") stats_msg = f"āœ… Mosaic Complete! \nGrid: {actual_cols}x{actual_rows} tiles.\nFinal Resolution: {final_w}x{final_h}px" # Append the error report to the UI if anything failed if failed_tiles_count > 0: stats_msg += f"\n\nāš ļø WARNING: {failed_tiles_count} tiles failed to load!" stats_msg += f"\nšŸ” TRACE: {first_error}" return final_canvas, output_filename, stats_msg # ================= GRADIO UI CONFIGURATION ================= with gr.Blocks() as demo: gr.Markdown("# šŸŒ GeoMosaic Engine") # TOP SECTION: 3 Columns with gr.Row(): with gr.Column(scale=1): img_input = gr.Image(type="filepath", label="Target Image") with gr.Column(scale=1): mode_input = gr.Radio( choices=["šŸ–Øļø Type 1: Printout", "šŸ’» Type 2: Finer Digital", "šŸ”¬ Type 3: Deep Zoom"], value="šŸ’» Type 2: Finer Digital", label="Select Mosaic Mode" ) submit_btn = gr.Button("Generate Mosaic", variant="primary") with gr.Column(scale=1): img_output = gr.Image(type="pil", label="Web Preview (Compressed)", interactive=False) file_output = gr.File(label="šŸ“„ Download High-Res Mosaic", interactive=False) stats_output = gr.Textbox(label="Build Stats", interactive=False) # BOTTOM SECTION: Full Width Parameters gr.Markdown("---") gr.Markdown("### āš™ļø Calculation Parameters") gr.Markdown("Adjust the targets for your selected mode. The engine will calculate the exact grid dynamically.") with gr.Row(): with gr.Column(): gr.Markdown("**Type 1: Print Targets**") print_w = gr.Number(value=24, label="Target Print Width (Inches)") print_dpi = gr.Number(value=300, label="Printer DPI") tile_in = gr.Number(value=0.25, label="Tile Size (Inches)") with gr.Column(): gr.Markdown("**Type 2: Digital Targets**") max_w = gr.Number(value=7680, label="Max Screen Width (px)") dig_tile = gr.Number(value=16, label="Digital Tile Size (px)") with gr.Column(): gr.Markdown("**Type 3: Deep Zoom Targets**") deep_patch = gr.Number(value=2, label="Original Image Sample (px)") deep_out = gr.Number(value=64, label="Output Tile Resolution (px)") submit_btn.click( fn=generate_mosaic, inputs=[img_input, mode_input, print_w, print_dpi, tile_in, max_w, dig_tile, deep_patch, deep_out], outputs=[img_output, file_output, stats_output] ) if __name__ == "__main__": demo.launch(theme=gr.themes.Soft())