Palettes and dithering
The palette machinery behind the GIF, palette PNG, BMP and TGA writers, opened up: build a palette or bring your own, reduce an
image to it in its own pixel type, get the indices, and pick from the published dithers. Types live in Egami.Quantization.
Reduce colors
// A 16-color poster with Atkinson's dither; the pixel type stays as it was.
photo.Apply(ops => ops.Quantize(new PaletteOptions { MaxColors = 16, Dither = DitherMode.Atkinson }), Ct);
photo.Save("poster.png");
RasterOps.Quantize gives each frame a palette (its own colors when they fit, Wu's quantizer otherwise) and replaces each pixel by
its palette color, keeping the pixel type: an Rgba16 image stays Rgba16 with at most MaxColors distinct values. The
PaletteOptions that the writers take drive it:
MaxColors,DitherandAlphaThreshold, as in 1.0.FixedPalette: a fixed palette instead of building one. Every writer that takesPaletteOptionshonours it, and throws, naming both numbers, when it has more colors than the format allows.DitherStrength, 0 to 1: how much of the error is diffused, or how tall the ordered pattern is.Serpentine: error diffusion alternates direction from row to row.RefineIterations: k-means passes (Lloyd's algorithm on every fourth pixel) that polish Wu's palette; they never make it fit worse.ExactMatching: each color goes to its exact nearest palette color rather than through the 64-level lookup table.Kernel: a customErrorDiffusionKernelinstead ofDither.
The defaults are 1.0's: Wu's palette, Floyd–Steinberg at full strength, left to right, table matching. Every GIF, PNG, BMP and TGA file Egami wrote before stays the same byte for byte.
Dithers
DitherMode has 1.0's FloydSteinberg, Ordered (8 × 8 Bayer) and None, plus Atkinson, Burkes, Jarvis–Judice–Ninke, Sierra
(three rows, two rows and Lite), Stevenson–Arce and Stucki error diffusion with their published kernels; Riemersma, error
diffusion along a Hilbert curve; and Bayer2x2, Bayer4x4, Bayer16x16 and Ordered3x3 patterns. Error diffusion runs row
after row; ordered patterns treat every pixel alike, so animation frames dither the same way. Posterize takes the same modes.
// A custom error-diffusion kernel: half the error to the right, a quarter down and a quarter down-left, at 80% strength,
// scanning every other row backwards.
ErrorDiffusionKernel kernel = new(new[,] { { 0, 0, 2 }, { 1, 1, 0 } }, divisor: 4, origin: 1);
photo.Apply(ops => ops.Quantize(new PaletteOptions { MaxColors = 8, Kernel = kernel, DitherStrength = 0.8f, Serpentine = true }), Ct);
Fixed palettes
// Map onto a fixed palette: here an e-ink display's six inks.
Palette inks = Palette.FromColors([Rgba8.Black, Rgba8.White, new(255, 0, 0, 255), new(255, 255, 0, 255), new(0, 0, 255, 255), new(0, 255, 0, 255)]);
photo.Apply(ops => ops.Remap(inks, DitherMode.FloydSteinberg), Ct);
Palette.FromColors takes up to 256 colors and an optional transparent entry. RasterOps.Remap maps onto a palette as
ImageMagick's -remap does, keeping each pixel's alpha when the palette is opaque. Palette.Build and Palette.FromRaster build
one from every frame of an image; Nearest and NearestIndex look colors up.
// Build one palette from an image, polished by k-means, and use it for other images and for every GIF frame.
Palette shared = Palette.Build(first, new PaletteOptions { MaxColors = 64, RefineIterations = 6 });
second.Apply(ops => ops.Remap(shared), Ct);
second.Save("egret.gif", new GifWriterOptions { Palette = new PaletteOptions { FixedPalette = shared, Dither = DitherMode.Bayer4x4 } });
Palettes has ready-made ones:
// Ready-made palettes: web-safe colors, black and white, gray levels, and color cubes such as 3-3-2.
using Raster web = photo.Derive(ops => ops.Remap(Quantization.Palettes.WebSafe, DitherMode.Ordered), Ct);
using Raster bilevel = photo.Derive(ops => ops.Remap(Quantization.Palettes.BlackAndWhite, DitherMode.Riemersma), Ct);
using Raster fourGrays = photo.Derive(ops => ops.Remap(Quantization.Palettes.Grayscale(4)), Ct);
using Raster rgb332 = photo.Derive(ops => ops.Remap(Quantization.Palettes.Uniform(8, 8, 4), DitherMode.Bayer16x16), Ct);
Indices
// Indices plus palette, for a PDF indexed-color image or a format of your own.
using IndexedImage indexed = photo.ToIndexed(new PaletteOptions { MaxColors = 64 });
byte[] lookup = [.. indexed.Palette.Colors.SelectMany(c => new[] { c.R, c.G, c.B })];
byte[] samples = indexed.Indices.ToArray();
Raster.ToIndexed returns an IndexedImage: a byte per pixel and the palette, as a palette PNG would store them. The indices are
pooled, so dispose it.
Dominant colors
// The main colors and how much of the picture each covers, e.g. to pick brand colors from a logo or a placeholder color.
IReadOnlyList<WeightedColor> colors = logo.DominantColors(5);
foreach ((Rgba8 color, double weight) in colors)
{
Console.WriteLine($"#{color.R:X2}{color.G:X2}{color.B:X2} covers {weight:P0}");
}
Raster.DominantColors returns WeightedColors, largest share first. The image is averaged down to at most
DominantColorOptions.MaxSamples pixels, transparent ones left out, and clustered by k-means in Oklab, seeded by Wu's palette so
the result is the same on every run; clusters closer than MergeDistance merge. The weights of the colors returned sum to 1.