Привет! Столкнулся с задачей сравнить два изображения, если уж совсем буквальнее – выбрать n-похожих на исходное и отсортировать по похожести. Не уверен, что PHP для этого подходит (точнее уверен, что это не лучший вариант), но для небольшого масштаба приложения решил изучить его возможности.
Представлю два опробованных мною способа.
Способ первый, алгоритм и GD
Довольно интересный способ, простой, но достаточно спорный, особенно если взглянуть на практические результаты.
Способ заключается в том, что каждое изображение приводится к формату 8x8px, далее вычисляется разница между каждым пикселем и последующим, численные значения записываются в массив, который и характеризует одно изображение. Далее два массива сравниваются между собой поэлементно (попиксельно) и высчитывается показатель разности изображений, чем меньше это число, тем меньше разницы между изображениями.
Привожу сразу класс с полной реализацией этого метода.
class ImagesComparer
{
const
BAD_ARGS = 1,
UNSUPPORTED_FILETYPE = 2,
ERROR_OPEN = 3;
public $Images = array();
private
$_types = array('', 'gif', 'jpeg', 'png', '', '', 'wbmp', '', '', '', '', '');
public $CompareWithFirst = TRUE;
public function __construct($Image1, $Image2 = null)
{
if (func_num_args() > 2)
$Images = func_get_args();
else if (is_array($Image1))
$Images = $Image1;
else
$Images = array($Image1, $Image2);
foreach ($Images as $Image)
{
if (is_string($Image))
$this->_openImage($Image);
else if (is_resource($Image))
$this->Images[] = array($this->_getPixelsDiff($Image), array());
else
throw new Exception('Bad arguments.', self::BAD_ARGS);
}
}
private function _getImageType($Image)
{
$Type = getimagesize($Image);
if (!$Type = $this->_types[$Type[2]])
throw new Exception('Image have an unsupported file type.', self::UNSUPPORTED_FILETYPE);
return 'imagecreatefrom' . $Type;
}
private function _openImage($Image)
{
$Type = $this->_getImageType($Image);
$Image = $Type($Image);
if (!$Image)
throw new Exception('Error opening image.', self::ERROR_OPEN);
$this->Images[] = array($this->_getPixelsDiff($Image), array());
imagedestroy($Image);
}
private function _getPixelsDiff($Image)
{
$Sample = imagecreatetruecolor(8, 8);
imagecopyresampled($Sample, $Image, 0, 0, 0, 0, 8, 8, imagesx($Image), imagesy($Image));
$Pixels = array();
$Color = array(0, 0, 0);
for ($y = 0; $y < 8; $y++)
{
for ($x = 0; $x < 8; $x++)
{
$Color1 = imagecolorat($Sample, $x, $y);
$Color1 = $this->_scale255To9(array(
($Color1 >> 16) & 0xFF,
($Color1 >> 8) & 0xFF,
$Color & 0xFF
));
if ($x != 0 || $y != 0)
{
$Pixels[] = array(
$Color1[0] - $Color[0],
$Color1[1] - $Color[1],
$Color1[2] - $Color[2]
);
}
$Color = $Color1;
}
}
imagedestroy($Sample);
return $Pixels;
}
private function _scale255To9($NumArr)
{
return array(
round($NumArr[0] / 28.3),
round($NumArr[1] / 28.3),
round($NumArr[2] / 28.3)
);
}
private function _getDiff($Img1, $Img2)
{
$Diff = 0;
for ($i = 0; $i < 63; $i++)
{
$Diff += abs($this->Images[$Img1][0][$i][0] - $this->Images[$Img2][0][$i][0]);
$Diff += abs($this->Images[$Img1][0][$i][1] - $this->Images[$Img2][0][$i][1]);
$Diff += abs($this->Images[$Img1][0][$i][2] - $this->Images[$Img2][0][$i][2]);
}
return $Diff;
}
public function Compare()
{
$count = count($this->Images);
if ($this->CompareWithFirst)
{
for ($i = 1; $i < $count; $i++)
{
$this->Images[0][1][$i] = intval($this->_getDiff(0, $i));
}
}
else
{
for ($i = 0; $i < $count; $i++)
{
for ($k = $i + 1; $k < $count; $k++)
{
$this->Images[$k][1][$i] =
$this->Images[$i][1][$k] = intval($this->_getDiff($i, $k));
}
}
}
}
}
Представленный класс может принимать массив изображений, по умолчанию сравнивает каждое изображение с каждым другим. Опционально можну установить свойство CompareWithFirst = TRUE, тогда каждое изображение будет сравниваться с первым. Далее вызывается метод Compare(), который заполняет свойство $this->Images данными с результатами сравнения.
Посмотрим на результаты:
Вверху изображения в исходном порядке, далее распечатка массива показателей соответствия с первым изображением, и, соответственно, изображения, расположенные в порядке убывания похожести, как его видит вышеприведенный алгоритм.
Очень спорно, хотя если постараться логику в таком сравнении найти можно, но такая похожая на мой взгляд серая фигурка слона отброшена на 4е место:))) Остался неудовлетворенным я этим способом и продолжил изучать вопрос.
автор алгоритма и оригинальный пост на
хабре
Способ второй, ImageMagick
Данный способ еще проще первого, ввиду использования класса Imagick для PHP, реализующего работу с
ImageMagick. У него есть простая обертка для сравнения двух изображений, вкратце одна строка
$Img1->compareImages($Img2, Imagick::METRIC_MEANSQUAREERROR);
Приведу сразу результаты сравнения этим способом:
Обратите внимание на показатель похожести – последние два изображения резко отброшены от первого, с которым все сравниваются. 3 других вполне похожи между собой визуально, что также коррелируется с цифрами. Работа этого метода мне показалась довольно схожей с визуальной оценкой изображений и я остановился на данном способе. Приведу класс целиком.
class ImagesComparer
{
const
WIDTH = 600,
HEIGHT = 400;
public $Images = array();
public function __construct($Image1, $Image2 = null)
{
if (is_array($Image1))
{
$this->Images = $Image1;
$this->mainImage = $Image1[0];
}
else
{
$this->Images = array($Image1);
$this->mainImage = $Image1;
}
foreach ($this->Images as $key => $Image)
{
if (is_string($Image))
$this->Images[$key] = array($Image, $this->_openImage($Image));
else
throw new Exception('Bad arguments.', self::BAD_ARGS);
}
}
private function _openImage($Image)
{
$obj = new Imagick($Image);
$d = $obj->getImageGeometry();
$w = $d['width'];
$h = $d['height'];
if ($w != self::WIDTH || $h != self::HEIGHT)
$obj->cropImage(self::WIDTH, self::HEIGHT, 0, 0);
return $obj;
}
private function _getDiff($Img1, $Img2)
{
$res = $Img1->compareImages($Img2, Imagick::METRIC_MEANSQUAREERROR);
$diff = round($res[1]*1000);
return $diff;
}
public function Compare()
{
$count = count($this->Images);
if ($count > 1)
{
foreach ($this->Images as $key => $value)
{
if ($key > 0)
{
$this->Images[$key][] = $this->_getDiff($this->Images[0][1], $this->Images[$key][1]);
}
}
}
}
}
Обратите внимание на то, что метод compareImages() применим для изображений одинакового размера. Я определил их в константах класса, вы же можете их вынести в публичные свойства и варьировать в зависимости от предмета сравнения.