File size: 10,746 Bytes
3a79668
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
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)