14

Edit:

With the answer given I made this function

function grabclosestcolor($r, $g, $b){
    $colors = array(array(124,12,12),array(7,7,11),array(110,224,219),array(123,123,123),array(124,177,74),array(130,86,53),array(77,77,77),array(164,124,68),array(204,196,132),array(164,148,147),array(163,123,67),array(26,122,26), array(195,195,50),array(193,193,193),array(255,248,73),array(243,243,243));
    $differencearray = array();
    foreach ($colors as $value) {
        $difference = sqrt(pow($r-$value[0],2)+pow($g-$value[1],2)+pow($b-$value[2],2));
        array_push($differencearray, $difference);
        $smallest = min($differencearray);
        $key = array_search($smallest, $differencearray);
        return $colors[$key];
        }
    }


My goal is this. I grab a picture and loop through each pixel and grab its x,y, and rgb.

Instead of just grabbing the rgb, I have a predefined array and I'm looking for the closest match from the color I grabbed to the predefined array. The goal here is to only use colors from the predefined array. Here is my array of colors.

$colors = array(array(124,12,12),array(7,7,11),array(110,224,219),array(123,123,123),array(124,177,74),array(130,86,53),array(77,77,77),array(164,124,68),array(204,196,132),array(164,148,147),array(163,123,67),array(26,122,26), array(195,195,50),array(193,193,193),array(255,248,73),array(243,243,243));

and here is my existing code that loops through it all.

$int = imagesx($im) - 1;
$int2 = imagesy($im) - 1;
$start2 = 0;
do{
    $start = 0;
    do{
        $rgb = imagecolorat($im, $start, $start2);
        $r = ($rgb >> 16) & 0xFF;
        $g = ($rgb >> 8) & 0xFF;
        $b = $rgb & 0xFF;
        $value = rgb2hex($r,$g,$b).":$start:$start2";
        array_push($colorsofimage, $value);
    } while($int > $start++);
} while($int2 > $start2++);

rgb2hex is a User Defined Function, but what I want to accomplish is to change that function with the function to grab the closest color.

$colorsofimage contains an array of each pixels info with hex:x:y what i want it to be is rgb2hex(NEWFUNCTION($r,$g,$b)); So that the new hex is the 1 out of the predefined array.

I hope you understood, because I have no clue how to do it because I don't know the algorithm of a color.

tzvi
  • 471
  • 3
  • 5
  • 19
Ugleh
  • 207
  • 1
  • 3
  • 6
  • 2
    Depending how hard out you want to go, the answer on my (similar) question may be of use - http://stackoverflow.com/questions/4057475/rounding-colour-values-to-the-nearest-of-a-small-set-of-colours – El Yobo Dec 19 '10 at 22:47
  • Again, I don't know PHP that well, but the function that you give as your solution looks considerably less efficient than the one I suggested earlier: http://stackoverflow.com/questions/4485229/rgb-to-closest-predefined-color/4485327#4485327 – beldaz Dec 20 '10 at 03:45
  • 1
    why is there a return in the foreach loop? – Joeri Sep 20 '15 at 13:47

5 Answers5

17

You have to calculate the distance to each color, and pick the smallest.

There are a few ways to do this. A simple method would be to calculate the distance would be:

sqrt((r-r1)^2+(g-g1)^2+(b-b1)^2)

A better method might be to incorporate the weighted values to calculate a distance, for instance the values used when converting RGB->YUV:

Y = 0.299 * R + 0.587 * G + 0.114 * B

in that case you would use

sqrt(((r - r1) * .299)^2 + ((g - g1) * .587)^2 + ((b - b1) * .114)^2)

Of course, since you don't need the exact distances, just a comparison, you can and probably should just skip the square root, making the last calculation:

((r - r1) * .299)^2 + ((g - g1) * .587)^2 + ((b - b1) * .114)^2
Kevin Stricker
  • 17,178
  • 5
  • 45
  • 71
  • Thanks for this, im going to edit the OP and put the function I made. – Ugleh Dec 19 '10 at 22:39
  • 5
    Random thought: Strictly speaking you don't need to take the square root and can just find the smallest square if performance is a concern. – Kevin Stricker Dec 19 '10 at 22:40
  • 1
    @mootinator Why do we need to square the values at all? Why not just shortest distance? – Albert Renshaw Jun 23 '13 at 19:53
  • @mootinator Are we squaring to eliminate negative values? Does anyone know if the absolute value function is faster than squaring? Or simply converting to a string and stripping the negative then converting back to a number, or maybe just multiplying itself by negative 1 if it's negative? – Albert Renshaw Jun 23 '13 at 19:55
  • 2
    @Albert Renshaw We're squaring to find the euclidean distance between two points. We're skipping taking the square root of the distance because that step doesn't affect the sort order of the distances. – Kevin Stricker Jun 24 '13 at 14:36
  • @mootinator Okay so it's the same as just taking the absolute value of the difference? – Albert Renshaw Jun 24 '13 at 23:17
  • No. Simple example where they are definitely not the same: 5+5+5=15 Squared before adding=75. 1+1+12 = 14 Squared before adding = 144 Several people wouldn't have suggested squaring the differences if there were no point to it other than to remove negative values. – Kevin Stricker Jun 25 '13 at 15:10
  • Obviously I meant to say 146 for that last calculation ;) – Kevin Stricker Jun 25 '13 at 15:42
  • Euclidian distance gives me shoddy results: a gray of similar saturation may be closer than a similar hue with "more" different saturation. Since hue is the first humans notice, it should take precedence -- Euclidian distance does not do that, with or without weights. – Raphael Apr 01 '17 at 07:28
11

The RGB colour-space is simply a cube. In 24-bit colour each side has a length of 256, allowing values from 0 to 255. In order to find the closest colour in within this cube, you need a distance function. The simplest and most intuitive is the Euclidean distance: if you have colour (r1, g1, b1) and another colour (r2, g2, b2) the distance would be sqrt((r2-r1)^2 + (g2-g1)^2 + (b2-b1)^2).

The challenge for you is then to find the best match across all the values in your predefined array. I suggest that you start simply by iterating over all your values and check the distance for each in turn. Note that for this purpose you do not need to perform the sqrt, simply comparing on the sum of the squares would be sufficient, and would have the benefit of being all based in integer maths. My PHP isn't great, but roughly you would do:

function dist($col1,$col2) {
  $delta_r = $col1[0] - $col2[0];
  $delta_g = $col1[1] - $col2[1];
  $delta_b = $col1[2] - $col2[2];
  return $delta_r * $delta_r + $delta_g * $delta_g + $delta_b * $delta_b;
} 

$closest=$colors[0];
$mindist=dist($rgb,$colors[0]);
$ncolors=sizeof($colors);
for($i = 1; $i < $ncolors; ++$i)
{
    $currdist = dist($rgb,$colors[$i]);
    if($currdist<$mindist) {
      $mindist=$currdist;
      $closest=$colors[$i];
    }
}

There are more complicated distance functions (for instance, taking better account of psychovisual interpretation of colour differences (look into Delta E) but I suspect this is more than you need.

unloco
  • 6,928
  • 2
  • 47
  • 58
beldaz
  • 4,299
  • 3
  • 43
  • 63
  • It's not necessarily a cube, even if it's commonly done so. It's primarily a vector space, and could as well be a paralleloid. – user502515 Dec 20 '10 at 02:12
  • @user502515: It _could_ be, but it's unlikely. All the RGB colour spaces I've encountered (most obviously sRGB) tend to be bounded in the range 0 to 1 or 0 to 255. Other colour spaces, such as CIE Lab, do indeed have different bounds, but they are not RGB. – beldaz Dec 20 '10 at 03:40
  • @beldaz why are we squaring the distance as well? Why not just take the sum of the distance of each color? – Albert Renshaw Jun 23 '13 at 19:40
  • Is it to eliminate negative values? Are there quicker ways of doing this? – Albert Renshaw Jun 23 '13 at 19:55
  • @Albert the sum of distances would be like walking around a square rather than walking directly across it: the distances are different (FWIW your approach is sometimes referred to as a Manhattan Distance). – beldaz Jul 02 '13 at 06:52
  • @beldaz Okay that makes sense on a euclidean plane... but with colors there are 3 values... We take the square to remove any negative values but you are still just summing the "manhattan distance" of 2d values on a 3d region. Wouldn't you need to cube at that point? – Albert Renshaw Jul 02 '13 at 16:42
  • @beldaz When you are comparing values to an array of fixed values if all distances are manhattan distances anyways wouldn't you still get the same result for "closest color" minus all the time you spend squaring those digits? – Albert Renshaw Jul 02 '13 at 16:42
  • @beldaz If we are going to not use the `sqrt` function I mean... If we are going to use the `sqrt` function than it's the different between euclidean distances and manhattan distances... but if you are eliminating that than you are just taking my value and squaring it... The only reason we square is for absolute values... the sqrt gets us the true value but it can be eliminated since we are comparing distances to a static array... but if you are going to eliminate the sqrt you are taking the manhattan distance again, your numbers are just larger is all... – Albert Renshaw Jul 02 '13 at 16:45
  • @AlbertRenshaw No, you don't need to cube your values. You always square them, regardless of the number of dimensions. This is from geometry. The point of squaring is not to get absolute numbers. The distance between two vectors using sum of the squares of the difference along each dimension will be different from Manhattan distance due to the cross terms generated by the squaring (not the sqrt of the result). – beldaz Jul 04 '13 at 08:05
  • @beldaz Isn't the distance between the two vectors only the sum of the distance after you square root it... if you are skipping the square root step you just have the manhattan distance with it's components squared. The point of squaring then square-rooting is to get the hypotenuse via the pythagorean theorem... but if you change the pythagorean theorem to not include a square root you are now just summing the squares of the legs (the manhattan distance with it's components squared) – Albert Renshaw Jul 04 '13 at 16:44
  • @beldaz You CAN ignore the square-root because the closest value will be the same wether you square-root all values or don't square-root any values because the smallest number will product the smallest square-root value. But by that logic you don't have to square it either, you just need to make sure you have positive values otherwise you might end up subtracting distances, that's why you are leaving the square in but taking the square-root out... and if you are going to do that, why not take both out and just use absolute value? Smaller numbers 0:) – Albert Renshaw Jul 04 '13 at 16:45
  • @AlbertRenshaw Yes, you can ignore the sqrt step, but I think you've misunderstood the squaring step. You square the distance in each dimension separately. The Euclidean distance is sqrt((r-r0)^2 + (g-g0)^2 + (b-b0)^2). This is different to the Manhattan distance (abs(r-r0) + abs(g-g0) + abs(b-b0)). Do the maths by expanding them out and you'll see that the former contains cross terms that the latter does not. It's not about positive or negative values. – beldaz Jul 05 '13 at 01:05
  • @beldaz Well it is also about negative and positive values because a distance of -15 and a distance of 10 sum to -5 total distance when really it's 25 total distance... but that's besides the point.. what I'm saying is you are no longer finding euclidean distance if you ignore the square-root, you are now finding the sum of two separate distances (the manhattan rows/columns) squared. Euclidean distance works because of the Pythagorean theorem taking two lines (manhattan columns and rows) and averaging them into 1 hypotenuse... once you remove the sqrt you are no longer doing this. – Albert Renshaw Jul 05 '13 at 05:27
  • @AlbertRenshaw Distances are typically magnitudes, with an associated direction vector. So you don't have a negative distance (Delta E) of -15, you'd have a distance of 15. A hypotenuse is not an average. Summing squares introduces cross terms that are there regardless of whether you sqrt the result or not, for anything more than one dimension. – beldaz Jul 05 '13 at 07:14
  • @beldaz Sorry, "Average" was the wrong word :o) and when I take one red value and subtract the other red value sometimes I get negative values (negative distance) so I have to absolute value it (or square it) that's all I meant!:) – Albert Renshaw Jul 05 '13 at 07:34
  • Doesn't this formula rather choose a different color instead of a different shade? I think I would go for a hsl translation first. ( not sure though ). – Joeri Sep 20 '15 at 13:50
  • @Joeri The distinction between color and shade is rather subjective. In RGB there is no difference. HSL doesn't have shades either, but it does have hues and saturation. The problem is that there's no rudimentary distance function between two HSL coordinates. For a proper psychovisual color space with a well defined distance function you should use CIELab. – beldaz Sep 20 '15 at 22:30
  • @beldaz thanks. I was just wondering if 255,255,255 was closer to 254,254,254 or 253,255,255. – Joeri Sep 21 '15 at 10:36
  • Keeping things simple the first distance would be sqrt(3) and the second would be sqrt(4), so the last color would be further away. Whether it would look a more different color is another matter, and this is what psychovisual color spaces and their distance functions are for. – beldaz Sep 21 '15 at 21:06
  • He thanks @beldaz. I've looked into CIElab but experts seem to agree that some color differences are evaluated differently between the color difference ΔE*ab and the human eye... I'm going to look into this some more. – Joeri Oct 18 '15 at 20:15
6

Since this question is displayed in the top ten of goolge search results, here is a more complex function I wrote some years ago, which produced better results than the existing PHP functions.

/*
 * Die Funktion gibt den Array-Schlüssel der Farbe ($palette),
 * die am ehesten der Farbe $givenColor entspricht.
 * 
 * Returns the index of the palette-color which is most similar
 * to $givenColor.
 * 
 * $givenColor und die Einträge in $palette können entweder
 * Strings im Format (#)rrggbb
 * (z. B. "ff0000", "4da4f3" oder auch "#b5d7f3")
 * oder Arrays mit je einem Wert für Rot, Grün und Blau 
 * (z. B. $givenColor = array( 0xff, 0x00, 0x00 ) )
 * sein.
 * 
 * $givenColor and the colors in $palette should be either
 * formatted as (#)rrggbb
 * (e. g. "ff0000", "4da4f3" or "#b5d7f3")
 * or arrays with values for red, green and blue
 * (e. g. $givenColor = array( 0xff, 0x00, 0x00 ) )
 *
 * Referenzen/References:
 * function rgb2lab
 *   - http://www.f4.fhtw-berlin.de/~barthel/ImageJ/ColorInspector//HTMLHilfe/farbraumJava.htm
 *   - http://www.brucelindbloom.com/index.html?Eqn_RGB_to_XYZ.html
 *   - http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Lab.html
 *
 * function deltaE
 *   - http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CMC.html
 */
function getNearestColor( $givenColor,
                          $palette = array('blue' => '0000ff','red' => 'ff0000','green' => '00ff00','yellow' => 'ffff00','black' => '000000','white' => 'ffffff','orange' => 'ff8800','purple' => 'ff00ff', 'teal' => '00ffff')
  )
{
  if(!function_exists('rgb2lab'))
  {
    function rgb2lab($rgb) {
      $eps = 216/24389; $k = 24389/27;
      // reference white D50
      $xr = 0.964221; $yr = 1.0; $zr = 0.825211;
      // reference white D65
      #$xr = 0.95047; $yr = 1.0; $zr = 1.08883;

      // RGB to XYZ
      $rgb[0] = $rgb[0]/255; //R 0..1
      $rgb[1] = $rgb[1]/255; //G 0..1
      $rgb[2] = $rgb[2]/255; //B 0..1

      // assuming sRGB (D65)
      $rgb[0] = ($rgb[0] <= 0.04045)?($rgb[0]/12.92):pow(($rgb[0]+0.055)/1.055,2.4);
      $rgb[1] = ($rgb[1] <= 0.04045)?($rgb[1]/12.92):pow(($rgb[1]+0.055)/1.055,2.4);
      $rgb[2] = ($rgb[2] <= 0.04045)?($rgb[2]/12.92):pow(($rgb[2]+0.055)/1.055,2.4);

      // sRGB D50
      $x =  0.4360747*$rgb[0] + 0.3850649*$rgb[1] + 0.1430804*$rgb[2];
      $y =  0.2225045*$rgb[0] + 0.7168786*$rgb[1] + 0.0606169*$rgb[2];
      $z =  0.0139322*$rgb[0] + 0.0971045*$rgb[1] + 0.7141733*$rgb[2];
      // sRGB D65
      /*$x =  0.412453*$rgb[0] + 0.357580*$rgb[1] + 0.180423*$rgb[2];
      $y =  0.212671*$rgb[0] + 0.715160*$rgb[1] + 0.072169*$rgb[2];
      $z =  0.019334*$rgb[0] + 0.119193*$rgb[1] + 0.950227*$rgb[2];*/

      // XYZ to Lab
      $xr = $x/$xr; $yr = $y/$yr; $zr = $z/$zr;

      $fx = ($xr > $eps)?pow($xr, 1/3):($fx = ($k * $xr + 16) / 116); $fy = ($yr > $eps)?pow($yr, 1/3):($fy = ($k * $yr + 16) / 116); $fz = ($zr > $eps)?pow($zr, 1/3):($fz = ($k * $zr + 16) / 116);

      $lab = array();
      $lab[] = round(( 116 * $fy ) - 16); $lab[] = round(500*($fx-$fy)); $lab[] = round(200*($fy-$fz));      
      return $lab;
    } // function rgb2lab
  }

  if(!function_exists('deltaE'))
  {
    function deltaE($lab1, $lab2)
    {
      // CMC 1:1
      $l = 1; $c = 1;

      $c1 = sqrt($lab1[1]*$lab1[1]+$lab1[2]*$lab1[2]); $c2 = sqrt($lab2[1]*$lab2[1]+$lab2[2]*$lab2[2]);

      $h1 = (((180000000/M_PI) * atan2($lab1[1],$lab1[2]) + 360000000) % 360000000)/1000000;

      $t = (164 <= $h1 AND $h1 <= 345)?(0.56 + abs(0.2 * cos($h1+168))):(0.36 + abs(0.4 * cos($h1+35)));
      $f = sqrt(pow($c1,4)/(pow($c1,4) + 1900));

      $sl = ($lab1[0] < 16)?(0.511):((0.040975*$lab1[0])/(1 + 0.01765*$lab1[0]));
      $sc = (0.0638 * $c1)/(1 + 0.0131 * $c1) + 0.638;
      $sh = $sc * ($f * $t + 1 -$f);

      return sqrt( pow(($lab1[0]-$lab2[0])/($l * $sl),2) + pow(($c1-$c2)/($c * $sc),2) + pow(sqrt(($lab1[1]-$lab2[1])*($lab1[1]-$lab2[1]) + ($lab1[2]-$lab2[2])*($lab1[2]-$lab2[2]) + ($c1-$c2)*($c1-$c2))/$sh,2) );
    } // function deltaE
  }

  if(!function_exists('colorDistance'))
  {
    function colorDistance($lab1,$lab2)
    {
      return sqrt(($lab1[0]-$lab2[0])*($lab1[0]-$lab2[0])+($lab1[1]-$lab2[1])*($lab1[1]-$lab2[1])+($lab1[2]-$lab2[2])*($lab1[2]-$lab2[2]));
    }
  }

  if(!function_exists('str2rgb'))
  {
    function str2rgb($str)
    {
      $str = preg_replace('~[^0-9a-f]~','',$str);
      $rgb = str_split($str,2);
      for($i=0;$i<3;$i++)
        $rgb[$i] = intval($rgb[$i],16);

      return $rgb;
    } // function str2rgb
  }

  // split into RGB, if not already done
  $givenColorRGB = is_array($givenColor)?$givenColor:str2rgb($givenColor);
  $min = 0xffff;
  $return = NULL;

  foreach($palette as $key => $color)
  {
    // split into RGB
    $color = is_array($color)?$color:str2rgb($color);
    // deltaE
    #if($min >= ($deltaE = deltaE(rgb2lab($color),rgb2lab($givenColorRGB))))
    // euclidean distance
    if($min >= ($deltaE = colorDistance(rgb2lab($color),rgb2lab($givenColorRGB))))
    {
      $min = $deltaE;
      $return = $key;
    }
  }

  return $return;
}
xong
  • 350
  • 4
  • 8
  • for me this is the only proper answer! – Andreas Linden Jun 22 '16 at 12:02
  • Be aware that meanwhile there are [replacements for the Delta E algorithm](https://en.wikipedia.org/wiki/Color_difference) I used in the above code. Nevertheless, if I were to implement a new color distance function today, I would use the DIN99c or DIN99d color spaces since they are easier and faster to calculate than CIE94 or CIEDE2000 while serving a similar quality. – xong Mar 12 '17 at 10:14
  • 1
    Some explanation is in order. What does this do? Why is it better? – Raphael Apr 01 '17 at 07:26
  • This function uses a color space in which the distances between the colors are similar to the human color perception. This allows for a certain color to get the best matching color from a given palette. But as I wrote: Today I would use an implementation of DIN99 to achieve this. The above function was written over 10 years ago. – xong Apr 08 '17 at 09:27
1

Calculate the distance from the input color to all possible candidates of your palette, and then pick the one with the smallest distance as the one to replace it with.

Distance can be defined in any way you like; Euclidean distance seems workable for RGB cubes, cylinders or HSL/HSV cones.

user502515
  • 4,346
  • 24
  • 20
0

There is no point in taking the square root. Finding the shortest distance is the same as finding the shortest squared distance. sqrt is an expensive operation, so just skip it.

Whether it really matters, of course, depends of how often your program will make this calculation, but it's still pointless to have it.

GraphicsMuncher
  • 4,583
  • 4
  • 35
  • 50
Pete
  • 1