Unexpected interactions of ROI objects with code

This continues the topic Configure ROI to match volume through code but I think that post was not phrased very well, so let’s start again:

I’m creating a ROI with code based on an example from the script repository:

self.roiNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsROINode")

cropVolumeParameters = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLCropVolumeParametersNode")
cropVolumeParameters.SetInputVolumeNodeID(self.volumeNode.GetID())
cropVolumeParameters.SetROINodeID(self.roiNode.GetID())
slicer.modules.cropvolume.logic().FitROIToInputVolume(cropVolumeParameters)
slicer.mrmlScene.RemoveNode(cropVolumeParameters)

I get this ROI object:

But now if I tell it its ObjectToNode matrix is updated, it jumps (I need this because, as you can see, I give users rotation handles, and I also want to give them a “reset” button). And indeed, the center in the world has changed:


Now, here come some funnier parts: If I try to reset the center in the world, it doesn’t work:

>>> roi.SetCenterWorld(center)
>>> roi.GetCenterWorld()
vtkmodules.vtkCommonDataModel.vtkVector3d([0.0, 0.0, 0.0])
>>> center
vtkmodules.vtkCommonDataModel.vtkVector3d([0.38079845905303955, -22.919204711914062, -175.25])

If I first reset the center to 0, then it semi-works: After

>>> roi.SetCenterWorld([0,0,0])
>>> roi.SetCenterWorld(center)

the ROI is visually back to its place fitting the volume, but

>>> roi.GetCenterWorld()
vtkmodules.vtkCommonDataModel.vtkVector3d([0.0, 0.0, 0.0])

I’m not even sure what exactly is the bug I should report, but I’m pretty convinced that this Can’t Be Right :tm:.

Slicer 5.6.2 on Ubuntu 20.04.

I cannot reproduce any “jump” with neither the latest Slicer Stable Release (Slicer-5.6.2) nor the latest Slicer Preview Release (Slicer-5.7):

import SampleData
volumeNode = SampleData.downloadSample('CTChest')

roiNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsROINode")
cropVolumeParameters = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLCropVolumeParametersNode")
cropVolumeParameters.SetInputVolumeNodeID(volumeNode.GetID())
cropVolumeParameters.SetROINodeID(roiNode.GetID())
slicer.modules.cropvolume.logic().FitROIToInputVolume(cropVolumeParameters)
slicer.mrmlScene.RemoveNode(cropVolumeParameters)

print(roiNode.GetCenter())
print(roiNode.GetCenterWorld())
roiNode.GetObjectToNodeMatrix().Modified()
print(roiNode.GetCenter())
print(roiNode.GetCenterWorld())

Thanks for the quick turnaround.

Following your message, I went and dug deeper, and was able to pinpoint the issue. Indeed, the code in the script repository is innocent. The issue turns out to be elsewhere.

Besides what I reported earlier, I have a callback for changes in the ROI. Among other things, it takes the ROI’s ObjectToWorldMatrix, and changes it a little in order to do some computations. I didn’t think this was relevant, because the documentation specifically says that “Changes made to the matrix will not be applied”. And indeed, in most uses, this appears to be the case – but not if the change is in a callback for modifications.

To reproduce the jump:

import SampleData
volumeNode = SampleData.downloadSample('CTChest')

roiNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsROINode")
cropVolumeParameters = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLCropVolumeParametersNode")
cropVolumeParameters.SetInputVolumeNodeID(volumeNode.GetID())
cropVolumeParameters.SetROINodeID(roiNode.GetID())
slicer.modules.cropvolume.logic().FitROIToInputVolume(cropVolumeParameters)
slicer.mrmlScene.RemoveNode(cropVolumeParameters)

def onROIModified(roi, event) -> None:
    mat4 = roi.GetObjectToWorldMatrix()
    for i in range(3):
        mat4.SetElement(i, 3, 0.0)
roiNode.AddObserver(vtk.vtkCommand.ModifiedEvent, onROIModified)

print(roiNode.GetCenter())
print(roiNode.GetCenterWorld())
roiNode.GetObjectToNodeMatrix().Modified()
print(roiNode.GetCenter())
print(roiNode.GetCenterWorld())

Now I know how to avoid the problem in my code; I maintain that the behavior shown here is surprising and even buggy, but I can totally see why fixing it would be more trouble than it’s worth – and perhaps some documentation fix is most appropriate.

ObjectToWorldMatrix is computed internally and must not be modified. The Changes made to the matrix will not be applied note meant to say that developers should not modify the matrix (if modifications has no effect then why would anyone change it?), but it seems that we have to be more explicit.

We could also change the API to require an input vtkMatrix4x4 object. It would prevent any invalid changes, but it would be a bit less convenient (you would need to create a new object before you call the Get… function).

I made a little initial PR for this.

As hinted, in my case, I’m using the ROI as a control element, to pick a rotation which I want to apply elsewhere; however, I don’t want to apply the translation. One way to achieve this, if I’m not mistaken, is to take this matrix, and zero out the first three cells of the last column.