Images in C++ with libpng and S/PHI/nX

Written by vasek on . Posted in All Posts, S/PHI/nX


Hi there! In this short text we will go through some code to create fractal images and then write them to image files. It is a small tutorial to S/PHI/nX and mainly its part SxAccelerate which is used as external library in this project (more about SxAccelerate and code examples). The “Sx” classes that are used are namely SxCLI, SxString, SxArray, SxNArray, SxException and SxComplex.

The first question is to choose some image file format to store our pixels.One is usually very happy just with PPM formats because they are so easy to create. (More details about PPM/PGM/PBM variants with examples). But we will go one step further and use PNG (Portable Network Graphics). It is practical. Just to mention few features: Lossless compression, palette/grayscale/truecolor color mode, up to 16-bit-per-sample support, transparency, and so on. Finally, the format has its MIME and we can put the images directly to web.

The writeImage() function

This function writes a file in PNG image file format in palette color mode from our pixels stored in image structure. We are using 2 dimensional SxNArray to represent bitmap raster and another 2 dimensional SxNArray for color palette. Palette color type is good choice when there are many pixels with very low number of unique colors. The RGB colors are stored separately in color palette and the pixels in the image contain only indices to this color palette. The type of indices and color values is uint8_t which is shorter to write than unsigned char. This gives us up to 256 different colors with each color value (red, green and blue) in range from 0 to 255. The most of this function is taken from example.c which is distributed together with libpng.

void writeImage (const SxString &filename,
                 const SxNArray<uint8_t,2> &image,
                 const SxNArray<uint8_t,2> &palette)
{
   // --- Debug assert macro to verify what is expected from parameters:
   //     palette is expected to have the first dimension of size 3.
   //     If the condition is false the program is aborted with debug
   //     informations and mainly a core file for inspection of the cause.
   SX_CHECK (palette.getDim (0) == 3);

   int width = image.getDim (0);
   int height = image.getDim (1);
   int depth = 8;

   FILE *fp = NULL;
   png_structp png = NULL;
   png_infop info = NULL;

   // --- open the file
   fp = fopen (filename.ascii (), "wb");
   if (!fp)  {
      // --- throw SxException with error message
      SX_THROW ("Can not open file '" + filename + "' for writing");
   }

   // --- create and initialize the png_struct
   png = png_create_write_struct (PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
   if (!png)  {
      fclose (fp);
      SX_THROW ("png_create_write_struct failed");
   }

   // --- allocate/initialize the image information data
   info = png_create_info_struct (png);
   if (!info)  {
      fclose (fp);
      png_destroy_write_struct (&png,  NULL);
      SX_THROW ("png_create_info_struct failed");
   }

   // --- set error handling
   if (setjmp(png_jmpbuf(png)))  {
      fclose (fp);
      png_destroy_write_struct (&png, &info);
      SX_THROW ("libpng error");
   }

   // --- set up the output control
   png_init_io (png, fp);

   // --- set the image information
   png_set_IHDR (png, info,
                 width, height, depth,
                 PNG_COLOR_TYPE_PALETTE,
                 PNG_INTERLACE_NONE,
                 PNG_COMPRESSION_TYPE_BASE,
                 PNG_FILTER_TYPE_BASE);

   // --- set the palette
   //     SxArray automatically releases the memory from the destructor.
   //     For example this also prevents possible memory leak from missing
   //     png_free after malloc as it could happen in the libpng exmple.c
   //     in a case of error handling libpng errors.
   int nColors = palette.getDim (1);
   SxArray<png_color> pngPalette(nColors);
   for (int i=0; i < nColors; i++)  {
      pngPalette(i).red   = palette(0, i);
      pngPalette(i).green = palette(1, i);
      pngPalette(i).blue  = palette(2, i);
   }
   png_set_PLTE (png, info, pngPalette.elements, pngPalette.getSize ());

   // --- write the file header information
   png_write_info (png, info);

   // --- setup start of each row in the image
   SxArray<png_bytep> rowTable(height);
   for (int y=0; y < height; y++)  {
      rowTable(y) = image.elements + (y * width);
   }

   // --- write the image data
   png_write_image (png, rowTable.elements);

   // --- finish writing the rest of the file
   png_write_end (png, info);

   // --- clean up after the write, and free any memory allocated
   png_destroy_write_struct (&png, &info);

   // --- close the file
   fclose (fp);
}

Generate some pixels


Now we need to generate some pixels to see if it is working. In the beginning we parse the program arguments. Then we create the image structure and color palette filled with few colors. Julia set is generated to fill all pixels in the image and in the end the image is written to a file.

Parsing program arguments with SxCLI is easy. Each argument has its description, optional default value and optional range of values which are allowed on the input. Please see the documentation for more features. SxCLI also automatically generates help (–help).

int main (int argc, char **argv)
{
   // --- parse input arguments
   SxCLI cli (argc, argv);
   SxString file = cli.option ("-o|--output", "string",
                               "Output filename for png image").toString ();
   int width     = cli.option ("-w|--width",  "int",
                               "Image width in pixels").toInt (1000, 1);
   int height    = cli.option ("-h|--height", "int",
                               "Image height in pixels").toInt (1000, 1);
   int max       = cli.option ("-max", "int",
                               "Maximal number of iterations").toInt (128, 1);
   double re     = cli.option ("-re", "double",
                               "Center point in z-plane re").toDouble (0.);
   double im     = cli.option ("-im", "double",
                               "Center point in z-plane im").toDouble (0.);
   double d      = cli.option ("-d", "double",
                               "Image width in complex plane").toDouble (3.24);
   double reC    = cli.option ("-cre", "double",
                               "Julia set constant real").toDouble (0.285);
   double imC    = cli.option ("-cim", "double",
                               "Julia set constant imaginary").toDouble (0.013);
   cli.finalize ();

In this part we create the color palette and the image structure.

   // --- color palette as 2D array
   SxNArray<uint8_t,2> palette;

   // --- three colors Red, Green, and Blue times 128 color indices
   palette.reformat (3, 128);

   // --- fill palette with some color gradients
   for (int i=0; i < 64; i++)  {
      palette(0, i) = 255 - (i * 4); // first dim at 0 is Red
      palette(1, i) = 255 - (i * 4); // first dim at 1 is Green
      palette(2, i) = 255 - (i * 4); // first dim at 2 is Blue

      palette(0, i + 64) = 0;
      palette(1, i + 64) = i * 2;
      palette(2, i + 64) = i * 4;
   }

   // --- 2D array of 8-bit pixels (pixels will be indices to a color palette)
   SxNArray<uint8_t,2> image;

   // --- create image of size width * height
   image.reformat (width, height);

Finally, the pixels! Let’s draw a Julia set with few parameters. In the algorithm we visit every pixel in the image and we use two complex numbers z and c. The value of z is changing from pixel to pixel relative to the points in complex plane and c is constant. The points z in complex plane which belong to Julia set does not go to infinity with iteration
\( z_{n+1} = {z_n^2 + c} \).
The colors are then taken from how many times we can iterate the equation for given z and c until we obtain large numbers. More about Julia set Fractal. There are also many different Julia sets depending on complex constant c.

   // --- draw Julia set with selected constant complex parameter c
   SxComplex<double> c(reC, imC);
   SxComplex<double> z;

   // --- image height in complex plane is given by ratio of image in pixels
   double ratio = double(width) / double(height);

   // --- set window of interest in complex plane
   SxComplex<double> px(d / width, d / ratio / height);
   SxComplex<double> zxy(re - d/2., im + (d/2. / ratio));

   // --- scale max iterations to the last index in color palette
   double step = double(palette.getDim(1) - 1) / max;

   // --- loop through all pixels in the image
   for (int y=0; y < height; y++)  {
      for (int x=0; x < width; x++)  {
         // --- initialize the complex number for this pixel and iterate
         z = zxy;
         zxy.re += px.re;
         int it = 0;
         while (it < max && z.absSqr () < 4.0)  {
            z = z*z + c;
            it++;
         }
         // --- write n iterations as pixel value to the image
         image(x, y) = uint8_t(it * step);
      }
      zxy.re = re - d/2.;
      zxy.im -= px.im;
   }

In the end the image is written to a file. Try catch block is used to handle possible exceptions from write permissions, invalid file names or an error in libpng. With SxEception we can decide what to do with the error.

   try {
      writeImage (file, image, palette);
   } catch (SxException e)  {
      cout << "ERROR: Can not write the image: " << e.getMessage () << endl;
   }

Building the project

To run this example we will continue as follows: the first step is to create some work directory for this small project, then we download all libraries and the source code example into this folder and install them there locally. This way we do not need root access and it is also easier to try, because if something goes wrong, we can just delete the work directory and start again. The final file structure in our work directory should look like this:

..
libpng
sphinx
julia.cpp

Installing SxAccelerate

First we install SxAccelerate. Please download sxaccelerate-1.0.1.tar.bz2 and sx-3rdparty-unix.tar.

Extract sxaccelerate-1.0.1.tar.bz2. This creates directory sphinx/sxaccelerate.

tar xvf sxaccelerate-1.0.1.tar.bz2

Extract sx-3rdparty-unix.tar in sphinx/sxaccelerate/3rd-party/packages/.

mv sx-3rdparty-unix.tar sphinx/sxaccelerate/3rd-party/packages/
cd sphinx/sxaccelerate/3rd-party/packages
tar xvf sx-3rdparty-unix.tar
cd ../../../..

Build SxAccelerate. If everything goes fine there will be sphinx/sxaccelerate/include and sphinx/sxaccelerate/lib directory with header files and libraries we can link to. There are also additional options and build instructions in a case of problems.

cd sphinx/sxaccelerate
./setup
./configure --prefix=`pwd`
make
make install

Installing libpng

On some systems, libpng is already installed but probably without the header files which we need in order to compile the example. Please download libpng-1.5.11.tar.gz from http://www.libpng.org/pub/png/libpng.html. Extract the downloaded file as libpng directory in your work directory (it is easier to have the directory without the version number). Again we use prefix with the current directory to install everything locally to libpng. If it is working there will be libpng/include and libpng/lib directory with header files and the library we can link to.

cd libpng
./configure --prefix=`pwd`
make
make install

Compile and run

Now we can download the complete example julia.cpp and compile it.

Because we have two external libraries, we need to provide the paths to include and lib directories so that gcc can find the header files and link to the libraries. Additionally we specify to which libraries we need to link. These are only lib png and sxutil. We also use O2 code optimization to make the program run quite faster.

g++ julia.cpp -Isphinx/sxaccelerate/include -Lsphinx/sxaccelerate/lib -Ilibpng/include -Llibpng/lib -lpng -lsxutil -O2 -o julia

If everything worked and the executable was generated, we can run the program with default arguments and only specify where to write the output to file (julia.png). (All possible arguments can be seen with –help).

./julia -o julia.png


The effect of increasing number of iterations which determine if a point in complex plane belongs to Julia set. The images for the animation were generated with this code.

Many thanks to all from PNG Group and libpng!

See you in the next tutorial about Audio signals.

Please follow the author on Twitter @vbubnik.

Tags: ,

© 2013 Gemmantics