diff --git a/Modules/CEST/autoload/IO/mitkCESTDICOMReaderService.cpp b/Modules/CEST/autoload/IO/mitkCESTDICOMReaderService.cpp index 805350c336..d4b4298683 100644 --- a/Modules/CEST/autoload/IO/mitkCESTDICOMReaderService.cpp +++ b/Modules/CEST/autoload/IO/mitkCESTDICOMReaderService.cpp @@ -1,115 +1,146 @@ /*=================================================================== 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 "mitkCESTDICOMReaderService.h" #include "mitkCESTIOMimeTypes.h" #include #include #include +#include "mitkCESTImageNormalizationFilter.h" #include namespace mitk { CESTDICOMReaderService::CESTDICOMReaderService() : BaseDICOMReaderService(CustomMimeType(MitkCESTIOMimeTypes::CEST_DICOM_MIMETYPE_NAME()), "MITK CEST DICOM Reader") { Options defaultOptions; std::vector parseStrategy; parseStrategy.push_back("Automatic"); parseStrategy.push_back("CEST/WASABI"); parseStrategy.push_back("T1"); defaultOptions["Force type"] = parseStrategy; std::vector mappingStrategy; mappingStrategy.push_back("Strict"); mappingStrategy.push_back("Fuzzy"); defaultOptions["Revision mapping"] = mappingStrategy; + std::vector normalizationStrategy; + normalizationStrategy.push_back("Automatic"); + normalizationStrategy.push_back("No"); + defaultOptions["Normalize data"] = normalizationStrategy; + + this->SetDefaultOptions(defaultOptions); this->RegisterService(); } DICOMFileReader::Pointer CESTDICOMReaderService::GetReader(const mitk::StringList &relevantFiles) const { mitk::DICOMFileReaderSelector::Pointer selector = mitk::DICOMFileReaderSelector::New(); selector->LoadBuiltIn3DConfigs(); selector->LoadBuiltIn3DnTConfigs(); selector->SetInputFiles(relevantFiles); mitk::DICOMFileReader::Pointer reader = selector->GetFirstReaderWithMinimumNumberOfOutputImages(); if (reader.IsNotNull()) { // reset tag cache to ensure that additional tags of interest // will be regarded by the reader if set later on. reader->SetTagCache(nullptr); } return reader; } std::vector> CESTDICOMReaderService::Read() { - std::vector result = BaseDICOMReaderService::Read(); + std::vector result; + std::vector dicomResult = BaseDICOMReaderService::Read(); const Options options = this->GetOptions(); const std::string parseStrategy = options.find("Force type")->second.ToString(); const std::string mappingStrategy = options.find("Revision mapping")->second.ToString(); + const std::string normalizationStrategy = options.find("Normalize data")->second.ToString(); - for (auto &item : result) + for (auto &item : dicomResult) { auto prop = item->GetProperty("files"); auto fileProp = dynamic_cast(prop.GetPointer()); if (!fileProp) { mitkThrow() << "Cannot load CEST file. Property \"files\" is missing after BaseDICOMReaderService::Read()."; } mitk::StringList relevantFiles = { fileProp->GetValue().GetTableValue(0) }; mitk::DICOMDCMTKTagScanner::Pointer scanner = mitk::DICOMDCMTKTagScanner::New(); DICOMTag siemensCESTprivateTag(0x0029, 0x1020); scanner->AddTag(siemensCESTprivateTag); scanner->SetInputFiles(relevantFiles); scanner->Scan(); mitk::DICOMTagCache::Pointer tagCache = scanner->GetScanCache(); DICOMImageFrameList imageFrameList = mitk::ConvertToDICOMImageFrameList(tagCache->GetFrameInfoList()); DICOMImageFrameInfo *firstFrame = imageFrameList.begin()->GetPointer(); std::string byteString = tagCache->GetTagValue(firstFrame, siemensCESTprivateTag).value; mitk::CustomTagParser tagParser(relevantFiles[0]); tagParser.SetParseStrategy(parseStrategy); tagParser.SetRevisionMappingStrategy(mappingStrategy); auto parsedPropertyList = tagParser.ParseDicomPropertyString(byteString); item->GetPropertyList()->ConcatenatePropertyList(parsedPropertyList); + + auto image = dynamic_cast(item.GetPointer()); + if (normalizationStrategy == "Automatic" && mitk::IsNotNormalizedCESTImage(image)) + { + MITK_INFO << "Unnormalized CEST image was loaded and will be normalized automatically."; + auto normalizationFilter = mitk::CESTImageNormalizationFilter::New(); + normalizationFilter->SetInput(image); + normalizationFilter->Update(); + auto normalizedImage = normalizationFilter->GetOutput(); + + auto nameProp = item->GetProperty("name"); + if (!nameProp) + { + mitkThrow() << "Cannot load CEST file. Property \"name\" is missing after BaseDICOMReaderService::Read()."; + } + normalizedImage->SetProperty("name", mitk::StringProperty::New(nameProp->GetValueAsString() + "_normalized")); + result.push_back(normalizedImage); + } + else + { + result.push_back(item); + } } return result; } CESTDICOMReaderService *CESTDICOMReaderService::Clone() const { return new CESTDICOMReaderService(*this); } } diff --git a/Modules/CEST/include/mitkCESTImageNormalizationFilter.h b/Modules/CEST/include/mitkCESTImageNormalizationFilter.h index 563fa22545..190fb4e123 100644 --- a/Modules/CEST/include/mitkCESTImageNormalizationFilter.h +++ b/Modules/CEST/include/mitkCESTImageNormalizationFilter.h @@ -1,79 +1,88 @@ /*=================================================================== 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 __mitkCESTImageNormalizationFilter_h #define __mitkCESTImageNormalizationFilter_h #include // MITK #include "mitkImageToImageFilter.h" namespace mitk { /** \brief Normalization filter for CEST images. * * This filter can be used to normalize CEST images, it only works with 4D images and assumes that the input * mitk::Image has a property called mitk::CustomTagParser::m_OffsetsPropertyName, whith offsets separated by * spaces. The number of offsets has to match the number of timesteps. * * Each timestep with a corresponding offset greater than 299 or less than -299 will be interpreted as normalization (M0) image. * If only one M0 image is present normalization will be done by dividing the voxel value by the corresponding * M0 voxel value. If multiple M0 images are present normalization between any two M0 images will be done by * dividing by a linear interpolation between the two. - * The M0 images themselves will be transferred to the result without any processing. - * - * The output image will have the same geometry as the input image and a double pixel type. + * The M0 images themselves will be removed from the result. + * The output image will have the same 3D geometry as the input image, a time geometry only consisting of non M0 images and a double pixel type. */ class MITKCEST_EXPORT CESTImageNormalizationFilter : public ImageToImageFilter { public: mitkClassMacro(CESTImageNormalizationFilter, ImageToImageFilter); itkFactorylessNewMacro(Self) itkCloneMacro(Self) protected: /*! \brief standard constructor */ CESTImageNormalizationFilter(); /*! \brief standard destructor */ ~CESTImageNormalizationFilter() override; /*! \brief Method generating the output information of this filter (e.g. image dimension, image type, etc.). The interface ImageToImageFilter requires this implementation. Everything is taken from the input image. */ void GenerateOutputInformation() override; /*! \brief Method generating the output of this filter. Called in the updated process of the pipeline. This method generates the normalized output image. */ void GenerateData() override; /** Internal templated method that normalizes across timesteps */ template void NormalizeTimeSteps(const itk::Image* image); /// Offsets without M0s std::string m_RealOffsets; /// non M0 indices std::vector< unsigned int > m_NonM0Indices; }; + + /** This helper function can be used to check if an image was already normalized. + * The function assumes that an image that is not normalized is indicated by the following properties: + * - mitk::Image has a property called mitk::CustomTagParser::m_OffsetsPropertyName, whith offsets separated by spaces. + * - The number of offsets has to match the number of timesteps. + * - At least one of the offsets is a normalization (M0) image. M0 are indicated by offsets greater than 299 or less than -299. + */ + MITKCEST_EXPORT bool IsNotNormalizedCESTImage(const Image* cestImage); + } // END mitk namespace + #endif diff --git a/Modules/CEST/src/mitkCESTImageNormalizationFilter.cpp b/Modules/CEST/src/mitkCESTImageNormalizationFilter.cpp index 5af92a31da..9b8b6ff04a 100644 --- a/Modules/CEST/src/mitkCESTImageNormalizationFilter.cpp +++ b/Modules/CEST/src/mitkCESTImageNormalizationFilter.cpp @@ -1,198 +1,233 @@ /*=================================================================== 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 "mitkCESTImageNormalizationFilter.h" #include #include #include #include -#include #include mitk::CESTImageNormalizationFilter::CESTImageNormalizationFilter() { } mitk::CESTImageNormalizationFilter::~CESTImageNormalizationFilter() { } void mitk::CESTImageNormalizationFilter::GenerateData() { mitk::Image::ConstPointer inputImage = this->GetInput(0); if ((inputImage->GetDimension() != 4)) { mitkThrow() << "mitk::CESTImageNormalizationFilter:GenerateData works only with 4D images, sorry."; return; } auto resultMitkImage = this->GetOutput(); AccessFixedDimensionByItk(inputImage, NormalizeTimeSteps, 4); auto originalTimeGeometry = this->GetInput()->GetTimeGeometry(); auto resultTimeGeometry = mitk::ProportionalTimeGeometry::New(); unsigned int numberOfNonM0s = m_NonM0Indices.size(); resultTimeGeometry->Expand(numberOfNonM0s); for (unsigned int index = 0; index < numberOfNonM0s; ++index) { resultTimeGeometry->SetTimeStepGeometry(originalTimeGeometry->GetGeometryCloneForTimeStep(m_NonM0Indices.at(index)), index); } resultMitkImage->SetTimeGeometry(resultTimeGeometry); resultMitkImage->SetPropertyList(this->GetInput()->GetPropertyList()->Clone()); resultMitkImage->GetPropertyList()->SetStringProperty(mitk::CustomTagParser::m_OffsetsPropertyName.c_str(), m_RealOffsets.c_str()); // remove uids resultMitkImage->GetPropertyList()->DeleteProperty("DICOM.0008.0018"); resultMitkImage->GetPropertyList()->DeleteProperty("DICOM.0020.000D"); resultMitkImage->GetPropertyList()->DeleteProperty("DICOM.0020.000E"); } +std::vector ExtractOffsets(const mitk::Image* image) +{ + std::vector result; + + if (image) + { + std::string offsets = ""; + std::vector parts; + if (image->GetPropertyList()->GetStringProperty(mitk::CustomTagParser::m_OffsetsPropertyName.c_str(), offsets)) + { + boost::algorithm::trim(offsets); + boost::split(parts, offsets, boost::is_any_of(" ")); + + for (auto part : parts) + { + std::istringstream iss(part); + iss.imbue(std::locale("C")); + double d; + iss >> d; + result.push_back(d); + } + } + } + + return result; +} + + template void mitk::CESTImageNormalizationFilter::NormalizeTimeSteps(const itk::Image* image) { - mitk::LocaleSwitch localeSwitch("C"); typedef itk::Image ImageType; typedef itk::Image OutputImageType; - std::string offsets = ""; - this->GetInput()->GetPropertyList()->GetStringProperty(mitk::CustomTagParser::m_OffsetsPropertyName.c_str(), offsets); - boost::algorithm::trim(offsets); - - std::vector parts; - boost::split(parts, offsets, boost::is_any_of(" ")); + auto offsets = ExtractOffsets(this->GetInput()); // determine normalization images std::vector mZeroIndices; std::stringstream offsetsWithoutM0; + offsetsWithoutM0.imbue(std::locale("C")); m_NonM0Indices.clear(); - for (unsigned int index = 0; index < parts.size(); ++index) + for (unsigned int index = 0; index < offsets.size(); ++index) { - if ((std::stod(parts.at(index)) < -299) || (std::stod(parts.at(index)) > 299)) + if ((offsets.at(index) < -299) || (offsets.at(index) > 299)) { mZeroIndices.push_back(index); } else { - offsetsWithoutM0 << parts.at(index) << " "; + offsetsWithoutM0 << offsets.at(index) << " "; m_NonM0Indices.push_back(index); } } - auto resultImage = OutputImageType::New(); typename ImageType::RegionType targetEntireRegion = image->GetLargestPossibleRegion(); targetEntireRegion.SetSize(3, m_NonM0Indices.size()); resultImage->SetRegions(targetEntireRegion); resultImage->Allocate(); resultImage->FillBuffer(0); unsigned int numberOfTimesteps = image->GetLargestPossibleRegion().GetSize(3); typename ImageType::RegionType lowerMZeroRegion = image->GetLargestPossibleRegion(); lowerMZeroRegion.SetSize(3, 1); typename ImageType::RegionType upperMZeroRegion = image->GetLargestPossibleRegion(); upperMZeroRegion.SetSize(3, 1); typename ImageType::RegionType sourceRegion = image->GetLargestPossibleRegion(); sourceRegion.SetSize(3, 1); typename OutputImageType::RegionType targetRegion = resultImage->GetLargestPossibleRegion(); targetRegion.SetSize(3, 1); unsigned int targetTimestep = 0; for (unsigned int sourceTimestep = 0; sourceTimestep < numberOfTimesteps; ++sourceTimestep) { unsigned int lowerMZeroIndex = mZeroIndices[0]; unsigned int upperMZeroIndex = mZeroIndices[0]; for (unsigned int loop = 0; loop < mZeroIndices.size(); ++loop) { if (mZeroIndices[loop] <= sourceTimestep) { lowerMZeroIndex = mZeroIndices[loop]; } if (mZeroIndices[loop] > sourceTimestep) { upperMZeroIndex = mZeroIndices[loop]; break; } } bool isMZero = (lowerMZeroIndex == sourceTimestep); double weight = 0.0; if (lowerMZeroIndex == upperMZeroIndex) { weight = 1.0; } else { weight = 1.0 - double(sourceTimestep - lowerMZeroIndex) / double(upperMZeroIndex - lowerMZeroIndex); } if (isMZero) { //do nothing } else { lowerMZeroRegion.SetIndex(3, lowerMZeroIndex); upperMZeroRegion.SetIndex(3, upperMZeroIndex); sourceRegion.SetIndex(3, sourceTimestep); targetRegion.SetIndex(3, targetTimestep); itk::ImageRegionConstIterator lowerMZeroIterator(image, lowerMZeroRegion); itk::ImageRegionConstIterator upperMZeroIterator(image, upperMZeroRegion); itk::ImageRegionConstIterator sourceIterator(image, sourceRegion); itk::ImageRegionIterator targetIterator(resultImage.GetPointer(), targetRegion); while (!sourceIterator.IsAtEnd()) { double normalizationFactor = weight * lowerMZeroIterator.Get() + (1.0 - weight) * upperMZeroIterator.Get(); if (mitk::Equal(normalizationFactor, 0)) { targetIterator.Set(0); } else { targetIterator.Set(double(sourceIterator.Get()) / normalizationFactor); } ++lowerMZeroIterator; ++upperMZeroIterator; ++sourceIterator; ++targetIterator; } ++targetTimestep; } } // get Pointer to output image mitk::Image::Pointer resultMitkImage = this->GetOutput(); // write into output image mitk::CastToMitkImage(resultImage, resultMitkImage); m_RealOffsets = offsetsWithoutM0.str(); } void mitk::CESTImageNormalizationFilter::GenerateOutputInformation() { mitk::Image::ConstPointer input = this->GetInput(); mitk::Image::Pointer output = this->GetOutput(); itkDebugMacro(<< "GenerateOutputInformation()"); } + +bool mitk::IsNotNormalizedCESTImage(const Image* cestImage) +{ + auto offsets = ExtractOffsets(cestImage); + + for (auto offset : offsets) + { + if (offset < -299 || offset > 299) + { + return true; + } + } + return false; +}; diff --git a/Modules/CEST/test/mitkCESTDICOMReaderServiceTest.cpp b/Modules/CEST/test/mitkCESTDICOMReaderServiceTest.cpp index 3f0f946877..70eb65e862 100644 --- a/Modules/CEST/test/mitkCESTDICOMReaderServiceTest.cpp +++ b/Modules/CEST/test/mitkCESTDICOMReaderServiceTest.cpp @@ -1,81 +1,97 @@ /*=================================================================== 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. ===================================================================*/ // Testing #include "mitkTestFixture.h" #include "mitkTestingMacros.h" // std includes #include // MITK includes #include #include #include // VTK includes #include class mitkCESTDICOMReaderServiceTestSuite : public mitk::TestFixture { CPPUNIT_TEST_SUITE(mitkCESTDICOMReaderServiceTestSuite); // Test the dicom property parsing MITK_TEST(LoadCESTDICOMData_Success); + MITK_TEST(LoadNormalizedCESTDICOMData_Success); MITK_TEST(LoadT1DICOMData_Success); CPPUNIT_TEST_SUITE_END(); private: public: void setUp() override { } void tearDown() override { } void LoadCESTDICOMData_Success() { mitk::IFileReader::Options options; options["Force type"] = std::string( "Automatic" ); options["Revision mapping"] = std::string( "Strict" ); + options["Normalize data"] = std::string("No"); mitk::Image::Pointer cestImage = mitk::IOUtil::Load(GetTestDataFilePath("CEST/B1=0.6MUT/MEI_NER_PHANTOM.MR.E0202_MEISSNER.0587.0001.2017.10.25.22.11.10.373351.41828677.IMA"), options); CPPUNIT_ASSERT_MESSAGE("Make certain offsets have been correctly loaded for CEST image." ,cestImage->GetProperty("CEST.Offsets")->GetValueAsString() == "-300 2 -2 1.92982 -1.92982 1.85965 -1.85965 1.78947 -1.78947 1.7193 -1.7193 1.64912 -1.64912 1.57895 -1.57895 1.50877 -1.50877 1.4386 -1.4386 1.36842 -1.36842 1.29825 -1.29825 1.22807 -1.22807 1.15789 -1.15789 1.08772 -1.08772 1.01754 -1.01754 0.947368 -0.947368 0.877193 -0.877193 0.807018 -0.807018 0.736842 -0.736842 0.666667 -0.666667 0.596491 -0.596491 0.526316 -0.526316 0.45614 -0.45614 0.385965 -0.385965 0.315789 -0.315789 0.245614 -0.245614 0.175439 -0.175439 0.105263 -0.105263 0.0350877 -0.0350877"); std::string temp; CPPUNIT_ASSERT_MESSAGE("Make certain image is not loaded as T1.", !cestImage->GetPropertyList()->GetStringProperty("CEST.TREC", temp)); } + void LoadNormalizedCESTDICOMData_Success() + { + mitk::IFileReader::Options options; + options["Force type"] = std::string("Automatic"); + options["Revision mapping"] = std::string("Strict"); + options["Normalize data"] = std::string("Automatic"); + + mitk::Image::Pointer cestImage = mitk::IOUtil::Load(GetTestDataFilePath("CEST/B1=0.6MUT/MEI_NER_PHANTOM.MR.E0202_MEISSNER.0587.0001.2017.10.25.22.11.10.373351.41828677.IMA"), options); + auto tempREsult = cestImage->GetProperty("CEST.Offsets")->GetValueAsString(); + CPPUNIT_ASSERT_MESSAGE("Make certain offsets have been correctly loaded for CEST image.", cestImage->GetProperty("CEST.Offsets")->GetValueAsString() == "2 -2 1.92982 -1.92982 1.85965 -1.85965 1.78947 -1.78947 1.7193 -1.7193 1.64912 -1.64912 1.57895 -1.57895 1.50877 -1.50877 1.4386 -1.4386 1.36842 -1.36842 1.29825 -1.29825 1.22807 -1.22807 1.15789 -1.15789 1.08772 -1.08772 1.01754 -1.01754 0.947368 -0.947368 0.877193 -0.877193 0.807018 -0.807018 0.736842 -0.736842 0.666667 -0.666667 0.596491 -0.596491 0.526316 -0.526316 0.45614 -0.45614 0.385965 -0.385965 0.315789 -0.315789 0.245614 -0.245614 0.175439 -0.175439 0.105263 -0.105263 0.0350877 -0.0350877 "); + std::string temp; + CPPUNIT_ASSERT_MESSAGE("Make certain image is not loaded as T1.", !cestImage->GetPropertyList()->GetStringProperty("CEST.TREC", temp)); + } + void LoadT1DICOMData_Success() { mitk::IFileReader::Options options; options["Force type"] = std::string("Automatic"); options["Revision mapping"] = std::string("Strict"); mitk::Image::Pointer cestImage = mitk::IOUtil::Load(GetTestDataFilePath("CEST/T1MAP/MEI_NER_PHANTOM.MR.E0202_MEISSNER.0279.0001.2017.10.25.20.21.27.996834.41803047.IMA"), options); std::string temp; CPPUNIT_ASSERT_MESSAGE("Make certain image is loaded as T1.", cestImage->GetPropertyList()->GetStringProperty("CEST.TREC", temp)); } }; MITK_TEST_SUITE_REGISTRATION(mitkCESTDICOMReaderService) diff --git a/Plugins/org.mitk.gui.qt.cest/files.cmake b/Plugins/org.mitk.gui.qt.cest/files.cmake index 9221869a59..54f1352424 100644 --- a/Plugins/org.mitk.gui.qt.cest/files.cmake +++ b/Plugins/org.mitk.gui.qt.cest/files.cmake @@ -1,43 +1,47 @@ set(SRC_CPP_FILES QmitkImageStatisticsCalculationThread.cpp ) set(INTERNAL_CPP_FILES org_mitk_gui_qt_cest_Activator.cpp QmitkCESTStatisticsView.cpp + QmitkCESTNormalizeView.cpp ) set(UI_FILES src/internal/QmitkCESTStatisticsViewControls.ui + src/internal/QmitkCESTNormalizeViewControls.ui ) set(MOC_H_FILES src/internal/org_mitk_gui_qt_cest_Activator.h src/internal/QmitkCESTStatisticsView.h + src/internal/QmitkCESTNormalizeView.h src/QmitkImageStatisticsCalculationThread.h ) # list of resource files which can be used by the plug-in # system without loading the plug-ins shared library, # for example the icon used in the menu and tabs for the # plug-in views in the workbench set(CACHED_RESOURCE_FILES resources/icon.svg + resources/icon_norm.png plugin.xml ) # list of Qt .qrc files which contain additional resources # specific to this plugin set(QRC_FILES ) set(CPP_FILES ) foreach(file ${SRC_CPP_FILES}) set(CPP_FILES ${CPP_FILES} src/${file}) endforeach(file ${SRC_CPP_FILES}) foreach(file ${INTERNAL_CPP_FILES}) set(CPP_FILES ${CPP_FILES} src/internal/${file}) endforeach(file ${INTERNAL_CPP_FILES}) diff --git a/Plugins/org.mitk.gui.qt.cest/plugin.xml b/Plugins/org.mitk.gui.qt.cest/plugin.xml index e44ea5da6e..fbfb259d10 100644 --- a/Plugins/org.mitk.gui.qt.cest/plugin.xml +++ b/Plugins/org.mitk.gui.qt.cest/plugin.xml @@ -1,12 +1,18 @@ - - + + + diff --git a/Plugins/org.mitk.gui.qt.cest/resources/icon_norm.png b/Plugins/org.mitk.gui.qt.cest/resources/icon_norm.png new file mode 100644 index 0000000000..66fcc6795d Binary files /dev/null and b/Plugins/org.mitk.gui.qt.cest/resources/icon_norm.png differ diff --git a/Plugins/org.mitk.gui.qt.cest/src/internal/QmitkCESTNormalizeView.cpp b/Plugins/org.mitk.gui.qt.cest/src/internal/QmitkCESTNormalizeView.cpp new file mode 100644 index 0000000000..01cd8935e5 --- /dev/null +++ b/Plugins/org.mitk.gui.qt.cest/src/internal/QmitkCESTNormalizeView.cpp @@ -0,0 +1,114 @@ +/*=================================================================== + +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 "QmitkCESTNormalizeView.h" + +#include + +#include "mitkWorkbenchUtil.h" + +#include "mitkNodePredicateAnd.h" +#include "mitkNodePredicateDataProperty.h" +#include "mitkNodePredicateDataType.h" + +#include "QmitkDataStorageComboBoxWithSelectNone.h" + +#include +#include "mitkCESTImageNormalizationFilter.h" +#include "mitkCustomTagParser.h" + +const std::string QmitkCESTNormalizeView::VIEW_ID = "org.mitk.gui.qt.cest.normalize"; + +void QmitkCESTNormalizeView::SetFocus() +{ + m_Controls.btnNormalize->setFocus(); +} + +void QmitkCESTNormalizeView::CreateQtPartControl(QWidget* parent) +{ + m_Controls.setupUi(parent); + + m_Controls.btnNormalize->setEnabled(false); + + m_Controls.comboCESTImage->SetPredicate(this->m_IsCESTImagePredicate); + m_Controls.comboCESTImage->SetDataStorage(this->GetDataStorage()); + + connect(m_Controls.btnNormalize, SIGNAL(clicked()), this, SLOT(OnNormalizeButtonClicked())); + connect(m_Controls.comboCESTImage, SIGNAL(OnSelectionChanged(const mitk::DataNode *)), this, SLOT(UpdateGUIControls())); + + UpdateGUIControls(); +} + +void QmitkCESTNormalizeView::UpdateGUIControls() +{ + m_Controls.btnNormalize->setEnabled(m_Controls.comboCESTImage->GetSelectedNode().IsNotNull()); +} + +void QmitkCESTNormalizeView::OnNormalizeButtonClicked() +{ + auto selectedImageNode = m_Controls.comboCESTImage->GetSelectedNode(); + if (!selectedImageNode) + { + MITK_ERROR << "Invalid system state. CEST selection is invalid. Selected node is null_ptr."; + return; + } + + auto selectedImage = dynamic_cast(selectedImageNode->GetData()); + if (!selectedImageNode) + { + MITK_ERROR << "Invalid system state. CEST selection is invalid. Selected node is not an image."; + return; + } + + std::string offsetsStr = ""; + bool hasOffsets = selectedImage->GetPropertyList()->GetStringProperty(mitk::CustomTagParser::m_OffsetsPropertyName.c_str(), offsetsStr); + if (!hasOffsets) + { + QMessageBox::information(nullptr, "CEST normalization", "Selected image was missing CEST offset information."); + return; + } + + if (!mitk::IsNotNormalizedCESTImage(selectedImage)) + { + QMessageBox::information(nullptr, "CEST normalization", "Selected image already seems to be normalized."); + return; + } + + if (selectedImage->GetDimension() == 4) + { + auto normalizationFilter = mitk::CESTImageNormalizationFilter::New(); + normalizationFilter->SetInput(selectedImage); + normalizationFilter->Update(); + + auto resultImage = normalizationFilter->GetOutput(); + + mitk::DataNode::Pointer dataNode = mitk::DataNode::New(); + dataNode->SetData(resultImage); + + std::string normalizedName = selectedImageNode->GetName() + "_normalized"; + dataNode->SetName(normalizedName); + + this->GetDataStorage()->Add(dataNode); + } +} + +QmitkCESTNormalizeView::QmitkCESTNormalizeView() +{ + auto isImage = mitk::NodePredicateDataType::New("Image"); + auto isCESTImage = mitk::NodePredicateDataProperty::New("CEST.TotalScanTime"); + + this->m_IsCESTImagePredicate = mitk::NodePredicateAnd::New(isImage, isCESTImage).GetPointer(); +} diff --git a/Plugins/org.mitk.gui.qt.cest/src/internal/QmitkCESTNormalizeView.h b/Plugins/org.mitk.gui.qt.cest/src/internal/QmitkCESTNormalizeView.h new file mode 100644 index 0000000000..0e5cd1f0b4 --- /dev/null +++ b/Plugins/org.mitk.gui.qt.cest/src/internal/QmitkCESTNormalizeView.h @@ -0,0 +1,78 @@ +/*=================================================================== + +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 QmitkCESTNormalizeView_h +#define QmitkCESTNormalizeView_h + +#include + +#include + +#include + +#include "ui_QmitkCESTNormalizeViewControls.h" + +#include + +/*! +* @brief Test Plugin for SUV calculations of PET images +*/ +class QmitkCESTNormalizeView : public QmitkAbstractView +{ + Q_OBJECT + +public: + + /*! @brief The view's unique ID - required by MITK */ + static const std::string VIEW_ID; + + QmitkCESTNormalizeView(); + +protected slots: + + void OnNormalizeButtonClicked(); + + /**Sets visibility and enabled state of the GUI depending on the settings and workflow state.*/ + void UpdateGUIControls(); + +protected: + using SelectedDataNodeVectorType = QList; + + // Overridden base class functions + + /*! + * @brief Sets up the UI controls and connects the slots and signals. Gets + * called by the framework to create the GUI at the right time. + * @param[in,out] parent The parent QWidget, as this class itself is not a QWidget + * subclass. + */ + virtual void CreateQtPartControl(QWidget* parent) override; + + /*! + * @brief Sets the focus to the plot curve button. Gets called by the framework to set the + * focus on the right widget. + */ + virtual void SetFocus() override; + + // Variables + + /*! @brief The view's UI controls */ + Ui::QmitkCESTNormalizeViewControls m_Controls; + + mitk::NodePredicateBase::Pointer m_IsCESTImagePredicate; +}; + +#endif \ No newline at end of file diff --git a/Plugins/org.mitk.gui.qt.cest/src/internal/QmitkCESTNormalizeViewControls.ui b/Plugins/org.mitk.gui.qt.cest/src/internal/QmitkCESTNormalizeViewControls.ui new file mode 100644 index 0000000000..4494ad57cd --- /dev/null +++ b/Plugins/org.mitk.gui.qt.cest/src/internal/QmitkCESTNormalizeViewControls.ui @@ -0,0 +1,79 @@ + + + QmitkCESTNormalizeViewControls + + + + 0 + 0 + 330 + 135 + + + + + 0 + 0 + + + + QmitkTemplate + + + + + + + + Selected CEST image: + + + + + + + true + + + + 0 + 0 + + + + + + + + + + Normalize CEST image + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + QmitkDataStorageComboBoxWithSelectNone + QWidget +
QmitkDataStorageComboBoxWithSelectNone.h
+
+
+ + +
diff --git a/Plugins/org.mitk.gui.qt.cest/src/internal/QmitkCESTStatisticsView.cpp b/Plugins/org.mitk.gui.qt.cest/src/internal/QmitkCESTStatisticsView.cpp index 4040af43a7..b3d856c624 100644 --- a/Plugins/org.mitk.gui.qt.cest/src/internal/QmitkCESTStatisticsView.cpp +++ b/Plugins/org.mitk.gui.qt.cest/src/internal/QmitkCESTStatisticsView.cpp @@ -1,956 +1,896 @@ /*=================================================================== 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. ===================================================================*/ //itk #include "itksys/SystemTools.hxx" #include #include // Blueberry #include #include // Qmitk #include "QmitkCESTStatisticsView.h" // Qt #include #include // qwt #include // mitk #include #include #include #include #include #include #include #include #include #include // boost #include #include //stl #include #include #include #include #include #include namespace { template void GetSortPermutation(std::vector &out, const std::vector &determiningVector, Compare compare = std::less()) { out.resize(determiningVector.size()); std::iota(out.begin(), out.end(), 0); std::sort(out.begin(), out.end(), [&](unsigned i, unsigned j) { return compare(determiningVector[i], determiningVector[j]); }); } template void ApplyPermutation(const std::vector &order, std::vector &vectorToSort) { assert(order.size() == vectorToSort.size()); std::vector tempVector(vectorToSort.size()); for (unsigned i = 0; i < vectorToSort.size(); i++) { tempVector[i] = vectorToSort[order[i]]; } vectorToSort = tempVector; } template void ApplyPermutation(const std::vector &order, std::vector ¤tVector, std::vector &... remainingVectors) { ApplyPermutation(order, currentVector); ApplyPermutation(order, remainingVectors...); } template void SortVectors(const std::vector &orderDeterminingVector, Compare comparison, std::vector &... vectorsToBeSorted) { std::vector order; GetSortPermutation(order, orderDeterminingVector, comparison); ApplyPermutation(order, vectorsToBeSorted...); } } const std::string QmitkCESTStatisticsView::VIEW_ID = "org.mitk.views.ceststatistics"; static const int STAT_TABLE_BASE_HEIGHT = 180; QmitkCESTStatisticsView::QmitkCESTStatisticsView(QObject* /*parent*/, const char* /*name*/) { this->m_CalculatorThread = new QmitkImageStatisticsCalculationThread; m_currentSelectedPosition.Fill(0.0); m_currentSelectedTimeStep = 0; m_CrosshairPointSet = mitk::PointSet::New(); } QmitkCESTStatisticsView::~QmitkCESTStatisticsView() { while (this->m_CalculatorThread->isRunning()) // wait until thread has finished { itksys::SystemTools::Delay(100); } delete this->m_CalculatorThread; } void QmitkCESTStatisticsView::SetFocus() { m_Controls.threeDimToFourDimPushButton->setFocus(); } void QmitkCESTStatisticsView::CreateQtPartControl( QWidget *parent ) { // create GUI widgets from the Qt Designer's .ui file m_Controls.setupUi( parent ); connect(m_Controls.threeDimToFourDimPushButton, SIGNAL(clicked()), this, SLOT(OnThreeDimToFourDimPushButtonClicked())); connect((QObject*) this->m_CalculatorThread, SIGNAL(finished()), this, SLOT(OnThreadedStatisticsCalculationEnds()), Qt::QueuedConnection); connect((QObject*)(this->m_Controls.m_CopyStatisticsToClipboardPushButton), SIGNAL(clicked()), (QObject*) this, SLOT(OnCopyStatisticsToClipboardPushButtonClicked())); - connect((QObject*)(this->m_Controls.normalizeImagePushButton), SIGNAL(clicked()), (QObject*) this, SLOT(OnNormalizeImagePushButtonClicked())); connect((QObject*)(this->m_Controls.fixedRangeCheckBox), SIGNAL(toggled(bool)), (QObject*) this, SLOT(OnFixedRangeCheckBoxToggled(bool))); connect((QObject*)(this->m_Controls.fixedRangeLowerDoubleSpinBox), SIGNAL(editingFinished()), (QObject*) this, SLOT(OnFixedRangeDoubleSpinBoxChanged())); connect((QObject*)(this->m_Controls.fixedRangeUpperDoubleSpinBox), SIGNAL(editingFinished()), (QObject*) this, SLOT(OnFixedRangeDoubleSpinBoxChanged())); - m_Controls.normalizeImagePushButton->setEnabled(false); m_Controls.threeDimToFourDimPushButton->setEnabled(false); this->m_SliceChangeListener.RenderWindowPartActivated(this->GetRenderWindowPart()); connect(&m_SliceChangeListener, SIGNAL(SliceChanged()), this, SLOT(OnSliceChanged())); } void QmitkCESTStatisticsView::RenderWindowPartActivated(mitk::IRenderWindowPart* renderWindowPart) { this->m_SliceChangeListener.RenderWindowPartActivated(renderWindowPart); } void QmitkCESTStatisticsView::RenderWindowPartDeactivated( mitk::IRenderWindowPart* renderWindowPart) { this->m_SliceChangeListener.RenderWindowPartDeactivated(renderWindowPart); } void QmitkCESTStatisticsView::OnSelectionChanged( berry::IWorkbenchPart::Pointer /*source*/, const QList& nodes ) { if (nodes.empty()) { std::stringstream message; message << "Please select an image."; m_Controls.labelWarning->setText(message.str().c_str()); m_Controls.labelWarning->show(); this->Clear(); return; } // iterate all selected objects bool atLeastOneWasCESTImage = false; foreach( mitk::DataNode::Pointer node, nodes ) { if (node.IsNull()) { continue; } if( dynamic_cast(node->GetData()) != nullptr ) { m_Controls.labelWarning->setVisible( false ); bool zSpectrumSet = SetZSpectrum(dynamic_cast(node->GetData()->GetProperty(mitk::CustomTagParser::m_OffsetsPropertyName.c_str()).GetPointer())); atLeastOneWasCESTImage = atLeastOneWasCESTImage || zSpectrumSet; if (zSpectrumSet) { m_ZImage = dynamic_cast(node->GetData()); } else { m_MaskImage = dynamic_cast(node->GetData()); } } if (dynamic_cast(node->GetData()) != nullptr) { m_MaskPlanarFigure = dynamic_cast(node->GetData()); } if (dynamic_cast(node->GetData()) != nullptr) { m_PointSet = dynamic_cast(node->GetData()); } } // We only want to offer normalization or timestep copying if one object is selected if (nodes.size() == 1) { if (dynamic_cast(nodes.front()->GetData()) ) { - m_Controls.normalizeImagePushButton->setEnabled(atLeastOneWasCESTImage); m_Controls.threeDimToFourDimPushButton->setDisabled(atLeastOneWasCESTImage); } else { - m_Controls.normalizeImagePushButton->setEnabled(false); m_Controls.threeDimToFourDimPushButton->setEnabled(false); std::stringstream message; message << "The selected node is not an image."; m_Controls.labelWarning->setText(message.str().c_str()); m_Controls.labelWarning->show(); } this->Clear(); return; } // we always need a mask, either image or planar figure as well as an image for further processing if (nodes.size() != 2) { this->Clear(); return; } - m_Controls.normalizeImagePushButton->setEnabled(false); m_Controls.threeDimToFourDimPushButton->setEnabled(false); if (!atLeastOneWasCESTImage) { std::stringstream message; message << "None of the selected data nodes contains required CEST meta information"; m_Controls.labelWarning->setText(message.str().c_str()); m_Controls.labelWarning->show(); this->Clear(); return; } bool bothAreImages = (m_ZImage.GetPointer() != nullptr) && (m_MaskImage.GetPointer() != nullptr); if (bothAreImages) { bool geometriesMatch = mitk::Equal(*(m_ZImage->GetTimeGeometry()), *(m_MaskImage->GetTimeGeometry()), mitk::eps, false); if (!geometriesMatch) { std::stringstream message; message << "The selected images have different geometries."; m_Controls.labelWarning->setText(message.str().c_str()); m_Controls.labelWarning->show(); this->Clear(); return; } } if (!this->DataSanityCheck()) { this->Clear(); return; } if (m_PointSet.IsNull()) { // initialize thread and trigger it this->m_CalculatorThread->SetIgnoreZeroValueVoxel(false); this->m_CalculatorThread->Initialize(m_ZImage, m_MaskImage, m_MaskPlanarFigure); std::stringstream message; message << "Calculating statistics..."; m_Controls.labelWarning->setText(message.str().c_str()); m_Controls.labelWarning->show(); try { // Compute statistics this->m_CalculatorThread->start(); } catch (const mitk::Exception& e) { std::stringstream message; message << "" << e.GetDescription() << ""; m_Controls.labelWarning->setText(message.str().c_str()); m_Controls.labelWarning->show(); } catch (const std::runtime_error &e) { // In case of exception, print error message on GUI std::stringstream message; message << "" << e.what() << ""; m_Controls.labelWarning->setText(message.str().c_str()); m_Controls.labelWarning->show(); } catch (const std::exception &e) { MITK_ERROR << "Caught exception: " << e.what(); // In case of exception, print error message on GUI std::stringstream message; message << "Error! Unequal Dimensions of Image and Segmentation. No recompute possible "; m_Controls.labelWarning->setText(message.str().c_str()); m_Controls.labelWarning->show(); } while (this->m_CalculatorThread->isRunning()) // wait until thread has finished { itksys::SystemTools::Delay(100); } } if (m_PointSet.IsNotNull()) { if (m_ZImage->GetDimension() == 4) { AccessFixedDimensionByItk(m_ZImage, PlotPointSet, 4); } else { MITK_WARN << "Expecting a 4D image."; } } } void QmitkCESTStatisticsView::OnThreadedStatisticsCalculationEnds() { this->m_Controls.m_DataViewWidget->SetAxisTitle(QwtPlot::Axis::xBottom, "delta w"); this->m_Controls.m_DataViewWidget->SetAxisTitle(QwtPlot::Axis::yLeft, "z"); const std::vector &statistics = this->m_CalculatorThread->GetStatisticsData(); QmitkPlotWidget::DataVector::size_type numberOfSpectra = this->m_zSpectrum.size(); QmitkPlotWidget::DataVector means(numberOfSpectra); QmitkPlotWidget::DataVector stdevs(numberOfSpectra); for (unsigned int index = 0; index < numberOfSpectra; ++index) { means[index] = statistics[index]->GetMean(); stdevs[index] = statistics[index]->GetStd(); } QmitkPlotWidget::DataVector xValues = this->m_zSpectrum; RemoveMZeros(xValues, means, stdevs); ::SortVectors(xValues, std::less(), xValues, means, stdevs); unsigned int curveId = this->m_Controls.m_DataViewWidget->InsertCurve("Spectrum"); this->m_Controls.m_DataViewWidget->SetCurveData(curveId, xValues, means, stdevs, stdevs); this->m_Controls.m_DataViewWidget->SetErrorPen(curveId, QPen(Qt::blue)); QwtSymbol* blueSymbol = new QwtSymbol(QwtSymbol::Rect, QColor(Qt::blue), QColor(Qt::blue), QSize(8, 8)); this->m_Controls.m_DataViewWidget->SetCurveSymbol(curveId, blueSymbol); this->m_Controls.m_DataViewWidget->SetLegendAttribute(curveId, QwtPlotCurve::LegendShowSymbol); QwtLegend* legend = new QwtLegend(); legend->setFrameShape(QFrame::Box); legend->setFrameShadow(QFrame::Sunken); legend->setLineWidth(1); this->m_Controls.m_DataViewWidget->SetLegend(legend, QwtPlot::BottomLegend); m_Controls.m_DataViewWidget->GetPlot()->axisScaleEngine(QwtPlot::Axis::xBottom)->setAttributes(QwtScaleEngine::Inverted); this->m_Controls.m_DataViewWidget->Replot(); m_Controls.labelWarning->setVisible(false); if (this->m_Controls.fixedRangeCheckBox->isChecked()) { this->m_Controls.m_DataViewWidget->GetPlot()->setAxisAutoScale(2, false); this->m_Controls.m_DataViewWidget->GetPlot()->setAxisScale(2, this->m_Controls.fixedRangeLowerDoubleSpinBox->value(), this->m_Controls.fixedRangeUpperDoubleSpinBox->value()); } else { this->m_Controls.m_DataViewWidget->GetPlot()->setAxisAutoScale(2, true); } if(this->DataSanityCheck()) { this->FillStatisticsTableView(this->m_CalculatorThread->GetStatisticsData(), this->m_CalculatorThread->GetStatisticsImage()); } else { this->Clear(); } } void QmitkCESTStatisticsView::OnFixedRangeDoubleSpinBoxChanged() { if (this->m_Controls.fixedRangeCheckBox->isChecked()) { this->m_Controls.m_DataViewWidget->GetPlot()->setAxisAutoScale(2, false); this->m_Controls.m_DataViewWidget->GetPlot()->setAxisScale(2, this->m_Controls.fixedRangeLowerDoubleSpinBox->value(), this->m_Controls.fixedRangeUpperDoubleSpinBox->value()); } this->m_Controls.m_DataViewWidget->Replot(); } template void QmitkCESTStatisticsView::PlotPointSet(itk::Image* image) { this->m_Controls.m_DataViewWidget->SetAxisTitle(QwtPlot::Axis::xBottom, "delta w"); this->m_Controls.m_DataViewWidget->SetAxisTitle(QwtPlot::Axis::yLeft, "z"); QmitkPlotWidget::DataVector::size_type numberOfSpectra = this->m_zSpectrum.size(); mitk::PointSet::Pointer internalPointset; if (m_PointSet.IsNotNull()) { internalPointset = m_PointSet; } else { internalPointset = m_CrosshairPointSet; } if (internalPointset.IsNull()) { return; } if (!this->DataSanityCheck()) { m_Controls.labelWarning->setText("Data can not be plotted, internally inconsistent."); m_Controls.labelWarning->show(); return; } auto maxIndex = internalPointset->GetMaxId().Index(); for (std::size_t number = 0; number < maxIndex + 1; ++number) { mitk::PointSet::PointType point; if (!internalPointset->GetPointIfExists(number, &point)) { continue; } if (!this->m_ZImage->GetGeometry()->IsInside(point)) { continue; } itk::Index<3> itkIndex; this->m_ZImage->GetGeometry()->WorldToIndex(point, itkIndex); itk::Index itkIndexTime; itkIndexTime[0] = itkIndex[0]; itkIndexTime[1] = itkIndex[1]; itkIndexTime[2] = itkIndex[2]; QmitkPlotWidget::DataVector values(numberOfSpectra); for (std::size_t step = 0; step < numberOfSpectra; ++step) { if( VImageDimension == 4 ) { itkIndexTime[3] = step; } values[step] = image->GetPixel(itkIndexTime); } std::stringstream name; name << "Point " << number; // Qcolor enums go from 0 to 19, but 19 is transparent and 0,1 are for bitmaps // 3 is white and thus not visible QColor color(static_cast(number % 17 + 4)); QmitkPlotWidget::DataVector xValues = this->m_zSpectrum; RemoveMZeros(xValues, values); ::SortVectors(xValues, std::less(), xValues, values); unsigned int curveId = this->m_Controls.m_DataViewWidget->InsertCurve(name.str().c_str()); this->m_Controls.m_DataViewWidget->SetCurveData(curveId, xValues, values); this->m_Controls.m_DataViewWidget->SetCurvePen(curveId, QPen(color)); QwtSymbol* symbol = new QwtSymbol(QwtSymbol::Rect, color, color, QSize(8, 8)); this->m_Controls.m_DataViewWidget->SetCurveSymbol(curveId, symbol); this->m_Controls.m_DataViewWidget->SetLegendAttribute(curveId, QwtPlotCurve::LegendShowSymbol); } if (this->m_Controls.fixedRangeCheckBox->isChecked()) { this->m_Controls.m_DataViewWidget->GetPlot()->setAxisAutoScale(2, false); this->m_Controls.m_DataViewWidget->GetPlot()->setAxisScale(2, this->m_Controls.fixedRangeLowerDoubleSpinBox->value(), this->m_Controls.fixedRangeUpperDoubleSpinBox->value()); } else { this->m_Controls.m_DataViewWidget->GetPlot()->setAxisAutoScale(2, true); } QwtLegend* legend = new QwtLegend(); legend->setFrameShape(QFrame::Box); legend->setFrameShadow(QFrame::Sunken); legend->setLineWidth(1); this->m_Controls.m_DataViewWidget->SetLegend(legend, QwtPlot::BottomLegend); m_Controls.m_DataViewWidget->GetPlot()->axisScaleEngine(QwtPlot::Axis::xBottom)->setAttributes(QwtScaleEngine::Inverted); this->m_Controls.m_DataViewWidget->Replot(); m_Controls.labelWarning->setVisible(false); } void QmitkCESTStatisticsView::OnFixedRangeCheckBoxToggled(bool state) { this->m_Controls.fixedRangeLowerDoubleSpinBox->setEnabled(state); this->m_Controls.fixedRangeUpperDoubleSpinBox->setEnabled(state); } - -void QmitkCESTStatisticsView::OnNormalizeImagePushButtonClicked() -{ - QList nodes = this->GetDataManagerSelection(); - if (nodes.empty()) return; - - mitk::DataNode* node = nodes.front(); - - if (!node) - { - // Nothing selected. Inform the user and return - QMessageBox::information(nullptr, "CEST View", "Please load and select an image before starting image processing."); - return; - } - - // here we have a valid mitk::DataNode - - // a node itself is not very useful, we need its data item (the image) - mitk::BaseData* data = node->GetData(); - if (data) - { - // test if this data item is an image or not (could also be a surface or something totally different) - mitk::Image* image = dynamic_cast(data); - - if (image) - { - std::string offsets = ""; - bool hasOffsets = image->GetPropertyList()->GetStringProperty( mitk::CustomTagParser::m_OffsetsPropertyName.c_str() ,offsets); - if (!hasOffsets) - { - QMessageBox::information(nullptr, "CEST View", "Selected image was missing CEST offset information."); - return; - } - if (image->GetDimension() == 4) - { - auto normalizationFilter = mitk::CESTImageNormalizationFilter::New(); - normalizationFilter->SetInput(image); - normalizationFilter->Update(); - - auto resultImage = normalizationFilter->GetOutput(); - - mitk::DataNode::Pointer dataNode = mitk::DataNode::New(); - dataNode->SetData(resultImage); - - std::string normalizedName = node->GetName() + "_normalized"; - dataNode->SetName(normalizedName); - - this->GetDataStorage()->Add(dataNode); - } - - this->Clear(); - } - } -} - void QmitkCESTStatisticsView::RemoveMZeros(QmitkPlotWidget::DataVector& xValues, QmitkPlotWidget::DataVector& yValues) { QmitkPlotWidget::DataVector tempX; QmitkPlotWidget::DataVector tempY; for (std::size_t index = 0; index < xValues.size(); ++index) { if ((xValues.at(index) < -299) || (xValues.at(index)) > 299) { // do not include } else { tempX.push_back(xValues.at(index)); tempY.push_back(yValues.at(index)); } } xValues = tempX; yValues = tempY; } void QmitkCESTStatisticsView::RemoveMZeros(QmitkPlotWidget::DataVector& xValues, QmitkPlotWidget::DataVector& yValues, QmitkPlotWidget::DataVector& stdDevs) { QmitkPlotWidget::DataVector tempX; QmitkPlotWidget::DataVector tempY; QmitkPlotWidget::DataVector tempDevs; for (std::size_t index = 0; index < xValues.size(); ++index) { if ((xValues.at(index) < -299) || (xValues.at(index)) > 299) { // do not include } else { tempX.push_back(xValues.at(index)); tempY.push_back(yValues.at(index)); tempDevs.push_back(stdDevs.at(index)); } } xValues = tempX; yValues = tempY; stdDevs = tempDevs; } void QmitkCESTStatisticsView::OnThreeDimToFourDimPushButtonClicked() { QList nodes = this->GetDataManagerSelection(); if (nodes.empty()) return; mitk::DataNode* node = nodes.front(); if (!node) { // Nothing selected. Inform the user and return QMessageBox::information( nullptr, "CEST View", "Please load and select an image before starting image processing."); return; } // here we have a valid mitk::DataNode // a node itself is not very useful, we need its data item (the image) mitk::BaseData* data = node->GetData(); if (data) { // test if this data item is an image or not (could also be a surface or something totally different) mitk::Image* image = dynamic_cast( data ); if (image) { if (image->GetDimension() == 4) { AccessFixedDimensionByItk(image, CopyTimesteps, 4); } this->Clear(); } } } template void QmitkCESTStatisticsView::CopyTimesteps(itk::Image* image) { typedef itk::Image ImageType; //typedef itk::PasteImageFilter PasteImageFilterType; unsigned int numberOfTimesteps = image->GetLargestPossibleRegion().GetSize(3); typename ImageType::RegionType sourceRegion = image->GetLargestPossibleRegion(); sourceRegion.SetSize(3, 1); typename ImageType::RegionType targetRegion = image->GetLargestPossibleRegion(); targetRegion.SetSize(3, 1); for (unsigned int timestep = 1; timestep < numberOfTimesteps; ++timestep) { targetRegion.SetIndex(3, timestep); itk::ImageRegionConstIterator sourceIterator(image, sourceRegion); itk::ImageRegionIterator targetIterator(image, targetRegion); while (!sourceIterator.IsAtEnd()) { targetIterator.Set(sourceIterator.Get()); ++sourceIterator; ++targetIterator; } } } bool QmitkCESTStatisticsView::SetZSpectrum(mitk::StringProperty* zSpectrumProperty) { if (zSpectrumProperty == nullptr) { return false; } mitk::LocaleSwitch localeSwitch("C"); std::string zSpectrumString = zSpectrumProperty->GetValueAsString(); std::istringstream iss(zSpectrumString); std::vector zSpectra; std::copy(std::istream_iterator(iss), std::istream_iterator(), std::back_inserter(zSpectra)); m_zSpectrum.clear(); m_zSpectrum.resize(0); for (auto const &spectrumString : zSpectra) { m_zSpectrum.push_back(std::stod(spectrumString)); } return (m_zSpectrum.size() > 0); } void QmitkCESTStatisticsView::FillStatisticsTableView( const std::vector &s, const mitk::Image *image) { this->m_Controls.m_StatisticsTable->setColumnCount(image->GetTimeSteps()); this->m_Controls.m_StatisticsTable->horizontalHeader()->setVisible(image->GetTimeSteps() > 1); int decimals = 2; mitk::PixelType doublePix = mitk::MakeScalarPixelType< double >(); mitk::PixelType floatPix = mitk::MakeScalarPixelType< float >(); if (image->GetPixelType() == doublePix || image->GetPixelType() == floatPix) { decimals = 5; } for (unsigned int t = 0; t < image->GetTimeSteps(); t++) { this->m_Controls.m_StatisticsTable->setHorizontalHeaderItem(t, new QTableWidgetItem(QString::number(m_zSpectrum[t]))); this->m_Controls.m_StatisticsTable->setItem(0, t, new QTableWidgetItem( QString("%1").arg(s[t]->GetMean(), 0, 'f', decimals))); this->m_Controls.m_StatisticsTable->setItem(1, t, new QTableWidgetItem( QString("%1").arg(s[t]->GetStd(), 0, 'f', decimals))); this->m_Controls.m_StatisticsTable->setItem(2, t, new QTableWidgetItem( QString("%1").arg(s[t]->GetRMS(), 0, 'f', decimals))); QString max; max.append(QString("%1").arg(s[t]->GetMax(), 0, 'f', decimals)); max += " ("; for (unsigned int i = 0; iGetMaxIndex().size(); i++) { max += QString::number(s[t]->GetMaxIndex()[i]); if (iGetMaxIndex().size() - 1) max += ","; } max += ")"; this->m_Controls.m_StatisticsTable->setItem(3, t, new QTableWidgetItem(max)); QString min; min.append(QString("%1").arg(s[t]->GetMin(), 0, 'f', decimals)); min += " ("; for (unsigned int i = 0; iGetMinIndex().size(); i++) { min += QString::number(s[t]->GetMinIndex()[i]); if (iGetMinIndex().size() - 1) min += ","; } min += ")"; this->m_Controls.m_StatisticsTable->setItem(4, t, new QTableWidgetItem(min)); this->m_Controls.m_StatisticsTable->setItem(5, t, new QTableWidgetItem( QString("%1").arg(s[t]->GetN()))); const mitk::BaseGeometry *geometry = image->GetGeometry(); if (geometry != nullptr) { const mitk::Vector3D &spacing = image->GetGeometry()->GetSpacing(); double volume = spacing[0] * spacing[1] * spacing[2] * (double)s[t]->GetN(); this->m_Controls.m_StatisticsTable->setItem(6, t, new QTableWidgetItem( QString("%1").arg(volume, 0, 'f', decimals))); } else { this->m_Controls.m_StatisticsTable->setItem(6, t, new QTableWidgetItem( "NA")); } } this->m_Controls.m_StatisticsTable->resizeColumnsToContents(); int height = STAT_TABLE_BASE_HEIGHT; if (this->m_Controls.m_StatisticsTable->horizontalHeader()->isVisible()) height += this->m_Controls.m_StatisticsTable->horizontalHeader()->height(); //if (this->m_Controls.m_StatisticsTable->horizontalScrollBar()->isVisible()) // height += this->m_Controls.m_StatisticsTable->horizontalScrollBar()->height(); this->m_Controls.m_StatisticsTable->setMinimumHeight(height); this->m_Controls.m_StatisticsGroupBox->setEnabled(true); this->m_Controls.m_StatisticsTable->setEnabled(true); } void QmitkCESTStatisticsView::InvalidateStatisticsTableView() { this->m_Controls.m_StatisticsTable->horizontalHeader()->setVisible(false); this->m_Controls.m_StatisticsTable->setColumnCount(1); for (int i = 0; i < this->m_Controls.m_StatisticsTable->rowCount(); ++i) { { this->m_Controls.m_StatisticsTable->setItem(i, 0, new QTableWidgetItem("NA")); } } this->m_Controls.m_StatisticsTable->setMinimumHeight(STAT_TABLE_BASE_HEIGHT); this->m_Controls.m_StatisticsTable->setEnabled(false); } bool QmitkCESTStatisticsView::DataSanityCheck() { QmitkPlotWidget::DataVector::size_type numberOfSpectra = m_zSpectrum.size(); // if we do not have a spectrum, the data can not be processed if (numberOfSpectra == 0) { return false; } // if we do not have CEST image data, the data can not be processed if (m_ZImage.IsNull()) { return false; } // if the CEST image data and the meta information do not match, the data can not be processed if (numberOfSpectra != m_ZImage->GetTimeSteps()) { MITK_INFO << "CEST meta information and number of volumes does not match."; return false; } // if we have neither a mask image, a point set nor a mask planar figure, we can not do statistics // statistics on the whole image would not make sense if (m_MaskImage.IsNull() && m_MaskPlanarFigure.IsNull() && m_PointSet.IsNull() && m_CrosshairPointSet->IsEmpty()) { return false; } // if we have a mask image and a mask planar figure, we can not do statistics // we do not know which one to use if (m_MaskImage.IsNotNull() && m_MaskPlanarFigure.IsNotNull()) { return false; } return true; } void QmitkCESTStatisticsView::Clear() { this->m_zSpectrum.clear(); this->m_zSpectrum.resize(0); this->m_ZImage = nullptr; this->m_MaskImage = nullptr; this->m_MaskPlanarFigure = nullptr; this->m_PointSet = nullptr; this->m_Controls.m_DataViewWidget->Clear(); this->InvalidateStatisticsTableView(); this->m_Controls.m_StatisticsGroupBox->setEnabled(false); } void QmitkCESTStatisticsView::OnCopyStatisticsToClipboardPushButtonClicked() { QLocale tempLocal; QLocale::setDefault(QLocale(QLocale::English, QLocale::UnitedStates)); const std::vector &statistics = this->m_CalculatorThread->GetStatisticsData(); QmitkPlotWidget::DataVector::size_type size = m_zSpectrum.size(); QString clipboard("delta_w \t Mean \t StdDev \t RMS \t Max \t Min \t N\n"); for (QmitkPlotWidget::DataVector::size_type index = 0; index < size; ++index) { // Copy statistics to clipboard ("%Ln" will use the default locale for // number formatting) clipboard = clipboard.append("%L1 \t %L2 \t %L3 \t %L4 \t %L5 \t %L6 \t %L7\n") .arg(m_zSpectrum[index], 0, 'f', 10) .arg(statistics[index]->GetMean(), 0, 'f', 10) .arg(statistics[index]->GetStd(), 0, 'f', 10) .arg(statistics[index]->GetRMS(), 0, 'f', 10) .arg(statistics[index]->GetMax(), 0, 'f', 10) .arg(statistics[index]->GetMin(), 0, 'f', 10) .arg(statistics[index]->GetN()); } QApplication::clipboard()->setText( clipboard, QClipboard::Clipboard); QLocale::setDefault(tempLocal); } void QmitkCESTStatisticsView::OnSliceChanged() { mitk::Point3D currentSelectedPosition = this->GetRenderWindowPart()->GetSelectedPosition(nullptr); unsigned int currentSelectedTimeStep = this->GetRenderWindowPart()->GetTimeNavigationController()->GetTime()->GetPos(); if (m_currentSelectedPosition != currentSelectedPosition || m_currentSelectedTimeStep != currentSelectedTimeStep) //|| m_selectedNodeTime > m_currentPositionTime) { //the current position has been changed or the selected node has been changed since the last position validation -> check position m_currentSelectedPosition = currentSelectedPosition; m_currentSelectedTimeStep = currentSelectedTimeStep; m_currentPositionTime.Modified(); m_CrosshairPointSet->Clear(); m_CrosshairPointSet->SetPoint(0, m_currentSelectedPosition); QList nodes = this->GetDataManagerSelection(); if (nodes.empty() || nodes.size() > 1) return; mitk::DataNode* node = nodes.front(); if (!node) { return; } if (dynamic_cast(node->GetData()) != nullptr) { m_Controls.labelWarning->setVisible(false); bool zSpectrumSet = SetZSpectrum(dynamic_cast( node->GetData()->GetProperty(mitk::CustomTagParser::m_OffsetsPropertyName.c_str()).GetPointer())); if (zSpectrumSet) { m_ZImage = dynamic_cast(node->GetData()); } else { return; } } else { return; } this->m_Controls.m_DataViewWidget->Clear(); AccessFixedDimensionByItk(m_ZImage, PlotPointSet, 4); } } diff --git a/Plugins/org.mitk.gui.qt.cest/src/internal/QmitkCESTStatisticsView.h b/Plugins/org.mitk.gui.qt.cest/src/internal/QmitkCESTStatisticsView.h index 6a7cd33fc4..d402f9ac14 100644 --- a/Plugins/org.mitk.gui.qt.cest/src/internal/QmitkCESTStatisticsView.h +++ b/Plugins/org.mitk.gui.qt.cest/src/internal/QmitkCESTStatisticsView.h @@ -1,153 +1,150 @@ /*=================================================================== 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 QmitkCESTStatisticsView_h #define QmitkCESTStatisticsView_h #include #include #include #include "ui_QmitkCESTStatisticsViewControls.h" #include #include #include /** \brief QmitkCESTStatisticsView \warning Basic statistics view for CEST data. \sa QmitkAbstractView \ingroup ${plugin_target}_internal */ class QmitkCESTStatisticsView : public QmitkAbstractView, public mitk::IRenderWindowPartListener { // this is needed for all Qt objects that should have a Qt meta-object // (everything that derives from QObject and wants to have signal/slots) Q_OBJECT public: static const std::string VIEW_ID; /*! \brief default constructor */ QmitkCESTStatisticsView(QObject *parent = nullptr, const char *name = nullptr); /*! \brief default destructor */ virtual ~QmitkCESTStatisticsView(); protected slots: /// \brief Called when the user clicks the GUI button void OnThreeDimToFourDimPushButtonClicked(); /// \brief takes care of processing the computed data void OnThreadedStatisticsCalculationEnds(); /// \brief copy statistics to clipboard void OnCopyStatisticsToClipboardPushButtonClicked(); - /// \brief normalize cest image - void OnNormalizeImagePushButtonClicked(); - /// \brief Toggle whether or not the plot uses a fixed x range void OnFixedRangeCheckBoxToggled(bool state); /// \brief Adapt axis scale when manual ranges are set void OnFixedRangeDoubleSpinBoxChanged(); /// \brief What to do if the crosshair moves void OnSliceChanged(); protected: virtual void CreateQtPartControl(QWidget *parent) override; virtual void SetFocus() override; virtual void RenderWindowPartActivated(mitk::IRenderWindowPart* renderWindowPart); virtual void RenderWindowPartDeactivated(mitk::IRenderWindowPart* renderWindowPart); /// \brief called by QmitkFunctionality when DataManager's selection has changed virtual void OnSelectionChanged( berry::IWorkbenchPart::Pointer source, const QList& nodes ) override; /** \brief Writes the calculated statistics to the GUI */ void FillStatisticsTableView(const std::vector &s, const mitk::Image *image); /** \brief Removes statistics from the GUI */ void InvalidateStatisticsTableView(); /// parse string and set data vector returns true if succesfull bool SetZSpectrum(mitk::StringProperty* zSpectrumProperty); /** Checks whether the currently set data appears reasonable */ bool DataSanityCheck(); /** Fills the plot based on a point set * * This will only use the first timestep */ template void PlotPointSet(itk::Image* image); /** Deletes all data */ void Clear(); /** Remove MZeros * * Will remove the data for the M0 images from the given input */ void RemoveMZeros(QmitkPlotWidget::DataVector& xValues, QmitkPlotWidget::DataVector& yValues); void RemoveMZeros(QmitkPlotWidget::DataVector& xValues, QmitkPlotWidget::DataVector& yValues, QmitkPlotWidget::DataVector& stdDevs); /** Copies the first timestep of a segmentation to all others */ template void CopyTimesteps(itk::Image* image); Ui::QmitkCESTStatisticsViewControls m_Controls; QmitkImageStatisticsCalculationThread* m_CalculatorThread; QmitkPlotWidget::DataVector m_zSpectrum; mitk::Image::Pointer m_ZImage; mitk::Image::Pointer m_MaskImage; mitk::PlanarFigure::Pointer m_MaskPlanarFigure; mitk::PointSet::Pointer m_PointSet; mitk::PointSet::Pointer m_CrosshairPointSet; QmitkSliceNavigationListener m_SliceChangeListener; itk::TimeStamp m_selectedNodeTime; itk::TimeStamp m_currentPositionTime; /** @brief currently valid selected position in the inspector*/ mitk::Point3D m_currentSelectedPosition; /** @brief indicates if the currently selected position is valid for the currently selected fit. * This it is within the input image */ unsigned int m_currentSelectedTimeStep; }; #endif // QmitkCESTStatisticsView_h diff --git a/Plugins/org.mitk.gui.qt.cest/src/internal/QmitkCESTStatisticsViewControls.ui b/Plugins/org.mitk.gui.qt.cest/src/internal/QmitkCESTStatisticsViewControls.ui index 0ffdcba7c7..6ecd840443 100644 --- a/Plugins/org.mitk.gui.qt.cest/src/internal/QmitkCESTStatisticsViewControls.ui +++ b/Plugins/org.mitk.gui.qt.cest/src/internal/QmitkCESTStatisticsViewControls.ui @@ -1,380 +1,373 @@ QmitkCESTStatisticsViewControls 0 0 696 963 0 0 QmitkTemplate QLabel { color: rgb(255, 0, 0) } Please select an image! Do image processing Copy first time step to rest - - - - Normalize CEST image - - - false Statistics 9 9 9 false 100 180 16777215 16777215 Qt::ScrollBarAsNeeded Qt::ScrollBarAsNeeded true QAbstractItemView::NoEditTriggers true true Qt::DotLine false 7 false false 80 true 80 false true true false 25 25 false false Mean StdDev RMS Max Min N V (mm³) 0 0 0 0 0 0 Copy to Clipboard Qt::Horizontal 40 20 QLayout::SetDefaultConstraint 0 0 Define manual range Qt::Horizontal 40 20 Lower limit false -299.899999999999977 299.990000000000009 Upper limit false 2 -299.899999999999977 299.990000000000009 Qt::Horizontal 40 20 Plot 0 0 0 400 Qt::Vertical QSizePolicy::Expanding 20 220 QmitkPlotWidget QWidget
QmitkPlotWidget.h
1
diff --git a/Plugins/org.mitk.gui.qt.cest/src/internal/org_mitk_gui_qt_cest_Activator.cpp b/Plugins/org.mitk.gui.qt.cest/src/internal/org_mitk_gui_qt_cest_Activator.cpp index 2445710d76..7585ff884a 100644 --- a/Plugins/org.mitk.gui.qt.cest/src/internal/org_mitk_gui_qt_cest_Activator.cpp +++ b/Plugins/org.mitk.gui.qt.cest/src/internal/org_mitk_gui_qt_cest_Activator.cpp @@ -1,27 +1,29 @@ /*=================================================================== 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 "org_mitk_gui_qt_cest_Activator.h" #include "QmitkCESTStatisticsView.h" +#include "QmitkCESTNormalizeView.h" void mitk::org_mitk_gui_qt_cest_Activator::start(ctkPluginContext* context) { BERRY_REGISTER_EXTENSION_CLASS(QmitkCESTStatisticsView, context) + BERRY_REGISTER_EXTENSION_CLASS(QmitkCESTNormalizeView, context) } void mitk::org_mitk_gui_qt_cest_Activator::stop(ctkPluginContext*) { }