6.4 Finalizing Interfaces to Third-Party Software


A decade ago, most software solutions were completely proprietary, in that all aspects of the application were developed in-house. There were plenty of software libraries available for purchase, but they were usually expensive or were considered inferior not because they didn't perform the intended function, but because they were not developed in-house. This "not invented here" syndrome created large in-house development groups that often duplicated functionality available elsewhere. The actual expense of developing these packages was enormous , especially considering that all maintenance was performed by the organization. Most of these issues vanished due to shrinking budgets and the advent of open -source software.

Modern software takes advantage of existing libraries to speed development and minimize the maintenance issues. It is now considered good design practice to design applications with interfaces that leverage existing code. We use the word delegates to refer to third-party software packages to which we delegate responsibility.

6.4.1 File Delegates

We have created a very flexible and extensible image processing framework. However, it still lacks the capability of interacting with the outside world. Unless our package can import and export images using many of the popular image formats, our framework is of little use.

There are many image storage formats, including JPEG, GIF, PNG, and TIFF. They all have their advantages and disadvantages, so supporting a single format is of limited use. We will design a simple interface so new file formats can be added with little difficulty. This design can be used by most image formats, although it may not take advantage of all the features of an individual format. Figure 6.17 provides an overview of the file delegate strategy.

Figure 6.17. File Delegate Interface Design

graphics/06fig17.gif

We create a base class, apImageIOBase , that defines the services we want and then we derive one class from apImageIOBase for every file format we want to support. apImageIOBase defines three essential methods , info () , read() , and write() , that check the file format and handle the actual reading and writing of each file format, respectively, as shown.

 class apImageIOBase { public:   virtual apDelegateInfo info (const std::string& filename) = 0;   template<class T, class S>   void read (const std::string& filename, apImage<T,S>& image);   template<class T, class S>   void write (const std::string& filename, apImage<T,S>& image,               const apDelegateParams& params = sNoParams)   // Default copy constructor and assignment operators ok. protected:   apImageIOBase ();   virtual ~apImageIOBase ();   static apDelegateParams sNoParams; }; 

graphics/dia.gif INFO()

info() determines whether a file is of the specified format and, if known, can provide the size. info() returns a structure, apDelegateInfo , which is shown here.

 struct apDelegateInfo {   bool         isDelegate;    // true if delegate knows this format   apRect       boundary;      // Image boundary (if known)   unsigned int bytesPerPixel; // Bytes per pixel (if known)   unsigned int planes;        // Number of planes (if known)   std::string  format;        // Additional image format information }; 

The optional field, format , is used to hold other information about a storage format. This is particularly important when an object, like apDelegateInfo , is shared among many objects. This prevents apDelegateInfo from containing a large number of fields, most of which are particular to a single image format. None of the image formats we support use the format member, but it is still good practice to include it for future use.

graphics/dia.gif READ()

The read() function reads an image into the specified apImage<> object. This is an excellent example of using templates inside a non-template object. The user can specify an image of any arbitrary type, and read() returns an image of that type. Most applications would use info() to determine the image type before using read() to read the image data from a file.

A very nice feature of read() is that it lets you pass it the name of a file containing a color image, and you receive a monochrome image in return. The color image is read, but converted to the monochrome image and returned.

graphics/dia.gif WRITE()

write() takes an apImage<> object and saves it in a particular image format. Optional parameters can be passed in an apDelegateParams structure:

 struct apDelegateParams {   float       quality;       // Quality parameter   std::string params;        // Other parameters   apDelegateParams (float qual=70.) : quality (qual) {} }; 

As with apDelegateInfo , we only place common functions directly in the structure. We added the quality parameter, because it is needed when JPEG images are stored, but other storage formats might use this parameter. We made quality a float to make it as useful as possible (although this is an integer value for JPEG compression). params is intended to hold format-specific parameters and prevents the structure from getting too large.

You may have wondered why the read() and write() methods are not virtual . The answer is that it is illegal. A member template function cannot be defined this way. Our definitions for read() and write() , however, will call virtual functions. The complete definition for apImageIOBase looks like this.

 class apImageIOBase { public:   virtual apDelegateInfo info (const std::string& filename) = 0;   template<class T, class S>   void read (const std::string& filename, apImage<T,S>& image)   {     if (typeid(apImage<apRGB>) == typeid(image))       image = readRGB (filename);     else if (typeid(apImage<Pel8>) == typeid(image))       image = readPel8 (filename);     else       copy (readRGB (filename), image);   }   template<class T, class S>   void write (const std::string& filename, apImage<T,S>& image,               const apDelegateParams& params = sNoParams)   {     if (typeid(apImage<apRGB>) == typeid(image))       write (filename, image.storage(), params);     else if (typeid(apImage<Pel8>) == typeid(image))       write (filename, image.storage(), params);     else {       apImage<apRGB> rgb = image;       write (filename, rgb.storage(), params);     }   }   virtual apImage<apRGB> readRGB (const std::string& filename) =0;   virtual apImage<Pel8>  readPel8(const std::string& filename) =0;   // File formats have limited ways that files are stored. These   // functions read RGB and 8-bit monochrome images.   virtual bool write (const std::string& filename,                       const apRectImageStorage& pixels,                       const apDelegateParams& params) = 0;   // Default copy constructor and assignment operators ok. protected:   apImageIOBase ();   virtual ~apImageIOBase ();   static apDelegateParams sNoParams; }; 

Our class makes the assumption that most images are stored in files. Our implementation has virtual functions, readRGB() and readPel8() , to read a color or monochrome (8-bit) image from the specified file. These are the two most important formats for our use. It would be a pretty simple matter to add additional functions as needed. For example, readPel32() might be useful for applications with 16- or 32-bit monochrome images. Before adding such a function, make sure that the image file format you are using supports such a resolution.

When you look at the read() method, you can see how it figures out whether to call readRGB() or readPel8() . By using typeid() , we are able to convert from a non-virtual member template to a regular virtual function. When you have the ability to enable or disable Run-Time Type Identification (RTTI), as with Microsoft compilers, you will need to enable RTTI so that this function works properly. readRGB() is called if the pixel type is not a Pel8 or apRGB , since a color image is more generic than a monochrome image.

To implement a new file delegate, you have to implement info() , write() , readRGB() , and readPel8() , although often readPel8() can simply call readRGB() .

Note that it is possible to extend this interface to include files that are stored in memory, but that is beyond the scope of this book.

File Delegate List

It is very nice to have a separate object to save or restore our apImage<> objects in each file format. However, it is better if we can maintain a current list of available file formats. Using our standard gOnly() technique, we write a repository object, apImageIODelegateList , to keep track of which file delegates are loaded, as shown here.

 class apImageIODelegateList { public:   static apImageIODelegateList& gOnly ();   apImageIOBase* getDelegate (const std::string& type);   void setDelegate (const std::string& type, apImageIOBase* object);   apImageIOBase* findDelegate (const std::string& filename); private:   typedef std::map<std::string, apImageIOBase*> map;   typedef map::iterator iterator;   map map_;   // Mapping of file type to a delegate to handle it   static apImageIODelegateList* sOnly_;   apImageIODelegateList (); }; 

With this object, we can see whether a particular file format can be read or written. getDelegate() returns a pointer to a file delegate object if the specified file type name exists. The type name is simply the standard file suffix used by a file format (i.e., jpg for a JPEG file).

You can extend apImageIODelegateList by adding read() and write() methods to handle all known file formats. These methods would find an appropriate delegate for the specified file, and then would call the delegate's corresponding read() or write() method to handle the file.

JPEG File Delegate

One of the most common file formats is Joint Photographic Expert's Group (JPEG). JPEG can store both monochrome and color images at various levels of compression. This format is considered lossy , meaning that if you save an image as a JPEG file, and then read it back, the image will be close but not identical to the original. For images intended for human viewing, this is usually acceptable, especially if you limit the amount of compression. However, for many applications, including medical imaging and machine vision, a lossy compression method should be avoided.

C and C++ API's are freely available and can be built on many platforms. Like most frameworks, we are using the Independent JPEG Group implementation (http://www.ijg.org). This is a pretty complicated API with many options. If this library does not already exist on your system and pre-compiled binaries are not available, you will need to do the following:

  1. Download the JPEG library from the Web site or the CD-ROM included with this book.

  2. Unpack the files onto your system.

  3. Read the building and installation instructions that come with the distribution.

  4. Build and install. For UNIX systems, this is as easy as typing make as the first command, followed by make install as the second.

The JPEG library is C-based and uses callback functions when errors or warnings occur. For our purposes, we define callback functions that generate C++ exceptions that we later catch in our file delegate object, as shown:

 class apJPEGException : public apException { public:   apJPEGException (std::string name="")     : apException ("apJPEGException: " + name) {} }; 

We define a JPEG callback function to receive error information and to generate an exception:

 void local_error_exit (jpeg_common_struct* cinfo) {   char msg[1024];   sprintf (msg, "error_exit: %d", cinfo->err->msg_code);   throw apJPEGException (msg); } 

jpeg_common_struct is an internal JPEG structure that contains information about the JPEG file, including any error information.

apJPEG is our file delegate object that creates an interface between our apImageIOBase interface and the JPEG library. Its definition is shown here.

 class apJPEG : public apImageIOBase { public:   static apJPEG& gOnly ();   virtual apDelegateInfo info (const std::string& filename);   virtual apImage<apRGB> readRGB   (const std::string& filename);   virtual apImage<Pel8>  readPel8  (const std::string& filename);   virtual bool write (const std::string& filename,                       const apRectImageStorage& pixels,                       const apDelegateParams& params = sNoParams);   // Default copy constructor and assignment operators ok. private:   static apJPEG* sOnly_;   apJPEG ();   ~apJPEG (); }; 

The implementation of these functions requires knowledge of the JPEG API. A rough outline of the implementation is as follows :

 apImage<apRGB>  apJPEG::readRGB  (const std::string& filename) {   apImage<apRGB> rgbImage;   struct jpeg_decompress_struct cinfo;   FILE *infile;   if ((infile = fopen(filename.c_str(), "rb")) == NULL) {     return rgbImage;   }   // Install our callback functions to stub out warning messages   // and generate our exception when an error occurs.   ...   // Read header information   try {     jpeg_read_header(&cinfo, TRUE);     ...   }   catch (apJPEGException&) {   }   ...   fclose(infile);   return rgbImage; } 

We place a try block around all of the JPEG API functionality. By doing this we can treat the JPEG library as a black box. We either get the image data we want, or else the catch block catches any errors generated by the JPEG library. Most file delegate functions are similar to this one.

We only need a single instance of any file delegate class, and gOnly() takes care of this for us. Instead of using apJPEG directly, we access this object by means of the apImageIODelegateList class. This gives us a way to automatically update our list of available file delegates, because having to manually update this list whenever a file delegate is added or subtracted is prone to error. Our solution is to take advantage of static initialization by defining a static function that adds the delegate to apImageIODelegateList :

 class apJPEG_installer { public:   static apJPEG_installer installer; private:   apJPEG_installer (); }; 

In our source file, we add:

 apJPEG_installer apJPEG_installer::installer; apJPEG_installer::apJPEG_installer () {   apImageIODelegateList::gOnly().setDelegate ("jpg",                                               &apJPEG::gOnly()); } 

During static initialization, apJPEG_installer::installer is initialized by calling the apJPEG_installer constructor, which adds the JPEG file delegate to the list of supported file types. apImageIODelegateList::gOnly() and apJPEG::gOnly() ensure that the Singleton objects are constructed .

Get in the habit of not referencing apJPEG directly. The preferred method is:

 apImageIOBase* delegate;   delegate = apImageIODelegateList::gOnly().getDelegate ("jpg");   if (delegate) {     ...   } 

This lookup converts the file type, .jpg in this case, to the object that handles this file format. This approach is clearly advantageous when you need to manage many file formats.

The method readRGB() does more than it appears. This function returns an apImage<apRGB> image, but it must be capable of reading the information contained in the file. The JPEG format can store an image in multiple ways, two of which are supported by our class. Using the nomenclature of the JPEG standard, the two color spaces that we support are a 24-bit color image (same as apImage<apRGB> ), and an 8-bit monochrome image (same as apImage<Pel8> ). readRGB() handles both cases and converts grayscale data to colors if necessary.

readPel8() could look very similar to readRGB() , in that it converts color data as it is received to monochrome. However, we do not have to worry about performance as much with save and restore operations, so we can take a huge shortcut:

 apImage<Pel8> apJPEG::readPel8 (const std::string& filename) { return readRGB (filename);} 

Since readRGB() can read both JPEG storage formats, we can simply convert the color image returned from readRGB() into an image of type apImage<Pel8> . The compiler handles the conversion from an apImage<apRGB> object to apImage<Pel8> by using the apImage<> copy constructor (defined inline in the apImage<> class definition):

 template<class T1, class S1>   apImage (const apImage<T1,S1>& src)   {     // Our copy() function does exactly what we want     copy (src, *this);   } 

By carefully creating the copy constructor and assignment operator in terms of different template parameters, we can reuse our copy() function.

TIFF File Delegate

The Tag Image File Format (TIFF) is another popular image format. It includes many internal formats for storing both color and monochrome images. It can store images in both lossy and loss-less fashion. The apTIFF object works like apJPEG , in that it handles the most common cases of reading and writing a TIFF image. We handle the warning or error callback issue in the same way by constructing an exception object, apTIFFException . Like the JPEG library, the C source code is freely available for this format from http://www.libtiff.org or on the CD-ROM included with this book.

6.4.2 Image Delegates

There are many third-party image processing packages available. The primary reasons to use third-party libraries is to broaden functionality and improve performance.

graphics/dia.gif I NTEL IPP

In our examples, we use the Intel Integrated Performance Primitives (IPP) library, which contains a large number of highly optimized, low-level functions that run on most Intel microprocessors. Once you take a look at the results of running a Laplacian filter with a 3x3 kernel on a 1024x1024 image, you'll understand why we have chosen this library.

Table 6.2 shows the performance results you can expect using our framework's wrapper functions to call the corresponding Intel IPP functions. Note that we performed these tests on our Intel Pentium 4 processor-based test platform running at 2.0 GHz.

Table 6.2. IPP Library Performance Results, 1024x1024 Image

Test

Pel8 image

apRGB image

Calling Intel IPP functions through our wrapper function

4 milliseconds IPPLaplacian3x3<Pel8>

11 milliseconds IPPLaplacian3x3<apRGB>

Convolution routine for Laplacian filtering with 3x3 kernel

45 milliseconds laplacian3x3<>

90 milliseconds laplacian3x3<>

General purpose convolution routine

89 milliseconds convolve<>

683 milliseconds convolve<>

graphics/dia.gif GENERIC CONVOLUTION FUNCTION

Before we deal with the specifics of IPP, let's discuss the broader design issue of how to interface any external library with our framework. To explore this issue, let's use our generic convolve() function and see how it changes, given different design strategies. Our original definition for convolve() is:

 template<class R, class T1, class T2, class S1, class S2> void convolve (const apImage<T1,S1>& src1,                const char* kernel, int size, int divisor,                apImage<T2,S2>& dst1) {   apFunction_s1d1Convolve<R,T1,T2,S1,S2> processor                                          (ap_convolve_generic);   processor.run (src1, kernel, size, divisor, dst1); } 
Strategy 1: A Fully Integrated Solution

If you want to fully integrate our framework with a third-party library, such that they operate as a single piece of code, we would need to modify our generic convolve() function as follows:

 template<class R, class T1, class T2, class S1, class S2> void convolve (const apImage<T1,S1>& src1,                const char* kernel, int size, int divisor,                apImage<T2,S2>& dst1) {   if (!thirdPartyFramework()) {     apFunction_s1d1Convolve<R,T1,T2,S1,S2> processor                                            (ap_convolve_generic);     processor.run (src1, kernel, size, divisor, dst1);   }   else {     thirdPartyFramework()->convolve (...);   } } 

We modify convolve() to query thirdpartyFramework() , which returns a pointer to an external library, or if none is available.

Let's consider the issues with this strategy. Our example shows that the external convolve() function is always called if an external convolve() function is available. What happens if the external function is less efficient than our built-in version? Our function definition should really include some kind of logic to determine which particular image processing function should be called.

In addition, our function assumes that as long as the third-party library exists, it must also support convolution. We should make sure to not only query the existence of the library, but also to verify that it supports convolution.

These changes are not unique to convolve() ; rather, we would need to make similar changes to all image processing functions that we would like to use with our third-party library.

And given the extensiveness of the changes, it is unlikely that the third-party library will exactly support our definition of an image. In our example, thirdpartyFramework() must return an object, which takes apImage<> objects as parameters and converts them, as needed, to an image format that is compatible with the image format used by the third-party library.

It is a very expensive proposition to make two distinct pieces of code act together as one, requiring numerous changes throughout the framework. This makes the solution prone to errors and difficult to maintain or extend.

We attempted to create a tightly integrated design that was very similar to how we handled file delegates. Although we don't highlight all the details in the book, we created a mapping object that would track which processing functions were available. We also looked at modifying our interface functions, like laplacian3x3<> , to detect whether an alternative version was available. This scheme quickly became unworkable because of the number of changes this approach forced us to make to the entire framework. Since many applications should be able to use our framework directly, without the need for any image delegates, we decided to abandon this strategy.

Because of these issues, we decided that the integration of a third-pary library should happen at a higher level. Let's explore a loosely coupled solution that does just that.

Strategy 2: A Loosely Coupled Solution

Let's look at a solution that provides a very loose coupling of our framework and a third-party library. We will leave both our framework and the third-party library unchanged. Instead, we will create interface functions and objects that convert our apImage<> references to a form compatible with the third-party library. In this solution, our original convolve() function also remains completely unchanged, as do all of the other image processing functions. To fully explore this solution, we use concrete examples with Intel IPP as the third-party library.

graphics/dia.gif IMAGES AND MEMORY ALIGNMENT

It turns out that the image definitions and memory alignment capabilities of the two frameworks are very compatible. In the IPP, an image is an array of image pixels, rather than a separate, well-defined structure. In apImage<> , our image storage also requires an array of image pixels. The memory alignment capabilities of apImage<> are also supported in the IPP.

IPP routines pass the pitch (or step , as the IPP documentation calls it) between rows. This is the number of bytes needed to move from one pixel in the image to the pixel just below it on the following row. While the IPP does not have specific memory alignment requirements, some alignments will result in higher performance because of the architecture of the Intel processors. Unless you must squeeze out every bit of performance, you can safely ignore the alignment details and simply let the libraries handle the alignment issues.

If you really must be concerned about every bit of performance, here are some issues to consider. Neighborhood processing routines, such as convolution, require preprocessing to determine which pixels in the operation are used. (See our discussion regarding intersections on page 213 for more information.) It is not possible to guarantee that the regions of interest (ROIs) being processed will be aligned for optimal performance. But this does not mean that the obvious solution of creating a temporary image will result in the performance gains of an aligned image. Using an aligned image adds four processing steps: creating temporary image(s), copying the pixels from the input images to the temporary images, copying the result to the destination image, and, finally, deleting the temporary image(s).

Interfacing apImage<> to other image processing libraries will not always be this easy. If you are fortunate, as we are in this example, the complexity will be limited to converting between the apImage<> data structure and the third-party library. However, if the third-party library uses an incompatible storage format, the underlying image data must be converted. Regardless of the complexity, you should always implement an interface object to encapsulate the details.

graphics/dia.gif TRAITS

Our interface object, apIPP<>, converts an apImage<> object into a form usable by the Intel library. Converting images is a simple operation in our case. IPP, however, is a C-interface library that contains hundreds of functions. Therefore, it is very useful to encapsulate some of these related datatypes into traits classes. Using traits, you can define one or more properties (or traits) for a datatype.

Let's start by reviewing our IPPTraits<> object, which maps an apImage<> pixel type to one used by the IPP, as shown.

 template<class T> struct IPPTraits {   typedef T ipptype;  // Intel IPP  type for this apImage type }; template<> struct IPPTraits<apRGB> {   typedef Ipp8u ipptype; }; template<> struct IPPTraits<apRGBPel32s> {   typedef Ipp32s ipptype; }; 

Even though we have called this a traits class, this object only contains a single typedef . We did this because the object will grow over time as other image-specific quantities are added. The generic version of IPPTraits<> defines a value, ipptype , to be the same type as the apImage<> pixel type. This works as expected for monochrome data types, but we must define specializations for our color data types to correctly map the apImage<> pixel types to the corresponding ones in the IPP.

For example, apImage<apRGB> defines an image of RGB pixels. However, the IPP considers this to be an image of bytes, taking three consecutive bytes as a color pixel. While the memory layouts are identical, the compiler will generate an error if we try to pass an apRGB pointer to an IPP function. So, we define a specialization that uses the native pixel types ( Ipp8u for an 8-bit unsigned integer and Ipp32s for a 32-bit signed integer) for the IPP.

graphics/dia.gif AP IPP<>

apIpp<> encapsulates our apImage<> objects and converts them into a form compatible with the IPP as shown here.

 class apIPPException : public apException { public:   apIPPException (std::string name="")     : apException ("apIPPException: " + name) {} }; template<class T, class S=apImageStorage<T> > class apIPP { public:   typedef typename IPPTraits<T>::ipptype ipptype;   enum eType {     eIPP_Unknown = 0,       // Unknown or uninitialized type     eIPP_8u_C1   = 1,       // 8-bit monochrome     eIPP_32s_C1  = 2,       // 32-bit (signed) monochrome     eIPP_8u_C3   = 3,       // 8-bit RGB     eIPP_32s_C3  = 4,       // 32-bit (signed) RGB   };   apIPP (apImage<T,S>& image);   ~apIPP () { cleanup ();}   apIPP (const apIPP& src);   apIPP& operator= (const apIPP& src);   // We need our own copy constructor and assignment operator.   ipptype* base       ()     { return reinterpret_cast<ipptype*>(align_.base());}   const ipptype* base () const     { return reinterpret_cast<ipptype*>(align_.base());}   // Base address of image data, in the proper pointer type   IppiSize imageSize () const   { IppiSize sz; sz.width = align_.width();     sz.height = align_.height(); return sz;}   // IPP image size (suitable for use in IPP routines)   unsigned int pitch () { return align_.begin()->bytes;}   // Pitch (spacing, in bytes, between rows)   void syncImage ();   // Called after image processing to sync up the image with the   // aligned image, if necessary protected:   void createIPPImage ();   // Convert the apImage<> image, if necessary, to a form for IPP   void cleanup ();   // Cleanup any memory allocated to the intel image object   apImage<T,S>& image_;   // Our storage object (A reference)   apImage<T,S>  align_;   // Our aligned image   eType         type_;    // Image type }; 

apIPP<> keeps a reference to the passed image, rather than a copy of the image. This is to ensure that any changes made to image_ will be reflected in the original image. Holding onto a copy of the image, even though the image storage uses reference counting, will not work. Reference counting does avoid making copies of the underlying pixel data, but it cannot guarantee that two images will always point to the same storage.

align_ is an image which is aligned in memory to take advantage of performance improvements in the IPP. Both align_ and image_ will refer to the same image pixels, unless createIPPImage() generates a temporary image to enhance performance.

graphics/dia.gif CREATE IPPI MAGE()

createIPPImage() is called by the constructor and allows IPP functions to use the apImage<> pixel data. The implementation does little more than use Run-Time Type Identification (RTTI) to map the pixel type to the IPP datatype name, as shown.

 template <class T, class S> void apIPP<T,S>::createIPPImage () {   // Cleanup any existing Intel object   cleanup ();   align_ = image_; // No special memory alignment by default   // Figure out our datatype   if (typeid(T) == typeid(Pel8)) {     type_ = eIPP_8u_C1;   }   else if (typeid(T) == typeid(Pel32s)) {     type_ = eIPP_32s_C1;   }   else if (typeid(T) == typeid(apRGB)) {     type_ = eIPP_8u_C3;   }   else if (typeid(T) == typeid(apRGBPel32s)) {     type_ = eIPP_32s_C3;   }   else {     throw apIPPException ("Unsupported image type");   } } 

graphics/dia.gif SYNC I MAGE()

After any image processing step that changes align_ , the syncImage() method should be called to make sure that image_ accurately reflects the results of the operation. If align_ and image_ point to the same storage, nothing needs to be done. The definition of syncImage() is shown here.

 template <class T, class S> void apIPP<T,S>::syncImage () {   if (align_.base() != image_.base())     copy (align_, image_); } 

graphics/dia.gif GENERIC INTERFACE TO FILTERING FUNCTIONS

Now that we have apIPP<> to interface our apImage<> object with the IPP data structures, we can turn our attention to writing an interface to some of its image processing functions.

We create a generic object, apIPPFilter<> , that defines an interface that can be used with most IPP filtering functions. Most of the IPP functions are similar to the following prototype:

 IppStatus prototype (const ipptype* src, int srcStep,                      ipptype* dst, int dstStep,                      IppiSize roi, IppiMaskSize mask); 

In this example, prototype() takes a source pixel pointer and step size (bytes between neighboring rows in the image), destination pixel pointer and step size, image size ( IppiSize ), and filter size ( IppiMaskSize ).

IPPFilter<> defines the call operator, operator() , to perform a specific image filtering operation. The definition of IPPFilter<> is shown here.

 template<typename T, IppiMaskSize S> class IPPFilter { public:   typedef typename IPPTraits<T>::ipptype ipptype;   typedef IppStatus  (__stdcall *fixedFilterFunction)(                                            const ipptype* /*pSrc*/,                                            int /*srcStep*/,                                            ipptype* /*pDst*/,                                            int /*dstStep*/,                                            IppiSize /*dstRoiSize*/,                                            IppiMaskSize /*mask*/);  IppStatus operator() (const apImage<T>& src, apImage<T>& dst)   {     if (src.isNull())       return ippStsNoErr;     apImageLocker<T> srcLocking (src);     apImage<T> source = src;     apImage<T> dest   = dst;     eMethod method = eRegular;     // If destination is specified, use it in computing the overlap.     if (dest.isNull()) {       // Our destination size is smaller than the source, but aligned       int size = IPPMaskSizeTraits<S>::xSize;       int expansion = (size-1) / 2;       apRect region = src.boundary ();       region.expand (-expansion, -expansion);       dst = apImage<T> (region, src.alignment());       dest = dst;       method = eCreatedDest;     }     else if (src.isIdentical (dst)) {       // In-place operation       dest = duplicate (source, apRectImageStorage::e16ByteAlign);       method = eInPlace;     }     else if (source.boundary() != dest.boundary()) {       // Restrict output to the intersection of the images       dest.window   (src.boundary());       source.window (dest.boundary());       method = eWindow;     }     // Lock destination after it is created (if necessary)     apImageLocker<T> dstLocking (dst);     // Compute the overlap between the images     apIntersectRects overlap = intersect (src.boundary(),                         IPPMaskSizeTraits<S>::xSize, IPPMaskSizeTraits<S>::ySize,                         dst.boundary());     // Return a null image if there is no overlap     if (overlap.dst.isNull()) {       dstLocking.unlock ();       dst = dst.sNull;       return ippStsNoErr;     }     // Work only within the overlap area     apImage<T> roi1 = src;     apImage<T> roi2 = dst;     roi1.window (overlap.dst);     roi2.window (overlap.dst);     apIPP<T> s (roi1);     apIPP<T> d (roi2);     // Call the IPP function     IppStatus status = func_ (s.base(), s.pitch(), d.base(),                               d.pitch(), s.imageSize(), S);     d.syncImage ();     // Post-processing     switch (method) {     case eInPlace:       copy (dest, source);       dst = dest;       break;     default:       break;     }     return status;   } protected:   enum eMethod { eRegular, eInPlace, eCreatedDest, eWindow};   IPPFilter (fixedFilterFunction f) : func_ (f) {}   fixedFilterFunction func_; }; 

IPPFilter<> is fairly straightforward. The typedef ipptype is identical to the one we find in the traits class, IPPTraits<> , and is defined to make it easier to refer to the IPP datatype. Note that you must use typename to equate ipptype with the corresponding type in IPPTraits<> .

The typedef fixedFilterFunction defines what the IPP filter functions will look like, and matches the prototype() function shown earlier on page 242.

You pass the desired IPP filter function to IPPFilter<> in its constructor, so that it can be used by operator() .

You may notice that we are only using a single template parameter, T , when referring to apImage<> , and we are relying on using the default values for the other parameter. If your images require non-default versions of the other parameter, you can modify this object very easily.

operator() allows some flexibility in how the destination argument is specified, as follows:

  • If an output image is specified, the returned image will be the intersection of the source and destination regions.

  • If no destination image is specified, a destination image is created to be the same size as the source image. This destination image will have the same alignment as the source image.

  • If the destination image is identical to the source image, the operation is performed in place. Internally, this creates a temporary image before the image processing is performed.

To keep track of what kind of processing is needed, the eMethod enumeration defines all of the possible states we may encounter.

operator() performs the following four steps:

  1. Convert the source image to the IPP format.

  2. Convert the destination image to the IPP format.

  3. Call the IPP function func_() to compute the filtered image.

  4. Synchronize our result with the destination image, in case a copy of the image was made for alignment reasons.

These steps are handled by the following four lines of code:

 apIPP<T> s (roi1);     apIPP<T> d (roi2);     IppStatus status = func_ (s.base(), s.pitch(), d.base(),                               d.pitch(), s.imageSize(), S);     d.syncImage (); 

graphics/dia.gif DERIVED OBJECTS FOR SPECIFIC FILTERING OPERATIONS

You can now derive objects from IPPFilter<> to define objects for specific Intel IPP image processing functions. For example, we derived an object that defines 3x3 and 5x5 Laplacian filtering operations, as follows:

 template<typename T> class IPPLaplacian3x3   : public IPPFilter<T, ippMskSize3x3> { public:   typedef typename IPPFilter<T, ippMskSize3x3>::fixedFilterFunction                                                 fixedFilterFunction;   static IPPLaplacian3x3 filter; protected:   IPPLaplacian3x3 (fixedFilterFunction f)     : IPPFilter<T,ippMskSize3x3> (f) {} }; IPPLaplacian3x3<Pel8>  IPPLaplacian3x3<Pel8>::filter                                 (ippiFilterLaplace_8u_C1R); IPPLaplacian3x3<apRGB> IPPLaplacian3x3<apRGB>::filter                                 (ippiFilterLaplace_8u_C3R); template<typename T> class IPPLaplacian5x5   : public IPPFilter<T, ippMskSize5x5> { public:   typedef typename IPPFilter<T, ippMskSize5x5>::fixedFilterFunction                                                 fixedFilterFunction;   static IPPLaplacian5x5 filter; protected:   IPPLaplacian5x5 (fixedFilterFunction f)     : IPPFilter<T, ippMskSize5x5> (f) {} }; IPPLaplacian5x5<Pel8>  IPPLaplacian5x5<Pel8>::filter                                 (ippiFilterLaplace_8u_C1R); IPPLaplacian5x5<apRGB> IPPLaplacian5x5<apRGB>::filter                                 (ippiFilterLaplace_8u_C3R); 

In the example above, you can see how our framework interfaces with the Intel IPP function, ippiFilterLaplace_8u_C3R() , to compute the Laplacian image of an 8-bit RGB image.

We can see how easy it is to use our image delegate by looking at a comparison of techniques for computing the Laplacian image of an 8-bit RGB image. To compute the Laplacian image using our framework, we write:

 apImage<apRGB> src;   apImage<apRGB> dst;   laplacian3x3<apRGBPel32s> (src, dst); 

When we rewrite this example using our image delegate, the code looks similar; however, the template parameter now specifies the pixel type instead of the internal pixel type required by our framework. The rewritten example that computes the Laplacian image of an 8-bit RGB image is as follows:

 apImage<apRGB> src;   apImage<apRGB> dst;   IPPLaplacian3x3<apRGB>::filter (src, dst); 

If we call the function, laplacian3x3<> , we call our built-in routine to compute the Laplacian image of an input image. If we call IPPLaplacian3x3<apRGB>::filter() , we call the IPP version of the same filtering operation.

An additional advantage in this particular example, is that both versions return the identical image results, because the Intel IPP library has the same boundary behavior as our framework. You cannot always count on this behavior from a third-party library, however, and it is best to leave it up to the application to choose the appropriate function.

Extending the Loosely Coupled Solution

Our loosely coupled strategy gives us the freedom to call functionality either in our image framework or from image delegates, depending on the application requirements. Many applications need to call both kinds of functions, depending upon such issues as the availability of image delegate run-time libraries, performance requirements, or accuracy requirements.

You can extend our loosely coupled design to create a framework, using inheritance, that enables the libraries to work efficiently together. Your application's framework may look similar to the following:

 class Image { public:   virtual void create (unsigned int width, unsigned int height);   virtual Image laplacian3x3 (); protected:   apImage<Pel8> image_; }; 

We have left out most details, but the idea is to create a wrapper object that manages a specific type of apImage<> (in this case, it is an 8-bit monochrome image). You can place whatever processing support you need in this object, or you can create a number of separate functions to add this functionality. These functions are all virtual functions, allowing derived classes to override their behavior as needed.

To handle the specifics of a third-party framework, such as the IPP, you create a derived object like IPP_Image , as shown:

 class IPP_Image : public Image { public:   virtual void create (unsigned int width, unsigned int height);   virtual Image laplacian3x3 (); protected:   apIPP<Pel8> ipp_; }; 

The derived object, IPP_Image in this example, can be very selective in what functions it overrides , and in what contraints it chooses to enforce. Your application will use Image when functionality from apImage<> is desired, and it will use IPP_Image when functionality from both apImage<> and the image delegate (IPP in this case) is needed.

Our Recommended Strategy

During our design phase, we spent a long time analyzing our image framework to determine whether or not to build hooks for third-party image processing packages. We explored a fully integrated design that was very similar to how we handled file delegates. There were many issues that arose from our design efforts, and we highlighted a few of them in the section Strategy 1: A Fully Integrated Solution on page 238. This design proved difficult to manage, and required extensive changes that rendered the use of image delegates more like a built-in feature instead of its intended purpose as an extension.

Based on the issues that arose, we decided that the fully integrated design was unsuitable. We explored a second strategy in the section Strategy 2: A Loosely Coupled Solution on page 239, which provided a loosely coupled connection between our framework and a third-party library (in this case, the Intel IPP). We found this to be a successful strategy that not only allowed us to build a general purpose framework, but also gave us the ability to provide additional tools, through a third-party libary, that are necessary for building robust applications.



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