Skip to content

Commit

Permalink
BUG: Various Fixes For ApplyTransformationToGeometry (#640)
Browse files Browse the repository at this point in the history
* + Update ApplyTransformationToGeometry to automatically add translations
to/from (0, 0, 0) for Scaling and Rotation transformation types.

+ Only calculate rotation args if absolutely required.

+ Set transformed origin to the minimum coordinate from the transformed
volume.

Signed-off-by: Joey Kleingers <[email protected]>
  • Loading branch information
joeykleingers authored Jul 5, 2023
1 parent 23e597d commit 6874864
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ take place are the actual coordinates of the vertices.
If the user selects an **Image Geometry** then the user should select one of the *Interpolation* methods and then also
select the appropriate *Cell Attribute Matrix*.

The **Scale** and **Rotation** transformation types will automatically translate the volume to (0, 0, 0), apply the scaling/rotation,
and then translate the volume back to its original location. If the **Manual Transformation Matrix** or **Pre-Computed Transformation
Matrix** types are selected, then it is up to the user to make sure that those translations are included, if necessary.

## Example Transformations

| Description | Example Output Image |
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#include "ApplyTransformationToGeometry.hpp"

#include "complex/DataStructure/DataArray.hpp"
#include "complex/DataStructure/DataGroup.hpp"
#include "complex/DataStructure/Geometry/INodeGeometry0D.hpp"
#include "complex/Utilities/DataGroupUtilities.hpp"
#include "complex/Utilities/ParallelAlgorithmUtilities.hpp"
Expand Down Expand Up @@ -32,7 +31,6 @@ const std::atomic_bool& ApplyTransformationToGeometry::getCancel()
// -----------------------------------------------------------------------------
Result<> ApplyTransformationToGeometry::applyImageGeometryTransformation()
{

// Pure translation for Image Geom, just return
if(m_InputValues->TransformationSelection == k_TranslationIdx)
{
Expand Down Expand Up @@ -81,9 +79,9 @@ Result<> ApplyTransformationToGeometry::applyImageGeometryTransformation()
const std::vector<usize> dims = {static_cast<usize>(rotateArgs.xpNew), static_cast<usize>(rotateArgs.ypNew), static_cast<usize>(rotateArgs.zpNew)};
const std::vector<float32> spacing = {rotateArgs.xResNew, rotateArgs.yResNew, rotateArgs.zResNew};
auto origin = srcImageGeom.getOrigin().toContainer<std::vector<float32>>();
origin[0] += rotateArgs.xMinNew;
origin[1] += rotateArgs.yMinNew;
origin[2] += rotateArgs.zMinNew;
origin[0] = rotateArgs.xMinNew;
origin[1] = rotateArgs.yMinNew;
origin[2] = rotateArgs.zMinNew;

std::vector<usize> const dataArrayShape = {dims[2], dims[1], dims[0]}; // The DataArray shape goes slowest to fastest (ZYX), opposite of ImageGeometry dimensions
destImageGeom.setDimensions(dims);
Expand Down Expand Up @@ -150,6 +148,20 @@ Result<> ApplyTransformationToGeometry::operator()()
return MakeErrorResult(-84500, fmt::format("Keeping the original geometry is not supported."));
}

auto* imageGeometryPtr = m_DataStructure.getDataAs<ImageGeom>(m_InputValues->SelectedGeometryPath);
const bool isNodeBased = (imageGeometryPtr == nullptr);

ImageRotationUtilities::Matrix4fR translationToGlobalOriginMat = ImageRotationUtilities::Matrix4fR::Identity();
ImageRotationUtilities::Matrix4fR translationFromGlobalOriginMat = ImageRotationUtilities::Matrix4fR::Identity();
if(isNodeBased)
{
auto& nodeGeometry0D = m_DataStructure.getDataRefAs<INodeGeometry0D>(m_InputValues->SelectedGeometryPath);
auto boundingBox = nodeGeometry0D.getBoundingBox();
Point3Df minPoint = boundingBox.getMinPoint();
translationToGlobalOriginMat = ImageRotationUtilities::GenerateTranslationTransformationMatrix({-minPoint[0], -minPoint[1], -minPoint[2]});
translationFromGlobalOriginMat = ImageRotationUtilities::GenerateTranslationTransformationMatrix({minPoint[0], minPoint[1], minPoint[2]});
}

switch(m_InputValues->TransformationSelection)
{
case k_NoTransformIdx: // No-Op
Expand All @@ -170,6 +182,12 @@ Result<> ApplyTransformationToGeometry::operator()()
case k_RotationIdx: // Rotation via axis-angle
{
m_TransformationMatrix = ImageRotationUtilities::GenerateRotationTransformationMatrix(m_InputValues->Rotation);
if(isNodeBased)
{
// Translate the geometry to/from the global origin
m_TransformationMatrix = translationFromGlobalOriginMat * m_TransformationMatrix * translationToGlobalOriginMat;
}

break;
}
case k_TranslationIdx: // Translation
Expand All @@ -180,19 +198,23 @@ Result<> ApplyTransformationToGeometry::operator()()
case k_ScaleIdx: // Scale
{
m_TransformationMatrix = ImageRotationUtilities::GenerateScaleTransformationMatrix(m_InputValues->Scale);
if(isNodeBased)
{
// Translate the geometry to/from the global origin
m_TransformationMatrix = translationFromGlobalOriginMat * m_TransformationMatrix * translationToGlobalOriginMat;
}
break;
}
}

auto* geometryPtr = m_DataStructure.getDataAs<ImageGeom>(m_InputValues->SelectedGeometryPath);

if(geometryPtr != nullptr) // Function for applying Image Transformation
// Apply geometry transformation
if(isNodeBased)
{
applyImageGeometryTransformation();
applyNodeGeometryTransformation();
}
else
{
applyNodeGeometryTransformation();
applyImageGeometryTransformation();
}

return {};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

#include "ComplexCore/Filters/Algorithms/ApplyTransformationToGeometry.hpp"

#include "complex/Common/Constants.hpp"
#include "complex/DataStructure/DataArray.hpp"
#include "complex/DataStructure/DataPath.hpp"
#include "complex/DataStructure/Geometry/ImageGeom.hpp"
Expand Down Expand Up @@ -148,72 +147,81 @@ IFilter::PreflightResult ApplyTransformationToGeometryFilter::preflightImpl(cons
std::string transformationMatrixDesc;
transformationMatrix.setIdentity();

switch(pTransformationMatrixTypeValue)
{
case k_NoTransformIdx: // No-Op
{
resultOutputActions.warnings().push_back(Warning{82001, "No transformation has been selected, so this filter will perform no operations"});
transformationMatrixDesc = "No transformation matrix selected.";
}
case k_PrecomputedTransformationMatrixIdx: // Transformation matrix from array
// if ImageGeom was selected to be transformed: This should work because if we didn't pass
// the earlier test, we should not have gotten to here.
const ImageGeom* imageGeomPtr = dataStructure.getDataAs<ImageGeom>(pSelectedGeometryPathValue);
if(imageGeomPtr != nullptr)
{
const Float32Array* precomputedMatrixPtr = dataStructure.getDataAs<Float32Array>(pComputedTransformationMatrixPath);
if(nullptr == precomputedMatrixPtr)
switch(pTransformationMatrixTypeValue)
{
case k_NoTransformIdx: // No-Op
{
return {MakeErrorResult<OutputActions>(-82010, fmt::format("Precomputed transformation matrix must have a valid path. Invalid path given: '{}'", pComputedTransformationMatrixPath.toString()))};
resultOutputActions.warnings().push_back(Warning{82001, "No transformation has been selected, so this filter will perform no operations"});
transformationMatrixDesc = "No transformation matrix selected.";
}
transformationMatrixDesc = K_UNKNOWN_PRECOMPUTED_MATRIX_STR;
break;
}
case k_ManualTransformationMatrixIdx: // Manual transformation matrix
{
const usize numTableRows = tableData.size();
const usize numTableCols = tableData[0].size();
if(numTableRows != 4)
case k_PrecomputedTransformationMatrixIdx: // Transformation matrix from array
{
return {MakeErrorResult<OutputActions>(-82002, "Manually entered transformation matrix must have exactly 4 rows")};
const Float32Array* precomputedMatrixPtr = dataStructure.getDataAs<Float32Array>(pComputedTransformationMatrixPath);
if(nullptr == precomputedMatrixPtr)
{
return {
MakeErrorResult<OutputActions>(-82010, fmt::format("Precomputed transformation matrix must have a valid path. Invalid path given: '{}'", pComputedTransformationMatrixPath.toString()))};
}
transformationMatrixDesc = K_UNKNOWN_PRECOMPUTED_MATRIX_STR;
break;
}
if(numTableCols != 4)
case k_ManualTransformationMatrixIdx: // Manual transformation matrix
{
return {MakeErrorResult<OutputActions>(-82006, "Manually entered transformation matrix must have exactly 4 columns")};
const usize numTableRows = tableData.size();
const usize numTableCols = tableData[0].size();
if(numTableRows != 4)
{
return {MakeErrorResult<OutputActions>(-82002, "Manually entered transformation matrix must have exactly 4 rows")};
}
if(numTableCols != 4)
{
return {MakeErrorResult<OutputActions>(-82006, "Manually entered transformation matrix must have exactly 4 columns")};
}
transformationMatrix = ImageRotationUtilities::GenerateManualTransformationMatrix(tableData);
transformationMatrixDesc = ImageRotationUtilities::GenerateTransformationMatrixDescription(transformationMatrix);
break;
}
case k_RotationIdx: // Rotation via axis-angle
{
auto pRotationValue = filterArgs.value<VectorFloat32Parameter::ValueType>(k_Rotation_Key);
auto origin = imageGeomPtr->getOrigin();
const ImageRotationUtilities::Matrix4fR translationToGlobalOriginMat = ImageRotationUtilities::GenerateTranslationTransformationMatrix({-origin[0], -origin[1], -origin[2]});
transformationMatrix = ImageRotationUtilities::GenerateRotationTransformationMatrix(pRotationValue);
const ImageRotationUtilities::Matrix4fR translationFromGlobalOriginMat = ImageRotationUtilities::GenerateTranslationTransformationMatrix({origin[0], origin[1], origin[2]});
transformationMatrix = translationFromGlobalOriginMat * transformationMatrix * translationToGlobalOriginMat;
transformationMatrixDesc = ImageRotationUtilities::GenerateTransformationMatrixDescription(transformationMatrix);
break;
}
case k_TranslationIdx: // Translation
{
auto pTranslationValue = filterArgs.value<VectorFloat32Parameter::ValueType>(k_Translation_Key);
transformationMatrix = ImageRotationUtilities::GenerateTranslationTransformationMatrix(pTranslationValue);
transformationMatrixDesc = ImageRotationUtilities::GenerateTransformationMatrixDescription(transformationMatrix);
break;
}
case k_ScaleIdx: // Scale
{
auto pScaleValue = filterArgs.value<VectorFloat32Parameter::ValueType>(k_Scale_Key);
auto origin = imageGeomPtr->getOrigin();
const ImageRotationUtilities::Matrix4fR translationToGlobalOriginMat = ImageRotationUtilities::GenerateTranslationTransformationMatrix({-origin[0], -origin[1], -origin[2]});
transformationMatrix = ImageRotationUtilities::GenerateScaleTransformationMatrix(pScaleValue);
const ImageRotationUtilities::Matrix4fR translationFromGlobalOriginMat = ImageRotationUtilities::GenerateTranslationTransformationMatrix({origin[0], origin[1], origin[2]});
transformationMatrix = translationFromGlobalOriginMat * transformationMatrix * translationToGlobalOriginMat;
transformationMatrixDesc = ImageRotationUtilities::GenerateTransformationMatrixDescription(transformationMatrix);
break;
}
default: {
return {MakeErrorResult<OutputActions>(-82003, "Invalid selection for transformation operation. Valid values are [0,5]")};
}
}
transformationMatrix = ImageRotationUtilities::GenerateManualTransformationMatrix(tableData);
transformationMatrixDesc = ImageRotationUtilities::GenerateTransformationMatrixDescription(transformationMatrix);
break;
}
case k_RotationIdx: // Rotation via axis-angle
{
auto pRotationValue = filterArgs.value<VectorFloat32Parameter::ValueType>(k_Rotation_Key);
transformationMatrix = ImageRotationUtilities::GenerateRotationTransformationMatrix(pRotationValue);
transformationMatrixDesc = ImageRotationUtilities::GenerateTransformationMatrixDescription(transformationMatrix);
break;
}
case k_TranslationIdx: // Translation
{
auto pTranslationValue = filterArgs.value<VectorFloat32Parameter::ValueType>(k_Translation_Key);
transformationMatrix = ImageRotationUtilities::GenerateTranslationTransformationMatrix(pTranslationValue);
transformationMatrixDesc = ImageRotationUtilities::GenerateTransformationMatrixDescription(transformationMatrix);
break;
}
case k_ScaleIdx: // Scale
{
auto pScaleValue = filterArgs.value<VectorFloat32Parameter::ValueType>(k_Scale_Key);
transformationMatrix = ImageRotationUtilities::GenerateScaleTransformationMatrix(pScaleValue);
transformationMatrixDesc = ImageRotationUtilities::GenerateTransformationMatrixDescription(transformationMatrix);
break;
}
default: {
return {MakeErrorResult<OutputActions>(-82003, "Invalid selection for transformation operation. Valid values are [0,5]")};
}
}

preflightUpdatedValues.push_back({"Generated Transformation Matrix", transformationMatrixDesc});
preflightUpdatedValues.push_back({"Generated Transformation Matrix", transformationMatrixDesc});

// if ImageGeom was selected to be transformed: This should work because if we didn't pass
// the earlier test, we should not have gotten to here.
const ImageGeom* imageGeomPtr = dataStructure.getDataAs<ImageGeom>(pSelectedGeometryPathValue);
if(imageGeomPtr != nullptr)
{
auto pInterpolationTypeValue = filterArgs.value<ChoicesParameter::ValueType>(k_InterpolationType_Key);
// auto pDataArraySelectionValue = filterArgs.value<MultiArraySelectionParameter::ValueType>(k_DataArraySelection_Key);

Expand Down Expand Up @@ -260,25 +268,28 @@ IFilter::PreflightResult ApplyTransformationToGeometryFilter::preflightImpl(cons
}
}

auto rotateArgs = ImageRotationUtilities::CreateRotationArgs(*imageGeomPtr, transformationMatrix);

// If the user is purely doing a translation then just adjust the origin and be done.
if(pTransformationMatrixTypeValue == k_TranslationIdx)
{
// If the user is purely doing a translation then just adjust the origin and be done.
auto pTranslationValue = filterArgs.value<VectorFloat32Parameter::ValueType>(k_Translation_Key);
FloatVec3 originVec = {rotateArgs.OriginalOrigin[0] + pTranslationValue[0], rotateArgs.OriginalOrigin[1] + pTranslationValue[1], rotateArgs.OriginalOrigin[2] + pTranslationValue[2]};
FloatVec3 originVec = imageGeomPtr->getOrigin();
originVec = {originVec[0] + pTranslationValue[0], originVec[1] + pTranslationValue[1], originVec[2] + pTranslationValue[2]};
auto spacingVec = imageGeomPtr->getSpacing();
resultOutputActions.value().appendAction(std::make_unique<UpdateImageGeomAction>(originVec, spacingVec, pSelectedGeometryPathValue));
}
else if(pTransformationMatrixTypeValue == k_ScaleIdx)
{
// If the user is purely doing a scaling then just adjust the spacing and be done.
auto pScaleValue = filterArgs.value<VectorFloat32Parameter::ValueType>(k_Scale_Key);
FloatVec3 spacingVec = {rotateArgs.OriginalSpacing[0] * pScaleValue[0], rotateArgs.OriginalSpacing[1] * pScaleValue[1], rotateArgs.OriginalSpacing[2] * pScaleValue[2]};
FloatVec3 spacingVec = imageGeomPtr->getSpacing();
spacingVec = {spacingVec[0] * pScaleValue[0], spacingVec[1] * pScaleValue[1], spacingVec[2] * pScaleValue[2]};
auto originVec = imageGeomPtr->getOrigin();
resultOutputActions.value().appendAction(std::make_unique<UpdateImageGeomAction>(originVec, spacingVec, pSelectedGeometryPathValue));
}
else // We are Rotating or scaling, manual transformation or precomputed. we need to create a brand new Image Geometry
{
auto rotateArgs = ImageRotationUtilities::CreateRotationArgs(*imageGeomPtr, transformationMatrix);

auto srcImagePath = filterArgs.value<DataPath>(k_SelectedImageGeometry_Key);
DataPath destImagePath = srcImagePath; // filterArgs.value<DataPath>(k_CreatedImageGeometry_Key);
auto pRemoveOriginalGeometry = true; // filterArgs.value<bool>(k_RemoveOriginalGeometry_Key);
Expand All @@ -287,9 +298,9 @@ IFilter::PreflightResult ApplyTransformationToGeometryFilter::preflightImpl(cons
const std::vector<usize> dims = {static_cast<usize>(rotateArgs.xpNew), static_cast<usize>(rotateArgs.ypNew), static_cast<usize>(rotateArgs.zpNew)};
const std::vector<float32> spacing = {rotateArgs.xResNew, rotateArgs.yResNew, rotateArgs.zResNew};
auto origin = selectedImageGeom.getOrigin().toContainer<std::vector<float32>>();
origin[0] += rotateArgs.xMinNew;
origin[1] += rotateArgs.yMinNew;
origin[2] += rotateArgs.zMinNew;
origin[0] = rotateArgs.xMinNew;
origin[1] = rotateArgs.yMinNew;
origin[2] = rotateArgs.zMinNew;

if(pRemoveOriginalGeometry)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ namespace

namespace apply_transformation_to_geometry
{
const complex::ChoicesParameter::ValueType k_NoTransformIdx = 0ULL;
const complex::ChoicesParameter::ValueType k_PrecomputedTransformationMatrixIdx = 1ULL;
const complex::ChoicesParameter::ValueType k_ManualTransformationMatrixIdx = 2ULL;
const complex::ChoicesParameter::ValueType k_RotationIdx = 3ULL;
Expand All @@ -45,7 +44,6 @@ const std::string k_TranslationGeometryName("6_6_Translation");
const std::string k_ManualGeometryName("6_6_Manual");
const std::string k_PrecomputedGeometryName("6_6_Precomputed");

const std::string k_InputGeometryName66("InputData");
const std::string k_RotationGeometryName66("6_6_Rotation");
const std::string k_ScaleGeometryName66("6_6_Scale");
const std::string k_TranslationGeometryName66("6_6_Translation");
Expand Down
4 changes: 2 additions & 2 deletions src/Plugins/ITKImageProcessing/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -307,8 +307,8 @@ if(EXISTS "${DREAM3D_DATA_DIR}" AND COMPLEX_DOWNLOAD_TEST_FILES)
endif()

download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR}
ARCHIVE_NAME ITKMhaFileReaderTest.tar.gz
SHA512 7b44c28dad8dc70ad79f9dc6247ab2a17e3c0e08cf0400e361acaea965b0881eaa3815af21c1ec9bdba19f70c0bdd67b7c4382e527c844a20da39d6a229dc5fa)
ARCHIVE_NAME ITKMhaFileReaderTest_rev2.tar.gz
SHA512 82592ffb6904389da1475e9c57f0be724d29db22ba9c942fd02d08ce3f8c73dc39a245503e584e56ca4ea7554d63cf1db5fb46e6e426768020f1ca0542822d0a)

download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR}
ARCHIVE_NAME Porosity_Image.tar.gz
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ using namespace complex;

TEST_CASE("ITKImageProcessing::ITKMhaFileReader: Read 2D & 3D Image Data", "[ITKImageProcessing][ITKMhaFileReader]")
{
const complex::UnitTest::TestFileSentinel testDataSentinel(complex::unit_test::k_CMakeExecutable, complex::unit_test::k_TestFilesDir, "ITKMhaFileReaderTest.tar.gz", "ITKMhaFileReaderTest");
const complex::UnitTest::TestFileSentinel testDataSentinel(complex::unit_test::k_CMakeExecutable, complex::unit_test::k_TestFilesDir, "ITKMhaFileReaderTest_rev2.tar.gz", "ITKMhaFileReaderTest");

// Load plugins (this is needed because ITKMhaFileReader needs access to the ComplexCore plugin)
const std::shared_ptr<UnitTest::make_shared_enabler> app = std::make_shared<UnitTest::make_shared_enabler>();
Expand Down
11 changes: 3 additions & 8 deletions src/complex/Utilities/ImageRotationUtilities.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ float DetermineSpacing(const FloatVec3& spacing, const Eigen::Vector3f& axisNew)

const std::array<float, 3> axes = {xAngle, yAngle, zAngle};

const std::array<float, 3>::const_iterator maxElementIter = std::max_element(axes.cbegin(), axes.cend());
const std::array<float, 3>::const_iterator maxElementIterPtr = std::max_element(axes.cbegin(), axes.cend());

const size_t index = std::distance(axes.cbegin(), maxElementIter);
const size_t index = std::distance(axes.cbegin(), maxElementIterPtr);

return spacing[index];
}
Expand Down Expand Up @@ -218,12 +218,7 @@ ImageRotationUtilities::Matrix4fR GenerateRotationTransformationMatrix(const Vec
//------------------------------------------------------------------------------
ImageRotationUtilities::Matrix4fR GenerateTranslationTransformationMatrix(const VectorFloat32Parameter::ValueType& pTranslationValue)
{
ImageRotationUtilities::Matrix4fR transformationMatrix;
transformationMatrix.fill(0.0F);
transformationMatrix(0, 0) = 1.0f;
transformationMatrix(1, 1) = 1.0f;
transformationMatrix(2, 2) = 1.0f;
transformationMatrix(3, 3) = 1.0f;
ImageRotationUtilities::Matrix4fR transformationMatrix = ImageRotationUtilities::Matrix4fR::Identity();
transformationMatrix(0, 3) = pTranslationValue[0];
transformationMatrix(1, 3) = pTranslationValue[1];
transformationMatrix(2, 3) = pTranslationValue[2];
Expand Down

0 comments on commit 6874864

Please sign in to comment.