Skip to content

Commit

Permalink
ENH: Add TransformixFilter::SetTransform(TransformBase::ConstPointer)
Browse files Browse the repository at this point in the history
Added support for single and composite transforms (itk::CompositeTransform). Tested with itk::TranslationTransform, itk::AffineTransform, itk::Euler2DTransform, itk::Euler3DTransform, itk::Similarity2DTransform, itk::Similarity3DTransform, and itk::BSplineTransform.
  • Loading branch information
N-Dekker committed Oct 21, 2022
1 parent e164dec commit d3b1e66
Show file tree
Hide file tree
Showing 3 changed files with 246 additions and 29 deletions.
193 changes: 166 additions & 27 deletions Core/Main/GTesting/itkTransformixFilterGTest.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -78,12 +79,12 @@ using DefaultConstructibleTransformixFilter = elx::DefaultConstruct<itk::Transfo

namespace
{
template <unsigned NDimension>
template <typename T>
auto
ConvertToItkVector(const itk::Size<NDimension> & size)
ConvertToItkVector(const T & arg)
{
itk::Vector<double, NDimension> result;
std::copy_n(size.begin(), NDimension, result.begin());
itk::Vector<double, T::Dimension> result;
std::copy_n(arg.begin(), T::Dimension, result.begin());
return result;
}

Expand Down Expand Up @@ -182,29 +183,17 @@ CreateTransformixFilter(itk::Image<TPixel, VImageDimension> &
{
const auto filter = CheckNew<itk::TransformixFilter<itk::Image<TPixel, VImageDimension>>>();
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<VImageDimension>() },
{ "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<VImageDimension>() },
{ "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;
}
Expand Down Expand Up @@ -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<ImageDimension>;
const itk::Offset<ImageDimension> translationOffset{ { 1, -2 } };
const auto translationVector = ConvertToItkVector(translationOffset);

const auto regionSize = SizeType::Filled(2);
const SizeType imageSize{ { 5, 6 } };
const itk::Index<ImageDimension> fixedImageRegionIndex{ { 1, 3 } };

using ImageType = itk::Image<PixelType, ImageDimension>;
using TransformixFilterType = itk::TransformixFilter<ImageType>;

elx::DefaultConstruct<ImageType> fixedImage{};
fixedImage.SetRegions(imageSize);
fixedImage.Allocate(true);
FillImageRegion(fixedImage, fixedImageRegionIndex, regionSize);

elx::DefaultConstruct<ImageType> movingImage{};
movingImage.SetRegions(imageSize);
movingImage.Allocate(true);
FillImageRegion(movingImage, fixedImageRegionIndex + translationOffset, regionSize);

elx::DefaultConstruct<itk::TranslationTransform<double, ImageDimension>> transform{};
transform.SetOffset(translationVector);

elx::DefaultConstruct<TransformixFilterType::MeshType> inputMesh{};
inputMesh.SetPoint(0, {});
inputMesh.SetPoint(1, itk::MakePoint(1.0f, 2.0f));

elx::DefaultConstruct<TransformixFilterType> transformixFilter{};
transformixFilter.SetInputMesh(&inputMesh);
transformixFilter.SetMovingImage(&movingImage);
transformixFilter.SetTransform(&transform);
transformixFilter.SetTransformParameterObject(
CreateParameterObject({ // Parameters in alphabetic order:
{ "Direction", CreateDefaultDirectionParameterValues<ImageDimension>() },
{ "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<PixelType, ImageDimension>;
const itk::Size<ImageDimension> imageSize{ { 5, 6 } };

elx::DefaultConstruct<ImageType> movingImage{};
movingImage.SetRegions(imageSize);
movingImage.Allocate(true);

elx::DefaultConstruct<itk::TranslationTransform<double, ImageDimension>> translationTransform{};
elx::DefaultConstruct<itk::CompositeTransform<double, ImageDimension>> compositeTransform{};
compositeTransform.AddTransform(&translationTransform);

const elx::DefaultConstruct<itk::CompositeTransform<double, ImageDimension>> emptyCompositeTransform{};

elx::DefaultConstruct<itk::TransformixFilter<ImageType>> transformixFilter{};
transformixFilter.SetMovingImage(&movingImage);
transformixFilter.SetTransformParameterObject(
CreateParameterObject({ // Parameters in alphabetic order:
{ "Direction", CreateDefaultDirectionParameterValues<ImageDimension>() },
{ "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<PixelType, ImageDimension>;

const auto inputImage = CreateImageFilledWithSequenceOfNaturalNumbers<PixelType>(imageSize);

using ParametersValueType = double;

elx::DefaultConstruct<itk::AffineTransform<ParametersValueType, ImageDimension>> scaleTransform{};
scaleTransform.Scale(2.0);

elx::DefaultConstruct<itk::TranslationTransform<ParametersValueType, ImageDimension>> translationTransform{};
translationTransform.SetOffset(itk::MakeVector(1.0, -2.0));

elx::DefaultConstruct<itk::CompositeTransform<double, 2>> compositeTransform{};
compositeTransform.AddTransform(&scaleTransform);
compositeTransform.AddTransform(&translationTransform);

const ParameterMapType transformParameterMap = {
// Parameters in alphabetic order:
{ "Direction", CreateDefaultDirectionParameterValues<ImageDimension>() },
{ "Index", ParameterValuesType(ImageDimension, "0") },
{ "Origin", ParameterValuesType(ImageDimension, "0") },
{ "ResampleInterpolator", { "FinalLinearInterpolator" } },
{ "Size", ConvertToParameterValues(imageSize) },
{ "Spacing", ParameterValuesType(ImageDimension, "1") }
};

elx::DefaultConstruct<elx::ParameterObject> transformParameterObject{};
transformParameterObject.SetParameterMap(ParameterMapVectorType(2, transformParameterMap));

elx::DefaultConstruct<itk::TransformixFilter<ImageType>> transformixFilter{};
transformixFilter.SetMovingImage(inputImage);
transformixFilter.SetTransform(&compositeTransform);
transformixFilter.SetTransformParameterObject(&transformParameterObject);
transformixFilter.Update();

EXPECT_EQ(Deref(transformixFilter.GetOutput()),
*(CreateResampleImageFilter(*inputImage, compositeTransform)->GetOutput()));
}
16 changes: 16 additions & 0 deletions Core/Main/itkTransformixFilter.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@

#include "itkImageSource.h"
#include "itkMesh.h"
#include "itkTransformBase.h"

#include "elxTransformixMain.h"
#include "elxParameterObject.h"
Expand Down Expand Up @@ -220,6 +221,19 @@ class ITK_TEMPLATE_EXPORT TransformixFilter : public ImageSource<TMovingImage>
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();

Expand Down Expand Up @@ -264,6 +278,8 @@ class ITK_TEMPLATE_EXPORT TransformixFilter : public ImageSource<TMovingImage>

typename MeshType::ConstPointer m_InputMesh{ nullptr };
typename MeshType::Pointer m_OutputMesh{ nullptr };

TransformBase::ConstPointer m_Transform;
};

} // namespace itk
Expand Down
66 changes: 64 additions & 2 deletions Core/Main/itkTransformixFilter.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@
#include "itkTransformixFilter.h"
#include "elxPixelTypeToString.h"
#include "elxTransformBase.h"
#include "elxTransformIO.h"
#include "elxDefaultConstruct.h"

#include <itkCompositeTransform.h>

#include <memory> // For unique_ptr.

namespace itk
Expand Down Expand Up @@ -168,14 +172,16 @@ TransformixFilter<TMovingImage>::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];

Expand All @@ -190,6 +196,62 @@ TransformixFilter<TMovingImage>::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<double> & optimizerParameters) {
ParameterValueVectorType parameterValues(optimizerParameters.size());
std::transform(optimizerParameters.begin(),
optimizerParameters.end(),
parameterValues.begin(),
itk::NumberToString<double>{});
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<const CompositeTransform<double, MovingImageDimension> *>(&*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
Expand Down

0 comments on commit d3b1e66

Please sign in to comment.