Skip to content

Commit

Permalink
ENH: Add interaction handle visualization for linear transform nodes (#…
Browse files Browse the repository at this point in the history
…7562)

This commit adds a visualization option for displaying/interacting
with linear transforms in both 2D and 3D views using separate
rotation/translation/scaling handles.

This method of interaction replaces the existing box widget that is used
currently to move linear transforms in 3D views only. Users now also have
the option to create/toggle display of a parent transform interaction
widget through the subject hierarchy by right-clicking on the display
column of any transformable node.

This new widget is based on and expands the functionality of the previous
Markups interaction pipeline. The base classes vtkMRMLInteractionWidget
and vtkMRMLInteractionWidgetRepresentation implement most functionality,
and can be overridden to display specialized use cases. Interaction widgets
for both Markups and Transforms are derived from these classes.

Co-authored-by: Andras Lasso <lasso@queensu.ca>
Co-authored-by: Jean-Christophe Fillion-Robin <jchris.fillionr@kitware.com>
  • Loading branch information
3 people committed Feb 2, 2024
1 parent 43d1b38 commit 4efda83
Show file tree
Hide file tree
Showing 79 changed files with 8,674 additions and 5,077 deletions.
6 changes: 4 additions & 2 deletions Applications/SlicerApp/Testing/Python/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,8 @@ if(Slicer_USE_QtTesting AND Slicer_USE_PYTHONQT)
slicer_add_python_unittest(SCRIPT RSNAVisTutorial.py)
slicer_add_python_unittest(SCRIPT RSNAQuantTutorial.py)
slicer_add_python_unittest(SCRIPT SlicerOrientationSelectorTest.py)
slicer_add_python_unittest(SCRIPT SlicerTransformInteractionTest1.py)
# Currently needs to be updated for new widget. PR-7562: https://github.com/Slicer/Slicer/pull/7562
#slicer_add_python_unittest(SCRIPT SlicerTransformInteractionTest1.py)
slicer_add_python_unittest(SCRIPT UtilTest.py)
slicer_add_python_unittest(SCRIPT ViewControllersSliceInterpolationBug1926.py)
slicer_add_python_unittest(SCRIPT RSNA2012ProstateDemo.py)
Expand All @@ -493,7 +494,8 @@ if(Slicer_USE_QtTesting AND Slicer_USE_PYTHONQT)
RSNAVisTutorial.py
RSNAQuantTutorial.py
SlicerOrientationSelectorTest.py
SlicerTransformInteractionTest1.py
# Currently needs to be updated for new widget. PR-7562: https://github.com/Slicer/Slicer/pull/7562
#SlicerTransformInteractionTest1.py
ViewControllersSliceInterpolationBug1926.py
RSNA2012ProstateDemo.py
JRC2013Vis.py
Expand Down
39 changes: 29 additions & 10 deletions Libs/MRML/Core/vtkITKTransformConverter.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ class vtkITKTransformConverter
static void RegisterInverseTransformTypes();

template<typename T>
static vtkAbstractTransform* CreateVTKTransformFromITK(vtkObject* loggerObject, typename itk::TransformBaseTemplate<T>::Pointer transformItk);
static vtkAbstractTransform* CreateVTKTransformFromITK(vtkObject* loggerObject, typename itk::TransformBaseTemplate<T>::Pointer transformItk,
double center_RAS[3]=nullptr);

///
/// Create an ITK transform from a VTK transform.
Expand All @@ -76,7 +77,7 @@ class vtkITKTransformConverter
/// Initialization takes a long time for kernel transforms with many points,
/// If a transform is created only to write it to file, initialization can be turned off to improve performance.
static itk::Object::Pointer CreateITKTransformFromVTK(vtkObject* loggerObject, vtkAbstractTransform* transformVtk,
itk::Object::Pointer& secondaryTransformItk, int preferITKv3CompatibleTransforms, bool initialize = true);
itk::Object::Pointer& secondaryTransformItk, int preferITKv3CompatibleTransforms, bool initialize = true, double center_RAS[3] = nullptr);

template <typename T> static bool SetVTKBSplineFromITKv3Generic(vtkObject* loggerObject, vtkOrientedBSplineTransform* bsplineVtk,
typename itk::TransformBaseTemplate<T>::Pointer warpTransformItk, typename itk::TransformBaseTemplate<T>::Pointer bulkTransformItk);
Expand All @@ -91,8 +92,9 @@ class vtkITKTransformConverter

template<typename T>
static bool SetVTKLinearTransformFromITK(vtkObject* loggerObject, vtkMatrix4x4* transformVtk_RAS,
typename itk::TransformBaseTemplate<T>::Pointer transformItk_LPS);
static bool SetITKLinearTransformFromVTK(vtkObject* loggerObject, itk::Object::Pointer& transformItk_LPS, vtkMatrix4x4* transformVtk_RAS);
typename itk::TransformBaseTemplate<T>::Pointer transformItk_LPS, double center_RAS[3]=nullptr);
static bool SetITKLinearTransformFromVTK(vtkObject* loggerObject, itk::Object::Pointer& transformItk_LPS, vtkMatrix4x4* transformVtk_RAS,
double center_RAS[3]=nullptr);

template<typename T>
static bool SetVTKOrientedGridTransformFromITK(vtkObject* loggerObject, vtkOrientedGridTransform* transformVtk_RAS,
Expand Down Expand Up @@ -155,7 +157,8 @@ template<typename T>
bool vtkITKTransformConverter::SetVTKLinearTransformFromITK(
vtkObject* /*loggerObject*/,
vtkMatrix4x4* transformVtk_RAS,
typename itk::TransformBaseTemplate<T>::Pointer transformItk_LPS)
typename itk::TransformBaseTemplate<T>::Pointer transformItk_LPS,
double center_RAS[3] /*=nullptr*/)
{
static const unsigned int D = VTKDimension;
typedef itk::MatrixOffsetTransformBase<T,D,D> LinearTransformType;
Expand Down Expand Up @@ -210,6 +213,14 @@ bool vtkITKTransformConverter::SetVTKLinearTransformFromITK(
}
transformVtk_LPS->SetElement(i, D, dlt->GetOffset()[i]);
}

if (center_RAS)
{
auto center_LPS = dlt->GetCenter();
center_RAS[0] = -center_LPS[0];
center_RAS[1] = -center_LPS[1];
center_RAS[2] = center_LPS[2];
}
}

// Identity transform of doubles or floats, dimension 3
Expand Down Expand Up @@ -260,7 +271,8 @@ bool vtkITKTransformConverter::SetVTKLinearTransformFromITK(
}

//----------------------------------------------------------------------------
bool vtkITKTransformConverter::SetITKLinearTransformFromVTK(vtkObject* loggerObject, itk::Object::Pointer& transformItk_LPS, vtkMatrix4x4* transformVtk_RAS)
bool vtkITKTransformConverter::SetITKLinearTransformFromVTK(vtkObject* loggerObject, itk::Object::Pointer& transformItk_LPS, vtkMatrix4x4* transformVtk_RAS,
double center_RAS[3] /*=nullptr*/)
{
typedef itk::AffineTransform<double, VTKDimension> AffineTransformType;

Expand Down Expand Up @@ -302,6 +314,11 @@ bool vtkITKTransformConverter::SetITKLinearTransformFromVTK(vtkObject* loggerObj
AffineTransformType::Pointer affine = AffineTransformType::New();
affine->SetMatrix(itkmat);
affine->SetOffset(itkoffset);
if (center_RAS)
{
double center_LPS[3] = { -center_RAS[0], -center_RAS[1], center_RAS[2] };
affine->SetCenter(center_LPS);
}

transformItk_LPS = affine;
return true;
Expand Down Expand Up @@ -1297,13 +1314,14 @@ bool vtkITKTransformConverter::SetITKThinPlateSplineTransformFromVTK(vtkObject*
template <typename T>
vtkAbstractTransform* vtkITKTransformConverter::CreateVTKTransformFromITK(
vtkObject* loggerObject,
typename itk::TransformBaseTemplate<T>::Pointer transformItk)
typename itk::TransformBaseTemplate<T>::Pointer transformItk,
double center_RAS[3]/*=nullptr*/)
{
bool conversionSuccess = false;

// Linear
vtkNew<vtkMatrix4x4> transformMatrixVtk;
conversionSuccess = SetVTKLinearTransformFromITK<T>(loggerObject, transformMatrixVtk.GetPointer(), transformItk);
conversionSuccess = SetVTKLinearTransformFromITK<T>(loggerObject, transformMatrixVtk.GetPointer(), transformItk, center_RAS);
if (conversionSuccess)
{
vtkNew<vtkTransform> linearTransformVtk;
Expand Down Expand Up @@ -1341,7 +1359,8 @@ vtkAbstractTransform* vtkITKTransformConverter::CreateVTKTransformFromITK(

//----------------------------------------------------------------------------
itk::Object::Pointer vtkITKTransformConverter::CreateITKTransformFromVTK(vtkObject* loggerObject,
vtkAbstractTransform* transformVtk, itk::Object::Pointer& secondaryTransformItk, int preferITKv3CompatibleTransforms, bool initialize /*= true*/)
vtkAbstractTransform* transformVtk, itk::Object::Pointer& secondaryTransformItk, int preferITKv3CompatibleTransforms, bool initialize /*= true*/,
double center_RAS[3] /*=nullptr*/)
{
typedef itk::CompositeTransform< double > CompositeTransformType;

Expand Down Expand Up @@ -1369,7 +1388,7 @@ itk::Object::Pointer vtkITKTransformConverter::CreateITKTransformFromVTK(vtkObje
{
vtkHomogeneousTransform* linearTransformVtk = vtkHomogeneousTransform::SafeDownCast(singleTransformVtk);
vtkMatrix4x4* transformMatrix = linearTransformVtk->GetMatrix();
if (!SetITKLinearTransformFromVTK(loggerObject, primaryTransformItk, transformMatrix))
if (!SetITKLinearTransformFromVTK(loggerObject, primaryTransformItk, transformMatrix, center_RAS))
{
// conversion failed
return nullptr;
Expand Down
4 changes: 4 additions & 0 deletions Libs/MRML/Core/vtkMRMLLinearTransformNode.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ vtkMRMLLinearTransformNode::vtkMRMLLinearTransformNode()
{
vtkNew<vtkMatrix4x4> matrix;
this->SetMatrixTransformToParent(matrix.GetPointer());

this->CenterOfTransformation[0] = 0.0;
this->CenterOfTransformation[1] = 0.0;
this->CenterOfTransformation[2] = 0.0;
}

//----------------------------------------------------------------------------
Expand Down
129 changes: 82 additions & 47 deletions Libs/MRML/Core/vtkMRMLTransformDisplayNode.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,14 @@ vtkMRMLTransformDisplayNode::vtkMRMLTransformDisplayNode()
this->EditorRotationEnabled = true;
this->EditorScalingEnabled = false;

for (int i = 0; i < 4; ++i)
{
this->RotationHandleComponentVisibility[i] = true;
this->ScaleHandleComponentVisibility[i] = true;
this->TranslationHandleComponentVisibility[i] = true;
}
this->RotationHandleComponentVisibility[3] = false;

vtkNew<vtkIntArray> regionModifiedEvents;
regionModifiedEvents->InsertNextValue(vtkCommand::ModifiedEvent);
regionModifiedEvents->InsertNextValue(vtkMRMLTransformableNode::TransformModifiedEvent);
Expand All @@ -114,6 +122,8 @@ void vtkMRMLTransformDisplayNode::WriteXML(ostream& of, int nIndent)
{
Superclass::WriteXML(of, nIndent);

vtkMRMLWriteXMLBeginMacro(of);

of << " VisualizationMode=\""<< ConvertVisualizationModeToString(this->VisualizationMode) << "\"";

of << " GlyphSpacingMm=\""<< this->GlyphSpacingMm << "\"";
Expand Down Expand Up @@ -141,14 +151,23 @@ void vtkMRMLTransformDisplayNode::WriteXML(ostream& of, int nIndent)
of << " EditorTranslationEnabled=\""<< this->EditorTranslationEnabled << "\"";
of << " EditorRotationEnabled=\"" << this->EditorRotationEnabled << "\"";
of << " EditorScalingEnabled=\""<< this->EditorScalingEnabled << "\"";

vtkMRMLWriteXMLFloatMacro(InteractionSizeAbsolute, InteractionSizeAbsolute);
vtkMRMLWriteXMLFloatMacro(InteractionSizeMm, InteractionSizeMm);
vtkMRMLWriteXMLFloatMacro(InteractionScalePercent, InteractionScalePercent);
vtkMRMLWriteXMLVectorMacro(TranslationHandleComponentVisibility, TranslationHandleComponentVisibility, bool, 4);
vtkMRMLWriteXMLVectorMacro(RotationHandleComponentVisibility, RotationHandleComponentVisibility, bool, 4)
vtkMRMLWriteXMLVectorMacro(ScaleHandleComponentVisibility, ScaleHandleComponentVisibility, bool, 4);

vtkMRMLWriteXMLEndMacro();
}


#define READ_FROM_ATT(varName) \
if (!strcmp(attName,#varName)) \
if (!strcmp(xmlReadAttName,#varName)) \
{ \
std::stringstream ss; \
ss << attValue; \
ss << xmlReadAttValue; \
ss >> this->varName; \
continue; \
}
Expand All @@ -157,56 +176,56 @@ void vtkMRMLTransformDisplayNode::WriteXML(ostream& of, int nIndent)
//----------------------------------------------------------------------------
void vtkMRMLTransformDisplayNode::ReadXMLAttributes(const char** atts)
{
int disabledModify = this->StartModify();

MRMLNodeModifyBlocker(this);
Superclass::ReadXMLAttributes(atts);

const char* attName;
const char* attValue;
while (*atts != nullptr)
{
attName = *(atts++);
attValue = *(atts++);
vtkMRMLReadXMLBeginMacro(atts);

if (!strcmp(attName,"VisualizationMode"))
{
this->VisualizationMode = ConvertVisualizationModeFromString(attValue);
continue;
}
READ_FROM_ATT(GlyphSpacingMm);
READ_FROM_ATT(GlyphScalePercent);
READ_FROM_ATT(GlyphDisplayRangeMaxMm);
READ_FROM_ATT(GlyphDisplayRangeMinMm);
if (!strcmp(attName,"GlyphType"))
{
this->GlyphType = ConvertGlyphTypeFromString(attValue);
continue;
}
READ_FROM_ATT(GlyphTipLengthPercent);
READ_FROM_ATT(GlyphDiameterMm);
READ_FROM_ATT(GlyphShaftDiameterPercent);
READ_FROM_ATT(GlyphResolution);
READ_FROM_ATT(GridScalePercent);
READ_FROM_ATT(GridSpacingMm);
READ_FROM_ATT(GridLineDiameterMm);
READ_FROM_ATT(GridResolutionMm);
READ_FROM_ATT(GridShowNonWarped);
READ_FROM_ATT(ContourResolutionMm);
READ_FROM_ATT(ContourOpacity);
if (!strcmp(attName,"ContourLevelsMm"))
{
SetContourLevelsMmFromString(attValue);
continue;
}
READ_FROM_ATT(EditorVisibility);
READ_FROM_ATT(EditorSliceIntersectionVisibility);
READ_FROM_ATT(EditorTranslationEnabled);
READ_FROM_ATT(EditorRotationEnabled);
READ_FROM_ATT(EditorScalingEnabled);
if (!strcmp(xmlReadAttName,"VisualizationMode"))
{
this->VisualizationMode = ConvertVisualizationModeFromString(xmlReadAttValue);
continue;
}
READ_FROM_ATT(GlyphSpacingMm);
READ_FROM_ATT(GlyphScalePercent);
READ_FROM_ATT(GlyphDisplayRangeMaxMm);
READ_FROM_ATT(GlyphDisplayRangeMinMm);
if (!strcmp(xmlReadAttName,"GlyphType"))
{
this->GlyphType = ConvertGlyphTypeFromString(xmlReadAttValue);
continue;
}
READ_FROM_ATT(GlyphTipLengthPercent);
READ_FROM_ATT(GlyphDiameterMm);
READ_FROM_ATT(GlyphShaftDiameterPercent);
READ_FROM_ATT(GlyphResolution);
READ_FROM_ATT(GridScalePercent);
READ_FROM_ATT(GridSpacingMm);
READ_FROM_ATT(GridLineDiameterMm);
READ_FROM_ATT(GridResolutionMm);
READ_FROM_ATT(GridShowNonWarped);
READ_FROM_ATT(ContourResolutionMm);
READ_FROM_ATT(ContourOpacity);
if (!strcmp(xmlReadAttName,"ContourLevelsMm"))
{
SetContourLevelsMmFromString(xmlReadAttValue);
continue;
}
READ_FROM_ATT(EditorVisibility);
READ_FROM_ATT(EditorSliceIntersectionVisibility);
READ_FROM_ATT(EditorTranslationEnabled);
READ_FROM_ATT(EditorRotationEnabled);
READ_FROM_ATT(EditorScalingEnabled);

this->Modified();
this->EndModify(disabledModify);
vtkMRMLReadXMLFloatMacro(InteractionSizeAbsolute, InteractionSizeAbsolute);
vtkMRMLReadXMLFloatMacro(InteractionSizeMm, InteractionSizeMm);
vtkMRMLReadXMLFloatMacro(InteractionScalePercent, InteractionScalePercent);

vtkMRMLReadXMLVectorMacro(RotationHandleComponentVisibility, RotationHandleComponentVisibility, bool, 4);
vtkMRMLReadXMLVectorMacro(ScaleHandleComponentVisibility, ScaleHandleComponentVisibility, bool, 4);
vtkMRMLReadXMLVectorMacro(TranslationHandleComponentVisibility, TranslationHandleComponentVisibility, bool, 4);

vtkMRMLReadXMLEndMacro();
}


Expand All @@ -221,6 +240,8 @@ void vtkMRMLTransformDisplayNode::CopyContent(vtkMRMLNode* anode, bool deepCopy/
return;
}

vtkMRMLCopyBeginMacro(anode);

this->VisualizationMode = node->VisualizationMode;

this->GlyphSpacingMm = node->GlyphSpacingMm;
Expand Down Expand Up @@ -248,13 +269,21 @@ void vtkMRMLTransformDisplayNode::CopyContent(vtkMRMLNode* anode, bool deepCopy/
this->EditorTranslationEnabled = node->EditorTranslationEnabled;
this->EditorRotationEnabled = node->EditorRotationEnabled;
this->EditorScalingEnabled = node->EditorScalingEnabled;

vtkMRMLCopyVectorMacro(RotationHandleComponentVisibility, bool, 4);
vtkMRMLCopyVectorMacro(ScaleHandleComponentVisibility, bool, 4);
vtkMRMLCopyVectorMacro(TranslationHandleComponentVisibility, bool, 4);

vtkMRMLCopyEndMacro();
}

//----------------------------------------------------------------------------
void vtkMRMLTransformDisplayNode::PrintSelf(ostream& os, vtkIndent indent)
{
Superclass::PrintSelf(os,indent);

vtkMRMLPrintBeginMacro(os, indent);

os << indent << "VisualizationMode = "<< ConvertVisualizationModeToString(this->VisualizationMode) << "\n";
os << indent << "GlyphScalePercent = "<< this->GlyphScalePercent << "\n";
os << indent << "GlyphDisplayRangeMaxMm = "<< this->GlyphDisplayRangeMaxMm << "\n";
Expand All @@ -280,6 +309,12 @@ void vtkMRMLTransformDisplayNode::PrintSelf(ostream& os, vtkIndent indent)
os << indent << " EditorTranslationEnabled=\""<< this->EditorTranslationEnabled << "\n";
os << indent << " EditorRotationEnabled=\"" << this->EditorRotationEnabled << "\n";
os << indent << " EditorScalingEnabled=\""<< this->EditorScalingEnabled << "\n";

vtkMRMLPrintVectorMacro(RotationHandleComponentVisibility, bool, 4);
vtkMRMLPrintVectorMacro(ScaleHandleComponentVisibility, bool, 4);
vtkMRMLPrintVectorMacro(TranslationHandleComponentVisibility, bool, 4);

vtkMRMLPrintEndMacro();
}

//---------------------------------------------------------------------------
Expand Down
44 changes: 44 additions & 0 deletions Libs/MRML/Core/vtkMRMLTransformDisplayNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,40 @@ class VTK_MRML_EXPORT vtkMRMLTransformDisplayNode : public vtkMRMLDisplayNode
vtkColorTransferFunction* GetColorMap();
void SetColorMap(vtkColorTransferFunction* newColorMap);

/// Absolute size of the intreaction handle widget in mm.
vtkSetMacro(InteractionSizeMm, double);
vtkGetMacro(InteractionSizeMm, double);

/// Relative size of the interaction handle widget as a percent of the view size.
vtkSetMacro(InteractionScalePercent, double);
vtkGetMacro(InteractionScalePercent, double);

/// If true, uses the absolute size for the interaction handle, otherwise uses the relative size.
vtkSetMacro(InteractionSizeAbsolute, bool);
vtkGetMacro(InteractionSizeAbsolute, bool);
vtkBooleanMacro(InteractionSizeAbsolute, bool);

/// The type of the active interaction handle.
vtkSetMacro(ActiveInteractionType, int);
vtkGetMacro(ActiveInteractionType, int);

/// The index of the active interaction handle.
vtkSetMacro(ActiveInteractionIndex, int);
vtkGetMacro(ActiveInteractionIndex, int);

//@{
/// Get/Set the visibility of the individual handle axes
/// The order of the vector is: [X, Y, Z, ViewPlane]
/// "ViewPlane" scale/translation allows transformations to take place along the active view plane.
/// (ex. center translation point and ROI corner scale handles.
vtkSetVector4Macro(RotationHandleComponentVisibility, bool);
vtkGetVector4Macro(RotationHandleComponentVisibility, bool);
vtkSetVector4Macro(ScaleHandleComponentVisibility, bool);
vtkGetVector4Macro(ScaleHandleComponentVisibility, bool);
vtkSetVector4Macro(TranslationHandleComponentVisibility, bool);
vtkGetVector4Macro(TranslationHandleComponentVisibility, bool);
//@}

protected:

static std::vector<double> StringToDoubleVector(const char* sourceStr);
Expand Down Expand Up @@ -241,6 +275,16 @@ class VTK_MRML_EXPORT vtkMRMLTransformDisplayNode : public vtkMRMLDisplayNode
bool EditorRotationEnabled;
bool EditorScalingEnabled;

int ActiveInteractionType{-1};
int ActiveInteractionIndex{-1};
bool InteractionSizeAbsolute{false};
double InteractionSizeMm{5.0};
double InteractionScalePercent{15.0};

bool RotationHandleComponentVisibility[4];
bool ScaleHandleComponentVisibility[4];
bool TranslationHandleComponentVisibility[4];

protected:
vtkMRMLTransformDisplayNode ( );
~vtkMRMLTransformDisplayNode ( ) override;
Expand Down

0 comments on commit 4efda83

Please sign in to comment.