Lee Traynor's PHP Tutorials - Panorama Slicer

Contents

 Back to IndexNext Tutorial

Mackinnon Pass, Fiordland, New Zealand: Mar 3, 2009

The ubiquity of digital cameras and the ease of data processing have led to the development of a number of programs for stitching together individual images to panoramas (freeware e.g. hugin). Also, a number of panorama viewers based on Java are freely available as share- or freeware (shareware: e.g. Panorado). However, these offer only limited utility (i.e. in popup windows on websites) and a project which would incorporate individual frames from a panorama into a pseudo movie cannot rely on these applets. One such viewer, PTViewer has been combined with an applet for reading out the individual images from a running panorama (PTImages). The idea is that a panorama being played by PTViewer is read off by PTImages in "real time". Horse feathers!

A number of enquiries can be found on the net asking for help in extracting frames from extensive panorama images. Obviously this could be done by hand by sequentially cropping the original image. However, with hundreds or tens of thousands of images to be created, this is a valid candidate for automation.

This tutorial demonstrates how individual frames can be read out from panoramic images and saved as an image sequence using a some native functions of PHP. The images can then be loaded into a video processing program of your choice to create video in standard formats, where e.g sound and other effects can be added.

You have to have access to a web server with PHP installed in the latest version (5+). You must be familiar with PHP scripting. If you are planning to dissect large panoramic images, know how to extend both the memory and time limits on PHP execution.

PHP provides two functions which will open a graphics file and directly copy a selection from the file and write it to an output file. These are: imagecreatefromjpeg () (also imagecreatefromgif () and imagecreatefrompng ()) and imagecopy ().

Assumption: A panoramic image with a height of 100 pixels is to be broken up into a series of images from left to right, each with the same width.

We will proceed in these steps:

  1. Load the source file;
  2. Create an empty destination file with the desired dimensions;
  3. Copy a selection of the source file to the destination file;
  4. Save the destination file;
  5. Remove the source and destination files from memory.
This is the code:

<?php
$im_src = imagecreatefromjpeg (source file name);
$im_dest = imagecreatetruecolor (133, 100);
imagecopy ($im_dest, $im_src, 0, 0, 0, 0, 133, 100);
imagejpeg ($im_dest, destination file name);
imagedestroy ($im_dest);
imagedestroy ($im_src);
?>

First, the source image file is loaded into memory with imagecreatefromjpeg () and memory space for a destination image file is provided by imagecreatetruecolor () and these are given the image identifiers $im_src and $im_dest respectively. The height of both images should be the same, so that the two arguments for imagecreatetruecolor () will depend on the height of the source image and the aspect ratio of the frame you wish to produce.

The function imagecopy () requires 8 variables: the 2 image identifiers (not their files names!) for destination and source in that order; and three pairs of integers:

  1. x and y coordinates of the destination point (upper left hand corner): the first 0, 0
  2. x and y coordinates of the source point (upper left hand corner): the second 0, 0; and
  3. source width and height: 133, 100.

Finally the images are removed from memory with imagedestroy ().

This results in a single image, the first, left hand frame of our panoramic image.

Now we can proceed throughout the whole panorama, creating a numbered series of files which represent a series of frames, each one pixel-column further advanced to the right until the right hand edge of the panorama is reached.

    Before entering the loop:
  1. The source file is loaded;
  2. The width and the height of the source file are read;
  3. The width of the destination image is calculated from the height of the source image and the aspect ratio;
  4. The maximum number of iterations is calculated from the width of the source image and the width of the destination image; and
    Inside the loop:
  1. Memory for the destination image is created;
  2. A partial image is copied from the source image, starting each time from a point which is progressively being shifted to the right;
  3. The destination image is saved to a file with an incremental file name; and
  4. The destination image is removed from memory.
    After completion of the loop:
  1. The source image is removed from memory.
This is the code:

<?php
$im_src = imagecreatefromjpeg (source file name);
$src_width = imagesx ($im_src);
$src_height = imagesy ($im_src);
$dest_width = floor ($src_height * 4 / 3);
$i_max = $src_width - $dest_width;
for ($i = 0; $i < $i_max; $i++)
{
    $im_dest = imagecreatetruecolor ($dest_width, $src_height);
    imagecopy ($im_dest, $im_src, 0, 0, $i, 0, $dest_width, $src_height);
    $file_dest = sprintf ("%04u", $i) . ".jpg";
    imagejpeg ($im_dest, $file_dest);
    imagedestroy ($im_dest);
}
imagedestroy ($im_src);
?>

We now have a minor degree of flexibility: The height of the source file can be read by imagesx () and this can be used to determine the height of the destination image and, by multiplying it with the aspect ratio (here 4:3), its width, which is at the same time the width of the selection we wish to copy. The number of iterations is obtained by subtracting the frame width from the width of the source image.
Inside the loop a serial number corresponding to the x coordinate of the copy being saved is generated. This serial number is padded with a series of zeroes on the left by sprintf () to give a four-digit number and appended with the suffix ".jpg".

Now that we have automated the most important process to extract frames from out panorama, let's see what else can be done to optimise performance.

The first thing is that we might want to select a file from a list, rather than program the name each time. Secondly, there are different types of panorama (full and partial) which require different processing. Thirdly, we might want to try different increment values, or a different method of incrementing an image. Fourthly, we want to be able to choose the aspect ratio, rather than have it fixed at 4:3. Finally, when processing different files it would be an advantage to store the results each in a separate subdirectory to tidy things up a little. This is best done with a form.

<html>
<head>
</head>
<body>

<?php
echo "<form method=\"post\" action=\"{$_SERVER["PHP_SELF"]}\">\r\n";
?>
<table>
<tr>
    <th align="right">Graphic file: </th>
    <td colspan="5"><input type="file" name="grfile" size="60"></td>
</tr>
<tr>
    <th colspan="3"><input type="radio" name="scope" value="full"> Full panorama (360°)</th>
    <th colspan="3"><input type="radio" name="scope" value="partial" checked="checked"> Partial panorama (<360°)</th>
</tr>
<tr>
    <th align="right">Increment: </th>
    <td><input type="text" name="incr" value="1" size="2"></td>
    <th><input type="radio" name="incr_type" value="increment" checked="checked"> pixels/frame
    <input type="radio" name="incr_type" value="frames"> frames/image</th>
    <th align="right">Aspect Ratio: </th>
    <th><input type="radio" name="aspect" value="4" checked="checked">4:3</th>
    <th><input type="radio" name="aspect" value="16">16:9</th>
</tr>
<tr>
    <th align="right">Destination: </th>
    <td colspan="5"><input type="text" name="subdir" size="24">
</tr>
<tr>
    <th colspan="6"><input type="submit" name="submit" value="Go!"></th>
</tr>
</table>
</form>

<!-- Insert php to process file here (see below)-->

</body>
</html>

This form will call itself when submitted, handing over the input variables grfile, scope, incr, incr_type, aspect, subdir and submit. The following PHP code which follows the table extracts these variables from the global variable $_POST and when the variable $submit is set and has the value "Go!" executes the loop.

<?php
extract ($_POST);
if (isset ($submit) and $submit == "Go!")
{
// Our loop
}
?>

It is important to note that this type of form hands over only the file name of the graphics file in the variable $_POST["grfile"] (which extracted gives us the variable $grfile) and not the path to the file. This means that the graphics file must be in the same directory as the processing file. In order to load the file itself, see the exercise 7.4. Loading a File, Not Just its Name.

We will now have a look at what can be done with the variables $scope, $incr, $incr_type, $aspect, and $subdir.

The processing of a partial panorama (< 360°) ends on reaching the right most pixel column. However an image containing a full panorama has to be processed beyond its right border until an image is produced that is one pixel to the left of the initial frame extracted. This has two consequences:

This is achieved as follows:

//Before the loop:
if ($scope == "partial") $i_max = $src_width - $dest_width;
else $i_max = $src_width;

//Inside the loop:
if ($i + $dest_width < $src_width) imagecopy ($im_dest, $im_src, 0, 0, $i, 0, $dest_width, $src_height);
else
{
    imagecopy ($im_dest, $im_src, 0, 0, $i, 0, $src_width - $i, $src_height);
    imagecopy ($im_dest, $im_src, $src_width - $i, 0, 0, 0, $i + $dest_width - $src_width, $src_height);
}

The expression ($i + $destwidth < $srcwidth) is always true for partial panoramas and true for full panoramas until the right edge is reached. When it is no longer true for full panoramas, two copying statements are executed.

Until now we have plodded on through our image one pixel at a time. Our form allows us to specify the number of pixels per step, or, alternatively, to determine the number of images we want to produce.

First off, we could simply input the desired pixel increment. We might want to make sure that this number will be interpreted correctly by rounding any value that is input (the image functions generally take integers and not floats), we will need to reduce the number of iterations by dividing our image width by the increment, and finally, define a new variable $start which is the product of our increment and $i, the step:

//Before the loop:
$incr = round ($incr);
if ($scope == "partial") $i_max = ceil ($src_width / $incr) - $dest_width;
else $_imax = ceil ($src_width / $incr);

//Inside the loop:
$start = $i * $incr;

And then replace all instances of $i within the loop with $start.

Alternatively we could determine the number of frames we wish to produce and have the program calculate the increment and the number of steps. In this case we will also have to round the value of $start so that the code looks like this:

//Before the loop:
$incr = round ($incr);
if ($incr_type == "frames" and $scope == "partial") $incr = ($src_width - $dest_width) / $incr;
if ($incr_type == "frames" and $scope == "full") $incr = $src_width / $incr;
if ($scope == "partial") $i_max = ceil ($src_width - $dest_width) / $incr;
else $_imax = ceil ($src_width / $incr);

//Inside the loop:
$start = round ($i * $incr);

A common viewing format is 4:3; newer digital televisions show images with an aspect ration of 16:9. A choice between these two aspect ratios can be accommodated:

// Before the loop:
switch ($aspect)
{
    case "4": $aspect_ratio = 4 / 3; break;
    case "16": $aspect_ratio = 16 / 9; break;
}
$dest_width = floor ($src_height * $aspect_ratio);

Here we have to check whether a subdirectory has been requested, whether a directory of the desired name already exists and, if not, create it. When saving the files, this directory name has to be prepended to the file name:

//Before the loop:
if ($subdir != "" and !is_dir ($subdir)) mkdir ($subdir);

//Inside the loop:
$file_dest = $subdir . "\\" . sprintf ("%04u", $i) . ".jpg";

PHP can deal with a small number of graphics formats besides JPEG. Two commonly used formats supported are PNG and GIF. These formats can be readily recognised from the file ending and used to call up suitable PHP functions. We will assume that the input format will also determine the output format and this involves a minor change to the procedure of the naming of the destination files.

A word of warning: the WBMP format supported by PHP is not Windows Bitmap, but rather WAP Bitmap.

//Before the loop:
$image_type = strtolower (substr ($grfile, strrpos ($grfile, ".") + 1));
switch ($image_type)
{
    case "jpg": $im_src = imagecreatefromjpeg ($grfile); break;
    case "gif": $im_src = imagecreatefromgif ($grfile); break;
    case "png": $im_src = imagecreatefrompng ($grfile); break;
}

//Inside the loop, after the image has been copied:
$file_dest = $subdir . "\\" . sprintf ("%04u", $n) . "." . $image_type;
switch ($image_type)
{
    case "jpg": imagejpeg ($im_dest, $file_dest, quality:0-100); break;
    case "gif": imagegif ($im_dest, $file_dest); break;
    case "png": imagepng ($im_dest, $file_dest, compression:0-9); break;
}

Assembling all of the above in logical order gives the following code:

<?php
extract ($_POST);
if (isset ($submit) and $submit == "Go!")
{
  $incr = round ($incr);
  $image_type = strtolower (substr ($grfile, strrpos ($grfile, ".") + 1));
  switch ($image_type)
  {
    case "jpg": $im_src = imagecreatefromjpeg ($grfile); break;
    case "gif": $im_src = imagecreatefromgif ($grfile); break;
    case "png": $im_src = imagecreatefrompng ($grfile); break;
  }
  $src_width = imagesx ($im_src);
  $src_height = imagesy ($im_src);
  switch ($aspect)
  {
    case "4": $aspect_ratio = 4 / 3; break;
    case "16": $aspect_ratio = 16 / 9; break;
  }
  $dest_width = floor ($src_height * $aspect_ratio);
  if ($incr_type == "frames" and $scope == "partial") $incr = ($src_width - $dest_width) / $incr;
  if ($incr_type == "frames" and $scope == "full") $incr = $src_width / $incr;
  if ($scope == "partial") $i_max = ceil ($src_width - $dest_width) / $incr;
  else $_imax = ceil ($src_width / $incr);
  if ($subdir != "" and !is_dir ($subdir)) mkdir ($subdir);
  for ($i = 0; $i < $i_max; $i++)
  {
    $start = $i * $incr;
    $im_dest = imagecreatetruecolor ($dest_width, $src_height);
    if ($start + $dest_width < $src_width) imagecopy ($im_dest, $im_src, 0, 0, $start, 0, $dest_width, $src_height);
    else
    {
      imagecopy ($im_dest, $im_src, 0, 0, $start, 0, $src_width - $start, $src_height);
      imagecopy ($im_dest, $im_src, $src_width - $start, 0, 0, 0, $start + $dest_width - $src_width, $src_height);
    }
    $file_dest = $subdir . "\\" . sprintf ("%04u", $i) . "." . $image_type;
    switch ($image_type)
    {
      case "jpg": imagejpeg ($im_dest, $file_dest, quality:0-100); break;
      case "gif": imagegif ($im_dest, $file_dest); break;
      case "png": imagepng ($im_dest, $file_dest, compression:0-9); break;
    }
    imagedestroy ($im_dest);
  }
  imagedestroy ($im_src);
}
?>

In order for this code to run the lines imagejpeg () and imagepng () must contain actual numbers instead of "quality" or "compression". Complete code for a functioning and tested web page can be viewed here.

Task: When downsizing some graphics, a column of black pixels is created on the right margin to compensate for rounding. Although the width of this black line cannot be read out of the file, allow the user to experiment by ignoring the n last columns of the image. Note that for partial panoramas, the same effect can be achieved by discarding the final images, but for full panoramas the effect has to be dealt with.

Solution Tips: Have the user input a value for the number of pixels to be ignored. Treat the image as if it were so many pixels narrower.

Task: Some programs for creating video sequences from sets of graphics files can only sort the files in one direction. Allow the user to determine whether the images run from left to right (default) for right to left (reverse) when they are sorted in ascending order.

Solution Tips: Try naming the files in descending order.

Task: Allow the user to determine the file output format.

Solution Tips: Create a further radio button field for the graphics format as well as a default setting to write graphics of the same kind as the input.

Task: Have the form transmit the graphics file and not just its name.

Solution Tips: The form will have to include the declaration "enctype='multipart/form-data'" and the uploaded file can be found in the global variable $_FORM. Use var_dump () to find where the file is stored and what its name is.

A further problem with this type of action is that the file invariably has the ending ".tmp", so that the ending can no longer be used to determine the input file type. Either have the user input the file type, or read the file type out of the file header.

Task: Vertical panoramas can also be split into frames using the methods described, but will require different copying actions. Have the program recognise whether a file is a horizontal or vertical panorama and code for appropriate action.

Solution Tips: If the width divided by the aspect ratio is less than the height, then the panorama is vertical.

Task: Reproject single images to give a 3D effect. This means that the sides of the image are to be stretched beyond the top and bottom margins.

Solution Tips: Map a straight edge onto the segment of a circle corresponding to the field of view (about 60°). At first assume symmetry (horizon is in the middle of the picture) and divide the picture in different sections according to the amount of stretch. Use imagecopyresampled () or imagecopyresized () to stretch parts of the image and imagecopy () to copy them onto the destination image.

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: ImageMagick® Interpreter: InterDraw