Palette from Image — C# source
Extract the dominant colors from any image as a reusable palette — median-cut quantization with population shares, hex and rgb, copyable — runs entirely in your browser.
This is the C# implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Palette from Image — median-cut quantization over RGBA pixels.
//
// Language: C# (.NET 8), standard library only
// CosmoDev polyglot showcase port of the `palette-from-image` tool.
// Ported from src/lib/palette-extract.ts — display source, part of CosmoDev's
// polyglot tool pages.
//
// Deterministic: stable sorts only, widest-channel median split, buckets
// average into swatches. Transparent pixels are skipped.
using System;
using System.Collections.Generic;
using System.Linq;
public readonly record struct Swatch(byte R, byte G, byte B, int Population);
public static class PaletteExtractor
{
// Down-sample so large images quantize in bounded time (TS: MAX_SAMPLES).
private const int MaxSamples = 16384;
public static List<Swatch> ExtractPalette(byte[] rgba, int maxColors = 8)
{
int total = rgba.Length / 4;
if (total == 0) return [];
var pixels = new List<(byte R, byte G, byte B)>();
int stride = Math.Max(1, total / MaxSamples);
for (int i = 0; i < total; i += stride)
{
int o = i * 4;
if (rgba[o + 3] == 0) continue; // fully transparent — skip
pixels.Add((rgba[o], rgba[o + 1], rgba[o + 2]));
}
if (pixels.Count == 0) return [];
var buckets = new List<List<(byte, byte, byte)>> { pixels };
while (buckets.Count < maxColors)
{
// Widest-range bucket with more than one distinct value splits.
int bestIdx = -1;
int bestRange = 1; // range 1 (exact duplicates) never splits
int bestChannel = 0;
for (int i = 0; i < buckets.Count; i++)
{
(int range, int channel) = ChannelRange(buckets[i]);
if (range > bestRange) { bestRange = range; bestIdx = i; bestChannel = channel; }
}
if (bestIdx < 0) break; // every bucket is uniform — done
var bucket = buckets[bestIdx];
// Stable sort on the widest channel, split at the median.
var sorted = bestChannel switch
{
0 => bucket.OrderBy(p => p.Item1, Comparer<byte>.Create((a, b) => a.CompareTo(b))).ToList(),
1 => bucket.OrderBy(p => p.Item2).ToList(),
_ => bucket.OrderBy(p => p.Item3).ToList(),
};
int mid = sorted.Count / 2;
buckets.RemoveAt(bestIdx);
buckets.Add(sorted.GetRange(0, mid));
buckets.Add(sorted.GetRange(mid, sorted.Count - mid));
}
// Each bucket averages into one swatch; population = pixel count.
return buckets
.Select(b => new Swatch(
(byte)Math.Round(b.Average(p => p.Item1)),
(byte)Math.Round(b.Average(p => p.Item2)),
(byte)Math.Round(b.Average(p => p.Item3)),
b.Count))
.OrderByDescending(s => s.Population)
.ToList();
}
private static (int range, int channel) ChannelRange(List<(byte R, byte G, byte B)> bucket)
{
int rMin = 255, rMax = 0, gMin = 255, gMax = 0, bMin = 255, bMax = 0;
foreach (var p in bucket)
{
rMin = Math.Min(rMin, p.R); rMax = Math.Max(rMax, p.R);
gMin = Math.Min(gMin, p.G); gMax = Math.Max(gMax, p.G);
bMin = Math.Min(bMin, p.B); bMax = Math.Max(bMax, p.B);
}
int r = rMax - rMin, g = gMax - gMin, b = bMax - bMin;
if (r >= g && r >= b) return (r, 0);
if (g >= b) return (g, 1);
return (b, 2);
}
}
public static class Program
{
public static void Main()
{
// Smoke: 2 red + 1 blue pixels → red swatch wins on population.
byte[] rgba = [200, 30, 30, 255, 202, 28, 32, 255, 30, 30, 200, 255];
foreach (var s in PaletteExtractor.ExtractPalette(rgba, 2))
Console.WriteLine($"#{s.R:X2}{s.G:X2}{s.B:X2} ×{s.Population}");
}
}
Also available in 9 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →