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