GenerateMosaic / app.py
input images
81a80cc
raw
history blame
11.5 kB
import gradio as gr
import cv2
import numpy as np
import os
from skimage.metrics import structural_similarity as ssim
# Folder containing your dataset of tiles (small images)
DATASET_FOLDER = "Dataset"
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)
"""
# Apply Gaussian blur to reduce noise before computing Lab color
blurred = cv2.GaussianBlur(image, (5, 5), 0)
img_lab = cv2.cvtColor(blurred, cv2.COLOR_RGB2LAB)
avg_lab = np.mean(img_lab, axis=(0, 1))
# Convert to grayscale for edge and texture computations
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
# Edge density: apply Canny and normalize the average edge intensity
edges = cv2.Canny(gray, 100, 200)
avg_edge = np.mean(edges) / 255.0 # Normalized edge density
# Texture measure: standard deviation of grayscale values (normalized)
avg_texture = np.std(gray) / 255.0
# Gradient magnitude: using Sobel operators in x and y directions, then average
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 load_dataset_images(folder_path, tile_size):
"""
Loads images from a folder, resizes them to tile_size, and computes a set of features:
(RGB image, average Lab color, edge density, texture measure, gradient magnitude, image path)
"""
dataset = []
image_paths = [os.path.join(folder_path, img) for img in os.listdir(folder_path)
if img.lower().endswith(('.png', '.jpg', '.jpeg'))]
for img_path in image_paths:
img = cv2.imread(img_path)
if img is None:
continue # Skip unreadable images
# Resize the image to the given tile size
img = cv2.resize(img, tile_size)
# Convert from BGR to RGB
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# Compute the feature vector for this dataset image
avg_lab, avg_edge, avg_texture, avg_grad = compute_features(img)
dataset.append((img, avg_lab, avg_edge, avg_texture, avg_grad, img_path))
return dataset
def find_best_match(tile_features, dataset, weights=(1.0, 0.5, 0.5, 0.5)):
"""
Finds the best matching dataset image based on a weighted combination of:
- Color difference (in Lab space)
- Edge density difference
- Texture difference
- Gradient magnitude difference
The weights parameter is a tuple with weights for each feature in the same order.
"""
tile_lab, tile_edge, tile_texture, tile_grad = tile_features
min_dist = float('inf')
best_match = None
for data in dataset:
ds_img, ds_lab, ds_edge, ds_texture, ds_grad, ds_path = data
# Compute the difference for each feature:
color_diff = np.linalg.norm(tile_lab - ds_lab)
edge_diff = abs(tile_edge - ds_edge)
texture_diff = abs(tile_texture - ds_texture)
grad_diff = abs(tile_grad - ds_grad)
# Compute a weighted distance (using a Euclidean combination)
dist = np.sqrt(weights[0] * (color_diff ** 2) +
weights[1] * (edge_diff ** 2) +
weights[2] * (texture_diff ** 2) +
weights[3] * (grad_diff ** 2))
if dist < min_dist:
min_dist = dist
best_match = ds_img
return best_match
def create_photo_mosaic(input_image, dataset_folder, num_tiles_y, progress=None):
"""
Creates an image mosaic using dataset images. For each tile of the input image,
it computes a feature vector (color, edge, texture, gradient) and finds the best
matching dataset image based on these features.
"""
# Assume the uploaded image from Gradio is in RGB format
original_image = input_image.copy()
height, width, _ = original_image.shape
# Compute tile height and determine tile width based on aspect ratio
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)")
# Load the dataset images with the new feature set
dataset = load_dataset_images(dataset_folder, (tile_width, tile_height))
if not dataset:
print("No images found in dataset folder!")
return None
# Create an empty mosaic image in RGB
mosaic = np.zeros_like(original_image)
# Calculate the grid ranges and total tile count for progress tracking
rows = list(range(0, height, tile_height))
cols = list(range(0, width, tile_width))
total_tiles = len(rows) * len(cols)
tile_count = 0
# Process each tile of the input image
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]
# Compute feature vector for the tile
tile_features = compute_features(tile)
# Find the best matching dataset image using the combined feature metric
best_match = find_best_match(tile_features, dataset)
if best_match is not None:
# Crop the dataset image if necessary to match the tile size
mosaic[y:y_end, x:x_end] = best_match[:y_end - y, :x_end - x]
tile_count += 1
if progress is not None:
progress(tile_count / total_tiles)
# Save the final mosaic. Since mosaic is in RGB, convert to BGR for cv2.imwrite.
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):
"""
Creates 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 Functions -----------------
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(original, mosaic):
"""
Compute Structural Similarity Index (SSIM) between two images.
"""
min_dim = min(original.shape[0], original.shape[1])
if min_dim >= 7:
win_size = 7
else:
# Ensure the window size is odd.
win_size = 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
def ensure_min_size(image, min_size=7):
"""
Ensure that the image has 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
# ----------------- Gradio Interface Function -----------------
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: Either "Color Mosaic" or "Image Mosaic"
Returns: (mosaic_image_file, performance_metrics_string)
"""
# Generate mosaic based on selected type
if mosaic_type == "Color Mosaic":
mosaic_path = create_color_mosaic(input_image, num_tiles_y, progress)
else:
mosaic_path = create_photo_mosaic(input_image, DATASET_FOLDER, num_tiles_y, progress)
# Load the mosaic image from file (convert from BGR to RGB)
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)
# Ensure both images meet minimum size requirements for metric calculations
input_for_metrics = ensure_min_size(input_image.copy())
mosaic_for_metrics = ensure_min_size(mosaic_image.copy())
# Compute performance metrics
mse_value = compute_mse(input_for_metrics, mosaic_for_metrics)
ssim_value = compute_ssim(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 -----------------
# Adding examples so that test images appear as clickable examples.
# Adjust the paths as needed.
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()