diff --git a/Core/Documentation/Doxygen/Concepts/DICOMTesting.dox b/Core/Documentation/Doxygen/Concepts/DICOMTesting.dox index c23ceb5cf9..b16019d910 100644 --- a/Core/Documentation/Doxygen/Concepts/DICOMTesting.dox +++ b/Core/Documentation/Doxygen/Concepts/DICOMTesting.dox @@ -1,154 +1,154 @@ /** \page DICOMTesting MITK DICOM testing \section DICOMTesting_introduction Introduction Reading DICOM data into mitk::Images is a complicated business since DICOM and MITK have very different ideas of images. -DicomSeriesReader brings DICOM and MITK as close together as possible by offering methods to load DICOM CT and MR images +DICOMITKSeriesGDCMReader (formerly: DicomSeriesReader) brings DICOM and MITK as close together as possible by offering methods to load DICOM CT and MR images into mitk::Images, optionally grouping slices to 3D+t images. Since there are so many possible sources for mistakes with any change to this loading process, testing the many -assumptions implemented in DicomSeriesReader is worthwhile. This document describes what and how theses kind of tests are implemented. +assumptions implemented in DICOMITKSeriesGDCMReader is worthwhile. This document describes what and how theses kind of tests are implemented. \section DICOMTesting_problem Problem description The task of loading DICOM files into mitk::Images is a challenge because of major differences in the way that DICOM and MITK represent images: - DICOM images + are mostly stored as one slice per file + do not describe how they can be arraged into image stacks with orthogonal axis + sometimes they cannot be arranged in image stacks as described above (e.g. tilted gantry) - mitk::Image (at least its mature areas) + represents image stacks with orthogonal axis (nothing like a tilted gantry) + have a concept of a fourth dimension (time steps) Because image processing applications based on MITK still need a way to load DICOM images into mitk::Image objects, MITK needs a way to build image stacks and this needs to be well tested. For more background information, see David Clunie's most valuable posts on comp.protocols.dicom, e.g.: - http://groups.google.com/group/comp.protocols.dicom/browse_thread/thread/6db91972e161f0d4/6e0304ac264a6eb5 - http://groups.google.com/group/comp.protocols.dicom/browse_thread/thread/e9bd1497bea3e66b/187a7dc8810613d2 - http://groups.google.com/group/comp.protocols.dicom/browse_thread/thread/5d80bb0b7fafcb81/cf96119e3b024ed8 - http://groups.google.com/group/comp.protocols.dicom/browse_thread/thread/4568635e083a3fba/e2a8ceec23032601 \section DICOMTesting_testidea Test principle -The general idea for DICOM loaing tests is to run a set of known DICOM files through DicomSeriesReader's methods +The general idea for DICOM loaing tests is to run a set of known DICOM files through DICOMITKSeriesGDCMReader's methods GetSeries() and LoadDicomSeries() to generate mitk::Images. These images are then compared to expected image properties, such as the number of individual mitk::Images, positions, orientations, spacings, etc. Stored expectations look like this (should be self-explanatory): \verbatim -- Image 1 Pixeltype: s BitsPerPixel: 16 Dimension: 4 Dimensions: 64 64 6 1 Geometry: Matrix: 5.25 0 0 0 5.2468 0.139598 0 -0.183222 3.99757 Offset: -159.672 -309.974 -69.0122 Center: 0 0 0 Translation: -159.672 -309.974 -69.0122 Scale: 1 1 1 Origin: -159.672 -309.974 -69.0122 Spacing: 5.25 5.25 4 TimeBounds: 0 1 -- Image 2 Pixeltype: s BitsPerPixel: 16 Dimension: 4 Dimensions: 64 64 41 1 Geometry: Matrix: 5.25 0 0 0 5.25 0 0 0 4 Offset: -160.672 -311.672 -285 Center: 0 0 0 Translation: -160.672 -311.672 -285 Scale: 1 1 1 Origin: -160.672 -311.672 -285 Spacing: 5.25 5.25 4 TimeBounds: 0 1 \endverbatim \section DICOMTesting_implementation Implementation \section DICOMTesting_implementation_utils Test helpers (applications and classes) Application DumpDICOMMitkImage Takes a list of DICOM images, loads them using TestDICOMLoading, then dumps information about the resulting mitk::Images to standard output. This application is helpful when defining reference data for tests. Application VerifyDICOMMitkImageDump Takes a list of DICOM images and loads them using TestDICOMLoading. Takes a dump file as generated by DumpDICOMMitkImage, parses it and compares it to actually generated mitk::Images. This application is used to implement the majority of test cases. They all load images, then verify the expected result structure. Class TestDICOMLoading \section PageDICOMLoadingTests_testcaseimplementation Test case implementation Individual test cases are stored in the MITK-Data repository and constructed by Core/Code/Testing/DICOMTesting/Testing/CMakeLists.txt The CMake file parses given directories for subdirectories containing specific test cases. Each directory contains two files: - File "input": lists DICOM files that should be loaded for this test case - File "expected.dump": contains the image properties in the above mentioned dump format Each test case is translated into a CTest test which evaluates the return value of a call to VerifyDICOMMitkImageDump. \section PageDICOMLoadingTests_testcases Implemented test cases From test set \b TinyCTAbdomen (see description.txt in this directory for details on test images): - singleslice : just a single slice (should work and contain meaningful spacing) - two_slices : two slices, spacing should be calculated correctly - all : load a "normal" series as a single 3D block - 3D_and_T : load a small set of slices with multiple time-steps - diff_orientation : load a set of files containing two differently oriented image blocks; at least two images (110,111) have minor errors in small decimal digits - diff_orientation_gaps : load a set of files containing two differently oriented image blocks, each missing slices, so blocks must be split - diff_spacing : load a set of files containint two set of slices with different spacings - gap : load slices that cannot form a single 3D block, because single slices are missing - gaps : slices missing in differnt locations, so multiple splits needed - unsorted_gaps : as before, just file names are unsorted - single_negative_spacing : from reported bug related to single MR images with misleading spacing information - tilted_gantry : slice origins do not align along first slice normal (happens with tilted gantries) - interleaved : two volumes of slices with origins along the same line. The volumes' slices interleave in their border region. This test is meant to correctly sort apart the two blocks instead of generating many two-slices groups in the interleaved region. - CR-MONO1-10-chest-spacing-none : CR image without spacing information - (1.0, 1.0) should be assumed - CR-MONO1-10-chest-spacing-pixelspacing : CR image with only "Pixel Spacing" tag - this should be used as spacing - CR-MONO1-10-chest-spacing-imagerpixelspacing : CR image with only "Imager Pixel Spacing" tag - this should be used as spacing - CR-MONO1-10-chest-spacing-calibrated : CR image with BOTH "Imager Pixel Spacing" and "Pixel Spacing" defined - "Pixel Spacing" should be used - OT-MONO2-8-colon : Secondary Capture image with gray value pixels - should be loaded - OT-PAL-8-face : Secondary Capture image with PALETTE pixel values - can be loaded, but display is not correct yet From test set \b TiltHead (see description.txt in this directory for details on test images): - head_ct_tilt : load a series with gantry tilt and check its orientation \section DICOMTesting_othertests Specific other tests This list is meant to provide an up-to-date list of all implemented DICOM loading tests. If you ever find this outdated, please update it or make the persons who invalidated the list update it. mitkDICOMTestingSanityTest_* -These tests implement basic testing of the implemented helper classes. The tests use DicomSeriesReader to load +These tests implement basic testing of the implemented helper classes. The tests use DICOMITKSeriesGDCMReader to load a number of DICOM image. They verify: - - DicomSeriesReader recognizes all input files as DICOM images - - DicomSeriesReader generates a number of mitk::Images from the DICOM images + - DICOMITKSeriesGDCMReader recognizes all input files as DICOM images + - DICOMITKSeriesGDCMReader generates a number of mitk::Images from the DICOM images - the number of mitk::Images matches a number given on the command line or CTest's add_test() - helper methods in class TestDICOMLoading make minimal sense (comparison of an image dump to itself must be valid) */ diff --git a/Examples/mitkdump/mitkdump.cpp b/Examples/mitkdump/mitkdump.cpp index 07047f5c02..ced2de99bf 100644 --- a/Examples/mitkdump/mitkdump.cpp +++ b/Examples/mitkdump/mitkdump.cpp @@ -1,61 +1,76 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ -#include "mitkClassicDICOMSeriesReader.h" +#include "mitkDICOMFileReaderSelector.h" using mitk::DICOMTag; int main(int argc, char* argv[]) { mitk::StringList inputFiles; for (int a = 1; a < argc; ++a) { inputFiles.push_back( std::string(argv[a]) ); } // ----------------- Configure reader ------------------- + mitk::DICOMFileReaderSelector::Pointer configSelector = mitk::DICOMFileReaderSelector::New(); + configSelector->LoadBuiltIn3DConfigs(); // a set of compiled in ressources with standard configurations that work well + configSelector->SetInputFiles( inputFiles ); + mitk::DICOMFileReader::Pointer reader = configSelector->GetFirstReaderWithMinimumNumberOfOutputImages(); + if (reader.IsNull()) + { + MITK_ERROR << "Could not configure any DICOM reader.. Exiting..."; + return EXIT_FAILURE; + } - // DOES fix tilt when detected - // DOES group 3D blocks into 3D+t if possible - mitk::ClassicDICOMSeriesReader::Pointer gdcmReader = mitk::ClassicDICOMSeriesReader::New(); + MITK_INFO << "---- Best reader configuration -----------------"; + MITK_DEBUG << "Found best reader with configuration '" << reader->GetConfigurationLabel() << "'"; + reader->PrintConfiguration( std::cout ); + MITK_INFO << "-------------------------------------------"; // ----------------- Load ------------------- - gdcmReader->SetInputFiles( inputFiles ); + reader->SetInputFiles( inputFiles ); MITK_INFO << "Analyzing " << inputFiles.size() << " files ..."; - gdcmReader->AnalyzeInputFiles(); - //gdcmReader->PrintOutputs(std::cout, false); + reader->AnalyzeInputFiles(); + reader->PrintOutputs(std::cout, false); + + return EXIT_SUCCESS; + MITK_INFO << "Loading " << inputFiles.size() << " files ..."; - gdcmReader->LoadImages(); + reader->LoadImages(); - unsigned int numberOfOutputs = gdcmReader->GetNumberOfOutputs(); + unsigned int numberOfOutputs = reader->GetNumberOfOutputs(); for (unsigned int o = 0; o < numberOfOutputs; ++o) { - const mitk::DICOMImageBlockDescriptor block = gdcmReader->GetOutput(o); + const mitk::DICOMImageBlockDescriptor block = reader->GetOutput(o); const mitk::DICOMImageFrameList& outputFiles = block.GetImageFrameList(); mitk::Image::Pointer mitkImage = block.GetMitkImage(); MITK_INFO << "-------------------------------------------"; MITK_INFO << "Output " << o << " at " << (void*) mitkImage.GetPointer(); MITK_INFO << " Number of files: " << outputFiles.size(); if (mitkImage.IsNotNull()) { MITK_INFO << " Dimensions: " << mitkImage->GetDimension(0) << " " << mitkImage->GetDimension(1) << " " << mitkImage->GetDimension(2); } } + + return EXIT_SUCCESS; } diff --git a/Modules/DICOMReader/Documentation/Doxygen/Modules.dox b/Modules/DICOMReader/Documentation/Doxygen/Modules.dox new file mode 100644 index 0000000000..a23b919558 --- /dev/null +++ b/Modules/DICOMReader/Documentation/Doxygen/Modules.dox @@ -0,0 +1,39 @@ +/** +\defgroup DICOMReaderModule The DICOMReader Module +\ingroup MITKModules + +\brief DICOM This module contains a number of classes for DICOM image loading within MITK. + +\section DICOMReaderModule_overview Scope + +When people around MITK talk about loading DICOM images or series, they are really not interested in the +images (2D slices) but in a 3D mitk::Image instance that groups DICOM images in a meaningful way. + +The task of this module is to provide + - an interface for the general "DICOM files to mitk::Images" transformation: mitk::DICOMFileReader + - a flexible default reader for the most common DICOM images: mitk::DICOMITKSeriesGDCMReader + +Most documentation is kept with these classes, so please have a look at their documentations. + +\remark Prior to this module, a class called DicomSeriesReader grew too much to be maintainable anymore. Its functionality is conserved by class mitk::ClassicDICOMSeriesReader. + +\section DICOMReaderModule_interface Module Interface + +The general loading proceduce is structured by mitk::DICOMFileReader, under the assumption that the best reader for a set of files is not known prior to inspection. +To facilitate applications that need to select between possible readers based on what would be loaded, +file readers are required to implement loading in two separate steps: + + 1. analysis of a list of files and description of potential mitk::Image%s by means of mitk::DICOMImageBlockDescriptor + 2. actual loading of pixel data into mitk::Image%s + +A very simple implementation is mitk::DICOMFileReaderSelector which selects the reader with the least possible number of mitk::Images (least confusing for the user?). + +\section DICOMReaderModule_tasks Tasks for future development + +Unstructured development tasks and ideas for future extensions + + - a reader for vector-valued diffusion images is currently being implemented + - a multi-frame image reader based on DCMTK + - perspective: slice-by-slice loading + +*/ diff --git a/Core/Documentation/images/tilt-correction.jpg b/Modules/DICOMReader/Documentation/Doxygen/tilt-correction.jpg similarity index 100% copy from Core/Documentation/images/tilt-correction.jpg copy to Modules/DICOMReader/Documentation/Doxygen/tilt-correction.jpg diff --git a/Modules/DICOMReader/Documentation/images/implementeditkseriesgdcmreader.jpg b/Modules/DICOMReader/Documentation/images/implementeditkseriesgdcmreader.jpg new file mode 100644 index 0000000000..ade39888f3 Binary files /dev/null and b/Modules/DICOMReader/Documentation/images/implementeditkseriesgdcmreader.jpg differ diff --git a/Core/Documentation/images/tilt-correction.jpg b/Modules/DICOMReader/Documentation/images/tilt-correction.jpg similarity index 100% rename from Core/Documentation/images/tilt-correction.jpg rename to Modules/DICOMReader/Documentation/images/tilt-correction.jpg diff --git a/Modules/DICOMReader/Resources/configurations/3D/classicreader.xml b/Modules/DICOMReader/Resources/configurations/3D/classicreader.xml new file mode 100644 index 0000000000..17f3e4453d --- /dev/null +++ b/Modules/DICOMReader/Resources/configurations/3D/classicreader.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/Modules/DICOMReader/Resources/configurations/3D/descriptionbasedgrouping.xml b/Modules/DICOMReader/Resources/configurations/3D/descriptionbasedgrouping.xml new file mode 100644 index 0000000000..783873bdf7 --- /dev/null +++ b/Modules/DICOMReader/Resources/configurations/3D/descriptionbasedgrouping.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Modules/DICOMReader/Resources/configurations/3D/imagetime.xml b/Modules/DICOMReader/Resources/configurations/3D/imagetime.xml new file mode 100644 index 0000000000..a841f8c960 --- /dev/null +++ b/Modules/DICOMReader/Resources/configurations/3D/imagetime.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + diff --git a/Modules/DICOMReader/Resources/configurations/3D/instancenumber.xml b/Modules/DICOMReader/Resources/configurations/3D/instancenumber.xml new file mode 100644 index 0000000000..a412da3a77 --- /dev/null +++ b/Modules/DICOMReader/Resources/configurations/3D/instancenumber.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + diff --git a/Modules/DICOMReader/Resources/configurations/3D/slicelocation.xml b/Modules/DICOMReader/Resources/configurations/3D/slicelocation.xml new file mode 100644 index 0000000000..33974e6303 --- /dev/null +++ b/Modules/DICOMReader/Resources/configurations/3D/slicelocation.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + diff --git a/Modules/DICOMReader/Resources/configurations/3DnT/classicreader.xml b/Modules/DICOMReader/Resources/configurations/3DnT/classicreader.xml new file mode 100644 index 0000000000..4bb181dda8 --- /dev/null +++ b/Modules/DICOMReader/Resources/configurations/3DnT/classicreader.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/Modules/DICOMReader/Testing/files.cmake b/Modules/DICOMReader/Testing/files.cmake index df7c751ba1..848c755de2 100644 --- a/Modules/DICOMReader/Testing/files.cmake +++ b/Modules/DICOMReader/Testing/files.cmake @@ -1,11 +1,12 @@ set(MODULE_TESTS ) set(MODULE_CUSTOM_TESTS mitkDICOMFileReaderTest.cpp mitkDICOMITKSeriesGDCMReaderBasicsTest.cpp ) set(TEST_CPP_FILES mitkDICOMNullFileReader.cpp + mitkDICOMFilenameSorter.cpp ) diff --git a/Modules/DICOMReader/mitkDICOMFilenameSorter.cpp b/Modules/DICOMReader/Testing/mitkDICOMFilenameSorter.cpp similarity index 89% rename from Modules/DICOMReader/mitkDICOMFilenameSorter.cpp rename to Modules/DICOMReader/Testing/mitkDICOMFilenameSorter.cpp index 23850e66a9..f4f83cbef5 100644 --- a/Modules/DICOMReader/mitkDICOMFilenameSorter.cpp +++ b/Modules/DICOMReader/Testing/mitkDICOMFilenameSorter.cpp @@ -1,73 +1,80 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #include "mitkDICOMFilenameSorter.h" #include mitk::DICOMFilenameSorter ::DICOMFilenameSorter() :DICOMDatasetSorter() { } mitk::DICOMFilenameSorter ::~DICOMFilenameSorter() { } mitk::DICOMFilenameSorter ::DICOMFilenameSorter(const DICOMFilenameSorter& other ) :DICOMDatasetSorter(other) { } mitk::DICOMFilenameSorter& mitk::DICOMFilenameSorter ::operator=(const DICOMFilenameSorter& other) { if (this != &other) { DICOMDatasetSorter::operator=(other); } return *this; } mitk::DICOMTagList mitk::DICOMFilenameSorter ::GetTagsOfInterest() { return DICOMTagList(); } +void +mitk::DICOMFilenameSorter +::PrintConfiguration(std::ostream& os, const std::string& indent) const +{ + os << indent << "Sort alphabetically based on filenames" << std::endl; +} + bool mitk::DICOMFilenameSorter::FilenameSort ::operator() (const mitk::DICOMDatasetAccess* left, const mitk::DICOMDatasetAccess* right) { return left->GetFilenameIfAvailable().compare( right->GetFilenameIfAvailable() ) < 0 ; } void mitk::DICOMFilenameSorter ::Sort() { DICOMDatasetList output = GetInput(); // copy std::sort( output.begin(), output.end(), FilenameSort() ); this->SetNumberOfOutputs(1); this->SetOutput(0, output); } diff --git a/Modules/DICOMReader/mitkDICOMFilenameSorter.h b/Modules/DICOMReader/Testing/mitkDICOMFilenameSorter.h similarity index 87% rename from Modules/DICOMReader/mitkDICOMFilenameSorter.h rename to Modules/DICOMReader/Testing/mitkDICOMFilenameSorter.h index d80cff0c13..49a6056869 100644 --- a/Modules/DICOMReader/mitkDICOMFilenameSorter.h +++ b/Modules/DICOMReader/Testing/mitkDICOMFilenameSorter.h @@ -1,55 +1,58 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #ifndef mitkDICOMFilenameSorter_h #define mitkDICOMFilenameSorter_h #include "mitkDICOMDatasetSorter.h" namespace mitk { /** + \ingroup DICOMReaderModule \brief sort files based on filename (last resort). */ -class DICOMReader_EXPORT DICOMFilenameSorter : public DICOMDatasetSorter +class DICOMFilenameSorter : public DICOMDatasetSorter { public: mitkClassMacro( DICOMFilenameSorter, DICOMDatasetSorter ) itkNewMacro( DICOMFilenameSorter ) virtual DICOMTagList GetTagsOfInterest(); virtual void Sort(); + virtual void PrintConfiguration(std::ostream& os, const std::string& indent = "") const; + protected: struct FilenameSort { bool operator() (const mitk::DICOMDatasetAccess* left, const mitk::DICOMDatasetAccess* right); }; DICOMFilenameSorter(); virtual ~DICOMFilenameSorter(); DICOMFilenameSorter(const DICOMFilenameSorter& other); DICOMFilenameSorter& operator=(const DICOMFilenameSorter& other); }; } #endif diff --git a/Modules/DICOMReader/Testing/mitkDICOMNullFileReader.cpp b/Modules/DICOMReader/Testing/mitkDICOMNullFileReader.cpp index 949cd0f728..8a6891b938 100644 --- a/Modules/DICOMReader/Testing/mitkDICOMNullFileReader.cpp +++ b/Modules/DICOMReader/Testing/mitkDICOMNullFileReader.cpp @@ -1,88 +1,95 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #include "mitkDICOMNullFileReader.h" mitk::DICOMNullFileReader ::DICOMNullFileReader() :DICOMFileReader() { } mitk::DICOMNullFileReader ::~DICOMNullFileReader() { } mitk::DICOMNullFileReader ::DICOMNullFileReader(const DICOMNullFileReader& other ) :DICOMFileReader(other) { } mitk::DICOMNullFileReader& mitk::DICOMNullFileReader ::operator=(const DICOMNullFileReader& other) { if (this != &other) { DICOMFileReader::operator=(other); } return *this; } +void +mitk::DICOMNullFileReader +::InternalPrintConfiguration(std::ostream& os) const +{ + os << "Nothing to configure" << std::endl; +} + void mitk::DICOMNullFileReader ::AnalyzeInputFiles() { this->ClearOutputs(); StringList inputFilenames = this->GetInputFiles(); this->SetNumberOfOutputs( inputFilenames.size() ); //generates one output for each input unsigned int o = 0; for (StringList::const_iterator inputIter = inputFilenames.begin(); inputIter != inputFilenames.end(); ++o, ++inputIter) { DICOMImageBlockDescriptor block; DICOMImageFrameList outputFrames; outputFrames.push_back( DICOMImageFrameInfo::New(*inputIter) ); block.SetImageFrameList( outputFrames ); this->SetOutput( o, block ); } } // void AllocateOutputImages(); bool mitk::DICOMNullFileReader ::LoadImages() { // does nothing return true; } bool mitk::DICOMNullFileReader ::CanHandleFile(const std::string& itkNotUsed(filename)) { return true; // can handle all } diff --git a/Modules/DICOMReader/Testing/mitkDICOMNullFileReader.h b/Modules/DICOMReader/Testing/mitkDICOMNullFileReader.h index 708a59e21f..77e671c995 100644 --- a/Modules/DICOMReader/Testing/mitkDICOMNullFileReader.h +++ b/Modules/DICOMReader/Testing/mitkDICOMNullFileReader.h @@ -1,53 +1,55 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #ifndef mitkDICOMNullFileReader_h #define mitkDICOMNullFileReader_h #include "mitkDICOMFileReader.h" namespace mitk { class DICOMNullFileReader : public DICOMFileReader { public: mitkClassMacro( DICOMNullFileReader, DICOMFileReader ); mitkCloneMacro( DICOMNullFileReader ); itkNewMacro( DICOMNullFileReader ); virtual void AnalyzeInputFiles(); // void AllocateOutputImages(); virtual bool LoadImages(); virtual bool CanHandleFile(const std::string& filename); protected: DICOMNullFileReader(); virtual ~DICOMNullFileReader(); DICOMNullFileReader(const DICOMNullFileReader& other); DICOMNullFileReader& operator=(const DICOMNullFileReader& other); + void InternalPrintConfiguration(std::ostream& os) const; + private: }; } #endif diff --git a/Modules/DICOMReader/files.cmake b/Modules/DICOMReader/files.cmake index b411d136cf..c106e0c49f 100644 --- a/Modules/DICOMReader/files.cmake +++ b/Modules/DICOMReader/files.cmake @@ -1,39 +1,51 @@ set(H_FILES mitkDICOMFileReader.h mitkDICOMImageFrameInfo.h mitkDICOMImageBlockDescriptor.h mitkDICOMGDCMImageFrameInfo.h mitkDICOMITKSeriesGDCMReader.h mitkDICOMDatasetSorter.h - mitkDICOMFilenameSorter.h mitkDICOMEnums.h mitkDICOMTagBasedSorter.h mitkDICOMSortCriterion.h mitkDICOMSortByTag.h mitkEquiDistantBlocksSorter.h mitkSortByImagePositionPatient.h mitkClassicDICOMSeriesReader.h mitkThreeDnTDICOMSeriesReader.h mitkDICOMTag.h + mitkDICOMReaderConfigurator.h + mitkDICOMFileReaderSelector.h ) set(CPP_FILES mitkDICOMFileReader.cpp mitkDICOMImageBlockDescriptor.cpp mitkDICOMITKSeriesGDCMReader.cpp mitkDICOMDatasetSorter.cpp - mitkDICOMFilenameSorter.cpp mitkDICOMTagBasedSorter.cpp mitkDICOMGDCMImageFrameInfo.cpp mitkDICOMImageFrameInfo.cpp mitkDICOMSortCriterion.cpp mitkDICOMSortByTag.cpp mitkITKDICOMSeriesReaderHelper.cpp mitkEquiDistantBlocksSorter.cpp mitkSortByImagePositionPatient.cpp mitkGantryTiltInformation.cpp mitkClassicDICOMSeriesReader.cpp mitkThreeDnTDICOMSeriesReader.cpp mitkDICOMTag.cpp mitkDICOMEnums.cpp + mitkDICOMReaderConfigurator.cpp + mitkDICOMFileReaderSelector.cpp +) + +set(RESOURCE_FILES + configurations/3D/classicreader.xml + configurations/3D/descriptionbasedgrouping.xml + configurations/3D/instancenumber.xml + configurations/3D/slicelocation.xml + configurations/3D/imagetime.xml + + configurations/3DnT/classicreader.xml ) diff --git a/Modules/DICOMReader/mitkClassicDICOMSeriesReader.h b/Modules/DICOMReader/mitkClassicDICOMSeriesReader.h index 4a9e19d465..e550c02b6c 100644 --- a/Modules/DICOMReader/mitkClassicDICOMSeriesReader.h +++ b/Modules/DICOMReader/mitkClassicDICOMSeriesReader.h @@ -1,50 +1,81 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #ifndef mitkClassicDICOMSeriesReader_h #define mitkClassicDICOMSeriesReader_h #include "mitkThreeDnTDICOMSeriesReader.h" #include "DICOMReaderExports.h" namespace mitk { -/* - \brief Sorting and grouping like mitk::DicomSeriesReader until 2013. +/** + \ingroup DICOMReaderModule + \brief Sorting and grouping like mitk::DicomSeriesReader until 2013. + + This class implements the same functionality as the legacy class DicomSeriesReader, + except that is is 75 lines instead of 2500 lines. + \warning Since the old class is known to have problems with some series, + it is advised to use a good configuration of DICOMITKSeriesGDCMReader, + which can be obtained by using DICOMFileReaderSelector. + + The following text documents the actual sorting logic of this reader. + + The class groups datasets that have different values in any of the following tags: + - (0028,0010) Number of Rows + - (0028,0011) Number of Columns + - (0028,0030) Pixel Spacing + - (0018,1164) Imager Pixel Spacing + - (0020,0037) %Image Orientation (Patient) + - (0018,0050) Slice Thickness + - (0028,0008) Number of Frames + - (0020,000e) Series Instance UID + + Within each of the groups, datasets are sorted by the value of the following tags (primary sorting first): + - (0020,0032) %Image Position (Patient) (distance from zero along normal of (0020,0037) %Image Orientation (Patient)) + - (0020,0012) Aqcuisition Number + - (0008,0032) Aqcuisition Time + - (0018,1060) Trigger Time + - (0008,0018) SOP Instance UID (last resort, not really meaningful but decides clearly) + + If the series was acquired using a tilted gantry, this will be "fixed" by applying a shear transformation. + + If multiple images occupy the same position in space, it is assumed that this indicated a 3D+t image. + */ class DICOMReader_EXPORT ClassicDICOMSeriesReader : public ThreeDnTDICOMSeriesReader { public: mitkClassMacro( ClassicDICOMSeriesReader, DICOMITKSeriesGDCMReader ); mitkCloneMacro( ClassicDICOMSeriesReader ); itkNewMacro( ClassicDICOMSeriesReader ); protected: ClassicDICOMSeriesReader(); virtual ~ClassicDICOMSeriesReader(); ClassicDICOMSeriesReader(const ClassicDICOMSeriesReader& other); ClassicDICOMSeriesReader& operator=(const ClassicDICOMSeriesReader& other); }; } #endif diff --git a/Modules/DICOMReader/mitkDICOMDatasetAccess.h b/Modules/DICOMReader/mitkDICOMDatasetAccess.h index 4f6742b2b8..f0c63a75e3 100644 --- a/Modules/DICOMReader/mitkDICOMDatasetAccess.h +++ b/Modules/DICOMReader/mitkDICOMDatasetAccess.h @@ -1,42 +1,52 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #ifndef mitkDICOMDatasetAccess_h #define mitkDICOMDatasetAccess_h #include "mitkDICOMTag.h" #include "DICOMReaderExports.h" namespace mitk { +/** + \ingroup DICOMReaderModule + \brief Interface to datasets that is presented to sorting classes such as DICOMDatasetSorter. + + Minimal interface to hide actual implementation, which might rely on GDCM. +*/ class DICOMReader_EXPORT DICOMDatasetAccess { public: + /// \brief Return a filename if possible. + /// If DICOM is not read from file but from somewhere else (network, database), we might not have files. virtual std::string GetFilenameIfAvailable() const = 0; + + /// \brief Return the raw value of the tag as a string virtual std::string GetTagValueAsString(const mitk::DICOMTag& tag) const = 0; virtual ~DICOMDatasetAccess() {}; }; typedef std::vector DICOMDatasetList; } #endif diff --git a/Modules/DICOMReader/mitkDICOMDatasetSorter.h b/Modules/DICOMReader/mitkDICOMDatasetSorter.h index f0772d99a3..43a5980ebb 100644 --- a/Modules/DICOMReader/mitkDICOMDatasetSorter.h +++ b/Modules/DICOMReader/mitkDICOMDatasetSorter.h @@ -1,65 +1,93 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #ifndef mitkDICOMDatasetSorter_h #define mitkDICOMDatasetSorter_h #include "itkObjectFactory.h" #include "mitkCommon.h" #include "mitkDICOMDatasetAccess.h" namespace mitk { +/** + \ingroup DICOMReaderModule + \brief The sorting/splitting building-block of DICOMITKSeriesGDCMReader. + + This class describes the interface of the sorting/splitting process + described as part of DICOMITKSeriesGDCMReader::AnalyzeInputFiles() + (see \ref DICOMITKSeriesGDCMReader_LoadingStrategy). + + The prodecure is simple: + - take a list of input datasets (DICOMDatasetAccess) + - sort them (to be defined by sub-classes, based on specific tags) + - return the sorting result as outputs (the single input might be distributed into multiple outputs) + + The simplest and most generic form of sorting is implemented in + sub-class DICOMTagBasedSorter. +*/ class DICOMReader_EXPORT DICOMDatasetSorter : public itk::LightObject { public: mitkClassMacro( DICOMDatasetSorter, itk::LightObject ) + /** + \brief Return the tags of interest (to facilitate scanning) + */ virtual DICOMTagList GetTagsOfInterest() = 0; + /// \brief Input for sorting void SetInput(DICOMDatasetList filenames); + /// \brief Input for sorting const DICOMDatasetList& GetInput() const; + /// \brief Sort input datasets into one or multiple outputs. virtual void Sort() = 0; + /// \brief Output of the sorting process. unsigned int GetNumberOfOutputs() const; + /// \brief Output of the sorting process. const DICOMDatasetList& GetOutput(unsigned int index) const; + /// \brief Output of the sorting process. DICOMDatasetList& GetOutput(unsigned int index); + /// \brief Print configuration details into stream. + virtual void PrintConfiguration(std::ostream& os, const std::string& indent = "") const = 0; + protected: DICOMDatasetSorter(); virtual ~DICOMDatasetSorter(); DICOMDatasetSorter(const DICOMDatasetSorter& other); DICOMDatasetSorter& operator=(const DICOMDatasetSorter& other); void ClearOutputs(); void SetNumberOfOutputs(unsigned int numberOfOutputs); void SetOutput(unsigned int index, const DICOMDatasetList& output); private: DICOMDatasetList m_Input; std::vector< DICOMDatasetList > m_Outputs; }; } #endif diff --git a/Modules/DICOMReader/mitkDICOMEnums.h b/Modules/DICOMReader/mitkDICOMEnums.h index 1332db0e6c..3dc047ec80 100644 --- a/Modules/DICOMReader/mitkDICOMEnums.h +++ b/Modules/DICOMReader/mitkDICOMEnums.h @@ -1,64 +1,68 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #ifndef mitkDICOMEnums_h #define mitkDICOMEnums_h #include #include #include namespace mitk { typedef std::vector StringList; typedef std::vector BoolList; /** - \brief How the mitk::Image spacing should be interpreted. + \ingroup DICOMReaderModule + \brief How the mitk::Image spacing should be interpreted (see mitk::DICOMFileReader). Compare DICOM PS 3.3 10.7 (Basic Pixel Spacing Calibration Macro). */ typedef enum { - SpacingInPatient, /// distances are mm within a patient - SpacingAtDetector, /// distances are mm at detector surface - SpacingUnknown /// NO spacing information is present, we use (1,1) as default + SpacingInPatient, ///< distances are mm within a patient + SpacingAtDetector, ///< distances are mm at detector surface + SpacingUnknown ///< NO spacing information is present, we use (1,1) as default } PixelSpacingInterpretation; /** - \brief Describes how well the reader is tested for a certain file type. + \ingroup DICOMReaderModule + \brief Describes how well the reader is tested for a certain file type (see mitk::DICOMFileReader). Applications should not rely on the outcome for images which are reported Implemented or Unsupported. Errors to load images which are reported as Supported are considered bugs. For PartlySupported please check the reader documentation on specifics. */ typedef enum { - SOPClassSupported, /// loader code and tests are established - SOPClassPartlySupported, /// loader code and tests are establised for specific parts of a SOP Class - SOPClassImplemented, /// loader code is implemented but not accompanied by tests - SOPClassUnsupported, /// loader code is not known to work with this SOP Class - SOPClassUnknown, /// loader did not yet inspect any images, unknown fitness + SOPClassSupported, ///< loader code and tests are established + SOPClassPartlySupported, ///< loader code and tests are establised for specific parts of a SOP Class + SOPClassImplemented, ///< loader code is implemented but not accompanied by tests + SOPClassUnsupported, ///< loader code is not known to work with this SOP Class + SOPClassUnknown, ///< loader did not yet inspect any images, unknown fitness } ReaderImplementationLevel; + /// Convert mitk::PixelSpacingInterpretation to a human readable string. std::string PixelSpacingInterpretationToString(const PixelSpacingInterpretation& value); + /// Convert mitk::ReaderImplementationLevel to a human readable string. std::string ReaderImplementationLevelToString( const ReaderImplementationLevel& enumValue ); } #endif diff --git a/Modules/DICOMReader/mitkDICOMFileReader.cpp b/Modules/DICOMReader/mitkDICOMFileReader.cpp index b7ae23911a..2a1240136a 100644 --- a/Modules/DICOMReader/mitkDICOMFileReader.cpp +++ b/Modules/DICOMReader/mitkDICOMFileReader.cpp @@ -1,155 +1,197 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #include "mitkDICOMFileReader.h" #include mitk::DICOMFileReader ::DICOMFileReader() :itk::LightObject() { } mitk::DICOMFileReader ::~DICOMFileReader() { } mitk::DICOMFileReader ::DICOMFileReader(const DICOMFileReader& other ) :itk::LightObject() ,m_Outputs( other.m_Outputs ) +,m_ConfigLabel( other.m_ConfigLabel ) +,m_ConfigDescription( other.m_ConfigDescription ) { } mitk::DICOMFileReader& mitk::DICOMFileReader ::operator=(const DICOMFileReader& other) { if (this != &other) { m_InputFilenames = other.m_InputFilenames; m_Outputs = other.m_Outputs; + m_ConfigLabel = other.m_ConfigLabel; + m_ConfigDescription = other.m_ConfigDescription; } return *this; } +void +mitk::DICOMFileReader +::SetConfigurationLabel(const std::string& label) +{ + m_ConfigLabel = label; +} + +std::string +mitk::DICOMFileReader +::GetConfigurationLabel() +{ + return m_ConfigLabel; +} + +void +mitk::DICOMFileReader +::SetConfigurationDescription(const std::string& desc) +{ + m_ConfigDescription = desc; +} + +std::string +mitk::DICOMFileReader +::GetConfigurationDescription() +{ + return m_ConfigDescription; +} + void mitk::DICOMFileReader ::SetInputFiles(StringList filenames) { m_InputFilenames = filenames; } const mitk::StringList& mitk::DICOMFileReader ::GetInputFiles() const { return m_InputFilenames; } unsigned int mitk::DICOMFileReader ::GetNumberOfOutputs() const { return m_Outputs.size(); } void mitk::DICOMFileReader ::ClearOutputs() { m_Outputs.clear(); } void mitk::DICOMFileReader ::SetNumberOfOutputs(unsigned int numberOfOutputs) { m_Outputs.resize(numberOfOutputs); } void mitk::DICOMFileReader ::SetOutput(unsigned int index, const mitk::DICOMImageBlockDescriptor& output) { if (index < m_Outputs.size()) { m_Outputs[index] = output; } else { std::stringstream ss; ss << "Index " << index << " out of range (" << m_Outputs.size() << " indices reserved)"; throw std::invalid_argument( ss.str() ); } } void mitk::DICOMFileReader -::PrintOutputs(std::ostream& os, bool filenameDetails) +::PrintConfiguration(std::ostream& os) const +{ + os << "---- Configuration of " << this->GetNameOfClass() <<" " << (void*)this << " ----"<< std::endl; + this->InternalPrintConfiguration(os); + os << "---- End of configuration ----" << std::endl; +} + + +void +mitk::DICOMFileReader +::PrintOutputs(std::ostream& os, bool filenameDetails) const { - os << "---- Outputs of DICOMFilereader " << (void*)this << "----"<< std::endl; + os << "---- Outputs of DICOMFilereader " << (void*)this << " ----"<< std::endl; for (unsigned int o = 0; o < m_Outputs.size(); ++o) { os << "-- Output " << o << std::endl; const DICOMImageBlockDescriptor& block = m_Outputs[o]; block.Print(os, filenameDetails); } os << "---- End of output list ----" << std::endl; } const mitk::DICOMImageBlockDescriptor& mitk::DICOMFileReader ::GetOutput(unsigned int index) const { if (index < m_Outputs.size()) { return m_Outputs[index]; } else { std::stringstream ss; ss << "Index " << index << " out of range (" << m_Outputs.size() << " indices reserved)"; throw std::invalid_argument( ss.str() ); } } mitk::DICOMImageBlockDescriptor& mitk::DICOMFileReader ::InternalGetOutput(unsigned int index) { if (index < m_Outputs.size()) { return m_Outputs[index]; } else { std::stringstream ss; ss << "Index " << index << " out of range (" << m_Outputs.size() << " indices reserved)"; throw std::invalid_argument( ss.str() ); } } bool mitk::DICOMFileReader ::IsDICOM(const std::string& filename) { itk::GDCMImageIO::Pointer io = itk::GDCMImageIO::New(); return io->CanReadFile( filename.c_str() ); } diff --git a/Modules/DICOMReader/mitkDICOMFileReader.h b/Modules/DICOMReader/mitkDICOMFileReader.h index e3c230d0f8..27e8a06a38 100644 --- a/Modules/DICOMReader/mitkDICOMFileReader.h +++ b/Modules/DICOMReader/mitkDICOMFileReader.h @@ -1,83 +1,126 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #ifndef mitkDICOMFileReader_h #define mitkDICOMFileReader_h #include "itkObjectFactory.h" #include "mitkCommon.h" #include "DICOMReaderExports.h" #include "mitkDICOMImageBlockDescriptor.h" namespace mitk { // TODO Philips3D! // TODO http://bugs.mitk.org/show_bug.cgi?id=11572 ? + +/** + \ingroup DICOMReaderModule + \brief Interface for DICOM readers that produce mitk::Images. + + As described in \ref DICOMReaderModule, this class structures + the reader's part in the process of analyzing a set of DICOM files + and selecting the most appropriate reader. + + The overall loading process is as follows: + - Define input files: a list of absolute filenames + - Analyze the potential output: see what can be made of the input files, describe with DICOMImageBlockDescriptor%s + - Load pixel data: an application will usually analyze files using multiple readers and only load with a single reader + + Sub-classes are required to implement a number of methods that + reflect above structure. See mitk::DICOMITKSeriesGDCMReader for + an example. + + To help applications in describing different readers to the user, each reader + brings a number of methods that describe its configuration/specifics by + means of a short label and a (longer) description. +*/ class DICOMReader_EXPORT DICOMFileReader : public itk::LightObject { public: - enum LoadingConfidence - { - NoSupport = 0, - FullSupport = 1, - PartialSupport = 2, - }; + mitkClassMacro( DICOMFileReader, itk::LightObject ); - mitkClassMacro( DICOMFileReader, itk::LightObject ) + /// Test whether a file is DICOM at all + static bool IsDICOM(const std::string& filename); + /// Indicate whether this reader can handle given file + virtual bool CanHandleFile(const std::string& filename) = 0; + /// This input files void SetInputFiles(StringList filenames); + /// This input files const StringList& GetInputFiles() const; + /// Analyze input files virtual void AnalyzeInputFiles() = 0; + + /// Number of outputs, only meaningful after calling AnalyzeInputFiles() unsigned int GetNumberOfOutputs() const; + /// Individual outputs, only meaningful after calling AnalyzeInputFiles(). \throws std::invalid_argument const DICOMImageBlockDescriptor& GetOutput(unsigned int index) const; - // void AllocateOutputImages(); + // void AllocateOutputImages(); TODO for later implementation of slice-by-slice loading + + /// Load the mitk::Image%s in our outputs, the DICOMImageBlockDescriptor. To be called only after AnalyzeInputFiles(). Take care of potential exceptions! virtual bool LoadImages() = 0; - virtual bool CanHandleFile(const std::string& filename) = 0; + /// Short label/name to describe this reader + void SetConfigurationLabel(const std::string&); + /// Short label/name to describe this reader + std::string GetConfigurationLabel(); + /// One-sentence description of the reader's loading "strategy" + void SetConfigurationDescription(const std::string&); + /// One-sentence description of the reader's loading "strategy" + std::string GetConfigurationDescription(); - void PrintOutputs(std::ostream& os, bool filenameDetails = false); - - static bool IsDICOM(const std::string& filename); + /// Print configuration description to given stream, for human reader + void PrintConfiguration(std::ostream& os) const; + /// Print output description to given stream, for human reader + void PrintOutputs(std::ostream& os, bool filenameDetails = false) const; protected: DICOMFileReader(); virtual ~DICOMFileReader(); DICOMFileReader(const DICOMFileReader& other); DICOMFileReader& operator=(const DICOMFileReader& other); void ClearOutputs(); void SetNumberOfOutputs(unsigned int numberOfOutputs); void SetOutput(unsigned int index, const DICOMImageBlockDescriptor& output); + /// non-const access to the DICOMImageBlockDescriptor DICOMImageBlockDescriptor& InternalGetOutput(unsigned int index); + /// Configuration description for human reader, to be implemented by sub-classes + virtual void InternalPrintConfiguration(std::ostream& os) const = 0; + private: StringList m_InputFilenames; std::vector< DICOMImageBlockDescriptor > m_Outputs; + + std::string m_ConfigLabel; + std::string m_ConfigDescription; }; } #endif diff --git a/Modules/DICOMReader/mitkDICOMFileReaderSelector.cpp b/Modules/DICOMReader/mitkDICOMFileReaderSelector.cpp new file mode 100644 index 0000000000..5d9509d169 --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMFileReaderSelector.cpp @@ -0,0 +1,184 @@ +/*=================================================================== + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center, +Division of Medical and Biological Informatics. +All rights reserved. + +This software is distributed WITHOUT ANY WARRANTY; without +even the implied warranty of MERCHANTABILITY or FITNESS FOR +A PARTICULAR PURPOSE. + +See LICENSE.txt or http://www.mitk.org for details. + +===================================================================*/ + +#include "mitkDICOMFileReaderSelector.h" +#include "mitkDICOMReaderConfigurator.h" + +#include "mitkModuleContext.h" + +#include +#include +#include +#include + +mitk::DICOMFileReaderSelector +::DICOMFileReaderSelector() +{ +} + +mitk::DICOMFileReaderSelector +::~DICOMFileReaderSelector() +{ +} + +void +mitk::DICOMFileReaderSelector +::AddConfigsFromResources(const std::string& path) +{ + std::vector configs = GetModuleContext()->GetModule()->FindResources(path, "*.xml", false); + + for (std::vector::iterator iter = configs.begin(); + iter != configs.end(); + ++iter) + { + ModuleResource& resource = *iter; + if (resource.IsValid()) + { + ModuleResourceStream stream(resource); + + // read all into string s + std::string s; + + stream.seekg(0, std::ios::end); + s.reserve(stream.tellg()); + stream.seekg(0, std::ios::beg); + + s.assign((std::istreambuf_iterator(stream)), + std::istreambuf_iterator()); + + //MITK_INFO << "------------ One resource with content\n" << s << "\n------- end -------"; + this->AddConfig(s); + } + } +} + +void +mitk::DICOMFileReaderSelector +::LoadBuiltIn3DConfigs() +{ + this->AddConfigsFromResources("configurations/3D"); +} + +void +mitk::DICOMFileReaderSelector +::LoadBuiltIn3DnTConfigs() +{ + this->AddConfigsFromResources("configurations/3DnT"); +} + +void +mitk::DICOMFileReaderSelector +::AddConfig(const std::string& xmlDescription) +{ + DICOMReaderConfigurator::Pointer configurator = DICOMReaderConfigurator::New(); + DICOMFileReader::Pointer reader = configurator->CreateFromUTF8ConfigString(xmlDescription); + + if (reader.IsNotNull()) + { + m_Readers.push_back( reader ); + m_PossibleConfigurations.push_back(xmlDescription); + } + else + { + std::stringstream ss; + ss << "Could not parse reader configuration. Ignoring it."; + throw std::invalid_argument( ss.str() ); + } +} + +void +mitk::DICOMFileReaderSelector +::AddConfigFile(const std::string& filename) +{ + std::ifstream file(filename.c_str()); + std::string s; + + file.seekg(0, std::ios::end); + s.reserve(file.tellg()); + file.seekg(0, std::ios::beg); + + s.assign((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + + this->AddConfig(s); +} + +void +mitk::DICOMFileReaderSelector +::SetInputFiles(StringList filenames) +{ + m_InputFilenames = filenames; +} + +const mitk::StringList& +mitk::DICOMFileReaderSelector +::GetInputFiles() const +{ + return m_InputFilenames; +} + +mitk::DICOMFileReader::Pointer +mitk::DICOMFileReaderSelector +::GetFirstReaderWithMinimumNumberOfOutputImages() +{ + ReaderList workingCandidates; + + // let all readers analyze the file set + unsigned int readerIndex(0); + for (ReaderList::iterator rIter = m_Readers.begin(); + rIter != m_Readers.end(); + ++readerIndex, ++rIter) + { + (*rIter)->SetInputFiles( m_InputFilenames ); + try + { + (*rIter)->AnalyzeInputFiles(); + workingCandidates.push_back( *rIter ); + MITK_INFO << "Reader " << readerIndex << " (" << (*rIter)->GetConfigurationLabel() << ") suggests " << (*rIter)->GetNumberOfOutputs() << " 3D blocks"; + } + catch (std::exception& e) + { + MITK_ERROR << "Reader " << readerIndex << " (" << (*rIter)->GetConfigurationLabel() << ") threw exception during file analysis, ignoring this reader. Exception: " << e.what(); + } + catch (...) + { + MITK_ERROR << "Reader " << readerIndex << " (" << (*rIter)->GetConfigurationLabel() << ") threw unknown exception during file analysis, ignoring this reader."; + } + } + + DICOMFileReader::Pointer bestReader; + + unsigned int minimumNumberOfOutputs = std::numeric_limits::max(); + readerIndex = 0; + unsigned int bestReaderIndex(0); + // select the reader with the minimum number of mitk::Images as output + for (ReaderList::iterator rIter = workingCandidates.begin(); + rIter != workingCandidates.end(); + ++readerIndex, ++rIter) + { + if ( (*rIter)->GetNumberOfOutputs() < minimumNumberOfOutputs ) + { + minimumNumberOfOutputs = (*rIter)->GetNumberOfOutputs(); + bestReader = *rIter; + bestReaderIndex = readerIndex; + } + } + + MITK_DEBUG << "Decided for reader #" << bestReaderIndex << " (" << bestReader->GetConfigurationLabel() << ")"; + MITK_DEBUG << m_PossibleConfigurations[bestReaderIndex]; + + return bestReader; +} diff --git a/Modules/DICOMReader/mitkDICOMFileReaderSelector.h b/Modules/DICOMReader/mitkDICOMFileReaderSelector.h new file mode 100644 index 0000000000..712a952265 --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMFileReaderSelector.h @@ -0,0 +1,91 @@ +/*=================================================================== + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center, +Division of Medical and Biological Informatics. +All rights reserved. + +This software is distributed WITHOUT ANY WARRANTY; without +even the implied warranty of MERCHANTABILITY or FITNESS FOR +A PARTICULAR PURPOSE. + +See LICENSE.txt or http://www.mitk.org for details. + +===================================================================*/ + +#ifndef mitkDICOMFileReaderSelector_h +#define mitkDICOMFileReaderSelector_h + +#include "mitkDICOMFileReader.h" + +namespace mitk +{ + +/** + \ingroup DICOMReaderModule + \brief Simple best-reader selection. + + This class implements a process of comparing different DICOMFileReader%s and selecting + the reader with the minimal number of mitk::Image%s in its output. + + The code found in this class can + - just be used to select a reader using this simple strategy + - be taken as an example of how to use DICOMFileReader%s + + To create a selection of potential readers, the class makes use + of mitk::DICOMReaderConfigurator, i.e. DICOMFileReaderSelector + also expects the configuration files/strings to be in the format + expected by mitk::DICOMReaderConfigurator. + + Two convenience methods load "default" configurations from + compiled-in resources: LoadBuiltIn3DConfigs() and LoadBuiltIn3DnTConfigs(). +*/ +class DICOMReader_EXPORT DICOMFileReaderSelector : public itk::LightObject +{ + public: + + mitkClassMacro( DICOMFileReaderSelector, itk::LightObject ) + itkNewMacro( DICOMFileReaderSelector ) + + /// \brief Add a configuration as expected by DICOMReaderConfigurator. + /// Configs can only be reset by instantiating a new DICOMFileReaderSelector. + void AddConfig(const std::string& xmlDescription); + /// \brief Add a configuration as expected by DICOMReaderConfigurator. + /// Configs can only be reset by instantiating a new DICOMFileReaderSelector. + void AddConfigFile(const std::string& filename); + + /// \brief Load 3D image creating configurations from the MITK module system (see \ref mitk::Module::FindResources). + /// For a default set of configurations, look into the directory Resources of the DICOMReader module. + void LoadBuiltIn3DConfigs(); + /// \brief Load 3D+t image creating configurations from the MITK module system (see \ref mitk::Module::FindResources). + /// For a default set of configurations, look into the directory Resources of the DICOMReader module. + void LoadBuiltIn3DnTConfigs(); + + /// Input files + void SetInputFiles(StringList filenames); + /// Input files + const StringList& GetInputFiles() const; + + /// Execute the analysis and selection process. The first reader with a minimal number of outputs will be returned. + DICOMFileReader::Pointer GetFirstReaderWithMinimumNumberOfOutputImages(); + + protected: + + DICOMFileReaderSelector(); + virtual ~DICOMFileReaderSelector(); + + void AddConfigsFromResources(const std::string& path); + + private: + + StringList m_PossibleConfigurations; + StringList m_InputFilenames; + typedef std::list ReaderList; + ReaderList m_Readers; + + }; + +} // namespace + +#endif // mitkDICOMFileReaderSelector_h diff --git a/Modules/DICOMReader/mitkDICOMGDCMImageFrameInfo.h b/Modules/DICOMReader/mitkDICOMGDCMImageFrameInfo.h index f508ccd9b9..de13c2a355 100644 --- a/Modules/DICOMReader/mitkDICOMGDCMImageFrameInfo.h +++ b/Modules/DICOMReader/mitkDICOMGDCMImageFrameInfo.h @@ -1,61 +1,71 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #ifndef mitkDICOMGDCMImageFrameInfo_h #define mitkDICOMGDCMImageFrameInfo_h #include "mitkDICOMImageFrameInfo.h" #include "mitkDICOMDatasetAccess.h" #include "gdcmScanner.h" namespace mitk { + /** + \ingroup DICOMReaderModule + \brief The dataset access implementation for DICOMITKSeriesGDCMReader, based on GDCM. + + This class combines a DICOMImageFrameInfo object with the scanning results + from gdcm::Scanner. The scanning results will be used to implement the tag + access methods of DICOMDatasetAccess. + */ class DICOMReader_EXPORT DICOMGDCMImageFrameInfo : public itk::LightObject, public DICOMDatasetAccess { public: mitkClassMacro(DICOMGDCMImageFrameInfo, itk::LightObject); itkNewMacro( DICOMGDCMImageFrameInfo ); mitkNewMacro1Param( DICOMGDCMImageFrameInfo, const std::string&); mitkNewMacro2Param( DICOMGDCMImageFrameInfo, const std::string&, unsigned int ); mitkNewMacro1Param( DICOMGDCMImageFrameInfo, DICOMImageFrameInfo::Pointer); mitkNewMacro2Param( DICOMGDCMImageFrameInfo, DICOMImageFrameInfo::Pointer, gdcm::Scanner::TagToValue const&); virtual ~DICOMGDCMImageFrameInfo(); virtual std::string GetTagValueAsString(const DICOMTag&) const; std::string GetFilenameIfAvailable() const; + /// The frame that this objects refers to DICOMImageFrameInfo::Pointer GetFrameInfo() const; + /// The frame that this objects refers to void SetFrameInfo(DICOMImageFrameInfo::Pointer frameinfo); protected: DICOMImageFrameInfo::Pointer m_FrameInfo; DICOMGDCMImageFrameInfo(DICOMImageFrameInfo::Pointer frameinfo); DICOMGDCMImageFrameInfo(DICOMImageFrameInfo::Pointer frameinfo, gdcm::Scanner::TagToValue const& tagToValueMapping); DICOMGDCMImageFrameInfo(const std::string& filename = "", unsigned int frameNo = 0); gdcm::Scanner::TagToValue const& m_TagForValue; }; typedef std::vector DICOMGDCMImageFrameList; } #endif diff --git a/Modules/DICOMReader/mitkDICOMITKSeriesGDCMReader.cpp b/Modules/DICOMReader/mitkDICOMITKSeriesGDCMReader.cpp index 7e915e22b3..5a07a0eee4 100644 --- a/Modules/DICOMReader/mitkDICOMITKSeriesGDCMReader.cpp +++ b/Modules/DICOMReader/mitkDICOMITKSeriesGDCMReader.cpp @@ -1,514 +1,545 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #include "mitkDICOMITKSeriesGDCMReader.h" #include "mitkITKDICOMSeriesReaderHelper.h" #include "mitkGantryTiltInformation.h" #include "mitkDICOMTagBasedSorter.h" #include #include #include mitk::DICOMITKSeriesGDCMReader ::DICOMITKSeriesGDCMReader() :DICOMFileReader() ,m_FixTiltByShearing(true) { + this->EnsureMandatorySortersArePresent(); } mitk::DICOMITKSeriesGDCMReader ::DICOMITKSeriesGDCMReader(const DICOMITKSeriesGDCMReader& other ) :DICOMFileReader(other) ,m_FixTiltByShearing(false) { + this->EnsureMandatorySortersArePresent(); } mitk::DICOMITKSeriesGDCMReader ::~DICOMITKSeriesGDCMReader() { } mitk::DICOMITKSeriesGDCMReader& mitk::DICOMITKSeriesGDCMReader ::operator=(const DICOMITKSeriesGDCMReader& other) { if (this != &other) { DICOMFileReader::operator=(other); this->m_FixTiltByShearing = other.m_FixTiltByShearing; this->m_Sorter = other.m_Sorter; // TODO should clone the list items this->m_EquiDistantBlocksSorter = other.m_EquiDistantBlocksSorter->Clone(); } return *this; } void mitk::DICOMITKSeriesGDCMReader ::SetFixTiltByShearing(bool on) { m_FixTiltByShearing = on; } mitk::DICOMGDCMImageFrameList mitk::DICOMITKSeriesGDCMReader -::FromDICOMDatasetList(DICOMDatasetList& input) +::FromDICOMDatasetList(const DICOMDatasetList& input) { DICOMGDCMImageFrameList output; output.reserve(input.size()); - for(DICOMDatasetList::iterator inputIter = input.begin(); + for(DICOMDatasetList::const_iterator inputIter = input.begin(); inputIter != input.end(); ++inputIter) { DICOMGDCMImageFrameInfo* gfi = dynamic_cast(*inputIter); assert(gfi); output.push_back(gfi); } return output; } mitk::DICOMDatasetList mitk::DICOMITKSeriesGDCMReader -::ToDICOMDatasetList(DICOMGDCMImageFrameList& input) +::ToDICOMDatasetList(const DICOMGDCMImageFrameList& input) { DICOMDatasetList output; output.reserve(input.size()); - for(DICOMGDCMImageFrameList::iterator inputIter = input.begin(); + for(DICOMGDCMImageFrameList::const_iterator inputIter = input.begin(); inputIter != input.end(); ++inputIter) { DICOMDatasetAccess* da = inputIter->GetPointer(); assert(da); output.push_back(da); } return output; } mitk::DICOMImageFrameList mitk::DICOMITKSeriesGDCMReader -::ToDICOMImageFrameList(DICOMGDCMImageFrameList& input) +::ToDICOMImageFrameList(const DICOMGDCMImageFrameList& input) { DICOMImageFrameList output; output.reserve(input.size()); - for(DICOMGDCMImageFrameList::iterator inputIter = input.begin(); + for(DICOMGDCMImageFrameList::const_iterator inputIter = input.begin(); inputIter != input.end(); ++inputIter) { DICOMImageFrameInfo::Pointer fi = (*inputIter)->GetFrameInfo(); assert(fi.IsNotNull()); output.push_back(fi); } return output; } +void +mitk::DICOMITKSeriesGDCMReader +::InternalPrintConfiguration(std::ostream& os) const +{ + unsigned int sortIndex(1); + for(SorterList::const_iterator sorterIter = m_Sorter.begin(); + sorterIter != m_Sorter.end(); + ++sortIndex, ++sorterIter) + { + os << "Sorting step " << sortIndex << ":" << std::endl; + (*sorterIter)->PrintConfiguration(os, " "); + } + + os << "Sorting step " << sortIndex << ":" << std::endl; + m_EquiDistantBlocksSorter->PrintConfiguration(os, " "); +} + + std::string mitk::DICOMITKSeriesGDCMReader ::GetActiveLocale() const { return setlocale(LC_NUMERIC, NULL); } void mitk::DICOMITKSeriesGDCMReader ::PushLocale() { std::string currentCLocale = setlocale(LC_NUMERIC, NULL); m_ReplacedCLocales.push( currentCLocale ); setlocale(LC_NUMERIC, "C"); std::locale currentCinLocale( std::cin.getloc() ); m_ReplacedCinLocales.push( currentCinLocale ); std::locale l( "C" ); std::cin.imbue(l); } void mitk::DICOMITKSeriesGDCMReader ::PopLocale() { if (!m_ReplacedCLocales.empty()) { setlocale(LC_NUMERIC, m_ReplacedCLocales.top().c_str()); m_ReplacedCLocales.pop(); } else { MITK_WARN << "Mismatched PopLocale on DICOMITKSeriesGDCMReader."; } if (!m_ReplacedCinLocales.empty()) { std::cin.imbue( m_ReplacedCinLocales.top() ); m_ReplacedCinLocales.pop(); } else { MITK_WARN << "Mismatched PopLocale on DICOMITKSeriesGDCMReader."; } } mitk::DICOMITKSeriesGDCMReader::SortingBlockList mitk::DICOMITKSeriesGDCMReader ::Condense3DBlocks(SortingBlockList& input) { return input; // to be implemented differently by sub-classes } void mitk::DICOMITKSeriesGDCMReader ::AnalyzeInputFiles() { - // at this point, make sure we have a sorting element at the end that splits geometrically separate blocks - this->EnsureMandatorySortersArePresent(); - itk::TimeProbesCollectorBase timer; timer.Start("Reset"); this->ClearOutputs(); m_InputFrameList.clear(); m_GDCMScanner.ClearTags(); timer.Stop("Reset"); // prepare initial sorting (== list of input files) StringList inputFilenames = this->GetInputFiles(); // scan files for sorting-relevant tags timer.Start("Setup scanning"); for(SorterList::iterator sorterIter = m_Sorter.begin(); sorterIter != m_Sorter.end(); ++sorterIter) { assert(sorterIter->IsNotNull()); DICOMTagList tags = (*sorterIter)->GetTagsOfInterest(); for(DICOMTagList::const_iterator tagIter = tags.begin(); tagIter != tags.end(); ++tagIter) { MITK_DEBUG << "Sorting uses tag " << tagIter->GetGroup() << "," << tagIter->GetElement(); m_GDCMScanner.AddTag( gdcm::Tag(tagIter->GetGroup(), tagIter->GetElement()) ); } } // Add some of our own interest // TODO all tags that are needed in DICOMImageBlockDescriptor should be added by DICOMFileReader (this code location here should query all superclasses for tags) m_GDCMScanner.AddTag( gdcm::Tag(0x0018,0x1164) ); // pixel spacing m_GDCMScanner.AddTag( gdcm::Tag(0x0028,0x0030) ); // imager pixel spacing m_GDCMScanner.AddTag( gdcm::Tag(0x0020,0x1041) ); // slice location m_GDCMScanner.AddTag( gdcm::Tag(0x0020,0x0013) ); // instance number m_GDCMScanner.AddTag( gdcm::Tag(0x0008,0x0016) ); // sop class UID m_GDCMScanner.AddTag( gdcm::Tag(0x0008,0x0018) ); // sop instance UID m_GDCMScanner.AddTag( gdcm::Tag(0x0008,0x0060) ); // modality m_GDCMScanner.AddTag( gdcm::Tag(0x0020,0x0012) ); // acquisition number m_GDCMScanner.AddTag( gdcm::Tag(0x0018,0x0024) ); // sequence name m_GDCMScanner.AddTag( gdcm::Tag(0x0020,0x0037) ); // image orientation m_GDCMScanner.AddTag( gdcm::Tag(0x0020,0x0032) ); // ipp timer.Stop("Setup scanning"); timer.Start("Tag scanning"); PushLocale(); m_GDCMScanner.Scan( inputFilenames ); PopLocale(); timer.Stop("Tag scanning"); timer.Start("Setup sorting"); for (StringList::const_iterator inputIter = inputFilenames.begin(); inputIter != inputFilenames.end(); ++inputIter) { m_InputFrameList.push_back( DICOMGDCMImageFrameInfo::New( DICOMImageFrameInfo::New(*inputIter, 0), m_GDCMScanner.GetMapping(inputIter->c_str()) ) ); } m_SortingResultInProgress.clear(); m_SortingResultInProgress.push_back( m_InputFrameList ); timer.Stop("Setup sorting"); // sort and split blocks as configured timer.Start("Sorting frames"); - SortingBlockList nextStepSorting; // we should not modify our input list while processing it unsigned int sorterIndex = 0; for(SorterList::iterator sorterIter = m_Sorter.begin(); sorterIter != m_Sorter.end(); ++sorterIndex, ++sorterIter) { - std::stringstream ss; ss << "Sorting step " << sorterIndex; - timer.Start( ss.str().c_str() ); - nextStepSorting.clear(); - DICOMDatasetSorter::Pointer& sorter = *sorterIter; - - MITK_DEBUG << "================================================================================"; - MITK_DEBUG << "DICOMITKSeriesGDCMReader: " << ss.str() << ": " << m_SortingResultInProgress.size() << " groups input"; - unsigned int groupIndex = 0; - - for(SortingBlockList::iterator blockIter = m_SortingResultInProgress.begin(); - blockIter != m_SortingResultInProgress.end(); - ++groupIndex, ++blockIter) - { - DICOMGDCMImageFrameList& gdcmInfoFrameList = *blockIter; - DICOMDatasetList datasetList = ToDICOMDatasetList( gdcmInfoFrameList ); - - MITK_DEBUG << "--------------------------------------------------------------------------------"; - MITK_DEBUG << "DICOMITKSeriesGDCMReader: " << ss.str() << ", dataset group " << groupIndex << " (" << datasetList.size() << " datasets): "; - for (DICOMDatasetList::iterator oi = datasetList.begin(); - oi != datasetList.end(); - ++oi) - { - MITK_DEBUG << " INPUT : " << (*oi)->GetFilenameIfAvailable(); - } - - sorter->SetInput(datasetList); - sorter->Sort(); - unsigned int numberOfResultingBlocks = sorter->GetNumberOfOutputs(); - - for (unsigned int b = 0; b < numberOfResultingBlocks; ++b) - { - DICOMDatasetList blockResult = sorter->GetOutput(b); - - for (DICOMDatasetList::iterator oi = blockResult.begin(); - oi != blockResult.end(); - ++oi) - { - MITK_DEBUG << " OUTPUT(" << b << ") :" << (*oi)->GetFilenameIfAvailable(); - } + m_SortingResultInProgress = this->InternalExecuteSortingStep(sorterIndex, *sorterIter, m_SortingResultInProgress, &timer); + } - DICOMGDCMImageFrameList sortedGdcmInfoFrameList = FromDICOMDatasetList(blockResult); - nextStepSorting.push_back( sortedGdcmInfoFrameList ); - } - } + // a last extra-sorting step: ensure equidistant slices + m_SortingResultInProgress = this->InternalExecuteSortingStep(sorterIndex, m_EquiDistantBlocksSorter.GetPointer(), m_SortingResultInProgress, &timer); - m_SortingResultInProgress = nextStepSorting; - timer.Stop( ss.str().c_str() ); - } timer.Stop("Sorting frames"); timer.Start("Condensing 3D blocks (3D+t or vector values)"); m_SortingResultInProgress = this->Condense3DBlocks( m_SortingResultInProgress ); timer.Stop("Condensing 3D blocks (3D+t or vector values)"); // provide final result as output timer.Start("Output"); unsigned int o = this->GetNumberOfOutputs(); this->SetNumberOfOutputs( o + m_SortingResultInProgress.size() ); // Condense3DBlocks may already have added outputs! for (SortingBlockList::iterator blockIter = m_SortingResultInProgress.begin(); blockIter != m_SortingResultInProgress.end(); ++o, ++blockIter) { DICOMGDCMImageFrameList& gdcmFrameInfoList = *blockIter; DICOMImageFrameList frameList = ToDICOMImageFrameList( gdcmFrameInfoList ); assert(!gdcmFrameInfoList.empty()); assert(!frameList.empty()); DICOMImageBlockDescriptor block; block.SetTagCache( this ); // important: this must be before SetImageFrameList(), because SetImageFrameList will trigger reading of lots of interesting tags! block.SetImageFrameList( frameList ); const GantryTiltInformation& tiltInfo = m_EquiDistantBlocksSorter->GetTiltInformation( (gdcmFrameInfoList.front())->GetFilenameIfAvailable() ); block.SetFlag("gantryTilt", tiltInfo.IsRegularGantryTilt()); block.SetTiltInformation( tiltInfo ); static const DICOMTag tagPixelSpacing(0x0028,0x0030); static const DICOMTag tagImagerPixelSpacing(0x0018,0x1164); std::string pixelSpacingString = (gdcmFrameInfoList.front())->GetTagValueAsString( tagPixelSpacing ); std::string imagerPixelSpacingString = gdcmFrameInfoList.front()->GetTagValueAsString( tagImagerPixelSpacing ); - block.SetPixelSpacingInformation(pixelSpacingString, imagerPixelSpacingString); + block.SetPixelSpacingTagValues(pixelSpacingString, imagerPixelSpacingString); static const DICOMTag tagSOPClassUID(0x0008,0x0016); std::string sopClassUID = (gdcmFrameInfoList.front())->GetTagValueAsString( tagSOPClassUID ); block.SetSOPClassUID(sopClassUID); block.SetReaderImplementationLevel( this->GetReaderImplementationLevel(sopClassUID) ); this->SetOutput( o, block ); } timer.Stop("Output"); #ifdef MBILOG_ENABLE_DEBUG std::cout << "---------------------------------------------------------------" << std::endl; timer.Report( std::cout ); std::cout << "---------------------------------------------------------------" << std::endl; #endif } +mitk::DICOMITKSeriesGDCMReader::SortingBlockList +mitk::DICOMITKSeriesGDCMReader +::InternalExecuteSortingStep( + unsigned int sortingStepIndex, + DICOMDatasetSorter::Pointer sorter, + const SortingBlockList& input, + itk::TimeProbesCollectorBase* timer) +{ + SortingBlockList nextStepSorting; // we should not modify our input list while processing it + std::stringstream ss; ss << "Sorting step " << sortingStepIndex; + timer->Start( ss.str().c_str() ); + nextStepSorting.clear(); + + MITK_DEBUG << "================================================================================"; + MITK_DEBUG << "DICOMITKSeriesGDCMReader: " << ss.str() << ": " << input.size() << " groups input"; + unsigned int groupIndex = 0; + + for(SortingBlockList::const_iterator blockIter = input.begin(); + blockIter != input.end(); + ++groupIndex, ++blockIter) + { + const DICOMGDCMImageFrameList& gdcmInfoFrameList = *blockIter; + DICOMDatasetList datasetList = ToDICOMDatasetList( gdcmInfoFrameList ); + + MITK_DEBUG << "--------------------------------------------------------------------------------"; + MITK_DEBUG << "DICOMITKSeriesGDCMReader: " << ss.str() << ", dataset group " << groupIndex << " (" << datasetList.size() << " datasets): "; + for (DICOMDatasetList::iterator oi = datasetList.begin(); + oi != datasetList.end(); + ++oi) + { + MITK_DEBUG << " INPUT : " << (*oi)->GetFilenameIfAvailable(); + } + + sorter->SetInput(datasetList); + sorter->Sort(); + unsigned int numberOfResultingBlocks = sorter->GetNumberOfOutputs(); + + for (unsigned int b = 0; b < numberOfResultingBlocks; ++b) + { + DICOMDatasetList blockResult = sorter->GetOutput(b); + + for (DICOMDatasetList::iterator oi = blockResult.begin(); + oi != blockResult.end(); + ++oi) + { + MITK_DEBUG << " OUTPUT(" << b << ") :" << (*oi)->GetFilenameIfAvailable(); + } + + DICOMGDCMImageFrameList sortedGdcmInfoFrameList = FromDICOMDatasetList(blockResult); + nextStepSorting.push_back( sortedGdcmInfoFrameList ); + } + } + + timer->Stop( ss.str().c_str() ); + + return nextStepSorting; +} + mitk::ReaderImplementationLevel mitk::DICOMITKSeriesGDCMReader -::GetReaderImplementationLevel(const std::string uid) const +::GetReaderImplementationLevel(const std::string sopClassUID) const { - if (uid.empty()) + if (sopClassUID.empty()) { return SOPClassUnknown; } gdcm::UIDs uidKnowledge; - uidKnowledge.SetFromUID( uid.c_str() ); + uidKnowledge.SetFromUID( sopClassUID.c_str() ); gdcm::UIDs::TSType gdcmType = uidKnowledge; switch (gdcmType) { case gdcm::UIDs::CTImageStorage: case gdcm::UIDs::MRImageStorage: case gdcm::UIDs::PositronEmissionTomographyImageStorage: case gdcm::UIDs::ComputedRadiographyImageStorage: case gdcm::UIDs::DigitalXRayImageStorageForPresentation: case gdcm::UIDs::DigitalXRayImageStorageForProcessing: return SOPClassSupported; case gdcm::UIDs::NuclearMedicineImageStorage: return SOPClassPartlySupported; case gdcm::UIDs::SecondaryCaptureImageStorage: return SOPClassImplemented; default: return SOPClassUnsupported; } } // void AllocateOutputImages(); bool mitk::DICOMITKSeriesGDCMReader ::LoadImages() { bool success = true; unsigned int numberOfOutputs = this->GetNumberOfOutputs(); for (unsigned int o = 0; o < numberOfOutputs; ++o) { success &= this->LoadMitkImageForOutput(o); } return success; } bool mitk::DICOMITKSeriesGDCMReader ::LoadMitkImageForOutput(unsigned int o) { PushLocale(); DICOMImageBlockDescriptor& block = this->InternalGetOutput(o); const DICOMImageFrameList& frames = block.GetImageFrameList(); const GantryTiltInformation tiltInfo = block.GetTiltInformation(); bool hasTilt = block.GetFlag("gantryTilt", false); if (hasTilt) { MITK_DEBUG << "When loading image " << o << ": got tilt info:"; //tiltInfo.Print(std::cout); } else { MITK_DEBUG << "When loading image " << o << ": has NO info."; } ITKDICOMSeriesReaderHelper::StringContainer filenames; for (DICOMImageFrameList::const_iterator frameIter = frames.begin(); frameIter != frames.end(); ++frameIter) { filenames.push_back( (*frameIter)->Filename ); } mitk::ITKDICOMSeriesReaderHelper helper; mitk::Image::Pointer mitkImage = helper.Load( filenames, m_FixTiltByShearing && hasTilt, tiltInfo ); // TODO preloaded images, caching..? block.SetMitkImage( mitkImage ); PopLocale(); return true; } bool mitk::DICOMITKSeriesGDCMReader ::CanHandleFile(const std::string& itkNotUsed(filename)) { return true; // can handle all - // peek into file, check DCM - // nice-to-have: check multi-framedness + // TODO peek into file, check DCM + // TODO nice-to-have: check multi-framedness } void mitk::DICOMITKSeriesGDCMReader ::AddSortingElement(DICOMDatasetSorter* sorter, bool atFront) { assert(sorter); if (atFront) { m_Sorter.push_front( sorter ); } else { m_Sorter.push_back( sorter ); } } void mitk::DICOMITKSeriesGDCMReader ::EnsureMandatorySortersArePresent() { - // TODO do just once! + // TODO do just once! Do it in c'tor and handle this step extra! DICOMTagBasedSorter::Pointer splitter = DICOMTagBasedSorter::New(); splitter->AddDistinguishingTag( DICOMTag(0x0028, 0x0010) ); // Number of Rows splitter->AddDistinguishingTag( DICOMTag(0x0028, 0x0011) ); // Number of Columns splitter->AddDistinguishingTag( DICOMTag(0x0028, 0x0030) ); // Pixel Spacing splitter->AddDistinguishingTag( DICOMTag(0x0018, 0x1164) ); // Imager Pixel Spacing splitter->AddDistinguishingTag( DICOMTag(0x0020, 0x0037), new mitk::DICOMTagBasedSorter::CutDecimalPlaces(5) ); // Image Orientation (Patient) // TODO: configurable! splitter->AddDistinguishingTag( DICOMTag(0x0018, 0x0050) ); // Slice Thickness splitter->AddDistinguishingTag( DICOMTag(0x0028, 0x0008) ); // Number of Frames this->AddSortingElement( splitter, true ); // true = at front if (m_EquiDistantBlocksSorter.IsNull()) { m_EquiDistantBlocksSorter = mitk::EquiDistantBlocksSorter::New(); } m_EquiDistantBlocksSorter->SetAcceptTilt( m_FixTiltByShearing ); - - this->AddSortingElement( m_EquiDistantBlocksSorter ); } std::string mitk::DICOMITKSeriesGDCMReader ::GetTagValue(DICOMImageFrameInfo* frame, const DICOMTag& tag) const { + // TODO inefficient. if (m_InputFrameList.contains(frame)) return frame->GetTagValueAsString(tag); for(DICOMGDCMImageFrameList::const_iterator frameIter = m_InputFrameList.begin(); frameIter != m_InputFrameList.end(); ++frameIter) { if ( (*frameIter)->GetFrameInfo().IsNotNull() && (*((*frameIter)->GetFrameInfo()) == *frame ) ) { return (*frameIter)->GetTagValueAsString(tag); } } return ""; } diff --git a/Modules/DICOMReader/mitkDICOMITKSeriesGDCMReader.h b/Modules/DICOMReader/mitkDICOMITKSeriesGDCMReader.h index bd8f7882b9..c24187421b 100644 --- a/Modules/DICOMReader/mitkDICOMITKSeriesGDCMReader.h +++ b/Modules/DICOMReader/mitkDICOMITKSeriesGDCMReader.h @@ -1,116 +1,320 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #ifndef mitkDICOMITKSeriesGDCMReader_h #define mitkDICOMITKSeriesGDCMReader_h #include "mitkDICOMFileReader.h" #include "mitkDICOMDatasetSorter.h" #include "mitkDICOMGDCMImageFrameInfo.h" #include "mitkEquiDistantBlocksSorter.h" #include "DICOMReaderExports.h" #include +// TODO should reject multi-frame // TODO tests seem to pass if reader creates NO output at all! // TODO preloaded volumes?? could be solved in a different way.. +namespace itk +{ + class TimeProbesCollectorBase; +} + namespace mitk { +/** + \ingroup DICOMReaderModule + \brief Flexible reader based on itk::ImageSeriesReader and GDCM, for single-slice modalities like CT, MR, PET, CR, etc. + + Implements the loading processed as structured by DICOMFileReader offers configuration + of its loading strategy. + + Documentation sections: + - \ref DICOMITKSeriesGDCMReader_LoadingStrategy + - \ref DICOMITKSeriesGDCMReader_ForcedConfiguration + - \ref DICOMITKSeriesGDCMReader_UserConfiguration + - \ref DICOMITKSeriesGDCMReader_GantryTilt + - \ref DICOMITKSeriesGDCMReader_Testing + - \ref DICOMITKSeriesGDCMReader_Internals + - \ref DICOMITKSeriesGDCMReader_RelatedClasses + - \ref DICOMITKSeriesGDCMReader_TiltInternals + - \ref DICOMITKSeriesGDCMReader_Condensing + + + \section DICOMITKSeriesGDCMReader_LoadingStrategy Loading strategy + + The set of input files is processed by a number of DICOMDatasetSorter objects which may do two sort of things: + 1. split a list of input frames into multiple lists, based on DICOM tags such as "Rows", "Columns", which cannot be mixed within a single mitk::Image + 2. sort the frames within the input lists, based on the values of DICOM tags such as "Image Position Patient" + + When the DICOMITKSeriesGDCMReader is configured with DICOMDatasetSorter%s, the list of input files is processed + as follows: + 1. build an initial set of output groups, simply by grouping all input files. + 2. for each configured DICOMDatasetSorter, process: + - for each output group: + 1. set this group's files as input to the sorter + 2. let the sorter sort (and split) + 3. integrate the sorter's output groups with our own output groups + + The output blocks described by DICOMImageBlockDescriptor will contains the following properties (compare DICOMImageBlockDescriptor): + - \b "gantryTilt": true if the image is has a gantry tilt that will be corrected during loading + + \section DICOMITKSeriesGDCMReader_ForcedConfiguration Forced Configuration + + In all cases, the reader will add two DICOMDatasetSorter objects that are required to load + mitk::Images properly via itk::ImageSeriesReader: + 1. As a \b first step, the input files will be split into groups that are not compatible because they differ in essential aspects: + - (0028,0010) Number of Rows + - (0028,0011) Number of Columns + - (0028,0030) Pixel Spacing + - (0018,1164) Imager Pixel Spacing + - (0020,0037) %Image Orientation (Patient) + - (0018,0050) Slice Thickness + - (0028,0008) Number of Frames + 2. As a \b last step, there will always be an instance of EquiDistantBlocksSorter, + which ensures that there is an equal distance between all the frames of an Image. + This is required to achieve correct geometrical positions in the mitk::Image, + i.e. it is essential to be able to make measurements in images. + - whether or not the distance is required to be orthogonal to the image planes is configured by SetFixTiltByShearing(). + + \section DICOMITKSeriesGDCMReader_UserConfiguration User Configuration + + The user of this class can add more sorting steps (similar to the one described in above section) by calling AddSortingElement(). + + Usually, an application will add sorting by "Image Position Patient", by "Instance Number", and by other relevant tags here. + + \section DICOMITKSeriesGDCMReader_GantryTilt Gantry tilt handling + + When CT gantry tilt is used, the gantry plane (= X-Ray source and detector ring) and the vertical plane do not align + anymore. This scanner feature is used for example to reduce metal artifacs (e.g. Lee C , Evaluation of Using CT + Gantry Tilt Scan on Head and Neck Cancer Patients with Dental Structure: Scans Show Less Metal Artifacts. Presented + at: Radiological Society of North America 2011 Scientific Assembly and Annual Meeting; November 27- December 2, + 2011 Chicago IL.). + + The acquired planes of such CT series do not match the expectations of a orthogonal geometry in mitk::Image: if you + stack the slices, they show a small shift along the Y axis: +\verbatim + + without tilt with tilt + + |||||| ////// + |||||| ////// + -- |||||| --------- ////// -------- table orientation + |||||| ////// + |||||| ////// + + Stacked slices: + + without tilt with tilt + + -------------- -------------- + -------------- -------------- + -------------- -------------- + -------------- -------------- + -------------- -------------- + +\endverbatim + + + As such gemetries do not "work" in conjunction with mitk::Image, DICOMITKSeriesGDCMReader is able to perform a correction for such series. + Whether or not such correction should be attempted is controlled by SetFixTiltByShearing(), the default being correction. + For details, see "Internals" below. + \section DICOMITKSeriesGDCMReader_Testing Testing + + A number of tests is implemented in module DICOMTesting, which is documented at \ref DICOMTesting. + + \section DICOMITKSeriesGDCMReader_Internals Class internals + + Internally, the class is based on GDCM and it depends heavily on the gdcm::Scanner class. + + Since the sorting elements (see DICOMDatasetSorter and DICOMSortCriterion) can access tags only via the DICOMDatasetAccess interface, + BUT DICOMITKSeriesGDCMReader holds a list of more specific classes DICOMGDCMImageFrameInfo, we must convert between the two + types sometimes. This explains the methods ToDICOMDatasetList(), FromDICOMDatasetList(). + + The intermediate result of all the sorting efforts is held in m_SortingResultInProgress, + which is modified through InternalExecuteSortingStep(). + + \subsection DICOMITKSeriesGDCMReader_RelatedClasses Overview of related classes + + The following diagram gives an overview of the related classes: + + \image html implementeditkseriesgdcmreader.jpg + + \subsection DICOMITKSeriesGDCMReader_TiltInternals Details about the tilt correction + + The gantry tilt "correction" algorithm fixes two errors introduced by ITK's ImageSeriesReader: + - the plane shift that is ignored by ITK's reader is recreated by applying a shearing transformation using itk::ResampleFilter. + - the spacing is corrected (it is calculated by ITK's reader from the distance between two origins, which is NOT the slice distance in this special case) + + Both errors are introduced in + itkImageSeriesReader.txx (ImageSeriesReader::GenerateOutputInformation(void)), lines 176 to 245 (as of ITK 3.20) + + For the correction, we examine two consecutive slices of a series, both described as a pair (origin/orientation): + - we calculate if the first origin is on a line along the normal of the second slice + - if this is not the case, the geometry will not fit a normal mitk::Image/mitk::Geometry3D + - we then project the second origin into the first slice's coordinate system to quantify the shift + - both is done in class GantryTiltInformation with quite some comments. + + The geometry of image stacks with tilted geometries is illustrated below: + - green: the DICOM images as described by their tags: origin as a point with the line indicating the orientation + - red: the output of ITK ImageSeriesReader: wrong, larger spacing, no tilt + - blue: how much a shear must correct + + \image html tilt-correction.jpg + + \subsection DICOMITKSeriesGDCMReader_Condensing Sub-classes can condense multiple blocks into a single larger block + + The sorting/splitting process described above is helpful for at least two more DICOM readers, which either try to load 3D+t images or which load diffusion data. + + In both cases, a single pixel of the mitk::Image is made up of multiple values, in one case values over time, in the other case multiple measurements of a single point. + + The specialized readers for these cases (e.g. ThreeDnTDICOMSeriesReader) can reuse most of the methods in DICOMITKSeriesGDCMReader, + except that they need an extra step after the usual sorting, in which they can merge already grouped 3D blocks. What blocks are merged + depends on the specialized reader's understanding of these images. To allow for such merging, a method Condense3DBlocks() is called + as an absolute last step of AnalyzeInputFiles(). Given this, a sub-class could implement only LoadImages() and Condense3DBlocks() instead + repeating most of AnalyzeInputFiles(). + +*/ class DICOMReader_EXPORT DICOMITKSeriesGDCMReader : public DICOMFileReader, protected DICOMTagCache { public: mitkClassMacro( DICOMITKSeriesGDCMReader, DICOMFileReader ); mitkCloneMacro( DICOMITKSeriesGDCMReader ); itkNewMacro( DICOMITKSeriesGDCMReader ); + /** + \brief Runs the sorting / splitting process described in \ref DICOMITKSeriesGDCMReader_LoadingStrategy. + Method required by DICOMFileReader. + */ virtual void AnalyzeInputFiles(); // void AllocateOutputImages(); + /** + \brief Loads images using itk::ImageSeriesReader, potentially applies shearing to correct gantry tilt. + */ virtual bool LoadImages(); + // re-implemented from super-class virtual bool CanHandleFile(const std::string& filename); + /** + \brief Add an element to the sorting procedure described in \ref DICOMITKSeriesGDCMReader_LoadingStrategy. + */ virtual void AddSortingElement(DICOMDatasetSorter* sorter, bool atFront = false); + /** + \brief Controls whether to "fix" tilted acquisitions by shearing the output (see \ref DICOMITKSeriesGDCMReader_GantryTilt). + */ void SetFixTiltByShearing(bool on); protected: + virtual void InternalPrintConfiguration(std::ostream& os) const; + + // From DICOMTagCache virtual std::string GetTagValue(DICOMImageFrameInfo* frame, const DICOMTag& tag) const; + /// \brief Return active C locale std::string GetActiveLocale() const; + /** + \brief Remember current locale on stack, activate "C" locale. + "C" locale is required for correct parsing of numbers by itk::ImageSeriesReader + */ void PushLocale(); + /** + \brief Activate last remembered locale from locale stack + "C" locale is required for correct parsing of numbers by itk::ImageSeriesReader + */ void PopLocale(); DICOMITKSeriesGDCMReader(); virtual ~DICOMITKSeriesGDCMReader(); DICOMITKSeriesGDCMReader(const DICOMITKSeriesGDCMReader& other); DICOMITKSeriesGDCMReader& operator=(const DICOMITKSeriesGDCMReader& other); - DICOMDatasetList ToDICOMDatasetList(DICOMGDCMImageFrameList& input); - DICOMGDCMImageFrameList FromDICOMDatasetList(DICOMDatasetList& input); - DICOMImageFrameList ToDICOMImageFrameList(DICOMGDCMImageFrameList& input); + /// \brief See \ref DICOMITKSeriesGDCMReader_Internals + DICOMDatasetList ToDICOMDatasetList(const DICOMGDCMImageFrameList& input); + /// \brief See \ref DICOMITKSeriesGDCMReader_Internals + DICOMGDCMImageFrameList FromDICOMDatasetList(const DICOMDatasetList& input); + /// \brief See \ref DICOMITKSeriesGDCMReader_Internals + DICOMImageFrameList ToDICOMImageFrameList(const DICOMGDCMImageFrameList& input); typedef std::list SortingBlockList; - /// \return REMAINING blocks + /** + \brief "Hook" for sub-classes, see \ref DICOMITKSeriesGDCMReader_Condensing + \return REMAINING blocks + */ virtual SortingBlockList Condense3DBlocks(SortingBlockList& resultOf3DGrouping); + /// \brief Sorting step as described in \ref DICOMITKSeriesGDCMReader_LoadingStrategy + SortingBlockList InternalExecuteSortingStep( + unsigned int sortingStepIndex, + DICOMDatasetSorter::Pointer sorter, + const SortingBlockList& input, + itk::TimeProbesCollectorBase* timer); + + /// \brief Creates the required sorting steps described in \ref DICOMITKSeriesGDCMReader_ForcedConfiguration void EnsureMandatorySortersArePresent(); + /// \brief Loads the mitk::Image by means of an itk::ImageSeriesReader virtual bool LoadMitkImageForOutput(unsigned int o); + /** + \brief Shear the loaded mitk::Image to "correct" a spatial error introduced by itk::ImageSeriesReader + See \ref DICOMITKSeriesGDCMReader_GantryTilt for details. + */ Image::Pointer FixupSpacing(Image* mitkImage, const DICOMImageBlockDescriptor& block) const; - ReaderImplementationLevel GetReaderImplementationLevel(const std::string uid) const; + /// \brief Describe this reader's confidence for given SOP class UID + ReaderImplementationLevel GetReaderImplementationLevel(const std::string sopClassUID) const; private: protected: // NOT nice, made available to ThreeDnTDICOMSeriesReader due to lack of time bool m_FixTiltByShearing; // could be removed by ITKDICOMSeriesReader NOT flagging tilt unless requested to fix it! private: - bool m_3DBlocksWereCondensed; SortingBlockList m_SortingResultInProgress; typedef std::list SorterList; SorterList m_Sorter; protected: // NOT nice, made available to ThreeDnTDICOMSeriesReader due to lack of time mitk::EquiDistantBlocksSorter::Pointer m_EquiDistantBlocksSorter; private: std::stack m_ReplacedCLocales; std::stack m_ReplacedCinLocales; DICOMGDCMImageFrameList m_InputFrameList; gdcm::Scanner m_GDCMScanner; }; } #endif diff --git a/Modules/DICOMReader/mitkDICOMImageBlockDescriptor.cpp b/Modules/DICOMReader/mitkDICOMImageBlockDescriptor.cpp index e167bce651..eba484950e 100644 --- a/Modules/DICOMReader/mitkDICOMImageBlockDescriptor.cpp +++ b/Modules/DICOMReader/mitkDICOMImageBlockDescriptor.cpp @@ -1,581 +1,581 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #include "mitkDICOMImageBlockDescriptor.h" #include "mitkStringProperty.h" #include mitk::DICOMImageBlockDescriptor ::DICOMImageBlockDescriptor() :m_ReaderImplementationLevel(SOPClassUnknown) ,m_PixelSpacing("") ,m_ImagerPixelSpacing("") ,m_SOPClassUID("") ,m_PropertyList(PropertyList::New()) ,m_TagCache(NULL) { } mitk::DICOMImageBlockDescriptor ::~DICOMImageBlockDescriptor() { } mitk::DICOMImageBlockDescriptor ::DICOMImageBlockDescriptor(const DICOMImageBlockDescriptor& other) :m_ImageFrameList( other.m_ImageFrameList ) ,m_MitkImage( other.m_MitkImage ) ,m_SliceIsLoaded( other.m_SliceIsLoaded ) ,m_ReaderImplementationLevel( other.m_ReaderImplementationLevel ) ,m_PixelSpacing( other.m_PixelSpacing ) ,m_ImagerPixelSpacing( other.m_ImagerPixelSpacing ) ,m_SOPClassUID( other.m_SOPClassUID ) ,m_TiltInformation( other.m_TiltInformation ) ,m_PropertyList( other.m_PropertyList->Clone() ) ,m_TagCache( other.m_TagCache ) { if (m_MitkImage) { m_MitkImage = m_MitkImage->Clone(); } } mitk::DICOMImageBlockDescriptor& mitk::DICOMImageBlockDescriptor ::operator=(const DICOMImageBlockDescriptor& other) { if (this != &other) { m_ImageFrameList = other.m_ImageFrameList; m_MitkImage = other.m_MitkImage; m_SliceIsLoaded = other.m_SliceIsLoaded; m_ReaderImplementationLevel = other.m_ReaderImplementationLevel; m_PixelSpacing = other.m_PixelSpacing; m_ImagerPixelSpacing = other.m_ImagerPixelSpacing; m_SOPClassUID = other.m_SOPClassUID; m_TiltInformation = other.m_TiltInformation; if (other.m_PropertyList) { m_PropertyList = other.m_PropertyList->Clone(); } if (other.m_MitkImage) { m_MitkImage = other.m_MitkImage->Clone(); } m_TagCache = other.m_TagCache; } return *this; } void mitk::DICOMImageBlockDescriptor ::SetTiltInformation(const GantryTiltInformation& info) { m_TiltInformation = info; } const mitk::GantryTiltInformation mitk::DICOMImageBlockDescriptor ::GetTiltInformation() const { return m_TiltInformation; } void mitk::DICOMImageBlockDescriptor ::SetImageFrameList(const DICOMImageFrameList& framelist) { m_ImageFrameList = framelist; m_SliceIsLoaded.resize(framelist.size()); m_SliceIsLoaded.assign(framelist.size(), false); this->UpdateImageDescribingProperties(); } const mitk::DICOMImageFrameList& mitk::DICOMImageBlockDescriptor ::GetImageFrameList() const { return m_ImageFrameList; } void mitk::DICOMImageBlockDescriptor ::SetMitkImage(Image::Pointer image) { if (m_MitkImage != image) { m_MitkImage = this->DescribeImageWithProperties( this->FixupSpacing(image) ); } } mitk::Image::Pointer mitk::DICOMImageBlockDescriptor ::GetMitkImage() const { return m_MitkImage; } mitk::Image::Pointer mitk::DICOMImageBlockDescriptor ::FixupSpacing(Image* mitkImage) { if (mitkImage) { Vector3D imageSpacing = mitkImage->GetGeometry()->GetSpacing(); ScalarType desiredSpacingX = imageSpacing[0]; ScalarType desiredSpacingY = imageSpacing[1]; this->GetDesiredMITKImagePixelSpacing( desiredSpacingX, desiredSpacingY ); // prefer pixel spacing over imager pixel spacing MITK_DEBUG << "Loaded image with spacing " << imageSpacing[0] << ", " << imageSpacing[1]; MITK_DEBUG << "Found correct spacing info " << desiredSpacingX << ", " << desiredSpacingY; imageSpacing[0] = desiredSpacingX; imageSpacing[1] = desiredSpacingY; mitkImage->GetGeometry()->SetSpacing( imageSpacing ); } return mitkImage; } void mitk::DICOMImageBlockDescriptor ::SetSliceIsLoaded(unsigned int index, bool isLoaded) { if (index < m_SliceIsLoaded.size()) { m_SliceIsLoaded[index] = isLoaded; } else { std::stringstream ss; ss << "Index " << index << " out of range (" << m_SliceIsLoaded.size() << " indices reserved)"; throw std::invalid_argument( ss.str() ); } } bool mitk::DICOMImageBlockDescriptor ::IsSliceLoaded(unsigned int index) const { if (index < m_SliceIsLoaded.size()) { return m_SliceIsLoaded[index]; } else { std::stringstream ss; ss << "Index " << index << " out of range (" << m_SliceIsLoaded.size() << " indices reserved)"; throw std::invalid_argument( ss.str() ); } } bool mitk::DICOMImageBlockDescriptor ::AllSlicesAreLoaded() const { bool allLoaded = true; for (BoolList::const_iterator iter = m_SliceIsLoaded.begin(); iter != m_SliceIsLoaded.end(); ++iter) { allLoaded &= *iter; } return allLoaded; } void mitk::DICOMImageBlockDescriptor -::SetPixelSpacingInformation(const std::string& pixelSpacing, const std::string& imagerPixelSpacing) +::SetPixelSpacingTagValues(const std::string& pixelSpacing, const std::string& imagerPixelSpacing) { m_PixelSpacing = pixelSpacing; m_ImagerPixelSpacing = imagerPixelSpacing; } /* PS defined IPS defined PS==IPS 0 0 --> UNKNOWN spacing, loader will invent 0 1 --> spacing as at detector surface 1 0 --> spacing as in patient 1 1 0 --> detector surface spacing CORRECTED for geometrical magnifications: spacing as in patient 1 1 1 --> detector surface spacing NOT corrected for geometrical magnifications: spacing as at detector */ mitk::PixelSpacingInterpretation mitk::DICOMImageBlockDescriptor ::GetPixelSpacingInterpretation() const { if (m_PixelSpacing.empty()) { if (m_ImagerPixelSpacing.empty()) { return SpacingUnknown; } else { return SpacingAtDetector; } } else // Pixel Spacing defined { if (m_ImagerPixelSpacing.empty()) { return SpacingInPatient; } else if (m_PixelSpacing != m_ImagerPixelSpacing) { return SpacingInPatient; } else { return SpacingAtDetector; } } } void mitk::DICOMImageBlockDescriptor ::GetDesiredMITKImagePixelSpacing( ScalarType& spacingX, ScalarType& spacingY) const { // preference for "in patient" pixel spacing if ( !DICOMStringToSpacing( m_PixelSpacing, spacingX, spacingY ) ) { // fallback to "on detector" spacing if ( !DICOMStringToSpacing( m_ImagerPixelSpacing, spacingX, spacingY ) ) { // last resort: invent something spacingX = spacingY = 1.0; } } } void mitk::DICOMImageBlockDescriptor ::SetProperty(const std::string& key, BaseProperty* value) { m_PropertyList->SetProperty(key, value); } mitk::BaseProperty* mitk::DICOMImageBlockDescriptor ::GetProperty(const std::string& key) const { return m_PropertyList->GetProperty(key); } std::string mitk::DICOMImageBlockDescriptor ::GetPropertyAsString(const std::string& key) const { mitk::BaseProperty::Pointer property = m_PropertyList->GetProperty(key); if (property.IsNotNull()) { return property->GetValueAsString(); } else { return std::string(""); } } void mitk::DICOMImageBlockDescriptor ::SetFlag(const std::string& key, bool value) { m_PropertyList->ReplaceProperty(key, BoolProperty::New(value)); } bool mitk::DICOMImageBlockDescriptor ::GetFlag(const std::string& key, bool defaultValue) const { BoolProperty::ConstPointer boolProp = dynamic_cast( this->GetProperty(key) ); if (boolProp.IsNotNull()) { return boolProp->GetValue(); } else { return defaultValue; } } void mitk::DICOMImageBlockDescriptor ::SetIntProperty(const std::string& key, int value) { m_PropertyList->ReplaceProperty(key, IntProperty::New(value)); } int mitk::DICOMImageBlockDescriptor ::GetIntProperty(const std::string& key, int defaultValue) const { IntProperty::ConstPointer intProp = dynamic_cast( this->GetProperty(key) ); if (intProp.IsNotNull()) { return intProp->GetValue(); } else { return defaultValue; } } mitk::Image::Pointer mitk::DICOMImageBlockDescriptor ::DescribeImageWithProperties(Image* mitkImage) { if (!mitkImage) return mitkImage; // first part: add some tags that describe individual slices // these propeties are defined at analysis time (see UpdateImageDescribingProperties()) std::string propertyKeySliceLocation = "dicom.image.0020.1041"; std::string propertyKeyInstanceNumber = "dicom.image.0020.0013"; std::string propertyKeySOPInstanceUID = "dicom.image.0008.0018"; mitkImage->SetProperty( propertyKeySliceLocation.c_str(), this->GetProperty("sliceLocationForSlices") ); mitkImage->SetProperty( propertyKeyInstanceNumber.c_str(), this->GetProperty("instanceNumberForSlices") ); mitkImage->SetProperty( propertyKeySOPInstanceUID.c_str(), this->GetProperty("SOPInstanceUIDForSlices") ); // second part: add properties that describe the whole image block mitkImage->SetProperty("dicomseriesreader.SOPClassUID", StringProperty::New( m_SOPClassUID ) ); mitkImage->SetProperty("dicomseriesreader.SOPClass", - StringProperty::New( this->GetSOPClassUIDAsString() )); + StringProperty::New( this->GetSOPClassUIDAsName() )); mitkImage->SetProperty("dicomseriesreader.PixelSpacingInterpretationString", StringProperty::New( PixelSpacingInterpretationToString( this->GetPixelSpacingInterpretation() )) ); mitkImage->SetProperty("dicomseriesreader.PixelSpacingInterpretation", GenericProperty::New( this->GetPixelSpacingInterpretation() )); mitkImage->SetProperty("dicomseriesreader.ReaderImplementationLevelString", StringProperty::New( ReaderImplementationLevelToString( m_ReaderImplementationLevel ) )); mitkImage->SetProperty("dicomseriesreader.ReaderImplementationLevel", GenericProperty::New( m_ReaderImplementationLevel )); mitkImage->SetProperty("dicomseriesreader.GantyTiltCorrected", BoolProperty::New( this->GetFlag("gantryTilt",false) )); mitkImage->SetProperty("dicomseriesreader.3D+t", BoolProperty::New( this->GetFlag("3D+t",false) )); // third part: get something from ImageIO. BUT this needs to be created elsewhere. or not at all! return mitkImage; } void mitk::DICOMImageBlockDescriptor ::SetReaderImplementationLevel(const ReaderImplementationLevel& level) { m_ReaderImplementationLevel = level; } mitk::ReaderImplementationLevel mitk::DICOMImageBlockDescriptor ::GetReaderImplementationLevel() const { return m_ReaderImplementationLevel; } void mitk::DICOMImageBlockDescriptor ::SetSOPClassUID(const std::string& uid) { m_SOPClassUID = uid; } std::string mitk::DICOMImageBlockDescriptor ::GetSOPClassUID() const { return m_SOPClassUID; } std::string mitk::DICOMImageBlockDescriptor -::GetSOPClassUIDAsString() const +::GetSOPClassUIDAsName() const { gdcm::UIDs uidKnowledge; uidKnowledge.SetFromUID( m_SOPClassUID.c_str() ); const char* name = uidKnowledge.GetName(); if (name) { return std::string(name); } else { return std::string(""); } } void mitk::DICOMImageBlockDescriptor ::SetTagCache(DICOMTagCache* privateCache) { // TODO this must only be used during loading and never afterwards // TODO better: somehow make possible to have smartpointers here m_TagCache = privateCache; } #define printPropertyRange(label, property_name) \ { \ std::string first = this->GetPropertyAsString( #property_name "First"); \ std::string last = this->GetPropertyAsString( #property_name "Last"); \ if (first == last) \ { \ os << " " label ": '" << first << "'" << std::endl; \ } \ else \ { \ os << " " label ": '" << first << "' - '" << last << "'" << std::endl; \ } \ } #define printProperty(label, property_name) \ { \ std::string first = this->GetPropertyAsString( #property_name ); \ os << " " label ": '" << first << "'" << std::endl; \ } #define printBool(label, commands) \ { \ os << " " label ": '" << (commands?"yes":"no") << "'" << std::endl; \ } void mitk::DICOMImageBlockDescriptor ::Print(std::ostream& os, bool filenameDetails) const { os << " Number of Frames: '" << m_ImageFrameList.size() << "'" << std::endl; - os << " SOP class: '" << this->GetSOPClassUIDAsString() << "'" << std::endl; + os << " SOP class: '" << this->GetSOPClassUIDAsName() << "'" << std::endl; printProperty("Modality", modality); printProperty("Sequence Name", sequenceName); printPropertyRange("Slice Location", sliceLocation); printPropertyRange("Acquisition Number", acquisitionNumber); printPropertyRange("Instance Number", instanceNumber); printPropertyRange("Image Position", imagePositionPatient); printProperty("Image Orientation", orientation); os << " Pixel spacing interpretation: '" << PixelSpacingInterpretationToString(this->GetPixelSpacingInterpretation()) << "'" << std::endl; printBool("Gantry Tilt", this->GetFlag("gantryTilt",false)) printBool("3D+t", this->GetFlag("3D+t",false)) os << " MITK image loaded: '" << (this->GetMitkImage().IsNotNull() ? "yes" : "no") << "'" << std::endl; if (filenameDetails) { for (DICOMImageFrameList::const_iterator frameIter = m_ImageFrameList.begin(); frameIter != m_ImageFrameList.end(); ++frameIter) { os << " File " << (*frameIter)->Filename; if ((*frameIter)->FrameNo > 0) { os << ", " << (*frameIter)->FrameNo; } os << std::endl; } } } #define storeTagValueToProperty(tag_name, tag_g, tag_e) \ { \ const DICOMTag t(tag_g, tag_e); \ std::string tagValue = m_TagCache->GetTagValue( firstFrame, t ); \ this->SetProperty(#tag_name, StringProperty::New( tagValue ) ); \ } #define storeTagValueRangeToProperty(tag_name, tag_g, tag_e) \ { \ const DICOMTag t(tag_g, tag_e); \ std::string tagValueFirst = m_TagCache->GetTagValue( firstFrame, t ); \ std::string tagValueLast = m_TagCache->GetTagValue( lastFrame, t ); \ this->SetProperty(#tag_name "First", StringProperty::New( tagValueFirst ) ); \ this->SetProperty(#tag_name "Last", StringProperty::New( tagValueLast ) ); \ } void mitk::DICOMImageBlockDescriptor ::UpdateImageDescribingProperties() { if (m_TagCache && !m_ImageFrameList.empty()) { DICOMImageFrameInfo::Pointer firstFrame = m_ImageFrameList.front();; DICOMImageFrameInfo::Pointer lastFrame = m_ImageFrameList.back();; // see macros above storeTagValueToProperty(modality,0x0008,0x0060) storeTagValueToProperty(sequenceName,0x0018,0x0024) storeTagValueToProperty(orientation,0x0020,0x0037) storeTagValueRangeToProperty(sliceLocation,0x0020,0x1041) storeTagValueRangeToProperty(acquisitionNumber,0x0020,0x0012) storeTagValueRangeToProperty(instanceNumber,0x0020,0x0013) storeTagValueRangeToProperty(imagePositionPatient,0x0020,0x0032) // some per-image attributes // frames are just numbered starting from 0. timestep 1 (the second time-step) has frames starting at (number-of-frames-per-timestep) std::string propertyKeySliceLocation = "dicom.image.0020.1041"; std::string propertyKeyInstanceNumber = "dicom.image.0020.0013"; std::string propertyKeySOPInstanceNumber = "dicom.image.0008.0018"; StringLookupTable sliceLocationForSlices; StringLookupTable instanceNumberForSlices; StringLookupTable SOPInstanceUIDForSlices; const DICOMTag tagSliceLocation(0x0020,0x1041); const DICOMTag tagInstanceNumber(0x0020,0x0013); const DICOMTag tagSOPInstanceNumber(0x0008,0x0018); unsigned int slice(0); for (DICOMImageFrameList::const_iterator frameIter = m_ImageFrameList.begin(); frameIter != m_ImageFrameList.end(); ++slice, ++frameIter) { std::string sliceLocation = m_TagCache->GetTagValue( *frameIter, tagSliceLocation ); sliceLocationForSlices.SetTableValue(slice, sliceLocation); std::string instanceNumber = m_TagCache->GetTagValue( *frameIter, tagInstanceNumber ); instanceNumberForSlices.SetTableValue(slice, instanceNumber); std::string sopInstanceUID = m_TagCache->GetTagValue( *frameIter, tagSOPInstanceNumber ); SOPInstanceUIDForSlices.SetTableValue(slice, sopInstanceUID); MITK_DEBUG << "Tag info for slice " << slice << ": SL '" << sliceLocation << "' IN '" << instanceNumber << "' SOP instance UID '" << sopInstanceUID << "'"; // add property or properties with proper names this->SetProperty( "sliceLocationForSlices", StringLookupTableProperty::New( sliceLocationForSlices ) ); this->SetProperty( "instanceNumberForSlices", StringLookupTableProperty::New( instanceNumberForSlices ) ); this->SetProperty( "SOPInstanceUIDForSlices", StringLookupTableProperty::New( SOPInstanceUIDForSlices ) ); } } } diff --git a/Modules/DICOMReader/mitkDICOMImageBlockDescriptor.h b/Modules/DICOMReader/mitkDICOMImageBlockDescriptor.h index 0698dfd2d1..f464da3e4d 100644 --- a/Modules/DICOMReader/mitkDICOMImageBlockDescriptor.h +++ b/Modules/DICOMReader/mitkDICOMImageBlockDescriptor.h @@ -1,109 +1,163 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #ifndef mitkDICOMImageBlockDescriptor_h #define mitkDICOMImageBlockDescriptor_h #include "mitkDICOMEnums.h" #include "mitkDICOMImageFrameInfo.h" #include "mitkDICOMTag.h" #include "mitkDICOMTagCache.h" #include "mitkImage.h" #include "mitkProperties.h" #include "mitkGantryTiltInformation.h" namespace mitk { -/* - TODO describe attributes common and different for all blocks of a series (perhaps not here) -*/ +//TODO describe attributes common and different for all blocks of a series (perhaps not here) + +/** + \ingroup DICOMReaderModule + \brief Output descriptor for DICOMFileReader. + + As a result of analysis by a mitk::DICOMFileReader, this class + describes the properties of a single mitk::Images that could + be loaded by the file reader. + + The descriptor contains the following information: + - the mitk::Image itself. This will be NULL after analysis and only be present after actual loading. + - a list of frames (mostly: filenames) that went into composition of the mitk::Image. + - an assessment of the reader's ability to load this set of files (ReaderImplementationLevel) + - this can be used for reader selection when one reader is able to load an image with correct colors and the other is able to produce only gray values, for example + - description of aspects of the image. Mostly a key-value list implemented by means of mitk::PropertyList. + - for specific keys and possible values, see documentation of specific readers. + \note an mitk::Image may both consist of multiple files (the "old" DICOM way) or a mitk::Image may be described by a single DICOM file or even only parts of a DICOM file (the newer multi-frame DICOM classes). To reflect this DICOMImageFrameList describes a list of frames from different or a single file. + + Described aspects of an image are: + - whether pixel spacing is meant to be in-patient or on-detector (mitk::PixelSpacingInterpretation) + - details about a possible gantry tilt (intended for use by file readers, may be hidden later) +*/ class DICOMReader_EXPORT DICOMImageBlockDescriptor { public: DICOMImageBlockDescriptor(); ~DICOMImageBlockDescriptor(); DICOMImageBlockDescriptor(const DICOMImageBlockDescriptor& other); DICOMImageBlockDescriptor& operator=(const DICOMImageBlockDescriptor& other); + /// List of frames that constitute the mitk::Image (DICOMImageFrame%s) void SetImageFrameList(const DICOMImageFrameList& framelist); + /// List of frames that constitute the mitk::Image (DICOMImageFrame%s) const DICOMImageFrameList& GetImageFrameList() const; + /// The 3D mitk::Image that is loaded from the DICOM files of a DICOMImageFrameList + void SetMitkImage(Image::Pointer image); + /// the 3D mitk::Image that is loaded from the DICOM files of a DICOMImageFrameList + Image::Pointer GetMitkImage() const; + + /// Reader's capability to appropriately load this set of frames + ReaderImplementationLevel GetReaderImplementationLevel() const; + /// Reader's capability to appropriately load this set of frames + void SetReaderImplementationLevel(const ReaderImplementationLevel& level); + + /// Key-value store describing aspects of the image to be loaded void SetProperty(const std::string& key, BaseProperty* value); + /// Key-value store describing aspects of the image to be loaded BaseProperty* GetProperty(const std::string& key) const; + + /// Convenience function around GetProperty() std::string GetPropertyAsString(const std::string&) const; + + /// Convenience function around SetProperty() void SetFlag(const std::string& key, bool value); + /// Convenience function around GetProperty() bool GetFlag(const std::string& key, bool defaultValue) const; + + /// Convenience function around SetProperty() void SetIntProperty(const std::string& key, int value); + /// Convenience function around GetProperty() int GetIntProperty(const std::string& key, int defaultValue) const; - void SetMitkImage(Image::Pointer image); - Image::Pointer GetMitkImage() const; + private: + // For future implementation: load slice-by-slice, mark this using these methods void SetSliceIsLoaded(unsigned int index, bool isLoaded); + // For future implementation: load slice-by-slice, mark this using these methods bool IsSliceLoaded(unsigned int index) const; + // For future implementation: load slice-by-slice, mark this using these methods bool AllSlicesAreLoaded() const; - void SetPixelSpacingInformation(const std::string& pixelSpacing, const std::string& imagerPixelSpacing); + public: + + /// Takes the raw value of the DICOM tags (0028,0030) Pixel Spacing and (0018,1164) Imager Pixel Spacing + void SetPixelSpacingTagValues(const std::string& pixelSpacing, const std::string& imagerPixelSpacing); + + /// Describe how the mitk::Image's pixel spacing should be interpreted PixelSpacingInterpretation GetPixelSpacingInterpretation() const; - void GetDesiredMITKImagePixelSpacing( ScalarType& spacingX, ScalarType& spacingY) const; + /// Describe the correct x/y pixel spacing of the mitk::Image (which some readers might need to adjust after loading) + void GetDesiredMITKImagePixelSpacing( ScalarType& spacingXinMM, ScalarType& spacingYinMM) const; + + /// Describe the gantry tilt of the acquisition void SetTiltInformation(const GantryTiltInformation& info); + /// Describe the gantry tilt of the acquisition const GantryTiltInformation GetTiltInformation() const; - ReaderImplementationLevel GetReaderImplementationLevel() const; - void SetReaderImplementationLevel(const ReaderImplementationLevel& level); - + /// SOP Class UID of this set of frames void SetSOPClassUID(const std::string& uid); + /// SOP Class UID of this set of frames std::string GetSOPClassUID() const; - std::string GetSOPClassUIDAsString() const; + /// SOP Class as human readable name (e.g. "CT Image Storage") + std::string GetSOPClassUIDAsName() const; void SetTagCache(DICOMTagCache* privateCache); + /// Print information about this image block to given stream void Print(std::ostream& os, bool filenameDetails) const; private: Image::Pointer FixupSpacing(Image* mitkImage); Image::Pointer DescribeImageWithProperties(Image* mitkImage); void UpdateImageDescribingProperties(); DICOMImageFrameList m_ImageFrameList; Image::Pointer m_MitkImage; BoolList m_SliceIsLoaded; PixelSpacingInterpretation m_PixelSpacingInterpretation; ReaderImplementationLevel m_ReaderImplementationLevel; std::string m_PixelSpacing; std::string m_ImagerPixelSpacing; std::string m_SOPClassUID; GantryTiltInformation m_TiltInformation; PropertyList::Pointer m_PropertyList; const DICOMTagCache* m_TagCache; }; } #endif diff --git a/Modules/DICOMReader/mitkDICOMImageFrameInfo.h b/Modules/DICOMReader/mitkDICOMImageFrameInfo.h index a379b771e7..713076e2f8 100644 --- a/Modules/DICOMReader/mitkDICOMImageFrameInfo.h +++ b/Modules/DICOMReader/mitkDICOMImageFrameInfo.h @@ -1,50 +1,60 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #ifndef mitkDICOMImageFrameInfo_h #define mitkDICOMImageFrameInfo_h #include "itkObjectFactory.h" #include "mitkCommon.h" #include "DICOMReaderExports.h" namespace mitk { + /** + \ingroup DICOMReaderModule + \brief Describes a frame within a DICOM file. + + This is a minimal data structure to describe a single frame + (think of DICOM multi-frame classes) within a DICOM file. + To be used by DICOMFileReader%s and as part of DICOMImageBlockDescriptor. + */ class DICOMReader_EXPORT DICOMImageFrameInfo : public itk::LightObject { public: + /// absolute filename std::string Filename; + /// frame number, starting with 0 unsigned int FrameNo; mitkClassMacro( DICOMImageFrameInfo, itk::LightObject ) itkNewMacro( DICOMImageFrameInfo ); mitkNewMacro1Param( DICOMImageFrameInfo, const std::string&); mitkNewMacro2Param( DICOMImageFrameInfo, const std::string&, unsigned int ); bool operator==(const DICOMImageFrameInfo& other) const; protected: DICOMImageFrameInfo(const std::string& filename = "", unsigned int frameNo = 0); }; typedef std::vector DICOMImageFrameList; } #endif diff --git a/Modules/DICOMReader/mitkDICOMReaderConfigurator.cpp b/Modules/DICOMReader/mitkDICOMReaderConfigurator.cpp new file mode 100644 index 0000000000..eaa184d1ef --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMReaderConfigurator.cpp @@ -0,0 +1,333 @@ +/*=================================================================== + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center, +Division of Medical and Biological Informatics. +All rights reserved. + +This software is distributed WITHOUT ANY WARRANTY; without +even the implied warranty of MERCHANTABILITY or FITNESS FOR +A PARTICULAR PURPOSE. + +See LICENSE.txt or http://www.mitk.org for details. + +===================================================================*/ + +#include "mitkDICOMReaderConfigurator.h" + +#include "mitkClassicDICOMSeriesReader.h" + +#include "mitkDICOMTagBasedSorter.h" + +#include "mitkDICOMSortByTag.h" +#include "mitkSortByImagePositionPatient.h" + +mitk::DICOMReaderConfigurator +::DICOMReaderConfigurator() +{ +} + +mitk::DICOMReaderConfigurator +::~DICOMReaderConfigurator() +{ +} + +mitk::DICOMFileReader::Pointer +mitk::DICOMReaderConfigurator +::CreateFromConfigFile(const std::string& filename) +{ + TiXmlDocument doc (filename); + if (doc.LoadFile()) + { + return this->CreateFromTiXmlDocument( doc ); + } + else + { + MITK_ERROR << "Unable to load file at '" << filename <<"'"; + return DICOMFileReader::Pointer(); + } +} + +mitk::DICOMFileReader::Pointer +mitk::DICOMReaderConfigurator +::CreateFromUTF8ConfigString(const std::string& xmlContents) +{ + TiXmlDocument doc; + doc.Parse(xmlContents.c_str(), 0, TIXML_ENCODING_UTF8); + + return this->CreateFromTiXmlDocument( doc ); +} + +mitk::DICOMFileReader::Pointer +mitk::DICOMReaderConfigurator +::CreateFromTiXmlDocument(TiXmlDocument& doc) +{ + TiXmlHandle root(doc.RootElement()); + + if (TiXmlElement* rootElement = root.ToElement()) + { + if (strcmp(rootElement->Value(), "DICOMFileReader")) // :-( no std::string methods + { + MITK_ERROR << "File should contain a tag at top-level! Found '" + << (rootElement->Value() ? std::string(rootElement->Value()) : std::string("!nothing!")) << "' instead"; + return NULL; + } + + const char* classnameC = rootElement->Attribute("class"); + if (!classnameC) + { + MITK_ERROR << "File should name a reader class in the class attribute: . Found nothing instead"; + return NULL; + } + + std::string classname(classnameC); + if (classname == "ThreeDnTDICOMSeriesReader") + { + mitk::ThreeDnTDICOMSeriesReader::Pointer reader = mitk::ThreeDnTDICOMSeriesReader::New(); + return ConfigureThreeDnTDICOMSeriesReader(reader, rootElement).GetPointer(); + } + else + if (classname == "DICOMITKSeriesGDCMReader") + { + mitk::DICOMITKSeriesGDCMReader::Pointer reader = mitk::DICOMITKSeriesGDCMReader::New(); + return ConfigureDICOMITKSeriesGDCMReader(reader, rootElement).GetPointer(); + } + else + { + MITK_ERROR << "DICOMFileReader tag names unknown class '" << classname << "'"; + return NULL; + } + } + else + { + MITK_ERROR << "Great confusion: no root element in XML document. Expecting a DICOMFileReader tag at top-level."; + return NULL; + } +} + +#define boolStringTrue(s) \ + ( s == "true" || s == "on" || s == "1" \ + || s == "TRUE" || s == "ON") + +mitk::ThreeDnTDICOMSeriesReader::Pointer +mitk::DICOMReaderConfigurator +::ConfigureThreeDnTDICOMSeriesReader(ThreeDnTDICOMSeriesReader::Pointer reader, TiXmlElement* element) +{ + assert(element); + + // use all the base class configuration + if (this->ConfigureDICOMITKSeriesGDCMReader( reader.GetPointer(), element ).IsNull()) + { + return NULL; + } + + // add the "group3DnT" flag + bool group3DnT(true); + const char* group3DnTC = element->Attribute("group3DnT"); + if (group3DnTC) + { + std::string group3DnTS(group3DnTC); + group3DnT = boolStringTrue(group3DnTS); + } + + reader->SetGroup3DandT( group3DnT ); + + return reader; +} + +mitk::DICOMITKSeriesGDCMReader::Pointer +mitk::DICOMReaderConfigurator +::ConfigureDICOMITKSeriesGDCMReader(DICOMITKSeriesGDCMReader::Pointer reader, TiXmlElement* element) +{ + assert(element); + + const char* configLabelC = element->Attribute("label"); + if (configLabelC) + { + std::string configLabel(configLabelC); + reader->SetConfigurationLabel(configLabel); + } + + const char* configDescriptionC = element->Attribute("label"); + if (configDescriptionC) + { + std::string configDescription(configDescriptionC); + reader->SetConfigurationDescription(configDescriptionC); + } + + // "fixTiltByShearing" flag + bool fixTiltByShearing(false); + const char* fixTiltByShearingC = element->Attribute("fixTiltByShearing"); + if (fixTiltByShearingC) + { + std::string fixTiltByShearingS(fixTiltByShearingC); + fixTiltByShearing = boolStringTrue(fixTiltByShearingS); + } + + reader->SetFixTiltByShearing( fixTiltByShearing ); + + + // "distinguishing tags" + mitk::DICOMTagBasedSorter::Pointer tagSorter = mitk::DICOMTagBasedSorter::New(); + TiXmlElement* dElement = element->FirstChildElement("Distinguishing"); + if (dElement) + { + for ( TiXmlElement* tChild = dElement->FirstChildElement(); + tChild != NULL; + tChild = tChild->NextSiblingElement() ) + { + try + { + mitk::DICOMTag tag = tagFromXMLElement(tChild); + tagSorter->AddDistinguishingTag( tag ); + } + catch(...) + { + return NULL; + } + } + } + + // "sorting tags" + TiXmlElement* sElement = element->FirstChildElement("Sorting"); + if (dElement) + { + DICOMSortCriterion::Pointer previousCriterion; + DICOMSortCriterion::Pointer currentCriterion; + + for ( TiXmlNode* tChildNode = sElement->LastChild(); + tChildNode != NULL; + tChildNode = tChildNode->PreviousSibling() ) + { + TiXmlElement* tChild = tChildNode->ToElement(); + if (!tChild) continue; + + if (!strcmp(tChild->Value(), "Tag")) + { + try + { + currentCriterion = this->CreateDICOMSortByTag(tChild, previousCriterion); + } + catch(...) + { + std::stringstream ss; + ss << "Could not parse element at (input line " << tChild->Row() << ", col. " << tChild->Column() << ")!"; + MITK_ERROR << ss.str(); + return NULL; + } + } + else + if (!strcmp(tChild->Value(), "ImagePositionPatient")) + { + try + { + currentCriterion = this->CreateSortByImagePositionPatient(tChild, previousCriterion); + } + catch(...) + { + std::stringstream ss; + ss << "Could not parse element at (input line " << tChild->Row() << ", col. " << tChild->Column() << ")!"; + MITK_ERROR << ss.str(); + return NULL; + } + } + else + { + MITK_ERROR << "File contain unknown tag <" << tChild->Value() << "> tag as child to ! Cannot interpret..."; + } + + previousCriterion = currentCriterion; + } + + tagSorter->SetSortCriterion( currentCriterion.GetPointer() ); // TODO get ConstPointer declarations right here + } + + reader->AddSortingElement( tagSorter ); + + return reader; +} + +std::string +mitk::DICOMReaderConfigurator +::requiredStringAttribute(TiXmlElement* xmlElement, const std::string& key) +{ + assert(xmlElement); + + const char* gC = xmlElement->Attribute(key.c_str()); + if (gC) + { + std::string gS(gC); + return gS; + } + else + { + std::stringstream ss; + ss << "Expected an attribute '" << key << "' at this position " + "(input line " << xmlElement->Row() << ", col. " << xmlElement->Column() << ")!"; + MITK_ERROR << ss.str(); + throw std::invalid_argument( ss.str() ); + } +} + +unsigned int +mitk::DICOMReaderConfigurator +::hexStringToUInt(const std::string& s) +{ + std::stringstream converter(s); + unsigned int ui; + converter >> std::hex >> ui; + MITK_DEBUG << "Converted string '" << s << "' to unsigned int " << ui; + return ui; +} + +mitk::DICOMTag +mitk::DICOMReaderConfigurator +::tagFromXMLElement(TiXmlElement* xmlElement) +{ + assert(xmlElement); + + if (strcmp(xmlElement->Value(), "Tag")) // :-( no std::string methods + { + std::stringstream ss; + ss << "Expected a tag at this position " + "(input line " << xmlElement->Row() << ", col. " << xmlElement->Column() << ")!"; + MITK_ERROR << ss.str(); + throw std::invalid_argument( ss.str() ); + } + + std::string name = requiredStringAttribute(xmlElement, "name"); + std::string groupS = requiredStringAttribute(xmlElement, "group"); + std::string elementS = requiredStringAttribute(xmlElement, "element"); + + try + { + // convert string to int (assuming string is in hex format with leading "0x" like "0x0020") + unsigned int group = hexStringToUInt(groupS); + unsigned int element = hexStringToUInt(elementS); + return DICOMTag(group, element); + } + catch(...) + { + std::stringstream ss; + ss << "Expected group and element values in to be hexadecimal with leading 0x, e.g. '0x0020'" + "(input line " << xmlElement->Row() << ", col. " << xmlElement->Column() << ")!"; + MITK_ERROR << ss.str(); + throw std::invalid_argument( ss.str() ); + } +} + +mitk::DICOMSortCriterion::Pointer +mitk::DICOMReaderConfigurator +::CreateDICOMSortByTag(TiXmlElement* xmlElement, DICOMSortCriterion::Pointer secondaryCriterion) +{ + mitk::DICOMTag tag = tagFromXMLElement(xmlElement); + return DICOMSortByTag::New(tag, secondaryCriterion).GetPointer(); +} + +mitk::DICOMSortCriterion::Pointer +mitk::DICOMReaderConfigurator +::CreateSortByImagePositionPatient(TiXmlElement*, DICOMSortCriterion::Pointer secondaryCriterion) +{ + return SortByImagePositionPatient::New(secondaryCriterion).GetPointer(); +} diff --git a/Modules/DICOMReader/mitkDICOMReaderConfigurator.h b/Modules/DICOMReader/mitkDICOMReaderConfigurator.h new file mode 100644 index 0000000000..51fd27f4b0 --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMReaderConfigurator.h @@ -0,0 +1,108 @@ +/*=================================================================== + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center, +Division of Medical and Biological Informatics. +All rights reserved. + +This software is distributed WITHOUT ANY WARRANTY; without +even the implied warranty of MERCHANTABILITY or FITNESS FOR +A PARTICULAR PURPOSE. + +See LICENSE.txt or http://www.mitk.org for details. + +===================================================================*/ + +#ifndef mitkDICOMReaderConfigurator_h +#define mitkDICOMReaderConfigurator_h + +#include "mitkThreeDnTDICOMSeriesReader.h" + +// to put into private implementation +#include "tinyxml.h" + +namespace mitk +{ + +/** + \ingroup DICOMReaderModule + \brief Too-simple factory to create DICOMFileReader%s. + + This class is able to instantiate and configure (where possible) DICOMFileReader%s from XML descriptions. + \note This is a bad factory example, because the factory is not extensible and needs to know all the specific readers. A flexible implementation should be provided in a future version. + + In its current version, the XML input is meant to be structured like +\verbatim + + + + + + + + + + + +\endverbatim + + The root-tag \i names the class to be instantiated, currently this can be one of + - DICOMITKSeriesGDCMReader + - ThreeDnTDICOMSeriesReader + + Both classes bring simple configuration flags with them and a description of how images are sorted prior to loading. + + Flag for DICOMITKSeriesGDCMReader: +
fixTiltByShearing="true|false"
+ Determines whether a potential gantry tilt should be "fixed" by shearing the output image. + + Flag for ThreeDnTDICOMSeriesReader: +
group3DnT="true|false"
+ Determines whether images at the same spatial position should be interpreted as 3D+t images. + + The tags and describe the basic loading strategy of both + reader mentioned above: first images are divided into incompatible groups (), + and afterwards the images within each group are sorted by means of DICOMSortCriterion, which + most commonly mentions a tag. + + Tag element and group are interpreted as the hexadecimal numbers + found all around the DICOM standard. The numbers can be prepended by a "0x" if this is preferred + by the programmer (but they are taken as hexadecimal in all cases). +*/ +class DICOMReader_EXPORT DICOMReaderConfigurator : public itk::LightObject +{ + public: + + mitkClassMacro( DICOMReaderConfigurator, itk::LightObject ) + itkNewMacro( DICOMReaderConfigurator ) + + DICOMFileReader::Pointer CreateFromConfigFile(const std::string& filename); + DICOMFileReader::Pointer CreateFromUTF8ConfigString(const std::string& xmlContents); + + protected: + + DICOMReaderConfigurator(); + virtual ~DICOMReaderConfigurator(); + + private: + + DICOMFileReader::Pointer CreateFromTiXmlDocument(TiXmlDocument& doc); + DICOMTag tagFromXMLElement(TiXmlElement*); + std::string requiredStringAttribute(TiXmlElement* xmlElement, const std::string& key); + unsigned int hexStringToUInt(const std::string& s); + + ThreeDnTDICOMSeriesReader::Pointer ConfigureThreeDnTDICOMSeriesReader(ThreeDnTDICOMSeriesReader::Pointer reader, TiXmlElement*); + DICOMITKSeriesGDCMReader::Pointer ConfigureDICOMITKSeriesGDCMReader(DICOMITKSeriesGDCMReader::Pointer reader, TiXmlElement*); + + DICOMSortCriterion::Pointer CreateDICOMSortByTag(TiXmlElement* xmlElement, DICOMSortCriterion::Pointer secondaryCriterion); + DICOMSortCriterion::Pointer CreateSortByImagePositionPatient(TiXmlElement* xmlElement, DICOMSortCriterion::Pointer secondaryCriterion); + }; + +} // namespace + +#endif // mitkDICOMReaderConfigurator_h diff --git a/Modules/DICOMReader/mitkDICOMSortByTag.cpp b/Modules/DICOMReader/mitkDICOMSortByTag.cpp index 5117e7390c..8cacc7fef5 100644 --- a/Modules/DICOMReader/mitkDICOMSortByTag.cpp +++ b/Modules/DICOMReader/mitkDICOMSortByTag.cpp @@ -1,117 +1,125 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #include "mitkDICOMSortByTag.h" mitk::DICOMSortByTag ::DICOMSortByTag(const DICOMTag& tag, DICOMSortCriterion::Pointer secondaryCriterion) :DICOMSortCriterion(secondaryCriterion) ,m_Tag(tag) { } mitk::DICOMSortByTag ::~DICOMSortByTag() { } mitk::DICOMSortByTag ::DICOMSortByTag(const DICOMSortByTag& other ) :DICOMSortCriterion(other) ,m_Tag(other.m_Tag) { } mitk::DICOMSortByTag& mitk::DICOMSortByTag ::operator=(const DICOMSortByTag& other) { if (this != &other) { DICOMSortCriterion::operator=(other); m_Tag = other.m_Tag; } return *this; } +void +mitk::DICOMSortByTag +::Print(std::ostream& os) const +{ + m_Tag.Print(os); +} + + mitk::DICOMTagList mitk::DICOMSortByTag ::GetTagsOfInterest() const { DICOMTagList list; list.push_back(m_Tag); return list; } bool mitk::DICOMSortByTag ::IsLeftBeforeRight(const mitk::DICOMDatasetAccess* left, const mitk::DICOMDatasetAccess* right) const { return this->NumericCompare(left, right, m_Tag); } bool mitk::DICOMSortByTag ::StringCompare(const mitk::DICOMDatasetAccess* left, const mitk::DICOMDatasetAccess* right, const DICOMTag& tag) const { assert(left); assert(right); std::string leftString = left->GetTagValueAsString(tag); std::string rightString = right->GetTagValueAsString(tag); if (leftString != rightString) { return leftString.compare(rightString) < 0; } else { return this->NextLevelIsLeftBeforeRight(left, right); } } bool mitk::DICOMSortByTag ::NumericCompare(const mitk::DICOMDatasetAccess* left, const mitk::DICOMDatasetAccess* right, const DICOMTag& tag) const { assert(left); assert(right); std::string leftString = left->GetTagValueAsString(tag); std::string rightString = right->GetTagValueAsString(tag); std::istringstream lefti(leftString); std::istringstream righti(rightString); double leftDouble(0); double rightDouble(0); if ( (lefti >> leftDouble) && (righti >> rightDouble) ) { if (leftDouble != rightDouble) // can we decide? { return leftDouble < rightDouble; } else // ask secondary criterion { return this->NextLevelIsLeftBeforeRight(left, right); } } else // no numerical conversion.. { return this->StringCompare(left,right, tag); // fallback to string compare } } diff --git a/Modules/DICOMReader/mitkDICOMSortByTag.h b/Modules/DICOMReader/mitkDICOMSortByTag.h index 8ca6c48596..33a228fa2e 100644 --- a/Modules/DICOMReader/mitkDICOMSortByTag.h +++ b/Modules/DICOMReader/mitkDICOMSortByTag.h @@ -1,54 +1,68 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #ifndef mitkDICOMSortByTag_h #define mitkDICOMSortByTag_h #include "mitkDICOMSortCriterion.h" namespace mitk { +/** + \ingroup DICOMReaderModule + \brief Compare two datasets by the value of a single tag (for use in DICOMTagBasedSorter). + + The class will compare the tag values by + 1. numerical value if possible (i.e. both datasets have a value that is numerical) + 2. alphabetical order otherwise + + If the comparison results in equalness, it is refered to the secondary criterion, see + DICOMSortByTag::NextLevelIsLeftBeforeRight(). + +*/ class DICOMReader_EXPORT DICOMSortByTag : public DICOMSortCriterion { public: mitkClassMacro( DICOMSortByTag, DICOMSortCriterion ); mitkNewMacro1Param( DICOMSortByTag, const DICOMTag& ); mitkNewMacro2Param( DICOMSortByTag, const DICOMTag&, DICOMSortCriterion::Pointer ); virtual DICOMTagList GetTagsOfInterest() const; virtual bool IsLeftBeforeRight(const mitk::DICOMDatasetAccess* left, const mitk::DICOMDatasetAccess* right) const; + virtual void Print(std::ostream& os) const; + protected: DICOMSortByTag( const DICOMTag& tag, DICOMSortCriterion::Pointer secondaryCriterion = NULL ); virtual ~DICOMSortByTag(); DICOMSortByTag(const DICOMSortByTag& other); DICOMSortByTag& operator=(const DICOMSortByTag& other); bool StringCompare(const mitk::DICOMDatasetAccess* left, const mitk::DICOMDatasetAccess* right, const DICOMTag& tag) const; bool NumericCompare(const mitk::DICOMDatasetAccess* left, const mitk::DICOMDatasetAccess* right, const DICOMTag& tag) const; private: DICOMTag m_Tag; }; } #endif diff --git a/Modules/DICOMReader/mitkDICOMSortCriterion.cpp b/Modules/DICOMReader/mitkDICOMSortCriterion.cpp index bbeebb07f0..7086f4305b 100644 --- a/Modules/DICOMReader/mitkDICOMSortCriterion.cpp +++ b/Modules/DICOMReader/mitkDICOMSortCriterion.cpp @@ -1,79 +1,86 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #include "mitkDICOMSortCriterion.h" mitk::DICOMSortCriterion ::DICOMSortCriterion(DICOMSortCriterion::Pointer secondaryCriterion) :itk::LightObject() ,m_SecondaryCriterion(secondaryCriterion) { } mitk::DICOMSortCriterion ::~DICOMSortCriterion() { } mitk::DICOMSortCriterion ::DICOMSortCriterion(const DICOMSortCriterion& other ) :itk::LightObject() ,m_SecondaryCriterion(other.m_SecondaryCriterion) { } mitk::DICOMSortCriterion& mitk::DICOMSortCriterion ::operator=(const DICOMSortCriterion& other) { if (this != &other) { m_SecondaryCriterion = other.m_SecondaryCriterion; } return *this; } +mitk::DICOMSortCriterion::ConstPointer +mitk::DICOMSortCriterion +::GetSecondaryCriterion() const +{ + return m_SecondaryCriterion.GetPointer(); +} + mitk::DICOMTagList mitk::DICOMSortCriterion ::GetAllTagsOfInterest() const { DICOMTagList allTags; // iterate all secondary criteria, return all tags const DICOMSortCriterion* criterionToCheck = this; while (criterionToCheck) { DICOMTagList newElements = criterionToCheck->GetTagsOfInterest(); allTags.insert( allTags.end(), newElements.begin(), newElements.end() ); // append criterionToCheck = criterionToCheck->m_SecondaryCriterion.GetPointer(); } return allTags; } bool mitk::DICOMSortCriterion ::NextLevelIsLeftBeforeRight(const mitk::DICOMDatasetAccess* left, const mitk::DICOMDatasetAccess* right) const { if (m_SecondaryCriterion.IsNotNull()) { return m_SecondaryCriterion->IsLeftBeforeRight(left, right); } else { return (void*)left < (void*)right; } } diff --git a/Modules/DICOMReader/mitkDICOMSortCriterion.h b/Modules/DICOMReader/mitkDICOMSortCriterion.h index 061ac3b542..8921bd9290 100644 --- a/Modules/DICOMReader/mitkDICOMSortCriterion.h +++ b/Modules/DICOMReader/mitkDICOMSortCriterion.h @@ -1,54 +1,76 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #ifndef mitkDICOMSortCriterion_h #define mitkDICOMSortCriterion_h #include "itkObjectFactory.h" #include "mitkCommon.h" #include "mitkDICOMDatasetAccess.h" namespace mitk { +/** + \ingroup DICOMReaderModule + \brief A tag based sorting criterion for use in DICOMTagBasedSorter. + + This class is used within std::sort (see DICOMTagBasedSorter::Sort()) + and has to answer a simple question by implementing IsLeftBeforeRight(). + Each time IsLeftBeforeRight() is called, the method should return whether + the left dataset should be sorted before the right dataset. + + Because there are identical tags values quite oftenly, a DICOMSortCriterion + will always hold a secondary DICOMSortCriterion. In cases of equal tag + values, the decision is refered to the secondary criterion. +*/ class DICOMReader_EXPORT DICOMSortCriterion : public itk::LightObject { public: mitkClassMacro( DICOMSortCriterion, itk::LightObject ); + /// \brief Tags used for comparison (includes seconary criteria). DICOMTagList GetAllTagsOfInterest() const; + /// \brief Tags used for comparison. virtual DICOMTagList GetTagsOfInterest() const = 0; + /// \brief Answer the sorting question. virtual bool IsLeftBeforeRight(const mitk::DICOMDatasetAccess* left, const mitk::DICOMDatasetAccess* right) const = 0; + /// \brief The fallback criterion. + DICOMSortCriterion::ConstPointer GetSecondaryCriterion() const; + + /// brief describe this class in given stream. + virtual void Print(std::ostream& os) const = 0; + protected: DICOMSortCriterion( DICOMSortCriterion::Pointer secondaryCriterion ); virtual ~DICOMSortCriterion(); bool NextLevelIsLeftBeforeRight(const mitk::DICOMDatasetAccess* left, const mitk::DICOMDatasetAccess* right) const; DICOMSortCriterion(const DICOMSortCriterion& other); DICOMSortCriterion& operator=(const DICOMSortCriterion& other); DICOMSortCriterion::Pointer m_SecondaryCriterion; }; } #endif diff --git a/Modules/DICOMReader/mitkDICOMTag.cpp b/Modules/DICOMReader/mitkDICOMTag.cpp index 3c9538d432..4a54e4d58e 100644 --- a/Modules/DICOMReader/mitkDICOMTag.cpp +++ b/Modules/DICOMReader/mitkDICOMTag.cpp @@ -1,166 +1,207 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY { } without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #include "mitkDICOMTag.h" +#include +#include + +#include "mitkLogMacros.h" + mitk::DICOMTag ::DICOMTag(unsigned int group, unsigned int element) :m_Group(group) ,m_Element(element) { } mitk::DICOMTag ::DICOMTag(const DICOMTag& other) :m_Group(other.m_Group) ,m_Element(other.m_Element) { } bool mitk::DICOMTag ::operator==(const DICOMTag& other) const { return m_Group == other.m_Group && m_Element == other.m_Element ; } mitk::DICOMTag& mitk::DICOMTag ::operator=(const DICOMTag& other) { if (this != &other) { m_Group = other.m_Group; m_Element = other.m_Element; } return *this; } unsigned int mitk::DICOMTag ::GetGroup() const { return m_Group; } unsigned int mitk::DICOMTag ::GetElement() const { return m_Element; } bool mitk::DICOMTag ::operator<(const DICOMTag& other) const { + // TODO check this comparison! return this->m_Group * 0x3000 + this->m_Element < other.m_Group * 0x3000 + other.m_Element; } +std::string +mitk::DICOMTag +::GetName() const +{ + gdcm::Tag t(m_Group, m_Element); + + const gdcm::Global& g = gdcm::Global::GetInstance(); // sum of all knowledge ! + const gdcm::Dicts& dicts = g.GetDicts(); + const gdcm::Dict& pub = dicts.GetPublicDict(); // Part 6 + + const gdcm::DictEntry& entry = pub.GetDictEntry(t); + std::string name = entry.GetName(); + if (name.empty()) + { + name = "Unknown Tag"; + } + + return name; +} + +std::string +mitk::DICOMTag +::toHexString(unsigned int i) const +{ + std::stringstream ss; + ss << std::setfill ('0') << std::setw(4) << std::hex << i; + return ss.str(); +} + +void +mitk::DICOMTag +::Print(std::ostream& os) const +{ + os << "(" << toHexString(m_Group) << "," << toHexString(m_Element) << ") " << this->GetName(); +} void mitk::DICOMStringToOrientationVectors(const std::string& s, Vector3D& right, Vector3D& up, bool& successful) { successful = true; std::istringstream orientationReader(s); std::string coordinate; unsigned int dim(0); while( std::getline( orientationReader, coordinate, '\\' ) && dim < 6 ) { if (dim<3) { right[dim++] = atof(coordinate.c_str()); } else { up[dim++ - 3] = atof(coordinate.c_str()); } } if (dim && dim != 6) { successful = false; } else if (dim == 0) { // fill with defaults right.Fill(0.0); right[0] = 1.0; up.Fill(0.0); up[1] = 1.0; successful = false; } } bool mitk::DICOMStringToSpacing(const std::string& s, ScalarType& spacingX, ScalarType& spacingY) { bool successful = false; std::istringstream spacingReader(s); std::string spacing; if ( std::getline( spacingReader, spacing, '\\' ) ) { spacingY = atof( spacing.c_str() ); if ( std::getline( spacingReader, spacing, '\\' ) ) { spacingX = atof( spacing.c_str() ); successful = true; } } return successful; } mitk::Point3D mitk::DICOMStringToPoint3D(const std::string& s, bool& successful) { Point3D p; successful = true; std::istringstream originReader(s); std::string coordinate; unsigned int dim(0); while( std::getline( originReader, coordinate, '\\' ) && dim < 3) { p[dim++]= atof(coordinate.c_str()); } if (dim && dim != 3) { successful = false; } else if (dim == 0) { successful = false; p.Fill(0.0); // assume default (0,0,0) } return p; } diff --git a/Modules/DICOMReader/mitkDICOMTag.h b/Modules/DICOMReader/mitkDICOMTag.h index c827e272b6..d693a96d24 100644 --- a/Modules/DICOMReader/mitkDICOMTag.h +++ b/Modules/DICOMReader/mitkDICOMTag.h @@ -1,73 +1,89 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #ifndef mitkTag_h #define mitkTag_h #include "mitkVector.h" #include "DICOMReaderExports.h" namespace mitk { +/** + \ingroup DICOMReaderModule + \brief Representation of a DICOM tag. + + This class is just meant to combine group and element + numbers for better readability and make handling tags + more easy by offering comparison methods. +*/ class DICOMReader_EXPORT DICOMTag { public: DICOMTag(unsigned int group, unsigned int element); DICOMTag(const DICOMTag& other); DICOMTag& operator=(const DICOMTag& other); bool operator==(const DICOMTag& other) const; bool operator<(const DICOMTag& other) const; unsigned int GetGroup() const; unsigned int GetElement() const; - protected: + /// Return the name of this tag (e.g. "SeriesDescription" instead of "(0008,103e)") + std::string GetName() const; + + /// add "(group-id,element-id) name" to given stream + void Print(std::ostream& os) const; + + private: + + std::string toHexString(unsigned int i) const; unsigned int m_Group; unsigned int m_Element; }; typedef std::vector DICOMTagList; /** \brief Convert DICOM string describing a point two Vector3D. DICOM tags like ImageOrientationPatient contain two vectors as float numbers separated by backslashes: \verbatim 42.7131\13.77\0.7\137.76\0.3 \endverbatim */ void DICOMStringToOrientationVectors(const std::string& s, Vector3D& right, Vector3D& up, bool& successful); bool DICOMStringToSpacing(const std::string& s, ScalarType& spacingX, ScalarType& spacingY); /** \brief Convert DICOM string describing a point to Point3D. DICOM tags like ImagePositionPatient contain a position as float numbers separated by backslashes: \verbatim 42.7131\13.77\0.7 \endverbatim */ mitk::Point3D DICOMStringToPoint3D(const std::string& s, bool& successful); } #endif diff --git a/Modules/DICOMReader/mitkDICOMTagBasedSorter.cpp b/Modules/DICOMReader/mitkDICOMTagBasedSorter.cpp index f615cb876f..a527e6ecf4 100644 --- a/Modules/DICOMReader/mitkDICOMTagBasedSorter.cpp +++ b/Modules/DICOMReader/mitkDICOMTagBasedSorter.cpp @@ -1,244 +1,270 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #include "mitkDICOMTagBasedSorter.h" #include mitk::DICOMTagBasedSorter::CutDecimalPlaces ::CutDecimalPlaces(unsigned int precision) :m_Precision(precision) { } std::string mitk::DICOMTagBasedSorter::CutDecimalPlaces ::operator()(const std::string& input) const { + // TODO make this work with all kind of numbers and lists of numbers!! // be a bit tolerant for tags such as image orientation orienatation, let only the first few digits matter (http://bugs.mitk.org/show_bug.cgi?id=12263) bool conversionError(false); Vector3D right; right.Fill(0.0); Vector3D up; right.Fill(0.0); DICOMStringToOrientationVectors( input, right, up, conversionError ); std::ostringstream ss; ss.setf(std::ios::fixed, std::ios::floatfield); ss.precision(m_Precision); ss << right[0] << "\\" << right[1] << "\\" << right[2] << "\\" << up[0] << "\\" << up[1] << "\\" << up[2]; return ss.str(); } mitk::DICOMTagBasedSorter ::DICOMTagBasedSorter() :DICOMDatasetSorter() { } mitk::DICOMTagBasedSorter ::~DICOMTagBasedSorter() { for(TagValueProcessorMap::iterator ti = m_TagValueProcessor.begin(); ti != m_TagValueProcessor.end(); ++ti) { delete ti->second; } } mitk::DICOMTagBasedSorter ::DICOMTagBasedSorter(const DICOMTagBasedSorter& other ) :DICOMDatasetSorter(other) { } mitk::DICOMTagBasedSorter& mitk::DICOMTagBasedSorter ::operator=(const DICOMTagBasedSorter& other) { if (this != &other) { DICOMDatasetSorter::operator=(other); } return *this; } +void +mitk::DICOMTagBasedSorter +::PrintConfiguration(std::ostream& os, const std::string& indent) const +{ + os << indent << "Tag based sorting:" << std::endl; + for (DICOMTagList::const_iterator tagIter = m_DistinguishingTags.begin(); + tagIter != m_DistinguishingTags.end(); + ++tagIter) + { + os << indent << " Split on "; + tagIter->Print(os); + os << std::endl; + } + + DICOMSortCriterion::ConstPointer crit = m_SortCriterion.GetPointer(); + while (crit.IsNotNull()) + { + os << indent << " Sort by "; + crit->Print(os); + os << std::endl; + crit = crit->GetSecondaryCriterion(); + } +} + + mitk::DICOMTagList mitk::DICOMTagBasedSorter ::GetTagsOfInterest() { DICOMTagList allTags = m_DistinguishingTags; DICOMTagList sortingRelevantTags = m_SortCriterion->GetAllTagsOfInterest(); allTags.insert( allTags.end(), sortingRelevantTags.begin(), sortingRelevantTags.end() ); // append return allTags; } void mitk::DICOMTagBasedSorter ::AddDistinguishingTag( const DICOMTag& tag, TagValueProcessor* tagValueProcessor ) { m_DistinguishingTags.push_back(tag); m_TagValueProcessor[tag] = tagValueProcessor; } void mitk::DICOMTagBasedSorter ::SetSortCriterion( DICOMSortCriterion::ConstPointer criterion ) { m_SortCriterion = criterion; } void mitk::DICOMTagBasedSorter ::Sort() { // 1. split // 2. sort each group GroupIDToListType groups = this->SplitInputGroups(); GroupIDToListType& sortedGroups = this->SortGroups( groups ); // 3. define output this->SetNumberOfOutputs(sortedGroups.size()); unsigned int outputIndex(0); for (GroupIDToListType::iterator groupIter = sortedGroups.begin(); groupIter != sortedGroups.end(); ++outputIndex, ++groupIter) { this->SetOutput(outputIndex, groupIter->second); } } std::string mitk::DICOMTagBasedSorter ::BuildGroupID( DICOMDatasetAccess* dataset ) { // just concatenate all tag values assert(dataset); std::stringstream groupID; groupID << "g"; for (DICOMTagList::iterator tagIter = m_DistinguishingTags.begin(); tagIter != m_DistinguishingTags.end(); ++tagIter) { groupID << tagIter->GetGroup() << tagIter->GetElement(); // make group/element part of the id to cover empty tags std::string rawTagValue = dataset->GetTagValueAsString(*tagIter); std::string processedTagValue; if ( m_TagValueProcessor[*tagIter] != NULL ) { processedTagValue = (*m_TagValueProcessor[*tagIter])(rawTagValue); } else { processedTagValue = rawTagValue; } groupID << processedTagValue; } // shorten ID? return groupID.str(); } mitk::DICOMTagBasedSorter::GroupIDToListType mitk::DICOMTagBasedSorter ::SplitInputGroups() { DICOMDatasetList input = GetInput(); // copy GroupIDToListType listForGroupID; for (DICOMDatasetList::iterator dsIter = input.begin(); dsIter != input.end(); ++dsIter) { DICOMDatasetAccess* dataset = *dsIter; assert(dataset); std::string groupID = this->BuildGroupID( dataset ); MITK_DEBUG << "Group ID for for " << dataset->GetFilenameIfAvailable() << ": " << groupID; listForGroupID[groupID].push_back(dataset); } MITK_DEBUG << "After tag based splitting: " << listForGroupID.size() << " groups"; return listForGroupID; } mitk::DICOMTagBasedSorter::GroupIDToListType& mitk::DICOMTagBasedSorter ::SortGroups(GroupIDToListType& groups) { if (m_SortCriterion.IsNotNull()) { // for each output // sort by all configured tags, use secondary tags when equal or empty // make configurable: // - sorting order (ascending, descending) // - sort numerically // - ... ? unsigned int groupIndex(0); for (GroupIDToListType::iterator gIter = groups.begin(); gIter != groups.end(); ++groupIndex, ++gIter) { DICOMDatasetList& dsList = gIter->second; MITK_DEBUG << " --------------------------------------------------------------------------------"; MITK_DEBUG << " DICOMTagBasedSorter before sorting group : " << groupIndex; for (DICOMDatasetList::iterator oi = dsList.begin(); oi != dsList.end(); ++oi) { MITK_DEBUG << " INPUT : " << (*oi)->GetFilenameIfAvailable(); } std::sort( dsList.begin(), dsList.end(), ParameterizedDatasetSort( m_SortCriterion ) ); MITK_DEBUG << " --------------------------------------------------------------------------------"; MITK_DEBUG << " DICOMTagBasedSorter after sorting group : " << groupIndex; for (DICOMDatasetList::iterator oi = dsList.begin(); oi != dsList.end(); ++oi) { MITK_DEBUG << " OUTPUT : " << (*oi)->GetFilenameIfAvailable(); } MITK_DEBUG << " --------------------------------------------------------------------------------"; } } return groups; } mitk::DICOMTagBasedSorter::ParameterizedDatasetSort ::ParameterizedDatasetSort(DICOMSortCriterion::ConstPointer criterion) :m_SortCriterion(criterion) { } bool mitk::DICOMTagBasedSorter::ParameterizedDatasetSort ::operator() (const mitk::DICOMDatasetAccess* left, const mitk::DICOMDatasetAccess* right) { assert(left); assert(right); assert(m_SortCriterion.IsNotNull()); return m_SortCriterion->IsLeftBeforeRight(left, right); } diff --git a/Modules/DICOMReader/mitkDICOMTagBasedSorter.h b/Modules/DICOMReader/mitkDICOMTagBasedSorter.h index efec719b9b..9f6c215451 100644 --- a/Modules/DICOMReader/mitkDICOMTagBasedSorter.h +++ b/Modules/DICOMReader/mitkDICOMTagBasedSorter.h @@ -1,91 +1,148 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #ifndef mitkDICOMTagBasedSorter_h #define mitkDICOMTagBasedSorter_h #include "mitkDICOMDatasetSorter.h" #include "mitkDICOMSortCriterion.h" namespace mitk { /** - \brief sort files based on filename (last resort). + \ingroup DICOMReaderModule + \brief Sort DICOM datasets based on configurable tags. + + This class implements sorting of input DICOM datasets into multiple outputs + as described in \ref DICOMITKSeriesGDCMReader_LoadingStrategy. + + The logic of sorting and splitting is most simple and most generic: + + 1. Datasets will be put into different groups, if they differ in their value of specific tags (defined by AddDistinguishingTag()) + - there might be multiple distinguishing tags defined + - tag values might be processed before comparison by means of TagValueProcessor (e.g. round to a number of decimal places) + 2. Each of the groups will be sorted by comparing their tag values using multiple DICOMSortCriterion + - DICOMSortCriterion might evaluate a single tag (e.g. Instance Number) or multiple values (as in SortByImagePositionPatient) + - only a single DICOMSortCriterion is defined for DICOMTagBasedSorter, because each DICOMSortCriterion holds a "secondary sort criterion", i.e. an application can define multiple tags for sorting by chaining \link DICOMSortCriterion DICOMSortCriteria \endlink + - applications should make sure that sorting is always defined (to avoid problems with standard containers), e.g. by adding a comparison of filenames or instance UIDs as a last sorting fallback. + */ class DICOMReader_EXPORT DICOMTagBasedSorter : public DICOMDatasetSorter { public: + /** + \brief Processes tag values before they are compared. + These classes could do some kind of normalization such as rounding, lower case formatting, etc. + */ class TagValueProcessor { public: + /// \brief Implements the "processing". virtual std::string operator()(const std::string&) const = 0; }; + /** + \brief Cuts a number after configured number of decimal places. + An instance of this class can be used to avoid errors when comparing minimally different image orientations. + */ class CutDecimalPlaces : public TagValueProcessor { public: CutDecimalPlaces(unsigned int precision); virtual std::string operator()(const std::string&) const; private: unsigned int m_Precision; }; mitkClassMacro( DICOMTagBasedSorter, DICOMDatasetSorter ) itkNewMacro( DICOMTagBasedSorter ) + /** + \brief Datasets that differ in given tag's value will be sorted into separate outputs. + */ void AddDistinguishingTag( const DICOMTag&, TagValueProcessor* tagValueProcessor = NULL ); + + /** + \brief Define the sorting criterion (which holds seconardy criteria) + */ void SetSortCriterion( DICOMSortCriterion::ConstPointer criterion ); + /** + \brief A list of all the tags needed for processing (facilitates scanning). + */ virtual DICOMTagList GetTagsOfInterest(); + /** + \brief Actually sort as described in the Detailed Description. + */ virtual void Sort(); + /** + \brief Print configuration details into given stream. + */ + virtual void PrintConfiguration(std::ostream& os, const std::string& indent = "") const; + protected: + /** + \brief Helper struct to feed into std::sort, configured via DICOMSortCriterion. + */ struct ParameterizedDatasetSort { ParameterizedDatasetSort(DICOMSortCriterion::ConstPointer); bool operator() (const mitk::DICOMDatasetAccess* left, const mitk::DICOMDatasetAccess* right); - bool StringCompare(const mitk::DICOMDatasetAccess* left, const mitk::DICOMDatasetAccess* right); - bool NumericCompare(const mitk::DICOMDatasetAccess* left, const mitk::DICOMDatasetAccess* right); DICOMSortCriterion::ConstPointer m_SortCriterion; }; DICOMTagBasedSorter(); virtual ~DICOMTagBasedSorter(); DICOMTagBasedSorter(const DICOMTagBasedSorter& other); DICOMTagBasedSorter& operator=(const DICOMTagBasedSorter& other); + /** + \brief Helper for SplitInputGroups(). + */ std::string BuildGroupID( DICOMDatasetAccess* dataset ); typedef std::map GroupIDToListType; + + /** + \brief Implements the "distiguishing tags". + To sort datasets into different groups, a long string will be built for each dataset. The string concatenates all tags and their respective values. + Datasets that match in all values will end up with the same string. + */ GroupIDToListType SplitInputGroups(); + + /** + \brief Implements the sorting step. + Relatively simple implementation thanks to std::sort and a parameterization via DICOMSortCriterion. + */ GroupIDToListType& SortGroups(GroupIDToListType& groups); DICOMTagList m_DistinguishingTags; typedef std::map TagValueProcessorMap; TagValueProcessorMap m_TagValueProcessor; DICOMSortCriterion::ConstPointer m_SortCriterion; }; } #endif diff --git a/Modules/DICOMReader/mitkDICOMTagCache.h b/Modules/DICOMReader/mitkDICOMTagCache.h index f2b8eb26dc..c07ddd7519 100644 --- a/Modules/DICOMReader/mitkDICOMTagCache.h +++ b/Modules/DICOMReader/mitkDICOMTagCache.h @@ -1,35 +1,38 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #ifndef mitkDICOMTagCache_h #define mitkDICOMTagCache_h #include "itkObjectFactory.h" #include "mitkCommon.h" #include "DICOMReaderExports.h" namespace mitk { +/** + \ingroup DICOMReaderModule +*/ class DICOMReader_EXPORT DICOMTagCache { public: virtual std::string GetTagValue(DICOMImageFrameInfo* frame, const DICOMTag& tag) const = 0; }; } #endif diff --git a/Modules/DICOMReader/mitkEquiDistantBlocksSorter.cpp b/Modules/DICOMReader/mitkEquiDistantBlocksSorter.cpp index 0979f8472d..2e1fb66c41 100644 --- a/Modules/DICOMReader/mitkEquiDistantBlocksSorter.cpp +++ b/Modules/DICOMReader/mitkEquiDistantBlocksSorter.cpp @@ -1,484 +1,500 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ +//#define MBILOG_ENABLE_DEBUG + #include "mitkEquiDistantBlocksSorter.h" mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::SliceGroupingAnalysisResult() { } mitk::DICOMDatasetList mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::GetBlockDatasets() { return m_GroupedFiles; } mitk::DICOMDatasetList mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::GetUnsortedDatasets() { return m_UnsortedFiles; } bool mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::ContainsGantryTilt() { return m_TiltInfo.IsRegularGantryTilt(); } void mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::AddFileToSortedBlock(DICOMDatasetAccess* dataset) { m_GroupedFiles.push_back( dataset ); } void mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::AddFileToUnsortedBlock(DICOMDatasetAccess* dataset) { m_UnsortedFiles.push_back( dataset ); } void mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::AddFilesToUnsortedBlock(const DICOMDatasetList& datasets) { m_UnsortedFiles.insert( m_UnsortedFiles.end(), datasets.begin(), datasets.end() ); } void mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::SetFirstFilenameOfBlock(const std::string& filename) { m_FirstFilenameOfBlock = filename; } std::string mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::GetFirstFilenameOfBlock() const { return m_FirstFilenameOfBlock; } void mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::FlagGantryTilt(const GantryTiltInformation& tiltInfo) { m_TiltInfo = tiltInfo; } const mitk::GantryTiltInformation& mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::GetTiltInfo() const { return m_TiltInfo; } void mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::UndoPrematureGrouping() { assert( !m_GroupedFiles.empty() ); m_UnsortedFiles.insert( m_UnsortedFiles.begin(), m_GroupedFiles.back() ); m_GroupedFiles.pop_back(); m_TiltInfo = GantryTiltInformation(); } // ------------------------ end helper class mitk::EquiDistantBlocksSorter ::EquiDistantBlocksSorter() :DICOMDatasetSorter() ,m_AcceptTilt(false) { } mitk::EquiDistantBlocksSorter ::EquiDistantBlocksSorter(const EquiDistantBlocksSorter& other ) :DICOMDatasetSorter(other) ,m_AcceptTilt(false) { } mitk::EquiDistantBlocksSorter ::~EquiDistantBlocksSorter() { } +void +mitk::EquiDistantBlocksSorter +::PrintConfiguration(std::ostream& os, const std::string& indent) const +{ + os << indent << "Sort into blocks of equidistant, well-aligned slices " << (m_AcceptTilt ? "(accepting a gantry tilt)" : "") << std::endl; +} + + void mitk::EquiDistantBlocksSorter ::SetAcceptTilt(bool accept) { m_AcceptTilt = accept; } mitk::EquiDistantBlocksSorter& mitk::EquiDistantBlocksSorter ::operator=(const EquiDistantBlocksSorter& other) { if (this != &other) { DICOMDatasetSorter::operator=(other); } return *this; } mitk::DICOMTagList mitk::EquiDistantBlocksSorter ::GetTagsOfInterest() { DICOMTagList tags; tags.push_back( DICOMTag(0x0020, 0x0032) ); // ImagePositionPatient tags.push_back( DICOMTag(0x0020, 0x0037) ); // ImageOrientationPatient tags.push_back( DICOMTag(0x0018, 0x1120) ); // GantryTilt return tags; } void mitk::EquiDistantBlocksSorter ::Sort() { DICOMDatasetList remainingInput = GetInput(); // copy typedef std::list OutputListType; OutputListType outputs; m_SliceGroupingResults.clear(); while (!remainingInput.empty()) // repeat until all files are grouped somehow { SliceGroupingAnalysisResult regularBlock = this->AnalyzeFileForITKImageSeriesReaderSpacingAssumption( remainingInput, m_AcceptTilt ); DICOMDatasetList inBlock = regularBlock.GetBlockDatasets(); DICOMDatasetList laterBlock = regularBlock.GetUnsortedDatasets(); MITK_DEBUG << "Result: sorted 3D group with " << inBlock.size() << " files"; for (DICOMDatasetList::const_iterator diter = inBlock.begin(); diter != inBlock.end(); ++diter) MITK_DEBUG << " IN " << (*diter)->GetFilenameIfAvailable(); for (DICOMDatasetList::const_iterator diter = laterBlock.begin(); diter != laterBlock.end(); ++diter) MITK_DEBUG << " OUT " << (*diter)->GetFilenameIfAvailable(); outputs.push_back( regularBlock.GetBlockDatasets() ); m_SliceGroupingResults.push_back( regularBlock ); remainingInput = regularBlock.GetUnsortedDatasets(); } unsigned int numberOfOutputs = outputs.size(); this->SetNumberOfOutputs(numberOfOutputs); unsigned int outputIndex(0); for (OutputListType::iterator oIter = outputs.begin(); oIter != outputs.end(); ++outputIndex, ++oIter) { this->SetOutput(outputIndex, *oIter); } } const mitk::GantryTiltInformation mitk::EquiDistantBlocksSorter ::GetTiltInformation(const std::string& filename) { for (ResultsList::iterator ri = m_SliceGroupingResults.begin(); ri != m_SliceGroupingResults.end(); ++ri) { SliceGroupingAnalysisResult& result = *ri; if (filename == result.GetFirstFilenameOfBlock()) { return result.GetTiltInfo(); } } return GantryTiltInformation(); // empty } std::string mitk::EquiDistantBlocksSorter ::ConstCharStarToString(const char* s) { return s ? std::string(s) : std::string(); } mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult mitk::EquiDistantBlocksSorter ::AnalyzeFileForITKImageSeriesReaderSpacingAssumption( const DICOMDatasetList& datasets, bool groupImagesWithGantryTilt) { // result.first = files that fit ITK's assumption // result.second = files that do not fit, should be run through AnalyzeFileForITKImageSeriesReaderSpacingAssumption() again SliceGroupingAnalysisResult result; // we const_cast here, because I could not use a map.at(), which would make the code much more readable const DICOMTag tagImagePositionPatient = DICOMTag(0x0020,0x0032); // Image Position (Patient) const DICOMTag tagImageOrientation = DICOMTag(0x0020, 0x0037); // Image Orientation const DICOMTag tagGantryTilt = DICOMTag(0x0018, 0x1120); // gantry tilt Vector3D fromFirstToSecondOrigin; fromFirstToSecondOrigin.Fill(0.0); bool fromFirstToSecondOriginInitialized(false); Point3D thisOrigin; thisOrigin.Fill(0.0f); Point3D lastOrigin; lastOrigin.Fill(0.0f); Point3D lastDifferentOrigin; lastDifferentOrigin.Fill(0.0f); bool lastOriginInitialized(false); MITK_DEBUG << "--------------------------------------------------------------------------------"; MITK_DEBUG << "Analyzing " << datasets.size() << " files for z-spacing assumption of ITK's ImageSeriesReader (group tilted: " << groupImagesWithGantryTilt << ")"; unsigned int fileIndex(0); + double toleratedOriginError(0.005); // default: max. 1/10mm error when measurement crosses 20 slices in z direction (too strict? we don't know better) for (DICOMDatasetList::const_iterator dsIter = datasets.begin(); dsIter != datasets.end(); ++dsIter, ++fileIndex) { bool fileFitsIntoPattern(false); std::string thisOriginString; // Read tag value into point3D. PLEASE replace this by appropriate GDCM code if you figure out how to do that thisOriginString = (*dsIter)->GetTagValueAsString( tagImagePositionPatient ); if (thisOriginString.empty()) { // don't let such files be in a common group. Everything without position information will be loaded as a single slice: // with standard DICOM files this can happen to: CR, DX, SC MITK_DEBUG << " ==> Sort away " << *dsIter << " for later analysis (no position information)"; // we already have one occupying this position if ( result.GetBlockDatasets().empty() ) // nothing WITH position information yet { // ==> this is a group of its own, stop processing, come back later result.AddFileToSortedBlock( *dsIter ); DICOMDatasetList remainingFiles; remainingFiles.insert( remainingFiles.end(), dsIter+1, datasets.end() ); result.AddFilesToUnsortedBlock( remainingFiles ); fileFitsIntoPattern = false; break; // no files anymore } else { // ==> this does not match, consider later result.AddFileToUnsortedBlock( *dsIter ); // sort away for further analysis fileFitsIntoPattern = false; continue; // next file } } bool ignoredConversionError(-42); // hard to get here, no graceful way to react thisOrigin = DICOMStringToPoint3D( thisOriginString, ignoredConversionError ); MITK_DEBUG << " " << fileIndex << " " << (*dsIter)->GetFilenameIfAvailable() << " at " /* << thisOriginString */ << "(" << thisOrigin[0] << "," << thisOrigin[1] << "," << thisOrigin[2] << ")"; if ( lastOriginInitialized && (thisOrigin == lastOrigin) ) { MITK_DEBUG << " ==> Sort away " << *dsIter << " for separate time step"; // we already have one occupying this position result.AddFileToUnsortedBlock( *dsIter ); // sort away for further analysis fileFitsIntoPattern = false; } else { if (!fromFirstToSecondOriginInitialized && lastOriginInitialized) // calculate vector as soon as possible when we get a new position { fromFirstToSecondOrigin = thisOrigin - lastDifferentOrigin; fromFirstToSecondOriginInitialized = true; + MITK_DEBUG << "Distance of two slices: " << fromFirstToSecondOrigin.GetNorm() << "mm"; + toleratedOriginError = + fromFirstToSecondOrigin.GetNorm() * 0.3; // a third of the slice distance + // (less than half, which would mean that a slice is displayed where another slice should actually be) + MITK_DEBUG << "Accepting errors in actual versus expected origin up to " << toleratedOriginError << "mm"; + // Here we calculate if this slice and the previous one are well aligned, // i.e. we test if the previous origin is on a line through the current // origin, directed into the normal direction of the current slice. // If this is NOT the case, then we have a data set with a TILTED GANTRY geometry, // which cannot be simply loaded into a single mitk::Image at the moment. // For this case, we flag this finding in the result and DicomSeriesReader // can correct for that later. Vector3D right; right.Fill(0.0); Vector3D up; right.Fill(0.0); // might be down as well, but it is just a name at this point std::string orientationValue = (*dsIter)->GetTagValueAsString( tagImageOrientation ); DICOMStringToOrientationVectors( orientationValue, right, up, ignoredConversionError ); GantryTiltInformation tiltInfo( lastDifferentOrigin, thisOrigin, right, up, 1 ); if ( tiltInfo.IsSheared() ) // mitk::eps is too small; 1/1000 of a mm should be enough to detect tilt { /* optimistic approach, accepting gantry tilt: save file for later, check all further files */ // at this point we have TWO slices analyzed! if they are the only two files, we still split, because there is no third to verify our tilting assumption. // later with a third being available, we must check if the initial tilting vector is still valid. if yes, continue. // if NO, we need to split the already sorted part (result.first) and the currently analyzed file (*dsIter) // tell apart gantry tilt from overall skewedness // sort out irregularly sheared slices, that IS NOT tilting if ( groupImagesWithGantryTilt && tiltInfo.IsRegularGantryTilt() ) { // check if this is at least roughly the same angle as recorded in DICOM tags double angle = 0.0; std::string tiltStr = (*dsIter)->GetTagValueAsString( tagGantryTilt ); std::istringstream i(tiltStr); if (i >> angle) { MITK_DEBUG << "Comparing recorded tilt angle " << angle << " against calculated value " << tiltInfo.GetTiltAngleInDegrees(); // TODO we probably want the signs correct, too (that depends: this is just a rough check, nothing serious) if ( fabs(angle) - tiltInfo.GetTiltAngleInDegrees() > 0.25) { result.AddFileToUnsortedBlock( *dsIter ); // sort away for further analysis fileFitsIntoPattern = false; } else // tilt angle from header is less than 0.25 degrees different from what we calculated, assume this is fine { assert(!datasets.empty()); result.FlagGantryTilt(tiltInfo); result.AddFileToSortedBlock( *dsIter ); // this file is good for current block result.SetFirstFilenameOfBlock( datasets.front()->GetFilenameIfAvailable() ); fileFitsIntoPattern = true; } } else // we cannot check the calculated tilt angle against the one from the dicom header (so we assume we are right) { assert(!datasets.empty()); result.FlagGantryTilt(tiltInfo); result.AddFileToSortedBlock( *dsIter ); // this file is good for current block result.SetFirstFilenameOfBlock( datasets.front()->GetFilenameIfAvailable() ); fileFitsIntoPattern = true; } } else // caller does not want tilt compensation OR shearing is more complicated than tilt { result.AddFileToUnsortedBlock( *dsIter ); // sort away for further analysis fileFitsIntoPattern = false; } } else // not sheared { result.AddFileToSortedBlock( *dsIter ); // this file is good for current block fileFitsIntoPattern = true; } } else if (fromFirstToSecondOriginInitialized) // we already know the offset between slices { Point3D assumedOrigin = lastDifferentOrigin + fromFirstToSecondOrigin; Vector3D originError = assumedOrigin - thisOrigin; double norm = originError.GetNorm(); - double toleratedError(0.005); // max. 1/10mm error when measurement crosses 20 slices in z direction - if (norm > toleratedError) + if (norm > toleratedOriginError) { MITK_DEBUG << " File does not fit into the inter-slice distance pattern (diff = " << norm << ", allowed " - << toleratedError << ")."; + << toleratedOriginError << ")."; MITK_DEBUG << " Expected position (" << assumedOrigin[0] << "," << assumedOrigin[1] << "," << assumedOrigin[2] << "), got position (" << thisOrigin[0] << "," << thisOrigin[1] << "," << thisOrigin[2] << ")"; MITK_DEBUG << " ==> Sort away " << *dsIter << " for later analysis"; // At this point we know we deviated from the expectation of ITK's ImageSeriesReader // We split the input file list at this point, i.e. all files up to this one (excluding it) // are returned as group 1, the remaining files (including the faulty one) are group 2 /* Optimistic approach: check if any of the remaining slices fits in */ result.AddFileToUnsortedBlock( *dsIter ); // sort away for further analysis fileFitsIntoPattern = false; } else { result.AddFileToSortedBlock( *dsIter ); // this file is good for current block fileFitsIntoPattern = true; } } else // this should be the very first slice { result.AddFileToSortedBlock( *dsIter ); // this file is good for current block fileFitsIntoPattern = true; } } // record current origin for reference in later iterations if ( !lastOriginInitialized || ( fileFitsIntoPattern && (thisOrigin != lastOrigin) ) ) { lastDifferentOrigin = thisOrigin; } lastOrigin = thisOrigin; lastOriginInitialized = true; } if ( result.ContainsGantryTilt() ) { // check here how many files were grouped. // IF it was only two files AND we assume tiltedness (e.g. save "distance") // THEN we would want to also split the two previous files (simple) because // we don't have any reason to assume they belong together if ( result.GetBlockDatasets().size() == 2 ) { result.UndoPrematureGrouping(); } } // update tilt info to get maximum precision // earlier, tilt was only calculated from first and second slice. // now that we know the whole range, we can re-calculate using the very first and last slice if ( result.ContainsGantryTilt() && result.GetBlockDatasets().size() > 1 ) { DICOMDatasetList datasets = result.GetBlockDatasets(); DICOMDatasetAccess* firstDataset = datasets.front(); DICOMDatasetAccess* lastDataset = datasets.back(); unsigned int numberOfSlicesApart = datasets.size() - 1; Vector3D right; right.Fill(0.0); Vector3D up; right.Fill(0.0); // might be down as well, but it is just a name at this point std::string orientationValue = firstDataset->GetTagValueAsString( tagImageOrientation ); bool orientationConversion(false); DICOMStringToOrientationVectors( orientationValue, right, up, orientationConversion ); if (orientationConversion) { std::string firstOriginString = firstDataset->GetTagValueAsString( tagImagePositionPatient ); std::string lastOriginString = lastDataset->GetTagValueAsString( tagImagePositionPatient ); if (!firstOriginString.empty() && !lastOriginString.empty()) { bool firstOriginConversion(false); bool lastOriginConversion(false); Point3D firstOrigin = DICOMStringToPoint3D( firstOriginString, firstOriginConversion ); Point3D lastOrigin = DICOMStringToPoint3D( lastOriginString, lastOriginConversion ); if (firstOriginConversion && lastOriginConversion) { GantryTiltInformation updatedTiltInfo( firstOrigin, lastOrigin, right, up, numberOfSlicesApart ); result.FlagGantryTilt(updatedTiltInfo); } } } } return result; } diff --git a/Modules/DICOMReader/mitkEquiDistantBlocksSorter.h b/Modules/DICOMReader/mitkEquiDistantBlocksSorter.h index cc6b2a6ba9..37e9083fbe 100644 --- a/Modules/DICOMReader/mitkEquiDistantBlocksSorter.h +++ b/Modules/DICOMReader/mitkEquiDistantBlocksSorter.h @@ -1,155 +1,180 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #ifndef mitkEquiDistantBlocksSorter_h #define mitkEquiDistantBlocksSorter_h #include "mitkDICOMDatasetSorter.h" #include "mitkDICOMSortCriterion.h" #include "mitkGantryTiltInformation.h" #include "mitkVector.h" #include namespace mitk { /** - \brief Split inputs into blocks of equidant slices. + \ingroup DICOMReaderModule + \brief Split inputs into blocks of equidistant slices (for use in DICOMITKSeriesGDCMReader). + + Since inter-slice distance is not recorded in DICOM tags, we must ensure that blocks are made up of + slices that have equal distances between neighboring slices. This is especially necessary because itk::ImageSeriesReader + is later used for the actual loading, and this class expects (and does nocht verify) equal inter-slice distance (see \ref DICOMITKSeriesGDCMReader_ForcedConfiguration). + + To achieve such grouping, the inter-slice distance is calculated from the first two different slice positions of a block. + Following slices are added to a block as long as they can be added by adding the calculated inter-slice distance to the + last slice of the block. Slices that do not fit into the expected distance pattern, are set aside for further analysis. + This grouping is done until each file has been assigned to a group. + + Slices that share a position in space are also sorted into separate blocks during this step. + So the result of this step is a set of blocks that contain only slices with equal z spacing + and uniqe slices at each position. + + Detailed implementation in AnalyzeFileForITKImageSeriesReaderSpacingAssumption(). - This kind of splitting is used as a check before loading a DICOM series with ITK ImageSeriesReader. */ class DICOMReader_EXPORT EquiDistantBlocksSorter : public DICOMDatasetSorter { public: mitkClassMacro( EquiDistantBlocksSorter, DICOMDatasetSorter ) itkNewMacro( EquiDistantBlocksSorter ) virtual DICOMTagList GetTagsOfInterest(); + /** + \brief Delegates work to AnalyzeFileForITKImageSeriesReaderSpacingAssumption(). + AnalyzeFileForITKImageSeriesReaderSpacingAssumption() is called until it does not + create multiple blocks anymore. + */ virtual void Sort(); + /** + \brief Whether or not to accept images from a tilted acquisition in a single output group. + */ void SetAcceptTilt(bool accept); const GantryTiltInformation GetTiltInformation(const std::string& filename); + virtual void PrintConfiguration(std::ostream& os, const std::string& indent = "") const; + protected: /** - \brief Return type of DicomSeriesReader::AnalyzeFileForITKImageSeriesReaderSpacingAssumption. + \brief Return type of AnalyzeFileForITKImageSeriesReaderSpacingAssumption(). - Class contains the grouping result of method DicomSeriesReader::AnalyzeFileForITKImageSeriesReaderSpacingAssumption, + Class contains the grouping result of method AnalyzeFileForITKImageSeriesReaderSpacingAssumption(), which takes as input a number of images, which are all equally oriented and spatially sorted along their normal direction. The result contains of two blocks: a first one is the grouping result, all of those images can be loaded into one image block because they have an equal origin-to-origin distance without any gaps in-between. */ class SliceGroupingAnalysisResult { public: SliceGroupingAnalysisResult(); /** \brief Grouping result, all same origin-to-origin distance w/o gaps. */ DICOMDatasetList GetBlockDatasets(); void SetFirstFilenameOfBlock(const std::string& filename); std::string GetFirstFilenameOfBlock() const; /** \brief Remaining files, which could not be grouped. */ DICOMDatasetList GetUnsortedDatasets(); /** \brief Wheter or not the grouped result contain a gantry tilt. */ bool ContainsGantryTilt(); /** \brief Detailed description of gantry tilt. */ const GantryTiltInformation& GetTiltInfo() const; /** \brief Meant for internal use by AnalyzeFileForITKImageSeriesReaderSpacingAssumption only. */ void AddFileToSortedBlock(DICOMDatasetAccess* dataset); /** \brief Meant for internal use by AnalyzeFileForITKImageSeriesReaderSpacingAssumption only. */ void AddFileToUnsortedBlock(DICOMDatasetAccess* dataset); void AddFilesToUnsortedBlock(const DICOMDatasetList& datasets); /** \brief Meant for internal use by AnalyzeFileForITKImageSeriesReaderSpacingAssumption only. \todo Could make sense to enhance this with an instance of GantryTiltInformation to store the whole result! */ void FlagGantryTilt(const GantryTiltInformation& tiltInfo); /** \brief Only meaningful for use by AnalyzeFileForITKImageSeriesReaderSpacingAssumption. */ void UndoPrematureGrouping(); protected: DICOMDatasetList m_GroupedFiles; DICOMDatasetList m_UnsortedFiles; GantryTiltInformation m_TiltInfo; std::string m_FirstFilenameOfBlock; }; /** \brief Ensure an equal z-spacing for a group of files. Takes as input a number of images, which are all equally oriented and spatially sorted along their normal direction. Internally used by GetSeries. Returns two lists: the first one contins slices of equal inter-slice spacing. The second list contains remaining files, which need to be run through AnalyzeFileForITKImageSeriesReaderSpacingAssumption again. Relevant code that is matched here is in itkImageSeriesReader.txx (ImageSeriesReader::GenerateOutputInformation(void)), lines 176 to 245 (as of ITK 3.20) */ SliceGroupingAnalysisResult AnalyzeFileForITKImageSeriesReaderSpacingAssumption(const DICOMDatasetList& files, bool groupsOfSimilarImages); /** \brief Safely convert const char* to std::string. */ std::string ConstCharStarToString(const char* s); EquiDistantBlocksSorter(); virtual ~EquiDistantBlocksSorter(); EquiDistantBlocksSorter(const EquiDistantBlocksSorter& other); EquiDistantBlocksSorter& operator=(const EquiDistantBlocksSorter& other); bool m_AcceptTilt; typedef std::vector ResultsList; ResultsList m_SliceGroupingResults; }; } #endif diff --git a/Modules/DICOMReader/mitkGantryTiltInformation.h b/Modules/DICOMReader/mitkGantryTiltInformation.h index 00a23bdd40..425818cc4e 100644 --- a/Modules/DICOMReader/mitkGantryTiltInformation.h +++ b/Modules/DICOMReader/mitkGantryTiltInformation.h @@ -1,129 +1,133 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #ifndef mitkGantryTiltInformation_h #define mitkGantryTiltInformation_h #include "mitkVector.h" namespace mitk { /** + \ingroup DICOMReaderModule \brief Gantry tilt analysis result. Takes geometry information for two slices of a DICOM series and calculates whether these fit into an orthogonal block or not. If NOT, they can either be the result of an acquisition with gantry tilt OR completly broken by some shearing transformation. Most calculations are done in the constructor, results can then be read via the remaining methods. + + This class is a helper to DICOMITKSeriesGDCMReader and can + not be used outside of \ref DICOMReaderModule */ class GantryTiltInformation { public: // two types to avoid any rounding errors typedef itk::Point Point3Dd; typedef itk::Vector Vector3Dd; /** \brief Just so we can create empty instances for assigning results later. */ GantryTiltInformation(); void Print(std::ostream& os) const; /** \brief THE constructor, which does all the calculations. Determining the amount of tilt is done by checking the distances of origin1 from planes through origin2. Two planes are considered: - normal vector along normal of slices (right x up): gives the slice distance - normal vector along orientation vector "up": gives the shift parallel to the plane orientation The tilt angle can then be calculated from these distances \param origin1 origin of the first slice \param origin2 origin of the second slice \param right right/up describe the orientatation of borth slices \param up right/up describe the orientatation of borth slices \param numberOfSlicesApart how many slices are the given origins apart (1 for neighboring slices) */ GantryTiltInformation( const Point3D& origin1, const Point3D& origin2, const Vector3D& right, const Vector3D& up, unsigned int numberOfSlicesApart); /** \brief Whether the slices were sheared. True if any of the shifts along right or up vector are non-zero. */ bool IsSheared() const; /** \brief Whether the shearing is a gantry tilt or more complicated. Gantry tilt will only produce shifts in ONE orientation, not in both. Since the correction code currently only coveres one tilt direction AND we don't know of medical images with two tilt directions, the loading code wants to check if our assumptions are true. */ bool IsRegularGantryTilt() const; /** \brief The offset distance in Y direction for each slice in mm (describes the tilt result). */ double GetMatrixCoefficientForCorrectionInWorldCoordinates() const; /** \brief The z / inter-slice spacing. Needed to correct ImageSeriesReader's result. */ double GetRealZSpacing() const; /** \brief The shift between first and last slice in mm. Needed to resize an orthogonal image volume. */ double GetTiltCorrectedAdditionalSize(unsigned int imageSizeZ) const; /** \brief Calculated tilt angle in degrees. */ double GetTiltAngleInDegrees() const; - protected: + private: /** \brief Projection of point p onto line through lineOrigin in direction of lineDirection. */ Point3D projectPointOnLine( Point3Dd p, Point3Dd lineOrigin, Vector3Dd lineDirection ); double m_ShiftUp; double m_ShiftRight; double m_ShiftNormal; double m_ITKAssumedSliceSpacing; unsigned int m_NumberOfSlicesApart; }; } // namespace #endif diff --git a/Modules/DICOMReader/mitkSortByImagePositionPatient.cpp b/Modules/DICOMReader/mitkSortByImagePositionPatient.cpp index 5b24cd2911..bea8ff52cf 100644 --- a/Modules/DICOMReader/mitkSortByImagePositionPatient.cpp +++ b/Modules/DICOMReader/mitkSortByImagePositionPatient.cpp @@ -1,128 +1,135 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #include "mitkSortByImagePositionPatient.h" #include "mitkDICOMTag.h" mitk::SortByImagePositionPatient ::SortByImagePositionPatient(DICOMSortCriterion::Pointer secondaryCriterion) :DICOMSortCriterion(secondaryCriterion) { } mitk::SortByImagePositionPatient ::~SortByImagePositionPatient() { } mitk::SortByImagePositionPatient ::SortByImagePositionPatient(const SortByImagePositionPatient& other ) :DICOMSortCriterion(other) { } mitk::SortByImagePositionPatient& mitk::SortByImagePositionPatient ::operator=(const SortByImagePositionPatient& other) { if (this != &other) { DICOMSortCriterion::operator=(other); } return *this; } +void +mitk::SortByImagePositionPatient +::Print(std::ostream& os) const +{ + os << "(0020,0032) Image Position (Patient) along normal of (0020,0037) Image Orientation (Patient)"; +} + mitk::DICOMTagList mitk::SortByImagePositionPatient ::GetTagsOfInterest() const { DICOMTagList tags; tags.push_back( DICOMTag(0x0020, 0x0032) ); // ImagePositionPatient tags.push_back( DICOMTag(0x0020, 0x0037) ); // ImageOrientationPatient return tags; } bool mitk::SortByImagePositionPatient ::IsLeftBeforeRight(const mitk::DICOMDatasetAccess* left, const mitk::DICOMDatasetAccess* right) const { // sort by distance to world origin, assuming (almost) equal orientation static const DICOMTag tagImagePositionPatient = DICOMTag(0x0020,0x0032); // Image Position (Patient) static const DICOMTag tagImageOrientation = DICOMTag(0x0020, 0x0037); // Image Orientation static Vector3D leftRight; leftRight.Fill(0.0); static Vector3D leftUp; leftUp.Fill(0.0); static bool leftHasOrientation(false); DICOMStringToOrientationVectors( left->GetTagValueAsString( tagImageOrientation ), leftRight, leftUp, leftHasOrientation ); static Vector3D rightRight; rightRight.Fill(0.0); static Vector3D rightUp; rightUp.Fill(0.0); static bool rightHasOrientation(false); DICOMStringToOrientationVectors( right->GetTagValueAsString( tagImageOrientation ), rightRight, rightUp, rightHasOrientation ); static Point3D leftOrigin; leftOrigin.Fill(0.0f); static bool leftHasOrigin(false); leftOrigin = DICOMStringToPoint3D( left->GetTagValueAsString( tagImagePositionPatient ), leftHasOrigin ); static Point3D rightOrigin; rightOrigin.Fill(0.0f); static bool rightHasOrigin(false); rightOrigin = DICOMStringToPoint3D( right->GetTagValueAsString( tagImagePositionPatient ), rightHasOrigin ); // we tolerate very small differences in image orientation, since we got to know about // acquisitions where these values change across a single series (7th decimal digit) // (http://bugs.mitk.org/show_bug.cgi?id=12263) // still, we want to check if our assumption of 'almost equal' orientations is valid for (unsigned int dim = 0; dim < 3; ++dim) { if ( fabs(leftRight[dim] - rightRight[dim]) > 0.0001 || fabs(leftUp[dim] - rightUp[dim]) > 0.0001) { MITK_ERROR << "Dicom images have different orientations."; throw std::logic_error("Dicom images have different orientations. Call GetSeries() first to separate images."); } } static Vector3D normal; normal[0] = leftRight[1] * leftUp[2] - leftRight[2] * leftUp[1]; normal[1] = leftRight[2] * leftUp[0] - leftRight[0] * leftUp[2]; normal[2] = leftRight[0] * leftUp[1] - leftRight[1] * leftUp[0]; static double leftDistance = 0.0; static double rightDistance = 0.0; leftDistance = 0.0; rightDistance = 0.0; // this computes the distance from world origin (0,0,0) ALONG THE NORMAL of the image planes for (unsigned int dim = 0; dim < 3; ++dim) { leftDistance += normal[dim] * leftOrigin[dim]; rightDistance += normal[dim] * rightOrigin[dim]; } // if we can sort by just comparing the distance, we do exactly that if ( fabs(leftDistance - rightDistance) >= mitk::eps) { // default: compare position return leftDistance < rightDistance; } else { return this->NextLevelIsLeftBeforeRight(left, right); } } diff --git a/Modules/DICOMReader/mitkSortByImagePositionPatient.h b/Modules/DICOMReader/mitkSortByImagePositionPatient.h index 510bb56ea3..950196a184 100644 --- a/Modules/DICOMReader/mitkSortByImagePositionPatient.h +++ b/Modules/DICOMReader/mitkSortByImagePositionPatient.h @@ -1,50 +1,62 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #ifndef mitkSortByImagePositionPatient_h #define mitkSortByImagePositionPatient_h #include "mitkDICOMSortCriterion.h" #include "mitkVector.h" namespace mitk { +/** + \ingroup DICOMReaderModule + \brief Sort by distance of image origin along image normal (for use in DICOMTagBasedSorter). + + To compare two datasets, their distance to the world origin is calculated. + This distance is calculated along the image normals because we do not know + the image orientation in advance, to any of the three coordinates could be identical for all datasets. + + \note This class assumes that the datasets have identical orientations! +*/ class DICOMReader_EXPORT SortByImagePositionPatient : public DICOMSortCriterion { public: mitkClassMacro( SortByImagePositionPatient, DICOMSortCriterion ); mitkNewMacro1Param( SortByImagePositionPatient, DICOMSortCriterion::Pointer ); virtual DICOMTagList GetTagsOfInterest() const; virtual bool IsLeftBeforeRight(const mitk::DICOMDatasetAccess* left, const mitk::DICOMDatasetAccess* right) const; + virtual void Print(std::ostream& os) const; + protected: SortByImagePositionPatient( DICOMSortCriterion::Pointer secondaryCriterion = NULL ); virtual ~SortByImagePositionPatient(); SortByImagePositionPatient(const SortByImagePositionPatient& other); SortByImagePositionPatient& operator=(const SortByImagePositionPatient& other); private: }; } #endif diff --git a/Modules/DICOMReader/mitkThreeDnTDICOMSeriesReader.cpp b/Modules/DICOMReader/mitkThreeDnTDICOMSeriesReader.cpp index e5ee67655a..0117b8621c 100644 --- a/Modules/DICOMReader/mitkThreeDnTDICOMSeriesReader.cpp +++ b/Modules/DICOMReader/mitkThreeDnTDICOMSeriesReader.cpp @@ -1,246 +1,246 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #include "mitkThreeDnTDICOMSeriesReader.h" #include "mitkITKDICOMSeriesReaderHelper.h" mitk::ThreeDnTDICOMSeriesReader ::ThreeDnTDICOMSeriesReader() :DICOMITKSeriesGDCMReader() ,m_Group3DandT(true) { } mitk::ThreeDnTDICOMSeriesReader ::ThreeDnTDICOMSeriesReader(const ThreeDnTDICOMSeriesReader& other ) :DICOMITKSeriesGDCMReader(other) ,m_Group3DandT(true) { } mitk::ThreeDnTDICOMSeriesReader ::~ThreeDnTDICOMSeriesReader() { } mitk::ThreeDnTDICOMSeriesReader& mitk::ThreeDnTDICOMSeriesReader ::operator=(const ThreeDnTDICOMSeriesReader& other) { if (this != &other) { DICOMITKSeriesGDCMReader::operator=(other); this->m_Group3DandT = other.m_Group3DandT; } return *this; } void mitk::ThreeDnTDICOMSeriesReader ::SetGroup3DandT(bool on) { m_Group3DandT = on; } mitk::DICOMITKSeriesGDCMReader::SortingBlockList mitk::ThreeDnTDICOMSeriesReader ::Condense3DBlocks(SortingBlockList& resultOf3DGrouping) { if (!m_Group3DandT) { return resultOf3DGrouping; // don't work if nobody asks us to } SortingBlockList remainingBlocks = resultOf3DGrouping; SortingBlockList non3DnTBlocks; SortingBlockList true3DnTBlocks; std::vector true3DnTBlocksTimeStepCount; // TODO we should provide this tag as needed via a function // (however, we currently know that the superclass will use this tag) const DICOMTag tagImagePositionPatient(0x0020, 0x0032); while (!remainingBlocks.empty()) { // new block to fill up DICOMGDCMImageFrameList& firstBlock = remainingBlocks.front(); DICOMGDCMImageFrameList current3DnTBlock = firstBlock; int current3DnTBlockNumberOfTimeSteps = 1; // get block characteristics of first block unsigned int currentBlockNumberOfSlices = firstBlock.size(); std::string currentBlockFirstOrigin = firstBlock.front()->GetTagValueAsString( tagImagePositionPatient ); std::string currentBlockLastOrigin = firstBlock.back()->GetTagValueAsString( tagImagePositionPatient ); remainingBlocks.pop_front(); // compare all other blocks against the first one for (SortingBlockList::iterator otherBlockIter = remainingBlocks.begin(); otherBlockIter != remainingBlocks.end(); /*++otherBlockIter*/) // <-- inside loop { // get block characteristics from first block DICOMGDCMImageFrameList& otherBlock = *otherBlockIter; unsigned int otherBlockNumberOfSlices = otherBlock.size(); std::string otherBlockFirstOrigin = otherBlock.front()->GetTagValueAsString( tagImagePositionPatient ); std::string otherBlockLastOrigin = otherBlock.back()->GetTagValueAsString( tagImagePositionPatient ); // add matching blocks to current3DnTBlock // keep other blocks for later if ( otherBlockNumberOfSlices == currentBlockNumberOfSlices && otherBlockFirstOrigin == currentBlockFirstOrigin && otherBlockLastOrigin == currentBlockLastOrigin ) { // matching block ++current3DnTBlockNumberOfTimeSteps; current3DnTBlock.insert( current3DnTBlock.end(), otherBlock.begin(), otherBlock.end() ); // append // remove this block from remainingBlocks otherBlockIter = remainingBlocks.erase(otherBlockIter); // make sure iterator otherBlockIter is valid afterwards } else { ++otherBlockIter; } } // in any case, we now now all about the first block of our list ... // ... and we wither call it 3D o 3D+t if (current3DnTBlockNumberOfTimeSteps > 1) { true3DnTBlocks.push_back(current3DnTBlock); true3DnTBlocksTimeStepCount.push_back(current3DnTBlockNumberOfTimeSteps); } else { non3DnTBlocks.push_back(current3DnTBlock); } } // create output for real 3D+t blocks (other outputs will be created by superclass) // set 3D+t flag on output block this->SetNumberOfOutputs( true3DnTBlocks.size() ); unsigned int o = 0; for (SortingBlockList::iterator blockIter = true3DnTBlocks.begin(); blockIter != true3DnTBlocks.end(); ++o, ++blockIter) { DICOMGDCMImageFrameList& gdcmFrameInfoList = *blockIter; DICOMImageFrameList frameList = ToDICOMImageFrameList( gdcmFrameInfoList ); assert(!gdcmFrameInfoList.empty()); assert(!frameList.empty()); DICOMImageBlockDescriptor block; block.SetTagCache( this ); // important: this must be before SetImageFrameList(), because SetImageFrameList will trigger reading of lots of interesting tags! block.SetImageFrameList( frameList ); // bad copy&paste code, should be handled in a better way const GantryTiltInformation& tiltInfo = m_EquiDistantBlocksSorter->GetTiltInformation( (gdcmFrameInfoList.front())->GetFilenameIfAvailable() ); block.SetTiltInformation( tiltInfo ); // assume static const DICOMTag tagPixelSpacing(0x0028,0x0030); static const DICOMTag tagImagerPixelSpacing(0x0018,0x1164); std::string pixelSpacingString = (gdcmFrameInfoList.front())->GetTagValueAsString( tagPixelSpacing ); std::string imagerPixelSpacingString = gdcmFrameInfoList.front()->GetTagValueAsString( tagImagerPixelSpacing ); - block.SetPixelSpacingInformation(pixelSpacingString, imagerPixelSpacingString); + block.SetPixelSpacingTagValues(pixelSpacingString, imagerPixelSpacingString); block.SetFlag("3D+t", true); block.SetIntProperty("timesteps", true3DnTBlocksTimeStepCount[o]); MITK_DEBUG << "Found " << true3DnTBlocksTimeStepCount[o] << " timesteps"; this->SetOutput( o, block ); } return non3DnTBlocks; } bool mitk::ThreeDnTDICOMSeriesReader ::LoadImages() { bool success = true; unsigned int numberOfOutputs = this->GetNumberOfOutputs(); for (unsigned int o = 0; o < numberOfOutputs; ++o) { DICOMImageBlockDescriptor& block = this->InternalGetOutput(o); if (block.GetFlag("3D+t", false)) { success &= this->LoadMitkImageForOutput(o); } else { success &= DICOMITKSeriesGDCMReader::LoadMitkImageForOutput(o); // let superclass handle non-3D+t } } return success; } // TODO why not handle 3D as a special case of 3D+t?? Late insight?.. .. because of ITK VImageDimension? .. could be a parameter bool mitk::ThreeDnTDICOMSeriesReader ::LoadMitkImageForOutput(unsigned int o) { PushLocale(); DICOMImageBlockDescriptor& block = this->InternalGetOutput(o); const DICOMImageFrameList& frames = block.GetImageFrameList(); const GantryTiltInformation tiltInfo = block.GetTiltInformation(); bool hasTilt = block.GetFlag("gantryTilt", false); if (hasTilt) { MITK_DEBUG << "When loading image " << o << ": got tilt info:"; //tiltInfo.Print(std::cout); } else { MITK_DEBUG << "When loading image " << o << ": has NO info."; } int numberOfTimesteps = block.GetIntProperty("timesteps", 1); int numberOfFramesPerTimestep = frames.size() / numberOfTimesteps; assert( int(double((double)frames.size() / (double)numberOfTimesteps )) == numberOfFramesPerTimestep ); // this should hold ITKDICOMSeriesReaderHelper::StringContainerList filenamesPerTimestep; for (int timeStep = 0; timeStepFilename ); } filenamesPerTimestep.push_back( filenamesOfThisTimeStep ); } mitk::ITKDICOMSeriesReaderHelper helper; mitk::Image::Pointer mitkImage = helper.Load3DnT( filenamesPerTimestep, m_FixTiltByShearing && hasTilt, tiltInfo ); // TODO preloaded images, caching..? block.SetMitkImage( mitkImage ); PopLocale(); return true; } diff --git a/Modules/DICOMReader/mitkThreeDnTDICOMSeriesReader.h b/Modules/DICOMReader/mitkThreeDnTDICOMSeriesReader.h index f71e71735a..c10b908a1b 100644 --- a/Modules/DICOMReader/mitkThreeDnTDICOMSeriesReader.h +++ b/Modules/DICOMReader/mitkThreeDnTDICOMSeriesReader.h @@ -1,61 +1,83 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #ifndef mitkThreeDnTDICOMSeriesReader_h #define mitkThreeDnTDICOMSeriesReader_h #include "mitkDICOMITKSeriesGDCMReader.h" #include "DICOMReaderExports.h" namespace mitk { -/* - \brief Extends sorting/grouping to ThreeD+t image blocks. +/** + \ingroup DICOMReader + \brief Extends DICOMITKSeriesGDCMReader by sorting/grouping into 3D+t image blocks. + + This class reuses the DICOMITKSeriesGDCMReader class and adds the option of + grouping 3D blocks at the same spatial position into a single block, which + is loaded as a 3D+t mitk::Image (compare to \ref DICOMITKSeriesGDCMReader_Condensing). + + To group two output blocks into a single 3D+t block, this class tests a number + of requirements that the two blocks must fulfill: + - the origin of the first slice must be identical + - the origin of the last slice must be identical + - the number of slices must be identical + + The output blocks described by DICOMImageBlockDescriptor will contains the following properties: + - \b "3D+t": true if the image is 3D+t + - \b "timesteps": number of timesteps of an image (only defined if "3D+t" is true) */ class DICOMReader_EXPORT ThreeDnTDICOMSeriesReader : public DICOMITKSeriesGDCMReader { public: mitkClassMacro( ThreeDnTDICOMSeriesReader, DICOMITKSeriesGDCMReader ); mitkCloneMacro( ThreeDnTDICOMSeriesReader ); itkNewMacro( ThreeDnTDICOMSeriesReader ); + /// \brief Control whether 3D+t grouping shall actually be attempted. void SetGroup3DandT(bool on); // void AllocateOutputImages(); + /// \brief Load via multiple calls to itk::ImageSeriesReader. virtual bool LoadImages(); protected: ThreeDnTDICOMSeriesReader(); virtual ~ThreeDnTDICOMSeriesReader(); ThreeDnTDICOMSeriesReader(const ThreeDnTDICOMSeriesReader& other); ThreeDnTDICOMSeriesReader& operator=(const ThreeDnTDICOMSeriesReader& other); + /** + \brief Analyze the groups produced by DICOMITKSeriesGDCMReader for 3D+t properties. + This method tests whether some blocks are at the same spatial position and groups + them into single blocks. + */ virtual SortingBlockList Condense3DBlocks(SortingBlockList&); bool LoadMitkImageForOutput(unsigned int o); bool m_Group3DandT; }; } #endif