From a9c75a8f8776af022d4a5bf9d686fd39c07fd096 Mon Sep 17 00:00:00 2001 From: Ye Han Date: Tue, 8 Apr 2025 22:11:56 -0400 Subject: [PATCH] ENH:Add AverageMesh module --- AverageMesh/AverageMesh.py | 328 ++++++++++++++++++++ AverageMesh/CMakeLists.txt | 31 ++ AverageMesh/Resources/Icons/AverageMesh.png | Bin 0 -> 21024 bytes AverageMesh/Resources/UI/AverageMesh.ui | 190 ++++++++++++ AverageMesh/Testing/CMakeLists.txt | 1 + AverageMesh/Testing/Python/CMakeLists.txt | 2 + CMakeLists.txt | 1 + 7 files changed, 553 insertions(+) create mode 100644 AverageMesh/AverageMesh.py create mode 100644 AverageMesh/CMakeLists.txt create mode 100644 AverageMesh/Resources/Icons/AverageMesh.png create mode 100644 AverageMesh/Resources/UI/AverageMesh.ui create mode 100644 AverageMesh/Testing/CMakeLists.txt create mode 100644 AverageMesh/Testing/Python/CMakeLists.txt diff --git a/AverageMesh/AverageMesh.py b/AverageMesh/AverageMesh.py new file mode 100644 index 0000000..3fbdff6 --- /dev/null +++ b/AverageMesh/AverageMesh.py @@ -0,0 +1,328 @@ +import os +import unittest +import logging +import vtk, qt, ctk, slicer +from slicer.ScriptedLoadableModule import * +from slicer.util import VTKObservationMixin + +# +# AverageMesh +# + +class AverageMesh(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 = "Average Mesh" + self.parent.categories = ["Surface Models"] + self.parent.dependencies = [] + self.parent.contributors = ["Ye Han, Jean-Christophe Fillion-Robin, Beatriz Paniagua (Kitware)"] + self.parent.helpText = """ +This module supports computing the arithmetic mean from a set of correspondence established input +models or weighted average between two models. For statistical shape analysis purpose the average model should be +computed after performing procrustes alignment using the Mesh Alignment module. +""" + self.parent.helpText += self.getDefaultModuleDocumentationLink() + self.parent.acknowledgementText = """ +The development of this module was supported by the NIH National Institute of Biomedical Imaging Bioengineering +R01EB021391 (Shape Analysis Toolbox for Medical Image Computing Projects), +with help from Andras Lasso (PerkLab, Queen's University) and Steve Pieper (Isomics, Inc.). +""" + +# +# AverageMeshWidget +# + +class AverageMeshWidget(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): + """ + 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/AverageMesh.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 = AverageMeshLogic() + self.logic.updateProcessCallback = self.updateProcess + + # 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.parameterEditWidgets = [ + # (self.ui.CheckableNodeComboBox_inputNodes, "inputNodes"), + (self.ui.MRMLNodeComboBox_outputNode, "outputNode"), + (self.ui.comboBox_method, "method"), + (self.ui.SliderWidget_weight, "weight") + ] + + slicer.util.addParameterEditWidgetConnections(self.parameterEditWidgets, self.updateParameterNodeFromGUI) + self.ui.CheckableNodeComboBox_inputNodes.connect("checkedNodesChanged()", self.updateGUIFromParameterNode) + + # Buttons + self.ui.applyButton.connect('clicked(bool)', self.onApplyButton) + + # Make sure parameter node is initialized (needed for module reload) + self.initializeParameterNode() + self.updateGUIFromParameterNode() + + 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("targetNode"): + firstModelNode = slicer.mrmlScene.GetFirstNodeByClass("vtkMRMLModelNode") + if firstModelNode: + self._parameterNode.SetNodeReferenceID("targetNode", firstModelNode.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. + """ + + 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() + + 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 + slicer.util.updateParameterEditWidgetsFromNode(self.parameterEditWidgets, self._parameterNode) + + # Update buttons states and tooltips + self.ui.SliderWidget_weight.setEnabled(self._parameterNode.GetParameter('method') == "Weighted") + self.ui.applyButton.setEnabled(self._parameterNode.GetNodeReference("outputNode") and + not self.ui.CheckableNodeComboBox_inputNodes.noneChecked()) + + # 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 + slicer.util.updateNodeFromParameterEditWidgets(self.parameterEditWidgets, self._parameterNode) + self._parameterNode.EndModify(wasModified) + + def updateProcess(self, value): + """Display changing process value""" + self.ui.applyButton.text = value + self.ui.applyButton.repaint() + + def onApplyButton(self): + """ + Run processing when user clicks "Apply" button. + """ + slicer.app.pauseRender() + qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) + try: + inputNodes = self.ui.CheckableNodeComboBox_inputNodes.checkedNodes() + outputNode = self._parameterNode.GetNodeReference("outputNode") + self.logic.computeAverage(inputNodes, outputNode, self._parameterNode) + slicer.app.processEvents() + self.ui.applyButton.text = "Apply" + except Exception as e: + slicer.util.errorDisplay("Failed to compute output model: " + str(e)) + import traceback + traceback.print_exc() + finally: + slicer.app.resumeRender() + qt.QApplication.restoreOverrideCursor() + + +class AverageMeshLogic(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) + self.updateProcessCallback = None + + def setDefaultParameters(self, parameterNode): + """ + Initialize parameter node with default settings. + """ + defaultValues = [ + ("method", "Arithmetic"), + ("weight", "0.5") + ] + for parameterName, defaultValue in defaultValues: + if not parameterNode.GetParameter(parameterName): + parameterNode.SetParameter(parameterName, defaultValue) + + def updateProcess(self, message): + if not self.updateProcessCallback: + return + self.updateProcessCallback(message) + + def computeAverage(self, inputNodes, outputNode, parameterNode): + from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy + import numpy as np + + if outputNode not in inputNodes: + if outputNode.GetPolyData() is None: + outputNode.SetAndObserveMesh(vtk.vtkPolyData()) + outputNode.GetPolyData().DeepCopy(inputNodes[0].GetPolyData()) + outputNode.CreateDefaultDisplayNodes() + outputNode.AddDefaultStorageNode() + + numberOfPoints = outputNode.GetPolyData().GetNumberOfPoints() + meanPoints = np.zeros([numberOfPoints, 3]) + if parameterNode.GetParameter("method") == "Arithmetic": + for inputNode in inputNodes: + meanPoints = meanPoints + vtk_to_numpy(inputNode.GetPolyData().GetPoints().GetData()) + meanPoints = meanPoints / len(inputNodes) + elif parameterNode.GetParameter("method") == "Weighted": + if len(inputNodes) != 2: + logging.error("Weighted average only supports 2 input models!") + return + points0 = vtk_to_numpy(inputNodes[0].GetPolyData().GetPoints().GetData()) + points1 = vtk_to_numpy(inputNodes[1].GetPolyData().GetPoints().GetData()) + weight = float(parameterNode.GetParameter("weight")) + meanPoints = (1 - weight) * points0 + weight * points1 + else: + logging.error("Wrong method selection.") + outputNode.GetPolyData().GetPoints().SetData(numpy_to_vtk(meanPoints)) + + +class AverageMeshTest(ScriptedLoadableModuleTest): + """ + This is the test case for your scripted module. + Uses ScriptedLoadableModuleTest base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ + + def setUp(self): + """ Do whatever is needed to reset the state - typically a scene clear will be enough. + """ + slicer.mrmlScene.Clear(0) + + def runTest(self): + """Run as few or as many tests as needed here. + """ + self.setUp() + self.test_AllProcessing() + + def test_AllProcessing(self): + """ Ideally you should have several levels of tests. At the lowest level + tests should exercise the functionality of the logic with different inputs + (both valid and invalid). At higher levels your tests should emulate the + way the user would interact with your code and confirm that it still works + the way you intended. + One of the most important features of the tests is that it should alert other + developers when their changes will have an impact on the behavior of your + module. For example, if a developer removes a feature that you depend on, + your test should break so they know that the feature is needed. + """ + + self.delayDisplay("Starting the test") + self.delayDisplay('Test passed!') diff --git a/AverageMesh/CMakeLists.txt b/AverageMesh/CMakeLists.txt new file mode 100644 index 0000000..8744092 --- /dev/null +++ b/AverageMesh/CMakeLists.txt @@ -0,0 +1,31 @@ +#----------------------------------------------------------------------------- +set(MODULE_NAME AverageMesh) + +#----------------------------------------------------------------------------- +set(MODULE_PYTHON_SCRIPTS + ${MODULE_NAME}.py + ) + +set(MODULE_PYTHON_RESOURCES + Resources/Icons/${MODULE_NAME}.png + Resources/UI/${MODULE_NAME}.ui + ) + +#----------------------------------------------------------------------------- +slicerMacroBuildScriptedModule( + NAME ${MODULE_NAME} + SCRIPTS ${MODULE_PYTHON_SCRIPTS} + RESOURCES ${MODULE_PYTHON_RESOURCES} + WITH_GENERIC_TESTS + ) + +#----------------------------------------------------------------------------- +if(BUILD_TESTING) + + # Register the unittest subclass in the main script as a ctest. + # Note that the test will also be available at runtime. + slicer_add_python_unittest(SCRIPT ${MODULE_NAME}.py) + + # Additional build-time testing + add_subdirectory(Testing) +endif() diff --git a/AverageMesh/Resources/Icons/AverageMesh.png b/AverageMesh/Resources/Icons/AverageMesh.png new file mode 100644 index 0000000000000000000000000000000000000000..5d83ab4f05067d6d5e30808fe07df6b4ac035349 GIT binary patch literal 21024 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

gM5Oyb7$7#KJA2&j@3Tx-)UlJ%O+Q7&{q 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 f>+Cwaj(z + + AverageMesh + + + + 0 + 0 + 340 + 367 + + + + + + + + + Input models: + + + + + + + + vtkMRMLModelNode + + + + + + + + + + + + + + Output Model: + + + + + + + + vtkMRMLModelNode + + + + + + + + Method + + + + + + + <html><head/><body><p>Procrustes alignment assumes shapes have establised correspondence between individual points.</p></body></html> + + + + Arithmetic + + + + + Weighted + + + + + + + + Weight + + + + + + + 0.010000000000000 + + + 0.100000000000000 + + + 1.000000000000000 + + + 0.500000000000000 + + + + + + + + + + + Perform selected processing steps. + + + Apply + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + qMRMLCheckableNodeComboBox + qMRMLNodeComboBox +

qMRMLCheckableNodeComboBox.h
+ + + qMRMLNodeComboBox + QWidget +
qMRMLNodeComboBox.h
+
+ + qMRMLWidget + QWidget +
qMRMLWidget.h
+ 1 +
+ + ctkSliderWidget + QWidget +
ctkSliderWidget.h
+
+ + + + + AverageMesh + mrmlSceneChanged(vtkMRMLScene*) + MRMLNodeComboBox_outputNode + setMRMLScene(vtkMRMLScene*) + + + 122 + 132 + + + 248 + 61 + + + + + AverageMesh + mrmlSceneChanged(vtkMRMLScene*) + CheckableNodeComboBox_inputNodes + setMRMLScene(vtkMRMLScene*) + + + 122 + 132 + + + 248 + 61 + + + + + diff --git a/AverageMesh/Testing/CMakeLists.txt b/AverageMesh/Testing/CMakeLists.txt new file mode 100644 index 0000000..655007a --- /dev/null +++ b/AverageMesh/Testing/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(Python) diff --git a/AverageMesh/Testing/Python/CMakeLists.txt b/AverageMesh/Testing/Python/CMakeLists.txt new file mode 100644 index 0000000..5658d8b --- /dev/null +++ b/AverageMesh/Testing/Python/CMakeLists.txt @@ -0,0 +1,2 @@ + +#slicer_add_python_unittest(SCRIPT ${MODULE_NAME}ModuleTest.py) diff --git a/CMakeLists.txt b/CMakeLists.txt index faad0d2..12a0d58 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,7 @@ include(${Slicer_USE_FILE}) add_subdirectory(Decimation) add_subdirectory(SurfaceToolbox) add_subdirectory(DynamicModeler) +add_subdirectory(AverageMesh) ## NEXT_MODULE