Ошибочный результат PHP для imageetruecolortopalette с PNG с прозрачностью
Я пытаюсь написать сценарий PHP, который изменяет размер изображения PNG, а затем преобразует его в режим PNG-8 бит. Таким образом, размер результирующего файла будет меньше, но без слишком большой потери качества.
Изменить: стиль цитирования для изображений, чтобы лучше показать их прозрачность
Изменение размера работает отлично, сохраняя также прозрачность изображения:
Проблема в том, что когда я конвертирую изображение в 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).
Примечание: я помещаю нижележащую сетку, чтобы лучше показать альфу.
У пингвина есть видимые цветные полосы, но с уткой все в порядке (хотя иногда цветовой тон темнее).
Только если в изображении есть только уже полностью прозрачные пиксели, оно работает очень хорошо (например, утка).
Он делает полностью прозрачными все пиксели с альфа-каналом, также, если этот альфа-канал очень низкий, можно увидеть тень под пингвином. Также некоторые пиксели на краю утки преобразуются в черный пиксель или в полностью прозрачный пиксель.
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