非常简单的程序架构
在默认扩展模块中程序架构非常简单,总共由4个部分组成4个类。分别是基类、组件类,逻辑类和测试类。无论是您自己创建的默认扩展模块还是slicer中自带的测试案例,所有的应用程序都是编写在这四个类中。
下面是基类FW初始化函数的部分程序
class EX(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 = "EX" # TODO: make this more human readable by adding spaces
self.parent.categories = ["Examples"] # TODO: set categories (folders where the module shows up in the module selector)
self.parent.dependencies = [] # TODO: add here list of module names that this module requires
self.parent.contributors = ["John Doe (AnyWare Corp.)"] # TODO: replace with "Firstname Lastname (Organization)"
# TODO: update with short description of the module and a link to online module documentation
self.parent.helpText = """
This is an example of scripted loadable module bundled in an extension.
See more information in module documentation.
"""
# TODO: replace with organization, grant and thanks
self.parent.acknowledgementText = """
This file was originally developed by Jean-Christophe Fillion-Robin, Kitware Inc., Andras Lasso, PerkLab,
and Steve Pieper, Isomics, Inc. and was partially funded by NIH grant 3P41RR013218-12S1.
"""
# Additional initialization step after application startup is complete
slicer.app.connect("startupCompleted()", registerSampleData)
程序中的内容与右边slicer扩展界面中help中的内容有着一致性。左边红色部分框住的【Examples】对应了我们打开此扩展模块的位置,【contributors】对应了右边界面中Acknowledgement下方对应的姓名,这样的对应还有很多,这里就不一一列举了。基类所对应的内容包含了help所包含的界面信息,不仅如此,基类还加载了slicer中自带的Sample数据、icon图标等。可以把这类理解为对默认拓展模块的外围完成了初步的加载工作 。
窗体类
这个类负责完成所有与Volume数据相关的组件的加载工作。基类与窗体类都是完成的组件的加载,那么两者有必要分开吗?很有必要!与基类不同的是,窗体类中的组件包含了对Volume的读取、创建,删除等操作,为了让整个slicer能够监测到这些操作的发生并作出最快的响应,slicer加入了MRML场景,这是slicer体系架构中最复杂的一部分,有关MRML场景的部分在后面会有更详细的介绍。
在默认扩展模块中的窗体类一共有12个函数,12个函数相互关联缺一不可,建议开发人员不要更改函数名,按照具体需求在函数内部更改功能即可。函数之间的关系如图所示(模块名为Hello3D):
__init__方法用于类的初始化,其中包括对ScriptedLoadableModuleWidget类和VTKObservationMixin类的初始化,另外还定义了三个属性并对其做初始化。
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
ScriptedLoadableModuleWidget类加载和初始化了默认扩展模块组件所需要的所有方法,想要启用MRML事件和观察者就必须要使用VTKObservationMixin类。Self.logic属性是逻辑类的实例化对象,self._parameterNode属性是参数节点,用于存储输入的全部参数节点。Self. _updatingGUIFromParameterNode是标志位,辅助updateGUIFromParameterNode方法从参数节点中更新GUI。
•setup方法用于对界面的组件做一系列必要的设置。
def setup(self):
"""
Called when the user opens the module the first time and the widget is initialized.
"""
ScriptedLoadableModuleWidget.setup(self)
# Load widget from .ui file (created by Qt Designer).
# Additional widgets can be instantiated manually and added to self.layout.
uiWidget = slicer.util.loadUI(self.resourcePath('UI/EX.ui'))
self.layout.addWidget(uiWidget)
self.ui = slicer.util.childWidgetVariables(uiWidget)
# Set scene in MRML widgets. Make sure that in Qt designer the top-level qMRMLWidget's
# "mrmlSceneChanged(vtkMRMLScene*)" signal in is connected to each MRML widget's.
# "setMRMLScene(vtkMRMLScene*)" slot.
uiWidget.setMRMLScene(slicer.mrmlScene)
# Create logic class. Logic implements all computations that should be possible to run
# in batch mode, without a graphical user interface.
self.logic = EXLogic()
# Connections
# 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)
# These connections ensure that whenever user changes some settings on the GUI, that is saved in the MRML scene
# (in the selected parameter node).
self.ui.inputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI)
self.ui.outputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI)
self.ui.imageThresholdSliderWidget.connect("valueChanged(double)", self.updateParameterNodeFromGUI)
self.ui.invertOutputCheckBox.connect("toggled(bool)", self.updateParameterNodeFromGUI)
self.ui.invertedOutputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI)
# Buttons
self.ui.applyButton.connect('clicked(bool)', self.onApplyButton)
# Make sure parameter node is initialized (needed for module reload)
self.initializeParameterNode()
ScriptedLoadableModuleWidget.setup方法对模块组件做一些设置。
slicer.util.loadUI方法用于加载默认拓展模块的ui文件(PyQt设计界面时会生成相应的ui文件)。
self.layout.addWidget方法用于将ui文件中的组件添加到现有界面上去。
slicer.util.childWidgetVariables方法用于将ui文件中的组件设置为界面中的子布局。
setMRMLScene方法设置该子布局中的场景为MRML场景。
self.logic = FWLogic()示例化一个逻辑类对象
self.addObserve()给组件类的onSceneStartClose()和onSceneEndClose()方法添加监视器,一旦用户关闭默认拓展模块,其对应的参数节点将立即改变。
self.ui.inputSelector.connect()、self.ui.outputSelector.connect()、self.ui.imageThresholdSliderWidget.connect()、self.ui.invertOutputCheckBox.connect()、self.ui.invertedOutputSelector.connect()、self.ui.applyButton.connect()这6个槽函数对应了6个控件,一旦控件状态发生改变时,对应的参数节点将立即发生改变。6个控件分别如下图所示。
self.initializeParameterNode()调用此函数初始化参数节点。
•cleanup方法用于关闭应用程序并销毁模块组件。
self.removeObservers()方法用于销毁MRML场景中的监视器。
def cleanup(self):
"""
Called when the application closes and the module widget is destroyed.
"""
self.removeObservers()
•enter方法用于初始化参数节点。
def enter(self):
"""
Called each time the user opens this module.
"""
# Make sure parameter node exists and observed
self.initializeParameterNode()
•exit方法用于销毁MRML场景中的监视器并更新GUI界面。
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)
•onSceneStartClose方法在场景开始关闭的时候调用,用于重置参数节点。
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)
•onSceneEndClose方法在场景关闭时显示此模块,则立即重新创建一个新参数节点。
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()
•initializeParameterNode方法将用户选择的参数节点存储在参数值、节点选择等中。
这样当场景被保存并重新加载时,这些设置就会被恢复。
其中,self._parameterNode.GetNodeReference()方法用于获取用户选择的输入Volume
self._parameterNode.SetNodeReferenceID("InputVolume", firstVolumeNode.GetID())方法用于设置用户输入的Volume的ID。
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())
•setParameterNode方法用于设置参数节点。
self.logic.setDefaultParameters(inputParameterNode)方法用于设置默认参数节点。
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.
"""
if inputParameterNode:
self.logic.setDefaultParameters(inputParameterNode)
# 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()
•updateParameterNodeFromGUI方法用于将参数节点的变化更新到GUI上。
self.ui.inputSelector.setCurrentNode()、self.ui.outputSelector.setCurrentNode()、self.ui.invertedOutputSelector.setCurrentNode()、self.ui.imageThresholdSliderWidget.value()、self.ui.invertOutputCheckBox.checked()都是获取参数节点中对应参数的方法。
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.ui.inputSelector.setCurrentNode(self._parameterNode.GetNodeReference("InputVolume"))
self.ui.outputSelector.setCurrentNode(self._parameterNode.GetNodeReference("OutputVolume"))
self.ui.invertedOutputSelector.setCurrentNode(self._parameterNode.GetNodeReference("OutputVolumeInverse"))
self.ui.imageThresholdSliderWidget.value = float(self._parameterNode.GetParameter("Threshold"))
self.ui.invertOutputCheckBox.checked = (self._parameterNode.GetParameter("Invert") == "true")
# Update buttons states and tooltips
if self._parameterNode.GetNodeReference("InputVolume") and self._parameterNode.GetNodeReference("OutputVolume"):
self.ui.applyButton.toolTip = "Compute output volume"
self.ui.applyButton.enabled = True
else:
self.ui.applyButton.toolTip = "Select input and output volume nodes"
self.ui.applyButton.enabled = False
# All the GUI updates are done
self._updatingGUIFromParameterNode = False
•updateParameterNodeFromGUI方法将GUI中参数的改变更新到参数节点中去。
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.ui.inputSelector.currentNodeID)
self._parameterNode.SetNodeReferenceID("OutputVolume", self.ui.outputSelector.currentNodeID)
self._parameterNode.SetParameter("Threshold", str(self.ui.imageThresholdSliderWidget.value))
self._parameterNode.SetParameter("Invert", "true" if self.ui.invertOutputCheckBox.checked else "false")
self._parameterNode.SetNodeReferenceID("OutputVolumeInverse", self.ui.invertedOutputSelector.currentNodeID)
self._parameterNode.EndModify(wasModified)
•onApplyButton放“Apply”按键被按下时此函数触发。
def onApplyButton(self):
"""
Run processing when user clicks "Apply" button.
"""
with slicer.util.tryWithErrorDisplay("Failed to compute results.", waitCursor=True):
# Compute output
self.logic.process(self.ui.inputSelector.currentNode(), self.ui.outputSelector.currentNode(),
self.ui.imageThresholdSliderWidget.value, self.ui.invertOutputCheckBox.checked)
# Compute inverted output (if needed)
if self.ui.invertedOutputSelector.currentNode():
# If additional output volume is selected then result with inverted threshold is written there
self.logic.process(self.ui.inputSelector.currentNode(), self.ui.invertedOutputSelector.currentNode(),
self.ui.imageThresholdSliderWidget.value, not self.ui.invertOutputCheckBox.checked, showResult=False)
逻辑类是整个应用程序的核心,所有的数据处理核心代码都放在这里。当按键按下时,逻辑类响应对应的逻辑功能。
def process(self, inputVolume, outputVolume, imageThreshold, invert=False, showResult=True):
"""
Run the processing algorithm.
Can be used without GUI widget.
:param inputVolume: volume to be thresholded
:param outputVolume: thresholding result
:param imageThreshold: values above/below this threshold will be set to 0
:param invert: if True then values above the threshold will be set to 0, otherwise values below are set to 0
:param showResult: show output volume in slice viewers
"""
if not inputVolume or not outputVolume:
raise ValueError("Input or output volume is invalid")
import time
startTime = time.time()
logging.info('Processing started')
# Compute the thresholded output volume using the "Threshold Scalar Volume" CLI module
cliParams = {
'InputVolume': inputVolume.GetID(),
'OutputVolume': outputVolume.GetID(),
'ThresholdValue' : imageThreshold,
'ThresholdType' : 'Above' if invert else 'Below'
}
cliNode = slicer.cli.run(slicer.modules.thresholdscalarvolume, None, cliParams, wait_for_completion=True, update_display=showResult)
# We don't need the CLI module node anymore, remove it to not clutter the scene with it
slicer.mrmlScene.RemoveNode(cliNode)
stopTime = time.time()
logging.info(f'Processing completed in {stopTime-startTime:.2f} seconds')
测试类用于对应用程序做一些适当的测试,以检验逻辑的准确性。在之前讲默认扩展模块的第二个折叠控件的“Reload and Test”按钮其实就是对这个类的功能做了简单介绍。此类方便开发人员对程序做测试。