Daksh17440's picture
Update app.py
f97d108 verified
Raw
History Blame Contribute Delete
8.96 kB
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())