GenerateMosaic / app.py
Add preprocess files
0404f22
import gradio as gr
import cv2
import numpy as np
import os
import pickle
import math
from skimage.metrics import structural_similarity as ssim
# ----------------- Constants -----------------
DATASET_FOLDER = "Dataset" # (Not used directly now)
KD_TREE_PATH = "kdtree_dataset.pkl" # Path to the precomputed KDTree file
KD_TILE_SIZE = (50, 50) # Must match the tile size used when building the KDTree
# ----------------- Feature Extraction Functions -----------------
def compute_features(image):
"""
Compute a set of features for an image:
- Average Lab color (using a Gaussian-blurred version)
- Edge density using Canny edge detection (normalized)
- Texture measure using the standard deviation of the grayscale image (normalized)
- Average gradient magnitude computed via Sobel operators (normalized)
Returns: (avg_lab, avg_edge, avg_texture, avg_grad)
"""
blurred = cv2.GaussianBlur(image, (5, 5), 0)
img_lab = cv2.cvtColor(blurred, cv2.COLOR_RGB2LAB)
avg_lab = np.mean(img_lab, axis=(0, 1))
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
edges = cv2.Canny(gray, 100, 200)
avg_edge = np.mean(edges) / 255.0
avg_texture = np.std(gray) / 255.0
grad_x = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
grad_y = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)
grad_mag = np.sqrt(grad_x**2 + grad_y**2)
avg_grad = np.mean(grad_mag) / 255.0
return avg_lab, avg_edge, avg_texture, avg_grad
def compute_weighted_features(image):
"""
Compute the weighted feature vector for KDTree search.
The image should be resized to KD_TILE_SIZE before feature extraction.
Weights: for Lab channels use 1.0; for edge, texture, and gradient use 0.5
(implemented as multiplying by sqrt(0.5)).
"""
scale = np.array([1.0, 1.0, 1.0, math.sqrt(0.5), math.sqrt(0.5), math.sqrt(0.5)])
avg_lab, avg_edge, avg_texture, avg_grad = compute_features(image)
raw_feature = np.concatenate([avg_lab, [avg_edge, avg_texture, avg_grad]])
weighted_feature = raw_feature * scale
return weighted_feature
# ----------------- Utility Function -----------------
def ensure_min_size(image, min_size=7):
"""
Ensure that the image has at least a minimum size; if not, resize it.
"""
h, w = image.shape[:2]
if h < min_size or w < min_size:
new_w = max(min_size, w)
new_h = max(min_size, h)
image = cv2.resize(image, (new_w, new_h))
return image
# ----------------- Mosaic Generation Functions -----------------
def create_photo_mosaic(input_image, kdtree_path, num_tiles_y, progress=None):
"""
Create an image mosaic using a precomputed KDTree.
For each mosaic tile in the input image, the tile is resized to KD_TILE_SIZE,
its weighted features are computed, and the KDTree is queried to find the best match.
The matched dataset image is then resized to the tile’s actual size before placement.
"""
# Load the precomputed KDTree and dataset images
with open(kdtree_path, "rb") as f:
tree_data = pickle.load(f)
tree = tree_data['tree']
dataset_images = tree_data['images']
original_image = input_image.copy()
height, width, _ = original_image.shape
# Determine mosaic grid dimensions
tile_height = height // num_tiles_y
aspect_ratio = width / height
num_tiles_x = int(num_tiles_y * aspect_ratio)
tile_width = width // num_tiles_x
print(f"Adjusted number of tiles: {num_tiles_x} (width) x {num_tiles_y} (height)")
mosaic = np.zeros_like(original_image)
rows = list(range(0, height, tile_height))
cols = list(range(0, width, tile_width))
total_tiles = len(rows) * len(cols)
tile_count = 0
for y in rows:
for x in cols:
y_end = min(y + tile_height, height)
x_end = min(x + tile_width, width)
tile = original_image[y:y_end, x:x_end]
# Resize the tile to the KDTree tile size for feature extraction
tile_resized = cv2.resize(tile, KD_TILE_SIZE)
query_feature = compute_weighted_features(tile_resized)
# Query the KDTree for the best match (returns index)
dist, ind = tree.query([query_feature], k=1)
best_index = ind[0][0]
best_match = dataset_images[best_index]
# Resize the best match image to the current tile size and place it into the mosaic
best_match_resized = cv2.resize(best_match, (x_end - x, y_end - y))
mosaic[y:y_end, x:x_end] = best_match_resized
tile_count += 1
if progress is not None:
progress(tile_count / total_tiles)
# Save the final mosaic (convert from RGB to BGR for saving with cv2)
output_path = "mosaic_output.jpg"
cv2.imwrite(output_path, cv2.cvtColor(mosaic, cv2.COLOR_RGB2BGR))
return output_path
def create_color_mosaic(input_image, num_tiles_y, progress=None):
"""
Create a simple color mosaic by dividing the image into grid cells and filling
each cell with its average RGB color.
"""
original_image = input_image.copy()
height, width, _ = original_image.shape
tile_height = height // num_tiles_y
aspect_ratio = width / height
num_tiles_x = int(num_tiles_y * aspect_ratio)
tile_width = width // num_tiles_x
print(f"Adjusted number of tiles: {num_tiles_x} (width) x {num_tiles_y} (height)")
mosaic = np.zeros_like(original_image)
rows = list(range(0, height, tile_height))
cols = list(range(0, width, tile_width))
total_tiles = len(rows) * len(cols)
tile_count = 0
for y in rows:
for x in cols:
y_end = min(y + tile_height, height)
x_end = min(x + tile_width, width)
tile = original_image[y:y_end, x:x_end]
avg_color = np.mean(tile, axis=(0, 1)).astype(np.uint8)
mosaic[y:y_end, x:x_end] = avg_color
tile_count += 1
if progress is not None:
progress(tile_count / total_tiles)
output_path = "color_mosaic_output.jpg"
cv2.imwrite(output_path, cv2.cvtColor(mosaic, cv2.COLOR_RGB2BGR))
return output_path
# ----------------- Performance Metrics -----------------
def compute_mse(original, mosaic):
"""
Compute Mean Squared Error (MSE) between two images.
"""
original = original.astype("float")
mosaic = mosaic.astype("float")
err = np.sum((original - mosaic) ** 2)
mse = err / float(original.shape[0] * original.shape[1] * original.shape[2])
return mse
def compute_ssim_metric(original, mosaic):
"""
Compute Structural Similarity Index (SSIM) between two images.
"""
min_dim = min(original.shape[0], original.shape[1])
win_size = 7 if min_dim >= 7 else (min_dim if min_dim % 2 == 1 else min_dim - 1)
ssim_value, _ = ssim(original, mosaic, win_size=win_size, channel_axis=-1, full=True)
return ssim_value
# ----------------- Gradio Interface -----------------
def mosaic_gradio(input_image, num_tiles_y, mosaic_type, progress=gr.Progress()):
"""
Gradio interface function to generate and return the mosaic image along with performance metrics.
mosaic_type: "Color Mosaic" or "Image Mosaic"
Returns: (mosaic_image_file, performance_metrics_string)
"""
if mosaic_type == "Color Mosaic":
mosaic_path = create_color_mosaic(input_image, num_tiles_y, progress)
else:
mosaic_path = create_photo_mosaic(input_image, KD_TREE_PATH, num_tiles_y, progress)
mosaic_image = cv2.imread(mosaic_path)
if mosaic_image is None:
return None, "Error: Mosaic image could not be loaded."
mosaic_image = cv2.cvtColor(mosaic_image, cv2.COLOR_BGR2RGB)
input_for_metrics = ensure_min_size(input_image.copy())
mosaic_for_metrics = ensure_min_size(mosaic_image.copy())
mse_value = compute_mse(input_for_metrics, mosaic_for_metrics)
ssim_value = compute_ssim_metric(input_for_metrics, mosaic_for_metrics)
metrics_text = f"MSE: {mse_value:.2f}\nSSIM: {ssim_value:.4f}"
return mosaic_path, metrics_text
# ----------------- Gradio App Setup -----------------
examples = [
["input_images/1.jpg", 90, "Image Mosaic"],
["input_images/2.jpg", 90, "Image Mosaic"],
["input_images/3.jpg", 90, "Image Mosaic"],
["input_images/6.jpg", 90, "Image Mosaic"],
["input_images/7.jpg", 90, "Image Mosaic"],
["input_images/8.jpg", 90, "Image Mosaic"],
["input_images/9.jpg", 90, "Image Mosaic"],
["input_images/10.jpg", 90, "Image Mosaic"]
]
iface = gr.Interface(
fn=mosaic_gradio,
inputs=[
gr.Image(type="numpy", label="Upload Image"),
gr.Slider(10, 200, value=90, step=5, label="Number of Tiles (Height)"),
gr.Radio(choices=["Color Mosaic", "Image Mosaic"], label="Mosaic Type", value="Image Mosaic")
],
outputs=[
gr.Image(type="filepath", label="Generated Mosaic"),
gr.Textbox(label="Performance Metrics")
],
title="Photo Mosaic Generator",
description=("Upload an image, choose the number of tiles (height) and mosaic type. "
"Select 'Color Mosaic' for a mosaic using average colors, or 'Image Mosaic' to use dataset images "
"matched by color, edge density, texture, and gradient features. "
"After mosaic generation, performance metrics (MSE and SSIM) will be displayed."),
examples=examples
)
iface.launch()