Spaces:
Sleeping
Sleeping
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() |