RGB 转最接近的预定义颜色

14

编辑:

根据给出的答案,我创建了这个函数。

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];
        }
    }
我的目标是这样的:我获取一张图片,然后循环遍历每个像素,在抓取其x、y和rgb值的同时,不仅仅只获取rgb值。我有一个预定义数组,并且我正在寻找与我所抓取颜色最接近的颜色,以达到只使用预定义数组中的颜色的目的。以下是我的颜色数组。
$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));

这是我现有的循环代码。

$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是一个用户自定义函数,但我想要实现的是将该函数更改为获取最接近颜色的函数。

$colorsofimage 包含每个像素信息的数组,具有 hex:x:y 的格式,我想要的是 rgb2hex(NEWFUNCTION($r,$g,$b))。以便新的十六进制颜色与预定义数组中的一项相匹配。

希望你理解我的意思,因为我不知道颜色算法该怎么做。


2
根据你想要达到的程度,我的(类似的)问题的答案可能会有用 - https://dev59.com/pG855IYBdhLWcg3w-JQr - El Yobo
我不是很了解PHP,但你提供的函数看起来比我之前建议的那个要低效得多:https://dev59.com/pW855IYBdhLWcg3wMBPV#4485327 - beldaz
1
为什么在foreach循环中有一个返回? - Joeri
5个回答

18

你需要计算到每种颜色的距离,并选择最小值。

有几种方法可以实现。一个简单的方法是计算距离:

sqrt((r-r1)^2+(g-g1)^2+(b-b1)^2)
更好的方法可能是将加权值纳入计算距离的过程中,例如在转换RGB->YUV时使用的值:
Y = 0.299 * R + 0.587 * G + 0.114 * B

在那种情况下,您将使用

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

当然,如果你不需要精确的距离,只需要比较大小,那么你可以并且可能应该跳过平方根运算,从而得出以下计算结果:

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

5
随想:严格来说,如果性能是一个问题,你不需要取平方根,只需找到最小的平方数即可。 - Kevin Stricker
1
@mootinator 为什么我们需要平方值呢?为什么不只用最短距离呢? - Albert Renshaw
@mootinator 我们是要平方以消除负值吗?有人知道绝对值函数是否比平方更快吗?或者只需将其转换为字符串并去除负数,然后再转换回数字,或者如果它是负数,只需将其乘以-1? - Albert Renshaw
2
@Albert Renshaw 我们正在平方以找到两点之间的欧几里得距离。我们跳过取距离平方根的步骤,因为这一步不会影响距离的排序顺序。 - Kevin Stricker
@mootinator 好的,那这跟直接取两数之差的绝对值一样吗? - Albert Renshaw
显示剩余4条评论

11

RGB颜色空间实际上是一个立方体。在24位色中,每个边的长度为256,允许值从0到255。为了找到在这个立方体内最接近的颜色,您需要一个距离函数。最简单和最直观的方法是使用欧几里得距离:如果您有颜色(r1,g1,b1)和另一种颜色(r2,g2,b2),则距离将是sqrt((r2-r1)^2 + (g2-g1)^2 + (b2-b1)^2)

然后,您需要在预定义数组的所有值中找到最佳匹配。我建议您只需迭代所有值并逐个检查距离即可。请注意,在此目的下,您不需要执行sqrt,仅比较平方和即可,并且具有基于整数的所有好处。我的PHP不是很好,但大致上您应该这样做:

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];
    }
}

还有更复杂的距离函数(例如,更好地考虑颜色差异的心理视觉解释(请参阅Delta E)),但我认为这不是你所需要的。


@beldaz 当您将值与固定值数组进行比较时,如果所有距离都是曼哈顿距离,那么对于“最接近的颜色”,您仍然会得到相同的结果,而不必花费时间平方这些数字。 - Albert Renshaw
这个公式是否应该选择不同的颜色而不是不同的阴影?我认为我会先尝试使用HSL转换(虽然不确定)。 - Joeri
@Joeri 颜色和阴影之间的区别相当主观。在RGB中没有区别。HSL也没有颜色阴影,但它有色调和饱和度。问题在于两个HSL坐标之间没有基本距离函数。对于一个具有良好定义距离函数的适当的心理视觉颜色空间,您应该使用CIELab。 - beldaz
@beldaz 谢谢。我只是想知道255,255,255是更接近254,254,254还是253,255,255。 - Joeri
保持简单,第一个距离将是sqrt(3),第二个将是sqrt(4),因此最后一种颜色会更远。它是否看起来更不同的颜色是另一回事,这就是心理视觉颜色空间及其距离函数的用途。 - beldaz
显示剩余15条评论

6

由于这个问题在谷歌搜索结果的前十名中显示,所以这里是我几年前编写的一个更复杂的函数,它产生的结果比现有的PHP函数更好。

/*
 * 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;
}

对我来说,这是唯一正确的答案! - Andreas Linden
请注意,尽管上述代码中使用的Delta E算法已经有替代品了。然而,如果我今天要实现一个新的颜色距离函数,我会使用DIN99c或DIN99d颜色空间,因为它们比CIE94或CIEDE2000更容易和更快地计算,同时具有类似的质量。 - xong
1
需要进行一些解释。这个做什么?为什么它更好? - Raphael
这个函数使用一种颜色空间,其中颜色之间的距离类似于人类的颜色感知。这允许从给定的调色板中获取最佳匹配颜色的某个特定颜色。但正如我所写的:今天我会使用DIN99的实现来实现这一点。上述函数是在10年前编写的。 - xong

1

计算输入颜色与您的调色板所有可能候选项之间的距离,然后选择最小距离的候选项作为替换它的颜色。

距离可以以任何您喜欢的方式定义; 欧几里得距离似乎适用于RGB立方体、圆柱体或HSL / HSV圆锥体。


0

开平方根没有意义。找到最短距离与找到最短平方距离是相同的。 sqrt 是一项昂贵的操作,所以可以直接跳过它。

当然,是否真的重要取决于您的程序有多频繁地进行此计算,但仍然没有意义。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接