| 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) |
| |
| pixels = img_array.reshape(-1, 3) |
| |
| 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) |
|
|
| |
| 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.""" |
| |
| hsv1 = rgb_to_hsv(color1) |
| hsv2 = rgb_to_hsv(color2) |
| |
| 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).""" |
| |
| 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.""" |
| |
| pixels = pixel_group.reshape(-1, 3) |
|
|
| |
| pixel_tuples = [tuple(pixel) for pixel in pixels] |
|
|
| |
| color_counts = Counter(pixel_tuples) |
|
|
| |
| 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) |
|
|
| |
| height, width = img_array.shape[:2] |
|
|
| |
| new_height = height // pixel_size |
| new_width = width // pixel_size |
|
|
| |
| output_array = np.zeros((new_height, new_width, 3), dtype=np.uint8) |
|
|
| |
| if mode == 'rgb': |
| distance_func = rgb_distance |
| elif mode == 'hue': |
| distance_func = hue_distance |
| else: |
| distance_func = brightness_distance |
|
|
| |
| for y in range(new_height): |
| for x in range(new_width): |
| |
| 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] |
|
|
| |
| mean_color = np.mean(pixel_group, axis=(0, 1)).astype(int) |
| mode_color = get_mode_color(pixel_group) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| if mean_min_distance <= mode_min_distance: |
| output_array[y, x] = mean_closest_color |
| else: |
| output_array[y, x] = mode_closest_color |
|
|
| |
| output = Image.fromarray(output_array) |
|
|
| |
| 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 |
|
|
| |
| 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() |