How to use Python to achieve the thresholding effect of the ‘Volumes’ module? (synchronized with the volume rendering)

Hello Mike and Charles,
Thank you very much for your kind and patient explanations. Now I can successfully run this code and achieve the purpose of thresholding the volume rendering image. I learned a lot from this exercise.
Thanks again!

1 Like

Hello Mike @mikebind ,
Following the advice and code in this post of yours, I’ve been able to successfully add this slider for adjusting the threshold of volume-rendered images to my module.
As shown below: For this part of my module, users needs to set the input volume first to do the next Thresholding step.

figure1

However now I find that when I switch the input volume to the a new one(after I load one more volume as input here), the slider doesn’t work anymore:
figure2

It seems that it only works on the first individual input volume.
I attached my code here, could you please give me some suggestions to solve this bug?
When you test it, please make sure the checkbox of ‘Synchronize with Volumes module’ is checked.
Thank you very much!!!

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 = "My Test Module" # TODO make this more human readable by adding spaces
    self.parent.categories = ["Test Extension 2"]
    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 = "Threshold"
    self.layout.addWidget(parametersCollapsibleButton)
    parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)

    #
    # 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." )
    parametersFormLayout.addRow("Input Volume: ", self.inputSelector)

    #
    # thresholdRangeSlider
    #
    self.thresholdRangeSlider1 = slicer.qMRMLVolumeThresholdWidget()
    parametersFormLayout.addRow(self.thresholdRangeSlider1)

    def updateThresholdOnVolume(volNode, lower, upper):
      displayNode = volNode.GetDisplayNode()
      displayNode.SetThreshold(lower, upper)
      displayNode.ApplyThresholdOn()

    updateThreshold = lambda lower, upper: updateThresholdOnVolume(volNode, lower, upper)

    volNode = slicer.util.getNode(self.inputSelector.currentNode().GetName())
    self.thresholdRangeSlider1.setMRMLVolumeNode(volNode)

    self.logic = ScriptedLoadableModuleTemplateLogic()
    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.thresholdRangeSlider1.connect('thresholdValuesChanged(double, double)', updateThreshold)


    self.initializeParameterNode()

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


  # def cleanup(self):
  #   pass
  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"))
    # 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.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)





The basic problem is that there is nothing which triggers an update of the volume node associated with the volume threshold widget when the input volume is changed. There are many possible ways to address this. Maybe the simplest is to do the following

# in your module widget's setup() function:
# Change the inputSelector connection callback to a new function
self.inputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onInputVolumeSelectorChange)

And then define that callback

# Add this function under the ScriptedLoadableModuleTemplateWidget class, the
# at the same indent level as setup(), updateParameterNodeFromGUI(), etc. 
def onInputVolumeSelectorChange(self, newInputVolumeNode):
    # Update the parameter node (so that your selection would be saved and restored with the scene)
    self._parameterNode.SetNodeReferenceID("InputVolume", newInputVolumeNode.currentNodeID)
    # Update the threshold slider's volume to the new selection
    self.thresholdRangeSlider1.setMRMLVolumeNode(newInputVolumeNode)

I haven’t had a chance to test this yet, but might be able to later.

Dear Mike @mikebind,
Thank you so much for your kind and fast reply!
I’ve tried the code you just provided.
However, some error logs pop up in the console:

Traceback (most recent call last):
  File "E:/MySlicerExtensions/ScriptedLoadableExtensionTemplate/ScriptedLoadableModuleTemplate/ScriptedLoadableModuleTemplate.py", line 215, in onInputVolumeSelectorChange
    self._parameterNode.SetNodeReferenceID("InputVolume", newInputVolumeNode.currentNodeID)
AttributeError: 'NoneType' object has no attribute 'currentNodeID'
Traceback (most recent call last):
  File "E:/MySlicerExtensions/ScriptedLoadableExtensionTemplate/ScriptedLoadableModuleTemplate/ScriptedLoadableModuleTemplate.py", line 215, in onInputVolumeSelectorChange
    self._parameterNode.SetNodeReferenceID("InputVolume", newInputVolumeNode.currentNodeID)
AttributeError: 'MRMLCore.vtkMRMLScalarVolumeNode' object has no attribute 'currentNodeID'

In fact, this bug has been bothering me for several days, I have been collecting a lot of materials to learn to solve it, but it still doesn’t work…

If possible, could you please make some changes based on my code to fix this bug? Or could you please just to create a module that only contains this slider using the ‘extension wizard’?
Your guidance is really like sunshine to me, thank you so much!!!

Hello Mike @mikebind ,

Based on the error logs Daniel posted, does it mean that the ‘newInputVolumeNode’ is empty? How to make it store the new volume node?

Thank you so much!

On testing, there are several problems. I’ll try to get a version working later today if I get a chance.

Hello Mike,
Thank you very much! :+1:

OK, this was harder than I expected :slight_smile: The following pretty much works…

The remaining problem is that the slider seems to continue to affect the threshold values for input volumes which have been used before but switched away from. I think this is a bug related to a Qt signal/slot issue, where a disconnect needs to happen in addition to adding a new connection when the setMRMLVolumeNode() function is used, but I’m not totally sure. I have to put this down at the moment, but hopefully it’s helpful.

Dear Mike @mikebind,

Thank you so much for your hard work!
I’ve test the scirpt you provided, but just as you mentioned, the remaining bug here is that the slider continue to affect the volume switched away from.
Since I’m a beginner, I also realize that although this effect looks simple, it is difficult to implement… I have referenced the ‘updateGUIFromParameterNode’ method and ‘updateParameterNodeFromGUI’ method in TemplateKey.py and tried to make GUI elements like the slider update synchronously when the parameter node changes. But it still doesn’t work…
If you can fix it in next few days that will be very great.
Thank you very much!
Dear @lassoan, or do you have any specific suggestions to address this based on the script TestDebug1.py @mikebind just provided?
Thanks again! :+1:

What you implemented should work, you just need to make sure that when you switch nodes you also switch which display nodes you modify.

By the way, I have never come across a use case when synchronization of the window/level between slice display and volume rendering was useful. The two visualization modes are just too different. Can you describe what is your overall goal?

If you just want to change the volume rendering settings then I would recommend to set the transfer function points directly. See code snippet in the script repository or a complete example in LungCTAnalyzer module.

1 Like

Hello Lassoan,

My goal is to implement the same ‘threshold’ slider as in the ‘Volumes’ module to control the threshold of the volume rendering image synchronously through a slider. I’m trying to extract this feature and transfer it to a python scripted module(This is one of step of the workflow of my module, next will be volume calculation etc.).

Currently, under @mikebind 's guidance, I can use the slider to modify the threshold of the volume rendering synchronously:

picture1
However, when I switch the input volume to a new one(after I load one more volume as input here), the slider doesn’t work anymore, it only works on the first individual input volume’s volume rendering.

you just need to make sure that when you switch nodes you also switch which display nodes you modify.

The basic problem is that there is nothing which triggers an update of the volume node associated with the volume threshold widget. when the input volume is changed.

What you and Mike mentioned seems to be the reason. But I’m still confused how to do it.

Therefore, I hope to get more help from both of you. Or could you please give me some specific guidance based on the code Mike provided TestDebug1? Because what I need to modify is the volume rendering, here is the key function to do the modification:

  def updateThresholdOnVolume(self, volNode, lower, upper):
    displayNode = volNode.GetDisplayNode()
    displayNode.SetThreshold(lower, upper)
    displayNode.ApplyThresholdOn()

I hope that through my description, you can further understand my purpose.
Thank you so much!

I would recommend what I described above - do not try to shift the existing points of the transfer functions but explicitly set the transfer functions that you want.

Install the LungCTAnalysis extension and try how volume rendering works in the Lung CT Analysis module (how the slider moves the transfer functions).

Another simple example is Pedicle Screw Simulator extension (it just sets a fixed transfer function):

It is just so much simpler to set the transfer functions than reimplementing the shift feature. Reimplementation is needed because the transfer function shift feature is inside a widget (qSlicerVolumeRenderingPresetComboBox.cxx) instead of a reusable logic class.

1 Like

Looking ahead, volume calculation is not possible from a volume rendering. Volume rendering works by dynamically casting rays through an image volume from the perspective of the camera and displaying the result. To measure the volume of what is visible would require creating a segmentation and then measuring the segmentation.

This problem is solved by the code in the gist above, which does switch the volume rendering display to the new volume.

This is also addressed in the code in the gist. This happens in the onInputVolumeSelectorChange() function, on line 111 of the gist.

I agree with @lassoan that it doesn’t seem very useful to have the visualization threshold in the slice view linked to the thresholds in the volume rendering, since the two views are quite different (it’s helpful to hide things in the rendering, but not usually helpful in the slice views). However, since that was what you had asked for, I was trying to help you see how to achieve that.

Andras, I think there may be a bug in the setMRMLVolumeNode() of the qMRMLVolumeThresholdWidget. When a new volume is set, I think a link is maintained to the previous volume. Possibly there is a missing disconnect of the signal and slot in Qt? I’m not very good at debugging in C++, but if I can generate a minimal example I’ll post a bug report in the Slicer github issues page (that’s the correct place, right?).

@WilliamDaniel , I think @lassoan 's suggestion to set the transfer function directly makes a lot of sense and would sidestep this bug. Also, definitely consider the following steps of your workflow. Do you even need volume rendering? Or do you need to segment? I would recommend that you try out your complete workflow interactively (just figuring out the sequence of steps in the Slicer GUI) and make sure you can accomplish all the steps you want using the approach you are considering.

1 Like

Hello Mike,
If possible, may I have a video chat with you for about 10-15 minutes to explain my workflow further to you? Any time when you are free is ok. Because I’m still confused about this point you mentioned:

it doesn’t seem very useful to have the visualization threshold in the slice view linked to the thresholds in the volume rendering, since the two views are quite different.
Just give me a chance to describe my workflow more clear.

Here is my email: realdaniel021@gmail.com
Thank you so much for your kind help! :pray:

@lassoan disregard my possible bug report here. It was just a failure to update the parameter node appropriately. The threshold widget was controlling one volume’s display node, and the module code was controlling another (getting an outdated reference from the parameter node).

@WilliamDaniel, I’ll follow up via email.

In the meantime, I’ve updated the gist which will now work without affecting the wrong image (the same gist link above will show the revised code).

2 Likes

Hello Mike,

Thank you very much, now the gist can run successfully!
But I found that there is still a small bug here, and that is the ‘crop’ function in ‘Volume rendering’ module seems to not work after I load this scripted module in slicer.

As you can see, I have the checked the checkbox of ‘Enable’ and ‘Display ROI’, but after I adjust the bounding box, the volume rendering image doesn’t change…

Meanwhile, I’ll also continue to learn from the script you provided, it’s really helpful!
Thank you!

Try that without using the custom module. I think the experimental MultiVolume renderer may not have the ROI crop functionality. ROI cropping works for me in using GPU or CPU ray casting renderers.

1 Like

Hello Mike,
All right, got you!
The last thing is that whenever I close Slicer, there are always an error popup here:

FIGURE
I tried adding displayNode.UnRegister(slicer.mrmlScene) but it doesn’t work.
Do you know how to avoid these errors?
Thank you so much!

You were on the right track. This was caused by the use of slicer.mrmlScene.GetNodesByClass(). See here for an explanation.

I updated the gist with the calls to properly unregister the extra references, so I don’t get the leak errors any more on closing Slicer. When you’re testing, make sure you close and reopen Slicer after updating the code to see whether the fix works for you.

1 Like

Hello Mike,

Thank you very much for your guidance, now it totally working! :+1: