Table of Contents

Color

Color operations follow the W3C Filter Effects definitions, so they match what a browser's CSS filter does.

photo.Apply(ops => ops
    .Brightness(1.05f)     // 1 changes nothing
    .Contrast(1.1f)
    .Saturation(1.2f)
    .HueRotate(10), Ct);    // these four run as one pass

using Raster bw = photo.Derive(ops => ops.Grayscale(LumaWeights.Rec709), Ct);
using Raster old = photo.Derive(ops => ops.Sepia(0.8f).Opacity(0.9f), Ct);
Operation 1 (or 0°) Range
Brightness(amount) No change 0 gives black; above 1 brightens
Contrast(amount) No change 0 gives flat gray
Saturation(amount) No change 0 gives gray; above 1 oversaturates
HueRotate(degrees) No change at 0° and 360° Any angle
Sepia(amount) Full sepia 0 to 1
Grayscale(weights, amount, colorSpace) Full gray 0 to 1; LumaWeights.Rec709 (default) or Rec601
Invert(amount) Full inversion 0 to 1
Opacity(amount) No change 0 to 1, multiplies alpha

Adjacent color steps are multiplied together and run as one pass over the pixels.

Color matrices

// A ColorMatrix is 4 rows of 5 values: each output channel is a weighted sum of R, G, B, A plus an offset.
// Combine the ready-made ones with Then; no clamping happens in between.
ColorMatrix warm = ColorMatrix.Saturation(1.1f).Then(new ColorMatrix(
[
    1.05f, 0, 0, 0, 0,
    0, 1, 0, 0, 0,
    0, 0, 0.92f, 0, 0,
    0, 0, 0, 1, 0,
]));
photo.Apply(ops => ops.ColorMatrix(warm), Ct);

default(ColorMatrix) is the identity, so an uninitialized matrix changes nothing.

Encoded or linear

Grayscale takes a ColorSpace: Encoded (the default) weighs the stored sRGB values as most software does; Linear weighs light, which keeps the perceived brightness of saturated colors. Converting a color raster to Gray8 with As<Gray8>() uses Rec. 709 luma in linear light.

Run it

samples/Color is a console app that does what this guide shows on the bundled images: dotnet run --project samples/Color writes its results to ./output.