using System.Collections.Generic;
using UnityEngine;
public class ImageQuantizer
{
///
/// Quantizes a texture and outputs a 3D pixel array.
///
/// The input texture.
/// The maximum number of colors in the quantized image.
/// The maximum number of iterations for the k-means algorithm.
/// A tuple containing a 3D pixel array and the color lookup table.
public static (float[,,], Vector3[,]) Quantize(Texture2D image, int maxColors, int maxIterations = 10)
{
int width = image.width;
int height = image.height;
List centroids = InitializeCentroids(image, maxColors);
Color[] pixels = image.GetPixels();
Vector3[] pixelColors = new Vector3[pixels.Length];
for (int i = 0; i < pixels.Length; i++)
{
pixelColors[i] = new Vector3(pixels[i].r, pixels[i].g, pixels[i].b);
}
// Storage for pixel-to-centroid assignments
int[] assignments = new int[pixelColors.Length];
for (int iteration = 0; iteration < maxIterations; iteration++)
{
bool centroidsChanged = false;
// Step 1: Assign each pixel to the closest centroid
for (int i = 0; i < pixelColors.Length; i++)
{
int closestCentroid = GetClosestCentroid(pixelColors[i], centroids);
if (assignments[i] != closestCentroid)
{
assignments[i] = closestCentroid;
centroidsChanged = true;
}
}
// Step 2: Recalculate centroids
Vector3[] newCentroids = new Vector3[centroids.Count];
int[] centroidCounts = new int[centroids.Count];
for (int i = 0; i < assignments.Length; i++)
{
int centroidIndex = assignments[i];
newCentroids[centroidIndex] += pixelColors[i];
centroidCounts[centroidIndex]++;
}
for (int i = 0; i < centroids.Count; i++)
{
if (centroidCounts[i] > 0)
{
newCentroids[i] /= centroidCounts[i];
}
else
{
newCentroids[i] = RandomizeCentroid(image);
}
}
if (!centroidsChanged) break;
centroids = new List(newCentroids);
}
float[,,] pixelArray = new float[width, height, 3];
for (int i = 0; i < pixelColors.Length; i++)
{
int x = i % width;
int y = i / width;
Vector3 centroidColor = centroids[assignments[i]];
pixelArray[x, y, 0] = centroidColor.x; // Red
pixelArray[x, y, 1] = centroidColor.y; // Green
pixelArray[x, y, 2] = centroidColor.z; // Blue
}
int actualColors = centroids.Count;
Vector3[,] clut = new Vector3[actualColors, 1];
for (int i = 0; i < actualColors; i++)
{
clut[i, 0] = centroids[i];
}
return (pixelArray, clut);
}
private static List InitializeCentroids(Texture2D image, int maxColors)
{
List centroids = new List();
Color[] pixels = image.GetPixels();
HashSet uniqueColors = new HashSet();
foreach (Color pixel in pixels)
{
Vector3 color = new Vector3(pixel.r, pixel.g, pixel.b);
if (!uniqueColors.Contains(color))
{
uniqueColors.Add(color);
centroids.Add(color);
if (centroids.Count >= maxColors) break;
}
}
return centroids;
}
private static Vector3 RandomizeCentroid(Texture2D image)
{
Color randomPixel = image.GetPixel(Random.Range(0, image.width), Random.Range(0, image.height));
return new Vector3(randomPixel.r, randomPixel.g, randomPixel.b);
}
private static int GetClosestCentroid(Vector3 color, List centroids)
{
int closestCentroid = 0;
float minDistanceSq = float.MaxValue;
for (int i = 0; i < centroids.Count; i++)
{
float distanceSq = (color - centroids[i]).sqrMagnitude;
if (distanceSq < minDistanceSq)
{
minDistanceSq = distanceSq;
closestCentroid = i;
}
}
return closestCentroid;
}
}