Interactively adding control point with a given label

I build a user interface in which the user will click on the 3D scene (or the scenes of the three anatomical planes) and a fiducial node control point with a predetermined label will be placed.
The problem is that I cannot set the label in the slicer.modules.markups.logic().StartPlaceMode function (there is no argument for that). A workaround I thought of is to place the control point with the label 3D Slicer automatically gives, and rename it after the point placement. To do so:

slicer.modules.markups.logic().StartPlaceMode(0)
last_point_index = self.markup_node.GetNumberOfControlPoints() - 1
self.markup_node.SetNthControlPointLabel(last_point_index, my_preferred_label)

However, the last two lines get executed before the user can click on the scene. How can I pause the execution of the code until StartPlaceMode returns? The best solution would be my initial intention, i.e. being able to provide the label of the control point before placing it on the scene.

I assume you use an older version of Slicer. In the latest version it is possible to pre-define fiducials with names. I haven’t used that feature yet personally but I assume the way to do that is to create a fiducial node with the points you want and keep them undefined. Then when the user clicks, the next undefined point will be defined.

1 Like

You may want to consider using a Markups Point List template. See the following linked post below. You can add undefined points to the list where you have predefined the name. Then when going into placement mode for this Point List node it will be going through the list to define the point location so will use your already defined labels.

1 Like

I have version 4.11.20210226 installed.
By fiducial, do you mean the Markups node object or the control points that are defined within the active node? In case of the former, I can set the name at construction time, for the control points I cannot. Could you please send me a link to the relevant API doc entry?

Thank you for the tip, that could work for me. However, following the referenced video, the button at 0:21 does not add control points (I can still add normal control points, so I don’t know why unnamed control points cannot be defined).
Assuming that the issue above is solved, here is a follow-up question. Can I select programmatically which unnamed control point to add? E.g. when the user clicks on the pushbutton P1, the control point with label P-1 will be placed by the mouse, while if he clicks on another pushbutton P7, the control point with label P-7 will be added to the scene. Note that the order may change, i.e. the user might first click on P7 and then on P1.

If you have a markup point list template, you can set the index of an unplaced point that will be placed on the next click using SetControlPointPlacementStartIndex() method.

I cannot access that method from my vtkMRMLMarkupsFiducialNode object, which is strange as SetControlPointPlacementStartIndex is a public method of the vtkMRMLMarkupsNode class. Maybe it is not exposed to the Python interface? This is the traceback I get:

Traceback (most recent call last):
  File "<console>", line 1, in <module>
AttributeError: 'vtkSlicerMarkupsModuleMRMLPython.vtkMRMLMarkupsFid' object has no attribute 'SetControlPointPlacementStartIndex'

My next try was looking into the source code to see what happens when I click on that button in the Advanced settings. Behind the scenes, that button calls this method (again, not available through Python). But even that function seems to need the (R,A,S) triplet, so how can you instantiate an undefined control point?

For all the features that we have been discussing here you need a recent Slicer Preview Release.

Thank you, it works interactively with the latest preview release! I will try it tomorrow programmatically.

2 Likes

I tried it, and it works just fine! All of you gave me very useful tips; since I can select only one solution, I accept the one from @jamesobutler as he pointed me to the GUI settings, which helped me find the corresponding implementation.

Here is an MWE so that others who stumble upon a similar problem can benefit from it.

  1. All the code enumerated below went into the main.py file, which was generated by the Extension Wizard.
  2. Created a Markup node in the setup method of the class mainWidget. This is the node I will populate with the control points.
    node_id = slicer.modules.markups.logic().AddNewFiducialNode('your_label')
    self.markup_node = slicer.mrmlScene.GetNodeByID(node_id)
    
  3. Added the class variable N_POINT = 9 into the class mainWidget, indicating the number of push buttons.
  4. Initialized the control points by calling self.initialize_points() in the setup method. This function is defined as
    def initialize_points(self):
       slicer.modules.markups.logic().SetActiveListID(self.markup_node)
       self.markup_node.SetControlPointLabelFormat('P%d')
       for i in range(1, self.N_POINT + 1):
           self.markup_node.AddControlPoint([0, 0, 0])
       self.markup_node.UnsetAllControlPoints()
       self.markup_node.SetMaximumNumberOfControlPoints(self.N_POINT)
    
  5. Created the nine push buttons in Qt Designer, and in the setup method I connected them with the function I want to execute. For instance, for button 4:
    self.ui.P4PushButton.connect('clicked(bool)', lambda: self.onPointPushButton('P4'))
    
    The function is defined as
    def onPointPushButton(self, button):
       slicer.modules.markups.logic().SetActiveListID(self.markup_node)
       n_point = self.markup_node.GetNumberOfControlPoints()
       index = None
       for i in range(n_point):
           label = self.markup_node.GetNthControlPointLabel(i)
           if label == button:
               index = i
               print('Index: ', index)
               break
       if not index:
           raise ValueError('Point {0} not found.'.format(button))
       self.markup_node.UnsetNthControlPointPosition(index)
       self.markup_node.SetControlPointPlacementStartIndex(index)
       self.markup_node.SetNthControlPointLabel(index, button)
       slicer.modules.markups.logic().StartPlaceMode(0)
    
    When writing this function, I was inspired by what the GUI does. Hence, I searched for the corresponding GUI function.

I will polish this code (e.g. moving the non-GUI related stuff into the mainLogic class), though the main functionality works.
One issue with this approach is that the control points are identified by labels, so if the user changes the label (e.g. by clicking on the markup in the scene and choosing Rename control point…), the control point is no longer found by the loop in the onPointPushButton method. Is there a way to disable the renaming, or better yet: identifying the control points by a kind of persistent property ?

You can whitelist certain right-click context menu actions. See the following:

https://slicer.readthedocs.io/en/latest/developer_guide/script_repository.html#use-whitelist-to-customize-view-menu

1 Like