Chapter 6. Extending ImageVision Library

Since ImageVision Library (IL) is implemented in C++, you can easily extend it by deriving new classes that provide support for the capabilities you need; for instance, to include another file format or image processing algorithm. You can derive from any C++ class, but you are most likely to want to derive from the foundation classes. Figure 6-1 shows the types of classes you are most likely to derive.


Note: If you are using the C interface to IL, extending the library is not quite so simple. You have to implement a new class in C++ and then generate a C interface for it.

This chapter contains the following major sections:

IL classes from which you might want to derive your own new classes are shown in Figure 6-1.

Figure 6-1. User-Defined Classes in IL

Figure 6-1 User-Defined Classes in IL

Each extension to IL can be designed to provide a certain set of capabilities and require the implementation of a matching set of functions, as described below:

The classes ilImage, ilCacheImg, ilMemCacheImg, ilOpImg, and ilRoi declare virtual functions that subclasses may be redefined to alter class behavior. Other functions can be added as necessary to provide the desired capabilities of the class.

The remaining sections in this chapter explain how to derive from ilImage, ilCacheImg, ilMemCacheImg, iflFileImg, ilOpImg, or ilRoi (or one of their generalized subclasses). Remember that when you derive from a class, you inherit all its public and protected data members and member functions, as well as the public and protected members from its superclasses. You should review beforehand the header files and the reference pages for any class you plan to derive from in order to become familiar with its data members and member functions. Many of the functions described in the following sections are protected, so they are available for use only by derived classes.

Deriving From ilImage

A class derived from ilImage must assign values to the image's attributes and implement ilImage's virtual functions. The image's attributes (data members) are listed in Table 6-1; they are generally initialized in the constructor.

Table 6-1. Image Attributes Needing Initialization in ilImage Subclass

Name

Data Type

Meaning

pageSize

iflSize

size of the image's pages in pixels

dtype

iflDataType

pixel data type

order

iflOrder

pixel data ordering

cm

iflColorModel

image's color model

orientation

iflOrientation

location of origin and orientation of axes

fillValue

iflPixel

value used to fill pixels beyond the image's edge

minValue, maxValue

double

minimum and maximum allowable pixel values

status

ilStatus

image's status (for example, ilOKAY)[a]

[a] Inherited from ilLink.

Typically, you will just set these attributes directly. However, there are convenience functions—for setting minValue, maxValue, cm, and status—that you might want to use (these functions are protected, so they are available only to classes derived from ilImage):

void initMinMax(int force=FALSE);
void initColorModel(int noAlpha=FALSE);
void initPagesize(const iflSize& pageSize);
ilStatus setStatus(ilStatus val); //inherited from ilLink
void clearStatus();              // inherited from ilLink

The initMinMax() function simultaneously sets both the minimum and maximum allowable pixel values. They are set to the smallest and largest possible values, respectively, allowed by the image's data type. Therefore, you must set the image's data type before you call initMinMax(). By default, this function's argument is FALSE, which means that the minimum and maximum values will not be changed if they have already been explicitly set; if you pass in TRUE as the argument to this function, both values will be set regardless of whether they have been set before.

The initColorModel() function sets the color model based on the channel dimension of the image. If the channel dimension is 1, the color model is iflLuminance; if it is 2, the color model is iflLuminanceAlpha (or iflultiSpectral if noAlpha is TRUE); if it is 3, the color model is iflRGB. If the channel dimension is 4 and the default value of FALSE is used for the noAlpha argument, the color model is iflRGBA. Otherwise, the color model is iflMultiSpectral.

The setStatus() function simply sets and returns the image's status. The clearStatus() function sets the image's status to ilOKAY. (Both of these functions are inherited from ilLink.) See “Error Codes” for a list of the error codes that IL defines as being of type ilStatus.

Another function you may want to use in a constructor is setNumInputs(). This function sets the maximum possible number of inputs to an image. Typically, you will use this function only when deriving an operator. See “Implementing an Image Processing Operator” for more information about doing this.

Data Access Functions

Image data can be accessed as pixels or as a rectangular region of arbitrary size called a tile. Both 2-D and 3-D tile access functions are provided.

The virtual access functions present a queued request model, which allows an application to issue non-blocking requests for image I/O and later inquire the status or wait for the operation to complete. The queued model also provides derived classes with the “hooks” needed to automatically distribute operations across multiple processors. These queued functions are distinguished by the prefix “q” on the function name. For convenience, there are access functions that do wait for their operation to complete, hiding the details of the queued model.

There are several different functions to read image data, all based on qGetSubTile3D(). ilQGetSubTile3D(). Similarly, there are several different functions to write image data based on qSetSubTile3D(). ilQSetSubTile3D(). Two fast-paths called qCopyTileCfg() and qCopyTile3D() ilQCopyTileCfg() and ilQCopyTile3D() are available for copying a tile from another ilImage.

Most of the virtual functions in ilImage are data access functions:

virtual ilStatus qGetSubTile3D(ilMpNode* parent, int x, int y, int z,
    int nx, int ny, int nz, void*& data, int dx, int dy, int dz, int dnx, 
    int dny, int dnz,const ilConfig* config=NULL, ilMpManager** pMgr=NULL);

virtual ilStatus qSetSubTile3D(ilMpNode* parent, int x, int y, int z,
    int nx, int ny, int nz, void* data, int dx, int dy, int dz, int dnx, 
    int dny, int dnz, const ilConfig* config=NULL, ilMpManager** pMgr=NULL);

virtual ilStatus qCopyTileCfg(ilMpNode* parent, int x, int y, int z,
    int nx, int ny, int nz, ilImage* other, int ox, int oy, int oz, 
    const ilConfig* config=NULL, ilMpManager** pMgr=NULL);
virtual ilStatus qDrawTile(ilMpNode* parent, int x, int y, int nx, int ny,
    ilImage* src, float sx, float sy, float sz, ilMpManager** pMgr=NULL);

virtual ilStatus qFillTile3D(ilMpNode* parent, int x, int y, int z,
    int nx, int ny, int nz, const void* data, const ilConfig* config=NULL,     const iflTile3Dint* fillMask=NULL, ilMpManager** pMgr=NULL);

virtual ilStatus qFillTileRGB(ilMpNode* parent, int x, int y, int z,
    int nx, int ny, int nz, float red, float green, float blue, 
    const iflTile3Dint* fillMask=NULL, 
    iflOrientation orientation=iflOrientation(0), ilMpManager** pMgr=NULL);

virtual ilStatus qLockPageSet(ilMpNode* parent, ilLockRequest* set,
    int mode=ilLMread, int count=1, ilMpManager** pMgr=NULL, 
    ilCallback* perPageCb=NULL); 

ilStatus qGetTile3D(ilMpNode* parent, int x, int y, int z, int nx, int ny, 
    int nz, void*& data, const ilConfig* config=NULL, ilMpManager** pMgr=NULL)

ilStatus qSetTile3D(ilMpNode* parent, int x, int y, int z, int nx, int ny, 
    int nz, void* data, const ilConfig* config=NULL, ilMpManager** pMgr=NULL)

When calling the base functions listed above, the caller must specify the origin (x, y, z) and size (nx, ny, nz) of the desired tile. For 2-D operations, z is set to 0 and nz is set to 1. For pixel operations, nx, ny and nz are set to 1. An object called iflConfig, is used to specify the configuration (that is, data type, order, number of channels and so forth) of the desired tile. If required, the image data is converted to a specified configuration while getting a tile, or converted from a specified configuration to that of the image while setting a tile.

All of these functions have default implementations that you can choose to override. The rest of this section explains how to implement these functions.

Implementing qGetSubTile3D()

You should implement qGetSubTile3D() so that it retrieves an arbitrary tile of data from the source image and puts it into the location indicated by data. The tile is located at position (x, y, z) in the source image and has the size indicated by nx, ny, and nz. The dx, dy, and dz parameters specify the data buffer's origin relative to the image; dnx, dny, and dnz specify the buffer's size. The optional config argument indicates how the data should be configured in the buffer. See “Three-dimensional Functions” for more information about qGetSubTile3D().

This function has a default implementation that returns ilUNSUPPORTED.

Implementing qSetSubTile3D()

Your version of the qSetSubTile3D() function should write the tile of data pointed to by data into the destination image. The arguments for qSetSubTile3D() have analogous meanings to those for qGetSubTile3D(): (x,y,z) and (nx, ny, nz) indicate the desired origin and size of the tile in the destination image; dx, dy, and dz specify the data buffer's origin relative to the image; and dnx, dny, and dnz specify the size of the data buffer. The optional config argument describes the configuration of the tile being passed or written; if it is NULL, assume that the tile's configuration matches that of the destination image. See “Three-dimensional Functions” for more information about qSetSubTile3D().

This function has a default implementation that returns ilUNSUPPORTED.

Implementing qCopyTileCfg()

The default implementation of qCopyTileCfg() copies a tile of data from one image to another. This implementation is not as efficient as possible, since it allocates a temporary buffer for holding the data as it performs the copy and then deletes the buffer when it completes the copy. You might want to override this function to provide a more efficient version.

Implementing qFillTile3D() and qFillTileRGB()

The default versions of qFillTile3D() and qFillTileRGB() do nothing; you will need to override them if you want their functionality. Your implementations should fill a specified tile with the specified pixel value or color.

Implementing qLockPageSet()

Your implementation of qLockPageSet() should set a read-only lock for a set of pages when accessing image data. A pointer to each page in the set is deposited in each corresponding ilLockRequest. As a result, the image data for all of the pages is computed. If all of the requests succeed, ilOKAY is returned. If one or more fail, an error code will be returned and the ilLockRequest structures will contain individual status codes.

Implementing qGetTile3D()

This function places the destination of a tile, pointed at by data, at coordinates, x, y, z using the size of the source image defined by dx, dy, dz.

Your class must overwrite qGetTile3D(). Its default function returns ilUNSUPPORTED.

Implementing qSetTile3D()

This function allows the source buffer to have a different position and size, specified by dx, dy, dnx, dny, dz, and dnz.

Your class must overwrite qGetTile3D(). Its default function returns ilUNSUPPORTED.

Support Functions

The outOfBound() support functions are provided to help implement the data access functions:

int outOfBound(int x, int y); 
int outOfBound(int x, int y, int z); 

These functions return TRUE if the specified point lies outside the image.

If you implement any of the data access functions, you need to hook them into the reset mechanism, which is described next.

Color Conversion

The checkColorModel() function matches the color model of an image with the number of channels. If there is a mismatch, the number of channels is updated to match the color model. However, if the number of channels was set and there is a mismatch, a status of ilBADCOLFMT is set.

void checkColorModel();

The needColorConv() function returns TRUE if the image's color model does not match the color model of other. The from flag indicates the direction that data is copied:

needColorConv(ilImage* other, int from, const ilConfig* cfg); 

The getCopyConverter() function chains one image to another provided the two images have different color models. If the images have the same color model, there is no color conversion. getCopyConverter() is defined as follows:

int getCopyConverter(ilImage*& other,const ilConfig* cfg) 

The getCopyConverter() function returns TRUE if the other image has a different color model than this image. In this case, a color converter operator is chained onto the other image.

The getCopyConverter() function returns FALSE if the color models are compatible, or if the cfg specifies a channel list or channel offset. In this case a converter operator is not chained to the other image.When cfg specifies a channel list or offset, no color conversion is performed.

Managing Image Attributes

An image has numerous attributes associated with it that describe the image. You can change some attributes; some change as a side effect of changing some other attribute. This section describes functions you can use to manage attribute values in a class derived from ilImage.

The reset() Function

An important virtual function in ilImage that you must be concerned with is reset():

virtual void reset(); // inherited from ilLink

This function is designed to adjust or validate an image's attributes if they have been altered, for example, by applying an operator or by setting an attribute explicitly. This function plays a key role in IL's execution model, which propagates image attribute values down an operator chain. (See “Propagating Image Attributes” for more information on propagating image attributes.)

The reset mechanism is triggered whenever an image is queried about its attributes or when its data is accessed. The query and access functions all call resetCheck() (which is inherited from ilLink) to initiate the reset process. If you implement qGetSubTile3D(), qSetSubTile3D(), qCopyTileCfg(), qFillTile3D(), qFillTileRGB(), qLockPageSet(), qGetTile3D(), qSetTile3D() or any attribute query, you need to call resetCheck() before you do anything else in your versions of these functions. This ensures that correct information about an image's attributes is returned and that image data is always valid before it is read, written, copied, filled, or updated.

The reset() function must be defined by derived classes to perform any necessary reset tasks. For example, the ilMemCacheImg class's version of reset() throws out any existing data in the cache since it is invalid; ilOpImg performs several chores in its reset() function and then calls resetOp(), which needs to be implemented by derived classes to perform more specific reset tasks.

Allowing Attributes to Change

Not every image attribute can be changed; by default, the fill value and the maximum and minimum pixel values are allowed to change. Each ilImage derived class can choose which attributes it allows to be modified by using the setAllowed() function (inherited from ilLink), typically in the constructor:

setAllowed(ilIPcolorModel|ilIPorientation);

The argument passed to setAllowed() is a mask composed of a logical combination of the enumerated type, ilImgParam, which is defined in the header file il/ilImage.h. The ilImgParam constants defined in IL are listed in Table 6-2. Each image attribute listed in the table is described elsewhere in this guide. Derived classes can add members to this structure to trace whether particular parameter values have changed and to control whether they can be explicitly modified.

Table 6-2. ilImgParam Constants

Defining Class

ilImgParam

Image Attribute

ilImage

ilIPdataType

data type

ilIPorder

pixel ordering

ilIPpageSize

page size

ilIPxsize

x dimension of page size

ilIPysize

y dimension of page size

ilIPzPageSize

z dimension of page size

ilIPxyPageSize

x,y dimension of page size

ilIPcPageSize

component value of a pixel

ilIPpageSize

red values of ilIPzPageSize, ilIPxyPageSize, and ilIPcPageSize

ilIPchans

number of channels

ilIPdepth

z dimension of the image

ilIPorientation

orientation

ilIPcolorModel

color model

ilIPminValue

minimum pixel value

ilIPmaxValue

maximum pixel value

ilIPscale

color scaling value

ilIPfill

fill value

ilIPcompression

compression

ilIPcmap

look-up table color map

ilIPpageBorder

page border for overlapping pages

ilFileImg

ilFPimageIdx

image index

ilOpImg

ilIPbias

bias value

ilIPclamp

clamp value

ilIPworkingType

working data type

ilSubImg

ilIPconfig

configuration

ilImgStat

ilISPzBounds

z dimension bounds

ilRoi

ilROIorientation

orientation


Preventing Attributes From Changing

An image can explicitly disallow any of these attributes to be modified. For this, it uses the clearAllowed() function (from ilLink) and passes in a logical combination of the ilImgParam parameters that should be disallowed.

Another function, isAllowed() (inherited from ilLink), checks whether a particular attribute can be modified:

canChange = myImg.isAllowed(ilIPsize);

This function takes the same sort of argument as clearAllowed() and returns TRUE if the attributes specified are not allowed to be modified.

Setting Altered and Stuck Flags

When an attribute's value is changed by the user (by calling the appropriate attribute setting function), setAltered() (from ilLink) should be called to set a flag indicating that a reset is needed. Thus, you must call setAltered() within any attribute setting functions you define. This function takes a mask of ilImgParam parameters as an argument and sets the altered flags for the specified attributes.

You can check whether any particular attributes have been altered with isAltered() (inherited from ilLink). This function takes an ilImgParam mask as an argument and returns TRUE if any of the specified attributes have been altered.

As explained in “Propagating Image Attributes”, IL programs need to keep track of attributes that have been explicitly set by the user so that they remain fixed during the reset process. To keep track of these attributes, you should call markSet() (inherited from ilLink) with an ilImgParam mask as an argument. This function marks the specified attributes with a stuck flag (yet another item inherited from the ilLink class), which indicates that their values should not be changed during a reset operation. markSet() is invoked automatically for you when setAltered() is called, so generally you do not need to call markSet() yourself.

You can determine whether any attributes are fixed with isSet() (inherited from ilLink). This function returns TRUE if any of the attributes specified in the mask passed in have been explicitly set.

Setting Attributes Directly

Sometimes within a derived class's implementation, you may want to change an attribute's value without triggering the reset mechanism and without causing the value to become fixed. You have already seen one situation where you want to do this: within a constructor, when attributes are being initialized. Another case is when you are computing attribute values during the reset operation itself. In these situations, you do not use a attribute setting function since it calls setAltered(), which in turn calls markSet(). Since derived classes have access to protected data members, simply set the value of the desired attribute directly:

dtype = iflFloat;     // changes value; no flag set

The initMinMax(), initColorModel(), and setStatus() functions described earlier in this section all set attributes directly.

Adding New Attributes

It is quite easy to add attributes to a newly derived class. You can use the header files for the already existing IL classes for examples. This an example is from the il/ilOpImg.h header file:

enum ilOpImgParam {
  ilIPbias = ilImgParamLast<<1,
  ilIPclamp = ilImgParamLast<<2,
  ilIPworkingType = ilImgParamLast<<3,
  ilOpImgParamLast = ilIPworkingType
};

The pattern is simple. Suppose you were to derive a new class from ilOpImg and add parameters to it. You might do the following:

enum ilMyClassParam {
  ilIPparam1 = ilOpImgParamLast<<1,
  ilIPparam2 = ilOpImgParamLast<<2,
  ilIPparam5 = ilOpImgParamLast<<5,
  ilMyClassParamLast = ilIPparam5
};


Deriving From ilCacheImg

The ilCacheImg class implements an abstract model of cached image data. The main purpose of this class is the definition of a common API for cached image objects. You can implement your own caching mechanism by deriving from ilCacheImg. The ilMemCacheImg class, derived from ilCacheImg, provides an example of the implementation of a caching mechanism.

If you derive from ilCacheImg, you must implement the data access methods inherited from ilImage. You must also implement the flush(), getCacheSize(), and listResident() functions if you derive from ilCacheImg.

The flush() function causes any modified data in the cache to be written out. Derived classes that access an image file can call this function in their destructor before they close the file to ensure that all data is written:

virtual ilStatus flush(int discard=FALSE);

The getCacheSize() function returns the amount of cache memory, in bytes, currently allocated by this image object:

virtual size_tgetCacheSize();

The listResident() function returns a list of all the resident pages:

virtual ilStatus listResident(ilCallback* cb);

The callback specified in cb is invoked once for each page resident in memory. The callback function should have prototype as defined in addPagingCallback().

Deriving From ilMemCacheImg

The ilMemCacheImg class implements a caching mechanism for efficiently manipulating image data in main memory. In managing the interface to an image's cache, ilMemCacheImg implements all of the ilImage virtual data access functions. The ilMemCacheImg class also implements the virtual function hasPages(), which is defined in ilImage. hasPages() should return TRUE only for classes that implement IL's paging mechanism (ilMemCacheImg does).

Classes that derive from ilMemCacheImg do not need to implement these functions; instead, they need to implement some or all of the following virtual functions:

virtual ilStatus prepareRequest(ilMpCacheRequest* req);
virtual ilStatus executeRequest(ilMpCacheRequest* req);
virtual ilStatus finishRequest(ilMpCacheRequest* req);
virtual ilStatus getPage(ilMpCacheRequest* req);
virtual ilStatus setPage(ilMpCacheRequest* req);

Image data requests are processed through the multi-processing scheme defined by the ilMpManager and ilMpRequest classes. The virtual functions, prepareRequest(), executeRequest(), and finishRequest(), define the API for multi-processing. To maintain the multi-processing scheme, you must sub-divide processing operations into these three stages:

  • prepareRequest() allocates buffer space for the pages an operator will work on and loads the image data from those pages into the buffer.

  • executeRequest() performs the image manipulation on the pages in the buffer.

  • finishRequest() deallocates the buffer space allocated in prepareRequest() and unlocks the input pages.

Derived classes must re-define these virtual functions. These functions are described in greater detail in “Handling Image Processing”.

The ilMpCacheRequest class (defined in the header file il/ilMemCacheImg.h) defines the page's location within the image and the amount of data to be processed:

class ilMpCacheRequest : public ilMpRequest, public iflXYZCint {
public:
    ilMpCacheRequest(ilMpManager* parent, int x, int y, int z, int c,
                     int mode = ilLMread);

    // methods to access mode fields
    int isRead() { return mode&ilLMread; }
    int isWrite() { return mode&ilLMwrite; }
    int isSeek() { return mode&ilLMseek; }
    int getPriority() { return mode&ilLMpriority; }

    // method to access page data
    void* getData() { return page->getData(); }

    int nx, ny, nz, nc; // size of valid data in page
};

Since an image's size is not generally an exact multiple of the page size, you are likely to encounter pages that are only partially full of data. The nx, ny, nz, and nc members define the actual limits of the data that you need to read or write within a given page buffer. You might want to use the getStrides() function to help you step through a page buffer. See “Data Access Support Functions” for more information about getStrides().

Table 6-3 lists additional attributes you might need to initialize for a class derived from ilMemCacheImg.

Table 6-3. Additional Attributes Needing Initialization in ilMemCacheImg Derived Classes

Name

Data Type

Meaning

pageSizeBytes

size_t

size of a page in bytes

pageSize

iflSize

pixel dimensions of the pages used to store data on disk

pageBorder

iflXYZint

pixel dimensions of page borders as stored on disk (default is zero)

You can also implement the allocPage() and freePage() functions. These functions allocate or free a page in main memory whose pixel includes (x,y,z,c). If you implement the function allocPage(), you must also call the function doUserPageAlloc() in the function that calls allocPage() to notify IL that the pages need to be defined.

The flush() function (defined by ilMemCacheImg) flushes data from an image's cache; it calls setPage() to ensure that the data is written to the proper place:

virtual ilStatus flush(int discard=FALSE);

This function takes one optional argument and returns an ilStatus to indicate whether the flush was successful. Calling flush() with a TRUE argument discards all data in the cache. This is useful for freeing up memory if you know you are never going to use the cached data again. When discard is FALSE, flush() writes any modified data from the cache to the image. The destructor for any class derived from ilMemCacheImg may need to call ilMemCacheImg's flush() (with discard equal to FALSE) before the class object is deleted to ensure that any modified data is written back to the image.

For more information about deriving from either of ilMemCacheImg's derived class ilOpImg, see “Implementing an Image Processing Operator”.

Implementing an Image Processing Operator

IL is designed to be easily extendable in C++ to include image processing algorithms you implement. You can derive a new operator directly from ilOpImg, or you can take advantage of the support provided by its subclasses, some of which are specifically designed to be derived from. This section explains in detail how to derive your own operator. It contains these sections:

The subclasses of ilOpImg handle the tasks of reading raw data from the cache and writing processed data back to the cache; if you derive from these classes, you are responsible for writing only the function that processes the data in a given input buffer and writes it to a given output buffer. If you derive directly from ilOpImg, you need to supply your own interface to the cache as well as your processing algorithm. Figure 6-2 shows the operator classes you are most likely to derive from.

Figure 6-2. ilOpImg and Its Subclasses for Deriving

Figure 6-2 ilOpImg and Its Subclasses for Deriving

Remember that when you derive from a class, you inherit all of its public and protected data members and member functions. You also inherit members from its superclasses. You should review the header file and the reference page for any class you plan to derive from (as well as the header file and reference pages of its superclasses) to become familiar with its data members and member functions. It is also a good idea to look at a few of its subclasses to see what general tasks they perform and what functions they implement. Finally, you might want to take a look at the selected IL source code that is provided online in /usr/share/src/il/src.

The next section contains information that is useful whether you derive directly from ilOpImg or from one of its subclasses. The sections that follow contain more detailed information about deriving from each of ilOpImg's subclasses shown in Figure 6-2.

Deriving From ilOpImg

A class derived from ilOpImg needs to implement these member functions:

  • The constructor, which creates the object, declares which data types and pixel orders are valid for the output, and sets the working data type.

  • resetOp(), which adapts to any attributes that have been altered, such as changing the input image

  • keepPrecision(), which maintains the data type of a returned value.

  • prepareRequest(), which queues the data accessed from the input image(s) for a requested page of the operator. It also allocates the buffer(s) to hold the input image data.

  • executeRequest() which performs the operator's processing when the input data is loaded. The result is placed directly in a page of the operator's cache.

  • finishRequest() frees any resources allocated in prepareRequest(). This is separate from executeRequest() so that aborted operations that have already done prepareRequest() can clean up without bothering with the work done in executeRequest().

  • Any public setParam() and getParam() parameter set or get functions provided to control the operator's algorithm.

You also need to implement a destructor if you allocate any memory or change state within the constructor or any other function you implement. Example 6-1 shows a typical header file for an ilOpImg subclass.

Example 6-1. Typical Header for a Class Derived From ilOpImg


#include <il/ilOpImg.h>
class myOperator : public ilOpImg {
public:
    myOperator(ilImage* img, float param1);
    void setParam1(float val)
               { param1 = val; setAltered(); }
    float getParam1()
               { resetCheck(); return param1; }
};
protected:
    void resetOp();
    ilStatus prepareRequest(ilMpCacheRequest *req);
    ilStatus executeRequest(ilMpCacheRequest *req);
    ilStatus finishRequest(ilMpCacheRequest *req);

private:
    float param1;

The resetOp() function should be declared protected if other programmers are likely to want to derive a class from the myOperator class.

The Constructor

The constructor takes a pointer to the source ilImage(s) and additional arguments as needed to provide parameters to control the operator's processing algorithm (for example, param1). If you do use additional parameters, you might want to define corresponding functions that allow the user to alter and retrieve the value of those parameters (such as setParam1() and getParam1()). These functions should probably take advantage of IL's reset mechanism by calling setAltered() and resetCheck(), respectively. (See “The reset() Function” for more information about how IL's reset mechanism works.) Example 6-2 shows you what a simple constructor might look like.

Example 6-2. Typical Constructor for a Class Derived From ilOpImg


myOperator::myOperator(ilImage* img=NULL, float param1=Param1Default) 
{
    setValidType(iflFloat|iflDouble);
    setValidOrder(iflInterleaved|iflSequential|iflSeparate);
    setWorkingType(iflDouble);
    setNumInputs(1);
    setInput(img);
    setParam1(param1);
}

In this example, myOperator can produce output of either iflFloat or iflDouble data type; the output has the same pixel ordering as the input image. Input image data that is of type iflFloat is cast to iflDouble before it is processed; this is the meaning of an operator's working type. Some operators can handle multiple inputs, but the setNumInputs() function is used here to limit myOperator to one input. The setInput() function sets the input to be the ilImage passed in; this step chains myOperator to the input image. Finally, param1's value is initialized.

The setValidType(), setValidOrder(), and setWorkingType() functions are all defined as protected in ilOpImg. They are discussed in more detail in ilOpImg's reference page. The ilImage class defines setNumInputs() (protected) and setInput().

The constructor should not contain any calculations that are based on the value of arguments passed in, since these arguments might change. Most operators that require arguments other than the input image in their constructors define functions for dynamically changing the value of those arguments (like setParam1()). Such calculations should be done in the resetOp() function described below. The resetOp() function is declared in ilOpImg, but its implementation is left to derived operators. Note that when any ilImage is created, it is considered “altered,” so resetOp() is always called before any data is computed.

The resetOp() Function

Since resetOp() is guaranteed to be called before prepareRequest(), executeRequest(), and finishRequest(), it can—and should—be used to calculate the values of variables needed by these methods, particularly if those variables depend on arguments passed in the operator's constructor. The resetOp() function also needs to reset any image attributes that change as a result of the image's data being processed, so that the proper attribute values can be propagated down an operator chain. As an example, imagine an operator that defined the following variables (probably as protected) in its header file (ilMonadicImg defines these variables):

iflXYZCint str;         // output (page) buffer strides
iflXYZCint istr;        // input image strides
int bufferSize;        // size of input buffer in bytes
int cBuffSize;         // number of channels in input buffer

As you might expect, these variables are used to determine the size of the internal buffer needed for reading in the image's data that is to be processed. This buffer is actually allocated in prepareRequest(), but the values for these variables are calculated in resetOp(), since they depend on the input image's page size and data type attributes. Example 6-3 illustrates this with ilMonadicImg's implementation of resetOp(). (The iflXYZCint struct holds four integers, one for each of an image's dimensions; see “Convenient Structures” for more information.)

Example 6-3. The resetOp() Function of ilMonadicImg


ilMonadicImg::resetOp()
{
    // make sure we have a valid input
    ilImage* img = getInput();
    if (img==NULL || getOrder() == iflSeparate && getCsize() != img->getCsize())
        { setStatus(ilStatusEncode(ilBADINPUT)); return; }

    // make sure page size info is in sync with color model/number channels
    checkColorModel();

    // determine whether or not we can use lockPage on our input
    int cps, icps;
    iflXYZint pgSize, pgDel, ipgSize, ipgDel;
    getPageSize(pgSize.x, pgSize.y, pgSize.z, cps);
    getPageDelta(pgDel.x, pgDel.y, pgDel.z, cps);
    img->getPageSize(ipgSize.x, ipgSize.y, ipgSize.z, icps);
    img->getPageDelta(ipgDel.x, ipgDel.y, ipgDel.z, icps);
    iflOrder inord = img->getOrder();
    usesIstr = 0; // XXX not supported yet
    useLock = !inPlace && (usesIstr || pgSize == ipgSize && pgDel == ipgDel) &&
              (cps == icps || cps == size.c && icps == img->getCsize()) && 
              img->getDataType() == wType && 
              img->getOrientation() == orientation &&
              (order == inord || 
               usesIstr && (order == iflSeparate) == (inord == iflSeparate));

    // get buffer strides
    getStrides(str.x, str.y, str.z, str.c);
    if (useLock) 
        img->getStrides(istr.x, istr.y, istr.z, istr.c);
    else 
        img->getStrides(istr.x, istr.y, istr.z, istr.c, 
                        pgSize.x, pgSize.y, pgSize.z, icps, getOrder());
}

As shown, the resetOp() function performs three tasks:

  • makes sure the input is valid

  • determines whether to use lockPage() or getTile()

  • computes the stride parameters used in most calcPage() implementations

The size of the internal buffer depends on the operator's working data type, on its page size, and on the input image's channel stride. Note that for this operator, the input and output buffers are the same size. (All the functions used in this example are described in Chapter 2, “The ImageVision Library Foundation,” except for iflDataSize(), which is described in the reference pages.) In this example, none of the image's attributes change as a result of this operator's image processing algorithm. An example of an operator that does change attributes is ilRotZoomImg, which changes the image's size, unless the user has explicitly specified a desired size:

if (!isSet(ilIPsize)) {
    // calculate newXsize and newYsize
    size.x = newXsize;
    size.y = newYsize;
}

Notice that the attributes are set directly; the setSize() function is not used since it would flag the size attribute as having been altered. You can use isDiff() to determine whether any parameters changed as a result of propagation. This function takes a mask of ilImgParam values and returns TRUE if any of the specified attributes changed.

The keepPrecision() Function

When keepPrecision() is enabled, the data type of an operator's input is maintained. If it is not enabled, the data type of the operator's input is translated into the smallest possible data type. If you disable this function, it is possible that non-integral operator input, such as float, will be cast into an integral data type, such as char. For example, if keepPrecision() is disabled, and the operator's input is really float values in the range of 0.0 and 1.0, the operator will change the data type of the range to char.

keepPrecision() is enabled by default.

To determine whether or not an operator maintains non-integral data types, use the isPrecisionKept() function.

Handling Image Processing

When deriving your own operator, it is important to follow the ilMpRequest procedure for operating on images. If you are deriving an operator from some class other than ilOpImg, such as the ilOpImg-derived classes ilMonadicImg, ilDyadicImg, or ilPolyadicImg, you can use the calcPage() method defined in those classes to operate on the requested pages in a buffer. If you derive an operator from ilOpImg, however, you cannot use calcPage(), nor can you use the pre-3.0 version of the ilOpImg::getPage() method. Instead, you must use the three-step process for operating on images summarized by three virtual functions in ilOpImg: prepareRequest(), executeRequest(), and finishRequest().

prepareRequest Phase

In your derived operator, you must override the prepareRequest() method so that it

  • allocates buffer space for the input image to the operator

  • asynchronously reads in image data into the buffer that will be processed by the operator

There are two ways to read the image data into a buffer:

  • Use qLockPageSet() if the operator can use the data of the stored image directly.

  • Use qGetTile3D() or qGetSubTile3D() if the operator cannot use the data of the stored image directly.

If the page size of the stored, input image matches that of the output image, and the operator can use the data type of the stored, input image directly, you can use qLockPageSet() to directly fill the buffer. qLockPageSet() returns a pointer to the page of the input image in cache. The advantage of using qLockPageSet() is that it avoids copying the image data.

If you cannot use qLockPageSet(), you use the asynchronous methods qGetTile3D() or qGetSubTile3D() to fill the buffer. These methods get a tile, change the data type, allocate the buffer, and fill it.


Note: Do not use the synchronous versions of these methods, GetTile3D() and GetSubTile3D() in the prepareRequest phase.

You only use qGetSubTile3D() if the page of image data to be operated on extends beyond the boundary of the image, as shown in Figure 6-3.

Figure 6-3. Using qgetSubTile3D()

Figure 6-3 Using qgetSubTile3D()

Each rectangle in the figure represents a page of image data. The pages on the right-side border spill beyond the image boundary. Loading the part of the page that lies outside of the image boundary is unnecessary and time consuming. Rather than loading the entire page, you use qGetSubTile3D() to load only that portion of the page that lies within the image boundary.

Whether you use qLockPageSet() or qGetSubTile3D() to read in the data, you pass them the cache request mentioned in the argument of prepareRequest() as the parent, for example:

ilMonadicImg::prepareRequest(ilMpCacheRequest* req)
...
ilMpMonadicRequest* r = (ilMpMonadicRequest*)req;
...
sts = im->qGetSubTile3D(r, r->x, r->y, r->z, r->nx, r->ny, r->nz,
                                r->in, r->x, r->y, r->z, 
                                pageSize.x, pageSize.y, pageSize.z,                                 &cfg);

executeRequest Phase

You override the ilOpImg::executeRequest() method to perform the image operation on the loaded image data. If, for example, you were writing a new sharpen operator, the executeRequest() method would implement the sharpening of the image data.

finishRequest Phase

You override the ilOpImg::finishRequest() method to deallocate the buffer space used by the image data and to unlock any pages locked (set by qLockPageSet()) in the prepareRequest() method. You enter the finishRequest phase either because the executeRequest() method completes or because the operation was aborted.

Image Processing Example

Example 6-4 shows what a request-processing implementation might look like under this model.

Example 6-4. A Request-Processing Implementation for a Class Derived From ilOpImg


ilStatus
ilMonadicImg::prepareRequest(ilMpCacheRequest* req)
{
    // do not proceed if things look bad
    if (status != ilOKAY) return status;

    // get the input image to read data from
    ilImage* im = getInput(0);
    assert(im != NULL);

    ilMpMonadicRequest* r = (ilMpMonadicRequest*)req;

    // queue request for the input data, either lockPage or getTile
    ilStatus sts;
    if (useLock) {
        // doing lockPage, the page in the input image is the input buffer
        r->lck.init(r->x, r->y, r->z, r->c);
        sts = im->qLockPageSet(r, &r->lck);
    }
    else {
        // doing getTile: if in place use our own page as destination, otherwise
        //                allocate an input buffer
        int nc = im->getCsize();
        if (order == iflSeparate && nc == getCsize()) nc = getPageSizeC();
        ilConfig cfg(wType, order, nc, NULL, r->c, getOrientation());
        if (inPlace) r->in = r->getData();
        sts = im->qGetSubTile3D(r, r->x, r->y, r->z, r->nx, r->ny, r->nz,
                                r->in, r->x, r->y, r->z, 
                                pageSize.x, pageSize.y, pageSize.z, &cfg);
    }

    return sts;
}

ilStatus
ilMonadicImg::executeRequest(ilMpCacheRequest* req)
{
    // do not proceed if things look bad
    if (status != ilOKAY) return status;

    ilMpMonadicRequest* r = (ilMpMonadicRequest*)req;

    // find the input buffer, 
    void* src;
    if (useLock) {
        // doing lock page, input page is the input buffer
        if (!r->lck.isLocked()) return r->lck.getStatus();
        src = r->lck.getData();
    }
    else
        // normal getTile, data was read into allocated buffer (or in place)
        src = r->in;

    // let the real operator code in derived class do it is thing
    return calcPage(src, r->getData(), *r);
}

ilStatus
ilMonadicImg::finishRequest(ilMpCacheRequest* req)
{
    ilMpMonadicRequest* r = (ilMpMonadicRequest*)req;

    // free up any allocations or locks
    
    if (r->in && !inPlace) {
        // junk the input buffer
        delete r->in;
    }
    else if (r->lck.getPage() != NULL) {
        // unlock the page
        ilImage* im = getInput(0);
        assert(im != NULL);
        im->unlockPageSet(&r->lck);
    }
        
    return ilOKAY;
}

The calcPage() function implements the image processing algorithm, taking care to handle each valid data type appropriately. For example, Example 6-5 shows how ilAddImg computes the pixelwise sum of two images.

Example 6-5. Computing the Pixelwise Sum of Two Images


#define doAdd(type) \
if (1) { \
    type tb = type(bias); \
    if (numIn == 2) { \
        void *ib0 = ib[0], *ib1 = ib[1]; \
        for (; idx < lim; idx += sx) \
            ((type*)ob)[idx] = ((type*)ib0)[idx]+((type*)ib1)[idx] + tb; \
    } else \
        for (; idx < lim; idx += sx) { \
            type sum = tb; \
            for (int in=0; in < numIn; in++) \
                sum += ((type*)ib[in])[idx]; \
            ((type*)ob)[idx] = sum; \
        } \
} else


ilStatus
ilAddImg::calcPage(void** ib, int numIn, void* ob, ilMpCacheRequest& req)
{
    // for interleaved case: combine x/c loops to improve performance
    int nc = req.nc, sc = str.c, nx = req.nx, sx = str.x;
    if (sc == 1 && sx == nc) { nx *= nc; nc = 1; sx = 1; sc = 0; }

    for (int z = 0; z < req.nz; z++) {
        for (int y = 0; y < req.ny; y++) {
            for (int c = 0; c < nc; c++) {
                int idx = z*str.z + y*str.y + c*sc, lim = idx + nx*sx;
                switch (dtype) {
                    case iflUChar:   doAdd(u_char); break;
                    case iflUShort:  doAdd(u_short); break;
                    case iflShort:   doAdd(short); break;
                    case iflLong:    doAdd(long); break;
                    case iflFloat:   doAdd(float); break;
                    case iflDouble:  doAdd(double); break;
                }
            }
        }
    }

    return ilOKAY;
}

Since ilAddImg is derived from ilPolyadicImg, this function uses ilPolyadicImg's stride data members—str.x, str.y, str.z, and str.c—to step through the data.

Because IL programs can be multi-threaded, the prepareRequest(), executeRequest(), finishRequest(), and calcPage() functions should not alter any member variables or do anything else that would make the algorithm non-reentrant. For example, the input buffer used by prepareRequest() is allocated locally and stored as a member of the request, rather than as a member in resetOp() so that concurrent execution of prepareRequest() uses unique buffers for the different portions of the input image at the same time.

Clamping Processed Data

Some operators might trigger overflow or underflow conditions as they process data. To solve this potential problem, you should set clamp values that will then be used automatically when overflow or underflow arises, as described below.

In your implementation of resetOp(), call setClamp():

void setClamp(iflDataType type = numilTypes); 
void setClamp(double min, double max);

This function sets the values that pixels will be clamped to if underflow or overflow occurs. The first version sets the clamp values to be the minimum and maximum values allowed for the data type type; the default value of numilTypes means to use the operator's current data type. The second version allows you to specify actual clamp values.

In the calcPage() function, use the initClamp() macro, passing in the operator's data type (for example, int or float). This macro initializes two temporary variables to hold the minimum and maximum clamp values. Then, after you process each pixel of data, call the clamp() macro and pass in the processed pixel value. This function clamps the pixel value, if necessary, to the minimum or maximum clamp value.

To allow a user to set clamp values, you need to add ilIPclamp to the ilImgParam mask passed to setAllowed() in the constructor.

Setting Minimum and Maximum Pixel Values

Another problem that might arise as a result of processing data is that the processed values might exceed the range of values. For example, if you multiply two images (the pixel values of which fall in the 0 to 255 range) and then display the result, you might end up with pixel data that appears to be invalid if the pixel values exceed 255. To solve this potential problem, operators that alter the data range of their inputs need to set the minValue and maxValue data members (inherited from ilImage) to ensure that the processed data can be displayed. When the data is displayed using ilDisplay, it is automatically scaled between these values so that a meaningful display is produced.

Here is how ilAddImg computes minValue and maxValue in its resetOp() function (ilAddImg performs pixelwise addition on two images; a user-specified bias value can also be added to each pixel of the output):

// compute worst case min/max values
double min = getInputMin(0) + getInputMin(1);
double max = getInputMax(0) + getInputMax(1);
setStatus(checkMinMax(min+bias, max+bias));

The getInputMin() and getInputMax() functions return the minimum and maximum pixel value attributes of the input image. The argument for these functions is the index of the desired image in the list of inputs (the first input is at index 0). These values are added (since that is what ilAddImg does), combined with the bias value, and then passed to checkMinMax(). This function first attempts to set the operator's data type to the smallest supported data type that can hold the range specified by its arguments. If the data type is explicitly set by the user, however, it will not be changed. Then, if minValue and maxValue are not explicitly set, they are set to the values passed to checkMinMax(). If checkMinMax() returns ilUNSUPPORTED, it is not able to change the data type to support the range; in this case, minValue and maxValue are set to the maximum range of the current data type.

Deriving From ilMonadicImg or ilPolyadicImg

Both ilMonadicImg and ilPolyadicImg follow the getPage()/calcPage() model described above. These two classes provide support for operators that take a single input image (ilMonadicImg) or multiple input images (ilPolyadicImg) and operate on all pixels of the input image data. Table 6-4 shows the classes that derive from ilMonadicImg and ilPolyadicImg.

Table 6-4. Classes Derived from ilMonaDicImg and ilPolyadicImg

Classes That Derive from
ilMonadicImg

Classes That Derive from
ilPolyadicImg

ilAbsImg

ilAddImg

ilFalseColorImg

ilANDImg

ilFFiltImg

ilBlendImg

ilInvertImg

ilDivImg

ilNegImg

ilMaxImg

ilThreshImg

ilMinImg

ilColorImg (& subclasses)

ilMultiplyImg

ilLutImg (& subclasses)

ilORImg

ilScaleImg (& subclasses)

ilSubtractImg

 

ilXorImg

Here are some things you need to keep in mind if you derive from either of these classes:

  • Do not redefine prepareRequest(), executeRequest(), or finishRequest(); use the version defined in ilMonadicImg or ilPolyadicImg. Just implement your algorithm in calcPage().

  • If you redefine resetOp(), call the superclass version in your resetOp() (so that buffers and page sizes are reset appropriately):

    // either 
    ilMonadicImg::resetOp();
    // or
    ilPolyadicImg::resetOp();
    

  • Use setWorkingType() if you want the input buffer to be read in as a type different from the operator image's data type. Note that the output buffer always uses the operator's data type.

Example 6-5 shows that ilAddImg's implementation of calcPage() takes three arguments. Similarly, ilMonadicImg's calcPage() function takes three arguments:

virtual ilStatus calcPage(void* inBuf, void* outBuf, 
               ilMpCacheRequest& req) = 0;

inBuf is the input buffer of data that needs to be processed, outBuf is the output buffer into which the processed data should be written, and req is the request that describes the page of data being processed. Your implementation of calcPage() (for any class derived directly or indirectly from ilMonadicImg) must accept this argument list.

Since ilPolyadicImg processes more than one input image at a time, its calcPage() function supplies an array of input buffers. As above, your implementation of calcPage() must accept this argument list:

virtual ilStatus calcPage(void* inBuf1, void* outBuf,                  ilMpCacheRequest& req) = 0;

When you derive from a class, you inherit all of its public and protected data members and member functions. All the public members for ilMonadicImg and ilPolyadicImg have been discussed in previous sections. The protected member functions are resetOp(), getPage(), and calcPage(). For reference purposes, here are ilMonadicImg's protected data members:

iflXYZCint str;     // output (page) buffer strides
iflXYZCint istr;    // input image strides
int bufferSize;    // size of input buffer in bytes
int cBuffSize;     // number of channels in input buffer

The protected data members defined in ilPolyadicImg are similar:

iflXYZCint str;              // output buffer strides
iflXYZCint istr1, istr2;     // input image strides
int buffSize1, buffSize2;   // size of input buffers in bytes
int cBuffSize1, cBuffSize2; // number of channels in input
//  buffers

Deriving From ilArithLutImg

As an abstract class, ilArithLutImg defines how to use look-up tables when performing arithmetic or radiometric operations. To derive from it, you implement your algorithm in calcRow() rather than in calcPage():

void calcRow(iflDataType intype, void *inBuf, void *outBuf, 
              int sx, int lim, int idx);

The intype parameter indicates the input image's data type. The next two arguments are the input buffer of data that needs to be processed and the output buffer into which processed data should be written. The next three arguments specify how to step through the data: sx is the x stride of the output buffer, lim is the maximum x stride, and idx is the starting index. The calcRow() function contains the algorithm for processing one row of input data. For efficiency, you can use the defined macro doRow() to obtain the proper data type and feed it to the macro doCalc(). (The doRow() macro is defined in ilArithLutImg's header file.) If you use these macros, your calcRow() definition would be just a call to doRow():

ilMyOpImg::calcRow(iflDataType inType, void* inBuf, 
               void* outBuf,int sx, int lim, int idx)
{ doRow(); }

and you would actually implement the computation algorithm in the macro doCalc(), as ilPowerImg does, for example, as shown in Example 6-6.

Example 6-6. Implementation of ilArithDoCalc() in ilPowerImg


#define ilArithDoCalc(outype, intype) \
if (1) { \
    if (inType == iflDouble || dtype == iflDouble)  { \
        for (; x < lim; x += sx) \
            ((outype*)outBuf)[x] = \
                (outype)pow((double)((intype*)inBuf)[x]*scale+bias, power); \
    } \
    else { \
        for (; x < lim; x += sx) \
            ((outype*)outBuf)[x] = \
                (outype)powf((double)((intype*)inBuf)[x]*scale+bias, power); \
    } \
} else

You also need to implement loadLut() to compute and load the appropriate values into the LUT. Example 6-7 shows ilPowerImg's version of loadLut().

Example 6-7. Implementation of loadLut() in ilPowerImg


void
ilPowerImg::loadLut()
{
    double low, high;
    lut->getDomain(low,high);
    double dstep = lut->getDomainStep();
    double lim = high+dstep/2;
    for (double i = low; i < lim; i += dstep)
        lut->setVal(pow(i*scale + bias, power), i);
}

For your convenience, ilArithLutImg has functions for scaling and biasing the input data before the LUT is applied:

void setScale(double scale);
double getScale();
void setBias(double bias);
double getBias();

Deriving From ilHistLutImg

The ilHistLutImg class provides support for operators that compute a look-up table from the histogram of the source image and then apply this table to the source image. It derives from ilArithLutImg and implements its own versions of calcPage(), calcRow(), and loadLut(). The only pure virtual function in ilHistLutImg is calcBreakpoints(), which all derived classes must implement:

virtual ilStatus calcBreakpoints(ilImage *src, ilImgStat *imgstat, 
    double **brPoints) = 0;

This function computes the breakpoints (brPoints) of a piecewise LUT. You can think of it as a pointer to a two-dimensional array whose members can be accessed by

double val = brPoints[i][j] where:
    i = 0,1,2,...,nc-1 
    j = 0,1,2,...,nbinsi 
    nc = number of channels in the source image
    nbinsi = number of bins in the histogram of channel i 

You can obtain the number of bins by using imgstat's getNbins() function. The variable val in the example shown above represents what the pixel intensity represented by the jth bin of the histogram for channel i maps to. For example, to invert pixel intensities of an image containing unsigned char data, you can use

brPoints[i][j] = 255-j;

All the members of brPoints need to be evaluated in calcBreakpoints(), using both the source image and a pointer to its associated data as inputs. Derived classes do not need to allocate and manage memory for brPoints, since ilHistImg does this for them. In addition, ilHistImg provides convenience functions for setting the ilImgStat and ilRoi objects:

void setImgStat(ilImgStat *imgstat);
void setRoi(ilRoi *roi, int xoffset=0, int yoffset=0);

If you implement resetOp() in a derived class, be sure to explicitly call ilHistLutImg's version of resetOp().

An example of a class derived from ilHistLutImg might be an operator called ilPixelCountImg, which replaces each pixel intensity by the number of times it occurs in that particular channel. Such an operator might be implemented as shown in Example 6-8.

Example 6-8. A Class Derived From ilHistLutImg to Count Pixels


class ilPixelCountImg:public ilHistLutImg {
    private: 
        ilStatus calcBreakpoints (ilImage *src, 
               ilImgStat *imgstat, double **brPoints);
    public: 
        ilPixelCountImg(ilImage *src);
}
ilPixelCountImg::ilPixelCountImg(ilImage *src)
:ilHistLutImg(src)
{
}
ilStatus calcBreakpoints (ilImage *src, ilImgStat *imgstat, 
        double **brPoints)
{
    if (src==NULL) return ilBADINPUT;
    int nch=src->getNumChans();
    for (int i=0; i<nch ; i++) {
        int *hist = imgstat->getHist(i);
        int nbins = imgstat->getNbins(i);
        int total = imgstat->getTotal(i);
        double max = src->getMaxValue(i);
        for (int j=0; j<nbins; j++) {
            brPoints[i][j]=(hist[j]*max)/total;
        }
    }
    return ilOKAY;
}

Deriving From ilSpatialImg

The ilSpatialImg class provides basic support for operators that adjust a pixel's value based on a weighted sum of its surrounding pixels. The kinds of operators that can use this support perform convolutions for particular purposes—for example, they calculate gradients or perform rank filtering. Table 6-5 shows ilSpatialImg's subclasses.

Table 6-5. ilSpatialImg's Subclasses

ilSepConvImg

ilSepConvImg
Subclasses

RankFltImg
Subclasses

ilLaplaceImg

ilBlurImg

ilMaxFltImg

ilRobertsImg

ilCompassImg

ilMedFltImg

ilSobelImg

ilSharpenImg

ilMinFltImg

The ilSpatialImg class follows the same getPage()/calcPage() model as ilMonadicImg does. All the following hints are also true about deriving from ilSpatialImg (and any of its subclasses):

  • Do not redefine prepareRequest(), executeRequest(), or finishRequest(), just implement your algorithm in calcPage().

  • If you redefine resetOp(), call the superclasses in your resetOp() (so that buffers and page sizes are reset appropriately):

    ilSpatialImg::resetOp();
    

  • Use wType as the working data type, but be sure the data you write into the output buffer is of type dType.

The calcPage() function for ilSpatialImg takes these arguments:

virtual ilStatus calcPage(void* inBuf, void* outBuf, 
        iflXYZCint start, iflXYZCint end) = 0;

The input buffer inBuf points to a buffer containing the data that needs to be processed, and outBuf points to a page in the cache where the processed data should go. Depending on the edge mode, some of the data in inBuf may have been set to the image's fill value. (Refer to “Spatial Domain Transformations” for further explanation of the possible edge modes.) start and end demarcate the beginning and the end of source data in inBuf that needs to be computed, so you should use them to delimit the computation.

ilSpatialImg provides several protected member variables that are likely to be useful as you implement your algorithm. These include strides, for use in stepping through the input and output buffers:

iflXYZCint inStr;    // input strides
iflXYZCint outStr;   // output strides

The iflXYZCint struct holds four integers; for more information about it, see “Convenient Structures”. ilSpatialImg also constructs a kernel offset table and a kernel value table based on the data in the kernel. The offset table contains offsets into the input buffer to access data corresponding to nonzero kernel elements. The value table contains the nonzero elements and corresponds to the offset table. These data members are shown below:

ilKernel* kernel;   // kernel object
int kernSz;         // number of nonzero kernel elements
int* kernOff;       // kernel offset table
void* kernVal;      // kernel value table

You can use these tables to improve the efficiency of your algorithm—for example, by avoiding multiplications by 0. A related function, setKernFlags(), allows you to set flags indicating that the offset table and/or value table must be created:

void setKernFlags(int of=0, int vf=0);

If you pass in a 1 for either the offset flag of or the value flag vf, the corresponding table will be created to match the current kernel. You should call this function in the constructor of your class (with ones as arguments) so that the tables are built.

The following code might be part of a calcPage() implementation for a convolution. It shows how kernel values multiply data values and how this result is accumulated. It also demonstrates how inBuf, outBuf, and the kernel are offset with respect to one another. This example is a bit simplified in that it assumes both wType and dtype are iflFloat, and it assumes that the kernel weights sum to 1.0 so that no clamping is necessary. Also, if you actually need to implement a convolution-based algorithm, consider deriving from ilConvImg, as described in Example 6-9.

Example 6-9. A Class Derived From ilConvImg to Multiply and Accumulate Data


// cast the buffers to be of type wType
float* in = (float* )inBuf;
float* out = (float* )outBuf;
// iterate through all channels
for (int ci = start.c; ci < end.c; ci++) {
    int cSrcIndex = ci*inStr.c;
    int cDstIndex = ci*outStr.c;
// iterate through z dimension
for (int zi = start.z; zi < end.z; zi++) {
    int zSrcIndex = zi*inStr.z + cSrcIndex;
    int zDstIndex = zi*outStr.z + cDstIndex;
// iterate through y dimension
for (int yi = start.y; yi < end.y; yi++) {
    int srcIndex = start.x*inStr.x + yi*inStr.y + zSrcIndex;
    int dstIndex = start.x*outStr.x + yi*outStr.y + zDstIndex; 
    // iterate through x dimension 
    for (int xi = start.x; xi < end.x; 
        xi++, srcIndex += inStr.x, dstIndex += outStr.x) {
        float sum = bias;						// bias is inherited from ilOpImg
        // cast kernVal to a float
        float* kr = (float* )kernVal;
        //iterate through nonzero kernel values
        for (int k = 0 ; k < kernSz ; k++) {
               sum += in[srcIndex+kernOff[k]] * kr[k];
        }
  // note use of kernOff to access the correct input value
        out[dstIndex] = sum; 
    }
  }
 }
}

Deriving From ilConvImg or ilSepConvImg

The ilConvImg class performs general convolution on an image, and the ilSepConvImg class performs separable convolution. You might want to derive from these classes if kernel values are not available at the time the operator is constructed because they depend on certain input parameters. In this case, you would define a resetOp() function in the derived class that computes the x and y kernel values from input parameters. Then you could use the inherited functions setXKernel(), setYKernel(), and setKernelSize() to specify the kernel and its size, after which you would need to explicitly call ilConvImg's or ilSepConvImg's version of resetOp(). Remember that the kernel for ilConvImg should be a two-dimensional matrix, while that for ilSepConvImg should be two separate vectors. You should also set the edge mode and bias value.

Deriving New Classes From ilWarpImg and ilWarp

ilWarpImg is an abstract, base class derived from ilOpImg. ilWarpImgprovides basic support for warping an image using up to seventh-order polynomials. Often, users know the kind of warp effect they want to achieve, but they do not know how to specify coefficients to achieve this effect. The two operators that derive from ilWarpImg—ilRotZoomImg and ilTieWarpImg—provide the user with an indirect way of specifying the coefficients. For example, ilRotZoomImg lets you specify an angle of rotation, and then it performs the work necessary to compute the coefficients needed to achieve the rotation.

There are three reasons for deriving your own warp operator:

  • You need a warping algorithm that uses higher-order polynomials (eighth-order and above).

  • You want to define a new way of specifying the warping coefficients.

Different types of warps are defined by deriving from ilWarp.

Deriving New Classes From ilWarp

The ilWarp class encapsulates general 3D coordinate transformations for use by ilWarpImg and its subclasses. A particular warp is defined by overriding the x(), y(), and z() virtual functions:

virtual float x(float u, float v=0,float w=0);
virtual float y(float u, float v=0, float w=0);
virtual float z(float u, float v=0, float w=0);

The x() function evaluates the x component of the warp function at a point. The default implementation is to return u.

The y() virtual function evaluates the y component of the warp function at a point. The default implementation is to return v.

The z() virtual function evaluates the y component of the warp function at a point. The default implementation is to return w.

Any derived warp class that transforms any of the x, y, or z components should overwrite the corresponding virtual function.

Deriving From ilFMonadicImg or ilFDyadicImg

The ilFMonadicImg and ilFDyadicImg classes provide the basic support for operators that perform pixelwise computations on images that have been converted to the frequency domain. To implement a frequency domain filter, derive from ilFFiltImg, as explained in “Deriving From ilFFiltImg” (or use ilFMultImg). Both ilFMonadicImg and ilFDyadicImg expect the input image(s) to be in the format produced by ilRFFTfImg. As their names suggest, ilFMonadicImg expects a single input image, and ilFDyadicImg expects two input images. Table 6-6 shows their subclasses. 

Table 6-6. The Subclasses of ilFMonadicImg and ilfDyadicImg

ilFMonadicImg's Subclasses

ilFDyadicImg's Subclasses

ilFConjImg

ilFCrCorrImg

ilFRaisePwrImg

ilFDivImg

ilFSpectImg

ilFMultImg

ilFFiltImg

 

Both classes implement prepareRequest(), executeRequest(), or finishRequest() functions for you so that you have to implement your algorithm only in cmplxVectorCalc(). This function processes a vector of complex values; executeRequest() calls it as needed to process an entire page of data. The calling sequence for ilFMonadicImg's cmplxVectorCalc() is shown below:

virtual void cmplxVectorCalc(float* vect, 
              int rr, int ri, int size);

The first argument, vect, is a pointer to a vector of size number of complex values. On input, vect holds the data to be processed, and on output it holds the processed data. Use rr and ri to step through this vector: rr is the stride between the real parts of two consecutive complex numbers in vect, and ri is the stride between the real and imaginary part of a complex number in vect.

An example of a class derived from ilFMonadicImg would be an operator that converts rectangular coordinates to polar coordinates. Such an operator would need to declare only two member functions:

class ilFPolarImg : public ilFMonadicImg {
protected:
    void cmplxVectorCalc(float* vect, int rr, int ri, 
               int size);
public:
    ilFPolarImg(ilImage* src);
}

In this example, cmplxVectorCalc() is declared protected since it is assumed that ilFPolarImg will have subclasses. Example 6-10 shows how the constructor and cmplxVectorCalc() functions might be implemented.

Example 6-10. Constructor and Member Functions of a Class Derived From ilFMonadicImg to Convert Coordinates


ilFPolarImg::ilFPolarImg(ilImage* src1)
{
    setValidType(iflFloat);
    addValidOrder(iflSeparate);
    setNumInputs(1);
    addInput(src1);
}
void
ilFPolarImg::cmplxVectorCalc(float* vect, int rr, int ri, 
int size)
{
 int i, k;
 for (i = k = 0; k < size; i += rr, k++) {
    float real = vect[i];
    float imag = vect[i + ri];
    vect[i] = fsqrt (real*real + imag*imag);
    vect[i+ri] = fatan2 (imag, real);
 }
}

For classes derived from ilFDyadicImg, cmplxVectorCalc() takes more arguments since there are two input vectors that need processing:

virtual void cmplxVectorCalc(float* vect1, int rr1, int ri1,
               float* vect2, int rr2, int ri2, 
               int size, int ch, int dc) = 0;

In this case, vect1 and vect2 are pointers to the input vectors, which are of the same size. On input, they hold data to be processed, and on output, vect1 holds the output data and vect2 is unchanged. You can use rr1, ri1, rr2, and ri2 to step through these vectors. The argument ch indicates which channel is currently being processed. This argument is ignored in most cases, but you can use it when the computation being performed depends on the channel. For example, when a cross-correlation is computed, each channel's output is normalized by the average value of that channel. The last argument, dc, indicates whether or not the vector includes a dc value.

Below is an example of what the declaration of ilFMultImg (which multiplies two Fourier images) might look like:

class ilFMultImg : ilFDyadicImg {
private:
    void cmplxVectorCalc(float* vect1, int rr1, int ri1, 
               float* vect2, int rr2, int ri2, 
               int size, int ch, int dc);
public:
    ilFMultImg(ilImage* src1, ilImage* src2);
}

A possible implementation of this class is shown in Example 6-11.

Example 6-11. A Class Derived From ilFDyadicImg to Multiply Two Fourier Images


ilFMultImg::ilFMultImg(ilImage* src1, ilImage* src2)
{
 setValidType(iflFloat);
 addValidOrder(iflSeparate);
 setNumInputs(2);
 addInput(src1);
 addInput(src2);
}
void
ilFMultImg::cmplxVectorCalc(float* vect1, int rr1, int ri1, 
 float* vect2, int rr2, int ri2, int size, int )
{
 int i, j, k;
 for (i = j = k = 0; k < size; i += rr1, j += rr2, k++) {
     float real1 = vect1[i];
     float imag1 = vect1[i + ri1];
     float real2 = vect2[j];
     float imag2 = vect1[j + ri2];
     vect[i] = real1*real2 + imag1*imag2;
     vect[i+ri1] = real2*imag1 - imag2*real1;
 }
}

Deriving From ilFFiltImg

The ilFFiltImg class provides basic support for operators that perform frequency filtering, such as ilFExpFiltImg and ilFGaussFiltImg. This class is particularly useful when the filter can be described as a real-valued analytic function. The input image must be in the format produced by ilRFFTfImg or by ilFFTOp's ilRfftf() function.

Since ilFFiltImg implements prepareRequest(), executeRequest(), or finishRequest() functions, all you have to do to derive from this class is provide your algorithm in the freqFilt() function:

virtual float freqFilt(int u, int v) = 0;

This function returns the filter value at the frequency coordinates u and v, which are the coordinates in the x and y directions, respectively. If nx and ny are the x and y dimensions of the original spatial-domain image, the following is true:

The following example shows a low-pass frequency filter implementation:

class ilFLowPassImg : public ilFFiltImg {
private:
 float cutoff;
 float freqFilt(int u, int v) 
 {return fexp(-(u**2 + v**2)/cutoff**2);}
public:
 ilFLowPassImg(ilImage* src, float cutoff); 
 void setCutOff(float val) {cutoff = val; setAltered();}
}
ilFLowPassImg::ilFLowPassImg(ilImage* src, float cutoff)
{
 setValidType(iflFloat);
 addValidOrder(iflSeparate);
 setNumInputs(1);
 addInput(src);
 setCutoff(cutoff);
}

The constructor for this class takes an input source image and a cutoff level as arguments. The freqFilt() function is implemented as shown below:

Deriving From ilRoi

ilRoi is an abstract base class, which means that an ilRoi cannot be created as an object. It is intended to be used as a base class for deriving new types of region of interests (ROIs). However, a pointer to an ilRoi can be declared for accessing any type of ROI.

ilRoi is derived from ilLink. As a consequence, ilRoi operators can be part of a chain of objects with parent and child dependencies.

ilRoi abstracts the idea of a “region of interest” by defining various functions common to all types of ROIs. A ROI is a 3-D object with its own x, y and z dimensions and its own orientation. One can imagine a ROI being laid on top of an ilImage.

Figure 6-4. Visualizing a ROI

Figure 6-4 Visualizing a ROI

All pixels of the ilImage falling inside the valid regions are ones that are operated on; the rest are not affected. The same ilRoi object can be associated with different images (which can be of different sizes), and it can be placed at different offsets within each image. An ilRoi or any object derived from it can be associated with an ilImage through a class called ilRoiImg.

You can use the getOrientation() and setOrientation() functions to manage the orientation of the ilRoi object.

Different types of ROIs have different ways of describing valid (or foreground) and invalid (or background) regions. A rectangular ROI (ilRectRoi) defines the valid region as being inside or outside a rectangular area. An image-mapped ROI (ilImgRoi) uses an input image as a ROI map; each pixel in the map is compared against a threshold value to determine if it is valid or not; the comparison may be any of the Boolean operators (equal, not equal, greater than, greater or equal, less than, less or equal). Alternatively, you can use an ilImgRoi to divide an image into many different regions, each one corresponding to a distinct pixel value in the image map.

Using an ROI: The ilRoiIter class

In order to apply an ilRoi object to an image, an iterator is required. The pure virtual method createIter() maps the ROI object onto an image at a given offset, then constructs and returns an iterator that can be used to step through the regions of the ilRoi.

Deriving New Classes From ilRoi

In order to define a new type of ROI, the developer must derive a new class of ilRoi as well as a new class of ilRoiIter. You must define the virtual function, createIter(), to construct and return an object of the new iterator class. The new ilRoi class usually needs some other methods specific to its behavior; for example, the ilImgRoi class has methods to set and get the image map, and set or get the comparison operator. These parameters may also be passed to the ilImgRoi constructor.

Deriving New Classes From ilRoiIter

The ilRoiIter class provides functions that can iterate through an ROI. These functions can be used within a specified rectangle (clip box) or an entire image. Once you create an ROI, you can construct an iterator that binds the ROI to an image at a specified offset.

An ilRoiIter object provides the following functions to cycle through valid or invalid data:

  • next()

  • nextMatch()

  • ilRoiIterNext()

  • ilRoiIterNextMatch()

The following functions return the starting location and lengths of the run lengths:

  • getX()

  • getY()

  • getZ()

  • getLen()

  • ilRoiIterGetX()

  • ilRoiIterGetY()

  • ilRoiIterGetZ()

  • ilRoiIterGetLen()

Once you create an ilRoiIter object, it may be used to step through the valid or invalid regions defined by the ROI.

Each derived class of ilRoi requires a derived ilRoiIter class that iterates over the run-lengths of the ROI. Deriving a new class requires only that you define the pure virtual next() to advance to the next segment of the ROI.

An ROI segment is a length of pixels, consecutive in the X dimension, that lie entirely inside or entirely outside the valid region. The iterator should advance in X first, then Y, and finally Z (for 3D ROI's).

The protected method update() performs some common post-processing that all iterators need to do. A typical recipe for next() is shown below:

  1. check if done

  2. if so return FALSE

  3. set last = pos

  4. remember where this segment started

  5. set fore flag based on first pixel in segment;

  6. (foreground/valid -> TRUE; background/invalid -> FALSE)

  7. scan pixels while foreground state remains the same

  8. call update()

  9. return TRUE;