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:
“Deriving From ilImage” tells you how to derive new classes from ilImage.
“Deriving From ilCacheImg” tells you how to derive new caching classes to manage data.
“Deriving From ilMemCacheImg” tells you how you can derive from ilMemCacheImg to manage images in main memory.
“Implementing an Image Processing Operator” tells you how to define operators that implement new image processing algorithms.
“Deriving From ilRoi” describes how you define new regions of interest in your images.
IL classes from which you might want to derive your own new classes are shown in Figure 6-1.
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:
newImgClass—A class derived from ilImage inherits all of its functions for handling an image's attributes; it needs to implement ilImage's pure virtual functions for reading and writing data. More information on deriving from ilImage is provided in “Deriving From ilImage”.
newCacheClass—A class derived from ilCacheImg inherits its caching mechanism; such a class is useful for managing a large amount of data that is accessed a portion at a time. More information on deriving from ilCacheImg is provided in “Deriving From ilCacheImg”.
new ilMemCacheImg class—A class derived from ilMemCacheImg inherits its main memory caching mechanism. Implement the pure virtual functions for storing and retrieving pages of image data. More information on deriving from ilMemCacheImg is provided in “Deriving From ilMemCacheImg”.
newOperator—To define a new operator, you need to implement the desired image processing algorithm and ensure that the processed image has the correct attributes. You can derive directly from ilOpImg or from one of its generalized subclasses. See “Implementing an Image Processing Operator” for more information.
newRoi—To define a new region of interest (ROI), you need to derive from ilRoi and implement functions that describe valid and invalid regions with respect to this new ROI. See “Deriving From ilRoi” for more information.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 |
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.
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.
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.
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 }; |
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().
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 |
---|---|---|
size_t | size of a page in bytes | |
iflSize | pixel dimensions of the pages used to store data on disk | |
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”.
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.
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.
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.
#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 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.
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.
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.)
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.
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.
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().
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.
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); |
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.
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.
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.
#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.
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.
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.
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 | Classes That Derive from |
---|---|
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 |
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.
#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().
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(); |
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.
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; } |
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 | RankFltImg |
---|---|---|
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.
// 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; } } } } |
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.
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.
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.
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.
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; } } |
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:
![]() |
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.
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.
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.
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.
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:
check if done
if so return FALSE
set last = pos
remember where this segment started
set fore flag based on first pixel in segment;
(foreground/valid -> TRUE; background/invalid -> FALSE)
scan pixels while foreground state remains the same
call update()
return TRUE;