Going farther with my color application, this class calculates the distance between two colors in the LAB color space. Delta E 2000 calculates the human perspective difference in colors between colors and is said to be very close to how human's perceive colors.

The CIEDE2000 Color-Difference Formula: Implementation Notes, Supplementary Test Data, and Mathematical Observations: for reference

class DeltaE2000 {

    // Lightness weight factor
    private const KL = 1;

    // Chroma weight factor
    private const KC = 1;

    // Hue weight factor
    private const KH = 1;

    // ¯L′
    private function averageLightness($L1, $L2)
    {
        return ($L1 + $L2) / 2;
    }

    // C1 && C2
    private function chroma($a, $b)
    {
        return sqrt(($a ** 2) + ($b ** 2));
    }

    // ¯C
    private function averageChroma($chroma1, $chroma2)
    {
        return ($chroma1 + $chroma2) / 2;
    }

    // G
    private function adjustChroma($averageChroma)
    {
        return .5 * (1 - sqrt( $averageChroma ** 7 / (($averageChroma ** 7) + (25 ** 7))));
    }

    // a′1 && a′2
    private function transformA($a, $adjustChroma)
    {
        return $a * (1 + $adjustChroma);
    }

    // C′1 && C′2
    private function transformChroma($a, $b)
    {
        return sqrt(($a ** 2) + ($b ** 2));
    }

    // ¯C′
    private function averageTransformedChroma($tc1, $tc2)
    {
        return ($tc1 + $tc2) / 2;
    }

    // h′1 && h′2
    private function hueAngle($ta, $b)
    {
        $angle = atan2($b, $ta);
        $angleDegrees = rad2deg($angle);

        return ($angleDegrees >= 0) ? $angleDegrees : $angleDegrees + 360;
    }

    // ¯H′
    private function averageHueAngle($hueAngle1, $hueAngle2, $transformedChroma1, $transformedChroma2)
    {
        $averageHueAngle = ($hueAngle1 + $hueAngle2);

        if ($transformedChroma1 * $transformedChroma2 === 0) {
            return $averageHueAngle;
        }

        if (abs($hueAngle1 - $hueAngle2) <= 180) {
            return $averageHueAngle / 2;
        } else if (abs($hueAngle1 - $hueAngle2) > 180 && $averageHueAngle < 360) {
            return ($averageHueAngle + 360) / 2;
        } else if (abs($hueAngle1 - $hueAngle2) > 180 && $averageHueAngle >= 360) {
            return ($averageHueAngle - 360) / 2;
        }
    }

    // T
    private function correctionFactor($averageHueAngle)
    {
        $averageHueAngle = deg2rad($averageHueAngle);
        return 1 - 0.17 * cos($averageHueAngle - 30) + 0.24
                        * cos(2 * $averageHueAngle) + 0.32
                        * cos(3 * $averageHueAngle + 6) - 0.20
                        * cos(4 * $averageHueAngle - 63);
    }

    // Δh′
    private function hueDifference($hueAngle1, $hueAngle2, $transformedChroma1, $transformedChroma2)
    {
        if ($transformedChroma1 * $transformedChroma2 === 0) {
            return 0;
        }

        $hueDifference = $hueAngle2 - $hueAngle1;

        if (abs($hueDifference) <= 180) {
            return $hueDifference;
        } else if (abs($hueDifference) > 180) {
            return $hueDifference - 360;
        } else if (abs($hueDifference) < -180) {
            return $hueDifference + 360;
        }
    }

    // ΔL′
    private function differenceInLightness($L1, $L2)
    {
        return $L2 - $L1;
    }

    // ΔC′
    private function differenceInChroma($chroma1, $chroma2)
    {
        return $chroma2 - $chroma1;
    }

    // ΔH′
    private function differenceInHue($transformedChroma1, $transformedChroma2, $hueDifference)
    {
        $hueDifference = deg2rad($hueDifference);
        return 2 * sqrt($transformedChroma1 * $transformedChroma2) * sin($hueDifference / 2);
    }

    // SL
    private function lightnessWeightFactor($averageLightness)
    {
        return 1 + (0.015 * ($averageLightness - 50) ** 2) / sqrt(20 + ($averageLightness - 50) ** 2);
    }

    // SC
    private function chromaWeightFactor($averageTransformedChroma)
    {
        return 1 + 0.045 * $averageTransformedChroma;
    }

    // SH
    private function hueWeightFactor($averageTransformedChroma, $correctionFactor)
    {
        return 1 + 0.015 * $averageTransformedChroma * $correctionFactor;
    }

    // Δθ
    private function hueAngleAdjustment($averageHueAngle)
    {
        return 30 * exp(-((($averageHueAngle - 275) / 25) ** 2));
    }

    // RC
    private function chromaCorrectionFactor($averageTransformedChroma)
    {
        return 2 * sqrt(($averageTransformedChroma ** 7) / (($averageTransformedChroma ** 7) + (25 ** 7)));
    }

    // RT
    private function chromaAndHueCorrectionFactor($chromaCorrectionFactor, $hueAngleAdjustment)
    {
        $hueAngleAdjustment = deg2rad($hueAngleAdjustment);
        return -$chromaCorrectionFactor * sin(2 * $hueAngleAdjustment);
    }

    // For reference on the formulas
    // https://hajim.rochester.edu/ece/sites/gsharma/ciede2000/ciede2000noteCRNA.pdf
    // http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE2000.html
    public function calculate($lab1, $lab2)
    {
        list($L1, $a1, $b1) = $lab1;
        list($L2, $a2, $b2) = $lab2;

        $averageLightness = $this->averageLightness($L1, $L2);
        $chroma1 = $this->chroma($a1, $b1);
        $chroma2 = $this->chroma($a2, $b2);
        $averageChroma = $this->averageChroma($chroma1, $chroma2);
        $adjustChroma = $this->adjustChroma($averageChroma);
        $transformedA1 = $this->transformA($a1, $adjustChroma);
        $transformedA2 = $this->transformA($a2, $adjustChroma);
        $transformedChroma1 = $this->transformChroma($transformedA1, $b1);
        $transformedChroma2 = $this->transformChroma($transformedA2, $b2);
        $averageTransformedChroma = $this->averageTransformedChroma($transformedChroma1, $transformedChroma2);
        $hueAngle1 = $this->hueAngle($transformedA1, $b1);
        $hueAngle2 = $this->hueAngle($transformedA2, $b2);
        $averageHueAngle = $this->averageHueAngle($hueAngle1, $hueAngle2, $transformedChroma1, $transformedChroma2);
        $correctionFactor = $this->correctionFactor($averageHueAngle);
        $hueDifference = $this->hueDifference($hueAngle1, $hueAngle2, $transformedChroma1, $transformedChroma2);
        $differenceInLightness = $this->differenceInLightness($L1, $L2);
        $differenceInChroma = $this->differenceInChroma($transformedChroma1, $transformedChroma2);
        $differenceInHue = $this->differenceInHue($transformedChroma1, $transformedChroma2, $hueDifference);
        $lightnessWeightFactor = $this->lightnessWeightFactor($averageLightness);
        $chromaWeightFactor = $this->chromaWeightFactor($averageTransformedChroma);
        $hueWeightFactor = $this->hueWeightFactor($averageTransformedChroma, $correctionFactor);
        $hueAngleAdjustment = $this->hueAngleAdjustment($averageHueAngle);
        $chromaCorrectionFactor = $this->chromaCorrectionFactor($averageTransformedChroma);
        $chromaAndHueCorrectionFactor = $this->chromaAndHueCorrectionFactor($chromaCorrectionFactor, $hueAngleAdjustment);

        $normalizedLightness = ($differenceInLightness / (self::KL * $lightnessWeightFactor)) ** 2;
        $normalizedChroma = ($differenceInChroma / (self::KC * $chromaWeightFactor)) ** 2;
        $normalizedHue = ($differenceInHue / (self::KH * $hueWeightFactor)) ** 2;
        $correctedChroma = $chromaAndHueCorrectionFactor * ($differenceInChroma / (self::KC * $chromaWeightFactor));
        $correctedHue = ($differenceInHue / (self::KH * $hueWeightFactor));

        return sqrt($normalizedLightness + $normalizedChroma + $normalizedHue + $correctedChroma * $correctedHue);
    }
}

This code snippet was published on It was last edited on

Contributing Authors

1

2 Comments

  • Votes
  • Oldest
  • Latest
BO
443 9
Commented
$lab1 = [50, 2.5, 0];  // rgb(123, 118, 119)
$lab2 = [73, 25, -18]; // rgb(210, 164, 212)

$delta = new DeltaE2000();
$difference = $delta->calculate($lab1, $lab2);
echo $difference;  // 27.175287079114
add a comment
0
Commented

This sounds interesting. Are you utilizing this to figure out if a color is similar to another color?

Many years ago I did something similar so that people could search for website templates by color.

I would have the script take a screenshot of the template, and then do a gaussian blur until the image was essentially a single color. From there I would extract the color and store the result in a database. I would then do this for all templates.

Finally when someone would do a search for a template one of the options was to select a color from a color-like wheel. I could then use the equation to compute color distances between what the user selected and all templates and it would return the results first that had the shortest distance. It worked well and for the most part templates had the colors they were looking for.

I’ll have to look for that code, maybe add it here as an alternative snippet for computing color distances or similarity.

  • 0
    From what I'm aware, besides the CIE standards (2000 being the most correct) another equation to calculate the distance from colors is the Euclidean equation which is much simpler and not as effective, especially if working within the RGB color space. The Euclidean equation calculates the distance as the computer sees it, while Delta E 2000 calculates the difference between colors as the human perceives color. — Bogey
  • 0
    I just realized you could also calculate the angle between colors and use that as measurement of difference/distance. — Bogey
  • 1
    Yeah, there are probably multiple ways you could approach this. I looked in the legacy codebase to see how that algorithm worked for my situation, and on the client side a person would pick a color and it would come in via a POST request in hex form, such as #ff0000 for red in this case. The script would then parse out the RGB component being ff, 00, and 00, and then run that through the PHP function hexdec to turn those values back to base 10 numbers. Then the magical part for computing color distance was: R1-R2^2 + G1-G2^2 + B1-B2^2. In this case R1 is the red component the user selected while R2 is the red component of one of the templates; the same for G1, G2, B1, and B2. So just to be clear, it was the difference in the red component squared plus the difference in the green component squared plus the difference in the blue component squared. I will create a snippet at some point for this and explain it better, but I remember how amazing/surprisingly accurate it was. I am guessing your algorithm above is more accurate, but for simplicity and speed, this worked great. — Brian Wozeniak
  • 1
    The formula you're using is called Euclidean distance formula and it's not as resource intensive as the delta e 2000 formula. Do you know of ways to convert RGB back to HEX? — Bogey
  • 1
    Thank you! It's nice to have a name to the formula I was using! I imagine it would be the same way, for each RGB component value you would pass it through the reverse PHP function called dechex, and then concatenate them back together to form the #ff00ff style. — Brian Wozeniak
  • 1
    Didn't know of either of those methods in PHP and google search results just gave me converters rather than how to convert them myself. — Bogey
add a comment
1