Thursday 21 April 2011

NetPNM ... or "A complete image class in C++ in less than 70 lines"

I really love computer graphics and I like creating images using mathematical algorithms. Of course there are many programs out there that let you do that but sometimes you just want to write your own code. My preferred language for this is C++. But then the question arises how to write a picture to disk without linking to some complicated library.

The solution comes in the form of the NetPBM (or portable any map) format, probably the simplest image format out there. Here I will present a very simple but complete image class, including a struct to hold RGB colour triplets and a function for writing the image to a file.

All this will take less than 70 lines of code. In these 70 lines I will even get to write a main function that will fill an image with some colours and save it to disk.

First, let's include some headers

#include <boost/scoped_array.hpp>
#include <iostream>
#include <fstream>
#include <cmath>

I like using the boost smart pointers. This eliminates many destructors and rids me of the worry about memory management.

Next comes a struct to hold RGB colours. It comes with a default constructor and a convenience constructor that takes the three colour values. Colours are stored in 8 bit unsigned chars.

struct Colour
{
    Colour() {}
    Colour(unsigned char r_, unsigned char g_ , unsigned
char b_)
      : r(r_), g(g_), b(b_) {}
    unsigned char r, g, b;
};

Now comes the image class. Internally it holds a smart pointer to an array of colours and two ints specifying the width and the height of the image.

I like to use typedefs for the smart pointers to make it more clear what type data the array holds.

class Image
{
  private:
   typedef boost::scoped_array<Colour> pColour;
    int width, height;
    pColour data;

Next we have two constructors, one default constructor creating an empty image and one constructor creating an image of given size.

public:
    Image() : data(0), width(0), height(0) {}
    Image(int width_, int height_)
      : width(width_), height(height_), data(new 
Colour[width_*height_]) {}

We have a default constructor, so we also need a way to change the size of the image after construction. For this I will write a function called resize. Note that the boost::scoped_array takes care of deallocating the array should it already contain data.


void resize(int width_, int height_)
    {
      width = width_;
      height = height_;
      data.reset(new Colour[width*height]);
    }

We also need some way of accessing the image data. Being a good boy, I will write a const and a non-const accessor.

Colour& at(int x, int y) { return data[x + width*y]; }
    const Colour& at(int x, int y) const { return 
data[x + width*y]; }

So much for the basic functionality. I promised a function that writes the image to a file. It turns out that the netpbm format in its ascii version is so simple that this function will only take about 10 lines of code. The file is completely in ASCII format, so it is readable with any text editor. It starts with the "magic number" P3 followed by any number of comment lines (starting with #). The next line contains the size of the image
As two numbers width and height. The last line of the header contains the bit depth of the image, normally 255. After that we just write all the R, G and B values in ASCII format separated by whitespace.

void write(std::ostream &os)
    {
      // writing the header
      os << "P3\n"
         << "# created using ctidbits by themathsgeek 
(themathsgeek.blogspot.com)\n"
         << width << " " << height << "\n"
         << "255\n";
      for (int i=0; i<width*height; ++i)
      {
        Colour c = data[i];
        os << int(c.r) << " " << int(c.g) << " " << 
int(c.b) << "\n";
      }
    }
   
};

That's it (the last bracket closes the class definition). Now we are ready to test our class.

int main()
{
  Image img(800,600);
  for (int x=0; x<800; ++x)
    for (int y=0; y<600; y++)
    {
      float r = cos(sqrt(x*x + y*y)/50.0);
      float g = sin(x/50.0);
      float b = cos(y/50.0);
      Colour c( (unsigned char)(255*(0.5*r + 0.5)),
                (unsigned char)(255*(0.5*g + 0.5)),
                (unsigned char)(255*(0.5*b + 0.5)));
      img.at(x,y) = c;
    }

  std::ofstream os("test.pnm");
  img.write(os);
  os.close();
}

The pnm format is a little bit bulky in terms of memory but you would normally convert it to some other format before storing or uploading anyway, so that doesn't really matter. On Linux you can use ImageMagick convert to quickly convert into other formats. On Windows you can use Photoshop or Paint Shop Pro.

No comments:

Post a Comment