Skip to content

Commit

Permalink
Add a parameter set controller.
Browse files Browse the repository at this point in the history
GuidedVeinSegmentation

Using a single wrapped parameter node and multiple vtkMRMLScriptedModuleNode
instances, each defining a parameter set.

At the same time, use the wrapped parameter node in logic.
  • Loading branch information
chir-set committed Nov 9, 2024
1 parent cc0b838 commit 6382571
Show file tree
Hide file tree
Showing 2 changed files with 233 additions and 62 deletions.
225 changes: 163 additions & 62 deletions GuidedVeinSegmentation/GuidedVeinSegmentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,6 @@ def __init__(self, parent):

@parameterNodeWrapper
class GuidedVeinSegmentationParameterNode:
"""
The parameters needed by module.
inputVolume - The volume to threshold.
imageThreshold - The value at which to threshold the input volume.
invertThreshold - If true, will invert the threshold.
thresholdedVolume - The output volume that will contain the thresholded volume.
invertedVolume - The output volume that will contain the inverted thresholded volume.
"""
inputCurve: slicer.vtkMRMLMarkupsCurveNode
inputVolume: slicer.vtkMRMLScalarVolumeNode
inputSegmentation: slicer.vtkMRMLSegmentationNode
Expand All @@ -81,8 +72,10 @@ def __init__(self, parent=None) -> None:
ScriptedLoadableModuleWidget.__init__(self, parent)
VTKObservationMixin.__init__(self) # needed for parameter node observation
self.logic = None
self._parameterNode = None
self._wrappedParameterNode = None
self._mrmlParameterNode = None
self._parameterNodeGuiTag = None
self._parameterNodeBeingModified = False

def setup(self) -> None:
"""
Expand All @@ -107,18 +100,32 @@ def setup(self) -> None:
# Create logic class. Logic implements all computations that should be possible to run
# in batch mode, without a graphical user interface.
self.logic = GuidedVeinSegmentationLogic()
'''
Do not use the 'ModuleName' attribute. The logic's vtkMRMLScriptedModuleNode inner
parameter node has the same 'ModuleName' attribute with the module's name as value.
This inner MRML parameter node will appear as a default node in the combobox, and
it must not be used here at all. Ensure that the basename of the selector is not
the same as the module's name.
'''
parameterSetBaseName = self.ui.parameterSetSelector.baseName
if parameterSetBaseName == self.moduleName:
logging.warning("The basename of the parameter set selector must not be equal to the module's name.")
# Don't return, though bad things will happen.
self.ui.parameterSetSelector.addAttribute("vtkMRMLScriptedModuleNode", "BaseName", parameterSetBaseName)

# Connections

self.ui.parameterSetSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onMrmlParameterNodeChanged)
self.ui.parameterSetSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.updateSliceViews)

# These connections ensure that we update parameter node when scene is closed
self.addObserver(slicer.mrmlScene, slicer.mrmlScene.StartCloseEvent, self.onSceneStartClose)
self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndCloseEvent, self.onSceneEndClose)

# Buttons
self.ui.applyButton.connect('clicked(bool)', self.onApplyButton)

# Make sure parameter node is initialized (needed for module reload)
self.initializeParameterNode()
self.initializeWrappedParameterNode()

def cleanup(self) -> None:
"""
Expand All @@ -131,79 +138,151 @@ def enter(self) -> None:
Called each time the user opens this module.
"""
# Make sure parameter node exists and observed
self.initializeParameterNode()
if not self._wrappedParameterNode:
self.initializeWrappedParameterNode()
else:
self.onMrmlParameterNodeChanged(self.ui.parameterSetSelector.currentNode())

def exit(self) -> None:
"""
Called each time the user opens a different module.
"""
# Do not react to parameter node changes (GUI will be updated when the user enters into the module)
if self._parameterNode:
self._parameterNode.disconnectGui(self._parameterNodeGuiTag)
self._parameterNodeGuiTag = None

def onSceneStartClose(self, caller, event) -> None:
"""
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) -> None:
"""
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) -> None:
# Restore wrapped parameter node to defaults and set MRML parameter node to None.
self.onMrmlParameterNodeChanged(None)

def restoreWrappedParameterNode(self) -> None:
if not self._wrappedParameterNode:
return
# Restore to default values.
self._wrappedParameterNode.inputCurve = None
self._wrappedParameterNode.inputVolume = None
self._wrappedParameterNode.inputSegmentation = None
self._wrappedParameterNode.extrusionKernelSize = 5.0
self._wrappedParameterNode.gaussianStandardDeviation = 2.0
self._wrappedParameterNode.seedRadius = 1.0
self._wrappedParameterNode.shellMargin = 18.0
self._wrappedParameterNode.shellThickness = 2.0
self._wrappedParameterNode.subtractOtherSegments = True

def initializeWrappedParameterNode(self) -> None:
"""
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())
# Called only once.
self.setWrappedParameterNode(self.logic.getParameterNode())

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

def setParameterNode(self, inputParameterNode: Optional[GuidedVeinSegmentationParameterNode]) -> None:
def setWrappedParameterNode(self, inputParameterNode: Optional[GuidedVeinSegmentationParameterNode]) -> None:
"""
Set and observe parameter node.
Observation is needed because when the parameter node is changed then the GUI must be updated immediately.
"""
# If the wrapped parameter node has already been initialized, don't do anything.
if self._wrappedParameterNode:
return

if self._parameterNode:
self._parameterNode.disconnectGui(self._parameterNodeGuiTag)
self._parameterNode = inputParameterNode
if self._parameterNode:
# We get here once only throughout a Slicer's session.
self._wrappedParameterNode = inputParameterNode

if self._wrappedParameterNode:
# Note: in the .ui file, a Qt dynamic property called "SlicerParameterName" is set on each
# ui element that needs connection.
self._parameterNodeGuiTag = self._parameterNode.connectGui(self.ui)
self._parameterNodeGuiTag = self._wrappedParameterNode.connectGui(self.ui)
self.addObserver(self._wrappedParameterNode, vtk.vtkCommand.ModifiedEvent, self.updateMrmlParameterNode)
# Add an initial MRML parameter node.
if self.ui.parameterSetSelector.nodeCount() == 0:
self.ui.parameterSetSelector.addNode("vtkMRMLScriptedModuleNode")

def onMrmlParameterNodeChanged(self, parameterNode: slicer.vtkMRMLScriptedModuleNode):
# The current item has changed in the selector.
if not self._wrappedParameterNode:
return

self._mrmlParameterNode = parameterNode
if not self._mrmlParameterNode:
self.restoreWrappedParameterNode()
return

if parameterNode.GetParameterCount() == 0:
# Set default values of an empty MRML parameter node.
parameterNode.AddNodeReferenceID("inputCurve", None)
parameterNode.AddNodeReferenceID("inputVolume", None)
parameterNode.AddNodeReferenceID("inputSegmentation", None)
parameterNode.SetParameter("extrusionKernelSize", "5.0")
parameterNode.SetParameter("gaussianStandardDeviation", "2.0")
parameterNode.SetParameter("seedRadius", "1.0")
parameterNode.SetParameter("shellMargin", "18.0")
parameterNode.SetParameter("shellThickness", "2.0")
parameterNode.SetParameter("subtractOtherSegments", "1")

# Avoid a loop with updateMrmlParameterNode.
self._parameterNodeBeingModified = True

# Update the wrapped parameter node and the connected GUI.
wrappedParameterNode = self._wrappedParameterNode
wrappedParameterNode.inputCurve = parameterNode.GetNodeReference("inputCurve")
wrappedParameterNode.inputVolume = parameterNode.GetNodeReference("inputVolume")
wrappedParameterNode.inputSegmentation = parameterNode.GetNodeReference("inputSegmentation")
wrappedParameterNode.extrusionKernelSize = float(parameterNode.GetParameter("extrusionKernelSize"))
wrappedParameterNode.gaussianStandardDeviation = float(parameterNode.GetParameter("gaussianStandardDeviation"))
wrappedParameterNode.seedRadius = float(parameterNode.GetParameter("seedRadius"))
wrappedParameterNode.shellMargin = float(parameterNode.GetParameter("shellMargin"))
wrappedParameterNode.shellThickness = float(parameterNode.GetParameter("shellThickness"))
wrappedParameterNode.subtractOtherSegments = False if (parameterNode.GetParameter("subtractOtherSegments") == "False") else True

self._parameterNodeBeingModified = False

def updateMrmlParameterNode(self, caller=None, event=None):
# Avoid a loop with onMrmlParameterNodeChanged.
if self._parameterNodeBeingModified:
return
if not self._wrappedParameterNode:
return

# Update the MRML parameter node of the combobox when a widget is changed bu user interaction.
mrmlParameterNode = self._mrmlParameterNode
wrappedParameterNode = self._wrappedParameterNode
if mrmlParameterNode:
mrmlParameterNode.SetNodeReferenceID("inputCurve", None if not wrappedParameterNode.inputCurve else wrappedParameterNode.inputCurve.GetID())
mrmlParameterNode.SetNodeReferenceID("inputVolume", None if not wrappedParameterNode.inputVolume else wrappedParameterNode.inputVolume.GetID())
mrmlParameterNode.SetNodeReferenceID("inputSegmentation", None if not wrappedParameterNode.inputSegmentation else wrappedParameterNode.inputSegmentation.GetID())
mrmlParameterNode.SetParameter("extrusionKernelSize", str(wrappedParameterNode.extrusionKernelSize))
mrmlParameterNode.SetParameter("gaussianStandardDeviation", str(wrappedParameterNode.gaussianStandardDeviation))
mrmlParameterNode.SetParameter("seedRadius", str(wrappedParameterNode.seedRadius))
mrmlParameterNode.SetParameter("shellMargin", str(wrappedParameterNode.shellMargin))
mrmlParameterNode.SetParameter("shellThickness", str(wrappedParameterNode.shellThickness))
mrmlParameterNode.SetParameter("subtractOtherSegments", str(wrappedParameterNode.subtractOtherSegments))

def updateSliceViews(self):
wrappedParameterNode = self._wrappedParameterNode
if (not wrappedParameterNode) or (not wrappedParameterNode.inputVolume):
return
slicer.util.setSliceViewerLayers(background = wrappedParameterNode.inputVolume.GetID(), fit = True)

def onApplyButton(self) -> None:
"""
Run processing when user clicks "Apply" button.
"""
with slicer.util.tryWithErrorDisplay(_("Failed to compute results."), waitCursor=True):
if not self.ui.parameterSetSelector.currentNode():
slicer.util.showStatusMessage(_("No parameter set defined."), 3000)
logging.error("No parameter set defined.")
return

# Compute output
self.logic.process(self._parameterNode.inputCurve,
self._parameterNode.inputVolume,
self._parameterNode.inputSegmentation,
self._parameterNode.extrusionKernelSize,
self._parameterNode.gaussianStandardDeviation,
self._parameterNode.seedRadius,
self._parameterNode.shellMargin,
self._parameterNode.shellThickness,
self._parameterNode.subtractOtherSegments)
self.logic.process()

#
# GuidedVeinSegmentationLogic
Expand All @@ -224,20 +303,42 @@ def __init__(self) -> None:
Called when the logic class is instantiated. Can be used for initializing member variables.
"""
ScriptedLoadableModuleLogic.__init__(self)
'''
Connecting a wrapped parameter node:
self._parameterNodeGuiTag = self._wrappedParameterNode.connectGui(self.ui)
_parameterNodeGuiTag is always None because connectGui() does not return.
Hence disconnectGui() doesn't seem to do anything.
Use a single wrapped parameter node in this Slicers' session.
Avoid connecting multiple wrapped parameter nodes disputing the UI.
Update this parameter node explicitly whenever required.
'''
self._parameterNode = None
self.initializeParameterNode()

def initializeParameterNode(self):
self._parameterNode = GuidedVeinSegmentationParameterNode(super().getParameterNode())

def getParameterNode(self):
return GuidedVeinSegmentationParameterNode(super().getParameterNode())

def process(self,
inputCurve: slicer.vtkMRMLMarkupsCurveNode,
inputVolume: slicer.vtkMRMLScalarVolumeNode,
inputSegmentation: slicer.vtkMRMLSegmentationNode,
extrusionKernelSize: float = 5.0,
gaussianStandardDeviation: float = 2.0,
seedRadius: float = 1.0,
shellMargin: float = 18.0,
shellThickness: float = 2.0,
subtractOtherSegments: bool = True) -> None:
'''
A single wrapped parameter node throughout, because a connected GUI is never disconnected.
This is consistent with the fact that super().getParameterNode()
gives a same vtkMRMLScriptedModuleNode object.
Update this wrapped parameter node whenever the parameter set changes.
'''
return self._parameterNode

def process(self) -> None:
inputCurve = self._parameterNode.inputCurve
inputVolume = self._parameterNode.inputVolume
inputSegmentation = self._parameterNode.inputSegmentation
extrusionKernelSize = self._parameterNode.extrusionKernelSize
gaussianStandardDeviation = self._parameterNode.gaussianStandardDeviation
seedRadius = self._parameterNode.seedRadius
shellMargin = self._parameterNode.shellMargin
shellThickness = self._parameterNode.shellThickness
subtractOtherSegments = self._parameterNode.subtractOtherSegments

if not inputCurve or not inputVolume or not inputSegmentation:
raise ValueError(_("Input curve or volume or segmentation is invalid."))
Expand Down
70 changes: 70 additions & 0 deletions GuidedVeinSegmentation/Resources/UI/GuidedVeinSegmentation.ui
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,60 @@
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QFormLayout" name="parameterSetLayout">
<item row="0" column="0">
<widget class="QLabel" name="parameterSetLabel">
<property name="text">
<string>Parameter set:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="qMRMLNodeComboBox" name="parameterSetSelector">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>A parameter set groups parameters that define a named study distinctly.</string>
</property>
<property name="nodeTypes">
<stringlist notr="true">
<string>vtkMRMLScriptedModuleNode</string>
</stringlist>
</property>
<property name="showHidden">
<bool>true</bool>
</property>
<property name="hideChildNodeTypes">
<stringlist notr="true"/>
</property>
<property name="baseName">
<string notr="true">GuidedVeinSegmentationStudy</string>
</property>
<property name="noneEnabled">
<bool>false</bool>
</property>
<property name="renameEnabled">
<bool>true</bool>
</property>
<property name="interactionNodeSingletonTag">
<string notr="true"/>
</property>
<property name="selectNodeUponCreation">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="Line" name="parameterSetLine">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<layout class="QFormLayout" name="formLayout">
<item row="1" column="0">
Expand Down Expand Up @@ -478,5 +532,21 @@ deviation:</string>
</hint>
</hints>
</connection>
<connection>
<sender>GuidedVeinSegmentation</sender>
<signal>mrmlSceneChanged(vtkMRMLScene*)</signal>
<receiver>parameterSetSelector</receiver>
<slot>setMRMLScene(vtkMRMLScene*)</slot>
<hints>
<hint type="sourcelabel">
<x>190</x>
<y>328</y>
</hint>
<hint type="destinationlabel">
<x>235</x>
<y>22</y>
</hint>
</hints>
</connection>
</connections>
</ui>

0 comments on commit 6382571

Please sign in to comment.