Last active
April 22, 2025 20:15
-
-
Save veldman1/976d1a2a153783c7c66be13955f24fa6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import colorsys | |
import os | |
import tkinter as tk | |
from datetime import datetime | |
from tkinter import filedialog, ttk | |
import matplotlib.pyplot as plt | |
import numpy as np | |
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg | |
from PIL import Image, ImageTk | |
from sklearn.cluster import KMeans | |
# ----- Image Analysis Logic ----- | |
def is_gray(color, tolerance: int = 30) -> bool: | |
r, g, b = map(int, color) | |
return ( | |
abs(r - g) < tolerance | |
and abs(g - b) < tolerance | |
and abs(r - b) < tolerance | |
) | |
def resize_image_to_max_pixels(image: Image.Image, max_pixels: int = 500000) -> Image.Image: | |
w, h = image.size | |
total_pixels = w * h | |
if total_pixels <= max_pixels: | |
return image | |
scale_factor = (max_pixels / total_pixels) ** 0.5 | |
new_size = (int(w * scale_factor), int(h * scale_factor)) | |
return image.resize(new_size, Image.LANCZOS) | |
def get_dominant_colors(image: np.ndarray, k: int = 10, gray_tolerance: int = 30) -> list[tuple[tuple[int, int, int], int]]: | |
pixels = image.reshape(-1, 3) | |
filtered = np.array([p for p in pixels if not is_gray(p, tolerance=gray_tolerance)]) | |
kmeans = KMeans(n_clusters=min(k, len(filtered)), random_state=42) | |
kmeans.fit(filtered) | |
labels = kmeans.labels_ | |
unique, counts = np.unique(labels, return_counts=True) | |
sorted_indices = unique[np.argsort(-counts)] | |
return [ | |
(tuple(map(int, kmeans.cluster_centers_[i])), counts[i]) for i in sorted_indices | |
] | |
def rgb_to_hls(rgb): | |
rgb = np.array(rgb) / 255 | |
return colorsys.rgb_to_hls(*rgb) | |
def plot_dominant_colors_radial(colors, image, lightness_weight: float = 0.8): | |
saturation_weight = 1.0 - lightness_weight | |
fig = plt.figure(figsize=(10, 5)) | |
ax1 = fig.add_subplot(1, 2, 1) | |
ax1.imshow(image) | |
ax1.axis("off") | |
ax1.set_title("Source Image") | |
ax2 = fig.add_subplot(1, 2, 2, projection="polar") | |
max_count = max(count for _, count in colors) | |
for color, count in colors: | |
h, l, s = rgb_to_hls(color) | |
angle = h * 2 * np.pi | |
radius = (lightness_weight * l + saturation_weight * s) / 2 | |
size = count / max_count * 20 | |
ax2.plot(angle, radius, "o", color=np.array(color) / 255, markersize=size) | |
ax2.set_rticks([]) | |
ax2.set_yticklabels([]) | |
ax2.set_xticks([]) | |
ax2.set_xticklabels([]) | |
ax2.grid(False) | |
ax2.set_title("Dominant Colors (HLS Polar Plot)") | |
fig.subplots_adjust(left=0.05, right=0.95, top=0.9, bottom=0.1, wspace=0.25) | |
return fig | |
# ----- GUI App ----- | |
class ColorAnalyzerApp(tk.Tk): | |
def __init__(self): | |
super().__init__() | |
self.title("Image Color Analyzer") | |
self.geometry("1000x750") | |
self.configure(bg="#f0f0f0") | |
# Control panel with three logical sections | |
self.control_frame = ttk.Frame(self) | |
self.control_frame.pack(fill=tk.X, pady=10, padx=10) | |
# Left side: Open Image | |
self.left_controls = ttk.Frame(self.control_frame) | |
self.left_controls.pack(side=tk.LEFT, anchor="w") | |
self.open_button = ttk.Button(self.left_controls, text="Open Image", command=self.open_image) | |
self.open_button.pack(side=tk.LEFT) | |
# Center: Cluster count and lightness/saturation | |
self.center_controls = ttk.Frame(self.control_frame) | |
self.center_controls.pack(side=tk.LEFT, expand=True) | |
ttk.Label(self.center_controls, text="Clusters:").pack(side=tk.LEFT, padx=(10, 2)) | |
self.cluster_entry = ttk.Scale(self.center_controls, from_=0.0, to=50.0, orient=tk.HORIZONTAL) | |
self.cluster_entry.set(10) | |
self.cluster_entry.pack(side=tk.LEFT) | |
ttk.Label(self.center_controls, text="Lightness ↔ Saturation:").pack(side=tk.LEFT, padx=(10, 2)) | |
self.lightness_scale = ttk.Scale(self.center_controls, from_=0.0, to=1.0, orient=tk.HORIZONTAL) | |
self.lightness_scale.set(0.8) | |
self.lightness_scale.pack(side=tk.LEFT, padx=(0, 5), fill=tk.X, expand=True) | |
# Gray tolerance slider | |
ttk.Label(self.center_controls, text="Gray Tolerance:").pack(side=tk.LEFT, padx=(10, 2)) | |
self.gray_tolerance_scale = ttk.Scale(self.center_controls, from_=0, to=100, orient=tk.HORIZONTAL) | |
self.gray_tolerance_scale.set(50) # Default tolerance value | |
self.gray_tolerance_scale.pack(side=tk.LEFT, padx=(0, 5)) | |
# Right side: Replot | |
self.right_controls = ttk.Frame(self.control_frame) | |
self.right_controls.pack(side=tk.RIGHT, anchor="e") | |
self.replot_button = ttk.Button(self.right_controls, text="Replot", command=self.replot) | |
self.replot_button.pack(side=tk.RIGHT) | |
# Canvas area | |
self.canvas_frame = ttk.Frame(self) | |
self.canvas_frame.pack(fill=tk.BOTH, expand=True) | |
self.canvas_widget = None | |
self.last_image = None | |
self.last_image_np = None | |
def open_image(self): | |
file_path = filedialog.askopenfilename( | |
filetypes=[("Image files", "*.jpg *.jpeg *.png")] | |
) | |
if not file_path: | |
return | |
image = Image.open(file_path).convert("RGB") | |
image = resize_image_to_max_pixels(image, max_pixels=1_000_000) | |
self.last_image = image | |
self.last_image_np = np.array(image) | |
self._replot_from_current_state() | |
def replot(self): | |
if self.last_image is not None and self.last_image_np is not None: | |
self._replot_from_current_state() | |
def _replot_from_current_state(self): | |
try: | |
k = int(self.cluster_entry.get()) | |
except ValueError: | |
k = 50 | |
k = max(1, k) | |
lightness_weight = self.lightness_scale.get() | |
gray_tolerance = 100 - int(self.gray_tolerance_scale.get()) # Get the gray tolerance from the slider | |
dominant_colors = get_dominant_colors(self.last_image_np, k=k, gray_tolerance=gray_tolerance) | |
fig = plot_dominant_colors_radial(dominant_colors, self.last_image, lightness_weight=lightness_weight) | |
if self.canvas_widget: | |
self.canvas_widget.get_tk_widget().destroy() | |
plt.close(fig) | |
self.canvas_widget = FigureCanvasTkAgg(fig, master=self.canvas_frame) | |
self.canvas_widget.draw() | |
self.canvas_widget.get_tk_widget().pack(fill=tk.BOTH, expand=True) | |
if __name__ == "__main__": | |
app = ColorAnalyzerApp() | |
app.mainloop() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment