6.2 Finalizing the Image Class


Instead of thinking of apImage<> as an object, you should think of it in terms of an API or toolbox. There are lots of possible image processing functions, and very few are part of the apImage<> class definition itself. Most are global functions, making it very easy to add new functionality without weighing down the object itself.

A common mistake when designing C++ classes is to make everything a member function of the underlying class. As disparate functionality is added to an object, understanding what the object does become more difficult. A simple class can grow to over 100 member functions with little effort if you add new functionality at every opportunity. If these methods consist of 20 to 30 different pieces of functionality with similar, but different, interfaces, the object is still of manageable size. The std::string class is a good example of this type of object. There are a lot of member functions, but the functionality can be divided into one of about 25 families of functions. Our image class, on the other hand, can have a large number of different routines, so instead most are implemented as functions.

apImageStorage<> contains most of the actual functionality exposed by apImage<> , but does not handle some of the special conditions we expect to encounter. For example, apImageStorage<> knows very little about image windows , other than how to create one. The image processing functions that we write will use image windows to determine which pixels to process. This will become especially important for image processing functions that take more than one image as an argument.

There are two consumers of our image class: imaging applications and application-specific frameworks:

  • For real applications, our API must be sufficiently broad so that images can be manipulated. This includes image filtering, transformation, and arithmetic operations.

  • For application-specific frameworks, apImage<> must be a generic collection of tools that can be extended to create a new API with a different set of capabilities.

Once we discuss the final design of apImage<> , we will apply that design to enhance the framework, so that it works with a number of image types that offer enhanced performance on some platforms.

6.2.1 Image Object

Our final image object, apImage<> , is rather modest. Besides exposing the features of the underlying storage object, only the most basic image processing methods are included. To keep this object generic, we modified the design from Prototype 3 to remove one template parameter (Section 3.2.6 on page 60), and we replaced it with another template parameter, as shown in Figure 6.5.

Figure 6.5. Evolution of apImage Design

graphics/06fig05.gif

In the final design, we kept the class T template parameter, but moved the class E template parameter, which is the internal pixel type used during computations , to the global image functions. Then we added an additional template parameter, class S , to make the declaration, as follows :

 template<class T, class S=apImageStorage<T> > class apImage; 

class T

The pixel type. This parameter has the same meaning as in earlier classes.

class S

The underlying image storage object. The default parameter, apImageStorage<T> , which uses apAlloc<> to allocate memory, is applicable for most applications. If you want to use another object to handle memory allocation, then specify it here. This object also contains the iterators necessary to process the image on a row or pixel basis.

Most applications can think of the image class as apImage<T> , rather than apImage<T,S> . This is certainly desirable, because everything is more readable and it makes the class look less onerous. When using apImage<> to write new image processing functions, you will still need to deal with both parameters, but this is usually nothing more than a bookkeeping task, and the compiler always reminds you if you made a mistake.

The apImage<T,S> class is shown here.

 template<class T, class S=apImageStorage<T> > class apImage { public:   typedef typename S::row_iterator row_iterator;   typedef typename S::iterator iterator;   static apImage sNull;   apImage () {}   apImage (const apRect& boundary,            apRectImageStorage::eAlignment align =            apRectImageStorage::eNoAlign)     : storage_ (boundary, align)   {}   apImage (S storage) : storage_ (storage) {}   virtual ~apImage () {}   apImage            (const apImage& src);   apImage& operator= (const apImage& src);   bool lockImage   () const { return storage_.lock ();}   bool unlockImage () const { return storage_.unlock ();}   bool lockState   () const { return storage_.lockState ();}   bool unlockState () const { return storage_.unlockState ();}   bool isNull        () const { return storage_.isNull();}   int  ref           () const { return storage_.ref();}   int  xoffset       () const { return storage_.xoffset();}   int  yoffset       () const { return storage_.yoffset();}   unsigned int bytesPerPixel () const     { return storage_.bytesPerPixel();}   unsigned int rowSpacing    () const     { return storage_.rowSpacing();}   apRectImageStorage::eAlignment alignment () const   { return storage_.alignment();}   S storage () { return storage_;}   const apRect& boundary () const { return storage_.boundary();}   int           x0       () const { return storage_.x0();}   int           y0       () const { return storage_.y0();}   int           x1       () const { return storage_.x1();}   int           y1       () const { return storage_.y1();}   unsigned int  width    () const { return storage_.width();}   unsigned int  height   () const { return storage_.height();}   const T* base       ()       const;   const T* rowAddress (int y) const ;   T*       rowAddress (int y);   const T& getPixel (int x, int y) const;   const T& getPixel (const apPoint& point) const;   void     setPixel (int x, int y, const T& pixel);   void     setPixel (const apPoint& point, const T& pixel);   void setRow (int y, const T& pixel);   void setCol (int x, const T& pixel);   // iterators   row_iterator       row_begin () { return storage_.row_begin();}   const row_iterator row_begin () const                                   { return storage_.row_begin();}   row_iterator       row_end   () { return storage_.row_end();}   const row_iterator row_end   () const                                   { return storage_.row_end();}   iterator       begin ()       { return storage_.begin();}   const iterator begin () const { return storage_.begin();}   iterator       end   ()       { return storage_.end();}   const iterator end   () const { return storage_.end();}   // Image memory operations   bool window (const apRect& window)   { return storage_.window (window);}   void trim ();   // Duplicate the image data, if it is shared, to use the   // minimum amount of memory possible. Use this function to   // duplicate the underlying image data. Thread-safe.   apRectImageStorage::eAlignment bestAlignment () const;   // Return best alignment of this image. This is a measured   // quantity so it will work with all images.   apImage<T,S> align   (apRectImageStorage::eAlignment align =                         apRectImageStorage::eNoAlign);   // Return an image that has the specified alignment. This   // may return a new image   apImage<T,S> duplicate   (apRectImageStorage::eAlignment align =                             apRectImageStorage::eNoAlign) const;   // Duplicate the image data such that it has the specified   // alignment. The image data is always duplicated, unlike align()   // Image operations with constants. Thread-safe   void set (const T& value);   void add (const T& value);   void sub (const T& value);   void mul (const T& value);   void div (const T& value);   void scale (float scaling);   // Conditionals   bool isIdentical (const apImage& src) const;   // Checks for same boundary and identical image storage only   bool operator== (const apImage& src) const;   bool operator!= (const apImage& src) const; protected:   S       storage_;  // Image storage }; 

Over half of this class is nothing more than a wrapper around the storage object. Although we allow complete access to the storage object by means of the storage() method, we still try to make direct access to the individual members as easy as possible. The remaining methods belong to two categories: image operations and image storage operations.

graphics/dia.gif IMAGE OPERATIONS

Most image operations alter the image with a constant value. The set() , add() , sub() , mul() , and div() functions modify each pixel value with a constant value. scale() is similar to mul() , except that a floating point scaling parameter is specified. These functions are very intuitive to use:

 apImage<Pel8> image; image.set (0);    // Set each pixel to 0 image.add (1);    // Each pixel is now 1 image.mul (2);    // Each pixel is now 2 image.scale (.5); // Each pixel is now 1 

graphics/dia.gif IMAGE STORAGE OPERATIONS

Image storage operations manipulate how an image is stored in memory. We spent a lot of time in earlier sections describing how important image alignment is, especially when it comes to performance on many platforms. Many image processing routines allocate and return new images that may not have the desired alignment. Another need to adjust alignment arises when window() is used to return a window of an image. The alignment of the image window will depend on the size and location of the window itself. If you need to perform a number of operations, and do not need to modify the parent image data, you should work with an aligned image. align() returns an image that has the desired alignment for each row in the image. If the image already has this alignment, the original image is returned; otherwise , a new image is returned that contains the same pixel values as the original image.

 apRect boundary (0, 0, 16, 16); apImage<Pel8> image1 (boundary); image.set (0); apImage<Pel8> image2   = image1.align (apRectImageStorage::eDoubleWordAlign); 

In this example, it isn't clear if image2 refers to the same memory as image1 , because image1 was created with no specified image alignment. If you need to know in advance whether an operation will create a new image, use the bestAlignment() method to measure the alignment of the image storage.

graphics/triangle.gif EXAMPLE

We now have enough functionality to write a simple application. Let's create an image and use an image window to demonstrate a useful feature of windows.

 int main() {   apRect boundary (0, 0, 16, 16);   apImage<Pel8> image1 (boundary);   image1.set (0);   apRect window (6, 6, 4, 4);   apImage<Pel8> image2 = image1;   image2.window (window);   image2.set (1);   apImage<Pel8>::row_iterator i;   for (i=image1.row_begin(); i != image1.row_end(); i++) {     Pel8* p = i->p;     for (unsigned int x=0; x<image1.width(); x++)       std::cout << static_cast<int>(*p++);     std::cout << std::endl;   }   return 0; } 

This application sets each pixel in a 16x16 image to . A 4x4 image window, image2 , is created, and these values are set to 1 . Finally, the pixels from the initial image are displayed. The following display demonstrates that no pixels were copied when the image window is created. image2 is pointing to the same pixels as those of image1 .

 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000001111000000 0000001111000000 0000001111000000 0000001111000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 

The static_cast<> reference in the example is just a simple cast, because we want to display the image pixel as a number and not as a character. Most stream packages will display a Pel8 (i.e., unsigned char ) as a character.

graphics/dia.gif ADD()

Let's look at the implementation of add() . It bears a close resemblance to the code that displayed the image to the console in the previous example.

 template<class T, class S> void apImage<T,S>::add (const T& value) {   apImageLocker<T,S> locking (*this); // Lock image   typename apImage<T,S>::row_iterator i;   unsigned int w = width ();   T* p;   for (i=row_begin(); i != row_end(); i++) {     p = i->p;     for (unsigned int x=0; x<w; x++)       *p++ += value;   } } 

There are a few differences between add() and the display code:

  • add() locks and unlocks the image to prevent other threads from accessing the image data or changing the state of the image until the function is finished.

  • add() includes performance enhancements. It defines a width variable, w , and a pointer variable p , outside of the loop to ensure that the compiler generates efficient code. Assigning width() to variable w once prevents width() from running each iteration. Moving p outside the loop ensures that the compiler does not attempt to reallocate the variable during each iteration. Our example code did not worry about this.



Applied C++
Applied C++: Practical Techniques for Building Better Software
ISBN: 0321108949
EAN: 2147483647
Year: 2003
Pages: 59

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net