Лечим шумы и артефакты при ресайзе библиотекой GD изображений с однородным (белым) фоном

В каталогах интернет магазинов можно часто встретить изображения товаров на однородном фоне. Особенно красиво, если белая подложка картинки сливается с белым фоном сайта, что создаёт эффект, будто товар лежит прямо на веб-странице. Просматривая сайты, написанные на PHP, можно обнаружить досадный момент - фон у этих фотографий оказывается не идеально белым, а слегка сероватым (особенно это хорошо заметно, если смотреть на монитор немного снизу). Открыв такую картинку в графическом редакторе и пощупав "пипеткой", мы увидим, что фон может быть испещрён пикселями серого цвета. Оказывается, очень часто виноваты не исходные файлы, а библиотека GD, при помощи которой создаются эскизы фотографий.


Пример картинки с испорченным белым фоном

Посмотрите на этот пример. С виду - картинка как картинка, но при взгляде с некоторых углов можно заметить, что фон отличается от чисто белой страницы моего блога. Для наглядности я вырезал кусочек фона, приблизил его и уменьшил яркость: теперь наличие дефектов цвета заметно невооружённому взгляду (при уменьшении яркости светло-серый цвет испорченных пикселей стал более заметен).

Почему фон становится неоднородным после ресайза?

Первая мысль, которая приходит - что качество создаваемых при ресайзе картинок установлено не на 100%. Мысль хорошая, а качество в любом случае не помешает проверить. Но этот пример был получен с использований функций gd imagecreatetruecolor(), imagecopyresampled() и imagejpeg() при максимальном качестве (100 из 100):


// Примерно так в общем случае выглядит создание эскиза с помощью GD
$oldImg = imagecreatefromjpeg($oldImgPath);
$newImg = imagecreatetruecolor($width, $height);
imagecopyresampled($newImg, $oldImg, 0, 0, 0, 0, $newWidth, $newHeight, $oldWidth, $oldHeight);
imagejpeg($newImg, $newImg, 100);
                    

Так что можно сказать с уверенностью, что фон при ресайзе портит gd. Скорее всего, она чего-нибудь там пытается оптимизировать, и явно не слишком удачно :) Кстати, что-то подобное происходит с однородным фоном любого цвета. Относится ли это к какой-то конкретной версии gd или же присуще всем библиотекам - этот вопрос я не исследовал. Зато попытался разобраться, как это дело исправить.

Очищаем фон картинки от мусора

Пощупав зловредные пиксели пипеткой, можно выяснить, что большинство из них имеет самые близкие к белому цвета: #fefefe (rgb 254, 254, 254) и #fdfdfd (rgb 253, 253, 253). Задача становится ясной - нужно заменить пиксели этих цветов на белые. Приведу здесь универсальный рецепт, который в общем-то подойдёт для любого движка, нужно лишь найти верное место, куда его вставить.

Итак, перво-наперво находим скрипт, который отвечает за создание эскизов и находим в нём место, где происходит изменение размера изображения. Можно запустить поиск по всем файлам вашего проекта по запросу imagecopyresampled:


// Что-то подобное присутствует в любом ресайзере на gd
$oldImg = imagecreatefromjpeg($oldImgPath);
$newImg = imagecreatetruecolor($width, $height);
imagecopyresampled($newImg, $oldImg, 0, 0, 0, 0, $newWidth, $newHeight, $oldWidth, $oldHeight);

/*
 * А вот и он - финт ушами для очистки белого фона от шумов и артефактов
 * Действует в лоб - пробегает картинку и заменяет на ней почти белые цвета на белый
 * Добавляется в код ресайзинга после imagecopyresampled
 */

// Это - цвет на который будем заменять (белый)
$colorWhite = imagecolorallocate($newImg, 255, 255, 255);

// Пробегаем все пиксели на изображении по вертикали и горизонтали
for($y=0; $y<($newHeight); ++$y)
{
    for($x=0; $x<($newWidth); ++$x)
    {
        $colorat=imagecolorat($newImg, $x, $y);
        $r = ($colorat >> 16) & 0xFF;
        $g = ($colorat >> 8) & 0xFF;
        $b = $colorat & 0xFF;

        // Если цвет пикселя нас не устраивает, заменяем его на белый
        if(($r == 253 && $g == 253 && $b == 253) || ($r == 254 && $g == 254 && $b ==254)) {
            imagesetpixel($newImg, $x, $y, $colorWhite);
        }
    }
}
/*
 * Вот и всё! Как видите, такую штуку можно использовать для любого цвета, не только белого :)
 */

// Сохранение изображения, оно и так у вас было ;)
imagejpeg($newImg, $newImgPath, 100);
                    

Здесь я привёл универсальный рецепт для сферического gd-скрипта в вакууме. Если вы примените его для какой-то конкретной CMS или библиотеки работы с картинками (такое поведение, к примеру, было точно замечено в 1С Битрикс, библиотеках PHPThumb, PHPImageWorkShop), пишите в комментарии, с удовольствием дополню статью рецептом под конкретную ситуацию.

Должен сказать, что если на картинке присутствовали полутени, переходные цвета, они могут пострадать, ведь заменяются не только фоновые пиксели, а вообще все пиксели определённых цветов. Но результат получается весьма и весьма неплохой:

Исправленная картинка с чистым белым фоном

Вот такая красота!

UPD1: Рецепт для PrestaShop (по просьбе читателя)

Для устранения шумов на картинках в галерее товаров движка Prestashop откройте файл /classes/ImageManager.php и добавьте туда следующий метод:


/*
 * Clear background noise
 * @param gd resource $newImg
 * @param int $newWidth - dest image width
 * @param int $newWidth - dest image height
 */
public static function clearNoise($newImg, $newWidth, $newHeight)
{
    $colorWhite = imagecolorallocate($newImg, 255, 255, 255);
    for($y=0; $y<($newHeight); ++$y)
    {
        for($x=0; $x<($newWidth); ++$x)
        {
            $colorat=imagecolorat($newImg, $x, $y);
            $r = ($colorat >> 16) & 0xFF;
            $g = ($colorat >> 8) & 0xFF;
            $b = $colorat & 0xFF;
            if(($r == 253 && $g == 253 && $b == 253) || ($r == 254 && $g == 254 && $b ==254)) {
                imagesetpixel($newImg, $x, $y, $colorWhite);
            }
        }
    }
}
                    

Затем в этом же классе находим метод resize() и добавляем в его конец (после вызова imagecopyresampled() и перед return() - 201 строка):


// Добавляем вызов нашего метода в конце resize() после imagecopyresampled() и перед return()
self::clearNoise($dest_image, $dst_width, $dst_height);
                    

Если вы используете не только метод ресайза, но и метод обредки картинок, можно вставить вызов очистки в метод cut() (359 строка):


// Добавляем вызов нашего метода в конце cut() после imagecopyresampled()
self::clearNoise($dest_image, $dest['width'], $dest['height']);