diff --git a/Core/Main/GTesting/itkTransformixFilterGTest.cxx b/Core/Main/GTesting/itkTransformixFilterGTest.cxx index 9a463db55..b6f9ea6f1 100644 --- a/Core/Main/GTesting/itkTransformixFilterGTest.cxx +++ b/Core/Main/GTesting/itkTransformixFilterGTest.cxx @@ -60,6 +60,7 @@ // Type aliases: using ParameterMapType = itk::ParameterFileParser::ParameterMapType; using ParameterValuesType = itk::ParameterFileParser::ParameterValuesType; +using ParameterMapVectorType = elx::ParameterObject::ParameterMapVectorType; // Using-declarations: using elx::CoreMainGTestUtilities::CheckNew; @@ -78,12 +79,12 @@ using DefaultConstructibleTransformixFilter = elx::DefaultConstruct +template auto -ConvertToItkVector(const itk::Size & size) +ConvertToItkVector(const T & arg) { - itk::Vector result; - std::copy_n(size.begin(), NDimension, result.begin()); + itk::Vector result; + std::copy_n(arg.begin(), T::Dimension, result.begin()); return result; } @@ -182,29 +183,17 @@ CreateTransformixFilter(itk::Image & { const auto filter = CheckNew>>(); filter->SetMovingImage(&image); - - std::string transformName = itkTransform.GetNameOfClass(); - - const auto dimensionPosition = transformName.find(std::to_string(VImageDimension) + "DTransform"); - if (dimensionPosition != std::string::npos) - { - // Erase "2D" or "3D". - transformName.erase(dimensionPosition, 2); - } - - filter->SetTransformParameterObject(CreateParameterObject( - { // Parameters in alphabetic order: - { "Direction", CreateDefaultDirectionParameterValues() }, - { "HowToCombineTransforms", { howToCombineTransforms } }, - { "Index", ParameterValuesType(VImageDimension, "0") }, - { "InitialTransformParametersFileName", { initialTransformParametersFileName } }, - { "ITKTransformParameters", ConvertToParameterValues(itkTransform.GetParameters()) }, - { "ITKTransformFixedParameters", ConvertToParameterValues(itkTransform.GetFixedParameters()) }, - { "Origin", ParameterValuesType(VImageDimension, "0") }, - { "ResampleInterpolator", { "FinalLinearInterpolator" } }, - { "Size", ConvertToParameterValues(image.GetBufferedRegion().GetSize()) }, - { "Transform", { transformName } }, - { "Spacing", ParameterValuesType(VImageDimension, "1") } })); + filter->SetTransform(&itkTransform); + filter->SetTransformParameterObject( + CreateParameterObject({ // Parameters in alphabetic order: + { "Direction", CreateDefaultDirectionParameterValues() }, + { "HowToCombineTransforms", { howToCombineTransforms } }, + { "Index", ParameterValuesType(VImageDimension, "0") }, + { "InitialTransformParametersFileName", { initialTransformParametersFileName } }, + { "Origin", ParameterValuesType(VImageDimension, "0") }, + { "ResampleInterpolator", { "FinalLinearInterpolator" } }, + { "Size", ConvertToParameterValues(image.GetBufferedRegion().GetSize()) }, + { "Spacing", ParameterValuesType(VImageDimension, "1") } })); filter->Update(); return filter; } @@ -966,3 +955,207 @@ GTEST_TEST(itkTransformixFilter, OutputEqualsRegistrationOutputForBSplineStackTr } } } + + +// Tests setting an `itk::TranslationTransform`, to transform a simple image and a small mesh. +GTEST_TEST(itkTransformixFilter, SetTranslationTransform) +{ + using PixelType = float; + constexpr unsigned int ImageDimension{ 2 }; + + using SizeType = itk::Size; + const itk::Offset translationOffset{ { 1, -2 } }; + const auto translationVector = ConvertToItkVector(translationOffset); + + const auto regionSize = SizeType::Filled(2); + const SizeType imageSize{ { 5, 6 } }; + const itk::Index fixedImageRegionIndex{ { 1, 3 } }; + + using ImageType = itk::Image; + using TransformixFilterType = itk::TransformixFilter; + + elx::DefaultConstruct fixedImage{}; + fixedImage.SetRegions(imageSize); + fixedImage.Allocate(true); + FillImageRegion(fixedImage, fixedImageRegionIndex, regionSize); + + elx::DefaultConstruct movingImage{}; + movingImage.SetRegions(imageSize); + movingImage.Allocate(true); + FillImageRegion(movingImage, fixedImageRegionIndex + translationOffset, regionSize); + + elx::DefaultConstruct> transform{}; + transform.SetOffset(translationVector); + + elx::DefaultConstruct inputMesh{}; + inputMesh.SetPoint(0, {}); + inputMesh.SetPoint(1, itk::MakePoint(1.0f, 2.0f)); + + elx::DefaultConstruct transformixFilter{}; + transformixFilter.SetInputMesh(&inputMesh); + transformixFilter.SetMovingImage(&movingImage); + transformixFilter.SetTransform(&transform); + transformixFilter.SetTransformParameterObject( + CreateParameterObject({ // Parameters in alphabetic order: + { "Direction", CreateDefaultDirectionParameterValues() }, + { "Index", ParameterValuesType(ImageDimension, "0") }, + { "Origin", ParameterValuesType(ImageDimension, "0") }, + { "ResampleInterpolator", { "FinalLinearInterpolator" } }, + { "Size", ConvertToParameterValues(imageSize) }, + { "Spacing", ParameterValuesType(ImageDimension, "1") } })); + transformixFilter.Update(); + + ExpectEqualImages(Deref(transformixFilter.GetOutput()), fixedImage); + + const auto outputMesh = transformixFilter.GetOutputMesh(); + const auto expectedNumberOfPoints = inputMesh.GetNumberOfPoints(); + + const auto & inputPoints = Deref(inputMesh.GetPoints()); + const auto & outputPoints = Deref(Deref(outputMesh).GetPoints()); + + ASSERT_EQ(outputPoints.size(), expectedNumberOfPoints); + + for (size_t i = 0; i < expectedNumberOfPoints; ++i) + { + EXPECT_EQ(outputPoints[i], inputPoints[i] + translationVector); + } +} + + +// Tests that Update() throws an exception when the transform parameter object has zero parameter maps. +GTEST_TEST(itkTransformixFilter, UpdateThrowsExceptionOnZeroParameterMaps) +{ + using PixelType = float; + constexpr unsigned int ImageDimension{ 2 }; + using ImageType = itk::Image; + const auto imageSize = ImageType::SizeType::Filled(2); + + for (const bool useZeroParameterMaps : { false, true }) + { + elx::DefaultConstruct image{}; + image.SetRegions(imageSize); + image.Allocate(true); + + elx::DefaultConstruct> transform{}; + + elx::DefaultConstruct transformParameterObject{}; + + const auto parameterMaps = useZeroParameterMaps + ? ParameterMapVectorType{} + : ParameterMapVectorType{ ParameterMapType{ + { "Direction", CreateDefaultDirectionParameterValues() }, + { "Index", ParameterValuesType(ImageDimension, "0") }, + { "Origin", ParameterValuesType(ImageDimension, "0") }, + { "ResampleInterpolator", { "FinalLinearInterpolator" } }, + { "Size", ConvertToParameterValues(imageSize) }, + { "Spacing", ParameterValuesType(ImageDimension, "1") } } }; + + transformParameterObject.SetParameterMap(parameterMaps); + + elx::DefaultConstruct> transformixFilter{}; + transformixFilter.SetMovingImage(&image); + transformixFilter.SetTransform(&transform); + transformixFilter.SetTransformParameterObject(&transformParameterObject); + + if (useZeroParameterMaps) + { + EXPECT_THROW(transformixFilter.Update(), itk::ExceptionObject); + } + else + { + // A valid parameter map was specified, do not expect an exception when calling Update(). (This is just a sanity + // check. The essential check is in the `if (useZeroParameterMaps)` clause.) + transformixFilter.Update(); + } + } +} + + +// Tests that Update() throws an exception when the transform is a CompositeTransform that has zero subtransforms. +GTEST_TEST(itkTransformixFilter, UpdateThrowsExceptionOnEmptyCompositeTransform) +{ + using PixelType = float; + constexpr unsigned int ImageDimension{ 2 }; + using ImageType = itk::Image; + const itk::Size imageSize{ { 5, 6 } }; + + elx::DefaultConstruct movingImage{}; + movingImage.SetRegions(imageSize); + movingImage.Allocate(true); + + elx::DefaultConstruct> translationTransform{}; + elx::DefaultConstruct> compositeTransform{}; + compositeTransform.AddTransform(&translationTransform); + + const elx::DefaultConstruct> emptyCompositeTransform{}; + + elx::DefaultConstruct> transformixFilter{}; + transformixFilter.SetMovingImage(&movingImage); + transformixFilter.SetTransformParameterObject( + CreateParameterObject({ // Parameters in alphabetic order: + { "Direction", CreateDefaultDirectionParameterValues() }, + { "Index", ParameterValuesType(ImageDimension, "0") }, + { "Origin", ParameterValuesType(ImageDimension, "0") }, + { "ResampleInterpolator", { "FinalLinearInterpolator" } }, + { "Size", ConvertToParameterValues(imageSize) }, + { "Spacing", ParameterValuesType(ImageDimension, "1") } })); + + for (const bool isSecondIteration : { false, true }) + { + transformixFilter.SetTransform(&emptyCompositeTransform); + EXPECT_THROW(transformixFilter.Update(), itk::ExceptionObject); + + // compositeTransform is non-empty. + transformixFilter.SetTransform(&compositeTransform); + transformixFilter.Update(); + } +} + + +// Tests setting an `itk::CompositeTransform` which consists of a translation and a scaling. +GTEST_TEST(itkTransformixFilter, SetCompositeTransformOfTranslationAndScale) +{ + using PixelType = float; + const auto imageSize = itk::MakeSize(5, 6); + constexpr unsigned int ImageDimension{ decltype(imageSize)::Dimension }; + using ImageType = itk::Image; + + const auto inputImage = CreateImageFilledWithSequenceOfNaturalNumbers(imageSize); + + using ParametersValueType = double; + + elx::DefaultConstruct> scaleTransform{}; + scaleTransform.Scale(2.0); + + elx::DefaultConstruct> translationTransform{}; + translationTransform.SetOffset(itk::MakeVector(1.0, -2.0)); + + elx::DefaultConstruct> compositeTransform{}; + compositeTransform.AddTransform(&scaleTransform); + compositeTransform.AddTransform(&translationTransform); + + const ParameterMapType transformParameterMap = { + // Parameters in alphabetic order: + { "Direction", CreateDefaultDirectionParameterValues() }, + { "Index", ParameterValuesType(ImageDimension, "0") }, + { "Origin", ParameterValuesType(ImageDimension, "0") }, + { "ResampleInterpolator", { "FinalLinearInterpolator" } }, + { "Size", ConvertToParameterValues(imageSize) }, + { "Spacing", ParameterValuesType(ImageDimension, "1") } + }; + + for (size_t numberOfParameterMaps{ 1 }; numberOfParameterMaps <= 3; ++numberOfParameterMaps) + { + elx::DefaultConstruct transformParameterObject{}; + transformParameterObject.SetParameterMap(ParameterMapVectorType(numberOfParameterMaps, transformParameterMap)); + + elx::DefaultConstruct> transformixFilter{}; + transformixFilter.SetMovingImage(inputImage); + transformixFilter.SetTransform(&compositeTransform); + transformixFilter.SetTransformParameterObject(&transformParameterObject); + transformixFilter.Update(); + + EXPECT_EQ(Deref(transformixFilter.GetOutput()), + *(CreateResampleImageFilter(*inputImage, compositeTransform)->GetOutput())); + } +} diff --git a/Core/Main/itkTransformixFilter.h b/Core/Main/itkTransformixFilter.h index 7ead33bdd..a1d99694f 100644 --- a/Core/Main/itkTransformixFilter.h +++ b/Core/Main/itkTransformixFilter.h @@ -37,6 +37,7 @@ #include "itkImageSource.h" #include "itkMesh.h" +#include "itkTransformBase.h" #include "elxTransformixMain.h" #include "elxParameterObject.h" @@ -220,6 +221,11 @@ class ITK_TEMPLATE_EXPORT TransformixFilter : public ImageSource return m_OutputMesh; } + /** Sets the transformation. If null, the transformation is entirely specified by the transform + * parameter object that is set by SetTransformParameterObject. Otherwise, the transformation is specified by this + * transform object, with additional information from the specified transform parameter object. */ + itkSetConstObjectMacro(Transform, TransformBase); + protected: TransformixFilter(); @@ -264,6 +270,8 @@ class ITK_TEMPLATE_EXPORT TransformixFilter : public ImageSource typename MeshType::ConstPointer m_InputMesh{ nullptr }; typename MeshType::Pointer m_OutputMesh{ nullptr }; + + TransformBase::ConstPointer m_Transform; }; } // namespace itk diff --git a/Core/Main/itkTransformixFilter.hxx b/Core/Main/itkTransformixFilter.hxx index db7567733..0eabb821d 100644 --- a/Core/Main/itkTransformixFilter.hxx +++ b/Core/Main/itkTransformixFilter.hxx @@ -39,7 +39,11 @@ #include "itkTransformixFilter.h" #include "elxPixelTypeToString.h" #include "elxTransformBase.h" +#include "elxTransformIO.h" #include "elxDefaultConstruct.h" + +#include + #include // For unique_ptr. namespace itk @@ -174,6 +178,73 @@ TransformixFilter::GenerateData() itkExceptionMacro("Empty parameter map in parameter object."); } + if (m_Transform) + { + // Adjust the local transformParameterMap according to this m_Transform. + + const auto transformToMap = [](const itk::TransformBase & transform, auto & transformParameterMap) { + const auto convertToParameterValues = [](const itk::OptimizerParameters & optimizerParameters) { + ParameterValueVectorType parameterValues(optimizerParameters.size()); + std::transform(optimizerParameters.begin(), + optimizerParameters.end(), + parameterValues.begin(), + itk::NumberToString{}); + return parameterValues; + }; + + transformParameterMap["ITKTransformFixedParameters"] = convertToParameterValues(transform.GetFixedParameters()); + transformParameterMap["ITKTransformParameters"] = convertToParameterValues(transform.GetParameters()); + transformParameterMap["ITKTransformType"] = { transform.GetTransformTypeAsString() }; + transformParameterMap["Transform"] = { elx::TransformIO::ConvertITKNameOfClassToElastixClassName( + transform.GetNameOfClass()) }; + }; + const auto compositeTransform = + dynamic_cast *>(&*m_Transform); + + if (compositeTransform) + { + const auto & transformQueue = compositeTransform->GetTransformQueue(); + + const auto numberOfTransforms = transformQueue.size(); + + if (numberOfTransforms == 0) + { + itkExceptionMacro( + "The specified composite transform has no subtransforms! At least one subtransform is required!"); + } + + if (numberOfTransforms != transformParameterMapVector.size()) + { + // The last TransformParameterMap is special, as it needs to be used for the final transformation. + auto lastTransformParameterMap = transformParameterMapVector.back(); + transformParameterMapVector.resize(numberOfTransforms); + transformParameterMapVector.back() = std::move(lastTransformParameterMap); + } + for (unsigned int i = 0; i < numberOfTransforms; ++i) + { + auto & transformParameterMap = transformParameterMapVector[numberOfTransforms - i - 1]; + const auto transform = transformQueue[i]; + + if (transform == nullptr) + { + itkExceptionMacro("One of the subtransforms of the specified composite transform is null!"); + } + transformToMap(*transform, transformParameterMap); + } + } + else + { + // Assume in this case that it is just a single transform. + assert((dynamic_cast *>(&*m_Transform)) == nullptr); + + // For a single transform, there should be only a single transform parameter map. + auto transformParameterMap = std::move(transformParameterMapVector.back()); + transformToMap(*m_Transform, transformParameterMap); + transformParameterMapVector.clear(); + transformParameterMapVector.push_back(std::move(transformParameterMap)); + } + } + // Set pixel types from input image, override user settings for (unsigned int i = 0; i < transformParameterMapVector.size(); ++i) {