NifTK  16.4.1 - 0798f20
CMIC's Translational Medical Imaging Platform
Understanding - File Input and Output (I/O)

Introduction

NiftyView and command line programs use a different mechanism for file I/O, and these are now explained in some detail. Future work may endeavour to make these methods more consistent.

Command Line Programs Use ITK Directly

Command line programs currently use ITK directly for image I/O. Please refer to the ITK Software Guide for a detailed explanation from the ITK authors. The general approach can be seen in the following incomplete code snippet:

#include "itkImage.h"
#include "itkImageFileReader.h"
typedef itk::Image< PixelType, Dimension > InputImageType;
typedef itk::ImageFileReader< InputImageType > InputImageReaderType;
typename InputImageReaderType::Pointer imageReader = InputImageReaderType::New();
imageReader->SetFileName(args.inputImage1);
imageReader->Update();

Within this example, the ImageFileReader is instantiated and assigned to a smart pointer. The ImageFileReader is templated based on image type, and the image type is templated on pixel type and dimensions. Thus, the ImageFileReader is potentially very generic, but in practice, all the command line programs make a choice as to which type of files they support. This is usually implemented using code like the following incomplete code snippet.

int dims = itk::PeekAtImageDimension(args.inputImage1);
if (dims != 2 && dims != 3)
{
std::cout << "Unsuported image dimension" << std::endl;
return EXIT_FAILURE;
}
int result;
switch (itk::PeekAtComponentType(args.inputImage1))
{
case itk::ImageIOBase::UCHAR:
if (dims == 2)
{
result = DoMain<2, unsigned char>(args);
}
else
{
result = DoMain<3, unsigned char>(args);
}
break;
case itk::ImageIOBase::CHAR:

The full code for this example can be seen in niftkAdd.cxx. In this example, it is possible to peek at the image dimensions using utility methods in the NifTK file

NifTK/Libraries/ITK/Common/itkCommandLineHelper.cxx

and in ITK's itk::ImageIOBase. Given the image dimensions and pixel type the right template function can be instantiated. The consequence of this per-program coding is that each command line program could potentially implement this in a different way, according to the requirements of each program. For example a program multiplying probability images would assume that all images were a float data type, with the range 0 to 1, and a program working with segmentation or label images would most likely only deal with integer types.

Loading Volumes using ITK

We now take a detour to explain how the ITK ObjectFactory Mechanism works. The ITK ImageFileReader uses the ObjectFactory method to instantiate the correct file reader. Figure 1. shows a pseudo-sequence diagram to help explain how the ObjectFactory works for loading files.

UnderstandingFileIOITKReader.png
Figure 1. Pseudo UML sequence diagram for loading an image using itk::ImageFileReader.

When a client creates an itk::ImageFileReader, and reads a file, the itk::ImageFileReader needs to create the correct itk::ImageIOBase subclass. It calls the static itk::ImageIOFactory::CreateImageIO which registers all the known itk::ImageIOFactory subclasses with the itk::ObjectFactoryBase. It then asks the ObjectFactoryBase for all objects implementing the itkImageIOBase interface. Each of these returned objects can be cast to itk::ImageIOBase, which contains a method CanReadFile which returns a boolean. So, the itk::ImageIOFactory iterates through the list of all the provided ImageIOBase classes, and the first one that returns true will be returned to the itk::ImageFileReader as the correct itk::ImageIOBase subclass that can read the data.

The advantage of this approach is that the client calling the itk::ImageFileReader and the itk::ImageFileReader itself has no idea how many, or even what class will implement that interface. The itk::ObjectFactory is able to manage all possible classes implementing the interface. The itk::ObjectFactory can also provide services such as dynamically loading classes at runtime, and provide methods to programmatically add and remove classes at runtime.

So the list of available classes can be extended without calling code knowing about it.

From this we also see that

So, when the command line programs are running, all except niftkDicomSeriesReadImageWrite.cxx use this default ITK mechanism.

Loading File Series using ITK

Reading image series using ITK can be seen as an extension of the above mechanism. Put simply, the itk::ImageSeriesReader provides the mechanism for repeatedly loading a series of slices, and uses itk::ImageFileReader to read each 2D slice. A full explanation can be found in the ITK Software Guide.

DICOM files within ITK are loaded using this approach, and an example can be seen in niftkDicomSeriesReadImageWrite.cxx. In that example it can be seen that a directory is scanned to pick up a list of all DICOM files, GDCM is then used to decide which series and which slices to load, the itk::ImageSeriesReader is then used to load each slice using itk::ImageFileReader, and to stack them into a volume.

NiftyView uses MITK Application Framework

NiftyView however uses the MITK application framework. The MITK application framework does mainly delegate down to an ITK based layer for most image file reading operations, but NifTK has implemented some notable differences. These notable differences therefore mean a departure from the ITK mechanism mentioned above. NiftyView loads a different Analyze and Nifti file reader. All other file types are loaded as per MITK.

Loading Files Within MITK

The MITK concept is that a given data type will require readers, writers, mappers for rendering, file extensions to add to the GUI dialog boxes and so on. This requires groups of objects to be created in a consistent fashion, similar to the Abstract Factory pattern. MITK therefore defines the mitk::CoreObjectFactoryBase, with most methods virtual, and the main implementation is mitk::CoreObjectFactory.

MITKCoreObjectFactoryBase.png
Figure 2. MITK provides multiple object factories to coordinate the registration of additional readers, writers, file extensions and mappers.

So, for example for an "Image" type, the mitk::CoreObjectFactory creates many individual factories to manufacture all the correct image file readers.

Looking at:

mitk::CoreObjectFactory::CoreObjectFactory()
{
static bool alreadyDone = false;
if (!alreadyDone)
{
itk::ObjectFactoryBase::RegisterFactory( PointSetIOFactory::New() );
itk::ObjectFactoryBase::RegisterFactory( STLFileIOFactory::New() );
itk::ObjectFactoryBase::RegisterFactory( VtkSurfaceIOFactory::New() );
itk::ObjectFactoryBase::RegisterFactory( VtkImageIOFactory::New() );
itk::ObjectFactoryBase::RegisterFactory( VtiFileIOFactory::New() );
itk::ObjectFactoryBase::RegisterFactory( ItkImageFileIOFactory::New() );
CreateFileExtensionsMap();
alreadyDone = true;
}
}

we can see how MITK is using ItkImageFileIOFactory which uses ITK classes to load images. The mitk::CoreObjectFactory instantiates mitk::ItkImageFileIOFactory and registers that with the ITK factory mechanism. Within mitk::ItkImageFileIOFactory we can see

ItkImageFileIOFactory::ItkImageFileIOFactory()
{
this->RegisterOverride("mitkIOAdapter",
"mitkItkImageFileReader",
"itk Image IO",
1,
itk::CreateObjectFunction<IOAdapter<ItkImageFileReader> >::New());
}

which means that we are registering a class that implements the "mitkIOAdaptor" capability. The itk::ObjectFactory could therefore contain many classes all of which implement the mitkIOAdaptor capability, and in this case the code above denotes the fact that "mitkItkImageFileReader" is a class that implements "mitkIOAdapter", and that in order to create it, we instantiate the templated mitk::IOAdaptor, giving it the mitk::ItkImageFileReader. The mitk::ItkImageFileReader can essentially use any ITK class it likes to load images, and convert them to MITK images.

Within NiftyView we do not want to use the standard ITK Analyze reader and Nifti reader, so we replace them with our own under

NifTK/MITK/Modules/niftkMitkExt/IO

We have itk::AnalyzeImageIO3160 and itk::DRCAnalyzeImageIO3160. The naming convention here is that itk::AnalyzeImageIO3160 is directly copied from the ITK project version 3.16.0. The itk::DRCAnalyzeImageIO3160 is a subclass of itk::AnalyzeImageIO3160 implementing DRC specific functionality. The DRC specific functionality is to correct the orientation field as in this piece of code:

itk::DRCAnalyzeImageIO3160::ValidDRCAnalyzeOrientationFlags temporient = static_cast<itk::DRCAnalyzeImageIO3160::ValidDRCAnalyzeOrientationFlags> (header.hist.orient);
itk::SpatialOrientation::ValidCoordinateOrientationFlags coord_orient;
switch (temporient) {
case itk::DRCAnalyzeImageIO3160::ITK_DRC_ANALYZE_ORIENTATION_RAI_AXIAL:
coord_orient = itk::SpatialOrientation::ITK_COORDINATE_ORIENTATION_RAI;
break;
case itk::DRCAnalyzeImageIO3160::ITK_DRC_ANALYZE_ORIENTATION_RSP_CORONAL:
coord_orient = itk::SpatialOrientation::ITK_COORDINATE_ORIENTATION_RSP;
break;
case itk::DRCAnalyzeImageIO3160::ITK_DRC_ANALYZE_ORIENTATION_ASL_SAGITTAL:
coord_orient = itk::SpatialOrientation::ITK_COORDINATE_ORIENTATION_ASL;
break;
default:
coord_orient = itk::SpatialOrientation::ITK_COORDINATE_ORIENTATION_RAI;
itkWarningMacro("Unknown orientation in file " << m_FileName);
}

Similarly itk::NiftiImageIO3201 is copied from ITK version 3.20.1, which is a patched version of ITK 3.20.0. In our Nifti reader, we additionally read the sform matrix if present. This code will eventually make it back into ITK.

In order to include these within NiftyView, we need therefore to replace the mitk::ItkImageFileReader with our own. So we create a factory class in mitk::NifTKItkImageFileIOFactory that derives from mitk::ItkImageFileReader which says:

NifTKItkImageFileIOFactory::NifTKItkImageFileIOFactory()
{
this->RegisterOverride("mitkIOAdapter",
"mitkNifTKItkImageFileReader",
"NifTK specific ITK based image IO",
1,
itk::CreateObjectFunction<IOAdapter<NifTKItkImageFileReader> >::New());
}

i.e. we are implementing the mitkIOAdaptor interface, by providing class mitk::NifTKItkImageFileReader (a subclass of the original mitkItkImageFileReader) and creating the NifTKItkImageFileReader class by passing it to the templated IOAdaptor class. Then in mitk::NifTKItkImageFileReader.cxx we can write our code to read images using our modified itk::DRCAnalyzeImageIO3160 and itk::NiftiImageIO3201.

The remaining hurdle is to make sure our class gets created as a replacement for the standard MITK factory. The reason is that the underlying itk::ObjectFactory stores all the objects it manages in an ordered list. So, if you have two classes implementing exactly the same interface, only the first one is used.

So we create mitk::NifTKCoreObjectFactory. Recall that in general each subclass of mitk::CoreObjectFactoryBase adds new functionality, like new readers, and new file extensions and new mappers. For example, take a look at the mitk::DiffusionImagingObjectFactory as this adds lots of new data-types to the main application. In our case however, we want to forcibly replace the existing functionality.

Thus we create mitk::NifTKCoreObjectFactory which is really a dummy factory, as all the required file extensions, mappers and readers are created in mitk::CoreObjectFactory. The main method of interest is the constructor:

mitk::NifTKCoreObjectFactory::NifTKCoreObjectFactory(bool /*registerSelf*/)
:CoreObjectFactoryBase()
{
static bool alreadyDone = false;
if (!alreadyDone)
{
MITK_INFO << "NifTKCoreObjectFactory c'tor" << std::endl;
// At this point in this constructor, the main MITK CoreObjectFactory has been created,
// so MITKs file reader for ITK images will already be available. So, now we remove it.
std::list<itk::ObjectFactoryBase*> listOfObjectFactories = itk::ObjectFactoryBase::GetRegisteredFactories();
std::list<itk::ObjectFactoryBase*>::iterator iter;
mitk::ItkImageFileIOFactory::Pointer itkIOFactory = NULL;
for (iter = listOfObjectFactories.begin(); iter != listOfObjectFactories.end(); iter++)
{
itkIOFactory = dynamic_cast<mitk::ItkImageFileIOFactory*>(*iter);
if (itkIOFactory.IsNotNull())
{
itk::ObjectFactoryBase::UnRegisterFactory(itkIOFactory.GetPointer());
m_ItkImageFileIOFactory = itkIOFactory;
break;
}
}
itk::ObjectFactoryBase::RegisterFactory(m_NifTKItkImageFileIOFactory);
itk::ObjectFactoryBase::RegisterFactory(m_PNMImageIOFactory);
itk::ObjectFactoryBase::RegisterFactory(m_CoordinateAxesDataReaderFactory);
itk::ObjectFactoryBase::RegisterFactory(m_CoordinateAxesDataWriterFactory);
// Carry on as per normal.
CreateFileExtensionsMap();
alreadyDone = true;
MITK_INFO << "NifTKCoreObjectFactory c'tor finished" << std::endl;
}
}

which can be seen to remove the mitk::ItkImageFileIOFactory and insert mitk::NifTKItkImageFileIOFactory. This mitk::NifTKCoreObjectFactory is simply created when NiftyView starts because the first thing that loads the niftkCore library will call

static RegisterNifTKCoreObjectFactory registerNifTKCoreObjectFactory;

found in mitkNifTKCoreObjectFactory.cpp.

To summarise, we create a factory called mitk::NifTKItkImageFileIOFactory to load all ITK image types, using the standard ITK mechanism, except for Analyze and Nifti files where we provide modified readers. This factory is registered with the ITK ObjectFactory at application startup, replacing the default MITK class.

Loading DICOM files in ITK

To-date, the only command line application that reads DICOM is niftkDicomSeriesReadImageWrite.cxx. It uses ITK, which uses GDCM. In brief detail, the program niftkDicomSeriesReadImageWrite.cxx

Within the ITK code, the GDCM files of interest are:

Code/IO/itkGDCMImageIO.cxx
Code/IO/itkGDCMImageIO.h
Code/IO/itkGDCMImageIOFactory.h
Code/IO/itkGDCMSeriesFileNames.cxx
Code/IO/itkGDCMSeriesFileNames.h

Loading DICOM files in MITK

Within MITK, all file based DICOM import is also performed using GDCM, and the files of interest are:

Modules/Core/src/mitkCoreObjectFactory.cxx
Modules/Core/src/mitkCoreObjectFactoryBase.cxx
Modules/Core/src/IO/mitkDicomSeriesReader.cxx
Modules/Core/include/mitkDicomSeriesReader.h
Modules/Core/include/mitkDicomSeriesReader.txx

We can see in the object factories that file name extensions are registered, but the DicomSeriesReader is not instantiated there. The DicomSeriesReader is used within mitk::DataNodeFactory to load an image into an MITK image, but all the logic of interest is in DicomSeriesReader. Again it can be seen that MITK uses GDCM, but have preferred to write their own reader rather than rely on the default ITK one. When troubleshooting DICOM loading in NiftyView, DicomSeriesReader is the place to start, and rather than patch MITK locally, contribute any changes back to MITK.