diff --git a/Modules/DICOMReader/files.cmake b/Modules/DICOMReader/files.cmake index e61be227c3..84de604b4f 100644 --- a/Modules/DICOMReader/files.cmake +++ b/Modules/DICOMReader/files.cmake @@ -1,51 +1,53 @@ set(H_FILES mitkDICOMFileReader.h mitkDICOMImageFrameInfo.h mitkDICOMImageBlockDescriptor.h mitkDICOMGDCMImageFrameInfo.h mitkDICOMITKSeriesGDCMReader.h mitkDICOMDatasetSorter.h mitkDICOMEnums.h mitkDICOMTagBasedSorter.h mitkDICOMSortCriterion.h mitkDICOMSortByTag.h mitkEquiDistantBlocksSorter.h + mitkNormalDirectionConsistencySorter.h mitkSortByImagePositionPatient.h mitkClassicDICOMSeriesReader.h mitkThreeDnTDICOMSeriesReader.h mitkDICOMTag.h mitkDICOMReaderConfigurator.h mitkDICOMFileReaderSelector.h ) set(CPP_FILES mitkDICOMFileReader.cpp mitkDICOMImageBlockDescriptor.cpp mitkDICOMITKSeriesGDCMReader.cpp mitkDICOMDatasetSorter.cpp mitkDICOMTagBasedSorter.cpp mitkDICOMGDCMImageFrameInfo.cpp mitkDICOMImageFrameInfo.cpp mitkDICOMSortCriterion.cpp mitkDICOMSortByTag.cpp mitkITKDICOMSeriesReaderHelper.cpp mitkEquiDistantBlocksSorter.cpp + mitkNormalDirectionConsistencySorter.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/imageposition.xml configurations/3D/instancenumber.xml configurations/3D/slicelocation.xml configurations/3D/imagetime.xml configurations/3DnT/classicreader.xml ) diff --git a/Modules/DICOMReader/mitkDICOMITKSeriesGDCMReader.cpp b/Modules/DICOMReader/mitkDICOMITKSeriesGDCMReader.cpp index c003655fb6..71b162f2dc 100644 --- a/Modules/DICOMReader/mitkDICOMITKSeriesGDCMReader.cpp +++ b/Modules/DICOMReader/mitkDICOMITKSeriesGDCMReader.cpp @@ -1,547 +1,557 @@ /*=================================================================== 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) +,m_Sorter( other.m_Sorter ) // TODO should clone the list items +,m_EquiDistantBlocksSorter( other.m_EquiDistantBlocksSorter->Clone() ) +,m_NormalDirectionConsistencySorter( other.m_NormalDirectionConsistencySorter->Clone() ) { 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(); + this->m_NormalDirectionConsistencySorter = other.m_NormalDirectionConsistencySorter->Clone(); } return *this; } void mitk::DICOMITKSeriesGDCMReader ::SetFixTiltByShearing(bool on) { m_FixTiltByShearing = on; } mitk::DICOMGDCMImageFrameList mitk::DICOMITKSeriesGDCMReader ::FromDICOMDatasetList(const DICOMDatasetList& input) { DICOMGDCMImageFrameList output; output.reserve(input.size()); 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(const DICOMGDCMImageFrameList& input) { DICOMDatasetList output; output.reserve(input.size()); 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(const DICOMGDCMImageFrameList& input) { DICOMImageFrameList output; output.reserve(input.size()); 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() { 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(0x0020,0x0011) ); // series number m_GDCMScanner.AddTag( gdcm::Tag(0x0008,0x103e) ); // series description 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"); unsigned int sorterIndex = 0; for(SorterList::iterator sorterIter = m_Sorter.begin(); sorterIter != m_Sorter.end(); ++sorterIndex, ++sorterIter) { m_SortingResultInProgress = this->InternalExecuteSortingStep(sorterIndex, *sorterIter, m_SortingResultInProgress, &timer); } // a last extra-sorting step: ensure equidistant slices - m_SortingResultInProgress = this->InternalExecuteSortingStep(sorterIndex, m_EquiDistantBlocksSorter.GetPointer(), m_SortingResultInProgress, &timer); + m_SortingResultInProgress = this->InternalExecuteSortingStep(sorterIndex++, m_EquiDistantBlocksSorter.GetPointer(), m_SortingResultInProgress, &timer); + m_SortingResultInProgress = this->InternalExecuteSortingStep(sorterIndex, m_NormalDirectionConsistencySorter.GetPointer(), m_SortingResultInProgress, &timer); 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.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 sopClassUID) const { if (sopClassUID.empty()) { return SOPClassUnknown; } gdcm::UIDs uidKnowledge; 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 // 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! 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 ); + + if (m_NormalDirectionConsistencySorter.IsNull()) + { + m_NormalDirectionConsistencySorter = mitk::NormalDirectionConsistencySorter::New(); + } } 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 c24187421b..d61bc7ae4f 100644 --- a/Modules/DICOMReader/mitkDICOMITKSeriesGDCMReader.h +++ b/Modules/DICOMReader/mitkDICOMITKSeriesGDCMReader.h @@ -1,320 +1,327 @@ /*=================================================================== 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 "mitkNormalDirectionConsistencySorter.h" + +#include "mitkITKDICOMSeriesReaderHelper.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(). + 2. As are two forced \b last steps: + 1. 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(). + 2. There is always an instance of NormalDirectionConsistencySorter, + which makes the order of images go along the image normals (see NormalDirectionConsistencySorter) \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); /// \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; /** \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; /// \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: 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; + mitk::NormalDirectionConsistencySorter::Pointer m_NormalDirectionConsistencySorter; private: std::stack m_ReplacedCLocales; std::stack m_ReplacedCinLocales; DICOMGDCMImageFrameList m_InputFrameList; gdcm::Scanner m_GDCMScanner; }; } #endif diff --git a/Modules/DICOMReader/mitkEquiDistantBlocksSorter.cpp b/Modules/DICOMReader/mitkEquiDistantBlocksSorter.cpp index 2e1fb66c41..3288ebe75f 100644 --- a/Modules/DICOMReader/mitkEquiDistantBlocksSorter.cpp +++ b/Modules/DICOMReader/mitkEquiDistantBlocksSorter.cpp @@ -1,500 +1,501 @@ /*=================================================================== 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); + m_AcceptTilt = other.m_AcceptTilt; } 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(); if (norm > toleratedOriginError) { MITK_DEBUG << " File does not fit into the inter-slice distance pattern (diff = " << norm << ", allowed " << 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/mitkITKDICOMSeriesReaderHelper.txx b/Modules/DICOMReader/mitkITKDICOMSeriesReaderHelper.txx index 898060e373..d1a1d28790 100644 --- a/Modules/DICOMReader/mitkITKDICOMSeriesReaderHelper.txx +++ b/Modules/DICOMReader/mitkITKDICOMSeriesReaderHelper.txx @@ -1,297 +1,306 @@ /*=================================================================== 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 "mitkITKDICOMSeriesReaderHelper.h" #include #include //#include //#include //#include template mitk::Image::Pointer mitk::ITKDICOMSeriesReaderHelper ::LoadDICOMByITK( const StringContainer& filenames, bool correctTilt, const GantryTiltInformation& tiltInfo, itk::GDCMImageIO::Pointer& io, Image::Pointer preLoadedImageBlock ) { /******** Normal Case, 3D (also for GDCM < 2 usable) ***************/ mitk::Image::Pointer image = mitk::Image::New(); typedef itk::Image ImageType; typedef itk::ImageSeriesReader ReaderType; io = itk::GDCMImageIO::New(); typename ReaderType::Pointer reader = ReaderType::New(); reader->SetImageIO(io); - reader->ReverseOrderOff(); // TODO check this.. + reader->ReverseOrderOff(); // at this point we require an order of input images so that + // the direction between the origin of the first and the last slice + // is the same direction as the image normals! Otherwise we might + // see images upside down. Unclear whether this is a bug in MITK, + // see NormalDirectionConsistencySorter. if (preLoadedImageBlock.IsNull()) { reader->SetFileNames(filenames); reader->Update(); typename ImageType::Pointer readVolume = reader->GetOutput(); // if we detected that the images are from a tilted gantry acquisition, we need to push some pixels into the right position if (correctTilt) { readVolume = FixUpTiltedGeometry( reader->GetOutput(), tiltInfo ); } image->InitializeByItk(readVolume.GetPointer()); image->SetImportVolume(readVolume->GetBufferPointer()); } else { image = preLoadedImageBlock; StringContainer fakeList; fakeList.push_back( filenames.front() ); reader->SetFileNames( fakeList ); // we always need to load at least one file to get the MetaDataDictionary reader->Update(); } MITK_DEBUG << "Volume dimension: [" << image->GetDimension(0) << ", " << image->GetDimension(1) << ", " << image->GetDimension(2) << "]"; MITK_DEBUG << "Volume spacing: [" << image->GetGeometry()->GetSpacing()[0] << ", " << image->GetGeometry()->GetSpacing()[1] << ", " << image->GetGeometry()->GetSpacing()[2] << "]"; return image; } #define MITK_DEBUG_OUTPUT_FILELIST(list)\ MITK_DEBUG << "-------------------------------------------"; \ for (StringContainer::const_iterator _iter = (list).begin(); _iter!=(list).end(); ++_iter) \ { \ MITK_DEBUG <<" file '" << *_iter<< "'"; \ } \ MITK_DEBUG << "-------------------------------------------"; template mitk::Image::Pointer mitk::ITKDICOMSeriesReaderHelper ::LoadDICOMByITK3DnT( const StringContainerList& filenamesForTimeSteps, bool correctTilt, const GantryTiltInformation& tiltInfo, itk::GDCMImageIO::Pointer& io, Image::Pointer preLoadedImageBlock ) { unsigned int numberOfTimeSteps = filenamesForTimeSteps.size(); mitk::Image::Pointer image = mitk::Image::New(); typedef itk::Image ImageType; typedef itk::ImageSeriesReader ReaderType; io = itk::GDCMImageIO::New(); typename ReaderType::Pointer reader = ReaderType::New(); reader->SetImageIO(io); - reader->ReverseOrderOff(); + reader->ReverseOrderOff(); // at this point we require an order of input images so that + // the direction between the origin of the first and the last slice + // is the same direction as the image normals! Otherwise we might + // see images upside down. Unclear whether this is a bug in MITK, + // see NormalDirectionConsistencySorter. + if (preLoadedImageBlock.IsNull()) { unsigned int currentTimeStep = 0; MITK_DEBUG << "Start loading timestep " << currentTimeStep; MITK_DEBUG_OUTPUT_FILELIST( filenamesForTimeSteps.front() ) reader->SetFileNames(filenamesForTimeSteps.front()); reader->Update(); typename ImageType::Pointer readVolume = reader->GetOutput(); // if we detected that the images are from a tilted gantry acquisition, we need to push some pixels into the right position if (correctTilt) { readVolume = FixUpTiltedGeometry( reader->GetOutput(), tiltInfo ); } image->InitializeByItk(readVolume.GetPointer(), 1, numberOfTimeSteps); image->SetImportVolume(readVolume->GetBufferPointer(), currentTimeStep++); // timestep 0 // for other time-steps for (StringContainerList::const_iterator timestepsIter = ++(filenamesForTimeSteps.begin()); // start with SECOND entry timestepsIter != filenamesForTimeSteps.end(); ++currentTimeStep, ++timestepsIter) { MITK_DEBUG << "Start loading timestep " << currentTimeStep; MITK_DEBUG_OUTPUT_FILELIST( *timestepsIter ) reader->SetFileNames(*timestepsIter); reader->Update(); readVolume = reader->GetOutput(); if (correctTilt) { readVolume = FixUpTiltedGeometry( reader->GetOutput(), tiltInfo ); } image->SetImportVolume(readVolume->GetBufferPointer(), currentTimeStep); } } else { // TODO check and fix image = preLoadedImageBlock; StringContainer fakeList; fakeList.push_back( filenamesForTimeSteps.front().front() ); reader->SetFileNames( fakeList ); // we always need to load at least one file to get the MetaDataDictionary reader->Update(); } MITK_DEBUG << "Volume dimension: [" << image->GetDimension(0) << ", " << image->GetDimension(1) << ", " << image->GetDimension(2) << "]"; MITK_DEBUG << "Volume spacing: [" << image->GetGeometry()->GetSpacing()[0] << ", " << image->GetGeometry()->GetSpacing()[1] << ", " << image->GetGeometry()->GetSpacing()[2] << "]"; return image; } template typename ImageType::Pointer mitk::ITKDICOMSeriesReaderHelper ::FixUpTiltedGeometry( ImageType* input, const GantryTiltInformation& tiltInfo ) { typedef itk::ResampleImageFilter ResampleFilterType; typename ResampleFilterType::Pointer resampler = ResampleFilterType::New(); resampler->SetInput( input ); /* Transform for a point is - transform from actual position to index coordinates - apply a shear that undoes the gantry tilt - transform back into world coordinates Anybody who does this in a simpler way: don't forget to write up how and why your solution works */ typedef itk::ScalableAffineTransform< double, ImageType::ImageDimension > TransformType; typename TransformType::Pointer transformShear = TransformType::New(); /** - apply a shear and spacing correction to the image block that corrects the ITK reader's error - ITK ignores the shear and loads slices into an orthogonal volume - ITK calculates the spacing from the origin distance, which is more than the actual spacing with gantry tilt images - to undo the effect - we have calculated some information in tiltInfo: - the shift in Y direction that is added with each additional slice is the most important information - the Y-shift is calculated in mm world coordinates - we apply a shearing transformation to the ITK-read image volume - to do this locally, - we transform the image volume back to origin and "normal" orientation by applying the inverse of its transform (this brings us into the image's "index coordinate" system) - we apply a shear with the Y-shift factor put into a unit transform at row 1, col 2 - we transform the image volume back to its actual position (from index to world coordinates) - we lastly apply modify the image spacing in z direction by replacing this number with the correctly calulcated inter-slice distance */ ScalarType factor = tiltInfo.GetMatrixCoefficientForCorrectionInWorldCoordinates() / input->GetSpacing()[1]; // row 1, column 2 corrects shear in parallel to Y axis, proportional to distance in Z direction transformShear->Shear( 1, 2, factor ); typename TransformType::Pointer imageIndexToWorld = TransformType::New(); imageIndexToWorld->SetOffset( input->GetOrigin().GetVectorFromOrigin() ); typename TransformType::MatrixType indexToWorldMatrix; indexToWorldMatrix = input->GetDirection(); typename ImageType::DirectionType scale; for ( unsigned int i = 0; i < ImageType::ImageDimension; i++ ) { scale[i][i] = input->GetSpacing()[i]; } indexToWorldMatrix *= scale; imageIndexToWorld->SetMatrix( indexToWorldMatrix ); typename TransformType::Pointer imageWorldToIndex = TransformType::New(); imageIndexToWorld->GetInverse( imageWorldToIndex ); typename TransformType::Pointer gantryTiltCorrection = TransformType::New(); gantryTiltCorrection->Compose( imageWorldToIndex ); gantryTiltCorrection->Compose( transformShear ); gantryTiltCorrection->Compose( imageIndexToWorld ); resampler->SetTransform( gantryTiltCorrection ); typedef itk::LinearInterpolateImageFunction< ImageType, double > InterpolatorType; typename InterpolatorType::Pointer interpolator = InterpolatorType::New(); resampler->SetInterpolator( interpolator ); /* This would be the right place to invent a meaningful value for positions outside of the image. For CT, HU -1000 might be meaningful, but a general solution seems not possible. Even for CT, -1000 would only look natural for many not all images. */ // TODO use (0028,0120) Pixel Padding Value if present resampler->SetDefaultPixelValue( itk::NumericTraits< typename ImageType::PixelType >::min() ); // adjust size in Y direction! (maybe just transform the outer last pixel to see how much space we would need resampler->SetOutputParametersFromImage( input ); // we basically need the same image again, just sheared // if tilt positive, then we need additional pixels BELOW origin, otherwise we need pixels behind the end of the block // in any case we need more size to accomodate shifted slices typename ImageType::SizeType largerSize = resampler->GetSize(); // now the resampler already holds the input image's size. double imageSizeZ = largerSize[2]; MITK_DEBUG <<"Calculate lager size = " << largerSize[1] << " + " << tiltInfo.GetTiltCorrectedAdditionalSize(imageSizeZ) << " / " << input->GetSpacing()[1] << "+ 2.0"; largerSize[1] += static_cast(tiltInfo.GetTiltCorrectedAdditionalSize(imageSizeZ) / input->GetSpacing()[1]+ 2.0); resampler->SetSize( largerSize ); MITK_DEBUG << "Fix Y size of image w/ spacing " << input->GetSpacing()[1] << " from " << input->GetLargestPossibleRegion().GetSize()[1] << " to " << largerSize[1]; // in SOME cases this additional size is below/behind origin if ( tiltInfo.GetMatrixCoefficientForCorrectionInWorldCoordinates() > 0.0 ) { typename ImageType::DirectionType imageDirection = input->GetDirection(); Vector3D yDirection; yDirection[0] = imageDirection[0][1]; yDirection[1] = imageDirection[1][1]; yDirection[2] = imageDirection[2][1]; yDirection.Normalize(); typename ImageType::PointType shiftedOrigin; shiftedOrigin = input->GetOrigin(); // add some pixels to make everything fit shiftedOrigin[0] -= yDirection[0] * (tiltInfo.GetTiltCorrectedAdditionalSize(imageSizeZ) + 1.0 * input->GetSpacing()[1]); shiftedOrigin[1] -= yDirection[1] * (tiltInfo.GetTiltCorrectedAdditionalSize(imageSizeZ) + 1.0 * input->GetSpacing()[1]); shiftedOrigin[2] -= yDirection[2] * (tiltInfo.GetTiltCorrectedAdditionalSize(imageSizeZ) + 1.0 * input->GetSpacing()[1]); resampler->SetOutputOrigin( shiftedOrigin ); } resampler->Update(); typename ImageType::Pointer result = resampler->GetOutput(); // ImageSeriesReader calculates z spacing as the distance between the first two origins. // This is not correct in case of gantry tilt, so we set our calculated spacing. typename ImageType::SpacingType correctedSpacing = result->GetSpacing(); correctedSpacing[2] = tiltInfo.GetRealZSpacing(); result->SetSpacing( correctedSpacing ); return result; } diff --git a/Modules/DICOMReader/mitkNormalDirectionConsistencySorter.cpp b/Modules/DICOMReader/mitkNormalDirectionConsistencySorter.cpp new file mode 100644 index 0000000000..9507acbada --- /dev/null +++ b/Modules/DICOMReader/mitkNormalDirectionConsistencySorter.cpp @@ -0,0 +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. + +===================================================================*/ + +//#define MBILOG_ENABLE_DEBUG + +#include "mitkNormalDirectionConsistencySorter.h" + +#include + +mitk::NormalDirectionConsistencySorter +::NormalDirectionConsistencySorter() +:DICOMDatasetSorter() +{ +} + +mitk::NormalDirectionConsistencySorter +::NormalDirectionConsistencySorter(const NormalDirectionConsistencySorter& other ) +:DICOMDatasetSorter(other) +{ +} + +mitk::NormalDirectionConsistencySorter +::~NormalDirectionConsistencySorter() +{ +} + +void +mitk::NormalDirectionConsistencySorter +::PrintConfiguration(std::ostream& os, const std::string& indent) const +{ + os << indent << "NormalDirectionConsistencySorter TODO" << std::endl; +} + + +mitk::NormalDirectionConsistencySorter& +mitk::NormalDirectionConsistencySorter +::operator=(const NormalDirectionConsistencySorter& other) +{ + if (this != &other) + { + DICOMDatasetSorter::operator=(other); + } + return *this; +} + +mitk::DICOMTagList +mitk::NormalDirectionConsistencySorter +::GetTagsOfInterest() +{ + DICOMTagList tags; + tags.push_back( DICOMTag(0x0020, 0x0032) ); // ImagePositionPatient + tags.push_back( DICOMTag(0x0020, 0x0037) ); // ImageOrientationPatient + + return tags; +} + +void +mitk::NormalDirectionConsistencySorter +::Sort() +{ + DICOMDatasetList datasets = GetInput(); + + if (datasets.size() > 1) + { + // at some point in the code, there is the expectation that + // the direction of the slice normals is the same as the direction between + // first and last slice origin. We need to make this sure here, because + // we want to feed the files into itk::ImageSeriesReader with the consistent + // setting of ReverseOrderOff. + + static const DICOMTag tagImagePositionPatient = DICOMTag(0x0020,0x0032); // Image Position (Patient) + static const DICOMTag tagImageOrientation = DICOMTag(0x0020, 0x0037); // Image Orientation + + DICOMDatasetAccess* firstDS = datasets.front(); + DICOMDatasetAccess* lastDS = datasets.back(); + + // make sure here that the direction from slice to slice is the direction of + // image normals... + std::string imageOrientationString = firstDS->GetTagValueAsString( tagImageOrientation ); + std::string imagePositionPatientFirst = firstDS->GetTagValueAsString( tagImagePositionPatient ); + std::string imagePositionPatientLast = lastDS->GetTagValueAsString( tagImagePositionPatient ); + + static Vector3D right; right.Fill(0.0); + static Vector3D up; up.Fill(0.0); + static bool hasOrientation(false); + DICOMStringToOrientationVectors( imageOrientationString, + right, up, hasOrientation ); + + static Point3D firstOrigin; firstOrigin.Fill(0.0f); + static bool firstHasOrigin(false); + firstOrigin = DICOMStringToPoint3D( imagePositionPatientFirst, firstHasOrigin ); + + static Point3D lastOrigin; lastOrigin.Fill(0.0f); + static bool lastHasOrigin(false); + lastOrigin = DICOMStringToPoint3D( imagePositionPatientLast, lastHasOrigin ); + + static Vector3D normal; + normal[0] = right[1] * up[2] - right[2] * up[1]; + normal[1] = right[2] * up[0] - right[0] * up[2]; + normal[2] = right[0] * up[1] - right[1] * up[0]; + normal.Normalize(); + + static Vector3D directionOfSlices; + directionOfSlices = lastOrigin - firstOrigin; + directionOfSlices.Normalize(); + + MITK_DEBUG << "Making sense of \norientation '" << imageOrientationString + << "'\nfirst position '" << imagePositionPatientFirst + << "'\nlast position '" << imagePositionPatientLast << "'"; + MITK_DEBUG << "Normal: " << normal; + MITK_DEBUG << "Direction of slices: " << directionOfSlices; + + if ( (directionOfSlices - normal).GetNorm() > 0.5 ) + { + MITK_DEBUG << "Need to reverse filenames"; + std::reverse( datasets.begin(), datasets.end() ); + } + } + + this->SetNumberOfOutputs(1); + this->SetOutput(0, datasets); +} diff --git a/Modules/DICOMReader/mitkNormalDirectionConsistencySorter.h b/Modules/DICOMReader/mitkNormalDirectionConsistencySorter.h new file mode 100644 index 0000000000..17b6c21210 --- /dev/null +++ b/Modules/DICOMReader/mitkNormalDirectionConsistencySorter.h @@ -0,0 +1,63 @@ +/*=================================================================== + +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 mitkNormalDirectionConsistencySorter_h +#define mitkNormalDirectionConsistencySorter_h + +#include "mitkDICOMDatasetSorter.h" + +namespace mitk +{ + +/** + \ingroup DICOMReaderModule + \brief Makes sure that the order of files is along the image plane normals. + + When loading with ImageSeriesReader and initializing an mitk::Image with the result + we need to make sure that the order of inputs for the ImageSeriesReader is along the + normal of the images. I.e. The direction of the normal needs to be the same direction + as the vector from the first to the last image origin. + + \note This class might be a workaround for another bug in MITK, but until this issue + is completely understood, the workaround fixes the problem of images that + appear upside-down. +*/ +class DICOMReader_EXPORT NormalDirectionConsistencySorter : public DICOMDatasetSorter +{ + public: + + mitkClassMacro( NormalDirectionConsistencySorter, DICOMDatasetSorter ) + itkNewMacro( NormalDirectionConsistencySorter ) + + virtual DICOMTagList GetTagsOfInterest(); + + /// See class description. + virtual void Sort(); + + virtual void PrintConfiguration(std::ostream& os, const std::string& indent = "") const; + + protected: + + NormalDirectionConsistencySorter(); + virtual ~NormalDirectionConsistencySorter(); + + NormalDirectionConsistencySorter(const NormalDirectionConsistencySorter& other); + NormalDirectionConsistencySorter& operator=(const NormalDirectionConsistencySorter& other); +}; + +} + +#endif