diff --git a/Modules/Core/include/mitkIOVolumeSplitReason.h b/Modules/Core/include/mitkIOVolumeSplitReason.h index 156afd069a..b27a9e96ef 100644 --- a/Modules/Core/include/mitkIOVolumeSplitReason.h +++ b/Modules/Core/include/mitkIOVolumeSplitReason.h @@ -1,84 +1,105 @@ /*============================================================================ 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 mitkIOVolumeSplitReason_h #define mitkIOVolumeSplitReason_h -#include "itkLightObject.h" -#include "mitkCommon.h" +#include +#include +#include #include "MitkCoreExports.h" namespace mitk { - class MITKCORE_EXPORT IOVolumeSplitReason : public itk::LightObject + class MITKCORE_EXPORT IOVolumeSplitReason { public: - mitkClassMacroItkParent(IOVolumeSplitReason, itk::LightObject); - itkFactorylessNewMacro(IOVolumeSplitReason); - itkCloneMacro(IOVolumeSplitReason); + using Self = IOVolumeSplitReason; + using Pointer = std::shared_ptr; + using ConstPointer = std::shared_ptr; enum class ReasonType { Unknown = 0, ValueSplitDifference, //*< split due to different values in splitting relevant dicom tags ValueSortDistance, //*< split due value distance of sort criterion too large for relevant dicom tag(s) ImagePostionMissing, //*< split because image position tag was missing in one of the compared files OverlappingSlices, //*< split because at least two input files are overlapping in world coordinate space GantryTiltDifference, //*< split because the gantry tilts of at least two input files were different SliceDistanceInconsistency, //*< split because the distance between slices were inconsistent. // This can either be evoked by volumes with heterogeneous z spacing or by missing slices. // Details for this reason will contain the detected slice distance inconsistency MissingSlices //*< Indicates that is a split was done due to missing slices. (It is a sub class of SliceDistanceInconsistency // as all SliceDistanceInconsistency with a positive distance inconsistency greater then one times the slice // thickness are deemed also missing slices as split reason. This sub class was introduced to make it easier // for parsing applications to react on this important split reason. // Details for this reason will contain the assumed/detected number of missing slices }; void AddReason(ReasonType type, const std::string& detail = ""); void RemoveReason(ReasonType type); bool HasReasons() const; bool HasReason(ReasonType type) const; std::string GetReasonDetails(ReasonType type) const; /** This methods generates a clone of this instances and extends the clone by the reason types and details provided by another IOVolumeSplitReason instance. @remark If the other instance contains a reason type that is already existing it will be ignored. Therefor only new types and details will be added to the extension. @pre otherReason must point to a valid instance. @return Pointer to the cloned and extended instance.*/ - Pointer ExtendReason(const Self* otherReason) const; + Pointer ExtendReason(ConstPointer otherReason) const; - static std::string ToJSON(const IOVolumeSplitReason*); - static Pointer FromJSON(const std::string& reasonStr); + static nlohmann::json ToJSON(ConstPointer); + static Pointer FromJSON(const nlohmann::json& j); static std::string TypeToString(ReasonType reasonType); static IOVolumeSplitReason::ReasonType StringToType(const std::string& reasonStr); - protected: - mitkCloneMacro(IOVolumeSplitReason); - - IOVolumeSplitReason(); - ~IOVolumeSplitReason() override; - IOVolumeSplitReason(const IOVolumeSplitReason& other); - IOVolumeSplitReason& operator=(const IOVolumeSplitReason& other) = delete; + Pointer Clone() const; + static Pointer New(); + protected: using ReasonMapType = std::map; ReasonMapType m_ReasonMap; }; + template + inline void to_json(BasicJsonType& j, const IOVolumeSplitReason::ReasonType& e) + { + static_assert(std::is_enum::value, + "IOVolumeSplitReason::ReasonType" + " must be an enum!"); + j = IOVolumeSplitReason::TypeToString(e); + } + + template + inline void from_json(const BasicJsonType& j, IOVolumeSplitReason::ReasonType& e) + { + static_assert(std::is_enum::value, + "IOVolumeSplitReason::ReasonType" + " must be an enum!"); + e = IOVolumeSplitReason::StringToType(j.template get()); + } + + + MITKCORE_EXPORT void to_json(nlohmann::json& j, IOVolumeSplitReason::ConstPointer reason); + MITKCORE_EXPORT void to_json(nlohmann::json& j, IOVolumeSplitReason::Pointer reason); + + MITKCORE_EXPORT void from_json(const nlohmann::json& j, IOVolumeSplitReason& reason); + } #endif diff --git a/Modules/Core/src/IO/mitkIOVolumeSplitReason.cpp b/Modules/Core/src/IO/mitkIOVolumeSplitReason.cpp index 13469475e9..0f7c5dea40 100644 --- a/Modules/Core/src/IO/mitkIOVolumeSplitReason.cpp +++ b/Modules/Core/src/IO/mitkIOVolumeSplitReason.cpp @@ -1,159 +1,169 @@ /*============================================================================ 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 #include void mitk::IOVolumeSplitReason::AddReason(ReasonType type, const std::string& detail) { m_ReasonMap[type] = detail; } void mitk::IOVolumeSplitReason::RemoveReason(ReasonType type) { m_ReasonMap.erase(type); } bool mitk::IOVolumeSplitReason::HasReasons() const { return !m_ReasonMap.empty(); } bool mitk::IOVolumeSplitReason::HasReason(ReasonType type) const { return m_ReasonMap.cend() != m_ReasonMap.find(type); } std::string mitk::IOVolumeSplitReason::GetReasonDetails(ReasonType type) const { auto finding = m_ReasonMap.find(type); if (m_ReasonMap.cend() == finding) mitkThrow() << "Cannot get details for inexistent type."; return finding->second; }; -mitk::IOVolumeSplitReason::Pointer mitk::IOVolumeSplitReason::ExtendReason(const Self* otherReason) const +mitk::IOVolumeSplitReason::Pointer mitk::IOVolumeSplitReason::ExtendReason(ConstPointer otherReason) const { if (nullptr == otherReason) mitkThrow() << "Cannot extend reason. Passed other reason is in valid."; Pointer result = this->Clone(); result->m_ReasonMap.insert(otherReason->m_ReasonMap.cbegin(), otherReason->m_ReasonMap.cend()); return result; } -mitk::IOVolumeSplitReason::IOVolumeSplitReason(): itk::LightObject() -{ -} - -mitk::IOVolumeSplitReason::~IOVolumeSplitReason() -{ -} - -mitk::IOVolumeSplitReason::IOVolumeSplitReason(const IOVolumeSplitReason& other) : m_ReasonMap(other.m_ReasonMap) -{ -} - std::string mitk::IOVolumeSplitReason::TypeToString(ReasonType reasonType) { switch (reasonType) { case ReasonType::GantryTiltDifference: return "gantry_tilt_difference"; case ReasonType::ImagePostionMissing: return "image_position_missing"; case ReasonType::OverlappingSlices: return "overlapping_slices"; case ReasonType::SliceDistanceInconsistency: return "slice_distance_inconsistency"; case ReasonType::ValueSortDistance: return "value_sort_distance"; case ReasonType::ValueSplitDifference: return "value_split_difference"; case ReasonType::MissingSlices: return "missing_slices"; default: return "unknown"; } } mitk::IOVolumeSplitReason::ReasonType mitk::IOVolumeSplitReason::StringToType(const std::string& reasonStr) { if (reasonStr == "gantry_tilt_difference") return ReasonType::GantryTiltDifference; else if (reasonStr == "image_position_missing") return ReasonType::ImagePostionMissing; else if (reasonStr == "overlapping_slices") return ReasonType::OverlappingSlices; else if (reasonStr == "slice_distance_inconsistency") return ReasonType::SliceDistanceInconsistency; else if (reasonStr == "value_sort_distance") return ReasonType::ValueSortDistance; else if (reasonStr == "value_split_difference") return ReasonType::ValueSplitDifference; else if (reasonStr == "missing_slices") return ReasonType::MissingSlices; return ReasonType::Unknown; } -std::string mitk::IOVolumeSplitReason::ToJSON(const IOVolumeSplitReason* reason) +mitk::IOVolumeSplitReason::Pointer mitk::IOVolumeSplitReason::Clone() const +{ + return std::make_shared(*this); +} + +mitk::IOVolumeSplitReason::Pointer mitk::IOVolumeSplitReason::New() +{ + return std::make_shared(); +} + + +nlohmann::json mitk::IOVolumeSplitReason::ToJSON(ConstPointer reason) { if (nullptr == reason) mitkThrow() << "Cannot extend reason. Passed other reason is in valid."; auto data = nlohmann::json::array(); for (const auto& [type, detail] : reason->m_ReasonMap) { auto details = nlohmann::json::array(); - details.push_back(TypeToString(type)); + details.push_back(type); if (!detail.empty()) { details.push_back(detail); } data.push_back(details); } - return data.dump(); + return data; } -mitk::IOVolumeSplitReason::Pointer mitk::IOVolumeSplitReason::FromJSON(const std::string& reasonStr) +mitk::IOVolumeSplitReason::Pointer mitk::IOVolumeSplitReason::FromJSON(const nlohmann::json& j) { - if (reasonStr.empty()) - return nullptr; - - auto reason = IOVolumeSplitReason::New(); - - auto data = nlohmann::json::parse(reasonStr); + auto reason = std::make_shared(); - for (const auto& jItem : data) + for (const auto& jItem : j) { if (!jItem.empty()) { - ReasonType reasonType = StringToType(jItem.at(0).get()); + ReasonType reasonType = jItem.at(0).get(); std::string detail; if (jItem.size() > 1) { detail = jItem.at(1).get(); } reason->AddReason(reasonType, detail); } } return reason; } + + +void mitk::to_json(nlohmann::json& j, IOVolumeSplitReason::ConstPointer reason) +{ + j = IOVolumeSplitReason::ToJSON(reason); +} + +void mitk::to_json(nlohmann::json& j, IOVolumeSplitReason::Pointer reason) +{ + j = IOVolumeSplitReason::ToJSON(reason); +} + +void mitk::from_json(const nlohmann::json& j, IOVolumeSplitReason& e) +{ + e = *IOVolumeSplitReason::FromJSON(j); +} diff --git a/Modules/Core/test/mitkIOVolumeSplitReasonTest.cpp b/Modules/Core/test/mitkIOVolumeSplitReasonTest.cpp index 2ac18dcd2c..a16d37af78 100644 --- a/Modules/Core/test/mitkIOVolumeSplitReasonTest.cpp +++ b/Modules/Core/test/mitkIOVolumeSplitReasonTest.cpp @@ -1,181 +1,180 @@ /*============================================================================ 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 "mitkTestingMacros.h" #include #include #include #include #include class mitkIOVolumeSplitReasonTestSuite : public mitk::TestFixture { CPPUNIT_TEST_SUITE(mitkIOVolumeSplitReasonTestSuite); MITK_TEST(TestAddNRemoveMethods); MITK_TEST(TestSerializeReasonType); MITK_TEST(TestSerializeReason); MITK_TEST(TestExtendReason); CPPUNIT_TEST_SUITE_END(); private: std::string m_ReasonStr; mitk::IOVolumeSplitReason::Pointer m_Reason1; mitk::IOVolumeSplitReason::Pointer m_Reason2; mitk::IOVolumeSplitReason::Pointer m_EmptyReason; public: void setUp() override { m_Reason1 = mitk::IOVolumeSplitReason::New(); m_Reason2 = mitk::IOVolumeSplitReason::New(); m_EmptyReason = mitk::IOVolumeSplitReason::New(); m_Reason1->AddReason(mitk::IOVolumeSplitReason::ReasonType::OverlappingSlices); m_Reason1->AddReason(mitk::IOVolumeSplitReason::ReasonType::MissingSlices,"1"); m_Reason2->AddReason(mitk::IOVolumeSplitReason::ReasonType::ImagePostionMissing); m_Reason2->AddReason(mitk::IOVolumeSplitReason::ReasonType::MissingSlices, "2"); m_ReasonStr = "[[\"value_sort_distance\",\"detail\"],[\"overlapping_slices\"],[\"missing_slices\",\"3\"]]"; } void TestAddNRemoveMethods() { CPPUNIT_ASSERT(!m_EmptyReason->HasReasons()); CPPUNIT_ASSERT(m_Reason1->HasReasons()); CPPUNIT_ASSERT(m_Reason2->HasReasons()); CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::GantryTiltDifference)); CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::ImagePostionMissing)); CPPUNIT_ASSERT(m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::MissingSlices)); CPPUNIT_ASSERT(m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::OverlappingSlices)); CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::SliceDistanceInconsistency)); CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::Unknown)); CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::ValueSortDistance)); CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::ValueSplitDifference)); CPPUNIT_ASSERT(!m_Reason2->HasReason(mitk::IOVolumeSplitReason::ReasonType::GantryTiltDifference)); CPPUNIT_ASSERT(m_Reason2->HasReason(mitk::IOVolumeSplitReason::ReasonType::ImagePostionMissing)); CPPUNIT_ASSERT(m_Reason2->HasReason(mitk::IOVolumeSplitReason::ReasonType::MissingSlices)); CPPUNIT_ASSERT(!m_Reason2->HasReason(mitk::IOVolumeSplitReason::ReasonType::OverlappingSlices)); CPPUNIT_ASSERT(!m_Reason2->HasReason(mitk::IOVolumeSplitReason::ReasonType::SliceDistanceInconsistency)); CPPUNIT_ASSERT(!m_Reason2->HasReason(mitk::IOVolumeSplitReason::ReasonType::Unknown)); CPPUNIT_ASSERT(!m_Reason2->HasReason(mitk::IOVolumeSplitReason::ReasonType::ValueSortDistance)); CPPUNIT_ASSERT(!m_Reason2->HasReason(mitk::IOVolumeSplitReason::ReasonType::ValueSplitDifference)); m_Reason1->RemoveReason(mitk::IOVolumeSplitReason::ReasonType::Unknown); CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::GantryTiltDifference)); CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::ImagePostionMissing)); CPPUNIT_ASSERT(m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::MissingSlices)); CPPUNIT_ASSERT(m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::OverlappingSlices)); CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::SliceDistanceInconsistency)); CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::Unknown)); CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::ValueSortDistance)); CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::ValueSplitDifference)); m_Reason1->RemoveReason(mitk::IOVolumeSplitReason::ReasonType::MissingSlices); CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::GantryTiltDifference)); CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::ImagePostionMissing)); CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::MissingSlices)); CPPUNIT_ASSERT(m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::OverlappingSlices)); CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::SliceDistanceInconsistency)); CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::Unknown)); CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::ValueSortDistance)); CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::ValueSplitDifference)); m_Reason1->AddReason(mitk::IOVolumeSplitReason::ReasonType::ValueSortDistance); m_Reason1->AddReason(mitk::IOVolumeSplitReason::ReasonType::ValueSplitDifference); CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::GantryTiltDifference)); CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::ImagePostionMissing)); CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::MissingSlices)); CPPUNIT_ASSERT(m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::OverlappingSlices)); CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::SliceDistanceInconsistency)); CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::Unknown)); CPPUNIT_ASSERT(m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::ValueSortDistance)); CPPUNIT_ASSERT(m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::ValueSplitDifference)); } static bool CheckType(mitk::IOVolumeSplitReason::ReasonType reasontype, const std::string& reasonStr) { if (mitk::IOVolumeSplitReason::TypeToString(reasontype) != reasonStr) return false; if (mitk::IOVolumeSplitReason::StringToType(reasonStr) != reasontype) return false; return true; } - static bool CheckReason(const std::string& reasonStr, const std::string& reasonStr2) + static bool CheckReason(const std::string& refStr, const nlohmann::json& j) { - auto json1 = nlohmann::json::parse(reasonStr); - auto json2 = nlohmann::json::parse(reasonStr2); + auto refJ = nlohmann::json::parse(refStr); - return json2 == json1; + return refJ == j; } void TestSerializeReasonType() { CPPUNIT_ASSERT(CheckType(mitk::IOVolumeSplitReason::ReasonType::GantryTiltDifference,"gantry_tilt_difference")); CPPUNIT_ASSERT(CheckType(mitk::IOVolumeSplitReason::ReasonType::ImagePostionMissing, "image_position_missing")); CPPUNIT_ASSERT(CheckType(mitk::IOVolumeSplitReason::ReasonType::MissingSlices, "missing_slices")); CPPUNIT_ASSERT(CheckType(mitk::IOVolumeSplitReason::ReasonType::OverlappingSlices, "overlapping_slices")); CPPUNIT_ASSERT(CheckType(mitk::IOVolumeSplitReason::ReasonType::SliceDistanceInconsistency, "slice_distance_inconsistency")); CPPUNIT_ASSERT(CheckType(mitk::IOVolumeSplitReason::ReasonType::Unknown, "unknown")); CPPUNIT_ASSERT(CheckType(mitk::IOVolumeSplitReason::ReasonType::ValueSortDistance, "value_sort_distance")); CPPUNIT_ASSERT(CheckType(mitk::IOVolumeSplitReason::ReasonType::ValueSplitDifference, "value_split_difference")); } void TestSerializeReason() { auto serializedReason1 = mitk::IOVolumeSplitReason::ToJSON(m_Reason1); auto serializedReason2 = mitk::IOVolumeSplitReason::ToJSON(m_Reason2); auto serializedReasonEmpty = mitk::IOVolumeSplitReason::ToJSON(m_EmptyReason); CPPUNIT_ASSERT(CheckReason("[[\"overlapping_slices\"],[\"missing_slices\",\"1\"]]", serializedReason1)); CPPUNIT_ASSERT(CheckReason("[[\"image_position_missing\"],[\"missing_slices\",\"2\"]]", serializedReason2)); CPPUNIT_ASSERT(CheckReason("[]", serializedReasonEmpty)); - auto newReason = mitk::IOVolumeSplitReason::FromJSON(m_ReasonStr); + auto newReason = mitk::IOVolumeSplitReason::FromJSON(nlohmann::json::parse(m_ReasonStr)); CPPUNIT_ASSERT(!newReason->HasReason(mitk::IOVolumeSplitReason::ReasonType::GantryTiltDifference)); CPPUNIT_ASSERT(!newReason->HasReason(mitk::IOVolumeSplitReason::ReasonType::ImagePostionMissing)); CPPUNIT_ASSERT(newReason->HasReason(mitk::IOVolumeSplitReason::ReasonType::MissingSlices)); CPPUNIT_ASSERT(newReason->HasReason(mitk::IOVolumeSplitReason::ReasonType::OverlappingSlices)); CPPUNIT_ASSERT(!newReason->HasReason(mitk::IOVolumeSplitReason::ReasonType::SliceDistanceInconsistency)); CPPUNIT_ASSERT(!newReason->HasReason(mitk::IOVolumeSplitReason::ReasonType::Unknown)); CPPUNIT_ASSERT(newReason->HasReason(mitk::IOVolumeSplitReason::ReasonType::ValueSortDistance)); CPPUNIT_ASSERT(!newReason->HasReason(mitk::IOVolumeSplitReason::ReasonType::ValueSplitDifference)); CPPUNIT_ASSERT(newReason->GetReasonDetails(mitk::IOVolumeSplitReason::ReasonType::ValueSortDistance) == "detail"); CPPUNIT_ASSERT(newReason->GetReasonDetails(mitk::IOVolumeSplitReason::ReasonType::MissingSlices) == "3"); CPPUNIT_ASSERT(newReason->GetReasonDetails(mitk::IOVolumeSplitReason::ReasonType::OverlappingSlices) == ""); } void TestExtendReason() { auto extendedReason = m_Reason1->ExtendReason(m_EmptyReason); CPPUNIT_ASSERT(CheckReason("[[\"overlapping_slices\"],[\"missing_slices\",\"1\"]]", mitk::IOVolumeSplitReason::ToJSON(extendedReason))); extendedReason = m_EmptyReason->ExtendReason(m_Reason1); CPPUNIT_ASSERT(CheckReason("[[\"overlapping_slices\"],[\"missing_slices\",\"1\"]]", mitk::IOVolumeSplitReason::ToJSON(extendedReason))); extendedReason = m_Reason2->ExtendReason(m_Reason1); CPPUNIT_ASSERT(CheckReason("[[\"image_position_missing\"],[\"overlapping_slices\"],[\"missing_slices\",\"2\"]]", mitk::IOVolumeSplitReason::ToJSON(extendedReason))); } }; MITK_TEST_SUITE_REGISTRATION(mitkIOVolumeSplitReason) diff --git a/Modules/CoreCmdApps/FileConverter.cpp b/Modules/CoreCmdApps/FileConverter.cpp index 025cc50a29..f9f52f936d 100644 --- a/Modules/CoreCmdApps/FileConverter.cpp +++ b/Modules/CoreCmdApps/FileConverter.cpp @@ -1,152 +1,152 @@ /*============================================================================ 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 "mitkProperties.h" #include "mitkCommandLineParser.h" #include "mitkIOUtil.h" #include #include "mitkPreferenceListReaderOptionsFunctor.h" #include "mitkIOMetaInformationPropertyConstants.h" #include "mitkIOVolumeSplitReason.h" #include "mitkPropertyKeyPath.h" int main(int argc, char* argv[]) { mitkCommandLineParser parser; parser.setTitle("File Converter"); parser.setCategory("Basic Image Processing"); parser.setDescription(""); parser.setContributor("German Cancer Research Center (DKFZ)"); parser.setArgumentPrefix("--","-"); // Add command line argument names parser.addArgument("help", "h",mitkCommandLineParser::Bool, "Help:", "Show this help text"); parser.addArgument("input", "i", mitkCommandLineParser::File, "Input file:", "Input path that should be loaded.",us::Any(),false, false, false, mitkCommandLineParser::Input); parser.addArgument("output", "o", mitkCommandLineParser::File, "Output file:", "Output path where the result should be stored. If the input generates multiple outputs the index will be added for all but the first output (before the extension; starting with 0).", us::Any(), false, false, false, mitkCommandLineParser::Output); parser.addArgument("reader", "r", mitkCommandLineParser::String, "Reader Name", "Enforce a certain reader to be used for loading the input.", us::Any()); parser.addArgument("list-readers", "lr", mitkCommandLineParser::Bool, "List reader names", "Print names of all available readers.", us::Any()); std::map parsedArgs = parser.parseArguments(argc, argv); if (parsedArgs.size()==0) return EXIT_FAILURE; // Show a help message if ( parsedArgs.count("help") || parsedArgs.count("h")) { std::cout << parser.helpText(); return EXIT_SUCCESS; } std::string inputFilename = us::any_cast(parsedArgs["input"]); std::string outputFilename = us::any_cast(parsedArgs["output"]); mitk::PreferenceListReaderOptionsFunctor::ListType preference = {}; if (parsedArgs.count("reader")) { preference.push_back(us::any_cast(parsedArgs["reader"])); } if (parsedArgs.count("list-readers")) { mitk::IOUtil::LoadInfo loadInfo(inputFilename); auto readers = loadInfo.m_ReaderSelector.Get(); std::string errMsg; if (readers.empty()) { if (!itksys::SystemTools::FileExists(loadInfo.m_Path.c_str())) { errMsg += "File '" + loadInfo.m_Path + "' does not exist\n"; } else { errMsg += "No reader available for '" + loadInfo.m_Path + "'\n"; } MITK_ERROR << errMsg; return 0; } std::cout << "Available Readers: "< 0) { writeName = path + filename + "_" + std::to_string(count) + extension; } mitk::IOUtil::Save(node, writeName); ++count; try { auto splitReasonProperty = node->GetProperty(mitk::PropertyKeyPathToPropertyName(mitk::IOMetaInformationPropertyConstants::VOLUME_SPLIT_REASON()).c_str()); if (splitReasonProperty.IsNotNull()) { auto reasonStr = splitReasonProperty->GetValueAsString(); auto reason = mitk::IOVolumeSplitReason::FromJSON(reasonStr); - if (reason.IsNotNull() && reason->HasReason(mitk::IOVolumeSplitReason::ReasonType::MissingSlices)) + if (nullptr != reason && reason->HasReason(mitk::IOVolumeSplitReason::ReasonType::MissingSlices)) { missingSlicesDetected += std::stoi(reason->GetReasonDetails(mitk::IOVolumeSplitReason::ReasonType::MissingSlices)); } } } catch (const std::exception& e) { std::cerr << "Error while checking for existing split reasons in volume #" << count << "." << std::endl; std::cerr << "Error details:" << e.what() << std::endl; } catch (...) { std::cerr << "Unknown error while checking for existing split reasons in volume #" << count << "." << std::endl; } } if (missingSlicesDetected > 0) { std::cout << std::endl; std::cout << "\n!!! WARNING: MISSING SLICES !!!\n" "Details: Reader indicated volume splitting due to missing slices. Converted data might be invalid/incomplete.\n" "Estimated number of missing slices: " << missingSlicesDetected << std::endl; } return EXIT_SUCCESS; } diff --git a/Modules/DICOM/include/mitkDICOMDatasetSorter.h b/Modules/DICOM/include/mitkDICOMDatasetSorter.h index 958c9d20c9..84898d35dd 100644 --- a/Modules/DICOM/include/mitkDICOMDatasetSorter.h +++ b/Modules/DICOM/include/mitkDICOMDatasetSorter.h @@ -1,95 +1,95 @@ /*============================================================================ 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 mitkDICOMDatasetSorter_h #define mitkDICOMDatasetSorter_h #include "itkObjectFactory.h" #include "mitkCommon.h" #include "mitkDICOMDatasetAccess.h" #include "mitkIOVolumeSplitReason.h" namespace mitk { /** \ingroup DICOMModule \brief The sorting/splitting building-block of DICOMITKSeriesGDCMReader. This class describes the interface of the sorting/splitting process described as part of DICOMITKSeriesGDCMReader::AnalyzeInputFiles() (see \ref DICOMITKSeriesGDCMReader_LoadingStrategy). The procedure is simple: - take a list of input datasets (DICOMDatasetAccess) - sort them (to be defined by sub-classes, based on specific tags) - return the sorting result as outputs (the single input might be distributed into multiple outputs) The simplest and most generic form of sorting is implemented in sub-class DICOMTagBasedSorter. */ class MITKDICOM_EXPORT DICOMDatasetSorter : public itk::LightObject { public: mitkClassMacroItkParent( DICOMDatasetSorter, itk::LightObject ); /** \brief Return the tags of interest (to facilitate scanning) */ virtual DICOMTagList GetTagsOfInterest() = 0; /// \brief Input for sorting void SetInput(DICOMDatasetList filenames); /// \brief Input for sorting const DICOMDatasetList& GetInput() const; /// \brief Sort input datasets into one or multiple outputs. virtual void Sort() = 0; /// \brief Output of the sorting process. unsigned int GetNumberOfOutputs() const; /// \brief Output of the sorting process. const DICOMDatasetList& GetOutput(unsigned int index) const; /// \brief Output of the sorting process. DICOMDatasetList& GetOutput(unsigned int index); - const IOVolumeSplitReason* GetSplitReason(unsigned int index) const; + IOVolumeSplitReason::ConstPointer GetSplitReason(unsigned int index) const; /// \brief Print configuration details into stream. virtual void PrintConfiguration(std::ostream& os, const std::string& indent = "") const = 0; virtual bool operator==(const DICOMDatasetSorter& other) const = 0; protected: DICOMDatasetSorter(); ~DICOMDatasetSorter() override; DICOMDatasetSorter(const DICOMDatasetSorter& other); DICOMDatasetSorter& operator=(const DICOMDatasetSorter& other); void ClearOutputs(); void SetNumberOfOutputs(unsigned int numberOfOutputs); - void SetOutput(unsigned int index, const DICOMDatasetList& output, const IOVolumeSplitReason* splitReason = nullptr); + void SetOutput(unsigned int index, const DICOMDatasetList& output, IOVolumeSplitReason::ConstPointer splitReason = nullptr); private: DICOMDatasetList m_Input; std::vector< DICOMDatasetList > m_Outputs; std::vector< IOVolumeSplitReason::Pointer > m_SplitReasons; }; } #endif diff --git a/Modules/DICOM/include/mitkDICOMImageBlockDescriptor.h b/Modules/DICOM/include/mitkDICOMImageBlockDescriptor.h index d9f14139d6..1434cda24f 100644 --- a/Modules/DICOM/include/mitkDICOMImageBlockDescriptor.h +++ b/Modules/DICOM/include/mitkDICOMImageBlockDescriptor.h @@ -1,247 +1,247 @@ /*============================================================================ 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 mitkDICOMImageBlockDescriptor_h #define mitkDICOMImageBlockDescriptor_h #include "mitkDICOMEnums.h" #include "mitkDICOMImageFrameInfo.h" #include "mitkDICOMTag.h" #include "mitkDICOMTagCache.h" #include "mitkIOVolumeSplitReason.h" #include "mitkImage.h" #include "mitkProperties.h" #include "mitkWeakPointer.h" #include "mitkIPropertyProvider.h" #include "mitkGantryTiltInformation.h" #include namespace mitk { struct DICOMCachedValueInfo { unsigned int TimePoint; unsigned int SliceInTimePoint; std::string Value; }; class DICOMCachedValueLookupTable : public GenericLookupTable< DICOMCachedValueInfo > { public: typedef DICOMCachedValueLookupTable Self; typedef GenericLookupTable< DICOMCachedValueInfo > Superclass; const char *GetNameOfClass() const override { return "DICOMCachedValueLookupTable"; } DICOMCachedValueLookupTable() {} Superclass& operator=(const Superclass& other) override { return Superclass::operator=(other); } ~DICOMCachedValueLookupTable() override {} }; /** \ingroup DICOMModule \brief Output descriptor for DICOMFileReader. As a result of analysis by a mitk::DICOMFileReader, this class describes the properties of a single mitk::Images that could be loaded by the file reader. The descriptor contains the following information: - the mitk::Image itself. This will be nullptr after analysis and only be present after actual loading. - a list of frames (mostly: filenames) that went into composition of the mitk::Image. - an assessment of the reader's ability to load this set of files (ReaderImplementationLevel) - this can be used for reader selection when one reader is able to load an image with correct colors and the other is able to produce only gray values, for example - description of aspects of the image. Mostly a key-value list implemented by means of mitk::PropertyList. - for specific keys and possible values, see documentation of specific readers. \note an mitk::Image may both consist of multiple files (the "old" DICOM way) or a mitk::Image may be described by a single DICOM file or even only parts of a DICOM file (the newer multi-frame DICOM classes). To reflect this DICOMImageFrameList describes a list of frames from different or a single file. Described aspects of an image are: - whether pixel spacing is meant to be in-patient or on-detector (mitk::PixelSpacingInterpretation) - details about a possible gantry tilt (intended for use by file readers, may be hidden later) */ class MITKDICOM_EXPORT DICOMImageBlockDescriptor: public IPropertyProvider { public: DICOMImageBlockDescriptor(); ~DICOMImageBlockDescriptor() override; DICOMImageBlockDescriptor(const DICOMImageBlockDescriptor& other); DICOMImageBlockDescriptor& operator=(const DICOMImageBlockDescriptor& other); static DICOMTagList GetTagsOfInterest(); /// List of frames that constitute the mitk::Image (DICOMImageFrame%s) void SetImageFrameList(const DICOMImageFrameList& framelist); /// List of frames that constitute the mitk::Image (DICOMImageFrame%s) const DICOMImageFrameList& GetImageFrameList() const; /// The 3D mitk::Image that is loaded from the DICOM files of a DICOMImageFrameList void SetMitkImage(Image::Pointer image); /// the 3D mitk::Image that is loaded from the DICOM files of a DICOMImageFrameList Image::Pointer GetMitkImage() const; /// Reader's capability to appropriately load this set of frames ReaderImplementationLevel GetReaderImplementationLevel() const; /// Reader's capability to appropriately load this set of frames void SetReaderImplementationLevel(const ReaderImplementationLevel& level); /// Key-value store describing aspects of the image to be loaded void SetProperty(const std::string& key, BaseProperty* value); /// Key-value store describing aspects of the image to be loaded BaseProperty* GetProperty(const std::string& key) const; /// Convenience function around GetProperty() std::string GetPropertyAsString(const std::string&) const; /** Returns the pointer to the split reason of this block descriptor.*/ - const IOVolumeSplitReason* GetSplitReason() const; + IOVolumeSplitReason::ConstPointer GetSplitReason() const; /** Returns the pointer to the split reason of this block descriptor.*/ - IOVolumeSplitReason* GetSplitReason(); + IOVolumeSplitReason::Pointer GetSplitReason(); /** Sets the split reason for the block descriptor.*/ - void SetSplitReason(IOVolumeSplitReason* reason); + void SetSplitReason(IOVolumeSplitReason::Pointer reason); /// Convenience function around SetProperty() void SetFlag(const std::string& key, bool value); /// Convenience function around GetProperty() bool GetFlag(const std::string& key, bool defaultValue) const; /// Convenience function around SetProperty() void SetIntProperty(const std::string& key, int value); /// Convenience function around GetProperty() int GetIntProperty(const std::string& key, int defaultValue) const; BaseProperty::ConstPointer GetConstProperty(const std::string &propertyKey, const std::string &contextName = "", bool fallBackOnDefaultContext = true) const override; std::vector GetPropertyKeys(const std::string &contextName = "", bool includeDefaultContext = false) const override; std::vector GetPropertyContextNames() const override; private: // For future implementation: load slice-by-slice, mark this using these methods void SetSliceIsLoaded(unsigned int index, bool isLoaded); // For future implementation: load slice-by-slice, mark this using these methods bool IsSliceLoaded(unsigned int index) const; // For future implementation: load slice-by-slice, mark this using these methods bool AllSlicesAreLoaded() const; public: /// Describe how the mitk::Image's pixel spacing should be interpreted PixelSpacingInterpretation GetPixelSpacingInterpretation() const; /// Describe the correct x/y pixel spacing of the mitk::Image (which some readers might need to adjust after loading) void GetDesiredMITKImagePixelSpacing(ScalarType& spacingXinMM, ScalarType& spacingYinMM) const; /// Describe the gantry tilt of the acquisition void SetTiltInformation(const GantryTiltInformation& info); /// Describe the gantry tilt of the acquisition const GantryTiltInformation GetTiltInformation() const; /// SOP Class UID of this set of frames void SetSOPClassUID(const std::string& uid); /// SOP Class UID of this set of frames std::string GetSOPClassUID() const; /// SOP Class as human readable name (e.g. "CT Image Storage") std::string GetSOPClassUIDAsName() const; /**Convenience method that returns the property timesteps*/ int GetNumberOfTimeSteps() const; /**return the number of frames that constitute one timestep.*/ int GetNumberOfFramesPerTimeStep() const; void SetTagCache(DICOMTagCache* privateCache); /** Type specifies additional tags of interest. Key is the tag path of interest. * The value is an optional user defined name for the property that should be used to store the tag value(s). * Empty value is default and will imply to use the found DICOMTagPath as property name.*/ typedef std::map AdditionalTagsMapType; /** * \brief Set a list of DICOMTagPaths that specify all DICOM-Tags that will be copied into the property of the mitk::Image. * * This method can be used to specify a list of DICOM-tags that shall be available after the loading. * The value in the tagMap is an optional user defined name for the property key that should be used * when storing the property). Empty value is default and will imply to use the found DICOMTagPath * as property key. * By default the content of the DICOM tags will be stored in a StringLookupTable on the mitk::Image. * This behaviour can be changed by setting a different TagLookupTableToPropertyFunctor via * SetTagLookupTableToPropertyFunctor(). */ void SetAdditionalTagsOfInterest(const AdditionalTagsMapType& tagMap); typedef std::function TagLookupTableToPropertyFunctor; /** * \brief Set a functor that defines how the slice-specific tag-values are stored in a Property. * * This method sets a functor that is given a StringLookupTable that contains the values of one DICOM tag * mapped to the slice index. * The functor is supposed to store these values in an mitk Property. * * By default, the StringLookupTable is stored in a StringLookupTableProperty except if all values are * identical. In this case, the unique value is stored only once in a StringProperty. */ void SetTagLookupTableToPropertyFunctor(TagLookupTableToPropertyFunctor); /// Print information about this image block to given stream void Print(std::ostream& os, bool filenameDetails) const; private: // read values from tag cache std::string GetPixelSpacing() const; std::string GetImagerPixelSpacing() const; Image::Pointer FixupSpacing(Image* mitkImage); Image::Pointer DescribeImageWithProperties(Image* mitkImage); void UpdateImageDescribingProperties() const; static mitk::BaseProperty::Pointer GetPropertyForDICOMValues(const DICOMCachedValueLookupTable& cacheLookupTable); double stringtodouble(const std::string& str) const; DICOMImageFrameList m_ImageFrameList; Image::Pointer m_MitkImage; BoolList m_SliceIsLoaded; ReaderImplementationLevel m_ReaderImplementationLevel; GantryTiltInformation m_TiltInformation; PropertyList::Pointer m_PropertyList; IOVolumeSplitReason::Pointer m_SplitReason; WeakPointer m_TagCache; mutable bool m_PropertiesOutOfDate; AdditionalTagsMapType m_AdditionalTagMap; std::set m_FoundAdditionalTags; TagLookupTableToPropertyFunctor m_PropertyFunctor; }; } #endif diff --git a/Modules/DICOM/include/mitkEquiDistantBlocksSorter.h b/Modules/DICOM/include/mitkEquiDistantBlocksSorter.h index bf3f508cb5..871d0e8341 100644 --- a/Modules/DICOM/include/mitkEquiDistantBlocksSorter.h +++ b/Modules/DICOM/include/mitkEquiDistantBlocksSorter.h @@ -1,222 +1,222 @@ /*============================================================================ 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 mitkEquiDistantBlocksSorter_h #define mitkEquiDistantBlocksSorter_h #include "mitkDICOMDatasetSorter.h" #include "mitkDICOMSortCriterion.h" #include "mitkGantryTiltInformation.h" #include "mitkVector.h" namespace mitk { /** \ingroup DICOMModule \brief Split inputs into blocks of equidistant slices (for use in DICOMITKSeriesGDCMReader). Since inter-slice distance is not recorded in DICOM tags, we must ensure that blocks are made up of slices that have equal distances between neighboring slices. This is especially necessary because itk::ImageSeriesReader is later used for the actual loading, and this class expects (and does nocht verify) equal inter-slice distance (see \ref DICOMITKSeriesGDCMReader_ForcedConfiguration). To achieve such grouping, the inter-slice distance is calculated from the first two different slice positions of a block. Following slices are added to a block as long as they can be added by adding the calculated inter-slice distance to the last slice of the block. Slices that do not fit into the expected distance pattern, are set aside for further analysis. This grouping is done until each file has been assigned to a group. Slices that share a position in space are also sorted into separate blocks during this step. So the result of this step is a set of blocks that contain only slices with equal z spacing and unique slices at each position. During sorting, the origins (documented in tag image position patient) are compared against expected origins (from former origin plus moving direction). As there will be minor differences in numbers (from both calculations and imprecise tag values), we must be a bit tolerant here. The default behavior is to expect that an origin is not further away from the expected position than 30% of the inter-slice distance. To support a legacy behavior of a former loader (DicomSeriesReader), this default can be restricted to a constant number of millimeters by calling SetToleratedOriginOffset(mm). REMARK: The EquiDistantBlocksSorter assumes that the order of the provided input is sorted by image position like it is preferred by the reader and does not sort it again. This assumption can lead to splittings even for complete volumes if the input is not sorted by image position. The reason is that gaps are detected because the next slice does not have the assumed distance and will be sorted out. Detailed implementation in AnalyzeFileForITKImageSeriesReaderSpacingAssumption(). */ class MITKDICOM_EXPORT EquiDistantBlocksSorter : public DICOMDatasetSorter { public: mitkClassMacro( EquiDistantBlocksSorter, DICOMDatasetSorter ); itkNewMacro( EquiDistantBlocksSorter ); DICOMTagList GetTagsOfInterest() override; /** \brief Delegates work to AnalyzeFileForITKImageSeriesReaderSpacingAssumption(). AnalyzeFileForITKImageSeriesReaderSpacingAssumption() is called until it does not create multiple blocks anymore. */ void Sort() override; /** \brief Whether or not to accept images from a tilted acquisition in a single output group. */ void SetAcceptTilt(bool accept); bool GetAcceptTilt() const; /** \brief See class description and SetToleratedOriginOffset(). */ void SetToleratedOriginOffsetToAdaptive(double fractionOfInterSliceDistanct = 0.3); /** \brief See class description and SetToleratedOriginOffsetToAdaptive(). Default value of 0.005 is calculated so that we get a maximum of 1/10mm error when having a measurement crosses 20 slices in z direction (too strict? we don't know better..). */ void SetToleratedOriginOffset(double millimeters = 0.005); double GetToleratedOriginOffset() const; bool IsToleratedOriginOffsetAbsolute() const; void SetAcceptTwoSlicesGroups(bool accept); bool GetAcceptTwoSlicesGroups() const; void PrintConfiguration(std::ostream& os, const std::string& indent = "") const override; bool operator==(const DICOMDatasetSorter& other) const override; protected: /** \brief Return type of AnalyzeFileForITKImageSeriesReaderSpacingAssumption(). Class contains the grouping result of method AnalyzeFileForITKImageSeriesReaderSpacingAssumption(), which takes as input a number of images, which are all equally oriented and spatially sorted along their normal direction. The result contains of two blocks: a first one is the grouping result, all of those images can be loaded into one image block because they have an equal origin-to-origin distance without any gaps in-between. */ class SliceGroupingAnalysisResult { public: SliceGroupingAnalysisResult(); /** \brief Grouping result, all same origin-to-origin distance w/o gaps. */ const DICOMDatasetList& GetBlockDatasets() const; void SetFirstFilenameOfBlock(const std::string& filename); std::string GetFirstFilenameOfBlock() const; void SetLastFilenameOfBlock(const std::string& filename); std::string GetLastFilenameOfBlock() const; /** \brief Remaining files, which could not be grouped. */ const DICOMDatasetList& GetUnsortedDatasets() const; - const IOVolumeSplitReason* GetSplitReason() const; - IOVolumeSplitReason* GetSplitReason(); + IOVolumeSplitReason::ConstPointer GetSplitReason() const; + IOVolumeSplitReason::Pointer GetSplitReason(); /** \brief Whether or not the grouped result contain a gantry tilt. */ bool ContainsGantryTilt(); /** \brief Detailed description of gantry tilt. */ const GantryTiltInformation& GetTiltInfo() const; /** \brief Meant for internal use by AnalyzeFileForITKImageSeriesReaderSpacingAssumption only. */ void AddFileToSortedBlock(DICOMDatasetAccess* dataset); /** \brief Meant for internal use by AnalyzeFileForITKImageSeriesReaderSpacingAssumption only. */ void AddFileToUnsortedBlock(DICOMDatasetAccess* dataset); void AddFilesToUnsortedBlock(const DICOMDatasetList& datasets); /** \brief Meant for internal use by AnalyzeFileForITKImageSeriesReaderSpacingAssumption only. \todo Could make sense to enhance this with an instance of GantryTiltInformation to store the whole result! */ void FlagGantryTilt(const GantryTiltInformation& tiltInfo); /** \brief Only meaningful for use by AnalyzeFileForITKImageSeriesReaderSpacingAssumption. */ void UndoPrematureGrouping(); protected: DICOMDatasetList m_GroupedFiles; DICOMDatasetList m_UnsortedFiles; IOVolumeSplitReason::Pointer m_SplitReason; GantryTiltInformation m_TiltInfo; std::string m_FirstFilenameOfBlock; std::string m_LastFilenameOfBlock; }; /** \brief Ensure an equal z-spacing for a group of files. Takes as input a number of images, which are all equally oriented and spatially sorted along their normal direction. Internally used by GetSeries. Returns two lists: the first one contains slices of equal inter-slice spacing. The second list contains remaining files, which need to be run through AnalyzeFileForITKImageSeriesReaderSpacingAssumption again. Relevant code that is matched here is in itkImageSeriesReader.txx (ImageSeriesReader::GenerateOutputInformation(void)), lines 176 to 245 (as of ITK 3.20) */ std::shared_ptr AnalyzeFileForITKImageSeriesReaderSpacingAssumption(const DICOMDatasetList& files, bool groupsOfSimilarImages); /** \brief Safely convert const char* to std::string. */ std::string ConstCharStarToString(const char* s); EquiDistantBlocksSorter(); ~EquiDistantBlocksSorter() override; EquiDistantBlocksSorter(const EquiDistantBlocksSorter& other); EquiDistantBlocksSorter& operator=(const EquiDistantBlocksSorter& other); bool m_AcceptTilt; typedef std::vector > ResultsList; ResultsList m_SliceGroupingResults; double m_ToleratedOriginOffset; bool m_ToleratedOriginOffsetIsAbsolute; bool m_AcceptTwoSlicesGroups; }; } #endif diff --git a/Modules/DICOM/src/mitkDICOMDatasetSorter.cpp b/Modules/DICOM/src/mitkDICOMDatasetSorter.cpp index 70da4fc1f8..957e913ed8 100644 --- a/Modules/DICOM/src/mitkDICOMDatasetSorter.cpp +++ b/Modules/DICOM/src/mitkDICOMDatasetSorter.cpp @@ -1,134 +1,134 @@ /*============================================================================ 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 "mitkDICOMDatasetSorter.h" mitk::DICOMDatasetSorter ::DICOMDatasetSorter() :itk::LightObject() { } mitk::DICOMDatasetSorter ::~DICOMDatasetSorter() { } mitk::DICOMDatasetSorter ::DICOMDatasetSorter(const DICOMDatasetSorter& other ) :itk::LightObject() ,m_Outputs( other.m_Outputs ) { } mitk::DICOMDatasetSorter& mitk::DICOMDatasetSorter ::operator=(const DICOMDatasetSorter& other) { if (this != &other) { m_Input = other.m_Input; m_Outputs = other.m_Outputs; } return *this; } void mitk::DICOMDatasetSorter ::SetInput(DICOMDatasetList datasets) { m_Input = datasets; } const mitk::DICOMDatasetList& mitk::DICOMDatasetSorter ::GetInput() const { return m_Input; } unsigned int mitk::DICOMDatasetSorter ::GetNumberOfOutputs() const { return m_Outputs.size(); } void mitk::DICOMDatasetSorter ::ClearOutputs() { m_Outputs.clear(); m_SplitReasons.clear(); } void mitk::DICOMDatasetSorter ::SetNumberOfOutputs(unsigned int numberOfOutputs) { m_Outputs.resize(numberOfOutputs); m_SplitReasons.resize(numberOfOutputs); } void mitk::DICOMDatasetSorter -::SetOutput(unsigned int index, const DICOMDatasetList& output, const IOVolumeSplitReason* splitReason) +::SetOutput(unsigned int index, const DICOMDatasetList& output, IOVolumeSplitReason::ConstPointer splitReason) { if (index < m_Outputs.size()) { m_Outputs[index] = output; m_SplitReasons[index] = (nullptr == splitReason) ? IOVolumeSplitReason::New() : splitReason->Clone(); } else { std::stringstream ss; ss << "Cannot get output. Index " << index << " out of range (" << m_Outputs.size() << " indices reserved)"; throw std::invalid_argument( ss.str() ); } } const mitk::DICOMDatasetList& mitk::DICOMDatasetSorter ::GetOutput(unsigned int index) const { return const_cast(this)->GetOutput(index); } mitk::DICOMDatasetList& mitk::DICOMDatasetSorter ::GetOutput(unsigned int index) { if (index < m_Outputs.size()) { return m_Outputs[index]; } else { std::stringstream ss; ss << "Cannot get output. Index " << index << " out of range (" << m_Outputs.size() << " indices reserved)"; throw std::invalid_argument( ss.str() ); } } -const mitk::IOVolumeSplitReason* +mitk::IOVolumeSplitReason::ConstPointer mitk::DICOMDatasetSorter ::GetSplitReason(unsigned int index) const { if (index >= m_Outputs.size()) { std::stringstream ss; ss << "Cannot get split reason. Index " << index << " out of range (" << m_Outputs.size() << " indices reserved)"; throw std::invalid_argument(ss.str()); } return m_SplitReasons[index]; } diff --git a/Modules/DICOM/src/mitkDICOMITKSeriesGDCMReader.cpp b/Modules/DICOM/src/mitkDICOMITKSeriesGDCMReader.cpp index b5f7064aa5..98808873f0 100644 --- a/Modules/DICOM/src/mitkDICOMITKSeriesGDCMReader.cpp +++ b/Modules/DICOM/src/mitkDICOMITKSeriesGDCMReader.cpp @@ -1,635 +1,634 @@ /*============================================================================ 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. ============================================================================*/ //#define MBILOG_ENABLE_DEBUG #define ENABLE_TIMING #include #include #include "mitkDICOMITKSeriesGDCMReader.h" #include "mitkITKDICOMSeriesReaderHelper.h" #include "mitkGantryTiltInformation.h" #include "mitkDICOMTagBasedSorter.h" #include "mitkDICOMGDCMTagScanner.h" std::mutex mitk::DICOMITKSeriesGDCMReader::s_LocaleMutex; mitk::DICOMITKSeriesGDCMReader::DICOMITKSeriesGDCMReader( unsigned int decimalPlacesForOrientation, bool simpleVolumeImport ) : DICOMFileReader() , m_FixTiltByShearing(m_DefaultFixTiltByShearing) , m_SimpleVolumeReading( simpleVolumeImport ) , m_DecimalPlacesForOrientation( decimalPlacesForOrientation ) , m_ExternalCache(false) { this->EnsureMandatorySortersArePresent( decimalPlacesForOrientation, simpleVolumeImport ); } mitk::DICOMITKSeriesGDCMReader::DICOMITKSeriesGDCMReader( const DICOMITKSeriesGDCMReader& other ) : DICOMFileReader( other ) , m_FixTiltByShearing( other.m_FixTiltByShearing) , m_SimpleVolumeReading( other.m_SimpleVolumeReading) , m_SortingResultInProgress( other.m_SortingResultInProgress ) , m_Sorter( other.m_Sorter ) , m_EquiDistantBlocksSorter( other.m_EquiDistantBlocksSorter->Clone() ) , m_NormalDirectionConsistencySorter( other.m_NormalDirectionConsistencySorter->Clone() ) , m_ReplacedCLocales( other.m_ReplacedCLocales ) , m_ReplacedCinLocales( other.m_ReplacedCinLocales ) , m_DecimalPlacesForOrientation( other.m_DecimalPlacesForOrientation ) , m_TagCache( other.m_TagCache ) , m_ExternalCache(other.m_ExternalCache) { } mitk::DICOMITKSeriesGDCMReader::~DICOMITKSeriesGDCMReader() { } mitk::DICOMITKSeriesGDCMReader& mitk::DICOMITKSeriesGDCMReader:: operator=( const DICOMITKSeriesGDCMReader& other ) { if ( this != &other ) { DICOMFileReader::operator =( other ); this->m_FixTiltByShearing = other.m_FixTiltByShearing; this->m_SimpleVolumeReading = other.m_SimpleVolumeReading; this->m_SortingResultInProgress = other.m_SortingResultInProgress; this->m_Sorter = other.m_Sorter; // TODO should clone the list items this->m_EquiDistantBlocksSorter = other.m_EquiDistantBlocksSorter->Clone(); this->m_NormalDirectionConsistencySorter = other.m_NormalDirectionConsistencySorter->Clone(); this->m_ReplacedCLocales = other.m_ReplacedCLocales; this->m_ReplacedCinLocales = other.m_ReplacedCinLocales; this->m_DecimalPlacesForOrientation = other.m_DecimalPlacesForOrientation; this->m_TagCache = other.m_TagCache; } return *this; } bool mitk::DICOMITKSeriesGDCMReader::operator==( const DICOMFileReader& other ) const { if ( const auto* otherSelf = dynamic_cast( &other ) ) { if ( this->m_FixTiltByShearing == otherSelf->m_FixTiltByShearing && *( this->m_EquiDistantBlocksSorter ) == *( otherSelf->m_EquiDistantBlocksSorter ) && ( fabs( this->m_DecimalPlacesForOrientation - otherSelf->m_DecimalPlacesForOrientation ) < eps ) ) { // test sorters for equality if ( this->m_Sorter.size() != otherSelf->m_Sorter.size() ) return false; auto mySorterIter = this->m_Sorter.cbegin(); auto oSorterIter = otherSelf->m_Sorter.cbegin(); for ( ; mySorterIter != this->m_Sorter.cend() && oSorterIter != otherSelf->m_Sorter.cend(); ++mySorterIter, ++oSorterIter ) { if ( !( **mySorterIter == **oSorterIter ) ) return false; // this sorter differs } // nothing differs ==> all is equal return true; } else { return false; } } else { return false; } } void mitk::DICOMITKSeriesGDCMReader::SetFixTiltByShearing( bool on ) { this->Modified(); m_FixTiltByShearing = on; } bool mitk::DICOMITKSeriesGDCMReader::GetFixTiltByShearing() const { return m_FixTiltByShearing; } void mitk::DICOMITKSeriesGDCMReader::SetAcceptTwoSlicesGroups( bool accept ) const { this->Modified(); m_EquiDistantBlocksSorter->SetAcceptTwoSlicesGroups( accept ); } bool mitk::DICOMITKSeriesGDCMReader::GetAcceptTwoSlicesGroups() const { return m_EquiDistantBlocksSorter->GetAcceptTwoSlicesGroups(); } void mitk::DICOMITKSeriesGDCMReader::InternalPrintConfiguration( std::ostream& os ) const { unsigned int sortIndex( 1 ); for ( auto sorterIter = m_Sorter.cbegin(); sorterIter != m_Sorter.cend(); ++sortIndex, ++sorterIter ) { os << "Sorting step " << sortIndex << ":" << std::endl; ( *sorterIter )->PrintConfiguration( os, " " ); } os << "Sorting step " << sortIndex << ":" << std::endl; m_EquiDistantBlocksSorter->PrintConfiguration( os, " " ); } std::string mitk::DICOMITKSeriesGDCMReader::GetActiveLocale() { return setlocale( LC_NUMERIC, nullptr ); } void mitk::DICOMITKSeriesGDCMReader::PushLocale() const { s_LocaleMutex.lock(); std::string currentCLocale = setlocale( LC_NUMERIC, nullptr ); m_ReplacedCLocales.push( currentCLocale ); setlocale( LC_NUMERIC, "C" ); std::locale currentCinLocale( std::cin.getloc() ); m_ReplacedCinLocales.push( currentCinLocale ); std::locale l( "C" ); std::cin.imbue( l ); s_LocaleMutex.unlock(); } void mitk::DICOMITKSeriesGDCMReader::PopLocale() const { s_LocaleMutex.lock(); if ( !m_ReplacedCLocales.empty() ) { setlocale( LC_NUMERIC, m_ReplacedCLocales.top().c_str() ); m_ReplacedCLocales.pop(); } else { MITK_WARN << "Mismatched PopLocale on DICOMITKSeriesGDCMReader."; } if ( !m_ReplacedCinLocales.empty() ) { std::cin.imbue( m_ReplacedCinLocales.top() ); m_ReplacedCinLocales.pop(); } else { MITK_WARN << "Mismatched PopLocale on DICOMITKSeriesGDCMReader."; } s_LocaleMutex.unlock(); } mitk::DICOMITKSeriesGDCMReader::SortingBlockList mitk::DICOMITKSeriesGDCMReader::Condense3DBlocks( SortingBlockList& input ) { return input; // to be implemented differently by sub-classes } #if defined( MBILOG_ENABLE_DEBUG ) || defined( ENABLE_TIMING ) #define timeStart( part ) timer.Start( part ); #define timeStop( part ) timer.Stop( part ); #else #define timeStart( part ) #define timeStop( part ) #endif void mitk::DICOMITKSeriesGDCMReader::AnalyzeInputFiles() { itk::TimeProbesCollectorBase timer; timeStart( "Reset" ); this->ClearOutputs(); timeStop( "Reset" ); // prepare initial sorting (== list of input files) const StringList inputFilenames = this->GetInputFiles(); timeStart( "Check input for DCM" ); if ( inputFilenames.empty() || !this->CanHandleFile( inputFilenames.front() ) // first || !this->CanHandleFile( inputFilenames.back() ) // last || !this->CanHandleFile( inputFilenames[inputFilenames.size() / 2] ) // roughly central file ) { // TODO a read-as-many-as-possible fallback could be implemented here MITK_DEBUG << "Reader unable to process files.."; return; } timeStop( "Check input for DCM" ); // scan files for sorting-relevant tags if ( m_TagCache.IsNull() || ( m_TagCache->GetMTime()GetMTime() && !m_ExternalCache )) { timeStart( "Tag scanning" ); DICOMGDCMTagScanner::Pointer filescanner = DICOMGDCMTagScanner::New(); filescanner->SetInputFiles( inputFilenames ); filescanner->AddTagPaths( this->GetTagsOfInterest() ); PushLocale(); filescanner->Scan(); PopLocale(); m_TagCache = filescanner->GetScanCache(); // keep alive and make accessible to sub-classes timeStop("Tag scanning"); } else { // ensure that the tag cache contains our required tags AND files and has scanned! } m_SortingResultInProgress.clear(); m_SortingResultInProgress.emplace_back(m_TagCache->GetFrameInfoList(), IOVolumeSplitReason::New()); // sort and split blocks as configured timeStart( "Sorting frames" ); unsigned int sorterIndex = 0; for ( auto sorterIter = m_Sorter.cbegin(); sorterIter != m_Sorter.cend(); ++sorterIndex, ++sorterIter ) { std::stringstream ss; ss << "Sorting step " << sorterIndex; timeStart( ss.str().c_str() ); m_SortingResultInProgress = this->InternalExecuteSortingStep( sorterIndex, *sorterIter, m_SortingResultInProgress ); timeStop( ss.str().c_str() ); } if ( !m_SimpleVolumeReading ) { // a last extra-sorting step: ensure equidistant slices timeStart( "EquiDistantBlocksSorter" ); m_SortingResultInProgress = this->InternalExecuteSortingStep( sorterIndex++, m_EquiDistantBlocksSorter.GetPointer(), m_SortingResultInProgress ); timeStop( "EquiDistantBlocksSorter" ); } timeStop( "Sorting frames" ); timeStart( "Condensing 3D blocks" ); m_SortingResultInProgress = this->Condense3DBlocks( m_SortingResultInProgress ); timeStop( "Condensing 3D blocks" ); // provide final result as output timeStart( "Output" ); unsigned int o = this->GetNumberOfOutputs(); this->SetNumberOfOutputs( o + m_SortingResultInProgress.size() ); // Condense3DBlocks may already have added outputs! for ( auto blockIter = m_SortingResultInProgress.cbegin(); blockIter != m_SortingResultInProgress.cend(); ++o, ++blockIter ) { const auto& gdcmFrameInfoList = blockIter->first; auto& splitReason = blockIter->second; assert( !gdcmFrameInfoList.empty() ); // reverse frames if necessary // update tilt information from absolute last sorting const DICOMDatasetList datasetList = ConvertToDICOMDatasetList( gdcmFrameInfoList ); m_NormalDirectionConsistencySorter->SetInput( datasetList ); m_NormalDirectionConsistencySorter->Sort(); const DICOMDatasetAccessingImageFrameList sortedGdcmInfoFrameList = ConvertToDICOMDatasetAccessingImageFrameList( m_NormalDirectionConsistencySorter->GetOutput( 0 ) ); const GantryTiltInformation& tiltInfo = m_NormalDirectionConsistencySorter->GetTiltInformation(); // set frame list for current block const DICOMImageFrameList frameList = ConvertToDICOMImageFrameList( sortedGdcmInfoFrameList ); assert( !frameList.empty() ); DICOMImageBlockDescriptor block; block.SetTagCache( this->GetTagCache() ); // important: this must be before SetImageFrameList(), because // SetImageFrameList will trigger reading of lots of interesting // tags! block.SetAdditionalTagsOfInterest( GetAdditionalTagsOfInterest() ); block.SetTagLookupTableToPropertyFunctor( GetTagLookupTableToPropertyFunctor() ); block.SetImageFrameList( frameList ); block.SetTiltInformation( tiltInfo ); block.SetSplitReason(splitReason); block.SetReaderImplementationLevel( this->GetReaderImplementationLevel( block.GetSOPClassUID() ) ); this->SetOutput( o, block ); } timeStop( "Output" ); #if defined( MBILOG_ENABLE_DEBUG ) || defined( ENABLE_TIMING ) std::cout << "---------------------------------------------------------------" << std::endl; timer.Report( std::cout ); std::cout << "---------------------------------------------------------------" << std::endl; #endif } mitk::DICOMITKSeriesGDCMReader::SortingBlockList mitk::DICOMITKSeriesGDCMReader::InternalExecuteSortingStep( unsigned int sortingStepIndex, const DICOMDatasetSorter::Pointer& sorter, const SortingBlockList& input ) { SortingBlockList nextStepSorting; // we should not modify our input list while processing it std::stringstream ss; ss << "Sorting step " << sortingStepIndex << " '"; #if defined( MBILOG_ENABLE_DEBUG ) sorter->PrintConfiguration( ss ); #endif ss << "'"; nextStepSorting.clear(); MITK_DEBUG << "================================================================================"; MITK_DEBUG << "DICOMITKSeriesGDCMReader: " << ss.str() << ": " << input.size() << " groups input"; #if defined( MBILOG_ENABLE_DEBUG ) unsigned int groupIndex = 0; #endif for ( auto blockIter = input.cbegin(); blockIter != input.cend(); #if defined( MBILOG_ENABLE_DEBUG ) ++groupIndex, #endif ++blockIter ) { - const auto& gdcmInfoFrameList = blockIter->first; - const auto& inputSplitReason = blockIter->second; + const auto& [gdcmInfoFrameList, inputSplitReason] = *blockIter; const DICOMDatasetList datasetList = ConvertToDICOMDatasetList( gdcmInfoFrameList ); #if defined( MBILOG_ENABLE_DEBUG ) MITK_DEBUG << "--------------------------------------------------------------------------------"; MITK_DEBUG << "DICOMITKSeriesGDCMReader: " << ss.str() << ", dataset group " << groupIndex << " (" << datasetList.size() << " datasets): "; for ( auto oi = datasetList.cbegin(); oi != datasetList.cend(); ++oi ) { MITK_DEBUG << " INPUT : " << ( *oi )->GetFilenameIfAvailable(); } #endif sorter->SetInput( datasetList ); sorter->Sort(); unsigned int numberOfResultingBlocks = sorter->GetNumberOfOutputs(); for ( unsigned int b = 0; b < numberOfResultingBlocks; ++b ) { const DICOMDatasetList blockResult = sorter->GetOutput( b ); for ( auto oi = blockResult.cbegin(); oi != blockResult.cend(); ++oi ) { MITK_DEBUG << " OUTPUT(" << b << ") :" << ( *oi )->GetFilenameIfAvailable(); } DICOMDatasetAccessingImageFrameList sortedGdcmInfoFrameList = ConvertToDICOMDatasetAccessingImageFrameList( blockResult ); nextStepSorting.emplace_back(sortedGdcmInfoFrameList, inputSplitReason->ExtendReason(sorter->GetSplitReason(b)) ); } } return nextStepSorting; } mitk::ReaderImplementationLevel mitk::DICOMITKSeriesGDCMReader::GetReaderImplementationLevel( const std::string sopClassUID ) { if ( sopClassUID.empty() ) { return SOPClassUnknown; } gdcm::UIDs uidKnowledge; uidKnowledge.SetFromUID( sopClassUID.c_str() ); gdcm::UIDs::TSName gdcmType = static_cast((gdcm::UIDs::TSType)uidKnowledge); switch ( gdcmType ) { case gdcm::UIDs::CTImageStorage: case gdcm::UIDs::MRImageStorage: case gdcm::UIDs::PositronEmissionTomographyImageStorage: case gdcm::UIDs::ComputedRadiographyImageStorage: case gdcm::UIDs::DigitalXRayImageStorageForPresentation: case gdcm::UIDs::DigitalXRayImageStorageForProcessing: return SOPClassSupported; case gdcm::UIDs::NuclearMedicineImageStorage: return SOPClassPartlySupported; case gdcm::UIDs::SecondaryCaptureImageStorage: return SOPClassImplemented; default: return SOPClassUnsupported; } } // void AllocateOutputImages(); bool mitk::DICOMITKSeriesGDCMReader::LoadImages() { bool success = true; unsigned int numberOfOutputs = this->GetNumberOfOutputs(); for ( unsigned int o = 0; o < numberOfOutputs; ++o ) { success &= this->LoadMitkImageForOutput( o ); } return success; } bool mitk::DICOMITKSeriesGDCMReader::LoadMitkImageForImageBlockDescriptor( DICOMImageBlockDescriptor& block ) const { PushLocale(); const DICOMImageFrameList& frames = block.GetImageFrameList(); const GantryTiltInformation tiltInfo = block.GetTiltInformation(); bool hasTilt = tiltInfo.IsRegularGantryTilt(); ITKDICOMSeriesReaderHelper::StringContainer filenames; filenames.reserve( frames.size() ); for ( auto frameIter = frames.cbegin(); frameIter != frames.cend(); ++frameIter ) { filenames.push_back( ( *frameIter )->Filename ); } mitk::ITKDICOMSeriesReaderHelper helper; bool success( true ); try { mitk::Image::Pointer mitkImage = helper.Load( filenames, m_FixTiltByShearing && hasTilt, tiltInfo ); block.SetMitkImage( mitkImage ); } catch ( const std::exception& e ) { success = false; MITK_ERROR << "Exception during image loading: " << e.what(); } PopLocale(); return success; } bool mitk::DICOMITKSeriesGDCMReader::LoadMitkImageForOutput( unsigned int o ) { DICOMImageBlockDescriptor& block = this->InternalGetOutput( o ); return this->LoadMitkImageForImageBlockDescriptor( block ); } bool mitk::DICOMITKSeriesGDCMReader::CanHandleFile( const std::string& filename ) { return ITKDICOMSeriesReaderHelper::CanHandleFile( filename ); } void mitk::DICOMITKSeriesGDCMReader::AddSortingElement( DICOMDatasetSorter* sorter, bool atFront ) { assert( sorter ); if ( atFront ) { m_Sorter.push_front( sorter ); } else { m_Sorter.push_back( sorter ); } this->Modified(); } mitk::DICOMITKSeriesGDCMReader::ConstSorterList mitk::DICOMITKSeriesGDCMReader::GetFreelyConfiguredSortingElements() const { std::list result; unsigned int sortIndex( 0 ); for ( auto sorterIter = m_Sorter.begin(); sorterIter != m_Sorter.end(); ++sortIndex, ++sorterIter ) { if ( sortIndex > 0 ) // ignore first element (see EnsureMandatorySortersArePresent) { result.push_back( ( *sorterIter ).GetPointer() ); } } return result; } void mitk::DICOMITKSeriesGDCMReader::EnsureMandatorySortersArePresent( unsigned int decimalPlacesForOrientation, bool simpleVolumeImport ) { DICOMTagBasedSorter::Pointer splitter = DICOMTagBasedSorter::New(); splitter->AddDistinguishingTag( DICOMTag(0x0028, 0x0010) ); // Number of Rows splitter->AddDistinguishingTag( DICOMTag(0x0028, 0x0011) ); // Number of Columns splitter->AddDistinguishingTag( DICOMTag(0x0028, 0x0030) ); // Pixel Spacing splitter->AddDistinguishingTag( DICOMTag(0x0018, 0x1164) ); // Imager Pixel Spacing splitter->AddDistinguishingTag( DICOMTag(0x0020, 0x0037), new mitk::DICOMTagBasedSorter::CutDecimalPlaces(decimalPlacesForOrientation) ); // Image Orientation (Patient) splitter->AddDistinguishingTag( DICOMTag(0x0018, 0x0050) ); // Slice Thickness if ( simpleVolumeImport ) { MITK_DEBUG << "Simple volume reading: ignoring number of frames"; } else { splitter->AddDistinguishingTag( DICOMTag(0x0028, 0x0008) ); // Number of Frames } this->AddSortingElement( splitter, true ); // true = at front if ( m_EquiDistantBlocksSorter.IsNull() ) { m_EquiDistantBlocksSorter = mitk::EquiDistantBlocksSorter::New(); } m_EquiDistantBlocksSorter->SetAcceptTilt( m_FixTiltByShearing ); if ( m_NormalDirectionConsistencySorter.IsNull() ) { m_NormalDirectionConsistencySorter = mitk::NormalDirectionConsistencySorter::New(); } } void mitk::DICOMITKSeriesGDCMReader::SetToleratedOriginOffsetToAdaptive( double fractionOfInterSliceDistance ) const { assert( m_EquiDistantBlocksSorter.IsNotNull() ); m_EquiDistantBlocksSorter->SetToleratedOriginOffsetToAdaptive( fractionOfInterSliceDistance ); this->Modified(); } void mitk::DICOMITKSeriesGDCMReader::SetToleratedOriginOffset( double millimeters ) const { assert( m_EquiDistantBlocksSorter.IsNotNull() ); m_EquiDistantBlocksSorter->SetToleratedOriginOffset( millimeters ); this->Modified(); } double mitk::DICOMITKSeriesGDCMReader::GetToleratedOriginError() const { assert( m_EquiDistantBlocksSorter.IsNotNull() ); return m_EquiDistantBlocksSorter->GetToleratedOriginOffset(); } bool mitk::DICOMITKSeriesGDCMReader::IsToleratedOriginOffsetAbsolute() const { assert( m_EquiDistantBlocksSorter.IsNotNull() ); return m_EquiDistantBlocksSorter->IsToleratedOriginOffsetAbsolute(); } double mitk::DICOMITKSeriesGDCMReader::GetDecimalPlacesForOrientation() const { return m_DecimalPlacesForOrientation; } mitk::DICOMTagCache::Pointer mitk::DICOMITKSeriesGDCMReader::GetTagCache() const { return m_TagCache; } void mitk::DICOMITKSeriesGDCMReader::SetTagCache( const DICOMTagCache::Pointer& tagCache ) { m_TagCache = tagCache; m_ExternalCache = tagCache.IsNotNull(); } mitk::DICOMTagPathList mitk::DICOMITKSeriesGDCMReader::GetTagsOfInterest() const { DICOMTagPathList completeList; // check all configured sorters for ( auto sorterIter = m_Sorter.cbegin(); sorterIter != m_Sorter.cend(); ++sorterIter ) { assert( sorterIter->IsNotNull() ); const DICOMTagList tags = ( *sorterIter )->GetTagsOfInterest(); completeList.insert( completeList.end(), tags.cbegin(), tags.cend() ); } // check our own forced sorters DICOMTagList tags = m_EquiDistantBlocksSorter->GetTagsOfInterest(); completeList.insert( completeList.end(), tags.cbegin(), tags.cend() ); tags = m_NormalDirectionConsistencySorter->GetTagsOfInterest(); completeList.insert( completeList.end(), tags.cbegin(), tags.cend() ); // add the tags for DICOMImageBlockDescriptor tags = DICOMImageBlockDescriptor::GetTagsOfInterest(); completeList.insert( completeList.end(), tags.cbegin(), tags.cend() ); const AdditionalTagsMapType tagList = GetAdditionalTagsOfInterest(); for ( auto iter = tagList.cbegin(); iter != tagList.cend(); ++iter ) { completeList.push_back( iter->first ) ; } return completeList; } diff --git a/Modules/DICOM/src/mitkDICOMImageBlockDescriptor.cpp b/Modules/DICOM/src/mitkDICOMImageBlockDescriptor.cpp index b19672530d..de4866f36c 100644 --- a/Modules/DICOM/src/mitkDICOMImageBlockDescriptor.cpp +++ b/Modules/DICOM/src/mitkDICOMImageBlockDescriptor.cpp @@ -1,940 +1,940 @@ /*============================================================================ 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 "mitkDICOMImageBlockDescriptor.h" #include "mitkStringProperty.h" #include "mitkLevelWindowProperty.h" #include "mitkPropertyKeyPath.h" #include "mitkDICOMIOMetaInformationPropertyConstants.h" #include "mitkIOMetaInformationPropertyConstants.h" #include #include #include #include mitk::DICOMImageBlockDescriptor::DICOMImageBlockDescriptor() : m_ReaderImplementationLevel( SOPClassUnknown ) , m_PropertyList( PropertyList::New() ) , m_SplitReason(IOVolumeSplitReason::New()) , m_TagCache( nullptr ) , m_PropertiesOutOfDate( true ) { m_PropertyFunctor = &mitk::DICOMImageBlockDescriptor::GetPropertyForDICOMValues; } mitk::DICOMImageBlockDescriptor::~DICOMImageBlockDescriptor() { } mitk::DICOMImageBlockDescriptor::DICOMImageBlockDescriptor( const DICOMImageBlockDescriptor& other ) : m_ImageFrameList( other.m_ImageFrameList ) , m_MitkImage( other.m_MitkImage ) , m_SliceIsLoaded( other.m_SliceIsLoaded ) , m_ReaderImplementationLevel( other.m_ReaderImplementationLevel ) , m_TiltInformation( other.m_TiltInformation ) , m_PropertyList( other.m_PropertyList->Clone() ) , m_SplitReason( other.m_SplitReason->Clone() ) , m_TagCache( other.m_TagCache ) , m_PropertiesOutOfDate( other.m_PropertiesOutOfDate ) , m_AdditionalTagMap(other.m_AdditionalTagMap) , m_FoundAdditionalTags(other.m_FoundAdditionalTags) , m_PropertyFunctor(other.m_PropertyFunctor) { if ( m_MitkImage ) { m_MitkImage = m_MitkImage->Clone(); } } mitk::DICOMImageBlockDescriptor& mitk::DICOMImageBlockDescriptor:: operator=( const DICOMImageBlockDescriptor& other ) { if ( this != &other ) { m_ImageFrameList = other.m_ImageFrameList; m_MitkImage = other.m_MitkImage; m_SliceIsLoaded = other.m_SliceIsLoaded; m_ReaderImplementationLevel = other.m_ReaderImplementationLevel; m_TiltInformation = other.m_TiltInformation; m_AdditionalTagMap = other.m_AdditionalTagMap; m_FoundAdditionalTags = other.m_FoundAdditionalTags; m_PropertyFunctor = other.m_PropertyFunctor; if ( other.m_PropertyList ) { m_PropertyList = other.m_PropertyList->Clone(); } if (other.m_SplitReason) { m_SplitReason = other.m_SplitReason->Clone(); } if ( other.m_MitkImage ) { m_MitkImage = other.m_MitkImage->Clone(); } m_TagCache = other.m_TagCache; m_PropertiesOutOfDate = other.m_PropertiesOutOfDate; } return *this; } mitk::DICOMTagList mitk::DICOMImageBlockDescriptor::GetTagsOfInterest() { DICOMTagList completeList; completeList.push_back( DICOMTag( 0x0018, 0x1164 ) ); // pixel spacing completeList.push_back( DICOMTag( 0x0028, 0x0030 ) ); // imager pixel spacing completeList.push_back( DICOMTag( 0x0008, 0x0018 ) ); // sop instance UID completeList.push_back( DICOMTag( 0x0008, 0x0016 ) ); // sop class UID completeList.push_back( DICOMTag( 0x0020, 0x0011 ) ); // series number completeList.push_back( DICOMTag( 0x0008, 0x1030 ) ); // study description completeList.push_back( DICOMTag( 0x0008, 0x103e ) ); // series description completeList.push_back( DICOMTag( 0x0008, 0x0060 ) ); // modality completeList.push_back( DICOMTag( 0x0018, 0x0024 ) ); // sequence name completeList.push_back( DICOMTag( 0x0020, 0x0037 ) ); // image orientation completeList.push_back( DICOMTag( 0x0020, 0x1041 ) ); // slice location completeList.push_back( DICOMTag( 0x0020, 0x0012 ) ); // acquisition number completeList.push_back( DICOMTag( 0x0020, 0x0013 ) ); // instance number completeList.push_back( DICOMTag( 0x0020, 0x0032 ) ); // image position patient completeList.push_back( DICOMTag( 0x0028, 0x1050 ) ); // window center completeList.push_back( DICOMTag( 0x0028, 0x1051 ) ); // window width completeList.push_back( DICOMTag( 0x0008, 0x0008 ) ); // image type completeList.push_back( DICOMTag( 0x0028, 0x0004 ) ); // photometric interpretation return completeList; } void mitk::DICOMImageBlockDescriptor::SetAdditionalTagsOfInterest( const AdditionalTagsMapType& tagMap) { m_AdditionalTagMap = tagMap; } void mitk::DICOMImageBlockDescriptor::SetTiltInformation( const GantryTiltInformation& info ) { m_TiltInformation = info; } const mitk::GantryTiltInformation mitk::DICOMImageBlockDescriptor::GetTiltInformation() const { return m_TiltInformation; } void mitk::DICOMImageBlockDescriptor::SetImageFrameList( const DICOMImageFrameList& framelist ) { m_ImageFrameList = framelist; m_SliceIsLoaded.resize( framelist.size() ); m_SliceIsLoaded.assign( framelist.size(), false ); m_PropertiesOutOfDate = true; } const mitk::DICOMImageFrameList& mitk::DICOMImageBlockDescriptor::GetImageFrameList() const { return m_ImageFrameList; } void mitk::DICOMImageBlockDescriptor::SetMitkImage( Image::Pointer image ) { if ( m_MitkImage != image ) { if ( m_TagCache.IsExpired() ) { MITK_ERROR << "Unable to describe MITK image with properties without a tag-cache object!"; m_MitkImage = nullptr; return; } if ( m_ImageFrameList.empty() ) { MITK_ERROR << "Unable to describe MITK image with properties without a frame list!"; m_MitkImage = nullptr; return; } // Should verify that the image matches m_ImageFrameList and m_TagCache // however, this is hard to do without re-analyzing all // TODO we should at least make sure that the number of frames is identical (plus rows/columns, // orientation) // without gantry tilt correction, we can also check image origin m_MitkImage = this->DescribeImageWithProperties( this->FixupSpacing( image ) ); } } mitk::Image::Pointer mitk::DICOMImageBlockDescriptor::GetMitkImage() const { return m_MitkImage; } mitk::Image::Pointer mitk::DICOMImageBlockDescriptor::FixupSpacing( Image* mitkImage ) { if ( mitkImage ) { Vector3D imageSpacing = mitkImage->GetGeometry()->GetSpacing(); ScalarType desiredSpacingX = imageSpacing[0]; ScalarType desiredSpacingY = imageSpacing[1]; this->GetDesiredMITKImagePixelSpacing( desiredSpacingX, desiredSpacingY ); // prefer pixel spacing over imager pixel spacing if ( desiredSpacingX <= 0 || desiredSpacingY <= 0 ) { return mitkImage; } MITK_DEBUG << "Loaded image with spacing " << imageSpacing[0] << ", " << imageSpacing[1]; MITK_DEBUG << "Found correct spacing info " << desiredSpacingX << ", " << desiredSpacingY; imageSpacing[0] = desiredSpacingX; imageSpacing[1] = desiredSpacingY; mitkImage->GetGeometry()->SetSpacing( imageSpacing ); } return mitkImage; } void mitk::DICOMImageBlockDescriptor::SetSliceIsLoaded( unsigned int index, bool isLoaded ) { if ( index < m_SliceIsLoaded.size() ) { m_SliceIsLoaded[index] = isLoaded; } else { std::stringstream ss; ss << "Index " << index << " out of range (" << m_SliceIsLoaded.size() << " indices reserved)"; throw std::invalid_argument( ss.str() ); } } bool mitk::DICOMImageBlockDescriptor::IsSliceLoaded( unsigned int index ) const { if ( index < m_SliceIsLoaded.size() ) { return m_SliceIsLoaded[index]; } else { std::stringstream ss; ss << "Index " << index << " out of range (" << m_SliceIsLoaded.size() << " indices reserved)"; throw std::invalid_argument( ss.str() ); } } bool mitk::DICOMImageBlockDescriptor::AllSlicesAreLoaded() const { bool allLoaded = true; for ( auto iter = m_SliceIsLoaded.cbegin(); iter != m_SliceIsLoaded.cend(); ++iter ) { allLoaded &= *iter; } return allLoaded; } /* PS defined IPS defined PS==IPS 0 0 --> UNKNOWN spacing, loader will invent 0 1 --> spacing as at detector surface 1 0 --> spacing as in patient 1 1 0 --> detector surface spacing CORRECTED for geometrical magnifications: spacing as in patient 1 1 1 --> detector surface spacing NOT corrected for geometrical magnifications: spacing as at detector */ mitk::PixelSpacingInterpretation mitk::DICOMImageBlockDescriptor::GetPixelSpacingInterpretation() const { if ( m_ImageFrameList.empty() || m_TagCache.IsExpired() ) { MITK_ERROR << "Invalid call to GetPixelSpacingInterpretation. Need to have initialized tag-cache!"; return SpacingUnknown; } const std::string pixelSpacing = this->GetPixelSpacing(); const std::string imagerPixelSpacing = this->GetImagerPixelSpacing(); if ( pixelSpacing.empty() ) { if ( imagerPixelSpacing.empty() ) { return SpacingUnknown; } else { return SpacingAtDetector; } } else // Pixel Spacing defined { if ( imagerPixelSpacing.empty() ) { return SpacingInPatient; } else if ( pixelSpacing != imagerPixelSpacing ) { return SpacingInPatient; } else { return SpacingAtDetector; } } } std::string mitk::DICOMImageBlockDescriptor::GetPixelSpacing() const { auto tagCache = m_TagCache.Lock(); if ( m_ImageFrameList.empty() || tagCache.IsNull() ) { MITK_ERROR << "Invalid call to GetPixelSpacing. Need to have initialized tag-cache!"; return std::string( "" ); } static const DICOMTag tagPixelSpacing( 0x0028, 0x0030 ); return tagCache->GetTagValue( m_ImageFrameList.front(), tagPixelSpacing ).value; } std::string mitk::DICOMImageBlockDescriptor::GetImagerPixelSpacing() const { auto tagCache = m_TagCache.Lock(); if ( m_ImageFrameList.empty() || tagCache.IsNull() ) { MITK_ERROR << "Invalid call to GetImagerPixelSpacing. Need to have initialized tag-cache!"; return std::string( "" ); } static const DICOMTag tagImagerPixelSpacing( 0x0018, 0x1164 ); return tagCache->GetTagValue( m_ImageFrameList.front(), tagImagerPixelSpacing ).value; } void mitk::DICOMImageBlockDescriptor::GetDesiredMITKImagePixelSpacing( ScalarType& spacingX, ScalarType& spacingY ) const { const std::string pixelSpacing = this->GetPixelSpacing(); // preference for "in patient" pixel spacing if ( !DICOMStringToSpacing( pixelSpacing, spacingX, spacingY ) ) { const std::string imagerPixelSpacing = this->GetImagerPixelSpacing(); // fallback to "on detector" spacing if ( !DICOMStringToSpacing( imagerPixelSpacing, spacingX, spacingY ) ) { // at this point we have no hints whether the spacing is correct // do a quick sanity check and either trust in the input or set both to 1 // We assume neither spacing to be negative, zero or unexpectedly large for // medical images if (spacingX < mitk::eps || spacingX > 1000 || spacingY < mitk::eps || spacingY > 1000) { spacingX = spacingY = 1.0; } } } } void mitk::DICOMImageBlockDescriptor::SetProperty( const std::string& key, BaseProperty* value ) { m_PropertyList->SetProperty( key, value ); } mitk::BaseProperty* mitk::DICOMImageBlockDescriptor::GetProperty( const std::string& key ) const { this->UpdateImageDescribingProperties(); return m_PropertyList->GetProperty( key ); } std::string mitk::DICOMImageBlockDescriptor::GetPropertyAsString( const std::string& key ) const { this->UpdateImageDescribingProperties(); const mitk::BaseProperty::Pointer property = m_PropertyList->GetProperty( key ); if ( property.IsNotNull() ) { return property->GetValueAsString(); } else { return std::string( "" ); } } -const mitk::IOVolumeSplitReason* mitk::DICOMImageBlockDescriptor::GetSplitReason() const +mitk::IOVolumeSplitReason::ConstPointer mitk::DICOMImageBlockDescriptor::GetSplitReason() const { return m_SplitReason; } -mitk::IOVolumeSplitReason* mitk::DICOMImageBlockDescriptor::GetSplitReason() +mitk::IOVolumeSplitReason::Pointer mitk::DICOMImageBlockDescriptor::GetSplitReason() { return m_SplitReason; } -void mitk::DICOMImageBlockDescriptor::SetSplitReason(IOVolumeSplitReason* reason) +void mitk::DICOMImageBlockDescriptor::SetSplitReason(IOVolumeSplitReason::Pointer reason) { m_SplitReason = reason; } void mitk::DICOMImageBlockDescriptor::SetFlag( const std::string& key, bool value ) { m_PropertyList->ReplaceProperty( key, BoolProperty::New( value ) ); } bool mitk::DICOMImageBlockDescriptor::GetFlag( const std::string& key, bool defaultValue ) const { this->UpdateImageDescribingProperties(); BoolProperty::ConstPointer boolProp = dynamic_cast( this->GetProperty( key ) ); if ( boolProp.IsNotNull() ) { return boolProp->GetValue(); } else { return defaultValue; } } void mitk::DICOMImageBlockDescriptor::SetIntProperty( const std::string& key, int value ) { m_PropertyList->ReplaceProperty( key, IntProperty::New( value ) ); } int mitk::DICOMImageBlockDescriptor::GetIntProperty( const std::string& key, int defaultValue ) const { this->UpdateImageDescribingProperties(); IntProperty::ConstPointer intProp = dynamic_cast( this->GetProperty( key ) ); if ( intProp.IsNotNull() ) { return intProp->GetValue(); } else { return defaultValue; } } double mitk::DICOMImageBlockDescriptor::stringtodouble( const std::string& str ) const { double d; std::string trimmedstring( str ); try { trimmedstring = trimmedstring.erase( trimmedstring.find_last_not_of( " \n\r\t" ) + 1 ); } catch ( ... ) { // no last not of } std::string firstcomponent( trimmedstring ); try { firstcomponent = trimmedstring.erase( trimmedstring.find_first_of( "\\" ) ); } catch ( ... ) { // no last not of } std::istringstream converter( firstcomponent ); if ( !firstcomponent.empty() && ( converter >> d ) && converter.eof() ) { return d; } else { throw std::invalid_argument( "Argument is not a convertible number" ); } } mitk::Image::Pointer mitk::DICOMImageBlockDescriptor::DescribeImageWithProperties( Image* mitkImage ) { // TODO: this is a collection of properties that have been provided by the // legacy DicomSeriesReader. // We should at some point clean up this collection and name them in a more // consistent way! if ( !mitkImage ) return mitkImage; mitkImage->SetProperty(PropertyKeyPathToPropertyName(DICOMIOMetaInformationPropertyConstants::READER_FILES()), this->GetProperty("filenamesForSlices")); mitkImage->SetProperty(PropertyKeyPathToPropertyName(DICOMIOMetaInformationPropertyConstants::READER_PIXEL_SPACING_INTERPRETATION_STRING()), StringProperty::New(PixelSpacingInterpretationToString(this->GetPixelSpacingInterpretation()))); mitkImage->SetProperty(PropertyKeyPathToPropertyName(DICOMIOMetaInformationPropertyConstants::READER_PIXEL_SPACING_INTERPRETATION()), GenericProperty::New(this->GetPixelSpacingInterpretation())); mitkImage->SetProperty(PropertyKeyPathToPropertyName(DICOMIOMetaInformationPropertyConstants::READER_IMPLEMENTATION_LEVEL_STRING()), StringProperty::New(ReaderImplementationLevelToString(m_ReaderImplementationLevel))); mitkImage->SetProperty(PropertyKeyPathToPropertyName(DICOMIOMetaInformationPropertyConstants::READER_IMPLEMENTATION_LEVEL()), GenericProperty::New(m_ReaderImplementationLevel)); mitkImage->SetProperty(PropertyKeyPathToPropertyName(DICOMIOMetaInformationPropertyConstants::READER_GANTRY_TILT_CORRECTED()), BoolProperty::New(this->GetTiltInformation().IsRegularGantryTilt())); mitkImage->SetProperty(PropertyKeyPathToPropertyName(DICOMIOMetaInformationPropertyConstants::READER_3D_plus_t()), BoolProperty::New(this->GetFlag("3D+t", false))); mitkImage->SetProperty(PropertyKeyPathToPropertyName(DICOMIOMetaInformationPropertyConstants::READER_GDCM()), StringProperty::New(gdcm::Version::GetVersion())); mitkImage->SetProperty(PropertyKeyPathToPropertyName(DICOMIOMetaInformationPropertyConstants::READER_DCMTK()), StringProperty::New(PACKAGE_VERSION)); - if (m_SplitReason.IsNotNull() && m_SplitReason->HasReasons()) + if (nullptr != m_SplitReason && m_SplitReason->HasReasons()) { - mitkImage->SetProperty(PropertyKeyPathToPropertyName(IOMetaInformationPropertyConstants::VOLUME_SPLIT_REASON()), StringProperty::New(IOVolumeSplitReason::ToJSON(m_SplitReason))); + mitkImage->SetProperty(PropertyKeyPathToPropertyName(IOMetaInformationPropertyConstants::VOLUME_SPLIT_REASON()), StringProperty::New(IOVolumeSplitReason::ToJSON(m_SplitReason).dump())); } // get all found additional tags of interest for (const auto &tag : m_FoundAdditionalTags) { BaseProperty* prop = this->GetProperty(tag); if (prop) { mitkImage->SetProperty(tag.c_str(), prop); } } ///////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////// //// Deprecated properties should be removed sooner then later (see above) ///////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////// // first part: add some tags that describe individual slices // these properties are defined at analysis time (see UpdateImageDescribingProperties()) const char* propertyKeySliceLocation = "dicom.image.0020.1041"; const char* propertyKeyInstanceNumber = "dicom.image.0020.0013"; const char* propertyKeySOPInstanceUID = "dicom.image.0008.0018"; mitkImage->SetProperty( propertyKeySliceLocation, this->GetProperty( "sliceLocationForSlices" ) ); mitkImage->SetProperty( propertyKeyInstanceNumber, this->GetProperty( "instanceNumberForSlices" ) ); mitkImage->SetProperty( propertyKeySOPInstanceUID, this->GetProperty( "SOPInstanceUIDForSlices" ) ); mitkImage->SetProperty( "files", this->GetProperty( "filenamesForSlices_deprecated" ) ); // second part: add properties that describe the whole image block mitkImage->SetProperty( "dicomseriesreader.SOPClassUID", StringProperty::New( this->GetSOPClassUID() ) ); mitkImage->SetProperty( "dicomseriesreader.SOPClass", StringProperty::New( this->GetSOPClassUIDAsName() ) ); mitkImage->SetProperty( "dicomseriesreader.PixelSpacingInterpretationString", StringProperty::New( PixelSpacingInterpretationToString( this->GetPixelSpacingInterpretation() ) ) ); mitkImage->SetProperty( "dicomseriesreader.PixelSpacingInterpretation", GenericProperty::New( this->GetPixelSpacingInterpretation() ) ); mitkImage->SetProperty( "dicomseriesreader.ReaderImplementationLevelString", StringProperty::New( ReaderImplementationLevelToString( m_ReaderImplementationLevel ) ) ); mitkImage->SetProperty( "dicomseriesreader.ReaderImplementationLevel", GenericProperty::New( m_ReaderImplementationLevel ) ); mitkImage->SetProperty( "dicomseriesreader.GantyTiltCorrected", BoolProperty::New( this->GetTiltInformation().IsRegularGantryTilt() ) ); mitkImage->SetProperty( "dicomseriesreader.3D+t", BoolProperty::New( this->GetFlag( "3D+t", false ) ) ); // level window const std::string windowCenter = this->GetPropertyAsString( "windowCenter" ); const std::string windowWidth = this->GetPropertyAsString( "windowWidth" ); try { const double level = stringtodouble( windowCenter ); const double window = stringtodouble( windowWidth ); mitkImage->SetProperty( "levelwindow", LevelWindowProperty::New( LevelWindow( level, window ) ) ); } catch ( ... ) { // nothing, no levelwindow to be predicted... } const std::string modality = this->GetPropertyAsString( "modality" ); mitkImage->SetProperty( "modality", StringProperty::New( modality ) ); mitkImage->SetProperty( "dicom.pixel.PhotometricInterpretation", this->GetProperty( "photometricInterpretation" ) ); mitkImage->SetProperty( "dicom.image.imagetype", this->GetProperty( "imagetype" ) ); mitkImage->SetProperty( "dicom.study.StudyDescription", this->GetProperty( "studyDescription" ) ); mitkImage->SetProperty( "dicom.series.SeriesDescription", this->GetProperty( "seriesDescription" ) ); mitkImage->SetProperty( "dicom.pixel.Rows", this->GetProperty( "rows" ) ); mitkImage->SetProperty( "dicom.pixel.Columns", this->GetProperty( "columns" ) ); // fourth part: get something from ImageIO. BUT this needs to be created elsewhere. or not at all! return mitkImage; } void mitk::DICOMImageBlockDescriptor::SetReaderImplementationLevel( const ReaderImplementationLevel& level ) { m_ReaderImplementationLevel = level; } mitk::ReaderImplementationLevel mitk::DICOMImageBlockDescriptor::GetReaderImplementationLevel() const { return m_ReaderImplementationLevel; } std::string mitk::DICOMImageBlockDescriptor::GetSOPClassUID() const { auto tagCache = m_TagCache.Lock(); if ( !m_ImageFrameList.empty() && tagCache.IsNotNull() ) { static const DICOMTag tagSOPClassUID( 0x0008, 0x0016 ); return tagCache->GetTagValue( m_ImageFrameList.front(), tagSOPClassUID ).value; } else { MITK_ERROR << "Invalid call to DICOMImageBlockDescriptor::GetSOPClassUID(). Need to have initialized tag-cache!"; return std::string( "" ); } } std::string mitk::DICOMImageBlockDescriptor::GetSOPClassUIDAsName() const { if ( !m_ImageFrameList.empty() && !m_TagCache.IsExpired() ) { gdcm::UIDs uidKnowledge; uidKnowledge.SetFromUID( this->GetSOPClassUID().c_str() ); const char* name = uidKnowledge.GetName(); if ( name ) { return std::string( name ); } else { return std::string( "" ); } } else { MITK_ERROR << "Invalid call to DICOMImageBlockDescriptor::GetSOPClassUIDAsName(). Need to have " "initialized tag-cache!"; return std::string( "" ); } } int mitk::DICOMImageBlockDescriptor::GetNumberOfTimeSteps() const { int result = 1; this->m_PropertyList->GetIntProperty("timesteps", result); return result; }; int mitk::DICOMImageBlockDescriptor::GetNumberOfFramesPerTimeStep() const { const int numberOfTimesteps = this->GetNumberOfTimeSteps(); int numberOfFramesPerTimestep = this->m_ImageFrameList.size() / numberOfTimesteps; assert(int(double((double)this->m_ImageFrameList.size() / (double)numberOfTimesteps)) == numberOfFramesPerTimestep); // this should hold return numberOfFramesPerTimestep; }; void mitk::DICOMImageBlockDescriptor::SetTagCache( DICOMTagCache* privateCache ) { // this must only be used during loading and never afterwards m_TagCache = privateCache; } #define printPropertyRange( label, property_name ) \ \ { \ const std::string first = this->GetPropertyAsString( #property_name "First" ); \ const std::string last = this->GetPropertyAsString( #property_name "Last" ); \ if ( !first.empty() || !last.empty() ) \ { \ if ( first == last ) \ { \ os << " " label ": '" << first << "'" << std::endl; \ } \ else \ { \ os << " " label ": '" << first << "' - '" << last << "'" << std::endl; \ } \ } \ \ } #define printProperty( label, property_name ) \ \ { \ const std::string first = this->GetPropertyAsString( #property_name ); \ if ( !first.empty() ) \ { \ os << " " label ": '" << first << "'" << std::endl; \ } \ \ } #define printBool( label, commands ) \ \ { \ os << " " label ": '" << ( commands ? "yes" : "no" ) << "'" << std::endl; \ \ } void mitk::DICOMImageBlockDescriptor::Print(std::ostream& os, bool filenameDetails) const { os << " Number of Frames: '" << m_ImageFrameList.size() << "'" << std::endl; os << " SOP class: '" << this->GetSOPClassUIDAsName() << "'" << std::endl; printProperty( "Series Number", seriesNumber ); printProperty( "Study Description", studyDescription ); printProperty( "Series Description", seriesDescription ); printProperty( "Modality", modality ); printProperty( "Sequence Name", sequenceName ); printPropertyRange( "Slice Location", sliceLocation ); printPropertyRange( "Acquisition Number", acquisitionNumber ); printPropertyRange( "Instance Number", instanceNumber ); printPropertyRange( "Image Position", imagePositionPatient ); printProperty( "Image Orientation", orientation ); os << " Pixel spacing interpretation: '" << PixelSpacingInterpretationToString( this->GetPixelSpacingInterpretation() ) << "'" << std::endl; printBool( "Gantry Tilt", this->GetTiltInformation().IsRegularGantryTilt() ) // printBool("3D+t", this->GetFlag("3D+t",false)) // os << " MITK image loaded: '" << (this->GetMitkImage().IsNotNull() ? "yes" : "no") << "'" << // std::endl; if ( filenameDetails ) { os << " Files in this image block:" << std::endl; for ( auto frameIter = m_ImageFrameList.begin(); frameIter != m_ImageFrameList.end(); ++frameIter ) { os << " " << ( *frameIter )->Filename; if ( ( *frameIter )->FrameNo > 0 ) { os << ", " << ( *frameIter )->FrameNo; } os << std::endl; } } } #define storeTagValueToProperty( tag_name, tag_g, tag_e ) \ \ { \ const DICOMTag t( tag_g, tag_e ); \ const std::string tagValue = tagCache->GetTagValue( firstFrame, t ).value; \ const_cast( this ) \ ->SetProperty( #tag_name, StringProperty::New( tagValue ) ); \ \ } #define storeTagValueRangeToProperty( tag_name, tag_g, tag_e ) \ \ { \ const DICOMTag t( tag_g, tag_e ); \ const std::string tagValueFirst = tagCache->GetTagValue( firstFrame, t ).value; \ const std::string tagValueLast = tagCache->GetTagValue( lastFrame, t ).value; \ const_cast( this ) \ ->SetProperty( #tag_name "First", StringProperty::New( tagValueFirst ) ); \ const_cast( this ) \ ->SetProperty( #tag_name "Last", StringProperty::New( tagValueLast ) ); \ \ } void mitk::DICOMImageBlockDescriptor::UpdateImageDescribingProperties() const { if ( !m_PropertiesOutOfDate ) return; if ( !m_ImageFrameList.empty() ) { auto tagCache = m_TagCache.Lock(); if (tagCache.IsNull()) { MITK_ERROR << "Invalid call to DICOMImageBlockDescriptor::UpdateImageDescribingProperties(). Need to " "have initialized tag-cache!"; return; } const DICOMImageFrameInfo::Pointer firstFrame = m_ImageFrameList.front(); const DICOMImageFrameInfo::Pointer lastFrame = m_ImageFrameList.back(); // see macros above storeTagValueToProperty( seriesNumber, 0x0020, 0x0011 ); storeTagValueToProperty( studyDescription, 0x0008, 0x1030 ); storeTagValueToProperty( seriesDescription, 0x0008, 0x103e ); storeTagValueToProperty( modality, 0x0008, 0x0060 ); storeTagValueToProperty( sequenceName, 0x0018, 0x0024 ); storeTagValueToProperty( orientation, 0x0020, 0x0037 ); storeTagValueToProperty( rows, 0x0028, 0x0010 ); storeTagValueToProperty( columns, 0x0028, 0x0011 ); storeTagValueRangeToProperty( sliceLocation, 0x0020, 0x1041 ); storeTagValueRangeToProperty( acquisitionNumber, 0x0020, 0x0012 ); storeTagValueRangeToProperty( instanceNumber, 0x0020, 0x0013 ); storeTagValueRangeToProperty( imagePositionPatient, 0x0020, 0x0032 ); storeTagValueToProperty( windowCenter, 0x0028, 0x1050 ); storeTagValueToProperty( windowWidth, 0x0028, 0x1051 ); storeTagValueToProperty( imageType, 0x0008, 0x0008 ); storeTagValueToProperty( photometricInterpretation, 0x0028, 0x0004 ); // some per-image attributes // frames are just numbered starting from 0. timestep 1 (the second time-step) has frames starting at // (number-of-frames-per-timestep) // std::string propertyKeySliceLocation = "dicom.image.0020.1041"; // std::string propertyKeyInstanceNumber = "dicom.image.0020.0013"; // std::string propertyKeySOPInstanceNumber = "dicom.image.0008.0018"; StringLookupTable sliceLocationForSlices; StringLookupTable instanceNumberForSlices; StringLookupTable SOPInstanceUIDForSlices; StringLookupTable filenamesForSlices_deprecated; DICOMCachedValueLookupTable filenamesForSlices; const DICOMTag tagSliceLocation( 0x0020, 0x1041 ); const DICOMTag tagInstanceNumber( 0x0020, 0x0013 ); const DICOMTag tagSOPInstanceNumber( 0x0008, 0x0018 ); std::unordered_map additionalTagResultList; unsigned int slice(0); int timePoint(-1); const int framesPerTimeStep = this->GetNumberOfFramesPerTimeStep(); for ( auto frameIter = m_ImageFrameList.begin(); frameIter != m_ImageFrameList.end(); ++slice, ++frameIter ) { unsigned int zSlice = slice%framesPerTimeStep; if ( zSlice == 0) { timePoint++; } const std::string sliceLocation = tagCache->GetTagValue( *frameIter, tagSliceLocation ).value; sliceLocationForSlices.SetTableValue( slice, sliceLocation ); const std::string instanceNumber = tagCache->GetTagValue( *frameIter, tagInstanceNumber ).value; instanceNumberForSlices.SetTableValue( slice, instanceNumber ); const std::string sopInstanceUID = tagCache->GetTagValue( *frameIter, tagSOPInstanceNumber ).value; SOPInstanceUIDForSlices.SetTableValue( slice, sopInstanceUID ); const std::string filename = ( *frameIter )->Filename; filenamesForSlices_deprecated.SetTableValue( slice, filename ); filenamesForSlices.SetTableValue(slice, { static_cast(timePoint), zSlice, filename }); MITK_DEBUG << "Tag info for slice " << slice << ": SL '" << sliceLocation << "' IN '" << instanceNumber << "' SOP instance UID '" << sopInstanceUID << "'"; for (const auto& tag : m_AdditionalTagMap) { const DICOMTagCache::FindingsListType findings = tagCache->GetTagValue( *frameIter, tag.first ); for (const auto& finding : findings) { if (finding.isValid) { std::string propKey = (tag.second.empty()) ? DICOMTagPathToPropertyName(finding.path) : tag.second; DICOMCachedValueInfo info{ static_cast(timePoint), zSlice, finding.value }; additionalTagResultList[propKey].SetTableValue(slice, info); } } } } // add property or properties with proper names auto* thisInstance = const_cast( this ); thisInstance->SetProperty( "sliceLocationForSlices", StringLookupTableProperty::New( sliceLocationForSlices ) ); thisInstance->SetProperty( "instanceNumberForSlices", StringLookupTableProperty::New( instanceNumberForSlices ) ); thisInstance->SetProperty( "SOPInstanceUIDForSlices", StringLookupTableProperty::New( SOPInstanceUIDForSlices ) ); thisInstance->SetProperty( "filenamesForSlices_deprecated", StringLookupTableProperty::New( filenamesForSlices_deprecated ) ); thisInstance->SetProperty("filenamesForSlices", m_PropertyFunctor(filenamesForSlices)); //add properties for additional tags of interest for ( auto iter = additionalTagResultList.cbegin(); iter != additionalTagResultList.cend(); ++iter ) { thisInstance->SetProperty( iter->first, m_PropertyFunctor( iter->second ) ); thisInstance->m_FoundAdditionalTags.insert(m_FoundAdditionalTags.cend(),iter->first); } m_PropertiesOutOfDate = false; } } mitk::BaseProperty::Pointer mitk::DICOMImageBlockDescriptor::GetPropertyForDICOMValues(const DICOMCachedValueLookupTable& cacheLookupTable) { const auto& lookupTable = cacheLookupTable.GetLookupTable(); typedef std::pair PairType; if ( std::adjacent_find( lookupTable.cbegin(), lookupTable.cend(), []( const PairType& lhs, const PairType& rhs ) { return lhs.second.Value != rhs.second.Value; } ) == lookupTable.cend() ) { return static_cast( mitk::StringProperty::New(cacheLookupTable.GetTableValue(0).Value).GetPointer()); } StringLookupTable stringTable; for (const auto &element : lookupTable) { stringTable.SetTableValue(element.first, element.second.Value); } return static_cast( mitk::StringLookupTableProperty::New(stringTable).GetPointer()); } void mitk::DICOMImageBlockDescriptor::SetTagLookupTableToPropertyFunctor( TagLookupTableToPropertyFunctor functor ) { if ( functor != nullptr ) { m_PropertyFunctor = functor; } } mitk::BaseProperty::ConstPointer mitk::DICOMImageBlockDescriptor::GetConstProperty(const std::string &propertyKey, const std::string &/*contextName*/, bool /*fallBackOnDefaultContext*/) const { this->UpdateImageDescribingProperties(); return m_PropertyList->GetConstProperty(propertyKey); }; std::vector mitk::DICOMImageBlockDescriptor::GetPropertyKeys(const std::string &/*contextName*/, bool /*includeDefaultContext*/) const { this->UpdateImageDescribingProperties(); return m_PropertyList->GetPropertyKeys(); }; std::vector mitk::DICOMImageBlockDescriptor::GetPropertyContextNames() const { return std::vector(); }; diff --git a/Modules/DICOM/src/mitkEquiDistantBlocksSorter.cpp b/Modules/DICOM/src/mitkEquiDistantBlocksSorter.cpp index abca9df4ee..2836e8404e 100644 --- a/Modules/DICOM/src/mitkEquiDistantBlocksSorter.cpp +++ b/Modules/DICOM/src/mitkEquiDistantBlocksSorter.cpp @@ -1,621 +1,621 @@ /*============================================================================ 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. ============================================================================*/ //#define MBILOG_ENABLE_DEBUG #include "mitkEquiDistantBlocksSorter.h" mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::SliceGroupingAnalysisResult() : m_SplitReason(IOVolumeSplitReason::New()) { } const mitk::DICOMDatasetList& mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::GetBlockDatasets() const { return m_GroupedFiles; } const mitk::DICOMDatasetList& mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::GetUnsortedDatasets() const { return m_UnsortedFiles; } -const mitk::IOVolumeSplitReason* +mitk::IOVolumeSplitReason::ConstPointer mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult::GetSplitReason() const { return m_SplitReason; } -mitk::IOVolumeSplitReason* +mitk::IOVolumeSplitReason::Pointer mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult::GetSplitReason() { return m_SplitReason; } bool mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::ContainsGantryTilt() { return m_TiltInfo.IsRegularGantryTilt(); } void mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::AddFileToSortedBlock(DICOMDatasetAccess* dataset) { m_GroupedFiles.push_back( dataset ); } void mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::AddFileToUnsortedBlock(DICOMDatasetAccess* dataset) { m_UnsortedFiles.push_back( dataset ); } void mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::AddFilesToUnsortedBlock(const DICOMDatasetList& datasets) { m_UnsortedFiles.insert( m_UnsortedFiles.end(), datasets.begin(), datasets.end() ); } void mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::SetFirstFilenameOfBlock(const std::string& filename) { m_FirstFilenameOfBlock = filename; } std::string mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::GetFirstFilenameOfBlock() const { return m_FirstFilenameOfBlock; } void mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::SetLastFilenameOfBlock(const std::string& filename) { m_LastFilenameOfBlock = filename; } std::string mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::GetLastFilenameOfBlock() const { return m_LastFilenameOfBlock; } void mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::FlagGantryTilt(const GantryTiltInformation& tiltInfo) { m_TiltInfo = tiltInfo; } const mitk::GantryTiltInformation& mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::GetTiltInfo() const { return m_TiltInfo; } void mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::UndoPrematureGrouping() { assert( !m_GroupedFiles.empty() ); m_UnsortedFiles.insert( m_UnsortedFiles.begin(), m_GroupedFiles.back() ); m_GroupedFiles.pop_back(); m_TiltInfo = GantryTiltInformation(); } // ------------------------ end helper class mitk::EquiDistantBlocksSorter ::EquiDistantBlocksSorter() :DICOMDatasetSorter() ,m_AcceptTilt(false) ,m_ToleratedOriginOffset(0.3) ,m_ToleratedOriginOffsetIsAbsolute(false) ,m_AcceptTwoSlicesGroups(true) { } mitk::EquiDistantBlocksSorter ::EquiDistantBlocksSorter(const EquiDistantBlocksSorter& other ) :DICOMDatasetSorter(other) ,m_AcceptTilt(other.m_AcceptTilt) ,m_ToleratedOriginOffset(other.m_ToleratedOriginOffset) ,m_ToleratedOriginOffsetIsAbsolute(other.m_ToleratedOriginOffsetIsAbsolute) ,m_AcceptTwoSlicesGroups(other.m_AcceptTwoSlicesGroups) { } mitk::EquiDistantBlocksSorter ::~EquiDistantBlocksSorter() { } bool mitk::EquiDistantBlocksSorter ::operator==(const DICOMDatasetSorter& other) const { if (const auto* otherSelf = dynamic_cast(&other)) { return this->m_AcceptTilt == otherSelf->m_AcceptTilt && this->m_ToleratedOriginOffsetIsAbsolute == otherSelf->m_ToleratedOriginOffsetIsAbsolute && this->m_AcceptTwoSlicesGroups == otherSelf->m_AcceptTwoSlicesGroups && (fabs(this->m_ToleratedOriginOffset - otherSelf->m_ToleratedOriginOffset) < eps); } else { return false; } } void mitk::EquiDistantBlocksSorter ::PrintConfiguration(std::ostream& os, const std::string& indent) const { std::stringstream ts; if (!m_ToleratedOriginOffsetIsAbsolute) { ts << "adaptive"; } else { ts << m_ToleratedOriginOffset << "mm"; } os << indent << "Sort into blocks of equidistant, well-aligned (tolerance " << ts.str() << ") slices " << (m_AcceptTilt ? "(accepting a gantry tilt)" : "") << std::endl; } void mitk::EquiDistantBlocksSorter ::SetAcceptTilt(bool accept) { m_AcceptTilt = accept; } bool mitk::EquiDistantBlocksSorter ::GetAcceptTilt() const { return m_AcceptTilt; } void mitk::EquiDistantBlocksSorter ::SetAcceptTwoSlicesGroups(bool accept) { m_AcceptTwoSlicesGroups = accept; } bool mitk::EquiDistantBlocksSorter ::GetAcceptTwoSlicesGroups() const { return m_AcceptTwoSlicesGroups; } mitk::EquiDistantBlocksSorter& mitk::EquiDistantBlocksSorter ::operator=(const EquiDistantBlocksSorter& other) { if (this != &other) { DICOMDatasetSorter::operator=(other); m_AcceptTilt = other.m_AcceptTilt; m_ToleratedOriginOffset = other.m_ToleratedOriginOffset; m_ToleratedOriginOffsetIsAbsolute = other.m_ToleratedOriginOffsetIsAbsolute; m_AcceptTwoSlicesGroups = other.m_AcceptTwoSlicesGroups; } return *this; } mitk::DICOMTagList mitk::EquiDistantBlocksSorter ::GetTagsOfInterest() { DICOMTagList tags; tags.push_back( DICOMTag(0x0020, 0x0032) ); // ImagePositionPatient tags.push_back( DICOMTag(0x0020, 0x0037) ); // ImageOrientationPatient tags.push_back( DICOMTag(0x0018, 0x1120) ); // GantryDetectorTilt return tags; } void mitk::EquiDistantBlocksSorter ::Sort() { DICOMDatasetList remainingInput = GetInput(); // copy m_SliceGroupingResults.clear(); while (!remainingInput.empty()) // repeat until all files are grouped somehow { auto regularBlock = this->AnalyzeFileForITKImageSeriesReaderSpacingAssumption( remainingInput, m_AcceptTilt ); #ifdef MBILOG_ENABLE_DEBUG DICOMDatasetList inBlock = regularBlock.GetBlockDatasets(); DICOMDatasetList laterBlock = regularBlock.GetUnsortedDatasets(); MITK_DEBUG << "Result: sorted 3D group with " << inBlock.size() << " files"; for (DICOMDatasetList::const_iterator diter = inBlock.cbegin(); diter != inBlock.cend(); ++diter) MITK_DEBUG << " IN " << (*diter)->GetFilenameIfAvailable(); for (DICOMDatasetList::const_iterator diter = laterBlock.cbegin(); diter != laterBlock.cend(); ++diter) MITK_DEBUG << " OUT " << (*diter)->GetFilenameIfAvailable(); #endif // MBILOG_ENABLE_DEBUG remainingInput = regularBlock->GetUnsortedDatasets(); if (remainingInput.empty() && !m_SliceGroupingResults.empty() && m_SliceGroupingResults.back()->GetSplitReason()->HasReason(IOVolumeSplitReason::ReasonType::OverlappingSlices)) { //if all inputs are processed and there is already a preceding grouping result that has overlapping as split reason, add also overlapping as split reason for the current block regularBlock->GetSplitReason()->AddReason(IOVolumeSplitReason::ReasonType::OverlappingSlices); } m_SliceGroupingResults.push_back( regularBlock ); } unsigned int numberOfOutputs = m_SliceGroupingResults.size(); this->SetNumberOfOutputs(numberOfOutputs); unsigned int outputIndex(0); for (auto oIter = m_SliceGroupingResults.cbegin(); oIter != m_SliceGroupingResults.cend(); ++outputIndex, ++oIter) { this->SetOutput(outputIndex, (*oIter)->GetBlockDatasets(), (*oIter)->GetSplitReason()); } } void mitk::EquiDistantBlocksSorter ::SetToleratedOriginOffsetToAdaptive(double fractionOfInterSliceDistance) { m_ToleratedOriginOffset = fractionOfInterSliceDistance; m_ToleratedOriginOffsetIsAbsolute = false; if (m_ToleratedOriginOffset < 0.0) { MITK_WARN << "Call SetToleratedOriginOffsetToAdaptive() only with positive numbers between 0.0 and 1.0, read documentation!"; } if (m_ToleratedOriginOffset > 0.5) { MITK_WARN << "EquiDistantBlocksSorter is now accepting large errors, take care of measurements, they could appear at imprecise locations!"; } } void mitk::EquiDistantBlocksSorter ::SetToleratedOriginOffset(double millimeters) { m_ToleratedOriginOffset = millimeters; m_ToleratedOriginOffsetIsAbsolute = true; if (m_ToleratedOriginOffset < 0.0) { MITK_WARN << "Negative tolerance set to SetToleratedOriginOffset()!"; } } double mitk::EquiDistantBlocksSorter ::GetToleratedOriginOffset() const { return m_ToleratedOriginOffset; } bool mitk::EquiDistantBlocksSorter ::IsToleratedOriginOffsetAbsolute() const { return m_ToleratedOriginOffsetIsAbsolute; } std::string mitk::EquiDistantBlocksSorter ::ConstCharStarToString(const char* s) { return s ? std::string(s) : std::string(); } std::shared_ptr mitk::EquiDistantBlocksSorter ::AnalyzeFileForITKImageSeriesReaderSpacingAssumption( const DICOMDatasetList& datasets, bool groupImagesWithGantryTilt) { auto result = std::make_shared(); const DICOMTag tagImagePositionPatient = DICOMTag(0x0020,0x0032); // Image Position (Patient) const DICOMTag tagImageOrientation = DICOMTag(0x0020, 0x0037); // Image Orientation Vector3D fromFirstToSecondOrigin; fromFirstToSecondOrigin.Fill(0.0); bool fromFirstToSecondOriginInitialized(false); Point3D thisOrigin; thisOrigin.Fill(0.0f); Point3D lastOrigin; lastOrigin.Fill(0.0f); Point3D lastDifferentOrigin; lastDifferentOrigin.Fill(0.0f); bool lastOriginInitialized(false); MITK_DEBUG << "--------------------------------------------------------------------------------"; MITK_DEBUG << "Analyzing " << datasets.size() << " files for z-spacing assumption of ITK's ImageSeriesReader (group tilted: " << groupImagesWithGantryTilt << ")"; unsigned int fileIndex(0); double toleratedOriginError(0.005); // default: max. 1/10mm error when measurement crosses 20 slices in z direction (too strict? we don't know better) for (auto dsIter = datasets.cbegin(); dsIter != datasets.cend(); ++dsIter, ++fileIndex) { bool fileFitsIntoPattern(false); std::string thisOriginString; // Read tag value into point3D. PLEASE replace this by appropriate GDCM code if you figure out how to do that thisOriginString = (*dsIter)->GetTagValueAsString(tagImagePositionPatient).value; if (thisOriginString.empty()) { // don't let such files be in a common group. Everything without position information will be loaded as a single slice: // with standard DICOM files this can happen to: CR, DX, SC MITK_DEBUG << " ==> Sort away " << *dsIter << " for later analysis (no position information)"; // we already have one occupying this position if ( result->GetBlockDatasets().empty() ) // nothing WITH position information yet { // ==> this is a group of its own, stop processing, come back later result->AddFileToSortedBlock( *dsIter ); DICOMDatasetList remainingFiles; remainingFiles.insert( remainingFiles.end(), dsIter+1, datasets.end() ); result->AddFilesToUnsortedBlock( remainingFiles ); if (!remainingFiles.empty()) //if there are remaining files add a split reason result->GetSplitReason()->AddReason(IOVolumeSplitReason::ReasonType::ImagePostionMissing); fileFitsIntoPattern = false; break; // no files anymore } else { // ==> this does not match, consider later result->AddFileToUnsortedBlock( *dsIter ); // sort away for further analysis fileFitsIntoPattern = false; continue; // next file } } bool ignoredConversionError(-42); // hard to get here, no graceful way to react thisOrigin = DICOMStringToPoint3D( thisOriginString, ignoredConversionError ); int missingSlicesCount = 0; MITK_DEBUG << " " << fileIndex << " " << (*dsIter)->GetFilenameIfAvailable() << " at " /* << thisOriginString */ << "(" << thisOrigin[0] << "," << thisOrigin[1] << "," << thisOrigin[2] << ")"; if ( lastOriginInitialized && (thisOrigin == lastOrigin) ) { MITK_DEBUG << " ==> Sort away " << *dsIter << " for separate time step"; // we already have one occupying this position result->AddFileToUnsortedBlock( *dsIter ); // sort away for further analysis result->GetSplitReason()->AddReason(IOVolumeSplitReason::ReasonType::OverlappingSlices); fileFitsIntoPattern = false; } else { if (!fromFirstToSecondOriginInitialized && lastOriginInitialized) // calculate vector as soon as possible when we get a new position { fromFirstToSecondOrigin = thisOrigin - lastDifferentOrigin; fromFirstToSecondOriginInitialized = true; // classic mode without tolerance! if (!m_ToleratedOriginOffsetIsAbsolute) { MITK_DEBUG << "Distance of two slices: " << fromFirstToSecondOrigin.GetNorm() << "mm"; toleratedOriginError = fromFirstToSecondOrigin.GetNorm() * 0.3; // a third of the slice distance // (less than half, which would mean that a slice is displayed where another slice should actually be) } else { toleratedOriginError = m_ToleratedOriginOffset; } MITK_DEBUG << "Accepting errors in actual versus expected origin up to " << toleratedOriginError << "mm"; // Here we calculate if this slice and the previous one are well aligned, // i.e. we test if the previous origin is on a line through the current // origin, directed into the normal direction of the current slice. // If this is NOT the case, then we have a data set with a TILTED GANTRY geometry, // which cannot be simply loaded into a single mitk::Image at the moment. // For this case, we flag this finding in the result and DicomSeriesReader // can correct for that later. Vector3D right; right.Fill(0.0); Vector3D up; right.Fill(0.0); // might be down as well, but it is just a name at this point std::string orientationValue = (*dsIter)->GetTagValueAsString( tagImageOrientation ).value; DICOMStringToOrientationVectors( orientationValue, right, up, ignoredConversionError ); GantryTiltInformation tiltInfo( lastDifferentOrigin, thisOrigin, right, up, 1 ); if ( tiltInfo.IsSheared() ) { /* optimistic approach, accepting gantry tilt: save file for later, check all further files */ // at this point we have TWO slices analyzed! if they are the only two files, we still split, because there is no third to verify our tilting assumption. // later with a third being available, we must check if the initial tilting vector is still valid. if yes, continue. // if NO, we need to split the already sorted part (result->first) and the currently analyzed file (*dsIter) // tell apart gantry tilt from overall skewedness // sort out irregularly sheared slices, that IS NOT tilting if ( groupImagesWithGantryTilt && tiltInfo.IsRegularGantryTilt() ) { assert(!datasets.empty()); result->FlagGantryTilt(tiltInfo); result->AddFileToSortedBlock( *dsIter ); // this file is good for current block result->SetFirstFilenameOfBlock( datasets.front()->GetFilenameIfAvailable() ); result->SetLastFilenameOfBlock( datasets.back()->GetFilenameIfAvailable() ); fileFitsIntoPattern = true; } else // caller does not want tilt compensation OR shearing is more complicated than tilt { result->AddFileToUnsortedBlock( *dsIter ); // sort away for further analysis result->GetSplitReason()->AddReason(IOVolumeSplitReason::ReasonType::GantryTiltDifference); fileFitsIntoPattern = false; } } else // not sheared { result->AddFileToSortedBlock( *dsIter ); // this file is good for current block fileFitsIntoPattern = true; } } else if (fromFirstToSecondOriginInitialized) // we already know the offset between slices { Point3D assumedOrigin = lastDifferentOrigin + fromFirstToSecondOrigin; Vector3D originError = assumedOrigin - thisOrigin; double norm = originError.GetNorm(); if (norm > toleratedOriginError) { MITK_DEBUG << " File does not fit into the inter-slice distance pattern (diff = " << norm << ", allowed " << toleratedOriginError << ")."; MITK_DEBUG << " Expected position (" << assumedOrigin[0] << "," << assumedOrigin[1] << "," << assumedOrigin[2] << "), got position (" << thisOrigin[0] << "," << thisOrigin[1] << "," << thisOrigin[2] << ")"; MITK_DEBUG << " ==> Sort away " << *dsIter << " for later analysis"; // At this point we know we deviated from the expectation of ITK's ImageSeriesReader // We split the input file list at this point, i.e. all files up to this one (excluding it) // are returned as group 1, the remaining files (including the faulty one) are group 2 /* Optimistic approach: check if any of the remaining slices fits in */ result->AddFileToUnsortedBlock( *dsIter ); // sort away for further analysis const auto fromLastToThisOriginDistance = (lastDifferentOrigin - thisOrigin).GetNorm(); const auto fromFirstToSecondOriginDistance = fromFirstToSecondOrigin.GetNorm(); auto currentMissCount = static_cast(std::round((fromLastToThisOriginDistance / fromFirstToSecondOriginDistance)-1)); if (missingSlicesCount == 0 || missingSlicesCount > currentMissCount) { missingSlicesCount = currentMissCount; } result->GetSplitReason()->AddReason(IOVolumeSplitReason::ReasonType::SliceDistanceInconsistency, std::to_string(fromLastToThisOriginDistance)); if (missingSlicesCount == 0) { result->GetSplitReason()->RemoveReason(IOVolumeSplitReason::ReasonType::MissingSlices); } else if (missingSlicesCount < 0) { result->GetSplitReason()->AddReason(IOVolumeSplitReason::ReasonType::OverlappingSlices); } else if (!result->GetSplitReason()->HasReason(IOVolumeSplitReason::ReasonType::OverlappingSlices)) { //If the missing slice count is positive, but no overlapping was flagged, add the missing slice reason. //We only do it if overlapping was not flagged, to avoid false positives, that could be triggered by slices //of the overlapping volume. result->GetSplitReason()->AddReason(IOVolumeSplitReason::ReasonType::MissingSlices, std::to_string(missingSlicesCount)); } fileFitsIntoPattern = false; } else { result->AddFileToSortedBlock( *dsIter ); // this file is good for current block result->GetSplitReason()->RemoveReason(IOVolumeSplitReason::ReasonType::SliceDistanceInconsistency); result->GetSplitReason()->RemoveReason(IOVolumeSplitReason::ReasonType::MissingSlices); fileFitsIntoPattern = true; missingSlicesCount = 0; } } else // this should be the very first slice { result->AddFileToSortedBlock( *dsIter ); // this file is good for current block fileFitsIntoPattern = true; } } // record current origin for reference in later iterations if ( !lastOriginInitialized || ( fileFitsIntoPattern && (thisOrigin != lastOrigin) ) ) { lastDifferentOrigin = thisOrigin; } lastOrigin = thisOrigin; lastOriginInitialized = true; } if ( result->ContainsGantryTilt() ) { // check here how many files were grouped. // IF it was only two files AND we assume tiltedness (e.g. save "distance") // THEN we would want to also split the two previous files (simple) because // we don't have any reason to assume they belong together // Above behavior can be configured via m_AcceptTwoSlicesGroups, the default being "do accept" if ( result->GetBlockDatasets().size() == 2 && !m_AcceptTwoSlicesGroups ) { result->UndoPrematureGrouping(); result->GetSplitReason()->AddReason(IOVolumeSplitReason::ReasonType::GantryTiltDifference); } } // update tilt info to get maximum precision // earlier, tilt was only calculated from first and second slice. // now that we know the whole range, we can re-calculate using the very first and last slice if ( result->ContainsGantryTilt() && result->GetBlockDatasets().size() > 1 ) { try { DICOMDatasetList datasets = result->GetBlockDatasets(); DICOMDatasetAccess* firstDataset = datasets.front(); DICOMDatasetAccess* lastDataset = datasets.back(); unsigned int numberOfSlicesApart = datasets.size() - 1; std::string orientationString = firstDataset->GetTagValueAsString( tagImageOrientation ).value; std::string firstOriginString = firstDataset->GetTagValueAsString( tagImagePositionPatient ).value; std::string lastOriginString = lastDataset->GetTagValueAsString( tagImagePositionPatient ).value; result->FlagGantryTilt( GantryTiltInformation::MakeFromTagValues( firstOriginString, lastOriginString, orientationString, numberOfSlicesApart )); } catch (...) { // just do not flag anything, we are ok } } return result; }