Slicer 5.10 qMRMLSubjectHierarchyComboBox turned to 'None' but no issue with 5.8.1

Dear all,

In a new Extension I recently made SlicerOrbitSurgerySim (installable in Extension Manager in Slicer 5.10), when I run a function called onInitialRegistrationPushButton from the plateRegistration module, the node selectors (qMRMLSubjectHierarchyComboBox) were set up to None, which then set all the connected parameterNode objects to None. However, I only encountered the issue in the new Slicer 5.10 and previous Slicer 5.9.1, not in Slicer 5.8.1.

In the script, each qMRMLSubjectHierarchyComboBox is connected to a parameter node, which is also connect to a function for connecting to a parameter node and also enable/disable visualization.

For example, a fiducial selector below is connected to a function: self.ui.orbitFiducialSelector.connect(“currentItemChanged(vtkIdType)”, self.onSelectOrbitLmNode)
In the onSelectOrbitLmNode function, the object is then connect to a parameter node:

def onSelectOrbitLmNode(self):
    shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
    try:
        orbitLmId = self.ui.orbitFiducialSelector.currentItem()
        self._parameterNode.orbitLm = shNode.GetItemDataNode(orbitLmId)
        self._parameterNode.orbitLm.GetDisplayNode().SetVisibility(True)
    except:
        pass

I found that, in onInitialRegistrationPushButton function, after creating a folder, and pass a parameterNode object into the folder (lines 704-711) like:

plateModelItem = self.folderNode.GetItemByDataNode(self._parameterNode.rigidRegisteredPlateModel)
self.folderNode.SetItemParent(plateModelItem, self.plateRegistrationFolder)

All qMRMLSubjectHierarchyComboBox would be set to None, and so did the parameterNode.

The current solution I found is to the UI signal by adding the below four lines before line 710 (i.e., before adding any item to the folder) in plateRegistration.py:

blockers = [
    qt.QSignalBlocker(self.ui.inputOrbitModelSelector),
    qt.QSignalBlocker(self.ui.orbitFiducialSelector),
    qt.QSignalBlocker(self.ui.plateModelSelector),
    qt.QSignalBlocker(self.ui.plateFiducialSelector),
]

It would be a bit difficult to directly debug the original module script. But it could be replicated by install SlicerOrbitSurgerySim from Extension Manager, restart Slicer, go to Sample Data, download the plateRegistration Sample Data and populate the combo boxes and click “initial registration”.

I tried to create a demo module for replicate this issue, hopefully it recaptured the issue properly. Here is the testing module.

To replicate, download the zipped module, install the demo extension in Slicer 5.10.0 using Extension Wizard.

After that, use the Markups module to create a point list, and click ‘Subject hierarchy folder test’ button at the very bottom (ignore everything else). You should see that the qt combo box at the first row changed to ‘None’.

This shouldn’t be the case. This module simply select a markup node in the first row’s qtMRMLSubjectHierarchyComboBox inputSelector and pass it to a parameter node as below:

# Connections
self.ui.inputSelector.connect("currentItemChanged(vtkIdType)", self.onInputSelector)
self.ui.inputSelector.setMRMLScene(slicer.mrmlScene)
self.ui.inputSelector.setNodeTypes(['vtkMRMLMarkupsFiducialNode'])
def onInputSelector(self):
    shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
    try:
        inputLmId = self.ui.inputSelector.currentItem()
        self._parameterNode.inputLM = shNode.GetItemDataNode(inputLmId)
        self._parameterNode.inputLM.GetDisplayNode().SetVisibility(True)
    except:
        pass

Afterwards, the subjectHierarchy folder test simply created a folder and pass self._parameterNode.inputLM into the folder.

def onShTestButton(self) -> None:
    """
    Test for the bug: move a parameter-node-referenced node into a SubjectHierarchy folder
    and see if parameter node / selectors get reset to None.
    """
    print("\n=== SubjectHierarchy folder test ===")

    # Make sure we have a parameter node
    if not self._parameterNode:
        print("No parameter node yet; initializing.")
        self.initializeParameterNode()

    p = self._parameterNode

    # Ensure we have an input volume
    inputNode = p.inputLM
    if not inputNode:
        inputNode = self.ui.inputSelector.currentNode()
        if inputNode:
            p.inputLM = inputNode

    if not inputNode:
        print("ERROR: No input volume selected; cannot proceed with test.")
        return

    # Create a SubjectHierarchy folder and move the parameter node under it
    shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
    sceneItemID = shNode.GetSceneItemID()

    folderName = "ParamNodeSHTestFolder"
    folderItemID = shNode.CreateFolderItem(sceneItemID, folderName)

    # print(f"Created folder item: {folderItemID} with name '{folderName}'")

    itemID = shNode.GetItemByDataNode(p.inputLM)

    shNode.SetItemParent(itemID, folderItemID)
    print(f'parameter node orbitLm check 7 {p.inputLM}')

In Slicer 5.10.0, the qtMRMLSubjectHierarchyComboBox would be turned to None.

What I found that disturbed the qtMRMLSubjectHierarchyComboBox (inputSelector) is probably due to the combox was connected to the onInputSelector function to connect it with a parameter node. If I disable the onInputSelector function at line 255 connected to the inputSelector as well as the connection at line 179, the combo box stopped switched to None anymore.

Again, in Slicer 5.8.1, there was no such issue.

Sorry, the explanation and error replication is lengthy. I could not find a shorter way to explain it. Also, interestingly, this error only appeared at the first run. If I ran the same module in the same scene again, the issue would not occur.

Thanks for looking at it!

This might be an important issue, but you probably don’t get reactions because nobody here has time to process this really long message with the loose snippets. I have to admit that I don’t…

Please share a self-contained script that makes it easy to understand and reproduce the issue with the latest Slicer and one of us will probably investigate. Thank you!

1 Like

Thanks for pointing it out! Yes, I realized that it was indeed too long. The potential bug is a little weird, so I tried to add a bit background.

Basically, my original module created a subject hierarchy folder than add a parameter node under it. It then switched the qtMRMLSubjectHierarchy combo boxes linked to all parameter nodes to None, thus cleared those nodes. The issue only occurred in Slicer 3.10, not 3.8.1.

Here is the testing module modified from the default Extension Wizard extension. I hope this one could recapture the bug.

To replicate:

  1. Download the zipped module (link), install the demo extension in Slicer 5.10.0 using Extension Wizard.
  2. Switch to test_paramNode extension.
  3. Use the Markups module to create a point list (could be an empty one),
  4. Then select the markup node (default ‘F’ in the below screenshot) in the ‘input volume’ row (the first row; sorry I forgot to change the text), which is a qtMRMLSubjectHierarchyComboBox.

image

  1. Ignore all other entries. Click the ‘SubjectHierarchy folder test’ button at the bottom.

image

  1. You should see the ‘input Volume’ row that you selected the markup node turned to None.

image

################### Explanation ########################

However, this should not be the case.

This module simply created a folder ‘ParamNodeSHTestFolder’, pass the markup node to a parameter node, and save it under the folder (switch to the Data module you could see something like below):

image

Here are the lines (255-262 in test_paramNode.py in the downloaded extension) pass the Markup node to the parameter node:

def onInputSelector(self):
    shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
    try:
        inputLmId = self.ui.inputSelector.currentItem()
        self._parameterNode.inputLM = shNode.GetItemDataNode(inputLmId)
        self._parameterNode.inputLM.GetDisplayNode().SetVisibility(True)
    except:
        pass

Here are the lines (314-325 test_paramNode.py) that added the selected Markup node to the created folder ‘ParamNodeSHTestFolder’

p = self._parameterNode
# Create a SubjectHierarchy folder and move thresholdedVolume under it
shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
    sceneItemID = shNode.GetSceneItemID()

folderName = "ParamNodeSHTestFolder"
folderItemID = shNode.CreateFolderItem(sceneItemID, folderName)

#Add to folder
itemID = shNode.GetItemByDataNode(p.inputLM)
shNode.SetItemParent(itemID, folderItemID)

Hope this clearer. Thanks for looking at the issue!

I understand more or less, but reproducing the issue is still quite complicated (need to download stuff, dig the relevant parts in a larger file etc.).

Sorry but if it’s not too much to ask, could you please provide a simple self-containing example? A code snippet that I can paste into the python console. That snippet can simply show the selector in a new window, it doesn’t need to be part of a module. Thank you so much!

Sorry for the late response. Thank you very much for your advice and time!

Yes. Initially, I could not repeat the same bug directly in the Python console but only in an extension. After some tests, the snippet finally worked and could repeat the same issue:

To run with the snippet below, in a fresh Slicer 5.10 scene,

  1. Create a markup fiducial node
  2. Copy-paste the below snippet in the Python console,
  3. In the pop-up GUI, select the node in the combo box, and click the button below it.

#Copy-paste below snippet in the Python console:

import slicer, qt
from slicer.parameterNodeWrapper import parameterNodeWrapper

@parameterNodeWrapper
class SHTestParameters:
    selectedFiducial: slicer.vtkMRMLMarkupsFiducialNode

dialog = qt.QDialog(slicer.util.mainWindow())
dialog.setWindowTitle("SHComboBox move -> GUI reset -> Param wipe test")
layout = qt.QVBoxLayout(dialog)

shComboBox = slicer.qMRMLSubjectHierarchyComboBox()
shComboBox.nodeTypes = ["vtkMRMLMarkupsFiducialNode"]
shComboBox.noneEnabled = True
shComboBox.setMRMLScene(slicer.mrmlScene)

layout.addWidget(qt.QLabel("Select a fiducial (SubjectHierarchyComboBox):"))
layout.addWidget(shComboBox)

runButton = qt.QPushButton("Move selected node into folder (triggers reset)")
layout.addWidget(runButton)

parameterNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScriptedModuleNode", "SHCombo_ParamNode_Test")
parameter = SHTestParameters(parameterNode)

shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)

def printState(tag):
    print(f"\n[{tag}]")
    print("  shComboBox.currentItem():", shComboBox.currentItem())
    print("  shComboBox.currentNode():", shComboBox.currentNode())
    print("  parameter.selectedFiducial:", parameter.selectedFiducial)

# sync combo box to the parameter node similar to the original module
def onComboChanged(itemId):
    node = shNode.GetItemDataNode(itemId)
    parameter.selectedFiducial = node
    printState("Combo changed (GUI->param sync fired)")

shComboBox.connect("currentItemChanged(vtkIdType)", onComboChanged)

#Add the parameter node to a folder
def onRun():
    currentItemID = shComboBox.currentItem()
    if currentItemID == 0:
        print("No selection (currentItemID==0)")
        return

    # seed parameter node explicitly (like your "store orbitLm/plateLm first")
    parameter.selectedFiducial = shNode.GetItemDataNode(currentItemID)

    printState("BEFORE MOVE")

    folderItemID = shNode.CreateFolderItem(shNode.GetSceneItemID(), "SHCombo_TestFolder")
    shNode.SetItemParent(currentItemID, folderItemID)

    printState("AFTER MOVE (post SetItemParent)")

runButton.connect("clicked()", onRun)

dialog.show()
dialog.raise_()
dialog.activateWindow()


You should see that the node is put under a newly created folder, but the comboBox is reset to ‘None’

In Python console, you should see parameter node set to None because the parameter node is synced with the combobox by def onComboChanged the as in the original module:

[AFTER MOVE (post SetItemParent)]
shComboBox.currentItem(): 0
shComboBox.currentNode(): None
parameter.selectedFiducial: None

Interestingly, in the same scene, when I simply ran the same function again, the combobox did not set to ‘None’ afterwards. The error only happened when run the function first time in a fresh Slicer scene.

Let me know if this one works.