[PHP]似た画像を検索して近い順番に並べる(類似画像検索)

過去に類似画像を検索する方法はいくつか提示しましたが、おそらく今回の手法が一番多く使われているものです。
このプログラムでは、用意された画像をもとに、フォルダ内にある複数の画像の中から最も似た画像を探し、似ているものから順番に並べます。

原理はいたってシンプルで、比較元の画像と比較先の画像を小さな画像に変換し、1ピクセルずつ RGB 値を取得します。
あとは RGB を Lab 色空間上の座標に変換し、同じ座標同士の距離を比較し、近いものから順番にソートします。

RGB を Lab に変換するには一旦 xyz 表色系に変換し、それを Lab に変換する必要があります。
詳細については以前の記事を御覧下さい。

<?php
//比較元となる画像
$filepath = "sample.jpg";
 
//比較対象用画像ディレクトリ
$dir = "images/";

$sample = load_image($filepath);
$sample_lab = image_lab($sample);
imagedestroy($sample);

$list = scandir($dir);

$files = array();
foreach($list as $value){
	if(is_file($dir . $value)){
		$files[] = $dir . $value;
	}
}
$diff = array();
foreach($files as $file){
	$image	 = load_image($file);
	$lab	 = image_lab($image);
	imagedestroy($image);
	
	$name	 = basename($file);
	
	$distance = 0;
	foreach($sample_lab as $key => $value){
		$distance += lab_distance($value, $lab[$key]);
	}
	$diff[$name] = $distance;
}
 
asort($diff);
$result = array_keys($diff);
header("Content-type: text/html;charset=utf-8");
foreach($result as $img){
	echo '<img src="' . $dir . $img . '" width="50" height="50" alt="" />';
}

// 画像の読み込み
function load_image($filepath){
	$checkimg = getimagesize($filepath);
	if($checkimg['mime'] == "image/jpeg" || $checkimg['mime'] == "image/pjpeg"){
		$extension = "jpg";
	} else if ($checkimg['mime'] == "image/gif"){
		$extension = "gif";
	} else if ($checkimg['mime'] == "image/png" || $checkimg['mime'] == "image/x-png"){
		$extension = "png";
	} else {
		exit;
	}
 
	if($extension == 'jpg'){$image = ImageCreateFromJPEG($filepath);}
	if($extension == 'gif'){$image = ImageCreateFromGIF($filepath);}
	if($extension == 'png'){$image = ImageCreateFromPNG($filepath);}

	return $image;
}

// 画像をリサイズしピクセルごとのLab色空間上の座標を取得する
function image_lab($image){
	$width	 = imagesx($image);
	$height	 = imagesy($image);

	$thumb_width	 = 4;
	$thumb_height	 = 4;
	$thumb = imagecreatetruecolor($thumb_width, $thumb_height);
	imagecopyresampled($thumb, $image, 0, 0, 0, 0, $thumb_width, $thumb_height, $width, $height);
	 
	$lab = array();
	$red	 = 0;
	$green	 = 0;
	$blue	 = 0;
	 
	for($x=0; $x < $thumb_width; $x++){
		for($y=0; $y < $thumb_height; $y++){
			$index	 = imagecolorat($thumb, $x, $y);
			$rgb	 = imagecolorsforindex($thumb, $index);
			$lab[]	 = rgb2lab( array($rgb['red'], $rgb['green'], $rgb['blue']) );
		}
	}
 
	return $lab;
}

// xyz色空間上の座標をlab色空間上の座標に変換する
function xyz2lab($xyz) {
	$threshold = 0.008856;
	 
	//Chromatic Adaptation Matrices
	// D50
	$ref_x = 0.96422;
	$ref_y = 1.0000;
	$ref_z = 0.82521;

	$var_x = $xyz[0] / ($ref_x * 100);
	$var_y = $xyz[1] / ($ref_y * 100);		
	$var_z = $xyz[2] / ($ref_z * 100);
	 
	$var_x = ($var_x > $threshold) ? $var_x = pow($var_x, 1/3 ) : (7.787 * $var_x) + (16 / 116);
	$var_y = ($var_y > $threshold) ? $var_y = pow($var_y, 1/3 ) : (7.787 * $var_y) + (16 / 116);
	$var_z = ($var_z > $threshold) ? $var_z = pow($var_z, 1/3 ) : (7.787 * $var_z) + (16 / 116);
	 

	$l = ( 116 * $var_y ) - 16;
	$a = 500 * ( $var_x - $var_y );
	$b = 200 * ( $var_y - $var_z );
	$lab = array();
	 
	$lab = array($l, $a, $b);
	 
	return $lab;
}

// rgb値をxyz色空間上の座標に変換する
function rgb2xyz($rgb) {
	$r = $rgb[0] / 255;
	$g = $rgb[1] / 255;
	$b = $rgb[2] / 255;

	$r = ($r > 0.04045) ? pow(($r + 0.055) / 1.055, 2.4) : $r / 12.92;
	$g = ($g > 0.04045) ? pow(($g + 0.055) / 1.055, 2.4) : $g / 12.92;
	$b = ($b > 0.04045) ? pow(($b + 0.055) / 1.055, 2.4) : $b / 12.92;

	$r = $r * 100;
	$g = $g * 100;
	$b = $b * 100;
	 
	$xyz = array();
	 
	//sRGB D50
	$xyz[] = $r * 0.4360747 + $g * 0.3850649 + $b * 0.1430804;
	$xyz[] = $r * 0.2225045 + $g * 0.7168786 + $b * 0.0606169;
	$xyz[] = $r * 0.0139322 + $g * 0.0971045 + $b * 0.7141733;
	return $xyz;
}

// rgb値をlab色空間上の座標に変換する
function rgb2lab($rgb) {
	$xyz = rgb2xyz($rgb);
	$lab = xyz2lab($xyz);
	return $lab;
}

// 2つの座標を比較し距離を返す
function lab_distance($p1, $p2){
	$dist = sqrt( pow($p2[0] - $p1[0], 2) + pow($p2[1] - $p1[1], 2) + pow($p2[2] - $p1[2], 2) );
	return $dist;
}

このサンプルでは画像のサイズ( $thumb_width, $thumb_height )を 4×4 に統一したものを比較しています。
より厳密な一致を求めるならこの数字を大きくします。ただし計算量が多くなるので気をつけて下さい。

[PHP]HSV(HSB)色空間を比較して似た色合いの画像を検索する

Lab 色空間を利用した精度の高い類似画像の検索についてはこちらの記事を御覧ください。


以前の記事でRGB値を元に画像を検索しましたが、今回はHSVモデルを使ってより人間の知覚に近い色比較を行なってみます。

HSV色空間は、色を「色相」「彩度」「明度」の要素に分けて考えます。
つまり、色の違い、鮮やかさ、明るさの違いを数値的に比較することができます。

<?php
//比較元となる画像
$filepath = "sample.jpg";
 
//比較対象用画像ディレクトリ
$dir = "images/";
 
//要素の重要度
$priority = array(
  'h' => 1,
  's' => 1,
  'v' => 1
);
 
$sample = loadImage($filepath);
$sample_hsv = imageHsv($sample);
imagedestroy($sample);
$list = scandir($dir);
 
$files = array();
foreach($list as $value){
  if(is_file($dir . $value)){
    $files[] = $dir . $value;
  }
}
 
$diff = array();
foreach($files as $file){
  $image = loadImage($file);
  $hsv = imageHsv($image);
  imagedestroy($image);
  $name = basename($file);
  $diff[$name] = hsvDistance($sample_hsv, $hsv, $priority);
}
 
asort($diff);
$result = array_keys($diff);
header("Content-type: text/html;charset=utf-8");
foreach($result as $img){
	echo '<img src="' . $dir . $img . '" width="50" height="50" alt="" />';
}
 
function loadImage($filepath){
  $checkimg = getimagesize($filepath);
 
  if($checkimg['mime'] == "image/jpeg" || $checkimg['mime'] == "image/pjpeg"){
    $extension = "jpg";
  } else if ($checkimg['mime'] == "image/gif"){
    $extension = "gif";
  } else if ($checkimg['mime'] == "image/png" || $checkimg['mime'] == "image/x-png"){
    $extension = "png";
  } else {
    exit;
  }
 
  if($extension == 'jpg'){$image = ImageCreateFromJPEG($filepath);}
  if($extension == 'gif'){$image = ImageCreateFromGIF($filepath);}
  if($extension == 'png'){$image = ImageCreateFromPNG($filepath);}
   
  return $image;
}
 
function imageHsv($image){
  $width	 = imagesx($image);
  $height	 = imagesy($image);

  $thumb_width	 = 16;
  $thumb_height	 = 16;
  $thumb = imagecreatetruecolor($thumb_width, $thumb_height);
  imagecopyresampled($thumb, $image, 0, 0, 0, 0, $thumb_width, $thumb_height, $width, $height);
   
  $red   = 0;
  $green   = 0;
  $blue   = 0;
   
  for($x=0; $x < $thumb_width; $x++){
    for($y=0; $y < $thumb_height; $y++){
      $index   = imagecolorat($thumb, $x, $y);
      $rgb   = imagecolorsforindex($thumb, $index);
      $red   += $rgb['red'];
      $green   += $rgb['green'];
      $blue   += $rgb['blue'];
    }
  }
   
  $average = array();
  $pixel = $thumb_width * $thumb_height;
  $average['red']     = round($red / $pixel);
  $average['green']   = round($green / $pixel);
  $average['blue']   = round($blue / $pixel);
 
  return rgb2hsv($average);
}
 
function hsvDistance($hsv1, $hsv2, $priority){
  $dist_h = abs($hsv1['h']   - $hsv2['h']);
  $distance = 0;
  $distance += min($dist_h, 360 - $dist_h) * $priority['h'];
  $distance += abs($hsv1['s']   - $hsv2['s']) * 180 * $priority['s'];
  $distance += abs($hsv1['v']   - $hsv2['v']) * 180 * $priority['v'];
  return $distance;
}
 
function rgb2hsv($rgb){
  $r = $rgb['red'] / 255;
  $g = $rgb['green'] / 255;
  $b = $rgb['blue'] / 255;
   
  $max = max($r, $g, $b);
  $min = min($r, $g, $b);
  $v = $max;
   
  if($max === $min){
    $h = 0;
  } else if($r === $max){
    $h = 60 * ( ($g - $b) / ($max - $min) ) + 0;
  } else if($g === $max){
    $h = 60 * ( ($b - $r) / ($max - $min) ) + 120;
  } else {
    $h = 60 * ( ($r - $g) / ($max - $min) ) + 240;
  }
  if($h < 0) $h = $h + 360;
 
  $s = ($v != 0) ? ($max - $min) / $max : 0;
   
  $hsv = array("h" => $h, "s" => $s, "v" => $v);
  return $hsv;
}

プログラムの構造は前回とほぼ同じです。
比較元の画像ファイルと、比較先の画像が複数入ったフォルダを指定し、最も近い色を持つ画像を表示するものです。

今回は $priority というパラメータをもたせました。
これは HSV のどの要素を重視するかというもので、数字が大きいほどその要素が重要視されます。色の違いを2倍重要視したいのであれば h を 2 にします。
画像の明るさだけを比較したい場合は v 以外の数値を 0 にします。
感覚的には色相(h)を重要視したほうがより近い画像が得られる気がします。

必要に応じて thumb_width や thumb_height も変更します。
これは、高速化のために、色を拾う際に画像を小さくしてから拾っています。
小さければ小さいほど精度は下がりますが処理は高速になります。

比較の結果は連想配列 $diff に入っています。キーがファイル名で値が相似の度合いです。数値が小さいほど似た画像であることを示し、完全に同じ時は 0 になります。
上のサンプルでは reset() で最も似た画像だけを表示しています。

注意すべき点は、本来彩度が 0 の時、色相は無視されるべきですが、便宜的に 0 としているのでモノクロの画像比較を行うには工夫が必要です。
モノクロの画像を HSV に変換する際、彩度を「FALSE」にしておき、比較を行わないようにすると良いと思います。

[PHP]似た色合いの画像をRGB値をもとに探す

<?php
//比較元となる画像
$filepath = "sample.jpg";
 
//比較対象用画像ディレクトリ
$dir = "images/";
 
$sample = loadImage($filepath);
$sample_rgb = colorAverage($sample);
imagedestroy($sample);
 
$list = scandir($dir);
 
$files = array();
foreach($list as $value){
  if(is_file($dir . $value)){
    $files[] = $dir . $value;
  }
}
 
$diff = array();
foreach($files as $file){
  $image = loadImage($file);
  $rgb = colorAverage($image);
  $name = basename($file);
  $diff[$name] = colorDistance($sample_rgb, $rgb);
}
 
asort($diff);
$result = array_keys($diff);
echo reset($result);
 
function loadImage($filepath){
  $checkimg = getimagesize($filepath);
 
  if($checkimg['mime'] == "image/jpeg" || $checkimg['mime'] == "image/pjpeg"){
    $extension = "jpg";
  } else if ($checkimg['mime'] == "image/gif"){
    $extension = "gif";
  } else if ($checkimg['mime'] == "image/png" || $checkimg['mime'] == "image/x-png"){
    $extension = "png";
  } else {
    return false;
  }
 
  if($extension == 'jpg'){$image = ImageCreateFromJPEG($filepath);}
  if($extension == 'gif'){$image = ImageCreateFromGIF($filepath);}
  if($extension == 'png'){$image = ImageCreateFromPNG($filepath);}
 
   
  return $image;
}
 
function colorAverage($image){
  $width = imagesx($image);
  $height = imagesy($image);
 
  $thumb_width   = 16;
  $thumb_height   = 16;
  $thumb = imagecreatetruecolor($thumb_width, $thumb_height);
  imagecopyresampled($thumb, $image, 0, 0, 0, 0, $thumb_width, $thumb_height, $width, $height);
   
  $red   = 0;
  $green   = 0;
  $blue   = 0;
   
  for($x=0; $x < $thumb_width; $x++){
    for($y=0; $y < $thumb_height; $y++){
      $index   = imagecolorat($thumb, $x, $y);
      $rgb   = imagecolorsforindex($thumb, $index);
      $red   += $rgb['red'];
      $green   += $rgb['green'];
      $blue   += $rgb['blue'];
    }
  }
   
  $average = array();
  $pixel = $thumb_width * $thumb_height;
  $average['red']     = round($red / $pixel);
  $average['green']   = round($green / $pixel);
  $average['blue']   = round($blue / $pixel);
   
  return $average;
}
 
function colorDistance($rgb1, $rgb2){
  $distance = 0;
  $distance += abs($rgb1['red']   - $rgb2['red']);
  $distance += abs($rgb1['green']   - $rgb2['green']);
  $distance += abs($rgb1['blue']   - $rgb2['blue']);
  return $distance;
}

ある画像のRGB値から平均色を算出し、似た平均色を持つ画像をディレクトリ内から探すというものです。

計算速度を早めるために、一度小さなサムネイルを作ってから色を拾っています。精度を高めるならこのサムネイルサイズを大きくすると良いと思います。

ただ、結論から言うとイマイチです。

計算には間違いないのですが、人間の目は明るく鮮やかな部分に目が行ってしまい、暗く地味な色を無意識に省略してしまいます。
また、明るさの違いよりも色の違いを重視する傾向があります。

このプログラムの計算方法だと、色の違いと明るさの違いを同じ価値として計算するため、同じ色合いで明暗が異なる画像より、多少色が違っても似た明るさを持つ画像が重視されてしまいます。

そのあたりを考慮するのであればHSV色空間(色相・彩度・明度)をもとに比較すべきなのかもしれません。

追記: HSV色空間を利用した画像検索も作りました。
また、Lab 色空間を利用した精度の高い類似画像検索についてはこちらの記事を御覧ください。