diff --git a/Modules/ImageStatisticsUI/CMakeLists.txt b/Modules/ImageStatisticsUI/CMakeLists.txt index 30b2a1de22..d13fcbba23 100644 --- a/Modules/ImageStatisticsUI/CMakeLists.txt +++ b/Modules/ImageStatisticsUI/CMakeLists.txt @@ -1,4 +1,10 @@ MITK_CREATE_MODULE( INCLUDE_DIRS Qmitk DEPENDS MitkCore MitkChart MitkImageStatistics MitkQtWidgets -) \ No newline at end of file +) + +if(BUILD_TESTING) + + add_subdirectory(test) + +endif(BUILD_TESTING) \ No newline at end of file diff --git a/Modules/ImageStatisticsUI/Qmitk/QmitkDataGenerationJobBase.h b/Modules/ImageStatisticsUI/Qmitk/QmitkDataGenerationJobBase.h index 3bc2d72feb..9c970e5c63 100644 --- a/Modules/ImageStatisticsUI/Qmitk/QmitkDataGenerationJobBase.h +++ b/Modules/ImageStatisticsUI/Qmitk/QmitkDataGenerationJobBase.h @@ -1,92 +1,93 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #ifndef __QMITK_DATA_GENERATION_JOB_BASE_H #define __QMITK_DATA_GENERATION_JOB_BASE_H //QT #include #include #include //MITK #include #include namespace mitk { static const std::string STATS_GENERATION_STATUS_PROPERTY_NAME = "MITK.statistic.generation.status"; static const std::string STATS_GENERATION_STATUS_VALUE_WORK_IN_PROGRESS = "workInProgress"; + static const std::string STATS_GENERATION_STATUS_VALUE_PENDING = "pending"; static const std::string STATS_GENERATION_STATUS_VALUE_BASE_DATA_FAILED = "failed"; } /*! \brief QmitkDataGenerationJobBase Base class for all jobs. Each job wraps an implementation of DataGenerationComputationInterface to compute a computation in a multi threaded environment with Qt \details the signal ResultsAvailable is emitted when the job is finished the signal Error is emitted in case of an error \example QThreadPool* threadPool = QThreadPool::globalInstance(); auto voxelizationJob = new QmitkVoxelizationJob(doseImage, structContourModelSet, voxelizationNode); connect(job, SIGNAL(ResultsAvailable(const mitk::DataStorage::SetOfObjects*, const QmitkDataGenerationJobBase*)), this, SLOT(OnFinalResultsAvailable(const mitk::DataStorage::SetOfObjects*, const QmitkDataGenerationJobBase*))); threadPool->start(job); */ class MITKIMAGESTATISTICSUI_EXPORT QmitkDataGenerationJobBase : public QObject, public QRunnable { // 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: QmitkDataGenerationJobBase(const QmitkDataGenerationJobBase& other) = delete; QmitkDataGenerationJobBase& operator=(const QmitkDataGenerationJobBase& other) = delete; /** Result map that indicates all results generated by the job. The key is a job specific label for the results.*/ using ResultMapType = std::map; virtual ResultMapType GetResults() const = 0; void run() final; /*! /brief Returns a flag the indicates if the jop computation was successfull.*/ bool GetComputationSuccessFlag() const; std::string GetLastErrorMessage() const; signals: void Error(QString err, const QmitkDataGenerationJobBase* job); /*! @brief Signal is emitted when results are available. @param results a of base date objects produces by the job and ready tu use, put into storage. @param the job that produced the data */ void ResultsAvailable(ResultMapType results, const QmitkDataGenerationJobBase* job); protected: QmitkDataGenerationJobBase() = default; virtual ~QmitkDataGenerationJobBase() = default; /**Does the real computation. Returns true if there where results produced.*/ virtual bool RunComputation() = 0; std::string m_LastErrorMessage; private: bool m_ComputationSuccessful = false; }; #endif diff --git a/Modules/ImageStatisticsUI/Qmitk/QmitkDataGeneratorBase.cpp b/Modules/ImageStatisticsUI/Qmitk/QmitkDataGeneratorBase.cpp index dfad7d0774..a683088eb9 100644 --- a/Modules/ImageStatisticsUI/Qmitk/QmitkDataGeneratorBase.cpp +++ b/Modules/ImageStatisticsUI/Qmitk/QmitkDataGeneratorBase.cpp @@ -1,222 +1,236 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #include "QmitkDataGeneratorBase.h" #include "QmitkDataGenerationJobBase.h" #include "mitkDataNode.h" #include "mitkProperties.h" #include QmitkDataGeneratorBase::QmitkDataGeneratorBase(mitk::DataStorage::Pointer storage, QObject* parent) : QObject(parent), m_Storage(storage) {} QmitkDataGeneratorBase::QmitkDataGeneratorBase(QObject* parent): QObject(parent) {} QmitkDataGeneratorBase::~QmitkDataGeneratorBase() { auto dataStorage = m_Storage.Lock(); if (dataStorage.IsNotNull()) { // remove "add node listener" from data storage dataStorage->AddNodeEvent.RemoveListener( mitk::MessageDelegate1(this, &QmitkDataGeneratorBase::NodeAddedOrModified)); // remove "remove node listener" from data storage dataStorage->ChangedNodeEvent.RemoveListener( mitk::MessageDelegate1(this, &QmitkDataGeneratorBase::NodeAddedOrModified)); } } mitk::DataStorage::Pointer QmitkDataGeneratorBase::GetDataStorage() const { return m_Storage.Lock(); } bool QmitkDataGeneratorBase::GetAutoUpdate() const { return m_AutoUpdate; } bool QmitkDataGeneratorBase::IsGenerating() const { - return m_RunningGeneration; + return m_WIP; } void QmitkDataGeneratorBase::SetDataStorage(mitk::DataStorage* storage) { if (storage == m_Storage) return; std::shared_lock mutexguard(m_DataMutex); auto oldStorage = m_Storage.Lock(); if (oldStorage.IsNotNull()) { // remove "add node listener" from old data storage oldStorage->AddNodeEvent.RemoveListener( mitk::MessageDelegate1(this, &QmitkDataGeneratorBase::NodeAddedOrModified)); // remove "remove node listener" from old data storage oldStorage->ChangedNodeEvent.RemoveListener( mitk::MessageDelegate1(this, &QmitkDataGeneratorBase::NodeAddedOrModified)); } m_Storage = storage; auto newStorage = m_Storage.Lock(); if (newStorage.IsNotNull()) { // add "add node listener" for new data storage newStorage->AddNodeEvent.AddListener( mitk::MessageDelegate1(this, &QmitkDataGeneratorBase::NodeAddedOrModified)); // add remove node listener for new data storage newStorage->ChangedNodeEvent.AddListener( mitk::MessageDelegate1(this, &QmitkDataGeneratorBase::NodeAddedOrModified)); } } void QmitkDataGeneratorBase::SetAutoUpdate(bool autoUpdate) { m_AutoUpdate = autoUpdate; } void QmitkDataGeneratorBase::OnJobError(QString error, const QmitkDataGenerationJobBase* failedJob) const { emit JobError(error, failedJob); } void QmitkDataGeneratorBase::OnFinalResultsAvailable(JobResultMapType results, const QmitkDataGenerationJobBase *job) const { auto resultnodes = mitk::DataStorage::SetOfObjects::New(); for (auto pos : results) { resultnodes->push_back(this->PrepareResultForStorage(pos.first, pos.second, job)); } { std::shared_lock mutexguard(m_DataMutex); auto storage = m_Storage.Lock(); if (storage.IsNotNull()) { for (auto pos = resultnodes->Begin(); pos != resultnodes->End(); ++pos) { storage->Add(pos->Value()); } } } emit NewDataAvailable(resultnodes.GetPointer()); + + if (!resultnodes->empty()) + { + this->EnsureRecheckingAndGeneration(); + } } void QmitkDataGeneratorBase::NodeAddedOrModified(const mitk::DataNode* node) { if (this->NodeChangeIsRelevant(node)) { this->EnsureRecheckingAndGeneration(); } } -void QmitkDataGeneratorBase::EnsureRecheckingAndGeneration() +void QmitkDataGeneratorBase::EnsureRecheckingAndGeneration() const { m_RestartGeneration = true; - if (!IsGenerating()) + if (!m_InGenerate) { this->Generate(); } } void QmitkDataGeneratorBase::Generate() const { - m_RunningGeneration = true; - m_RestartGeneration = true; - while (m_RestartGeneration) + if (m_InGenerate) { - m_RestartGeneration = false; - DoGenerate(); + m_RestartGeneration = true; } + else + { + m_InGenerate = true; + m_RestartGeneration = true; + while (m_RestartGeneration) + { + m_RestartGeneration = false; + DoGenerate(); + } - m_RunningGeneration = false; + m_InGenerate = false; + } } mitk::DataNode::Pointer QmitkDataGeneratorBase::CreateWIPDataNode(mitk::BaseData* dataDummy, const std::string& nodeName) { if (!dataDummy) { mitkThrow() << "data is nullptr"; } auto interimResultNode = mitk::DataNode::New(); interimResultNode->SetProperty("helper object", mitk::BoolProperty::New(true)); - dataDummy->SetProperty(mitk::STATS_GENERATION_STATUS_PROPERTY_NAME.c_str(), mitk::StringProperty::New(mitk::STATS_GENERATION_STATUS_VALUE_WORK_IN_PROGRESS)); + dataDummy->SetProperty(mitk::STATS_GENERATION_STATUS_PROPERTY_NAME.c_str(), mitk::StringProperty::New(mitk::STATS_GENERATION_STATUS_VALUE_PENDING)); interimResultNode->SetVisibility(false); interimResultNode->SetData(dataDummy); if (!nodeName.empty()) { interimResultNode->SetName(nodeName); } return interimResultNode; } void QmitkDataGeneratorBase::DoGenerate() const { auto imageSegCombinations = this->GetAllImageROICombinations(); QThreadPool* threadPool = QThreadPool::globalInstance(); bool everythingValid = true; for (const auto& imageAndSeg : imageSegCombinations) { MITK_INFO << "processing node " << imageAndSeg.first << " and struct " << imageAndSeg.second; if (!this->IsValidResultAvailable(imageAndSeg.first.GetPointer(), imageAndSeg.second.GetPointer())) { this->IndicateFutureResults(imageAndSeg.first.GetPointer(), imageAndSeg.second.GetPointer()); if (everythingValid) { + m_WIP = true; emit GenerationStarted(); everythingValid = false; } MITK_INFO << "no valid result available. Triggering computation of necessary jobs."; auto job = this->GetNextMissingGenerationJob(imageAndSeg.first.GetPointer(), imageAndSeg.second.GetPointer()); //other jobs are pending, nothing has to be done if (!job) { MITK_INFO << "waiting for other jobs to finish"; } else { job->setAutoDelete(true); connect(job, &QmitkDataGenerationJobBase::Error, this, &QmitkDataGeneratorBase::OnJobError, Qt::BlockingQueuedConnection); connect(job, &QmitkDataGenerationJobBase::ResultsAvailable, this, &QmitkDataGeneratorBase::OnFinalResultsAvailable, Qt::BlockingQueuedConnection); threadPool->start(job); } } else { this->RemoveObsoleteDataNodes(imageAndSeg.first.GetPointer(), imageAndSeg.second.GetPointer()); } } - if (everythingValid) + if (everythingValid && m_WIP) { + m_WIP = false; emit GenerationFinished(); } } diff --git a/Modules/ImageStatisticsUI/Qmitk/QmitkDataGeneratorBase.h b/Modules/ImageStatisticsUI/Qmitk/QmitkDataGeneratorBase.h index 2d182cfefa..2324a02838 100644 --- a/Modules/ImageStatisticsUI/Qmitk/QmitkDataGeneratorBase.h +++ b/Modules/ImageStatisticsUI/Qmitk/QmitkDataGeneratorBase.h @@ -1,127 +1,131 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #ifndef __QMITK_DATA_GENERATOR_BASE_H #define __QMITK_DATA_GENERATOR_BASE_H #include //QT #include //MITK #include #include "QmitkDataGenerationJobBase.h" #include /*! \brief QmitkDataGeneratorBase BaseClass that implements the organisation of data generation. Use/see the class QmitkDataGenerator for more details. */ class MITKIMAGESTATISTICSUI_EXPORT QmitkDataGeneratorBase : public QObject { Q_OBJECT public: QmitkDataGeneratorBase(const QmitkDataGeneratorBase& other) = delete; QmitkDataGeneratorBase& operator=(const QmitkDataGeneratorBase& other) = delete; ~QmitkDataGeneratorBase(); using JobResultMapType = QmitkDataGenerationJobBase::ResultMapType; mitk::DataStorage::Pointer GetDataStorage() const; bool GetAutoUpdate() const; + /** Indicates if currently there is work in progress, so data generation jobs are running or pending. + It is set to true when GenerationStarted is triggered and becomes false as soon as GenerationFinished is triggered. + */ bool IsGenerating() const; void Generate() const; /** Indicates if for a given image and ROI a valid final result is available.*/ virtual bool IsValidResultAvailable(const mitk::DataNode* imageNode, const mitk::DataNode* roiNode) const = 0; public slots: void SetDataStorage(mitk::DataStorage* storage); void SetAutoUpdate(bool autoUpdate); protected slots: void OnJobError(QString error, const QmitkDataGenerationJobBase* failedJob) const; /*! @brief Wraps the resulting BaseData* into DataNode objects */ void OnFinalResultsAvailable(JobResultMapType results, const QmitkDataGenerationJobBase *job) const; signals: /*! @brief Signal that is emitted if new final data is produced. */ void NewDataAvailable(mitk::DataStorage::SetOfObjects::ConstPointer data) const; /*! @brief Signal that is emitted if the generator emits new jobs. */ void GenerationStarted() const; /*! @brief Signal that is emitted if all jobs are finished. */ void GenerationFinished() const; /*! @brief Signal that is emitted in case of job errors. */ void JobError(QString error, const QmitkDataGenerationJobBase* failedJob) const; protected: /*! @brief Constructor @param storage the data storage where all produced data should be stored */ QmitkDataGeneratorBase(mitk::DataStorage::Pointer storage, QObject* parent = nullptr); QmitkDataGeneratorBase(QObject* parent = nullptr); using InputPairVectorType = std::vector>; virtual bool NodeChangeIsRelevant(const mitk::DataNode* changedNode) const = 0; virtual InputPairVectorType GetAllImageROICombinations() const = 0; /** Indicates if there is already an valid and up-to-date result for the given node pair.*/ virtual void IndicateFutureResults(const mitk::DataNode* imageNode, const mitk::DataNode* roiNode) const = 0; /*! @brief Returns the next job that needs to be done in order to complete the workflow. If no job instance is passed back, it either indicated that there is nothing to do (IsValidResultAvailable == true) or that currently a interim result is still missing and therefore the next job cannot be triggered.*/ virtual QmitkDataGenerationJobBase* GetNextMissingGenerationJob(const mitk::DataNode* imageNode, const mitk::DataNode* roiNode) const =0; /** Remove all obsolete data nodes for the given image and seg node from the data storage. Obsolete nodes are (interim) result nodes that are not the most recent any more.*/ virtual void RemoveObsoleteDataNodes(const mitk::DataNode* imageNode, const mitk::DataNode* roiNode) const = 0; /** Prepares result to be added to the storage in an appropriate way and returns the data node for that.*/ virtual mitk::DataNode::Pointer PrepareResultForStorage(const std::string& label, mitk::BaseData* result, const QmitkDataGenerationJobBase* job) const = 0; /*! @brief Creates a data node for interim results @details It's the jobs responsibility to write the final results to the data of the DataNode. */ static mitk::DataNode::Pointer CreateWIPDataNode(mitk::BaseData* dataDummy, const std::string& nodeName); void DoGenerate() const; mitk::WeakPointer m_Storage; bool m_AutoUpdate = false; mutable std::shared_mutex m_DataMutex; - mutable bool m_RunningGeneration = false; + mutable bool m_InGenerate = false; mutable bool m_RestartGeneration = false; + mutable bool m_WIP = false; /**Member is called when a node is added to the storage.*/ void NodeAddedOrModified(const mitk::DataNode* node); - void EnsureRecheckingAndGeneration(); + void EnsureRecheckingAndGeneration() const; unsigned long m_DataStorageDeletedTag; }; #endif diff --git a/Modules/ImageStatisticsUI/Qmitk/QmitkImageStatisticsDataGenerator.cpp b/Modules/ImageStatisticsUI/Qmitk/QmitkImageStatisticsDataGenerator.cpp index ba394ca121..15d42bb90b 100644 --- a/Modules/ImageStatisticsUI/Qmitk/QmitkImageStatisticsDataGenerator.cpp +++ b/Modules/ImageStatisticsUI/Qmitk/QmitkImageStatisticsDataGenerator.cpp @@ -1,312 +1,314 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #include "QmitkImageStatisticsDataGenerator.h" #include "mitkImageStatisticsContainer.h" #include "mitkStatisticsToImageRelationRule.h" #include "mitkStatisticsToMaskRelationRule.h" #include "mitkNodePredicateFunction.h" #include "mitkNodePredicateAnd.h" +#include "mitkNodePredicateNot.h" #include "mitkNodePredicateDataProperty.h" #include "mitkProperties.h" #include "mitkImageStatisticsContainerManager.h" #include "QmitkImageStatisticsCalculationRunnable.h" void QmitkImageStatisticsDataGenerator::SetIgnoreZeroValueVoxel(bool _arg) { if (m_IgnoreZeroValueVoxel != _arg) { m_IgnoreZeroValueVoxel = _arg; this->EnsureRecheckingAndGeneration(); } } bool QmitkImageStatisticsDataGenerator::GetIgnoreZeroValueVoxel() const { return this->m_IgnoreZeroValueVoxel; } void QmitkImageStatisticsDataGenerator::SetHistogramNBins(unsigned int nbins) { if (m_HistogramNBins != nbins) { m_HistogramNBins = nbins; this->EnsureRecheckingAndGeneration(); } } unsigned int QmitkImageStatisticsDataGenerator::GetHistogramNBins() const { return this->m_HistogramNBins; } bool QmitkImageStatisticsDataGenerator::NodeChangeIsRelevant(const mitk::DataNode* changedNode) const { auto result = QmitkImageAndRoiDataGeneratorBase::NodeChangeIsRelevant(changedNode); if (!result) { std::string status; - if (!changedNode->GetStringProperty(mitk::STATS_GENERATION_STATUS_PROPERTY_NAME.c_str(), status) || status != mitk::STATS_GENERATION_STATUS_VALUE_WORK_IN_PROGRESS) + if (!changedNode->GetStringProperty(mitk::STATS_GENERATION_STATUS_PROPERTY_NAME.c_str(), status)) { auto stats = dynamic_cast(changedNode->GetData()); return stats != nullptr; } } return false; } bool QmitkImageStatisticsDataGenerator::IsValidResultAvailable(const mitk::DataNode* imageNode, const mitk::DataNode* roiNode) const { auto resultNode = this->GetLatestResult(imageNode, roiNode, true, true); return resultNode.IsNotNull(); } mitk::DataNode::Pointer QmitkImageStatisticsDataGenerator::GetLatestResult(const mitk::DataNode* imageNode, const mitk::DataNode* roiNode, bool onlyIfUpToDate, bool noWIP) const { mitk::DataNode::Pointer result; auto storage = m_Storage.Lock(); if (storage) { if (!imageNode || !imageNode->GetData()) { mitkThrow() << "Image is nullptr"; } const auto image = imageNode->GetData(); const mitk::BaseData* mask = nullptr; if (roiNode) { mask = roiNode->GetData(); } mitk::NodePredicateBase::ConstPointer predicate = mitk::ImageStatisticsContainerManager::GetStatisticsPredicateForSources(image, mask); if (predicate) { auto binPredicate = mitk::NodePredicateDataProperty::New(mitk::STATS_HISTOGRAM_BIN_PROPERTY_NAME.c_str(), mitk::UIntProperty::New(m_HistogramNBins)); auto zeroPredicate = mitk::NodePredicateDataProperty::New(mitk::STATS_IGNORE_ZERO_VOXEL_PROPERTY_NAME.c_str(), mitk::BoolProperty::New(m_IgnoreZeroValueVoxel)); predicate = mitk::NodePredicateAnd::New(predicate, binPredicate, zeroPredicate); if (noWIP) { - predicate = mitk::NodePredicateAnd::New(predicate, mitk::NodePredicateDataProperty::New(mitk::STATS_GENERATION_STATUS_PROPERTY_NAME.c_str())); + auto noWIPPredicate = mitk::NodePredicateNot::New(mitk::NodePredicateDataProperty::New(mitk::STATS_GENERATION_STATUS_PROPERTY_NAME.c_str())); + predicate = mitk::NodePredicateAnd::New(predicate, noWIPPredicate); } std::shared_lock mutexguard(m_DataMutex); auto statisticContainerCandidateNodes = storage->GetSubset(predicate); auto statisticContainerCandidateNodesFiltered = mitk::DataStorage::SetOfObjects::New(); for (const auto& node : *statisticContainerCandidateNodes) { - auto isUpToDate = image->GetMTime() > node->GetData()->GetMTime() - && (mask == nullptr || mask->GetMTime() > node->GetData()->GetMTime()); + auto isUpToDate = image->GetMTime() < node->GetData()->GetMTime() + && (mask == nullptr || mask->GetMTime() < node->GetData()->GetMTime()); if (!onlyIfUpToDate || isUpToDate) { statisticContainerCandidateNodesFiltered->push_back(node); } } if (!statisticContainerCandidateNodesFiltered->empty()) { auto newestElement = statisticContainerCandidateNodesFiltered->front(); if (statisticContainerCandidateNodesFiltered->size() > 1) { //in case of multiple found statistics, return only newest one auto newestIter = std::max_element(std::begin(*statisticContainerCandidateNodesFiltered), std::end(*statisticContainerCandidateNodesFiltered), [](mitk::DataNode::Pointer a, mitk::DataNode::Pointer b) { return a->GetData()->GetMTime() < b->GetData()->GetMTime(); }); newestElement = *newestIter; MITK_WARN << "multiple statistics (" << statisticContainerCandidateNodesFiltered->size() << ") for image/mask found. Returning only newest one."; for (const auto& node : *statisticContainerCandidateNodesFiltered) { MITK_DEBUG << node->GetName() << ", timestamp: " << node->GetData()->GetMTime(); } } result = newestElement; } } } return result; } void QmitkImageStatisticsDataGenerator::IndicateFutureResults(const mitk::DataNode* imageNode, const mitk::DataNode* roiNode) const { if (!imageNode || !imageNode->GetData()) { mitkThrow() << "Image node is nullptr"; } auto image = dynamic_cast(imageNode->GetData()); if (!image) { mitkThrow() << "Image node date is nullptr or no image."; } const mitk::BaseData* mask = nullptr; if (roiNode) { mask = roiNode->GetData(); } if (!this->IsValidResultAvailable(imageNode, roiNode)) { auto dummyStats = mitk::ImageStatisticsContainer::New(); auto imageRule = mitk::StatisticsToImageRelationRule::New(); imageRule->Connect(dummyStats, image); if (nullptr != mask) { auto maskRule = mitk::StatisticsToMaskRelationRule::New(); maskRule->Connect(dummyStats, mask); } dummyStats->SetProperty(mitk::STATS_HISTOGRAM_BIN_PROPERTY_NAME.c_str(), mitk::UIntProperty::New(m_HistogramNBins)); dummyStats->SetProperty(mitk::STATS_IGNORE_ZERO_VOXEL_PROPERTY_NAME.c_str(), mitk::BoolProperty::New(m_IgnoreZeroValueVoxel)); auto dummyNode = CreateWIPDataNode(dummyStats, "WIP_"+GenerateStatisticsNodeName(image, mask)); auto storage = m_Storage.Lock(); if (storage) { std::shared_lock mutexguard(m_DataMutex); storage->Add(dummyNode); } } } QmitkDataGenerationJobBase* QmitkImageStatisticsDataGenerator::GetNextMissingGenerationJob(const mitk::DataNode* imageNode, const mitk::DataNode* roiNode) const { auto resultDataNode = this->GetLatestResult(imageNode, roiNode, true, false); - if (resultDataNode.IsNull()) + std::string status; + if (resultDataNode.IsNull() || (resultDataNode->GetStringProperty(mitk::STATS_GENERATION_STATUS_PROPERTY_NAME.c_str(), status) && status == mitk::STATS_GENERATION_STATUS_VALUE_PENDING)) { if (!imageNode || !imageNode->GetData()) { mitkThrow() << "Image node is nullptr"; } auto image = dynamic_cast(imageNode->GetData()); if (!image) { mitkThrow() << "Image node date is nullptr or no image."; } const mitk::Image* mask = nullptr; const mitk::PlanarFigure* planar = nullptr; if (roiNode) { mask = dynamic_cast(roiNode->GetData()); planar = dynamic_cast(roiNode->GetData()); } auto newJob = new QmitkImageStatisticsCalculationRunnable; newJob->Initialize(image, mask, planar); newJob->SetIgnoreZeroValueVoxel(m_IgnoreZeroValueVoxel); return newJob; } - return nullptr; } void QmitkImageStatisticsDataGenerator::RemoveObsoleteDataNodes(const mitk::DataNode* imageNode, const mitk::DataNode* roiNode) const { if (!imageNode || !imageNode->GetData()) { mitkThrow() << "Image is nullptr"; } const auto image = imageNode->GetData(); const mitk::BaseData* mask = nullptr; if (roiNode) { mask = roiNode->GetData(); } auto lastResult = this->GetLatestResult(imageNode, roiNode, false, false); auto rulePredicate = mitk::ImageStatisticsContainerManager::GetStatisticsPredicateForSources(image, mask); auto notLatestPredicate = mitk::NodePredicateFunction::New([lastResult](const mitk::DataNode* node) { return node != lastResult; }); auto binPredicate = mitk::NodePredicateDataProperty::New(mitk::STATS_HISTOGRAM_BIN_PROPERTY_NAME.c_str(), mitk::UIntProperty::New(m_HistogramNBins)); auto zeroPredicate = mitk::NodePredicateDataProperty::New(mitk::STATS_IGNORE_ZERO_VOXEL_PROPERTY_NAME.c_str(), mitk::BoolProperty::New(m_IgnoreZeroValueVoxel)); - mitk::NodePredicateBase::ConstPointer predicate = mitk::NodePredicateAnd::New(predicate, notLatestPredicate); + mitk::NodePredicateBase::ConstPointer predicate = mitk::NodePredicateAnd::New(rulePredicate, notLatestPredicate); predicate = mitk::NodePredicateAnd::New(predicate, binPredicate, zeroPredicate); auto storage = m_Storage.Lock(); if (storage) { std::shared_lock mutexguard(m_DataMutex); auto oldStatisticContainerNodes = storage->GetSubset(predicate); storage->Remove(oldStatisticContainerNodes); } } mitk::DataNode::Pointer QmitkImageStatisticsDataGenerator::PrepareResultForStorage(const std::string& label, mitk::BaseData* result, const QmitkDataGenerationJobBase* job) const { auto statsJob = dynamic_cast(job); if (statsJob) { auto resultNode = mitk::DataNode::New(); resultNode->SetProperty("helper object", mitk::BoolProperty::New(true)); resultNode->SetVisibility(false); resultNode->SetData(result); const mitk::BaseData* roi = statsJob->GetMaskImage(); if (!roi) { roi = statsJob->GetPlanarFigure(); } resultNode->SetName(this->GenerateStatisticsNodeName(statsJob->GetStatisticsImage(), roi)); return resultNode; } return nullptr; } std::string QmitkImageStatisticsDataGenerator::GenerateStatisticsNodeName(const mitk::Image* image, const mitk::BaseData* roi) const { std::stringstream statisticsNodeName; statisticsNodeName << "statistics_bins-" << m_HistogramNBins <<"_"; if (m_IgnoreZeroValueVoxel) { statisticsNodeName << "noZeros_"; } if (!image) { mitkThrow() << "Image is nullptr"; } statisticsNodeName << image->GetUID(); if (roi) { statisticsNodeName << "_" + roi->GetUID(); } return statisticsNodeName.str(); } diff --git a/Modules/ImageStatisticsUI/test/CMakeLists.txt b/Modules/ImageStatisticsUI/test/CMakeLists.txt new file mode 100644 index 0000000000..153cd81e2e --- /dev/null +++ b/Modules/ImageStatisticsUI/test/CMakeLists.txt @@ -0,0 +1 @@ +MITK_CREATE_MODULE_TESTS() diff --git a/Modules/ImageStatisticsUI/test/QmitkImageStatisticsDataGeneratorTest.cpp b/Modules/ImageStatisticsUI/test/QmitkImageStatisticsDataGeneratorTest.cpp new file mode 100644 index 0000000000..419e861f45 --- /dev/null +++ b/Modules/ImageStatisticsUI/test/QmitkImageStatisticsDataGeneratorTest.cpp @@ -0,0 +1,223 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include "QmitkImageStatisticsDataGenerator.h" +#include +#include +#include "mitkImage.h" +#include "mitkPlanarFigure.h" +#include "mitkIOUtil.h" + +#include +#include + +class TestQmitkImageStatisticsDataGenerator : public QmitkImageStatisticsDataGenerator +{ +public: + TestQmitkImageStatisticsDataGenerator(mitk::DataStorage::Pointer storage, QObject* parent = nullptr) : QmitkImageStatisticsDataGenerator(storage, parent) + { + connect(this, &QmitkDataGeneratorBase::NewDataAvailable, this, &TestQmitkImageStatisticsDataGenerator::NewDataAvailableEmited); + connect(this, &QmitkDataGeneratorBase::GenerationStarted, this, &TestQmitkImageStatisticsDataGenerator::GenerationStartedEmited); + connect(this, &QmitkDataGeneratorBase::GenerationFinished, this, &TestQmitkImageStatisticsDataGenerator::GenerationFinishedEmited); + connect(this, &QmitkDataGeneratorBase::JobError, this, &TestQmitkImageStatisticsDataGenerator::JobErrorEmited); + }; + + mutable std::vector m_NewDataAvailable; + void NewDataAvailableEmited(mitk::DataStorage::SetOfObjects::ConstPointer data) const + { + m_NewDataAvailable.emplace_back(data); + }; + + mutable int m_GenerationStartedEmited = 0; + void GenerationStartedEmited() const + { + m_GenerationStartedEmited++; + } + + mutable int m_GenerationFinishedEmited = 0; + void GenerationFinishedEmited() const + { + m_GenerationFinishedEmited++; + QCoreApplication::instance()->quit(); + } + + mutable std::vector m_JobErrorEmited_error; + void JobErrorEmited(QString error, const QmitkDataGenerationJobBase* failedJob) const + { + m_JobErrorEmited_error.emplace_back(error); + } + +}; + +class QmitkImageStatisticsDataGeneratorTestSuite : public mitk::TestFixture +{ + CPPUNIT_TEST_SUITE(QmitkImageStatisticsDataGeneratorTestSuite); + MITK_TEST(GetterSetterTest); + MITK_TEST(NullTest); + MITK_TEST(OneImageTest); + MITK_TEST(MultiImageTest); + MITK_TEST(ImageAndROITest); + MITK_TEST(ImageAndMultiROITest); + MITK_TEST(MultiMultiTest); + MITK_TEST(InputChangedTest); + MITK_TEST(SettingsChangedTest); + MITK_TEST(DataStorageModificationTest); + CPPUNIT_TEST_SUITE_END(); + + mitk::DataStorage::Pointer m_DataStorage; + mitk::DataNode::Pointer m_ImageNode1; + mitk::DataNode::Pointer m_ImageNode2; + mitk::DataNode::Pointer m_MaskImageNode; + mitk::DataNode::Pointer m_PFNode; + mitk::Image::Pointer m_Image1; + mitk::Image::Pointer m_Image2; + mitk::Image::Pointer m_Mask; + mitk::PlanarFigure::Pointer m_PF; + + QCoreApplication* m_TestApp; + +public: + void setUp() override + { + m_DataStorage = mitk::StandaloneDataStorage::New(); + + m_ImageNode1 = mitk::DataNode::New(); + m_ImageNode1->SetName("Image_1"); + auto pic3DCroppedFile = this->GetTestDataFilePath("ImageStatisticsTestData/Pic3D_cropped.nrrd"); + m_Image1 = mitk::IOUtil::Load(pic3DCroppedFile); + CPPUNIT_ASSERT_MESSAGE("Failed loading Pic3D_cropped", m_Image1.IsNotNull()); + m_ImageNode1->SetData(m_Image1); + + m_ImageNode2 = mitk::DataNode::New(); + m_ImageNode2->SetName("Image_2"); + m_Image2 = mitk::IOUtil::Load(pic3DCroppedFile); + CPPUNIT_ASSERT_MESSAGE("Failed loading Pic3D_cropped", m_Image2.IsNotNull()); + m_ImageNode2->SetData(m_Image2); + + m_MaskImageNode = mitk::DataNode::New(); + m_MaskImageNode->SetName("Mask"); + auto pic3DCroppedBinMaskFile = this->GetTestDataFilePath("ImageStatisticsTestData/Pic3D_croppedBinMask.nrrd"); + m_Mask = mitk::IOUtil::Load(pic3DCroppedBinMaskFile); + CPPUNIT_ASSERT_MESSAGE("Failed loading Pic3D binary mask", m_Mask.IsNotNull()); + m_MaskImageNode->SetData(m_Mask); + + m_PFNode = mitk::DataNode::New(); + m_PFNode->SetName("PF"); + auto pic3DCroppedPlanarFigureFile = this->GetTestDataFilePath("ImageStatisticsTestData/Pic3D_croppedPF.pf"); + m_PF = mitk::IOUtil::Load(pic3DCroppedPlanarFigureFile); + CPPUNIT_ASSERT_MESSAGE("Failed loading Pic3D planar figure", m_PF.IsNotNull()); + m_PFNode->SetData(m_PF); + + int argc = 0; + char** argv = nullptr; + m_TestApp = new QCoreApplication(argc, argv); + } + + void tearDown() override + { + delete m_TestApp; + } + + + void NullTest() + { + TestQmitkImageStatisticsDataGenerator generator(nullptr); + + generator.Generate(); + CPPUNIT_ASSERT_EQUAL(0, generator.m_GenerationFinishedEmited); + CPPUNIT_ASSERT_EQUAL(0, generator.m_GenerationStartedEmited); + CPPUNIT_ASSERT(generator.m_JobErrorEmited_error.empty()); + CPPUNIT_ASSERT(generator.m_NewDataAvailable.empty()); + + generator.SetDataStorage(m_DataStorage); + generator.Generate(); + CPPUNIT_ASSERT_EQUAL(0, generator.m_GenerationFinishedEmited); + CPPUNIT_ASSERT_EQUAL(0, generator.m_GenerationStartedEmited); + CPPUNIT_ASSERT(generator.m_JobErrorEmited_error.empty()); + CPPUNIT_ASSERT(generator.m_NewDataAvailable.empty()); + } + + void GetterSetterTest() + { + TestQmitkImageStatisticsDataGenerator generator(nullptr); + CPPUNIT_ASSERT(nullptr == generator.GetDataStorage()); + generator.SetDataStorage(m_DataStorage); + CPPUNIT_ASSERT(m_DataStorage == generator.GetDataStorage()); + + TestQmitkImageStatisticsDataGenerator generator2(m_DataStorage); + CPPUNIT_ASSERT(m_DataStorage == generator.GetDataStorage()); + + CPPUNIT_ASSERT_EQUAL(100u, generator.GetHistogramNBins()); + CPPUNIT_ASSERT_EQUAL(false, generator.GetIgnoreZeroValueVoxel()); + CPPUNIT_ASSERT_EQUAL(false, generator.GetAutoUpdate()); + + generator.SetHistogramNBins(3); + CPPUNIT_ASSERT_EQUAL(3u, generator.GetHistogramNBins()); + CPPUNIT_ASSERT_EQUAL(false, generator.GetIgnoreZeroValueVoxel()); + CPPUNIT_ASSERT_EQUAL(false, generator.GetAutoUpdate()); + + generator.SetIgnoreZeroValueVoxel(true); + CPPUNIT_ASSERT_EQUAL(3u, generator.GetHistogramNBins()); + CPPUNIT_ASSERT_EQUAL(true, generator.GetIgnoreZeroValueVoxel()); + CPPUNIT_ASSERT_EQUAL(false, generator.GetAutoUpdate()); + + generator.SetAutoUpdate(true); + CPPUNIT_ASSERT_EQUAL(3u, generator.GetHistogramNBins()); + CPPUNIT_ASSERT_EQUAL(true, generator.GetIgnoreZeroValueVoxel()); + CPPUNIT_ASSERT_EQUAL(true, generator.GetAutoUpdate()); + } + + void OneImageTest() + { + TestQmitkImageStatisticsDataGenerator generator(m_DataStorage); + + generator.SetImageNodes({ m_ImageNode1.GetPointer() }); + generator.Generate(); + m_TestApp->exec(); + + CPPUNIT_ASSERT_EQUAL(1, generator.m_GenerationFinishedEmited); + CPPUNIT_ASSERT_EQUAL(1, generator.m_GenerationStartedEmited); + CPPUNIT_ASSERT(generator.m_JobErrorEmited_error.empty()); + CPPUNIT_ASSERT(generator.m_NewDataAvailable.empty()); + } + + void MultiImageTest() + { + } + + void ImageAndROITest() + { + } + + void ImageAndMultiROITest() + { + } + + void MultiMultiTest() + { + } + + void InputChangedTest() + { + } + + void SettingsChangedTest() + { + } + + void DataStorageModificationTest() + { + } + +}; + +MITK_TEST_SUITE_REGISTRATION(QmitkImageStatisticsDataGenerator) diff --git a/Modules/ImageStatisticsUI/test/files.cmake b/Modules/ImageStatisticsUI/test/files.cmake new file mode 100644 index 0000000000..36d9d98569 --- /dev/null +++ b/Modules/ImageStatisticsUI/test/files.cmake @@ -0,0 +1,3 @@ +set(MODULE_TESTS + QmitkImageStatisticsDataGeneratorTest.cpp +)