Skip to content

Instantly share code, notes, and snippets.

@veldman1
Last active April 22, 2025 20:15
Show Gist options
  • Save veldman1/976d1a2a153783c7c66be13955f24fa6 to your computer and use it in GitHub Desktop.
Save veldman1/976d1a2a153783c7c66be13955f24fa6 to your computer and use it in GitHub Desktop.
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