import gradio as gr import cv2 import numpy as np from PIL import Image import matplotlib.pyplot as plt import io def create_histogram(image, title="Histogram"): """Create histogram for grayscale or RGB image""" fig, ax = plt.subplots(figsize=(8, 4)) if len(image.shape) == 2: # Grayscale hist = cv2.calcHist([image], [0], None, [256], [0, 256]) ax.plot(hist, color='black') ax.set_xlim([0, 256]) ax.set_xlabel('Pixel Intensity') ax.set_ylabel('Frequency') ax.set_title(title) ax.grid(True, alpha=0.3) else: # RGB colors = ('r', 'g', 'b') for i, color in enumerate(colors): hist = cv2.calcHist([image], [i], None, [256], [0, 256]) ax.plot(hist, color=color, label=color.upper()) ax.set_xlim([0, 256]) ax.set_xlabel('Pixel Intensity') ax.set_ylabel('Frequency') ax.set_title(title) ax.legend() ax.grid(True, alpha=0.3) # Convert plot to image buf = io.BytesIO() plt.tight_layout() plt.savefig(buf, format='png', dpi=100, bbox_inches='tight') buf.seek(0) plot_image = Image.open(buf) plt.close() return np.array(plot_image) def get_pixel_info(image, x, y): """Get detailed pixel information""" if image is None: return "No image loaded" h, w = image.shape[:2] if x < 0 or x >= w or y < 0 or y >= h: return "Click within image bounds" if len(image.shape) == 2: # Grayscale pixel_value = image[y, x] info = f""" **Pixel Information at ({x}, {y})** - **Gray Value**: {pixel_value} - **Image Size**: {w} x {h} """ else: # RGB b, g, r = image[y, x] info = f""" **Pixel Information at ({x}, {y})** - **RGB**: ({r}, {g}, {b}) - **Hex**: #{r:02x}{g:02x}{b:02x} - **Image Size**: {w} x {h} """ return info def apply_clahe(image, clip_limit, tile_size): """Apply CLAHE with adjustable parameters""" if isinstance(image, Image.Image): image = np.array(image) # Convert to grayscale if RGB if len(image.shape) == 3: gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) else: gray = image # Apply CLAHE clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=(tile_size, tile_size)) result = clahe.apply(gray) # Create histograms hist_before = create_histogram(gray, "Histogram - Before CLAHE") hist_after = create_histogram(result, "Histogram - After CLAHE") # Convert back to RGB for display result_rgb = cv2.cvtColor(result, cv2.COLOR_GRAY2RGB) gray_rgb = cv2.cvtColor(gray, cv2.COLOR_GRAY2RGB) info = f""" ### CLAHE Applied - **Clip Limit**: {clip_limit} - **Tile Size**: {tile_size}x{tile_size} - **Effect**: Enhances local contrast by equalizing histograms in small tiles - **Use Case**: Improves visibility in shadowed or low-contrast regions """ return result_rgb, hist_before, hist_after, info def apply_gaussian_blur(image, kernel_size, sigma): """Apply Gaussian blur with adjustable parameters""" if isinstance(image, Image.Image): image = np.array(image) # Ensure kernel size is odd if kernel_size % 2 == 0: kernel_size += 1 result = cv2.GaussianBlur(image, (kernel_size, kernel_size), sigma) info = f""" ### Gaussian Blur Applied - **Kernel Size**: {kernel_size}x{kernel_size} - **Sigma**: {sigma} - **Effect**: Smooths image by averaging pixels with Gaussian weights - **Use Case**: Noise reduction, preparing for edge detection """ return result, info def apply_bilateral_filter(image, diameter, sigma_color, sigma_space): """Apply bilateral filter with adjustable parameters""" if isinstance(image, Image.Image): image = np.array(image) # Convert to grayscale for processing if len(image.shape) == 3: gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) else: gray = image result = cv2.bilateralFilter(gray, diameter, sigma_color, sigma_space) # Convert back to RGB result_rgb = cv2.cvtColor(result, cv2.COLOR_GRAY2RGB) info = f""" ### Bilateral Filter Applied - **Diameter**: {diameter} - **Sigma Color**: {sigma_color} - **Sigma Space**: {sigma_space} - **Effect**: Edge-preserving smoothing filter - **Use Case**: Noise reduction while keeping edges sharp """ return result_rgb, info def apply_median_filter(image, kernel_size): """Apply median filter""" if isinstance(image, Image.Image): image = np.array(image) # Ensure kernel size is odd if kernel_size % 2 == 0: kernel_size += 1 result = cv2.medianBlur(image, kernel_size) info = f""" ### Median Filter Applied - **Kernel Size**: {kernel_size}x{kernel_size} - **Effect**: Replaces each pixel with median of surrounding pixels - **Use Case**: Excellent for removing salt-and-pepper noise """ return result, info def apply_morphology(image, operation, kernel_size, iterations): """Apply morphological operations""" if isinstance(image, Image.Image): image = np.array(image) # Convert to grayscale if len(image.shape) == 3: gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) else: gray = image # Create kernel kernel = np.ones((kernel_size, kernel_size), np.uint8) # Apply operation if operation == "Erosion": result = cv2.erode(gray, kernel, iterations=iterations) desc = "Shrinks white regions, removes small white noise" elif operation == "Dilation": result = cv2.dilate(gray, kernel, iterations=iterations) desc = "Expands white regions, fills small holes" elif operation == "Opening": result = cv2.morphologyEx(gray, cv2.MORPH_OPEN, kernel, iterations=iterations) desc = "Erosion followed by dilation, removes small white noise" elif operation == "Closing": result = cv2.morphologyEx(gray, cv2.MORPH_CLOSE, kernel, iterations=iterations) desc = "Dilation followed by erosion, fills small holes" elif operation == "Gradient": result = cv2.morphologyEx(gray, cv2.MORPH_GRADIENT, kernel) desc = "Difference between dilation and erosion, shows outlines" else: result = gray desc = "No operation" # Convert back to RGB result_rgb = cv2.cvtColor(result, cv2.COLOR_GRAY2RGB) info = f""" ### Morphological Operation: {operation} - **Kernel Size**: {kernel_size}x{kernel_size} - **Iterations**: {iterations} - **Effect**: {desc} """ return result_rgb, info def apply_edge_detection(image, method, threshold1, threshold2): """Apply edge detection methods""" if isinstance(image, Image.Image): image = np.array(image) # Convert to grayscale if len(image.shape) == 3: gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) else: gray = image if method == "Canny": edges = cv2.Canny(gray, threshold1, threshold2) desc = "Multi-stage edge detection algorithm" elif method == "Sobel": sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=5) sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=5) edges = np.sqrt(sobelx**2 + sobely**2) edges = np.uint8(edges / edges.max() * 255) desc = "Gradient-based edge detection" elif method == "Laplacian": edges = cv2.Laplacian(gray, cv2.CV_64F) edges = np.uint8(np.abs(edges)) desc = "Second derivative edge detection" else: edges = gray desc = "No operation" # Convert to RGB edges_rgb = cv2.cvtColor(edges, cv2.COLOR_GRAY2RGB) info = f""" ### Edge Detection: {method} - **Method**: {desc} - **Threshold 1**: {threshold1} - **Threshold 2**: {threshold2} """ return edges_rgb, info def apply_color_space(image, color_space): """Convert to different color spaces""" if isinstance(image, Image.Image): image = np.array(image) if color_space == "RGB": result = image desc = "Standard Red-Green-Blue color space" elif color_space == "HSV": result = cv2.cvtColor(image, cv2.COLOR_RGB2HSV) desc = "Hue-Saturation-Value: Separates color from intensity" elif color_space == "LAB": result = cv2.cvtColor(image, cv2.COLOR_RGB2LAB) desc = "Perceptually uniform color space" elif color_space == "Grayscale": gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) result = cv2.cvtColor(gray, cv2.COLOR_GRAY2RGB) desc = "Single channel intensity image" elif color_space == "YCrCb": result = cv2.cvtColor(image, cv2.COLOR_RGB2YCrCb) desc = "Luma and chroma components" else: result = image desc = "No conversion" # Create histogram hist = create_histogram(result if color_space != "Grayscale" else gray, f"Histogram - {color_space}") info = f""" ### Color Space: {color_space} - **Description**: {desc} """ return result, hist, info def apply_thresholding(image, method, threshold_value, max_value): """Apply different thresholding methods""" if isinstance(image, Image.Image): image = np.array(image) # Convert to grayscale if len(image.shape) == 3: gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) else: gray = image if method == "Binary": _, result = cv2.threshold(gray, threshold_value, max_value, cv2.THRESH_BINARY) desc = "Pixels > threshold become max_value, others become 0" elif method == "Binary Inverse": _, result = cv2.threshold(gray, threshold_value, max_value, cv2.THRESH_BINARY_INV) desc = "Inverse of binary threshold" elif method == "Truncate": _, result = cv2.threshold(gray, threshold_value, max_value, cv2.THRESH_TRUNC) desc = "Pixels > threshold become threshold value" elif method == "To Zero": _, result = cv2.threshold(gray, threshold_value, max_value, cv2.THRESH_TOZERO) desc = "Pixels < threshold become 0" elif method == "Otsu": _, result = cv2.threshold(gray, 0, max_value, cv2.THRESH_BINARY + cv2.THRESH_OTSU) desc = "Automatic threshold calculation using Otsu's method" elif method == "Adaptive Mean": result = cv2.adaptiveThreshold(gray, max_value, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 11, 2) desc = "Threshold calculated for small regions using mean" elif method == "Adaptive Gaussian": result = cv2.adaptiveThreshold(gray, max_value, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2) desc = "Threshold calculated for small regions using Gaussian weights" else: result = gray desc = "No thresholding" # Convert to RGB result_rgb = cv2.cvtColor(result, cv2.COLOR_GRAY2RGB) info = f""" ### Thresholding: {method} - **Threshold Value**: {threshold_value} - **Max Value**: {max_value} - **Effect**: {desc} """ return result_rgb, info def apply_grabcut(image, margin_percent, iterations): """ Apply GrabCut algorithm for background subtraction GrabCut is an interactive foreground extraction algorithm that uses graph cuts and Gaussian Mixture Models (GMM) to separate foreground from background. """ if isinstance(image, Image.Image): image = np.array(image) # Ensure RGB format if len(image.shape) == 2: image = cv2.cvtColor(image, cv2.COLOR_GRAY2RGB) # Create a copy for processing img = image.copy() h, w = img.shape[:2] # Create mask (0 = background, 1 = foreground, 2 = probably background, 3 = probably foreground) mask = np.zeros(img.shape[:2], np.uint8) # Initialize background and foreground models (used internally by GrabCut) bgd_model = np.zeros((1, 65), np.float64) fgd_model = np.zeros((1, 65), np.float64) # Define rectangle around the object (margin from edges) margin_h = int(h * margin_percent / 100) margin_w = int(w * margin_percent / 100) rect = (margin_w, margin_h, w - 2*margin_w, h - 2*margin_h) # Apply GrabCut algorithm # Iterations: more iterations = more accurate but slower cv2.grabCut(img, mask, rect, bgd_model, fgd_model, iterations, cv2.GC_INIT_WITH_RECT) # Create binary mask where foreground (1 or 3) = 1, background (0 or 2) = 0 mask_binary = np.where((mask == 2) | (mask == 0), 0, 1).astype('uint8') # Extract foreground foreground = img * mask_binary[:, :, np.newaxis] # Create visualization showing the mask mask_vis = mask_binary * 255 mask_rgb = cv2.cvtColor(mask_vis, cv2.COLOR_GRAY2RGB) # Create combined view: Original | Mask | Foreground # Resize for side-by-side display scale = 0.33 h_new, w_new = int(h * scale), int(w * scale) original_small = cv2.resize(image, (w_new, h_new)) mask_small = cv2.resize(mask_rgb, (w_new, h_new)) foreground_small = cv2.resize(foreground, (w_new, h_new)) # Concatenate horizontally combined = np.hstack([original_small, mask_small, foreground_small]) # Add labels label_height = 30 labeled_img = np.zeros((combined.shape[0] + label_height, combined.shape[1], 3), dtype=np.uint8) labeled_img[label_height:, :] = combined # Add text labels cv2.putText(labeled_img, "Original", (10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1) cv2.putText(labeled_img, "Mask", (w_new + 10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1) cv2.putText(labeled_img, "Foreground", (2*w_new + 10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1) info = f""" ### GrabCut Background Subtraction Applied - **Margin**: {margin_percent}% from edges - **Iterations**: {iterations} - **Algorithm**: Graph cuts with Gaussian Mixture Models - **Output**: Shows Original | Mask | Extracted Foreground - **Use Case**: Object extraction, background removal, photo editing **How it works**: 1. Rectangle defines initial foreground region (inside margins) 2. GMM models learn foreground/background color distributions 3. Graph cuts optimize the boundary between them 4. White mask = foreground, Black = background """ return labeled_img, foreground, info def apply_fourier_filter(image, filter_type, cutoff_freq): """ Apply Fourier Transform filtering in frequency domain Fourier Transform decomposes image into frequency components. Low frequencies = smooth regions, High frequencies = edges/details """ if isinstance(image, Image.Image): image = np.array(image) # Convert to grayscale for Fourier if len(image.shape) == 3: gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) else: gray = image # Apply Fourier Transform f = np.fft.fft2(gray) fshift = np.fft.fftshift(f) # Shift zero frequency to center # Get magnitude spectrum for visualization magnitude_spectrum = 20 * np.log(np.abs(fshift) + 1) # Create filter mask rows, cols = gray.shape crow, ccol = rows // 2, cols // 2 # Create coordinate matrices y, x = np.ogrid[:rows, :cols] distance = np.sqrt((x - ccol)**2 + (y - crow)**2) if filter_type == "Low-Pass (Blur)": # Allow low frequencies, block high frequencies mask = np.zeros((rows, cols), np.uint8) mask[distance <= cutoff_freq] = 1 desc = "Removes high frequencies (edges/details), keeps smooth regions" elif filter_type == "High-Pass (Sharpen)": # Block low frequencies, allow high frequencies mask = np.ones((rows, cols), np.uint8) mask[distance <= cutoff_freq] = 0 desc = "Removes low frequencies (smooth regions), keeps edges/details" elif filter_type == "Band-Pass": # Allow middle frequencies mask = np.zeros((rows, cols), np.uint8) mask[(distance >= cutoff_freq/2) & (distance <= cutoff_freq)] = 1 desc = "Keeps only middle-range frequencies" elif filter_type == "Band-Stop (Notch)": # Block middle frequencies mask = np.ones((rows, cols), np.uint8) mask[(distance >= cutoff_freq/2) & (distance <= cutoff_freq)] = 0 desc = "Removes middle-range frequencies, keeps very low and very high" else: mask = np.ones((rows, cols), np.uint8) desc = "No filtering" # Apply mask fshift_filtered = fshift * mask # Inverse Fourier Transform f_ishift = np.fft.ifftshift(fshift_filtered) img_back = np.fft.ifft2(f_ishift) img_back = np.real(img_back) # Normalize to 0-255 img_back = np.clip(img_back, 0, 255).astype(np.uint8) # Convert back to RGB result_rgb = cv2.cvtColor(img_back, cv2.COLOR_GRAY2RGB) # Create visualization of spectrum magnitude_vis = np.clip(magnitude_spectrum, 0, 255).astype(np.uint8) magnitude_rgb = cv2.cvtColor(magnitude_vis, cv2.COLOR_GRAY2RGB) info = f""" ### Fourier Transform Filtering Applied - **Filter Type**: {filter_type} - **Cutoff Frequency**: {cutoff_freq} pixels - **Effect**: {desc} - **How it works**: Transforms to frequency domain, filters, transforms back - **Use Case**: Periodic noise removal, sharpening, custom filtering """ return result_rgb, magnitude_rgb, info def apply_gray_world(image, percentile): """ Apply Gray-World color constancy algorithm Assumes the average color of the scene should be gray. Adjusts color channels to achieve this, correcting color casts. """ if isinstance(image, Image.Image): image = np.array(image) if len(image.shape) != 3: return cv2.cvtColor(image, cv2.COLOR_GRAY2RGB), "Image must be in color for Gray-World" # Convert to float img_float = image.astype(np.float32) # Calculate average or percentile of each channel if percentile == 50: # Standard Gray-World: use mean avg_r = np.mean(img_float[:, :, 0]) avg_g = np.mean(img_float[:, :, 1]) avg_b = np.mean(img_float[:, :, 2]) method = "Mean" else: # Robust Gray-World: use percentile (less sensitive to outliers) avg_r = np.percentile(img_float[:, :, 0], percentile) avg_g = np.percentile(img_float[:, :, 1], percentile) avg_b = np.percentile(img_float[:, :, 2], percentile) method = f"{percentile}th Percentile" # Calculate gray value (average of all channels) gray_value = (avg_r + avg_g + avg_b) / 3 # Calculate scaling factors scale_r = gray_value / (avg_r + 1e-6) scale_g = gray_value / (avg_g + 1e-6) scale_b = gray_value / (avg_b + 1e-6) # Apply scaling result = img_float.copy() result[:, :, 0] = np.clip(result[:, :, 0] * scale_r, 0, 255) result[:, :, 1] = np.clip(result[:, :, 1] * scale_g, 0, 255) result[:, :, 2] = np.clip(result[:, :, 2] * scale_b, 0, 255) result = result.astype(np.uint8) info = f""" ### Gray-World Color Constancy Applied - **Method**: {method} - **Scaling Factors**: R={scale_r:.3f}, G={scale_g:.3f}, B={scale_b:.3f} - **Effect**: Removes color cast by balancing channel averages - **Assumption**: Average scene color should be neutral gray - **Use Case**: Correct lighting color casts (blue/yellow/green tints) **Original Averages**: R={avg_r:.1f}, G={avg_g:.1f}, B={avg_b:.1f} **Target Gray**: {gray_value:.1f} """ return result, info def apply_anisotropic_diffusion(image, iterations, kappa, gamma): """ Apply Anisotropic Diffusion (Perona-Malik) Edge-preserving smoothing that reduces noise while maintaining edges. Diffusion is stronger in smooth regions, weaker near edges. """ if isinstance(image, Image.Image): image = np.array(image) # Convert to grayscale if len(image.shape) == 3: gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) else: gray = image # Convert to float img = gray.astype(np.float32) # Perform anisotropic diffusion for _ in range(iterations): # Calculate gradients in 4 directions gradN = np.roll(img, 1, axis=0) - img # North gradS = np.roll(img, -1, axis=0) - img # South gradE = np.roll(img, -1, axis=1) - img # East gradW = np.roll(img, 1, axis=1) - img # West # Calculate diffusion coefficients (edge-stopping function) # Option 1: Exponential (preserves wide regions) cN = np.exp(-(gradN / kappa) ** 2) cS = np.exp(-(gradS / kappa) ** 2) cE = np.exp(-(gradE / kappa) ** 2) cW = np.exp(-(gradW / kappa) ** 2) # Update image img = img + gamma * (cN * gradN + cS * gradS + cE * gradE + cW * gradW) # Clip and convert back result = np.clip(img, 0, 255).astype(np.uint8) result_rgb = cv2.cvtColor(result, cv2.COLOR_GRAY2RGB) info = f""" ### Anisotropic Diffusion Applied - **Iterations**: {iterations} - **Kappa (Edge threshold)**: {kappa} - **Gamma (Step size)**: {gamma} - **Algorithm**: Perona-Malik diffusion - **Effect**: Smooths noise while preserving edges - **How it works**: Diffusion is adaptive - strong in flat regions, weak at edges - **Use Case**: Medical imaging, noise reduction with edge preservation **Parameters Guide**: - Kappa: Controls what's considered an edge (10-50 typical) - Gamma: Controls diffusion speed (0.1-0.25 typical, must be ≤0.25 for stability) - Iterations: More = more smoothing (5-20 typical) """ return result_rgb, info def analyze_image_stats(image): """Provide detailed statistical analysis""" if isinstance(image, Image.Image): image = np.array(image) if len(image.shape) == 3: gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) channels = cv2.split(image) stats = f""" ### Image Statistics **Dimensions**: {image.shape[1]} x {image.shape[0]} pixels **RGB Channel Statistics**: - **Red**: Mean={np.mean(channels[0]):.2f}, Std={np.std(channels[0]):.2f}, Min={np.min(channels[0])}, Max={np.max(channels[0])} - **Green**: Mean={np.mean(channels[1]):.2f}, Std={np.std(channels[1]):.2f}, Min={np.min(channels[1])}, Max={np.max(channels[1])} - **Blue**: Mean={np.mean(channels[2]):.2f}, Std={np.std(channels[2]):.2f}, Min={np.min(channels[2])}, Max={np.max(channels[2])} **Grayscale Statistics**: - **Mean Intensity**: {np.mean(gray):.2f} - **Standard Deviation**: {np.std(gray):.2f} - **Min Value**: {np.min(gray)} - **Max Value**: {np.max(gray)} - **Median**: {np.median(gray):.2f} **Brightness Assessment**: {"Dark" if np.mean(gray) < 85 else "Medium" if np.mean(gray) < 170 else "Bright"} **Contrast Assessment**: {"Low" if np.std(gray) < 30 else "Medium" if np.std(gray) < 60 else "High"} """ else: stats = f""" ### Image Statistics **Dimensions**: {image.shape[1]} x {image.shape[0]} pixels **Grayscale Statistics**: - **Mean Intensity**: {np.mean(image):.2f} - **Standard Deviation**: {np.std(image):.2f} - **Min Value**: {np.min(image)} - **Max Value**: {np.max(image)} - **Median**: {np.median(image):.2f} **Brightness Assessment**: {"Dark" if np.mean(image) < 85 else "Medium" if np.mean(image) < 170 else "Bright"} **Contrast Assessment**: {"Low" if np.std(image) < 30 else "Medium" if np.std(image) < 60 else "High"} """ return stats # Create Gradio Interface with gr.Blocks(title="Image Preprocessing Analyzer", theme=gr.themes.Soft()) as demo: gr.Markdown(""" # 🔬 Image Preprocessing Analyzer ### Understand Image Processing at Pixel Level Upload an image and explore various preprocessing techniques with real-time parameter adjustments. See histograms, pixel-level information, and understand how each filter affects your image. """) with gr.Row(): input_image = gr.Image(label="Upload Image", type="pil", height=400) original_hist = gr.Image(label="Original Histogram") with gr.Row(): stats_output = gr.Markdown(label="Image Statistics") # Update stats when image is loaded input_image.change( fn=lambda img: (analyze_image_stats(img), create_histogram(np.array(img), "Original Histogram")) if img else ("No image", None), inputs=[input_image], outputs=[stats_output, original_hist] ) with gr.Tabs(): # CLAHE Tab with gr.TabItem("🎨 CLAHE (Contrast Enhancement)"): gr.Markdown(""" **CLAHE** (Contrast Limited Adaptive Histogram Equalization) enhances local contrast. Adjust parameters to see how it affects different image regions. """) with gr.Row(): clahe_clip = gr.Slider(0.5, 10.0, value=2.0, step=0.5, label="Clip Limit") clahe_tile = gr.Slider(2, 32, value=8, step=2, label="Tile Size") clahe_btn = gr.Button("Apply CLAHE", variant="primary") with gr.Row(): clahe_output = gr.Image(label="Result") clahe_hist_before = gr.Image(label="Histogram - Before") with gr.Row(): clahe_hist_after = gr.Image(label="Histogram - After") clahe_info = gr.Markdown() clahe_btn.click( fn=apply_clahe, inputs=[input_image, clahe_clip, clahe_tile], outputs=[clahe_output, clahe_hist_before, clahe_hist_after, clahe_info] ) # Smoothing Filters Tab with gr.TabItem("🌊 Smoothing Filters"): filter_type = gr.Radio( ["Gaussian Blur", "Bilateral Filter", "Median Filter"], value="Gaussian Blur", label="Filter Type" ) with gr.Row(): with gr.Column(): # Gaussian parameters gauss_kernel = gr.Slider(1, 31, value=5, step=2, label="Kernel Size (Gaussian)") gauss_sigma = gr.Slider(0, 10, value=0, step=0.5, label="Sigma (Gaussian)") with gr.Column(): # Bilateral parameters bilat_diameter = gr.Slider(1, 15, value=9, step=2, label="Diameter (Bilateral)") bilat_sigma_color = gr.Slider(1, 150, value=75, step=5, label="Sigma Color (Bilateral)") bilat_sigma_space = gr.Slider(1, 150, value=75, step=5, label="Sigma Space (Bilateral)") with gr.Column(): # Median parameters median_kernel = gr.Slider(1, 31, value=5, step=2, label="Kernel Size (Median)") smooth_btn = gr.Button("Apply Filter", variant="primary") with gr.Row(): smooth_output = gr.Image(label="Result") smooth_info = gr.Markdown() def apply_smoothing(image, filter_type, gk, gs, bd, bsc, bss, mk): if filter_type == "Gaussian Blur": return apply_gaussian_blur(image, gk, gs) elif filter_type == "Bilateral Filter": return apply_bilateral_filter(image, bd, bsc, bss) else: return apply_median_filter(image, mk) smooth_btn.click( fn=apply_smoothing, inputs=[input_image, filter_type, gauss_kernel, gauss_sigma, bilat_diameter, bilat_sigma_color, bilat_sigma_space, median_kernel], outputs=[smooth_output, smooth_info] ) # Edge Detection Tab with gr.TabItem("📐 Edge Detection"): edge_method = gr.Radio( ["Canny", "Sobel", "Laplacian"], value="Canny", label="Edge Detection Method" ) with gr.Row(): edge_thresh1 = gr.Slider(0, 255, value=50, step=5, label="Threshold 1") edge_thresh2 = gr.Slider(0, 255, value=150, step=5, label="Threshold 2") edge_btn = gr.Button("Detect Edges", variant="primary") with gr.Row(): edge_output = gr.Image(label="Result") edge_info = gr.Markdown() edge_btn.click( fn=apply_edge_detection, inputs=[input_image, edge_method, edge_thresh1, edge_thresh2], outputs=[edge_output, edge_info] ) # Morphological Operations Tab with gr.TabItem("🔲 Morphological Operations"): morph_op = gr.Radio( ["Erosion", "Dilation", "Opening", "Closing", "Gradient"], value="Closing", label="Operation" ) with gr.Row(): morph_kernel = gr.Slider(1, 21, value=3, step=2, label="Kernel Size") morph_iter = gr.Slider(1, 5, value=1, step=1, label="Iterations") morph_btn = gr.Button("Apply Operation", variant="primary") with gr.Row(): morph_output = gr.Image(label="Result") morph_info = gr.Markdown() morph_btn.click( fn=apply_morphology, inputs=[input_image, morph_op, morph_kernel, morph_iter], outputs=[morph_output, morph_info] ) # Color Spaces Tab with gr.TabItem("🎨 Color Spaces"): color_space = gr.Radio( ["RGB", "HSV", "LAB", "YCrCb", "Grayscale"], value="RGB", label="Color Space" ) color_btn = gr.Button("Convert Color Space", variant="primary") with gr.Row(): color_output = gr.Image(label="Result") color_hist = gr.Image(label="Histogram") color_info = gr.Markdown() color_btn.click( fn=apply_color_space, inputs=[input_image, color_space], outputs=[color_output, color_hist, color_info] ) # Thresholding Tab with gr.TabItem("⚫⚪ Thresholding"): thresh_method = gr.Radio( ["Binary", "Binary Inverse", "Truncate", "To Zero", "Otsu", "Adaptive Mean", "Adaptive Gaussian"], value="Binary", label="Thresholding Method" ) with gr.Row(): thresh_value = gr.Slider(0, 255, value=127, step=1, label="Threshold Value") thresh_max = gr.Slider(0, 255, value=255, step=1, label="Max Value") thresh_btn = gr.Button("Apply Threshold", variant="primary") with gr.Row(): thresh_output = gr.Image(label="Result") thresh_info = gr.Markdown() thresh_btn.click( fn=apply_thresholding, inputs=[input_image, thresh_method, thresh_value, thresh_max], outputs=[thresh_output, thresh_info] ) # GrabCut Background Subtraction Tab with gr.TabItem("✂️ Background Subtraction (GrabCut)"): gr.Markdown(""" **GrabCut** is an advanced algorithm for extracting foreground objects from images. It uses graph cuts and Gaussian Mixture Models to intelligently separate foreground from background. Perfect for: Product photography, portrait backgrounds, object isolation """) with gr.Row(): grabcut_margin = gr.Slider(5, 25, value=10, step=1, label="Margin from Edges (%)") grabcut_iter = gr.Slider(1, 10, value=5, step=1, label="Iterations") grabcut_btn = gr.Button("Extract Foreground", variant="primary") with gr.Row(): grabcut_output = gr.Image(label="Comparison View (Original | Mask | Foreground)") with gr.Row(): grabcut_foreground = gr.Image(label="Extracted Foreground (Full Size)") grabcut_info = gr.Markdown() grabcut_btn.click( fn=apply_grabcut, inputs=[input_image, grabcut_margin, grabcut_iter], outputs=[grabcut_output, grabcut_foreground, grabcut_info] ) # Fourier Transform Filtering Tab with gr.TabItem("🌊 Fourier Transform Filtering"): gr.Markdown(""" **Fourier Transform** decomposes images into frequency components. Filter in frequency domain to remove periodic noise or enhance specific features. """) with gr.Row(): fourier_type = gr.Radio( ["Low-Pass (Blur)", "High-Pass (Sharpen)", "Band-Pass", "Band-Stop (Notch)"], value="Low-Pass (Blur)", label="Filter Type" ) fourier_cutoff = gr.Slider(10, 200, value=30, step=5, label="Cutoff Frequency (pixels)") fourier_btn = gr.Button("Apply Fourier Filter", variant="primary") with gr.Row(): fourier_output = gr.Image(label="Filtered Result") fourier_spectrum = gr.Image(label="Frequency Spectrum") fourier_info = gr.Markdown() fourier_btn.click( fn=apply_fourier_filter, inputs=[input_image, fourier_type, fourier_cutoff], outputs=[fourier_output, fourier_spectrum, fourier_info] ) # Gray-World Color Constancy Tab with gr.TabItem("🎨 Color Constancy (Gray-World)"): gr.Markdown(""" **Gray-World Algorithm** corrects color casts caused by lighting. Assumes the average color of a scene should be neutral gray. """) with gr.Row(): grayworld_percentile = gr.Slider( 40, 60, value=50, step=5, label="Percentile (50=Mean, 40-45=Robust to highlights)" ) grayworld_btn = gr.Button("Apply Gray-World", variant="primary") with gr.Row(): grayworld_output = gr.Image(label="Color Corrected") grayworld_info = gr.Markdown() grayworld_btn.click( fn=apply_gray_world, inputs=[input_image, grayworld_percentile], outputs=[grayworld_output, grayworld_info] ) # Anisotropic Diffusion Tab with gr.TabItem("🔬 Anisotropic Diffusion"): gr.Markdown(""" **Anisotropic Diffusion** (Perona-Malik) performs edge-preserving smoothing. Reduces noise while maintaining sharp edges - ideal for medical imaging. """) with gr.Row(): aniso_iter = gr.Slider(1, 30, value=10, step=1, label="Iterations") aniso_kappa = gr.Slider(5, 100, value=20, step=5, label="Kappa (Edge threshold)") aniso_gamma = gr.Slider(0.05, 0.25, value=0.15, step=0.05, label="Gamma (Step size)") aniso_btn = gr.Button("Apply Anisotropic Diffusion", variant="primary") with gr.Row(): aniso_output = gr.Image(label="Smoothed Result") aniso_info = gr.Markdown() aniso_btn.click( fn=apply_anisotropic_diffusion, inputs=[input_image, aniso_iter, aniso_kappa, aniso_gamma], outputs=[aniso_output, aniso_info] ) # Documentation with gr.Accordion("📚 Filter Documentation", open=False): gr.Markdown(""" ### Filter Explanations #### CLAHE (Contrast Limited Adaptive Histogram Equalization) - **Purpose**: Enhance local contrast in images - **How it works**: Divides image into tiles and equalizes histogram in each tile - **Clip Limit**: Controls contrast enhancement (higher = more enhancement) - **Tile Size**: Size of local regions (smaller = more local adaptation) - **Use Case**: Medical imaging, underwater images, shadowed regions #### Gaussian Blur - **Purpose**: Smooth images and reduce noise - **How it works**: Weighted average of neighboring pixels using Gaussian function - **Kernel Size**: Larger = more blur - **Sigma**: Standard deviation of Gaussian (0 = auto-calculated) - **Use Case**: Preprocessing for edge detection, noise reduction #### Bilateral Filter - **Purpose**: Edge-preserving smoothing - **How it works**: Averages pixels but preserves edges by considering both spatial and color distance - **Diameter**: Size of pixel neighborhood - **Sigma Color**: How much color difference matters - **Sigma Space**: How much spatial distance matters - **Use Case**: Noise reduction while keeping edges sharp #### Median Filter - **Purpose**: Remove salt-and-pepper noise - **How it works**: Replaces each pixel with median of surrounding pixels - **Kernel Size**: Size of neighborhood - **Use Case**: Impulse noise removal #### Morphological Operations - **Erosion**: Shrinks white regions, removes small noise - **Dilation**: Expands white regions, fills small holes - **Opening**: Erosion then dilation, removes small objects - **Closing**: Dilation then erosion, fills small holes - **Gradient**: Difference between dilation and erosion, shows boundaries #### Edge Detection - **Canny**: Multi-stage algorithm, best overall edge detector - **Sobel**: Gradient-based, sensitive to horizontal/vertical edges - **Laplacian**: Second derivative, sensitive to rapid intensity changes #### Thresholding - **Binary**: Simple cutoff threshold - **Otsu**: Automatically finds optimal threshold - **Adaptive**: Different thresholds for different regions #### GrabCut Background Subtraction - **Purpose**: Extract foreground objects from images - **How it works**: Uses graph cuts and Gaussian Mixture Models (GMM) - **Margin**: Defines initial foreground region (rectangle inside margins) - **Iterations**: More iterations = more accurate segmentation (but slower) - **Use Case**: Product photography, portrait background removal, object isolation - **Algorithm**: Iteratively learns color distributions of foreground/background - **Output**: Binary mask and extracted foreground object #### Fourier Transform Filtering - **Purpose**: Filter images in frequency domain - **How it works**: Converts to frequency domain, applies filter, converts back - **Low-Pass**: Removes high frequencies (edges), keeps smooth regions → blur effect - **High-Pass**: Removes low frequencies (smooth regions), keeps edges → sharpen effect - **Band-Pass**: Keeps only middle-range frequencies - **Band-Stop**: Removes middle-range frequencies (notch filter) - **Use Case**: Periodic noise removal, custom filtering, pattern analysis #### Gray-World Color Constancy - **Purpose**: Correct color casts from lighting - **How it works**: Assumes average scene color should be neutral gray - **Method**: Balances RGB channels so their average equals gray - **Percentile**: 50=standard mean, 40-45=robust to bright highlights - **Use Case**: Indoor/outdoor lighting correction, white balance adjustment #### Anisotropic Diffusion - **Purpose**: Edge-preserving noise reduction - **How it works**: Perona-Malik diffusion - smooths flat regions, preserves edges - **Kappa**: Edge threshold (10-50 typical, higher = more edges preserved) - **Gamma**: Diffusion speed (0.1-0.25, must be ≤0.25 for stability) - **Iterations**: More = more smoothing (5-20 typical) - **Use Case**: Medical imaging, noise reduction without edge loss """) if __name__ == "__main__": demo.launch()