Skip to content

Instantly share code, notes, and snippets.

@pieper
Created March 26, 2026 20:49
Show Gist options
  • Select an option

  • Save pieper/703d37355796cd876d84f866d10cdd2f to your computer and use it in GitHub Desktop.

Select an option

Save pieper/703d37355796cd876d84f866d10cdd2f to your computer and use it in GitHub Desktop.

Let me fetch the forum post to understand Mike's question.

Web Fetchhttps://discourse.slicer.org/t/bug-in-importlabelmaptosegmentationnode-if-multiple-parent-transforms/46524

Fetched from https://discourse.slicer.org/t/bug-in-importlabelmaptosegmentationnode-if-multiple-parent-transforms/46524

Let me look more carefully at the forum post to understand the details, and also look at the relevant source code in the Slicer repositories to understand the bug better.

Web Fetchhttps://discourse.slicer.org/t/bug-in-importlabelmaptosegmentationnode-if-multiple-parent-transforms/46524

Fetched from https://discourse.slicer.org/t/bug-in-importlabelmaptosegmentationnode-if-multiple-parent-transforms/46524

Agent:Search Slicer segmentations logic for transform handling

IN

Search the Slicer codebase for the function GetTransformBetweenRepresentationAndSegmentation in vtkSlicerSegmentationsModuleLogic.cxx. Look in /Users/pieper/slicer/latest/ directory tree. Also look for how ImportLabelmapToSegmentationNode handles parent transforms around line 1459. Read the relevant code sections and explain what the transform handling logic does and where a bug with multiple parent transforms might exist.

Bash

IN

find /Users/pieper/slicer/latest -name "vtkSlicerSegmentationsModuleLogic.cxx" -type f

OUT

/Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx /Users/pieper/slicer/latest/SlicerGitSVNArchive/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx

Grep "ImportLabelmapToSegmentationNode" (in /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic)

3 lines of output

Read vtkSlicerSegmentationsModuleLogic.cxx

Bash

IN

grep -n "GetTransformBetweenRepresentationAndSegmentation|ImportLabelmapToSegmentationNode" /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx | head -20

OUT

548: vtkSlicerSegmentationsModuleLogic::GetTransformBetweenRepresentationAndSegmentation(labelmapVolumeNode, segmentationNode, labelmapToSegmentationTransform); 617: vtkSlicerSegmentationsModuleLogic::GetTransformBetweenRepresentationAndSegmentation(modelNode, segmentationNode, modelToSegmentationTransform); 1410:bool vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode(vtkMRMLLabelMapVolumeNode* labelmapNode, 1418: segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Invalid segmentation node"); 1424: segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Invalid labelmap volume node"); 1463: vtkSlicerSegmentationsModuleLogic::GetTransformBetweenRepresentationAndSegmentation(labelmapNode, segmentationNode, labelmapToSegmentationTransform); 1518: "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", 1524: "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", 1529: segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Failed to cast image to integer type."); 1538: segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Failed to cast image to a valid integer type"); 1548: segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Failed to add segment to segmentation"); 1557:bool vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode(vtkOrientedImageData* labelmapImage, 1566: segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Invalid segmentation node"); 1571: vtkErrorToMessageCollectionWithObjectMacro(segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Invalid labelmap image"); 1583: "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", 1589: "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", 1594: segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Failed to cast image to integer type."); 1603: segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Failed to cast image to a valid integer type"); 1655: segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Failed to add segment to segmentation"); 1664:bool vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode(vtkMRMLLabelMapVolumeNode* labelmapNode,

Bash

IN

grep -n "^bool vtkSlicerSegmentationsModuleLogic::GetTransformBetweenRepresentationAndSegmentation|^void vtkSlicerSegmentationsModuleLogic::GetTransformBetweenRepresentationAndSegmentation" /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx

OUT

1935:bool vtkSlicerSegmentationsModuleLogic::GetTransformBetweenRepresentationAndSegmentation(vtkMRMLTransformableNode* representationNode,

Read vtkSlicerSegmentationsModuleLogic.cxx (lines 1411-1510)

Bash

IN

find /Users/pieper/slicer/latest -name ".cxx" -o -name ".h" | xargs grep -l "GetTransformBetweenNodes" | head -5

OUT

/Users/pieper/slicer/latest/Slicer/Libs/MRML/Core/vtkMRMLMarkupsROINode.cxx /Users/pieper/slicer/latest/Slicer/Libs/MRML/Core/vtkMRMLTransformableNode.h /Users/pieper/slicer/latest/Slicer/Libs/MRML/Core/vtkMRMLMarkupsNode.cxx /Users/pieper/slicer/latest/Slicer/Libs/MRML/Core/vtkMRMLVolumeNode.cxx /Users/pieper/slicer/latest/Slicer/Libs/MRML/Core/vtkMRMLTransformNode.cxx xargs: grep: terminated with signal 13; aborting

Bash

IN

grep -n "GetTransformBetweenNodes" /Users/pieper/slicer/latest/Slicer/Libs/MRML/Core/vtkMRMLTransformNode.cxx | head -5

OUT

443: vtkMRMLTransformNode::GetTransformBetweenNodes(this, nullptr, transformToWorld); 454: vtkMRMLTransformNode::GetTransformBetweenNodes(nullptr, this, transformFromWorld); 506: vtkMRMLTransformNode::GetTransformBetweenNodes(this, node, transformToNode); 512: vtkMRMLTransformNode::GetTransformBetweenNodes(node, this, transformFromNode); 516:void vtkMRMLTransformNode::GetTransformBetweenNodes(vtkMRMLTransformNode* sourceNode, vtkMRMLTransformNode* targetNode, vtkGeneralTransform* transformSourceToTarget)

Read vtkMRMLTransformNode.cxx (lines 517-666)

Bash

IN

grep -A 30 "^int vtkMRMLTransformableNode::GetNumberOfParentTransformNodes" /Users/pieper/slicer/latest/Slicer/Libs/MRML/Core/vtkMRMLTransformableNode.cxx

OUT

(Bash completed with no output)

Bash

IN

grep -n "GetParentTransformNode|GetNumberOfParentTransformNodes|ParentTransformNode" /Users/pieper/slicer/latest/Slicer/Libs/MRML/Core/vtkMRMLTransformableNode.h | head -20

OUT

64: vtkMRMLTransformNode* GetParentTransformNode();

Bash

IN

grep -rn "SetParentTransformNodeID|AddParentTransformNodeID" /Users/pieper/slicer/latest/Slicer/Libs/MRML/Core/vtkMRMLTransformableNode.h

OUT

(Bash completed with no output)

Read vtkMRMLTransformableNode.h

Bash

IN

grep -B 5 -A 15 "GetFirstCommonParent" /Users/pieper/slicer/latest/Slicer/Libs/MRML/Core/vtkMRMLTransformNode.cxx | head -50

OUT

} return 1; } else { vtkMRMLTransformNode* firstCommonParentNode = this->GetFirstCommonParent(targetNode); if (this->IsTransformToNodeLinear(firstCommonParentNode) && // targetNode->IsTransformToNodeLinear(firstCommonParentNode)) { return 1; } else { return 0; } } } //---------------------------------------------------------------------------- void vtkMRMLTransformNode::GetTransformToNode(vtkMRMLTransformNode* node, vtkGeneralTransform* transformToNode) { -- // need to invert to get sourceNode->targetNode transformSourceToTarget->Inverse(); } else { vtkMRMLTransformNode* firstCommonParentNode = sourceNode->GetFirstCommonParent(targetNode); sourceNode->GetTransformToNode(firstCommonParentNode, transformSourceToTarget); vtkNew transformFromCommonParentNode; targetNode->GetTransformToNode(firstCommonParentNode, transformFromCommonParentNode.GetPointer()); transformFromCommonParentNode->Inverse(); transformSourceToTarget->Concatenate(transformFromCommonParentNode.GetPointer()); } } //---------------------------------------------------------------------------- int vtkMRMLTransformNode::IsTransformNodeMyParent(vtkMRMLTransformNode* node) { if (node == nullptr) -- } return node->IsTransformNodeMyParent(this); } //---------------------------------------------------------------------------- vtkMRMLTransformNode* vtkMRMLTransformNode::GetFirstCommonParent(vtkMRMLTransformNode* targetNode)

Bash

IN

grep -rn "multiple.*parent|parent.*transform" /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx | head -20

OUT

/Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:414: // Apply parent transform of the volume node if any /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:542: // Apply parent transforms if any /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:552: // Get parent transform which is the representation to world transform in absence of a segmentation node /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:611: // Apply parent transforms if any /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:621: // Get parent transform which is the representation to world transform in absence of a segmentation node /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:793: // Set segmentation's parent transform to exported node /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:835: // Set segmentation's parent transform to exported node /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:1187: // Use reference volume's parent transform if available, otherwise put under the same transform as segmentation node /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:1459: // Apply parent transforms if any /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:1782: // Apply parent transforms if any /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:1890: vtkMRMLTransformNode* parentTransformNode = transformableNode->GetParentTransformNode(); /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:1893: // There is no parent transform for segmentation, nothing to apply /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:1915: vtkMRMLTransformNode* parentTransformNode = transformableNode->GetParentTransformNode(); /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:1918: // There is no parent transform for segmentation, nothing to apply /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:2000: // Apply parent transformation nodes if necessary /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:2010: "vtkSlicerSegmentationsModuleLogic::GetSegmentRepresentation: Failed to apply parent transform of " /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:2020: "vtkSlicerSegmentationsModuleLogic::GetSegmentRepresentation: Failed to apply parent transform of " /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:2028: "vtkSlicerSegmentationsModuleLogic::GetSegmentRepresentation: Failed to apply parent transform of "

Read vtkSlicerSegmentationsModuleLogic.cxx (lines 1450-1479)

Read vtkMRMLTransformNode.cxx (lines 633-662)

Read vtkMRMLTransformNode.cxx (lines 587-606)

Bash

IN

grep -rn "bug|BUG|TODO|FIXME|issue|multiple" /Users/pieper/slicer/latest/Slicer/Libs/MRML/Core/vtkMRMLTransformNode.cxx | grep -i "parent|transform" | head -20

OUT

/Users/pieper/slicer/latest/Slicer/Libs/MRML/Core/vtkMRMLTransformNode.cxx:534: // See issue Slicer/Slicer#6355. /Users/pieper/slicer/latest/Slicer/Libs/MRML/Core/vtkMRMLTransformNode.cxx:554: // See issue Slicer/Slicer#6355. /Users/pieper/slicer/latest/Slicer/Libs/MRML/Core/vtkMRMLTransformNode.cxx:576: // See issue Slicer/Slicer#6355. /Users/pieper/slicer/latest/Slicer/Libs/MRML/Core/vtkMRMLTransformNode.cxx:1749: vtkDebugMacro(<< " setting CenterOfTransformation to (" << x << "," << y << "," << z << ")");

Bash

IN

grep -B 5 -A 20 "GetTransformBetweenNodes" /Users/pieper/slicer/latest/Slicer/Libs/MRML/Core/vtkMRMLTransformNode.h

OUT

int IsTransformToNodeLinear(vtkMRMLTransformNode* node); /// /// Get concatenated transforms to world. /// The method may change the PreMultiply/PostMultiply flag of the transform. /// \sa GetTransformBetweenNodes void GetTransformToWorld(vtkGeneralTransform* transformToWorld); /// /// Get concatenated transforms from world. /// The method may change the PreMultiply/PostMultiply flag of the transform. /// \sa GetTransformBetweenNodes void GetTransformFromWorld(vtkGeneralTransform* transformFromWorld); /// /// Get concatenated transforms to the specified node. /// The method may change the PreMultiply/PostMultiply flag of the transform. /// \sa GetTransformBetweenNodes void GetTransformToNode(vtkMRMLTransformNode* node, vtkGeneralTransform* transformToNode); /// /// Get concatenated transforms from the specified node. /// The method may change the PreMultiply/PostMultiply flag of the transform. /// \sa GetTransformBetweenNodes void GetTransformFromNode(vtkMRMLTransformNode* node, vtkGeneralTransform* transformFromNode); /// /// Get concatenated transforms from source to target node /// Source and target nodes are allowed to be nullptr, which means that transform is the world transform. /// The method may change the PreMultiply/PostMultiply flag of the transform. static void GetTransformBetweenNodes(vtkMRMLTransformNode* sourceNode, vtkMRMLTransformNode* targetNode, vtkGeneralTransform* transformSourceToTarget); /// /// Get concatenated transforms to world. /// Returns 0 if the transform is not linear (cannot be described by a matrix). /// \sa GetMatrixTransformBetweenNodes virtual int GetMatrixTransformToWorld(vtkMatrix4x4* transformToWorld); /// /// Get concatenated transforms from world. /// Returns 0 if the transform is not linear (cannot be described by a matrix). /// \sa GetMatrixTransformBetweenNodes virtual int GetMatrixTransformFromWorld(vtkMatrix4x4* transformFromWorld); /// /// Get concatenated transforms to the specified node. /// Returns 0 if the transform is not linear (cannot be described by a matrix). /// \sa GetMatrixTransformBetweenNodes virtual int GetMatrixTransformToNode(vtkMRMLTransformNode* node, vtkMatrix4x4* transformToNode); ///

Read vtkMRMLTransformNode.cxx (lines 540-599)

The agent found the relevant code. Now let me look at the actual code more carefully to understand the bug. The agent mentions GetTransformBetweenNodes - let me check what that function does to understand if the issue is there or in GetTransformBetweenRepresentationAndSegmentation.

The key thing is: Mike says the bug is that "parent transforms are being applied in the wrong order" when there are MULTIPLE PARENT TRANSFORMS in a hierarchy (transform A has parent transform B - so the labelmap has a nested transform hierarchy). The MRML scene allows transform hierarchies where a transform node itself has a parent transform.

So: labelmapNode -> transformA -> transformB

I see that GetTransformBetweenRepresentationAndSegmentation calls GetTransformBetweenNodes with the parent transforms from both nodes, which should theoretically handle the hierarchy correctly by traversing up the transform tree. But I need to examine the actual implementation to see where the ordering might be going wrong.

Bash Find relevant function lines in segmentations logic

IN

grep -n "GetTransformBetweenRepresentationAndSegmentation|GetTransformBetweenNodes|ImportLabelmapToSegmentationNode" /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx | head -50

OUT

418: vtkMRMLTransformNode::GetTransformBetweenNodes(volumeNode->GetParentTransformNode(), outputParentTransformNode, nodeToOutputTransform); 548: vtkSlicerSegmentationsModuleLogic::GetTransformBetweenRepresentationAndSegmentation(labelmapVolumeNode, segmentationNode, labelmapToSegmentationTransform); 617: vtkSlicerSegmentationsModuleLogic::GetTransformBetweenRepresentationAndSegmentation(modelNode, segmentationNode, modelToSegmentationTransform); 1011: vtkMRMLTransformNode::GetTransformBetweenNodes( 1410:bool vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode(vtkMRMLLabelMapVolumeNode* labelmapNode, 1418: segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Invalid segmentation node"); 1424: segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Invalid labelmap volume node"); 1463: vtkSlicerSegmentationsModuleLogic::GetTransformBetweenRepresentationAndSegmentation(labelmapNode, segmentationNode, labelmapToSegmentationTransform); 1518: "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", 1524: "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", 1529: segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Failed to cast image to integer type."); 1538: segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Failed to cast image to a valid integer type"); 1548: segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Failed to add segment to segmentation"); 1557:bool vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode(vtkOrientedImageData* labelmapImage, 1566: segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Invalid segmentation node"); 1571: vtkErrorToMessageCollectionWithObjectMacro(segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Invalid labelmap image"); 1583: "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", 1589: "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", 1594: segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Failed to cast image to integer type."); 1603: segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Failed to cast image to a valid integer type"); 1655: segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Failed to add segment to segmentation"); 1664:bool vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode(vtkMRMLLabelMapVolumeNode* labelmapNode, 1671: vtkErrorToMessageCollectionWithObjectMacro(segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Invalid segmentation node"); 1677: segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Invalid labelmap volume node"); 1693: vtkSlicerSegmentationsModuleLogic::GetTransformBetweenRepresentationAndSegmentation(labelmapNode, segmentationNode, labelmapToSegmentationTransform); 1697: return vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode( 1702:bool vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode(vtkOrientedImageData* labelmapImage, 1711: segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Invalid segmentation node"); 1716: vtkErrorToMessageCollectionWithObjectMacro(segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Invalid labelmap volume"); 1722: segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Invalid updatedSegmentIDs"); 1732: "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", 1758: "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", 1764: "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", 1769: segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Failed to cast image to integer type."); 1778: segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Failed to cast image to a valid integer type"); 1804: vtkErrorToMessageCollectionWithObjectMacro(segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode", "Could not add segment"); 1822:bool vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNodeWithTerminology(vtkMRMLLabelMapVolumeNode* labelmapNode, 1831: if (!vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNode(labelmapNode, segmentationNode, insertBeforeSegmentId, userMessages)) 1834: segmentationNode, userMessages, "vtkSlicerSegmentationsModuleLogic::ImportLabelmapToSegmentationNodeWithTerminology", "Invalid labelmap volume"); 1935:bool vtkSlicerSegmentationsModuleLogic::GetTransformBetweenRepresentationAndSegmentation(vtkMRMLTransformableNode* representationNode, 1941: vtkGenericWarningMacro("vtkSlicerSegmentationsModuleLogic::GetTransformBetweenRepresentationAndSegmentation: Invalid inputs"); 1944: vtkMRMLTransformNode::GetTransformBetweenNodes(representationNode->GetParentTransformNode(), segmentationNode->GetParentTransformNode(), representationToSegmentationTransform); 3030: vtkMRMLTransformNode::GetTransformBetweenNodes(segmentationNode->GetParentTransformNode(), referenceVolumeNode->GetParentTransformNode(), segmentationToReferenceTransform);

Read vtkSlicerSegmentationsModuleLogic.cxx (lines 1451-1480)

Read vtkSlicerSegmentationsModuleLogic.cxx (lines 1665-1704)

Read vtkSlicerSegmentationsModuleLogic.cxx (lines 1936-1950)

Now let me look at GetTransformBetweenNodes to understand how it handles the transform hierarchy.

Bash Find GetTransformBetweenNodes in MRML

IN

grep -n "GetTransformBetweenNodes" /Users/pieper/slicer/latest/Slicer/Libs/MRML/Core/vtkMRMLTransformNode.cxx | head -20

OUT

443: vtkMRMLTransformNode::GetTransformBetweenNodes(this, nullptr, transformToWorld); 454: vtkMRMLTransformNode::GetTransformBetweenNodes(nullptr, this, transformFromWorld); 506: vtkMRMLTransformNode::GetTransformBetweenNodes(this, node, transformToNode); 512: vtkMRMLTransformNode::GetTransformBetweenNodes(node, this, transformFromNode); 516:void vtkMRMLTransformNode::GetTransformBetweenNodes(vtkMRMLTransformNode* sourceNode, vtkMRMLTransformNode* targetNode, vtkGeneralTransform* transformSourceToTarget) 555: vtkGenericWarningMacro("vtkMRMLTransformNode::GetTransformBetweenNodes: Loop detected between transform nodes"); 577: vtkGenericWarningMacro("vtkMRMLTransformNode::GetTransformBetweenNodes: Loop detected between transform nodes");

Read vtkMRMLTransformNode.cxx (lines 517-596)

Now I understand the full picture. The GetTransformBetweenNodes function correctly handles transform hierarchies - it traverses from source to common parent to target. The function should handle the case where labelmapNode has a transform with a parent transform (nested transforms).

But wait - GetTransformBetweenRepresentationAndSegmentation calls:

vtkMRMLTransformNode::GetTransformBetweenNodes(representationNode->GetParentTransformNode(), segmentationNode->GetParentTransformNode(), ...)

This passes GetParentTransformNode() - the immediate parent transform of each node.

So if the labelmap has TransformA as parent and TransformA itself has TransformB as parent, the function receives TransformA and nullptr (if segmentation has no parent). When computing the transform between TransformA and world, it should correctly traverse up the hierarchy to include TransformB in the calculation.

Now I'm checking what happens when targetNode is nullptr representing world. The condition checks if sourceNode->IsTransformNodeMyParent(nullptr) — essentially asking whether world is a parent of the source node. Since every transform should have world as its root ancestor, this should return true, which would make the first branch execute and properly compute the full transform chain up to world.

Bash Find transform hierarchy traversal functions

IN

grep -n "IsTransformNodeMyParent|IsTransformNodeMyChild" /Users/pieper/slicer/latest/Slicer/Libs/MRML/Core/vtkMRMLTransformNode.cxx | head -20

OUT

464: if (this->IsTransformNodeMyParent(targetNode)) 476: else if (this->IsTransformNodeMyChild(targetNode)) 539: if (sourceNode != nullptr && sourceNode->IsTransformNodeMyParent(targetNode)) 561: else if (sourceNode == nullptr || sourceNode->IsTransformNodeMyChild(targetNode)) 601:int vtkMRMLTransformNode::IsTransformNodeMyParent(vtkMRMLTransformNode* node) 621:int vtkMRMLTransformNode::IsTransformNodeMyChild(vtkMRMLTransformNode* node) 625: vtkErrorMacro("vtkMRMLTransformNode::IsTransformNodeMyChild failed: input node is invalid"); 628: return node->IsTransformNodeMyParent(this); 642: if (targetNode->IsTransformNodeMyParent(current)) 965: if (sourceNode && sourceNode->IsTransformNodeMyParent(targetNode)) 981: else if (sourceNode == nullptr || sourceNode->IsTransformNodeMyChild(targetNode))

Read vtkMRMLTransformNode.cxx (lines 602-636)

Now I understand the issue. Let me trace through the logic:

Scenario: labelmapNode has parent TransformA, which has parent TransformB. segmentationNode has no parent transform.

GetTransformBetweenRepresentationAndSegmentation calls:

vtkMRMLTransformNode::GetTransformBetweenNodes(
    labelmapNode->GetParentTransformNode(),  // = TransformA
    segmentationNode->GetParentTransformNode(),  // = nullptr
    ...)


In GetTransformBetweenNodes(sourceNode=TransformA, targetNode=nullptr, ...), the check sourceNode->IsTransformNodeMyParent(targetNode) returns true since nullptr is always considered a parent, so it traverses from TransformA up through its parent chain, concatenating each transform to parent along the way.

The loop iterates through TransformA, then TransformB, concatenating their transforms to their respective parents, which should correctly compute the full transform from TransformA all the way up to world. So the function appears to be working as intended for hierarchical transforms. Let me look back at the forum post to understand where the actual bug is occurring.

cpp
vtkSmartPointer<vtkOrientedImageData> labelOrientedImageData = vtkSmartPointer<vtkOrientedImageData>::New();
labelOrientedImageData->vtkImageData::DeepCopy(labelmapNode->GetImageData());
labelOrientedImageData->SetGeometryFromImageToWorldMatrix(labelmapIjkToRasMatrix);

I need to trace where labelmapIjkToRasMatrix comes from and whether it's using GetIJKToRASMatrix, which might not include parent transforms.

Read vtkSlicerSegmentationsModuleLogic.cxx (lines 1411-1460)

Now I see what's happening at line 1430: labelmapNode->GetIJKToRASMatrix(labelmapIjkToRasMatrix). This gets the IJK to RAS (local/node) matrix without considering parent transforms. Then at lines 1459-1465, the parent transforms are applied.

The issue could be: GetTransformBetweenRepresentationAndSegmentation is computing the transform from labelmapNode->GetParentTransformNode() to segmentationNode->GetParentTransformNode(). This is the transform between the two parent transform nodes.

But what we actually need is the full coordinate chain: labelmap IJK converts to labelmap RAS via the stored matrix, then labelmap RAS goes to world space through the complete parent transform hierarchy, and finally world space maps to segmentation RAS through the inverse of the segmentation's full parent transform hierarchy. The problem is that GetTransformBetweenRepresentationAndSegmentation only handles the parent transform nodes themselves, not the complete hierarchical chain needed.

Looking at the actual code, I see that at line 1430 we're getting just the pure IJK-to-RAS matrix without any parent transforms, then at line 1457 we're setting the image's ImageToWorld matrix to only that IJK-to-RAS matrix, which means the parent transforms aren't being factored in at all.

Then TransformOrientedImage is supposed to apply the parent transforms afterward, but I'm wondering if it's actually modifying the geometry correctly or if there's a mismatch in how the transform is being applied to the ImageToWorld matrix. So GetTransformBetweenNodes(TransformA, nullptr) should give me the transform from the labelmap's RAS space all the way through to world coordinates, since that's TransformA's input space. But I'm second-guessing whether GetTransformBetweenNodes actually works that way—I need to verify what coordinate space it's pulling from for the source node.

Looking at how the function traverses from TransformA up to nullptr, it concatenates each transform along the chain, which means it's composing TransformA followed by TransformB. That's exactly what I need to map from labelmap RAS to world, so this approach is correct.

But wait, let me trace through the actual call more carefully. When GetTransformBetweenRepresentationAndSegmentation passes sourceNode as the labelmap's parent transform (TransformA) and targetNode as the segmentation's parent (nullptr), the function should traverse from TransformA up to nullptr and build the composed transform. Since TransformA is indeed a parent of nullptr in the hierarchy, it takes that branch and concatenates the transforms correctly.

So the code is handling the nested transforms properly - it's building the full transform chain from TransformA through TransformB to world. But Mike mentioned this doesn't work with multiple parent transforms, so maybe the issue is in how GetIJKToRASMatrix is being used or whether it's accounting for these parent transforms at all.

Let me look at how GetIJKToRASMatrix works and the TransformOrientedImage function:

Bash Find TransformOrientedImage in vtkOrientedImageDataResample

IN

grep -n "TransformOrientedImage" /Users/pieper/slicer/latest/Slicer/Libs/vtkSegmentationCore/vtkOrientedImageDataResample.cxx | head -10

OUT

894:void vtkOrientedImageDataResample::TransformOrientedImageDataBounds(vtkOrientedImageData* image, vtkAbstractTransform* transform, double transformedBounds[6]) 1224:void vtkOrientedImageDataResample::TransformOrientedImage(vtkOrientedImageData* image, 1262: vtkOrientedImageDataResample::TransformOrientedImageDataBounds(image, transform, transformedBoundsWorld);

Bash Find GetIJKToRASMatrix implementation

IN

grep -rn "GetIJKToRASMatrix" /Users/pieper/slicer/latest/Slicer/Libs/MRML/Core/vtkMRMLVolumeNode.cxx | head -10

OUT

/Users/pieper/slicer/latest/Slicer/Libs/MRML/Core/vtkMRMLVolumeNode.cxx:195: node->GetIJKToRASMatrix(ijkToRasmatrix); /Users/pieper/slicer/latest/Slicer/Libs/MRML/Core/vtkMRMLVolumeNode.cxx:443:void vtkMRMLVolumeNode::GetIJKToRASMatrix(vtkMatrix4x4* mat) /Users/pieper/slicer/latest/Slicer/Libs/MRML/Core/vtkMRMLVolumeNode.cxx:461: this->GetIJKToRASMatrix(mat); /Users/pieper/slicer/latest/Slicer/Libs/MRML/Core/vtkMRMLVolumeNode.cxx:880: this->GetIJKToRASMatrix(ijkToRASMatrix.GetPointer()); /Users/pieper/slicer/latest/Slicer/Libs/MRML/Core/vtkMRMLVolumeNode.cxx:926: this->GetIJKToRASMatrix(ijkToRAS.GetPointer()); /Users/pieper/slicer/latest/Slicer/Libs/MRML/Core/vtkMRMLVolumeNode.cxx:1192: this->GetIJKToRASMatrix(ijkToRasMatrix); /Users/pieper/slicer/latest/Slicer/Libs/MRML/Core/vtkMRMLVolumeNode.cxx:1293: this->GetIJKToRASMatrix(ijkToRasMatrix); /Users/pieper/slicer/latest/Slicer/Libs/MRML/Core/vtkMRMLVolumeNode.cxx:1487: this->GetIJKToRASMatrix(ijkToRAS);

Read vtkMRMLVolumeNode.cxx (lines 444-463)

Read vtkOrientedImageDataResample.cxx (lines 1225-1284)

Now I see something interesting in TransformOrientedImage at lines 1243-1247:

vtkSmartPointer<vtkTransform> imageToTransformedWorldTransform = vtkSmartPointer<vtkTransform>::New();
imageToTransformedWorldTransform->Concatenate(worldToTransformedWorldLinearTransform);
imageToTransformedWorldTransform->Concatenate(imageToWorldMatrix);

This is doing: worldToTransformedWorldLinearTransform first concatenated with imageToWorldMatrix.

I need to understand how VTK's vtkTransform::Concatenate actually works. By default, vtkTransform operates in pre-multiply mode, which means each concatenated transform gets added to the left of the pipeline and executes first. So when I concatenate worldToTransformedWorldLinearTransform followed by imageToWorldMatrix, the actual execution order is reversed from what the code order suggests. I need to figure out the correct order for composing these transforms in VTK's pre-multiply mode. Since transforms apply right to left like matrix multiplication, I need worldToTransformedWorldLinearTransform * imageToWorldMatrix. In pre-multiply mode, each Concatenate call multiplies the new transform on the left, so applying worldToTransformedWorldLinearTransform first gives me the world-to-transformed-world part, then imageToWorldMatrix gives me the full composition I need. the left) In PostMultiply mode: T = T * M (new matrix goes on the right)

When you call transform->TransformPoint(p), the transform is applied as T * p.

So in the code, I'm building up the transformation by concatenating matrices in PreMultiply mode, which means each new matrix gets multiplied on the left. The final result applies worldToTransformedWorldLinearTransform first, then imageToWorldMatrix — but that's backwards for what I need, since I want to go from image space to world space first, then apply the linear transformation.

Actually, let me reconsider what the correct order should be. I need image IJK to transform through imageToWorldMatrix into RAS, then through worldToTransformedWorldLinearTransform into transformed world space. That means the composed transformation should be worldToTransformedWorldLinearTransform * imageToWorldMatrix * p. But the current code concatenates them in the opposite order, giving me the wrong composition.

Wait, I should check how vtkTransform actually works. By default it's in pre-multiply mode, which means Concatenate prepends matrices to the left. So if I start with identity and concatenate worldToTransformedWorldLinearTransform first, then imageToWorldMatrix, I end up with imageToWorldMatrix * worldToTransformedWorldLinearTransform, which applies the transforms in the wrong order—it's backwards from what I need. But we want the OPPOSITE: apply imageToWorldMatrix first, then worldToTransformedWorldLinearTransform.

Hmm, but this is used as imageToTransformedWorldTransform. Then image->SetGeometryFromImageToWorldMatrix(imageToTransformedWorldTransform->GetMatrix()). So imageToWorldMatrix in the new geometry would be imageToWorldMatrix * worldToTransformedWorldLinearTransform.

Actually, I've been overthinking this. Let me look at the documentation for vtkTransform::Concatenate more carefully. In VTK, by default vtkTransform is in pre-multiply mode. In pre-multiply mode, when you call Concatenate(M), the new matrix is multiplied on the LEFT, meaning the last-concatenated transform is applied FIRST when TransformPoint is called.

For matrix multiplication, if you want to apply transform A first, then B: the combined matrix is BA (B on the left, A on the right). In pre-multiply mode, concatenating B then A gives you T = AB, which applies B first, then A.

So in the code: concatenating worldToTransformedWorldLinearTransform first, then imageToWorldMatrix gives imageToWorldMatrix * worldToTransformedWorldLinearTransform, which applies worldToTransformedWorldLinearTransform first, then imageToWorldMatrix — exactly what we need. I need to verify my understanding of VTK's pre-multiply mode by checking the source. In pre-multiply mode, Concatenate(M) applies as T = T * M, meaning the existing transform runs first and the new one second. So tracing through: starting with identity, after concatenating worldToTransformedWorldLinearTransform I get that transform, then concatenating imageToWorldMatrix gives me their product, which applies imageToWorldMatrix first (image to world) followed by worldToTransformedWorldLinearTransform (world to transformed world) — exactly the order we need.

So the linear transform logic is correct. Now I'm wondering if the bug lies elsewhere, maybe in how GetTransformBetweenNodes handles the case where both source and target are non-null and unrelated, or perhaps in the typical scenario where the labelmap has a parent transform that itself has a parent, and we're querying from that intermediate transform to null.

Looking at the actual call in GetTransformBetweenRepresentationAndSegmentation, it's passing the immediate parent of the representation node and the immediate parent of the segmentation node to compute the transform between them. But I need to think through what "TransformA's space" actually means in the MRML hierarchy.

When TransformA has a parent TransformB, GetTransformBetweenNodes(TransformA, nullptr) traverses up the chain applying each transform in sequence—first TransformA-to-TransformB, then TransformB-to-world—which correctly maps from the labelmap's coordinate space to world space. So the computation should be working as intended.

Let me look back at the forum post to see what the actual issue is.

Let me get the full details of the forum post including any code Mike posted:

Web Fetchhttps://discourse.slicer.org/t/bug-in-importlabelmaptosegmentationnode-if-multiple-parent-transforms/46524

Fetched from https://discourse.slicer.org/t/bug-in-importlabelmaptosegmentationnode-if-multiple-parent-transforms/46524

OK! Very interesting. The forum post mentions Steve Pieper (which is the user I'm talking to) and suggested it's "a one-line change." Let me now think about what the actual bug could be.

Let me re-read the code. The key is: in the first overload of ImportLabelmapToSegmentationNode (line 1410-1465):

  1. Line 1429-1430: Gets labelmapIjkToRasMatrix via GetIJKToRASMatrix (pure IJK-to-RAS, no transforms)
  2. Line 1457: Sets labelOrientedImageData's geometry to labelmapIjkToRasMatrix
  3. Line 1460-1465: Applies parent transforms using GetTransformBetweenRepresentationAndSegmentation, which retrieves the transform between the immediate parent nodes of the representation and segmentation.

Now looking at the second overload around line 1664, I see a similar pattern where it extracts the IJK-to-RAS matrix from the labelmap node and applies it to the oriented image data, then checks whether parent transforms differ before computing any additional transforms needed. I'm realizing the issue might actually be in GetTransformBetweenRepresentationAndSegmentation itself—it's computing the transform between the parent transform nodes, which gives us the space transformation between where the labelmap and segmentation nodes sit, but the image data is already set up in world coordinates using the labelmap's matrix, so there's a mismatch in what coordinate system we're actually transforming to. I need to trace through what happens with chained parent transforms more carefully. If the labelmap has a parent transform that itself has a parent transform, then GetTransformBetweenRepresentationAndSegmentation should be computing the cumulative transform from the labelmap's entire transform chain up to world space, which is exactly what we need to pass to TransformOrientedImage. Both the labelmap and segmentation have the same parent transform T1, which itself has T2 as its parent, so GetTransformBetweenNodes(T1, T1) correctly returns identity—they're already in the same coordinate system relative to each other. But I need to reconsider what happens when a labelmap is exported with its own transform and then re-imported, since that might be where the misalignment occurs. The imported labelmap data ends up in T1's coordinate space, which matches where the original segments already exist, so the geometry should align correctly. I'm puzzled about where the actual bug is—it might be lurking in a different code path, or perhaps the IJK-to-RAS matrix is inadvertently picking up parent transforms somewhere. Let me reconsider whether GetIJKToRASMatrix is returning the local matrix without parent transforms applied, or if there's an off-by-one error in how the transform hierarchy is being traversed. The original segments should display the same way whether imported or not since they're both in T1's local space with the same transform hierarchy, so I'm not seeing an obvious bug in the code itself. Let me check if this is a known issue in version 5.10.0 or if there's a specific workaround I should suggest to Mike.

Looking more carefully at GetTransformBetweenRepresentationAndSegmentation — when both the representation and segmentation have the same parent transform node, the function returns identity, which seems correct. But I need to verify whether the labelOrientedImageData geometry is actually in the right coordinate space for this to work properly. I'm checking the condition that compares parent transform nodes between the two inputs—if they share the same parent, the condition evaluates to false and no transform gets applied, which is correct behavior. But I'm going in circles trying to pinpoint where the actual bug is, so let me look at the git history to see if there's a clue about what was fixed.

Steve mentioned it's likely a one-line change, and I'm noticing the GetTransformBetweenRepresentationAndSegmentation function passes the parent transform nodes to GetTransformBetweenNodes. What if the bug is that it should be passing the nodes themselves instead of their parents? The issue is that the representation and segmentation nodes are transformable nodes, not transform nodes, so they can't be directly cast to what GetTransformBetweenNodes expects. I'm wondering if there's a different approach using the nodes' transform-to-world matrices that other parts of Slicer use.

Looking at the same pattern repeated elsewhere in the codebase, I'm thinking the real bug might be in a different code path—specifically when the labelmap node has a parent transform but the segmentation doesn't. In that scenario, passing nullptr as one of the parent transforms could be causing the issue. The code iterates through the transform hierarchy from the source node up to the target, concatenating each transform in sequence, and I'm checking what type of transform object is being used to accumulate these operations. to-world) * p, which applies the world transform first, then the source-to-target transform—that's backwards. Let me reconsider how vtkGeneralTransform actually orders its concatenated transforms when applying them to a point.

What I really need is a transform that takes a point from T1's space directly to world space: first apply T1 to get to T2's space, then apply T2 to reach world. With post-multiply concatenation, each new transform gets applied last in the computation, so appending T1 then T2 should give me exactly that—T2(T1(p))—which is what I want.

So the concatenation logic checks out. The bug must be somewhere else. Steve mentioned it's likely a one-line fix, which suggests he's already spotted the issue. Looking back at the test case, the rotation seems correct but the translation doesn't accumulate properly across the hierarchy—like only part of the transform is being applied. Let me think about what could cause that specific symptom with two transforms.

Retrieving the IJK to RAS matrix...

So the "World" in ImageToWorld actually refers to the node's RAS space, not the full physical world coordinates that would include parent transforms. When TransformOrientedImage applies the labelmap-to-segmentation transform, it's concatenating transforms to update that ImageToWorld matrix, which should map the image geometry through the full transform chain.

Now I'm checking what IsTransformLinear actually does—it extracts the linear component of the transform, but the naming is a bit misleading about what direction it represents.

Bash Find IsTransformLinear definition

IN

grep -n "IsTransformLinear" /Users/pieper/slicer/latest/Slicer/Libs/vtkSegmentationCore/vtkOrientedImageDataResample.cxx | head -10

OUT

987:bool vtkOrientedImageDataResample::IsTransformLinear(vtkAbstractTransform* inputTransform, vtkTransform* outputLinearTransform) 1027: vtkErrorWithObjectMacro(inputTransform, "vtkOrientedImageDataResample::IsTransformLinear: Unsupported input transform with type " << inputTransform->GetClassName()); 1238: if (!alwaysResample && vtkOrientedImageDataResample::IsTransformLinear(transform, worldToTransformedWorldLinearTransform))

Read vtkOrientedImageDataResample.cxx (lines 988-1032)

OK! I think I've found the bug now, and it's in IsTransformLinear interacting with TransformOrientedImage.

When GetTransformBetweenNodes(T1, nullptr) is called and the transform is a vtkGeneralTransform in PostMultiply mode, it concatenates two linear transforms (T1-to-T2 and T2-to-world). The result is a vtkGeneralTransform with 2 concatenated transforms.

Now in TransformOrientedImage, IsTransformLinear is called. It iterates over the concatenated transforms in the vtkGeneralTransform and concatenates them into outputLinearTransform (a vtkTransform in default/pre-multiply mode).

I'm trying to work through how the concatenation order affects the final transform. When outputLinearTransform->Concatenate(currentLinearTransform) is called on a pre-multiply mode transform, the new matrix gets applied first to points, but I need to verify whether the order of operations matches what was originally in the vtkGeneralTransform. in VTK's vtkTransform:

  • Pre-multiply: Concatenate(M) means M is applied FIRST (to the left of the current transform stack). Result: T_new * p = M * T_old * p
  • Post-multiply: Concatenate(M) means M is applied LAST. Result: T_new * p = T_old * M * p

No wait, I think the VTK convention is the opposite. Let me just think about what the code actually does.

In GetTransformBetweenNodes with vtkGeneralTransform:

  • transformSourceToTarget->PostMultiply() is set
  • Concatenate(T1->GetTransformToParent()) — first concatenation
  • Concatenate(T2->GetTransformToParent()) — second concatenation

When vtkGeneralTransform applies these concatenated transforms in PostMultiply mode, they're applied in order from first to last, so T1 transforms the point first, then T2 transforms the result. This gives us T2(T1(p)), which correctly goes from T1's input space to world. Then in IsTransformLinear, those same transforms are concatenated into a vtkTransform in the same order.

Extracting linear transforms...

Actually, I need to verify: when vtkTransform (in pre-multiply mode, the default) has Concatenate(M) called on it, does M get applied first or last? Let me think about the matrix multiplication order. If T_new = M * T_old, then when we apply to a point: T_new * p = M * T_old * p, so M is applied first (to the right in matrix notation, but first in the transformation chain). So concatenating T1 then T2 gives T2 * T1, and applying to p gives T2 * T1 * p = T2(T1(p)). That matches what we want.

So the linear transforms should also give us T2(T1(p)) when applied to a point. That's correct. Looking at VTK's vtkTransform documentation, I see there are two modes that control how new operations combine with existing transformations: PreMultiply() places new matrices on the right (so T_new = T_old * M, applying new transforms after), while PostMultiply() places them on the left (so T_new = M * T_old, applying new transforms before). The naming is a bit counterintuitive since different sources describe it differently, but the key distinction is where the new matrix gets inserted in the multiplication order.

Let me test this empirically by tracing through how IsTransformLinear handles a vtkGeneralTransform with concatenated transforms...

Actually, the core issue is whether IsTransformLinear correctly preserves the order when it iterates through the transforms and applies them to a fresh vtkTransform. If the iteration order doesn't match how vtkGeneralTransform actually applies the transforms, that's where the bug lies. I'm getting confused about the order of operations here—let me step back and figure out what's actually happening by checking the VTK source directly. The key question is whether vtkGeneralTransform in PostMultiply mode and vtkTransform in PreMultiply mode apply transforms in the same order when you concatenate T1 then T2, because if they don't, that would indicate a bug in how IsTransformLinear is reversing the transform.

Let me check vtkGeneralTransform's behavior with concatenated transforms and ordering:

Bash Check transform ordering in OrientedImageDataResample

IN

grep -n "GetConcatenatedTransform|NumberOfConcatenatedTransforms|TransformPoint|PostMultiply|PreMultiply" /Users/pieper/slicer/latest/Slicer/Libs/vtkSegmentationCore/vtkOrientedImageDataResample.cxx | head -30

OUT

299: if (inputImageTransformGeneral && inputImageTransformGeneral->GetNumberOfConcatenatedTransforms() == 0) 364: inputImageToReferenceImageTransform->PostMultiply(); 491: referenceImageToInputImageTransform->PostMultiply(); 815: inputToOutputTransform->TransformPoint(inputBoxCorner, outputIjkExtentCorner); 885: inputToOutputTransform->TransformPoint(inputBoxCorner, outputBoxCorner); 969: image1ToImage2Transform->PostMultiply(); 1009: for (int transformIndex = 0; transformIndex < generalTransform->GetNumberOfConcatenatedTransforms(); ++transformIndex) 1011: vtkLinearTransform* currentLinearTransform = vtkLinearTransform::SafeDownCast(generalTransform->GetConcatenatedTransform(transformIndex)); 1299: resliceTransform->PostMultiply();

I need to check the ordering of transform application in vtkGeneralTransform vs vtkTransform. Let me look at a concrete example to understand the ordering.

Actually, let me look at a key test: in GetTransformBetweenNodes, with transformSourceToTarget->PostMultiply(), the transforms are:

  1. Concatenate(T1->GetTransformToParent()) — T1 to T2
  2. Concatenate(T2->GetTransformToParent()) — T2 to world

For vtkGeneralTransform, this list [T1_to_T2, T2_to_world] applies T1_to_T2 first, then T2_to_world, correctly mapping from T1-input-space to world. But in IsTransformLinear, when iterating through these transforms with PreMultiply, the first concatenation sets outputLinearTransform to T1_to_T2, and the second should concatenate T2_to_world, though the thought cuts off there.

With PreMultiply semantics, the second step would compute T2_to_world * T1_to_T2, which when applied to a point gives the correct order: T2_to_world(T1_to_T2(p)). So the ordering actually seems right, which means I need to look elsewhere for what's causing the discrepancy.

Let me reconsider the VTK documentation more carefully—it says "M = M * A" in PreMultiply mode, which would put the new matrix A on the right, but the description about operations occurring "before those already represented" suggests the new operation is applied first to the data. I need to clarify exactly how vtkTransform interprets this matrix multiplication order.

Now tracing through IsTransformLinear with the correct understanding: starting with identity, concatenating T1_to_T2 gives T1_to_T2, then concatenating T2_to_world gives T1_to_T2 * T2_to_world. When applied to a point, this means T2_to_world executes first (rightmost in the multiplication), but the intent is to apply T1_to_T2 first—so there's a mismatch in the transformation order.

The real issue is that vtkGeneralTransform applies its concatenated transforms in the order they're added (T1_to_T2 then T2_to_world), but IsTransformLinear with PreMultiply produces the opposite execution order. I need to verify exactly how vtkGeneralTransform handles PostMultiply concatenations to confirm where the reversal happens. After working through the VTK documentation, I see that PostMultiply means new operations go on the left side of the matrix equation—so when concatenating transforms in a loop, each new transform gets multiplied on the left, building up the full chain from source to target. I'm verifying the order of operations: when a point is transformed, it encounters T1_to_T2 first (rightmost in the matrix multiplication), then T2_to_world applies after. Now I need to figure out how vtkGeneralTransform actually applies its concatenated transforms when using PreMultiply versus PostMultiply, and whether the stored order matches the application order. A * T_old (new goes LEFT)

  • After [Concatenate(T1), Concatenate(T2)] with PostMultiply:
    • After first: T = T1
    • After second: T = T2 * T1 (T2 prepended)

When applied to p: T * p = T2 * T1 * p → T1 applied first, T2 second.

The key question is whether GetConcatenatedTransform(i) returns transforms in application order or concatenation order. If they're stored in application order (T2 at index 0, T1 at index 1), then using PreMultiply to concatenate them gives T = T2 * T1, which correctly applies T1 first. But if they're stored in concatenation order (T1 at index 0, T2 at index 1), then PreMultiply would give T = T1 * T2, applying them in the wrong order. I'm getting caught up in the implementation details when I should focus on what actually matters for Steve's problem. Let me step back and test this directly in Slicer's Python console rather than speculating about VTK's internal behavior, or check if there's already a known issue in the repository about this transform ordering bug. hierarchy 2. The function is called with the PARENT transform nodes (not the data nodes themselves) 3. The ordering and computation seems correct in theory

One thing I notice: in the GetTransformBetweenRepresentationAndSegmentation function, it ONLY looks at the immediate parent transforms and then calls GetTransformBetweenNodes. This SHOULD work since GetTransformBetweenNodes traverses the full hierarchy from those parent nodes.

But wait - there's another scenario. What if:

  • labelmapNode.parent = T1
  • segmentationNode.parent = T2 (different transform!)
  • T1.parent = nullptr
  • T2.parent = nullptr

Then GetTransformBetweenNodes(T1, T2) should compute the transform from T1's space to T2's space. Since they're unrelated (no common parent except world), it needs to find the common ancestor and chain the transforms through world space.

Read vtkMRMLTransformNode.cxx (lines 587-606)

OK so for the "else" case (when source and target are in different branches of the hierarchy):

  1. Find common parent
  2. Get transform from source to common parent
  3. Get transform from target to common parent, invert it
  4. Concatenate them: source-to-common-parent then (common-parent-to-target)

This represents: source → common_parent → target. Correct.

Now, in this case transformSourceToTarget is already PostMultiply. And sourceNode->GetTransformToNode(firstCommonParentNode, transformSourceToTarget) would fill in the transform from T1 to its parent (world), and then transformSourceToTarget->Concatenate(transformFromCommonParentNode) adds the world-to-T2 part.

The logic checks out. Let me step back and search for any existing GitHub issues or recent commits that might shed light on this problem.

Bash Recent commits to segmentations logic file

IN

cd /Users/pieper/slicer/latest/Slicer && git log --oneline --since="2024-01-01" -- Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx | head -20

OUT

711b666069 BUG: Fix crash in ExportSegmentsToColorTableNode (#8942) c805ba7687 ENH: Propagate terminology when exporting segments to models fdcec42905 COMP: Remove set-but-unused compiler warnings c504bb40bf DOC: Fix typo in comment b09443100b BUG: Fix removal of dash characters from exported segmentation filename b26d778bca STYLE: Consistent formatting of C++ sources using clang-format 2d10f6bd0a STYLE: Add // comments to preserve wrapping before clang-format integration ed73eb8f70 STYLE: Normalize whitespace in C++ assignments and declarations 83294207db STYLE: Normalize pointer/reference formatting in preparation for clang-format 49a251067d ENH: Add convenience methods to get/set terminology in segments (#8296) 65409219f7 ENH: Add CSV file format support for color tables e2ffe29763 BUG: Fix warning in ExportSegmentsToLabelmapNode faff6c2662 COMP: Replace vtkStdString with std::string 024b340baa BUG: Fix issues caused by invalid Segmentation binary labelmap scalar types 2762f0c9f3 STYLE: Convert C++ source files from old-style "Whitesmiths" to "Allman" style Shell cwd was reset to /Users/pieper/slicer/slicer-skill

No recent commits related to transforms or ImportLabelmap. Let me search for related GitHub issues.

Actually, wait. Let me just look at what happens in GetTransformToNode. Let me trace through the code one more time with a fresh perspective.

In the scenario:

  • labelmapNode.parent = T1, T1.parent = T2
  • segmentationNode.parent = T1, T1.parent = T2

GetTransformBetweenRepresentationAndSegmentation(labelmapNode, segNode):

  • sourceNode = T1 (labelmapNode's parent)
  • targetNode = T1 (segmentationNode's parent)
  • GetTransformBetweenNodes(T1, T1) → returns identity (sourceNode == targetNode)

No transform applied → imported segmentation has same geometry as original. CORRECT.

Now for the case where segmentationNode has no parent: sourceNode is T1, but targetNode is nullptr, so GetTransformBetweenNodes(T1, nullptr) needs to find the transform from T1 up to the root.

Since nullptr is the world, this traverses from T1 through T2 to world, accumulating the full transform chain. The labelmap's geometry is in T1's local space, and we're transforming it to world space, which matches segmentationNode's space since it has no parent. That's correct.

In Mike's scenario where both nodes start as children of T1, then T1 gets parented to T2, the import operation needs to handle this updated hierarchy properly.

When I check the transform between T1 and itself, I get identity—no transform applied. So labelOrientedImageData retains its IJK-to-RAS geometry in T1's coordinate space, matching the original segments which are also in T1's space. They should align correctly, so the bug shouldn't appear in that scenario unless I'm misunderstanding what "applied first transform to both" actually means. Maybe it means T1 is applied to both nodes, then T2 is applied to T1, leaving both the labelmap and segmentation under the same transform chain (labelmap → T1 → T2 → world and segNode → T1 → T2 → world), which would display correctly.

Now I'm wondering if the issue is that the wrong overload of ImportLabelmapToSegmentationNode is being called. Looking at the available overloads, when Python calls the function with just two arguments (the labelmap node and segmentation node), it would match the first overload that takes a string as the third parameter rather than the one requiring a vtkStringArray, since that one has fewer required arguments.

So the Python call is definitely hitting the first overload at line 1410, which retrieves the IJK-to-RAS matrix from the labelmap node.

Looking at the actual implementation, it sets the labeled image geometry using that matrix, then applies any transforms between the parent nodes. In the case where both nodes share T1 as a parent (with T1 having T2 as its parent), the transform between them is identity, so the segment gets added in T1's input space—which is exactly where the original segments already are, so they should align.

I'm not spotting the issue yet. Maybe the problem occurs when the nodes don't share the same parent hierarchy. Let me reconsider the bug report more carefully.

Steve mentioned this might be a one-line fix, so maybe I'm overcomplicating this. Let me look at what GetTransformToNode actually does and whether there's a simpler solution. Or maybe I should check if there are related GitHub issues that could point me in the right direction.

Actually, let me think about this differently. The blog post mentions code around line 1459 that applies parent transforms. What should happen versus what actually happens when both nodes have hierarchical transforms? If the labelmap has T1 as parent and the segmentation has T2 as parent, and both T1 and T2 share a common ancestor, then GetTransformBetweenNodes(T1, T2) would need to find the path between these sibling transforms.

When T1 is the parent and T2 is a child of T1, the logic changes—I need to check whether IsTransformNodeMyParent correctly identifies this relationship. The condition evaluates to true, so we take the second branch that traverses from T2 to T1 and inverts the accumulated transforms, correctly mapping from T1's space to T2's space. I've verified the transform logic holds across all the cases I've tested. Now I'm wondering if the issue might be elsewhere—perhaps the code is relying on GetParentTransformNode() when it should use a different method, or maybe the labelmap's parent transforms are already being applied somewhere they shouldn't be.

Actually, GetIJKToRASMatrix() only returns spacing, directions, and origin—it never includes parent transforms, so those only get accounted for through the explicit GetTransformBetweenRepresentationAndSegmentation() call. But what if the labelmap was created by exporting from a segmentation that had transforms applied? When you export to labelmap, does that operation flatten the transforms into the IJK matrix itself?

Let me look at what the actual GitHub issue page for this might be and check if there's a test case in Slicer:

Bash Recent commits to transform node

IN

cd /Users/pieper/slicer/latest/Slicer && git log --oneline -- Libs/MRML/Core/vtkMRMLTransformNode.cxx | head -10

OUT

46ae272f79 BUG: Fixed cloning of composite transforms 11df881b46 ENH: Allow storing 5D images and displacement fields in NRRD files c210ff3a17 ENH: Harmonize vtkMRMLNode DefaultNodeNamePrefix and TypeDisplayName behavior b26d778bca STYLE: Consistent formatting of C++ sources using clang-format 2d10f6bd0a STYLE: Add // comments to preserve wrapping before clang-format integration 11a1fae3b2 STYLE: Apply miscellaneous whitespace formatting improvements ed73eb8f70 STYLE: Normalize whitespace in C++ assignments and declarations 9164d1787e STYLE: Normalize spacing in cast expressions and template arguments 83294207db STYLE: Normalize pointer/reference formatting in preparation for clang-format cd62f821c9 BUG: Fix hardening of inverted composite transform Shell cwd was reset to /Users/pieper/slicer/slicer-skill

Bash All uses of GetTransformBetween functions in segmentations logic

IN

grep -rn "GetTransformBetweenRepresentationAndSegmentation|GetTransformBetweenNodes" /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx | grep -v "//.*GetTransform"

OUT

/Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:418: vtkMRMLTransformNode::GetTransformBetweenNodes(volumeNode->GetParentTransformNode(), outputParentTransformNode, nodeToOutputTransform); /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:548: vtkSlicerSegmentationsModuleLogic::GetTransformBetweenRepresentationAndSegmentation(labelmapVolumeNode, segmentationNode, labelmapToSegmentationTransform); /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:617: vtkSlicerSegmentationsModuleLogic::GetTransformBetweenRepresentationAndSegmentation(modelNode, segmentationNode, modelToSegmentationTransform); /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:1011: vtkMRMLTransformNode::GetTransformBetweenNodes( /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:1463: vtkSlicerSegmentationsModuleLogic::GetTransformBetweenRepresentationAndSegmentation(labelmapNode, segmentationNode, labelmapToSegmentationTransform); /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:1693: vtkSlicerSegmentationsModuleLogic::GetTransformBetweenRepresentationAndSegmentation(labelmapNode, segmentationNode, labelmapToSegmentationTransform); /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:1935:bool vtkSlicerSegmentationsModuleLogic::GetTransformBetweenRepresentationAndSegmentation(vtkMRMLTransformableNode* representationNode, /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:1941: vtkGenericWarningMacro("vtkSlicerSegmentationsModuleLogic::GetTransformBetweenRepresentationAndSegmentation: Invalid inputs"); /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:1944: vtkMRMLTransformNode::GetTransformBetweenNodes(representationNode->GetParentTransformNode(), segmentationNode->GetParentTransformNode(), representationToSegmentationTransform); /Users/pieper/slicer/latest/Slicer/Modules/Loadable/Segmentations/Logic/vtkSlicerSegmentationsModuleLogic.cxx:3030: vtkMRMLTransformNode::GetTransformBetweenNodes(segmentationNode->GetParentTransformNode(), referenceVolumeNode->GetParentTransformNode(), segmentationToReferenceTransform);

Let me look at line 418 more closely - it uses a different pattern: it passes volumeNode->GetParentTransformNode() and outputParentTransformNode. Let me look at that code.

Also let me look at GetTransformToNode called at lines 464 and 476 - maybe there's a simpler API to use.

Wait, let me look at what GetTransformToNode does vs GetTransformBetweenNodes. The former takes MRML nodes directly, while the latter takes transform nodes.

Actually, I wonder if the solution is simpler: Instead of using GetTransformBetweenNodes with the parent transform nodes, use something like vtkMRMLTransformNode::GetTransformToNode(labelmapNode, segNode) directly on the data nodes...

But that won't work since the data nodes themselves aren't transform nodes. I need to find a way to get the world transform of a regular MRML node instead.

Read vtkSlicerSegmentationsModuleLogic.cxx (lines 406-425)

OK, I need to look at this from yet another angle. Maybe the issue isn't in GetTransformBetweenNodes or GetTransformBetweenRepresentationAndSegmentation. Maybe the issue is that when you export a segmentation to a labelmap, and the segmentation has parent transforms, the exported labelmap's IJK-to-RAS matrix already accounts for something? And then when you import it back, there's double-counting?

Let me look at ExportSegmentsToLabelmapNode to see what happens during export.

Actually, you know what? Let me think about this from Mike's actual test case. Mike said:

  1. Create a segmentation with 3 segments → export to labelmap (they align perfectly) ✓
  2. Create two transforms T1 and T2 where T1 has a rotation and translation, and T2 also has a rotation and translation with T1 as its child
  3. Apply T1 to both the labelmap and segmentation (both become children of T1)
  4. Then make T1 a child of T2

So after these steps, the labelmap has T1 as its parent and T1 has T2 as its parent, while the segmentation is also under T1 with T2 above it.

When I run the import function, it calculates the transform between T1 and T1, which is identity, so no transform gets applied. The labelmap's geometry stays as-is with its IJK-to-RAS mapping, and the new segment gets added to the segmentation in that same coordinate space. Since both the original segments and the imported labelmap are in T1's input space, they should align perfectly.

But Mike says they don't align. Maybe he's actually testing a different setup where the labelmap and segmentation have different parent transforms, not the same T1 like he described.

Or wait—could the second transform be applied to both nodes as well, not just to T1? Let me reconsider what he said about applying transforms. He said the second transform is applied to the first transform, not directly to the nodes themselves. So both should still have T1 as parent with T2 as T1's parent, giving an identity transform result.

I'm wondering if the misalignment is actually a display issue rather than a geometric one, or maybe something about how the transforms get hardened is affecting the original segments. Let me think about whether Mike might have only applied the transforms to the labelmap and not the segmentation, even though he said both.

Actually, here's what might be happening: the new segmentation created by labelMapToSeg has no parent transform at all, while the original segmentation is under the T1→T2 transform chain. So when comparing them, the new one's segments are in world space while the original's are in T1's input space, which would explain any mismatch.

The transform chain should handle this correctly—it traverses from T1 up through T2 to world space, applying both transforms to the labelmap geometry. But maybe the comparison itself is flawed somehow, or there's an issue with how the transforms are being applied that I'm not seeing yet. The function should correctly apply both transforms in sequence, but there might be a subtle bug where GetTransformToParent() isn't returning what I expect. Let me trace through the logic: when T1 is a child of T2, GetTransformToParent() for T1 should give just T1's own contribution (from its input space to T2's input space), and then the loop should concatenate T2's transform on top of that. If the rotation matches but translation is off, it suggests T2's translational component isn't being applied correctly in the concatenation.

Actually, I'm wondering if the issue is in how GetTransformToNode initializes or modifies the transform object that's passed in—whether it resets the existing state or just appends to it.

Read vtkMRMLTransformNode.cxx (lines 456-515)

OK so GetTransformToNode just calls GetTransformBetweenNodes. And GetTransformBetweenNodes calls Identity() and PostMultiply() at the beginning. So the transformSourceToTarget output is reset. That's fine.

Let me now look at the else branch more carefully:

vtkMRMLTransformNode* firstCommonParentNode = sourceNode->GetFirstCommonParent(targetNode);
sourceNode->GetTransformToNode(firstCommonParentNode, transformSourceToTarget);


This resets transformSourceToTarget and fills it with the transform from sourceNode to the common parent. Then it gets the transform from the target to that same parent, inverts it, and concatenates it to build the complete path from source to target.

Now I'm second-guessing myself on how Concatenate actually works in PostMultiply mode—whether it applies the new transform before or after the existing one when transforming a point. Let me think through the VTK documentation more carefully to get this right.

Concatenate(T1->GetTransformToParent())

Here is the next thinking you need to rewrite: → T = T1_transform Concatenate(T2->GetTransformToParent()) → T = T2_transform * T1_transform

Applied: T2_transform * T1_transform * p → T1 first, T2 second. CORRECT for going from T1's input space to world.

Now for the `else` branch:
1. `sourceNode->GetTransformToNode(firstCommonParentNode, transformSourceToTarget)` fills transformSourceToTarget with transform from source to common parent. This calls `GetTransformBetweenNodes(source, commonParent, transformSourceToTarget)` which resets transformSourceToTarget and PostMultiplies transforms to get source→commonParent.

2. `transformFromCommonParentNode` = transform from target to commonParent.

3. `transformFromCommonParentNode->Inverse()` inverts it: now it's commonParent→target.

4. `transformSourceToTarget->Concatenate(transformFromCommonParentNode)` -

So the else branch first gets the transform from source up to the common ancestor, then gets the transform from target back down to that ancestor, inverts it to go forward from ancestor to target, and concatenates them together to chain source→ancestor→target.

The math checks out—applying this composed transform maps points correctly from source space through the common parent to target space. I'm now thinking the bug might not be in the transform computation itself, but somewhere else in the pipeline, or possibly in how VTK handles pre-multiply versus post-multiply semantics internally.

Let me reconsider the `IsTransformLinear` function...

Checking the loop that iterates through concatenated transforms...

I'm realizing I need to understand how `vtkGeneralTransform` actually stores its transforms internally when using PostMultiply mode. When I concatenate transforms with PostMultiply, the order they're stored in the internal list might not match the mathematical order I'm assuming—it could be reversed or stored differently than I expect.

The key insight is that if `GetConcatenatedTransform(0)` returns T1 and `GetConcatenatedTransform(1)` returns T2 in chronological order, but I'm reconstructing them with PreMultiply semantics, I'd end up computing T1 * T2 * p instead of T2 * T1 * p. That's backwards—T1 should be applied first, then T2. This mismatch between how the transforms are stored and how I'm reconstructing them could absolutely be the source of the bug. So the transform order is definitely reversed—applying T1 then T2 gives the opposite of what we want. But wait, if that were true, even single transforms should fail, yet they work fine. With two transforms though, the reversal would flip the order, which could explain why rotations look similar in magnitude but the total translation ends up wrong—rotations around the same axis might appear equivalent, but translations don't commute with rotations the same way, so the combined result gets scrambled.

My working theory is that `GetTransformBetweenNodes` correctly builds T2 * T1 (applying T1 first), but `IsTransformLinear` extracts the transforms in chronological order and builds T1 * T2 instead, effectively applying the reversed transform. To verify this, I need to check how `vtkGeneralTransform::GetConcatenatedTransform` actually orders its internal list—whether index 0 is applied first or last.

Rather than getting lost in VTK's implementation details, I should probably just give Steve the analysis and point him toward what needs checking: the core logic in `GetTransformBetweenNodes` looks sound, but the issue likely lies in how `IsTransformLinear` extracts and reorders those transforms when converting to a linear representation.

Examining the transform extraction logic...

I'm tracing through how `IsTransformLinear` iterates over concatenated transforms and applies them sequentially to an identity matrix using `Concatenate`. Since the output starts in PreMultiply mode (the default), each concatenation does `T = T * A`, which means transforms are applied in the order they appear in the general transform. But I need to verify whether this matches how `GetTransformBetweenNodes` builds its transform chain with PostMultiply, since the two approaches might be reversing the order of operations. I'm working through how PostMultiply concatenates transforms in sequence—each new transform gets prepended to the result, so applying T1_to_T2 first, then T2_to_world, correctly transforms a point from T1's space through T2 and into world space. I'm working through how PostMultiply actually orders transforms in the pipeline—when I append T2_to_world after T1_to_T2, the pipeline applies them sequentially from index 0 onward, so T1_to_T2 executes first, then T2_to_world transforms the result.

But with PreMultiply, prepending creates the opposite problem: I'm building up the concatenated transform as T = T * A, which means T2_to_world gets multiplied first in the matrix chain, causing it to apply before T1_to_T2 when I actually need the reverse order.

The solution is to call `PostMultiply()` on the output transform before the loop so the transforms concatenate in the correct sequence. Let me verify this theory applies to the actual code in the Segmentations module. I'm verifying how VTK's `vtkGeneralTransform` actually orders its transform pipeline—specifically whether `GetConcatenatedTransform(i)` returns transforms in application order, and whether `PostMultiply` mode means new transforms are appended to the end and applied last. Let me trace through the concatenation logic to confirm whether this is genuinely a bug or if I've misunderstood the transform ordering. So the bug is definitely in `IsTransformLinear` — when there are multiple transforms, the multiplication order gets reversed. A single transform works fine, but two or more chained transforms apply in the wrong sequence. The fix should be straightforward: either add `PostMultiply()` at the start of `IsTransformLinear` or iterate through the transforms in reverse order. Since Steve mentioned a one-line change, adding that `PostMultiply()` call is probably the simplest solution.

Now I need to verify how `vtkGeneralTransform` actually applies its transforms and what `GetConcatenatedTransform` returns — whether it's the raw matrix or something else.

When `TransformOrientedImage` calls `IsTransformLinear`, the transform should map from labelmap-space to segmentation-space, which is what `GetTransformBetweenRepresentationAndSegmentation` provides. If `outputLinearTransform` reverses the concatenation order, we'd end up applying transforms in the wrong sequence, mapping through world-space incorrectly instead of directly between the two spaces.

The issue is in how `TransformOrientedImage` chains the transforms together — I need to check whether the concatenation order there matches what `IsTransformLinear` is building.

Looking at the variable naming, `worldToTransformedWorldLinearTransform` is actually the extracted linear transform from labelmap to segmentation space, not what the name suggests. If `IsTransformLinear` reverses the transform order, then applying two sequential transforms would partially work (rotations on the same axis might appear correct due to commutativity) but translations would be completely wrong — which matches exactly what Mike described.

The root cause is that `IsTransformLinear` builds the output `vtkTransform` in PreMultiply mode by default, reversing the order when concatenating multiple transforms. I can fix this by switching to PostMultiply mode before the concatenation loop, or by iterating through the transforms in reverse order instead.

Iterating in reverse...

Now I'm checking whether `IsTransformLinear` is called elsewhere and whether switching to PostMultiply would have any unintended side effects on the rest of the codebase.

Now I have a clearer picture of where the bug is. Let me check how IsTransformLinear is called elsewhere:

Bash Find all calls to IsTransformLinear

IN

grep -rn "IsTransformLinear" /Users/pieper/slicer/latest/Slicer/Libs/vtkSegmentationCore/ | grep -v ".h:"

OUT

/Users/pieper/slicer/latest/Slicer/Libs/vtkSegmentationCore/vtkOrientedImageDataResample.cxx:987:bool vtkOrientedImageDataResample::IsTransformLinear(vtkAbstractTransform* inputTransform, vtkTransform* outputLinearTransform) /Users/pieper/slicer/latest/Slicer/Libs/vtkSegmentationCore/vtkOrientedImageDataResample.cxx:1027: vtkErrorWithObjectMacro(inputTransform, "vtkOrientedImageDataResample::IsTransformLinear: Unsupported input transform with type " << inputTransform->GetClassName()); /Users/pieper/slicer/latest/Slicer/Libs/vtkSegmentationCore/vtkOrientedImageDataResample.cxx:1238: if (!alwaysResample && vtkOrientedImageDataResample::IsTransformLinear(transform, worldToTransformedWorldLinearTransform)) /Users/pieper/slicer/latest/Slicer/Libs/vtkSegmentationCore/vtkSegmentation.cxx:1075: if (!vtkOrientedImageDataResample::IsTransformLinear(transform, linearTransform)) /Users/pieper/slicer/latest/Slicer/Libs/vtkSegmentationCore/vtkSegmentation.cxx:1130: if (vtkOrientedImageDataResample::IsTransformLinear(transform, linearTransform))

Read vtkSegmentation.cxx (lines 1066-1105)

OK so IsTransformLinear is also used in vtkSegmentation::ApplyLinearTransform at line 1075, but there it's just checking if the transform is linear (using the result as a bool check). The extracted linearTransform is then used in the subsequent code.

Let me check line 1130.

Read vtkSegmentation.cxx (lines 1121-1150)

OK so at line 1130, IsTransformLinear is used just to check if it's linear (and warn). The linearTransform extracted there is not used. So that's fine.

At line 1075, ApplyLinearTransform uses IsTransformLinear to extract the linear transform and then use it. Let me see how it uses it.

Read vtkSegmentation.cxx (lines 1100-1124)

So in ApplyLinearTransform, the linearTransform is used at lines 1108 and 1115. If the transform order is reversed in IsTransformLinear, then ApplyLinearTransform would also be affected. But that's a separate issue.

Now let me think about whether the bug is definitively in IsTransformLinear.

Actually, wait. I need to verify my assumption about vtkGeneralTransform ordering. Let me check whether GetConcatenatedTransform(i) iterates from 0 to N-1 in the order transforms are applied.

Without access to the VTK source directly, I should look at the concrete evidence Mike provided. He mentioned that the rotation amounts appear correct but the total translation is wrong, which suggests the transforms are being applied in the wrong order—if T1 and T2 are composed as T1∘T2 instead of T2∘T1, the rotations would still match but the translation would be incorrect since the second rotation wouldn't properly affect the first translation. I need to reconsider how PostMultiply actually works—when I concatenate transforms with PostMultiply, the new transform gets applied after the existing one, so T = T2 * T1 gives me the correct composition order that matches how vtkGeneralTransform applies them sequentially. I'm confirming the bug: with PreMultiply mode, the transforms concatenate in the wrong order—T2 gets applied before T1 when it should be the other way around. The fix is to switch to PostMultiply, which correctly builds T2 * T1. Now I need to verify how vtkGeneralTransform actually handles PostMultiply concatenation to make sure this solution works. grows as [T1, T2, T3, ...]

  • TransformPoint applies index 0 first, then 1, then 2...
  • So T1 is applied first (index 0), T2 second (index 1)

This seems consistent with what I described.

Actually, I just want to double check one more thing. The vtkGeneralTransform is built in GetTransformBetweenNodes with PostMultiply(). In PostMultiply mode, Concatenate(M) should add M at the END of the pipeline (applied LAST). So:

  1. Concatenate(T1_to_T2): pipeline = [T1_to_T2]
  2. Concatenate(T2_to_world): pipeline = [T1_to_T2, T2_to_world]

When applied to a point, index 0 executes first—T1_to_T2 transforms from T1's space to T2's space, then T2_to_world takes it to world space. That's correct. But in IsTransformLinear with PreMultiply mode, the transforms get composed differently: T1_to_T2 * T2_to_world means T2_to_world applies first to the point, which is backwards. Switching to PostMultiply fixes this ordering issue.

For immediate workarounds, Mike can either harden both transforms together or manually call HardenTransform on the labelmap before importing. I should also check where IsTransformLinear is being called to understand the full scope of the fix.

The actual fix needs to go in vtkOrientedImageDataResample.cxx around line 995, right after initializing the output transform. Let me trace through the specific scenario again: when the labelmap has a parent transform that itself has a parent, the traversal needs to apply transforms in the correct order to get the right composition. space TO T2's space. T2→world is from T2's space to world. So the correct matrix multiplication order is M2 * M1, where M1 applies first then M2 — that's what PostMultiply gives me. With IsTransformLinear extracting [M1, M2] in that order, I need to verify the multiplication is happening correctly.

For a single transform, both approaches work fine since there's no ordering ambiguity. But when chaining multiple transforms, PreMultiply would apply them backwards — M1 * M2 means M2 gets applied to the point first, which is wrong. The image would end up transformed incorrectly and in the wrong space entirely. This is the bug: IsTransformLinear is concatenating the matrices in the wrong order.

The fix is to call PostMultiply on the output transform before the concatenation loop in vtkOrientedImageDataResample.cxx around line 1009, so the matrix multiplication order matches how vtkGeneralTransform actually applies the transforms. As a workaround, hardening the transforms before importing them would also work.

Now I'm double-checking my understanding of how PreMultiply actually behaves in vtkTransform... = M1 → M1(p) 2. Concatenate(M2): T = M2 * M1 → M1 first, then M2.

In PostMultiply mode, matrices are applied in the order they're concatenated. Since vtkGeneralTransform uses PostMultiply and applies transforms in order [M1, M2], switching to PostMultiply in the code will preserve that order correctly, whereas PreMultiply reverses it.

The bug is clear: the current PreMultiply approach builds M1 * M2, which applies M2 first and M1 second—backwards. Switching to PostMultiply builds M2 * M1, applying M1 first and M2 second, which matches the intended order.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment