vtkMRMLAnnotationROINode instantiation before changing Interaction Mode

Hi Slicer Community,

I apologize in advance if the answer to my question is out there. I’ve seen related threads, but I haven’t been able to piece it together to get my solution working.

Basically, I would like to instantiate a vtkMRMLAnnotationROINode before having the user switch Interaction Modes (to Place mode) in order to change certain properties of the AnnotationROINode, such as the Name etc. so that the user doesn’t see the default names (like R, R_1, R_2), etc.

Basically, I have created a button addBoundingBoxButton which is connected to a function onAddBoundingBox, which I would like to do the following:

def onAddBoundingBox(self):

  boundingBoxName = 'Finding {}'.format(self.findingCounter) #internal counter for number of findings identified

  #add vtkMRMLAnnotationROINode and change name
  annotationROINode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLAnnotationROINode',boundingBoxName)

  slicer.mrmlScene.GetNodeByID("vtkMRMLSelectionNodeSingleton").SetReferenceActivePlaceNodeClassName("vtkMRMLAnnotationROINode")
  slicer.mrmlScene.GetNodeByID("vtkMRMLInteractionNodeSingleton").SetCurrentInteractionMode(1)
  

The problem is that the annotationROINode object is not the one modified when the InteractionNode is changed to Place mode, and when the vtkMRMLAnnotationROINode is placed, a new object is created with a default name. Weirdly, when I do this same approach with vtkMRMLMarkupsLineNode, I get the desired behavior (i.e., I can create the vtkMRMLMarkupsLineNode beforehand and the InteractionNode lets me place the line that I have already created in my script). Perhaps this behavior is because of some inherent differences in the Annotations vs the Markups modules? Is there a way to link the annotationROINode object I’ve created to the one that will be created in Place mode?

Another method I was thinking of was creating the vtkMRMLAnnotationROINode and then using two fiducial points to let the user choose the center and radius of the bounding box (I suspect this is how it is done in practice, but I couldn’t find a clear procedure for this in the source code, and I would like to take advantage of this being implemented in Place mode through the Interaction Node). I have looked at other threads like this one and this one, but it’s not clear to me how to create a button with a connecting function that (1) lets me place two fiducial points, (2) waits for the user to place them, (3) then extracts the Control Points, and (4) creates/modifies the vtkMRMLAnnotationROINode with the desired properties. It seems that when I put all of these functions in a connector function, it runs to the very end of the function without waiting for the fiducial points to be placed, so obviously this produces errors downstream.

One last thing I will say is that I am aware that the AnotationROI nodes will be moved to the Markups module sometime in the near future, but I unfortunately can’t wait that long (although, I’m excited for it!).

Thanks for any guidance or help,
Eric

1 Like

Hello Community,

Thanks to a great tip by @Fernando, who suggested to connect the event with a function that listens and does something if there are two fiducials in the markups node, I was able to figure out how to place a vtkMRMLAnnotationROINode with two Fiducial Points.

I still have some follow-up questions (see below), so I would appreciate any help on those, but otherwise, here is some working code:

import numpy as np

def onFiducialPlaced(caller, event):
    numberOfFiducials = caller.GetNumberOfFiducials()
    if numberOfFiducials != 2:
        print("Not yet 2")
        return
    #turn off fiducial placement
    slicer.mrmlScene.GetNodeByID("vtkMRMLInteractionNodeSingleton").SetCurrentInteractionMode(2)
    fiducialWorldCoordinatesDict = {}
    for n in range(numberOfFiducials):
      p = [0,0,0,1]
      caller.GetNthFiducialWorldCoordinates(n,p)
      fiducialWorldCoordinatesDict[n] = p
      print("Fiducial Point {}: {}".format(n+1,p))
    #Get XYZ and RadiusXYZ
    XYZ = np.array(fiducialWorldCoordinatesDict[0])[:3] #only take x,y,z
    R = np.array(fiducialWorldCoordinatesDict[1])[:3] #only take x,y,z
    dx,dy,_ = np.abs(XYZ-R)
    #Create annotationROINode and set location
    annotationROINode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLAnnotationROINode',caller.GetName())
    annotationROINode.SetXYZ(XYZ)
    annotationROINode.SetRadiusXYZ(dx,dy,10) #fix radius in z direction to 10
    #delete markups
    caller.RemoveAllMarkups()

def onAddBoundingBox():
    fiducialList = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode",'MyNode')
    tag = fiducialList.AddObserver(slicer.vtkMRMLMarkupsNode.PointPositionDefinedEvent, lambda caller, event: onFiducialPlaced(caller,event))
    placeWidget = slicer.qSlicerMarkupsPlaceWidget()
    placeWidget.setMRMLScene(slicer.mrmlScene)
    placeWidget.setCurrentNode(fiducialList)
    placeWidget.setPlaceMultipleMarkups(placeWidget.ForcePlaceMultipleMarkups)
    placeWidget.placeButton().click()

addBoundingBoxButton = qt.QPushButton('Add')
addBoundingBoxButton.clicked.connect(onAddBoundingBox)
addBoundingBoxButton.show()

Question 1: I would have expected that slicer.vtkMRMLMarkupsNode.PointEndInteractionEvent would have been the event for dropping the Fiducial point the first time (with the way my code is written), yet this signal is only emitted after picking it back up and dropping it (i.e., it is only emitted if you pick up the fiducial point, drag it, and drop it a second, third, etc. time). The code seems to do exactly what I want with slicer.vtkMRMLMarkupsNode.PointPositionDefinedEvent, so I’m not complaining, but any insights would make me feel a bit more comfortable.

Question 2: Is there a way to specify a priori how many Markups can be added to a MarkupsFiducialNode whn using placeWidget.setPlaceMultipleMarkups(placeWidget.ForcePlaceMultipleMarkups)? For a bounding box, I just need two, so I have an if loop in onFiducialPlaced to check how many Fiducials are part of the node, and once I’m satisfied, I change the InteractionMode to 2 to get out of Place mode. However, this seems quite “hacky”. Is there a better way to do this?

Hope this can help the general Slicer community,
Eric M

1 Like

The event is exactly for this purpose. For example, can be used for performing operations that you don’t want to do continuously, but when the user finished interaction with the control point.

Yes, you can use SetMaximumNumberOfControlPoints to set the expected number of points. If that number is reached then placement is finished. We will tune/optimize the behavior, including predefine name of markups (@muratmaga’s team is working on this).

Also note that @Sunderlandkyl is working on adding a markups ROI, which would be a much improved version of Annotation ROI (and annotation module will be deprecated).

Hi Iassoan,

Thanks for the answer.

Regarding, my second question, I modified onAddBoundingBox as below and commented out the line where I add an observer just to see if Slicer goes out of Place mode after the second control point.

def onAddBoundingBox():
    fiducialList = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode",'MyNode')
    fiducialList.SetMaximumNumberOfControlPoints(2)
    #tag = fiducialList.AddObserver(slicer.vtkMRMLMarkupsNode.PointPositionDefinedEvent, lambda caller, event: onFiducialPlaced(caller,event))
    placeWidget = slicer.qSlicerMarkupsPlaceWidget()
    placeWidget.setMRMLScene(slicer.mrmlScene)
    placeWidget.setCurrentNode(fiducialList)
    placeWidget.setPlaceMultipleMarkups(placeWidget.ForcePlaceMultipleMarkups)
    placeWidget.placeButton().click()

But after clicking the placeButton, it just keeps letting me add more fiducial points (‘MyNode’, ‘F’,‘F_1’,‘F_2’, etc.). I tried this with placeWidget.ForcePlaceSingleMarkup, but it stops after the first Control Point. I imagine that I coded this incorrectly. In any case, my “hack” is working for now.

I’m really looking forward to the new and improved markups ROI!

Thanks again,
Eric