На выходных удалось выкроить пару часов и допилить идею из комментариев.
Под катом код класса с обработчиком событий под элементы инфоблока.
[spoiler]
В идеале хотелось разбить его на несколько классов и использовать мощь композиции как подхода к проектированию.
Но поскольку это только для init.php, то пока представлено в виде одного класса.
Собственно код с поясняющими комментариями:
UPD: процедура расчёта вынесена в отдельный метод guessColor
//Класс ColorGuess использует справочник цветов из хайлоадблока.
//для этого в этом блоке надо добавить числовые поля UF_COLOR_R, UF_COLOR_G и UF_COLOR_B.
//значения этих полей будут заполнены при первом запуске автоматически.
//или вы можете задать их руками.
$colorG = new ColorGuess("eshop_color_reference");
$colorG->handleIblock(1, 92);
/**
* Используется для определения цвета товара представленного на картинке.
* Класс реализует паттерн стратегия. Для модификации аспектов его поведения
* достаточно перекрыть соответствущие методы.
*
* @see http://en.wikipedia.org/wiki/Color_difference
**/
class ColorGuess
{
protected $iblocks = array();
protected $imageFilePath = '';
protected $colorReferenceId = '';
protected $colorReference = array();
/** @var resource */
protected $image = null;
/** @var resource */
protected $maskImage = false;
/** @var integer */
protected $maskColor = false;
/** @var integer */
public $threshold = 48;
public function __construct($colorReferenceId)
{
$this->colorReferenceId = $colorReferenceId;
}
/**
* Функция добавляет инфоблок в "обработку".
*
* @param integer $iblockId Идентификатор инфоблока.
* @param integer $propertyId Идентификатор свойства для сохранения значения найденного цвета.
*
* @return void
**/
public function handleIblock($iblockId, $propertyId)
{
if (empty($this->iblocks))
{
AddEventHandler("iblock", "OnBeforeIBlockElementAdd", array($this, "eventHandler"));
AddEventHandler("iblock", "OnBeforeIBlockElementUpdate", array($this, "eventHandler"));
}
$this->iblocks[$iblockId] = $propertyId;
}
/**
* Вызывается в самом начале обработки события.
* Должна удостовериться, что вызов соответствует инфоблокам обрабатываемым
* этим экземпляром.
*
* @param mixed $arFields поля передаваемые в обработчик.
*
* @return boolean
* @see http://dev.1c-bitrix.ru/api_help/iblock/events/onbeforeiblockelementadd.php
* @see http://dev.1c-bitrix.ru/api_help/iblock/events/onbeforeiblockelementupdate.php
**/
public function checkFields($arFields)
{
if (!isset($this->iblocks[$arFields["IBLOCK_ID"]]))
return false;
else
return true;
}
/**
* Функция инициализирует справочник референсных цветов.
* В этом примере используется справочник цветов из демонстрациооной версии магазина.
* Можно использовать: https://ru.wikipedia.org/wiki/%D0%A1%D0%BF%D0%B8%D1%81%D0%BE%D0%BA_%D1%86%D0%B2%D0%B5%D1%82%D0%BE%D0%B2
* Главное чтобы член класса colorReference был заполнен.
* Ключи этого массива - уникальные идентификаторы которые будут использованы для заполненения значений свойства.
* Значения - массивы из трех элементов задающие цвет array("R" => ...,"G" => ..., "B" => ...);
* В случае успеха возвращает true.
*
* @return boolean
* @see http://dev.1c-bitrix.ru/api_help/iblock/fields.php#felement
**/
public function initColorReference()
{
if (!CModule::IncludeModule('highloadblock'))
return false;
$hlblock = \Bitrix\Highloadblock\HighloadBlockTable::getList(array(
"filter" => array(
"=TABLE_NAME" => $this->colorReferenceId,
)))->fetch();
$entity = Bitrix\Highloadblock\HighloadBlockTable::compileEntity($hlblock);
$entity_data_class = $entity->getDataClass();
$rsData = $entity_data_class::getList(array(
"select" => array("ID", "UF_NAME", "UF_XML_ID", "UF_FILE", "UF_COLOR_R", "UF_COLOR_G", "UF_COLOR_B"),
));
$this->colorReference = array();
while($arData = $rsData->fetch())
{
if (
$arData["UF_FILE"]
&& (
!isset($arData["UF_COLOR_R"])
|| !isset($arData["UF_COLOR_G"])
|| !isset($arData["UF_COLOR_B"])
)
)
{
$arFile = CFile::MakeFileArray($arData["UF_FILE"]);
if (
$arFile
&& CFile::ResizeImage($arFile, array("width"=>40,"height"=>40))
&& ($image = CFile::CreateImage($arFile["tmp_name"]))
)
{
$color = $this->getPictureColor($image);
$arData["UF_COLOR_R"] = $color["R"];
$arData["UF_COLOR_G"] = $color["G"];
$arData["UF_COLOR_B"] = $color["B"];
$entity_data_class::update($arData["ID"], array(
"UF_COLOR_R" => $color["R"],
"UF_COLOR_G" => $color["G"],
"UF_COLOR_B" => $color["B"],
));
}
}
if (
$arData["UF_XML_ID"] == 'orangered'
|| $arData["UF_XML_ID"] == 'redblue'
|| $arData["UF_XML_ID"] == 'jeans'
|| $arData["UF_XML_ID"] == 'flowers'
)
continue;
$this->colorReference[$arData["UF_XML_ID"]] = array(
"R" => $arData["UF_COLOR_R"],
"G" => $arData["UF_COLOR_G"],
"B" => $arData["UF_COLOR_B"],
);
}
return true;
}
/**
* Функция инициализирует переменные необходимые для загрузки изображения
* из нужных полей инфоблока.
* В случае не успеха (поле не заполнено) возвращает false.
*
* @param mixed $arFields поля передаваемые в обработчик.
*
* @return boolean
* @see http://dev.1c-bitrix.ru/api_help/iblock/fields.php#felement
**/
public function initFile($arFields)
{
if (!$arFields["PREVIEW_PICTURE"]["tmp_name"])
return false;
$arFile = CFile::MakeFileArray($arFields["PREVIEW_PICTURE"]["tmp_name"]);
if (!$arFile)
return false;
if (!CFile::CheckImageFile($arFile["tmp_name"]))
return false;
$this->imageFilePath = $arFile["tmp_name"];
return true;
}
/**
* Функция загружает изображение из файла и преобразует его в true color, если это необходимо.
*
* @return boolean
**/
public function loadFile()
{
$this->image = CFile::CreateImage($this->imageFilePath);
if (!$this->image)
return false;
if (!imageistruecolor($this->image))
{
$sx = imagesx($this->image);
$sy = imagesy($this->image);
$tmp = imagecreatetruecolor($sx, $sy);
if ($tmp)
{
imagecopy($tmp, $this->image, 0, 0, 0, 0, $sx, $sy);
imagedestroy($this->image);
}
$this->image = $tmp;
}
return is_resource($this->image);
}
/**
* Функция задаёт маску которая исключит из анализа ненужный обрамляющий фон.
*
* @return boolean
* @see http://developers.lyst.com/data/images/2014/02/13/background-removal/
**/
public function createMask()
{
if (!$this->image)
return false;
$sx = imagesx($this->image);
$sy = imagesy($this->image);
$this->maskImage = imagecreatetruecolor($sx, $sy);
$this->maskColor = imagecolorallocate($this->maskImage, 255, 0, 255);
imagealphablending($this->maskImage, false);
imagecopy($this->maskImage, $this->image, 0, 0, 0, 0, $sx, $sy);
//Негатив
$this->imageNegate($this->maskImage);
//Выделение края
$this->imageSobel($this->maskImage);
//Размытие
$this->imageBlur($this->maskImage);
//Фильтрация по порогу
$this->imageThresholdToWhite($this->maskImage, $this->threshold);
//Заливка с углов магентой.
$this->imageFillCorners($this->maskImage, $this->maskColor);
return is_resource($this->maskImage);
}
/**
* Функция определяет цвет товара на картинке.
*
* @return mixed
*/
public function guessColor()
{
$distances = array();
$color = $this->getPictureColor($this->image, $this->maskImage, $this->maskColor);
foreach ($this->colorReference as $key => $arData)
{
$c1 = $this->rgb2xyz($arData);
$c1 = $this->xyz2lab($c1);
$c2 = $this->rgb2xyz($color);
$c2 = $this->xyz2lab($c2);
$dL = $c1["L"] - $c2["L"];
$dA = $c1["a"] - $c2["a"];
$dB = $c1["b"] - $c2["b"];
$distances[$key] = $dL*$dL + $dA*$dA + $dB*$dB;
}
asort($distances);
reset($distances);
return key($distances);
}
/**
* Функция применяет найденный ответ к полям обработчика.
*
* @param mixed &$arFields
* @param mixed $colorId
*
* @return void
*/
public function modifyResult(&$arFields, $colorId)
{
$iblockId = $arFields["IBLOCK_ID"];
$propertyId = $this->iblocks[$iblockId];
$arFields["PROPERTY_VALUES"][$propertyId] = array(
"n0" => array(
"VALUE" => $colorId,
)
);
}
/**
* Обработчик событий инфоблока.
*
* @param mixed &$arFields Поля элемента.
*
* @return void
*/
public function eventHandler(&$arFields)
{
if (!$this->checkFields($arFields))
return;
if (!$this->colorReference)
{
if (!$this->initColorReference())
return;
}
if (!$this->initFile($arFields))
return;
if (!$this->loadFile())
return;
if (!$this->createMask())
return;
$luckyGuess = $this->guessColor();
$this->modifyResult($arFields, $luckyGuess);
}
/**
* Функция фозвращает усреднённый цвет картинки (RGB).
* Если задана маска, то пикселы маски с заданным цветом исключаются.
*
* @param resource $image Изображение.
* @param null|resource $alpha Маска.
* @param integer $magenta цвет маски.
*
* @return array
*/
protected function getPictureColor($image, $alpha = null, $magenta = 0)
{
$result = array(
"R" => 0,
"G" => 0,
"B" => 0,
);
$width = imagesx($image);
$height = imagesy($image);
$c = 0;
for ($x = 0; $x < $width; $x++)
{
for ($y = 0; $y < $height; $y++)
{
$rgb = imagecolorat($image, $x, $y);
if ($alpha)
{
$a = imagecolorat($alpha, $x, $y);
if ($a == $magenta)
continue;
}
$result["R"] += (($rgb >> 16) & 0xFF);
$result["G"] += (($rgb >> 8) & 0xFF);
$result["B"] += ($rgb & 0xFF);
$c++;
}
}
if ($c)
{
foreach ($result as $i => $s)
{
$result[$i] = $s/$c;
}
}
return $result;
}
/**
* Функция преобразования цветовых координат из rgb в xyz
*
* @param array $color Массив описывающий цвет в виде array("R" => ..., "G" => ..., "B" => ...).
*
* @return array
* @see http://www.easyrgb.com/?X=MATH
**/
public static function rgb2xyz($color)
{
$var_R = ( $color["R"] / 255 ); //R from 0 to 255
$var_G = ( $color["G"] / 255 ); //G from 0 to 255
$var_B = ( $color["B"] / 255 ); //B from 0 to 255
if ($var_R > 0.04045)
$var_R = pow(($var_R + 0.055) / 1.055, 2.4);
else
$var_R = $var_R / 12.92;
if ($var_G > 0.04045)
$var_G = pow(($var_G + 0.055) / 1.055, 2.4);
else
$var_G = $var_G / 12.92;
if ($var_B > 0.04045)
$var_B = pow(($var_B + 0.055) / 1.055, 2.4);
else
$var_B = $var_B / 12.92;
$var_R = $var_R * 100;
$var_G = $var_G * 100;
$var_B = $var_B * 100;
//Observer. = 2°, Illuminant = D65
$result = array(
"X" => $var_R * 0.4124 + $var_G * 0.3576 + $var_B * 0.1805,
"Y" => $var_R * 0.2126 + $var_G * 0.7152 + $var_B * 0.0722,
"Z" => $var_R * 0.0193 + $var_G * 0.1192 + $var_B * 0.9505,
);
return $result;
}
/**
* Функция преобразования цветовых координат из xyz в L*a*b*
*
* @param array $color Массив описывающий цвет в виде array("X" => ..., "Y" => ..., "Z" => ...).
*
* @return array
* @see http://www.easyrgb.com/?X=MATH
* @see http://en.wikipedia.org/wiki/Lab_color_space
**/
public static function xyz2lab($color)
{
//Observer= 2°, Illuminant= D65
$var_X = $color["X"] / 95.047;
$var_Y = $color["Y"] / 100.000;
$var_Z = $color["Z"] / 108.883;
if ($var_X > 0.008856)
$var_X = pow($var_X, 1/3);
else
$var_X = (7.787 * $var_X) + (16 / 116);
if ($var_Y > 0.008856)
$var_Y = pow($var_Y, 1/3);
else
$var_Y = (7.787 * $var_Y) + (16 / 116);
if ($var_Z > 0.008856)
$var_Z = pow($var_Z, 1/3);
else
$var_Z = (7.787 * $var_Z) + (16 / 116);
$result = array(
"L" => ( 116 * $var_Y ) - 16,
"a" => 500 * ($var_X - $var_Y),
"b" => 200 * ($var_Y - $var_Z),
);
return $result;
}
/**
* Делает негативное изображение.
*
* @param resource $picture Изображение над которым проводится манипуляция.
*
* @return void
**/
public static function imageNegate($picture)
{
imagefilter($picture, IMG_FILTER_NEGATE);
}
/**
* Выделение края.
*
* @param resource $picture Изображение над которым проводится манипуляция.
*
* @return void
* @see http://www.emanueleferonato.com/2010/10/19/image-edge-detection-algorithm-php-version/
**/
function imageSobel($picture)
{
$sx = imagesx($picture);
$sy = imagesy($picture);
$backup = imagecreatetruecolor($sx, $sy);
imagealphablending($backup, false);
imagecopy($backup, $picture, 0, 0, 0, 0, $sx, $sy);
$matrix1 = array(
array(-1, -2 ,-1),
array(0 ,0,0),
array(1,2,1),
);
$matrix2 = array(
array(-1, 0 ,1),
array(-2 ,0,2),
array(-1,0,1),
);
for($y = 0; $y < $sy; ++$y)
{
for($x = 0; $x < $sx; ++$x)
{
$alpha = (imagecolorat($backup, $x, $y) >> 24) & 0xFF;
$new1_r = $new1_g = $new1_b = 0;
$new2_r = $new2_g = $new2_b = 0;
for ($j = 0; $j < 3; ++$j)
{
$yv = $y - 1 + $j;
if($yv < 0)
$yv = 0;
elseif($yv >= $sy)
$yv = $sy - 1;
for ($i = 0; $i < 3; ++$i)
{
$xv = $x - 1 + $i;
if($xv < 0)
$xv = 0;
elseif($xv >= $sx)
$xv = $sx - 1;
$rgb = imagecolorat($backup, $xv, $yv);
$m1 = $matrix1[$j][$i];
$new1_r += (($rgb >> 16) & 0xFF) * $m1;
$new1_g += (($rgb >> 8) & 0xFF) * $m1;
$new1_b += ($rgb & 0xFF) * $m1;
$m2 = $matrix2[$j][$i];
$new2_r += (($rgb >> 16) & 0xFF) * $m2;
$new2_g += (($rgb >> 8) & 0xFF) * $m2;
$new2_b += ($rgb & 0xFF) * $m2;
}
}
$lum1 = $new1_r*0.30+$new1_g*0.59+$new1_b*0.11;
$lum2 = $new2_r*0.30+$new2_g*0.59+$new2_b*0.11;
$gray = sqrt($lum1*$lum1+$lum2*$lum2);
$new_pxl = imagecolorallocatealpha($picture, $gray, $gray, $gray, $alpha);
imagesetpixel($picture, $x, $y, $new_pxl);
}
}
imagedestroy($backup);
}
/**
* Размытие изображения.
*
* @param resource $picture Изображение над которым проводится манипуляция.
*
* @return void
**/
public static function imageBlur($picture)
{
imagefilter($picture, IMG_FILTER_GAUSSIAN_BLUR);
}
/**
* Делает Ч/Б изображение основываясь на пороговом значении.
*
* @param resource $picture Изображение над которым проводится манипуляция.
* @param integer $threshold Пороговое значение делающее пиксел белым.
*
* @return void
**/
public static function imageThresholdToWhite($picture, $threshold)
{
$sx = imagesx($picture);
$sy = imagesy($picture);
$white_pxl = imagecolorallocate($picture, 255, 255, 255);
$black_pxl = imagecolorallocate($picture, 0, 0, 0);
for($y = 0; $y < $sy; ++$y)
{
for($x = 0; $x < $sx; ++$x)
{
$rgb = imagecolorat($picture, $x, $y);
$r = (($rgb >> 16) & 0xFF);
$g = (($rgb >> 8) & 0xFF);
$b = ($rgb & 0xFF);
if ($r > $threshold && $g > $threshold && $b > $threshold)
imagesetpixel($picture, $x, $y, $white_pxl);
else
imagesetpixel($picture, $x, $y, $black_pxl);
}
}
}
/**
* Заливка изображения заданным цветом с четырёх углов.
*
* @param resource $picture Изображение над которым проводится манипуляция.
* @param integer $color Цвет заливки.
*
* @return void
**/
public static function imageFillCorners($picture, $color)
{
$sx = imagesx($picture);
$sy = imagesy($picture);
imagefill($picture, 0, 0, $color);
imagefill($picture, $sx-1, 0, $color);
imagefill($picture, 0, $sy-1, $color);
imagefill($picture, $sx-1, $sy-1, $color);
}
}
|
При сохранении товара вычисляем цвет и записываем его в значение свойства.
После этого хоть обычный, хоть умный фильтр можно применять уже к этому свойству.
Цвета не создаются, а выбираются из справочника по степени похожести.
Черно-белое, красно-зеленое или более чем два цвета, на которые должен реагировать поиск
Да и не ставилась цель написать искусственный разум за выходные
Здесь взял таблицу цветов.
Вставил её в LibreOffice Calc и экспортировал в csv.
Завёл инфоблок с четырьмя свойствами (HSV, RGB, CMYK и НЕХ) и загрузил в него данные.
Получился справочник на 960 цветов.
Для его использования создался такой код:
class ColorGuessBasedOnIblock extends ColorGuess { public function initColorReference() { $this->colorReference = array(); $rsData = CIBlockElement::getList(array(), array( "IBLOCK_ID" => $this->colorReferenceId, ), false, false, array("ID", "NAME", "PROPERTY_RGB")); while($arData = $rsData->fetch()) { if ($arData["PROPERTY_RGB_VALUE"]) { list($r, $g, $b) = explode(",", $arData["PROPERTY_RGB_VALUE"], 3); $this->colorReference[$arData["ID"]] = array( "R" => $r, "G" => $g, "B" => $b, ); } } return true; } } $colorG = new ColorGuessBasedOnIblock(6); // Справочник $colorG->handleIblock(1, 97);Модель в красном платье "усреднялась" до .... ))
Тогда написал очень не оптимальный код, но тут анализируется каждый пиксель изображения отдельно.
И затем выбирается цвет встречающийся в большинстве случаев.
Эту функцию поместите в класс наследник ColorGuess:
public function guessColor() { $histogram = array(); foreach ($this->colorReference as $key => $arData) { $c1 = $this->rgb2xyz($arData); $c1 = $this->xyz2lab($c1); $c1["C"] = 0; //счётчик близких цветов. $histogram[$key] = $c1; } $cache = array(); $width = imagesx($this->image); $height = imagesy($this->image); for ($x = 0; $x < $width; $x++) { for ($y = 0; $y < $height; $y++) { $a = imagecolorat($this->maskImage, $x, $y); if ($a == $this->maskColor) continue; $rgb = imagecolorat($this->image, $x, $y); $rgb = $rgb & 0xFFFFFF; if (!isset($cache[$rgb])) { $imageColor = array( "R" => (($rgb >> 16) & 0xFF), "G" => (($rgb >> 8) & 0xFF), "B" => ($rgb & 0xFF), ); $imageColor = $this->rgb2xyz($imageColor); $imageColor = $this->xyz2lab($imageColor); $distances = array(); foreach ($histogram as $key => $referenceColor) { $dL = $imageColor["L"] - $referenceColor["L"]; $dA = $imageColor["a"] - $referenceColor["a"]; $dB = $imageColor["b"] - $referenceColor["b"]; $distances[$key] = $dL*$dL + $dA*$dA + $dB*$dB; } asort($distances); reset($distances); $cache[$rgb] = key($distances); } $key = $cache[$rgb]; $histogram[$key]["C"]++; } } \Bitrix\Main\Type\Collection::sortByColumn($histogram, array("C" => SORT_DESC), '', null, true); reset($histogram); return key($histogram); }PS желающие могут парой строчек сделать множественное угадывание например по персентилю. Из guessColor возвращаем массив наиболее встречающихся цветов, а в modifyResult "сгружаем" их во множественное свойство.