Ошибочный результат PHP для imageetruecolortopalette с PNG с прозрачностью

Я пытаюсь написать сценарий PHP, который изменяет размер изображения PNG, а затем преобразует его в режим PNG-8 бит. Таким образом, размер результирующего файла будет меньше, но без слишком большой потери качества.

Изменить: стиль цитирования для изображений, чтобы лучше показать их прозрачность

Изменение размера работает отлично, сохраняя также прозрачность изображения:

originalImage

Проблема в том, что когда я конвертирую изображение в 8 бит:

imagetruecolortopalette($resizedImg, true, 255);

imagealphablending($resizedImg, false);

$transparent = imagecolorallocatealpha($resizedImg, 255, 255, 255, 127);
if(!imagefill($resizedImg, 0, 0, $transparent)) return false;

imagesavealpha($resizedImg, true);

В результате получается вот такое изображение с прозрачностью вокруг и немного внутри изображения:

Если я установлю 256 цветов вместо 255:

imagetruecolortopalette($resizedImg, true, 256);

изображение будет с черным фоном:

Аналогичный результат происходит с этим изображением (обратите внимание на полупрозрачность для случая с 255 цветами):

оригинал: 255 цветов: 256 цветов:

Полный код функции:

function resizePng($originalPath, $xImgNew='', $yImgNew='', $newPath='')
{
    if(!trim($originalPath) || !$xyOriginalPath = getimagesize("$originalPath")) return false;
    list($xImg, $yImg) = $xyOriginalPath;

    if(!$originalImg = imagecreatefrompng($originalPath)) return false;

    if(!$resizedImg = imagecreatetruecolor($xImgNew, $yImgNew)) return false;

    // preserve alpha
    imagealphablending($resizedImg, false);
    $transparent = imagecolorallocatealpha($resizedImg, 255, 255, 255, 127);
    if(!imagefill($resizedImg, 0, 0, $transparent)) return false;
    imagesavealpha($resizedImg, true);

    // copy content from originalImg to resizedImg
    if(!imagecopyresampled($resizedImg, $originalImg, 0, 0, 0, 0, $xImgNew, $yImgNew, $xImg, $yImg)) return false;

    // PNG-8 bit conversion
    imagetruecolortopalette($resizedImg, true, 255);

    // preserve alpha
    imagealphablending($resizedImg, false);
    $transparent = imagecolorallocatealpha($resizedImg, 255, 255, 255, 127);
    if(!imagefill($resizedImg, 0, 0, $transparent)) return false;
    imagesavealpha($resizedImg, true);

    if(!imagepng($resizedImg, ($newPath) ?: null, 8)) return false;

    return true;
}

Что пробовал:

  • /questions/35167968/php-gd-imagetruecolortopalette-ne-sohranyaet-prozrachnost/35167980#35167980

    // PNG-8 bit conversion
    imagetruecolortopalette($resizedImg, true, 255);
    
    imagesavealpha($resizedImg, true);
    imagecolortransparent($resizedImg, imagecolorat($resizedImg,0,0));
    
    // preserve alpha
    imagealphablending($resizedImg, false);
    $transparent = imagecolorallocatealpha($resizedImg, 255, 255, 255, 127);
    if(!imagefill($resizedImg, 0, 0, $transparent)) return false;
    imagesavealpha($resizedImg, true);
    
    if(!imagepng($resizedImg, ($newPath) ?: null, 8)) return false;
    

полученные результаты:

ничего не меняется

  • другие сообщения SO и некоторые в Интернете

Также без изменения размера изображения (удаление imagecopyresampled и адаптируя имя переменных) результат тот же.

Не могли бы вы помочь мне заставить его работать и понять причину такого странного поведения?

Некоторая информация в phpinfo():

  • PHP 7.0.33
  • GD в комплекте (совместим с 2.1.0)
  • PNG Support включен
  • libPNG 1.5.13.

Редактировать:

В GIMP v.2.8.22 я могу сохранить изображение для Интернета со следующими свойствами:

PNG-8
256 colors palette
Dither: Floyd-Steinberg / Floyd-Steinberg 2 / positioned

и это дает уменьшенное изображение, почти идентичное оригиналу.

Также pngquant, tinypng и многие другие выполняют ту же работу, но мне нужно делать это с помощью PHP.

Edit2:

К сожалению, я не могу использовать ImageMagick, потому что мой код находится на общем хостинге без его установки.

Edit3:

в phpinfo() приводит к тому, что imagemagick модуль не установлен.

Edit4:

Срок действия бонуса истекает, в следующие дни я проведу несколько тестов с вашими ответами, возможно, есть решение, использующее только PHP.

Edit5:

Это мои попытки с вашими ответами (обновлено 02.10.2019).

Примечание: я помещаю нижележащую сетку, чтобы лучше показать альфу.

Ответ Томаса Хейзера:

У пингвина есть видимые цветные полосы, но с уткой все в порядке (хотя иногда цветовой тон темнее).

Ответ EPB:

Только если в изображении есть только уже полностью прозрачные пиксели, оно работает очень хорошо (например, утка).

Ответ Марка Сетчелла:

Он делает полностью прозрачными все пиксели с альфа-каналом, также, если этот альфа-канал очень низкий, можно увидеть тень под пингвином. Также некоторые пиксели на краю утки преобразуются в черный пиксель или в полностью прозрачный пиксель.

5 ответов

Вы можете сделать это довольно легко в ImageMagick, который распространяется в Linux и доступен для Windows и Mac OSX. Помимо командной строки, существует множество других API. Вот как это сделать в командной строке ImageMagick.

Вход:

convert image.png PNG8:result1.png


PNG8: означает 256 цветов и двоичную прозрачность. Это означает либо полную прозрачность, либо ее отсутствие. Это вызывает сглаживание (ступенчатый переход) по краям. Если вы хотите установить цвет фона вместо прозрачности, вы можете сохранить в результате гладкий (сглаженный) контур. Итак, для белого фона.

convert image.png -background white -flatten PNG8:result2.png


ImageMagick работает под управлением PHP Imagick. Таким образом, вы сможете сделать это с помощью PHP Imagick. Или вы можете вызвать командную строку ImageMagick из PHP exec().

Updated Answer

I had a a bit more time to work out the full code to answer you - I have simplified what you had quite considerably and it seems to do what I think you want now!

#!/usr/bin/php -f
<?php

function extractAlpha($im){

   // Ensure input image is truecolour, not palette
   if(!imageistruecolor($im)){
      printf("DEBUG: Converting input image to truecolour\n");
      imagepalettetotruecolor($im);
   }

   // Get width and height
   $w = imagesx($im);
   $h = imagesy($im);

   // Allocate a new greyscale, palette (non-alpha!) image to hold the alpha layer, since it only needs to hold alpha values 0..127
   $alpha = imagecreate($w,$h);
   // Create a palette for 0..127
   for($i=0;$i<128;$i++){
      imagecolorallocate($alpha,$i,$i,$i);
   }

   for ($x = 0; $x < $w; $x++) {
      for ($y = 0; $y < $h; $y++) {
         // Get current color
         $rgba = imagecolorat($im, $x, $y);
         // $r = ($rgba >> 16) & 0xff;
         // $g = ($rgba >> 8) & 0xff;
         // $b = $rgba & 0xf;
         $a = ($rgba & 0x7F000000) >> 24;
         imagesetpixel($alpha,$x,$y,$a);
         //printf("DEBUG: alpha[%d,%d] = %d\n",$x,$y,$a);
      }
   }
   return $alpha;
}

function applyAlpha($im,$alpha){
   // If output image is truecolour
   //    iterate over pixels getting current color and just replacing alpha component
   // else (palettised)
   //    // find a transparent colour in the palette
   //    if not successful
   //       allocate transparent colour in palette
   //    iterate over pixels replacing transparent ones with allocated transparent colour

   // Get width and height
   $w = imagesx($im);
   $h = imagesy($im);

   // Ensure all the lovely new alpha we create will be saved when written to PNG 
   imagealphablending($im, false);
   imagesavealpha($im, true);

   // If output image is truecolour, we can set alpha 0..127
   if(imageistruecolor($im)){
      printf("DEBUG: Target image is truecolour\n");
      for ($x = 0; $x < $w; $x++) {
         for ($y = 0; $y < $h; $y++) {
            // Get current color 
            $rgba = imagecolorat($im, $x, $y);
            // Get alpha
            $a = imagecolorat($alpha,$x,$y);
            // printf("DEBUG: Setting alpha[%d,%d] = %d\n",$x,$y,$a);
            $new = ($rgba & 0xffffff) | ($a<<24);
            imagesetpixel($im,$x,$y,$new);
         }
      }
   } else {
      printf("DEBUG: Target image is palettised\n");
      // Must be palette image, get index of a fully transparent color
      $transp = -1;
      for($index=0;$index<imagecolorstotal($im);$index++){
         $c = imagecolorsforindex($im,$index);
         if($c["alpha"]==127){
            $transp = $index;
            printf("DEBUG: Found a transparent colour at index %d\n",$index);
         }
      }
      // If we didn't find a transparent colour in the palette, allocate one
      $transp = imagecolorallocatealpha($im,0,0,0,127);
      // Scan image replacing all pixels that are transparent in the original copied alpha channel with the index of a transparent pixel in current palette
      for ($x = 0; $x < $w; $x++) {
         for ($y = 0; $y < $h; $y++) {
            // Essentially we are thresholding the alpha here. If it was more than 50% transparent in original it will become fully trasnparent now
            $grey = imagecolorat($alpha,$x,$y) & 0xFF;
            if($grey>64){
               //printf("DEBUG: Replacing transparency at %d,%d\n",$x,$y);
               imagesetpixel($im,$x,$y,$transp);
            }
         }
      }
   }
   return $im;
}

// Set new width and height
$wNew = 300;
$hNew = 400;

// Open input image and get dimensions
$src = imagecreatefrompng('tux.png');
$w = imagesx($src);
$h = imagesy($src);

// Extract the alpha and save as greyscale for inspection
$alpha = extractAlpha($src);
// Resize alpha to match resized source image
$alpha = imagescale($alpha,$wNew,$hNew,IMG_NEAREST_NEIGHBOUR);
imagepng($alpha,'alpha.png');

// Resize original image
$resizedImg = imagecreatetruecolor($wNew, $hNew);
imagecopyresampled($resizedImg, $src, 0, 0, 0, 0, $wNew, $hNew, $w, $h);

// Palettise
imagetruecolortopalette($resizedImg, true, 250);

// Apply extracted alpha and save
$res = applyAlpha($resizedImg,$alpha);
imagepng($res,'result.png');
?>

Result

Extracted alpha channel:

Original Answer

I created a PHP function to extract the alpha channel from an image, and then to apply that alpha channel to another image.

If you apply the copied alpha channel to a truecolour image, it will permit a smooth alpha with 7-bit resolution, i.e. up to 127. If you apply the copied alpha to a palettised image, it will threshold it at 50% (you can change it) so that the output image has binary (on/off) alpha.

So, I extracted the alpha from this image - you can hopefully see there is an alpha ramp/gradient in the middle.

And applied the copied alpha to this image.

Where the second image was truecolour, the alpha comes across like this:

Where the second image was palettised, the alpha comes across like this:

The code should be pretty self-explanatory. Uncomment printf() statements containing DEBUG: for lots of output:

#!/usr/bin/php -f
<?php

// Make test images with ImageMagick as follows:
// convert -size 200x100 xc:magenta  \( -size 80x180 gradient: -rotate 90 -bordercolor white  -border 10 \) -compose copyopacity -composite png32:image1.png
// convert -size 200x100 xc:blue image2.png       # Makes palettised image
// or
// convert -size 200x100 xc:blue PNG24:image2.png # Makes truecolour image

function extractAlpha($im){

   // Ensure input image is truecolour, not palette
   if(!imageistruecolor($im)){
      printf("DEBUG: Converting input image to truecolour\n");
      imagepalettetotruecolor($im);
   }

   // Get width and height
   $w = imagesx($im);
   $h = imagesy($im);

   // Allocate a new greyscale, palette (non-alpha!) image to hold the alpha layer, since it only needs to hold alpha values 0..127
   $alpha = imagecreate($w,$h);
   // Create a palette for 0..127
   for($i=0;$i<128;$i++){
      imagecolorallocate($alpha,$i,$i,$i);
   }

   for ($x = 0; $x < $w; $x++) {
      for ($y = 0; $y < $h; $y++) {
         // Get current color
         $rgba = imagecolorat($im, $x, $y);
         // $r = ($rgba >> 16) & 0xff;
         // $g = ($rgba >> 8) & 0xff;
         // $b = $rgba & 0xf;
         $a = ($rgba & 0x7F000000) >> 24;
         imagesetpixel($alpha,$x,$y,$a);
         //printf("DEBUG: alpha[%d,%d] = %d\n",$x,$y,$a);
      }
   }
   return $alpha;
}

function applyAlpha($im,$alpha){
   // If image is truecolour
   //    iterate over pixels getting current color and just replacing alpha component
   // else (palettised)
   //    allocate a transparent black in the palette
   //    if not successful
   //       find any other transparent colour in palette
   //    iterate over pixels replacing transparent ones with allocated transparent colour

   // We expect the alpha image to be non-truecolour, i.e. palette-based - check!
   if(imageistruecolor($alpha)){
      printf("ERROR: Alpha image is truecolour, not palette-based as expected\n");
   }

   // Get width and height
   $w = imagesx($im);
   $h = imagesy($im);

   // Ensure all the lovely new alpha we create will be saved when written to PNG 
   imagealphablending($im, false);
   imagesavealpha($im, true);

   if(imageistruecolor($im)){
      printf("DEBUG: Target image is truecolour\n");
      for ($x = 0; $x < $w; $x++) {
         for ($y = 0; $y < $h; $y++) {
            // Get current color 
            $rgba = imagecolorat($im, $x, $y);
            // Get alpha
            $a = imagecolorat($alpha,$x,$y);
            // printf("DEBUG: Setting alpha[%d,%d] = %d\n",$x,$y,$a);
            $new = ($rgba & 0xffffff) | ($a<<24);
            imagesetpixel($im,$x,$y,$new);
         }
      }
   } else {
      printf("DEBUG: Target image is palettised\n");
      // Must be palette image, get index of a fully transparent color
      $trans = imagecolorallocatealpha($im,0,0,0,127);
      if($trans===FALSE){
         printf("ERROR: Failed to allocate a transparent colour in palette. Either pass image with fewer colours, or look through palette and re-use some other index with alpha=127\n");
      } else {
         // Scan image replacing all pixels that are transparent in the original copied alpha channel with the index of a transparent pixel in current palette
         for ($x = 0; $x < $w; $x++) {
            for ($y = 0; $y < $h; $y++) {
               // Essentially we are thresholding the alpha here. If it was more than 50% transparent in original it will become fully trasnparent now
               if (imagecolorat($alpha,$x,$y) > 64){
                  imagesetpixel($im,$x,$y,$trans);
                  //printf("DEBUG: Setting alpha[%d,%d]=%d\n",$x,$y,$trans);
               }
            }
         }
      }
   }
   return $im;
}

// Open images to copy alpha from and to
$src = imagecreatefrompng('image1.png');
$dst = imagecreatefrompng('image2.png');

// Extract the alpha and save as greyscale for inspection
$alpha = extractAlpha($src);
imagepng($alpha,'alpha.png');

// Apply extracted alpha to second image and save
$res = applyAlpha($dst,$alpha);
imagepng($res,'result.png');
?>

Вот извлеченный альфа-слой, просто для удовольствия. Обратите внимание, что на самом деле это изображение в оттенках серого, представляющее альфа-канал - оно не имеет самого альфа-компонента.

Ключевые слова: PHP, gd, изображение, обработка изображений, альфа, альфа-слой, извлечение альфа, копирование альфа, применение альфа, замена альфа.

Не думаю, что это странное поведение.

В документации PHP об этом не говорится, но я предполагаю, что imagefill() работает так же, как и в большинстве других приложений: путем заливки связанных пикселей тем же цветом, что и пиксель, с которого началась заливка (0, 0).

Поскольку вы сначала устанавливаете палитру на 255 пикселей (или 256), вы конвертируете все темные области в черный цвет и теряете всю прозрачность. Когда вы затем заливаете заливку, начиная с левого верхнего угла, все связанные пиксели (также внутри пингвина и утки) станут прозрачными.

Я думаю, что единственный способ сделать это без ImageMagick - пройти все пиксели измененного изображения и вручную установить цвет пикселя на ограниченную палитру.

Некоторое время назад я написал небольшой скрипт, который уменьшает цвета PNG, сохраняя при этом полную альфа-информацию (1). Это уменьшит размер поддона, используемого файлом PNG, и, следовательно, размер файла. Не имеет большого значения, если размер результирующего PNG по-прежнему превышает 8 бит. Небольшой поддон в любом случае уменьшит размер файла.

(1) https://bitbucket.org/thuijzer/pngreduce/

Изменить: я просто использовал ваш измененный размер PNG (с прозрачностью) в качестве входных данных для моего скрипта и преобразовал его из файла размером 12 КБ в файл 7 КБ, используя только 32 цвета:

Reduced to 62.28% of original, 12.1kB to 7.54kB

Редактировать 2: я обновил свой скрипт и добавил необязательное дизеринг Флойда-Стейнберга. Результат с 16 цветами на канал:

Reduced to 66.94% of original, 12.1kB to 8.1kB

Обратите внимание, что дизеринг также влияет на размер файла, потому что "сложнее" сжать PNG, когда соседние пиксели имеют разные цвета.

На данный момент я не нашел способа сделать это , за исключением повторной реализации pngquant в PHP/GD, что, я думаю, возможно. (То есть, квантование альфа-канала тоже. Я также не смог заставить GD надежно дизерировать альфа ожидаемым образом.)

Тем не менее, следующее может быть полезным компромиссом. (Для вас или других, кто застрял с GD.) Функция изменения размера принимает матовый цвет в качестве фона, а затем устанавливает прозрачные (или очень близкие) пиксели в прозрачный индекс. Существует пороговое значение, чтобы установить, сколько альфы следует учитывать. (Более низкие значения для$alphaThreshold будет показывать меньше предоставленного матового цвета, но постепенно удалять альфа-прозрачные части оригинала.)

function resizePng2($originalPath, $xImgNew='', $yImgNew='', $newPath='', $backgroundMatte = [255,255,255], $alphaThreshold = 120)
{
    if(!trim($originalPath) || !$xyOriginalPath = getimagesize("$originalPath")) return false;
    list($xImg, $yImg) = $xyOriginalPath;

    if(!$originalImg = imagecreatefrompng($originalPath)) return false;

    if(!$resizedImg = imagecreatetruecolor($xImgNew, $yImgNew)) return false;
    if(!$refResizedImg = imagecreatetruecolor($xImgNew, $yImgNew)) return false;

    //Fill our resize target with the matte color.
    imagealphablending($resizedImg, true);
    $matte = imagecolorallocatealpha($resizedImg, $backgroundMatte[0], $backgroundMatte[1], $backgroundMatte[2], 0);
    if(!imagefill($resizedImg, 0, 0, $matte)) return false;
    imagesavealpha($resizedImg, true);


    // copy content from originalImg to resizedImg
    if(!imagecopyresampled($resizedImg, $originalImg, 0, 0, 0, 0, $xImgNew, $yImgNew, $xImg, $yImg)) return false;

    //Copy to our reference.
    $refTransparent = imagecolorallocatealpha($refResizedImg, 0, 0, 0, 127);
    if(!imagefill($refResizedImg, 0, 0, $refTransparent)) return false;
    if(!imagecopyresampled($refResizedImg, $originalImg, 0, 0, 0, 0, $xImgNew, $yImgNew, $xImg, $yImg)) return false;

    // PNG-8 bit conversion (Not the greatest, but it does have basic dithering)
    imagetruecolortopalette($resizedImg, true, 255);

    //Allocate our transparent index.
    imagealphablending($resizedImg, true);
    $transparent = imagecolorallocatealpha($resizedImg, 0,0,0,127);

    //Set the pixels in the output image to transparent where they were transparent
    //(or nearly so) in our original image. Set $alphaThreshold lower to adjust affect.
    for($x = 0; $x < $xImgNew; $x++) {
        for($y = 0; $y < $yImgNew; $y++) {
            $alpha = (imagecolorat($refResizedImg, $x, $y) >> 24);
            if($alpha >= $alphaThreshold) {
                imagesetpixel($resizedImg, $x, $y, $transparent);
            }
        }
    }

    if(!imagepng($resizedImg, ($newPath) ?: null, 8)) return false;

    return true;
}

Итак, вот пример с белым фоном и зеленым фоном. У пингвина слева белый матовый цвет. У пингвина справа зеленый матовый цвет.

Вот результат моего тестового пингвина:


Приложение: Ну и что, если вам нужны частично альфа-прозрачные пиксели, но есть только GD. Вам нужно будет самостоятельно обработать квантование / дизеринг. Итак, в качестве примера: я попытался это сделать, построив существующую библиотеку дизеринга и соединив ее с моим собственным рудиментарным квантователем. (Я бы не стал использовать это в производстве. На момент написания код был немного беспорядочным и очень непроверенным, и я не улучшил часть дизеринга для обработки больших палитр, поэтому он ОЧЕНЬ медленный. [Изменить: я добавил слой кэширования, так что теперь это не так, теперь его можно использовать в большинстве случаев.])

https://github.com/b65sol/gd-indexed-color-converter

// create an image
$image = imagecreatefrompng('76457185_p0.png');

// create a gd indexed color converter
$converter = new GDIndexedColorConverter();

// the color palette produced by the quantizer phase.
// Could manually add additional colors here.
$palette = $converter->quantize($image, 128, 5);

// THIS IS VERY SLOW! Need to speed up closestColor matching.
// Perhaps with a quadtree.
// convert the image to indexed color mode
$new_image = $converter->convertToIndexedColor($image, $palette, 0.2);

// save the new image
imagepng($new_image, 'example_indexed_color_alpha.png', 8);

Вот пример с сохранением альфа-прозрачности в индексированном изображении:

Как вы можете видеть на https://www.php.net/manual/en/function.imagetruecolortopalette.php:

Это работает не так хорошо, как можно было бы надеяться. Обычно лучше вместо этого просто создавать полноцветное изображение, что гарантирует высочайшее качество вывода.

вы можете использовать ImageMagick: https://www.php.net/manual/en/imagick.affinetransformimage.php

Другие вопросы по тегам