diff --git a/Common/GTesting/elxConversionGTest.cxx b/Common/GTesting/elxConversionGTest.cxx index 8bd8e58b8..39220f834 100644 --- a/Common/GTesting/elxConversionGTest.cxx +++ b/Common/GTesting/elxConversionGTest.cxx @@ -325,11 +325,14 @@ GTEST_TEST(Conversion, ToOptimizerParameters) GTEST_TEST(Conversion, ParameterMapToString) { - EXPECT_EQ(Conversion::ParameterMapToString({}), std::string{}); - EXPECT_EQ(Conversion::ParameterMapToString({ { "A", {} } }), "(A)\n"); - EXPECT_EQ(Conversion::ParameterMapToString({ { "Numbers", { "0", "1" } } }), "(Numbers 0 1)\n"); - EXPECT_EQ(Conversion::ParameterMapToString({ { "Letters", { "a", "z" } } }), "(Letters \"a\" \"z\")\n"); - EXPECT_EQ(Conversion::ParameterMapToString(TestExample::parameterMap), TestExample::parameterMapTextString); + EXPECT_EQ(Conversion::ParameterMapToString({}, elx::ParameterMapStringFormat::LegacyTxt), std::string{}); + EXPECT_EQ(Conversion::ParameterMapToString({ { "A", {} } }, elx::ParameterMapStringFormat::LegacyTxt), "(A)\n"); + EXPECT_EQ(Conversion::ParameterMapToString({ { "Numbers", { "0", "1" } } }, elx::ParameterMapStringFormat::LegacyTxt), + "(Numbers 0 1)\n"); + EXPECT_EQ(Conversion::ParameterMapToString({ { "Letters", { "a", "z" } } }, elx::ParameterMapStringFormat::LegacyTxt), + "(Letters \"a\" \"z\")\n"); + EXPECT_EQ(Conversion::ParameterMapToString(TestExample::parameterMap, elx::ParameterMapStringFormat::LegacyTxt), + TestExample::parameterMapTextString); } diff --git a/Core/ComponentBaseClasses/elxResampleInterpolatorBase.hxx b/Core/ComponentBaseClasses/elxResampleInterpolatorBase.hxx index 59204eb2e..4a72dbbfb 100644 --- a/Core/ComponentBaseClasses/elxResampleInterpolatorBase.hxx +++ b/Core/ComponentBaseClasses/elxResampleInterpolatorBase.hxx @@ -20,8 +20,11 @@ #define elxResampleInterpolatorBase_hxx #include "elxResampleInterpolatorBase.h" +#include "elxConfiguration.h" #include "elxConversion.h" +#include "itkDeref.h" + #include namespace elastix @@ -48,12 +51,21 @@ template void ResampleInterpolatorBase::WriteToFile(std::ostream & transformationParameterInfo) const { + const Configuration & configuration = itk::Deref(Superclass::GetConfiguration()); + ParameterMapType parameterMap; this->CreateTransformParameterMap(parameterMap); + const std::string parameterMapFileFormat = + configuration.RetrieveParameterStringValue("", "OutputTransformParameterFileFormat", 0, false); + + const auto format = Conversion::StringToParameterMapStringFormat(parameterMapFileFormat); + /** Write ResampleInterpolator specific things. */ - transformationParameterInfo << ("\n// ResampleInterpolator specific\n" + - Conversion::ParameterMapToString(parameterMap)); + transformationParameterInfo << '\n' + << Conversion::ParameterMapStartOfCommentString(format) + << " ResampleInterpolator specific\n " + + Conversion::ParameterMapToString(parameterMap, format); } // end WriteToFile() diff --git a/Core/ComponentBaseClasses/elxResamplerBase.hxx b/Core/ComponentBaseClasses/elxResamplerBase.hxx index 0503bb9ce..8944198d0 100644 --- a/Core/ComponentBaseClasses/elxResamplerBase.hxx +++ b/Core/ComponentBaseClasses/elxResamplerBase.hxx @@ -641,11 +641,20 @@ template void ResamplerBase::WriteToFile(std::ostream & transformationParameterInfo) const { + const Configuration & configuration = itk::Deref(Superclass::GetConfiguration()); + ParameterMapType parameterMap; Self::CreateTransformParameterMap(parameterMap); + const std::string parameterMapFileFormat = + configuration.RetrieveParameterStringValue("", "OutputTransformParameterFileFormat", 0, false); + + const auto format = Conversion::StringToParameterMapStringFormat(parameterMapFileFormat); + /** Write resampler specific things. */ - transformationParameterInfo << ("\n// Resampler specific\n" + Conversion::ParameterMapToString(parameterMap)); + transformationParameterInfo << '\n' + << Conversion::ParameterMapStartOfCommentString(format) + << " Resampler specific\n " + Conversion::ParameterMapToString(parameterMap, format); } // end WriteToFile() diff --git a/Core/ComponentBaseClasses/elxTransformBase.hxx b/Core/ComponentBaseClasses/elxTransformBase.hxx index bbb1b0a7c..a02f19d6e 100644 --- a/Core/ComponentBaseClasses/elxTransformBase.hxx +++ b/Core/ComponentBaseClasses/elxTransformBase.hxx @@ -561,7 +561,12 @@ TransformBase::WriteToFile(std::ostream & transformationParameterInfo, } } - transformationParameterInfo << Conversion::ParameterMapToString(parameterMap); + const std::string parameterMapFileFormat = + configuration.RetrieveParameterStringValue("", "OutputTransformParameterFileFormat", 0, false); + + const auto format = Conversion::StringToParameterMapStringFormat(parameterMapFileFormat); + + transformationParameterInfo << Conversion::ParameterMapToString(parameterMap, format); WriteDerivedTransformDataToFile(); diff --git a/Core/Configuration/elxConfiguration.cxx b/Core/Configuration/elxConfiguration.cxx index 807d770b5..1b850892e 100644 --- a/Core/Configuration/elxConfiguration.cxx +++ b/Core/Configuration/elxConfiguration.cxx @@ -109,7 +109,8 @@ Configuration::PrintParameterMap() const // Separate clearly in log-file, before and after writing the parameter map. log::info_to_log_file(std::ostringstream{} << "\n=============== start of ParameterMap ===============\n" - << Conversion::ParameterMapToString(m_ParameterMapInterface->GetParameterMap()) + << Conversion::ParameterMapToString(m_ParameterMapInterface->GetParameterMap(), + ParameterMapStringFormat::LegacyTxt) << "\n=============== end of ParameterMap ===============\n"); } // end PrintParameterMap() diff --git a/Core/Install/elxConversion.cxx b/Core/Install/elxConversion.cxx index 5411ba241..b36b02054 100644 --- a/Core/Install/elxConversion.cxx +++ b/Core/Install/elxConversion.cxx @@ -219,53 +219,103 @@ Conversion::ToOptimizerParameters(const std::vector & stdVector) */ std::string -Conversion::ParameterMapToString(const ParameterMapType & parameterMap) +Conversion::ParameterMapToString(const ParameterMapType & parameterMap, const ParameterMapStringFormat format) { - const auto expectedNumberOfChars = std::accumulate( - parameterMap.cbegin(), - parameterMap.cend(), - std::size_t{}, - [](const std::size_t numberOfChars, const std::pair & parameter) { - return numberOfChars + - std::accumulate(parameter.second.cbegin(), - parameter.second.cend(), - // Two parentheses and a linebreak are added for each parameter. - parameter.first.size() + 3, - [](const std::size_t numberOfCharsPerParameter, const std::string & value) { - // A space character is added for each of the values. - // Plus two double-quotes, if the value is not a number. - return numberOfCharsPerParameter + value.size() + (Conversion::IsNumber(value) ? 1 : 3); - }); - }); - std::string result; - result.reserve(expectedNumberOfChars); - for (const auto & parameter : parameterMap) + if (format == ParameterMapStringFormat::Toml) { - result.push_back('('); - result.append(parameter.first); + const auto parameterValueToTomlValue = [](const std::string & parameterValue) { + // Use the same representation in TOML as in elastix for booleans and numbers. + if (parameterValue == BoolToString(false) || parameterValue == BoolToString(true) || IsNumber(parameterValue)) + { + return parameterValue; + } + else + { + return '"' + parameterValue + '"'; + } + }; - for (const auto & value : parameter.second) + for (const auto & parameter : parameterMap) { - result.push_back(' '); + result.append(parameter.first).append(" = "); - if (Conversion::IsNumber(value)) + const auto & parameterValues = parameter.second; + + if (parameterValues.size() == 1) { - result.append(value); + result.append(parameterValueToTomlValue(parameterValues.front())); } else { - result.push_back('"'); - result.append(value); - result.push_back('"'); + result.push_back('['); + + if (!parameterValues.empty()) + { + result.append(parameterValueToTomlValue(parameterValues.front())); + + const std::size_t numberOfParameterValues = parameterValues.size(); + + for (std::size_t i{ 1 }; i < numberOfParameterValues; ++i) + { + result.append(", ").append(parameterValueToTomlValue(parameterValues[i])); + } + } + result.push_back(']'); + } + result.push_back('\n'); + } + } + else + { + const auto expectedNumberOfChars = std::accumulate( + parameterMap.cbegin(), + parameterMap.cend(), + std::size_t{}, + [](const std::size_t numberOfChars, const std::pair & parameter) { + return numberOfChars + + std::accumulate(parameter.second.cbegin(), + parameter.second.cend(), + // Two parentheses and a linebreak are added for each parameter. + parameter.first.size() + 3, + [](const std::size_t numberOfCharsPerParameter, const std::string & value) { + // A space character is added for each of the values. + // Plus two double-quotes, if the value is not a number. + return numberOfCharsPerParameter + value.size() + + (Conversion::IsNumber(value) ? 1 : 3); + }); + }); + + result.reserve(expectedNumberOfChars); + + for (const auto & parameter : parameterMap) + { + result.push_back('('); + result.append(parameter.first); + + for (const auto & value : parameter.second) + { + result.push_back(' '); + + if (Conversion::IsNumber(value)) + { + result.append(value); + } + else + { + result.push_back('"'); + result.append(value); + result.push_back('"'); + } } + result.append(")\n"); } - result.append(")\n"); + + // Assert that the correct number of characters was reserved. + assert(result.size() == expectedNumberOfChars); } - // Assert that the correct number of characters was reserved. - assert(result.size() == expectedNumberOfChars); return result; } @@ -426,4 +476,32 @@ Conversion::StringToValue(const std::string & str, itk::Object *& value) return hasSucceeded; } + +ParameterMapStringFormat +Conversion::StringToParameterMapStringFormat(const std::string & str) +{ + if (str.empty() || str == "txt") + { + return ParameterMapStringFormat::LegacyTxt; + } + if (str == "TOML") + { + return ParameterMapStringFormat::Toml; + } + itkGenericExceptionMacro("Failed to convert to the following string to ParameterMapStringFormat: \"" << str << '"'); +} + +std::string +Conversion::CreateParameterMapFileNameExtension(const ParameterMapStringFormat format) +{ + switch (format) + { + case ParameterMapStringFormat::LegacyTxt: + return ".txt"; + case ParameterMapStringFormat::Toml: + return ".toml"; + } + itkGenericExceptionMacro("Failed to create a file name extension for '" << static_cast(format) << '\''); +} + } // end namespace elastix diff --git a/Core/Install/elxConversion.h b/Core/Install/elxConversion.h index 689a81b61..c23718fcc 100644 --- a/Core/Install/elxConversion.h +++ b/Core/Install/elxConversion.h @@ -35,6 +35,12 @@ class ITK_TEMPLATE_EXPORT OptimizerParameters; namespace elastix { +enum class ParameterMapStringFormat +{ + LegacyTxt, + Toml +}; + /** * \class Conversion * @@ -68,9 +74,16 @@ class Conversion static itk::OptimizerParameters ToOptimizerParameters(const std::vector &); - /** Converts the specified parameter map to a text string, according to the elastix parameter text file format. */ + /** Converts the specified parameter map to a string, according to the specified format. */ static std::string - ParameterMapToString(const ParameterMapType &); + ParameterMapToString(const ParameterMapType &, const ParameterMapStringFormat); + + /** Resturns the character sequence to indicate the start of a comment, for the specified format. */ + static std::string_view + ParameterMapStartOfCommentString(const ParameterMapStringFormat format) + { + return format == ParameterMapStringFormat::Toml ? "#" : "//"; + } /** Convenience function overload to convert a Boolean to a text string. */ static std::string @@ -235,6 +248,15 @@ class Conversion */ static bool StringToValue(const std::string & str, itk::Object *& value); + + /** Converts the specified string to the corresponding enum value. Returns ParameterMapStringFormat::LegacyTxt when + * the string is empty. */ + static ParameterMapStringFormat + StringToParameterMapStringFormat(const std::string & str); + + /** Creates a parameter map file name extension for the corresponding format. */ + static std::string + CreateParameterMapFileNameExtension(const ParameterMapStringFormat format); }; } // end namespace elastix diff --git a/Core/Kernel/elxElastixTemplate.hxx b/Core/Kernel/elxElastixTemplate.hxx index c42353f12..e10dced0f 100644 --- a/Core/Kernel/elxElastixTemplate.hxx +++ b/Core/Kernel/elxElastixTemplate.hxx @@ -732,8 +732,13 @@ ElastixTemplate::AfterRegistration() if (writeFinalTansformParameters && !outputDirectoryPath.empty()) { + const std::string parameterMapFileFormat = + configuration.RetrieveParameterStringValue("", "OutputTransformParameterFileFormat", 0, false); + const auto format = Conversion::StringToParameterMapStringFormat(parameterMapFileFormat); + std::ostringstream makeFileName; - makeFileName << outputDirectoryPath << "TransformParameters." << configuration.GetElastixLevel() << ".txt"; + makeFileName << outputDirectoryPath << "TransformParameters." << configuration.GetElastixLevel() + << Conversion::CreateParameterMapFileNameExtension(format); std::string FileName = makeFileName.str(); /** Create a final TransformParameterFile. */ diff --git a/Core/Main/GTesting/itkElastixRegistrationMethodGTest.cxx b/Core/Main/GTesting/itkElastixRegistrationMethodGTest.cxx index a4f6f612b..67237aefb 100644 --- a/Core/Main/GTesting/itkElastixRegistrationMethodGTest.cxx +++ b/Core/Main/GTesting/itkElastixRegistrationMethodGTest.cxx @@ -2622,3 +2622,49 @@ GTEST_TEST(itkElastixRegistrationMethod, EuclideanDistancePointMetric) const auto transformParameters = GetTransformParametersFromFilter(registration); EXPECT_EQ(ConvertToOffset(transformParameters), translationOffset); } + + +// Tests the use of the OutputTransformParameterFileFormat parameter. +GTEST_TEST(itkElastixRegistrationMethod, OutputTransformParameterFileFormat) +{ + const std::string rootOutputDirectoryPath = GetCurrentBinaryDirectoryPath() + '/' + GetNameOfTest(*this); + itk::FileTools::CreateDirectory(rootOutputDirectoryPath); + + using ImageType = itk::Image; + elx::DefaultConstruct image{}; + image.SetRegions(itk::MakeSize(5, 6)); + image.AllocateInitialized(); + + elx::DefaultConstruct parameterObject{}; + elx::DefaultConstruct> registration{}; + + registration.SetFixedImage(&image); + registration.SetMovingImage(&image); + registration.SetParameterObject(¶meterObject); + + elx::ParameterObject::ParameterMapType parameterMap{ // Parameters in alphabetic order: + { "ImageSampler", { "Full" } }, + { "MaximumNumberOfIterations", { "2" } }, + { "Metric", { "AdvancedNormalizedCorrelation" } }, + { "Optimizer", { "AdaptiveStochasticGradientDescent" } }, + { "Transform", { "TranslationTransform" } } + }; + + const auto check = [¶meterMap, ¶meterObject, ®istration, rootOutputDirectoryPath]( + const std::string & outputTransformParameterFileFormat, + const std::string & expectedTransformParameterFileExtension) { + const std::string outputDirectoryPath = rootOutputDirectoryPath + "/" + outputTransformParameterFileFormat; + itk::FileTools::CreateDirectory(outputDirectoryPath); + + registration.SetOutputDirectory(outputDirectoryPath); + parameterMap["OutputTransformParameterFileFormat"] = { outputTransformParameterFileFormat }; + parameterObject.SetParameterMap(parameterMap); + registration.Update(); + + EXPECT_TRUE(itksys::SystemTools::FileExists(outputDirectoryPath + "/TransformParameters.0" + + expectedTransformParameterFileExtension)); + }; + + check("TOML", ".toml"); + check("txt", ".txt"); +} diff --git a/Core/Main/elxParameterObject.cxx b/Core/Main/elxParameterObject.cxx index 2e034ac39..4f9ef8501 100644 --- a/Core/Main/elxParameterObject.cxx +++ b/Core/Main/elxParameterObject.cxx @@ -280,7 +280,12 @@ ParameterObject::WriteParameterFile(const ParameterMapType & parameterMap, try { - parameterFile << Conversion::ParameterMapToString(parameterMap); + const auto format = itksys::SystemTools::GetFilenameLastExtension(parameterFileName) == + Conversion::CreateParameterMapFileNameExtension(ParameterMapStringFormat::Toml) + ? ParameterMapStringFormat::Toml + : ParameterMapStringFormat::LegacyTxt; + + parameterFile << Conversion::ParameterMapToString(parameterMap, format); } catch (const std::ios_base::failure & e) {