Lee Traynor's PHP Tutorials - ImageMagick® and Panorama Processing

Contents

Previous TutorialBack to IndexNext Tutorial

Las Cañadas, Tenerife, Spain: Dec 31, 2009.

ImageMagick® provides image manipulation from the command line, or alternatively, via PHP. Since many of the PHP functions are poorly documented, getting PHP to do what is intended can be a bit of a nightmare. Here we will look at how the Imagick extension of PHP (which is how ImageMagick is known within PHP) can be used to split up a panoramic image into individual frames (that then can be assembled into AVI or MPG format), (re-)introducing a naturalistic barrel distortion common to many cameras, thus giving our frame sequence the feeling as if the frames were being taken by an actual camera, and, finally, to consider how the Imagick extension could be use for digital zoom sequences.

First of all, ImageMagick® must be installed as a program in its own right on your computer. ImageMagick's homepage is www.imagemagick.org and Windows binaries complete with installers can be found here. The advice on the website is to install ImageMagick-6.5.9-3-Q16-windows-dll.exe, which is what I did. Note that this installation must also include a path entry so that the program can be found.

Next, the corresponding PECL library must be installed and entered into php.ini. If you do not know how to build the dll file from the source, then try Mikko's Blog which has this link to some builds. Here I chose the latest ts (thread-safe) V9 build and downloaded php_imagick_ts.dll (3.0.0-dev) from November 18, 2009. This file must then be saved to the folder with the PHP extension files. On my machine that is c:\Program Files\PHP\ext. Now php.ini in the PHP main directory has to be modified. Open it with a text editor (Notepad++) and find the place where the installed extensions are listed, normally at the very end of the file. In my case this looks looks like this:

[PHP_GD2]
extension=php_gd2.dll
[PHP_MYSQL]
extension=php_mysql.dll

To which I add another two lines:

[PHP_IMAGICK]
extension=php_imagick_ts.dll

Now, before general doom breaks out, it is important to note that Mikko's build is for PHP 5.3, so that if you haven't done this installation you will have to do that. (The alternative is to make your own V6 build which will run with PHP 5.2.x). Save php.ini, restart Apache and see whether or not phpinfo () contains the following:

imagick

imagick moduleenabled
imagick module version 3.0.0-dev
imagick classes Imagick, ImagickDraw, ImagickPixel, ImagickPixelIterator
ImageMagick version ImageMagick 6.5.8-8 2009-12-18 Q16 http://www.imagemagick.org
ImageMagick copyright Copyright (C) 1999-2010 ImageMagick Studio LLC
ImageMagick release date 2009-12-18
ImageMagick Number of supported formats: 211
ImageMagick Supported formats 3FR, A, AI, ART, ARW, AVI, AVS, B, BGR, BIE, BMP, BMP2, BMP3, BRF, BRG, C, CAL, CALS, CAPTION, CIN, CIP, CLIP, CLIPBOARD, CMYK, CMYKA, CR2, CRW, CUR, CUT, DCM, DCR, DCX, DDS, DFONT, DJVU, DNG, DOT, DPS, DPX, EMF, EPDF, EPI, EPS, EPS2, EPS3, EPSF, EPSI, EPT, EPT2, EPT3, ERF, EXR, FAX, FITS, FPX, FRACTAL, FTS, G, G3, GBR, GIF, GIF87, GRADIENT, GRAY, GRB, GROUP4, HALD, HISTOGRAM, HRZ, HTM, HTML, ICB, ICO, ICON, INFO, INLINE, IPL, ISOBRL, J2C, JBG, JBIG, JNG, JP2, JPC, JPEG, JPG, JPX, K, K25, KDC, LABEL, M, M2V, M4V, MAP, MAT, MATTE, MIFF, MNG, MONO, MOV, MP4, MPC, MPEG, MPG, MRW, MSL, MSVG, MTV, MVG, NEF, NULL, O, ORF, OTB, OTF, PAL, PALM, PAM, PATTERN, PBM, PCD, PCDS, PCL, PCT, PCX, PDB, PDF, PDFA, PEF, PFA, PFB, PFM, PGM, PGX, PICON, PICT, PIX, PJPEG, PLASMA, PNG, PNG24, PNG32, PNG8, PNM, PPM, PREVIEW, PS, PS2, PS3, PSD, PTIF, PWP, R, RADIAL-GRADIENT, RAF, RAS, RBG, RGB, RGBA, RGBO, RLA, RLE, SCR, SCT, SFW, SGI, SHTML, SR2, SRF, STEGANO, SUN, SVG, SVGZ, TEXT, TGA, THUMBNAIL, TIFF, TIFF64, TILE, TIM, TTC, TTF, TXT, UBRL, UIL, UYVY, VDA, VICAR, VID, VIFF, VST, WBMP, WMF, WMFWIN32, WMV, WMZ, WPG, X, X3F, XBM, XC, XCF, XPM, XPS, XV, XWD, Y, YCbCr, YCbCrA, YUV

If so, then Bob's your uncle and you can proceed with trying it out.

PHP comes with the GD extension already in the package, so no additional installation is required. However sometimes the GD2 package may be required (if you look closely, you'll see that I have it installed on my machine, although I'm not aware of what it was useful for...). In any case, GD is limited in two important aspects: the number of formats it can deal with, and the types of things it can do with these formats.

The formats that GD is limited to are ones typically found on web sites: JPG, GIF, and PNG; and something called WBMP which is not a Windows Bitmap, but a WAP bitmap. In this day and age, converting from one graphics format to another is not much of a question (Irfanview will convert the most common formats), apart from comfort.

More importantly, the actions that can be taken on images is rather limited. Fore example, if I want to resample an image with imagecopyresampled (), then only a standard filter is applied, although there are dozens on the market. Depending on how much the image is to be resized and what regularities the image contains, another filter may be preferable (e.g. to prevent Moiré patterning). Imagick provides access to these filters.

For the case in point, let's consider how multiple images are dealt with. In the previous tutorial a panoramic image was split up into a series of partially overlapping images that became the frames of a film. These images were numbered "0000.png", "0001.png", "0002.png" etc. To provide some semblance of order all these files were saved in a subdirectory named after the mother file.
Another, perhaps more elegant way of dealing with multiple files might be to store them as a multipage TIFF, which might then be used in the GIMP Animation Package to produce the movie directly. But GD cannot deal with TIFF. Imagick can.

Secondly, consider a possible solution to adding 3D or barrel distortions to a series of images. One of the simpler solutions goes along these lines:
We'll vertically distort an image (either to a barrel—fat in the middle, or a pincushion—squashed in the middle) by cutting the picture up into stripes and progressively stretching the stripes towards the middle (barrel) or starting with a large stretch at one end which decreases towards the middle (pincushion). Although we aim to stretch the picture to fit a smooth curve (and it doesn't really matter whether that is a parabola or a segment of a circle), image manipulations can only be done on integers, so that we will initially calculate a set of stripe dimensions to cut the picture into, e.g. stripe 1 will be stretched by 0 pixels and will be 24 pixels wide; stripe 2 will be stretched by 2 pixels and will be 20 pixels wide, etc. After the stretching, the images is composited from all the stretched stripes and the edges trimmed. If the image is not resized smaller after these operations, sharp edges will be detectable, especially in animations. Resizing can mask these edges, but because only the standard filter is available, the image may be blurry and require sharpening.

Enter Imagick. Here the function distortImage () will apparently do all the distortion we want without cutting the picture up into stripes. After that resizeImage () will resize the image with the desired filter and sharpen it at the same time.

Sound like magic? Yeah. Now the shoddy documentation of Imagick sails plainly into view. Although these two particular functions have names that approximate to what they do, a number of more esoterically named functions are required to keep the process on track. First of all. Secondly, the argument lists for the functions rarely tell you anything useful. It can be of help to look at the command line version of ImageMagick® to see what it requires and how it works and then to experiment to see if the PHP function can work with these arguments. At least one function does not work at all (morphImages ()) and a number of functions do things completely unrelated to their meaning in ordinary language. And then there are the lists of constants which are entirely unmemorable, but which constantly want to remind us of what an integer is. If you can bear the inexecrable language, try the ImageMagick® usage examples here.

Imagick promises improvements over GD in three further fields: process speed, image quality, and image (memory) size. In short, the Imagick extension has potential, but there is still a long way to go before it can be used by other than a specialised elite.

Imagick uses object-oriented syntax, that is, the object (variable) that is being acted upon by a function is handed over to that function with the "->" operator. Functions may contain arguments (or not). New objects can be created by the operator New or may be the result of certain functions. Which functions produce a new object can be seen from the function result in the PHP documentation. Boolean in general indicates only that the function was successfully carried out, whereas Imagick indicates the creation of a new object of the type Imagick.

We'll start with a form so that a number of parameters and functions can be comfortably set. The main options we have are:

Note that this script will call itself when submitted and first attempt to read out the $_POST variables that were sent with it.

<?php
extract ($_POST);
$b = "\r\n";
echo "<form method=\"post\" action=\"{$_SERVER["PHP_SELF"]}\">$b";
?>

<table>
<tr>
  <th align="right">Graphic file:&nbsp;</th>
  <td colspan="3"><input type="file" name="grfile" size="60"></td>
</tr>
<tr>
  <th>Increment:&nbsp;<select name="incr" size="1">
  <option value="1" selected>1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option><option value="6">6</option><option value="7">7</option><option value="8">8</option><option value="9">9</option><option value="10">10</option>
  <option value="12">12</option><option value="14">14</option><option value="16">16</option><option value="18">18</option><option value="20">20</option><option value="22">22</option><option value="24">24</option><option value="26">26</option><option value="28">28</option><option value="30">30</option>
  </select></th>
  <th align="left"><input type="radio" name="incr_type" value="increment" checked="checked">&nbsp;pixels/frame <input type="radio" name="incr_type" value="frames">&nbsp;frames/image</th>
  <th align="right">Aspect Ratio:&nbsp;</th>
  <th align="left"><input type="radio" name="aspect" value="4" checked="checked">4:3&nbsp;<input type="radio" name="aspect" value="16">16:9</th>
</tr>
<tr>
  <th><input type="checkbox" name="resample" checked="checked">&nbsp;Resample</th>
  <th>Width:&nbsp;<select name="sample_width" size="1"><option value="480">480</option> <option value="640" selected>640</option> <option value="768">768</option> <option value="1280">1280</option> </select></th>
  <th>Distort&nbsp;(perpendicular):&nbsp;<input type="text" size="2" name="rpfactor" value="0">%&nbsp;(+3D/-Fisheye)</th>
  <th><input type="checkbox" name="rptype">&nbsp;+Parallel</th>
</tr>
<tr>
  <th>Sharpen:&nbsp;<select name="sharp" size="1"><option value="0" selected>0</option><option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option></select></th>
  <th><input type="submit" name="submit" value="Go!"></th>
  <th><input type="submit" name="submit" value="Test Sharpen"></th>
  <th><input type="submit" name="submit" value="Test Distortion"></th>
</tr>
</table>
</form>
</center>

Now onto the PHP proper. For reasons I do not fathom, PHP will require the time zone to be set and you will also have to set the time limit independently of any time limit set in php.ini. When that's done, the chosen image is loaded as a new Imagick object and some data (the image height and width) are read out. Finally we will set a destination file, assuming that we won't be overwriting an existing file (although be careful with this).

<?php
date_default_timezone_set ("Europe/Berlin");
if (isset ($submit))
{
    set_time_limit (18000);
    $imsrc = new Imagick ($grfile);
    $srcwidth = $imsrc->getImageWidth ();
    $srcheight = $imsrc->getImageHeight ();
    $file_dest = substr ($grfile, 0, strpos ($grfile, ".")) . ".tif";
#   More follows ...
}
?>

Next we'll want some data about the aspect ratio ($aspectratio, calculated from the integers passed from the form), the initial destination width ($destwidth, calculated from the source height3presuming a landscape orientation3 and the aspect ratio), how many files will be produced ($imax), and the pixel increment (if this has been set indirectly via frames/image rather than pixels/frame) $incr.

switch ($aspect)
{
    case "4": $aspectratio = 4 / 3; break;
    case "16": $aspectratio = 16 / 9; break;
}
$destwidth = floor ($srcheight * $aspectratio);
if ($incr_type == "frames") $incr = $srcwidth / $incr;
$imax = ceil (($srcwidth - $destwidth) / $incr);

Since we need all of this information for testing the sharpening and distortion factors as well as for processing the image, all of this data is calculated before we reach a decision point which is implemented with switch ():

switch ($submit)
{
    case "Go!":
    [...]
    break;
    case "Test sharpen":
    break;
    case "Test distortion":
    break;
}

Let's now have a closer look at what will be happening within the branch represented by [...].
First, we'll need to calculate some variables and create a new instance of Imagick for our destination images.
Then, we'll loop $imax times and on every loop we need to:

  1. Cut our a portion of our image to a temporary image;
  2. Reduce it in size;
  3. Add it to the destination image; and
  4. Free the resources associated with the temporary image.
In particular we need the variables $geometry, $page and $sample_height for all instances of the loop. Within the loop we will also need a variable $crop. Thus, before the loop begins:

$geometry = $destwidth . "x$srcheight";
$page = $geometry . "+0+0";
$sample_height = round ($sample_width / $aspectratio);

Then the loop itself:

for ($i = 0; $i < $imax; $i++)
{
    set_time_limit (200);
    $start = round ($i * $incr);
    $crop = "$geometry+$start+0";
    $imtemp = $imsrc->transformImage ($crop, $geometry);
    $imtemp->setImageFormat ("png");
    $imtemp->resetImagePage ($page);
    if (isset ($resample)) $imtemp->resizeImage ($sample_width, $sample_height, imagick::FILTER_LANCZOS, 1);
    $imdest->addImage ($imtemp);
    $imtemp->destroy ();
}

Before the action starts, an additional variable $crop is assembled. Note that $page and $crop are strings of the format: X (width) x Y (height) + initial X + initial Y and $geometry has the format X (width) x Y (height) (all without any spaces).
The four steps in the loop are implemented as follows:

  1. A new instance of Imagick is produced by the action of imageTransform () on $imsrc by cutting out an excerpt of width X, height Y, starting at initial X (= $start) and initial Y (= 0) (all taken together, the information in the string $crop) and transferring it to a new image $imtemp with the dimensions $geometry. The image format of $imtemp is set at png (although this step could conceivably be omitted.) Finally the image is repaged, so that the upper lefthand corner of the image has the coordinates (0, 0). Again, this step could be left out, but it will prove to be of crucial importance when distorting the image;
  2. The image is resized by resizeImage () operating on $imtemp to produce an image with the dimensions $sample_width by $sample_height, using the Lanczos filter and a sharpening factor of 1;
  3. $imtemp is added to $imdest by the action of addImage (); and
  4. Destroy () frees up the resources allocated to $imtemp.

After the loop has been completed, it is time to:

  1. Save the multipage file to its destination; and
  2. Free up the resources allocated to $imdest and $imsrc.

$imdest->writeImages ($file_dest, true);
$imdest->destroy ();
$imsrc->destroy ();

In this section:

  1. WriteImages () writes the image(s) to the destination file. As we have chosen the tif format, which supports multipage images, all the images will be written to one file if the second argument is true, otherwise multiple indexed images will be produced. As only tif and gif support multipage images, setting the second argument to true only makes sense with these file formats, otherwise only one image will be saved to the destination file;
  2. Destroy () then frees the resources allocated to $imdest and $imsrc.

We are left with two tasks: distorting the picture and sharpening it. the logical sequence here would be to distort the picture in its original size, resize it and sharpen it. In fact Imagick's imageResize () does the resizing and sharpening in one go. It is also possible to use convolution matrices separately to do the sharpening, but in any case it would be the last thing that is done to the image. But before we consider the depths of distortion, let's get sharpening out of the way first.

We've seen that imageResize () has a fourth argument, the sharpening factor. At a value of 1, this does nothing, more than 1 blurs the image, less than 1 sharpens it. The form allows a value of 0-5 to be set for the sharpening factor; this will have to be mapped to 1 or less in several steps in the preliminary section of the "Go!" block. I do this by assuming an initial value of 1, with values decreasing from 0.9 in steps of 0.2 for the higher sharpening values:

if ($sharp != 0) $sharp = 1.1 - 0.2 * $sharp; else $sharp = 1;

And then including this value when resizing:

if (isset ($resample)) $imtemp->resizeImage ($sample_width, $sample_height, imagick::FILTER_LANCZOS, $sharp);

However, before committing computing time to recalculating all of those image it might be an idea to test the sharpening factors to see which desirable results. At this point we are dealing with the "Test Sharpen" block of instructions, but we will have a look at an alternative way of using Imagick to take a section of a picture, resize and sharpen it. In this method:

  1. The image is cloned;
  2. The clone is then cropped and repaged;
  3. Then it is resized and saved to a series of files; before being
  4. displayed and then removed from memory.

echo "<table>$b";
for ($i = 0; $i < 6; $i++)
{
    set_time_limit (60);
    if ($i == 0) $sharp = 1; else $sharp = 1.1 - 0.2 * $i;
    echo "<tr><th>Sharpening factor: $i</th></tr>\r\n";
    $imdest = $imsrc->clone ();
    $imdest->setImageFormat ("png");
    $imdest->cropImage ($destwidth, $srcheight, 0, 0);
    $page = $destwidth . "x" . $srcheight . "+0+0";
    $imdest->resetImagePage ($page);
    $imdest->resizeImage ($sample_width, round ($sample_width / $aspectratio), imagick::FILTER_LANCZOS, $sharp);
    $imdest->writeImage ("Test$i.png");
    echo "<tr><td><img src=\"Test$i.png\"</td></tr>$b";
    $imdest->destroy ();
}
echo "</table>$b<hr>$b";

Our steps are executed as follows:

  1. A copy of the mother image, $imdest is the result of clone () acting on $imsrc;
  2. The format of $imdest is set as png by setImageFormat (graphic format: string), cropped to the desired size by cropImage (X width: integer, Y height: integer, start X: integer, start Y: integer), and then repaged with resetImagePage (geometry: string);
  3. It is then resized with imageResize () with varying degrees of sharpening. WriteImage (filename: string) writes the file which is then displayed in a table, before resources are freed with destroy ().

Probably selecting a sharpening value of 1 or 2 on the form (corresponding to sharpening factors of 0.9 - 0.7) will give satisfactory results for most images.

Imagick uses the function distortImage (distortion method: constant, distortion values: array, bestfit: boolean). We will be using the BARREL distortion constant (imagick::DISTORTION_BARREL). Now comes the hard part: What are the distortion values? The available usage examples at imagemagick.org say that barrel distortion can take an array of 3, 4, 6, 8 or 10 values (!?!). Do not despair. The whales have survived.

The 4-value case supplies the coefficients A, B, C and D of the Dersch equation (Rsrc = Ar4 + Br3 + Cr2 + Dr) for stretching in one dimension; the 8-value case for stretching in two dimensions. The 6- and 10-value cases supply the X, Y coordinates of the centre of distortion; if not given, the centre of the image is taken as the centre of distortion.

Be that as it may, setting to A and B values to anything other than 0 results in rather bizarre distortions; and distorting the image just the right amount to fit it into the dimensions without any blank areas requires a special relationship between the C and D values. After some experimenting the following appears to work just fine (this code is placed above the decision tree). Remember that the form asks for a percentage value for the distortion. This will have to be multiplied by a factor to allow it to be used in the Dersch equation. After that C and D values for both X and Y dimensions are calculated so that none of the image is pushed so far into the boundaries that corners or midlines have missing spaces.

if ($rpfactor != 0)
{
    $cy = -$rpfactor * 0.02;
    if (isset ($rptype) and $rptype == "on") $cx = -$rpfactor * 0.02; else $cx = 0.0;
    if ($rpfactor < 0)
    {
        $dx = 1 - $cx * ($aspectratio + 1 / 3);
        $dy = 1 - $cy * ($aspectratio + 1 / 3);
    } else
    {
        $dx = 1 - $cx * $aspectratio;
        $dy = 1 - $cy;
    }
    $distort = array (0.0, 0.0, $cx, $dx, 0.0, 0.0, $cy, $dy);
}

The real action of distortion will come in the "Go!" block, after repaging the image and before resizing it.

if ($rpfactor != 0) $imtemp->distortImage (imagick::DISTORTION_BARREL, $distort, true);

Again, it would be a nice idea to test the distortions before beginning on the long haul and we'll also use a method to reveal whether part of our image has not made it through the distortion.

Now we'll move into the ":Test Distortion" block and do pretty much the same as when we were testing the sharpening factor: produce a series of distortion arrays and transform the image accordingly.

echo "<table>$b";
for ($i = -3; $i < 4; $i++)
{
    set_time_limit (60);
    echo "<tr><th>Distortion factor: " . (2 * $i) . "%</th></tr>\r\n";
    $cx = $cy = -$i * 0.04;
    if ($i < 0)
    {
        $dx = 1 - $cx * ($aspectratio + 1 / 3);
        $dy = 1 - $cy * ($aspectratio + 1 / 3);
    } else
    {
        $dx = 1 - $cx * $aspectratio;
        $dy = 1 - $cy;
    }
    $distort = array (0.0, 0.0, $cx, $dx, 0.0, 0.0, $cy, $dy);
    $imdest = $imsrc->clone ();
    $imdest->setImageFormat ("png");
    $imdest->cropImage ($destwidth, $srcheight, 0, 0);
    $page = $destwidth . "x" . $srcheight . "+0+0";
    $imdest->resetImagePage ($page);
    $imdest->setImageVirtualPixelMethod(Imagick::VIRTUALPIXELMETHOD_GRAY);
    $imdest->distortImage (imagick::DISTORTION_BARREL, $distort, true);
    $imdest->resizeImage ($sample_width, round ($sample_width / $aspectratio), imagick::FILTER_LANCZOS, 0.7);
    $imdest->writeImage ("Test" . ($i + 3) . ".png");
    echo "<tr><td><img src=\"Test" . ($i + 3) . ".png\"</td></tr>$b";
    $imdest->destroy ();
}
echo "</table>$b<hr>$b";

One problem that may turn up when you experiment with the values in the distortion array is that the image is so distorted, that parts of the canvas no longer contain any data. The way Imagick deals with this is to fill in these empty pixels with virtual pixels according to a predetermined method. This is why I have included a call to the function setImageVirtualPixelMethod (method: constant) before the distortion, so that such areas will be easily visible as grey. There are also constants for white and black, and the default is to extend the edge pixel of the distorted image to the edge of the canvas.

It is quite conceivable to program PHP to write a batch file to run ImageMagick® directly from the command line. The disadvantages of this approach would seem to be 1) a batch file would have to load the original graphic repeatedly, whereas within PHP the original only has to be loaded once; and 2) you would need to be very adept at knowing and applying the IM commandos. I personally don't see any particular advantage to offset these.

A complete script that works as intended can be found at this location in the current subdirectory (i.e http://www.skeptic.de/PHP_Tutorials/): download01 dot php question_mark file underscore name equals Imagick with the bold words replaced by the corresponding symbols and no spaces. This can also be used as a starting point for the exercises.

Task: Panning a vertical (portrait orientation) image requires moving along the Y axis, rather than the X. Such an orientation would be easily recognisable because its Y dimension would be larger than the height of an extracted portion with a set aspect ratio. Using a variable $vertical as a flag, work through the script and add code to cope with vertical panoramas.

Solution tip:The image distortions perpendicular and parallel to the pan movement are now also on swapped axes.

Task: Most full panoramas are exactly 360° wide. A continuous panorama sequence that begins and ends with the same frame extends beyond 360°, with the final frames containing part of the right hand side of the panorama and part of the left hand side. Also the number of frames will be higher. Write code to account for this.

Solution tip: Imagick has the function compositeImage (image to be added: Imagick, composition method: constant, place at X: integer, place at Y: integer) which will join two images. But before calling it you will have to restrict the extent of the first (partial) image by calling setImageExtent (width: integer, height: integer).

Comments and Questions

Can be directed to me here. See what the reassembled panorama images look like in the YouTube video (reassembled by MakeAVI).
Thanks and have fun!

Lee Traynor


Next tutorial: Making an Interactive Tour from a KML File