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.

1 Like

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

Hi Andras,
Would it be possible to select the point (x,y,z) location in a ply file, for all the ply files opening up one by one and closing once the point is selected.
The point needed to be selected by the user on the model and then the next model file opens up automatically and the user selects a point on that model and will do until points in all the ply files are selected. can this process be automated.

Thank you

regards,
Saima

Yes, this can be very easily automated. Examples for all the necessary operations (loading a model, adding a markup point list, observing a markup node to get notified about changes) are available in the script repository but if you get stuck with automation of any specific step then let us know.

1 Like

Hi Andras,
I started working on the script. I loaded models from a given folder. used a for loop to load models one by one.

But i do not get the control point selection for each model.
How can I stop for loop to get input from user first and then iterate the next one.
The code is below:

import time
startTime = time.time()
logging.info(‘Processing started’)
logging.info(‘Search for .ply files’)
modelDir = inputDir
modelFileExt = “ply”

    import math
    import os
    
    modelFiles = list(f for f in os.listdir(modelDir) if f.endswith("."+modelFileExt))
    print(modelFiles)
    
    #load models and show in 3D view
    for modelIndex, modelFile in enumerate(modelFiles):
        name = os.path.basename(modelFile)
        modelNode = slicer.util.loadModel(modelDir + "/" + modelFile)
        placeModePersistence = 1
        slicer.modules.markups.logic().StartPlaceMode(placeModePersistence)    
        markup_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode")
        placeModePersistence = 0
        slicer.modules.markups.logic().StartPlaceMode(placeModePersistence)    
        #slicer.util.updateMarkupsControlPointsFromArray(markup_node, np.random.rand(10,3))
        #markup_node.GetDisplayNode().SetTextScale(0)    
        markup_node.GetDisplayNode().SetPointLabelsVisibility(False)

You don’t need a loop. Instead, you can add an observer to the markup node. If an event invoked that indicates a new point is added then in your callback function you save the point and load the next model.

1 Like

could you please refer to the script for adding an observer.

Thanks alot.

regards,
Saima

Do i need to keep the index for modelFiles[index] and pass it through the function in addObserver.

modelFiles = list(f for f in os.listdir(modelDir) if f.endswith(“.”+modelFileExt))
print(modelFiles)

    #load models and show in 3D view
    #for modelIndex, modelFile in enumerate(modelFiles):
    global index = 0
    name = os.path.basename(modelFile)
    modelNode = slicer.util.loadModel(modelDir + "/" + modelFiles[index])
    placeModePersistence = 1
    slicer.modules.markups.logic().StartPlaceMode(placeModePersistence)    
    markup_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode")
    updateindex
    markup_node.AddObserver(slicer.vtkMRMLMarkupsNode.PointModifiedEvent, onMarkupsAdded, index)

and in the function onMarkupsAdded I load the next model. right?

1 Like

Hi Andras,
Could you please refer to the code snippets or scripts of what you suggested?

thank you

See Script repository — 3D Slicer documentation

It is probably better to observe the new point definition event instead of any point modification.

This observation would immediately switch to the next case when you add the point, therefore you could not adjust the point after placement. So, maybe it is better to add a keyboard shortcut for going to the next case.

is new point definition event is PointAddedEvent?
Also, I want to explore the model in the 3D view before placing a point on the model but because the mouse is in the fiducial placement mode I cant do that. whats the solution?

regards,
Saima