CIEDE2000 implementation in C

Function version: v1.0.0
Site statistics
Number of visits847
Number of files viewed900 + 380

This page presents a reference implementation of the CIEDE2000 color difference formula in C. If you want to ensure perfect compatibility (to the tenth decimal place) with certain third-party implementations, it may be necessary to modify the comments in the source code; the following link automates this operation for you.

Diagram of the full-form CIEDE2000 formula with L*a*b* components and adjustments

The ΔE2000 function in C

Let’s consider the more common and academic (Sharma, 2005) of the two formulations.

// This function written in C is not affiliated with the CIE (International Commission on Illumination),
// and is released into the public domain. It is provided "as is" without any warranty, express or implied.

#include <math.h>

// Expressly defining pi ensures that the code works on different platforms.
#ifndef M_PI
#define M_PI 3.14159265358979323846264338328
#endif

// The classic CIE Ξ”E2000 implementation, which operates on two L*a*b* colors, and returns their difference.
// "l" ranges from 0 to 100, while "a" and "b" are unbounded and commonly clamped to the range of -128 to 127.
static double ciede_2000(const double l_1, const double a_1, const double b_1, const double l_2, const double a_2, const double b_2) {
	// Working in C with the CIEDE2000 color-difference formula.
	// k_l, k_c, k_h are parametric factors to be adjusted according to
	// different viewing parameters such as textures, backgrounds...
	const double k_l = 1.0;
	const double k_c = 1.0;
	const double k_h = 1.0;
	double n = (sqrt(a_1 * a_1 + b_1 * b_1) + sqrt(a_2 * a_2 + b_2 * b_2)) * 0.5;
	n = n * n * n * n * n * n * n;
	// A factor involving chroma raised to the power of 7 designed to make
	// the influence of chroma on the total color difference more accurate.
	n = 1.0 + 0.5 * (1.0 - sqrt(n / (n + 6103515625.0)));
	// Application of the chroma correction factor.
	const double c_1 = sqrt(a_1 * a_1 * n * n + b_1 * b_1);
	const double c_2 = sqrt(a_2 * a_2 * n * n + b_2 * b_2);
	// atan2 is preferred over atan because it accurately computes the angle of
	// a point (x, y) in all quadrants, handling the signs of both coordinates.
	double h_1 = atan2(b_1, a_1 * n);
	double h_2 = atan2(b_2, a_2 * n);
	h_1 += (h_1 < 0.0) * 2.0 * M_PI;
	h_2 += (h_2 < 0.0) * 2.0 * M_PI;
	n = fabs(h_2 - h_1);
	// Cross-implementation consistent rounding.
	if (M_PI - 1E-14 < n && n < M_PI + 1E-14)
		n = M_PI;
	// When the hue angles lie in different quadrants, the straightforward
	// average can produce a mean that incorrectly suggests a hue angle in
	// the wrong quadrant, the next lines handle this issue.
	double h_m = (h_1 + h_2) * 0.5;
	double h_d = (h_2 - h_1) * 0.5;
	h_d += (M_PI < n) * M_PI;
	// πŸ“œ Sharma’s formulation doesn’t use the next line, but the one after it,
	// and these two variants differ by Β±0.0003 on the final color differences.
	h_m += (M_PI < n) * M_PI;
	// h_m += (M_PI < n) * ((h_m < M_PI) - (M_PI <= h_m)) * M_PI;
	const double p = 36.0 * h_m - 55.0 * M_PI;
	n = (c_1 + c_2) * 0.5;
	n = n * n * n * n * n * n * n;
	// The hue rotation correction term is designed to account for the
	// non-linear behavior of hue differences in the blue region.
	const double r_t = -2.0 * sqrt(n / (n + 6103515625.0))
			* sin(M_PI / 3.0 * exp(p * p / (-25.0 * M_PI * M_PI)));
	n = (l_1 + l_2) * 0.5;
	n = (n - 50.0) * (n - 50.0);
	// Lightness.
	const double l = (l_2 - l_1) / (k_l * (1.0 + 0.015 * n / sqrt(20.0 + n)));
	// These coefficients adjust the impact of different harmonic
	// components on the hue difference calculation.
	const double t = 1.0	+ 0.24 * sin(2.0 * h_m + M_PI / 2.0)
				+ 0.32 * sin(3.0 * h_m + 8.0 * M_PI / 15.0)
				- 0.17 * sin(h_m + M_PI / 3.0)
				- 0.20 * sin(4.0 * h_m + 3.0 * M_PI / 20.0);
	n = c_1 + c_2;
	// Hue.
	const double h = 2.0 * sqrt(c_1 * c_2) * sin(h_d) / (k_h * (1.0 + 0.0075 * n * t));
	// Chroma.
	const double c = (c_2 - c_1) / (k_c * (1.0 + 0.0225 * n));
	// Returning the square root ensures that dE00 accurately reflects the
	// geometric distance in color space, which can range from 0 to around 185.
	return sqrt(l * l + h * h + c * c + c * h * r_t);
}

// Compilation is done using GCC or CLang :
// - gcc -std=c99 -Wall -Wextra -pedantic -Ofast -o ciede-2000-compiled ciede-2000.c -lm
// - clang -std=c99 -Wall -Wextra -pedantic -Ofast -o ciede-2000-compiled ciede-2000.c -lm

// GitHub Project : https://github.com/michel-leonard/ciede2000-color-matching
//   Online Tests : https://michel-leonard.github.io/ciede2000-color-matching

// L1 = 65.4   a1 = 32.8   b1 = -3.5
// L2 = 64.1   a2 = 26.8   b2 = 4.2
// CIE Ξ”E00 = 5.5643553151 (Bruce Lindbloom, Netflix’s VMAF, ...)
// CIE Ξ”E00 = 5.5643412178 (Gaurav Sharma, OpenJDK, ...)
// Deviation between implementations β‰ˆ 1.4e-5

// See the source code comments for easy switching between these two widely used Ξ”E*00 implementation variants.

k_l, k_c and k_h parameters

The parameters k_l, k_c and k_h in the CIEDE2000 formula are weighting factors applied to the brightness (Ξ”L*), chroma (Ξ”C*) and hue (Ξ”H*) components respectively. In the source code, they are defined as constants with a default value of 1, corresponding to the standard observation conditions laid down by the International Commission on Illumination (CIE). In practice, you might need to adjust these coefficients to reflect specific conditions: for example, k_l = 2 is sometimes used to give more weight to differences in brightness (a common occurrence in the textile industry), while k_c or k_h can be reduced to increase tolerance for variations in saturation or hue. In summary, these coefficients typically range from 0.5 to 2, with 1 being the most common value.

Source code accuracy and reliability

The difference between Sharma’s academic formulation and Lindbloom’s simplified formulation does not exceed Β±0.0003 on the final Ξ”E2000. This corresponds to the difference usually measured between two 32-bit implementations and is imperceptible to the human eye. The implementation presented on this page is 64-bit and guarantees at least 10 correct decimal places; the choice of one formulation over another is, therefore, a technical detail. The default formula on this page is the one most often presented in the community, it is slightly easier to vectorize.

How do you convert RGB colors to L*a*b*?

You will need to use the XYZ intermediate color space for the conversion, and if you need help, the source code is provided at the bottom of this page (using the D65 white point formalized in 1964).

CIELAB value ranges and interpretation of the Ξ”E2000

In the CIELAB color space, the L* component represents lightness and typically ranges from 0 (black) to 100 (white). The a* and b* components represent color axes: a* goes from green to red, while b* goes from blue to yellow. In practice, the values of a* and b* are almost always limited to a range between -128 and +127, although the standard does not specify an official limit for these two components.

Example of two colors presenting a just-noticeable difference (JND) according to CIEDE2000
Color 1Color 2Value of Ξ”E2000
1
2
3
Examples of CIEDE2000 values calculated between two distinct colors
Color 1Color 2Value of Ξ”E2000
5
10
15

Ξ”E2000 (CIEDE2000) measures the perceived difference between two colors: 0 means the colors are identical, and higher values (up to 185 and more) indicate a larger difference. For example, a ΔE2000 value around 5 means the colors are close, while a value around 15 means they are clearly different. When the ΔE2000 value exceeds 40, the colors being compared have virtually nothing in common, and we can no longer derive any precise information from them.

Example of use in C

// Compute the Delta E (CIEDE2000) color difference between two L*a*b* colors in C99

const double l_1 = 28.9, a_1 = 47.5, b_1 = 2.0;
const double l_2 = 28.8, a_2 = 41.6, b_2 = -1.7;

const double delta_e = ciede_2000(l_1, a_1, b_1, l_2, a_2, b_2);
printf("%.12f\n", delta_e);

// .................................................. This shows a ΔE2000 of 2.7749016764
// As explained in the comments, compliance with Gaurav Sharma would display 2.7749152801

Test results

This C function has been tested with the multi-precision Julia driver designed for this purpose.

CIEDE2000 Verification Summary :
  First Verified Line : 48,104,115.93,93,27,56.7,40.47788983936747087
             Duration : 18.26 s
            Successes : 10000000
               Errors : 0
      Average Delta E : 62.9586
    Average Deviation : 6.0772934018515914e-15
    Maximum Deviation : 2.4158453015843406e-13

Files to download

Feel free to use these files provided by Michel, even for commercial purposes.

Site statistics : downloads
FileSizeNumber of clicks
ciede-2000.c4 KB115
ciede-2000-driver.c47 KB143
ciede-2000-random.c7 KB121
ciede-2000-single-precision.c5 KB128
compare-hex-colors.c11 KB117
compare-rgb-colors.c11 KB124
test-c.yml3 KB76
vs-netflix.yml6 KB76
reference-dataset.txt4 KB380
Click on c.zip to receive all these files in an archive.

Community

What do you think of this source code or CIEDE2000? Your opinion is important to us! The guestbook already contains 9 messages - including 1 in English. Take a look and share your opinion.