A step-by-step guide to the use of the Intel OpenCV 1 library

Processing an image using the Strategy design pattern

The preceding example has several problems. First the output image is allocated inside the OnOpen method using a local variable. This means that this image has to be de-allocated also inside this same method; it would therefore be complex to perform additional processing on this image. Also, when you process several images, the output image is allocated and de-allocated for each input image. This could be a waste of resources if all images have the same size (in such case, the same output image should be re-used). But more important, the processing is realized inside the GUI class which violates a fundamental principle of good programming design: the processing aspect of your program should be separated from the GUI management aspects.

A separate class will therefore be created as a container of the image processing task. The Strategy Pattern is a software design pattern that is used to encapsulate an algorithm into a class. The pattern is often used as a mechanism to select an algorithm at run-time. In our case, it will facilitate the interchange and the deployment of our image processing algorithms inside more complex computer vision systems.

Here is then the general structure of our processing classes:

 
#if !defined PROCESSOR
#define PROCESSOR
 
#include "cv.h"
 
class Processor {
 
  private:
 
        // private attributes
 
  public:
 
        // empty constructor
        Processor() {
 
              // default parameter initialization here
        }
 
        // Add here all getters and setters for the parameters
 
        // to check if an initialization is required
        virtual bool isInitialized(IplImage *image)=0;
 
        // for all memory allocation
        virtual void initialize(IplImage *image)=0;  
 
        // the processing of the image
        virtual void process(IplImage *image)=0;
 
        // the method that checks for initilization
        // and then process the image
        inline void processImage(IplImage *image) {
 
              if (!isInitialized(image)) {
 
                    initialize(image);
              }
 
              process(image);
        }
 
        // memory de-allocation
        virtual void release() =0;
 
        ~Processor() {
 
              release();
        }
};
#endif
First we start with a 0-parameter default constructor. This makes program initialization much easier because then all objects can be created with a-priori knowledge. The constructor simply makes sure that the object is in a valid state by initializing all the parameters to their default values. You also include setters and getters for your parameters such that the user can change them at run-time, using the GUI for example.

Memory allocation has to be accomplished by a separate method. The reason for this is that to allocate the memory space for the images, we have to know the size of the input image. The goal of the isInitialized method is to check if an initialization is required; this can happen for two reasons: i) memory allocation has not been performed yet (all pointer are set to NULL, or; ii) the new image is of different size than the image that has been previously processed, we therefore need to de-allocate all memory previously allocated and then allocate new memory space. Note that in the current design, the user has the choice to himself call isInitialized when required and then process or to let the class to systematically check if an initialization is required by calling the processImage method.

The Processor class could be used as a base class for the image processing classes to be created. However, in computer vision and especially in video processing, computational efficiency is a must. Therefore, the cost of calling a virtual method could be too high (the overhead is in the order of 10% to 20%). So we will rather use this class as a model. In the case of the erosion, this class would be written as:

 
class Eroder {
 
  private:
 
        // private attributes
        IplImage *output;
        int nIterations;
 
  public:
 
        // empty constructor
        Eroder() : nIterations(DEFAULT_NITERATIONS), output(0) {
 
              // default parameter initialization here
        }
 
        // getters and setters
        void setNumberOfIterations(int n) {
 
              nIterations= n;
        }
 
        int getNumberOfIterations() {
 
              return nIterations;
        }
 
        IplImage* getOutputImage() {
 
              return output;
        }
 
        // to check if an initialization is required
        bool isInitialized(IplImage *image) {
 
              return output && (output->width == image->width)
                              && (output->height == image->height);
        }
 
        // for all memory allocation
        void initialize(IplImage *image) {
 
              cvReleaseImage(&output);
              output= cvCreateImage(
                       cvSize(image->width,image->height),
                              image->depth, image->nChannels);
        }
 
        // the processing of the image
        void process(IplImage *image) {
 
              cvErode(image, output, 0, nIterations);
        }
 
        // the method that checks for initilization
        // and then process the image
        inline void processImage(IplImage *image) {
 
              if (!isInitialized(image)) {
 
                    initialize(image);
              }
 
              process(image);
        }
 
        // memory de-allocation
        void release() {
 
              cvReleaseImage(&output);
        }
 
        ~Eroder() {
 
              release();
        }
};
The corresponding object is created in the application simply as an instance variable in the dialog class (file xxxDlg.h); same thing for the input image pointer.

 
      IplImage *image;  // This is the image pointer (input)
      Eroder eroder;    // image processor as an automatic variable
An automatic variable is here used for the processor class (which implies automatic object instantiation); dynamic allocation could have also been used. We also decided to modify the GUI to separate image loading and image processing.

Then the handlers of the two buttons are:

 
void CcvisionDlg::OnOpen()
{
  CFileDialog dlg(TRUE, _T("*.bmp"), NULL,
    OFN_FILEMUSTEXIST|OFN_PATHMUSTEXIST|OFN_HIDEREADONLY,
    _T("image files (*.bmp; *.jpg) |*.bmp;*.jpg|
                 All Files (*.*)|*.*||"),NULL);
 
  dlg.m_ofn.lpstrTitle= _T("Open Image");
 
  if (dlg.DoModal() == IDOK) {
 
    CString path= dlg.GetPathName();// contain the selected filename
 
      image= cvLoadImage(path);              // load the image
      cvShowImage("Original Image", image);  // display it
 
  }
}
and:
  
void CcvisionDlg::OnProcess()
{
      eroder.processImage(image);
 
      cvShowImage("Processed Image", eroder.getOutputImage());
}
Test the application with images of different sizes. Make sure to understand how the out image is reallocated when necessary. Also, we use here the default value for the (unique) parameter of the processor; however a call to setNumberOfIterations would be done in case one wants to change this value.

Check point #3: source code of the above example.

The encapsulation of the algorithm into a class is there to facilitate its deployment; proper initialization and memory management being done by the class. There is however one major flaw in this design: the method getOutputImage which returns a pointer to a dynamically allocated instance variable. This is unsafe because this pointer can be deleted by the class destructor and the returned pointer that could have been stored in some variable of the application would then dangle; or worst, the application could release the allocated memory without permission. There are two solutions to this problem. The first one, which is the one adopted by OpenCV, consists in requiring the user of an algorithm to provide all the memory buffers that is needed. In our case, this would mean passing an output image together with the input image when calling the process method. This is an approach that applies well in a procedural paradigm context, but under the object-oriented paradigm this is not very convenient. We indeed want the class to manage itself all aspects of the algorithm. Encapsulation and information hiding are indeed the two key principles in object-oriented programming.

The second solution would consist in using smart pointers. Smart pointers are special classes that take care of memory de-allocation. In a sense, they play the role of garbage collector. However, in order to keep this tutorial as simple as possible, we will not use this solution here. We will simply accept our returned pointer design flaw and work under the (unreasonable) assumption that programmers will make a good use of the class and of its returned pointers.

Top of the page

 

(c) Robert Laganiere 2011