Skip to content

Commit

Permalink
Improved UI and code readability
Browse files Browse the repository at this point in the history
  • Loading branch information
meangrinch committed Nov 16, 2024
1 parent 494a460 commit 72a1fe4
Showing 1 changed file with 89 additions and 68 deletions.
157 changes: 89 additions & 68 deletions palettechanger.py
Original file line number Diff line number Diff line change
@@ -1,100 +1,124 @@
from tkinter import *
from tkinter import filedialog
import tkinter as tk
from tkinter import filedialog, Frame, Label, Button, Scale, HORIZONTAL
from PIL import Image, ImageTk
import numpy as np
import os

class PaletteChanger:
def __init__(self, root):
self.root = root
self.img = None
self.palette_path = None
self.file_path = None
self.img = None
self.setup_ui()

def setup_ui(self):
self.root.title("PaletteChanger")
self.root.geometry("250x420")
self.root.geometry("250x470")
self.root.resizable(False, False)

self.image_frame = Frame(self.root, height=250)
self.image_frame.pack()

self.img_label = Label(self.root)
self.img_label.pack()
image_button = tk.Button(self.root, text='Select Image', command=self.get_image)
image_button.pack(pady=5)

palette_button = tk.Button(self.root, text='Select Color Palette', command=self.select_palette)
palette_button.pack(pady=5)
self.palette_label = Label(self.root, text='')
self.image_frame = Frame(self.root, width=256, height=256)
self.image_frame.pack_propagate(False)
self.image_frame.pack(pady=5)
self.img_label = Label(self.image_frame)
self.img_label.pack(expand=True)
Button(self.root, text="Select Image", command=self.get_image).pack(pady=5)
Button(self.root, text="Select Color Palette", command=self.select_palette).pack(pady=5)
self.palette_label = Label(self.root, text="")
self.palette_label.pack()
Label(self.root, text="Colors to Use:").pack(pady=(5, 0))
self.scale = Scale(self.root, from_=1, to=256, orient=HORIZONTAL, highlightthickness=0)
self.scale.pack()
self.scale.focus_set()
self.scale.bind("<Left>", self.change_scale(-1))
self.scale.bind("<Right>", self.change_scale(1))
self.scale.bind("<Down>", self.change_scale(-1))
self.scale.bind("<Up>", self.change_scale(1))
Button(self.root, text="Convert Image", command=self.convert_image).pack(pady=5)

convert_button = tk.Button(self.root, text='Convert Image', command=self.convert_image)
convert_button.pack(pady=5)
def change_scale(self, delta):
def handler(event):
current_value = self.scale.get()
new_value = current_value + delta
if self.scale.cget("from") <= new_value <= self.scale.cget("to"):
self.scale.set(new_value)
return "break"
return handler

def select_palette(self):
self.palette_path = filedialog.askopenfilename(title='Select Color Palette Image')
self.palette_label.config(text=os.path.basename(self.palette_path))
if self.palette_path:
palette_image = Image.open(self.palette_path)
colors = np.array(palette_image).reshape(-1, 3)
max_colors = min(len(np.unique(colors, axis=0)), 256)
self.scale.config(to=max_colors)
self.scale.set(max_colors)
# Truncate filename if too long
filename = os.path.basename(self.palette_path)
max_length = 40
if len(filename) > max_length:
filename = filename[:max_length-3] + '...'
self.palette_label.config(text=filename)

def get_image(self):
self.file_path = filedialog.askopenfilename()
if not self.file_path:
return
self.img = Image.open(self.file_path)
if self.img.mode != 'RGBA':
self.img = self.img.convert('RGB')
img_preview = self.img.copy()
max_size = (250, 250)
img_preview.thumbnail(max_size, Image.LANCZOS)
self.image_frame.pack_forget()
img_preview = ImageTk.PhotoImage(img_preview)
self.img_label.config(image=img_preview)
self.img_label.image = img_preview
self.img.thumbnail((256, 256), Image.Resampling.LANCZOS)
self.img_preview = ImageTk.PhotoImage(self.img)
self.img_label.config(image=self.img_preview) # type: ignore

def convert_image(self):
if not self.palette_path or not self.file_path:
print("Please select both an image and a color palette first.")
return
palette_image = Image.open(self.palette_path)
small_img = palette_image.resize((256, 256))
result = small_img.quantize(colors=256)
self.img = Image.open(self.file_path)
if self.img.mode != 'RGBA':
self.img = self.img.convert('RGB')
colors = self._quantize_palette(palette_image)
new_image = self._map_pixel_to_palette(self.img, colors)
self._save_converted_image(new_image)

def _quantize_palette(self, palette_image):
resized_palette_image = palette_image.resize((256, 256))
num_colors = int(self.scale.get())
result = resized_palette_image.quantize(colors=num_colors)
result = result.convert('RGB')
color_counts = result.getcolors()
colors = np.array([color for count, color in color_counts])
return np.array([color for count, color in color_counts])

if self.img.mode == 'RGBA':
new_image = Image.new('RGBA', self.img.size)
img_array = np.array(self.img)
memo = {}
for x in range(self.img.width):
for y in range(self.img.height):
pixel_color = tuple(img_array[y, x])
if pixel_color[:3] == (255, 255, 255):
new_image.putpixel((x, y), (0, 0, 0, 0))
else:
if pixel_color[:3] in memo:
closest_color = memo[pixel_color[:3]]
else:
distances = np.sum((colors - pixel_color[:3]) ** 2, axis=1)
closest_color_index = np.argmin(distances)
closest_color = tuple(colors[closest_color_index])
memo[pixel_color[:3]] = closest_color
new_image.putpixel((x, y), closest_color + (pixel_color[3],))
else:
new_image = Image.new('RGB', self.img.size)
img_array = np.array(self.img)
memo = {}
for x in range(self.img.width):
for y in range(self.img.height):
pixel_color = tuple(img_array[y, x])
if pixel_color in memo:
closest_color = memo[pixel_color]
else:
distances = np.sum((colors - pixel_color) ** 2, axis=1)
closest_color_index = np.argmin(distances)
closest_color = tuple(colors[closest_color_index])
memo[pixel_color] = closest_color
def _map_pixel_to_palette(self, image, colors):
new_image = Image.new(image.mode, image.size)
img_array = np.array(image)
memo = {}
for x in range(image.width):
for y in range(image.height):
pixel_color = tuple(img_array[y, x])
# Handle transparent pixels in RGBA images
if image.mode == 'RGBA' and pixel_color[:3] == (255, 255, 255):
new_image.putpixel((x, y), (0, 0, 0, 0))
continue
# Use only RGB values for distance calculation
color_key = pixel_color[:3] if image.mode == 'RGBA' else pixel_color
if color_key in memo:
closest_color = memo[color_key]
else:
distances = np.sum((colors - color_key) ** 2, axis=1)
closest_color_index = np.argmin(distances)
closest_color = tuple(colors[closest_color_index])
memo[color_key] = closest_color
if image.mode == 'RGBA':
new_image.putpixel((x, y), closest_color + (pixel_color[3],))
else:
new_image.putpixel((x, y), closest_color)
return new_image

file_name = os.path.basename(self.file_path)
file_name = os.path.splitext(file_name)[0] + "_converted_palette.png"
def _save_converted_image(self, new_image):
if self.file_path is None:
print("Please select an image first.")
return
file_name = os.path.splitext(os.path.basename(self.file_path))[0] + "_converted_palette.png"
save_file_path = filedialog.asksaveasfilename(defaultextension=".png", initialfile=file_name)
if save_file_path:
new_image.save(save_file_path)
Expand All @@ -104,9 +128,7 @@ def main():
icon_data = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAATklEQVQ4jWNk5hb+z4AGmFi50IUg4myY4kxYVZIAhoEBLGysmhiCrL/FsCoW+69NfReMGsDAwGLMkoMhKMaBGdoMDAwMorw61HfBMDAAAKMZBStpAogmAAAAAElFTkSuQmCC"
icon = tk.PhotoImage(data=icon_data)
root.iconphoto(True, icon)

app = PaletteChanger(root)

root.update_idletasks()
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
Expand All @@ -115,7 +137,6 @@ def main():
x = (screen_width // 2) - (window_width // 2)
y = (screen_height // 2) - (window_height // 2)
root.geometry(f'{window_width}x{window_height}+{x}+{y}')

root.mainloop()

if __name__ == "__main__":
Expand Down

0 comments on commit 72a1fe4

Please sign in to comment.