| | 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() |