The main functionality facilitated by the Colourful library is the conversion between color spaces.
Colourful supports a range of color spaces out of the box. In addition to that, it's possible to implement your own color space.
Since version 3 of the library, you need to build a converter object if you want to perform any sort of conversion. The converter's job is to convert colors from a single source color space (noted as "From") to a single target color space (noted as "To").
To create a converter, you use a thing called converter builder, i.e. an instance of the ConverterBuilder
class.
The ConverterBuilder
has a fluent interface which works in several stages:
- Specify the source "From" color space, e.g.
.FromRGB()
. - Specify the target "To" color space, e.g.
.ToXYZ()
. - Call
.Build()
that produces the converter object.
For example, if you wish to build a converter from RGB to XYZ color space, you'd build it using the builder as:
var rgbToXyz = new ConverterBuilder().FromRGB().ToXYZ().Build();
If you need to convert in the opposite direction (from XYZ color space to RGB), you need another converter (remember converter always has a single source space and single target space):
var xyzToRgb = new ConverterBuilder().FromXYZ().ToRGB().Build();
However, if you try to use these examples, you'll likely receive a MissingConversionMetadataException
saying "White point is not specified, but is required for the conversion."
This indicates that for this conversion, a white point parameter (in the XYZ color space in this case) is necessary. See the section below for explanation.
In addition to specifying the source and target color spaces, you can also specify additional conversion metadata. The metadata is related to either the source or target color spaces and can further specify the conversion parameters (e.g. which RGB working space we use, or what white point we use in each end).
The metadata is specified in the fluent interface converter builder as arguments to the .FromXXX(...)
and .ToXXX(...)
extension methods.
For example, the RGB color space has one additional parameter, which is the RGB working space (by default, it uses sRGB).
On the other end, in the XYZ color space, we also have one parameter, which is the white point used in the XYZ color space. For this one there is no default value.
// assumes the source color is sRGB (which has the D65 white point), but we don't know the target white point, so we get MissingConversionMetadataException
var converter1 = new ConverterBuilder().FromRGB().ToXYZ().Build();
// assumes the source color is sRGB (which has the D65 white point), target will be XYZ with the D65 white point, same as sRGB, so we won't need to perform chromatic adaptation
var converter2 = new ConverterBuilder().FromRGB().ToXYZ(Illuminants.D65).Build();
// assumes the source color is sRGB (which has the D65 white point), target will be XYZ with the D50 white point, chromatic adaptation from D65 to D50 will be performed during the conversion
var converter3 = new ConverterBuilder().FromRGB().ToXYZ(Illuminants.D50).Build();
// assumes the source color is ProPhoto RGB (which has D50 white point), target will be XYZ with the D50 white point, because both white points are the same, no chromatic adaptation will be performed
var converter4 = new ConverterBuilder().FromRGB(RGBWorkingSpaces.ProPhotoRGB).ToXYZ(Illuminants.D50).Build();
Note that the white points might not always be necessary. For example, when converting from xy chromaticity space to XYZ color space, the reference white points are optional, but you'd probably not usually specify them.
// note no reference white points are needed here (they'd only be specified if they are different)
var converter5 = new ConverterBuilder().Fromxy().ToXYZ().Build();
Other color spaces might have different metadata that might be useful or even required to configure for some kinds of conversions. To see what conversion metadata is available, always check the parameters of the .FromXXX(...)
and .ToXXX(...)
methods to see what you can configure.
For most color spaces, you'll see a white point setting.
The type of the converter you build will be IColorConverter<TSource, TTarget>
, e.g. IColorConverter<RGBColor, XYZColor>
in our first example.
The IColorConverter
interface is very simple, it only has one method:
TTarget Convert(in TSource sourceColor);
The converter is intended to be built once, and then re-used for each conversion you need to do.
// create the converter once (e.g. store it in a field somewhere)
private readonly IColorConverter<RGBColor, XYZColor> _rgbToXyz = new ConverterBuilder().FromRGB().ToXYZ(Illuminants.D65).Build();
// use it for conversion
var rgbColor1 = new RGBColor(1, 0, 0.5);
var xyzColor1 = _rgbToXyz.Convert(rgbColor1);
// not creating another converter, re-using the same
var rgbColor2 = new RGBColor(0.7, 0.2, 1);
var xyzColor2 = _rgbToXyz.Convert(rgbColor2);
There is one important change for users coming from v2. During the conversion, the target values might actually end up outside of the range of the target color space.
For example, in RGB, we are used to work with channel values from 0 to 1 (correspond to 0 to 100% of red/green/blue). But if we're converting from a color space with a wider gamut than RGB, we might end up with values either below 0 or above 1. These values are NOT automatically clamped to fit the range.
This is done mainly for these reasons:
- So we wouldn't lose the color information in case we're doing some processing and then converting the color to another color space.
- Because in different scenarios you might want to treat the outside values differently. For example, sometimes you might want to simply clamp them (i.e. if it's higher than 1, clamp it to 1, etc.), but in other scenarios, you might want to normalize the values (e.g. by finding the highest channel value, and then divide all channels by it, for RGB, see the
.NormalizeIntensity()
function).
This additional processing is now left to the user. For more information about channel ranges, clamping, and the helpers that Colourful provides, see the Ranges of channel values and clamping page.
In case you want to perform chromatic adaptation, you can also use the converter builder to accomplish this. This is because the source and target color spaces can actually be the same space. That is, they'd only differ in their parameters.
For example, you might:
- Convert between XYZ relative to D50 to XYZ relative to D65.
- Convert between Lab relative to D50 to Lab relative to D65.
- Convert between RGB in sRGB working space to RGB in Adobe RGB working space.
- etc.
var xyzD50ToD65 = new ConverterBuilder().FromXYZ(Illuminants.D50).ToXYZ(Illuminants.D65).Build();
var xyzAdapted = xyzD50ToD65.Convert(new XYZColor(0.5, 0.5, 0.5));
var labD50ToD65 = new ConverterBuilder().FromLab(Illuminants.D50).ToLab(Illuminants.D65).Build();
var labAdapted = labD50ToD65.Convert(new LabColor(50, -30, 75));
var sRgbToAdobe = new ConverterBuilder().FromRGB(RGBWorkingSpaces.sRGB).ToRGB(RGBWorkingSpaces.AdobeRGB1998).Build();
var rgbAdapted = sRgbToAdobe.Convert(new RGBColor(0.25, 0.65, 0.1));
You can also create your own custom illuminant and use it to "white balance" the color to the desired illuminant.
// adapt a XYZ color relative to D65 to a custom white point
var xyzCustomWhitePoint = new XYZColor(0.95, 0.54, 0.72);
var xyzCustomWhitePointAdapter = new ConverterBuilder().FromXYZ(Illuminants.D65).ToXYZ(xyzCustomWhitePoint).Build();
var xyzAdapted = xyzCustomWhitePointAdapter.Convert(new XYZColor(0.5, 0.5, 0.5));
Chromatic adaptation between different white points is implemented by converting to the LMS color space where the adaptation is performed.
The conversion to LMS is implemented via a matrix that's applied on a vector in the XYZ color space. There are many different matrices that can be used to facilitate this conversion. By default, the Colourful library will use the Bradford matrix:
public static readonly double[,] Bradford =
{
{ 0.8951, 0.2664, -0.1614 },
{ -0.7502, 1.7135, 0.0367 },
{ 0.0389, -0.0685, 1.0296 },
};
You can setup different LMS transformation matrices that are built into Colourful, or alternatively, you can also plug your own in.
One way to set up this LMS transformation matrix is via a constructor parameter of the ConverterBuilder
class.
// Default; Bradford chromatic adaptation transform matrix (used in CMCCAT97)
new ConverterBuilder();
// Von Kries chromatic adaptation transform matrix (Hunt-Pointer-Estevez adjusted for D65).
new ConverterBuilder(LMSTransformationMatrix.VonKriesHPEAdjusted);
// Von Kries chromatic adaptation transform matrix (Hunt-Pointer-Estevez for equal energy).
new ConverterBuilder(LMSTransformationMatrix.VonKriesHPE);
// XYZ scaling chromatic adaptation transform matrix.
new ConverterBuilder(LMSTransformationMatrix.XYZScaling);
// Spectral sharpening and the Bradford transform.
new ConverterBuilder(LMSTransformationMatrix.BradfordSharp);
// CMCCAT2000 (fitted from all available color data sets).
new ConverterBuilder(LMSTransformationMatrix.CMCCAT2000);
// CAT02 (optimized for minimizing CIELAB differences).
new ConverterBuilder(LMSTransformationMatrix.CAT02);
// custom LMS transformation matrix
double[,] myCustomMatrix =
{
{ 1.2694, -0.0988, -0.1706 },
{ -0.8364, 1.8006, 0.0357 },
{ 0.0297, -0.0315, 1.0018 },
};
new ConverterBuilder(myCustomMatrix);
Note that if no chromatic adaptation is performed during the conversion, then the chosen matrix has no effect.
What you might see as a user of the ConverterBuilder
class is a mention of conversion strategies. This is an advanced concept used if you want to extend Colourful with your own custom color spaces, or if you want to further customize the conversion process. This might not be useful for most cases.
Implenting custom color spaces is possible, but not yet documented. See #93.