Resultado incorrecto de PHP para imagetruecolortopalette con PNG con transparencia




image-processing php-gd (4)

Hasta el momento, no he encontrado una manera de hacer esto exactamente menos de reimplementar pngquant en PHP / GD, lo que creo que es posible. (Es decir, también cuantificando el canal alfa. Tampoco pude hacer que GD oscureciera alfa de la manera esperada tampoco).

Sin embargo, lo siguiente podría ser un término medio útil. (Para usted u otros que están atrapados con GD). La función de cambio de tamaño acepta un color mate como fondo y luego establece los píxeles que son transparentes (o casi) en un índice transparente. Hay un valor de umbral para establecer la cantidad de alfa a considerar. (Los valores más bajos para $alphaThreshold mostrarían menos del color mate proporcionado, pero eliminarían progresivamente más secciones alfa-transparentes del original).

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;
}

Así que aquí sería un ejemplo con un fondo blanco y un fondo verde. El pingüino de la izquierda tiene un blanco mate. El pingüino de la derecha tiene un verde mate.

Aquí está la salida con mi pingüino de prueba:

Estoy tratando de escribir un script que cambie el tamaño de una imagen PNG y luego lo convierta al modo PNG-8 bit. Por lo tanto, el tamaño del archivo resultante será más pequeño pero sin demasiada pérdida de calidad.

Editar: estilo de cotización para imágenes para mostrar mejor la transparencia

El cambio de tamaño funciona perfectamente, preservando también la transparencia:

El problema es cuando convierto la imagen en 8 bits:

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);

La imagen resultante es esta, con la transparencia alrededor y un poco dentro de la imagen:

Si configuro 256 colores en lugar de 255:

imagetruecolortopalette($resizedImg, true, 256);

la imagen será con fondo negro:

Se produce un resultado similar con esta imagen (tenga en cuenta la media transparencia para el caso con 255 colores):

original: 255 colores: 256 colores:

El código de la función completa:

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;
}

Lo que probé:

  • https://stackoverflow.com/a/8144620/2342558

    // 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;

resultados:

  • https://stackoverflow.com/a/55402802/2342558

nada cambia

  • otros mensajes SO y algunos en la Web

Además, sin cambiar el tamaño de la imagen (eliminando el imagecopyresampled y adaptando el nombre de las variables) el resultado es el mismo.

¿Pueden ayudarme a que funcione y a comprender la razón de este extraño comportamiento?

Alguna información en phpinfo() :

  • PHP 7.0.33
  • GD incluido (compatible con 2.1.0)
  • PNG Support habilitado
  • libPNG 1.5.13.

Editar :

En GIMP v.2.8.22 puedo guardar una imagen para Web con estas propiedades:

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

y produce una imagen reducida casi idéntica a la original.

También pngquant, tinypng y muchos otros hacen el mismo trabajo, pero necesito hacerlo con PHP .

Edit2 :

Desafortunadamente, no puedo usar ImageMagick porque mi código está en un alojamiento compartido sin que esté instalado.

Edit3 :

en phpinfo() resulta que el módulo imagemagick no está instalado.

Edit4 :

La recompensa caduca, en los próximos días permítanme hacer algunas pruebas con sus respuestas, tal vez haya una solución solo con PHP.


No creo que este sea un comportamiento extraño.

La documentación de PHP no dice esto, pero supongo que imagefill() funciona como en la mayoría de las otras aplicaciones: al llenar los píxeles conectados con el mismo color que el píxel donde comenzó el relleno (0, 0) .

Debido a que primero configura la paleta en 255 píxeles (o 256), convierte todas las áreas oscuras a un color negro y pierde toda la transparencia. Cuando inunda el relleno comenzando en la parte superior izquierda, todos los píxeles conectados (también dentro del pingüino y el pato) se volverán transparentes.

Creo que la única forma de hacer esto sin ImageMagick es atravesar todos los píxeles de la imagen redimensionada y establecer manualmente el color del píxel en una paleta limitada.

Hace algún tiempo escribí un pequeño script que reduce los colores de un PNG mientras mantiene la información alfa completa (1). Esto reducirá la paleta que usa el archivo PNG y, por lo tanto, el tamaño del archivo. No importa mucho si el PNG resultante aún tiene más de 8 bits. Una paleta pequeña reducirá el tamaño del archivo de todos modos.

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

Editar: Acabo de usar su PNG redimensionado (con transparencia) como entrada para mi script y lo convertí de un archivo de 12 kB a un archivo de 7 kB usando solo 32 colores:

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



Respuesta actualizada

Tuve un poco más de tiempo para resolver el código completo para responderte. ¡He simplificado lo que tenías considerablemente y parece que hace lo que creo que quieres ahora!

#!/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');
?>

Resultado

Canal alfa extraído:

Respuesta original

Creé una función PHP para extraer el canal alfa de una imagen y luego aplicar ese canal alfa a otra imagen.

Si aplica el canal alfa copiado a una imagen de color verdadero, permitirá un alfa uniforme con una resolución de 7 bits, es decir, hasta 127. Si aplica el alfa copiado a una imagen paletizada, lo limitará al 50% (puede cámbielo) para que la imagen de salida tenga alfa binario (activar / desactivar).

Entonces, extraje el alfa de esta imagen; es de esperar que pueda ver que hay una rampa / gradiente alfa en el medio.

Y aplicó el alfa copiado a esta imagen.

Donde la segunda imagen era color verdadero, el alfa aparece así:

Donde la segunda imagen fue paletizada, el alfa aparece así:

El código debería explicarse bastante por sí mismo. Descomente las declaraciones printf() que contienen DEBUG: para muchos resultados:

#!/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');
?>

Aquí está la capa alfa extraída, solo por diversión. Tenga en cuenta que en realidad es una imagen en escala de grises que representa el canal alfa; no tiene ningún componente alfa en sí mismo.

Palabras clave : PHP, gd, imagen, procesamiento de imágenes, alfa, capa alfa, extraer alfa, copiar alfa, aplicar alfa, reemplazar alfa.







php-gd