File size: 6,598 Bytes
0ed11ca
 
 
 
c913ad2
0ed11ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c913ad2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ed11ca
 
 
 
 
c913ad2
 
 
 
 
 
 
 
 
0ed11ca
 
 
 
 
 
 
 
 
c913ad2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ed11ca
 
c913ad2
0ed11ca
c913ad2
 
0ed11ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import numpy as np
from PIL import Image
from sklearn.cluster import KMeans
import gradio as gr
from collections import Counter

def extract_palette_from_image(palette_image):
    """Extract unique colors from a palette image."""
    img = Image.open(palette_image)
    img_array = np.array(img)
    # Reshape to get all pixels
    pixels = img_array.reshape(-1, 3)
    # Get unique colors
    unique_colors = np.unique(pixels, axis=0)
    return unique_colors

def generate_dynamic_palette(image, n_colors):
    """Generate a palette using K-means clustering."""
    img = Image.open(image)
    img_array = np.array(img)
    pixels = img_array.reshape(-1, 3)

    # Use K-means to find the most representative colors
    kmeans = KMeans(n_clusters=n_colors, random_state=42)
    kmeans.fit(pixels)
    return kmeans.cluster_centers_.astype(int)

def rgb_distance(color1, color2):
    """Calculate Euclidean distance between two RGB colors."""
    return np.sqrt(np.sum((color1 - color2) ** 2))

def hue_distance(color1, color2):
    """Calculate distance based on hue."""
    # Convert RGB to HSV
    hsv1 = rgb_to_hsv(color1)
    hsv2 = rgb_to_hsv(color2)
    # Calculate hue difference (considering circular nature of hue)
    hue_diff = min(abs(hsv1[0] - hsv2[0]), 1 - abs(hsv1[0] - hsv2[0]))
    return hue_diff

def brightness_distance(color1, color2):
    """Calculate distance based on brightness (grayscale)."""
    # Convert to grayscale using standard weights
    gray1 = np.dot(color1, [0.299, 0.587, 0.114])
    gray2 = np.dot(color2, [0.299, 0.587, 0.114])
    return abs(gray1 - gray2)

def rgb_to_hsv(rgb):
    """Convert RGB to HSV."""
    rgb = rgb / 255.0
    r, g, b = rgb

    maxc = max(r, g, b)
    minc = min(r, g, b)
    v = maxc

    if maxc == minc:
        return 0, 0, v

    s = (maxc - minc) / maxc
    rc = (maxc - r) / (maxc - minc)
    gc = (maxc - g) / (maxc - minc)
    bc = (maxc - b) / (maxc - minc)

    if r == maxc:
        h = bc - gc
    elif g == maxc:
        h = 2.0 + rc - bc
    else:
        h = 4.0 + gc - rc

    h = (h / 6.0) % 1.0
    return h, s, v

def get_mode_color(pixel_group):
    """Calculate the mode color of a pixel group."""
    # Reshape to get all pixels
    pixels = pixel_group.reshape(-1, 3)

    # Convert to tuple for counting
    pixel_tuples = [tuple(pixel) for pixel in pixels]

    # Count occurrences of each color
    color_counts = Counter(pixel_tuples)

    # Get the most common color
    mode_color = np.array(color_counts.most_common(1)[0][0])
    return mode_color

def pixelize_image(image, palette, mode='rgb', pixel_size=1):
    """Convert image to pixel art using the given palette."""
    img = Image.open(image)
    img_array = np.array(img)

    # Get image dimensions
    height, width = img_array.shape[:2]

    # Calculate new dimensions based on pixel_size
    new_height = height // pixel_size
    new_width = width // pixel_size

    # Initialize output array
    output_array = np.zeros((new_height, new_width, 3), dtype=np.uint8)

    # Choose distance function based on mode
    if mode == 'rgb':
        distance_func = rgb_distance
    elif mode == 'hue':
        distance_func = hue_distance
    else:  # brightness
        distance_func = brightness_distance

    # Process each pixel group
    for y in range(new_height):
        for x in range(new_width):
            # Get the pixel group
            y_start = y * pixel_size
            y_end = min((y + 1) * pixel_size, height)
            x_start = x * pixel_size
            x_end = min((x + 1) * pixel_size, width)

            pixel_group = img_array[y_start:y_end, x_start:x_end]

            # Calculate mean and mode colors
            mean_color = np.mean(pixel_group, axis=(0, 1)).astype(int)
            mode_color = get_mode_color(pixel_group)

            # Find closest palette color for mean
            mean_distances = np.array([distance_func(mean_color, palette_color) for palette_color in palette])
            mean_closest_color = palette[np.argmin(mean_distances)]
            mean_min_distance = np.min(mean_distances)

            # Find closest palette color for mode
            mode_distances = np.array([distance_func(mode_color, palette_color) for palette_color in palette])
            mode_closest_color = palette[np.argmin(mode_distances)]
            mode_min_distance = np.min(mode_distances)

            # Choose the color with the smaller distance to palette
            if mean_min_distance <= mode_min_distance:
                output_array[y, x] = mean_closest_color
            else:
                output_array[y, x] = mode_closest_color

    # Create output image
    output = Image.fromarray(output_array)

    # Resize back to original dimensions
    output = output.resize((width, height), Image.NEAREST)

    return output

def process_image(input_image, palette_image, n_colors, mode, pixel_size, use_dynamic_palette):
    """Process the image with the given parameters."""
    if use_dynamic_palette:
        palette = generate_dynamic_palette(input_image, n_colors)
    else:
        palette = extract_palette_from_image(palette_image)

    result = pixelize_image(input_image, palette, mode, pixel_size)
    return result

# Create Gradio interface
def create_interface():
    with gr.Blocks(title="Pixel Art Converter") as interface:
        gr.Markdown("# Pixel Art Converter")
        gr.Markdown("Convert your images into pixel art with customizable palettes!")

        with gr.Row():
            with gr.Column():
                input_image = gr.Image(type="filepath", label="Input Image")
                palette_image = gr.Image(type="filepath", label="Palette Image (for fixed palette mode)")
                use_dynamic_palette = gr.Checkbox(label="Use Dynamic Palette", value=True)
                n_colors = gr.Slider(minimum=2, maximum=32, value=8, step=1, label="Number of Colors (for dynamic palette)")
                mode = gr.Radio(["rgb", "hue", "brightness"], label="Color Matching Mode", value="hue")
                pixel_size = gr.Slider(minimum=1, maximum=32, value=4, step=1, label="Pixel Size")
                process_btn = gr.Button("Convert to Pixel Art")

            with gr.Column():
                output_image = gr.Image(label="Pixel Art Result")

        process_btn.click(
            fn=process_image,
            inputs=[input_image, palette_image, n_colors, mode, pixel_size, use_dynamic_palette],
            outputs=output_image
        )

    return interface

if __name__ == "__main__":
    interface = create_interface()
    interface.launch()