diff --git a/Core/Code/Common/mitkTestingMacros.h b/Core/Code/Common/mitkTestingMacros.h index 8bf909a5cb..7c16e82748 100644 --- a/Core/Code/Common/mitkTestingMacros.h +++ b/Core/Code/Common/mitkTestingMacros.h @@ -1,139 +1,143 @@ /*=================================================================== 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 mitkTestingMacros_h +#define mitkTestingMacros_h + #include #include #include #include #include namespace mitk { /** \brief Indicate a failed test. */ class TestFailedException : public std::exception { public: TestFailedException() {} }; } /** * * \brief Output some text without generating a terminating newline. Include * * */ #define MITK_TEST_OUTPUT_NO_ENDL(x) \ std::cout x ; /** \brief Output some text. */ #define MITK_TEST_OUTPUT(x) \ MITK_TEST_OUTPUT_NO_ENDL(x << "\n") /** \brief Do some general test preparations. Must be called first in the main test function. */ #define MITK_TEST_BEGIN(testName) \ std::string mitkTestName(#testName); \ mitk::TestManager::GetInstance()->Initialize(); \ try { /** \brief Fail and finish test with message MSG */ #define MITK_TEST_FAILED_MSG(MSG) \ MITK_TEST_OUTPUT(MSG) \ throw mitk::TestFailedException(); /** \brief Must be called last in the main test function. */ #define MITK_TEST_END() \ } catch (mitk::TestFailedException ex) { \ MITK_TEST_OUTPUT(<< "Further test execution skipped.") \ mitk::TestManager::GetInstance()->TestFailed(); \ } catch (std::exception ex) { \ MITK_TEST_OUTPUT(<< "std::exception occured " << ex.what()) \ mitk::TestManager::GetInstance()->TestFailed(); \ } \ if (mitk::TestManager::GetInstance()->NumberOfFailedTests() > 0) { \ MITK_TEST_OUTPUT(<< mitkTestName << ": [DONE FAILED] , subtests passed: " << \ mitk::TestManager::GetInstance()->NumberOfPassedTests() << " failed: " << \ mitk::TestManager::GetInstance()->NumberOfFailedTests() ) \ return EXIT_FAILURE; \ } else { \ MITK_TEST_OUTPUT(<< mitkTestName << ": " \ << mitk::TestManager::GetInstance()->NumberOfPassedTests() \ << " tests [DONE PASSED]") \ return EXIT_SUCCESS; \ } \ #define MITK_TEST_CONDITION(COND,MSG) \ MITK_TEST_OUTPUT_NO_ENDL(<< MSG) \ if ( ! (COND) ) { \ mitk::TestManager::GetInstance()->TestFailed(); \ MITK_TEST_OUTPUT(<< " [FAILED]\n" << "In " << __FILE__ \ << ", line " << __LINE__ \ << ": " #COND " : [FAILED]") \ } else { \ MITK_TEST_OUTPUT(<< " [PASSED]") \ mitk::TestManager::GetInstance()->TestPassed(); \ } #define MITK_TEST_CONDITION_REQUIRED(COND,MSG) \ MITK_TEST_OUTPUT_NO_ENDL(<< MSG) \ if ( ! (COND) ) { \ MITK_TEST_FAILED_MSG(<< " [FAILED]\n" << " +--> in " << __FILE__ \ << ", line " << __LINE__ \ << ", expression is false: \"" #COND "\"") \ } else { \ MITK_TEST_OUTPUT(<< " [PASSED]") \ mitk::TestManager::GetInstance()->TestPassed(); \ } /** * \brief Begin block which should be checked for exceptions * * This macro, together with MITK_TEST_FOR_EXCEPTION_END, can be used * to test whether a code block throws an expected exception. The test FAILS if the * exception is NOT thrown. A simple example: * MITK_TEST_FOR_EXCEPTION_BEGIN(itk::ImageFileReaderException) typedef itk::ImageFileReader< itk::Image > ReaderType; ReaderType::Pointer reader = ReaderType::New(); reader->SetFileName("/tmp/not-existing"); reader->Update(); MITK_TEST_FOR_EXCEPTION_END(itk::ImageFileReaderException) * */ #define MITK_TEST_FOR_EXCEPTION_BEGIN(EXCEPTIONCLASS) \ try { #define MITK_TEST_FOR_EXCEPTION_END(EXCEPTIONCLASS) \ mitk::TestManager::GetInstance()->TestFailed(); \ MITK_TEST_OUTPUT( << "Expected an '" << #EXCEPTIONCLASS << "' exception. [FAILED]") \ } \ catch (EXCEPTIONCLASS) { \ MITK_TEST_OUTPUT( << "Caught an expected '" << #EXCEPTIONCLASS \ << "' exception. [PASSED]") \ mitk::TestManager::GetInstance()->TestPassed(); \ } /** * \brief Simplified version of MITK_TEST_FOR_EXCEPTION_BEGIN / END for * a single statement */ #define MITK_TEST_FOR_EXCEPTION(EXCEPTIONCLASS, STATEMENT) \ MITK_TEST_FOR_EXCEPTION_BEGIN(EXCEPTIONCLASS) \ STATEMENT ; \ MITK_TEST_FOR_EXCEPTION_END(EXCEPTIONCLASS) +#endif diff --git a/Core/Code/DataManagement/mitkLevelWindow.cpp b/Core/Code/DataManagement/mitkLevelWindow.cpp index f6d7c6385b..56e6fb5017 100644 --- a/Core/Code/DataManagement/mitkLevelWindow.cpp +++ b/Core/Code/DataManagement/mitkLevelWindow.cpp @@ -1,442 +1,443 @@ /*=================================================================== 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 "mitkLevelWindow.h" #include "mitkImageSliceSelector.h" #include "mitkImageStatisticsHolder.h" #include void mitk::LevelWindow::EnsureConsistency() { // Check if total range is ok { if ( m_RangeMin > m_RangeMax ) std::swap(m_RangeMin,m_RangeMax); if (m_RangeMin == m_RangeMax ) m_RangeMin = m_RangeMax - 1; } // Check if current window is ok { if ( m_LowerWindowBound > m_UpperWindowBound ) std::swap(m_LowerWindowBound,m_UpperWindowBound); if ( m_LowerWindowBound < m_RangeMin ) m_LowerWindowBound = m_RangeMin; if ( m_UpperWindowBound < m_RangeMin ) m_UpperWindowBound = m_RangeMin; if ( m_LowerWindowBound > m_RangeMax ) m_LowerWindowBound = m_RangeMax; if ( m_UpperWindowBound > m_RangeMax ) m_UpperWindowBound = m_RangeMax; if (m_LowerWindowBound == m_UpperWindowBound ) { if(m_LowerWindowBound == m_RangeMin ) m_UpperWindowBound++; else m_LowerWindowBound--; } } } mitk::LevelWindow::LevelWindow(mitk::ScalarType level, mitk::ScalarType window) : m_LowerWindowBound( level - window / 2.0 ), m_UpperWindowBound( level + window / 2.0 ), m_RangeMin( -2048.0 ), m_RangeMax( 4096.0 ), m_DefaultLowerBound( -2048.0 ), m_DefaultUpperBound( 4096.0 ), m_Fixed( false ) { SetDefaultLevelWindow(level, window); + SetLevelWindow(level, window, true); } mitk::LevelWindow::LevelWindow(const mitk::LevelWindow& levWin) : m_LowerWindowBound( levWin.GetLowerWindowBound() ) , m_UpperWindowBound( levWin.GetUpperWindowBound() ) , m_RangeMin( levWin.GetRangeMin() ) , m_RangeMax( levWin.GetRangeMax() ) , m_DefaultLowerBound( levWin.GetDefaultLowerBound() ) , m_DefaultUpperBound( levWin.GetDefaultUpperBound() ) , m_Fixed( levWin.GetFixed() ) { } mitk::LevelWindow::~LevelWindow() { } mitk::ScalarType mitk::LevelWindow::GetLevel() const { return (m_UpperWindowBound-m_LowerWindowBound) / 2.0 + m_LowerWindowBound; } mitk::ScalarType mitk::LevelWindow::GetWindow() const { return (m_UpperWindowBound-m_LowerWindowBound); } mitk::ScalarType mitk::LevelWindow::GetDefaultLevel() const { return ((m_DefaultUpperBound+m_DefaultLowerBound)/2.0); } mitk::ScalarType mitk::LevelWindow::GetDefaultWindow() const { return ((m_DefaultUpperBound-m_DefaultLowerBound)); } void mitk::LevelWindow::ResetDefaultLevelWindow() { SetLevelWindow(GetDefaultLevel(), GetDefaultWindow()); } mitk::ScalarType mitk::LevelWindow::GetLowerWindowBound() const { return m_LowerWindowBound; } mitk::ScalarType mitk::LevelWindow::GetUpperWindowBound() const { return m_UpperWindowBound; } void mitk::LevelWindow::SetDefaultLevelWindow(mitk::ScalarType level, mitk::ScalarType window) { SetDefaultBoundaries((level-(window/2.0)), (level+(window/2.0))); } void mitk::LevelWindow::SetLevelWindow(mitk::ScalarType level, mitk::ScalarType window, bool expandRangesIfNecessary) { SetWindowBounds( (level-(window/2.0)), (level+(window/2.0)), expandRangesIfNecessary ); } void mitk::LevelWindow::SetWindowBounds(mitk::ScalarType lowerBound, mitk::ScalarType upperBound, bool expandRangesIfNecessary) { if ( IsFixed() ) return; m_LowerWindowBound = lowerBound; m_UpperWindowBound = upperBound; if (expandRangesIfNecessary) { /* if caller is sure he wants exactly that level/window, we make sure the limits match */ if (m_LowerWindowBound > m_UpperWindowBound) std::swap(m_LowerWindowBound, m_UpperWindowBound); if ( m_LowerWindowBound < m_RangeMin ) { m_RangeMin = m_LowerWindowBound; } if ( m_UpperWindowBound > m_RangeMax ) { m_RangeMax = m_UpperWindowBound; } } EnsureConsistency(); } void mitk::LevelWindow::SetRangeMinMax(mitk::ScalarType min, mitk::ScalarType max) { if ( IsFixed() ) return; m_RangeMin = min; m_RangeMax = max; EnsureConsistency(); } void mitk::LevelWindow::SetDefaultBoundaries(mitk::ScalarType low, mitk::ScalarType up) { if ( IsFixed() ) return; m_DefaultLowerBound = low; m_DefaultUpperBound = up; // Check if default window is ok { if ( m_DefaultLowerBound > m_DefaultUpperBound ) std::swap(m_DefaultLowerBound,m_DefaultUpperBound); if (m_DefaultLowerBound == m_DefaultUpperBound ) m_DefaultLowerBound--; } EnsureConsistency(); } void mitk::LevelWindow::SetToMaxWindowSize() { SetWindowBounds( m_RangeMin , m_RangeMax ); } mitk::ScalarType mitk::LevelWindow::GetRangeMin() const { return m_RangeMin; } mitk::ScalarType mitk::LevelWindow::GetRangeMax() const { return m_RangeMax; } mitk::ScalarType mitk::LevelWindow::GetRange() const { return m_RangeMax - m_RangeMin; } mitk::ScalarType mitk::LevelWindow::GetDefaultUpperBound() const { return m_DefaultUpperBound; } mitk::ScalarType mitk::LevelWindow::GetDefaultLowerBound() const { return m_DefaultLowerBound; } void mitk::LevelWindow::ResetDefaultRangeMinMax() { SetRangeMinMax(m_DefaultLowerBound, m_DefaultUpperBound); } /*! This method initializes a mitk::LevelWindow from an mitk::Image. The algorithm is as follows: Default to taking the central image slice for quick analysis. Compute the smallest (minValue), second smallest (min2ndValue), second largest (max2ndValue), and largest (maxValue) data value by traversing the pixel values only once. In the same scan it also computes the count of minValue values and maxValue values. After that a basic histogram with specific information about the extrems is complete. If minValue == maxValue, the center slice is uniform and the above scan is repeated for the complete image, not just one slice Next, special cases of images with only 1, 2 or 3 distinct data values have hand assigned level window ranges. Next the level window is set relative to the inner range IR = lengthOf([min2ndValue, max2ndValue]) For count(minValue) > 20% the smallest values are frequent and should be distinct from the min2ndValue and larger values (minValue may be std:min, may signify something special) hence the lower end of the level window is set to min2ndValue - 0.5 * IR For count(minValue) <= 20% the smallest values are not so important and can blend with the next ones => min(level window) = min2ndValue And analog for max(level window): count(max2ndValue) > 20%: max(level window) = max2ndValue + 0.5 * IR count(max2ndValue) < 20%: max(level window) = max2ndValue In both 20%+ cases the level window bounds are clamped to the [minValue, maxValue] range In consequence the level window maximizes contrast with minimal amount of computation and does do useful things if the data contains std::min or std:max values or has only 1 or 2 or 3 data values. */ void mitk::LevelWindow::SetAuto(const mitk::Image* image, bool /*tryPicTags*/, bool guessByCentralSlice) { if ( IsFixed() ) return; if ( image == NULL || !image->IsInitialized() ) return; const mitk::Image* wholeImage = image; ScalarType minValue = 0.0; ScalarType maxValue = 0.0; ScalarType min2ndValue = 0.0; ScalarType max2ndValue = 0.0; mitk::ImageSliceSelector::Pointer sliceSelector = mitk::ImageSliceSelector::New(); if ( guessByCentralSlice ) { sliceSelector->SetInput(image); sliceSelector->SetSliceNr(image->GetDimension(2)/2); sliceSelector->SetTimeNr(image->GetDimension(3)/2); sliceSelector->SetChannelNr(image->GetDimension(4)/2); sliceSelector->Update(); image = sliceSelector->GetOutput(); if ( image == NULL || !image->IsInitialized() ) return; minValue = image->GetStatistics()->GetScalarValueMin(); maxValue = image->GetStatistics()->GetScalarValueMaxNoRecompute(); min2ndValue = image->GetStatistics()->GetScalarValue2ndMinNoRecompute(); max2ndValue = image->GetStatistics()->GetScalarValue2ndMaxNoRecompute(); if ( minValue == maxValue ) { // guessByCentralSlice seems to have failed, lets look at all data image = wholeImage; minValue = image->GetStatistics()->GetScalarValueMin(); maxValue = image->GetStatistics()->GetScalarValueMaxNoRecompute(); min2ndValue = image->GetStatistics()->GetScalarValue2ndMinNoRecompute(); max2ndValue = image->GetStatistics()->GetScalarValue2ndMaxNoRecompute(); } } else { const_cast(image)->Update(); minValue = image->GetStatistics()->GetScalarValueMin(0); maxValue = image->GetStatistics()->GetScalarValueMaxNoRecompute(0); min2ndValue = image->GetStatistics()->GetScalarValue2ndMinNoRecompute(0); max2ndValue = image->GetStatistics()->GetScalarValue2ndMaxNoRecompute(0); for (unsigned int i = 1; i < image->GetDimension(3); ++i) { ScalarType minValueTemp = image->GetStatistics()->GetScalarValueMin(i); if (minValue > minValueTemp) minValue = minValueTemp; ScalarType maxValueTemp = image->GetStatistics()->GetScalarValueMaxNoRecompute(i); if (maxValue < maxValueTemp) maxValue = maxValueTemp; ScalarType min2ndValueTemp = image->GetStatistics()->GetScalarValue2ndMinNoRecompute(i); if (min2ndValue > min2ndValueTemp) min2ndValue = min2ndValueTemp; ScalarType max2ndValueTemp = image->GetStatistics()->GetScalarValue2ndMaxNoRecompute(i); if (max2ndValue > max2ndValueTemp) max2ndValue = max2ndValueTemp; } } // Fix for bug# 344 Level Window wird bei Eris Cut bildern nicht richtig gesetzt if ( image->GetPixelType().GetPixelType()==itk::ImageIOBase::SCALAR && image->GetPixelType().GetComponentType() == itk::ImageIOBase::INT && image->GetPixelType().GetBpe() >= 8) { // the windows compiler complains about ambiguos 'pow' call, therefore static casting to (double, int) if (minValue == -( pow( (double) 2.0, static_cast(image->GetPixelType().GetBpe()/2) ) ) ) { minValue = min2ndValue; } } // End fix //// uniform image if ( minValue == maxValue ) { minValue = maxValue-1; } else { //Due to bug #8690 level window now is no longer of fixed range by default but the range adapts according to levelwindow interaction //This is done because the range should be a little bit larger from the beginning so that the scale doesn't start to resize right from the beginning double additionalRange = 0.15*(maxValue-minValue); minValue -= additionalRange; maxValue += additionalRange; } SetRangeMinMax(minValue, maxValue); SetDefaultBoundaries(minValue, maxValue); /* if ( tryPicTags ) // level and window will be set by informations provided directly by the mitkIpPicDescriptor { if ( SetAutoByPicTags(const_cast(image)->GetPic()) ) { return; } } */ unsigned int numPixelsInDataset = image->GetDimensions()[0]; for ( unsigned int k=0; kGetDimension(); ++k ) numPixelsInDataset *= image->GetDimensions()[k]; unsigned int minCount = image->GetStatistics()->GetCountOfMinValuedVoxelsNoRecompute(); unsigned int maxCount = image->GetStatistics()->GetCountOfMaxValuedVoxelsNoRecompute(); float minCountFraction = minCount/float(numPixelsInDataset); float maxCountFraction = maxCount/float(numPixelsInDataset); //// binary image if ( min2ndValue == maxValue ) { // noop; full range is fine } //// triple value image, put middle value in center of gray level ramp else if ( min2ndValue == max2ndValue ) { ScalarType minDelta = std::min(min2ndValue-minValue, maxValue-min2ndValue); minValue = min2ndValue - minDelta; maxValue = min2ndValue + minDelta; } // now we can assume more than three distict scalar values else { ScalarType innerRange = max2ndValue - min2ndValue; if ( minCountFraction > 0.2 ) //// lots of min values -> make different from rest, but not miles away { ScalarType halfInnerRangeGapMinValue = min2ndValue - innerRange/2.0; minValue = std::max(minValue, halfInnerRangeGapMinValue); } else //// few min values -> focus on innerRange { minValue = min2ndValue; } if ( maxCountFraction > 0.2 ) //// lots of max values -> make different from rest { ScalarType halfInnerRangeGapMaxValue = max2ndValue + innerRange/2.0; maxValue = std::min(maxValue, halfInnerRangeGapMaxValue); } else //// few max values -> focus on innerRange { maxValue = max2ndValue; } } SetWindowBounds(minValue, maxValue); SetDefaultLevelWindow((maxValue - minValue) / 2 + minValue, maxValue - minValue); } void mitk::LevelWindow::SetFixed( bool fixed ) { m_Fixed = fixed; } bool mitk::LevelWindow::GetFixed() const { return m_Fixed; } bool mitk::LevelWindow::IsFixed() const { return m_Fixed; } bool mitk::LevelWindow::operator==(const mitk::LevelWindow& levWin) const { if ( m_RangeMin == levWin.GetRangeMin() && m_RangeMax == levWin.GetRangeMax() && m_LowerWindowBound == levWin.GetLowerWindowBound() && m_UpperWindowBound == levWin.GetUpperWindowBound() && m_DefaultLowerBound == levWin.GetDefaultLowerBound() && m_DefaultUpperBound == levWin.GetDefaultUpperBound() && m_Fixed == levWin.IsFixed() ) { return true; } else { return false; } } bool mitk::LevelWindow::operator!=(const mitk::LevelWindow& levWin) const { return ! ( (*this) == levWin); } mitk::LevelWindow& mitk::LevelWindow::operator=(const mitk::LevelWindow& levWin) { if (this == &levWin) { return *this; } else { m_RangeMin = levWin.GetRangeMin(); m_RangeMax = levWin.GetRangeMax(); m_LowerWindowBound= levWin.GetLowerWindowBound(); m_UpperWindowBound= levWin.GetUpperWindowBound(); m_DefaultLowerBound = levWin.GetDefaultLowerBound(); m_DefaultUpperBound = levWin.GetDefaultUpperBound(); m_Fixed = levWin.GetFixed(); return *this; } } diff --git a/Core/Code/IO/mitkDicomSeriesReader.cpp b/Core/Code/IO/mitkDicomSeriesReader.cpp index cb1ac6d205..a816609fa7 100644 --- a/Core/Code/IO/mitkDicomSeriesReader.cpp +++ b/Core/Code/IO/mitkDicomSeriesReader.cpp @@ -1,1772 +1,1773 @@ /*=================================================================== 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. ===================================================================*/ // uncomment for learning more about the internal sorting mechanisms //#define MBILOG_ENABLE_DEBUG #include #include #include #include #include #include #include #include #include #include #include "mitkProperties.h" namespace mitk { std::string DicomSeriesReader::ReaderImplementationLevelToString( const ReaderImplementationLevel& enumValue ) { switch (enumValue) { case ReaderImplementationLevel_Supported: return "Supported"; case ReaderImplementationLevel_PartlySupported: return "PartlySupported"; case ReaderImplementationLevel_Implemented: return "Implemented"; case ReaderImplementationLevel_Unsupported: return "Unsupported"; default: return ""; }; } std::string DicomSeriesReader::PixelSpacingInterpretationToString( const PixelSpacingInterpretation& enumValue ) { switch (enumValue) { case PixelSpacingInterpretation_SpacingInPatient: return "In Patient"; case PixelSpacingInterpretation_SpacingAtDetector: return "At Detector"; case PixelSpacingInterpretation_SpacingUnknown: return "Unknown spacing"; default: return ""; }; } const DicomSeriesReader::TagToPropertyMapType& DicomSeriesReader::GetDICOMTagsToMITKPropertyMap() { static bool initialized = false; static TagToPropertyMapType dictionary; if (!initialized) { /* Selection criteria: - no sequences because we cannot represent that - nothing animal related (specied, breed registration number), MITK focusses on human medical image processing. - only general attributes so far When extending this, we should make use of a real dictionary (GDCM/DCMTK and lookup the tag names there) */ // Patient module dictionary["0010|0010"] = "dicom.patient.PatientsName"; dictionary["0010|0020"] = "dicom.patient.PatientID"; dictionary["0010|0030"] = "dicom.patient.PatientsBirthDate"; dictionary["0010|0040"] = "dicom.patient.PatientsSex"; dictionary["0010|0032"] = "dicom.patient.PatientsBirthTime"; dictionary["0010|1000"] = "dicom.patient.OtherPatientIDs"; dictionary["0010|1001"] = "dicom.patient.OtherPatientNames"; dictionary["0010|2160"] = "dicom.patient.EthnicGroup"; dictionary["0010|4000"] = "dicom.patient.PatientComments"; dictionary["0012|0062"] = "dicom.patient.PatientIdentityRemoved"; dictionary["0012|0063"] = "dicom.patient.DeIdentificationMethod"; // General Study module dictionary["0020|000d"] = "dicom.study.StudyInstanceUID"; dictionary["0008|0020"] = "dicom.study.StudyDate"; dictionary["0008|0030"] = "dicom.study.StudyTime"; dictionary["0008|0090"] = "dicom.study.ReferringPhysiciansName"; dictionary["0020|0010"] = "dicom.study.StudyID"; dictionary["0008|0050"] = "dicom.study.AccessionNumber"; dictionary["0008|1030"] = "dicom.study.StudyDescription"; dictionary["0008|1048"] = "dicom.study.PhysiciansOfRecord"; dictionary["0008|1060"] = "dicom.study.NameOfPhysicianReadingStudy"; // General Series module dictionary["0008|0060"] = "dicom.series.Modality"; dictionary["0020|000e"] = "dicom.series.SeriesInstanceUID"; dictionary["0020|0011"] = "dicom.series.SeriesNumber"; dictionary["0020|0060"] = "dicom.series.Laterality"; dictionary["0008|0021"] = "dicom.series.SeriesDate"; dictionary["0008|0031"] = "dicom.series.SeriesTime"; dictionary["0008|1050"] = "dicom.series.PerformingPhysiciansName"; dictionary["0018|1030"] = "dicom.series.ProtocolName"; dictionary["0008|103e"] = "dicom.series.SeriesDescription"; dictionary["0008|1070"] = "dicom.series.OperatorsName"; dictionary["0018|0015"] = "dicom.series.BodyPartExamined"; dictionary["0018|5100"] = "dicom.series.PatientPosition"; dictionary["0028|0108"] = "dicom.series.SmallestPixelValueInSeries"; dictionary["0028|0109"] = "dicom.series.LargestPixelValueInSeries"; // VOI LUT module dictionary["0028|1050"] = "dicom.voilut.WindowCenter"; dictionary["0028|1051"] = "dicom.voilut.WindowWidth"; dictionary["0028|1055"] = "dicom.voilut.WindowCenterAndWidthExplanation"; // Image Pixel module dictionary["0028|0004"] = "dicom.pixel.PhotometricInterpretation"; dictionary["0028|0010"] = "dicom.pixel.Rows"; dictionary["0028|0011"] = "dicom.pixel.Columns"; // Image Plane module dictionary["0028|0030"] = "dicom.PixelSpacing"; dictionary["0018|1164"] = "dicom.ImagerPixelSpacing"; initialized = true; } return dictionary; } DataNode::Pointer DicomSeriesReader::LoadDicomSeries(const StringContainer &filenames, bool sort, bool check_4d, bool correctTilt, UpdateCallBackMethod callback, Image::Pointer preLoadedImageBlock) { DataNode::Pointer node = DataNode::New(); if (DicomSeriesReader::LoadDicomSeries(filenames, *node, sort, check_4d, correctTilt, callback, preLoadedImageBlock)) { if( filenames.empty() ) { return NULL; } return node; } else { return NULL; } } bool DicomSeriesReader::LoadDicomSeries( const StringContainer &filenames, DataNode &node, bool sort, bool check_4d, bool correctTilt, UpdateCallBackMethod callback, Image::Pointer preLoadedImageBlock) { if( filenames.empty() ) { MITK_DEBUG << "Calling LoadDicomSeries with empty filename string container. Probably invalid application logic."; node.SetData(NULL); return true; // this is not actually an error but the result is very simple } DcmIoType::Pointer io = DcmIoType::New(); try { if (io->CanReadFile(filenames.front().c_str())) { io->SetFileName(filenames.front().c_str()); io->ReadImageInformation(); if (io->GetPixelType() == itk::ImageIOBase::SCALAR || io->GetPixelType() == itk::ImageIOBase::RGB) { LoadDicom(filenames, node, sort, check_4d, correctTilt, callback, preLoadedImageBlock); } if (node.GetData()) { return true; } } } catch(itk::MemoryAllocationError& e) { MITK_ERROR << "Out of memory. Cannot load DICOM series: " << e.what(); } catch(std::exception& e) { MITK_ERROR << "Error encountered when loading DICOM series:" << e.what(); } catch(...) { MITK_ERROR << "Unspecified error encountered when loading DICOM series."; } return false; } bool DicomSeriesReader::IsDicom(const std::string &filename) { DcmIoType::Pointer io = DcmIoType::New(); return io->CanReadFile(filename.c_str()); } bool DicomSeriesReader::IsPhilips3DDicom(const std::string &filename) { DcmIoType::Pointer io = DcmIoType::New(); if (io->CanReadFile(filename.c_str())) { //Look at header Tag 3001,0010 if it is "Philips3D" gdcm::Reader reader; reader.SetFileName(filename.c_str()); reader.Read(); gdcm::DataSet &data_set = reader.GetFile().GetDataSet(); gdcm::StringFilter sf; sf.SetFile(reader.GetFile()); if (data_set.FindDataElement(gdcm::Tag(0x3001, 0x0010)) && (sf.ToString(gdcm::Tag(0x3001, 0x0010)) == "Philips3D ")) { return true; } } return false; } bool DicomSeriesReader::ReadPhilips3DDicom(const std::string &filename, mitk::Image::Pointer output_image) { // Now get PhilipsSpecific Tags gdcm::PixmapReader reader; reader.SetFileName(filename.c_str()); reader.Read(); gdcm::DataSet &data_set = reader.GetFile().GetDataSet(); gdcm::StringFilter sf; sf.SetFile(reader.GetFile()); gdcm::Attribute<0x0028,0x0011> dimTagX; // coloumns || sagittal gdcm::Attribute<0x3001,0x1001, gdcm::VR::UL, gdcm::VM::VM1> dimTagZ; //I have no idea what is VM1. // (Philips specific) // axial gdcm::Attribute<0x0028,0x0010> dimTagY; // rows || coronal gdcm::Attribute<0x0028,0x0008> dimTagT; // how many frames gdcm::Attribute<0x0018,0x602c> spaceTagX; // Spacing in X , unit is "physicalTagx" (usually centimeter) gdcm::Attribute<0x0018,0x602e> spaceTagY; gdcm::Attribute<0x3001,0x1003, gdcm::VR::FD, gdcm::VM::VM1> spaceTagZ; // (Philips specific) gdcm::Attribute<0x0018,0x6024> physicalTagX; // if 3, then spacing params are centimeter gdcm::Attribute<0x0018,0x6026> physicalTagY; gdcm::Attribute<0x3001,0x1002, gdcm::VR::US, gdcm::VM::VM1> physicalTagZ; // (Philips specific) dimTagX.Set(data_set); dimTagY.Set(data_set); dimTagZ.Set(data_set); dimTagT.Set(data_set); spaceTagX.Set(data_set); spaceTagY.Set(data_set); spaceTagZ.Set(data_set); physicalTagX.Set(data_set); physicalTagY.Set(data_set); physicalTagZ.Set(data_set); unsigned int dimX = dimTagX.GetValue(), dimY = dimTagY.GetValue(), dimZ = dimTagZ.GetValue(), dimT = dimTagT.GetValue(), physicalX = physicalTagX.GetValue(), physicalY = physicalTagY.GetValue(), physicalZ = physicalTagZ.GetValue(); float spaceX = spaceTagX.GetValue(), spaceY = spaceTagY.GetValue(), spaceZ = spaceTagZ.GetValue(); if (physicalX == 3) // spacing parameter in cm, have to convert it to mm. spaceX = spaceX * 10; if (physicalY == 3) // spacing parameter in cm, have to convert it to mm. spaceY = spaceY * 10; if (physicalZ == 3) // spacing parameter in cm, have to convert it to mm. spaceZ = spaceZ * 10; // Ok, got all necessary Tags! // Now read Pixeldata (7fe0,0010) X x Y x Z x T Elements const gdcm::Pixmap &pixels = reader.GetPixmap(); gdcm::RAWCodec codec; codec.SetPhotometricInterpretation(gdcm::PhotometricInterpretation::MONOCHROME2); codec.SetPixelFormat(pixels.GetPixelFormat()); codec.SetPlanarConfiguration(0); gdcm::DataElement out; codec.Decode(data_set.GetDataElement(gdcm::Tag(0x7fe0, 0x0010)), out); const gdcm::ByteValue *bv = out.GetByteValue(); const char *new_pixels = bv->GetPointer(); // Create MITK Image + Geometry typedef itk::Image ImageType; //Pixeltype might be different sometimes? Maybe read it out from header ImageType::RegionType myRegion; ImageType::SizeType mySize; ImageType::IndexType myIndex; ImageType::SpacingType mySpacing; ImageType::Pointer imageItk = ImageType::New(); mySpacing[0] = spaceX; mySpacing[1] = spaceY; mySpacing[2] = spaceZ; mySpacing[3] = 1; myIndex[0] = 0; myIndex[1] = 0; myIndex[2] = 0; myIndex[3] = 0; mySize[0] = dimX; mySize[1] = dimY; mySize[2] = dimZ; mySize[3] = dimT; myRegion.SetSize( mySize); myRegion.SetIndex( myIndex ); imageItk->SetSpacing(mySpacing); imageItk->SetRegions( myRegion); imageItk->Allocate(); imageItk->FillBuffer(0); itk::ImageRegionIterator iterator(imageItk, imageItk->GetLargestPossibleRegion()); iterator.GoToBegin(); unsigned long pixCount = 0; unsigned long planeSize = dimX*dimY; unsigned long planeCount = 0; unsigned long timeCount = 0; unsigned long numberOfSlices = dimZ; while (!iterator.IsAtEnd()) { unsigned long adressedPixel = pixCount + (numberOfSlices-1-planeCount)*planeSize // add offset to adress the first pixel of current plane + timeCount*numberOfSlices*planeSize; // add time offset iterator.Set( new_pixels[ adressedPixel ] ); pixCount++; ++iterator; if (pixCount == planeSize) { pixCount = 0; planeCount++; } if (planeCount == numberOfSlices) { planeCount = 0; timeCount++; } if (timeCount == dimT) { break; } } mitk::CastToMitkImage(imageItk, output_image); return true; // actually never returns false yet.. but exception possible } std::string DicomSeriesReader::ConstCharStarToString(const char* s) { return s ? std::string(s) : std::string(); } bool DicomSeriesReader::DICOMStringToSpacing(const std::string& s, float& spacingX, float& 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; } Point3D DicomSeriesReader::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; MITK_ERROR << "Reader implementation made wrong assumption on tag (0020,0032). Found " << dim << " instead of 3 values."; } else if (dim == 0) { successful = false; p.Fill(0.0); // assume default (0,0,0) } return p; } void DicomSeriesReader::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; MITK_ERROR << "Reader implementation made wrong assumption on tag (0020,0037). Found " << dim << " instead of 6 values."; } 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; } } DicomSeriesReader::SliceGroupingAnalysisResult DicomSeriesReader::AnalyzeFileForITKImageSeriesReaderSpacingAssumption( const StringContainer& files, bool groupImagesWithGantryTilt, const gdcm::Scanner::MappingType& tagValueMappings_) { // 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 gdcm::Scanner::MappingType& tagValueMappings = const_cast(tagValueMappings_); const gdcm::Tag tagImagePositionPatient(0x0020,0x0032); // Image Position (Patient) const gdcm::Tag tagImageOrientation(0x0020, 0x0037); // Image Orientation const gdcm::Tag tagGantryTilt(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 files for z-spacing assumption of ITK's ImageSeriesReader (group tilted: " << groupImagesWithGantryTilt << ")"; unsigned int fileIndex(0); for (StringContainer::const_iterator fileIter = files.begin(); fileIter != files.end(); ++fileIter, ++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 = ConstCharStarToString( tagValueMappings[fileIter->c_str()][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 " << *fileIter << " for later analysis (no position information)"; // we already have one occupying this position if ( result.GetBlockFilenames().empty() ) // nothing WITH position information yet { // ==> this is a group of its own, stop processing, come back later result.AddFileToSortedBlock( *fileIter ); StringContainer remainingFiles; remainingFiles.insert( remainingFiles.end(), fileIter+1, files.end() ); result.AddFilesToUnsortedBlock( remainingFiles ); fileFitsIntoPattern = false; break; // no files anymore } else { // ==> this does not match, consider later result.AddFileToUnsortedBlock( *fileIter ); fileFitsIntoPattern = false; continue; // next file } } bool ignoredConversionError(-42); // hard to get here, no graceful way to react thisOrigin = DICOMStringToPoint3D( thisOriginString, ignoredConversionError ); MITK_DEBUG << " " << fileIndex << " " << *fileIter << " at " /* << thisOriginString */ << "(" << thisOrigin[0] << "," << thisOrigin[1] << "," << thisOrigin[2] << ")"; if ( lastOriginInitialized && (thisOrigin == lastOrigin) ) { MITK_DEBUG << " ==> Sort away " << *fileIter << " for separate time step"; // we already have one occupying this position result.AddFileToUnsortedBlock( *fileIter ); fileFitsIntoPattern = false; } else { if (!fromFirstToSecondOriginInitialized && lastOriginInitialized) // calculate vector as soon as possible when we get a new position { fromFirstToSecondOrigin = thisOrigin - lastDifferentOrigin; fromFirstToSecondOriginInitialized = true; // 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 DICOMStringToOrientationVectors( tagValueMappings[fileIter->c_str()][tagImageOrientation], 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 (*fileIter) // 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 if ( tagValueMappings[fileIter->c_str()].find(tagGantryTilt) != tagValueMappings[fileIter->c_str()].end() ) { // read value, compare to calculated angle std::string tiltStr = ConstCharStarToString( tagValueMappings[fileIter->c_str()][tagGantryTilt] ); double angle = atof(tiltStr.c_str()); 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) + // TODO TODO TODO when angle -27 and tiltangle 63, this will never trigger the if-clause... useless check in this case! old bug..?! if ( fabs(angle) - tiltInfo.GetTiltAngleInDegrees() > 0.25) { result.AddFileToUnsortedBlock( *fileIter ); // 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 { result.FlagGantryTilt(); result.AddFileToSortedBlock(*fileIter); // this file is good for current block fileFitsIntoPattern = true; } } else // we cannot check the calculated tilt angle against the one from the dicom header (so we assume we are right) { result.FlagGantryTilt(); result.AddFileToSortedBlock(*fileIter); // this file is good for current block fileFitsIntoPattern = true; } } else // caller does not want tilt compensation OR shearing is more complicated than tilt { result.AddFileToUnsortedBlock( *fileIter ); // sort away for further analysis fileFitsIntoPattern = false; } } else // not sheared { result.AddFileToSortedBlock(*fileIter); // 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) { MITK_DEBUG << " File does not fit into the inter-slice distance pattern (diff = " << norm << ", allowed " << toleratedError << ")."; MITK_DEBUG << " Expected position (" << assumedOrigin[0] << "," << assumedOrigin[1] << "," << assumedOrigin[2] << "), got position (" << thisOrigin[0] << "," << thisOrigin[1] << "," << thisOrigin[2] << ")"; MITK_DEBUG << " ==> Sort away " << *fileIter << " 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( *fileIter ); // sort away for further analysis fileFitsIntoPattern = false; } else { result.AddFileToSortedBlock(*fileIter); // this file is good for current block fileFitsIntoPattern = true; } } else // this should be the very first slice { result.AddFileToSortedBlock(*fileIter); // 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.GetBlockFilenames().size() == 2 ) { result.UndoPrematureGrouping(); } } return result; } DicomSeriesReader::FileNamesGrouping DicomSeriesReader::GetSeries(const StringContainer& files, bool groupImagesWithGantryTilt, const StringContainer &restrictions) { return GetSeries(files, true, groupImagesWithGantryTilt, restrictions); } DicomSeriesReader::FileNamesGrouping DicomSeriesReader::GetSeries(const StringContainer& files, bool sortTo3DPlust, bool groupImagesWithGantryTilt, const StringContainer& /*restrictions*/) { /** assumption about this method: returns a map of uid-like-key --> list(filename) each entry should contain filenames that have images of same - series instance uid (automatically done by GDCMSeriesFileNames - 0020,0037 image orientation (patient) - 0028,0030 pixel spacing (x,y) - 0018,0050 slice thickness */ // use GDCM directly, itk::GDCMSeriesFileNames does not work with GDCM 2 // PART I: scan files for sorting relevant DICOM tags, // separate images that differ in any of those // attributes (they cannot possibly form a 3D block) // scan for relevant tags in dicom files gdcm::Scanner scanner; const gdcm::Tag tagSOPClassUID(0x0008, 0x0016); // SOP class UID scanner.AddTag( tagSOPClassUID ); const gdcm::Tag tagSeriesInstanceUID(0x0020,0x000e); // Series Instance UID scanner.AddTag( tagSeriesInstanceUID ); const gdcm::Tag tagImageOrientation(0x0020, 0x0037); // image orientation scanner.AddTag( tagImageOrientation ); const gdcm::Tag tagPixelSpacing(0x0028, 0x0030); // pixel spacing scanner.AddTag( tagPixelSpacing ); const gdcm::Tag tagImagerPixelSpacing(0x0018, 0x1164); // imager pixel spacing scanner.AddTag( tagImagerPixelSpacing ); const gdcm::Tag tagSliceThickness(0x0018, 0x0050); // slice thickness scanner.AddTag( tagSliceThickness ); const gdcm::Tag tagNumberOfRows(0x0028, 0x0010); // number rows scanner.AddTag( tagNumberOfRows ); const gdcm::Tag tagNumberOfColumns(0x0028, 0x0011); // number cols scanner.AddTag( tagNumberOfColumns ); const gdcm::Tag tagGantryTilt(0x0018, 0x1120); // gantry tilt scanner.AddTag( tagGantryTilt ); const gdcm::Tag tagModality(0x0008, 0x0060); // modality scanner.AddTag( tagModality ); const gdcm::Tag tagNumberOfFrames(0x0028, 0x0008); // number of frames scanner.AddTag( tagNumberOfFrames ); // additional tags read in this scan to allow later analysis // THESE tag are not used for initial separating of files const gdcm::Tag tagImagePositionPatient(0x0020,0x0032); // Image Position (Patient) scanner.AddTag( tagImagePositionPatient ); // TODO add further restrictions from arguments (when anybody asks for it) FileNamesGrouping result; // let GDCM scan files if ( !scanner.Scan( files ) ) { MITK_ERROR << "gdcm::Scanner failed when scanning " << files.size() << " input files."; return result; } // assign files IDs that will separate them for loading into image blocks for (gdcm::Scanner::ConstIterator fileIter = scanner.Begin(); fileIter != scanner.End(); ++fileIter) { if ( std::string(fileIter->first).empty() ) continue; // TODO understand why Scanner has empty string entries if ( std::string(fileIter->first) == std::string("DICOMDIR") ) continue; /* sort out multi-frame if ( scanner.GetValue( fileIter->first , tagNumberOfFrames ) ) { MITK_INFO << "Ignoring " << fileIter->first << " because we cannot handle multi-frame images."; continue; } */ // we const_cast here, because I could not use a map.at() function in CreateMoreUniqueSeriesIdentifier. // doing the same thing with find would make the code less readable. Since we forget the Scanner results // anyway after this function, we can simply tolerate empty map entries introduced by bad operator[] access std::string moreUniqueSeriesId = CreateMoreUniqueSeriesIdentifier( const_cast(fileIter->second) ); result[ moreUniqueSeriesId ].AddFile( fileIter->first ); } // PART II: sort slices spatially (or at least consistently if this is NOT possible, see method) for ( FileNamesGrouping::const_iterator groupIter = result.begin(); groupIter != result.end(); ++groupIter ) { try { result[ groupIter->first ] = ImageBlockDescriptor( SortSeriesSlices( groupIter->second.GetFilenames() ) ); // sort each slice group spatially } catch(...) { MITK_ERROR << "Caught something."; } } // PART III: analyze pre-sorted images for valid blocks (i.e. blocks of equal z-spacing), // separate into multiple blocks if necessary. // // Analysis performs the following steps: // * imitate itk::ImageSeriesReader: use the distance between the first two images as z-spacing // * check what images actually fulfill ITK's z-spacing assumption // * separate all images that fail the test into new blocks, re-iterate analysis for these blocks // * this includes images which DO NOT PROVIDE spatial information, i.e. all images w/o ImagePositionPatient will be loaded separately FileNamesGrouping groupsOf3DPlusTBlocks; // final result of this function for ( FileNamesGrouping::const_iterator groupIter = result.begin(); groupIter != result.end(); ++groupIter ) { FileNamesGrouping groupsOf3DBlocks; // intermediate result for only this group(!) std::map mapOf3DBlockAnalysisResults; StringContainer filesStillToAnalyze = groupIter->second.GetFilenames(); std::string groupUID = groupIter->first; unsigned int subgroup(0); - MITK_DEBUG << "Analyze group " << groupUID; + MITK_DEBUG << "Analyze group " << groupUID << " of " << groupIter->second.GetFilenames().size() << " files"; while (!filesStillToAnalyze.empty()) // repeat until all files are grouped somehow { SliceGroupingAnalysisResult analysisResult = AnalyzeFileForITKImageSeriesReaderSpacingAssumption( filesStillToAnalyze, groupImagesWithGantryTilt, scanner.GetMappings() ); // enhance the UID for additional groups std::stringstream newGroupUID; newGroupUID << groupUID << '.' << subgroup; ImageBlockDescriptor thisBlock( analysisResult.GetBlockFilenames() ); std::string firstFileInBlock = thisBlock.GetFilenames().front(); thisBlock.SetImageBlockUID( newGroupUID.str() ); thisBlock.SetSeriesInstanceUID( DicomSeriesReader::ConstCharStarToString( scanner.GetValue( firstFileInBlock.c_str(), tagSeriesInstanceUID ) ) ); thisBlock.SetHasGantryTiltCorrected( analysisResult.ContainsGantryTilt() ); thisBlock.SetSOPClassUID( DicomSeriesReader::ConstCharStarToString( scanner.GetValue( firstFileInBlock.c_str(), tagSOPClassUID ) ) ); thisBlock.SetNumberOfFrames( ConstCharStarToString( scanner.GetValue( firstFileInBlock.c_str(), tagNumberOfFrames ) ) ); thisBlock.SetModality( DicomSeriesReader::ConstCharStarToString( scanner.GetValue( firstFileInBlock.c_str(), tagModality ) ) ); thisBlock.SetPixelSpacingInformation( DicomSeriesReader::ConstCharStarToString( scanner.GetValue( firstFileInBlock.c_str(), tagPixelSpacing ) ), DicomSeriesReader::ConstCharStarToString( scanner.GetValue( firstFileInBlock.c_str(), tagImagerPixelSpacing ) ) ); thisBlock.SetHasMultipleTimePoints( false ); groupsOf3DBlocks[ newGroupUID.str() ] = thisBlock; //MITK_DEBUG << "Result: sorted 3D group " << newGroupUID.str() << " with " << groupsOf3DBlocks[ newGroupUID.str() ].GetFilenames().size() << " files"; MITK_DEBUG << "Result: sorted 3D group with " << groupsOf3DBlocks[ newGroupUID.str() ].GetFilenames().size() << " files"; StringContainer debugOutputFiles = analysisResult.GetBlockFilenames(); for (StringContainer::const_iterator siter = debugOutputFiles.begin(); siter != debugOutputFiles.end(); ++siter) MITK_DEBUG << " IN " << *siter; ++subgroup; filesStillToAnalyze = analysisResult.GetUnsortedFilenames(); // remember what needs further analysis for (StringContainer::const_iterator siter = filesStillToAnalyze.begin(); siter != filesStillToAnalyze.end(); ++siter) MITK_DEBUG << " OUT " << *siter; } // end of grouping, now post-process groups // PART IV: attempt to group blocks to 3D+t blocks if requested // inspect entries of groupsOf3DBlocks // - if number of files is identical to previous entry, collect for 3D+t block // - as soon as number of files changes from previous entry, record collected blocks as 3D+t block, start a new one, continue // decide whether or not to group 3D blocks into 3D+t blocks where possible if ( !sortTo3DPlust ) { // copy 3D blocks to output groupsOf3DPlusTBlocks.insert( groupsOf3DBlocks.begin(), groupsOf3DBlocks.end() ); } else { // sort 3D+t (as described in "PART IV") MITK_DEBUG << "================================================================================"; MITK_DEBUG << "3D+t analysis:"; unsigned int numberOfFilesInPreviousBlock(0); std::string previousBlockKey; for ( FileNamesGrouping::const_iterator block3DIter = groupsOf3DBlocks.begin(); block3DIter != groupsOf3DBlocks.end(); ++block3DIter ) { unsigned int numberOfFilesInThisBlock = block3DIter->second.GetFilenames().size(); std::string thisBlockKey = block3DIter->first; if (numberOfFilesInPreviousBlock == 0) { numberOfFilesInPreviousBlock = numberOfFilesInThisBlock; groupsOf3DPlusTBlocks[thisBlockKey] = block3DIter->second; MITK_DEBUG << " 3D+t group " << thisBlockKey; previousBlockKey = thisBlockKey; } else { bool identicalOrigins; try { // check whether this and the previous block share a comon origin // TODO should be safe, but a little try/catch or other error handling wouldn't hurt const char *origin_value = scanner.GetValue( groupsOf3DBlocks[thisBlockKey].GetFilenames().front().c_str(), tagImagePositionPatient ), *previous_origin_value = scanner.GetValue( groupsOf3DBlocks[previousBlockKey].GetFilenames().front().c_str(), tagImagePositionPatient ), *destination_value = scanner.GetValue( groupsOf3DBlocks[thisBlockKey].GetFilenames().back().c_str(), tagImagePositionPatient ), *previous_destination_value = scanner.GetValue( groupsOf3DBlocks[previousBlockKey].GetFilenames().back().c_str(), tagImagePositionPatient ); if (!origin_value || !previous_origin_value || !destination_value || !previous_destination_value) { identicalOrigins = false; } else { std::string thisOriginString = ConstCharStarToString( origin_value ); std::string previousOriginString = ConstCharStarToString( previous_origin_value ); // also compare last origin, because this might differ if z-spacing is different std::string thisDestinationString = ConstCharStarToString( destination_value ); std::string previousDestinationString = ConstCharStarToString( previous_destination_value ); identicalOrigins = ( (thisOriginString == previousOriginString) && (thisDestinationString == previousDestinationString) ); } } catch(...) { identicalOrigins = false; } if (identicalOrigins && (numberOfFilesInPreviousBlock == numberOfFilesInThisBlock)) { // group with previous block groupsOf3DPlusTBlocks[previousBlockKey].AddFiles( block3DIter->second.GetFilenames() ); groupsOf3DPlusTBlocks[previousBlockKey].SetHasMultipleTimePoints(true); MITK_DEBUG << " --> group enhanced with another timestep"; } else { // start a new block groupsOf3DPlusTBlocks[thisBlockKey] = block3DIter->second; int numberOfTimeSteps = groupsOf3DPlusTBlocks[previousBlockKey].GetFilenames().size() / numberOfFilesInPreviousBlock; MITK_DEBUG << " ==> group closed with " << numberOfTimeSteps << " time steps"; previousBlockKey = thisBlockKey; MITK_DEBUG << " 3D+t group " << thisBlockKey << " started"; } } numberOfFilesInPreviousBlock = numberOfFilesInThisBlock; } } } MITK_DEBUG << "================================================================================"; MITK_DEBUG << "Summary: "; for ( FileNamesGrouping::const_iterator groupIter = groupsOf3DPlusTBlocks.begin(); groupIter != groupsOf3DPlusTBlocks.end(); ++groupIter ) { ImageBlockDescriptor block = groupIter->second; MITK_DEBUG << " " << block.GetFilenames().size() << " '" << block.GetModality() << "' images (" << block.GetSOPClassUIDAsString() << ") in volume " << block.GetImageBlockUID(); MITK_DEBUG << " (gantry tilt : " << (block.HasGantryTiltCorrected()?"Yes":"No") << "; " "pixel spacing : " << PixelSpacingInterpretationToString( block.GetPixelSpacingType() ) << "; " "3D+t: " << (block.HasMultipleTimePoints()?"Yes":"No") << "; " "reader support: " << ReaderImplementationLevelToString( block.GetReaderImplementationLevel() ) << ")"; StringContainer debugOutputFiles = block.GetFilenames(); for (StringContainer::const_iterator siter = debugOutputFiles.begin(); siter != debugOutputFiles.end(); ++siter) MITK_DEBUG << " F " << *siter; } MITK_DEBUG << "================================================================================"; return groupsOf3DPlusTBlocks; } DicomSeriesReader::FileNamesGrouping DicomSeriesReader::GetSeries(const std::string &dir, bool groupImagesWithGantryTilt, const StringContainer &restrictions) { gdcm::Directory directoryLister; directoryLister.Load( dir.c_str(), false ); // non-recursive return GetSeries(directoryLister.GetFilenames(), groupImagesWithGantryTilt, restrictions); } std::string DicomSeriesReader::CreateSeriesIdentifierPart( gdcm::Scanner::TagToValue& tagValueMap, const gdcm::Tag& tag ) { std::string result; try { result = IDifyTagValue( tagValueMap[ tag ] ? tagValueMap[ tag ] : std::string("") ); } catch (std::exception&) { // we are happy with even nothing, this will just group images of a series //MITK_WARN << "Could not access tag " << tag << ": " << e.what(); } return result; } std::string DicomSeriesReader::CreateMoreUniqueSeriesIdentifier( gdcm::Scanner::TagToValue& tagValueMap ) { const gdcm::Tag tagSeriesInstanceUID(0x0020,0x000e); // Series Instance UID const gdcm::Tag tagImageOrientation(0x0020, 0x0037); // image orientation const gdcm::Tag tagPixelSpacing(0x0028, 0x0030); // pixel spacing const gdcm::Tag tagImagerPixelSpacing(0x0018, 0x1164); // imager pixel spacing const gdcm::Tag tagSliceThickness(0x0018, 0x0050); // slice thickness const gdcm::Tag tagNumberOfRows(0x0028, 0x0010); // number rows const gdcm::Tag tagNumberOfColumns(0x0028, 0x0011); // number cols const gdcm::Tag tagNumberOfFrames(0x0028, 0x0008); // number of frames const char* tagSeriesInstanceUid = tagValueMap[tagSeriesInstanceUID]; if (!tagSeriesInstanceUid) { mitkThrow() << "CreateMoreUniqueSeriesIdentifier() could not access series instance UID. Something is seriously wrong with this image, so stopping here."; } std::string constructedID = tagSeriesInstanceUid; constructedID += CreateSeriesIdentifierPart( tagValueMap, tagNumberOfRows ); constructedID += CreateSeriesIdentifierPart( tagValueMap, tagNumberOfColumns ); constructedID += CreateSeriesIdentifierPart( tagValueMap, tagPixelSpacing ); constructedID += CreateSeriesIdentifierPart( tagValueMap, tagImagerPixelSpacing ); constructedID += CreateSeriesIdentifierPart( tagValueMap, tagSliceThickness ); constructedID += CreateSeriesIdentifierPart( tagValueMap, tagNumberOfFrames ); // be a bit tolerant for orienatation, let only the first few digits matter (http://bugs.mitk.org/show_bug.cgi?id=12263) // NOT constructedID += CreateSeriesIdentifierPart( tagValueMap, tagImageOrientation ); if (tagValueMap.find(tagImageOrientation) != tagValueMap.end()) { bool conversionError(false); Vector3D right; right.Fill(0.0); Vector3D up; right.Fill(0.0); DICOMStringToOrientationVectors( tagValueMap[tagImageOrientation], right, up, conversionError ); //string newstring sprintf(simplifiedOrientationString, "%.3f\\%.3f\\%.3f\\%.3f\\%.3f\\%.3f", right[0], right[1], right[2], up[0], up[1], up[2]); std::ostringstream ss; ss.setf(std::ios::fixed, std::ios::floatfield); ss.precision(5); ss << right[0] << "\\" << right[1] << "\\" << right[2] << "\\" << up[0] << "\\" << up[1] << "\\" << up[2]; std::string simplifiedOrientationString(ss.str()); constructedID += IDifyTagValue( simplifiedOrientationString ); } constructedID.resize( constructedID.length() - 1 ); // cut of trailing '.' return constructedID; } std::string DicomSeriesReader::IDifyTagValue(const std::string& value) { std::string IDifiedValue( value ); if (value.empty()) throw std::logic_error("IDifyTagValue() illegaly called with empty tag value"); // Eliminate non-alnum characters, including whitespace... // that may have been introduced by concats. for(std::size_t i=0; i= 'a' && IDifiedValue[i] <= 'z') || (IDifiedValue[i] >= '0' && IDifiedValue[i] <= '9') || (IDifiedValue[i] >= 'A' && IDifiedValue[i] <= 'Z'))) { IDifiedValue.erase(i, 1); } } IDifiedValue += "."; return IDifiedValue; } DicomSeriesReader::StringContainer DicomSeriesReader::GetSeries(const std::string &dir, const std::string &series_uid, bool groupImagesWithGantryTilt, const StringContainer &restrictions) { FileNamesGrouping allSeries = GetSeries(dir, groupImagesWithGantryTilt, restrictions); StringContainer resultingFileList; for ( FileNamesGrouping::const_iterator idIter = allSeries.begin(); idIter != allSeries.end(); ++idIter ) { if ( idIter->first.find( series_uid ) == 0 ) // this ID starts with given series_uid { return idIter->second.GetFilenames(); } } return resultingFileList; } DicomSeriesReader::StringContainer DicomSeriesReader::SortSeriesSlices(const StringContainer &unsortedFilenames) { /* we CAN expect a group of equal - series instance uid - image orientation - pixel spacing - imager pixel spacing - slice thickness - number of rows/columns (each piece of information except the rows/columns might be missing) sorting with GdcmSortFunction tries its best by sorting by spatial position and more hints (acquisition number, acquisition time, trigger time) but will always produce a sorting by falling back to SOP Instance UID. */ gdcm::Sorter sorter; sorter.SetSortFunction(DicomSeriesReader::GdcmSortFunction); try { sorter.Sort(unsortedFilenames); return sorter.GetFilenames(); } catch(std::logic_error&) { MITK_WARN << "Sorting error. Leaving series unsorted."; return unsortedFilenames; } } bool DicomSeriesReader::GdcmSortFunction(const gdcm::DataSet &ds1, const gdcm::DataSet &ds2) { // This method MUST accept missing location and position information (and all else, too) // because we cannot rely on anything // (restriction on the sentence before: we have to provide consistent sorting, so we // rely on the minimum information all DICOM files need to provide: SOP Instance UID) /* we CAN expect a group of equal - series instance uid - image orientation - pixel spacing - imager pixel spacing - slice thickness - number of rows/columns */ static const gdcm::Tag tagImagePositionPatient(0x0020,0x0032); // Image Position (Patient) static const gdcm::Tag tagImageOrientation(0x0020, 0x0037); // Image Orientation // see if we have Image Position and Orientation if ( ds1.FindDataElement(tagImagePositionPatient) && ds1.FindDataElement(tagImageOrientation) && ds2.FindDataElement(tagImagePositionPatient) && ds2.FindDataElement(tagImageOrientation) ) { gdcm::Attribute<0x0020,0x0032> image_pos1; // Image Position (Patient) gdcm::Attribute<0x0020,0x0037> image_orientation1; // Image Orientation (Patient) image_pos1.Set(ds1); image_orientation1.Set(ds1); gdcm::Attribute<0x0020,0x0032> image_pos2; gdcm::Attribute<0x0020,0x0037> image_orientation2; image_pos2.Set(ds2); image_orientation2.Set(ds2); /* 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 < 6; ++dim) { if ( fabs(image_orientation2[dim] - image_orientation1[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."); } } double normal[3]; normal[0] = image_orientation1[1] * image_orientation1[5] - image_orientation1[2] * image_orientation1[4]; normal[1] = image_orientation1[2] * image_orientation1[3] - image_orientation1[0] * image_orientation1[5]; normal[2] = image_orientation1[0] * image_orientation1[4] - image_orientation1[1] * image_orientation1[3]; double dist1 = 0.0, dist2 = 0.0; // this computes the distance from world origin (0,0,0) ALONG THE NORMAL of the image planes for (unsigned char i = 0u; i < 3u; ++i) { dist1 += normal[i] * image_pos1[i]; dist2 += normal[i] * image_pos2[i]; } // if we can sort by just comparing the distance, we do exactly that if ( fabs(dist1 - dist2) >= mitk::eps) { // default: compare position return dist1 < dist2; } else // we need to check more properties to distinguish slices { // try to sort by Acquisition Number static const gdcm::Tag tagAcquisitionNumber(0x0020, 0x0012); if (ds1.FindDataElement(tagAcquisitionNumber) && ds2.FindDataElement(tagAcquisitionNumber)) { gdcm::Attribute<0x0020,0x0012> acquisition_number1; // Acquisition number gdcm::Attribute<0x0020,0x0012> acquisition_number2; acquisition_number1.Set(ds1); acquisition_number2.Set(ds2); if (acquisition_number1 != acquisition_number2) { return acquisition_number1 < acquisition_number2; } else // neither position nor acquisition number are good for sorting, so check more { // try to sort by Acquisition Time static const gdcm::Tag tagAcquisitionTime(0x0008, 0x0032); if (ds1.FindDataElement(tagAcquisitionTime) && ds2.FindDataElement(tagAcquisitionTime)) { gdcm::Attribute<0x0008,0x0032> acquisition_time1; // Acquisition time gdcm::Attribute<0x0008,0x0032> acquisition_time2; acquisition_time1.Set(ds1); acquisition_time2.Set(ds2); if (acquisition_time1 != acquisition_time2) { return acquisition_time1 < acquisition_time2; } else // we gave up on image position, acquisition number and acquisition time now { // let's try trigger time static const gdcm::Tag tagTriggerTime(0x0018, 0x1060); if (ds1.FindDataElement(tagTriggerTime) && ds2.FindDataElement(tagTriggerTime)) { gdcm::Attribute<0x0018,0x1060> trigger_time1; // Trigger time gdcm::Attribute<0x0018,0x1060> trigger_time2; trigger_time1.Set(ds1); trigger_time2.Set(ds2); if (trigger_time1 != trigger_time2) { return trigger_time1 < trigger_time2; } // ELSE! // for this and many previous ifs we fall through if nothing lets us sort } // . } // . } // . } } } } // . // LAST RESORT: all valuable information for sorting is missing. // Sort by some meaningless but unique identifiers to satisfy the sort function static const gdcm::Tag tagSOPInstanceUID(0x0008, 0x0018); if (ds1.FindDataElement(tagSOPInstanceUID) && ds2.FindDataElement(tagSOPInstanceUID)) { MITK_DEBUG << "Dicom images are missing attributes for a meaningful sorting, falling back to SOP instance UID comparison."; gdcm::Attribute<0x0008,0x0018> SOPInstanceUID1; // SOP instance UID is mandatory and unique gdcm::Attribute<0x0008,0x0018> SOPInstanceUID2; SOPInstanceUID1.Set(ds1); SOPInstanceUID2.Set(ds2); return SOPInstanceUID1 < SOPInstanceUID2; } else { // no DICOM file should really come down here, this should only be reached with unskillful and unlucky manipulation of files std::string error_message("Malformed DICOM images, which do not even contain a SOP Instance UID."); MITK_ERROR << error_message; throw std::logic_error( error_message ); } } std::string DicomSeriesReader::GetConfigurationString() { std::stringstream configuration; configuration << "MITK_USE_GDCMIO: "; configuration << "true"; configuration << "\n"; configuration << "GDCM_VERSION: "; #ifdef GDCM_MAJOR_VERSION configuration << GDCM_VERSION; #endif //configuration << "\n"; return configuration.str(); } void DicomSeriesReader::CopyMetaDataToImageProperties(StringContainer filenames, const gdcm::Scanner::MappingType &tagValueMappings_, DcmIoType *io, const ImageBlockDescriptor& blockInfo, Image *image) { std::list imageBlock; imageBlock.push_back(filenames); CopyMetaDataToImageProperties(imageBlock, tagValueMappings_, io, blockInfo, image); } void DicomSeriesReader::CopyMetaDataToImageProperties( std::list imageBlock, const gdcm::Scanner::MappingType& tagValueMappings_, DcmIoType* io, const ImageBlockDescriptor& blockInfo, Image* image) { if (!io || !image) return; StringLookupTable filesForSlices; StringLookupTable sliceLocationForSlices; StringLookupTable instanceNumberForSlices; StringLookupTable SOPInstanceNumberForSlices; gdcm::Scanner::MappingType& tagValueMappings = const_cast(tagValueMappings_); //DICOM tags which should be added to the image properties const gdcm::Tag tagSliceLocation(0x0020, 0x1041); // slice location const gdcm::Tag tagInstanceNumber(0x0020, 0x0013); // (image) instance number const gdcm::Tag tagSOPInstanceNumber(0x0008, 0x0018); // SOP instance number unsigned int timeStep(0); std::string propertyKeySliceLocation = "dicom.image.0020.1041"; std::string propertyKeyInstanceNumber = "dicom.image.0020.0013"; std::string propertyKeySOPInstanceNumber = "dicom.image.0008.0018"; // tags for each image for ( std::list::iterator i = imageBlock.begin(); i != imageBlock.end(); i++, timeStep++ ) { const StringContainer& files = (*i); unsigned int slice(0); for ( StringContainer::const_iterator fIter = files.begin(); fIter != files.end(); ++fIter, ++slice ) { filesForSlices.SetTableValue( slice, *fIter ); gdcm::Scanner::TagToValue tagValueMapForFile = tagValueMappings[fIter->c_str()]; if(tagValueMapForFile.find(tagSliceLocation) != tagValueMapForFile.end()) sliceLocationForSlices.SetTableValue(slice, tagValueMapForFile[tagSliceLocation]); if(tagValueMapForFile.find(tagInstanceNumber) != tagValueMapForFile.end()) instanceNumberForSlices.SetTableValue(slice, tagValueMapForFile[tagInstanceNumber]); if(tagValueMapForFile.find(tagSOPInstanceNumber) != tagValueMapForFile.end()) SOPInstanceNumberForSlices.SetTableValue(slice, tagValueMapForFile[tagSOPInstanceNumber]); } image->SetProperty( "files", StringLookupTableProperty::New( filesForSlices ) ); //If more than one time step add postfix ".t" + timestep if(timeStep != 0) { std::ostringstream postfix; postfix << ".t" << timeStep; propertyKeySliceLocation.append(postfix.str()); propertyKeyInstanceNumber.append(postfix.str()); propertyKeySOPInstanceNumber.append(postfix.str()); } image->SetProperty( propertyKeySliceLocation.c_str(), StringLookupTableProperty::New( sliceLocationForSlices ) ); image->SetProperty( propertyKeyInstanceNumber.c_str(), StringLookupTableProperty::New( instanceNumberForSlices ) ); image->SetProperty( propertyKeySOPInstanceNumber.c_str(), StringLookupTableProperty::New( SOPInstanceNumberForSlices ) ); } // Copy tags for series, study, patient level (leave interpretation to application). // These properties will be copied to the DataNode by DicomSeriesReader. // tags for the series (we just use the one that ITK copied to its dictionary (proably that of the last slice) const itk::MetaDataDictionary& dict = io->GetMetaDataDictionary(); const TagToPropertyMapType& propertyLookup = DicomSeriesReader::GetDICOMTagsToMITKPropertyMap(); itk::MetaDataDictionary::ConstIterator dictIter = dict.Begin(); while ( dictIter != dict.End() ) { //MITK_DEBUG << "Key " << dictIter->first; std::string value; if ( itk::ExposeMetaData( dict, dictIter->first, value ) ) { //MITK_DEBUG << "Value " << value; TagToPropertyMapType::const_iterator valuePosition = propertyLookup.find( dictIter->first ); if ( valuePosition != propertyLookup.end() ) { std::string propertyKey = valuePosition->second; //MITK_DEBUG << "--> " << propertyKey; image->SetProperty( propertyKey.c_str(), StringProperty::New(value) ); } } else { MITK_WARN << "Tag " << dictIter->first << " not read as string as expected. Ignoring..." ; } ++dictIter; } // copy imageblockdescriptor as properties image->SetProperty("dicomseriesreader.SOPClass", StringProperty::New(blockInfo.GetSOPClassUIDAsString())); image->SetProperty("dicomseriesreader.ReaderImplementationLevelString", StringProperty::New(ReaderImplementationLevelToString( blockInfo.GetReaderImplementationLevel() ))); image->SetProperty("dicomseriesreader.ReaderImplementationLevel", GenericProperty::New( blockInfo.GetReaderImplementationLevel() )); image->SetProperty("dicomseriesreader.PixelSpacingInterpretationString", StringProperty::New(PixelSpacingInterpretationToString( blockInfo.GetPixelSpacingType() ))); image->SetProperty("dicomseriesreader.PixelSpacingInterpretation", GenericProperty::New(blockInfo.GetPixelSpacingType())); image->SetProperty("dicomseriesreader.MultiFrameImage", BoolProperty::New(blockInfo.IsMultiFrameImage())); image->SetProperty("dicomseriesreader.GantyTiltCorrected", BoolProperty::New(blockInfo.HasGantryTiltCorrected())); image->SetProperty("dicomseriesreader.3D+t", BoolProperty::New(blockInfo.HasMultipleTimePoints())); } void DicomSeriesReader::FixSpacingInformation( mitk::Image* image, const ImageBlockDescriptor& imageBlockDescriptor ) { // spacing provided by ITK/GDCM Vector3D imageSpacing = image->GetGeometry()->GetSpacing(); ScalarType imageSpacingX = imageSpacing[0]; ScalarType imageSpacingY = imageSpacing[1]; // spacing as desired by MITK (preference for "in patient", else "on detector", or "1.0/1.0") ScalarType desiredSpacingX = imageSpacingX; ScalarType desiredSpacingY = imageSpacingY; imageBlockDescriptor.GetDesiredMITKImagePixelSpacing( desiredSpacingX, desiredSpacingY ); MITK_DEBUG << "Loaded spacing: " << imageSpacingX << "/" << imageSpacingY; MITK_DEBUG << "Corrected spacing: " << desiredSpacingX << "/" << desiredSpacingY; imageSpacing[0] = desiredSpacingX; imageSpacing[1] = desiredSpacingY; image->GetGeometry()->SetSpacing( imageSpacing ); } void DicomSeriesReader::LoadDicom(const StringContainer &filenames, DataNode &node, bool sort, bool load4D, bool correctTilt, UpdateCallBackMethod callback, Image::Pointer preLoadedImageBlock) { const char* previousCLocale = setlocale(LC_NUMERIC, NULL); setlocale(LC_NUMERIC, "C"); std::locale previousCppLocale( std::cin.getloc() ); std::locale l( "C" ); std::cin.imbue(l); ImageBlockDescriptor imageBlockDescriptor; const gdcm::Tag tagImagePositionPatient(0x0020,0x0032); // Image Position (Patient) const gdcm::Tag tagImageOrientation(0x0020, 0x0037); // Image Orientation const gdcm::Tag tagSeriesInstanceUID(0x0020, 0x000e); // Series Instance UID const gdcm::Tag tagSOPClassUID(0x0008, 0x0016); // SOP class UID const gdcm::Tag tagModality(0x0008, 0x0060); // modality const gdcm::Tag tagPixelSpacing(0x0028, 0x0030); // pixel spacing const gdcm::Tag tagImagerPixelSpacing(0x0018, 0x1164); // imager pixel spacing const gdcm::Tag tagNumberOfFrames(0x0028, 0x0008); // number of frames try { Image::Pointer image = preLoadedImageBlock.IsNull() ? Image::New() : preLoadedImageBlock; CallbackCommand *command = callback ? new CallbackCommand(callback) : 0; bool initialize_node = false; /* special case for Philips 3D+t ultrasound images */ if ( DicomSeriesReader::IsPhilips3DDicom(filenames.front().c_str()) ) { // TODO what about imageBlockDescriptor? // TODO what about preLoadedImageBlock? ReadPhilips3DDicom(filenames.front().c_str(), image); initialize_node = true; } else { /* default case: assume "normal" image blocks, possibly 3D+t */ bool canLoadAs4D(true); gdcm::Scanner scanner; ScanForSliceInformation(filenames, scanner); // need non-const access for map gdcm::Scanner::MappingType& tagValueMappings = const_cast(scanner.GetMappings()); std::list imageBlocks = SortIntoBlocksFor3DplusT( filenames, tagValueMappings, sort, canLoadAs4D ); unsigned int volume_count = imageBlocks.size(); imageBlockDescriptor.SetSeriesInstanceUID( DicomSeriesReader::ConstCharStarToString( scanner.GetValue( filenames.front().c_str(), tagSeriesInstanceUID ) ) ); imageBlockDescriptor.SetSOPClassUID( DicomSeriesReader::ConstCharStarToString( scanner.GetValue( filenames.front().c_str(), tagSOPClassUID ) ) ); imageBlockDescriptor.SetModality( DicomSeriesReader::ConstCharStarToString( scanner.GetValue( filenames.front().c_str(), tagModality ) ) ); imageBlockDescriptor.SetNumberOfFrames( ConstCharStarToString( scanner.GetValue( filenames.front().c_str(), tagNumberOfFrames ) ) ); imageBlockDescriptor.SetPixelSpacingInformation( ConstCharStarToString( scanner.GetValue( filenames.front().c_str(), tagPixelSpacing ) ), ConstCharStarToString( scanner.GetValue( filenames.front().c_str(), tagImagerPixelSpacing ) ) ); GantryTiltInformation tiltInfo; // check possibility of a single slice with many timesteps. In this case, don't check for tilt, no second slice possible if ( !imageBlocks.empty() && imageBlocks.front().size() > 1 && correctTilt) { // check tiltedness here, potentially fixup ITK's loading result by shifting slice contents // check first and last position slice from tags, make some calculations to detect tilt std::string firstFilename(imageBlocks.front().front()); // calculate from first and last slice to minimize rounding errors std::string secondFilename(imageBlocks.front().back()); std::string imagePosition1( ConstCharStarToString( tagValueMappings[ firstFilename.c_str() ][ tagImagePositionPatient ] ) ); std::string imageOrientation( ConstCharStarToString( tagValueMappings[ firstFilename.c_str() ][ tagImageOrientation ] ) ); std::string imagePosition2( ConstCharStarToString( tagValueMappings[secondFilename.c_str() ][ tagImagePositionPatient ] ) ); bool ignoredConversionError(-42); // hard to get here, no graceful way to react Point3D origin1( DICOMStringToPoint3D( imagePosition1, ignoredConversionError ) ); Point3D origin2( DICOMStringToPoint3D( imagePosition2, ignoredConversionError ) ); 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 DICOMStringToOrientationVectors( imageOrientation, right, up, ignoredConversionError ); tiltInfo = GantryTiltInformation ( origin1, origin2, right, up, filenames.size()-1 ); correctTilt = tiltInfo.IsSheared() && tiltInfo.IsRegularGantryTilt(); } else { correctTilt = false; // we CANNOT do that } imageBlockDescriptor.SetHasGantryTiltCorrected( correctTilt ); if (volume_count == 1 || !canLoadAs4D || !load4D) { DcmIoType::Pointer io; image = MultiplexLoadDICOMByITK( imageBlocks.front(), correctTilt, tiltInfo, io, command, preLoadedImageBlock ); // load first 3D block imageBlockDescriptor.AddFiles(imageBlocks.front()); // only the first part is loaded imageBlockDescriptor.SetHasMultipleTimePoints( false ); FixSpacingInformation( image, imageBlockDescriptor ); CopyMetaDataToImageProperties( imageBlocks.front(), scanner.GetMappings(), io, imageBlockDescriptor, image); initialize_node = true; } else if (volume_count > 1) { imageBlockDescriptor.AddFiles(filenames); // all is loaded imageBlockDescriptor.SetHasMultipleTimePoints( true ); DcmIoType::Pointer io; image = MultiplexLoadDICOMByITK4D( imageBlocks, imageBlockDescriptor, correctTilt, tiltInfo, io, command, preLoadedImageBlock ); initialize_node = true; } } if (initialize_node) { // forward some image properties to node node.GetPropertyList()->ConcatenatePropertyList( image->GetPropertyList(), true ); node.SetData( image ); setlocale(LC_NUMERIC, previousCLocale); std::cin.imbue(previousCppLocale); } MITK_DEBUG << "--------------------------------------------------------------------------------"; MITK_DEBUG << "DICOM files loaded (from series UID " << imageBlockDescriptor.GetSeriesInstanceUID() << "):"; MITK_DEBUG << " " << imageBlockDescriptor.GetFilenames().size() << " '" << imageBlockDescriptor.GetModality() << "' files (" << imageBlockDescriptor.GetSOPClassUIDAsString() << ") loaded into 1 mitk::Image"; MITK_DEBUG << " multi-frame: " << (imageBlockDescriptor.IsMultiFrameImage()?"Yes":"No"); MITK_DEBUG << " reader support: " << ReaderImplementationLevelToString(imageBlockDescriptor.GetReaderImplementationLevel()); MITK_DEBUG << " pixel spacing type: " << PixelSpacingInterpretationToString( imageBlockDescriptor.GetPixelSpacingType() ) << " " << image->GetGeometry()->GetSpacing()[0] << "/" << image->GetGeometry()->GetSpacing()[0]; MITK_DEBUG << " gantry tilt corrected: " << (imageBlockDescriptor.HasGantryTiltCorrected()?"Yes":"No"); MITK_DEBUG << " 3D+t: " << (imageBlockDescriptor.HasMultipleTimePoints()?"Yes":"No"); MITK_DEBUG << "--------------------------------------------------------------------------------"; } catch (std::exception& e) { // reset locale then throw up setlocale(LC_NUMERIC, previousCLocale); std::cin.imbue(previousCppLocale); MITK_DEBUG << "Caught exception in DicomSeriesReader::LoadDicom"; throw e; } } void DicomSeriesReader::ScanForSliceInformation(const StringContainer &filenames, gdcm::Scanner& scanner) { const gdcm::Tag tagImagePositionPatient(0x0020,0x0032); //Image position (Patient) scanner.AddTag(tagImagePositionPatient); const gdcm::Tag tagSeriesInstanceUID(0x0020, 0x000e); // Series Instance UID scanner.AddTag(tagSeriesInstanceUID); const gdcm::Tag tagImageOrientation(0x0020,0x0037); //Image orientation scanner.AddTag(tagImageOrientation); const gdcm::Tag tagSliceLocation(0x0020, 0x1041); // slice location scanner.AddTag( tagSliceLocation ); const gdcm::Tag tagInstanceNumber(0x0020, 0x0013); // (image) instance number scanner.AddTag( tagInstanceNumber ); const gdcm::Tag tagSOPInstanceNumber(0x0008, 0x0018); // SOP instance number scanner.AddTag( tagSOPInstanceNumber ); const gdcm::Tag tagPixelSpacing(0x0028, 0x0030); // Pixel Spacing scanner.AddTag( tagPixelSpacing ); const gdcm::Tag tagImagerPixelSpacing(0x0018, 0x1164); // Imager Pixel Spacing scanner.AddTag( tagImagerPixelSpacing ); const gdcm::Tag tagModality(0x0008, 0x0060); // Modality scanner.AddTag( tagModality ); const gdcm::Tag tagSOPClassUID(0x0008, 0x0016); // SOP Class UID scanner.AddTag( tagSOPClassUID ); const gdcm::Tag tagNumberOfFrames(0x0028, 0x0008); // number of frames scanner.AddTag( tagNumberOfFrames ); scanner.Scan(filenames); // make available image information for each file } std::list DicomSeriesReader::SortIntoBlocksFor3DplusT( const StringContainer& presortedFilenames, const gdcm::Scanner::MappingType& tagValueMappings, bool /*sort*/, bool& canLoadAs4D ) { std::list imageBlocks; // ignore sort request, because most likely re-sorting is now needed due to changes in GetSeries(bug #8022) StringContainer sorted_filenames = DicomSeriesReader::SortSeriesSlices(presortedFilenames); std::string firstPosition; unsigned int numberOfBlocks(0); // number of 3D image blocks static const gdcm::Tag tagImagePositionPatient(0x0020,0x0032); //Image position (Patient) // loop files to determine number of image blocks for (StringContainer::const_iterator fileIter = sorted_filenames.begin(); fileIter != sorted_filenames.end(); ++fileIter) { gdcm::Scanner::TagToValue tagToValueMap = tagValueMappings.find( fileIter->c_str() )->second; if(tagToValueMap.find(tagImagePositionPatient) == tagToValueMap.end()) { // we expect to get images w/ missing position information ONLY as separated blocks. assert( presortedFilenames.size() == 1 ); numberOfBlocks = 1; break; } std::string position = tagToValueMap.find(tagImagePositionPatient)->second; MITK_DEBUG << " " << *fileIter << " at " << position; if (firstPosition.empty()) { firstPosition = position; } if ( position == firstPosition ) { ++numberOfBlocks; } else { break; // enough information to know the number of image blocks } } MITK_DEBUG << " ==> Assuming " << numberOfBlocks << " time steps"; if (numberOfBlocks == 0) return imageBlocks; // only possible if called with no files // loop files to sort them into image blocks unsigned int numberOfExpectedSlices(0); for (unsigned int block = 0; block < numberOfBlocks; ++block) { StringContainer filesOfCurrentBlock; for ( StringContainer::const_iterator fileIter = sorted_filenames.begin() + block; fileIter != sorted_filenames.end(); //fileIter += numberOfBlocks) // TODO shouldn't this work? give invalid iterators on first attempts ) { filesOfCurrentBlock.push_back( *fileIter ); for (unsigned int b = 0; b < numberOfBlocks; ++b) { if (fileIter != sorted_filenames.end()) ++fileIter; } } imageBlocks.push_back(filesOfCurrentBlock); if (block == 0) { numberOfExpectedSlices = filesOfCurrentBlock.size(); } else { if (filesOfCurrentBlock.size() != numberOfExpectedSlices) { MITK_WARN << "DicomSeriesReader expected " << numberOfBlocks << " image blocks of " << numberOfExpectedSlices << " images each. Block " << block << " got " << filesOfCurrentBlock.size() << " instead. Cannot load this as 3D+t"; // TODO implement recovery (load as many slices 3D+t as much as possible) canLoadAs4D = false; } } } return imageBlocks; } Image::Pointer DicomSeriesReader ::MultiplexLoadDICOMByITK(const StringContainer& filenames, bool correctTilt, const GantryTiltInformation& tiltInfo, DcmIoType::Pointer& io, CallbackCommand* command, Image::Pointer preLoadedImageBlock) { io = DcmIoType::New(); io->SetFileName(filenames.front().c_str()); io->ReadImageInformation(); if (io->GetPixelType() == itk::ImageIOBase::SCALAR) { return MultiplexLoadDICOMByITKScalar(filenames, correctTilt, tiltInfo, io, command ,preLoadedImageBlock); } else if (io->GetPixelType() == itk::ImageIOBase::RGB) { return MultiplexLoadDICOMByITKRGBPixel(filenames, correctTilt, tiltInfo, io, command ,preLoadedImageBlock); } else { return NULL; } } Image::Pointer DicomSeriesReader ::MultiplexLoadDICOMByITK4D( std::list& imageBlocks, ImageBlockDescriptor imageBlockDescriptor, bool correctTilt, const GantryTiltInformation& tiltInfo, DcmIoType::Pointer& io, CallbackCommand* command, Image::Pointer preLoadedImageBlock) { io = DcmIoType::New(); io->SetFileName(imageBlocks.front().front().c_str()); io->ReadImageInformation(); if (io->GetPixelType() == itk::ImageIOBase::SCALAR) { return MultiplexLoadDICOMByITK4DScalar(imageBlocks, imageBlockDescriptor, correctTilt, tiltInfo, io, command ,preLoadedImageBlock); } else if (io->GetPixelType() == itk::ImageIOBase::RGB) { return MultiplexLoadDICOMByITK4DRGBPixel(imageBlocks, imageBlockDescriptor, correctTilt, tiltInfo, io, command ,preLoadedImageBlock); } else { return NULL; } } } // end namespace mitk diff --git a/Core/Code/Rendering/mitkImageVtkMapper2D.cpp b/Core/Code/Rendering/mitkImageVtkMapper2D.cpp index 7d6a88dfef..fb4bd00898 100644 --- a/Core/Code/Rendering/mitkImageVtkMapper2D.cpp +++ b/Core/Code/Rendering/mitkImageVtkMapper2D.cpp @@ -1,1077 +1,1078 @@ /*=================================================================== 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. ===================================================================*/ //MITK #include #include #include #include #include #include #include #include #include #include #include #include //#include #include #include "mitkImageStatisticsHolder.h" #include "mitkPlaneClipping.h" //MITK Rendering #include "mitkImageVtkMapper2D.h" #include "vtkMitkThickSlicesFilter.h" #include "vtkMitkLevelWindowFilter.h" #include "vtkNeverTranslucentTexture.h" //VTK #include #include #include #include #include #include #include #include #include #include #include #include #include #include //ITK #include #include mitk::ImageVtkMapper2D::ImageVtkMapper2D() { } mitk::ImageVtkMapper2D::~ImageVtkMapper2D() { //The 3D RW Mapper (Geometry2DDataVtkMapper3D) is listening to this event, //in order to delete the images from the 3D RW. this->InvokeEvent( itk::DeleteEvent() ); } //set the two points defining the textured plane according to the dimension and spacing void mitk::ImageVtkMapper2D::GeneratePlane(mitk::BaseRenderer* renderer, vtkFloatingPointType planeBounds[6]) { LocalStorage *localStorage = m_LSH.GetLocalStorage(renderer); float depth = this->CalculateLayerDepth(renderer); //Set the origin to (xMin; yMin; depth) of the plane. This is necessary for obtaining the correct //plane size in crosshair rotation and swivel mode. localStorage->m_Plane->SetOrigin(planeBounds[0], planeBounds[2], depth); //These two points define the axes of the plane in combination with the origin. //Point 1 is the x-axis and point 2 the y-axis. //Each plane is transformed according to the view (axial, coronal and saggital) afterwards. localStorage->m_Plane->SetPoint1(planeBounds[1] , planeBounds[2], depth); //P1: (xMax, yMin, depth) localStorage->m_Plane->SetPoint2(planeBounds[0], planeBounds[3], depth); //P2: (xMin, yMax, depth) } float mitk::ImageVtkMapper2D::CalculateLayerDepth(mitk::BaseRenderer* renderer) { //get the clipping range to check how deep into z direction we can render images double maxRange = renderer->GetVtkRenderer()->GetActiveCamera()->GetClippingRange()[1]; //Due to a VTK bug, we cannot use the whole clipping range. /100 is empirically determined float depth = -maxRange*0.01; // divide by 100 int layer = 0; GetDataNode()->GetIntProperty( "layer", layer, renderer); //add the layer property for each image to render images with a higher layer on top of the others depth += layer*10; //*10: keep some room for each image (e.g. for QBalls in between) if(depth > 0.0f) { depth = 0.0f; MITK_WARN << "Layer value exceeds clipping range. Set to minimum instead."; } return depth; } const mitk::Image* mitk::ImageVtkMapper2D::GetInput( void ) { return static_cast< const mitk::Image * >( GetDataNode()->GetData() ); } vtkProp* mitk::ImageVtkMapper2D::GetVtkProp(mitk::BaseRenderer* renderer) { //return the actor corresponding to the renderer return m_LSH.GetLocalStorage(renderer)->m_Actors; } void mitk::ImageVtkMapper2D::GenerateDataForRenderer( mitk::BaseRenderer *renderer ) { LocalStorage *localStorage = m_LSH.GetLocalStorage(renderer); mitk::Image *input = const_cast< mitk::Image * >( this->GetInput() ); mitk::DataNode* datanode = this->GetDataNode(); if ( input == NULL || input->IsInitialized() == false ) { return; } //check if there is a valid worldGeometry const Geometry2D *worldGeometry = renderer->GetCurrentWorldGeometry2D(); if( ( worldGeometry == NULL ) || ( !worldGeometry->IsValid() ) || ( !worldGeometry->HasReferenceGeometry() )) { return; } input->Update(); // early out if there is no intersection of the current rendering geometry // and the geometry of the image that is to be rendered. if ( !RenderingGeometryIntersectsImage( worldGeometry, input->GetSlicedGeometry() ) ) { // set image to NULL, to clear the texture in 3D, because // the latest image is used there if the plane is out of the geometry // see bug-13275 localStorage->m_ReslicedImage = NULL; localStorage->m_Mapper->SetInput( localStorage->m_EmptyPolyData ); return; } //set main input for ExtractSliceFilter localStorage->m_Reslicer->SetInput(input); localStorage->m_Reslicer->SetWorldGeometry(worldGeometry); localStorage->m_Reslicer->SetTimeStep( this->GetTimestep() ); //set the transformation of the image to adapt reslice axis localStorage->m_Reslicer->SetResliceTransformByGeometry( input->GetTimeSlicedGeometry()->GetGeometry3D( this->GetTimestep() ) ); //is the geometry of the slice based on the input image or the worldgeometry? bool inPlaneResampleExtentByGeometry = false; datanode->GetBoolProperty("in plane resample extent by geometry", inPlaneResampleExtentByGeometry, renderer); localStorage->m_Reslicer->SetInPlaneResampleExtentByGeometry(inPlaneResampleExtentByGeometry); // Initialize the interpolation mode for resampling; switch to nearest // neighbor if the input image is too small. if ( (input->GetDimension() >= 3) && (input->GetDimension(2) > 1) ) { VtkResliceInterpolationProperty *resliceInterpolationProperty; datanode->GetProperty( resliceInterpolationProperty, "reslice interpolation" ); int interpolationMode = VTK_RESLICE_NEAREST; if ( resliceInterpolationProperty != NULL ) { interpolationMode = resliceInterpolationProperty->GetInterpolation(); } switch ( interpolationMode ) { case VTK_RESLICE_NEAREST: localStorage->m_Reslicer->SetInterpolationMode(ExtractSliceFilter::RESLICE_NEAREST); break; case VTK_RESLICE_LINEAR: localStorage->m_Reslicer->SetInterpolationMode(ExtractSliceFilter::RESLICE_LINEAR); break; case VTK_RESLICE_CUBIC: localStorage->m_Reslicer->SetInterpolationMode(ExtractSliceFilter::RESLICE_CUBIC); break; } } else { localStorage->m_Reslicer->SetInterpolationMode(ExtractSliceFilter::RESLICE_NEAREST); } //set the vtk output property to true, makes sure that no unneeded mitk image convertion //is done. localStorage->m_Reslicer->SetVtkOutputRequest(true); //Thickslicing int thickSlicesMode = 0; int thickSlicesNum = 1; // Thick slices parameters if( input->GetPixelType().GetNumberOfComponents() == 1 ) // for now only single component are allowed { DataNode *dn=renderer->GetCurrentWorldGeometry2DNode(); if(dn) { ResliceMethodProperty *resliceMethodEnumProperty=0; if( dn->GetProperty( resliceMethodEnumProperty, "reslice.thickslices" ) && resliceMethodEnumProperty ) thickSlicesMode = resliceMethodEnumProperty->GetValueAsId(); IntProperty *intProperty=0; if( dn->GetProperty( intProperty, "reslice.thickslices.num" ) && intProperty ) { thickSlicesNum = intProperty->GetValue(); if(thickSlicesNum < 1) thickSlicesNum=1; if(thickSlicesNum > 10) thickSlicesNum=10; } } else { MITK_WARN << "no associated widget plane data tree node found"; } } const PlaneGeometry *planeGeometry = dynamic_cast< const PlaneGeometry * >( worldGeometry ); if(thickSlicesMode > 0) { double dataZSpacing = 1.0; Vector3D normInIndex, normal; if ( planeGeometry != NULL ){ normal = planeGeometry->GetNormal(); }else{ const mitk::AbstractTransformGeometry* abstractGeometry = dynamic_cast< const AbstractTransformGeometry * >(worldGeometry); if(abstractGeometry != NULL) normal = abstractGeometry->GetPlane()->GetNormal(); else return; //no fitting geometry set } normal.Normalize(); input->GetTimeSlicedGeometry()->GetGeometry3D( this->GetTimestep() )->WorldToIndex( normal, normInIndex ); dataZSpacing = 1.0 / normInIndex.GetNorm(); localStorage->m_Reslicer->SetOutputDimensionality( 3 ); localStorage->m_Reslicer->SetOutputSpacingZDirection(dataZSpacing); localStorage->m_Reslicer->SetOutputExtentZDirection( -thickSlicesNum, 0+thickSlicesNum ); // Do the reslicing. Modified() is called to make sure that the reslicer is // executed even though the input geometry information did not change; this // is necessary when the input /em data, but not the /em geometry changes. localStorage->m_TSFilter->SetThickSliceMode( thickSlicesMode-1 ); localStorage->m_TSFilter->SetInput( localStorage->m_Reslicer->GetVtkOutput() ); //vtkFilter=>mitkFilter=>vtkFilter update mechanism will fail without calling manually localStorage->m_Reslicer->Modified(); localStorage->m_Reslicer->Update(); localStorage->m_TSFilter->Modified(); localStorage->m_TSFilter->Update(); localStorage->m_ReslicedImage = localStorage->m_TSFilter->GetOutput(); } else { //this is needed when thick mode was enable bevore. These variable have to be reset to default values localStorage->m_Reslicer->SetOutputDimensionality( 2 ); localStorage->m_Reslicer->SetOutputSpacingZDirection(1.0); localStorage->m_Reslicer->SetOutputExtentZDirection( 0, 0 ); localStorage->m_Reslicer->Modified(); //start the pipeline with updating the largest possible, needed if the geometry of the input has changed localStorage->m_Reslicer->UpdateLargestPossibleRegion(); localStorage->m_ReslicedImage = localStorage->m_Reslicer->GetVtkOutput(); } // Bounds information for reslicing (only reuqired if reference geometry // is present) //this used for generating a vtkPLaneSource with the right size vtkFloatingPointType sliceBounds[6]; for ( int i = 0; i < 6; ++i ) { sliceBounds[i] = 0.0; } localStorage->m_Reslicer->GetClippedPlaneBounds(sliceBounds); //get the spacing of the slice localStorage->m_mmPerPixel = localStorage->m_Reslicer->GetOutputSpacing(); // calculate minimum bounding rect of IMAGE in texture { vtkFloatingPointType textureClippingBounds[6]; for ( int i = 0; i < 6; ++i ) { textureClippingBounds[i] = 0.0; } // Calculate the actual bounds of the transformed plane clipped by the // dataset bounding box; this is required for drawing the texture at the // correct position during 3D mapping. mitk::PlaneClipping::CalculateClippedPlaneBounds( input->GetGeometry(), planeGeometry, textureClippingBounds ); textureClippingBounds[0] = static_cast< int >( textureClippingBounds[0] / localStorage->m_mmPerPixel[0] + 0.5 ); textureClippingBounds[1] = static_cast< int >( textureClippingBounds[1] / localStorage->m_mmPerPixel[0] + 0.5 ); textureClippingBounds[2] = static_cast< int >( textureClippingBounds[2] / localStorage->m_mmPerPixel[1] + 0.5 ); textureClippingBounds[3] = static_cast< int >( textureClippingBounds[3] / localStorage->m_mmPerPixel[1] + 0.5 ); //clipping bounds for cutting the image localStorage->m_LevelWindowFilter->SetClippingBounds(textureClippingBounds); } //get the number of scalar components to distinguish between different image types int numberOfComponents = localStorage->m_ReslicedImage->GetNumberOfScalarComponents(); //get the binary property bool binary = false; bool binaryOutline = false; datanode->GetBoolProperty( "binary", binary, renderer ); if(binary) //binary image { datanode->GetBoolProperty( "outline binary", binaryOutline, renderer ); if(binaryOutline) //contour rendering { if ( input->GetPixelType().GetBpe() <= 8 ) { //generate contours/outlines localStorage->m_OutlinePolyData = CreateOutlinePolyData(renderer); float binaryOutlineWidth(1.0); if ( datanode->GetFloatProperty( "outline width", binaryOutlineWidth, renderer ) ) { if ( localStorage->m_Actors->GetNumberOfPaths() > 1 ) { float binaryOutlineShadowWidth(1.5); datanode->GetFloatProperty( "outline shadow width", binaryOutlineShadowWidth, renderer ); dynamic_cast(localStorage->m_Actors->GetParts()->GetItemAsObject(0)) ->GetProperty()->SetLineWidth( binaryOutlineWidth * binaryOutlineShadowWidth ); } localStorage->m_Actor->GetProperty()->SetLineWidth( binaryOutlineWidth ); } } else { binaryOutline = false; this->ApplyLookuptable(renderer); MITK_WARN << "Type of all binary images should be (un)signed char. Outline does not work on other pixel types!"; } } else //standard binary image { if(numberOfComponents != 1) { MITK_ERROR << "Rendering Error: Binary Images with more then 1 component are not supported!"; } } } if (!(numberOfComponents == 1 || numberOfComponents == 3 || numberOfComponents == 4)) { MITK_WARN << "Unknown number of components!"; } this->ApplyOpacity( renderer ); this->ApplyRenderingMode(renderer); // do not use a VTK lookup table (we do that ourselves in m_LevelWindowFilter) localStorage->m_Texture->MapColorScalarsThroughLookupTableOff(); //connect the input with the levelwindow filter localStorage->m_LevelWindowFilter->SetInput(localStorage->m_ReslicedImage); //connect the texture with the output of the levelwindow filter // check for texture interpolation property bool textureInterpolation = false; GetDataNode()->GetBoolProperty( "texture interpolation", textureInterpolation, renderer ); //set the interpolation modus according to the property localStorage->m_Texture->SetInterpolate(textureInterpolation); localStorage->m_Texture->SetInputConnection(localStorage->m_LevelWindowFilter->GetOutputPort()); this->TransformActor( renderer ); vtkActor* contourShadowActor = dynamic_cast (localStorage->m_Actors->GetParts()->GetItemAsObject(0)); if(binary && binaryOutline) //connect the mapper with the polyData which contains the lines { //We need the contour for the binary outline property as actor localStorage->m_Mapper->SetInput(localStorage->m_OutlinePolyData); localStorage->m_Actor->SetTexture(NULL); //no texture for contours bool binaryOutlineShadow( false ); datanode->GetBoolProperty( "outline binary shadow", binaryOutlineShadow, renderer ); if ( binaryOutlineShadow ) contourShadowActor->SetVisibility( true ); else contourShadowActor->SetVisibility( false ); } else { //Connect the mapper with the input texture. This is the standard case. //setup the textured plane this->GeneratePlane( renderer, sliceBounds ); //set the plane as input for the mapper localStorage->m_Mapper->SetInputConnection(localStorage->m_Plane->GetOutputPort()); //set the texture for the actor localStorage->m_Actor->SetTexture(localStorage->m_Texture); contourShadowActor->SetVisibility( false ); } // We have been modified => save this for next Update() localStorage->m_LastUpdateTime.Modified(); } void mitk::ImageVtkMapper2D::ApplyLevelWindow(mitk::BaseRenderer *renderer) { LocalStorage *localStorage = this->GetLocalStorage( renderer ); LevelWindow levelWindow; this->GetDataNode()->GetLevelWindow( levelWindow, renderer, "levelwindow" ); localStorage->m_LevelWindowFilter->GetLookupTable()->SetRange( levelWindow.GetLowerWindowBound(), levelWindow.GetUpperWindowBound() ); mitk::LevelWindow opacLevelWindow; if( this->GetDataNode()->GetLevelWindow( opacLevelWindow, renderer, "opaclevelwindow" ) ) { //pass the opaque level window to the filter localStorage->m_LevelWindowFilter->SetMinOpacity(opacLevelWindow.GetLowerWindowBound()); localStorage->m_LevelWindowFilter->SetMaxOpacity(opacLevelWindow.GetUpperWindowBound()); } else { //no opaque level window localStorage->m_LevelWindowFilter->SetMinOpacity(0.0); localStorage->m_LevelWindowFilter->SetMaxOpacity(255.0); } } void mitk::ImageVtkMapper2D::ApplyColor( mitk::BaseRenderer* renderer ) { LocalStorage *localStorage = this->GetLocalStorage( renderer ); float rgb[3]= { 1.0f, 1.0f, 1.0f }; // check for color prop and use it for rendering if it exists // binary image hovering & binary image selection bool hover = false; bool selected = false; GetDataNode()->GetBoolProperty("binaryimage.ishovering", hover, renderer); GetDataNode()->GetBoolProperty("selected", selected, renderer); if(hover && !selected) { mitk::ColorProperty::Pointer colorprop = dynamic_cast(GetDataNode()->GetProperty ("binaryimage.hoveringcolor", renderer)); if(colorprop.IsNotNull()) { memcpy(rgb, colorprop->GetColor().GetDataPointer(), 3*sizeof(float)); } else { GetDataNode()->GetColor( rgb, renderer, "color" ); } } if(selected) { mitk::ColorProperty::Pointer colorprop = dynamic_cast(GetDataNode()->GetProperty ("binaryimage.selectedcolor", renderer)); if(colorprop.IsNotNull()) { memcpy(rgb, colorprop->GetColor().GetDataPointer(), 3*sizeof(float)); } else { GetDataNode()->GetColor(rgb, renderer, "color"); } } if(!hover && !selected) { GetDataNode()->GetColor( rgb, renderer, "color" ); } double rgbConv[3] = {(double)rgb[0], (double)rgb[1], (double)rgb[2]}; //conversion to double for VTK dynamic_cast (localStorage->m_Actors->GetParts()->GetItemAsObject(0))->GetProperty()->SetColor(rgbConv); localStorage->m_Actor->GetProperty()->SetColor(rgbConv); if ( localStorage->m_Actors->GetParts()->GetNumberOfItems() > 1 ) { float rgb[3]= { 1.0f, 1.0f, 1.0f }; mitk::ColorProperty::Pointer colorprop = dynamic_cast(GetDataNode()->GetProperty ("outline binary shadow color", renderer)); if(colorprop.IsNotNull()) { memcpy(rgb, colorprop->GetColor().GetDataPointer(), 3*sizeof(float)); } double rgbConv[3] = {(double)rgb[0], (double)rgb[1], (double)rgb[2]}; //conversion to double for VTK dynamic_cast( localStorage->m_Actors->GetParts()->GetItemAsObject(0) )->GetProperty()->SetColor(rgbConv); } } void mitk::ImageVtkMapper2D::ApplyOpacity( mitk::BaseRenderer* renderer ) { LocalStorage* localStorage = this->GetLocalStorage( renderer ); float opacity = 1.0f; // check for opacity prop and use it for rendering if it exists GetDataNode()->GetOpacity( opacity, renderer, "opacity" ); //set the opacity according to the properties localStorage->m_Actor->GetProperty()->SetOpacity(opacity); if ( localStorage->m_Actors->GetParts()->GetNumberOfItems() > 1 ) { dynamic_cast( localStorage->m_Actors->GetParts()->GetItemAsObject(0) )->GetProperty()->SetOpacity(opacity); } } void mitk::ImageVtkMapper2D::ApplyRenderingMode( mitk::BaseRenderer* renderer ) { LocalStorage* localStorage = m_LSH.GetLocalStorage(renderer); bool binary = false; this->GetDataNode()->GetBoolProperty( "binary", binary, renderer ); if(binary) // is it a binary image? { //for binary images, we always use our default LuT and map every value to (0,1) //the opacity of 0 will always be 0.0. We never a apply a LuT/TfF nor a level window. localStorage->m_LevelWindowFilter->SetLookupTable(localStorage->m_BinaryLookupTable); } else { //all other image types can make use of the rendering mode int renderingMode = mitk::RenderingModeProperty::LEVELWINDOW_COLOR; mitk::RenderingModeProperty::Pointer mode = dynamic_cast(this->GetDataNode()->GetProperty( "Image Rendering.Mode", renderer )); if(mode.IsNotNull()) { renderingMode = mode->GetRenderingMode(); } switch(renderingMode) { case mitk::RenderingModeProperty::LEVELWINDOW_COLOR: MITK_DEBUG << "'Image Rendering.Mode' = LevelWindow_Color"; localStorage->m_LevelWindowFilter->SetLookupTable( localStorage->m_DefaultLookupTable ); this->ApplyLevelWindow( renderer ); break; case mitk::RenderingModeProperty::LOOKUPTABLE_LEVELWINDOW_COLOR: MITK_DEBUG << "'Image Rendering.Mode' = LevelWindow_LookupTable_Color"; this->ApplyLookuptable( renderer ); this->ApplyLevelWindow( renderer ); break; case mitk::RenderingModeProperty::COLORTRANSFERFUNCTION_LEVELWINDOW_COLOR: MITK_DEBUG << "'Image Rendering.Mode' = LevelWindow_ColorTransferFunction_Color"; this->ApplyColorTransferFunction( renderer ); this->ApplyLevelWindow( renderer ); break; case mitk::RenderingModeProperty::LOOKUPTABLE_COLOR: MITK_DEBUG << "'Image Rendering.Mode' = LookupTable_Color"; this->ApplyLookuptable( renderer ); break; case mitk::RenderingModeProperty::COLORTRANSFERFUNCTION_COLOR: MITK_DEBUG << "'Image Rendering.Mode' = ColorTransferFunction_Color"; this->ApplyColorTransferFunction( renderer ); break; default: MITK_ERROR << "No valid 'Image Rendering.Mode' set"; break; } } //we apply color for all images (including binaries). this->ApplyColor( renderer ); } void mitk::ImageVtkMapper2D::ApplyLookuptable( mitk::BaseRenderer* renderer ) { LocalStorage* localStorage = m_LSH.GetLocalStorage(renderer); vtkLookupTable* usedLookupTable = localStorage->m_ColorLookupTable; // If lookup table or transferfunction use is requested... mitk::LookupTableProperty::Pointer lookupTableProp = dynamic_cast(this->GetDataNode()->GetProperty("LookupTable")); if( lookupTableProp.IsNotNull() ) // is a lookuptable set? { usedLookupTable = lookupTableProp->GetLookupTable()->GetVtkLookupTable(); } else { MITK_WARN << "Image Rendering.Mode was set to use a lookup table but there is no property 'LookupTable'. A default (rainbow) lookup table will be used."; } localStorage->m_LevelWindowFilter->SetLookupTable(usedLookupTable); } void mitk::ImageVtkMapper2D::ApplyColorTransferFunction(mitk::BaseRenderer *renderer) { mitk::TransferFunctionProperty::Pointer transferFunctionProp = dynamic_cast(this->GetDataNode()->GetProperty("Image Rendering.Transfer Function",renderer )); if( transferFunctionProp.IsNull() ) { MITK_ERROR << "'Image Rendering.Mode'' was set to use a color transfer function but there is no property 'Image Rendering.Transfer Function'. Nothing will be done."; return; } LocalStorage* localStorage = m_LSH.GetLocalStorage(renderer); //pass the transfer function to our level window filter localStorage->m_LevelWindowFilter->SetLookupTable(transferFunctionProp->GetValue()->GetColorTransferFunction()); } void mitk::ImageVtkMapper2D::Update(mitk::BaseRenderer* renderer) { bool visible = true; GetDataNode()->GetVisibility(visible, renderer, "visible"); if ( !visible ) { return; } mitk::Image* data = const_cast( this->GetInput() ); if ( data == NULL ) { return; } // Calculate time step of the input data for the specified renderer (integer value) this->CalculateTimeStep( renderer ); // Check if time step is valid const TimeSlicedGeometry *dataTimeGeometry = data->GetTimeSlicedGeometry(); if ( ( dataTimeGeometry == NULL ) || ( dataTimeGeometry->GetTimeSteps() == 0 ) || ( !dataTimeGeometry->IsValidTime( this->GetTimestep() ) ) ) { return; } const DataNode *node = this->GetDataNode(); data->UpdateOutputInformation(); LocalStorage *localStorage = m_LSH.GetLocalStorage(renderer); //check if something important has changed and we need to rerender if ( (localStorage->m_LastUpdateTime < node->GetMTime()) //was the node modified? || (localStorage->m_LastUpdateTime < data->GetPipelineMTime()) //Was the data modified? || (localStorage->m_LastUpdateTime < renderer->GetCurrentWorldGeometry2DUpdateTime()) //was the geometry modified? || (localStorage->m_LastUpdateTime < renderer->GetCurrentWorldGeometry2D()->GetMTime()) || (localStorage->m_LastUpdateTime < node->GetPropertyList()->GetMTime()) //was a property modified? || (localStorage->m_LastUpdateTime < node->GetPropertyList(renderer)->GetMTime()) ) { this->GenerateDataForRenderer( renderer ); } // since we have checked that nothing important has changed, we can set // m_LastUpdateTime to the current time localStorage->m_LastUpdateTime.Modified(); } void mitk::ImageVtkMapper2D::SetDefaultProperties(mitk::DataNode* node, mitk::BaseRenderer* renderer, bool overwrite) { mitk::Image::Pointer image = dynamic_cast(node->GetData()); // Properties common for both images and segmentations node->AddProperty( "depthOffset", mitk::FloatProperty::New( 0.0 ), renderer, overwrite ); node->AddProperty( "outline binary", mitk::BoolProperty::New( false ), renderer, overwrite ); node->AddProperty( "outline width", mitk::FloatProperty::New( 1.0 ), renderer, overwrite ); node->AddProperty( "outline binary shadow", mitk::BoolProperty::New( false ), renderer, overwrite ); node->AddProperty( "outline binary shadow color", ColorProperty::New(0.0,0.0,0.0), renderer, overwrite ); node->AddProperty( "outline shadow width", mitk::FloatProperty::New( 1.5 ), renderer, overwrite ); if(image->IsRotated()) node->AddProperty( "reslice interpolation", mitk::VtkResliceInterpolationProperty::New(VTK_RESLICE_CUBIC) ); else node->AddProperty( "reslice interpolation", mitk::VtkResliceInterpolationProperty::New() ); node->AddProperty( "texture interpolation", mitk::BoolProperty::New( mitk::DataNodeFactory::m_TextureInterpolationActive ) ); // set to user configurable default value (see global options) node->AddProperty( "in plane resample extent by geometry", mitk::BoolProperty::New( false ) ); node->AddProperty( "bounding box", mitk::BoolProperty::New( false ) ); mitk::RenderingModeProperty::Pointer renderingModeProperty = mitk::RenderingModeProperty::New(); node->AddProperty( "Image Rendering.Mode", renderingModeProperty); std::string photometricInterpretation; // DICOM tag telling us how pixel values should be displayed if ( node->GetStringProperty( "dicom.pixel.PhotometricInterpretation", photometricInterpretation ) ) { // modality provided by DICOM or other reader if ( photometricInterpretation.find("MONOCHROME1") != std::string::npos ) // meaning: display MINIMUM pixels as WHITE { // generate LUT (white to black) mitk::LookupTable::Pointer mitkLut = mitk::LookupTable::New(); vtkLookupTable* bwLut = mitkLut->GetVtkLookupTable(); bwLut->SetTableRange (0, 1); bwLut->SetSaturationRange (0, 0); bwLut->SetHueRange (0, 0); bwLut->SetValueRange (1, 0); bwLut->SetAlphaRange (1, 1); bwLut->SetRampToLinear(); bwLut->Build(); mitk::LookupTableProperty::Pointer mitkLutProp = mitk::LookupTableProperty::New(); mitkLutProp->SetLookupTable(mitkLut); node->SetProperty( "LookupTable", mitkLutProp ); + renderingModeProperty->SetValue( mitk::RenderingModeProperty::LOOKUPTABLE_LEVELWINDOW_COLOR ); // USE lookuptable } else if ( photometricInterpretation.find("MONOCHROME2") != std::string::npos ) // meaning: display MINIMUM pixels as BLACK { // apply default LUT (black to white) node->SetProperty( "color", mitk::ColorProperty::New( 1,1,1 ), renderer ); } // PALETTE interpretation should be handled ok by RGB loading } bool isBinaryImage(false); if ( ! node->GetBoolProperty("binary", isBinaryImage) ) { // ok, property is not set, use heuristic to determine if this // is a binary image mitk::Image::Pointer centralSliceImage; ScalarType minValue = 0.0; ScalarType maxValue = 0.0; ScalarType min2ndValue = 0.0; ScalarType max2ndValue = 0.0; mitk::ImageSliceSelector::Pointer sliceSelector = mitk::ImageSliceSelector::New(); sliceSelector->SetInput(image); sliceSelector->SetSliceNr(image->GetDimension(2)/2); sliceSelector->SetTimeNr(image->GetDimension(3)/2); sliceSelector->SetChannelNr(image->GetDimension(4)/2); sliceSelector->Update(); centralSliceImage = sliceSelector->GetOutput(); if ( centralSliceImage.IsNotNull() && centralSliceImage->IsInitialized() ) { minValue = centralSliceImage->GetStatistics()->GetScalarValueMin(); maxValue = centralSliceImage->GetStatistics()->GetScalarValueMax(); min2ndValue = centralSliceImage->GetStatistics()->GetScalarValue2ndMin(); max2ndValue = centralSliceImage->GetStatistics()->GetScalarValue2ndMax(); } if ((maxValue == min2ndValue && minValue == max2ndValue) || minValue == maxValue) { // centralSlice is strange, lets look at all data minValue = image->GetStatistics()->GetScalarValueMin(); maxValue = image->GetStatistics()->GetScalarValueMaxNoRecompute(); min2ndValue = image->GetStatistics()->GetScalarValue2ndMinNoRecompute(); max2ndValue = image->GetStatistics()->GetScalarValue2ndMaxNoRecompute(); } isBinaryImage = ( maxValue == min2ndValue && minValue == max2ndValue ); } // some more properties specific for a binary... if (isBinaryImage) { node->AddProperty( "opacity", mitk::FloatProperty::New(0.3f), renderer, overwrite ); node->AddProperty( "color", ColorProperty::New(1.0,0.0,0.0), renderer, overwrite ); node->AddProperty( "binaryimage.selectedcolor", ColorProperty::New(1.0,0.0,0.0), renderer, overwrite ); node->AddProperty( "binaryimage.selectedannotationcolor", ColorProperty::New(1.0,0.0,0.0), renderer, overwrite ); node->AddProperty( "binaryimage.hoveringcolor", ColorProperty::New(1.0,0.0,0.0), renderer, overwrite ); node->AddProperty( "binaryimage.hoveringannotationcolor", ColorProperty::New(1.0,0.0,0.0), renderer, overwrite ); node->AddProperty( "binary", mitk::BoolProperty::New( true ), renderer, overwrite ); node->AddProperty("layer", mitk::IntProperty::New(10), renderer, overwrite); } else //...or image type object { node->AddProperty( "opacity", mitk::FloatProperty::New(1.0f), renderer, overwrite ); node->AddProperty( "color", ColorProperty::New(1.0,1.0,1.0), renderer, overwrite ); node->AddProperty( "binary", mitk::BoolProperty::New( false ), renderer, overwrite ); node->AddProperty("layer", mitk::IntProperty::New(0), renderer, overwrite); } if(image.IsNotNull() && image->IsInitialized()) { if((overwrite) || (node->GetProperty("levelwindow", renderer)==NULL)) { /* initialize level/window from DICOM tags */ std::string sLevel; std::string sWindow; if ( image->GetPropertyList()->GetStringProperty( "dicom.voilut.WindowCenter", sLevel ) && image->GetPropertyList()->GetStringProperty( "dicom.voilut.WindowWidth", sWindow ) ) { float level = atof( sLevel.c_str() ); float window = atof( sWindow.c_str() ); mitk::LevelWindow contrast; std::string sSmallestPixelValueInSeries; std::string sLargestPixelValueInSeries; if ( image->GetPropertyList()->GetStringProperty( "dicom.series.SmallestPixelValueInSeries", sSmallestPixelValueInSeries ) && image->GetPropertyList()->GetStringProperty( "dicom.series.LargestPixelValueInSeries", sLargestPixelValueInSeries ) ) { float smallestPixelValueInSeries = atof( sSmallestPixelValueInSeries.c_str() ); float largestPixelValueInSeries = atof( sLargestPixelValueInSeries.c_str() ); contrast.SetRangeMinMax( smallestPixelValueInSeries-1, largestPixelValueInSeries+1 ); // why not a little buffer? // might remedy some l/w widget challenges } else { contrast.SetAuto( static_cast(node->GetData()), false, true ); // we need this as a fallback } contrast.SetLevelWindow( level, window, true ); node->SetProperty( "levelwindow", LevelWindowProperty::New( contrast ), renderer ); } } if(((overwrite) || (node->GetProperty("opaclevelwindow", renderer)==NULL)) && (image->GetPixelType().GetPixelType() == itk::ImageIOBase::RGBA) && (image->GetPixelType().GetComponentType() == itk::ImageIOBase::UCHAR) ) { mitk::LevelWindow opaclevwin; opaclevwin.SetRangeMinMax(0,255); opaclevwin.SetWindowBounds(0,255); mitk::LevelWindowProperty::Pointer prop = mitk::LevelWindowProperty::New(opaclevwin); node->SetProperty( "opaclevelwindow", prop, renderer ); } } Superclass::SetDefaultProperties(node, renderer, overwrite); } mitk::ImageVtkMapper2D::LocalStorage* mitk::ImageVtkMapper2D::GetLocalStorage(mitk::BaseRenderer* renderer) { return m_LSH.GetLocalStorage(renderer); } vtkSmartPointer mitk::ImageVtkMapper2D::CreateOutlinePolyData(mitk::BaseRenderer* renderer ){ LocalStorage* localStorage = this->GetLocalStorage(renderer); //get the min and max index values of each direction int* extent = localStorage->m_ReslicedImage->GetExtent(); int xMin = extent[0]; int xMax = extent[1]; int yMin = extent[2]; int yMax = extent[3]; int* dims = localStorage->m_ReslicedImage->GetDimensions(); //dimensions of the image int line = dims[0]; //how many pixels per line? int x = xMin; //pixel index x int y = yMin; //pixel index y char* currentPixel; //get the depth for each contour float depth = CalculateLayerDepth(renderer); vtkSmartPointer points = vtkSmartPointer::New(); //the points to draw vtkSmartPointer lines = vtkSmartPointer::New(); //the lines to connect the points // We take the pointer to the first pixel of the image currentPixel = static_cast(localStorage->m_ReslicedImage->GetScalarPointer() ); while (y <= yMax) { //if the current pixel value is set to something if ((currentPixel) && (*currentPixel != 0)) { //check in which direction a line is necessary //a line is added if the neighbor of the current pixel has the value 0 //and if the pixel is located at the edge of the image //if vvvvv not the first line vvvvv if (y > yMin && *(currentPixel-line) == 0) { //x direction - bottom edge of the pixel //add the 2 points vtkIdType p1 = points->InsertNextPoint(x*localStorage->m_mmPerPixel[0], y*localStorage->m_mmPerPixel[1], depth); vtkIdType p2 = points->InsertNextPoint((x+1)*localStorage->m_mmPerPixel[0], y*localStorage->m_mmPerPixel[1], depth); //add the line between both points lines->InsertNextCell(2); lines->InsertCellPoint(p1); lines->InsertCellPoint(p2); } //if vvvvv not the last line vvvvv if (y < yMax && *(currentPixel+line) == 0) { //x direction - top edge of the pixel vtkIdType p1 = points->InsertNextPoint(x*localStorage->m_mmPerPixel[0], (y+1)*localStorage->m_mmPerPixel[1], depth); vtkIdType p2 = points->InsertNextPoint((x+1)*localStorage->m_mmPerPixel[0], (y+1)*localStorage->m_mmPerPixel[1], depth); lines->InsertNextCell(2); lines->InsertCellPoint(p1); lines->InsertCellPoint(p2); } //if vvvvv not the first pixel vvvvv if ( (x > xMin || y > yMin) && *(currentPixel-1) == 0) { //y direction - left edge of the pixel vtkIdType p1 = points->InsertNextPoint(x*localStorage->m_mmPerPixel[0], y*localStorage->m_mmPerPixel[1], depth); vtkIdType p2 = points->InsertNextPoint(x*localStorage->m_mmPerPixel[0], (y+1)*localStorage->m_mmPerPixel[1], depth); lines->InsertNextCell(2); lines->InsertCellPoint(p1); lines->InsertCellPoint(p2); } //if vvvvv not the last pixel vvvvv if ( (y < yMax || (x < xMax) ) && *(currentPixel+1) == 0) { //y direction - right edge of the pixel vtkIdType p1 = points->InsertNextPoint((x+1)*localStorage->m_mmPerPixel[0], y*localStorage->m_mmPerPixel[1], depth); vtkIdType p2 = points->InsertNextPoint((x+1)*localStorage->m_mmPerPixel[0], (y+1)*localStorage->m_mmPerPixel[1], depth); lines->InsertNextCell(2); lines->InsertCellPoint(p1); lines->InsertCellPoint(p2); } /* now consider pixels at the edge of the image */ //if vvvvv left edge of image vvvvv if (x == xMin) { //draw left edge of the pixel vtkIdType p1 = points->InsertNextPoint(x*localStorage->m_mmPerPixel[0], y*localStorage->m_mmPerPixel[1], depth); vtkIdType p2 = points->InsertNextPoint(x*localStorage->m_mmPerPixel[0], (y+1)*localStorage->m_mmPerPixel[1], depth); lines->InsertNextCell(2); lines->InsertCellPoint(p1); lines->InsertCellPoint(p2); } //if vvvvv right edge of image vvvvv if (x == xMax) { //draw right edge of the pixel vtkIdType p1 = points->InsertNextPoint((x+1)*localStorage->m_mmPerPixel[0], y*localStorage->m_mmPerPixel[1], depth); vtkIdType p2 = points->InsertNextPoint((x+1)*localStorage->m_mmPerPixel[0], (y+1)*localStorage->m_mmPerPixel[1], depth); lines->InsertNextCell(2); lines->InsertCellPoint(p1); lines->InsertCellPoint(p2); } //if vvvvv bottom edge of image vvvvv if (y == yMin) { //draw bottom edge of the pixel vtkIdType p1 = points->InsertNextPoint(x*localStorage->m_mmPerPixel[0], y*localStorage->m_mmPerPixel[1], depth); vtkIdType p2 = points->InsertNextPoint((x+1)*localStorage->m_mmPerPixel[0], y*localStorage->m_mmPerPixel[1], depth); lines->InsertNextCell(2); lines->InsertCellPoint(p1); lines->InsertCellPoint(p2); } //if vvvvv top edge of image vvvvv if (y == yMax) { //draw top edge of the pixel vtkIdType p1 = points->InsertNextPoint(x*localStorage->m_mmPerPixel[0], (y+1)*localStorage->m_mmPerPixel[1], depth); vtkIdType p2 = points->InsertNextPoint((x+1)*localStorage->m_mmPerPixel[0], (y+1)*localStorage->m_mmPerPixel[1], depth); lines->InsertNextCell(2); lines->InsertCellPoint(p1); lines->InsertCellPoint(p2); } }//end if currentpixel is set x++; if (x > xMax) { //reached end of line x = xMin; y++; } // Increase the pointer-position to the next pixel. // This is safe, as the while-loop and the x-reset logic above makes // sure we do not exceed the bounds of the image currentPixel++; }//end of while // Create a polydata to store everything in vtkSmartPointer polyData = vtkSmartPointer::New(); // Add the points to the dataset polyData->SetPoints(points); // Add the lines to the dataset polyData->SetLines(lines); return polyData; } void mitk::ImageVtkMapper2D::TransformActor(mitk::BaseRenderer* renderer) { LocalStorage *localStorage = m_LSH.GetLocalStorage(renderer); //get the transformation matrix of the reslicer in order to render the slice as axial, coronal or saggital vtkSmartPointer trans = vtkSmartPointer::New(); vtkSmartPointer matrix = localStorage->m_Reslicer->GetResliceAxes(); trans->SetMatrix(matrix); //transform the plane/contour (the actual actor) to the corresponding view (axial, coronal or saggital) localStorage->m_Actor->SetUserTransform(trans); //transform the origin to center based coordinates, because MITK is center based. localStorage->m_Actor->SetPosition( -0.5*localStorage->m_mmPerPixel[0], -0.5*localStorage->m_mmPerPixel[1], 0.0); if ( localStorage->m_Actors->GetNumberOfPaths() > 1 ) { vtkActor* secondaryActor = dynamic_cast( localStorage->m_Actors->GetParts()->GetItemAsObject(0) ); secondaryActor->SetUserTransform(trans); secondaryActor->SetPosition( -0.5*localStorage->m_mmPerPixel[0], -0.5*localStorage->m_mmPerPixel[1], 0.0); } } bool mitk::ImageVtkMapper2D::RenderingGeometryIntersectsImage( const Geometry2D* renderingGeometry, SlicedGeometry3D* imageGeometry ) { // if either one of the two geometries is NULL we return true // for safety reasons if ( renderingGeometry == NULL || imageGeometry == NULL ) return true; // get the distance for the first cornerpoint ScalarType initialDistance = renderingGeometry->SignedDistance( imageGeometry->GetCornerPoint( 0 ) ); for( int i=1; i<8; i++ ) { mitk::Point3D cornerPoint = imageGeometry->GetCornerPoint( i ); // get the distance to the other cornerpoints ScalarType distance = renderingGeometry->SignedDistance( cornerPoint ); // if it has not the same signing as the distance of the first point if ( initialDistance * distance < 0 ) { // we have an intersection and return true return true; } } // all distances have the same sign, no intersection and we return false return false; } mitk::ImageVtkMapper2D::LocalStorage::~LocalStorage() { } mitk::ImageVtkMapper2D::LocalStorage::LocalStorage() { m_LevelWindowFilter = vtkSmartPointer::New(); //Do as much actions as possible in here to avoid double executions. m_Plane = vtkSmartPointer::New(); m_Texture = vtkSmartPointer::New().GetPointer(); m_DefaultLookupTable = vtkSmartPointer::New(); m_BinaryLookupTable = vtkSmartPointer::New(); m_ColorLookupTable = vtkSmartPointer::New(); m_Mapper = vtkSmartPointer::New(); m_Actor = vtkSmartPointer::New(); m_Actors = vtkSmartPointer::New(); m_Reslicer = mitk::ExtractSliceFilter::New(); m_TSFilter = vtkSmartPointer::New(); m_OutlinePolyData = vtkSmartPointer::New(); m_ReslicedImage = vtkSmartPointer::New(); m_EmptyPolyData = vtkSmartPointer::New(); //the following actions are always the same and thus can be performed //in the constructor for each image (i.e. the image-corresponding local storage) m_TSFilter->ReleaseDataFlagOn(); //built a default lookuptable m_DefaultLookupTable->SetRampToLinear(); m_DefaultLookupTable->SetSaturationRange( 0.0, 0.0 ); m_DefaultLookupTable->SetHueRange( 0.0, 0.0 ); m_DefaultLookupTable->SetValueRange( 0.0, 1.0 ); m_DefaultLookupTable->Build(); m_BinaryLookupTable->SetRampToLinear(); m_BinaryLookupTable->SetSaturationRange( 0.0, 0.0 ); m_BinaryLookupTable->SetHueRange( 0.0, 0.0 ); m_BinaryLookupTable->SetValueRange( 0.0, 1.0 ); m_BinaryLookupTable->SetRange(0.0, 1.0); m_BinaryLookupTable->Build(); // add a default rainbow lookup table for color mapping m_ColorLookupTable->SetRampToLinear(); m_ColorLookupTable->SetHueRange(0.6667, 0.0); m_ColorLookupTable->SetTableRange(0.0, 20.0); m_ColorLookupTable->Build(); // make first value transparent { double rgba[4]; m_BinaryLookupTable->GetTableValue(0, rgba); m_BinaryLookupTable->SetTableValue(0, rgba[0], rgba[1], rgba[2], 0.0); // background to 0 } //do not repeat the texture (the image) m_Texture->RepeatOff(); //set the mapper for the actor m_Actor->SetMapper( m_Mapper ); vtkSmartPointer outlineShadowActor = vtkSmartPointer::New(); outlineShadowActor->SetMapper( m_Mapper ); m_Actors->AddPart( outlineShadowActor ); m_Actors->AddPart( m_Actor ); } diff --git a/Core/Code/Testing/CMakeLists.txt b/Core/Code/Testing/CMakeLists.txt index a59d896ad2..cf0f186153 100644 --- a/Core/Code/Testing/CMakeLists.txt +++ b/Core/Code/Testing/CMakeLists.txt @@ -1,171 +1,164 @@ # The core tests need relaxed compiler flags... # TODO fix core tests to compile without these additional no-error flags if(MSVC_VERSION) # disable deprecated warnings (they would lead to errors) mitkFunctionCheckCAndCXXCompilerFlags("/wd4996" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) else() mitkFunctionCheckCAndCXXCompilerFlags("-Wno-error=deprecated" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) mitkFunctionCheckCAndCXXCompilerFlags("-Wno-error=deprecated-declarations" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) endif() MITK_CREATE_MODULE_TESTS(LABELS MITK-Core) # MITK_INSTALL_TARGETS(EXECUTABLES MitkTestDriver) -mitkAddCustomModuleTest(mitkDICOMLocaleTest_spacingOk_CT mitkDICOMLocaleTest ${MITK_DATA_DIR}/spacing-ok-ct.dcm) -mitkAddCustomModuleTest(mitkDICOMLocaleTest_spacingOk_MR mitkDICOMLocaleTest ${MITK_DATA_DIR}/spacing-ok-mr.dcm) -mitkAddCustomModuleTest(mitkDICOMLocaleTest_spacingOk_SC mitkDICOMLocaleTest ${MITK_DATA_DIR}/spacing-ok-sc.dcm) - mitkAddCustomModuleTest(mitkVolumeCalculatorTest_Png2D-bw mitkVolumeCalculatorTest ${MITK_DATA_DIR}/Png2D-bw.png ${MITK_DATA_DIR}/Pic2DplusT.nrrd) mitkAddCustomModuleTest(mitkEventMapperTest_Test1And2 mitkEventMapperTest ${MITK_DATA_DIR}/TestStateMachine1.xml ${MITK_DATA_DIR}/TestStateMachine2.xml) mitkAddCustomModuleTest(mitkEventConfigTest_CreateObjectInDifferentWays mitkEventConfigTest ${MITK_SOURCE_DIR}/Core/Code/Testing/Resources/Interactions/StatemachineConfigTest.xml) #mitkAddCustomModuleTest(mitkNodeDependentPointSetInteractorTest mitkNodeDependentPointSetInteractorTest ${MITK_DATA_DIR}/Pic3D.pic.gz ${MITK_DATA_DIR}/BallBinary30x30x30.pic.gz) mitkAddCustomModuleTest(mitkNodeDependentPointSetInteractorTest mitkNodeDependentPointSetInteractorTest ${MITK_DATA_DIR}/Pic3D.nrrd ${MITK_DATA_DIR}/BallBinary30x30x30.nrrd) mitkAddCustomModuleTest(mitkDataStorageTest_US4DCyl mitkDataStorageTest ${MITK_DATA_DIR}/US4DCyl.nrrd) mitkAddCustomModuleTest(mitkStateMachineFactoryTest_TestStateMachine1_2 mitkStateMachineFactoryTest ${MITK_DATA_DIR}/TestStateMachine1.xml ${MITK_DATA_DIR}/TestStateMachine2.xml) -mitkAddCustomModuleTest(mitkDicomSeriesReaderTest_CTImage mitkDicomSeriesReaderTest ${MITK_DATA_DIR}/TinyCTAbdomen) - mitkAddCustomModuleTest(mitkPointSetReaderTest mitkPointSetReaderTest ${MITK_DATA_DIR}/PointSetReaderTestData.mps) mitkAddCustomModuleTest(mitkImageTest_4DImageData mitkImageTest ${MITK_DATA_DIR}/US4DCyl.nrrd) mitkAddCustomModuleTest(mitkImageTest_2D+tImageData mitkImageTest ${MITK_DATA_DIR}/Pic2DplusT.nrrd) mitkAddCustomModuleTest(mitkImageTest_3DImageData mitkImageTest ${MITK_DATA_DIR}/Pic3D.nrrd) mitkAddCustomModuleTest(mitkImageTest_brainImage mitkImageTest ${MITK_DATA_DIR}/brain.mhd) #mitkAddCustomModuleTest(mitkImageTest_color2DImage mitkImageTest ${MITK_DATA_DIR}/NrrdWritingTestImage.jpg) mitkAddCustomModuleTest(mitkImageTest_3DImageData mitkImageGeneratorTest ${MITK_DATA_DIR}/Pic3D.nrrd) mitkAddCustomModuleTest(mitkIOUtilTest mitkIOUtilTest #test for a randomly chosen Pic3D swivelled slice ${MITK_DATA_DIR}/Pic3D.nrrd ${MITK_DATA_DIR}/pointSet.mps ${MITK_DATA_DIR}/binary.stl ) mitkAddCustomModuleTest(mitkLevelWindowManagerTest mitkLevelWindowManagerTest ${MITK_DATA_DIR}/Pic3D.nrrd ) if(WIN32 OR APPLE OR MITK_ENABLE_GUI_TESTING) ### since the rendering test's do not run in ubuntu, yet, we build them only for other systems or if the user explicitly sets the variable MITK_ENABLE_GUI_TESTING mitkAddCustomModuleTest(mitkImageVtkMapper2D_rgbaImage640x480 mitkImageVtkMapper2DTest ${MITK_DATA_DIR}/RenderingTestData/rgbaImage.png #input image to load in data storage -V ${MITK_DATA_DIR}/RenderingTestData/ReferenceScreenshots/rgbaImage640x480REF.png #corresponding reference screenshot ) mitkAddCustomModuleTest(mitkImageVtkMapper2D_pic3d640x480 mitkImageVtkMapper2DTest #test for standard Pic3D axial slice ${MITK_DATA_DIR}/Pic3D.nrrd #input image to load in data storage -V ${MITK_DATA_DIR}/RenderingTestData/ReferenceScreenshots/pic3d640x480REF.png #corresponding reference screenshot ) mitkAddCustomModuleTest(mitkImageVtkMapper2D_pic3dColorBlue640x480 mitkImageVtkMapper2DColorTest #test for color property (=blue) Pic3D sagittal slice ${MITK_DATA_DIR}/Pic3D.nrrd #input image to load in data storage -V ${MITK_DATA_DIR}/RenderingTestData/ReferenceScreenshots/pic3dColorBlue640x480REF.png #corresponding reference screenshot ) mitkAddCustomModuleTest(mitkImageVtkMapper2D_pic3dLevelWindow640x480 mitkImageVtkMapper2DLevelWindowTest #test for levelwindow property (=blood) #Pic3D sagittal slice ${MITK_DATA_DIR}/Pic3D.nrrd #input image to load in data storage -V ${MITK_DATA_DIR}/RenderingTestData/ReferenceScreenshots/pic3dLevelWindowBlood640x480REF.png #corresponding reference #screenshot ) mitkAddCustomModuleTest(mitkImageVtkMapper2D_pic3dOpacity640x480 mitkImageVtkMapper2DOpacityTest #test for opacity (=0.5) Pic3D coronal slice ${MITK_DATA_DIR}/Pic3D.nrrd #input image to load in data storage -V ${MITK_DATA_DIR}/RenderingTestData/ReferenceScreenshots/pic3dOpacity640x480REF.png corresponding reference screenshot ) mitkAddCustomModuleTest(mitkImageVtkMapper2D_pic3dSwivel640x480 mitkImageVtkMapper2DSwivelTest #test for a randomly chosen Pic3D swivelled slice ${MITK_DATA_DIR}/Pic3D.nrrd #input image to load in data storage -V ${MITK_DATA_DIR}/RenderingTestData/ReferenceScreenshots/pic3dSwivel640x480REF.png #corresponding reference screenshot ) mitkAddCustomModuleTest(mitkPointSetVtkMapper2D_openMeAlone640x480 mitkPointSetVtkMapper2DTest ${MITK_DATA_DIR}/RenderingTestData/openMeAlone.mps #input point set to load in data storage -V ${MITK_DATA_DIR}/RenderingTestData/ReferenceScreenshots/openMeAlone640x480REF.png #corresponding reference screenshot ) mitkAddCustomModuleTest(mitkPointSetVtkMapper2D_Pic3DPointSetForPic3D640x480 mitkPointSetVtkMapper2DImageTest ${MITK_DATA_DIR}/Pic3D.nrrd ${MITK_DATA_DIR}/RenderingTestData/PointSetForPic3D.mps #input point set and image to load in data storage -V ${MITK_DATA_DIR}/RenderingTestData/ReferenceScreenshots/Pic3DPointSetForPic3D640x480REF.png #corresponding reference screenshot ) mitkAddCustomModuleTest(mitkPointSetVtkMapper2D_openMeAloneGlyphType640x480 mitkPointSetVtkMapper2DGlyphTypeTest ${MITK_DATA_DIR}/RenderingTestData/openMeAlone.mps #input point set to load in data storage -V ${MITK_DATA_DIR}/RenderingTestData/ReferenceScreenshots/openMeAloneGlyphType640x480REF.png #corresponding reference screenshot ) #Test reslice interpolation #note: nearest mode is already tested by swivel test mitkAddCustomModuleTest(ResliceInterpolationIsLinear mitkImageVtkMapper2DResliceInterpolationPropertyTest 1 #linear ${MITK_DATA_DIR}/Pic3D.nrrd -V ${MITK_DATA_DIR}/RenderingTestData/ReferenceScreenshots/pic3dRefLinear.png #corresponding reference screenshot LINEAR ) mitkAddCustomModuleTest(ResliceInterpolationIsCubic mitkImageVtkMapper2DResliceInterpolationPropertyTest 3 #cubic ${MITK_DATA_DIR}/Pic3D.nrrd -V ${MITK_DATA_DIR}/RenderingTestData/ReferenceScreenshots/pic3dRefCubic.png #corresponding reference screenshot CUBIC ) #End test reslice interpolation # Testing of the rendering of binary images mitkAddCustomModuleTest(mitkImageVtkMapper2D_binaryTestImage640x480 mitkImageVtkMapper2DTest #test for standard Pic3D axial slice ${MITK_DATA_DIR}/RenderingTestData/binaryImage.nrrd #input image to load in data storage -V ${MITK_DATA_DIR}/RenderingTestData/ReferenceScreenshots/binaryImage640x480REF.png #corresponding reference screenshot ) mitkAddCustomModuleTest(mitkImageVtkMapper2D_binaryTestImageWithRef640x480 mitkImageVtkMapper2DTest #test for standard Pic3D axial slice ${MITK_DATA_DIR}/Pic3D.nrrd ${MITK_DATA_DIR}/RenderingTestData/binaryImage.nrrd #input image to load in data storage -V ${MITK_DATA_DIR}/RenderingTestData/ReferenceScreenshots/binaryImageWithRef640x480REF.png #corresponding reference screenshot ) # End of binary image tests mitkAddCustomModuleTest(mitkSurfaceVtkMapper3DTest_TextureProperty mitkSurfaceVtkMapper3DTest ${MITK_DATA_DIR}/ToF-Data/Kinect_LiverPhantom.vtp ${MITK_DATA_DIR}/ToF-Data/Kinect_LiverPhantom_RGBImage.nrrd -V ${MITK_DATA_DIR}/RenderingTestData/ReferenceScreenshots/texturedLiver640x480REF.png #corresponding reference screenshot ) mitkAddCustomModuleTest(mitkImageVtkMapper2DTransferFunctionTest_Png2D-bw mitkImageVtkMapper2DTransferFunctionTest ${MITK_DATA_DIR}/Png2D-bw.png -V ${MITK_DATA_DIR}/RenderingTestData/ReferenceScreenshots/Png2D-bw-TransferFunctionRGBImage640x480REF.png #corresponding reference screenshot ) mitkAddCustomModuleTest(mitkSurfaceGLMapper2DColorTest_RedBall mitkSurfaceGLMapper2DColorTest ${MITK_DATA_DIR}/ball.stl -V ${MITK_DATA_DIR}/RenderingTestData/ReferenceScreenshots/ballColorRed640x480REF.png #corresponding reference screenshot ) mitkAddCustomModuleTest(mitkSurfaceGLMapper2DColorTest_DasArmeSchwein mitkSurfaceGLMapper2DColorTest ${MITK_DATA_DIR}/binary.stl -V ${MITK_DATA_DIR}/RenderingTestData/ReferenceScreenshots/binaryColorRed640x480REF.png #corresponding reference screenshot ) mitkAddCustomModuleTest(mitkSurfaceGLMapper2DOpacityTest_BallOpacity mitkSurfaceGLMapper2DOpacityTest #opacity = 50% (0.5) ${MITK_DATA_DIR}/ball.stl -V ${MITK_DATA_DIR}/RenderingTestData/ReferenceScreenshots/ballOpacity640x480REF.png #corresponding reference screenshot ) #Removed due to high rendering error. #mitkAddCustomModuleTest(mitkSurfaceVtkMapper3DTexturedSphereTest_Football mitkSurfaceVtkMapper3DTexturedSphereTest # ${MITK_DATA_DIR}/RenderingTestData/texture.jpg #input texture # -V # ${MITK_DATA_DIR}/RenderingTestData/ReferenceScreenshots/texturedSphere640x480REF.png corresponding reference screenshot #) SET_PROPERTY(TEST mitkImageVtkMapper2D_rgbaImage640x480 mitkImageVtkMapper2D_pic3d640x480 mitkImageVtkMapper2D_pic3dColorBlue640x480 mitkImageVtkMapper2D_pic3dLevelWindow640x480 mitkImageVtkMapper2D_pic3dSwivel640x480 mitkImageVtkMapper2DTransferFunctionTest_Png2D-bw mitkImageVtkMapper2D_pic3dOpacity640x480 mitkSurfaceGLMapper2DOpacityTest_BallOpacity mitkSurfaceGLMapper2DColorTest_DasArmeSchwein mitkSurfaceGLMapper2DColorTest_RedBall mitkSurfaceVtkMapper3DTest_TextureProperty mitkPointSetVtkMapper2D_Pic3DPointSetForPic3D640x480 mitkPointSetVtkMapper2D_openMeAlone640x480 mitkPointSetVtkMapper2D_openMeAloneGlyphType640x480 #mitkSurfaceVtkMapper3DTexturedSphereTest_Football PROPERTY RUN_SERIAL TRUE) endif() add_test(mitkPointSetLocaleTest ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${TESTDRIVER} mitkPointSetLocaleTest ${MITK_DATA_DIR}/pointSet.mps) set_property(TEST mitkPointSetLocaleTest PROPERTY LABELS MITK-Core) add_test(mitkImageWriterTest_nrrdImage ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${TESTDRIVER} mitkImageWriterTest ${MITK_DATA_DIR}/NrrdWritingTestImage.jpg) add_test(mitkImageWriterTest_2DPNGImage ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${TESTDRIVER} mitkImageWriterTest ${MITK_DATA_DIR}/Png2D-bw.png) add_test(mitkImageWriterTest_rgbPNGImage ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${TESTDRIVER} mitkImageWriterTest ${MITK_DATA_DIR}/RenderingTestData/rgbImage.png) add_test(mitkImageWriterTest_rgbaPNGImage ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${TESTDRIVER} mitkImageWriterTest ${MITK_DATA_DIR}/RenderingTestData/rgbaImage.png) set_property(TEST mitkImageWriterTest_nrrdImage PROPERTY LABELS MITK-Core) set_property(TEST mitkImageWriterTest_2DPNGImage PROPERTY LABELS MITK-Core) set_property(TEST mitkImageWriterTest_rgbPNGImage PROPERTY LABELS MITK-Core) set_property(TEST mitkImageWriterTest_rgbaPNGImage PROPERTY LABELS MITK-Core) -add_subdirectory(DICOMTesting) - +add_subdirectory(DCMTesting) diff --git a/Core/Code/Testing/DCMTesting/CMakeLists.txt b/Core/Code/Testing/DCMTesting/CMakeLists.txt new file mode 100644 index 0000000000..5671220e3f --- /dev/null +++ b/Core/Code/Testing/DCMTesting/CMakeLists.txt @@ -0,0 +1,59 @@ +# +# IMPORTANT NOTE: +# +# This module is a testing module for the DicomSeriesReader class, +# which is meant to be replaced by the Module DCMReader. +# To have the "old" code tested before it is removed, we still +# keep this DCMTesting module. +# +# Note that the DCMTesting module is practically a copy of +# this DCMTesting module, doing the same kind of testing, just +# for the new readers instead of DicomSeriesReader. +# + +if(BUILD_TESTING) +if(GDCM_DIR) + +# clear variables from prior files.cmake +# Else CMake would use the content of these variables and would try to create tests (which are not present in DCMTesting). +set(MODULE_TESTS) +set(MODULE_IMAGE_TESTS) +set(MODULE_SURFACE_TESTS) +set(MODULE_TESTIMAGES) +set(MODULE_TESTSURFACES) +set(MODULE_CUSTOM_TESTS) +set(H_FILES) +set(CPP_FILES) + +# now create a new module only for testing purposes +MITK_CREATE_MODULE( mitkDCMTesting + DEPENDS Mitk # Mitk.so +) + +MITK_CHECK_MODULE(_RESULT mitkDCMTesting) +if(_RESULT) + message(STATUS "mitkDCMTesting application won't be built. Missing: ${_RESULT}") +else(_RESULT) + +# add helpful applications +include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) + +# remove with newer MITK version +MITK_USE_MODULE( mitkDCMTesting ) +include_directories(${ALL_INCLUDE_DIRECTORIES}) + +# dumps out image information +add_executable(DumpDCMMitkImage DumpDCMMitkImage.cpp) +#Newer code: mitk_use_modules(TARGET DumpDCMMitkImage MODULES mitkDCMTesting) +target_link_libraries(DumpDCMMitkImage ${ALL_LIBRARIES}) + +# compares dumped out image information against reference dump +add_executable(VerifyDCMMitkImageDump VerifyDCMMitkImageDump.cpp) +#Newer code: mitk_use_modules(TARGET VerifyDCMMitkImageDump MODULES mitkDCMTesting) +target_link_libraries(VerifyDCMMitkImageDump ${ALL_LIBRARIES}) + +add_subdirectory(Testing) +endif() + +endif() +endif() diff --git a/Core/Code/Testing/DICOMTesting/DumpDICOMMitkImage.cpp b/Core/Code/Testing/DCMTesting/DumpDCMMitkImage.cpp similarity index 69% copy from Core/Code/Testing/DICOMTesting/DumpDICOMMitkImage.cpp copy to Core/Code/Testing/DCMTesting/DumpDCMMitkImage.cpp index 23f0a4f17c..43730db971 100644 --- a/Core/Code/Testing/DICOMTesting/DumpDICOMMitkImage.cpp +++ b/Core/Code/Testing/DCMTesting/DumpDCMMitkImage.cpp @@ -1,38 +1,37 @@ /*=================================================================== 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 "mitkTestDICOMLoading.h" +#include "mitkTestDCMLoading.h" int main(int argc, char** argv) { - mitk::TestDICOMLoading loader; - mitk::TestDICOMLoading::StringContainer files; + mitk::TestDCMLoading loader; + mitk::TestDCMLoading::StringContainer files; for (int arg = 1; arg < argc; ++arg) files.push_back( argv[arg] ); - mitk::TestDICOMLoading::ImageList images = loader.LoadFiles(files); + mitk::TestDCMLoading::ImageList images = loader.LoadFiles(files); - // combine individual dumps in a way that VerifyDICOMMitkImageDump is able to separate again. - // I.e.: when changing this piece of code, always change VerifyDICOMMitkImageDump, too. + // combine individual dumps in a way that VerifyDCMMitkImageDump is able to separate again. + // I.e.: when changing this piece of code, always change VerifyDCMMitkImageDump, too. unsigned int imageCounter(0); - for ( mitk::TestDICOMLoading::ImageList::const_iterator imageIter = images.begin(); + for ( mitk::TestDCMLoading::ImageList::const_iterator imageIter = images.begin(); imageIter != images.end(); ++imageIter ) { std::cout << "-- Image " << ++imageCounter << "\n"; std::cout << loader.DumpImageInformation( *imageIter ) << "\n"; } } - diff --git a/Core/Code/Testing/DICOMTesting/Testing/CMakeLists.txt b/Core/Code/Testing/DCMTesting/Testing/CMakeLists.txt similarity index 54% copy from Core/Code/Testing/DICOMTesting/Testing/CMakeLists.txt copy to Core/Code/Testing/DCMTesting/Testing/CMakeLists.txt index 0aca28227f..651e168041 100644 --- a/Core/Code/Testing/DICOMTesting/Testing/CMakeLists.txt +++ b/Core/Code/Testing/DCMTesting/Testing/CMakeLists.txt @@ -1,88 +1,86 @@ MITK_CREATE_MODULE_TESTS(LABELS MITK-Core) include(mitkFunctionAddTestLabel) # verify minimum expectations: -# files are recognized as DICOM +# files are recognized as DCM # loading files results in a given number of images -mitkAddCustomModuleTest(mitkDICOMTestingSanityTest_NoFiles mitkDICOMTestingSanityTest 0) -mitkAddCustomModuleTest(mitkDICOMTestingSanityTest_CTImage mitkDICOMTestingSanityTest 1 ${MITK_DATA_DIR}/spacing-ok-ct.dcm) -mitkAddCustomModuleTest(mitkDICOMTestingSanityTest_MRImage mitkDICOMTestingSanityTest 1 ${MITK_DATA_DIR}/spacing-ok-mr.dcm) -mitkAddCustomModuleTest(mitkDICOMTestingSanityTest_SCImage mitkDICOMTestingSanityTest 1 ${MITK_DATA_DIR}/spacing-ok-sc.dcm) -mitkAddCustomModuleTest(mitkDICOMTestingSanityTest_NoImagePositionPatient mitkDICOMTestingSanityTest 1 ${MITK_DATA_DIR}/spacing-ok-sc-no2032.dcm) +mitkAddCustomModuleTest(mitkDCMTestingSanityTest_NoFiles mitkDCMTestingSanityTest 0) +mitkAddCustomModuleTest(mitkDCMTestingSanityTest_CTImage mitkDCMTestingSanityTest 1 ${MITK_DATA_DIR}/spacing-ok-ct.dcm) +mitkAddCustomModuleTest(mitkDCMTestingSanityTest_MRImage mitkDCMTestingSanityTest 1 ${MITK_DATA_DIR}/spacing-ok-mr.dcm) +mitkAddCustomModuleTest(mitkDCMTestingSanityTest_SCImage mitkDCMTestingSanityTest 1 ${MITK_DATA_DIR}/spacing-ok-sc.dcm) +mitkAddCustomModuleTest(mitkDCMTestingSanityTest_NoImagePositionPatient mitkDCMTestingSanityTest 1 ${MITK_DATA_DIR}/spacing-ok-sc-no2032.dcm) # verifies that the loader can also be used to just scan for tags and provide them in mitk::Properties (parameter preLoadedVolume) -mitkAddCustomModuleTest(mitkDICOMPreloadedVolumeTest_Slice mitkDICOMPreloadedVolumeTest ${MITK_DATA_DIR}/spacing-ok-ct.dcm) +mitkAddCustomModuleTest(mitkDCMPreloadedVolumeTest_Slice mitkDCMPreloadedVolumeTest ${MITK_DATA_DIR}/spacing-ok-ct.dcm) -set(VERIFY_DUMP_CMD ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/VerifyDICOMMitkImageDump) +set(VERIFY_DUMP_CMD ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/VerifyDCMMitkImageDump) set(CT_ABDOMEN_DIR ${MITK_DATA_DIR}/TinyCTAbdomen) set(MR_HEART_DIR ${MITK_DATA_DIR}/3D+t-Heart) set(CT_TILT_HEAD_DIR ${MITK_DATA_DIR}/TiltHead) set(CT_TILT_DIR ${MITK_DATA_DIR}/TiltedData) -set(SOP_CLASSES_DIR ${MITK_DATA_DIR}/DICOMReader) +set(SOP_CLASSES_DIR ${MITK_DATA_DIR}/DCMReader) # these variables could be passed as parameters to a generic test creation function set(TESTS_DIR Tests) set(INPUT_LISTNAME input) set(EXPECTED_DUMP expected.dump) -function(AddDicomTestsFromDataRepository CURRENT_DATASET_DIR TESTS_DIR INPUT_LISTNAME EXPECTED_DUMP) +function(AddDcmTestsFromDataRepository CURRENT_DATASET_DIR TESTS_DIR INPUT_LISTNAME EXPECTED_DUMP) # find all test input lists file(GLOB_RECURSE allInputs ${CURRENT_DATASET_DIR}/${TESTS_DIR}/*/${INPUT_LISTNAME}) function(expectFileExists filename) if(NOT EXISTS ${filename}) message(SEND_ERROR "Test case expected file ${filename} which does not exist! Please fix your CMake code or file layout.") endif(NOT EXISTS ${filename}) endfunction(expectFileExists) foreach(input ${allInputs}) # extract only the name of the directory of this very test case string(REGEX REPLACE ".*${TESTS_DIR}/([^/]+)/.*" "\\1" input ${input}) set(inputfilelist "${CURRENT_DATASET_DIR}/${TESTS_DIR}/${input}/${INPUT_LISTNAME}") set(expecteddump "${CURRENT_DATASET_DIR}/${TESTS_DIR}/${input}/${EXPECTED_DUMP}") - set(testname "DICOM_Load_${input}") + set(testname "DCM_Load_${input}") - #message(STATUS "DICOM loading test case '${input}'") + #message(STATUS "DCM loading test case '${input}'") expectFileExists(${inputfilelist}) expectFileExists(${expecteddump}) # TODO provide error messages if input not valid set(dicomFiles) # clear list # read list of file names from file "input" - file(STRINGS ${inputfilelist} rawDicomFiles) - foreach(raw ${rawDicomFiles}) + file(STRINGS ${inputfilelist} rawDcmFiles) + foreach(raw ${rawDcmFiles}) # prepend each file with an abosolute path set(fileWithFullPath ${CURRENT_DATASET_DIR}/${raw}) list(APPEND dicomFiles ${fileWithFullPath}) - endforeach(raw ${rawDicomFiles}) + endforeach(raw ${rawDcmFiles}) #message(STATUS " Load ${dicomFiles}") add_test(${testname} ${VERIFY_DUMP_CMD} ${expecteddump} ${dicomFiles}) mitkFunctionAddTestLabel(${testname}) endforeach(input allInputs) -endfunction(AddDicomTestsFromDataRepository CURRENT_DATASET_DIR TESTS_DIR INPUT_LISTNAME EXPECTED_DUMP) +endfunction(AddDcmTestsFromDataRepository CURRENT_DATASET_DIR TESTS_DIR INPUT_LISTNAME EXPECTED_DUMP) -AddDicomTestsFromDataRepository(${CT_ABDOMEN_DIR} ${TESTS_DIR} ${INPUT_LISTNAME} ${EXPECTED_DUMP}) -AddDicomTestsFromDataRepository(${CT_TILT_HEAD_DIR} ${TESTS_DIR} ${INPUT_LISTNAME} ${EXPECTED_DUMP}) -AddDicomTestsFromDataRepository(${CT_TILT_DIR} ${TESTS_DIR} ${INPUT_LISTNAME} ${EXPECTED_DUMP}) -AddDicomTestsFromDataRepository(${MR_HEART_DIR} ${TESTS_DIR} ${INPUT_LISTNAME} ${EXPECTED_DUMP}) +AddDcmTestsFromDataRepository(${CT_ABDOMEN_DIR} ${TESTS_DIR} ${INPUT_LISTNAME} ${EXPECTED_DUMP}) +AddDcmTestsFromDataRepository(${CT_TILT_HEAD_DIR} ${TESTS_DIR} ${INPUT_LISTNAME} ${EXPECTED_DUMP}) +AddDcmTestsFromDataRepository(${CT_TILT_DIR} ${TESTS_DIR} ${INPUT_LISTNAME} ${EXPECTED_DUMP}) +AddDcmTestsFromDataRepository(${MR_HEART_DIR} ${TESTS_DIR} ${INPUT_LISTNAME} ${EXPECTED_DUMP}) -AddDicomTestsFromDataRepository(${SOP_CLASSES_DIR} ${TESTS_DIR} ${INPUT_LISTNAME} ${EXPECTED_DUMP}) +AddDcmTestsFromDataRepository(${SOP_CLASSES_DIR} ${TESTS_DIR} ${INPUT_LISTNAME} ${EXPECTED_DUMP}) # use one more realistic series for testing property lists file(GLOB_RECURSE abdomenImages ${CT_ABDOMEN_DIR}/14?) # this is just one small volume -mitkAddCustomModuleTest(mitkDICOMPreloadedVolumeTest_Abdomen mitkDICOMPreloadedVolumeTest ${abdomenImages}) - - +mitkAddCustomModuleTest(mitkDCMPreloadedVolumeTest_Abdomen mitkDCMPreloadedVolumeTest ${abdomenImages}) diff --git a/Core/Code/Testing/DICOMTesting/Testing/files.cmake b/Core/Code/Testing/DCMTesting/Testing/files.cmake similarity index 77% copy from Core/Code/Testing/DICOMTesting/Testing/files.cmake copy to Core/Code/Testing/DCMTesting/Testing/files.cmake index eb8f989025..1b775fa79a 100644 --- a/Core/Code/Testing/DICOMTesting/Testing/files.cmake +++ b/Core/Code/Testing/DCMTesting/Testing/files.cmake @@ -1,12 +1,11 @@ # tests with no extra command line parameter set(MODULE_CUSTOM_TESTS - mitkDICOMTestingSanityTest.cpp - mitkDICOMPreloadedVolumeTest.cpp + mitkDCMTestingSanityTest.cpp + mitkDCMPreloadedVolumeTest.cpp ) # this shouldn't be necessary if this variable # would actually be a parameter of the MITK_CREATE_MODULE_TESTS # macro. See bug #10592 set(TEST_CPP_FILES "") - diff --git a/Core/Code/Testing/DICOMTesting/Testing/mitkDICOMPreloadedVolumeTest.cpp b/Core/Code/Testing/DCMTesting/Testing/mitkDCMPreloadedVolumeTest.cpp similarity index 86% copy from Core/Code/Testing/DICOMTesting/Testing/mitkDICOMPreloadedVolumeTest.cpp copy to Core/Code/Testing/DCMTesting/Testing/mitkDCMPreloadedVolumeTest.cpp index c1864b1330..7ca59ee05b 100644 --- a/Core/Code/Testing/DICOMTesting/Testing/mitkDICOMPreloadedVolumeTest.cpp +++ b/Core/Code/Testing/DCMTesting/Testing/mitkDCMPreloadedVolumeTest.cpp @@ -1,105 +1,104 @@ /*=================================================================== 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 "mitkTestDICOMLoading.h" +#include "mitkTestDCMLoading.h" #include "mitkTestingMacros.h" bool CheckAllPropertiesAreInOtherList(const mitk::PropertyList* list, const mitk::PropertyList* otherList) { MITK_TEST_CONDITION_REQUIRED(list && otherList, "Comparison is passed two non-empty property lists") const mitk::PropertyList::PropertyMap* listM = list->GetMap(); const mitk::PropertyList::PropertyMap* otherListM = otherList->GetMap(); bool equal = true; for ( mitk::PropertyList::PropertyMap::const_iterator iter = listM->begin(); iter != listM->end(); ++iter ) { std::string key = iter->first; mitk::BaseProperty* property = iter->second; mitk::PropertyList::PropertyMap::const_iterator otherEntry = otherListM->find( key ); MITK_TEST_CONDITION( otherEntry != otherListM->end(), " Property '" << key << "' is contained in other list" ) mitk::BaseProperty* otherProperty = otherEntry->second; MITK_TEST_CONDITION( equal &= (*property == *otherProperty), " Property '" << key << "' is equal in both list" ) } return equal; } bool VerifyPropertyListsEquality(const mitk::PropertyList* testList, const mitk::PropertyList* referenceList) { bool allTestPropsInReference = CheckAllPropertiesAreInOtherList(testList, referenceList); MITK_TEST_CONDITION(allTestPropsInReference, "All test properties found in reference properties") bool allReferencePropsInTest = CheckAllPropertiesAreInOtherList(referenceList, testList); MITK_TEST_CONDITION(allReferencePropsInTest, "All reference properties found in test properties") return allTestPropsInReference && allReferencePropsInTest; } -int mitkDICOMPreloadedVolumeTest(int argc, char** const argv) +int mitkDCMPreloadedVolumeTest(int argc, char** const argv) { - MITK_TEST_BEGIN("DICOMPreloadedVolume") + MITK_TEST_BEGIN("DCMPreloadedVolume") - mitk::TestDICOMLoading loader; - mitk::TestDICOMLoading::StringContainer files; + mitk::TestDCMLoading loader; + mitk::TestDCMLoading::StringContainer files; // adapt expectations depending on configuration std::string configuration = mitk::DicomSeriesReader::GetConfigurationString(); MITK_TEST_OUTPUT(<< "Configuration: " << configuration) // load files from commandline for (int arg = 1; arg < argc; ++arg) { MITK_TEST_OUTPUT(<< "Test file " << argv[arg]) files.push_back( argv[arg] ); } - // verify all files are DICOM - for (mitk::TestDICOMLoading::StringContainer::const_iterator fileIter = files.begin(); + // verify all files are DCM + for (mitk::TestDCMLoading::StringContainer::const_iterator fileIter = files.begin(); fileIter != files.end(); ++fileIter) { - MITK_TEST_CONDITION_REQUIRED( mitk::DicomSeriesReader::IsDicom(*fileIter) , *fileIter << " is recognized as loadable DICOM object" ) + MITK_TEST_CONDITION_REQUIRED( mitk::DicomSeriesReader::IsDicom(*fileIter) , *fileIter << " is recognized as loadable DCM object" ) } // load for a first time - mitk::TestDICOMLoading::ImageList images = loader.LoadFiles(files); + mitk::TestDCMLoading::ImageList images = loader.LoadFiles(files); MITK_TEST_OUTPUT(<< "Loaded " << images.size() << " images. Remembering properties of the first one for later comparison.") mitk::Image::Pointer firstImage = images.front(); mitk::PropertyList::Pointer originalProperties = firstImage->GetPropertyList(); // save the original firstImage->SetPropertyList( mitk::PropertyList::New() ); // clear image properties // what about DEFAULT properties? currently ONLY nodes get default properties // load for a second time, this time provide the image volume as a pointer // expectation is that the reader will provide the same properties to this image (without actually loading a new mitk::Image) - mitk::TestDICOMLoading::ImageList reloadedImages = loader.LoadFiles(files, firstImage); + mitk::TestDCMLoading::ImageList reloadedImages = loader.LoadFiles(files, firstImage); MITK_TEST_OUTPUT(<< "Again loaded " << reloadedImages.size() << " images. Comparing to previously loaded version.") mitk::Image::Pointer reloadedImage = reloadedImages.front(); mitk::PropertyList::Pointer regeneratedProperties = reloadedImage->GetPropertyList(); // get the version of the second load attempt bool listsAreEqual = VerifyPropertyListsEquality(regeneratedProperties, originalProperties); MITK_TEST_CONDITION(listsAreEqual, "LoadDicomSeries generates a valid property list when provided a pre-loaded image"); MITK_TEST_END() } - diff --git a/Core/Code/Testing/DICOMTesting/Testing/mitkDICOMTestingSanityTest.cpp b/Core/Code/Testing/DCMTesting/Testing/mitkDCMTestingSanityTest.cpp similarity index 79% copy from Core/Code/Testing/DICOMTesting/Testing/mitkDICOMTestingSanityTest.cpp copy to Core/Code/Testing/DCMTesting/Testing/mitkDCMTestingSanityTest.cpp index a7bd359b70..88f782becf 100644 --- a/Core/Code/Testing/DICOMTesting/Testing/mitkDICOMTestingSanityTest.cpp +++ b/Core/Code/Testing/DCMTesting/Testing/mitkDCMTestingSanityTest.cpp @@ -1,69 +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. ===================================================================*/ -#include "mitkTestDICOMLoading.h" +#include "mitkTestDCMLoading.h" #include "mitkTestingMacros.h" -int mitkDICOMTestingSanityTest(int argc, char** const argv) +int mitkDCMTestingSanityTest(int argc, char** const argv) { - MITK_TEST_BEGIN("DICOMTestingSanity") + MITK_TEST_BEGIN("DCMTestingSanity") - mitk::TestDICOMLoading loader; - mitk::TestDICOMLoading::StringContainer files; + mitk::TestDCMLoading loader; + mitk::TestDCMLoading::StringContainer files; // adapt expectations depending on configuration std::string configuration = mitk::DicomSeriesReader::GetConfigurationString(); MITK_TEST_OUTPUT(<< "Configuration: " << configuration) /* MITK_TEST_CONDITION_REQUIRED( configuration.find( "GDCM_VERSION: 2." ) != std::string::npos, "Expect at least GDCM version 2" ) */ // load files from commandline unsigned int numberOfExpectedImages = 0; if (argc > 1) numberOfExpectedImages = atoi(argv[1]); for (int arg = 2; arg < argc; ++arg) files.push_back( argv[arg] ); - // verify all files are DICOM - for (mitk::TestDICOMLoading::StringContainer::const_iterator fileIter = files.begin(); + // verify all files are DCM + for (mitk::TestDCMLoading::StringContainer::const_iterator fileIter = files.begin(); fileIter != files.end(); ++fileIter) { - MITK_TEST_CONDITION_REQUIRED( mitk::DicomSeriesReader::IsDicom(*fileIter) , *fileIter << " is recognized as loadable DICOM object" ) + MITK_TEST_CONDITION_REQUIRED( mitk::DicomSeriesReader::IsDicom(*fileIter) , *fileIter << " is recognized as loadable DCM object" ) } // compare with expected number of images from commandline - mitk::TestDICOMLoading::ImageList images = loader.LoadFiles(files); + mitk::TestDCMLoading::ImageList images = loader.LoadFiles(files); MITK_TEST_CONDITION_REQUIRED( images.size() == numberOfExpectedImages, "Loading " << files.size() << " files from commandline results in " << numberOfExpectedImages << " images (see test invocation)" ) // check dump equality (dumping image information must always equal itself) - for ( mitk::TestDICOMLoading::ImageList::const_iterator imageIter = images.begin(); + for ( mitk::TestDCMLoading::ImageList::const_iterator imageIter = images.begin(); imageIter != images.end(); ++imageIter ) { const mitk::Image* image = *imageIter; MITK_TEST_CONDITION( loader.CompareImageInformationDumps( loader.DumpImageInformation(image), loader.DumpImageInformation(image) ) == true, "Image information dumping is able to reproduce its result." ) } MITK_TEST_END() } - diff --git a/Core/Code/Testing/DICOMTesting/VerifyDICOMMitkImageDump.cpp b/Core/Code/Testing/DCMTesting/VerifyDCMMitkImageDump.cpp similarity index 84% copy from Core/Code/Testing/DICOMTesting/VerifyDICOMMitkImageDump.cpp copy to Core/Code/Testing/DCMTesting/VerifyDCMMitkImageDump.cpp index 7772e8e460..4212855c4a 100644 --- a/Core/Code/Testing/DICOMTesting/VerifyDICOMMitkImageDump.cpp +++ b/Core/Code/Testing/DCMTesting/VerifyDCMMitkImageDump.cpp @@ -1,122 +1,121 @@ /*=================================================================== 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 "mitkTestDICOMLoading.h" +#include "mitkTestDCMLoading.h" std::vector LoadDumps(const std::string& fileName) { std::vector separatedDumps; std::ifstream fileStream( fileName.c_str() ); std::string buffer; std::string line; while(fileStream){ std::getline(fileStream, line); if (line.find("-- Image ") == 0) { // separator: starts a new image block if ( !buffer.empty() ) { // unless this is the first block separatedDumps.push_back(buffer); buffer.clear(); } } else { buffer += line + "\n"; } } fileStream.close(); // eat last image dump if ( !buffer.empty() ) { separatedDumps.push_back(buffer); buffer.clear(); } return separatedDumps; } int main(int argc, char** argv) { /** - Loads a list of DICOM images, compares generated mitk::Images against stored references. + Loads a list of DCM images, compares generated mitk::Images against stored references. first argument: file with reference dumps following arguments: file names to load */ if (argc < 2) { MITK_ERROR << "Usage:"; MITK_ERROR << " " << argv[0] << " reference.dump file1 [file2 .. fileN]"; MITK_ERROR << " "; - MITK_ERROR << " Loads all DICOM images in file1 to fileN as MITK images "; + MITK_ERROR << " Loads all DCM images in file1 to fileN as MITK images "; MITK_ERROR << " and compares loaded images against stored expectations (dumps)."; - MITK_ERROR << " See also DumpDICOMMitkImage (generates dumps)"; + MITK_ERROR << " See also DumpDCMMitkImage (generates dumps)"; return EXIT_FAILURE; } - mitk::TestDICOMLoading loader; - mitk::TestDICOMLoading::StringContainer files; + mitk::TestDCMLoading loader; + mitk::TestDCMLoading::StringContainer files; // TODO load reference dumps // load from argv[1] // separate at "-- Image n" // store in an array of dumps std::vector expectedDumps = LoadDumps(argv[1]); for (int arg = 2; arg < argc; ++arg) files.push_back( argv[arg] ); - mitk::TestDICOMLoading::ImageList images = loader.LoadFiles(files); + mitk::TestDCMLoading::ImageList images = loader.LoadFiles(files); unsigned int imageCounter(0); - for ( mitk::TestDICOMLoading::ImageList::const_iterator imageIter = images.begin(); + for ( mitk::TestDCMLoading::ImageList::const_iterator imageIter = images.begin(); imageIter != images.end(); ++imageIter, ++imageCounter ) { std::string imageDump = loader.DumpImageInformation( *imageIter ); if (imageCounter >= expectedDumps.size()) { MITK_ERROR << "Loader produces more images than expected. Aborting after image " << (imageCounter-1); MITK_INFO << "Image " << imageCounter << " loaded as:\n" << imageDump; return EXIT_FAILURE; } bool loadedAsExpected = loader.CompareImageInformationDumps( expectedDumps[imageCounter], imageDump ); if (loadedAsExpected) { MITK_INFO << "Image " << imageCounter << " loads as expected."; } else { MITK_ERROR << "Image " << imageCounter << " did not load as expected."; MITK_INFO << "Expected: \n" << expectedDumps[imageCounter] << "\nGot:\n" << imageDump; return EXIT_FAILURE; } } return EXIT_SUCCESS; } - diff --git a/Core/Code/Testing/DCMTesting/files.cmake b/Core/Code/Testing/DCMTesting/files.cmake new file mode 100644 index 0000000000..7a17edf5d3 --- /dev/null +++ b/Core/Code/Testing/DCMTesting/files.cmake @@ -0,0 +1,4 @@ + +set(CPP_FILES + mitkTestDCMLoading.cpp +) diff --git a/Core/Code/Testing/DICOMTesting/mitkTestDICOMLoading.cpp b/Core/Code/Testing/DCMTesting/mitkTestDCMLoading.cpp similarity index 92% copy from Core/Code/Testing/DICOMTesting/mitkTestDICOMLoading.cpp copy to Core/Code/Testing/DCMTesting/mitkTestDCMLoading.cpp index 4cc6059762..196a725082 100644 --- a/Core/Code/Testing/DICOMTesting/mitkTestDICOMLoading.cpp +++ b/Core/Code/Testing/DCMTesting/mitkTestDCMLoading.cpp @@ -1,443 +1,448 @@ /*=================================================================== 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 "mitkTestDICOMLoading.h" +#include +#include "mitkTestDCMLoading.h" #include -mitk::TestDICOMLoading::TestDICOMLoading() +mitk::TestDCMLoading::TestDCMLoading() :m_PreviousCLocale(NULL) { } -void mitk::TestDICOMLoading::SetDefaultLocale() +void mitk::TestDCMLoading::SetDefaultLocale() { // remember old locale only once if (m_PreviousCLocale == NULL) { m_PreviousCLocale = setlocale(LC_NUMERIC, NULL); // set to "C" setlocale(LC_NUMERIC, "C"); m_PreviousCppLocale = std::cin.getloc(); std::locale l( "C" ); std::cin.imbue(l); std::cout.imbue(l); } } -void mitk::TestDICOMLoading::ResetUserLocale() +void mitk::TestDCMLoading::ResetUserLocale() { if (m_PreviousCLocale) { setlocale(LC_NUMERIC, m_PreviousCLocale); std::cin.imbue(m_PreviousCppLocale); std::cout.imbue(m_PreviousCppLocale); m_PreviousCLocale = NULL; } } -mitk::TestDICOMLoading::ImageList mitk::TestDICOMLoading::LoadFiles( const StringContainer& files, Image::Pointer preLoadedVolume ) +mitk::TestDCMLoading::ImageList mitk::TestDCMLoading::LoadFiles( const StringContainer& files, Image::Pointer preLoadedVolume ) { for (StringContainer::const_iterator iter = files.begin(); iter != files.end(); ++iter) { MITK_DEBUG << "File " << *iter; } ImageList result; DicomSeriesReader::FileNamesGrouping seriesInFiles = DicomSeriesReader::GetSeries( files, true ); // TODO sort series UIDs, implementation of map iterator might differ on different platforms (or verify this is a standard topic??) for (DicomSeriesReader::FileNamesGrouping::const_iterator seriesIter = seriesInFiles.begin(); seriesIter != seriesInFiles.end(); ++seriesIter) { StringContainer files = seriesIter->second.GetFilenames(); DataNode::Pointer node = DicomSeriesReader::LoadDicomSeries( files, true, true, true, 0, preLoadedVolume ); // true, true, true ist just a copy of the default values if (node.IsNotNull()) { Image::Pointer image = dynamic_cast( node->GetData() ); result.push_back( image ); } else { } } return result; } std::string -mitk::TestDICOMLoading::ComponentTypeToString(int type) +mitk::TestDCMLoading::ComponentTypeToString(int type) { if (type == itk::ImageIOBase::UCHAR) return "UCHAR"; else if (type == itk::ImageIOBase::CHAR) return "CHAR"; else if (type == itk::ImageIOBase::USHORT) return "USHORT"; else if (type == itk::ImageIOBase::SHORT) return "SHORT"; else if (type == itk::ImageIOBase::UINT) return "UINT"; else if (type == itk::ImageIOBase::INT) return "INT"; else if (type == itk::ImageIOBase::ULONG) return "ULONG"; else if (type == itk::ImageIOBase::LONG) return "LONG"; else if (type == itk::ImageIOBase::FLOAT) return "FLOAT"; else if (type == itk::ImageIOBase::DOUBLE) return "DOUBLE"; else return "UNKNOWN"; } // add a line to stringstream result (see DumpImageInformation #define DumpLine(field, data) DumpILine(0, field, data) // add an indented(!) line to stringstream result (see DumpImageInformation #define DumpILine(indent, field, data) \ { \ std::string DumpLine_INDENT; DumpLine_INDENT.resize(indent, ' ' ); \ result << DumpLine_INDENT << field << ": " << data << "\n"; \ } std::string -mitk::TestDICOMLoading::DumpImageInformation( const Image* image ) +mitk::TestDCMLoading::DumpImageInformation( const Image* image ) { + std::stringstream result; if (image == NULL) return result.str(); SetDefaultLocale(); // basic image data DumpLine( "Pixeltype", ComponentTypeToString(image->GetPixelType().GetComponentType()) ); DumpLine( "BitsPerPixel", image->GetPixelType().GetBpe() ); DumpLine( "Dimension", image->GetDimension() ); result << "Dimensions: "; for (unsigned int dim = 0; dim < image->GetDimension(); ++dim) result << image->GetDimension(dim) << " "; result << "\n"; // geometry data result << "Geometry: \n"; Geometry3D* geometry = image->GetGeometry(); if (geometry) { AffineTransform3D* transform = geometry->GetIndexToWorldTransform(); if (transform) { result << " " << "Matrix: "; const AffineTransform3D::MatrixType& matrix = transform->GetMatrix(); for (unsigned int i = 0; i < 3; ++i) for (unsigned int j = 0; j < 3; ++j) result << matrix[i][j] << " "; result << "\n"; result << " " << "Offset: "; const AffineTransform3D::OutputVectorType& offset = transform->GetOffset(); for (unsigned int i = 0; i < 3; ++i) result << offset[i] << " "; result << "\n"; result << " " << "Center: "; const AffineTransform3D::InputPointType& center = transform->GetCenter(); for (unsigned int i = 0; i < 3; ++i) result << center[i] << " "; result << "\n"; result << " " << "Translation: "; const AffineTransform3D::OutputVectorType& translation = transform->GetTranslation(); for (unsigned int i = 0; i < 3; ++i) result << translation[i] << " "; result << "\n"; result << " " << "Scale: "; const double* scale = transform->GetScale(); for (unsigned int i = 0; i < 3; ++i) result << scale[i] << " "; result << "\n"; result << " " << "Origin: "; const Point3D& origin = geometry->GetOrigin(); for (unsigned int i = 0; i < 3; ++i) result << origin[i] << " "; result << "\n"; result << " " << "Spacing: "; const Vector3D& spacing = geometry->GetSpacing(); for (unsigned int i = 0; i < 3; ++i) result << spacing[i] << " "; result << "\n"; result << " " << "TimeBounds: "; const TimeBounds timeBounds = geometry->GetTimeBounds(); for (unsigned int i = 0; i < 2; ++i) result << timeBounds[i] << " "; result << "\n"; } } ResetUserLocale(); return result.str(); } std::string -mitk::TestDICOMLoading::trim(const std::string& pString, +mitk::TestDCMLoading::trim(const std::string& pString, const std::string& pWhitespace) { const size_t beginStr = pString.find_first_not_of(pWhitespace); if (beginStr == std::string::npos) { // no content return ""; } const size_t endStr = pString.find_last_not_of(pWhitespace); const size_t range = endStr - beginStr + 1; return pString.substr(beginStr, range); } std::string -mitk::TestDICOMLoading::reduce(const std::string& pString, +mitk::TestDCMLoading::reduce(const std::string& pString, const std::string& pFill, const std::string& pWhitespace) { // trim first std::string result(trim(pString, pWhitespace)); // replace sub ranges size_t beginSpace = result.find_first_of(pWhitespace); while (beginSpace != std::string::npos) { const size_t endSpace = result.find_first_not_of(pWhitespace, beginSpace); const size_t range = endSpace - beginSpace; result.replace(beginSpace, range, pFill); const size_t newStart = beginSpace + pFill.length(); beginSpace = result.find_first_of(pWhitespace, newStart); } return result; } bool -mitk::TestDICOMLoading::CompareSpacedValueFields( const std::string& reference, +mitk::TestDCMLoading::CompareSpacedValueFields( const std::string& reference, const std::string& test, double /*eps*/ ) { + bool result(true); // tokenize string, compare each token, if possible by float comparison std::stringstream referenceStream(reduce(reference)); std::stringstream testStream(reduce(test)); std::string refToken; std::string testToken; while ( std::getline( referenceStream, refToken, ' ' ) && std::getline ( testStream, testToken, ' ' ) ) { float refNumber; float testNumber; if ( this->StringToNumber(refToken, refNumber) ) { if ( this->StringToNumber(testToken, testNumber) ) { // print-out compared tokens if DEBUG output allowed MITK_DEBUG << "Reference Token '" << refToken << "'" << " value " << refNumber << ", test Token '" << testToken << "'" << " value " << testNumber; + + bool old_result = result; - result &= ( fabs(refNumber - testNumber) < mitk::eps ); + + result &= ( fabs(refNumber - testNumber) < 0.0001 /*mitk::eps*/ ); // log the token/number which causes the test to fail if( old_result != result) { MITK_ERROR << std::setprecision(16) << "Reference Token '" << refToken << "'" << " value " << refNumber << ", test Token '" << testToken << "'" << " value " << testNumber; - MITK_ERROR << "[FALSE] - difference: " << std::setprecision(16) << fabs(refNumber - testNumber) << " EPS: " << mitk::eps; + MITK_ERROR << "[FALSE] - difference: " << std::setprecision(16) << fabs(refNumber - testNumber) << " EPS: " << 0.0001;// mitk::eps; } } else { MITK_ERROR << refNumber << " cannot be compared to '" << testToken << "'"; } } else { MITK_DEBUG << "Token '" << refToken << "'" << " handled as string"; result &= refToken == testToken; } } if ( std::getline( referenceStream, refToken, ' ' ) ) { MITK_ERROR << "Reference string still had values when test string was already parsed: ref '" << reference << "', test '" << test << "'"; result = false; } else if ( std::getline( testStream, testToken, ' ' ) ) { MITK_ERROR << "Test string still had values when reference string was already parsed: ref '" << reference << "', test '" << test << "'"; result = false; } return result; } bool -mitk::TestDICOMLoading::CompareImageInformationDumps( const std::string& referenceDump, +mitk::TestDCMLoading::CompareImageInformationDumps( const std::string& referenceDump, const std::string& testDump ) { KeyValueMap reference = ParseDump(referenceDump); KeyValueMap test = ParseDump(testDump); bool testResult(true); // verify all expected values for (KeyValueMap::const_iterator refIter = reference.begin(); refIter != reference.end(); ++refIter) { const std::string& refKey = refIter->first; const std::string& refValue = refIter->second; if ( test.find(refKey) != test.end() ) { const std::string& testValue = test[refKey]; bool thisTestResult = CompareSpacedValueFields( refValue, testValue ); testResult &= thisTestResult; MITK_DEBUG << refKey << ": '" << refValue << "' == '" << testValue << "' ? " << (thisTestResult?"YES":"NO"); } else { MITK_ERROR << "Reference dump contains a key'" << refKey << "' (value '" << refValue << "')." ; MITK_ERROR << "This key is expected to be generated for tests (but was not). Most probably you need to update your test data."; return false; } } // now check test dump does not contain any additional keys for (KeyValueMap::const_iterator testIter = test.begin(); testIter != test.end(); ++testIter) { const std::string& key = testIter->first; const std::string& value = testIter->second; if ( reference.find(key) == reference.end() ) { MITK_ERROR << "Test dump contains an unexpected key'" << key << "' (value '" << value << "')." ; MITK_ERROR << "This key is not expected. Most probably you need to update your test data."; return false; } } return testResult; } -mitk::TestDICOMLoading::KeyValueMap -mitk::TestDICOMLoading::ParseDump( const std::string& dump ) +mitk::TestDCMLoading::KeyValueMap +mitk::TestDCMLoading::ParseDump( const std::string& dump ) { KeyValueMap parsedResult; std::string shredder(dump); std::stack surroundingKeys; std::stack expectedIndents; expectedIndents.push(0); while (true) { std::string::size_type newLinePos = shredder.find( '\n' ); if (newLinePos == std::string::npos || newLinePos == 0) break; std::string line = shredder.substr( 0, newLinePos ); shredder = shredder.erase( 0, newLinePos+1 ); std::string::size_type keyPosition = line.find_first_not_of( ' ' ); std::string::size_type colonPosition = line.find( ':' ); std::string key = line.substr(keyPosition, colonPosition - keyPosition); std::string::size_type firstSpacePosition = key.find_first_of(" "); if (firstSpacePosition != std::string::npos) { key.erase(firstSpacePosition); } if ( keyPosition > expectedIndents.top() ) { // more indent than before expectedIndents.push(keyPosition); } else if (keyPosition == expectedIndents.top() ) { if (!surroundingKeys.empty()) { surroundingKeys.pop(); // last of same length } } else { // less indent than before do expectedIndents.pop(); while (expectedIndents.top() != keyPosition); // unwind until current indent is found } if (!surroundingKeys.empty()) { key = surroundingKeys.top() + "." + key; // construct current key name } surroundingKeys.push(key); // this is the new embracing key std::string value = line.substr(colonPosition+1); MITK_DEBUG << " Key: '" << key << "' value '" << value << "'" ; parsedResult[key] = value; // store parsing result } return parsedResult; } - diff --git a/Core/Code/Testing/DICOMTesting/mitkTestDICOMLoading.h b/Core/Code/Testing/DCMTesting/mitkTestDCMLoading.h similarity index 91% copy from Core/Code/Testing/DICOMTesting/mitkTestDICOMLoading.h copy to Core/Code/Testing/DCMTesting/mitkTestDCMLoading.h index a64b48415c..a84d7c24c2 100644 --- a/Core/Code/Testing/DICOMTesting/mitkTestDICOMLoading.h +++ b/Core/Code/Testing/DCMTesting/mitkTestDCMLoading.h @@ -1,105 +1,106 @@ /*=================================================================== 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 mitkTestDICOMLoading_h -#define mitkTestDICOMLoading_h +#ifndef mitkTestDCMLoading_h +#define mitkTestDCMLoading_h #include "mitkDicomSeriesReader.h" -#include "mitkDICOMTestingExports.h" +#include "mitkDCMTestingExports.h" namespace mitk { -class mitkDICOMTesting_EXPORT TestDICOMLoading +class mitkDCMTesting_EXPORT TestDCMLoading { public: typedef DicomSeriesReader::StringContainer StringContainer; typedef std::list NodeList; typedef std::list ImageList; - TestDICOMLoading(); + TestDCMLoading(); ImageList LoadFiles( const StringContainer& files, Image::Pointer preLoadedVolume = NULL ); /** \brief Dump relevant image information for later comparison. \sa CompareImageInformationDumps */ std::string DumpImageInformation( const Image* image ); /** \brief Compare two image information dumps. \return true, if dumps are sufficiently equal (see parameters) \sa DumpImageInformation */ bool CompareImageInformationDumps( const std::string& reference, const std::string& test ); private: typedef std::map KeyValueMap; void SetDefaultLocale(); void ResetUserLocale(); std::string ComponentTypeToString( int type ); KeyValueMap ParseDump( const std::string& dump ); bool CompareSpacedValueFields( const std::string& reference, const std::string& test, double eps = mitk::eps ); /** Compress whitespace in string \param pString input string \param pFill replacement whitespace (only whitespace in string after reduction) \param pWhitespace characters handled as whitespace */ std::string reduce(const std::string& pString, const std::string& pFill = " ", const std::string& pWhitespace = " \t"); /** Remove leading and trailing whitespace \param pString input string \param pWhitespace characters handled as whitespace */ std::string trim(const std::string& pString, const std::string& pWhitespace = " \t"); + + template bool StringToNumber(const std::string& s, T& value) { std::stringstream stream(s); stream >> value; - return !stream.fail(); + return (!stream.fail()) && (fabs(value) < std::numeric_limits::max()); } const char* m_PreviousCLocale; std::locale m_PreviousCppLocale; }; } #endif - diff --git a/Core/Code/Testing/files.cmake b/Core/Code/Testing/files.cmake index 6d3a6c11f6..cfdafac099 100644 --- a/Core/Code/Testing/files.cmake +++ b/Core/Code/Testing/files.cmake @@ -1,156 +1,154 @@ # tests with no extra command line parameter set(MODULE_TESTS mitkAccessByItkTest.cpp mitkCoreObjectFactoryTest.cpp mitkMaterialTest.cpp mitkActionTest.cpp mitkDispatcherTest.cpp mitkEnumerationPropertyTest.cpp mitkEventTest.cpp mitkFocusManagerTest.cpp mitkGenericPropertyTest.cpp #mitkGeometry3DTest.cpp mitkGeometryDataToSurfaceFilterTest.cpp mitkGlobalInteractionTest.cpp mitkImageDataItemTest.cpp #mitkImageMapper2DTest.cpp mitkImageGeneratorTest.cpp mitkBaseDataTest.cpp #mitkImageToItkTest.cpp mitkImportItkImageTest.cpp mitkGrabItkImageMemoryTest.cpp mitkInstantiateAccessFunctionTest.cpp mitkInteractorTest.cpp #mitkITKThreadingTest.cpp mitkLevelWindowTest.cpp mitkMessageTest.cpp #mitkPipelineSmartPointerCorrectnessTest.cpp mitkPixelTypeTest.cpp mitkPlaneGeometryTest.cpp mitkPointSetFileIOTest.cpp mitkPointSetTest.cpp mitkPointSetWriterTest.cpp mitkPointSetReaderTest.cpp mitkPointSetInteractorTest.cpp mitkPropertyTest.cpp mitkPropertyListTest.cpp #mitkRegistrationBaseTest.cpp #mitkSegmentationInterpolationTest.cpp mitkSlicedGeometry3DTest.cpp mitkSliceNavigationControllerTest.cpp mitkStateMachineTest.cpp ##mitkStateMachineContainerTest.cpp ## rewrite test, indirect since no longer exported Bug 14529 mitkStateTest.cpp mitkSurfaceTest.cpp mitkSurfaceToSurfaceFilterTest.cpp mitkTimeSlicedGeometryTest.cpp mitkTransitionTest.cpp mitkUndoControllerTest.cpp mitkVtkWidgetRenderingTest.cpp mitkVerboseLimitedLinearUndoTest.cpp mitkWeakPointerTest.cpp mitkTransferFunctionTest.cpp #mitkAbstractTransformGeometryTest.cpp mitkStepperTest.cpp itkTotalVariationDenoisingImageFilterTest.cpp mitkRenderingManagerTest.cpp vtkMitkThickSlicesFilterTest.cpp mitkNodePredicateSourceTest.cpp mitkVectorTest.cpp mitkClippedSurfaceBoundsCalculatorTest.cpp mitkExceptionTest.cpp mitkExtractSliceFilterTest.cpp mitkLogTest.cpp mitkImageDimensionConverterTest.cpp mitkLoggingAdapterTest.cpp mitkUIDGeneratorTest.cpp mitkShaderRepositoryTest.cpp mitkPlanePositionManagerTest.cpp ) # test with image filename as an extra command line parameter set(MODULE_IMAGE_TESTS mitkImageTimeSelectorTest.cpp #only runs on images #mitkImageAccessorTest.cpp #only runs on images #TODO: activate test if bug 15821 has been fixed mitkDataNodeFactoryTest.cpp #runs on all types of data ) set(MODULE_SURFACE_TESTS mitkSurfaceVtkWriterTest.cpp #only runs on surfaces mitkDataNodeFactoryTest.cpp #runs on all types of data ) # list of images for which the tests are run set(MODULE_TESTIMAGES US4DCyl.nrrd Pic3D.nrrd Pic2DplusT.nrrd BallBinary30x30x30.nrrd Png2D-bw.png ) set(MODULE_TESTSURFACES binary.stl ball.stl ) set(MODULE_CUSTOM_TESTS #mitkLabeledImageToSurfaceFilterTest.cpp #mitkExternalToolsTest.cpp mitkDataStorageTest.cpp mitkDataNodeTest.cpp - mitkDicomSeriesReaderTest.cpp - mitkDICOMLocaleTest.cpp mitkEventMapperTest.cpp mitkEventConfigTest.cpp mitkNodeDependentPointSetInteractorTest.cpp mitkStateMachineFactoryTest.cpp mitkPointSetLocaleTest.cpp mitkImageTest.cpp mitkImageWriterTest.cpp mitkImageVtkMapper2DTest.cpp mitkImageVtkMapper2DLevelWindowTest.cpp mitkImageVtkMapper2DOpacityTest.cpp mitkImageVtkMapper2DResliceInterpolationPropertyTest.cpp mitkImageVtkMapper2DColorTest.cpp mitkImageVtkMapper2DSwivelTest.cpp mitkImageVtkMapper2DTransferFunctionTest.cpp mitkIOUtilTest.cpp mitkSurfaceVtkMapper3DTest mitkSurfaceVtkMapper3DTexturedSphereTest.cpp mitkSurfaceGLMapper2DColorTest.cpp mitkSurfaceGLMapper2DOpacityTest.cpp mitkVolumeCalculatorTest.cpp mitkLevelWindowManagerTest.cpp mitkPointSetVtkMapper2DTest.cpp mitkPointSetVtkMapper2DImageTest.cpp mitkPointSetVtkMapper2DGlyphTypeTest.cpp mitkVTKRenderWindowSizeTest.cpp ) if (${VTK_MAJOR_VERSION} VERSION_LESS 6) # test can be removed with VTK 6 set(MODULE_TESTS ${MODULE_TESTS} mitkVTKRenderWindowSizeTest.cpp) endif() set(MODULE_RESOURCE_FILES Interactions/AddAndRemovePoints.xml Interactions/globalConfig.xml Interactions/StatemachineTest.xml Interactions/StatemachineConfigTest.xml ) # Create an artificial module initializing class for # the usServiceListenerTest.cpp usFunctionGenerateExecutableInit(testdriver_init_file IDENTIFIER ${MODULE_NAME}TestDriver ) # Embed the resources set(testdriver_resources ) usFunctionEmbedResources(testdriver_resources EXECUTABLE_NAME ${MODULE_NAME}TestDriver ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/Resources FILES ${MODULE_RESOURCE_FILES} ) set(TEST_CPP_FILES ${testdriver_init_file} ${testdriver_resources}) diff --git a/Core/Code/Testing/mitkDicomSeriesReaderTest.cpp b/Core/Code/Testing/mitkDicomSeriesReaderTest.cpp deleted file mode 100644 index 6e670f6d99..0000000000 --- a/Core/Code/Testing/mitkDicomSeriesReaderTest.cpp +++ /dev/null @@ -1,162 +0,0 @@ -/*=================================================================== - -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 "mitkTestingMacros.h" - -#include - -#include "mitkDicomSeriesReader.h" -#include "mitkProperties.h" - - -static std::map > GetTagInformationFromFile(mitk::DicomSeriesReader::StringContainer files) -{ - gdcm::Scanner scanner; - std::map > tagInformations; - - const gdcm::Tag tagSliceLocation(0x0020, 0x1041); // slice location - scanner.AddTag( tagSliceLocation ); - - const gdcm::Tag tagInstanceNumber(0x0020, 0x0013); // (image) instance number - scanner.AddTag( tagInstanceNumber ); - - const gdcm::Tag tagSOPInstanceNumber(0x0008, 0x0018); // SOP instance number - scanner.AddTag( tagSOPInstanceNumber ); - - //unsigned int slice(0); - scanner.Scan(files); - -// return const_cast(scanner.GetMappings()); - gdcm::Scanner::MappingType& tagValueMappings = const_cast(scanner.GetMappings()); - - for(std::vector::const_iterator fIter = files.begin(); - fIter != files.end(); - ++fIter) - { - std::map tags; - tags.insert(std::pair (tagSliceLocation, tagValueMappings[fIter->c_str()][tagSliceLocation])); - tags.insert(std::pair (tagInstanceNumber, tagValueMappings[fIter->c_str()][tagInstanceNumber])); - tags.insert(std::pair (tagSOPInstanceNumber, tagValueMappings[fIter->c_str()][tagSOPInstanceNumber])); - - tagInformations.insert(std::pair > (fIter->c_str(), tags)); - } - - return tagInformations; -} - -int mitkDicomSeriesReaderTest(int argc, char* argv[]) -{ - // always start with this! - MITK_TEST_BEGIN("DicomSeriesReader") - if(argc < 1) - { - MITK_ERROR << "No directory given!"; - return -1; - } - - char* dir; - dir = argv[1]; - - //check if DICOMTags have been set as property for mitk::Image - mitk::DicomSeriesReader::FileNamesGrouping seriesInFiles = mitk::DicomSeriesReader::GetSeries( dir, true ); - std::list images; - std::map fileMap; - - // TODO sort series UIDs, implementation of map iterator might differ on different platforms (or verify this is a standard topic??) - for (mitk::DicomSeriesReader::FileNamesGrouping::const_iterator seriesIter = seriesInFiles.begin(); - seriesIter != seriesInFiles.end(); - ++seriesIter) - { - mitk::DicomSeriesReader::StringContainer files = seriesIter->second.GetFilenames(); - - mitk::DataNode::Pointer node = mitk::DicomSeriesReader::LoadDicomSeries( files ); - MITK_TEST_CONDITION_REQUIRED(node.IsNotNull(),"Testing node") - - if (node.IsNotNull()) - { - mitk::Image::Pointer image = dynamic_cast( node->GetData() ); - - images.push_back( image ); - fileMap.insert( std::pair(image,files)); - } - } - - //Test if DICOM tags have been added correctly to the mitk::image properties - - const gdcm::Tag tagSliceLocation(0x0020, 0x1041); // slice location - const gdcm::Tag tagInstanceNumber(0x0020, 0x0013); // (image) instance number - const gdcm::Tag tagSOPInstanceNumber(0x0008, 0x0018); // SOP instance number - - for ( std::list::const_iterator imageIter = images.begin(); - imageIter != images.end(); - ++imageIter ) - { - const mitk::Image::Pointer image = *imageIter; - - //Get tag information for all dicom files of this image - std::map > tagInformations = GetTagInformationFromFile((*fileMap.find(image)).second); - - mitk::StringLookupTableProperty* sliceLocation = dynamic_cast(image->GetProperty("dicom.image.0020.1041").GetPointer()); - mitk::StringLookupTableProperty* instanceNumber = dynamic_cast(image->GetProperty("dicom.image.0020.0013").GetPointer()); - mitk::StringLookupTableProperty* SOPInstnaceNumber = dynamic_cast(image->GetProperty("dicom.image.0008.0018").GetPointer()); - - mitk::StringLookupTableProperty* files = dynamic_cast(image->GetProperty("files").GetPointer()); - - MITK_TEST_CONDITION(sliceLocation != NULL, "Test if tag for slice location has been set to mitk image"); - if(sliceLocation != NULL) - { - for(int i = 0; i < (int)sliceLocation->GetValue().GetLookupTable().size(); i++) - { - if(i < (int)files->GetValue().GetLookupTable().size()) - { - MITK_INFO << "Table value: " << sliceLocation->GetValue().GetTableValue(i) << " and File value: " << tagInformations[files->GetValue().GetTableValue(i).c_str()][tagSliceLocation] << std::endl; - MITK_INFO << "Filename: " << files->GetValue().GetTableValue(i).c_str() << std::endl; - MITK_TEST_CONDITION(sliceLocation->GetValue().GetTableValue(i) == tagInformations[files->GetValue().GetTableValue(i).c_str()][tagSliceLocation], "Test if value for slice location is correct"); - } - } - } - - MITK_TEST_CONDITION(instanceNumber != NULL, "Test if tag for image instance number has been set to mitk image"); - if(instanceNumber != NULL) - { - for(int i = 0; i < (int)instanceNumber->GetValue().GetLookupTable().size(); i++) - { - if(i < (int)files->GetValue().GetLookupTable().size()) - { - MITK_INFO << "Table value: " << instanceNumber->GetValue().GetTableValue(i) << " and File value: " << tagInformations[files->GetValue().GetTableValue(i).c_str()][tagInstanceNumber] << std::endl; - MITK_INFO << "Filename: " << files->GetValue().GetTableValue(i).c_str() << std::endl; - MITK_TEST_CONDITION(instanceNumber->GetValue().GetTableValue(i) == tagInformations[files->GetValue().GetTableValue(i).c_str()][tagInstanceNumber], "Test if value for instance number is correct"); - } - } - } - - MITK_TEST_CONDITION(SOPInstnaceNumber != NULL, "Test if tag for SOP instance number has been set to mitk image"); - if(SOPInstnaceNumber != NULL) - { - for(int i = 0; i < (int)SOPInstnaceNumber->GetValue().GetLookupTable().size(); i++) - { - if(i < (int)files->GetValue().GetLookupTable().size()) - { - MITK_INFO << "Table value: " << instanceNumber->GetValue().GetTableValue(i) << " and File value: " << tagInformations[files->GetValue().GetTableValue(i).c_str()][tagSOPInstanceNumber] << std::endl; - MITK_INFO << "Filename: " << files->GetValue().GetTableValue(i).c_str() << std::endl; - MITK_TEST_CONDITION(SOPInstnaceNumber->GetValue().GetTableValue(i) == tagInformations[files->GetValue().GetTableValue(i).c_str()][tagSOPInstanceNumber], "Test if value for SOP instance number is correct"); - } - } - } - } - - MITK_TEST_END() -} diff --git a/Core/Code/Testing/mitkLevelWindowTest.cpp b/Core/Code/Testing/mitkLevelWindowTest.cpp index 212ed1c12b..0cf189e25b 100644 --- a/Core/Code/Testing/mitkLevelWindowTest.cpp +++ b/Core/Code/Testing/mitkLevelWindowTest.cpp @@ -1,960 +1,971 @@ /*=================================================================== 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 "mitkLevelWindow.h" #include /* * Reseting the Levelwindow to default values: * i.e. Range = -1000..1000, Level = 0 and Window = 500 */ void resetLevelWindow(mitk::LevelWindow &lw) { //Default window bounds lw.SetRangeMinMax(-10000,10000); lw.SetLevelWindow(0,500,false); if (lw.GetRangeMin() != -10000 || lw.GetRangeMax() != 10000 || lw.GetLevel() != 0 || lw.GetWindow() != 500) { std::cout << "[Failed] To reset Levelwindow"<GetDefaultWindow(); if (!(defaultWindow == 500)) { std::cout<GetDefaultLevel(); if (!(defaultLevel == 256)) { std::cout<<"[FAILED]"<GetWindow(); if (!(window == 500)) { std::cout<<"[FAILED]"<GetLowerWindowBound() == 6)) { std::cout<<"[FAILED]"<GetUpperWindowBound() == 506)) { std::cout<<"[FAILED]"<GetLevel(); if (!(level == 256)) { std::cout<<"[FAILED]"<SetLevelWindow(20, 100); if (!(levWin->GetLevel() == 20)) { std::cout<<"[FAILED]"<GetWindow() == 100)) { std::cout<<"[FAILED]"<SetLevelWindow(levWin->GetDefaultLevel(), levWin->GetDefaultWindow()); if (!(levWin->GetLevel() == 256) && !(levWin->GetWindow() == 500)) { std::cout<<"[FAILED]"<SetDefaultLevelWindow(20, 200); if (!(levWin->GetDefaultLevel() == 20) && !(levWin->GetDefaultWindow() == 200)) { std::cout<<"[FAILED]"<SetLevelWindow(100, 50); levWin->ResetDefaultLevelWindow(); //double a = levWin->GetLevel(); //double d = levWin->GetWindow(); if (!((levWin->GetLevel() == 20) &&(levWin->GetWindow() == 200))) { std::cout<<"[FAILED]"<SetWindowBounds(0, 2); if (!((levWin->GetLowerWindowBound() == 0) && (levWin->GetUpperWindowBound() == 2) && (levWin->GetLevel() == 1) && (levWin->GetWindow() == 2))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"<SetRangeMinMax(2000, 2000); if (!(levWin->GetRangeMin() == 1999 && levWin->GetRangeMax() == 2000)) { std::cout<<"[FAILED]"< rangemax"; levWin->SetRangeMinMax(2100, 2000); if (!(levWin->GetRangeMin() == 2000 && levWin->GetRangeMax() == 2100)) { std::cout<<"[FAILED]"<SetRangeMinMax(-1000, 2000); std::cout<<"[PASSED]"<GetRangeMin() == -1000)) { std::cout<<"[FAILED]"<GetRangeMax() == 2000)) { std::cout<<"[FAILED]"<GetRangeMax() - levWin->GetRangeMin()) == levWin->GetRange())) { std::cout<<"[FAILED]"<SetDefaultBoundaries(2000, 2000); if (!(levWin->GetDefaultLowerBound() == 1999 && levWin->GetDefaultUpperBound() == 2000)) { std::cout<<"[FAILED]"< rangemax"; levWin->SetDefaultBoundaries(2100, 2000); if (!(levWin->GetDefaultLowerBound() == 2000 && levWin->GetDefaultUpperBound() == 2100)) { std::cout<<"[FAILED]"<SetDefaultBoundaries(-2000, 8000); std::cout<<"[PASSED]"<GetDefaultLowerBound() == -2000)) { std::cout<<"[FAILED]"<GetDefaultUpperBound() == 8000)) { std::cout<<"[FAILED]"<ResetDefaultRangeMinMax(); if (!((levWin->GetRangeMin() == levWin->GetDefaultLowerBound()) && (levWin->GetRangeMax() == levWin->GetDefaultUpperBound()))) { std::cout<<"[FAILED]"< maxRange "; levWin->SetRangeMinMax(2000, 1000); if (!((levWin->GetRangeMin() == 1000) && (levWin->GetRangeMax() == 2000))) { std::cout<<"[FAILED]"<SetRangeMinMax(2000, -1000); if (!((levWin->GetRangeMin() == -1000) && (levWin->GetRangeMax() == 2000))) { std::cout<<"[FAILED]"<SetRangeMinMax(-2000, -3000); if (!((levWin->GetRangeMin() == -3000) && (levWin->GetRangeMax() == -2000))) { std::cout<<"[FAILED]"<SetRangeMinMax(0, -1000); if (!((levWin->GetRangeMin() == -1000) && (levWin->GetRangeMax() == 0))) { std::cout<<"[FAILED]"<SetRangeMinMax(2000, 0); if (!((levWin->GetRangeMin() == 0) && (levWin->GetRangeMax() == 2000))) { std::cout<<"[FAILED]"<SetRangeMinMax(-10000, 10000); std::cout<<"[PASSED]"< defaultMaxRange "; levWin->SetDefaultBoundaries(2000, 1000); if (!((levWin->GetDefaultLowerBound() == 1000) && (levWin->GetDefaultUpperBound() == 2000))) { std::cout<<"[FAILED]"<SetDefaultBoundaries(2000, -1000); if (!((levWin->GetDefaultLowerBound() == -1000) && (levWin->GetDefaultUpperBound() == 2000))) { std::cout<<"[FAILED]"<SetDefaultBoundaries(-2000, -3000); if (!((levWin->GetDefaultLowerBound() == -3000) && (levWin->GetDefaultUpperBound() == -2000))) { std::cout<<"[FAILED]"<SetDefaultBoundaries(0, -1000); if (!((levWin->GetDefaultLowerBound() == -1000) && (levWin->GetDefaultUpperBound() == 0))) { std::cout<<"[FAILED]"<SetDefaultBoundaries(2000, 0); if (!((levWin->GetDefaultLowerBound() == 0) && (levWin->GetDefaultUpperBound() == 2000))) { std::cout<<"[FAILED]"<SetDefaultBoundaries(-10000, 10000); std::cout<<"[PASSED]"< max "; levWin->SetWindowBounds(2000, 1000); if (!((levWin->GetLowerWindowBound() == 1000) && (levWin->GetUpperWindowBound() == 2000))) { std::cout<<"[FAILED]"<SetWindowBounds(2000, -1000); if (!((levWin->GetLowerWindowBound() == -1000) && (levWin->GetUpperWindowBound() == 2000))) { std::cout<<"[FAILED]"<SetWindowBounds(-2000, -3000); if (!((levWin->GetLowerWindowBound() == -3000) && (levWin->GetUpperWindowBound() == -2000))) { std::cout<<"[FAILED]"<SetWindowBounds(0, -1000); if (!((levWin->GetLowerWindowBound() == -1000) && (levWin->GetUpperWindowBound() == 0))) { std::cout<<"[FAILED]"<SetWindowBounds(2000, 0); if (!((levWin->GetLowerWindowBound() == 0) && (levWin->GetUpperWindowBound() == 2000))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"< maxrange, minmax < minrange, minmaxrange, min < minrange & max > minrange // max < minrange & min > minrange, min > maxrange & max < maxrange, min < minrange & max > maxrange // min > maxrange & max < minrange std::cout << "Testing mitk::LevelWindow max > min > maxrange autoexpand = FALSE"; levWin->SetWindowBounds(11000, 12000, false); if (!((levWin->GetLowerWindowBound() == 9999) && (levWin->GetUpperWindowBound() == 10000))) { std::cout<<"[FAILED]"<GetLowerWindowBound()<<","<GetUpperWindowBound()<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"< min > maxrange autoexpand = TRUE"; levWin->SetWindowBounds(11000, 12000); if (!((levWin->GetLowerWindowBound() == 11000) && (levWin->GetUpperWindowBound() == 12000))) { std::cout<<"[FAILED]"<GetLowerWindowBound()<<","<GetUpperWindowBound()<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"< max > maxrange autoexpand = FALSE"; levWin->SetWindowBounds(12000, 11000, false); if (!((levWin->GetLowerWindowBound() == 9999) && (levWin->GetUpperWindowBound() == 10000))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"< max > maxrange autoexpand = TRUE"; levWin->SetWindowBounds(12000, 11000); if (!((levWin->GetLowerWindowBound() == 11000) && (levWin->GetUpperWindowBound() == 12000))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"<SetWindowBounds(-12000, -11000, false); if (!((levWin->GetLowerWindowBound() == -10000) && (levWin->GetUpperWindowBound() == -9999))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"<SetWindowBounds(-12000, -11000); if (!((levWin->GetLowerWindowBound() == -12000) && (levWin->GetUpperWindowBound() == -11000))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"<SetWindowBounds(-11000, -12000, false); if (!((levWin->GetLowerWindowBound() == -10000) && (levWin->GetUpperWindowBound() == -9999))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"<SetWindowBounds(-11000, -12000); if (!((levWin->GetLowerWindowBound() == -12000) && (levWin->GetUpperWindowBound() == -11000))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"< maxrange autoexpand = FALSE"; levWin->SetWindowBounds(9999, 12000, false); if (!((levWin->GetLowerWindowBound() == 9999) && (levWin->GetUpperWindowBound() == 10000))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"< maxrange autoexpand = TRUE"; levWin->SetWindowBounds(9999, 12000); if (!((levWin->GetLowerWindowBound() == 9999) && (levWin->GetUpperWindowBound() == 12000))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"< minrange autoexpand = FALSE"; levWin->SetWindowBounds(-11000, -9999, false); if (!((levWin->GetLowerWindowBound() == -10000) && (levWin->GetUpperWindowBound() == -9999))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"< minrange autoexpand = TRUE"; levWin->SetWindowBounds(-11000, -9999); if (!((levWin->GetLowerWindowBound() == -11000) && (levWin->GetUpperWindowBound() == -9999))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"< maxrange autoexpand = FALSE"; levWin->SetWindowBounds(-11000, 11000, false); if (!((levWin->GetLowerWindowBound() == -10000) && (levWin->GetUpperWindowBound() == 10000))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"< maxrange autoexpand = TRUE"; levWin->SetWindowBounds(-11000, 11000); if (!((levWin->GetLowerWindowBound() == -11000) && (levWin->GetUpperWindowBound() == 11000))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"< min = max > minrange autoexpand = FALSE"; levWin->SetWindowBounds(5000, 5000, false); if (!((levWin->GetLowerWindowBound() == 4999) && (levWin->GetUpperWindowBound() == 5000))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"< min = max > minrange autoexpand = TRUE"; levWin->SetWindowBounds(5000, 5000); if (!((levWin->GetLowerWindowBound() == 4999) && (levWin->GetUpperWindowBound() == 5000))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"<SetWindowBounds(-10000, -10000, false); if (!((levWin->GetLowerWindowBound() == -10000) && (levWin->GetUpperWindowBound() == -9999))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"<SetWindowBounds(-10000, -10000); if (!((levWin->GetLowerWindowBound() == -10000) && (levWin->GetUpperWindowBound() == -9999))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"<SetWindowBounds(10000, 10000, false); if (!((levWin->GetLowerWindowBound() == 9999) && (levWin->GetUpperWindowBound() == 10000))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"<SetWindowBounds(10000, 10000); if (!((levWin->GetLowerWindowBound() == 9999) && (levWin->GetUpperWindowBound() == 10000))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"< maxrange autoexpand = FALSE"; levWin->SetWindowBounds(11000, 11000, false); if (!((levWin->GetLowerWindowBound() == 9999) && (levWin->GetUpperWindowBound() == 10000))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"< maxrange autoexpand = TRUE"; levWin->SetWindowBounds(11000, 11000); if (!((levWin->GetLowerWindowBound() == 10999) && (levWin->GetUpperWindowBound() == 11000))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"<SetWindowBounds(-11000, -11000, false); if (!((levWin->GetLowerWindowBound() == -10000) && (levWin->GetUpperWindowBound() == -9999))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"<SetWindowBounds(-11000, -11000); if (!((levWin->GetLowerWindowBound() == -11000) && (levWin->GetUpperWindowBound() == -10999))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"< min > minrange > max autoexpand = FALSE"; levWin->SetWindowBounds(-9000, -11000, false); if (!((levWin->GetLowerWindowBound() == -10000) && (levWin->GetUpperWindowBound() == -9000))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"< min > minrange > max autoexpand = TRUE"; levWin->SetWindowBounds(-9000, -11000, true); if (!((levWin->GetLowerWindowBound() == -11000) && (levWin->GetUpperWindowBound() == -9000))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"< maxrange > minrange > max autoexpand = FALSE"; levWin->SetWindowBounds(11000, -11000, false); if (!((levWin->GetLowerWindowBound() == -10000) && (levWin->GetUpperWindowBound() == 10000))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"< maxrange > minrange > max autoexpand = TRUE"; levWin->SetWindowBounds(11000, -11000); if (!((levWin->GetLowerWindowBound() == -11000) && (levWin->GetUpperWindowBound() == 11000))) { std::cout<<"[FAILED]"<ResetDefaultLevelWindow(); std::cout<<"[PASSED]"<SetRangeMinMax(-20000, -15000); if (!((levWin->GetLowerWindowBound() == -15001) && (levWin->GetUpperWindowBound() == -15000))) { std::cout<<"[FAILED]"<ResetDefaultRangeMinMax(); levWin->ResetDefaultLevelWindow(); std::cout<<"[PASSED]"< maxrange & maxrange < min < max "; levWin->ResetDefaultLevelWindow(); levWin->SetRangeMinMax(-15000, -20000); if (!((levWin->GetLowerWindowBound() == -15001) && (levWin->GetUpperWindowBound() == -15000))) { std::cout<<"[FAILED]"<ResetDefaultRangeMinMax(); levWin->ResetDefaultLevelWindow(); std::cout<<"[PASSED]"<SetRangeMinMax(-80, 1000); levWin->SetWindowBounds(-1000,110, false); if (!((levWin->GetLowerWindowBound() == -80) && (levWin->GetUpperWindowBound() == 110))) { std::cout<<"[FAILED]"<ResetDefaultRangeMinMax(); levWin->ResetDefaultLevelWindow(); std::cout<<"[PASSED]"<SetRangeMinMax(-80, 1000); levWin->SetWindowBounds(-1000,110); if (!((levWin->GetLowerWindowBound() == -1000) && (levWin->GetUpperWindowBound() == 110))) { std::cout<<"[FAILED]"<ResetDefaultRangeMinMax(); levWin->ResetDefaultLevelWindow(); std::cout<<"[PASSED]"<SetRangeMinMax(1000,-80); levWin->SetWindowBounds(-1000,110, false ); if (!((levWin->GetLowerWindowBound() == -80) && (levWin->GetUpperWindowBound() == 110))) { std::cout<<"[FAILED]"<ResetDefaultRangeMinMax(); levWin->ResetDefaultLevelWindow(); std::cout<<"[PASSED]"<SetRangeMinMax(1000,-80); levWin->SetWindowBounds(-1000,110 ); if (!((levWin->GetLowerWindowBound() == -1000) && (levWin->GetUpperWindowBound() == 110))) { std::cout<<"[FAILED]"<ResetDefaultRangeMinMax(); levWin->ResetDefaultLevelWindow(); std::cout<<"[PASSED]"<SetRangeMinMax(20, 110); if (!((levWin->GetLowerWindowBound() == 20) && (levWin->GetUpperWindowBound() == 110))) { std::cout<<"[FAILED]"<ResetDefaultRangeMinMax(); levWin->ResetDefaultLevelWindow(); std::cout<<"[PASSED]"< maxRange & min < maxrange < max "; levWin->SetWindowBounds(-90,1000); levWin->SetRangeMinMax(100, -80); if (!((levWin->GetLowerWindowBound() == -80) && (levWin->GetUpperWindowBound() == 100))) { std::cout<<"[FAILED]"<ResetDefaultRangeMinMax(); levWin->ResetDefaultLevelWindow(); std::cout<<"[PASSED]"< maxRange & min < minrange < maxrange SetRangeMinMax(20, 100); if (!((levWin->GetLowerWindowBound() == 20) && (levWin->GetUpperWindowBound() == 100))) { std::cout<<"[FAILED]"<ResetDefaultRangeMinMax(); levWin->ResetDefaultLevelWindow(); std::cout<<"[PASSED]"<SetRangeMinMax(20000, 15000); if (!((levWin->GetLowerWindowBound() == 15000) && (levWin->GetUpperWindowBound() == 15001))) { std::cout<<"[FAILED]"<ResetDefaultRangeMinMax(); levWin->ResetDefaultLevelWindow(); std::cout<<"[PASSED]"< maxrange & min < max < minrange "; levWin->SetRangeMinMax(20000, 15000); if (!((levWin->GetLowerWindowBound() == 15000) && (levWin->GetUpperWindowBound() == 15001))) { std::cout<<"[FAILED]"<ResetDefaultRangeMinMax(); levWin->ResetDefaultLevelWindow(); std::cout<<"[PASSED]"<SetRangeMinMax(-20000, -15000); if (!((levWin->GetLowerWindowBound() == -15001) && (levWin->GetUpperWindowBound() == -15000))) { std::cout<<"[FAILED]"<ResetDefaultRangeMinMax(); levWin->ResetDefaultLevelWindow(); std::cout<<"[PASSED]"<DebugOn(); image->Initialize( mitk::MakePixelType(), 3, dim); int *p = (int*)image->GetData(); int size = dim[0]*dim[1]*dim[2]; int i; for(i=0; iGetRange() == levelwindow.GetRange())) { std::cout<<"[FAILED]"< #include #include #include #include #include #include US_BEGIN_NAMESPACE struct TestModuleH { virtual ~TestModuleH() {} }; struct TestModuleH2 { virtual ~TestModuleH2() {} }; US_END_NAMESPACE US_DECLARE_SERVICE_INTERFACE(US_PREPEND_NAMESPACE(TestModuleH), "org.cppmicroservices.TestModuleH") US_DECLARE_SERVICE_INTERFACE(US_PREPEND_NAMESPACE(TestModuleH2), "org.cppmicroservices.TestModuleH2") US_BEGIN_NAMESPACE class TestProduct : public TestModuleH { - // Module* caller; + Module* caller; public: - TestProduct(Module* /*caller*/) - //: caller(caller) + TestProduct(Module* caller) + : caller(caller) {} }; class TestProduct2 : public TestProduct, public TestModuleH2 { public: TestProduct2(Module* caller) : TestProduct(caller) {} }; class TestModuleHPrototypeServiceFactory : public PrototypeServiceFactory { std::map > fcbind; // Map calling module with implementation public: InterfaceMap GetService(Module* caller, const ServiceRegistrationBase& /*sReg*/) { std::cout << "GetService (prototype) in H" << std::endl; TestProduct2* product = new TestProduct2(caller); fcbind[caller->GetModuleId()].push_back(product); return MakeInterfaceMap(product); } void UngetService(Module* caller, const ServiceRegistrationBase& /*sReg*/, const InterfaceMap& service) { TestProduct2* product = dynamic_cast(ExtractInterface(service)); delete product; fcbind[caller->GetModuleId()].remove(product); } }; class TestModuleHActivator : public ModuleActivator, public ServiceFactory { std::string thisServiceName; ServiceRegistration factoryService; ServiceRegistration prototypeFactoryService; ModuleContext* mc; std::map fcbind; // Map calling module with implementation TestModuleHPrototypeServiceFactory prototypeFactory; public: TestModuleHActivator() : thisServiceName(us_service_interface_iid()) , mc(NULL) {} void Load(ModuleContext* mc) { std::cout << "start in H" << std::endl; this->mc = mc; factoryService = mc->RegisterService(this); prototypeFactoryService = mc->RegisterService(static_cast(&prototypeFactory)); } void Unload(ModuleContext* /*mc*/) { factoryService.Unregister(); } InterfaceMap GetService(Module* caller, const ServiceRegistrationBase& /*sReg*/) { std::cout << "GetService in H" << std::endl; TestProduct* product = new TestProduct(caller); fcbind.insert(std::make_pair(caller->GetModuleId(), product)); return MakeInterfaceMap(product); } void UngetService(Module* caller, const ServiceRegistrationBase& /*sReg*/, const InterfaceMap& service) { TestModuleH* product = ExtractInterface(service); delete product; fcbind.erase(caller->GetModuleId()); } }; US_END_NAMESPACE US_EXPORT_MODULE_ACTIVATOR(TestModuleH, us::TestModuleHActivator) 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/CMakeLists.txt b/Examples/CMakeLists.txt index cb14b1cd73..7bbfe8d866 100644 --- a/Examples/CMakeLists.txt +++ b/Examples/CMakeLists.txt @@ -1,65 +1,66 @@ set(MITK_DEFAULT_SUBPROJECTS MITK-Examples) #----------------------------------------------------------------------------- # Set-up example plugins #----------------------------------------------------------------------------- if(MITK_USE_BLUEBERRY) # Specify which plug-ins belong to this project macro(GetMyTargetLibraries all_target_libraries varname) set(re_ctkplugin_mitk "^org_mitk_example_[a-zA-Z0-9_]+$") set(_tmp_list) list(APPEND _tmp_list ${all_target_libraries}) ctkMacroListFilter(_tmp_list re_ctkplugin_mitk OUTPUT_VARIABLE ${varname}) endmacro() include("${CMAKE_CURRENT_SOURCE_DIR}/Plugins/PluginList.cmake") set(mitk_example_plugins_fullpath ) foreach(mitk_example_plugin ${MITK_EXAMPLE_PLUGINS}) list(APPEND mitk_example_plugins_fullpath Plugins/${mitk_example_plugin}) endforeach() ctkMacroSetupPlugins(${mitk_example_plugins_fullpath} BUILD_OPTION_PREFIX MITK_BUILD_ COMPACT_OPTIONS) set(MITK_EXAMPLE_PLUGIN_TARGETS ) foreach(mitk_example_plugin ${MITK_EXAMPLE_PLUGINS}) ctkFunctionExtractOptionNameAndValue(${mitk_example_plugin} plugin_name plugin_value) string(REPLACE "." "_" _plugin_target ${plugin_name}) list(APPEND MITK_EXAMPLE_PLUGIN_TARGETS ${_plugin_target}) mark_as_advanced(${${_plugin_target}_option_name}) endforeach() endif() #----------------------------------------------------------------------------- # Add example executables #----------------------------------------------------------------------------- set(MITK_DIR ${PROJECT_BINARY_DIR}) set(MITK_EXPORTS_FILE_INCLUDED 1) set(_example_dirs MbiLog QtFreeRender ) if(MITK_USE_QT) list(APPEND _example_dirs Tutorial QtAppExample + mitkdump ) endif() if(MITK_USE_BLUEBERRY) list(APPEND _example_dirs BlueBerryExampleLauncher ) endif() foreach(_example_dir ${_example_dirs}) add_subdirectory(${_example_dir}) endforeach() diff --git a/Examples/mitkdump/CMakeLists.txt b/Examples/mitkdump/CMakeLists.txt new file mode 100644 index 0000000000..d19d6d14fa --- /dev/null +++ b/Examples/mitkdump/CMakeLists.txt @@ -0,0 +1,29 @@ +project(mitkdump) +find_package(MITK) + +# Check prerequisites for this application. +# We need the Mitk module. +MITK_CHECK_MODULE(result Qmitk) +if(result) + message(SEND_ERROR "MITK module(s) \"${result}\" not available from the MITK build at ${MITK_DIR}") +endif() + +# Set-up the build system to use the Mitk module +MITK_USE_MODULE(Qmitk DICOMReader) +include_directories(${ALL_INCLUDE_DIRECTORIES}) +link_directories(${ALL_LIBRARY_DIRS}) + +add_executable(mitkdump mitkdump.cpp) +target_link_libraries(mitkdump ${ALL_LIBRARIES} ) + +add_executable(mitkdumpdir mitkdumpdir.cpp) +target_link_libraries(mitkdumpdir ${ALL_LIBRARIES} ) + +# subproject support +set_property(TARGET mitkdump PROPERTY LABELS ${MITK_DEFAULT_SUBPROJECTS}) +set_property(TARGET mitkdumpdir PROPERTY LABELS ${MITK_DEFAULT_SUBPROJECTS}) +foreach(subproject ${MITK_DEFAULT_SUBPROJECTS}) + add_dependencies(${subproject} mitkdump) + add_dependencies(${subproject} mitkdumpdir) +endforeach() + diff --git a/Examples/mitkdump/mitkdump.cpp b/Examples/mitkdump/mitkdump.cpp new file mode 100644 index 0000000000..116797a5b3 --- /dev/null +++ b/Examples/mitkdump/mitkdump.cpp @@ -0,0 +1,231 @@ +/*=================================================================== + + 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. + + ===================================================================*/ + +/** + \file mitkdump.cpp + + \brief Commandline application to see what DICOMFileReaderSelector would produce from a set of files. + + Usage: + \verbatim + mitkdump [-v] file1 [... fileN] + + -v output more details on commandline + fileN DICOM file + \endverbatim + + The application will ask a DICOMFileReaderSelector to analyze the files given as file1 .. fileN. + Once the reader with the least number of files is selected, this result is printed to commandline. + + If the "-v" flag is used (as a first parameter), the output will contain details about filenames, + which can make the output considerably harder to read. + + Output is also written to a log file of name "%gt;datetime-stamp%lt;_dir_>directory-name<.mitkdump +*/ + +#include "mitkDICOMFileReaderSelector.h" + +using mitk::DICOMTag; + +std::string buildDateString() +{ + std::time_t rawtime; + std::tm* timeinfo; + char buffer [80]; + + std::time(&rawtime); + timeinfo = std::localtime(&rawtime); + + std::strftime(buffer,80,"%Y%m%d-%H%M%S",timeinfo); + + return std::string(buffer); +} + +void gen_random(char *s, const int len) +{ + static const char alphanum[] = + "0123456789" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + for (int i = 0; i < len; ++i) + { + s[i] = alphanum[rand() % (sizeof(alphanum) - 1)]; + } + + s[len] = 0; +} + +std::string gen_random(const int len) +{ + if (len > 0 && len < 100) + { + char retval[100]; + gen_random(retval, len); + return std::string(retval); + } + else + { + return std::string(""); + } +} + +std::string removeUnsafeChars(const std::string& str) +{ + std::string retval; + for(std::string::const_iterator it = str.begin(); it != str.end(); ++it) + { + const char& c = *it; + if ( (c >= '0' && c <= '9') + || (c >= 'A' && c <= 'Z') + || (c >= 'a' && c <= 'z') + || (c == '.') + || (c == '-') + || (c == '_') + ) + { + retval += c; + } + } + return retval; +} + +std::string extractDirString(const std::string& dirString) +{ + std::string wholeprefix = dirString.substr(0, dirString.find_last_of("/\\")); + std::string lastDirectoryPart = wholeprefix.substr(wholeprefix.find_last_of("/\\")+1); + std::string cleanLastDirectoryPart = removeUnsafeChars(lastDirectoryPart); + if (!cleanLastDirectoryPart.empty()) + { + return cleanLastDirectoryPart; + } + else + { + std::stringstream emptydirname; + emptydirname << "noname_" << gen_random(6); + + return emptydirname.str(); + } +} + +int main(int argc, char* argv[]) +{ + bool fileDetails(false); + bool loadimage(false); + int firstFileIndex = 1; + + // see if we got the '-v' flag to output file details + if (argc > 1 && std::string(argv[firstFileIndex]) == "-v") + { + fileDetails = true; + ++firstFileIndex; + } + + // see if we got the '-l' flag + if (argc > 1 && std::string(argv[firstFileIndex]) == "-l") + { + loadimage = true; + ++firstFileIndex; + } + + + // analyze files from argv + mitk::StringList inputFiles; + for (int a = firstFileIndex; a < argc; ++a) + { + inputFiles.push_back( std::string(argv[a]) ); + } + + if (inputFiles.empty()) + { + MITK_INFO << "0 input files given, exiting..."; + return EXIT_SUCCESS; + } + + 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; + } + + // output best reader result + MITK_INFO << "---- Best reader configuration '" << reader->GetConfigurationLabel() << "' with " << reader->GetNumberOfOutputs() << " outputs"; + if (fileDetails) + { + reader->PrintOutputs(std::cout, fileDetails); + } + + // construct the name of a log file + std::string datestring = buildDateString();; + std::string dirString = extractDirString(argv[firstFileIndex]); + std::string logfilename = datestring + "_dir_" + dirString + ".mitkdump"; + MITK_INFO << "Logfile " << logfilename; + + // write output to file for later analysis + std::ofstream fs; + fs.open(logfilename.c_str()); + fs << "---- " << dirString << ": Best reader configuration '" << reader->GetConfigurationLabel() << "' with " << reader->GetNumberOfOutputs() << " outputs" << std::endl; + reader->PrintOutputs( fs, true); // always verbose in log file + fs.close(); + + if (loadimage) + { + MITK_INFO << "Loading..."; + reader->LoadImages(); + mitk::Image::Pointer image = reader->GetOutput(0).GetMitkImage(); + MITK_INFO << "---- Output image:"; + mitk::Geometry3D::Pointer geo3D = image->GetGeometry(); + if (geo3D.IsNotNull()) + { + mitk::SlicedGeometry3D::Pointer sg = dynamic_cast(geo3D.GetPointer()); + if (sg.IsNotNull()) + { + unsigned int nos = sg->GetSlices(); + mitk::Geometry2D::Pointer first = sg->GetGeometry2D(0); + mitk::Geometry2D::Pointer last = sg->GetGeometry2D(nos-1); + + mitk::Point3D firstOrigin = first->GetOrigin(); + mitk::Point3D lastOrigin = last->GetOrigin(); + MITK_INFO << "Geometry says: First slice at " << firstOrigin << ", last slice at " << lastOrigin; + + mitk::StringLookupTableProperty::Pointer sliceLocations = + dynamic_cast( image->GetProperty("dicom.image.0020.1041").GetPointer() ); + if (sliceLocations.IsNotNull()) + { + std::string firstSliceLocation = sliceLocations->GetValue().GetTableValue(0); + std::string lastSliceLocation = sliceLocations->GetValue().GetTableValue(nos-1); + MITK_INFO << "Image properties says: first slice location at " << firstSliceLocation << ", last slice location at " << lastSliceLocation; + } + + mitk::StringLookupTableProperty::Pointer instanceNumbers = + dynamic_cast( image->GetProperty("dicom.image.0020.0013").GetPointer() ); + if (instanceNumbers.IsNotNull()) + { + std::string firstInstanceNumber = instanceNumbers->GetValue().GetTableValue(0); + std::string lastInstanceNumber = instanceNumbers->GetValue().GetTableValue(nos-1); + MITK_INFO << "Image properties says: first instance number at " << firstInstanceNumber << ", last instance number at " << lastInstanceNumber; + } + } + } + MITK_INFO << "---- End of output"; + } + + // if we got so far, everything is fine + return EXIT_SUCCESS; +} diff --git a/Examples/mitkdump/mitkdumpdir.cpp b/Examples/mitkdump/mitkdumpdir.cpp new file mode 100644 index 0000000000..fbf3be0871 --- /dev/null +++ b/Examples/mitkdump/mitkdumpdir.cpp @@ -0,0 +1,127 @@ +/*=================================================================== + + 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. + + ===================================================================*/ + +/** + \file mitkdumpdir.cpp + + \brief Commandline application that calls the mitkdump application on multiple directories. + + Usage: + \verbatim + mitkdumpdir [-v] dir1 [... dirN] + + -v output more details on commandline + dirN directory names + \endverbatim + + The application will traverse each directory for sub-directories. + For each leaf-directory (i.e. directories without sub-directories), + the mitkdump application will be called, given all files from the + leaf directory. + + If the "-v" flag is used, it will be passed to the mitkdump application, + which can create considerably more output. +*/ + +#include + +bool printEachOutput(false); + +bool processLeafNode(const QString& dirname, const QString& executablename) +{ + qDebug() << "Processing " << qPrintable(dirname); + + QFileInfoList fileinfos = QDir( dirname ).entryInfoList( QDir::Files | QDir::Readable ); + + QStringList filenames; + foreach(QFileInfo info, fileinfos) + { + filenames << info.absoluteFilePath(); + } + + QProcess mitkdump; + mitkdump.start(executablename, filenames); + + if (!mitkdump.waitForStarted()) + return false; + + if (!mitkdump.waitForFinished()) + return false; + + if (printEachOutput) + { + QString output(mitkdump.readAll()); + qDebug() << output; + } + + return true; +} + +void processLeafNodes(const QString& rootDir, const QString& executablename) +{ + QDirIterator dirIter(rootDir, + QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable, + QDirIterator::Subdirectories); + + while (dirIter.hasNext()) + { + dirIter.next(); + + if ( QDir( dirIter.fileInfo().absoluteFilePath() ).entryList( QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable ).empty() ) + { + processLeafNodes( dirIter.filePath(), executablename ); + } + } + + if ( QDir( rootDir ).entryList( QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable ).empty() ) + { + bool successful = processLeafNode( rootDir, executablename ); + } +} + +int main(int argc, char* argv[]) +{ + QCoreApplication a(argc,argv); + + int firstDirIndex = 1; + + // see if we got the '-v' flag to output file details + if (argc > 1 && std::string(argv[firstDirIndex]) == "-v") + { + printEachOutput = true; + ++firstDirIndex; + } + + QString executablename( QCoreApplication::applicationDirPath() + QDir::separator() + "mitkdump" ); +#ifdef WIN32 + executablename += ".exe"; +#endif + + // analyze dirs from argv + QStringList inputDirs; + for (int a = firstDirIndex; a < argc; ++a) + { + inputDirs << argv[a]; + } + + foreach(QString dirname, inputDirs) + { + processLeafNodes(dirname, executablename); + } + + // if we got so far, everything is fine + return EXIT_SUCCESS; +} diff --git a/Modules/CMakeLists.txt b/Modules/CMakeLists.txt index 93d8d43421..91736bb5ef 100644 --- a/Modules/CMakeLists.txt +++ b/Modules/CMakeLists.txt @@ -1,66 +1,68 @@ set(LIBPOSTFIX "Ext") # Modules must be listed according to their dependencies set(module_dirs SeedsImage + DICOMReader + DICOMTesting SceneSerializationBase PlanarFigure ImageExtraction ImageStatistics LegacyAdaptors IpPicSupport MitkExt SceneSerialization GraphAlgorithms SurfaceInterpolation Segmentation PlanarFigureSegmentation Qmitk QmitkExt SegmentationUI Properties DiffusionImaging GPGPU IGT CameraCalibration IGTUI RigidRegistration RigidRegistrationUI DeformableRegistration DeformableRegistrationUI OpenCL OpenCVVideoSupport Overlays InputDevices ToFHardware ToFProcessing ToFUI US ClippingTools USUI DicomUI Simulation Remeshing Python BreakpadCrashReporting ) set(MITK_DEFAULT_SUBPROJECTS MITK-Modules) foreach(module_dir ${module_dirs}) add_subdirectory(${module_dir}) endforeach() if(MITK_PRIVATE_MODULES) file(GLOB all_subdirs RELATIVE ${MITK_PRIVATE_MODULES} ${MITK_PRIVATE_MODULES}/*) foreach(subdir ${all_subdirs}) string(FIND ${subdir} "." _result) if(_result EQUAL -1) if(EXISTS ${MITK_PRIVATE_MODULES}/${subdir}/CMakeLists.txt) message(STATUS "Found private module ${subdir}") add_subdirectory(${MITK_PRIVATE_MODULES}/${subdir} private_modules/${subdir}) endif() endif() endforeach() endif(MITK_PRIVATE_MODULES) diff --git a/Modules/DICOMReader/CMakeLists.txt b/Modules/DICOMReader/CMakeLists.txt new file mode 100644 index 0000000000..355cc4eced --- /dev/null +++ b/Modules/DICOMReader/CMakeLists.txt @@ -0,0 +1,6 @@ + +MITK_CREATE_MODULE(DICOMReader + DEPENDS Mitk +) + +add_subdirectory(Testing) 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..ee3e6c3053 --- /dev/null +++ b/Modules/DICOMReader/Resources/configurations/3D/classicreader.xml @@ -0,0 +1,10 @@ + + + diff --git a/Modules/DICOMReader/Resources/configurations/3D/imageposition.xml b/Modules/DICOMReader/Resources/configurations/3D/imageposition.xml new file mode 100644 index 0000000000..5307c094f5 --- /dev/null +++ b/Modules/DICOMReader/Resources/configurations/3D/imageposition.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Modules/DICOMReader/Resources/configurations/3D/imageposition_byacquisition.xml b/Modules/DICOMReader/Resources/configurations/3D/imageposition_byacquisition.xml new file mode 100644 index 0000000000..02309cdfb8 --- /dev/null +++ b/Modules/DICOMReader/Resources/configurations/3D/imageposition_byacquisition.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Modules/DICOMReader/Resources/configurations/3D/imagetime.xml b/Modules/DICOMReader/Resources/configurations/3D/imagetime.xml new file mode 100644 index 0000000000..d705ba630b --- /dev/null +++ b/Modules/DICOMReader/Resources/configurations/3D/imagetime.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Modules/DICOMReader/Resources/configurations/3D/instancenumber.xml b/Modules/DICOMReader/Resources/configurations/3D/instancenumber.xml new file mode 100644 index 0000000000..e8813cbb00 --- /dev/null +++ b/Modules/DICOMReader/Resources/configurations/3D/instancenumber.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Modules/DICOMReader/Resources/configurations/3D/slicelocation.xml b/Modules/DICOMReader/Resources/configurations/3D/slicelocation.xml new file mode 100644 index 0000000000..4023c92764 --- /dev/null +++ b/Modules/DICOMReader/Resources/configurations/3D/slicelocation.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Modules/DICOMReader/Resources/configurations/3DnT/classicreader.xml b/Modules/DICOMReader/Resources/configurations/3DnT/classicreader.xml new file mode 100644 index 0000000000..0f382e3a0d --- /dev/null +++ b/Modules/DICOMReader/Resources/configurations/3DnT/classicreader.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + diff --git a/Modules/DICOMReader/TODOs.txt b/Modules/DICOMReader/TODOs.txt new file mode 100644 index 0000000000..b947c3ce66 --- /dev/null +++ b/Modules/DICOMReader/TODOs.txt @@ -0,0 +1,64 @@ +Important + +[x] Make sure that ..Selector does tag scanning only once +[x] Check Gantry Tilt ToDo/Bug in old reader +[x] - and in new reader, how do we WANT to handle this? + - old and new code both did not correctly check the stored value against the calculated value + - it SEEMS like tilts below 45 degrees were never checked against the tag, and bigger values are probably unrealistic(?) + - so, nothing wrong. It just seems that we calculated a DIFFERENT angle than the one recorded + - in the new reader we remove the check. We are right as far as our shearing is affected. +[x] Keep OLD DICOMTesting module, in order to test code until it is removed + - restored this module as DCMTesting module +[x] ONLY load a pre-sorted list + - Should work now by: + - get the list of images, construct FrameInfo list (order must be known from prior sorting) + - prepare a tagcache object that provides tag information (from some kind of database, or run AnalyzeInputFiles() on file reader) + - create DICOMImageBlockDescriptor with frame list and tag cache + - Call SetFixTiltByShearing() and TiltInfo object as appropriate... + - call DICOMITKSeriesGDCMReader::LoadMitkImageForImageBlockDescriptor(block) +[x] Option to just provide properties to a pre-loaded mitk::Image + - see TestDICOMLoading::DecorateVerifyCachedImage() +[x] Performance of mitkdump / DICOMReaderSelector (TagCache?) + - the note in GDCM..Reader::GetTagValue() is correct. + This piece of code is taking lots of time. + Approx. half of the time is tag scanning, the other + half is construction of block describing properties, + which accesses GetTagValue. +[x] - maybe we can evaluate these properties in a lazy way + (only when asked for). + Solution: Yes, this helps, implemented. +[x] Gantry tilt broken during implementation + - Was a wrong implementation of NormalDirectionConsistencySorter +[x] Accepted image origin error during EquiDistantBlocksSorter + must be configurable. To support legacy behavior of the reader, + the tolerance must be fixable to a constant value. For newer + readers, it should be adapted to the actual slice distance. +[x] Sorting by "Image Time" seems undeterministic (clarkson test data) +[x] - Numeric conversion of "1.2.3.4" yields 1.2 on Windows, seems to fail on Linux(?) + - need to verify that the whole string (except whitespace at the begin/end) was converted + Solution: GetTagValue as String does a right-trim plus we check after conversion! Works. +[x] Implement Configurator::GetBuiltIn3DReaders() (don't ignore "classic reader") +[x] Complete MITK properties of images: level/window and color type (MONOCHROME1/2) +[x] Check input images fo DICOMness and non-multi-frameness +[x] Use CanHandleFile() in selection! + - implicitly now, the reader checks itself and produces no output if it cannot handle the files +[x] Check ToDo in mitkDICOMTag.cpp + - operator< for DICOMTag has been re-implemented in a readable and safer way. +[x] Configurable precision for tag value comparisons +[x] Images are upside-down in some cases + - error was hidden assumption somewhere: filenames for ImageSeriesReader need + to be in an order that goes along the image normals, not in the opposite + direction + +Questionable + +[ ] Fallback to filename instead of instance UID? + +Nice-to-have + +[ ] Multi-Frame images (DCMTK) +[ ] ... +[x] Add the tags used by DICOMImageBlockDescriptor dynamically instead of hard-coded + (look for ToDo around m_GDCMScanner.AddTag() in mitkDICOMITKSeriesGDCMReader.cpp) +[ ] Let DICOMImageBlockDescriptor describe attributes common and different for all blocks of a series +[ ] Let DICOMImageBlockDescriptor check image properties when mitk::Image is set (pre-loaded case) diff --git a/Modules/DICOMReader/Testing/CMakeLists.txt b/Modules/DICOMReader/Testing/CMakeLists.txt new file mode 100644 index 0000000000..559920cb95 --- /dev/null +++ b/Modules/DICOMReader/Testing/CMakeLists.txt @@ -0,0 +1,10 @@ +MITK_CREATE_MODULE_TESTS() + +file(GLOB_RECURSE tinyCTSlices ${MITK_DATA_DIR}/TinyCTAbdomen/1??) + +#foreach(f ${tinyCTSlices}) +# message(" ${f}") +#endforeach() + +mitkAddCustomModuleTest(mitkDICOMFileReaderTest_Basics mitkDICOMFileReaderTest ${tinyCTSlices}) +mitkAddCustomModuleTest(mitkDICOMITKSeriesGDCMReaderBasicsTest_Basics mitkDICOMITKSeriesGDCMReaderBasicsTest ${tinyCTSlices}) diff --git a/Modules/DICOMReader/Testing/files.cmake b/Modules/DICOMReader/Testing/files.cmake new file mode 100644 index 0000000000..3333b9f2ed --- /dev/null +++ b/Modules/DICOMReader/Testing/files.cmake @@ -0,0 +1,13 @@ +set(MODULE_TESTS + mitkDICOMReaderConfiguratorTest.cpp +) + +set(MODULE_CUSTOM_TESTS + mitkDICOMFileReaderTest.cpp + mitkDICOMITKSeriesGDCMReaderBasicsTest.cpp +) + +set(TEST_CPP_FILES + mitkDICOMNullFileReader.cpp + mitkDICOMFilenameSorter.cpp +) diff --git a/Modules/DICOMReader/Testing/mitkDICOMFileReaderTest.cpp b/Modules/DICOMReader/Testing/mitkDICOMFileReaderTest.cpp new file mode 100644 index 0000000000..cfbf3bb29d --- /dev/null +++ b/Modules/DICOMReader/Testing/mitkDICOMFileReaderTest.cpp @@ -0,0 +1,35 @@ +/*=================================================================== + +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" +#include "mitkDICOMFileReaderTestHelper.h" + +#include "mitkTestingMacros.h" + +int mitkDICOMFileReaderTest(int argc, char* argv[]) +{ + MITK_TEST_BEGIN("mitkDICOMFileReaderTest"); + + mitk::DICOMNullFileReader::Pointer simpleReader = mitk::DICOMNullFileReader::New(); + MITK_TEST_CONDITION_REQUIRED(simpleReader.IsNotNull(), "DICOMNullFileReader can be instantiated."); + + mitk::DICOMFileReaderTestHelper::SetTestInputFilenames( argc,argv ); + + mitk::DICOMFileReaderTestHelper::TestInputFilenames( simpleReader ); + mitk::DICOMFileReaderTestHelper::TestOutputsContainInputs( simpleReader ); + + MITK_TEST_END(); +} diff --git a/Modules/DICOMReader/Testing/mitkDICOMFileReaderTestHelper.h b/Modules/DICOMReader/Testing/mitkDICOMFileReaderTestHelper.h new file mode 100644 index 0000000000..6ec8b7a954 --- /dev/null +++ b/Modules/DICOMReader/Testing/mitkDICOMFileReaderTestHelper.h @@ -0,0 +1,151 @@ +/*=================================================================== + +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 mitkDICOMFileReaderTestHelper_h +#define mitkDICOMFileReaderTestHelper_h + +#include "mitkDICOMFileReader.h" +#include "mitkDICOMEnums.h" + +#include "mitkTestingMacros.h" + +namespace mitk +{ + +class DICOMFileReaderTestHelper +{ + public: + +static StringList& GetInputFilenames() +{ + static StringList inputs; + return inputs; +} + +static void SetTestInputFilenames(int argc, char* argv[]) +{ + mitk::StringList inputFiles; + + for (int a = 1; a < argc; ++a) + { + inputFiles.push_back( argv[a] ); + } + + GetInputFilenames() = inputFiles; +} + + +static void SetTestInputFilenames(const StringList& filenames) +{ + GetInputFilenames() = filenames; +} + +static void TestInputFilenames(DICOMFileReader* reader) +{ + StringList inputFiles = GetInputFilenames(); + reader->SetInputFiles( inputFiles ); + + const StringList& inputFilesReturned = reader->GetInputFiles(); + MITK_TEST_CONDITION( inputFilesReturned.size() == inputFiles.size(), "Input file list is received") + MITK_TEST_CONDITION( reader->GetNumberOfOutputs() == 0, "No outputs without analysis") +} + +static void TestOutputsContainInputs(DICOMFileReader* reader) +{ + StringList inputFiles = GetInputFilenames(); + reader->SetInputFiles( inputFiles ); + + reader->AnalyzeInputFiles(); + + StringList allSortedInputsFiles; + + unsigned int numberOfOutputs = reader->GetNumberOfOutputs(); + for (unsigned int o = 0; o < numberOfOutputs; ++o) + { + const DICOMImageBlockDescriptor block = reader->GetOutput(o); + + const DICOMImageFrameList& outputFiles = block.GetImageFrameList(); + for(DICOMImageFrameList::const_iterator iter = outputFiles.begin(); + iter != outputFiles.end(); + ++iter) + { + // check that output is part of input + StringList::iterator inputPositionOfCurrentOutput = std::find( inputFiles.begin(), inputFiles.end(), (*iter)->Filename ); + if (inputPositionOfCurrentOutput != inputFiles.end()) + { + // check that output is only part of ONE output + StringList::iterator outputPositionOfCurrentOutput = std::find( allSortedInputsFiles.begin(), allSortedInputsFiles.end(), (*iter)->Filename ); + if (outputPositionOfCurrentOutput == allSortedInputsFiles.end()) + { + // was not in list before + allSortedInputsFiles.push_back( *inputPositionOfCurrentOutput ); + } + else + { + reader->PrintOutputs(std::cout); + MITK_TEST_CONDITION_REQUIRED(false, "File '" << (*iter)->Filename << "' appears in TWO outputs. Readers are expected to use each frame only once." ) + } + } + else + { + reader->PrintOutputs(std::cout); + MITK_TEST_CONDITION_REQUIRED(false, "File '" << (*iter)->Filename << "' appears in output, but it was never part of the input list." ) + } + } + } + + MITK_TEST_CONDITION( allSortedInputsFiles.size() == inputFiles.size(), "Output list size (" << allSortedInputsFiles.size() << ") equals input list size (" << inputFiles.size() << ")" ) + + try + { + const DICOMImageBlockDescriptor block = reader->GetOutput( inputFiles.size() ); + MITK_TEST_CONDITION(false, "Invalid indices for GetOutput() should throw exception") + } + catch( std::invalid_argument& ) + { + MITK_TEST_CONDITION(true, "Invalid indices for GetOutput() should throw exception") + } +} + +static void TestMitkImagesAreLoaded(DICOMFileReader* reader) +{ + StringList inputFiles = GetInputFilenames(); + reader->SetInputFiles( inputFiles ); + + reader->AnalyzeInputFiles(); + reader->LoadImages(); + + unsigned int numberOfOutputs = reader->GetNumberOfOutputs(); + for (unsigned int o = 0; o < numberOfOutputs; ++o) + { + const DICOMImageBlockDescriptor block = reader->GetOutput(o); + + const DICOMImageFrameList& outputFiles = block.GetImageFrameList(); + mitk::Image::Pointer mitkImage = block.GetMitkImage(); + + MITK_DEBUG << "-------------------------------------------"; + MITK_DEBUG << "Output " << o << " at " << (void*) mitkImage.GetPointer(); + MITK_DEBUG << " Number of files: " << outputFiles.size(); + MITK_DEBUG << " Dimensions: " << mitkImage->GetDimension(0) << " " << mitkImage->GetDimension(1) << " " << mitkImage->GetDimension(2); + } +} + + +}; // end test class + +} // namespace + +#endif diff --git a/Modules/DICOMReader/Testing/mitkDICOMFilenameSorter.cpp b/Modules/DICOMReader/Testing/mitkDICOMFilenameSorter.cpp new file mode 100644 index 0000000000..bdd6ae0c5f --- /dev/null +++ b/Modules/DICOMReader/Testing/mitkDICOMFilenameSorter.cpp @@ -0,0 +1,87 @@ +/*=================================================================== + +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; +} + +bool +mitk::DICOMFilenameSorter +::operator==(const DICOMDatasetSorter& other) const +{ + return dynamic_cast(&other) != NULL; +} + +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/Testing/mitkDICOMFilenameSorter.h b/Modules/DICOMReader/Testing/mitkDICOMFilenameSorter.h new file mode 100644 index 0000000000..0d1ba8e694 --- /dev/null +++ b/Modules/DICOMReader/Testing/mitkDICOMFilenameSorter.h @@ -0,0 +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 mitkDICOMFilenameSorter_h +#define mitkDICOMFilenameSorter_h + +#include "mitkDICOMDatasetSorter.h" + +namespace mitk +{ + +/** + \ingroup DICOMReaderModule + \brief sort files based on filename (last resort). +*/ +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; + + virtual bool operator==(const DICOMDatasetSorter& other) 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/mitkDICOMITKSeriesGDCMReaderBasicsTest.cpp b/Modules/DICOMReader/Testing/mitkDICOMITKSeriesGDCMReaderBasicsTest.cpp new file mode 100644 index 0000000000..e009cb5d02 --- /dev/null +++ b/Modules/DICOMReader/Testing/mitkDICOMITKSeriesGDCMReaderBasicsTest.cpp @@ -0,0 +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 "mitkDICOMITKSeriesGDCMReader.h" +#include "mitkDICOMFileReaderTestHelper.h" +#include "mitkDICOMFilenameSorter.h" +#include "mitkDICOMTagBasedSorter.h" +#include "mitkDICOMSortByTag.h" + +#include "mitkTestingMacros.h" + +using mitk::DICOMTag; + +int mitkDICOMITKSeriesGDCMReaderBasicsTest(int argc, char* argv[]) +{ + MITK_TEST_BEGIN("mitkDICOMITKSeriesGDCMReaderBasicsTest"); + + mitk::DICOMITKSeriesGDCMReader::Pointer gdcmReader = mitk::DICOMITKSeriesGDCMReader::New(); + MITK_TEST_CONDITION_REQUIRED(gdcmReader.IsNotNull(), "DICOMITKSeriesGDCMReader can be instantiated."); + + mitk::DICOMFileReaderTestHelper::SetTestInputFilenames( argc,argv ); + + // check the Set/GetInput function + mitk::DICOMFileReaderTestHelper::TestInputFilenames( gdcmReader ); + + // check that output is a good reproduction of input (no duplicates, no new elements) + mitk::DICOMFileReaderTestHelper::TestOutputsContainInputs( gdcmReader ); + + + // repeat test with filename based sorter in-between + mitk::DICOMFilenameSorter::Pointer filenameSorter = mitk::DICOMFilenameSorter::New(); + gdcmReader->AddSortingElement( filenameSorter ); + mitk::DICOMFileReaderTestHelper::TestOutputsContainInputs( gdcmReader ); + + // repeat test with some more realistic sorting + gdcmReader = mitk::DICOMITKSeriesGDCMReader::New(); // this also tests destruction + mitk::DICOMTagBasedSorter::Pointer tagSorter = mitk::DICOMTagBasedSorter::New(); + + // all the things that split by tag in DicomSeriesReader + tagSorter->AddDistinguishingTag( DICOMTag(0x0028, 0x0010) ); // Number of Rows + tagSorter->AddDistinguishingTag( DICOMTag(0x0028, 0x0011) ); // Number of Columns + tagSorter->AddDistinguishingTag( DICOMTag(0x0028, 0x0030) ); // Pixel Spacing + tagSorter->AddDistinguishingTag( DICOMTag(0x0018, 0x1164) ); // Imager Pixel Spacing + tagSorter->AddDistinguishingTag( DICOMTag(0x0020, 0x0037), new mitk::DICOMTagBasedSorter::CutDecimalPlaces(5) ); // Image Orientation (Patient) + tagSorter->AddDistinguishingTag( DICOMTag(0x0020, 0x000e) ); // Series Instance UID + tagSorter->AddDistinguishingTag( DICOMTag(0x0018, 0x0050) ); // Slice Thickness + tagSorter->AddDistinguishingTag( DICOMTag(0x0028, 0x0008) ); // Number of Frames + tagSorter->AddDistinguishingTag( DICOMTag(0x0020, 0x0052) ); // Frame of Reference UID + + // a sorter... + mitk::DICOMSortCriterion::ConstPointer sorting = + mitk::DICOMSortByTag::New( DICOMTag(0x0020, 0x0013), // instance number + mitk::DICOMSortByTag::New( DICOMTag(0x0020, 0x0012), // aqcuisition number + mitk::DICOMSortByTag::New( DICOMTag(0x0008, 0x0032), // aqcuisition time + mitk::DICOMSortByTag::New( DICOMTag(0x0018, 0x1060), // trigger time + mitk::DICOMSortByTag::New( DICOMTag(0x0008, 0x0018) // SOP instance UID (last resort, not really meaningful but decides clearly) + ).GetPointer() + ).GetPointer() + ).GetPointer() + ).GetPointer() + ).GetPointer(); + tagSorter->SetSortCriterion( sorting ); + + gdcmReader->AddSortingElement( tagSorter ); + mitk::DICOMFileReaderTestHelper::TestOutputsContainInputs( gdcmReader ); + + gdcmReader->PrintOutputs(std::cout, true); + + // really load images + mitk::DICOMFileReaderTestHelper::TestMitkImagesAreLoaded( gdcmReader ); + + MITK_TEST_END(); +} diff --git a/Modules/DICOMReader/Testing/mitkDICOMNullFileReader.cpp b/Modules/DICOMReader/Testing/mitkDICOMNullFileReader.cpp new file mode 100644 index 0000000000..53c6831844 --- /dev/null +++ b/Modules/DICOMReader/Testing/mitkDICOMNullFileReader.cpp @@ -0,0 +1,101 @@ +/*=================================================================== + +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; +} + +bool +mitk::DICOMNullFileReader +::operator==(const DICOMFileReader& other) const +{ + return dynamic_cast(&other) != NULL; // same class, we don't have members +} +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 new file mode 100644 index 0000000000..e276813bf1 --- /dev/null +++ b/Modules/DICOMReader/Testing/mitkDICOMNullFileReader.h @@ -0,0 +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 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); + + bool operator==(const DICOMFileReader& other) const; + + virtual DICOMTagList GetTagsOfInterest() const { return DICOMTagList(); } + virtual void SetTagCache( DICOMTagCache::Pointer ) {} + + 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/Testing/mitkDICOMReaderConfiguratorTest.cpp b/Modules/DICOMReader/Testing/mitkDICOMReaderConfiguratorTest.cpp new file mode 100644 index 0000000000..cb442bde7b --- /dev/null +++ b/Modules/DICOMReader/Testing/mitkDICOMReaderConfiguratorTest.cpp @@ -0,0 +1,66 @@ +/*=================================================================== + +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 "mitkDICOMFileReaderSelector.h" + +#include "mitkTestingMacros.h" + +/** + \brief Verify serialization ability of DICOMReaderConfigurator. +*/ +int mitkDICOMReaderConfiguratorTest(int /*argc*/, char* /*argv*/[]) +{ + MITK_TEST_BEGIN("mitkDICOMReaderConfiguratorTest"); + + mitk::DICOMFileReaderSelector::Pointer readerSource = mitk::DICOMFileReaderSelector::New(); + readerSource->LoadBuiltIn3DConfigs(); + readerSource->LoadBuiltIn3DnTConfigs(); + + mitk::DICOMFileReaderSelector::ReaderList allReaders = readerSource->GetAllConfiguredReaders(); // this already parses XML config for this reader + for (mitk::DICOMFileReaderSelector::ReaderList::iterator rIter = allReaders.begin(); + rIter != allReaders.end(); + ++rIter) + { + mitk::DICOMFileReader::Pointer originalReader = *rIter; + + MITK_TEST_OUTPUT(<< "********************************************************************************"); + MITK_TEST_OUTPUT(<< "Testing reader '" << originalReader->GetConfigurationLabel() << "'" ); + MITK_TEST_OUTPUT(<< "********************************************************************************"); + // ask for an XML serialization of this reader, then compare the XML files. + mitk::DICOMReaderConfigurator::Pointer serializer = mitk::DICOMReaderConfigurator::New(); + std::string xmlSerialization = serializer->CreateConfigStringFromReader( originalReader.GetPointer() ); + MITK_TEST_CONDITION( !xmlSerialization.empty(), "DICOMReaderConfigurator is able to serialize reader"); + MITK_INFO << "Got serialization:"; + std::cout << xmlSerialization << std::endl; + + mitk::DICOMReaderConfigurator::Pointer creator = mitk::DICOMReaderConfigurator::New(); + try + { + mitk::DICOMFileReader::Pointer reconstructedReader = creator->CreateFromUTF8ConfigString(xmlSerialization); + MITK_TEST_CONDITION( reconstructedReader.IsNotNull(), "DICOMReaderConfigurator is able to create reader from XML"); + + MITK_TEST_CONDITION( *reconstructedReader == *originalReader, "Readers are equal before and after serialization" ) + } + catch(std::exception& e) + { + MITK_TEST_CONDITION_REQUIRED(false, "Exception from DICOMReaderConfigurator: " << e.what()); + } + } + + MITK_TEST_END(); +} + diff --git a/Modules/DICOMReader/files.cmake b/Modules/DICOMReader/files.cmake new file mode 100644 index 0000000000..e83fc4b691 --- /dev/null +++ b/Modules/DICOMReader/files.cmake @@ -0,0 +1,58 @@ +set(H_FILES + mitkDICOMFileReader.h + mitkDICOMGDCMTagScanner.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 + mitkDICOMTagCache.h + mitkDICOMReaderConfigurator.h + mitkDICOMFileReaderSelector.h +) + +set(CPP_FILES + mitkDICOMFileReader.cpp + mitkDICOMGDCMTagScanner.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 + mitkDICOMTagCache.cpp + mitkDICOMEnums.cpp + mitkDICOMReaderConfigurator.cpp + mitkDICOMFileReaderSelector.cpp +) + +set(RESOURCE_FILES + configurations/3D/classicreader.xml + configurations/3D/imageposition.xml + configurations/3D/imageposition_byacquisition.xml + configurations/3D/instancenumber.xml + configurations/3D/slicelocation.xml + configurations/3D/imagetime.xml + + configurations/3DnT/classicreader.xml +) diff --git a/Modules/DICOMReader/mitkClassicDICOMSeriesReader.cpp b/Modules/DICOMReader/mitkClassicDICOMSeriesReader.cpp new file mode 100644 index 0000000000..012f953827 --- /dev/null +++ b/Modules/DICOMReader/mitkClassicDICOMSeriesReader.cpp @@ -0,0 +1,98 @@ +/*=================================================================== + +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 "mitkDICOMTagBasedSorter.h" +#include "mitkDICOMSortByTag.h" +#include "mitkSortByImagePositionPatient.h" + + +mitk::ClassicDICOMSeriesReader +::ClassicDICOMSeriesReader() +:ThreeDnTDICOMSeriesReader() +{ + mitk::DICOMTagBasedSorter::Pointer tagSorter = mitk::DICOMTagBasedSorter::New(); + + // all the things that split by tag in mitk::DicomSeriesReader + tagSorter->AddDistinguishingTag( DICOMTag(0x0020, 0x000e) ); // Series Instance UID + //tagSorter->AddDistinguishingTag( DICOMTag(0x0020, 0x0052) ); // Frame of Reference UID + + // a sorter... + mitk::DICOMSortCriterion::ConstPointer sorting = + mitk::SortByImagePositionPatient::New( // image position patient and image orientation + mitk::DICOMSortByTag::New( DICOMTag(0x0020, 0x0012), // aqcuisition number + mitk::DICOMSortByTag::New( DICOMTag(0x0008, 0x0032), // aqcuisition time + mitk::DICOMSortByTag::New( DICOMTag(0x0018, 0x1060), // trigger time + mitk::DICOMSortByTag::New( DICOMTag(0x0008, 0x0018) // SOP instance UID (last resort, not really meaningful but decides clearly) + ).GetPointer() + ).GetPointer() + ).GetPointer() + ).GetPointer() + ).GetPointer(); + tagSorter->SetSortCriterion( sorting ); + tagSorter->SetStrictSorting(false); // nothing did enforce constant distances before, there was just the EquiDistantBlocksSorter logic + + // define above sorting for this class + this->AddSortingElement( tagSorter ); + + this->SetFixTiltByShearing(true); // that was configurable, default was true + this->SetToleratedOriginOffset(0.005); // was hard-coded + this->SetGroup3DandT(true); // that was configurable, default was true + + m_EquiDistantBlocksSorter->SetAcceptTwoSlicesGroups(false); // old reader did not accept that + + this->SetConfigurationLabel("2013 sorting logic"); + this->SetConfigurationDescription("Sort by Image Position, then Acquisition Number, Time, Trigger time, group by 3D+t, group tilted images"); +} + +mitk::ClassicDICOMSeriesReader +::ClassicDICOMSeriesReader(const ClassicDICOMSeriesReader& other ) +:ThreeDnTDICOMSeriesReader(other) +{ +} + +mitk::ClassicDICOMSeriesReader +::~ClassicDICOMSeriesReader() +{ +} + +mitk::ClassicDICOMSeriesReader& +mitk::ClassicDICOMSeriesReader +::operator=(const ClassicDICOMSeriesReader& other) +{ + if (this != &other) + { + ThreeDnTDICOMSeriesReader::operator=(other); + } + return *this; +} + +bool +mitk::ClassicDICOMSeriesReader +::operator==(const DICOMFileReader& other) const +{ + if (dynamic_cast(&other)) + { + return true; + } + else + { + return false; + } +} + + diff --git a/Modules/DICOMReader/mitkClassicDICOMSeriesReader.h b/Modules/DICOMReader/mitkClassicDICOMSeriesReader.h new file mode 100644 index 0000000000..00540d8429 --- /dev/null +++ b/Modules/DICOMReader/mitkClassicDICOMSeriesReader.h @@ -0,0 +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 mitkClassicDICOMSeriesReader_h +#define mitkClassicDICOMSeriesReader_h + +#include "mitkThreeDnTDICOMSeriesReader.h" + +#include "DICOMReaderExports.h" + +namespace mitk +{ + +/** + \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 ); + + virtual bool operator==(const DICOMFileReader& other) const; + + 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 new file mode 100644 index 0000000000..f0c63a75e3 --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMDatasetAccess.h @@ -0,0 +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.cpp b/Modules/DICOMReader/mitkDICOMDatasetSorter.cpp new file mode 100644 index 0000000000..5864c2510c --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMDatasetSorter.cpp @@ -0,0 +1,121 @@ +/*=================================================================== + +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 "mitkDICOMDatasetSorter.h" + +mitk::DICOMDatasetSorter +::DICOMDatasetSorter() +:itk::LightObject() +{ +} + +mitk::DICOMDatasetSorter +::~DICOMDatasetSorter() +{ +} + +mitk::DICOMDatasetSorter +::DICOMDatasetSorter(const DICOMDatasetSorter& other ) +:itk::LightObject() +,m_Outputs( other.m_Outputs ) +{ +} + +mitk::DICOMDatasetSorter& +mitk::DICOMDatasetSorter +::operator=(const DICOMDatasetSorter& other) +{ + if (this != &other) + { + m_Input = other.m_Input; + m_Outputs = other.m_Outputs; + } + return *this; +} + +void +mitk::DICOMDatasetSorter +::SetInput(DICOMDatasetList datasets) +{ + m_Input = datasets; +} + +const mitk::DICOMDatasetList& +mitk::DICOMDatasetSorter +::GetInput() const +{ + return m_Input; +} + +unsigned int +mitk::DICOMDatasetSorter +::GetNumberOfOutputs() const +{ + return m_Outputs.size(); +} + +void +mitk::DICOMDatasetSorter +::ClearOutputs() +{ + m_Outputs.clear(); +} + +void +mitk::DICOMDatasetSorter +::SetNumberOfOutputs(unsigned int numberOfOutputs) +{ + m_Outputs.resize(numberOfOutputs); +} + +void +mitk::DICOMDatasetSorter +::SetOutput(unsigned int index, const DICOMDatasetList& 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() ); + } +} + +const mitk::DICOMDatasetList& +mitk::DICOMDatasetSorter +::GetOutput(unsigned int index) const +{ + return const_cast(this)->GetOutput(index); +} + +mitk::DICOMDatasetList& +mitk::DICOMDatasetSorter +::GetOutput(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() ); + } +} diff --git a/Modules/DICOMReader/mitkDICOMDatasetSorter.h b/Modules/DICOMReader/mitkDICOMDatasetSorter.h new file mode 100644 index 0000000000..fcda4551af --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMDatasetSorter.h @@ -0,0 +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. + +===================================================================*/ + +#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; + + virtual bool operator==(const DICOMDatasetSorter& other) 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.cpp b/Modules/DICOMReader/mitkDICOMEnums.cpp new file mode 100644 index 0000000000..766d62b16b --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMEnums.cpp @@ -0,0 +1,43 @@ +/*=================================================================== + +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 "mitkDICOMEnums.h" + +std::string +mitk::PixelSpacingInterpretationToString(const PixelSpacingInterpretation& value) +{ + switch (value) + { + case SpacingInPatient: return "In Patient"; + case SpacingAtDetector: return "At Detector"; + case SpacingUnknown: return "Unknown spacing"; + default: return ""; + }; +} + +std::string +mitk::ReaderImplementationLevelToString( const ReaderImplementationLevel& value ) +{ + switch (value) + { + case SOPClassSupported: return "SOPClassSupported"; + case SOPClassPartlySupported: return "SOPClassPartlySupported"; + case SOPClassImplemented: return "SOPClassImplemented"; + case SOPClassUnsupported: return "SOPClassUnsupported"; + case SOPClassUnknown: return "SOPClassUnknown"; + default: return ""; + }; +} diff --git a/Modules/DICOMReader/mitkDICOMEnums.h b/Modules/DICOMReader/mitkDICOMEnums.h new file mode 100644 index 0000000000..3dc047ec80 --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMEnums.h @@ -0,0 +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; + + /** + \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 + } PixelSpacingInterpretation; + + /** + \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 + } 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 new file mode 100644 index 0000000000..49ae5824f2 --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMFileReader.cpp @@ -0,0 +1,198 @@ +/*=================================================================== + +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::Object() +{ +} + +mitk::DICOMFileReader +::~DICOMFileReader() +{ +} + +mitk::DICOMFileReader +::DICOMFileReader(const DICOMFileReader& other ) +:itk::Object() +,m_InputFilenames( other.m_InputFilenames ) +,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() const +{ + return m_ConfigLabel; +} + +void +mitk::DICOMFileReader +::SetConfigurationDescription(const std::string& desc) +{ + m_ConfigDescription = desc; +} + +std::string +mitk::DICOMFileReader +::GetConfigurationDescription() const +{ + 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 +::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; + + 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 new file mode 100644 index 0000000000..e942cd0b1e --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMFileReader.h @@ -0,0 +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. + +===================================================================*/ + +#ifndef mitkDICOMFileReader_h +#define mitkDICOMFileReader_h + +#include "itkObjectFactory.h" +#include "mitkCommon.h" + +#include "DICOMReaderExports.h" + +#include "mitkDICOMTagCache.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::Object +{ + public: + + mitkClassMacro( DICOMFileReader, itk::Object ); + + /// 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(); 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 DICOMTagList GetTagsOfInterest() const = 0; + + /// A way to provide external knowledge about files and tag values is appreciated. + virtual void SetTagCache(DICOMTagCache::Pointer) = 0; + + /// Short label/name to describe this reader + void SetConfigurationLabel(const std::string&); + /// Short label/name to describe this reader + std::string GetConfigurationLabel() const; + /// 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() const; + + /// 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; + + virtual bool operator==(const DICOMFileReader& other) const = 0; + + 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..a02c473b58 --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMFileReaderSelector.cpp @@ -0,0 +1,260 @@ +/*=================================================================== + +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 "mitkDICOMGDCMTagScanner.h" + +#include +#include +#include +#include +#include + +mitk::DICOMFileReaderSelector +::DICOMFileReaderSelector() +{ +} + +mitk::DICOMFileReaderSelector +::~DICOMFileReaderSelector() +{ +} + +std::list +mitk::DICOMFileReaderSelector +::GetAllConfiguredReaders() const +{ + return m_Readers; +} + +void +mitk::DICOMFileReaderSelector +::AddConfigsFromResources(const std::string& path) +{ + std::vector configs = us::GetModuleContext()->GetModule()->FindResources(path, "*.xml", false); + + for (std::vector::iterator iter = configs.begin(); + iter != configs.end(); + ++iter) + { + us::ModuleResource& resource = *iter; + if (resource.IsValid()) + { + us::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()); + + this->AddConfig(s); + } + } +} + +void +mitk::DICOMFileReaderSelector +::AddConfigFromResource(us::ModuleResource& resource) +{ + if (resource.IsValid()) + { + us::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()); + + this->AddConfig(s); + } +} + +void +mitk::DICOMFileReaderSelector +::AddConfigFromResource(const std::string& resourcename) +{ + us::ModuleResource r = us::GetModuleContext()->GetModule()->GetResource(resourcename); + this->AddConfigFromResource(r); +} + +void +mitk::DICOMFileReaderSelector +::AddFileReaderCanditate(DICOMFileReader::Pointer reader) +{ + if (reader.IsNotNull()) + { + m_Readers.push_back( reader ); + } +} + +void +mitk::DICOMFileReaderSelector +::LoadBuiltIn3DConfigs() +{ + //this->AddConfigsFromResources("configurations/3D"); + // in this order of preference... + this->AddConfigFromResource("configurations/3D/instancenumber.xml"); + this->AddConfigFromResource("configurations/3D/slicelocation.xml"); + this->AddConfigFromResource("configurations/3D/imageposition.xml"); + this->AddConfigFromResource("configurations/3D/imageposition_byacquisition.xml"); + //this->AddConfigFromResource("configurations/3D/imagetime.xml"); // no sense in this one? want to see a real-world example first + this->AddConfigFromResource("configurations/3D/classicreader.xml"); // currently is 3D+t actually +} + +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; + + // do the tag scanning externally and just ONCE + DICOMGDCMTagScanner::Pointer gdcmScanner = DICOMGDCMTagScanner::New(); + gdcmScanner->SetInputFiles( m_InputFilenames ); + + // let all readers analyze the file set + for (ReaderList::iterator rIter = m_Readers.begin(); + rIter != m_Readers.end(); + ++rIter) + { + gdcmScanner->AddTags( (*rIter)->GetTagsOfInterest() ); + } + + gdcmScanner->Scan(); + + // 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 ); + (*rIter)->SetTagCache( gdcmScanner.GetPointer() ); + try + { + (*rIter)->AnalyzeInputFiles(); + workingCandidates.push_back( *rIter ); + MITK_INFO << "Reader " << readerIndex << " (" << (*rIter)->GetConfigurationLabel() << ") suggests " << (*rIter)->GetNumberOfOutputs() << " 3D blocks"; + if ((*rIter)->GetNumberOfOutputs() == 1) + { + MITK_DEBUG << "Early out with reader #" << readerIndex << " (" << (*rIter)->GetConfigurationLabel() << "), less than 1 block is not possible"; + return *rIter; + } + } + 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) + { + unsigned int thisReadersNumberOfOutputs = (*rIter)->GetNumberOfOutputs(); + if ( thisReadersNumberOfOutputs > 0 // we don't count readers that don't actually produce output + && thisReadersNumberOfOutputs < 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..765e2bfa10 --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMFileReaderSelector.h @@ -0,0 +1,104 @@ +/*=================================================================== + +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" + +#include + +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: + + typedef std::list ReaderList; + + 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 Add a whole pre-configured reader to the selection process. + void AddFileReaderCanditate(DICOMFileReader::Pointer reader); + + /// \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(); + + /// \brief Return all the DICOMFileReader%s that are currently used for selection by this class. + /// The readers returned by this method depend on what config files have been added earlier + /// (or which of the built-in readers have been loaded) + ReaderList GetAllConfiguredReaders() const; + + /// 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); + void AddConfigFromResource(const std::string& resourcename); + void AddConfigFromResource(us::ModuleResource& resource); + + private: + + StringList m_PossibleConfigurations; + StringList m_InputFilenames; + ReaderList m_Readers; + + }; + +} // namespace + +#endif // mitkDICOMFileReaderSelector_h diff --git a/Modules/DICOMReader/mitkDICOMGDCMImageFrameInfo.cpp b/Modules/DICOMReader/mitkDICOMGDCMImageFrameInfo.cpp new file mode 100644 index 0000000000..3ad71934c2 --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMGDCMImageFrameInfo.cpp @@ -0,0 +1,112 @@ +/*=================================================================== + + 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 "mitkDICOMGDCMImageFrameInfo.h" + +mitk::DICOMGDCMImageFrameInfo +::DICOMGDCMImageFrameInfo(const std::string& filename, unsigned int frameNo) +:itk::LightObject() +,m_FrameInfo( DICOMImageFrameInfo::New(filename, frameNo) ) +,m_TagForValue(gdcm::Scanner::TagToValue()) // empty +{ +} + +mitk::DICOMGDCMImageFrameInfo +::DICOMGDCMImageFrameInfo(DICOMImageFrameInfo::Pointer frameinfo) +:itk::LightObject() +,m_FrameInfo(frameinfo) +,m_TagForValue(gdcm::Scanner::TagToValue()) // empty +{ +} + +mitk::DICOMGDCMImageFrameInfo +::DICOMGDCMImageFrameInfo(DICOMImageFrameInfo::Pointer frameinfo, gdcm::Scanner::TagToValue const& tagToValueMapping) +:itk::LightObject() +,m_FrameInfo(frameinfo) +,m_TagForValue(tagToValueMapping) +{ +} + +mitk::DICOMGDCMImageFrameInfo:: +~DICOMGDCMImageFrameInfo() +{ +} + +std::string +mitk::DICOMGDCMImageFrameInfo +::GetTagValueAsString(const DICOMTag& tag) const +{ + gdcm::Scanner::TagToValue::const_iterator mappedValue = m_TagForValue.find( gdcm::Tag(tag.GetGroup(), tag.GetElement()) ); + + if (mappedValue != m_TagForValue.end()) + { + if (mappedValue->second != NULL) + { + std::string s(mappedValue->second); + return s.erase(s.find_last_not_of(" \n\r\t")+1); + } + else + { + return std::string(""); + } + } + else + { + const DICOMTag tagImagePositionPatient = DICOMTag(0x0020,0x0032); // Image Position (Patient) + const DICOMTag tagImageOrientation = DICOMTag(0x0020, 0x0037); // Image Orientation + + if (tag == tagImagePositionPatient) + { + return std::string("0\\0\\0"); + } + else if (tag == tagImageOrientation) + { + return std::string("1\\0\\0\\0\\1\\0"); + } + else + { + return std::string(""); + } + } +} + +std::string +mitk::DICOMGDCMImageFrameInfo +::GetFilenameIfAvailable() const +{ + if (m_FrameInfo.IsNotNull()) + { + return m_FrameInfo->Filename; + } + else + { + return std::string(""); + } +} + +mitk::DICOMImageFrameInfo::Pointer +mitk::DICOMGDCMImageFrameInfo +::GetFrameInfo() const +{ + return m_FrameInfo; +} + +void + mitk::DICOMGDCMImageFrameInfo +::SetFrameInfo(DICOMImageFrameInfo::Pointer frameinfo) +{ + m_FrameInfo = frameinfo; +} diff --git a/Modules/DICOMReader/mitkDICOMGDCMImageFrameInfo.h b/Modules/DICOMReader/mitkDICOMGDCMImageFrameInfo.h new file mode 100644 index 0000000000..de13c2a355 --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMGDCMImageFrameInfo.h @@ -0,0 +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/mitkDICOMGDCMTagScanner.cpp b/Modules/DICOMReader/mitkDICOMGDCMTagScanner.cpp new file mode 100644 index 0000000000..73bf7ae0e6 --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMGDCMTagScanner.cpp @@ -0,0 +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 "mitkDICOMGDCMTagScanner.h" + +mitk::DICOMGDCMTagScanner +::DICOMGDCMTagScanner() +{ +} + +mitk::DICOMGDCMTagScanner +::DICOMGDCMTagScanner(const DICOMGDCMTagScanner& other) +:DICOMTagCache(other) +{ +} + +mitk::DICOMGDCMTagScanner +::~DICOMGDCMTagScanner() +{ +} + +std::string +mitk::DICOMGDCMTagScanner +::GetTagValue(DICOMImageFrameInfo* frame, const DICOMTag& tag) const +{ + assert(frame); + + for(DICOMGDCMImageFrameList::const_iterator frameIter = m_ScanResult.begin(); + frameIter != m_ScanResult.end(); + ++frameIter) + { + if ( (*frameIter)->GetFrameInfo().IsNotNull() && + (*((*frameIter)->GetFrameInfo()) == *frame ) + ) + { + return (*frameIter)->GetTagValueAsString(tag); + } + + } + + if ( m_ScannedTags.find(tag) != m_ScannedTags.end() ) + { + if ( std::find( m_InputFilenames.begin(), m_InputFilenames.end(), frame->Filename ) != m_InputFilenames.end() ) + { + // precondition of gdcm::Scanner::GetValue() fulfilled + return m_GDCMScanner.GetValue( frame->Filename.c_str(), gdcm::Tag( tag.GetGroup(), tag.GetElement() ) ); + } + else + { + // callers are required to tell us about the filenames they are interested in + // this is a helpful reminder for them to inform us + std::stringstream errorstring; + errorstring << "Invalid call to DICOMGDCMTagScanner::GetTagValue( " + << "'" << frame->Filename << "', frame " << frame->FrameNo + << " ). Filename was never mentioned before!"; + MITK_ERROR << errorstring.str(); + throw std::invalid_argument(errorstring.str()); + } + } + else + { + // callers are required to tell us about the tags they are interested in + // this is a helpful reminder for them to inform us + std::stringstream errorstring; + errorstring << "Invalid call to DICOMGDCMTagScanner::GetTagValue( "; + tag.Print(errorstring); + errorstring << " ). Tag was never mentioned before!"; + MITK_ERROR << errorstring.str(); + throw std::invalid_argument(errorstring.str()); + } +} + +void +mitk::DICOMGDCMTagScanner +::AddTag(const DICOMTag& tag) +{ + m_ScannedTags.insert(tag); + m_GDCMScanner.AddTag( gdcm::Tag(tag.GetGroup(), tag.GetElement()) ); // also a set, duplicate calls to AddTag don't hurt +} + +void +mitk::DICOMGDCMTagScanner +::AddTags(const DICOMTagList& tags) +{ + for(DICOMTagList::const_iterator tagIter = tags.begin(); + tagIter != tags.end(); + ++tagIter) + { + this->AddTag(*tagIter); + } +} + +void +mitk::DICOMGDCMTagScanner +::SetInputFiles(const StringList& filenames) +{ + m_InputFilenames = filenames; +} + + +void +mitk::DICOMGDCMTagScanner +::Scan() +{ + // TODO integrate push/pop locale?? + m_GDCMScanner.Scan( m_InputFilenames ); + + m_ScanResult.clear(); + for (StringList::const_iterator inputIter = m_InputFilenames.begin(); + inputIter != m_InputFilenames.end(); + ++inputIter) + { + m_ScanResult.push_back( DICOMGDCMImageFrameInfo::New( DICOMImageFrameInfo::New(*inputIter, 0), m_GDCMScanner.GetMapping(inputIter->c_str()) ) ); + } +} + +mitk::DICOMGDCMImageFrameList +mitk::DICOMGDCMTagScanner +::GetFrameInfoList() const +{ + return m_ScanResult; +} diff --git a/Modules/DICOMReader/mitkDICOMGDCMTagScanner.h b/Modules/DICOMReader/mitkDICOMGDCMTagScanner.h new file mode 100644 index 0000000000..76f7725843 --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMGDCMTagScanner.h @@ -0,0 +1,106 @@ +/*=================================================================== + +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 mitkDICOMGDCMTagScanner_h +#define mitkDICOMGDCMTagScanner_h + +#include "mitkDICOMTagCache.h" +#include "mitkDICOMEnums.h" + +#include "mitkDICOMGDCMImageFrameInfo.h" + +#include + +namespace mitk +{ + + /** + \ingroup DICOMReaderModule + \brief Encapsulates the tag scanning process for a set of DICOM files. + + Formerly integrated as a part of DICOMITKSeriesGDCMReader, the tag + scanning part has been factored out into this DICOMGDCMTagScanner class + in order to allow a single scan for multiple reader alternatives. This + helps much in the selection process of e.g. DICOMFileReaderSelector. + + The class works similar to gdcm::Scanner, just with the MITK set of classes: + - add a number of DICOM tags that should be read + - set a list of files that should be scanned for named tags + - call Scan() + - retrieve the scan results + - via GetFrameInfoList() or + - via GetTagValue() + + When used in a process where multiple classes will access the scan + results, care should be taken that all the tags and files of interst + are communicated to DICOMGDCMTagScanner before requesting the results! + */ + class DICOMReader_EXPORT DICOMGDCMTagScanner : public DICOMTagCache + { + public: + + mitkClassMacro( DICOMGDCMTagScanner, DICOMTagCache ); + itkNewMacro( DICOMGDCMTagScanner ); + + /** + \brief Add this tag to the scanning process. + */ + virtual void AddTag(const DICOMTag& tag); + /** + \brief Add a list of tags to the scanning process. + */ + virtual void AddTags(const DICOMTagList& tags); + + /** + \brief Define the list of files to scan. + This does not ADD to an internal list, but it replaces the + whole list of files. + */ + virtual void SetInputFiles(const StringList& filenames); + + /** + \brief Start the scanning process. + Calling Scan() will invalidate previous scans, forgetting + all about files and tags from files that have been scanned + previously. + */ + virtual void Scan(); + + /** + \brief Retrieve a result list for file-by-file tag access. + */ + virtual DICOMGDCMImageFrameList GetFrameInfoList() const; + + /** + \brief Directly retrieve the tag value for a given frame and tag. + */ + virtual std::string GetTagValue(DICOMImageFrameInfo* frame, const DICOMTag& tag) const; + + protected: + + DICOMGDCMTagScanner(); + DICOMGDCMTagScanner(const DICOMGDCMTagScanner&); + virtual ~DICOMGDCMTagScanner(); + + std::set m_ScannedTags; + + gdcm::Scanner m_GDCMScanner; + StringList m_InputFilenames; + DICOMGDCMImageFrameList m_ScanResult; + }; +} + +#endif diff --git a/Modules/DICOMReader/mitkDICOMITKSeriesGDCMReader.cpp b/Modules/DICOMReader/mitkDICOMITKSeriesGDCMReader.cpp new file mode 100644 index 0000000000..00eab84d98 --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMITKSeriesGDCMReader.cpp @@ -0,0 +1,728 @@ +/*=================================================================== + +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 +//#define ENABLE_TIMING + +#include "mitkDICOMITKSeriesGDCMReader.h" +#include "mitkITKDICOMSeriesReaderHelper.h" +#include "mitkGantryTiltInformation.h" +#include "mitkDICOMTagBasedSorter.h" +#include "mitkDICOMGDCMTagScanner.h" + +#include + +#include + +mitk::DICOMITKSeriesGDCMReader +::DICOMITKSeriesGDCMReader(unsigned int decimalPlacesForOrientation) +:DICOMFileReader() +,m_FixTiltByShearing(true) +,m_DecimalPlacesForOrientation(decimalPlacesForOrientation) +{ + this->EnsureMandatorySortersArePresent(decimalPlacesForOrientation); +} + +mitk::DICOMITKSeriesGDCMReader +::DICOMITKSeriesGDCMReader(const DICOMITKSeriesGDCMReader& other ) +:DICOMFileReader(other) +,m_FixTiltByShearing(false) +,m_SortingResultInProgress( other.m_SortingResultInProgress ) +,m_Sorter( other.m_Sorter ) +,m_EquiDistantBlocksSorter( other.m_EquiDistantBlocksSorter->Clone() ) +,m_NormalDirectionConsistencySorter( other.m_NormalDirectionConsistencySorter->Clone() ) +,m_ReplacedCLocales( other.m_ReplacedCLocales ) +,m_ReplacedCinLocales( other.m_ReplacedCinLocales ) +,m_DecimalPlacesForOrientation(other.m_DecimalPlacesForOrientation) +,m_TagCache( other.m_TagCache ) +{ +} + +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_SortingResultInProgress = other.m_SortingResultInProgress; + 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(); + this->m_ReplacedCLocales = other.m_ReplacedCLocales; + this->m_ReplacedCinLocales = other.m_ReplacedCinLocales; + this->m_DecimalPlacesForOrientation = other.m_DecimalPlacesForOrientation; + this->m_TagCache = other.m_TagCache; + } + return *this; +} + +bool +mitk::DICOMITKSeriesGDCMReader +::operator==(const DICOMFileReader& other) const +{ + if (const Self* otherSelf = dynamic_cast(&other)) + { + if ( this->m_FixTiltByShearing == otherSelf->m_FixTiltByShearing + && *(this->m_EquiDistantBlocksSorter) == *(otherSelf->m_EquiDistantBlocksSorter) + && (fabs(this->m_DecimalPlacesForOrientation - otherSelf->m_DecimalPlacesForOrientation) < eps) + ) + { + // test sorters for equality + if (this->m_Sorter.size() != otherSelf->m_Sorter.size()) return false; + + SorterList::const_iterator mySorterIter = this->m_Sorter.begin(); + SorterList::const_iterator oSorterIter = otherSelf->m_Sorter.begin(); + for(; mySorterIter != this->m_Sorter.end() && oSorterIter != otherSelf->m_Sorter.end(); + ++mySorterIter, ++oSorterIter) + { + if ( ! (**mySorterIter == **oSorterIter ) ) return false; // this sorter differs + } + + // nothing differs ==> all is equal + return true; + } + else + { + return false; + } + } + else + { + return false; + } +} + +void +mitk::DICOMITKSeriesGDCMReader +::SetFixTiltByShearing(bool on) +{ + m_FixTiltByShearing = on; +} + +bool +mitk::DICOMITKSeriesGDCMReader +::GetFixTiltByShearing() const +{ + return m_FixTiltByShearing; +} + +void +mitk::DICOMITKSeriesGDCMReader +::SetAcceptTwoSlicesGroups(bool accept) +{ + m_EquiDistantBlocksSorter->SetAcceptTwoSlicesGroups(accept); +} + +bool +mitk::DICOMITKSeriesGDCMReader +::GetAcceptTwoSlicesGroups() const +{ + return m_EquiDistantBlocksSorter->GetAcceptTwoSlicesGroups(); +} + + +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() const +{ + 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() const +{ + 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 +} + +#if defined(MBILOG_ENABLE_DEBUG) || defined (ENABLE_TIMING) + #define timeStart(part) timer.Start(part); + #define timeStop(part) timer.Stop(part); +#else + #define timeStart(part) + #define timeStop(part) +#endif + +void +mitk::DICOMITKSeriesGDCMReader +::AnalyzeInputFiles() +{ + itk::TimeProbesCollectorBase timer; + + timeStart("Reset"); + this->ClearOutputs(); + timeStop("Reset"); + + // prepare initial sorting (== list of input files) + StringList inputFilenames = this->GetInputFiles(); + timeStart("Check input for DCM"); + if ( inputFilenames.empty() + || + !this->CanHandleFile( inputFilenames.front() ) // first + || + !this->CanHandleFile( inputFilenames.back() ) // last + || + !this->CanHandleFile( inputFilenames[ inputFilenames.size() / 2] ) // roughly central file + ) + { + // TODO a read-as-many-as-possible fallback could be implemented here + MITK_DEBUG << "Reader unable to process files.."; + return; + } + + timeStop("Check input for DCM"); + + // scan files for sorting-relevant tags + if (m_TagCache.IsNull()) + { + timeStart("Tag scanning"); + DICOMGDCMTagScanner::Pointer filescanner = DICOMGDCMTagScanner::New(); + m_TagCache = filescanner.GetPointer(); // keep alive and make accessible to sub-classes + + filescanner->SetInputFiles(inputFilenames); + filescanner->AddTags( this->GetTagsOfInterest() ); + + PushLocale(); + filescanner->Scan(); + PopLocale(); + + timeStop("Tag scanning"); + } + else + { + // ensure that the tag cache contains our required tags AND files and has scanned! + } + + m_SortingResultInProgress.clear(); + // TODO We should remove the following cast + // DICOMImageFrameInfo would need to inherit DICOMDatasetAccess! + // - then the DICOMGDCMTagScanner class could create a DICOMGDCMImageFrameList internally + // - and return it as a DICOMImageFrameList + // - like this, DICOMITKSeriesGDCMReader could use the DICOMImageFrameInfoList to feed its sorters + // - problem: + // - DICOMImageFrameInfo is also part of DICOMImageBlockDescriptor, which is meant + // to describe the scanner output, even after the reader (and its scanner) is deleted. + // - if DICOMImageFrameInfo now inherits DICOMDatasetAccess, it would also need to implement + // GetTagValueAsString(). + // - so this could all work if we implement a default response in DICOMImageFrameInfo::GetTagValueAsString() (like in GetFilenameIfAvailable) + // and overwrite it in DICOMGDCMImageFrameInfo, which also knows about a specific GDCM scanner result + // (which again COULD (no need to?) be hidden as a point to a DICOMGDCMTagScanner class) + // + if ( DICOMGDCMTagScanner* tagCache = dynamic_cast(m_TagCache.GetPointer()) ) + { + m_SortingResultInProgress.push_back( tagCache->GetFrameInfoList() ); + } + else + { + throw std::logic_error("Bad implementation error: DICOMITKSeriesGDCMReader now unable to find dataset/tag information for its input."); + } + + // sort and split blocks as configured + + timeStart("Sorting frames"); + unsigned int sorterIndex = 0; + for(SorterList::iterator sorterIter = m_Sorter.begin(); + sorterIter != m_Sorter.end(); + ++sorterIndex, ++sorterIter) + { + std::stringstream ss; ss << "Sorting step " << sorterIndex; + timeStart( ss.str().c_str() ); + m_SortingResultInProgress = this->InternalExecuteSortingStep(sorterIndex, *sorterIter, m_SortingResultInProgress); + timeStop( ss.str().c_str() ); + } + + // a last extra-sorting step: ensure equidistant slices + timeStart( "EquiDistantBlocksSorter" ); + m_SortingResultInProgress = this->InternalExecuteSortingStep(sorterIndex++, m_EquiDistantBlocksSorter.GetPointer(), m_SortingResultInProgress); + timeStop( "EquiDistantBlocksSorter" ); + + timeStop("Sorting frames"); + + timeStart("Condensing 3D blocks"); + m_SortingResultInProgress = this->Condense3DBlocks( m_SortingResultInProgress ); + timeStop("Condensing 3D blocks"); + + // provide final result as output + + timeStart("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; + assert(!gdcmFrameInfoList.empty()); + + // reverse frames if necessary + // update tilt information from absolute last sorting + DICOMDatasetList datasetList = ToDICOMDatasetList( gdcmFrameInfoList ); + m_NormalDirectionConsistencySorter->SetInput( datasetList ); + m_NormalDirectionConsistencySorter->Sort(); + DICOMGDCMImageFrameList sortedGdcmInfoFrameList = FromDICOMDatasetList( m_NormalDirectionConsistencySorter->GetOutput(0) ); + const GantryTiltInformation& tiltInfo = m_NormalDirectionConsistencySorter->GetTiltInformation(); + + // set frame list for current block + DICOMImageFrameList frameList = ToDICOMImageFrameList( sortedGdcmInfoFrameList ); + assert(!frameList.empty()); + + DICOMImageBlockDescriptor block; + block.SetTagCache( this->GetTagCache() ); // important: this must be before SetImageFrameList(), because SetImageFrameList will trigger reading of lots of interesting tags! + block.SetImageFrameList( frameList ); + block.SetTiltInformation( tiltInfo ); + + block.SetReaderImplementationLevel( this->GetReaderImplementationLevel( block.GetSOPClassUID() ) ); + + this->SetOutput( o, block ); + } + timeStop("Output"); + +#if defined(MBILOG_ENABLE_DEBUG) || defined (ENABLE_TIMING) + 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) +{ + SortingBlockList nextStepSorting; // we should not modify our input list while processing it + std::stringstream ss; ss << "Sorting step " << sortingStepIndex << " '"; +#if defined(MBILOG_ENABLE_DEBUG) + sorter->PrintConfiguration(ss); +#endif + ss << "'"; + 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 ); + } + } + + 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 +::LoadMitkImageForImageBlockDescriptor(DICOMImageBlockDescriptor& block) const +{ + PushLocale(); + const DICOMImageFrameList& frames = block.GetImageFrameList(); + const GantryTiltInformation tiltInfo = block.GetTiltInformation(); + bool hasTilt = tiltInfo.IsRegularGantryTilt(); + + ITKDICOMSeriesReaderHelper::StringContainer filenames; + for (DICOMImageFrameList::const_iterator frameIter = frames.begin(); + frameIter != frames.end(); + ++frameIter) + { + filenames.push_back( (*frameIter)->Filename ); + } + + mitk::ITKDICOMSeriesReaderHelper helper; + bool success(true); + try + { + mitk::Image::Pointer mitkImage = helper.Load( filenames, m_FixTiltByShearing && hasTilt, tiltInfo ); + block.SetMitkImage( mitkImage ); + } + catch (std::exception& e) + { + success = false; + MITK_ERROR << "Exception during image loading: " << e.what(); + } + + PopLocale(); + + return success; +} + + +bool +mitk::DICOMITKSeriesGDCMReader +::LoadMitkImageForOutput(unsigned int o) +{ + DICOMImageBlockDescriptor& block = this->InternalGetOutput(o); + return this->LoadMitkImageForImageBlockDescriptor(block); +} + + +bool +mitk::DICOMITKSeriesGDCMReader +::CanHandleFile(const std::string& filename) +{ + return ITKDICOMSeriesReaderHelper::CanHandleFile(filename); +} + +void +mitk::DICOMITKSeriesGDCMReader +::AddSortingElement(DICOMDatasetSorter* sorter, bool atFront) +{ + assert(sorter); + + if (atFront) + { + m_Sorter.push_front( sorter ); + } + else + { + m_Sorter.push_back( sorter ); + } +} + +mitk::DICOMITKSeriesGDCMReader::ConstSorterList +mitk::DICOMITKSeriesGDCMReader +::GetFreelyConfiguredSortingElements() const +{ + std::list result; + + unsigned int sortIndex(0); + for(SorterList::const_iterator sorterIter = m_Sorter.begin(); + sorterIter != m_Sorter.end(); + ++sortIndex, ++sorterIter) + { + if (sortIndex > 0) // ignore first element (see EnsureMandatorySortersArePresent) + { + result.push_back( (*sorterIter).GetPointer() ); + } + } + + return result; +} + +void +mitk::DICOMITKSeriesGDCMReader +::EnsureMandatorySortersArePresent(unsigned int decimalPlacesForOrientation) +{ + 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(decimalPlacesForOrientation) ); // Image Orientation (Patient) + 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(); + } +} + +void +mitk::DICOMITKSeriesGDCMReader +::SetToleratedOriginOffsetToAdaptive(double fractionOfInterSliceDistance) +{ + assert( m_EquiDistantBlocksSorter.IsNotNull() ); + m_EquiDistantBlocksSorter->SetToleratedOriginOffsetToAdaptive(fractionOfInterSliceDistance); +} + +void +mitk::DICOMITKSeriesGDCMReader +::SetToleratedOriginOffset(double millimeters) +{ + assert( m_EquiDistantBlocksSorter.IsNotNull() ); + m_EquiDistantBlocksSorter->SetToleratedOriginOffset(millimeters); +} + +double +mitk::DICOMITKSeriesGDCMReader +::GetToleratedOriginError() const +{ + assert( m_EquiDistantBlocksSorter.IsNotNull() ); + return m_EquiDistantBlocksSorter->GetToleratedOriginOffset(); +} + +bool +mitk::DICOMITKSeriesGDCMReader +::IsToleratedOriginOffsetAbsolute() const +{ + assert( m_EquiDistantBlocksSorter.IsNotNull() ); + return m_EquiDistantBlocksSorter->IsToleratedOriginOffsetAbsolute(); +} + +double +mitk::DICOMITKSeriesGDCMReader +::GetDecimalPlacesForOrientation() const +{ + return m_DecimalPlacesForOrientation; +} + +mitk::DICOMTagCache::Pointer +mitk::DICOMITKSeriesGDCMReader +::GetTagCache() const +{ + return m_TagCache; +} + +void +mitk::DICOMITKSeriesGDCMReader +::SetTagCache(DICOMTagCache::Pointer tagCache) +{ + m_TagCache = tagCache; +} + +mitk::DICOMTagList +mitk::DICOMITKSeriesGDCMReader +::GetTagsOfInterest() const +{ + DICOMTagList completeList; + + // check all configured sorters + for(SorterList::const_iterator sorterIter = m_Sorter.begin(); + sorterIter != m_Sorter.end(); + ++sorterIter) + { + assert(sorterIter->IsNotNull()); + + DICOMTagList tags = (*sorterIter)->GetTagsOfInterest(); + completeList.insert( completeList.end(), tags.begin(), tags.end() ); + } + + // check our own forced sorters + DICOMTagList tags = m_EquiDistantBlocksSorter->GetTagsOfInterest(); + completeList.insert( completeList.end(), tags.begin(), tags.end() ); + + tags = m_NormalDirectionConsistencySorter->GetTagsOfInterest(); + completeList.insert( completeList.end(), tags.begin(), tags.end() ); + + // add the tags for DICOMImageBlockDescriptor + tags = DICOMImageBlockDescriptor::GetTagsOfInterest(); + completeList.insert( completeList.end(), tags.begin(), tags.end() ); + + return completeList; +} diff --git a/Modules/DICOMReader/mitkDICOMITKSeriesGDCMReader.h b/Modules/DICOMReader/mitkDICOMITKSeriesGDCMReader.h new file mode 100644 index 0000000000..63f58c2630 --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMITKSeriesGDCMReader.h @@ -0,0 +1,356 @@ +/*=================================================================== + +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 + +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 + + \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 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(). + - during this check, we need to tolerate some minor errors in documented vs. calculated image origins. + The amount of tolerance can be adjusted by SetToleratedOriginOffset() and SetToleratedOriginOffsetToAdaptive(). + Please see EquiDistantBlocksSorter for more details. The default should be good for most cases. + 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 +{ + public: + + mitkClassMacro( DICOMITKSeriesGDCMReader, DICOMFileReader ); + mitkCloneMacro( DICOMITKSeriesGDCMReader ); + itkNewMacro( DICOMITKSeriesGDCMReader ); + mitkNewMacro1Param( DICOMITKSeriesGDCMReader, unsigned int ); + + /** + \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); + + typedef const std::list ConstSorterList; + ConstSorterList GetFreelyConfiguredSortingElements() const; + + /** + \brief Controls whether to "fix" tilted acquisitions by shearing the output (see \ref DICOMITKSeriesGDCMReader_GantryTilt). + */ + void SetFixTiltByShearing(bool on); + + bool GetFixTiltByShearing() const; + + /** + \brief Controls whether groups of only two images are accepted when ensuring consecutive slices via EquiDistantBlocksSorter. + */ + void SetAcceptTwoSlicesGroups(bool accept); + bool GetAcceptTwoSlicesGroups() const; + + /** + \brief See \ref DICOMITKSeriesGDCMReader_ForcedConfiguration. + */ + void SetToleratedOriginOffsetToAdaptive(double fractionOfInterSliceDistanct = 0.3); + + /** + \brief See \ref DICOMITKSeriesGDCMReader_ForcedConfiguration. + */ + void SetToleratedOriginOffset(double millimeters = 0.005); + + double GetToleratedOriginError() const; + bool IsToleratedOriginOffsetAbsolute() const; + + double GetDecimalPlacesForOrientation() const; + + virtual bool operator==(const DICOMFileReader& other) const; + + virtual DICOMTagList GetTagsOfInterest() const; + + protected: + + virtual void InternalPrintConfiguration(std::ostream& os) 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() const; + /** + \brief Activate last remembered locale from locale stack + "C" locale is required for correct parsing of numbers by itk::ImageSeriesReader + */ + void PopLocale() const; + + DICOMITKSeriesGDCMReader(unsigned int decimalPlacesForOrientation = 5); + 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); + + virtual DICOMTagCache::Pointer GetTagCache() const; + void SetTagCache(DICOMTagCache::Pointer); + + /// \brief Sorting step as described in \ref DICOMITKSeriesGDCMReader_LoadingStrategy + SortingBlockList InternalExecuteSortingStep( + unsigned int sortingStepIndex, + DICOMDatasetSorter::Pointer sorter, + const SortingBlockList& input); + + /// \brief Loads the mitk::Image by means of an itk::ImageSeriesReader + virtual bool LoadMitkImageForOutput(unsigned int o); + + virtual bool LoadMitkImageForImageBlockDescriptor(DICOMImageBlockDescriptor& block) const; + + /** + \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: + + /// \brief Creates the required sorting steps described in \ref DICOMITKSeriesGDCMReader_ForcedConfiguration + void EnsureMandatorySortersArePresent(unsigned int decimalPlacesForOrientation); + + 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 and ClassicDICOMSeriesReader due to lack of time + mitk::EquiDistantBlocksSorter::Pointer m_EquiDistantBlocksSorter; + + mitk::NormalDirectionConsistencySorter::Pointer m_NormalDirectionConsistencySorter; + + private: + + mutable std::stack m_ReplacedCLocales; + mutable std::stack m_ReplacedCinLocales; + + double m_DecimalPlacesForOrientation; + + DICOMTagCache::Pointer m_TagCache; +}; + +} + +#endif diff --git a/Modules/DICOMReader/mitkDICOMImageBlockDescriptor.cpp b/Modules/DICOMReader/mitkDICOMImageBlockDescriptor.cpp new file mode 100644 index 0000000000..a9e145e0b6 --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMImageBlockDescriptor.cpp @@ -0,0 +1,742 @@ +/*=================================================================== + +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 "mitkLevelWindowProperty.h" +#include + +mitk::DICOMImageBlockDescriptor +::DICOMImageBlockDescriptor() +:m_ReaderImplementationLevel(SOPClassUnknown) +,m_PropertyList(PropertyList::New()) +,m_TagCache(NULL) +,m_PropertiesOutOfDate(true) +{ +} + +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_TiltInformation( other.m_TiltInformation ) +,m_PropertyList( other.m_PropertyList->Clone() ) +,m_TagCache( other.m_TagCache ) +,m_PropertiesOutOfDate( other.m_PropertiesOutOfDate ) +{ + 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_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; + m_PropertiesOutOfDate = other.m_PropertiesOutOfDate; + } + return *this; +} + +mitk::DICOMTagList +mitk::DICOMImageBlockDescriptor::GetTagsOfInterest() +{ + DICOMTagList completeList; + + completeList.push_back( DICOMTag(0x0018,0x1164) ); // pixel spacing + completeList.push_back( DICOMTag(0x0028,0x0030) ); // imager pixel spacing + + completeList.push_back( DICOMTag(0x0008,0x0018) ); // sop instance UID + completeList.push_back( DICOMTag(0x0008,0x0016) ); // sop class UID + + completeList.push_back( DICOMTag(0x0020,0x0011) ); // series number + completeList.push_back( DICOMTag(0x0008,0x1030) ); // study description + completeList.push_back( DICOMTag(0x0008,0x103e) ); // series description + completeList.push_back( DICOMTag(0x0008,0x0060) ); // modality + completeList.push_back( DICOMTag(0x0018,0x0024) ); // sequence name + completeList.push_back( DICOMTag(0x0020,0x0037) ); // image orientation + + completeList.push_back( DICOMTag(0x0020,0x1041) ); // slice location + completeList.push_back( DICOMTag(0x0020,0x0012) ); // acquisition number + completeList.push_back( DICOMTag(0x0020,0x0013) ); // instance number + completeList.push_back( DICOMTag(0x0020,0x0032) ); // image position patient + + completeList.push_back( DICOMTag(0x0028,0x1050) ); // window center + completeList.push_back( DICOMTag(0x0028,0x1051) ); // window width + completeList.push_back( DICOMTag(0x0008,0x0008) ); // image type + completeList.push_back( DICOMTag(0x0028,0x0004) ); // photometric interpretation + + return completeList; +} + +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); + + m_PropertiesOutOfDate = true; +} + +const mitk::DICOMImageFrameList& +mitk::DICOMImageBlockDescriptor +::GetImageFrameList() const +{ + return m_ImageFrameList; +} + +void +mitk::DICOMImageBlockDescriptor +::SetMitkImage(Image::Pointer image) +{ + if (m_MitkImage != image) + { + if (m_TagCache.IsNull()) + { + MITK_ERROR << "Unable to describe MITK image with properties without a tag-cache object!"; + m_MitkImage = NULL; + return; + } + + if (m_ImageFrameList.empty()) + { + MITK_ERROR << "Unable to describe MITK image with properties without a frame list!"; + m_MitkImage = NULL; + return; + } + + // Should verify that the image matches m_ImageFrameList and m_TagCache + // however, this is hard to do without re-analyzing all + // TODO we should at least make sure that the number of frames is identical (plus rows/columns, orientation) + // without gantry tilt correction, we can also check image origin + + 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; +} + +/* + 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_ImageFrameList.empty() || m_TagCache.IsNull() ) + { + MITK_ERROR << "Invalid call to GetPixelSpacingInterpretation. Need to have initialized tag-cache!"; + return SpacingUnknown; + } + + std::string pixelSpacing = this->GetPixelSpacing(); + std::string imagerPixelSpacing = this->GetImagerPixelSpacing(); + + if (pixelSpacing.empty()) + { + if (imagerPixelSpacing.empty()) + { + return SpacingUnknown; + } + else + { + return SpacingAtDetector; + } + } + else // Pixel Spacing defined + { + if (imagerPixelSpacing.empty()) + { + return SpacingInPatient; + } + else if (pixelSpacing != imagerPixelSpacing) + { + return SpacingInPatient; + } + else + { + return SpacingAtDetector; + } + } +} + +std::string +mitk::DICOMImageBlockDescriptor +::GetPixelSpacing() const +{ + if ( m_ImageFrameList.empty() || m_TagCache.IsNull() ) + { + MITK_ERROR << "Invalid call to GetPixelSpacing. Need to have initialized tag-cache!"; + return std::string(""); + } + + static const DICOMTag tagPixelSpacing(0x0028,0x0030); + return m_TagCache->GetTagValue( m_ImageFrameList.front(), tagPixelSpacing ); +} + +std::string +mitk::DICOMImageBlockDescriptor +::GetImagerPixelSpacing() const +{ + if ( m_ImageFrameList.empty() || m_TagCache.IsNull() ) + { + MITK_ERROR << "Invalid call to GetImagerPixelSpacing. Need to have initialized tag-cache!"; + return std::string(""); + } + + static const DICOMTag tagImagerPixelSpacing(0x0018,0x1164); + return m_TagCache->GetTagValue( m_ImageFrameList.front(), tagImagerPixelSpacing ); +} + +void +mitk::DICOMImageBlockDescriptor +::GetDesiredMITKImagePixelSpacing( ScalarType& spacingX, ScalarType& spacingY) const +{ + std::string pixelSpacing = this->GetPixelSpacing(); + // preference for "in patient" pixel spacing + if ( !DICOMStringToSpacing( pixelSpacing, spacingX, spacingY ) ) + { + std::string imagerPixelSpacing = this->GetImagerPixelSpacing(); + // fallback to "on detector" spacing + if ( !DICOMStringToSpacing( 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 +{ + this->UpdateImageDescribingProperties(); + return m_PropertyList->GetProperty(key); +} + +std::string +mitk::DICOMImageBlockDescriptor +::GetPropertyAsString(const std::string& key) const +{ + this->UpdateImageDescribingProperties(); + 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 +{ + this->UpdateImageDescribingProperties(); + 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 +{ + this->UpdateImageDescribingProperties(); + IntProperty::ConstPointer intProp = dynamic_cast( this->GetProperty(key) ); + if (intProp.IsNotNull()) + { + return intProp->GetValue(); + } + else + { + return defaultValue; + } +} + +double +mitk::DICOMImageBlockDescriptor +::stringtodouble(const std::string& str) const +{ + double d; + std::string trimmedstring(str); + trimmedstring = trimmedstring.erase(trimmedstring.find_last_not_of(" \n\r\t")+1); + + std::istringstream converter(trimmedstring); + if ( !trimmedstring.empty() && (converter >> d) && converter.eof() ) + { + return d; + } + else + { + throw std::invalid_argument("Argument is not a convertable number"); + } +} + +mitk::Image::Pointer +mitk::DICOMImageBlockDescriptor +::DescribeImageWithProperties(Image* mitkImage) +{ + // TODO: this is a collection of properties that have been provided by the + // legacy DicomSeriesReader. + // We should at some point clean up this collection and name them in a more + // consistent way! + + 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( this->GetSOPClassUID() ) ); + + mitkImage->SetProperty("dicomseriesreader.SOPClass", + 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->GetTiltInformation().IsRegularGantryTilt() )); + + mitkImage->SetProperty("dicomseriesreader.3D+t", + BoolProperty::New( this->GetFlag("3D+t",false) )); + + // level window + std::string windowCenter = this->GetPropertyAsString("windowCenter"); + std::string windowWidth = this->GetPropertyAsString("windowWidth"); + try + { + double level = stringtodouble( windowCenter ); + double window = stringtodouble( windowWidth ); + mitkImage->SetProperty("levelwindow", LevelWindowProperty::New(LevelWindow(level,window)) ); + } + catch (...) + { + // nothing, no levelwindow to be predicted... + } + + mitkImage->SetProperty("dicom.pixel.PhotometricInterpretation", this->GetProperty("photometricInterpretation") ); + mitkImage->SetProperty("dicom.image.imagetype", this->GetProperty("imagetype") ); + + mitkImage->SetProperty("dicom.study.StudyDescription", this->GetProperty("studyDescription") ); + mitkImage->SetProperty("dicom.series.SeriesDescription", this->GetProperty("seriesDescription") ); + + // 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; +} + +std::string +mitk::DICOMImageBlockDescriptor +::GetSOPClassUID() const +{ + if ( !m_ImageFrameList.empty() && m_TagCache.IsNotNull() ) + { + static const DICOMTag tagSOPClassUID(0x0008,0x0016); + return m_TagCache->GetTagValue( m_ImageFrameList.front(), tagSOPClassUID ); + } + else + { + MITK_ERROR << "Invalid call to DICOMImageBlockDescriptor::GetSOPClassUID(). Need to have initialized tag-cache!"; + return std::string(""); + } +} + +std::string +mitk::DICOMImageBlockDescriptor +::GetSOPClassUIDAsName() const +{ + if ( !m_ImageFrameList.empty() && m_TagCache.IsNotNull() ) + { + gdcm::UIDs uidKnowledge; + uidKnowledge.SetFromUID( this->GetSOPClassUID().c_str() ); + + const char* name = uidKnowledge.GetName(); + if (name) + { + return std::string(name); + } + else + { + return std::string(""); + } + } + else + { + MITK_ERROR << "Invalid call to DICOMImageBlockDescriptor::GetSOPClassUIDAsName(). Need to have initialized tag-cache!"; + return std::string(""); + } +} + + +void +mitk::DICOMImageBlockDescriptor +::SetTagCache(DICOMTagCache* privateCache) +{ + // this must only be used during loading and never afterwards + 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.empty() || !last.empty()) \ + { \ + 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 ); \ + if (!first.empty()) \ + { \ + 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->GetSOPClassUIDAsName() << "'" << std::endl; + + printProperty("Series Number", seriesNumber); + printProperty("Study Description", studyDescription); + printProperty("Series Description", seriesDescription); + 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->GetTiltInformation().IsRegularGantryTilt()) + //printBool("3D+t", this->GetFlag("3D+t",false)) + + //os << " MITK image loaded: '" << (this->GetMitkImage().IsNotNull() ? "yes" : "no") << "'" << std::endl; + if (filenameDetails) + { + os << " Files in this image block:" << std::endl; + for (DICOMImageFrameList::const_iterator frameIter = m_ImageFrameList.begin(); + frameIter != m_ImageFrameList.end(); + ++frameIter) + { + os << " " << (*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 ); \ + const_cast(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 ); \ + const_cast(this)->SetProperty(#tag_name "First", StringProperty::New( tagValueFirst ) ); \ + const_cast(this)->SetProperty(#tag_name "Last", StringProperty::New( tagValueLast ) ); \ +} + + +void +mitk::DICOMImageBlockDescriptor +::UpdateImageDescribingProperties() const +{ + if (!m_PropertiesOutOfDate) return; + + if (!m_ImageFrameList.empty()) + { + if (m_TagCache.IsNull()) + { + MITK_ERROR << "Invalid call to DICOMImageBlockDescriptor::UpdateImageDescribingProperties(). Need to have initialized tag-cache!"; + return; + } + + DICOMImageFrameInfo::Pointer firstFrame = m_ImageFrameList.front(); + DICOMImageFrameInfo::Pointer lastFrame = m_ImageFrameList.back(); + + // see macros above + storeTagValueToProperty(seriesNumber,0x0020,0x0011) + storeTagValueToProperty(studyDescription,0x0008,0x1030) + storeTagValueToProperty(seriesDescription,0x0008,0x103e) + 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) + + storeTagValueToProperty(windowCenter,0x0028,0x1050) + storeTagValueToProperty(windowWidth,0x0028,0x1051) + storeTagValueToProperty(imageType,0x0008,0x0008) + storeTagValueToProperty(photometricInterpretation,0x0028,0x0004) + + // 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 + const_cast(this)->SetProperty( "sliceLocationForSlices", StringLookupTableProperty::New( sliceLocationForSlices ) ); + const_cast(this)->SetProperty( "instanceNumberForSlices", StringLookupTableProperty::New( instanceNumberForSlices ) ); + const_cast(this)->SetProperty( "SOPInstanceUIDForSlices", StringLookupTableProperty::New( SOPInstanceUIDForSlices ) ); + } + + m_PropertiesOutOfDate = false; + } +} diff --git a/Modules/DICOMReader/mitkDICOMImageBlockDescriptor.h b/Modules/DICOMReader/mitkDICOMImageBlockDescriptor.h new file mode 100644 index 0000000000..e3e060b6ba --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMImageBlockDescriptor.h @@ -0,0 +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 "mitkWeakPointer.h" + +#include "mitkGantryTiltInformation.h" + +namespace mitk +{ + +/** + \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); + + static DICOMTagList GetTagsOfInterest(); + + /// 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; + + 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; + + public: + + /// Describe how the mitk::Image's pixel spacing should be interpreted + PixelSpacingInterpretation GetPixelSpacingInterpretation() 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; + + /// 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; + /// 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: + + // read values from tag cache + std::string GetPixelSpacing() const; + std::string GetImagerPixelSpacing() const; + + Image::Pointer FixupSpacing(Image* mitkImage); + Image::Pointer DescribeImageWithProperties(Image* mitkImage); + void UpdateImageDescribingProperties() const; + + double stringtodouble(const std::string& str) const; + + DICOMImageFrameList m_ImageFrameList; + Image::Pointer m_MitkImage; + BoolList m_SliceIsLoaded; + ReaderImplementationLevel m_ReaderImplementationLevel; + + GantryTiltInformation m_TiltInformation; + + PropertyList::Pointer m_PropertyList; + + mitk::WeakPointer m_TagCache; + + mutable bool m_PropertiesOutOfDate; +}; + +} + +#endif diff --git a/Modules/DICOMReader/mitkDICOMImageFrameInfo.cpp b/Modules/DICOMReader/mitkDICOMImageFrameInfo.cpp new file mode 100644 index 0000000000..5b6cd65b2a --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMImageFrameInfo.cpp @@ -0,0 +1,34 @@ +/*=================================================================== + +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 "mitkDICOMImageFrameInfo.h" + +mitk::DICOMImageFrameInfo +::DICOMImageFrameInfo(const std::string& filename, unsigned int frameNo) +:Filename(filename) +,FrameNo(frameNo) +{ +} + +bool +mitk::DICOMImageFrameInfo +::operator==(const DICOMImageFrameInfo& other) const +{ + return + this->Filename == other.Filename + && this->FrameNo == other.FrameNo; + +} diff --git a/Modules/DICOMReader/mitkDICOMImageFrameInfo.h b/Modules/DICOMReader/mitkDICOMImageFrameInfo.h new file mode 100644 index 0000000000..713076e2f8 --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMImageFrameInfo.h @@ -0,0 +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..baa0252984 --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMReaderConfigurator.cpp @@ -0,0 +1,679 @@ +/*=================================================================== + +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 "mitkDICOMSortByTag.h" +#include "mitkSortByImagePositionPatient.h" + +mitk::DICOMReaderConfigurator +::DICOMReaderConfigurator() +{ +} + +mitk::DICOMReaderConfigurator +::~DICOMReaderConfigurator() +{ +} + +mitk::DICOMFileReader::Pointer +mitk::DICOMReaderConfigurator +::CreateFromConfigFile(const std::string& filename) const +{ + 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) const +{ + TiXmlDocument doc; + doc.Parse(xmlContents.c_str(), 0, TIXML_ENCODING_UTF8); + + return this->CreateFromTiXmlDocument( doc ); +} + +mitk::DICOMFileReader::Pointer +mitk::DICOMReaderConfigurator +::CreateFromTiXmlDocument(TiXmlDocument& doc) const +{ + 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; + } + + int version(1); + if ( rootElement->QueryIntAttribute("version", &version) == TIXML_SUCCESS) + { + if (version != 1) + { + MITK_WARN << "This reader is only capable of creating DICOMFileReaders of version 1. " + << "Will not continue, because given configuration is meant for version " << version << "."; + return NULL; + } + } + else + { + MITK_ERROR << "File should name the version of the reader class in the version attribute: ." + << " Found nothing instead, assuming version 1!"; + version = 1; + } + + std::string classname(classnameC); + + double decimalPlacesForOrientation(5); + bool useDecimalPlacesForOrientation(false); + useDecimalPlacesForOrientation = + rootElement->QueryDoubleAttribute("decimalPlacesForOrientation", &decimalPlacesForOrientation) == TIXML_SUCCESS; // attribute present and a double value + + if (classname == "ClassicDICOMSeriesReader") + { + mitk::ClassicDICOMSeriesReader::Pointer reader = mitk::ClassicDICOMSeriesReader::New(); + this->ConfigureCommonPropertiesOfDICOMITKSeriesGDCMReader(reader.GetPointer(), rootElement); + this->ConfigureCommonPropertiesOfThreeDnTDICOMSeriesReader(reader.GetPointer(), rootElement); + return reader.GetPointer(); + } + if (classname == "ThreeDnTDICOMSeriesReader") + { + mitk::ThreeDnTDICOMSeriesReader::Pointer reader; + if (useDecimalPlacesForOrientation) + reader = mitk::ThreeDnTDICOMSeriesReader::New(decimalPlacesForOrientation); + else + reader = mitk::ThreeDnTDICOMSeriesReader::New(); + + return ConfigureThreeDnTDICOMSeriesReader(reader, rootElement).GetPointer(); + } + else + if (classname == "DICOMITKSeriesGDCMReader") + { + mitk::DICOMITKSeriesGDCMReader::Pointer reader; + if (useDecimalPlacesForOrientation) + reader = mitk::DICOMITKSeriesGDCMReader::New(decimalPlacesForOrientation); + else + 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") + +void +mitk::DICOMReaderConfigurator +::ConfigureCommonPropertiesOfThreeDnTDICOMSeriesReader(ThreeDnTDICOMSeriesReader::Pointer reader, TiXmlElement* element) const +{ + // 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 ); +} + +mitk::ThreeDnTDICOMSeriesReader::Pointer +mitk::DICOMReaderConfigurator +::ConfigureThreeDnTDICOMSeriesReader(ThreeDnTDICOMSeriesReader::Pointer reader, TiXmlElement* element) const +{ + assert(element); + + // use all the base class configuration + if (this->ConfigureDICOMITKSeriesGDCMReader( reader.GetPointer(), element ).IsNull()) + { + return NULL; + } + + this->ConfigureCommonPropertiesOfThreeDnTDICOMSeriesReader(reader,element); + return reader; +} + +void +mitk::DICOMReaderConfigurator +::ConfigureCommonPropertiesOfDICOMITKSeriesGDCMReader(DICOMITKSeriesGDCMReader::Pointer reader, TiXmlElement* element) const +{ + assert(element); + + const char* configLabelC = element->Attribute("label"); + if (configLabelC) + { + std::string configLabel(configLabelC); + reader->SetConfigurationLabel(configLabel); + } + + const char* configDescriptionC = element->Attribute("description"); + 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 ); + +} + +mitk::DICOMITKSeriesGDCMReader::Pointer +mitk::DICOMReaderConfigurator +::ConfigureDICOMITKSeriesGDCMReader(DICOMITKSeriesGDCMReader::Pointer reader, TiXmlElement* element) const +{ + assert(element); + + this->ConfigureCommonPropertiesOfDICOMITKSeriesGDCMReader(reader, element); + + // "acceptTwoSlicesGroups" flag + bool acceptTwoSlicesGroups(true); + const char* acceptTwoSlicesGroupsC = element->Attribute("acceptTwoSlicesGroups"); + if (acceptTwoSlicesGroupsC) + { + std::string acceptTwoSlicesGroupsS(acceptTwoSlicesGroupsC); + acceptTwoSlicesGroups = boolStringTrue(acceptTwoSlicesGroupsS); + } + + reader->SetAcceptTwoSlicesGroups( acceptTwoSlicesGroups ); + + // "toleratedOriginError" attribute (double) + bool toleratedOriginErrorIsAbsolute(false); + const char* toleratedOriginErrorIsAbsoluteC = element->Attribute("toleratedOriginErrorIsAbsolute"); + if (toleratedOriginErrorIsAbsoluteC) + { + std::string toleratedOriginErrorIsAbsoluteS(toleratedOriginErrorIsAbsoluteC); + toleratedOriginErrorIsAbsolute = boolStringTrue(toleratedOriginErrorIsAbsoluteS); + } + + double toleratedOriginError(0.3); + if (element->QueryDoubleAttribute("toleratedOriginError", &toleratedOriginError) == TIXML_SUCCESS) // attribute present and a double value + { + if (toleratedOriginErrorIsAbsolute) + { + reader->SetToleratedOriginOffset( toleratedOriginError ); + } + else + { + reader->SetToleratedOriginOffsetToAdaptive( toleratedOriginError ); + } + } + + // DICOMTagBasedSorters are the only thing we create at this point + // TODO for-loop over all child elements of type DICOMTagBasedSorter, BUT actually a single sorter of this type is enough. + TiXmlElement* dElement = element->FirstChildElement("DICOMDatasetSorter"); + if (dElement) + { + const char* classnameC = dElement->Attribute("class"); + if (!classnameC) + { + MITK_ERROR << "File should name a DICOMDatasetSorter class in the class attribute of . Found nothing instead"; + return NULL; + } + + std::string classname(classnameC); + + if (classname == "DICOMTagBasedSorter") + { + DICOMTagBasedSorter::Pointer tagSorter = CreateDICOMTagBasedSorter(dElement); + if (tagSorter.IsNotNull()) + { + reader->AddSortingElement( tagSorter ); + } + } + else + { + MITK_ERROR << "DICOMDatasetSorter tag names unknown class '" << classname << "'"; + return NULL; + } + } + + return reader; +} + +mitk::DICOMTagBasedSorter::Pointer +mitk::DICOMReaderConfigurator +::CreateDICOMTagBasedSorter(TiXmlElement* element) const +{ + mitk::DICOMTagBasedSorter::Pointer tagSorter = mitk::DICOMTagBasedSorter::New(); + + // "strictSorting" parameter! + bool strictSorting(true); + const char* strictSortingC = element->Attribute("strictSorting"); + if (strictSortingC) + { + std::string strictSortingS(strictSortingC); + strictSorting = boolStringTrue(strictSortingS); + } + tagSorter->SetStrictSorting(strictSorting); + + TiXmlElement* dElement = element->FirstChildElement("Distinguishing"); + if (dElement) + { + for ( TiXmlElement* tChild = dElement->FirstChildElement(); + tChild != NULL; + tChild = tChild->NextSiblingElement() ) + { + try + { + mitk::DICOMTag tag = tagFromXMLElement(tChild); + int i(5); + if (tChild->QueryIntAttribute("cutDecimalPlaces", &i) == TIXML_SUCCESS) + { + tagSorter->AddDistinguishingTag( tag, new mitk::DICOMTagBasedSorter::CutDecimalPlaces(i) ); + } + else + { + tagSorter->AddDistinguishingTag( tag ); + } + } + catch(...) + { + return NULL; + } + } + } + + // "sorting tags" + TiXmlElement* sElement = element->FirstChildElement("Sorting"); + if (sElement) + { + 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() ); + } + + return tagSorter; +} + +std::string +mitk::DICOMReaderConfigurator +::requiredStringAttribute(TiXmlElement* xmlElement, const std::string& key) const +{ + 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) const +{ + 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) const +{ + 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) const +{ + mitk::DICOMTag tag = tagFromXMLElement(xmlElement); + return DICOMSortByTag::New(tag, secondaryCriterion).GetPointer(); +} + +mitk::DICOMSortCriterion::Pointer +mitk::DICOMReaderConfigurator +::CreateSortByImagePositionPatient(TiXmlElement*, DICOMSortCriterion::Pointer secondaryCriterion) const +{ + return SortByImagePositionPatient::New(secondaryCriterion).GetPointer(); +} + + + +std::string +mitk::DICOMReaderConfigurator +::CreateConfigStringFromReader(DICOMFileReader::ConstPointer reader) const +{ + // check possible sub-classes from the most-specific one up to the most generic one + const DICOMFileReader* cPointer = reader; + TiXmlElement* root; + if (const ClassicDICOMSeriesReader* specificReader = dynamic_cast(cPointer)) + { + root = this->CreateConfigStringFromReader(specificReader); + } + else + if (const ThreeDnTDICOMSeriesReader* specificReader = dynamic_cast(cPointer)) + { + root = this->CreateConfigStringFromReader(specificReader); + } + else + if (const DICOMITKSeriesGDCMReader* specificReader = dynamic_cast(cPointer)) + { + root = this->CreateConfigStringFromReader(specificReader); + } + else + { + MITK_WARN << "Unknown reader class passed to DICOMReaderConfigurator::CreateConfigStringFromReader(). Cannot serialize."; + return ""; // no serialization, what a pity + } + + if (root) + { + TiXmlDocument document; + document.LinkEndChild( root ); + + TiXmlPrinter printer; + printer.SetIndent( " " ); + + document.Accept( &printer ); + std::string xmltext = printer.CStr(); + return xmltext; + } + else + { + MITK_WARN << "DICOMReaderConfigurator::CreateConfigStringFromReader() created empty serialization. Problem?"; + return ""; + } +} + +TiXmlElement* +mitk::DICOMReaderConfigurator +::CreateConfigStringFromReader(const DICOMITKSeriesGDCMReader* reader) const +{ + TiXmlElement* root = this->CreateDICOMFileReaderTag(reader); + assert(root); + + root->SetAttribute("fixTiltByShearing", toString(reader->GetFixTiltByShearing())); + root->SetAttribute("acceptTwoSlicesGroups", toString(reader->GetAcceptTwoSlicesGroups())); + root->SetDoubleAttribute("toleratedOriginError", reader->GetToleratedOriginError()); + root->SetAttribute("toleratedOriginErrorIsAbsolute", toString(reader->IsToleratedOriginOffsetAbsolute())); + root->SetDoubleAttribute("decimalPlacesForOrientation", reader->GetDecimalPlacesForOrientation()); + + // iterate DICOMDatasetSorter objects + DICOMITKSeriesGDCMReader::ConstSorterList sorterList = reader->GetFreelyConfiguredSortingElements(); + for(DICOMITKSeriesGDCMReader::ConstSorterList::const_iterator sorterIter = sorterList.begin(); + sorterIter != sorterList.end(); + ++sorterIter) + { + const DICOMDatasetSorter* sorter = *sorterIter; + if (const DICOMTagBasedSorter* specificSorter = dynamic_cast(sorter)) + { + TiXmlElement* sorterTag = this->CreateConfigStringFromDICOMDatasetSorter(specificSorter); + root->LinkEndChild(sorterTag); + } + else + { + MITK_WARN << "Unknown DICOMDatasetSorter class passed to DICOMReaderConfigurator::CreateConfigStringFromReader(). Cannot serialize."; + return NULL; + } + } + + return root; +} + +TiXmlElement* +mitk::DICOMReaderConfigurator +::CreateConfigStringFromDICOMDatasetSorter(const DICOMTagBasedSorter* sorter) const +{ + assert(sorter); + + TiXmlElement* sorterTag = new TiXmlElement("DICOMDatasetSorter"); + sorterTag->SetAttribute("class", sorter->GetNameOfClass()); + sorterTag->SetAttribute("strictSorting", toString(sorter->GetStrictSorting())); + + TiXmlElement* distinguishingTagsElement = new TiXmlElement("Distinguishing"); + sorterTag->LinkEndChild(distinguishingTagsElement); + mitk::DICOMTagList distinguishingTags = sorter->GetDistinguishingTags(); + for (DICOMTagList::iterator tagIter = distinguishingTags.begin(); + tagIter != distinguishingTags.end(); + ++tagIter) + { + TiXmlElement* tag = this->CreateConfigStringFromDICOMTag(*tagIter); + distinguishingTagsElement->LinkEndChild(tag); + + const DICOMTagBasedSorter::TagValueProcessor* processor = sorter->GetTagValueProcessorForDistinguishingTag(*tagIter); + if (const DICOMTagBasedSorter::CutDecimalPlaces* specificProcessor = dynamic_cast(processor)) + { + tag->SetDoubleAttribute("cutDecimalPlaces", specificProcessor->GetPrecision()); + } + } + + TiXmlElement* sortingElement = new TiXmlElement("Sorting"); + sorterTag->LinkEndChild(sortingElement); + mitk::DICOMSortCriterion::ConstPointer sortCriterion = sorter->GetSortCriterion(); + while (sortCriterion.IsNotNull()) + { + std::string classname = sortCriterion->GetNameOfClass(); + if (classname == "SortByImagePositionPatient") + { + sortingElement->LinkEndChild( new TiXmlElement("ImagePositionPatient") ); // no parameters + } + else + if (classname == "DICOMSortByTag") + { + DICOMTagList pseudoTagList = sortCriterion->GetTagsOfInterest(); + if (pseudoTagList.size() == 1) + { + DICOMTag firstTag = pseudoTagList.front(); + + TiXmlElement* tagElement = this->CreateConfigStringFromDICOMTag(firstTag); + + sortingElement->LinkEndChild( tagElement ); + } + else + { + MITK_ERROR << "Encountered SortByTag class with MULTIPLE tag in CreateConfigStringFromDICOMDatasetSorter. Cannot serialize."; + return NULL; + } + } + else + { + MITK_ERROR << "Encountered unknown class '" << classname << "' in CreateConfigStringFromDICOMDatasetSorter. Cannot serialize."; + return NULL; + } + + sortCriterion = sortCriterion->GetSecondaryCriterion(); + } + + return sorterTag; +} + +TiXmlElement* +mitk::DICOMReaderConfigurator +::CreateConfigStringFromDICOMTag(const DICOMTag& tag) const +{ + TiXmlElement* tagElement = new TiXmlElement("Tag"); // name group element + tagElement->SetAttribute("name", tag.GetName().c_str()); + tagElement->SetAttribute("group", toHexString(tag.GetGroup())); + tagElement->SetAttribute("element", toHexString(tag.GetElement())); + return tagElement; +} + +std::string +mitk::DICOMReaderConfigurator +::toHexString(unsigned int i) const +{ + std::stringstream ss; + ss << "0x" << std::setfill ('0') << std::setw(4) << std::hex << i; + return ss.str(); +} + + +TiXmlElement* +mitk::DICOMReaderConfigurator +::CreateConfigStringFromReader(const ThreeDnTDICOMSeriesReader* reader) const +{ + TiXmlElement* root = this->CreateConfigStringFromReader(static_cast(reader)); + assert(root); + + root->SetAttribute("group3DnT", toString(reader->GetGroup3DandT())); + + return root; +} + +const char* +mitk::DICOMReaderConfigurator +::toString(bool b) const +{ + return b ? "true" : "false"; +} + +TiXmlElement* +mitk::DICOMReaderConfigurator +::CreateConfigStringFromReader(const ClassicDICOMSeriesReader* reader) const +{ + return this->CreateDICOMFileReaderTag(reader); +} + +TiXmlElement* +mitk::DICOMReaderConfigurator +::CreateDICOMFileReaderTag(const DICOMFileReader* reader) const +{ + TiXmlElement* readerTag = new TiXmlElement("DICOMFileReader"); + readerTag->SetAttribute("class", reader->GetNameOfClass()); + readerTag->SetAttribute("label", reader->GetConfigurationLabel().c_str()); + readerTag->SetAttribute("description", reader->GetConfigurationDescription().c_str()); + readerTag->SetAttribute("version", "1"); + + return readerTag; +} diff --git a/Modules/DICOMReader/mitkDICOMReaderConfigurator.h b/Modules/DICOMReader/mitkDICOMReaderConfigurator.h new file mode 100644 index 0000000000..83c1e213bc --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMReaderConfigurator.h @@ -0,0 +1,143 @@ +/*=================================================================== + +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 "mitkClassicDICOMSeriesReader.h" +#include "mitkDICOMTagBasedSorter.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 exadecimal 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). + + \section DICOMReaderConfigurator_AboutTheFuture About the future evolution of this class + + This first version is hard coded for the current state of the implementation. + + If things should evolve in a way that needs us to splitt off readers for "old" versions, + time should be taken to refactor this class. + + Basically, a serializer class should accompany each of the configurable classes. Such + serializer classes should be registered and discovered via micro-services (to support extensions). + A serializer should offer both methods to serialize a class and to desirialize it again. + + A "version" attribute at the top-level tag should be used to distinguish versions. + + Usually it should be enough to keep DE-serializers for all versions. Writers for the most + recent version should be enough. +*/ +class DICOMReader_EXPORT DICOMReaderConfigurator : public itk::LightObject +{ + public: + + mitkClassMacro( DICOMReaderConfigurator, itk::LightObject ) + itkNewMacro( DICOMReaderConfigurator ) + + DICOMFileReader::Pointer CreateFromConfigFile(const std::string& filename) const; + DICOMFileReader::Pointer CreateFromUTF8ConfigString(const std::string& xmlContents) const; + + std::string CreateConfigStringFromReader(DICOMFileReader::ConstPointer reader) const; + + protected: + + DICOMReaderConfigurator(); + virtual ~DICOMReaderConfigurator(); + + private: + + DICOMFileReader::Pointer CreateFromTiXmlDocument(TiXmlDocument& doc) const; + DICOMTag tagFromXMLElement(TiXmlElement*) const; + std::string requiredStringAttribute(TiXmlElement* xmlElement, const std::string& key) const; + unsigned int hexStringToUInt(const std::string& s) const; + + ThreeDnTDICOMSeriesReader::Pointer ConfigureThreeDnTDICOMSeriesReader(ThreeDnTDICOMSeriesReader::Pointer reader, TiXmlElement*) const; + DICOMITKSeriesGDCMReader::Pointer ConfigureDICOMITKSeriesGDCMReader(DICOMITKSeriesGDCMReader::Pointer reader, TiXmlElement*) const; + void ConfigureCommonPropertiesOfDICOMITKSeriesGDCMReader(DICOMITKSeriesGDCMReader::Pointer reader, TiXmlElement* element) const; + void ConfigureCommonPropertiesOfThreeDnTDICOMSeriesReader(ThreeDnTDICOMSeriesReader::Pointer reader, TiXmlElement* element) const; + + DICOMSortCriterion::Pointer CreateDICOMSortByTag(TiXmlElement* xmlElement, DICOMSortCriterion::Pointer secondaryCriterion) const; + DICOMSortCriterion::Pointer CreateSortByImagePositionPatient(TiXmlElement* xmlElement, DICOMSortCriterion::Pointer secondaryCriterion) const; + + mitk::DICOMTagBasedSorter::Pointer CreateDICOMTagBasedSorter(TiXmlElement* element) const; + + TiXmlElement* CreateConfigStringFromReader(const DICOMITKSeriesGDCMReader* reader) const; + TiXmlElement* CreateConfigStringFromReader(const ThreeDnTDICOMSeriesReader* reader) const; + TiXmlElement* CreateConfigStringFromReader(const ClassicDICOMSeriesReader* reader) const; + + TiXmlElement* CreateConfigStringFromDICOMDatasetSorter(const DICOMTagBasedSorter* sorter) const; + + TiXmlElement* CreateConfigStringFromDICOMTag(const DICOMTag& tag) const; + + TiXmlElement* CreateDICOMFileReaderTag(const DICOMFileReader* reader) const; + const char* toString(bool) const; + std::string toHexString(unsigned int i) const; + }; + +} // namespace + +#endif // mitkDICOMReaderConfigurator_h diff --git a/Modules/DICOMReader/mitkDICOMSortByTag.cpp b/Modules/DICOMReader/mitkDICOMSortByTag.cpp new file mode 100644 index 0000000000..1f56d826d1 --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMSortByTag.cpp @@ -0,0 +1,175 @@ +/*=================================================================== + +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; +} + +bool +mitk::DICOMSortByTag +::operator==(const DICOMSortCriterion& other) const +{ + if (const DICOMSortByTag* otherSelf = dynamic_cast(&other)) + { + if (!(this->m_Tag == otherSelf->m_Tag)) return false; + + if (this->m_SecondaryCriterion.IsNull() && otherSelf->m_SecondaryCriterion.IsNull()) return true; + + if (this->m_SecondaryCriterion.IsNull() || otherSelf->m_SecondaryCriterion.IsNull()) return false; + + return *(this->m_SecondaryCriterion) == *(otherSelf->m_SecondaryCriterion); + } + else + { + return false; + } +} + +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) + && lefti.eof() && righti.eof() ) + { + 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 + } +} + +double +mitk::DICOMSortByTag +::NumericDistance(const mitk::DICOMDatasetAccess* from, const mitk::DICOMDatasetAccess* to) const +{ + assert(from); + assert(to); + + std::string fromString = from->GetTagValueAsString(m_Tag); + std::string toString = to->GetTagValueAsString(m_Tag); + + std::istringstream fromi(fromString); + std::istringstream toi(toString); + + double fromDouble(0); + double toDouble(0); + + if ( (fromi >> fromDouble) && (toi >> toDouble) + && fromi.eof() && toi.eof() ) + { + return toDouble - fromDouble; + } + else + { + return 0; + } + + // TODO second-level compare? +} diff --git a/Modules/DICOMReader/mitkDICOMSortByTag.h b/Modules/DICOMReader/mitkDICOMSortByTag.h new file mode 100644 index 0000000000..27784420ba --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMSortByTag.h @@ -0,0 +1,72 @@ +/*=================================================================== + +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 double NumericDistance(const mitk::DICOMDatasetAccess* from, const mitk::DICOMDatasetAccess* to) const; + + virtual void Print(std::ostream& os) const; + + virtual bool operator==(const DICOMSortCriterion& other) 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 new file mode 100644 index 0000000000..7086f4305b --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMSortCriterion.cpp @@ -0,0 +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 new file mode 100644 index 0000000000..0364a44b47 --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMSortCriterion.h @@ -0,0 +1,82 @@ +/*=================================================================== + +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 Calculate a distance between two datasets. + /// This ansers the question of consecutive datasets. + virtual double NumericDistance(const mitk::DICOMDatasetAccess* from, const mitk::DICOMDatasetAccess* to) 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; + + virtual bool operator==(const DICOMSortCriterion& other) 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 new file mode 100644 index 0000000000..1fad5a5653 --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMTag.cpp @@ -0,0 +1,211 @@ +/*=================================================================== + +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 +{ + return + ( this->m_Group < other.m_Group ) + || + ( ( this->m_Group == other.m_Group ) + && + ( this->m_Element < 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 new file mode 100644 index 0000000000..d693a96d24 --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMTag.h @@ -0,0 +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; + + /// 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 new file mode 100644 index 0000000000..185814d2e6 --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMTagBasedSorter.cpp @@ -0,0 +1,540 @@ +/*=================================================================== +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 +#include + +mitk::DICOMTagBasedSorter::CutDecimalPlaces +::CutDecimalPlaces(unsigned int precision) +:m_Precision(precision) +{ +} + +mitk::DICOMTagBasedSorter::CutDecimalPlaces +::CutDecimalPlaces(const CutDecimalPlaces& other) +:m_Precision(other.m_Precision) +{ +} + +std::string +mitk::DICOMTagBasedSorter::CutDecimalPlaces +::operator()(const std::string& input) const +{ + // 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) + // iterate all fields, convert each to a number, cut this number as configured, then return a concatenated string with all cut-off numbers + static std::ostringstream resultString; + resultString.str(std::string()); + resultString.clear(); + resultString.setf(std::ios::fixed, std::ios::floatfield); + resultString.precision(m_Precision); + + static std::stringstream ss(input); + ss.str(input); + ss.clear(); + static std::string item; + while (std::getline(ss, item, '\\')) + { + static std::istringstream converter(item); + converter.str(item); + converter.clear(); + static double number(0); + if (converter >> number && converter.eof()) + { + // converted to double + resultString << number; + } + else + { + // did not convert to double + resultString << item; // just paste the unmodified string + } + + if (!ss.eof()) + { + resultString << "\\"; + } + } + + return resultString.str(); +} + +mitk::DICOMTagBasedSorter::TagValueProcessor* +mitk::DICOMTagBasedSorter::CutDecimalPlaces +::Clone() const +{ + return new CutDecimalPlaces(*this); +} + + +unsigned int +mitk::DICOMTagBasedSorter::CutDecimalPlaces +::GetPrecision() const +{ + return m_Precision; +} + +mitk::DICOMTagBasedSorter +::DICOMTagBasedSorter() +:DICOMDatasetSorter() +,m_StrictSorting(true) +{ +} + +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) +,m_DistinguishingTags( other.m_DistinguishingTags ) +,m_SortCriterion( other.m_SortCriterion ) +,m_StrictSorting( other.m_StrictSorting ) +{ + for(TagValueProcessorMap::const_iterator ti = other.m_TagValueProcessor.begin(); + ti != other.m_TagValueProcessor.end(); + ++ti) + { + m_TagValueProcessor[ti->first] = ti->second->Clone(); + } +} + +mitk::DICOMTagBasedSorter& +mitk::DICOMTagBasedSorter +::operator=(const DICOMTagBasedSorter& other) +{ + if (this != &other) + { + DICOMDatasetSorter::operator=(other); + m_DistinguishingTags = other.m_DistinguishingTags; + m_SortCriterion = other.m_SortCriterion; + m_StrictSorting = other.m_StrictSorting; + + for(TagValueProcessorMap::const_iterator ti = other.m_TagValueProcessor.begin(); + ti != other.m_TagValueProcessor.end(); + ++ti) + { + m_TagValueProcessor[ti->first] = ti->second->Clone(); + } + } + return *this; +} + +bool +mitk::DICOMTagBasedSorter +::operator==(const DICOMDatasetSorter& other) const +{ + if (const DICOMTagBasedSorter* otherSelf = dynamic_cast(&other)) + { + if (this->m_StrictSorting != otherSelf->m_StrictSorting) return false; + + bool allTagsPresentAndEqual(true); + if (this->m_DistinguishingTags.size() != otherSelf->m_DistinguishingTags.size()) + return false; + + for (DICOMTagList::const_iterator myTag = this->m_DistinguishingTags.begin(); + myTag != this->m_DistinguishingTags.end(); + ++myTag) + { + allTagsPresentAndEqual &= (std::find( otherSelf->m_DistinguishingTags.begin(), otherSelf->m_DistinguishingTags.end(), *myTag ) + != otherSelf->m_DistinguishingTags.end()); // other contains this tags + // since size is equal, we don't need to check the inverse + } + + if (!allTagsPresentAndEqual) return false; + + if (this->m_SortCriterion.IsNotNull() && otherSelf->m_SortCriterion.IsNotNull()) + { + return *(this->m_SortCriterion) == *(otherSelf->m_SortCriterion); + } + else + { + return this->m_SortCriterion.IsNull() && otherSelf->m_SortCriterion.IsNull(); + } + } + else + { + return false; + } +} + +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(); + } +} + +void +mitk::DICOMTagBasedSorter +::SetStrictSorting(bool strict) +{ + m_StrictSorting = strict; +} + +bool +mitk::DICOMTagBasedSorter +::GetStrictSorting() const +{ + return m_StrictSorting; +} + +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; +} + +mitk::DICOMTagList +mitk::DICOMTagBasedSorter +::GetDistinguishingTags() const +{ + return m_DistinguishingTags; +} + +const mitk::DICOMTagBasedSorter::TagValueProcessor* +mitk::DICOMTagBasedSorter +::GetTagValueProcessorForDistinguishingTag(const DICOMTag& tag) const +{ + TagValueProcessorMap::const_iterator loc = m_TagValueProcessor.find(tag); + if (loc != m_TagValueProcessor.end()) + { + return loc->second; + } + else + { + return NULL; + } +} + +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; +} + +mitk::DICOMSortCriterion::ConstPointer +mitk::DICOMTagBasedSorter +::GetSortCriterion() const +{ + return m_SortCriterion; +} + +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()) + { + /* + Three steps here: + 1. sort within each group + - this may result in orders such as 1 2 3 4 6 7 8 10 12 13 14 + 2. create new groups by enforcing consecutive order within each group + - resorts above example like 1 2 3 4 ; 6 7 8 ; 10 ; 12 13 14 + 3. sort all of the groups (not WITHIN each group) by their first frame + - if earlier "distinguish" steps created groups like 6 7 8 ; 1 2 3 4 ; 10, + then this step would sort them like 1 2 3 4 ; 6 7 8 ; 10 + */ + + // Step 1: sort within the groups + // 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 << " --------------------------------------------------------------------------------"; + } + + GroupIDToListType consecutiveGroups; + if (m_StrictSorting) + { + // Step 2: create new groups by enforcing consecutive order within each group + unsigned int groupIndex(0); + for (GroupIDToListType::iterator gIter = groups.begin(); + gIter != groups.end(); + ++gIter) + { + std::stringstream groupKey; + groupKey << std::setfill('0') << std::setw(6) << groupIndex++; + + DICOMDatasetList& dsList = gIter->second; + DICOMDatasetAccess* previousDS(NULL); + unsigned int dsIndex(0); + double constantDistance(0.0); + bool constantDistanceInitialized(false); + for (DICOMDatasetList::iterator dataset = dsList.begin(); + dataset != dsList.end(); + ++dsIndex, ++dataset) + { + if (dsIndex >0) // ignore the first dataset, we cannot check any distances yet.. + { + // for the second and every following dataset: + // let the sorting criterion calculate a "distance" + // if the distance is not 1, split off a new group! + double currentDistance = m_SortCriterion->NumericDistance(previousDS, *dataset); + if (constantDistanceInitialized) + { + if (fabs(currentDistance - constantDistance) < constantDistance * 0.01) // ok, deviation of up to 1% of distance is tolerated + { + // nothing to do, just ok + } + else if (currentDistance < mitk::eps) // close enough to 0 + { + // no numeric comparison possible? + // rare case(?), just accept + } + else + { + // split! this is done by simply creating a new group (key) + groupKey.str(std::string()); + groupKey.clear(); + groupKey << std::setfill('0') << std::setw(6) << groupIndex++; + } + } + else + { + // second slice: learn about the expected distance! + + // heuristic: if distance is an integer, we check for a special case: + // if the distance is integer and not 1/-1, then we assume + // a missing slice right after the first slice + // ==> split off slices + // in all other cases: second dataset at this position, no need to split already, we are still learning about the images + if ((currentDistance - (int)currentDistance == 0.0) && fabs(currentDistance) != 1.0) + // exact comparison. An integer should not be expressed as 1.000000000000000000000000001! + { + groupKey.str(std::string()); + groupKey.clear(); + groupKey << std::setfill('0') << std::setw(6) << groupIndex++; + } + + constantDistance = currentDistance; + constantDistanceInitialized = true; + } + } + consecutiveGroups[groupKey.str()].push_back(*dataset); + previousDS = *dataset; + } + } + } + else + { + consecutiveGroups = groups; + } + + // Step 3: sort all of the groups (not WITHIN each group) by their first frame + /* + build a list-1 of datasets with the first dataset one of each group + sort this list-1 + build a new result list-2: + - iterate list-1, for each dataset + - find the group that contains this dataset + - add this group as the next element to list-2 + return list-2 as the sorted output + */ + DICOMDatasetList firstSlices; + for (GroupIDToListType::iterator gIter = consecutiveGroups.begin(); + gIter != consecutiveGroups.end(); + ++gIter) + { + assert(!gIter->second.empty()); + firstSlices.push_back(gIter->second.front()); + } + + std::sort( firstSlices.begin(), firstSlices.end(), ParameterizedDatasetSort( m_SortCriterion ) ); + + GroupIDToListType sortedResultBlocks; + unsigned int groupKeyValue(0); + for (DICOMDatasetList::iterator firstSlice = firstSlices.begin(); + firstSlice != firstSlices.end(); + ++firstSlice) + { + for (GroupIDToListType::iterator gIter = consecutiveGroups.begin(); + gIter != consecutiveGroups.end(); + ++groupKeyValue, ++gIter) + { + if (gIter->second.front() == *firstSlice) + { + std::stringstream groupKey; + groupKey << std::setfill('0') << std::setw(6) << groupKeyValue; // try more than 999,999 groups and you are doomed (your application already is) + sortedResultBlocks[groupKey.str()] = gIter->second; + } + } + } + + groups = sortedResultBlocks; + } + + 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 new file mode 100644 index 0000000000..603f2b51a9 --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMTagBasedSorter.h @@ -0,0 +1,165 @@ +/*=================================================================== + +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 +{ + +/** + \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 DICOMReader_EXPORT TagValueProcessor + { + public: + /// \brief Implements the "processing". + virtual std::string operator()(const std::string&) const = 0; + virtual TagValueProcessor* Clone() 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 DICOMReader_EXPORT CutDecimalPlaces : public TagValueProcessor + { + public: + CutDecimalPlaces(unsigned int precision); + CutDecimalPlaces(const CutDecimalPlaces& other); + unsigned int GetPrecision() const; + virtual std::string operator()(const std::string&) const; + virtual TagValueProcessor* Clone() 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 ); + DICOMTagList GetDistinguishingTags() const; + const TagValueProcessor* GetTagValueProcessorForDistinguishingTag(const DICOMTag&) const; + + /** + \brief Define the sorting criterion (which holds seconardy criteria) + */ + void SetSortCriterion( DICOMSortCriterion::ConstPointer criterion ); + DICOMSortCriterion::ConstPointer GetSortCriterion() const; + + /** + \brief A list of all the tags needed for processing (facilitates scanning). + */ + virtual DICOMTagList GetTagsOfInterest(); + + /** + \brief Whether or not groups should be checked for consecutive tag values. + */ + void SetStrictSorting(bool strict); + bool GetStrictSorting() const; + + /** + \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; + + virtual bool operator==(const DICOMDatasetSorter& other) 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); + 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; + + bool m_StrictSorting; +}; + +} + +#endif diff --git a/Modules/DICOMReader/mitkDICOMTagCache.cpp b/Modules/DICOMReader/mitkDICOMTagCache.cpp new file mode 100644 index 0000000000..ede0742826 --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMTagCache.cpp @@ -0,0 +1,31 @@ +/*=================================================================== + +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 "mitkDICOMTagCache.h" + +mitk::DICOMTagCache::DICOMTagCache() +:itk::Object() +{ +} + +mitk::DICOMTagCache::DICOMTagCache( const DICOMTagCache&) +:itk::Object() +{ +} + +mitk::DICOMTagCache::~DICOMTagCache() +{ +} diff --git a/Modules/DICOMReader/mitkDICOMTagCache.h b/Modules/DICOMReader/mitkDICOMTagCache.h new file mode 100644 index 0000000000..bd4a7802f7 --- /dev/null +++ b/Modules/DICOMReader/mitkDICOMTagCache.h @@ -0,0 +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 mitkDICOMTagCache_h +#define mitkDICOMTagCache_h + +#include "itkObjectFactory.h" +#include "mitkCommon.h" + +#include "mitkDICOMTag.h" + +#include "DICOMReaderExports.h" + +namespace mitk +{ + + class DICOMImageFrameInfo; + + /** + \ingroup DICOMReaderModule + \brief ... + */ + class DICOMReader_EXPORT DICOMTagCache : public itk::Object + { + public: + + mitkClassMacro( DICOMTagCache, itk::Object ); + + virtual std::string GetTagValue(DICOMImageFrameInfo* frame, const DICOMTag& tag) const = 0; + + protected: + + DICOMTagCache(); + DICOMTagCache(const DICOMTagCache&); + virtual ~DICOMTagCache(); + }; +} + +#endif diff --git a/Modules/DICOMReader/mitkEquiDistantBlocksSorter.cpp b/Modules/DICOMReader/mitkEquiDistantBlocksSorter.cpp new file mode 100644 index 0000000000..c840004922 --- /dev/null +++ b/Modules/DICOMReader/mitkEquiDistantBlocksSorter.cpp @@ -0,0 +1,571 @@ +/*=================================================================== 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 +::SetLastFilenameOfBlock(const std::string& filename) +{ + m_LastFilenameOfBlock = filename; +} + +std::string +mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult +::GetLastFilenameOfBlock() const +{ + return m_LastFilenameOfBlock; +} + + +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) +,m_ToleratedOriginOffset(0.3) +,m_ToleratedOriginOffsetIsAbsolute(false) +,m_AcceptTwoSlicesGroups(true) +{ +} + +mitk::EquiDistantBlocksSorter +::EquiDistantBlocksSorter(const EquiDistantBlocksSorter& other ) +:DICOMDatasetSorter(other) +,m_AcceptTilt(other.m_AcceptTilt) +,m_ToleratedOriginOffset(other.m_ToleratedOriginOffset) +,m_ToleratedOriginOffsetIsAbsolute(other.m_ToleratedOriginOffsetIsAbsolute) +,m_AcceptTwoSlicesGroups(other.m_AcceptTwoSlicesGroups) +{ +} + +mitk::EquiDistantBlocksSorter +::~EquiDistantBlocksSorter() +{ +} + +bool +mitk::EquiDistantBlocksSorter +::operator==(const DICOMDatasetSorter& other) const +{ + if (const EquiDistantBlocksSorter* otherSelf = dynamic_cast(&other)) + { + return this->m_AcceptTilt == otherSelf->m_AcceptTilt + && this->m_ToleratedOriginOffsetIsAbsolute == otherSelf->m_ToleratedOriginOffsetIsAbsolute + && this->m_AcceptTwoSlicesGroups == otherSelf->m_AcceptTwoSlicesGroups + && (fabs(this->m_ToleratedOriginOffset - otherSelf->m_ToleratedOriginOffset) < eps); + } + else + { + return false; + } +} + +void +mitk::EquiDistantBlocksSorter +::PrintConfiguration(std::ostream& os, const std::string& indent) const +{ + std::stringstream ts; + if (!m_ToleratedOriginOffsetIsAbsolute) + { + ts << "adaptive"; + } + else + { + ts << m_ToleratedOriginOffset << "mm"; + } + + os << indent << "Sort into blocks of equidistant, well-aligned (tolerance " + << ts.str() << ") slices " + << (m_AcceptTilt ? "(accepting a gantry tilt)" : "") + << std::endl; +} + + +void +mitk::EquiDistantBlocksSorter +::SetAcceptTilt(bool accept) +{ + m_AcceptTilt = accept; +} + + +bool +mitk::EquiDistantBlocksSorter +::GetAcceptTilt() const +{ + return m_AcceptTilt; +} + +void +mitk::EquiDistantBlocksSorter +::SetAcceptTwoSlicesGroups(bool accept) +{ + m_AcceptTwoSlicesGroups = accept; +} + +bool +mitk::EquiDistantBlocksSorter +::GetAcceptTwoSlicesGroups() const +{ + return m_AcceptTwoSlicesGroups; +} + + +mitk::EquiDistantBlocksSorter& +mitk::EquiDistantBlocksSorter +::operator=(const EquiDistantBlocksSorter& other) +{ + if (this != &other) + { + DICOMDatasetSorter::operator=(other); + m_AcceptTilt = other.m_AcceptTilt; + m_ToleratedOriginOffset = other.m_ToleratedOriginOffset; + m_ToleratedOriginOffsetIsAbsolute = other.m_ToleratedOriginOffsetIsAbsolute; + m_AcceptTwoSlicesGroups = other.m_AcceptTwoSlicesGroups; + } + 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) ); // GantryDetectorTilt + + 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); + } +} + +void +mitk::EquiDistantBlocksSorter +::SetToleratedOriginOffsetToAdaptive(double fractionOfInterSliceDistance) +{ + m_ToleratedOriginOffset = fractionOfInterSliceDistance; + m_ToleratedOriginOffsetIsAbsolute = false; + + if (m_ToleratedOriginOffset < 0.0) + { + MITK_WARN << "Call SetToleratedOriginOffsetToAdaptive() only with positive numbers between 0.0 and 1.0, read documentation!"; + } + + if (m_ToleratedOriginOffset > 0.5) + { + MITK_WARN << "EquiDistantBlocksSorter is now accepting large errors, take care of measurements, they could appear at unprecise locations!"; + } +} + +void +mitk::EquiDistantBlocksSorter +::SetToleratedOriginOffset(double millimeters) +{ + m_ToleratedOriginOffset = millimeters; + m_ToleratedOriginOffsetIsAbsolute = true; + if (m_ToleratedOriginOffset < 0.0) + { + MITK_WARN << "Negative tolerance set to SetToleratedOriginOffset()!"; + } +} + +double +mitk::EquiDistantBlocksSorter +::GetToleratedOriginOffset() const +{ + return m_ToleratedOriginOffset; +} + +bool +mitk::EquiDistantBlocksSorter +::IsToleratedOriginOffsetAbsolute() const +{ + return m_ToleratedOriginOffsetIsAbsolute; +} + + +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 + + 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; + + // classic mode without tolerance! + if (!m_ToleratedOriginOffsetIsAbsolute) + { + 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) + } + else + { + toleratedOriginError = m_ToleratedOriginOffset; + } + 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() ) + { + /* 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() ) + { + assert(!datasets.empty()); + + result.FlagGantryTilt(tiltInfo); + result.AddFileToSortedBlock( *dsIter ); // this file is good for current block + result.SetFirstFilenameOfBlock( datasets.front()->GetFilenameIfAvailable() ); + result.SetLastFilenameOfBlock( datasets.back()->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 + + // Above behavior can be configured via m_AcceptTwoSlicesGroups, the default being "do accept" + if ( result.GetBlockDatasets().size() == 2 && !m_AcceptTwoSlicesGroups ) + { + 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 ) + { + try + { + DICOMDatasetList datasets = result.GetBlockDatasets(); + DICOMDatasetAccess* firstDataset = datasets.front(); + DICOMDatasetAccess* lastDataset = datasets.back(); + unsigned int numberOfSlicesApart = datasets.size() - 1; + + std::string orientationString = firstDataset->GetTagValueAsString( tagImageOrientation ); + std::string firstOriginString = firstDataset->GetTagValueAsString( tagImagePositionPatient ); + std::string lastOriginString = lastDataset->GetTagValueAsString( tagImagePositionPatient ); + + result.FlagGantryTilt( GantryTiltInformation::MakeFromTagValues( firstOriginString, lastOriginString, orientationString, numberOfSlicesApart )); + } + catch (...) + { + // just do not flag anything, we are ok + } + } + + return result; +} diff --git a/Modules/DICOMReader/mitkEquiDistantBlocksSorter.h b/Modules/DICOMReader/mitkEquiDistantBlocksSorter.h new file mode 100644 index 0000000000..a756eafb39 --- /dev/null +++ b/Modules/DICOMReader/mitkEquiDistantBlocksSorter.h @@ -0,0 +1,214 @@ +/*=================================================================== + +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" + +namespace mitk +{ + +/** + \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. + + During sorting, the origins (documented in tag image position patient) are compared + against expected origins (from former origin plus moving direction). As there will + be minor differences in numbers (from both calculations and unprecise tag values), + we must be a bit tolerant here. The default behavior is to expect that an origin is + not further away from the expected position than 30% of the inter-slice distance. + To support a legacy behavior of a former loader (DicomSeriesReader), this default can + be restricted to a constant number of millimeters by calling SetToleratedOriginOffset(mm). + + Detailed implementation in AnalyzeFileForITKImageSeriesReaderSpacingAssumption(). + +*/ +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); + bool GetAcceptTilt() const; + + /** + \brief See class description and SetToleratedOriginOffset(). + */ + void SetToleratedOriginOffsetToAdaptive(double fractionOfInterSliceDistanct = 0.3); + /** + \brief See class description and SetToleratedOriginOffsetToAdaptive(). + + Default value of 0.005 is calculated so that we get a maximum of 1/10mm + error when having a measurement crosses 20 slices in z direction (too strict? we don't know better..). + */ + void SetToleratedOriginOffset(double millimeters = 0.005); + + double GetToleratedOriginOffset() const; + bool IsToleratedOriginOffsetAbsolute() const; + + void SetAcceptTwoSlicesGroups(bool accept); + bool GetAcceptTwoSlicesGroups() const; + + virtual void PrintConfiguration(std::ostream& os, const std::string& indent = "") const; + + virtual bool operator==(const DICOMDatasetSorter& other) const; + + protected: + + /** + \brief Return type of 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; + void SetLastFilenameOfBlock(const std::string& filename); + std::string GetLastFilenameOfBlock() 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; + std::string m_LastFilenameOfBlock; + }; + + /** + \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; + + double m_ToleratedOriginOffset; + bool m_ToleratedOriginOffsetIsAbsolute; + + bool m_AcceptTwoSlicesGroups; +}; + +} + +#endif diff --git a/Modules/DICOMReader/mitkGantryTiltInformation.cpp b/Modules/DICOMReader/mitkGantryTiltInformation.cpp new file mode 100644 index 0000000000..d4b1dea4f0 --- /dev/null +++ b/Modules/DICOMReader/mitkGantryTiltInformation.cpp @@ -0,0 +1,258 @@ +/*=================================================================== + +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 "mitkGantryTiltInformation.h" + +#include "mitkDICOMTag.h" + +#include "mitkLogMacros.h" + +mitk::GantryTiltInformation::GantryTiltInformation() +: m_ShiftUp(0.0) +, m_ShiftRight(0.0) +, m_ShiftNormal(0.0) +, m_ITKAssumedSliceSpacing(0.0) +, m_NumberOfSlicesApart(0) +{ +} + + +#define doublepoint(x) \ + Point3Dd x; \ + x[0] = x ## f[0]; \ + x[1] = x ## f[1]; \ + x[2] = x ## f[2]; + + +#define doublevector(x) \ + Vector3Dd x; \ + x[0] = x ## f[0]; \ + x[1] = x ## f[1]; \ + x[2] = x ## f[2]; + +mitk::GantryTiltInformation::GantryTiltInformation( + const Point3D& origin1f, const Point3D& origin2f, + const Vector3D& rightf, const Vector3D& upf, + unsigned int numberOfSlicesApart) +: m_ShiftUp(0.0) +, m_ShiftRight(0.0) +, m_ShiftNormal(0.0) +, m_NumberOfSlicesApart(numberOfSlicesApart) +{ + assert(numberOfSlicesApart); + + doublepoint(origin1); + doublepoint(origin2); + doublevector(right); + doublevector(up); + + // determine if slice 1 (imagePosition1 and imageOrientation1) and slice 2 can be in one orthogonal slice stack: + // calculate a line from origin 1, directed along the normal of slice (calculated as the cross product of orientation 1) + // check if this line passes through origin 2 + + /* + Determine if line (imagePosition2 + l * normal) contains imagePosition1. + Done by calculating the distance of imagePosition1 from line (imagePosition2 + l *normal) + + E.g. http://mathworld.wolfram.com/Point-LineDistance3-Dimensional.html + + squared distance = | (pointAlongNormal - origin2) x (origin2 - origin1) | ^ 2 + / + |pointAlongNormal - origin2| ^ 2 + + ( x meaning the cross product ) + */ + + Vector3Dd normal = itk::CrossProduct(right, up); + Point3Dd pointAlongNormal = origin2 + normal; + + double numerator = itk::CrossProduct( pointAlongNormal - origin2 , origin2 - origin1 ).GetSquaredNorm(); + double denominator = (pointAlongNormal - origin2).GetSquaredNorm(); + + double distance = sqrt(numerator / denominator); + + if ( distance > 0.001 ) // mitk::eps is too small; 1/1000 of a mm should be enough to detect tilt + { + MITK_DEBUG << " Series seems to contain a tilted (or sheared) geometry"; + MITK_DEBUG << " Distance of expected slice origin from actual slice origin: " << distance; + MITK_DEBUG << " ==> storing this shift for later analysis:"; + MITK_DEBUG << " v origin1: " << origin1; + MITK_DEBUG << " v origin2: " << origin2; + MITK_DEBUG << " v right: " << right; + MITK_DEBUG << " v up: " << up; + MITK_DEBUG << " v normal: " << normal; + + Point3Dd projectionRight = projectPointOnLine( origin1, origin2, right ); + Point3Dd projectionNormal = projectPointOnLine( origin1, origin2, normal ); + + m_ShiftRight = (projectionRight - origin2).GetNorm(); + m_ShiftNormal = (projectionNormal - origin2).GetNorm(); + + /* + now also check to which side the image is shifted. + + Calculation e.g. from + http://mathworld.wolfram.com/Point-PlaneDistance.html + */ + + Point3Dd testPoint = origin1; + Vector3Dd planeNormal = up; + + double signedDistance = ( + planeNormal[0] * testPoint[0] + + planeNormal[1] * testPoint[1] + + planeNormal[2] * testPoint[2] + - ( + planeNormal[0] * origin2[0] + + planeNormal[1] * origin2[1] + + planeNormal[2] * origin2[2] + ) + ) + / + sqrt( planeNormal[0] * planeNormal[0] + + planeNormal[1] * planeNormal[1] + + planeNormal[2] * planeNormal[2] + ); + + m_ShiftUp = signedDistance; + + m_ITKAssumedSliceSpacing = (origin2 - origin1).GetNorm(); + // How do we now this is assumed? See header documentation for ITK code references + //double itkAssumedSliceSpacing = sqrt( m_ShiftUp * m_ShiftUp + m_ShiftNormal * m_ShiftNormal ); + + MITK_DEBUG << " calculated from slices " << m_NumberOfSlicesApart << " slices apart"; + MITK_DEBUG << " shift normal: " << m_ShiftNormal; + MITK_DEBUG << " shift normal assumed by ITK: " << m_ITKAssumedSliceSpacing; + MITK_DEBUG << " shift up: " << m_ShiftUp; + MITK_DEBUG << " shift right: " << m_ShiftRight; + + MITK_DEBUG << " tilt angle (deg): " << atan( m_ShiftUp / m_ShiftNormal ) * 180.0 / 3.1415926535; + } +} + +mitk::GantryTiltInformation +mitk::GantryTiltInformation +::MakeFromTagValues( + const std::string& origin1String, + const std::string& origin2String, + const std::string orientationString, + unsigned int numberOfSlicesApart) +{ + 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 + bool orientationConversion(false); + DICOMStringToOrientationVectors( orientationString, right, up, orientationConversion ); + + if (orientationConversion + && !origin1String.empty() && !origin2String.empty() + ) + { + bool firstOriginConversion(false); + bool lastOriginConversion(false); + + Point3D firstOrigin = DICOMStringToPoint3D( origin1String, firstOriginConversion ); + Point3D lastOrigin = DICOMStringToPoint3D( origin2String, lastOriginConversion ); + + if (firstOriginConversion && lastOriginConversion) + { + return GantryTiltInformation( firstOrigin, lastOrigin, right, up, numberOfSlicesApart ); + } + } + + std::stringstream ss; + ss << "Invalid tag values when constructing tilt information from origin1 '" << origin1String + << "', origin2 '" << origin2String + << "', and orientation '" << orientationString << "'"; + + throw std::invalid_argument(ss.str()); +} + +void +mitk::GantryTiltInformation +::Print(std::ostream& os) const +{ + os << " calculated from slices " << m_NumberOfSlicesApart << " slices apart" << std::endl; + os << " shift normal: " << m_ShiftNormal << std::endl; + os << " shift normal assumed by ITK: " << m_ITKAssumedSliceSpacing << std::endl; + os << " shift up: " << m_ShiftUp << std::endl; + os << " shift right: " << m_ShiftRight << std::endl; + + os << " tilt angle (deg): " << atan( m_ShiftUp / m_ShiftNormal ) * 180.0 / 3.1415926535 << std::endl; +} + +mitk::Point3D +mitk::GantryTiltInformation::projectPointOnLine( Point3Dd p, Point3Dd lineOrigin, Vector3Dd lineDirection ) +{ + /** + See illustration at http://mo.mathematik.uni-stuttgart.de/inhalt/aussage/aussage472/ + + vector(lineOrigin,p) = normal * ( innerproduct((p - lineOrigin),normal) / squared-length(normal) ) + */ + + Vector3Dd lineOriginToP = p - lineOrigin; + double innerProduct = lineOriginToP * lineDirection; + + double factor = innerProduct / lineDirection.GetSquaredNorm(); + Point3Dd projection = lineOrigin + factor * lineDirection; + + return projection; +} + +double +mitk::GantryTiltInformation::GetTiltCorrectedAdditionalSize(unsigned int imageSizeZ) const +{ + return fabs(m_ShiftUp / static_cast(m_NumberOfSlicesApart) * static_cast(imageSizeZ-1)); +} + +double +mitk::GantryTiltInformation::GetTiltAngleInDegrees() const +{ + return atan( fabs(m_ShiftUp) / m_ShiftNormal ) * 180.0 / 3.1415926535; +} + +double +mitk::GantryTiltInformation::GetMatrixCoefficientForCorrectionInWorldCoordinates() const +{ + // so many mm need to be shifted per slice! + return m_ShiftUp / static_cast(m_NumberOfSlicesApart); +} + +double +mitk::GantryTiltInformation::GetRealZSpacing() const +{ + return m_ShiftNormal / static_cast(m_NumberOfSlicesApart); +} + + +bool +mitk::GantryTiltInformation::IsSheared() const +{ + return m_NumberOfSlicesApart && + ( fabs(m_ShiftRight) > 0.001 + || fabs(m_ShiftUp) > 0.001); +} + + +bool +mitk::GantryTiltInformation::IsRegularGantryTilt() const +{ + return m_NumberOfSlicesApart && + ( fabs(m_ShiftRight) < 0.001 + && fabs(m_ShiftUp) > 0.001); +} + diff --git a/Modules/DICOMReader/mitkGantryTiltInformation.h b/Modules/DICOMReader/mitkGantryTiltInformation.h new file mode 100644 index 0000000000..18b4312ac4 --- /dev/null +++ b/Modules/DICOMReader/mitkGantryTiltInformation.h @@ -0,0 +1,145 @@ +/*=================================================================== + +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 Factory method to create GantryTiltInformation from tag values (strings). + + Parameters as the regular c'tor. + */ + static + GantryTiltInformation + MakeFromTagValues( const std::string& origin1String, + const std::string& origin2String, + const std::string orientationString, + 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; + + 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/mitkITKDICOMSeriesReaderHelper.cpp b/Modules/DICOMReader/mitkITKDICOMSeriesReaderHelper.cpp new file mode 100644 index 0000000000..a227eb355d --- /dev/null +++ b/Modules/DICOMReader/mitkITKDICOMSeriesReaderHelper.cpp @@ -0,0 +1,219 @@ +/*=================================================================== + +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 "mitkITKDICOMSeriesReaderHelper.h" +#include "mitkITKDICOMSeriesReaderHelper.txx" + +#define switch3DCase(IOType, T) \ + case IOType: return LoadDICOMByITK< T >(filenames, correctTilt, tiltInfo, io); + +bool +mitk::ITKDICOMSeriesReaderHelper +::CanHandleFile(const std::string& filename) +{ + MITK_DEBUG << "ITKDICOMSeriesReaderHelper::CanHandleFile " << filename; + static itk::GDCMImageIO::Pointer tester = itk::GDCMImageIO::New(); + if ( tester->CanReadFile(filename.c_str()) ) + { + tester->SetFileName( filename.c_str() ); + tester->ReadImageInformation(); + + std::string numberOfFrames; + if (tester->GetValueFromTag("0028|0008", numberOfFrames)) + { + std::istringstream converter(numberOfFrames); + int i; + if (converter >> i) + { + MITK_DEBUG << "Number of Frames for " << filename << ": " << numberOfFrames; + return (i <= 1); // cannot handle multi-frame + } + else + { + return true; // we assume single-frame + } + } + else + { + MITK_DEBUG << "No Number of Frames tag for " << filename; + // friendly old single-frame file + return true; + } + } + + MITK_DEBUG << "GDCMImageIO found: No DICOM in " << filename; + // does not seem to be DICOM + return false; +} + +mitk::Image::Pointer +mitk::ITKDICOMSeriesReaderHelper +::Load( const StringContainer& filenames, bool correctTilt, const GantryTiltInformation& tiltInfo ) +{ + if( filenames.empty() ) + { + MITK_DEBUG << "Calling LoadDicomSeries with empty filename string container. Probably invalid application logic."; + return NULL; // this is not actually an error but the result is very simple + } + + typedef itk::GDCMImageIO DcmIoType; + DcmIoType::Pointer io = DcmIoType::New(); + + try + { + if (io->CanReadFile(filenames.front().c_str())) + { + io->SetFileName(filenames.front().c_str()); + io->ReadImageInformation(); + + if (io->GetPixelType() == itk::ImageIOBase::SCALAR) + { + switch (io->GetComponentType()) + { + switch3DCase(DcmIoType::UCHAR, unsigned char) + switch3DCase(DcmIoType::CHAR, char) + switch3DCase(DcmIoType::USHORT, unsigned short) + switch3DCase(DcmIoType::SHORT, short) + switch3DCase(DcmIoType::UINT, unsigned int) + switch3DCase(DcmIoType::INT, int) + switch3DCase(DcmIoType::ULONG, long unsigned int) + switch3DCase(DcmIoType::LONG, long int) + switch3DCase(DcmIoType::FLOAT, float) + switch3DCase(DcmIoType::DOUBLE, double) + default: + MITK_ERROR << "Found unsupported DICOM scalar pixel type: (enum value) " << io->GetComponentType(); + } + } + else if (io->GetPixelType() == itk::ImageIOBase::RGB) + { + switch (io->GetComponentType()) + { + switch3DCase(DcmIoType::UCHAR, itk::RGBPixel) + switch3DCase(DcmIoType::CHAR, itk::RGBPixel) + switch3DCase(DcmIoType::USHORT, itk::RGBPixel) + switch3DCase(DcmIoType::SHORT, itk::RGBPixel) + switch3DCase(DcmIoType::UINT, itk::RGBPixel) + switch3DCase(DcmIoType::INT, itk::RGBPixel) + switch3DCase(DcmIoType::ULONG, itk::RGBPixel) + switch3DCase(DcmIoType::LONG, itk::RGBPixel) + switch3DCase(DcmIoType::FLOAT, itk::RGBPixel) + switch3DCase(DcmIoType::DOUBLE, itk::RGBPixel) + default: + MITK_ERROR << "Found unsupported DICOM scalar pixel type: (enum value) " << io->GetComponentType(); + } + } + + MITK_ERROR << "Unsupported DICOM pixel type"; + return NULL; + } + } + catch(itk::MemoryAllocationError& e) + { + MITK_ERROR << "Out of memory. Cannot load DICOM series: " << e.what(); + } + catch(std::exception& e) + { + MITK_ERROR << "Error encountered when loading DICOM series:" << e.what(); + } + catch(...) + { + MITK_ERROR << "Unspecified error encountered when loading DICOM series."; + } + + return NULL; +} + +#define switch3DnTCase(IOType, T) \ + case IOType: return LoadDICOMByITK3DnT< T >(filenamesLists, correctTilt, tiltInfo, io); + +mitk::Image::Pointer +mitk::ITKDICOMSeriesReaderHelper +::Load3DnT( const StringContainerList& filenamesLists, bool correctTilt, const GantryTiltInformation& tiltInfo ) +{ + if( filenamesLists.empty() || filenamesLists.front().empty() ) + { + MITK_DEBUG << "Calling LoadDicomSeries with empty filename string container. Probably invalid application logic."; + return NULL; // this is not actually an error but the result is very simple + } + + typedef itk::GDCMImageIO DcmIoType; + DcmIoType::Pointer io = DcmIoType::New(); + + try + { + if (io->CanReadFile(filenamesLists.front().front().c_str())) + { + io->SetFileName(filenamesLists.front().front().c_str()); + io->ReadImageInformation(); + + if (io->GetPixelType() == itk::ImageIOBase::SCALAR) + { + switch (io->GetComponentType()) + { + switch3DnTCase(DcmIoType::UCHAR, unsigned char) + switch3DnTCase(DcmIoType::CHAR, char) + switch3DnTCase(DcmIoType::USHORT, unsigned short) + switch3DnTCase(DcmIoType::SHORT, short) + switch3DnTCase(DcmIoType::UINT, unsigned int) + switch3DnTCase(DcmIoType::INT, int) + switch3DnTCase(DcmIoType::ULONG, long unsigned int) + switch3DnTCase(DcmIoType::LONG, long int) + switch3DnTCase(DcmIoType::FLOAT, float) + switch3DnTCase(DcmIoType::DOUBLE, double) + default: + MITK_ERROR << "Found unsupported DICOM scalar pixel type: (enum value) " << io->GetComponentType(); + } + } + else if (io->GetPixelType() == itk::ImageIOBase::RGB) + { + switch (io->GetComponentType()) + { + switch3DnTCase(DcmIoType::UCHAR, itk::RGBPixel) + switch3DnTCase(DcmIoType::CHAR, itk::RGBPixel) + switch3DnTCase(DcmIoType::USHORT, itk::RGBPixel) + switch3DnTCase(DcmIoType::SHORT, itk::RGBPixel) + switch3DnTCase(DcmIoType::UINT, itk::RGBPixel) + switch3DnTCase(DcmIoType::INT, itk::RGBPixel) + switch3DnTCase(DcmIoType::ULONG, itk::RGBPixel) + switch3DnTCase(DcmIoType::LONG, itk::RGBPixel) + switch3DnTCase(DcmIoType::FLOAT, itk::RGBPixel) + switch3DnTCase(DcmIoType::DOUBLE, itk::RGBPixel) + default: + MITK_ERROR << "Found unsupported DICOM scalar pixel type: (enum value) " << io->GetComponentType(); + } + } + + MITK_ERROR << "Unsupported DICOM pixel type"; + return NULL; + } + } + catch(itk::MemoryAllocationError& e) + { + MITK_ERROR << "Out of memory. Cannot load DICOM series: " << e.what(); + } + catch(std::exception& e) + { + MITK_ERROR << "Error encountered when loading DICOM series:" << e.what(); + } + catch(...) + { + MITK_ERROR << "Unspecified error encountered when loading DICOM series."; + } + + return NULL; +} diff --git a/Modules/DICOMReader/mitkITKDICOMSeriesReaderHelper.h b/Modules/DICOMReader/mitkITKDICOMSeriesReaderHelper.h new file mode 100644 index 0000000000..ff639ecc4c --- /dev/null +++ b/Modules/DICOMReader/mitkITKDICOMSeriesReaderHelper.h @@ -0,0 +1,65 @@ +/*=================================================================== + +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 mitkDICOMSeriesReaderHelper_h +#define mitkDICOMSeriesReaderHelper_h + +#include "mitkImage.h" +#include "mitkGantryTiltInformation.h" + +#include + +namespace mitk +{ + +class ITKDICOMSeriesReaderHelper +{ + public: + + typedef std::vector StringContainer; + typedef std::list StringContainerList; + + Image::Pointer Load( const StringContainer& filenames, bool correctTilt, const GantryTiltInformation& tiltInfo ); + Image::Pointer Load3DnT( const StringContainerList& filenamesLists, bool correctTilt, const GantryTiltInformation& tiltInfo ); + + static bool CanHandleFile(const std::string& filename); + + private: + + template + typename ImageType::Pointer + FixUpTiltedGeometry( ImageType* input, const GantryTiltInformation& tiltInfo ); + + template + Image::Pointer + LoadDICOMByITK( const StringContainer& filenames, + bool correctTilt, + const GantryTiltInformation& tiltInfo, + itk::GDCMImageIO::Pointer& io); + + template + Image::Pointer + LoadDICOMByITK3DnT( const StringContainerList& filenames, + bool correctTilt, + const GantryTiltInformation& tiltInfo, + itk::GDCMImageIO::Pointer& io); + + +}; + +} + +#endif diff --git a/Modules/DICOMReader/mitkITKDICOMSeriesReaderHelper.txx b/Modules/DICOMReader/mitkITKDICOMSeriesReaderHelper.txx new file mode 100644 index 0000000000..8096041f9d --- /dev/null +++ b/Modules/DICOMReader/mitkITKDICOMSeriesReaderHelper.txx @@ -0,0 +1,282 @@ +/*=================================================================== + +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) +{ + /******** 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(); // 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. + + 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()); + + 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) +{ + 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(); // 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. + + + 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); + } + + 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..d152d03a6b --- /dev/null +++ b/Modules/DICOMReader/mitkNormalDirectionConsistencySorter.cpp @@ -0,0 +1,173 @@ +/*=================================================================== 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" << std::endl; +} + + +mitk::NormalDirectionConsistencySorter& +mitk::NormalDirectionConsistencySorter +::operator=(const NormalDirectionConsistencySorter& other) +{ + if (this != &other) + { + DICOMDatasetSorter::operator=(other); + } + return *this; +} + +bool +mitk::NormalDirectionConsistencySorter +::operator==(const DICOMDatasetSorter& other) const +{ + return dynamic_cast(&other) != NULL; +} + + +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(); + + static double projection = 0.0; + projection = 0.0; + projection = normal * directionOfSlices; + + MITK_DEBUG << "Making sense of \norientation '" << imageOrientationString + << "'\nfirst position '" << imagePositionPatientFirst + << "'\nlast position '" << imagePositionPatientLast << "'"; + MITK_DEBUG << "Normal: " << normal; + MITK_DEBUG << "Direction of slices: " << directionOfSlices; + MITK_DEBUG << "Projection of direction onto slice normal: " << projection; + + if ( projection < 0.0 ) + { + MITK_DEBUG << "Need to reverse filenames"; + std::reverse( datasets.begin(), datasets.end() ); + + m_TiltInfo = GantryTiltInformation::MakeFromTagValues( + imagePositionPatientLast, + imagePositionPatientFirst, + imageOrientationString, + datasets.size() - 1 + ); + } + else + { + m_TiltInfo = GantryTiltInformation::MakeFromTagValues( + imagePositionPatientFirst, + imagePositionPatientLast, + imageOrientationString, + datasets.size() - 1 + ); + } + } + else // just ONE dataset, do not forget to reset tilt information + { + m_TiltInfo = GantryTiltInformation(); // empty info + } + + this->SetNumberOfOutputs(1); + this->SetOutput(0, datasets); +} + +mitk::GantryTiltInformation +mitk::NormalDirectionConsistencySorter +::GetTiltInformation() const +{ + return m_TiltInfo; +} diff --git a/Modules/DICOMReader/mitkNormalDirectionConsistencySorter.h b/Modules/DICOMReader/mitkNormalDirectionConsistencySorter.h new file mode 100644 index 0000000000..8fec03b966 --- /dev/null +++ b/Modules/DICOMReader/mitkNormalDirectionConsistencySorter.h @@ -0,0 +1,74 @@ +/*=================================================================== + +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" +#include "mitkGantryTiltInformation.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. + + Since this class is used as a last sorting step before loading, it will also + calculate (and return) an updated GantryTiltInformation object. + + \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(); + + /// See class description and DICOMITKSeriesGDCMReader. + GantryTiltInformation GetTiltInformation() const; + + virtual void PrintConfiguration(std::ostream& os, const std::string& indent = "") const; + + virtual bool operator==(const DICOMDatasetSorter& other) const; + + protected: + + NormalDirectionConsistencySorter(); + virtual ~NormalDirectionConsistencySorter(); + + NormalDirectionConsistencySorter(const NormalDirectionConsistencySorter& other); + NormalDirectionConsistencySorter& operator=(const NormalDirectionConsistencySorter& other); + + GantryTiltInformation m_TiltInfo; +}; + +} + +#endif diff --git a/Modules/DICOMReader/mitkSortByImagePositionPatient.cpp b/Modules/DICOMReader/mitkSortByImagePositionPatient.cpp new file mode 100644 index 0000000000..a3b141811e --- /dev/null +++ b/Modules/DICOMReader/mitkSortByImagePositionPatient.cpp @@ -0,0 +1,170 @@ +/*=================================================================== + +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; +} + +bool +mitk::SortByImagePositionPatient +::operator==(const DICOMSortCriterion& other) const +{ + return dynamic_cast(&other) != NULL; // same class +} + +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 +{ + bool possible(false); + double distance = InternalNumericDistance(left, right, possible); // returns 0.0 if not possible + if (possible) + { + return distance > 0.0; + } + else + { + return this->NextLevelIsLeftBeforeRight(left, right); + } +} + +double +mitk::SortByImagePositionPatient +::InternalNumericDistance(const mitk::DICOMDatasetAccess* left, const mitk::DICOMDatasetAccess* right, bool& possible) 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) + { + possible = true; + // default: compare position + return rightDistance - leftDistance; // if (left < right> ==> diff > 0 + } + else + { + possible = false; + return 0.0; + } +} + + +double +mitk::SortByImagePositionPatient +::NumericDistance(const mitk::DICOMDatasetAccess* from, const mitk::DICOMDatasetAccess* to) const +{ + bool possible(false); + double retVal = InternalNumericDistance(from, to, possible); // returns 0.0 if not possible + return possible ? retVal : 0.0; +} diff --git a/Modules/DICOMReader/mitkSortByImagePositionPatient.h b/Modules/DICOMReader/mitkSortByImagePositionPatient.h new file mode 100644 index 0000000000..1542edaa05 --- /dev/null +++ b/Modules/DICOMReader/mitkSortByImagePositionPatient.h @@ -0,0 +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 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 double NumericDistance(const mitk::DICOMDatasetAccess* from, const mitk::DICOMDatasetAccess* to) const; + + virtual void Print(std::ostream& os) const; + + virtual bool operator==(const DICOMSortCriterion& other) const; + + protected: + + SortByImagePositionPatient( DICOMSortCriterion::Pointer secondaryCriterion = NULL ); + virtual ~SortByImagePositionPatient(); + + SortByImagePositionPatient(const SortByImagePositionPatient& other); + SortByImagePositionPatient& operator=(const SortByImagePositionPatient& other); + + double InternalNumericDistance(const mitk::DICOMDatasetAccess* from, const mitk::DICOMDatasetAccess* to, bool& possible) const; + + private: +}; + +} + +#endif diff --git a/Modules/DICOMReader/mitkThreeDnTDICOMSeriesReader.cpp b/Modules/DICOMReader/mitkThreeDnTDICOMSeriesReader.cpp new file mode 100644 index 0000000000..24c6fdfff6 --- /dev/null +++ b/Modules/DICOMReader/mitkThreeDnTDICOMSeriesReader.cpp @@ -0,0 +1,264 @@ +/*=================================================================== + +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(unsigned int decimalPlacesForOrientation) +:DICOMITKSeriesGDCMReader(decimalPlacesForOrientation) +,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; +} + +bool +mitk::ThreeDnTDICOMSeriesReader +::operator==(const DICOMFileReader& other) const +{ + if (const Self* otherSelf = dynamic_cast(&other)) + { + return + DICOMITKSeriesGDCMReader::operator==(other) + && this->m_Group3DandT == otherSelf->m_Group3DandT; + } + else + { + return false; + } +} + +void +mitk::ThreeDnTDICOMSeriesReader +::SetGroup3DandT(bool on) +{ + m_Group3DandT = on; +} + +bool +mitk::ThreeDnTDICOMSeriesReader +::GetGroup3DandT() const +{ + return m_Group3DandT; +} + +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; + + // we should describe our need for this tag as needed via a function + // (however, we currently know that the superclass will always need 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) + { + // bad copy&paste code from DICOMITKSeriesGDCMReader, should be handled in a better way + DICOMGDCMImageFrameList& gdcmFrameInfoList = *blockIter; + assert(!gdcmFrameInfoList.empty()); + + // reverse frames if necessary + // update tilt information from absolute last sorting + DICOMDatasetList datasetList = ToDICOMDatasetList( gdcmFrameInfoList ); + m_NormalDirectionConsistencySorter->SetInput( datasetList ); + m_NormalDirectionConsistencySorter->Sort(); + DICOMGDCMImageFrameList sortedGdcmInfoFrameList = FromDICOMDatasetList( m_NormalDirectionConsistencySorter->GetOutput(0) ); + const GantryTiltInformation& tiltInfo = m_NormalDirectionConsistencySorter->GetTiltInformation(); + + // set frame list for current block + DICOMImageFrameList frameList = ToDICOMImageFrameList( sortedGdcmInfoFrameList ); + assert(!frameList.empty()); + + DICOMImageBlockDescriptor block; + block.SetTagCache( this->GetTagCache() ); // important: this must be before SetImageFrameList(), because SetImageFrameList will trigger reading of lots of interesting tags! + block.SetImageFrameList( frameList ); + block.SetTiltInformation( tiltInfo ); + + 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; +} + +bool +mitk::ThreeDnTDICOMSeriesReader +::LoadMitkImageForImageBlockDescriptor(DICOMImageBlockDescriptor& block) const +{ + PushLocale(); + const DICOMImageFrameList& frames = block.GetImageFrameList(); + const GantryTiltInformation tiltInfo = block.GetTiltInformation(); + bool hasTilt = tiltInfo.IsRegularGantryTilt(); + + int numberOfTimesteps = block.GetIntProperty("timesteps", 1); + + if (numberOfTimesteps == 1) + { + return DICOMITKSeriesGDCMReader::LoadMitkImageForImageBlockDescriptor(block); + } + + 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 ); + + block.SetMitkImage( mitkImage ); + + PopLocale(); + + return true; +} diff --git a/Modules/DICOMReader/mitkThreeDnTDICOMSeriesReader.h b/Modules/DICOMReader/mitkThreeDnTDICOMSeriesReader.h new file mode 100644 index 0000000000..d977c9814a --- /dev/null +++ b/Modules/DICOMReader/mitkThreeDnTDICOMSeriesReader.h @@ -0,0 +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. + +===================================================================*/ + +#ifndef mitkThreeDnTDICOMSeriesReader_h +#define mitkThreeDnTDICOMSeriesReader_h + +#include "mitkDICOMITKSeriesGDCMReader.h" + +#include "DICOMReaderExports.h" + +namespace mitk +{ + +/** + \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 ); + mitkNewMacro1Param( ThreeDnTDICOMSeriesReader, unsigned int ); + + /// \brief Control whether 3D+t grouping shall actually be attempted. + void SetGroup3DandT(bool on); + bool GetGroup3DandT() const; + + // void AllocateOutputImages(); + /// \brief Load via multiple calls to itk::ImageSeriesReader. + virtual bool LoadImages(); + + virtual bool operator==(const DICOMFileReader& other) const; + + protected: + + ThreeDnTDICOMSeriesReader(unsigned int decimalPlacesForOrientation = 5); + 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 LoadMitkImageForImageBlockDescriptor(DICOMImageBlockDescriptor& block) const; + + bool m_Group3DandT; +}; + +} + +#endif diff --git a/Core/Code/Testing/DICOMTesting/CMakeLists.txt b/Modules/DICOMTesting/CMakeLists.txt similarity index 81% rename from Core/Code/Testing/DICOMTesting/CMakeLists.txt rename to Modules/DICOMTesting/CMakeLists.txt index 5e1277b2c5..3721dbe93b 100644 --- a/Core/Code/Testing/DICOMTesting/CMakeLists.txt +++ b/Modules/DICOMTesting/CMakeLists.txt @@ -1,41 +1,41 @@ if(BUILD_TESTING) if(GDCM_DIR) # clear variables from prior files.cmake # Else CMake would use the content of these variables and would try to create tests (which are not present in DICOMTesting). set(MODULE_TESTS) set(MODULE_IMAGE_TESTS) set(MODULE_SURFACE_TESTS) set(MODULE_TESTIMAGES) set(MODULE_TESTSURFACES) set(MODULE_CUSTOM_TESTS) set(H_FILES) set(CPP_FILES) # now create a new module only for testing purposes -MITK_CREATE_MODULE( mitkDICOMTesting - DEPENDS Mitk # Mitk.so +MITK_CREATE_MODULE( DICOMTesting + DEPENDS DICOMReader ) -MITK_CHECK_MODULE(_RESULT mitkDICOMTesting) +MITK_CHECK_MODULE(_RESULT DICOMTesting) if(_RESULT) - message(STATUS "mitkDICOMTesting application won't be built. Missing: ${_RESULT}") + message(STATUS "DICOMTesting application won't be built. Missing: ${_RESULT}") else(_RESULT) # add helpful applications -MITK_USE_MODULE( mitkDICOMTesting ) +MITK_USE_MODULE( DICOMTesting ) include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ${ALL_INCLUDE_DIRECTORIES}) # dumps out image information add_executable(DumpDICOMMitkImage DumpDICOMMitkImage.cpp) target_link_libraries(DumpDICOMMitkImage ${ALL_LIBRARIES}) # compares dumped out image information against reference dump add_executable(VerifyDICOMMitkImageDump VerifyDICOMMitkImageDump.cpp) target_link_libraries(VerifyDICOMMitkImageDump ${ALL_LIBRARIES}) add_subdirectory(Testing) endif() endif() endif() diff --git a/Core/Code/Testing/DICOMTesting/DumpDICOMMitkImage.cpp b/Modules/DICOMTesting/DumpDICOMMitkImage.cpp similarity index 96% rename from Core/Code/Testing/DICOMTesting/DumpDICOMMitkImage.cpp rename to Modules/DICOMTesting/DumpDICOMMitkImage.cpp index 23f0a4f17c..6183b2e34e 100644 --- a/Core/Code/Testing/DICOMTesting/DumpDICOMMitkImage.cpp +++ b/Modules/DICOMTesting/DumpDICOMMitkImage.cpp @@ -1,38 +1,39 @@ /*=================================================================== 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 "mitkTestDICOMLoading.h" int main(int argc, char** argv) { mitk::TestDICOMLoading loader; - mitk::TestDICOMLoading::StringContainer files; + mitk::StringList files; for (int arg = 1; arg < argc; ++arg) files.push_back( argv[arg] ); mitk::TestDICOMLoading::ImageList images = loader.LoadFiles(files); // combine individual dumps in a way that VerifyDICOMMitkImageDump is able to separate again. // I.e.: when changing this piece of code, always change VerifyDICOMMitkImageDump, too. unsigned int imageCounter(0); for ( mitk::TestDICOMLoading::ImageList::const_iterator imageIter = images.begin(); imageIter != images.end(); ++imageIter ) { std::cout << "-- Image " << ++imageCounter << "\n"; std::cout << loader.DumpImageInformation( *imageIter ) << "\n"; } } diff --git a/Core/Code/Testing/DICOMTesting/Testing/CMakeLists.txt b/Modules/DICOMTesting/Testing/CMakeLists.txt similarity index 98% rename from Core/Code/Testing/DICOMTesting/Testing/CMakeLists.txt rename to Modules/DICOMTesting/Testing/CMakeLists.txt index 0aca28227f..c48c5b50c7 100644 --- a/Core/Code/Testing/DICOMTesting/Testing/CMakeLists.txt +++ b/Modules/DICOMTesting/Testing/CMakeLists.txt @@ -1,88 +1,87 @@ -MITK_CREATE_MODULE_TESTS(LABELS MITK-Core) - +MITK_CREATE_MODULE_TESTS(LABELS) include(mitkFunctionAddTestLabel) # verify minimum expectations: # files are recognized as DICOM # loading files results in a given number of images mitkAddCustomModuleTest(mitkDICOMTestingSanityTest_NoFiles mitkDICOMTestingSanityTest 0) mitkAddCustomModuleTest(mitkDICOMTestingSanityTest_CTImage mitkDICOMTestingSanityTest 1 ${MITK_DATA_DIR}/spacing-ok-ct.dcm) mitkAddCustomModuleTest(mitkDICOMTestingSanityTest_MRImage mitkDICOMTestingSanityTest 1 ${MITK_DATA_DIR}/spacing-ok-mr.dcm) mitkAddCustomModuleTest(mitkDICOMTestingSanityTest_SCImage mitkDICOMTestingSanityTest 1 ${MITK_DATA_DIR}/spacing-ok-sc.dcm) mitkAddCustomModuleTest(mitkDICOMTestingSanityTest_NoImagePositionPatient mitkDICOMTestingSanityTest 1 ${MITK_DATA_DIR}/spacing-ok-sc-no2032.dcm) # verifies that the loader can also be used to just scan for tags and provide them in mitk::Properties (parameter preLoadedVolume) mitkAddCustomModuleTest(mitkDICOMPreloadedVolumeTest_Slice mitkDICOMPreloadedVolumeTest ${MITK_DATA_DIR}/spacing-ok-ct.dcm) set(VERIFY_DUMP_CMD ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/VerifyDICOMMitkImageDump) set(CT_ABDOMEN_DIR ${MITK_DATA_DIR}/TinyCTAbdomen) set(MR_HEART_DIR ${MITK_DATA_DIR}/3D+t-Heart) set(CT_TILT_HEAD_DIR ${MITK_DATA_DIR}/TiltHead) set(CT_TILT_DIR ${MITK_DATA_DIR}/TiltedData) set(SOP_CLASSES_DIR ${MITK_DATA_DIR}/DICOMReader) # these variables could be passed as parameters to a generic test creation function set(TESTS_DIR Tests) set(INPUT_LISTNAME input) set(EXPECTED_DUMP expected.dump) function(AddDicomTestsFromDataRepository CURRENT_DATASET_DIR TESTS_DIR INPUT_LISTNAME EXPECTED_DUMP) # find all test input lists file(GLOB_RECURSE allInputs ${CURRENT_DATASET_DIR}/${TESTS_DIR}/*/${INPUT_LISTNAME}) function(expectFileExists filename) if(NOT EXISTS ${filename}) message(SEND_ERROR "Test case expected file ${filename} which does not exist! Please fix your CMake code or file layout.") endif(NOT EXISTS ${filename}) endfunction(expectFileExists) foreach(input ${allInputs}) # extract only the name of the directory of this very test case string(REGEX REPLACE ".*${TESTS_DIR}/([^/]+)/.*" "\\1" input ${input}) set(inputfilelist "${CURRENT_DATASET_DIR}/${TESTS_DIR}/${input}/${INPUT_LISTNAME}") set(expecteddump "${CURRENT_DATASET_DIR}/${TESTS_DIR}/${input}/${EXPECTED_DUMP}") set(testname "DICOM_Load_${input}") #message(STATUS "DICOM loading test case '${input}'") expectFileExists(${inputfilelist}) expectFileExists(${expecteddump}) # TODO provide error messages if input not valid set(dicomFiles) # clear list # read list of file names from file "input" file(STRINGS ${inputfilelist} rawDicomFiles) foreach(raw ${rawDicomFiles}) # prepend each file with an abosolute path set(fileWithFullPath ${CURRENT_DATASET_DIR}/${raw}) list(APPEND dicomFiles ${fileWithFullPath}) endforeach(raw ${rawDicomFiles}) #message(STATUS " Load ${dicomFiles}") add_test(${testname} ${VERIFY_DUMP_CMD} ${expecteddump} ${dicomFiles}) mitkFunctionAddTestLabel(${testname}) endforeach(input allInputs) endfunction(AddDicomTestsFromDataRepository CURRENT_DATASET_DIR TESTS_DIR INPUT_LISTNAME EXPECTED_DUMP) AddDicomTestsFromDataRepository(${CT_ABDOMEN_DIR} ${TESTS_DIR} ${INPUT_LISTNAME} ${EXPECTED_DUMP}) AddDicomTestsFromDataRepository(${CT_TILT_HEAD_DIR} ${TESTS_DIR} ${INPUT_LISTNAME} ${EXPECTED_DUMP}) AddDicomTestsFromDataRepository(${CT_TILT_DIR} ${TESTS_DIR} ${INPUT_LISTNAME} ${EXPECTED_DUMP}) AddDicomTestsFromDataRepository(${MR_HEART_DIR} ${TESTS_DIR} ${INPUT_LISTNAME} ${EXPECTED_DUMP}) AddDicomTestsFromDataRepository(${SOP_CLASSES_DIR} ${TESTS_DIR} ${INPUT_LISTNAME} ${EXPECTED_DUMP}) # use one more realistic series for testing property lists file(GLOB_RECURSE abdomenImages ${CT_ABDOMEN_DIR}/14?) # this is just one small volume mitkAddCustomModuleTest(mitkDICOMPreloadedVolumeTest_Abdomen mitkDICOMPreloadedVolumeTest ${abdomenImages}) diff --git a/Core/Code/Testing/DICOMTesting/Testing/files.cmake b/Modules/DICOMTesting/Testing/files.cmake similarity index 100% rename from Core/Code/Testing/DICOMTesting/Testing/files.cmake rename to Modules/DICOMTesting/Testing/files.cmake diff --git a/Modules/DICOMTesting/Testing/mitkDICOMLocaleTest.cpp b/Modules/DICOMTesting/Testing/mitkDICOMLocaleTest.cpp new file mode 100644 index 0000000000..1ed7789357 --- /dev/null +++ b/Modules/DICOMTesting/Testing/mitkDICOMLocaleTest.cpp @@ -0,0 +1,131 @@ +/*=================================================================== + +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. + +===================================================================*/ + +/* + This test is meant to reproduce the following error: + + - The machine or current user has a German locale. + - This esp. means that stream IO expects the decimal separator as a comma: "," + - DICOM files use a point "." as the decimal separator to be locale independent + - The parser used by MITK (ITK's GDCM) seems to use the current locale instead of the "C" or "POSIX" locale + - This leads to spacings (and probably other numbers) being trimmed/rounded, + e.g. the correct spacing of 0.314 is read as 1.0 etc. + +*/ + +#include "mitkDataNodeFactory.h" +#include "mitkStandardFileLocations.h" +#include "mitkDicomSeriesReader.h" + +#include "mitkTestingMacros.h" + +#include +#include +#include + +bool mitkDICOMLocaleTestChangeLocale(const std::string& locale) +{ + try + { + MITK_TEST_OUTPUT(<< " ** Changing locale from " << setlocale(LC_ALL, NULL) << " to '" << locale << "'"); + setlocale(LC_ALL, locale.c_str()); + std::locale l( locale.c_str() ); + std::cin.imbue(l); + return true; + } + catch(...) + { + MITK_TEST_OUTPUT(<< "Could not activate locale " << locale); + return false; + } + +} + +void mitkDICOMLocaleTestWithReferenceImage(std::string filename) +{ + mitk::Image::Pointer image; + mitk::DataNodeFactory::Pointer factory = mitk::DataNodeFactory::New(); + factory->SetFileName( filename ); + factory->Update(); + MITK_TEST_CONDITION_REQUIRED(factory->GetNumberOfOutputs() > 0, "file " << filename << " loaded"); + + mitk::DataNode::Pointer node = factory->GetOutput( 0 ); + image = dynamic_cast(node->GetData()); + if(image.IsNull()) + { + MITK_TEST_FAILED_MSG(<< "File "<< filename << " is not an image - test will not be applied." ); + + return; + } + + // note importance of minor differences in spacings: + // DICOM has order y-spacing, x-spacing, while in MITK we assume x-spacing, y-spacing (both meant for 0 and 1 index in array) + MITK_TEST_CONDITION_REQUIRED(mitk::Equal(image->GetGeometry()->GetSpacing()[0], 0.3141592), "correct x spacing? found " + << image->GetGeometry()->GetSpacing()[0]); + MITK_TEST_CONDITION_REQUIRED(mitk::Equal(image->GetGeometry()->GetSpacing()[1], 0.3411592), "correct y spacing? found " + << image->GetGeometry()->GetSpacing()[1]); +} + +int mitkDICOMLocaleTest(int argc, char* argv[]) +{ + MITK_TEST_BEGIN("DICOMLocaleTest"); + + MITK_TEST_CONDITION_REQUIRED(argc >= 2, "File to load has been specified on commandline"); + + MITK_TEST_OUTPUT(<< "Configuration: \n" << mitk::DicomSeriesReader::GetConfigurationString() ); + + std::string filename = argv[1]; + + // load a reference DICOM file with the "C" locale being set + mitkDICOMLocaleTestChangeLocale("C"); + mitkDICOMLocaleTestWithReferenceImage(filename); + // load a reference DICOM file with German locales being set + typedef std::list StringList; + StringList alllocales; + alllocales.push_back("de_DE"); + alllocales.push_back("de_DE.utf8"); + alllocales.push_back("de_DE.UTF8"); + alllocales.push_back("de_DE@euro"); + alllocales.push_back("German_Germany"); + + // supressing this test to be run on MacOS X + // See bug #3894 +#if defined (__APPLE__) || defined(MACOSX) + alllocales.push_back("C"); +#endif + + unsigned int numberOfTestedGermanLocales(0); + + for (StringList::iterator iter = alllocales.begin(); + iter != alllocales.end(); + ++iter) + { + if ( mitkDICOMLocaleTestChangeLocale(*iter) ) + { + ++numberOfTestedGermanLocales; + mitkDICOMLocaleTestWithReferenceImage(filename); + } + } + + if(numberOfTestedGermanLocales == 0) + { + MITK_TEST_OUTPUT(<< "Warning: No German locale was found on the system."); + } + //MITK_TEST_CONDITION_REQUIRED( numberOfTestedGermanLocales > 0, "Verify that at least one German locale has been tested."); + + MITK_TEST_END(); +} + diff --git a/Core/Code/Testing/DICOMTesting/Testing/mitkDICOMPreloadedVolumeTest.cpp b/Modules/DICOMTesting/Testing/mitkDICOMPreloadedVolumeTest.cpp similarity index 68% rename from Core/Code/Testing/DICOMTesting/Testing/mitkDICOMPreloadedVolumeTest.cpp rename to Modules/DICOMTesting/Testing/mitkDICOMPreloadedVolumeTest.cpp index c1864b1330..afa52fe8b4 100644 --- a/Core/Code/Testing/DICOMTesting/Testing/mitkDICOMPreloadedVolumeTest.cpp +++ b/Modules/DICOMTesting/Testing/mitkDICOMPreloadedVolumeTest.cpp @@ -1,105 +1,116 @@ /*=================================================================== 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 "mitkTestDICOMLoading.h" #include "mitkTestingMacros.h" +#include "mitkDICOMTagCache.h" + bool CheckAllPropertiesAreInOtherList(const mitk::PropertyList* list, const mitk::PropertyList* otherList) { MITK_TEST_CONDITION_REQUIRED(list && otherList, "Comparison is passed two non-empty property lists") const mitk::PropertyList::PropertyMap* listM = list->GetMap(); const mitk::PropertyList::PropertyMap* otherListM = otherList->GetMap(); bool equal = true; for ( mitk::PropertyList::PropertyMap::const_iterator iter = listM->begin(); iter != listM->end(); ++iter ) { std::string key = iter->first; mitk::BaseProperty* property = iter->second; mitk::PropertyList::PropertyMap::const_iterator otherEntry = otherListM->find( key ); MITK_TEST_CONDITION( otherEntry != otherListM->end(), " Property '" << key << "' is contained in other list" ) mitk::BaseProperty* otherProperty = otherEntry->second; MITK_TEST_CONDITION( equal &= (*property == *otherProperty), " Property '" << key << "' is equal in both list" ) } return equal; } bool VerifyPropertyListsEquality(const mitk::PropertyList* testList, const mitk::PropertyList* referenceList) { bool allTestPropsInReference = CheckAllPropertiesAreInOtherList(testList, referenceList); MITK_TEST_CONDITION(allTestPropsInReference, "All test properties found in reference properties") bool allReferencePropsInTest = CheckAllPropertiesAreInOtherList(referenceList, testList); MITK_TEST_CONDITION(allReferencePropsInTest, "All reference properties found in test properties") return allTestPropsInReference && allReferencePropsInTest; } +// !!! we expect that this tests get a list of files that load as ONE SINGLE mitk::Image! int mitkDICOMPreloadedVolumeTest(int argc, char** const argv) { MITK_TEST_BEGIN("DICOMPreloadedVolume") mitk::TestDICOMLoading loader; - mitk::TestDICOMLoading::StringContainer files; - - // adapt expectations depending on configuration - std::string configuration = mitk::DicomSeriesReader::GetConfigurationString(); - MITK_TEST_OUTPUT(<< "Configuration: " << configuration) + mitk::StringList files; // load files from commandline for (int arg = 1; arg < argc; ++arg) { MITK_TEST_OUTPUT(<< "Test file " << argv[arg]) files.push_back( argv[arg] ); } // verify all files are DICOM - for (mitk::TestDICOMLoading::StringContainer::const_iterator fileIter = files.begin(); + for (mitk::StringList::const_iterator fileIter = files.begin(); fileIter != files.end(); ++fileIter) { - MITK_TEST_CONDITION_REQUIRED( mitk::DicomSeriesReader::IsDicom(*fileIter) , *fileIter << " is recognized as loadable DICOM object" ) - + MITK_TEST_CONDITION_REQUIRED( mitk::DICOMFileReader::IsDICOM(*fileIter) , *fileIter << " is recognized as loadable DICOM object" ) } // load for a first time mitk::TestDICOMLoading::ImageList images = loader.LoadFiles(files); MITK_TEST_OUTPUT(<< "Loaded " << images.size() << " images. Remembering properties of the first one for later comparison.") mitk::Image::Pointer firstImage = images.front(); mitk::PropertyList::Pointer originalProperties = firstImage->GetPropertyList(); // save the original firstImage->SetPropertyList( mitk::PropertyList::New() ); // clear image properties // what about DEFAULT properties? currently ONLY nodes get default properties // load for a second time, this time provide the image volume as a pointer // expectation is that the reader will provide the same properties to this image (without actually loading a new mitk::Image) - mitk::TestDICOMLoading::ImageList reloadedImages = loader.LoadFiles(files, firstImage); - MITK_TEST_OUTPUT(<< "Again loaded " << reloadedImages.size() << " images. Comparing to previously loaded version.") - mitk::Image::Pointer reloadedImage = reloadedImages.front(); + + // !!! we expect that this tests get a list of files that load as ONE SINGLE mitk::Image! + MITK_TEST_CONDITION_REQUIRED( images.size() == 1, "Not more than 1 images loaded." ); + // otherwise, we would need to determine the correct set of files here + MITK_TEST_OUTPUT(<< "Generating properties via reader. Comparing new properties to previously loaded version.") + mitk::Image::Pointer reloadedImage = loader.DecorateVerifyCachedImage(files, firstImage); + MITK_TEST_CONDITION_REQUIRED(reloadedImage.IsNotNull(), "Reader was able to property-decorate image."); mitk::PropertyList::Pointer regeneratedProperties = reloadedImage->GetPropertyList(); // get the version of the second load attempt bool listsAreEqual = VerifyPropertyListsEquality(regeneratedProperties, originalProperties); - MITK_TEST_CONDITION(listsAreEqual, "LoadDicomSeries generates a valid property list when provided a pre-loaded image"); + MITK_TEST_CONDITION(listsAreEqual, "DICOM file reader generates a valid property list when provided a pre-loaded image"); + + + // test again, this time provide a tag cache. + // expectation is, that an empty tag cache will lead to NO image + mitk::DICOMTagCache::Pointer tagCache; // empty + MITK_TEST_OUTPUT(<< "Generating properties via reader. Comparing new properties to previously loaded version.") + firstImage->SetPropertyList( mitk::PropertyList::New() ); // clear image properties + reloadedImage = loader.DecorateVerifyCachedImage(files, tagCache, firstImage); + MITK_TEST_CONDITION_REQUIRED(reloadedImage.IsNull(), "Reader was able to detect missing tag-cache."); MITK_TEST_END() } diff --git a/Core/Code/Testing/DICOMTesting/Testing/mitkDICOMTestingSanityTest.cpp b/Modules/DICOMTesting/Testing/mitkDICOMTestingSanityTest.cpp similarity index 76% rename from Core/Code/Testing/DICOMTesting/Testing/mitkDICOMTestingSanityTest.cpp rename to Modules/DICOMTesting/Testing/mitkDICOMTestingSanityTest.cpp index a7bd359b70..5391aba6be 100644 --- a/Core/Code/Testing/DICOMTesting/Testing/mitkDICOMTestingSanityTest.cpp +++ b/Modules/DICOMTesting/Testing/mitkDICOMTestingSanityTest.cpp @@ -1,69 +1,61 @@ /*=================================================================== 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 "mitkTestDICOMLoading.h" + #include "mitkTestingMacros.h" int mitkDICOMTestingSanityTest(int argc, char** const argv) { MITK_TEST_BEGIN("DICOMTestingSanity") mitk::TestDICOMLoading loader; - mitk::TestDICOMLoading::StringContainer files; - - // adapt expectations depending on configuration - std::string configuration = mitk::DicomSeriesReader::GetConfigurationString(); - MITK_TEST_OUTPUT(<< "Configuration: " << configuration) - /* - MITK_TEST_CONDITION_REQUIRED( configuration.find( "GDCM_VERSION: 2." ) != std::string::npos, - "Expect at least GDCM version 2" ) - */ + mitk::StringList files; // load files from commandline unsigned int numberOfExpectedImages = 0; if (argc > 1) numberOfExpectedImages = atoi(argv[1]); for (int arg = 2; arg < argc; ++arg) files.push_back( argv[arg] ); // verify all files are DICOM - for (mitk::TestDICOMLoading::StringContainer::const_iterator fileIter = files.begin(); + for (mitk::StringList::const_iterator fileIter = files.begin(); fileIter != files.end(); ++fileIter) { - MITK_TEST_CONDITION_REQUIRED( mitk::DicomSeriesReader::IsDicom(*fileIter) , *fileIter << " is recognized as loadable DICOM object" ) - + MITK_TEST_CONDITION_REQUIRED( mitk::DICOMFileReader::IsDICOM(*fileIter) , *fileIter << " is recognized as loadable DICOM object" ) } // compare with expected number of images from commandline mitk::TestDICOMLoading::ImageList images = loader.LoadFiles(files); MITK_TEST_CONDITION_REQUIRED( images.size() == numberOfExpectedImages, "Loading " << files.size() << " files from commandline results in " << numberOfExpectedImages << " images (see test invocation)" ) // check dump equality (dumping image information must always equal itself) for ( mitk::TestDICOMLoading::ImageList::const_iterator imageIter = images.begin(); imageIter != images.end(); ++imageIter ) { const mitk::Image* image = *imageIter; MITK_TEST_CONDITION( loader.CompareImageInformationDumps( loader.DumpImageInformation(image), loader.DumpImageInformation(image) ) == true, "Image information dumping is able to reproduce its result." ) } MITK_TEST_END() } diff --git a/Core/Code/Testing/DICOMTesting/VerifyDICOMMitkImageDump.cpp b/Modules/DICOMTesting/VerifyDICOMMitkImageDump.cpp similarity index 93% rename from Core/Code/Testing/DICOMTesting/VerifyDICOMMitkImageDump.cpp rename to Modules/DICOMTesting/VerifyDICOMMitkImageDump.cpp index 7772e8e460..7160b1b3f7 100644 --- a/Core/Code/Testing/DICOMTesting/VerifyDICOMMitkImageDump.cpp +++ b/Modules/DICOMTesting/VerifyDICOMMitkImageDump.cpp @@ -1,122 +1,127 @@ /*=================================================================== 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 "mitkTestDICOMLoading.h" std::vector LoadDumps(const std::string& fileName) { std::vector separatedDumps; std::ifstream fileStream( fileName.c_str() ); std::string buffer; std::string line; while(fileStream){ std::getline(fileStream, line); if (line.find("-- Image ") == 0) { // separator: starts a new image block if ( !buffer.empty() ) { // unless this is the first block separatedDumps.push_back(buffer); buffer.clear(); } } else { buffer += line + "\n"; } } fileStream.close(); // eat last image dump if ( !buffer.empty() ) { separatedDumps.push_back(buffer); buffer.clear(); } return separatedDumps; } int main(int argc, char** argv) { /** Loads a list of DICOM images, compares generated mitk::Images against stored references. first argument: file with reference dumps following arguments: file names to load */ if (argc < 2) { MITK_ERROR << "Usage:"; MITK_ERROR << " " << argv[0] << " reference.dump file1 [file2 .. fileN]"; MITK_ERROR << " "; MITK_ERROR << " Loads all DICOM images in file1 to fileN as MITK images "; MITK_ERROR << " and compares loaded images against stored expectations (dumps)."; MITK_ERROR << " See also DumpDICOMMitkImage (generates dumps)"; return EXIT_FAILURE; } mitk::TestDICOMLoading loader; - mitk::TestDICOMLoading::StringContainer files; - - // TODO load reference dumps + mitk::StringList files; // load from argv[1] // separate at "-- Image n" // store in an array of dumps std::vector expectedDumps = LoadDumps(argv[1]); for (int arg = 2; arg < argc; ++arg) files.push_back( argv[arg] ); mitk::TestDICOMLoading::ImageList images = loader.LoadFiles(files); unsigned int imageCounter(0); for ( mitk::TestDICOMLoading::ImageList::const_iterator imageIter = images.begin(); imageIter != images.end(); ++imageIter, ++imageCounter ) { std::string imageDump = loader.DumpImageInformation( *imageIter ); if (imageCounter >= expectedDumps.size()) { MITK_ERROR << "Loader produces more images than expected. Aborting after image " << (imageCounter-1); MITK_INFO << "Image " << imageCounter << " loaded as:\n" << imageDump; return EXIT_FAILURE; } bool loadedAsExpected = loader.CompareImageInformationDumps( expectedDumps[imageCounter], imageDump ); if (loadedAsExpected) { MITK_INFO << "Image " << imageCounter << " loads as expected."; } else { MITK_ERROR << "Image " << imageCounter << " did not load as expected."; MITK_INFO << "Expected: \n" << expectedDumps[imageCounter] << "\nGot:\n" << imageDump; return EXIT_FAILURE; } } + if (images.size() < expectedDumps.size()) + { + MITK_ERROR << "Loader produced only " << images.size() << " mitk::Images, while test expected " << expectedDumps.size() << " images"; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; } diff --git a/Core/Code/Testing/DICOMTesting/files.cmake b/Modules/DICOMTesting/files.cmake similarity index 100% rename from Core/Code/Testing/DICOMTesting/files.cmake rename to Modules/DICOMTesting/files.cmake diff --git a/Core/Code/Testing/DICOMTesting/mitkTestDICOMLoading.cpp b/Modules/DICOMTesting/mitkTestDICOMLoading.cpp similarity index 82% rename from Core/Code/Testing/DICOMTesting/mitkTestDICOMLoading.cpp rename to Modules/DICOMTesting/mitkTestDICOMLoading.cpp index 4cc6059762..87ea982f03 100644 --- a/Core/Code/Testing/DICOMTesting/mitkTestDICOMLoading.cpp +++ b/Modules/DICOMTesting/mitkTestDICOMLoading.cpp @@ -1,443 +1,490 @@ /*=================================================================== 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 "mitkTestDICOMLoading.h" #include mitk::TestDICOMLoading::TestDICOMLoading() :m_PreviousCLocale(NULL) { } void mitk::TestDICOMLoading::SetDefaultLocale() { // remember old locale only once if (m_PreviousCLocale == NULL) { m_PreviousCLocale = setlocale(LC_NUMERIC, NULL); // set to "C" setlocale(LC_NUMERIC, "C"); m_PreviousCppLocale = std::cin.getloc(); std::locale l( "C" ); std::cin.imbue(l); std::cout.imbue(l); } } void mitk::TestDICOMLoading::ResetUserLocale() { if (m_PreviousCLocale) { setlocale(LC_NUMERIC, m_PreviousCLocale); std::cin.imbue(m_PreviousCppLocale); std::cout.imbue(m_PreviousCppLocale); m_PreviousCLocale = NULL; } } -mitk::TestDICOMLoading::ImageList mitk::TestDICOMLoading::LoadFiles( const StringContainer& files, Image::Pointer preLoadedVolume ) +mitk::TestDICOMLoading::ImageList +mitk::TestDICOMLoading +::LoadFiles( const StringList& files ) { - for (StringContainer::const_iterator iter = files.begin(); + for (StringList::const_iterator iter = files.begin(); iter != files.end(); ++iter) { MITK_DEBUG << "File " << *iter; } ImageList result; - DicomSeriesReader::FileNamesGrouping seriesInFiles = DicomSeriesReader::GetSeries( files, true ); + ClassicDICOMSeriesReader::Pointer reader = this->BuildDICOMReader(); + reader->SetInputFiles( files ); + reader->AnalyzeInputFiles(); + reader->PrintOutputs(std::cout,true); + reader->LoadImages(); - // TODO sort series UIDs, implementation of map iterator might differ on different platforms (or verify this is a standard topic??) - for (DicomSeriesReader::FileNamesGrouping::const_iterator seriesIter = seriesInFiles.begin(); - seriesIter != seriesInFiles.end(); - ++seriesIter) + unsigned int numberOfImages = reader->GetNumberOfOutputs(); + for (unsigned imageIndex = 0; imageIndex < numberOfImages; ++imageIndex) { - StringContainer files = seriesIter->second.GetFilenames(); + const DICOMImageBlockDescriptor& block = reader->GetOutput(imageIndex); + result.push_back( block.GetMitkImage() ); + } - DataNode::Pointer node = DicomSeriesReader::LoadDicomSeries( files, true, true, true, 0, preLoadedVolume ); // true, true, true ist just a copy of the default values + return result; +} - if (node.IsNotNull()) - { - Image::Pointer image = dynamic_cast( node->GetData() ); +mitk::ClassicDICOMSeriesReader::Pointer +mitk::TestDICOMLoading +::BuildDICOMReader() +{ + ClassicDICOMSeriesReader::Pointer reader = ClassicDICOMSeriesReader::New(); + reader->SetFixTiltByShearing(true); + return reader; +} - result.push_back( image ); - } - else - { - } +mitk::Image::Pointer +mitk::TestDICOMLoading +::DecorateVerifyCachedImage( const StringList& files, mitk::DICOMTagCache* tagCache, mitk::Image::Pointer cachedImage ) +{ + DICOMImageBlockDescriptor block; + + DICOMImageFrameList framelist; + for (StringList::const_iterator iter = files.begin(); + iter != files.end(); + ++iter) + { + framelist.push_back( DICOMImageFrameInfo::New(*iter) ); } - return result; + block.SetImageFrameList( framelist ); + + block.SetTagCache( tagCache ); + block.SetMitkImage( cachedImage ); // this should/will create a propertylist describing the image slices + return block.GetMitkImage(); +} + + +mitk::Image::Pointer +mitk::TestDICOMLoading +::DecorateVerifyCachedImage( const StringList& files, mitk::Image::Pointer cachedImage ) +{ + ClassicDICOMSeriesReader::Pointer reader = this->BuildDICOMReader(); + reader->SetInputFiles( files ); + reader->AnalyzeInputFiles(); // This just creates a "tag cache and a nice DICOMImageBlockDescriptor. + // Both of these could also be produced in a different way. The only + // important thing is, that the DICOMImageBlockDescriptor knows a + // tag-cache object when PropertyDecorateCachedMitkImageForImageBlockDescriptor + // is called. + + if ( reader->GetNumberOfOutputs() != 1 ) + { + MITK_ERROR << "Reader produce " << reader->GetNumberOfOutputs() << " images instead of 1 expected.."; + return NULL; + } + + DICOMImageBlockDescriptor block = reader->GetOutput(0); // creates a block copy + block.SetMitkImage( cachedImage ); // this should/will create a propertylist describing the image slices + return block.GetMitkImage(); } std::string mitk::TestDICOMLoading::ComponentTypeToString(int type) { if (type == itk::ImageIOBase::UCHAR) return "UCHAR"; else if (type == itk::ImageIOBase::CHAR) return "CHAR"; else if (type == itk::ImageIOBase::USHORT) return "USHORT"; else if (type == itk::ImageIOBase::SHORT) return "SHORT"; else if (type == itk::ImageIOBase::UINT) return "UINT"; else if (type == itk::ImageIOBase::INT) return "INT"; else if (type == itk::ImageIOBase::ULONG) return "ULONG"; else if (type == itk::ImageIOBase::LONG) return "LONG"; else if (type == itk::ImageIOBase::FLOAT) return "FLOAT"; else if (type == itk::ImageIOBase::DOUBLE) return "DOUBLE"; else return "UNKNOWN"; } // add a line to stringstream result (see DumpImageInformation #define DumpLine(field, data) DumpILine(0, field, data) // add an indented(!) line to stringstream result (see DumpImageInformation #define DumpILine(indent, field, data) \ { \ std::string DumpLine_INDENT; DumpLine_INDENT.resize(indent, ' ' ); \ result << DumpLine_INDENT << field << ": " << data << "\n"; \ } std::string mitk::TestDICOMLoading::DumpImageInformation( const Image* image ) { std::stringstream result; if (image == NULL) return result.str(); SetDefaultLocale(); // basic image data DumpLine( "Pixeltype", ComponentTypeToString(image->GetPixelType().GetComponentType()) ); DumpLine( "BitsPerPixel", image->GetPixelType().GetBpe() ); DumpLine( "Dimension", image->GetDimension() ); result << "Dimensions: "; for (unsigned int dim = 0; dim < image->GetDimension(); ++dim) result << image->GetDimension(dim) << " "; result << "\n"; // geometry data result << "Geometry: \n"; Geometry3D* geometry = image->GetGeometry(); if (geometry) { AffineTransform3D* transform = geometry->GetIndexToWorldTransform(); if (transform) { result << " " << "Matrix: "; const AffineTransform3D::MatrixType& matrix = transform->GetMatrix(); for (unsigned int i = 0; i < 3; ++i) for (unsigned int j = 0; j < 3; ++j) result << matrix[i][j] << " "; result << "\n"; result << " " << "Offset: "; const AffineTransform3D::OutputVectorType& offset = transform->GetOffset(); for (unsigned int i = 0; i < 3; ++i) result << offset[i] << " "; result << "\n"; result << " " << "Center: "; const AffineTransform3D::InputPointType& center = transform->GetCenter(); for (unsigned int i = 0; i < 3; ++i) result << center[i] << " "; result << "\n"; result << " " << "Translation: "; const AffineTransform3D::OutputVectorType& translation = transform->GetTranslation(); for (unsigned int i = 0; i < 3; ++i) result << translation[i] << " "; result << "\n"; result << " " << "Scale: "; const double* scale = transform->GetScale(); for (unsigned int i = 0; i < 3; ++i) result << scale[i] << " "; result << "\n"; result << " " << "Origin: "; const Point3D& origin = geometry->GetOrigin(); for (unsigned int i = 0; i < 3; ++i) result << origin[i] << " "; result << "\n"; result << " " << "Spacing: "; const Vector3D& spacing = geometry->GetSpacing(); for (unsigned int i = 0; i < 3; ++i) result << spacing[i] << " "; result << "\n"; result << " " << "TimeBounds: "; const TimeBounds timeBounds = geometry->GetTimeBounds(); for (unsigned int i = 0; i < 2; ++i) result << timeBounds[i] << " "; result << "\n"; } } ResetUserLocale(); return result.str(); } std::string mitk::TestDICOMLoading::trim(const std::string& pString, const std::string& pWhitespace) { const size_t beginStr = pString.find_first_not_of(pWhitespace); if (beginStr == std::string::npos) { // no content return ""; } const size_t endStr = pString.find_last_not_of(pWhitespace); const size_t range = endStr - beginStr + 1; return pString.substr(beginStr, range); } std::string mitk::TestDICOMLoading::reduce(const std::string& pString, const std::string& pFill, const std::string& pWhitespace) { // trim first std::string result(trim(pString, pWhitespace)); // replace sub ranges size_t beginSpace = result.find_first_of(pWhitespace); while (beginSpace != std::string::npos) { const size_t endSpace = result.find_first_not_of(pWhitespace, beginSpace); const size_t range = endSpace - beginSpace; result.replace(beginSpace, range, pFill); const size_t newStart = beginSpace + pFill.length(); beginSpace = result.find_first_of(pWhitespace, newStart); } return result; } bool mitk::TestDICOMLoading::CompareSpacedValueFields( const std::string& reference, const std::string& test, double /*eps*/ ) { bool result(true); // tokenize string, compare each token, if possible by float comparison std::stringstream referenceStream(reduce(reference)); std::stringstream testStream(reduce(test)); std::string refToken; std::string testToken; while ( std::getline( referenceStream, refToken, ' ' ) && std::getline ( testStream, testToken, ' ' ) ) { float refNumber; float testNumber; if ( this->StringToNumber(refToken, refNumber) ) { if ( this->StringToNumber(testToken, testNumber) ) { // print-out compared tokens if DEBUG output allowed MITK_DEBUG << "Reference Token '" << refToken << "'" << " value " << refNumber << ", test Token '" << testToken << "'" << " value " << testNumber; bool old_result = result; result &= ( fabs(refNumber - testNumber) < mitk::eps ); // log the token/number which causes the test to fail if( old_result != result) { MITK_ERROR << std::setprecision(16) << "Reference Token '" << refToken << "'" << " value " << refNumber << ", test Token '" << testToken << "'" << " value " << testNumber; MITK_ERROR << "[FALSE] - difference: " << std::setprecision(16) << fabs(refNumber - testNumber) << " EPS: " << mitk::eps; } } else { MITK_ERROR << refNumber << " cannot be compared to '" << testToken << "'"; } } else { MITK_DEBUG << "Token '" << refToken << "'" << " handled as string"; result &= refToken == testToken; } } if ( std::getline( referenceStream, refToken, ' ' ) ) { MITK_ERROR << "Reference string still had values when test string was already parsed: ref '" << reference << "', test '" << test << "'"; result = false; } else if ( std::getline( testStream, testToken, ' ' ) ) { MITK_ERROR << "Test string still had values when reference string was already parsed: ref '" << reference << "', test '" << test << "'"; result = false; } return result; } bool mitk::TestDICOMLoading::CompareImageInformationDumps( const std::string& referenceDump, const std::string& testDump ) { KeyValueMap reference = ParseDump(referenceDump); KeyValueMap test = ParseDump(testDump); bool testResult(true); // verify all expected values for (KeyValueMap::const_iterator refIter = reference.begin(); refIter != reference.end(); ++refIter) { const std::string& refKey = refIter->first; const std::string& refValue = refIter->second; if ( test.find(refKey) != test.end() ) { const std::string& testValue = test[refKey]; bool thisTestResult = CompareSpacedValueFields( refValue, testValue ); testResult &= thisTestResult; MITK_DEBUG << refKey << ": '" << refValue << "' == '" << testValue << "' ? " << (thisTestResult?"YES":"NO"); } else { MITK_ERROR << "Reference dump contains a key'" << refKey << "' (value '" << refValue << "')." ; MITK_ERROR << "This key is expected to be generated for tests (but was not). Most probably you need to update your test data."; return false; } } // now check test dump does not contain any additional keys for (KeyValueMap::const_iterator testIter = test.begin(); testIter != test.end(); ++testIter) { const std::string& key = testIter->first; const std::string& value = testIter->second; if ( reference.find(key) == reference.end() ) { MITK_ERROR << "Test dump contains an unexpected key'" << key << "' (value '" << value << "')." ; MITK_ERROR << "This key is not expected. Most probably you need to update your test data."; return false; } } return testResult; } mitk::TestDICOMLoading::KeyValueMap mitk::TestDICOMLoading::ParseDump( const std::string& dump ) { KeyValueMap parsedResult; std::string shredder(dump); std::stack surroundingKeys; std::stack expectedIndents; expectedIndents.push(0); while (true) { std::string::size_type newLinePos = shredder.find( '\n' ); if (newLinePos == std::string::npos || newLinePos == 0) break; std::string line = shredder.substr( 0, newLinePos ); shredder = shredder.erase( 0, newLinePos+1 ); std::string::size_type keyPosition = line.find_first_not_of( ' ' ); std::string::size_type colonPosition = line.find( ':' ); std::string key = line.substr(keyPosition, colonPosition - keyPosition); std::string::size_type firstSpacePosition = key.find_first_of(" "); if (firstSpacePosition != std::string::npos) { key.erase(firstSpacePosition); } if ( keyPosition > expectedIndents.top() ) { // more indent than before expectedIndents.push(keyPosition); } else if (keyPosition == expectedIndents.top() ) { if (!surroundingKeys.empty()) { surroundingKeys.pop(); // last of same length } } else { // less indent than before do expectedIndents.pop(); while (expectedIndents.top() != keyPosition); // unwind until current indent is found } if (!surroundingKeys.empty()) { key = surroundingKeys.top() + "." + key; // construct current key name } surroundingKeys.push(key); // this is the new embracing key std::string value = line.substr(colonPosition+1); MITK_DEBUG << " Key: '" << key << "' value '" << value << "'" ; parsedResult[key] = value; // store parsing result } return parsedResult; } diff --git a/Core/Code/Testing/DICOMTesting/mitkTestDICOMLoading.h b/Modules/DICOMTesting/mitkTestDICOMLoading.h similarity index 84% rename from Core/Code/Testing/DICOMTesting/mitkTestDICOMLoading.h rename to Modules/DICOMTesting/mitkTestDICOMLoading.h index a64b48415c..a6f86d75bc 100644 --- a/Core/Code/Testing/DICOMTesting/mitkTestDICOMLoading.h +++ b/Modules/DICOMTesting/mitkTestDICOMLoading.h @@ -1,105 +1,112 @@ /*=================================================================== 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 mitkTestDICOMLoading_h #define mitkTestDICOMLoading_h -#include "mitkDicomSeriesReader.h" +#include "mitkClassicDICOMSeriesReader.h" -#include "mitkDICOMTestingExports.h" +#include "DICOMTestingExports.h" namespace mitk { -class mitkDICOMTesting_EXPORT TestDICOMLoading +class DICOMTesting_EXPORT TestDICOMLoading { public: - typedef DicomSeriesReader::StringContainer StringContainer; - typedef std::list NodeList; typedef std::list ImageList; TestDICOMLoading(); ImageList - LoadFiles( const StringContainer& files, Image::Pointer preLoadedVolume = NULL ); + LoadFiles( const StringList & files ); + + Image::Pointer + DecorateVerifyCachedImage( const StringList& files, mitk::Image::Pointer cachedImage ); + + Image::Pointer + DecorateVerifyCachedImage( const StringList& files, DICOMTagCache*, mitk::Image::Pointer cachedImage ); /** \brief Dump relevant image information for later comparison. \sa CompareImageInformationDumps */ std::string DumpImageInformation( const Image* image ); /** \brief Compare two image information dumps. \return true, if dumps are sufficiently equal (see parameters) \sa DumpImageInformation */ bool CompareImageInformationDumps( const std::string& reference, const std::string& test ); private: typedef std::map KeyValueMap; + ClassicDICOMSeriesReader::Pointer + BuildDICOMReader(); + void SetDefaultLocale(); void ResetUserLocale(); std::string ComponentTypeToString( int type ); KeyValueMap ParseDump( const std::string& dump ); bool CompareSpacedValueFields( const std::string& reference, const std::string& test, double eps = mitk::eps ); /** Compress whitespace in string \param pString input string \param pFill replacement whitespace (only whitespace in string after reduction) \param pWhitespace characters handled as whitespace */ std::string reduce(const std::string& pString, const std::string& pFill = " ", const std::string& pWhitespace = " \t"); /** Remove leading and trailing whitespace \param pString input string \param pWhitespace characters handled as whitespace */ std::string trim(const std::string& pString, const std::string& pWhitespace = " \t"); template bool StringToNumber(const std::string& s, T& value) { std::stringstream stream(s); stream >> value; return !stream.fail(); } const char* m_PreviousCLocale; std::locale m_PreviousCppLocale; }; } #endif diff --git a/Modules/ImageStatistics/Testing/CMakeLists.txt b/Modules/ImageStatistics/Testing/CMakeLists.txt index 012a6a7a63..dc4516c54f 100644 --- a/Modules/ImageStatistics/Testing/CMakeLists.txt +++ b/Modules/ImageStatistics/Testing/CMakeLists.txt @@ -1,3 +1,3 @@ -MITK_CREATE_MODULE_TESTS() +MITK_CREATE_MODULE_TESTS(EXTRA_DEPENDS DICOMReader) mitkAddCustomModuleTest(mitkImageStatisticsHotspotTest_Case1 mitkImageStatisticsHotspotTest ${CMAKE_CURRENT_SOURCE_DIR}/Data/Hotspot_Case1.xml) diff --git a/Modules/ImageStatistics/Testing/mitkImageStatisticsCalculatorTest.cpp b/Modules/ImageStatistics/Testing/mitkImageStatisticsCalculatorTest.cpp index 0e3c62345e..e7c3415a6d 100644 --- a/Modules/ImageStatistics/Testing/mitkImageStatisticsCalculatorTest.cpp +++ b/Modules/ImageStatistics/Testing/mitkImageStatisticsCalculatorTest.cpp @@ -1,496 +1,499 @@ /*=================================================================== 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 "mitkStandardFileLocations.h" #include "mitkDicomSeriesReader.h" #include "mitkTestingMacros.h" #include "mitkImageStatisticsCalculator.h" #include "mitkPlanarPolygon.h" -#include "mitkDicomSeriesReader.h" -#include +#include "mitkClassicDICOMSeriesReader.h" #include "vtkStreamingDemandDrivenPipeline.h" #include #include //#include /** * \brief Test class for mitkImageStatisticsCalculator * * This test covers: * - instantiation of an ImageStatisticsCalculator class * - correctness of statistics when using PlanarFigures for masking */ class mitkImageStatisticsCalculatorTestClass { public: struct testCase { int id; mitk::PlanarFigure::Pointer figure; double mean; double sd; }; // calculate statistics for the given image and planarpolygon static const mitk::ImageStatisticsCalculator::Statistics TestStatistics( mitk::Image::Pointer image, mitk::PlanarFigure::Pointer polygon ) { mitk::ImageStatisticsCalculator::Pointer statisticsCalculator = mitk::ImageStatisticsCalculator::New(); statisticsCalculator->SetImage( image ); statisticsCalculator->SetMaskingModeToPlanarFigure(); statisticsCalculator->SetPlanarFigure( polygon ); statisticsCalculator->ComputeStatistics(); return statisticsCalculator->GetStatistics(); } // returns a vector of defined test-cases static std::vector InitializeTestCases( mitk::Geometry2D::Pointer geom ) { std::vector testCases; { /***************************** * one whole white pixel * -> mean of 255 expected ******************************/ mitk::PlanarPolygon::Pointer figure1 = mitk::PlanarPolygon::New(); figure1->SetGeometry2D( geom ); mitk::Point2D pnt1; pnt1[0] = 10.5 ; pnt1[1] = 3.5; figure1->PlaceFigure( pnt1 ); mitk::Point2D pnt2; pnt2[0] = 9.5; pnt2[1] = 3.5; figure1->SetControlPoint( 1, pnt2, true ); mitk::Point2D pnt3; pnt3[0] = 9.5; pnt3[1] = 4.5; figure1->SetControlPoint( 2, pnt3, true ); mitk::Point2D pnt4; pnt4[0] = 10.5; pnt4[1] = 4.5; figure1->SetControlPoint( 3, pnt4, true ); figure1->GetPolyLine(0); testCase test; test.id = testCases.size(); test.figure = figure1; test.mean = 255.0; test.sd = 0.0; testCases.push_back( test ); } { /***************************** * half pixel in x-direction (white) * -> mean of 255 expected ******************************/ mitk::PlanarPolygon::Pointer figure1 = mitk::PlanarPolygon::New(); figure1->SetGeometry2D( geom ); mitk::Point2D pnt1; pnt1[0] = 10.0 ; pnt1[1] = 3.5; figure1->PlaceFigure( pnt1 ); mitk::Point2D pnt2; pnt2[0] = 9.5; pnt2[1] = 3.5; figure1->SetControlPoint( 1, pnt2, true ); mitk::Point2D pnt3; pnt3[0] = 9.5; pnt3[1] = 4.5; figure1->SetControlPoint( 2, pnt3, true ); mitk::Point2D pnt4; pnt4[0] = 10.0; pnt4[1] = 4.5; figure1->SetControlPoint( 3, pnt4, true ); figure1->GetPolyLine(0); testCase test; test.id = testCases.size(); test.figure = figure1; test.mean = 255.0; test.sd = 0.0; testCases.push_back( test ); } { /***************************** * half pixel in diagonal-direction (white) * -> mean of 255 expected ******************************/ mitk::PlanarPolygon::Pointer figure1 = mitk::PlanarPolygon::New(); figure1->SetGeometry2D( geom ); mitk::Point2D pnt1; pnt1[0] = 10.5 ; pnt1[1] = 3.5; figure1->PlaceFigure( pnt1 ); mitk::Point2D pnt2; pnt2[0] = 9.5; pnt2[1] = 3.5; figure1->SetControlPoint( 1, pnt2, true ); mitk::Point2D pnt3; pnt3[0] = 9.5; pnt3[1] = 4.5; figure1->SetControlPoint( 2, pnt3, true ); figure1->GetPolyLine(0); testCase test; test.id = testCases.size(); test.figure = figure1; test.mean = 255.0; test.sd = 0.0; testCases.push_back( test ); } { /***************************** * one pixel (white) + 2 half pixels (white) + 1 half pixel (black) * -> mean of 191.25 expected ******************************/ mitk::PlanarPolygon::Pointer figure1 = mitk::PlanarPolygon::New(); figure1->SetGeometry2D( geom ); mitk::Point2D pnt1; pnt1[0] = 1.1; pnt1[1] = 1.1; figure1->PlaceFigure( pnt1 ); mitk::Point2D pnt2; pnt2[0] = 2.0; pnt2[1] = 2.0; figure1->SetControlPoint( 1, pnt2, true ); mitk::Point2D pnt3; pnt3[0] = 3.0; pnt3[1] = 1.0; figure1->SetControlPoint( 2, pnt3, true ); mitk::Point2D pnt4; pnt4[0] = 2.0; pnt4[1] = 0.0; figure1->SetControlPoint( 3, pnt4, true ); figure1->GetPolyLine(0); testCase test; test.id = testCases.size(); test.figure = figure1; test.mean = 191.25; test.sd = 127.5; testCases.push_back( test ); } { /***************************** * whole pixel (white) + half pixel (gray) in x-direction * -> mean of 191.5 expected ******************************/ mitk::PlanarPolygon::Pointer figure1 = mitk::PlanarPolygon::New(); figure1->SetGeometry2D( geom ); mitk::Point2D pnt1; pnt1[0] = 11.0; pnt1[1] = 3.5; figure1->PlaceFigure( pnt1 ); mitk::Point2D pnt2; pnt2[0] = 9.5; pnt2[1] = 3.5; figure1->SetControlPoint( 1, pnt2, true ); mitk::Point2D pnt3; pnt3[0] = 9.5; pnt3[1] = 4.5; figure1->SetControlPoint( 2, pnt3, true ); mitk::Point2D pnt4; pnt4[0] = 11.0; pnt4[1] = 4.5; figure1->SetControlPoint( 3, pnt4, true ); figure1->GetPolyLine(0); testCase test; test.id = testCases.size(); test.figure = figure1; test.mean = 191.50; test.sd = 89.80; testCases.push_back( test ); } { /***************************** * quarter pixel (black) + whole pixel (white) + half pixel (gray) in x-direction * -> mean of 191.5 expected ******************************/ mitk::PlanarPolygon::Pointer figure1 = mitk::PlanarPolygon::New(); figure1->SetGeometry2D( geom ); mitk::Point2D pnt1; pnt1[0] = 11.0; pnt1[1] = 3.5; figure1->PlaceFigure( pnt1 ); mitk::Point2D pnt2; pnt2[0] = 9.25; pnt2[1] = 3.5; figure1->SetControlPoint( 1, pnt2, true ); mitk::Point2D pnt3; pnt3[0] = 9.25; pnt3[1] = 4.5; figure1->SetControlPoint( 2, pnt3, true ); mitk::Point2D pnt4; pnt4[0] = 11.0; pnt4[1] = 4.5; figure1->SetControlPoint( 3, pnt4, true ); figure1->GetPolyLine(0); testCase test; test.id = testCases.size(); test.figure = figure1; test.mean = 191.5; test.sd = 89.80; testCases.push_back( test ); } { /***************************** * half pixel (black) + whole pixel (white) + half pixel (gray) in x-direction * -> mean of 127.66 expected ******************************/ mitk::PlanarPolygon::Pointer figure1 = mitk::PlanarPolygon::New(); figure1->SetGeometry2D( geom ); mitk::Point2D pnt1; pnt1[0] = 11.0; pnt1[1] = 3.5; figure1->PlaceFigure( pnt1 ); mitk::Point2D pnt2; pnt2[0] = 9.0; pnt2[1] = 3.5; figure1->SetControlPoint( 1, pnt2, true ); mitk::Point2D pnt3; pnt3[0] = 9.0; pnt3[1] = 4.0; figure1->SetControlPoint( 2, pnt3, true ); mitk::Point2D pnt4; pnt4[0] = 11.0; pnt4[1] = 4.0; figure1->SetControlPoint( 3, pnt4, true ); figure1->GetPolyLine(0); testCase test; test.id = testCases.size(); test.figure = figure1; test.mean = 127.66; test.sd = 127.5; testCases.push_back( test ); } { /***************************** * whole pixel (gray) * -> mean of 128 expected ******************************/ mitk::PlanarPolygon::Pointer figure2 = mitk::PlanarPolygon::New(); figure2->SetGeometry2D( geom ); mitk::Point2D pnt1; pnt1[0] = 11.5; pnt1[1] = 10.5; figure2->PlaceFigure( pnt1 ); mitk::Point2D pnt2; pnt2[0] = 11.5; pnt2[1] = 11.5; figure2->SetControlPoint( 1, pnt2, true ); mitk::Point2D pnt3; pnt3[0] = 12.5; pnt3[1] = 11.5; figure2->SetControlPoint( 2, pnt3, true ); mitk::Point2D pnt4; pnt4[0] = 12.5; pnt4[1] = 10.5; figure2->SetControlPoint( 3, pnt4, true ); figure2->GetPolyLine(0); testCase test; test.id = testCases.size(); test.figure = figure2; test.mean = 128.0; test.sd = 0.0; testCases.push_back( test ); } { /***************************** * whole pixel (gray) + half pixel (white) in y-direction * -> mean of 191.5 expected ******************************/ mitk::PlanarPolygon::Pointer figure2 = mitk::PlanarPolygon::New(); figure2->SetGeometry2D( geom ); mitk::Point2D pnt1; pnt1[0] = 11.5; pnt1[1] = 10.5; figure2->PlaceFigure( pnt1 ); mitk::Point2D pnt2; pnt2[0] = 11.5; pnt2[1] = 12.0; figure2->SetControlPoint( 1, pnt2, true ); mitk::Point2D pnt3; pnt3[0] = 12.5; pnt3[1] = 12.0; figure2->SetControlPoint( 2, pnt3, true ); mitk::Point2D pnt4; pnt4[0] = 12.5; pnt4[1] = 10.5; figure2->SetControlPoint( 3, pnt4, true ); figure2->GetPolyLine(0); testCase test; test.id = testCases.size(); test.figure = figure2; test.mean = 191.5; test.sd = 89.80; testCases.push_back( test ); } { /***************************** * 2 whole pixel (white) + 2 whole pixel (black) in y-direction * -> mean of 127.66 expected ******************************/ mitk::PlanarPolygon::Pointer figure2 = mitk::PlanarPolygon::New(); figure2->SetGeometry2D( geom ); mitk::Point2D pnt1; pnt1[0] = 11.5; pnt1[1] = 10.5; figure2->PlaceFigure( pnt1 ); mitk::Point2D pnt2; pnt2[0] = 11.5; pnt2[1] = 13.5; figure2->SetControlPoint( 1, pnt2, true ); mitk::Point2D pnt3; pnt3[0] = 12.5; pnt3[1] = 13.5; figure2->SetControlPoint( 2, pnt3, true ); mitk::Point2D pnt4; pnt4[0] = 12.5; pnt4[1] = 10.5; figure2->SetControlPoint( 3, pnt4, true ); figure2->GetPolyLine(0); testCase test; test.id = testCases.size(); test.figure = figure2; test.mean = 127.66; test.sd = 127.5; testCases.push_back( test ); } { /***************************** * 9 whole pixels (white) + 3 half pixels (white) * + 3 whole pixel (black) [ + 3 slightly less than half pixels (black)] * -> mean of 204.0 expected ******************************/ mitk::PlanarPolygon::Pointer figure2 = mitk::PlanarPolygon::New(); figure2->SetGeometry2D( geom ); mitk::Point2D pnt1; pnt1[0] = 0.5; pnt1[1] = 0.5; figure2->PlaceFigure( pnt1 ); mitk::Point2D pnt2; pnt2[0] = 3.5; pnt2[1] = 3.5; figure2->SetControlPoint( 1, pnt2, true ); mitk::Point2D pnt3; pnt3[0] = 8.4999; pnt3[1] = 3.5; figure2->SetControlPoint( 2, pnt3, true ); mitk::Point2D pnt4; pnt4[0] = 5.4999; pnt4[1] = 0.5; figure2->SetControlPoint( 3, pnt4, true ); figure2->GetPolyLine(0); testCase test; test.id = testCases.size(); test.figure = figure2; test.mean = 204.0; test.sd = 105.58; testCases.push_back( test ); } { /***************************** * half pixel (white) + whole pixel (white) + half pixel (black) * -> mean of 212.66 expected ******************************/ mitk::PlanarPolygon::Pointer figure2 = mitk::PlanarPolygon::New(); figure2->SetGeometry2D( geom ); mitk::Point2D pnt1; pnt1[0] = 9.5; pnt1[1] = 0.5; figure2->PlaceFigure( pnt1 ); mitk::Point2D pnt2; pnt2[0] = 9.5; pnt2[1] = 2.5; figure2->SetControlPoint( 1, pnt2, true ); mitk::Point2D pnt3; pnt3[0] = 11.5; pnt3[1] = 2.5; figure2->SetControlPoint( 2, pnt3, true ); figure2->GetPolyLine(0); testCase test; test.id = testCases.size(); test.figure = figure2; test.mean = 212.66; test.sd = 73.32; testCases.push_back( test ); } return testCases; } // loads the test image static mitk::Image::Pointer GetTestImage() { mitk::StandardFileLocations::Pointer locator = mitk::StandardFileLocations::GetInstance(); const std::string filename = locator->FindFile("testimage.dcm", "Modules/MitkExt/Testing/Data"); if (filename.empty()) { MITK_ERROR << "Could not find test file"; return NULL; } else { MITK_INFO << "Found testimage.dcm"; } - itk::FilenamesContainer file; - file.push_back( filename ); - - mitk::DicomSeriesReader* reader = new mitk::DicomSeriesReader; + MITK_TEST_OUTPUT(<< "Loading test image '" << filename << "'") + mitk::StringList files; + files.push_back( filename ); - mitk::DataNode::Pointer node = reader->LoadDicomSeries( file, false, false ); - mitk::Image::Pointer image = dynamic_cast( node->GetData() ); + mitk::ClassicDICOMSeriesReader::Pointer reader = mitk::ClassicDICOMSeriesReader::New(); + reader->SetInputFiles( files ); + reader->AnalyzeInputFiles(); + reader->LoadImages(); + MITK_TEST_CONDITION_REQUIRED( reader->GetNumberOfOutputs() == 1, "Loaded one result from file" ); + mitk::Image::Pointer image = reader->GetOutput(0).GetMitkImage(); + MITK_TEST_CONDITION_REQUIRED( image.IsNotNull(), "Loaded an mitk::Image" ); return image; } }; int TestUnitilizedImage() { /***************************** * loading uninitialized image to datastorage ******************************/ std::cout << "Testing loading uninitialized image to datastorage:"; try{ MITK_TEST_FOR_EXCEPTION_BEGIN(mitk::Exception) mitk::Image::Pointer image = mitk::Image::New(); mitk::DataNode::Pointer node = mitk::DataNode::New(); node->SetData(image); mitk::ImageStatisticsCalculator::Pointer is = mitk::ImageStatisticsCalculator::New(); is->ComputeStatistics(); MITK_TEST_FOR_EXCEPTION_END(mitk::Exception) } catch (const mitk::Exception&) { std::cout << "Success: Loading uninitialized image has thrown an exception." << std::endl; } return 0; } int mitkImageStatisticsCalculatorTest(int, char* []) { // always start with this! MITK_TEST_BEGIN("mitkImageStatisticsCalculatorTest") //QCoreApplication app(argc, argv); - mitk::Image::Pointer image = mitkImageStatisticsCalculatorTestClass::GetTestImage(); + mitk::Image::Pointer image = mitkImageStatisticsCalculatorTestClass::GetTestImage(); MITK_TEST_CONDITION_REQUIRED( image.IsNotNull(), "Loading test image" ); mitk::Geometry2D::Pointer geom = image->GetSlicedGeometry()->GetGeometry2D(0); std::vector allTestCases = mitkImageStatisticsCalculatorTestClass::InitializeTestCases( geom ); for ( std::vector::size_type i=0; i