The
ml::ImageProperties
class describes the basic image properties
6 dimensional extent as a ImageVector (Section 2.4.1, “ImageVector
, ImageVector”),
the voxel data type
(MLDataType
),
the minimum and maximum limits of voxel values, and
Images are rectangular grids without gaps, and all voxels are of identical extent and types. The six image dimensions in the ML are interpreted as the three spatial dimensions (x, y and z), a color extent (c dimension), a time extent (t dimension) and a user (u) dimension. For example, a dynamic sequence of three dimensional color images that exist in different image acquisitions or reconstructions can be handled by the ML as a single image object.
See
mlImageProperties.h
in project ML for
more information.
The
ml::MedicalImageProperties
class is derived from
ImageProperties
(Section 2.3.1, “
ImageProperties
”). It contains additional information
specialized for medical data sets:
a voxel size,
a 4x4 transformation matrix (world matrix) to specify 3D transformations (e.g., for registration purposes),
an anonymous reference to a list that stores DICOM tag information if the input file(s) have been in DICOM file format,
color channel information (as strings) for the 4th
dimension. The string list std::vector<std::string> &getCDimensionInfos()
describes the significance for the
channels e.g., "RED", "GREEN", and "BLUE" for channels 0, 1 and 2
when the RGB color model is used,
time point information for the t extent of the image. The
list std::vector<DateTime> &getTDimensionInfos()
contains this information for each
time point,
u dimension information given as a list accessible with
std::vector<std::string> &getUDimensionInfos()
. The stored strings describe the
subimages with different u components. Often, strings such as
"CT", "MR", etc. are stored.
Note | |
---|---|
For the c (color) and u dimension, there is a set of
constants available describing the image contents, such as
|
mlImageProperties.h
in project ML
for more information.
ml::ImagePropertyExtension
is used to append
additional and user-defined property information to an ML image. This
class is independent of the classes ImageProperties
and
MedicalImageProperties
(see Section 2.3.1, “
ImageProperties
” and Section 2.3.2, “
MedicalImageProperties
”). It is an abstract class that
serves as a base class from which an application or programmer can
derive new properties. These properties are added to the
ImagePropertyExtensionContainer
that is a
member of the class MedicalProperties
.
A derived ImagePropertyExtension must meet some requirements:
It must implement the copy constructor and assignment operator correctly, because objects of its type are copied from one image to another.
It requires a virtual createClone()
method that returns a copy or a new instance of the
class so that a copy of the correct derived class is
returned.
It must implement ML runtime typing and must be registered in the runtime type system of the ML in such a way that the ML can create new instances of the user-defined class only from its name and compare class types.
It must implement set and get methods to set/get the property value as a string, because ML modules must be able to store/load property settings in/from a file.
It must implement equality and inequality operators to compare instances.
Most methods to be implemented are pure virtual in the base
class ImagePropertyExtension
, hence compilation
will not work without implementing them
The following programming example demonstrates how to implement
a newly derived ImagePropertyExtension
:
#include "mlModuleIncludes.h" ML_START_NAMESPACE //! Implement a ImagePropertyExtension object which can be passed to the ML. class MODULE_TESTS_EXPORT OwnImagePropertyExtension : public ImagePropertyExtension { public: //! Constructor. OwnImagePropertyExtension() : ImagePropertyExtension() { _extInfoString = "NewImageInfosString"; } //! Destructor. virtual ~OwnImagePropertyExtension() { } //! Implement correct copy construction. OwnImagePropertyExtension(const OwnImagePropertyExtension &origObj) : ImagePropertyExtension(origObj) { _extInfoString = origObj._extInfoString; } //! Implement correct assignment. OwnImagePropertyExtension &operator=(const OwnImagePropertyExtension &origObj) { if (&origObj != this) { _extInfoString = origObj._extInfoString; } return *this; } //! Implement pure virtual equality operation to work even on base class pointers. virtual bool equals(const ImagePropertyExtension &extImageProps) const { if (extImageProps.getTypeId() == getClassTypeId()) { // Types are equal, compare contents. return _extInfoString == ((OwnImagePropertyExtension&)(extImageProps))._extInfoString; } else { return false; // Types differ, thus objects also differ. } } //! Creates a copy of the correct derived object (for comparisons / runtime type determination). virtual ImagePropertyExtension *createClone() const { return new OwnImagePropertyExtension(*this); } //! Returns value of property as string. virtual std::string getValueAsString() const { return _extInfoString; } //! Set value of property from string value. virtual MLErrorCode setValueFromString(const std::string &str) { _extInfoString = str; return ML_RESULT_OK; } private: //! The string values used as additional image property. std::string _extInfoString; //! Implements interface for the runtime type system of the ML. ML_CLASS_HEADER(OwnImagePropertyExtension) }; ML_END_NAMESPACE
Implement the C++ part of the class interface to the runtime type system:
ML_START_NAMESPACE //! Implements code for the runtime type system of the ML. ML_CLASS_SOURCE(OwnImagePropertyExtension, ImagePropertyExtension); ML_END_NAMESPACE
Register the class to the runtime type system of the ML when the .dll/.so file is loaded. This is typically done in the InitDll file of the project:
ML_START_NAMESPACE int MyProjectInit() { OwnImagePropertyExtension::initClass(); } ML_END_NAMESPACE
In the method calculateOutputImageProperties of your ML module, you can add a copy of your own image property to the output image:
ML_START_NAMESPACE MyModule::calculateOutputImageProperties(int outIndex, PagedImage* outImage) { OwnImagePropertyExtension myNewImgProp; outImage->getImagePropertyContainer().appendEntry(&myNewImgProp, true); } ML_END_NAMESPACE
See
mlImagePropertyExtension.h
and
mlImagePropertyExtensionContainer.h
in project ML
for more information.
The class
ml::PagedImage
is dedicated to managing paged images in the ML
and to representing image outputs of ML modules. See
mlPagedImage.h
in project ML.
The ML mainly works with pages and tiles. Since ML does usually not process entire images, it is necessary to break them down into smaller fractions of identical extent, the so-called pages. Pages can easily be buffered, cached and processed in parallel without spending too much memory or time. Caching in the ML works exclusively with pages. Moreover, only the pages that overlap with the actually requested image region must be processed. All other pages are not processed. Common page extents are, for example, 128x128x1x1x1x1 voxels. However, they may also have a real six-dimensional extent as do all images in the ML. Often, other image fractions (also called tiles) which do not have standard page extents are needed. Tiles are usually composed from pages and are used by the application or as input for image processing algorithms. In the ML, tiles are usually only temporary, i.e., the ML does not cache tiles.
For algorithms where a page-based implementation is difficult,
classes such as VirtualVolume
,
BitImage
or MemoryImage
provide special interfaces to simplify efficient implementations. See
Section 2.3.7, “
VirtualVolume
”, Section 2.3.6, “
BitImage
” and
Section 2.3.8, “
MemoryImage
” for more information.
ml::SubImage
is an important class representing image and subimage
buffers. It is used to manage, copy, etc. chunks of voxel data.
It contains fast data access methods. See
mlSubImage.h
.
ml::TSubImage
is the typed version of
SubImage
which permits typed data accesses.
See
mlTSubImage.h
in
project ML.
ml::TSubImageCursor
and
ml::ConstTSubImageCursor
are cursor
classes that allow access to a given TSubImage using cursor positioning and movement.
The SubImage
class represents a rectangular 6D image region with
linearly organized memory. It offers:
Methods for setting/accessing the datatype, the box defining the subimage region, the source image extent and the valid region (which is the intersection of the source image extent and the box).
A pointer to the memory data containing the image data as
a void pointer. Alternatively the data can be stored as a MLMemoryBlockHandle
to manage data via the MLMemoryManager
.
With this class, the Host
manages
and encapsulates rectangular image regions (e.g., for pages,
tiles, cached image results) and passes them to the image
processing algorithms. The Host
usually
does not need information about the actual data.
The typical image processing methods in the ML are located
in overloaded methods of
Module::calculateOutputSubImage()
. In these
methods, the untyped memory chunks given as
SubImage
are usually wrapped again to typed
subimages. See TSubImage
for more
information.
The type of the image data in memory is handled via a void
pointer; the type, however, is managed as an
enum
type to support typed access in
derived classes (see TSubImage
).
Consequently, SubImage
does not support
typed access to image voxels.
The typed access to voxels is implemented on top of this
class as the template class
TSubImage
.
The following paragraphs show some typical use cases of the
class SubImage
.
This creates a SubImage
instance that
provides access to a chunk of double data of 16 x 32 x 8 x 1 x 1 x 1
voxels given by the pointer dataPtr
:
SubImage subImgBuf(SubImageBox(ImageVector(0,0,0,0,0,0), ImageVector(15,31,7,0,0,0)), MLdoubleType, dataPtr);
The caller is responsible for the data chunk to be sufficiently large.
This first fills the entire subimage with the value
7.7
. Then the rectangular region outside the
area given by (3,3,3,0,0,0) and (5,5,5,0,0,0) is filled with the
value 19.3
:
subImgBuf.fill(7.7); subImgBuf.fillBordersWithLDoubleValue(SubImageBox(ImageVector(3,3,3,0,0,0), ImageVector(5,5,5,0,0,0)), 19.3);Assuming another
SubImage
object
srcSubImg
, the overlapping areas can simply
be copied (and if necessary, cast to the target type) into
subImgBuf
, and optionally rescaled with value
0.5:subImgBuf.copySubImage(srcSubImg, ScaleShiftData(0.5, 0) );
Untyped data access to the voxel data is available for example at position (1,2,3,0,0,0) with
void *voxPtr = subImgBuf.getImagePointer( ImageVector(1,2,3,0,0,0,0) );
For typed data management, the class
TSubImage
can be used almost in the same
way:
TSubImage<MLdouble> subImgBufT(SubImageBox(ImageVector(0,0,0,0,0,0), ImageVector(15,31,7,0,0,0)), MLdoubleType, dataPtr);
The class TSubImage
, however, provides a
number of typed access functions, such as
MLdouble *voxPtrT = subImgBufT.getImagePointer( ImageVector(1,2,3,0,0,0,0) ); *voxPtrT = 29.2;
The untyped SubImage
and the templated
TSubImage
classes also provide a variety of
other methods to manipulate, copy, fill, allocate and delete data,
or to check for a certain value, or to retrieve statistical
information such as minimum or maximum. They are powerful classes
that can be used in many contexts when memory or voxel buffers have
to be managed.
See files
mlSubImage.h
and
mlTSubImage.h
for more information.
In the page-based image processing concept of the ML, Boolean data types are not available (nor are they planned).
The BitImage
class can be used as an
alternative.
The following set of operations is available for this class type:
full 6D support in all methods,
set, get, clear and toggle bits at coordinates,
filling (=clearing or setting) and inverting subimage boxes,
copying from/to subimages (with thresholding),
saving/loading to/from file,
position checking,
creating downscaled BitImage
s,
creating BitImage
s from image data
where first the mask area is determined and then the smallest
possible BitImage
is returned,
cursor movement in all dimensions,
exception handling support for safe operations on images.
The
ml::VirtualVolume
and the
ml::TVirtualVolume
classes manage efficient voxel
access to the output image of an input module or to a 'standalone'
image.
So it is possible to implement random access to a paged input
image or to a pure virtual image without mapping more than a limited
number of bytes. Pages of the input volume are mapped temporarily into
memory when needed. If no input volume is specified, the pages are
created and filled with a fill value. When the permitted memory size
is exceeded, older mapped pages are removed. When pages are written,
they are mapped until the virtual volume instance is removed or until
they are explicitly cleared by the application. Virtual volumes can
easily be accessed by using setValue
and
getValue
. These kinds of access are
well-optimized code that might need 9 (1D), 18 (3D) and 36 (6D)
instructions per voxel if the page at the position is already
mapped.
A cursor manager for moving the cursor with moveCursor*
(forward) and reverseMoveCursor*
(backward) is
also available. setCursorValue
and
getCursorValue
provide voxel access. Good
compilers and already mapped pages might require about 5-7
instructions. So the cursor approach will probably be faster for data
volumes with more than 2 dimensions.
All the virtual volume access calls can be executed with or
without error handling (see last and default constructor parameters).
If areExceptionsOn
is
true
, every access to the virtual volume is
tested and if necessary, exceptions are thrown that can be caught by
the code calling the virtual volume methods. Otherwise, most functions
do not perform error handling.
Note | |
---|---|
Exception handling versions are slower than versions with disabled exceptions. However, this is the only way to handle accesses safely. |
Tip | |
---|---|
This class is the recommended alternative to global image processing algorithms. |
The following code gives an example of how to use the
VirtualVolume
class:
Example 2.6. How to Use the VirtualVolume
Class
Header:
VirtualVolume *_virtVol;
Constructor:
_virtVol = NULL;
Create/Update the virtual volume in
calculateOutputImageProperties()
and invalidate the
output image on errors, so that
calculateOutputSubImage
() is not called on bad
virtual volume later.
if (_virtVolume != NULL) { delete _virtVolume; } _virtVolume = new VirtualVolume(this, 0, getInputImage(0)->getDataType()); if (!_virtVolume || (_virtVolume && !_virtVolume->isValid())){ outImage->setInvalid(); return;
When you do not want to use a 'standalone' virtual volume:
_virtVolume = new VirtualVolume(ImageVector(1024,1024,1,1,1,1), 0, MLuint8Type)); if ((_virtVolume == NULL) || (_virtVolume && !_virtVolume->isValid())){ outImage->setInvalid(); return; }
Example of how to access image data directly:
calculateOutputSubImage()
// Create wrapper for typed voxel access. TVirtualVolume<DATATYPE> vVol(*_virtVolume); ImageVector pos(7,3,0,0,0,0); DATATYPE value; vVol.setValue(pos, value); // Simple setting of an arbitrary voxel. value = vVol.getValue(pos); // Reading of an arbitrary voxel. vVol.fill(outSubImg->getBox(), value); // Fill region with value. // Now copy valid region of virtVolume to outSubimg. vVol.copySubImage(*outSubImg);
Example of how to access image data via a cursor:
calculateOutputSubImage()
:
// Create wrapper for typed voxel access. TVirtualVolume<DATATYPE> vVol(*_virtVolume); ImageVector pos(7,3,0,0,0,0); DATATYPE value; vVol.setCursorPosition(pos); // Set cursor to any position in volume. vVol.moveCursorX(); // Move cursor >F<orward vVol.moveCursorY(); // in (positive) X, C and U direction. vVol.moveCursorZ(); vVol.reverseMoveCursorT(); // Move cursor >B<ackwards in (negative) T vVol.reverseMoveCursorZ(); // and Z direction. val = vVol.getCursorValue(); // Reading voxel below cursor. vVol.setCursorValue(10); // Set voxel value below cursor to 10.
Additionally, the following helper routines are available:
// Fill region of virtual volume with a certain value. void fill(const SubImageBox &box, DATATYPE value); // Copy region from the virtual volume into a typed subimg. void copyToSubImage(TSubImage<DATATYPE> &outSubImg); // Copy a region from a typed subimg into the virtual volume. void copyFromSubImage(TSubImage<DATATYPE> &inImg, const SubImageBox &box, const ImageVector &pos);
There are also some routines to get the boxes of the currently written pages. It is also possible to read/write the data of the written pages directly.
Note | |
---|---|
The class However, there are also convenience constructors of the
|
Using virtual Volume instances that create untyped virtual volume instances automatically.
Creating a TVirtualVolume
with a
convenience constructor. It creates a
VirtualVolume
internally. It provides float
data access to the input image 0, even if the input image is of
another type. Note that the connected input image must be
valid:
// Create a typed VirtualVolume from input connector 0 of // this Module with voxels of type float directly without // creating the untyped VirtualVolume manually. TVirtualVolume<float> vVol(this, 0);
The standard usage of the VirtualVolume
and the TVirtualVolume
classes does not
include error handling. For safe usage,
areExceptionsOn
== true
is passed as a parameter to the constructor, and errors will throw
the following exceptions:
Note that areExceptionsOn
==
true
degrades voxel access performance.
ML_OUT_OF_RANGE
ML Error Code
MLErrorCode
is thrown if cursor
positioning or voxel addressing tries to access invalid image
regions. The exception leaves the virtual volume, the cursor
position, the voxel content, etc. unchanged and the invalid flag
of the virtual volume is not
set. The call is just terminated and ignored, i.e., The call can
continue and accesses to other voxels are attempted.
ML_NO_MEMORY
MLErrorCode
is thrown if an
allocation fails because of insufficient memory. The valid
virtual volume is invalidated, i.e., Its valid flag is
cleared.
ML_BAD_DIMENSION
MLErrorCode
is thrown if the image
data extent is invalid. This could indicate a programming error
or invalid input image data. The valid virtual volume is
invalidated, i.e., Its valid flag is cleared.
ML_BAD_DATA_TYPE
MLErrorCode
is thrown if an invalid
image data type is encountered. This could indicate a
programming error or invalid input image data. The valid virtual
volume is invalidated, i.e., Its valid flag is cleared.
Other exceptions that result from page request failures
could also be thrown. They are usually returned, when a
getTile
command that attempts to request
data from an input image fails.
If the areExceptionsOn
==
false
, no exception is thrown and many errors
are handled by calling the ML_PRINT*() error macros and terminating
the function/method. The virtual volume instance will be
invalidated. Invalid voxel access or memory failures will destroy
the program state or cause unknown exceptions.
Voxel access performance is best when the page extents of input pages are powers of 2.
Working locally on virtual volumes is generally faster than jumping randomly through the image, because less pages must be swapped.
Coordinate-specific voxel access performance is better for images of a lower dimension, because less calculations have to be performed.
If the virtual volume wraps a paged input image, voxel access is not permitted when the input connection or the module has become invalid.
The virtual volume must not be used in parallel in
calculateOutputSubImage()
calls, because
getValue
and
setValue
methods potentially call
getTile*()
which would start recursive
multithreading. Therefore be sure that multithreading remains
disabled in areas where VirtualVolume
or TVirtualVolume
use
calculateOutputSubImage()
or you must make
accesses to them thread-safe by using critical sections,
semaphores or similar concepts. Even if no paged image is used
as an input, write access is not capable of multithreading due
to performance reasons.
If an image has n dimensions (e.g., 3), components >= n in cursor positioning and voxel access are simply ignored for performance reasons and do not cause errors if they are set even if this means that the cursor was outside the image.
In some cases, the virtual volume approach is slower than a global approach.
Consider the following reasons:
The virtual volume approach is completely page-based, i.e., it fits perfectly in the optimized page-based concept of the ML.
The virtual volume approach only requests image areas of the input image that are really needed (processing on demand) so that less input image regions are calculated. Global approaches always request the entire input image which is often expensive to calculate.
The virtual volume approach usually locks less memory than the global approach, so the operating system must swap less memory, and other modules can work faster.
Next versions will not duplicate memory as their own tiles (as a global approach needs to), but will directly try to use ML cache pages.
MemoryImage
can be used for algorithms that
need fast random access to entire images, especially if they work
“against“ paging e.g., OrthoReformat
,
MPR
, MemCache
.
Important | |
---|---|
|
Properties:
The MemoryImage
object is part of each
PagedImage
, i.e., there is one (usually empty
and unused) MemoryImage
object per
output.
If possible, all connected ML modules copy or reference data
directly from the MemoryImage
object.
There are two ways of how to use the memory image at a module output:
The module completely controls the
MemoryImage
object at the image output
(reset, clear, set, resize, update...). Thus connected modules
benefit (see Version 1).
The entire input image is requested as one page (with the note to buffer it as a memory image). Further requests (also from other modules) will be answered immediately by passing the pointer to the memory image or by copying page data from it (see Version 2).
Version 1: The module controls the memory image at the output:
Example 2.7. Controlling the MemoryImage
by the Module
// Constructor: Enables the operator control of the memory output at output 0. getOutputImage(0)->getMemoryImage().setUserControlled(true); // Resize and copy input image into the memory image output: MLErrorCode result = getOutputImage(0)->getMemoryImage().update( getInputImage(0), getInputImage(0)->getImageExtent(), getInputImage(0)->getDataType()); if (ML_RESULT_OK != result) { handleErrorHere(result); } // Get data pointer and draw into memory image at output: drawSomethingsIntoImg(getOutputImage(0)->getMemoryImage().getImage());
Version 2: The memory image is cached at the output of preceding module:
Example 2.8. Using/Requesting a MemoryImage
of the Predecessor Module
// Request input tile caching in output of input module void MemoryInTest::calculateOutputImageProperties(int outIndex, PagedImage* outImage) { ... outImage->setInputSubImageUseMemoryImage(0, true); } // Request input tile of size of input volume (other sizes cause warnings!) SubImageBox MemoryInTest::calculateInputSubImageBox (int /*inIndex*/, const SubImageBox & /*outSubImgBox*/, int /*outIndex*/) { return getInputImage(0)->getBoxFromImageExtent(); }
Advantages:
All connected modules can benefit from the memory image, because it is part of the image output.
It is easy to implement and fast; it does not break the paging concept.
Disadvantages:
The image size is limited by the size of the largest free memory chunk.
It cannot/should not be used in bigger networks or applications.
It must map the entire image and blocks large memory areas for a long time.
© 2024 MeVis Medical Solutions AG