javaandroidcolorscolor-space

how calculate distance between two RGB colors


Writing an Android application for color matching, I found that euclidean distance in the RGB color space is not the best function, as incorrect matches are provided by this type of distance. For example if we consider the list of HTML colors and look the color with minimum distance from (5,68,174) we get the MediumBlue color (0, 0, 205) that does not contain any red and green component. Which alternative functions can provide better result?

Here is the euclidean distance in the RGB space in Java:

public double euclideanColorDistance(Color c1, Color c2) {
    int redDiff = c1.red - c2.red;
    int greenDiff = c1.green - c2.green;
    int blueDiff = c1.blue - c2.blue;
    return Math.sqrt(redDiff * redDiff + greenDiff * greenDiff + blueDiff * blueDiff);
}

Solution

  • Color distance based on euclidean distance in the RGB space is not the best choice, in fact it is possible to use the distance defined in other color spaces that are more appropriate to model human vision, e.g CIE, HSL HSB or YCbCr.

    Looking for the same color (5,68,174) of the question against the same list of HTML colors but using a distance function based on YCbCr color space, we get the RoyalBlue color (65, 105, 225) that looks better near the original color.

    The function based on YCbCr color space may be coded in Java as:

        public double yCbCrColorDistance(Color c1, Color c2) {
            double[] result1 = chrominance_luminance(c1);
            double[] result2 = chrominance_luminance(c2);
            double delta_luminance = result1[0] - result2[0];
            double delta_Cb = result1[1] - result2[1];
            double delta_Cr = result1[2] - result2[2];
            return Math.sqrt(delta_luminance * delta_luminance + delta_Cb * delta_Cb + delta_Cr * delta_Cr);
        }
    
        private double[] chrominance_luminance(Color color) {
            double luminance = 0.299 * color.red + 0.587 * color.green + 0.114 * color.blue;
            double Cb = 0.564 * (color.blue - luminance);
            double Cr = 0.713 * (color.red - luminance);
            double[] result = {luminance, Cb, Cr};
            return result;
        }
    

    A better color distance is provided by delta E, as suggested by Wacton, here is an implementation in Java based on RGBToLab:

        public static double deltaE(Color c1, Color c2) {
            double [] outLab1 = new double[3];
            RGBToLAB(c1, outLab1);
    
            double [] outLab2 = new double[3];
            RGBToLAB(c2, outLab2);
    
            double diff_l = outLab1[0] - outLab2[0]; 
            double diff_a = outLab1[1] - outLab2[1]; 
            double diff_b = outLab1[2] - outLab2[2]; 
            return Math.sqrt(diff_l * diff_l + diff_a * diff_a + diff_b * diff_b);
        }
    
        public static void RGBToLAB(Color color, double [] outLab) {
            RGBToXYZ(color.red, color.green, color.blue, outLab);
            XYZToLAB(outLab[0], outLab[1], outLab[2], outLab);
        }
    
        private static final double XYZ_WHITE_REFERENCE_X = 95.047;
        private static final double XYZ_WHITE_REFERENCE_Y = 100;
        private static final double XYZ_WHITE_REFERENCE_Z = 108.883;
        private static final double XYZ_EPSILON = 0.008856;
        private static final double XYZ_KAPPA = 903.3;
    
        public static void RGBToXYZ(int r, int g, int b, double [] outXyz) {
            double sr = r / 255.0;
            sr = sr < 0.04045 ? sr / 12.92 : Math.pow((sr + 0.055) / 1.055, 2.4);
            double sg = g / 255.0;
            sg = sg < 0.04045 ? sg / 12.92 : Math.pow((sg + 0.055) / 1.055, 2.4);
            double sb = b / 255.0;
            sb = sb < 0.04045 ? sb / 12.92 : Math.pow((sb + 0.055) / 1.055, 2.4);
            outXyz[0] = 100 * (sr * 0.4124 + sg * 0.3576 + sb * 0.1805);
            outXyz[1] = 100 * (sr * 0.2126 + sg * 0.7152 + sb * 0.0722);
            outXyz[2] = 100 * (sr * 0.0193 + sg * 0.1192 + sb * 0.9505);
        }
    
        public static void XYZToLAB(double x, double y, double z, double [] outLab) {
            x = pivotXyzComponent(x / XYZ_WHITE_REFERENCE_X);
            y = pivotXyzComponent(y / XYZ_WHITE_REFERENCE_Y);
            z = pivotXyzComponent(z / XYZ_WHITE_REFERENCE_Z);
            outLab[0] = Math.max(0, 116 * y - 16);
            outLab[1] = 500 * (x - y);
            outLab[2] = 200 * (y - z);
        }
    
        private static double pivotXyzComponent(double component) {
            return component > XYZ_EPSILON ? Math.pow(component, 1 / 3.0): (XYZ_KAPPA * component + 16) / 116;
        }
    
    

    The CIE based distance is more accurate, but uses more CPU clock cycles as it uses Math.pow()