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()