How to update the state of checkbox when switching input volume node

Hi there,
I’m new to 3D Slicer and now I’m developing a Python scripted module.
Currently, I’m trying to add a checkbox to control the display of the ROI bounding box in the volume rendering scene, which is actually the same as the ‘Display ROI’ checkbox in the volume rendering module.
Now the ROI bounding box can be shown by clicking the checkbox, but the problem is that when I switch the input volume, the state of the checkbox on the GUI panel is always the same.

As shown in the figure, I first turned on the ROI bounding box for ‘MRHead’, but when I switch the input volume to a new volume(‘CTChest’), the state of the checkbox is also checked, but I haven’t checked it yet:


Is there any way to make the state of the checkbox and the input volume correspond one-to-one?

This seems to be because there doesn’t seem to be a connection between the input volume node and the checkbox widget.
However, the checkbox widget derived from CTK:

self.ROICheckBox = ctk.ctkCheckBox()

It seems means I can’t connect it to the input volume by something like:

self.ROICheckBox.setMRMLVolumeNode(VolumeNode)

I attached my source code below, hoping to get specific guidance on how to fix this bug. Please make sure you turn on the volume rendering for both volumes first to test it.

Thank you very much in advance!

import logging
import os
import unittest
import vtk, qt, ctk, slicer
import SegmentStatistics
from slicer.ScriptedLoadableModule import *
from slicer.util import TESTING_DATA_URL
from slicer.util import VTKObservationMixin



class ScriptedLoadableModuleTemplate(ScriptedLoadableModule):
  """Uses ScriptedLoadableModule base class, available at:
  https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  """

  def __init__(self, parent):
    ScriptedLoadableModule.__init__(self, parent)
    self.parent.title = "TestCheckbox" # TODO make this more human readable by adding spaces
    self.parent.categories = ["Examples"]
    self.parent.dependencies = []
    self.parent.contributors = ["John Doe (AnyWare Corp.)"] # replace with "Firstname Lastname (Organization)"
    self.parent.helpText = """
    The Help text for this scripted module.
"""


    self.parent.helpText += self.getDefaultModuleDocumentationLink()

    self.parent.acknowledgementText = """
   The acknowledgementText
"""



#
# ScriptedLoadableModuleTemplateWidget
#

class ScriptedLoadableModuleTemplateWidget(ScriptedLoadableModuleWidget, VTKObservationMixin):
  """Uses ScriptedLoadableModuleWidget base class, available at:
  https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  """



  def __init__(self, parent=None):
    """
    Called when the user opens the module the first time and the widget is initialized.
    """
    ScriptedLoadableModuleWidget.__init__(self, parent)
    VTKObservationMixin.__init__(self)  # needed for parameter node observation
    self.logic = None
    self._parameterNode = None
    self._updatingGUIFromParameterNode = False



  def setup(self):
    ScriptedLoadableModuleWidget.setup(self)

    # Instantiate and connect widgets ...


    #
    # Parameters Area
    #
    parametersCollapsibleButton = ctk.ctkCollapsibleButton()
    parametersCollapsibleButton.text = "ROI"
    self.layout.addWidget(parametersCollapsibleButton)
    parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)

    self.logic = ScriptedLoadableModuleTemplateLogic()


    #
    # input volume selector
    #
    self.inputSelector = slicer.qMRMLNodeComboBox()
    self.inputSelector.nodeTypes = ["vtkMRMLScalarVolumeNode"]
    self.inputSelector.selectNodeUponCreation = True
    self.inputSelector.addEnabled = False
    self.inputSelector.removeEnabled = False
    self.inputSelector.noneEnabled = False
    self.inputSelector.showHidden = False
    self.inputSelector.showChildNodeTypes = False
    self.inputSelector.setMRMLScene(slicer.mrmlScene)
    self.inputSelector.setToolTip( "Pick the input to the algorithm." )
    parametersFormLayout.addRow("Input Volume: ", self.inputSelector)


    #
    # ROI checkbox
    #
    self.ROICheckBox =  ctk.ctkCheckBox()
    self.ROICheckBox.enabled = True
    self.ROICheckBox.checked = False
    parametersFormLayout.addRow("ROI:", self.ROICheckBox)
    # self.ROICheckBox.setMRMLVolumeNode(volNode)


    self.addObserver(slicer.mrmlScene, slicer.mrmlScene.StartCloseEvent, self.onSceneStartClose)
    self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndCloseEvent, self.onSceneEndClose)

    # connections
    self.inputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI)
    self.ROICheckBox.connect('clicked(bool)', self.onCheckBox)


    self.initializeParameterNode()

    # Add vertical spacer
    self.layout.addStretch(1)


  def onCheckBox(self):
    VolumeNode = self._parameterNode.GetNodeReference('InputVolume')
    if VolumeNode:
      self.logic.updateROIOnVolume(VolumeNode, self.ROICheckBox.checked)



  def cleanup(self):
    """
    Called when the application closes and the module widget is destroyed.
    """
    self.removeObservers()


  def enter(self):
    """
    Called each time the user opens this module.
    """
    # Make sure parameter node exists and observed
    self.initializeParameterNode()


  def exit(self):
    """
    Called each time the user opens a different module.
    """
    # Do not react to parameter node changes (GUI wlil be updated when the user enters into the module)
    self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode)

  def onSceneStartClose(self, caller, event):
    """
    Called just before the scene is closed.
    """
    # Parameter node will be reset, do not use it anymore
    self.setParameterNode(None)

  def onSceneEndClose(self, caller, event):
    """
    Called just after the scene is closed.
    """
    # If this module is shown while the scene is closed then recreate a new parameter node immediately
    if self.parent.isEntered:
      self.initializeParameterNode()




  def initializeParameterNode(self):
    """
    Ensure parameter node exists and observed.
    """
    # Parameter node stores all user choices in parameter values, node selections, etc.
    # so that when the scene is saved and reloaded, these settings are restored.

    self.setParameterNode(self.logic.getParameterNode())

    # Select default input nodes if nothing is selected yet to save a few clicks for the user
    if not self._parameterNode.GetNodeReference("InputVolume"):
      firstVolumeNode = slicer.mrmlScene.GetFirstNodeByClass("vtkMRMLScalarVolumeNode")
      if firstVolumeNode:
        self._parameterNode.SetNodeReferenceID("InputVolume", firstVolumeNode.GetID())


  def setParameterNode(self, inputParameterNode):
    """
    Set and observe parameter node.
    Observation is needed because when the parameter node is changed then the GUI must be updated immediately.
    """
    # Unobserve previously selected parameter node and add an observer to the newly selected.
    # Changes of parameter node are observed so that whenever parameters are changed by a script or any other module
    # those are reflected immediately in the GUI.
    if self._parameterNode is not None:
      self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode)
    self._parameterNode = inputParameterNode
    if self._parameterNode is not None:
      self.addObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode)

    # Initial GUI update
    self.updateGUIFromParameterNode()


  def updateGUIFromParameterNode(self, caller=None, event=None):
    """
    This method is called whenever parameter node is changed.
    The module GUI is updated to show the current state of the parameter node.
    """

    if self._parameterNode is None or self._updatingGUIFromParameterNode:
      return

    # Make sure GUI changes do not call updateParameterNodeFromGUI (it could cause infinite loop)
    self._updatingGUIFromParameterNode = True

    # Update node selectors and sliders
    self.inputSelector.setCurrentNode(self._parameterNode.GetNodeReference("InputVolume"))
    self.ROICheckBox.checked = (self._parameterNode.GetParameter("ROI") == "true")

    # All the GUI updates are done
    self._updatingGUIFromParameterNode = False



  def updateParameterNodeFromGUI(self, caller=None, event=None):
    """
    This method is called when the user makes any change in the GUI.
    The changes are saved into the parameter node (so that they are restored when the scene is saved and loaded).
    """

    if self._parameterNode is None or self._updatingGUIFromParameterNode:
      return

    wasModified = self._parameterNode.StartModify()  # Modify all properties in a single batch

    self._parameterNode.SetNodeReferenceID("InputVolume", self.inputSelector.currentNodeID)
    self._parameterNode.SetParameter("ROI", "true" if self.ROICheckBox.checked else "false")

    self._parameterNode.EndModify(wasModified)


#
# ScriptedLoadableModuleTemplateLogic
#

class ScriptedLoadableModuleTemplateLogic(ScriptedLoadableModuleLogic):
  """This class should implement all the actual
  computation done by your module.  The interface
  should be such that other python code can import
  this class and make use of the functionality without
  requiring an instance of the Widget.
  Uses ScriptedLoadableModuleLogic base class, available at:
  https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  """


  def __init__(self):
    """
    Called when the logic class is instantiated. Can be used for initializing member variables.
    """
    ScriptedLoadableModuleLogic.__init__(self)



  def updateROIOnVolume(self, VolumeNode, ROIchecked=False):
    VolumeLogic = slicer.modules.volumerendering.logic()
    displayNode = VolumeLogic.GetFirstVolumeRenderingDisplayNode(VolumeNode)
    displayNode.UnRegister(slicer.mrmlScene)
    slicer.mrmlScene.AddNode(displayNode)
    VolumeNode.AddAndObserveDisplayNodeID(displayNode.GetID())
    VolumeLogic.UpdateDisplayNodeFromVolumeNode(displayNode, VolumeNode)
    if ROIchecked == True:
      displayNode.GetROINode().SetDisplayVisibility(True)
      displayNode.SetCroppingEnabled(True)
    else:
      displayNode.GetROINode().SetDisplayVisibility(False)




Hello @mikebind ,
Could you please take a look at my code and point out how to address it?
Thank you!

There is no direct linkage between the checkbox, the ROI node, and the volume rendering which would mean that a change in the checkbox or in the volume would lead to a change in which ROI is shown. Your module code needs to handle that process. Here is what I think you want to happen (correct me if I am wrong!):

Changing to a different input volume in the “Input volume” combobox should

  1. Make the ROI from the prior image volume invisible, regardless of the ROI checkbox state, and
  2. Make the ROI from the new image volume visible, only if the ROI checkbox state is checked.

Independently, toggling the ROI checkbox should make the ROI associated with the current input volume visible when checked and invisible when unchecked.

Is that correct?

Hello @mikebind, Yes, you are correct, you mentioned above is what exactly I want. However, about the first thing you mentioned:

Make the ROI from the prior image volume invisible, regardless of the ROI checkbox state.

I think when user change the input volume in the combobox, the ROI from the prior image volume can still be visible only if he did just turn on its checkbox.

Could you make some change based on the code I just post to address it? Thank you very much!

And what I’d like to achieve is actually same as the ‘Display ROI’ checkbox in the volume rendering module:


This gist starts from your code (first revision) and then has all the changes to make the behavior I described in the post above (How to update the state of checkbox when switching input volume node - #3 by mikebind) (see the second revision).

See if you can use that to get the behavior you want. If I understand correctly, you should be able to get that behavior just by removing the code which hides the prior volume ROI.

Hello @mikebind,
Thank you for your kind reply!
I tested the code you just provided. However, I found that the ROI Checkbox doesn’t work for the second input volume, as you can see, when I uncheck it, the ROI checkbox is still there:

figure2
figure1

Could you take a look at my previous/last post and figures:

However, about the first thing you mentioned: Make the ROI from the prior image volume invisible, regardless of the ROI checkbox state. I think when user change the input volume in the combobox, the ROI from the prior image volume can still be visible only if he did just turn on its checkbox. And what I’d like to achieve is actually same as the ‘Display ROI’ checkbox in the volume rendering module.

, and see if you can totally get what I describe? Thank you so much!

On my machine the ROI visibility follows the checkbox with that code. Can you check to see if an error was generated on yours? There is an indicator in the lower right of the window which turns into a red X if there has been an error, and if you click on it you can see the log.
image

Note that I changed the name of the .py file to TestDebug2.py and made the corresponding changes to all the python class names (e.g. ScriptedLoadableModuleTemplateLogic became TestDebug2Logic). So, you would need to add the path to TestDebug2.py in your “Additional Module Paths” in the application settings (or, maybe simpler, change the class names back to what you had earlier; they have to match the file name or the module will not load).

OK, so you want the ROI checkbox to just reflect the visibility state of the ROI associated with the chosen node? So, if a user changes the input volume, the ROI checkbox should show whether or not the ROI for the new volume is already visible. And then when the user toggles the checkbox, that should also toggle the ROI visibility for the current input volume?

I updated the gist to behave like the toggle in the Volume Rendering module. Same link: https://gist.github.com/mikebind/a3a4ee96076ab23d0e0339a47d02adfb

1 Like

Hello @mikebind ,
Thank you so much for your kindness and patience! Now everything working smoothly! :+1:

1 Like

I am trying to add a checkbox to update the model display. How can I do this?

I am running into error

object has no attribute