Spaces:
Sleeping
Sleeping
| import cv2 | |
| import numpy as np | |
| import matplotlib.pyplot as plt | |
| from pathlib import Path | |
| import gradio as gr | |
| import tempfile | |
| import os | |
| import shutil | |
| def edge_directed_antialiasing(img, power=2.0): | |
| """ | |
| Apply edge-directed anti-aliasing with adjustable power | |
| Parameters: | |
| - img: Input image (numpy array) | |
| - power: Anti-aliasing strength (1.0 is standard, higher values increase the effect) | |
| Returns: | |
| - Output image with anti-aliasing applied | |
| """ | |
| # If image has alpha channel, separate it | |
| has_alpha = img.shape[2] == 4 if len(img.shape) > 2 else False | |
| if has_alpha: | |
| bgr = img[:, :, :3] | |
| alpha = img[:, :, 3] | |
| else: | |
| bgr = img | |
| # Create binary mask from grayscale image if no alpha | |
| gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY) | |
| _, alpha = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY) | |
| # Convert to grayscale for edge detection | |
| gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY) | |
| # Step 1: Detect edges using Canny | |
| # Lower thresholds to catch more edges when power is high | |
| canny_threshold1 = int(100 / power) # Lower threshold when power is high | |
| canny_threshold2 = int(200 / power) # Lower threshold when power is high | |
| edges = cv2.Canny(gray, canny_threshold1, canny_threshold2) | |
| # Dilate edges more when power is high | |
| kernel_size = int(3 * power) # Increase kernel size with power | |
| kernel_size = max(3, kernel_size if kernel_size % 2 == 1 else kernel_size + 1) # Ensure odd kernel size | |
| kernel = np.ones((kernel_size, kernel_size), np.uint8) | |
| # More iterations for higher power | |
| dilation_iterations = max(1, int(power)) | |
| dilated_edges = cv2.dilate(edges, kernel, iterations=dilation_iterations) | |
| # Step 2: Calculate gradient direction using Sobel | |
| # Increase kernel size for higher power | |
| sobel_ksize = 3 | |
| if power > 2.0: | |
| sobel_ksize = 5 | |
| if power > 3.0: | |
| sobel_ksize = 7 | |
| sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=sobel_ksize) | |
| sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=sobel_ksize) | |
| # Calculate gradient magnitude and direction | |
| magnitude = np.sqrt(sobelx**2 + sobely**2) | |
| direction = np.arctan2(sobely, sobelx) * 180 / np.pi | |
| # Create output image, starting with the original | |
| output = bgr.copy() | |
| h, w = output.shape[:2] | |
| # Step 3: Apply targeted smoothing along edge directions | |
| # Sample farther away for higher power | |
| radius = max(1, int(power)) | |
| edge_pixels = np.where(dilated_edges > 0) | |
| for y, x in zip(edge_pixels[0], edge_pixels[1]): | |
| # Skip border pixels | |
| if x < radius or y < radius or x >= w-radius or y >= h-radius: | |
| continue | |
| # Get local direction (perpendicular to gradient) | |
| local_dir = direction[y, x] + 90 | |
| if local_dir > 180: | |
| local_dir -= 360 | |
| # Normalize direction to 0-180 degrees | |
| local_dir = ((local_dir + 180) % 180) | |
| # Determine interpolation direction based on edge angle | |
| if 22.5 <= local_dir < 67.5: # ~45 degree diagonal | |
| # Diagonal top-left to bottom-right | |
| neighbors = [(y-radius, x-radius), (y+radius, x+radius)] | |
| weights = [0.5, 0.5] | |
| elif 67.5 <= local_dir < 112.5: # Vertical | |
| # Top to bottom | |
| neighbors = [(y-radius, x), (y+radius, x)] | |
| weights = [0.5, 0.5] | |
| elif 112.5 <= local_dir < 157.5: # ~135 degree diagonal | |
| # Diagonal top-right to bottom-left | |
| neighbors = [(y-radius, x+radius), (y+radius, x-radius)] | |
| weights = [0.5, 0.5] | |
| else: # Horizontal | |
| # Left to right | |
| neighbors = [(y, x-radius), (y, x+radius)] | |
| weights = [0.5, 0.5] | |
| # Only interpolate if we're between different colors (at the border) | |
| center_value = gray[y, x] | |
| neighbor_values = [gray[ny, nx] for ny, nx in neighbors] | |
| # Lower contrast threshold when power is high | |
| contrast_threshold = int(50 / power) | |
| # Check if this is an edge between very different values | |
| if abs(neighbor_values[0] - neighbor_values[1]) > contrast_threshold: | |
| # Apply interpolation based on local contrast | |
| for c in range(3): # RGB channels | |
| weighted_sum = sum(weights[i] * bgr[ny, nx, c] for i, (ny, nx) in enumerate(neighbors)) | |
| # More interpolation weight when power is high | |
| blend_factor = min(0.9, 0.3 * power) | |
| # Apply it with a blend factor to preserve some original detail | |
| output[y, x, c] = int((1-blend_factor) * weighted_sum + blend_factor * bgr[y, x, c]) | |
| # Update alpha channel with the same smoothing for edges | |
| if has_alpha: | |
| new_alpha = alpha.copy() | |
| # Apply a specific smoothing to the alpha channel's edges | |
| alpha_edges = cv2.Canny(alpha, int(100/power), int(200/power)) | |
| # More dilation iterations for stronger effect | |
| alpha_dilation_iter = max(2, int(power * 2)) | |
| dilated_alpha_edges = cv2.dilate(alpha_edges, kernel, iterations=alpha_dilation_iter) | |
| # Radius for sampling neighborhood | |
| alpha_radius = max(2, int(power * 2)) | |
| # For each edge pixel in alpha | |
| alpha_edge_pixels = np.where(dilated_alpha_edges > 0) | |
| for y, x in zip(alpha_edge_pixels[0], alpha_edge_pixels[1]): | |
| if x < alpha_radius or y < alpha_radius or x >= w-alpha_radius or y >= h-alpha_radius: | |
| continue | |
| # Use a larger neighborhood for better smoothing of alpha edges | |
| # Size increases with power | |
| window_radius = alpha_radius | |
| neighborhood = alpha[y-window_radius:y+window_radius+1, x-window_radius:x+window_radius+1].astype(np.float32) | |
| # Generate gaussian-like weights based on distance from center | |
| kernel_size = 2 * window_radius + 1 | |
| weight_matrix = np.zeros((kernel_size, kernel_size), dtype=np.float32) | |
| # Create distance-based weights | |
| center = window_radius | |
| for wy in range(kernel_size): | |
| for wx in range(kernel_size): | |
| # Calculate distance from center | |
| dist = np.sqrt((wy - center)**2 + (wx - center)**2) | |
| # Adjust falloff based on power | |
| falloff = 1.0 / power | |
| # Gaussian-like weight | |
| weight_matrix[wy, wx] = np.exp(-(dist**2) / (2 * (window_radius * falloff)**2)) | |
| # Normalize weights | |
| weight_matrix = weight_matrix / weight_matrix.sum() | |
| # Apply weighted average | |
| new_alpha[y, x] = int(np.sum(neighborhood * weight_matrix)) | |
| # Merge BGR with new alpha | |
| output = np.dstack([output, new_alpha]) | |
| return output | |
| def save_as_jpg(img, file_path): | |
| """ | |
| Save image as JPG with high quality | |
| """ | |
| # If image has alpha channel, blend with white background | |
| if len(img.shape) > 2 and img.shape[2] == 4: | |
| bgr = img[:, :, :3] | |
| alpha = img[:, :, 3].astype(float) / 255 | |
| # Create white background | |
| bg = np.ones_like(bgr) * 255 | |
| # Blend with background | |
| alpha = np.expand_dims(alpha, axis=2) | |
| alpha = np.repeat(alpha, 3, axis=2) | |
| result = (bgr * alpha + bg * (1 - alpha)).astype(np.uint8) | |
| else: | |
| result = img | |
| # Save as JPG | |
| cv2.imwrite(file_path, result, [cv2.IMWRITE_JPEG_QUALITY, 95]) | |
| return file_path | |
| def create_output_dirs(): | |
| """Create necessary output directories""" | |
| output_dir = os.path.join(tempfile.gettempdir(), "antialiasing_output") | |
| os.makedirs(output_dir, exist_ok=True) | |
| return output_dir | |
| def process_image(input_image): | |
| """ | |
| Process image function for Gradio interface | |
| """ | |
| # Create output directory for our files | |
| output_dir = create_output_dirs() | |
| # Convert from RGB (Gradio) to BGR (OpenCV) | |
| img_bgr = cv2.cvtColor(input_image, cv2.COLOR_RGB2BGR) | |
| # Apply edge directed anti-aliasing with power=2.0 | |
| processed_bgr = edge_directed_antialiasing(img_bgr, power=2.0) | |
| # Save the processed image explicitly as JPG | |
| jpg_path = os.path.join(output_dir, "antialiased_image.jpg") | |
| save_as_jpg(processed_bgr, jpg_path) | |
| # Convert back to RGB for display in Gradio | |
| if processed_bgr.shape[2] == 4: # Has alpha channel | |
| # Blend with white background | |
| bg = np.ones_like(processed_bgr[:,:,:3]) * 255 | |
| alpha = processed_bgr[:,:,3] | |
| alpha_norm = alpha.astype(float) / 255 | |
| alpha_norm = np.expand_dims(alpha_norm, axis=2) | |
| alpha_norm = np.repeat(alpha_norm, 3, axis=2) | |
| processed_rgb = processed_bgr[:,:,:3] * alpha_norm + bg * (1 - alpha_norm) | |
| processed_rgb = processed_rgb.astype(np.uint8) | |
| else: | |
| processed_rgb = cv2.cvtColor(processed_bgr, cv2.COLOR_BGR2RGB) | |
| # Create comparison visualization | |
| h, w = input_image.shape[:2] | |
| dpi = 100 | |
| plt.figure(figsize=(w*2/dpi, h/dpi), dpi=dpi) | |
| plt.subplot(1, 2, 1) | |
| plt.imshow(input_image) | |
| plt.title("Original") | |
| plt.axis('off') | |
| plt.subplot(1, 2, 2) | |
| plt.imshow(processed_rgb) | |
| plt.title("Anti-aliased (Power = 2.0)") | |
| plt.axis('off') | |
| plt.tight_layout() | |
| # Save the comparison | |
| comparison_file = os.path.join(output_dir, "comparison.jpg") | |
| plt.savefig(comparison_file, dpi=dpi, bbox_inches='tight') | |
| plt.close() | |
| return processed_rgb, jpg_path, comparison_file | |
| # Create Gradio interface | |
| with gr.Blocks(title="Edge-Directed Anti-Aliasing") as app: | |
| gr.Markdown("# Edge-Directed Anti-Aliasing Tool") | |
| gr.Markdown("Upload an image and apply edge-directed anti-aliasing to smooth jagged edges.") | |
| with gr.Row(): | |
| input_image = gr.Image(label="Upload Image", type="numpy") | |
| output_image = gr.Image(label="Anti-Aliased Result", type="numpy") | |
| with gr.Row(): | |
| process_button = gr.Button("Apply Anti-Aliasing (Power = 2.0)") | |
| with gr.Row(): | |
| download_jpg = gr.File(label="Download Anti-Aliased JPG", type="filepath") | |
| comparison_view = gr.Image(label="Comparison", type="filepath") | |
| # Process button functionality | |
| process_button.click( | |
| fn=process_image, | |
| inputs=[input_image], | |
| outputs=[output_image, download_jpg, comparison_view] | |
| ) | |
| # Launch the app | |
| if __name__ == "__main__": | |
| app.launch(share=True) |