Spaces:
Running
Running
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) |