Spaces:
Sleeping
Sleeping
from huggingface_hub import hf_hub_download, login | |
import cv2 | |
import numpy as np | |
import pickle # for loading tile features and raw images | |
from skimage.feature import local_binary_pattern, graycomatrix, graycoprops, hog | |
from skimage.metrics import structural_similarity as ssim, peak_signal_noise_ratio as psnr | |
from PIL import Image | |
import gradio as gr | |
import time | |
import os | |
# --------------------------------------------------------------------- | |
# Feature Extraction Functions | |
# --------------------------------------------------------------------- | |
def get_average_color(image): | |
"""Compute the average color (per channel) of the image (BGR format).""" | |
return np.mean(image, axis=(0, 1)) | |
def get_color_histogram(image, bins=(8, 8, 8)): | |
"""Compute a normalized color histogram in HSV color space.""" | |
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) | |
hist = cv2.calcHist([hsv], [0, 1, 2], None, bins, [0, 180, 0, 256, 0, 256]) | |
cv2.normalize(hist, hist) | |
return hist.flatten() | |
def get_lbp_histogram(image, numPoints=24, radius=8, bins=59): | |
"""Compute a histogram of Local Binary Patterns (LBP) from the grayscale image.""" | |
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) | |
lbp = local_binary_pattern(gray, numPoints, radius, method="uniform") | |
hist, _ = np.histogram(lbp.ravel(), bins=bins, range=(0, bins)) | |
hist = hist.astype("float") | |
hist /= (hist.sum() + 1e-7) | |
return hist | |
def get_glcm_features(image, distances=[1, 2, 4], angles=[0, np.pi/4, np.pi/2, 3*np.pi/4], | |
properties=('contrast', 'dissimilarity', 'homogeneity', 'energy', 'correlation', 'ASM')): | |
""" | |
Compute GLCM (Gray Level Co-occurrence Matrix) features (Haralick features). | |
Returns a concatenated feature vector of all requested properties, for each distance & angle. | |
""" | |
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) | |
glcm = graycomatrix(gray, distances=distances, angles=angles, levels=256, | |
symmetric=True, normed=True) | |
feats = [] | |
for prop in properties: | |
vals = graycoprops(glcm, prop) | |
feats.append(vals.ravel()) | |
return np.hstack(feats) | |
def get_hog_features(image, orientations=9, pixels_per_cell=(8, 8), | |
cells_per_block=(2, 2), block_norm='L2-Hys'): | |
""" | |
Compute Histogram of Oriented Gradients (HOG) from the grayscale image. | |
The image is forcibly resized to 16×16 to avoid errors. | |
""" | |
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) | |
return hog(gray, orientations=orientations, pixels_per_cell=pixels_per_cell, | |
cells_per_block=(2, 2), block_norm=block_norm) | |
def get_combined_features(image): | |
""" | |
Compute and combine all features in the following order: | |
- Average Color (3) | |
- HSV Color Histogram (512) | |
- LBP Histogram (59) | |
- GLCM Features (72) | |
- HOG Features (36) | |
Total length = 682. | |
""" | |
avg_color = get_average_color(image) | |
color_hist = get_color_histogram(image) | |
lbp_hist = get_lbp_histogram(image) | |
glcm_feats = get_glcm_features(image) | |
hog_feats = get_hog_features(cv2.resize(image, (16, 16), interpolation=cv2.INTER_LINEAR)) | |
return np.concatenate([avg_color, color_hist, lbp_hist, glcm_feats, hog_feats]) | |
# --------------------------------------------------------------------- | |
# Feature Dictionary and Order | |
# --------------------------------------------------------------------- | |
FEATURES = { | |
"Average Color (Color, Fast)": { | |
"func": get_average_color, | |
"range": (0, 3) | |
}, | |
"HSV Histogram (Color Dist., Slow)": { | |
"func": get_color_histogram, | |
"range": (3, 515) | |
}, | |
"LBP Histogram (Texture, Normal)": { | |
"func": get_lbp_histogram, | |
"range": (515, 574) | |
}, | |
"GLCM Features (Texture Stats, Very Slow)": { | |
"func": get_glcm_features, | |
"range": (574, 646) | |
}, | |
"HOG Features (Edges/Shapes, Normal)": { | |
"func": lambda image: get_hog_features(cv2.resize(image, (16, 16), interpolation=cv2.INTER_LINEAR)), | |
"range": (646, 682) | |
} | |
} | |
FEATURE_ORDER = list(FEATURES.keys()) | |
def get_selected_features(image, selected_features): | |
""" | |
Compute and combine only the selected features from the image. | |
Uses the canonical order defined in FEATURE_ORDER. | |
""" | |
feats = [] | |
for feat in FEATURE_ORDER: | |
if feat in selected_features: | |
feats.append(FEATURES[feat]["func"](image)) | |
if not feats: | |
return np.array([], dtype=np.float32) | |
return np.concatenate(feats).astype(np.float32) | |
# --------------------------------------------------------------------- | |
# Load Precomputed Tile Features & Raw Images | |
# --------------------------------------------------------------------- | |
try: | |
with open("tile_features.pkl", "rb") as f: | |
data = pickle.load(f) | |
tile_features = data["features"] # shape: (num_tiles, 682) | |
tile_paths = data["paths"] # e.g. "image_dataset/21837.jpg" | |
print(f"Loaded {len(tile_paths)} tile features from tile_features.pkl") | |
except Exception as e: | |
print("Error loading tile features from local file:", e) | |
tile_features = None | |
tile_paths = None | |
try: | |
with open("tile_images_raw.pkl", "rb") as f: | |
raw_images_dict = pickle.load(f) | |
print(f"Loaded raw images dictionary with {len(raw_images_dict)} entries.") | |
except Exception as e: | |
print("Error loading raw images dictionary:", e) | |
raw_images_dict = {} | |
def get_tile_image(tile_path): | |
""" | |
Given a tile image path from the features pickle (e.g. "image_dataset\\21837.jpg"), | |
decode it from the raw_images_dict. Expects tile to be ~150×150. | |
""" | |
fixed_path = tile_path.replace("\\", "/").strip() | |
if fixed_path in raw_images_dict: | |
raw_bytes = raw_images_dict[fixed_path] | |
np_arr = np.frombuffer(raw_bytes, np.uint8) | |
img = cv2.imdecode(np_arr, cv2.IMREAD_COLOR) | |
if img is None: | |
print(f"cv2.imdecode failed for: {fixed_path}") | |
return img | |
else: | |
print(f"Tile image '{fixed_path}' not found.") | |
return None | |
# --------------------------------------------------------------------- | |
# Mosaic Generation Function (No tile scaling, with Output Scale) | |
# --------------------------------------------------------------------- | |
def mosaic_generator(user_img, block_size, output_scale=1.0, | |
weight_avg_color=1.0, weight_hsv_hist=1.0, | |
weight_lbp=1.0, weight_glcm=1.0, weight_hog=1.0): | |
""" | |
Create a photomosaic using 150×150 tiles with no tile scaling. | |
For each block (block_size x block_size) in the cropped user image, compute the selected features | |
and perform a weighted linear search over the tile_features subset. | |
Each block is replaced by one 150×150 tile, so the final mosaic dimensions are: | |
(grid_rows * 150) x (grid_cols * 150). | |
The final mosaic is optionally rescaled by output_scale (range: 0.1 to 1.0; default 1.0). | |
Performance metrics (MSE, SSIM, PSNR) compare the original cropped image with a downsized version | |
of the mosaic. | |
""" | |
start_time = time.time() | |
# Build a dictionary of feature weights. | |
feature_weights = { | |
"Average Color (Color, Fast)": weight_avg_color, | |
"HSV Histogram (Color Dist., Slow)": weight_hsv_hist, | |
"LBP Histogram (Texture, Normal)": weight_lbp, | |
"GLCM Features (Texture Stats, Very Slow)": weight_glcm, | |
"HOG Features (Edges/Shapes, Normal)": weight_hog | |
} | |
effective_features = [f for f in FEATURE_ORDER if feature_weights.get(f, 0) > 0] | |
if not effective_features: | |
return "Error: All features have weight = 0. Please enable at least one feature.", "" | |
# Build the tile_feature subset for only the selected features. | |
selected_indices = [] | |
weights_list = [] | |
for feat in FEATURE_ORDER: | |
if feat in effective_features: | |
start_idx, end_idx = FEATURES[feat]["range"] | |
selected_indices.extend(range(start_idx, end_idx)) | |
w = feature_weights[feat] | |
weights_list.extend([w] * (end_idx - start_idx)) | |
weights_vector = np.array(weights_list, dtype=np.float32) | |
if tile_features is None or tile_paths is None: | |
return "Error: Tile features are not loaded or incompatible.", "" | |
tile_subset = tile_features[:, selected_indices].astype(np.float32) | |
# Crop the user image to multiples of block_size. | |
user_img_bgr = cv2.cvtColor(np.array(user_img), cv2.COLOR_RGB2BGR) | |
h, w, _ = user_img_bgr.shape | |
new_h = (h // block_size) * block_size | |
new_w = (w // block_size) * block_size | |
user_img_bgr = user_img_bgr[:new_h, :new_w] | |
grid_rows = new_h // block_size | |
grid_cols = new_w // block_size | |
# Save a copy in RGB for final metrics. | |
original_cropped_rgb = cv2.cvtColor(user_img_bgr, cv2.COLOR_BGR2RGB) | |
mosaic_grid = [] | |
progress = gr.Progress() # Row-by-row progress bar | |
for row in range(grid_rows): | |
row_tiles = [] | |
for col in range(grid_cols): | |
y = row * block_size | |
x = col * block_size | |
block = user_img_bgr[y:y+block_size, x:x+block_size] | |
# Compute only the selected features from this block. | |
query_feats = get_selected_features(block, effective_features) | |
if query_feats.size == 0: | |
best_tile = np.zeros((150, 150, 3), dtype=np.uint8) | |
row_tiles.append(best_tile) | |
continue | |
query_feats = query_feats.reshape(1, -1) | |
query_weighted = query_feats * weights_vector | |
tile_subset_weighted = tile_subset * weights_vector | |
dists = np.linalg.norm(tile_subset_weighted - query_weighted, axis=1) | |
best_idx = np.argmin(dists) | |
best_tile_path = tile_paths[best_idx] | |
best_tile = get_tile_image(best_tile_path) | |
if best_tile is None: | |
best_tile = np.zeros((150, 150, 3), dtype=np.uint8) | |
else: | |
if best_tile.shape[:2] != (150, 150): | |
best_tile = cv2.resize(best_tile, (150, 150), interpolation=cv2.INTER_AREA) | |
row_tiles.append(best_tile) | |
row_image = np.hstack(row_tiles) | |
mosaic_grid.append(row_image) | |
progress((row + 1) / grid_rows, desc=f"Processed row {row+1}/{grid_rows}") | |
mosaic_bgr = np.vstack(mosaic_grid) | |
mosaic_rgb = cv2.cvtColor(mosaic_bgr, cv2.COLOR_BGR2RGB) | |
# Rescale mosaic output if output_scale is not 1.0. | |
if output_scale != 1.0: | |
out_w = int(mosaic_rgb.shape[1] * output_scale) | |
out_h = int(mosaic_rgb.shape[0] * output_scale) | |
mosaic_rgb = cv2.resize(mosaic_rgb, (out_w, out_h), interpolation=cv2.INTER_LINEAR) | |
end_time = time.time() | |
processing_time = end_time - start_time | |
total_blocks = grid_rows * grid_cols | |
# For performance metrics, downsize the mosaic to match original cropped dimensions. | |
orig_h, orig_w, _ = original_cropped_rgb.shape | |
mosaic_resized_for_metrics = cv2.resize(mosaic_rgb, (orig_w, orig_h), interpolation=cv2.INTER_AREA) | |
mse_val = np.mean((original_cropped_rgb.astype(np.float32) - mosaic_resized_for_metrics.astype(np.float32)) ** 2) | |
ssim_val = ssim(original_cropped_rgb, mosaic_resized_for_metrics, channel_axis=-1, win_size=3) | |
psnr_val = psnr(original_cropped_rgb, mosaic_resized_for_metrics) | |
metrics = ( | |
f"Processing Time: {processing_time:.2f} seconds\n" | |
f"Grid Dimensions: {grid_rows} rows x {grid_cols} columns\n" | |
f"Total Blocks Processed: {total_blocks}\n" | |
f"MSE: {mse_val:.2f}\n" | |
f"SSIM: {ssim_val:.4f}\n" | |
f"PSNR: {psnr_val:.2f} dB\n" | |
) | |
return mosaic_rgb, metrics | |
# --------------------------------------------------------------------- | |
# Gradio Interface | |
# --------------------------------------------------------------------- | |
iface = gr.Interface( | |
fn=mosaic_generator, | |
cache_examples=True, | |
inputs=[ | |
gr.Image(type="pil", label="Upload Your Image"), | |
gr.Slider(minimum=1, maximum=32, step=1, value=20, | |
label="Block Size (px) for Feature Extraction"), | |
gr.Slider(minimum=0.1, maximum=1.0, step=0.1, value=1.0, | |
label="Output Scale (0.1 to 1.0)"), | |
# Feature priority sliders: | |
gr.Slider(minimum=0.0, maximum=5.0, step=0.1, value=3.5, | |
label="Priority for Average Color (Fast)"), | |
gr.Slider(minimum=0.0, maximum=5.0, step=0.1, value=5.0, | |
label="Priority for HSV Histogram (Slow)"), | |
gr.Slider(minimum=0.0, maximum=5.0, step=0.1, value=0.2, | |
label="Priority for LBP Histogram (Normal)"), | |
gr.Slider(minimum=0.0, maximum=5.0, step=0.1, value=0.2, | |
label="Priority for GLCM Features (Very Slow)"), | |
gr.Slider(minimum=0.0, maximum=5.0, step=0.1, value=0.2, | |
label="Priority for HOG Features (Normal)") | |
], | |
outputs=[ | |
gr.Image(type="numpy", label="Mosaic Image", format="png"), | |
gr.Textbox(label="Performance Metrics") | |
], | |
title="Photomosaic Generator", | |
description=( | |
"Turn your image into a mesmerizing photomosaic, crafted from carefully selected 150×150 tiles. Each block is replaced with the best-matching tile, preserving the essence of your original picture. Customize the look by adjusting feature priorities and output scale. The final mosaic captures intricate details while maintaining artistic harmony, creating a unique visual story." | |
), | |
examples=[ | |
# For each sample image, all examples use an output scale of 0.1. | |
# -- SAMPLE (1).png -- | |
[ | |
"samples/sample (1).png", | |
20, | |
0.1, # Output Scale set to 0.1 | |
5.0, # Priority for Average Color only | |
0.0, # HSV | |
0.0, # LBP | |
0.0, # GLCM | |
0.0 # HOG | |
], | |
[ | |
"samples/sample (1).png", | |
20, | |
0.1, # Output Scale set to 0.1 | |
0.0, # Priority for Average Color | |
5.0, # Priority for HSV only | |
0.0, # LBP | |
0.0, # GLCM | |
0.0 # HOG | |
], | |
[ | |
"samples/sample (1).png", | |
20, | |
0.1, # Output Scale set to 0.1 | |
3.5, # Combination: avg=3.5, hsv=5, rest=0.2 | |
5.0, | |
0.2, | |
0.2, | |
0.2 | |
], | |
# -- SAMPLE (2).jpg -- | |
[ | |
"samples/sample (2).jpg", | |
20, | |
0.1, | |
5.0, | |
0.0, | |
0.0, | |
0.0, | |
0.0 | |
], | |
[ | |
"samples/sample (2).jpg", | |
20, | |
0.1, | |
0.0, | |
5.0, | |
0.0, | |
0.0, | |
0.0 | |
], | |
[ | |
"samples/sample (2).jpg", | |
20, | |
0.1, | |
3.5, | |
5.0, | |
0.2, | |
0.2, | |
0.2 | |
], | |
# -- SAMPLE (3).jpg -- | |
[ | |
"samples/sample (3).jpg", | |
20, | |
0.1, | |
5.0, | |
0.0, | |
0.0, | |
0.0, | |
0.0 | |
], | |
[ | |
"samples/sample (3).jpg", | |
20, | |
0.1, | |
0.0, | |
5.0, | |
0.0, | |
0.0, | |
0.0 | |
], | |
[ | |
"samples/sample (3).jpg", | |
20, | |
0.1, | |
3.5, | |
5.0, | |
0.2, | |
0.2, | |
0.2 | |
], | |
# -- SAMPLE (4).webp -- | |
[ | |
"samples/sample (4).webp", | |
20, | |
0.1, | |
5.0, | |
0.0, | |
0.0, | |
0.0, | |
0.0 | |
], | |
[ | |
"samples/sample (4).webp", | |
20, | |
0.1, | |
0.0, | |
5.0, | |
0.0, | |
0.0, | |
0.0 | |
], | |
[ | |
"samples/sample (4).webp", | |
20, | |
0.1, | |
3.5, | |
5.0, | |
0.2, | |
0.2, | |
0.2 | |
], | |
# -- SAMPLE (5).jpg -- | |
[ | |
"samples/sample (5).jpg", | |
20, | |
0.1, | |
5.0, | |
0.0, | |
0.0, | |
0.0, | |
0.0 | |
], | |
[ | |
"samples/sample (5).jpg", | |
20, | |
0.1, | |
0.0, | |
5.0, | |
0.0, | |
0.0, | |
0.0 | |
], | |
[ | |
"samples/sample (5).jpg", | |
20, | |
0.1, | |
3.5, | |
5.0, | |
0.2, | |
0.2, | |
0.2 | |
] | |
] | |
) | |
if __name__ == "__main__": | |
iface.launch() | |