diff --git a/Core/Main/GTesting/itkTransformixFilterGTest.cxx b/Core/Main/GTesting/itkTransformixFilterGTest.cxx index 9a463db55..37bf17619 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,153 @@ 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); + } +} + + +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(); + } +} + + +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") } + }; + + elx::DefaultConstruct transformParameterObject{}; + transformParameterObject.SetParameterMap(ParameterMapVectorType(2, 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..25b1ee19d 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,19 @@ 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. */ + void + SetTransform(TransformBase::ConstPointer transform) + { + if (transform != m_Transform) + { + m_Transform = transform; + this->Modified(); + } + } + protected: TransformixFilter(); @@ -264,6 +278,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..84f3c62f7 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 @@ -168,14 +172,16 @@ TransformixFilter::GenerateData() ParameterObjectPointer transformParameterObject = this->GetTransformParameterObject(); ParameterMapVectorType transformParameterMapVector = transformParameterObject->GetParameterMap(); + const auto numberOfTransformParameterMaps = transformParameterMapVector.size(); + // Assert user did not set empty parameter map - if (transformParameterMapVector.empty()) + if (numberOfTransformParameterMaps == 0) { itkExceptionMacro("Empty parameter map in parameter object."); } // Set pixel types from input image, override user settings - for (unsigned int i = 0; i < transformParameterMapVector.size(); ++i) + for (unsigned int i = 0; i < numberOfTransformParameterMaps; ++i) { auto & transformParameterMap = transformParameterMapVector[i]; @@ -190,6 +196,62 @@ TransformixFilter::GenerateData() } } + 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 != numberOfTransformParameterMaps) + { + itkExceptionMacro("The composite transform specified by SetTransform has the wrong number of transforms (" + << numberOfTransforms << "). Expected: " << numberOfTransformParameterMaps); + } + for (unsigned int i = 0; i < numberOfTransformParameterMaps; ++i) + { + auto & transformParameterMap = transformParameterMapVector[numberOfTransformParameterMaps - 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 + { + for (auto & transformParameterMap : transformParameterMapVector) + { + // Use the same transform for all parameter maps. + transformToMap(*m_Transform, transformParameterMap); + } + } + } + // Run transformix unsigned int isError = 0; try