creating pictures with imagemagick is too slow. how to improve? - php

I'm coding a little CMS, where you can upload several images.
These images are converted into 3 versions (big, mid und thumbnail size) with imagemagick.
The problem is that imagemagick needs 5 minutes for creating these 3 versions of 4 pictures (which were uploaded).
here is the part with the imagemagick commands:
foreach($upIMGS as $key => $filename){
list($width, $height) = getimagesize($path.$filename);
if ($width > $height) $size = "x96";
else $size = "96x";
exec(P_IMAGEMAGICK." ".$path.$filename." -resize $size -gravity center -crop 96x96+0+0 +repage ".$path."th-".$filename);
exec(P_IMAGEMAGICK." ".$path.$filename." -resize 320x320 ".$path."hl-".$filename);
exec(P_IMAGEMAGICK." ".$path.$filename." -resize 514x ".$path."fl-".$filename);
unlink($path.$filename);
}
[the $upIMGS is an array with all the filenames of the recently uploaded images]
I mean.. it does work, but toooo slow and after 5min the server gives me an error. some of the files are generated and some are not...
Would be very nice if you can give me a tip.

I recently ran into the same issue, but I was only passing through the images once to resize them from their original 2592x1944 to 300xbestFit or bestFitx300
I am using the PHP class imagick instead of command lines but I cut my time in half by changing to -scale or scaleImage in my case. Here is a snippet of my test code.
while ($images = readdir($handle)) {
// check to see if the first or second character is a '.' or '..',
// if so then remove from list
if (substr($images,0,1) != '.') {
//Check files to see if there extensions match any of the following image extensions.
// GLOB_BRACE looks for all glob criteria within the {braces}
$images = glob($dir."{*.gif,*.jpg,*.png,*.jpeg}", GLOB_BRACE);
// the glob function gives us an array of images
$i = 0;
foreach ($images as $image) {
// parse the data given and remove the images/ $dir,
// imagemagick will not take paths, only image names.
$i++;
list ($dir, $image) = split('[/]', $image);
echo $i, " ", $image, "<br />";
$magick = new Imagick($dir."/".$image);
$imageprops = $magick->getImageGeometry();
if ($imageprops['width'] <= 300 && $imageprops['height'] <= 300) {
// don't upscale
} else {
// 29 Images at 2592x1944 takes 11.555036068 seconds ->
// output size = 300 x 255
$magick->scaleImage(300,300, true);
// 29 Images at 2592x1944 takes 23.3927891254 seconds ->
// output size = 300 x 255
//$magick->resizeImage(300,300, imagick::FILTER_LANCZOS, 0.9, true);
$magick->writeImage("thumb_".$image);
}
}
}
}
I am processing 29 images at 2592x1944 and went from 23.3927891254 seconds to 11.555036068 seconds. I hope this helps.
Edit:
In addition to what I said above I just ran into to the following, which might be helpful, on ImageMagick v6 Examples -- API & Scripting:
"Shell scripts are inherently slow. It is interpreted, require
multiple steps and extra file handling to disk. This is of course
better thanks to the new IM v6 option handling, allowing you to do a
large number of image processing operations in a single command. Even
so you can rarely can do everything in a single convert command, so
you often have to use multiple commands to achieve what you want."
"When reading large or even a large number of images, it is better to
use a Read
Modifier to
resize, or crop them, as IM does not them read in the full image,
thus reducing its memory requirement."
"If you call ImageMagick as an Apache module it will also reduce
startup time, as parts will be loaded once and kept available for
multiple use, rather than needing re-loading over and over. This may
become more practical in the future with a permanently running
'daemon' IM process."

This example may help as it loads the main image into the memory and works on that to make the other images:
$cmd = " input.jpg \( -clone 0 -thumbnail x480 -write 480_wide.jpg +delete \)".
" \( -clone 0 -thumbnail x250 -write 250_wide.jpg +delete \) ".
" \( -clone 0 -thumbnail x100 -write 100_wide.jpg +delete \) -thumbnail 64x64! null: ";
exec("convert $cmd 64_square.jpg ");
This is creating 4 different size images.

Wait what? It should not take five minutes to generate 12 pictures from three uploaded pictures.
I don't see the rest of your code, but why is your getimagesize path $path.$filename but your unlink P_UPLOADS.$value? Is there a reason they are different? Where does $value come from anyway, it is not defined in your foreach() loop. Maybe you simply have a bug that causes the script to hang. I've used ImageMagick (though not with exec(), but with actual class) and it's been very fast.
Have you run diagnostics on your foreach loop()? Like print out milliseconds it takes to do each of those exec() commands.

A little trick would be to create the largest picture first and then create subsequent pictures from this one, not original.
However, 5 min seems too much for just 12 images anyway.
You have to do some investigations.
does the resize take considerable time if the source is relatively small (say 1024x768)?
does it take the same time called directly from the shell?
If the delay is inevitable, you may wish to move resize facility into dedicated server, put them in the queue and check the process with simple AJAX snippet.

Related

PHP ImageMagick change color in every frame Animated Gif

I am trying to change RGB values in an animated GIF to another RGB value. However in all my attempts it's only changing the color in one frame instead of all frames.
$imgif = new Imagick(HOME_PATH.'/images/6.gif');
$target = 'rgba(238,131,41, 1.0)';
$fill = 'rgba(163,145,144, 1.0)';
$fuzz = 0.05 * $imgif->getQuantumRange()['quantumRangeLong'];
$imgif->opaquePaintImage($target, $fill, $fuzz, false, Imagick::CHANNEL_DEFAULT);
$imgifblob = $imgif->getImagesBlob();
Is there anyway to index a color for the entire GIF and change it? I am rather lost as I don't have much experience with image manipulation.
Sorry, I do not know Imagick that well. But here is how to do it in Imagemagick command line. You may have to loop over each frame in Imagick, since it may not allow for multi-frame images to be processing. But perhaps using readImages would work. See https://www.php.net/manual/en/imagick.readimages.php, where you specify all frames in the gif as 6.gif[0--1], which is all frames from frame 0 to the last frame -1. Sorry, I just do not know. An Imagick expert might be able to help further.
Input:
convert bunny_anim.gif -coalesce -fuzz 10% -fill red -opaque "rgb(51,77,204)" -layers optimize new_bunny.gif

Exploding Animated GIF and Manipulating Frames (GD Library)

So since animated GIFs are a series of GIFs concatenated together with "\x00\x21\xF9\x04", I am able to explode the GIF and implode it to take it apart and build it again. However I can't seem to get GD to create an image from the data.
Is there something I need to append in order to have GD recognize the data?
$im = file_get_contents('test.gif'); //get the data for the file
$imgarray = explode("\x00\x21\xF9\x04", $im); //split up the frames
foreach ($imgarray as $frame) {
$img[] = imagecreatefromstring($frame); //this is the line that fails
}
$new_gif = implode("\x00\x21\xF9\x04", $img); //this should work but imagecreatefromstring fails
$new_gif = implode("\x00\x21\xF9\x04", $imgarray); (Does work as it just puts the image back together)
A GIF does not contain just separate images appended after each other. A frame in a GIF may change just a part of the image - it does not have to cover the whole frame. It can also contain a local palette, but otherwise it relies on the global palette of the image - which is stored for the file itself and not just the frame.
I.e. - you can't just explode the file and decode each segment separately and except to get useful images from GD.
You'll at least have to add the gif header to each set of image data, but I strongly recommend using the PHP ImageMagick interface instead if possible - it has far better support for iterating through frames in an image.
Another option is using a pure PHP implementation that does what you want, such as GifFrameExtractor.
The relevant code is located at line 137 of the source file:
$img = imagecreatefromstring(
$this->fileHeader["gifheader"] .
$this->frameSources[$i]["graphicsextension"] .
$this->frameSources[$i]["imagedata"] .
chr(0x3b)
);
As you can see, there is far more data necessary (the header, the extension (87a vs 89) and a terminating character) to make it valid GIF data.
In Imagemagick, this is pretty trivial. You can coalesce the image to fill out any frames that have been optimized, then do your processing, then optimize it again.
Input:
convert bunny.gif -coalesce -resize 50% -layers optimize bunny_small.gif

Detect if image is grayscale or color using Imagick

I'm attempting to try and assign a value to an image based on its 'saturation level', to see if the image is black and white or color. I'm using Imagick, and have found what seems to be the perfect code for the command line and am trying to replicate it using the PHP library.
I think I understand the concept:
Convert image to HSL.
Extract the 'g' channel (which is the S channel in HSL).
Calculate the mean of this channel.
Command line code
convert '$image_path' -colorspace HSL -channel g -separate +channel -format '%[fx:mean]' info:
My PHP code
$imagick = new Imagick($image_path);
$imagick->setColorspace(imagick::COLORSPACE_HSL);
print_r($imagick->getImageChannelMean(imagick::CHANNEL_GREEN));
Output
My PHP code isn't outputting the same sort of values as the command line code, though. For example, a grayscale image gives 0 for the command line code, but the PHP code gives [mean] => 10845.392051182 [standardDeviation] => 7367.5888849872.
Similarly, another grayscale image gives 0 vs. [mean] => 31380.528443457 [standardDeviation] => 19703.501101904.
A colorful image gives 0.565309 vs. [mean] => 33991.552881892 [standardDeviation] => 16254.018540044.
There just doesn't seem to be any kind of pattern between the different values. Am I doing something obviously wrong?
Thanks.
Just to add, I've also tried this PHP code
$imagick = new Imagick($image_path);
$imagick->setColorspace(imagick::COLORSPACE_HSL);
$imagick->separateImageChannel(imagick::CHANNEL_GREEN);
$imagick->setFormat('%[fx:mean]');
But I get an Unable to set format error when I try and set the format. I've also tried setFormat('%[fx:mean] info:'), setFormat('%[mean]'), setFormat('%mean'), etc.
Update — FIXED!
Thanks to #danack for figuring out I needed to use transformImageColorspace() and not setColorspace(). The working code is below.
$imagick = new Imagick($image_path);
$imagick->transformImageColorspace(imagick::COLORSPACE_HSL);
$saturation_channel = $imagick->getImageChannelMean(imagick::CHANNEL_GREEN);
$saturation_level = $saturation_channel['mean']/65535;
setFormat doesn't replicate the command line option -format - the one in Imagick tries to set the image format, which should be png, jpg etc. The one in the command line is setting the format for info - the closest match for which in Imagick is calling $imagick->identifyImage(true) and parsing the results of that.
Also you're just calling the wrong function - it should be transformImageColorspace not setColorSpace. If you use that you can use the statistics from getImageChannelMean.
There are other ways to test for greyness which may be more appropriate under certain circumstances. The first is to convert the a clone of the image to grayscale, and then compare it to the original image:
$imagick = new Imagick($image_path);
$imagickGrey = clone $imagick;
$imagickGrey->setimagetype(\Imagick::IMGTYPE_GRAYSCALE);
$differenceInfo = $imagick->compareimages($imagickGrey, \Imagick::METRIC_MEANABSOLUTEERROR);
if ($differenceInfo['mean'] <= 0.0000001) {
echo "Grey enough";
}
That would probably be appropriate if you image had areas of color that were also transparent - so they theoretically have color, but not visibly.
Or if you only care about whether the image is of a grayscale format:
$imageType = $imagick->getImageType();
if ($imageType === \Imagick::IMGTYPE_GRAYSCALE ||
$imageType === Imagick::IMGTYPE_GRAYSCALEMATTE) {
//This is grayscale
}
I found a command line here:
convert image.png -colorspace HSL -channel g -separate +channel -format "%[fx:mean]" info:
It prints a number between 0 and 1, where zero means grayscale.
If your images have tint. (from scanner as example) you should do auto color for them before detecting gray scale.
You should normalizeImage(imagick::CHANNEL_ALL) for all separate channels of image. separateImageChannel()
But

Combining 2 GraphicsMagick commands into 1 command, like in ImageMagick

I was using ImageMagick to create a new resized image with watermark, with this single command (in PHP):
exec("convert -filter Lanczos {$original_image} -thumbnail {$max_width}x{$max_height} -quality 90 {$watermark} -gravity center -unsharp 2x0.5+0.7+0 -composite {$cached}");
Now I switched to GM and am looking for a way to run 1 command to do the same task. The only way I found was to split it to 2 separate commands:
//create the resized image
exec("gm convert -filter Lanczos {$original_image} -thumbnail {$max_width}x{$max_height} -quality 90 -unsharp 2x0.5+0.7+0 {$cached}");
//apply the watermark and recreate the watermarked image, overwriting the previously resized image
exec("gm composite -quality 90 -dissolve 100 -gravity center {$watermark} {$cached} {$cached}");
Is there a way to combine them into 1 single command and by that maybe also reduce resources & drive usage?
I have received the following reply on this from Bob Friesenhahn, GraphicsMagick Maintainer:
You did not say what version of GraphicsMagick you are using. Modern
versions support a '-compose' option which may be put on the command
line after the input file name to remember the composition algorithm
to use. This composition algorithm is then used if the -mosaic or
-extent operators are used to do a composition. You can also use a
-page option after the input file name to locate the image when it
is composited with prior images in the list. Due to a weakness in
GM's convert command processing, the -mosaic or -extent operators must
be the last command prior to saving the output file. I believe that
ImageMagick's -composite must be a version of -mosaic which adds more
features (e.g. -mosaic might not support gravity but -composite does).
It seems like GraphicsMagick should implement something completely
compatible with ImageMagick's -composite.
Regardless, there is an effective workaround available if you need to
use your existing GM commands.
If you have a modern GraphicsMagick which supports 'gm batch', then
you can use the 'mpr' coder ("Magick Persistent Registry") to remember
intermediate images between commands and you can easily adapt your two
commands to execute with full efficiency using the existing command
lines. This Unix shell example should give you some ideas:
{
echo convert seaworld.jpg mpr:temporary
echo convert mpr:temporary crap.jpg
} | gm batch -prompt off -echo on
convert seaworld.jpg mpr:temporary
convert mpr:temporary crap.jpg
Notice that the output of the first command was saved (as an image
handle as natively used within GraphicsMagick) into 'mpr:temporary'
and then the second command took input from 'mpr:temporary' and wrote
the final output file. You can use arbitrary string arguments to
'mpr:' so you can have several images "in flight".
With this approach you can use 'gm convert' and 'gm composite' in the
same batch command.
I am not sure how one would best access this batch facility from PHP
but if PHP can stream commands to it from a pipe, then it can run for
quite a long time as a co-process to PHP and save considerable compute
time and overhead.

How can I trim just the left and right side of an image using Imagemagick in PHP?

I'm trying to trim a variable amount of whitespace in an image only the left and right side using ImageMagick and PHP. Does anyone know how to do this (perhaps using something other than imagemagick?)?
Here's an example.
I have these two images:
Each has a variable amount of text that is dynamically created in a fixed width image.
What I need to do is trim the background off the right and left side so the images come out like this:
If ImageMagick can't do it, I am willing to use something else, but I will need help on how exactly because I am not much of a programmer. Thanks!
Here's my current code that trims all sides of an image:
<?php
/* Create the object and read the image in */
$i = '3';
$im = new Imagick("test".$i.".png");
/* Trim the image. */
$im->trimImage(0);
/* Ouput the image */
//header("Content-Type: image/" . $im->getImageFormat());
//echo $im;
/*** Write the trimmed image to disk ***/
$im->writeImage(dirname(__FILE__) . '/test'.$i.'.png');
/*Display Image*/
echo $img = "<img src=\"test".$i.".png\">";
?>
I think you are on the right track with ImageMagick's -trim operator 1), but the trick would be to get it tell you what it would do without actually doing it, and then modify that to do what you really want...
So, to get the trim-box ImageMagick calculates for your first image, you do this:
convert -fuzz 10% image.jpg -format "%#" info:
60x29+21+31
That is a 60x29 pixel rectangle, offset 21 across and 31 down from the top left corner. Now, we want to get these values into bash variables, so I set the IFS (Input Field Separator) to split fields on spaces, x and also + signs:
#!/bin/bash
IFS=" x+" read a b c d < <(convert -fuzz 10% image.jpg -format "%#" info:)
echo $a $b $c $d
60 29 21 31
Now I can ignore the 29 and the 31 because we are only interested in cropping the width, and crop like this:
convert image.jpg -crop "${a}x+${c}+0" out.jpg
So, for your 2 images, I get these:
and the full procedure is this:
#!/bin/bash
IFS=" x+" read a b c d < <(convert -fuzz 10% image.jpg -format "%#" info:)
convert image.jpg -crop "${a}x+${c}+0" out.jpg
Notes
1) The -format %# is just a shorthand for the -trim operator, which would be this in full
convert image.jpg -trim info:
image.jpg JPEG 72x40 200x100+16+24 8-bit sRGB 0.000u 0:00.000
From what I can see in the ImageMagick docs on cropping and borders, it doesn't seem to be possible.
you can't specify an edge for "intelligent" cropping (known as-trim on the command line), and all the cropping methods that accept a geometry argument need a fixed number for cropping.
The only idea that comes to mind is to get the colour of the shaved area in a separate call, run trimImage, and add the lost areas back using -border.
Edit: The IM manual is suggesting something similar. Check out Trimming Just One Side of an Image. I'm not familiar with IM's PHP extension to translate the code into PHP calls but it should be half-way straightforward.
The GD based library WideImage has something similar. It's called autoCrop, by default it works on all four sides.
However, you could just add another parameter and based on it only crop top/bottom or left/right.
autoCrop code
It's pretty well documented. $img is a WideImage_Image type. There is also an interactive online demo of it.
Related question: Removing black bars off video thumbnail.
Using GD:
function imageautocrop( &$img) {
$emptycol = function ( $img, $x, $min, $max) {
for( $y=$min; $y<$max; $y++) {
$col = imagecolorsforindex( $img, imagecolorat( $img, $x, $y));
if( $col['alpha'] != 127) return false;
}
return true;
}
$trim = Array('top'=>0,'bot'=>0,'lft'=>0,'rgt'=>0);
$size = Array('x'=>imagesx($img)-1,'y'=>imagesy($img)-1);
// code for affecting rows removed due to question asked
while( $emptycol( $img, $trim['lft'], $trim['top'], $size['y']-$trim['bot'])) $trim['lft']++;
while( $emptycol( $img, $size['x']-$trim['rgt'], $trim['top'], $size['y']-$trim['bot'])) $trim['rgt']++;
$newimg = imagecreate( $size['x']-$trim['lft']-$trim['rgt']+1, $size['y']-$trim['top']-$trim['bot']+1);
imagecopy( $newimg, $img, 0, 0, $trim['lft'], $trim['top'], imagesx($newimg)+1, imagesy($newimg)+1);
imagedestroy($img);
$img = $newimg;
}
It's very old code of mine, so probably not optimal, but it does the job.
It is a two step process as text is dynamically generated
Generate the text image, determine width(image)
Overlay text image into background, determine width(background)
Use one tool mentioned above, crop (width(background)-width(image)/2 on either side
The trick is figuring out the width(image). See: How can I auto adjust the width of a GD-generated image to fit the text?
Then again, if you know width(image), you can crop the width(background) first before overlay
Use cropImage() instead. Something like this, perhaps:
$img_x_size = 800; // Set these to relevant values
$img_y_size = 600;
$crop_pixels = 20; // How many pixels to crop
// cropImage(XsizeOfCrop, YsizeOfCrop, CropXPos, CropYPos)
$im->cropImage($img_x_size - $crop_pixels, $img_y_size, 0, $crop_pixels / 2);

Categories