Skip to content

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 →