import numpy as np from PIL import Image import gradio as gr import tempfile import os def initialize_centroids(X, k): np.random.seed(42) indices = np.random.choice(X.shape[0], k, replace=False) return X[indices] def compute_centroids(X, labels, k): centroids = [] for i in range(k): cluster_points = X[labels == i] if len(cluster_points) > 0: centroids.append(cluster_points.mean(axis=0)) else: centroids.append(X[np.random.choice(len(X))]) return np.array(centroids) def assign_clusters(X, centroids): distances = np.linalg.norm(X[:, np.newaxis] - centroids, axis=2) return np.argmin(distances, axis=1) def kmeans(X, k, max_iters=100): centroids = initialize_centroids(X, k) for _ in range(max_iters): labels = assign_clusters(X, centroids) new_centroids = compute_centroids(X, labels, k) if np.allclose(centroids, new_centroids): break centroids = new_centroids return centroids, labels def compress_image(img, clusters=16): if clusters < 2: return None, "Minimum 3 clusters required." # Resize for speed & consistency max_dim = 512 w, h = img.size scale = max_dim / max(w, h) img = img.resize((int(w * scale), int(h * scale))) img_np = np.array(img) shape = img_np.shape img_flat = img_np.reshape(-1, 3).astype(float) centroids, labels = kmeans(img_flat, clusters, max_iters=30) compressed_flat = centroids[labels].astype(np.uint8) compressed_img = compressed_flat.reshape(shape) # Save original and compressed to compare file sizes (use PNG to avoid JPEG inconsistency) with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as orig: Image.fromarray(img_np).save(orig.name, format='PNG') orig_size = os.path.getsize(orig.name) with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as comp: Image.fromarray(compressed_img).save(comp.name, format='PNG') comp_size = os.path.getsize(comp.name) os.remove(orig.name) os.remove(comp.name) percent = 100 * (orig_size - comp_size) / orig_size if percent < 0: status = f"⚠️ Larger by {abs(percent):.2f}% (due to more color detail)" else: status = f"✅ Compressed by {percent:.2f}%" return Image.fromarray(compressed_img), status iface = gr.Interface( fn=compress_image, inputs=[ gr.Image(type="pil", label="Upload Image"), gr.Slider(2, 64, value=16, step=1, label="Number of Clusters") ], outputs=[ gr.Image(type="pil", label="Compressed Image"), gr.Label(label="Compression Info") ], title="Fixed K-Means Image Compressor", description="Uses KMeans clustering on color pixels. PNG is used to give accurate compression size comparison." ) if __name__ == "__main__": iface.launch()