pixelize / pixelize.py
thalismind
fix pixelization
c913ad2
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()