Skip to content

Commit e66a8bc

Browse files
committed
ENH: Add 4 MeshMath modules
1 parent aa2950d commit e66a8bc

File tree

26 files changed

+2807
-0
lines changed

26 files changed

+2807
-0
lines changed

AverageMesh/AverageMesh.py

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
import os
2+
import unittest
3+
import logging
4+
import vtk, qt, ctk, slicer
5+
from slicer.ScriptedLoadableModule import *
6+
from slicer.util import VTKObservationMixin
7+
8+
#
9+
# AverageMesh
10+
#
11+
12+
class AverageMesh(ScriptedLoadableModule):
13+
"""Uses ScriptedLoadableModule base class, available at:
14+
https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
15+
"""
16+
17+
def __init__(self, parent):
18+
ScriptedLoadableModule.__init__(self, parent)
19+
self.parent.title = "Average Mesh"
20+
self.parent.categories = ["Surface Models.Advanced"]
21+
self.parent.dependencies = []
22+
self.parent.contributors = ["Ye Han, Jean-Christophe Fillion-Robin, Beatriz Paniagua (Kitware)"]
23+
self.parent.helpText = """
24+
This module supports computing the arithmetic mean from a set of correspondence established input
25+
models or weighted average between two models. For statistical shape analysis purpose the average model should be
26+
computed after performing procrustes alignment using the Mesh Alignment module.
27+
"""
28+
self.parent.helpText += self.getDefaultModuleDocumentationLink()
29+
self.parent.acknowledgementText = """
30+
The development of this module was supported by the NIH National Institute of Biomedical Imaging Bioengineering
31+
R01EB021391 (Shape Analysis Toolbox for Medical Image Computing Projects),
32+
with help from Andras Lasso (PerkLab, Queen's University) and Steve Pieper (Isomics, Inc.).
33+
"""
34+
35+
#
36+
# AverageMeshWidget
37+
#
38+
39+
class AverageMeshWidget(ScriptedLoadableModuleWidget, VTKObservationMixin):
40+
"""Uses ScriptedLoadableModuleWidget base class, available at:
41+
https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
42+
"""
43+
44+
def __init__(self, parent=None):
45+
"""
46+
Called when the user opens the module the first time and the widget is initialized.
47+
"""
48+
ScriptedLoadableModuleWidget.__init__(self, parent)
49+
VTKObservationMixin.__init__(self) # needed for parameter node observation
50+
self.logic = None
51+
self._parameterNode = None
52+
self._updatingGUIFromParameterNode = False
53+
54+
def setup(self):
55+
"""
56+
Called when the user opens the module the first time and the widget is initialized.
57+
"""
58+
ScriptedLoadableModuleWidget.setup(self)
59+
60+
# Load widget from .ui file (created by Qt Designer).
61+
# Additional widgets can be instantiated manually and added to self.layout.
62+
uiWidget = slicer.util.loadUI(self.resourcePath('UI/AverageMesh.ui'))
63+
self.layout.addWidget(uiWidget)
64+
self.ui = slicer.util.childWidgetVariables(uiWidget)
65+
66+
# Set scene in MRML widgets. Make sure that in Qt designer the top-level qMRMLWidget's
67+
# "mrmlSceneChanged(vtkMRMLScene*)" signal in is connected to each MRML widget's.
68+
# "setMRMLScene(vtkMRMLScene*)" slot.
69+
uiWidget.setMRMLScene(slicer.mrmlScene)
70+
71+
# Create logic class. Logic implements all computations that should be possible to run
72+
# in batch mode, without a graphical user interface.
73+
self.logic = AverageMeshLogic()
74+
self.logic.updateProcessCallback = self.updateProcess
75+
76+
# Connections
77+
78+
# These connections ensure that we update parameter node when scene is closed
79+
self.addObserver(slicer.mrmlScene, slicer.mrmlScene.StartCloseEvent, self.onSceneStartClose)
80+
self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndCloseEvent, self.onSceneEndClose)
81+
82+
# These connections ensure that whenever user changes some settings on the GUI, that is saved in the MRML scene
83+
# (in the selected parameter node).
84+
85+
self.parameterEditWidgets = [
86+
# (self.ui.CheckableNodeComboBox_inputNodes, "inputNodes"),
87+
(self.ui.MRMLNodeComboBox_outputNode, "outputNode"),
88+
(self.ui.comboBox_method, "method"),
89+
(self.ui.SliderWidget_weight, "weight")
90+
]
91+
92+
slicer.util.addParameterEditWidgetConnections(self.parameterEditWidgets, self.updateParameterNodeFromGUI)
93+
self.ui.CheckableNodeComboBox_inputNodes.connect("checkedNodesChanged()", self.updateGUIFromParameterNode)
94+
95+
# Buttons
96+
self.ui.applyButton.connect('clicked(bool)', self.onApplyButton)
97+
98+
# Make sure parameter node is initialized (needed for module reload)
99+
self.initializeParameterNode()
100+
self.updateGUIFromParameterNode()
101+
102+
def cleanup(self):
103+
"""
104+
Called when the application closes and the module widget is destroyed.
105+
"""
106+
self.removeObservers()
107+
108+
def enter(self):
109+
"""
110+
Called each time the user opens this module.
111+
"""
112+
# Make sure parameter node exists and observed
113+
self.initializeParameterNode()
114+
115+
def exit(self):
116+
"""
117+
Called each time the user opens a different module.
118+
"""
119+
# Do not react to parameter node changes (GUI wlil be updated when the user enters into the module)
120+
self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode)
121+
122+
def onSceneStartClose(self, caller, event):
123+
"""
124+
Called just before the scene is closed.
125+
"""
126+
# Parameter node will be reset, do not use it anymore
127+
self.setParameterNode(None)
128+
129+
def onSceneEndClose(self, caller, event):
130+
"""
131+
Called just after the scene is closed.
132+
"""
133+
# If this module is shown while the scene is closed then recreate a new parameter node immediately
134+
if self.parent.isEntered:
135+
self.initializeParameterNode()
136+
137+
def initializeParameterNode(self):
138+
"""
139+
Ensure parameter node exists and observed.
140+
"""
141+
# Parameter node stores all user choices in parameter values, node selections, etc.
142+
# so that when the scene is saved and reloaded, these settings are restored.
143+
144+
self.setParameterNode(self.logic.getParameterNode())
145+
146+
# Select default input nodes if nothing is selected yet to save a few clicks for the user
147+
if not self._parameterNode.GetNodeReference("targetNode"):
148+
firstModelNode = slicer.mrmlScene.GetFirstNodeByClass("vtkMRMLModelNode")
149+
if firstModelNode:
150+
self._parameterNode.SetNodeReferenceID("targetNode", firstModelNode.GetID())
151+
152+
def setParameterNode(self, inputParameterNode):
153+
"""
154+
Set and observe parameter node.
155+
Observation is needed because when the parameter node is changed then the GUI must be updated immediately.
156+
"""
157+
158+
if inputParameterNode:
159+
self.logic.setDefaultParameters(inputParameterNode)
160+
161+
# Unobserve previously selected parameter node and add an observer to the newly selected.
162+
# Changes of parameter node are observed so that whenever parameters are changed by a script or any other module
163+
# those are reflected immediately in the GUI.
164+
if self._parameterNode is not None:
165+
self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode)
166+
self._parameterNode = inputParameterNode
167+
if self._parameterNode is not None:
168+
self.addObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode)
169+
170+
# Initial GUI update
171+
self.updateGUIFromParameterNode()
172+
173+
def updateGUIFromParameterNode(self, caller=None, event=None):
174+
"""
175+
This method is called whenever parameter node is changed.
176+
The module GUI is updated to show the current state of the parameter node.
177+
"""
178+
179+
if self._parameterNode is None or self._updatingGUIFromParameterNode:
180+
return
181+
182+
# Make sure GUI changes do not call updateParameterNodeFromGUI (it could cause infinite loop)
183+
self._updatingGUIFromParameterNode = True
184+
185+
# Update node selectors and sliders
186+
slicer.util.updateParameterEditWidgetsFromNode(self.parameterEditWidgets, self._parameterNode)
187+
188+
# Update buttons states and tooltips
189+
self.ui.SliderWidget_weight.setEnabled(self._parameterNode.GetParameter('method') == "Weighted")
190+
self.ui.applyButton.setEnabled(self._parameterNode.GetNodeReference("outputNode") and
191+
not self.ui.CheckableNodeComboBox_inputNodes.noneChecked())
192+
193+
# All the GUI updates are done
194+
self._updatingGUIFromParameterNode = False
195+
196+
def updateParameterNodeFromGUI(self, caller=None, event=None):
197+
"""
198+
This method is called when the user makes any change in the GUI.
199+
The changes are saved into the parameter node (so that they are restored when the scene is saved and loaded).
200+
"""
201+
if self._parameterNode is None or self._updatingGUIFromParameterNode:
202+
return
203+
wasModified = self._parameterNode.StartModify() # Modify all properties in a single batch
204+
slicer.util.updateNodeFromParameterEditWidgets(self.parameterEditWidgets, self._parameterNode)
205+
self._parameterNode.EndModify(wasModified)
206+
207+
def updateProcess(self, value):
208+
"""Display changing process value"""
209+
self.ui.applyButton.text = value
210+
self.ui.applyButton.repaint()
211+
212+
def onApplyButton(self):
213+
"""
214+
Run processing when user clicks "Apply" button.
215+
"""
216+
slicer.app.pauseRender()
217+
qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
218+
try:
219+
inputNodes = self.ui.CheckableNodeComboBox_inputNodes.checkedNodes()
220+
outputNode = self._parameterNode.GetNodeReference("outputNode")
221+
self.logic.computeAverage(inputNodes, outputNode, self._parameterNode)
222+
slicer.app.processEvents()
223+
self.ui.applyButton.text = "Apply"
224+
except Exception as e:
225+
slicer.util.errorDisplay("Failed to compute output model: " + str(e))
226+
import traceback
227+
traceback.print_exc()
228+
finally:
229+
slicer.app.resumeRender()
230+
qt.QApplication.restoreOverrideCursor()
231+
232+
233+
class AverageMeshLogic(ScriptedLoadableModuleLogic):
234+
"""This class should implement all the actual
235+
computation done by your module. The interface
236+
should be such that other python code can import
237+
this class and make use of the functionality without
238+
requiring an instance of the Widget.
239+
Uses ScriptedLoadableModuleLogic base class, available at:
240+
https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
241+
"""
242+
243+
def __init__(self):
244+
"""
245+
Called when the logic class is instantiated. Can be used for initializing member variables.
246+
"""
247+
ScriptedLoadableModuleLogic.__init__(self)
248+
self.updateProcessCallback = None
249+
250+
def setDefaultParameters(self, parameterNode):
251+
"""
252+
Initialize parameter node with default settings.
253+
"""
254+
defaultValues = [
255+
("method", "Arithmetic"),
256+
("weight", "0.5")
257+
]
258+
for parameterName, defaultValue in defaultValues:
259+
if not parameterNode.GetParameter(parameterName):
260+
parameterNode.SetParameter(parameterName, defaultValue)
261+
262+
def updateProcess(self, message):
263+
if not self.updateProcessCallback:
264+
return
265+
self.updateProcessCallback(message)
266+
267+
def computeAverage(self, inputNodes, outputNode, parameterNode):
268+
from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy
269+
import numpy as np
270+
271+
if outputNode not in inputNodes:
272+
if outputNode.GetPolyData() is None:
273+
outputNode.SetAndObserveMesh(vtk.vtkPolyData())
274+
outputNode.GetPolyData().DeepCopy(inputNodes[0].GetPolyData())
275+
outputNode.CreateDefaultDisplayNodes()
276+
outputNode.AddDefaultStorageNode()
277+
278+
numberOfPoints = outputNode.GetPolyData().GetNumberOfPoints()
279+
meanPoints = np.zeros([numberOfPoints, 3])
280+
if parameterNode.GetParameter("method") == "Arithmetic":
281+
for inputNode in inputNodes:
282+
meanPoints = meanPoints + vtk_to_numpy(inputNode.GetPolyData().GetPoints().GetData())
283+
meanPoints = meanPoints / len(inputNodes)
284+
elif parameterNode.GetParameter("method") == "Weighted":
285+
if len(inputNodes) != 2:
286+
logging.error("Weighted average only supports 2 input models!")
287+
return
288+
points0 = vtk_to_numpy(inputNodes[0].GetPolyData().GetPoints().GetData())
289+
points1 = vtk_to_numpy(inputNodes[1].GetPolyData().GetPoints().GetData())
290+
weight = float(parameterNode.GetParameter("weight"))
291+
meanPoints = (1 - weight) * points0 + weight * points1
292+
else:
293+
logging.error("Wrong method selection.")
294+
outputNode.GetPolyData().GetPoints().SetData(numpy_to_vtk(meanPoints))
295+
296+
297+
class AverageMeshTest(ScriptedLoadableModuleTest):
298+
"""
299+
This is the test case for your scripted module.
300+
Uses ScriptedLoadableModuleTest base class, available at:
301+
https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
302+
"""
303+
304+
def setUp(self):
305+
""" Do whatever is needed to reset the state - typically a scene clear will be enough.
306+
"""
307+
slicer.mrmlScene.Clear(0)
308+
309+
def runTest(self):
310+
"""Run as few or as many tests as needed here.
311+
"""
312+
self.setUp()
313+
self.test_AllProcessing()
314+
315+
def test_AllProcessing(self):
316+
""" Ideally you should have several levels of tests. At the lowest level
317+
tests should exercise the functionality of the logic with different inputs
318+
(both valid and invalid). At higher levels your tests should emulate the
319+
way the user would interact with your code and confirm that it still works
320+
the way you intended.
321+
One of the most important features of the tests is that it should alert other
322+
developers when their changes will have an impact on the behavior of your
323+
module. For example, if a developer removes a feature that you depend on,
324+
your test should break so they know that the feature is needed.
325+
"""
326+
327+
self.delayDisplay("Starting the test")
328+
self.delayDisplay('Test passed!')

AverageMesh/CMakeLists.txt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#-----------------------------------------------------------------------------
2+
set(MODULE_NAME AverageMesh)
3+
4+
#-----------------------------------------------------------------------------
5+
set(MODULE_PYTHON_SCRIPTS
6+
${MODULE_NAME}.py
7+
)
8+
9+
set(MODULE_PYTHON_RESOURCES
10+
Resources/Icons/${MODULE_NAME}.png
11+
Resources/UI/${MODULE_NAME}.ui
12+
)
13+
14+
#-----------------------------------------------------------------------------
15+
slicerMacroBuildScriptedModule(
16+
NAME ${MODULE_NAME}
17+
SCRIPTS ${MODULE_PYTHON_SCRIPTS}
18+
RESOURCES ${MODULE_PYTHON_RESOURCES}
19+
WITH_GENERIC_TESTS
20+
)
21+
22+
#-----------------------------------------------------------------------------
23+
if(BUILD_TESTING)
24+
25+
# Register the unittest subclass in the main script as a ctest.
26+
# Note that the test will also be available at runtime.
27+
slicer_add_python_unittest(SCRIPT ${MODULE_NAME}.py)
28+
29+
# Additional build-time testing
30+
add_subdirectory(Testing)
31+
endif()
20.5 KB
Loading

0 commit comments

Comments
 (0)