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