├── .gitignore
├── 02__Interact_Debug_&_Pipeline
├── CMakeLists.txt
├── README.md
├── SL_Tutorials.png
├── sl_02__Summary.png
└── t_ApplyThreshold
│ ├── CMakeLists.txt
│ ├── Resources
│ ├── Icons
│ │ └── t_ApplyThreshold.png
│ └── UI
│ │ └── t_ApplyThreshold.ui
│ ├── Testing
│ ├── CMakeLists.txt
│ └── Python
│ │ └── CMakeLists.txt
│ └── t_ApplyThreshold.py
├── 03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld
├── CMakeLists.txt
├── README.md
├── SL_Tutorials.png
├── sl_03__Summary.png
└── sl__GUI_HelloWorld
│ ├── CMakeLists.txt
│ ├── Resources
│ ├── Icons
│ │ └── sl__GUI_HelloWorld.png
│ └── UI
│ │ └── sl__GUI_HelloWorld.ui
│ ├── Testing
│ ├── CMakeLists.txt
│ └── Python
│ │ └── CMakeLists.txt
│ └── sl__GUI_HelloWorld.py
├── 04__CodeStyle_MethodGroups_&_US_SeqViewer
├── CMakeLists.txt
├── README.md
├── SL_Tutorials.png
├── sl_04__Summary.png
└── sl__US_SeqViewer
│ ├── CMakeLists.txt
│ ├── Resources
│ ├── Icons
│ │ └── sl__US_SeqViewer.png
│ └── UI
│ │ └── sl__US_SeqViewer.ui
│ ├── Testing
│ ├── CMakeLists.txt
│ └── Python
│ │ └── CMakeLists.txt
│ └── sl__US_SeqViewer.py
├── LICENSE
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | # Large Figures' Folder (all saved on GoogleDrive)
2 | _Figures/
3 |
4 |
5 | 99__Test__ToBeReduced/
6 |
7 |
8 |
9 | # PyCharm Files
10 | .idea/
11 |
12 |
13 | # Byte-compiled / optimized / DLL files
14 | __pycache__/
15 | *.py[cod]
16 | *$py.class
17 |
18 | # C extensions
19 | *.so
20 |
21 | # Distribution / packaging
22 | .Python
23 | build/
24 | develop-eggs/
25 | dist/
26 | downloads/
27 | eggs/
28 | .eggs/
29 | lib/
30 | lib64/
31 | parts/
32 | sdist/
33 | var/
34 | wheels/
35 | pip-wheel-metadata/
36 | share/python-wheels/
37 | *.egg-info/
38 | .installed.cfg
39 | *.egg
40 | MANIFEST
41 |
42 | # PyInstaller
43 | # Usually these files are written by a python script from a template
44 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
45 | *.manifest
46 | *.spec
47 |
48 | # Installer logs
49 | pip-log.txt
50 | pip-delete-this-directory.txt
51 |
52 | # Unit test / coverage reports
53 | htmlcov/
54 | .tox/
55 | .nox/
56 | .coverage
57 | .coverage.*
58 | .cache
59 | nosetests.xml
60 | coverage.xml
61 | *.cover
62 | *.py,cover
63 | .hypothesis/
64 | .pytest_cache/
65 |
66 | # Translations
67 | *.mo
68 | *.pot
69 |
70 | # Django stuff:
71 | *.log
72 | local_settings.py
73 | db.sqlite3
74 | db.sqlite3-journal
75 |
76 | # Flask stuff:
77 | instance/
78 | .webassets-cache
79 |
80 | # Scrapy stuff:
81 | .scrapy
82 |
83 | # Sphinx documentation
84 | docs/_build/
85 |
86 | # PyBuilder
87 | target/
88 |
89 | # Jupyter Notebook
90 | .ipynb_checkpoints
91 |
92 | # IPython
93 | profile_default/
94 | ipython_config.py
95 |
96 | # pyenv
97 | .python-version
98 |
99 | # pipenv
100 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
101 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
102 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
103 | # install all needed dependencies.
104 | #Pipfile.lock
105 |
106 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
107 | __pypackages__/
108 |
109 | # Celery stuff
110 | celerybeat-schedule
111 | celerybeat.pid
112 |
113 | # SageMath parsed files
114 | *.sage.py
115 |
116 | # Environments
117 | .env
118 | .venv
119 | env/
120 | venv/
121 | ENV/
122 | env.bak/
123 | venv.bak/
124 |
125 | # Spyder project settings
126 | .spyderproject
127 | .spyproject
128 |
129 | # Rope project settings
130 | .ropeproject
131 |
132 | # mkdocs documentation
133 | /site
134 |
135 | # mypy
136 | .mypy_cache/
137 | .dmypy.json
138 | dmypy.json
139 |
140 | # Pyre type checker
141 | .pyre/
142 |
--------------------------------------------------------------------------------
/02__Interact_Debug_&_Pipeline/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.16.3...3.19.7 FATAL_ERROR)
2 |
3 | project(SL_Tutorials)
4 |
5 | #-----------------------------------------------------------------------------
6 | # Extension meta-information
7 | set(EXTENSION_HOMEPAGE "https://www.slicer.org/wiki/Documentation/Nightly/Extensions/SL_Tutorials")
8 | set(EXTENSION_CATEGORY "Tutorials")
9 | set(EXTENSION_CONTRIBUTORS "Sen Li (ETS)")
10 | set(EXTENSION_DESCRIPTION "This is an example of a simple extension")
11 | set(EXTENSION_ICONURL "https://www.example.com/Slicer/Extensions/SL_Tutorials.png")
12 | set(EXTENSION_SCREENSHOTURLS "https://www.example.com/Slicer/Extensions/SL_Tutorials/Screenshots/1.png")
13 | set(EXTENSION_DEPENDS "NA") # Specified as a list or "NA" if no dependencies
14 |
15 | #-----------------------------------------------------------------------------
16 | # Extension dependencies
17 | find_package(Slicer REQUIRED)
18 | include(${Slicer_USE_FILE})
19 |
20 | #-----------------------------------------------------------------------------
21 | # Extension modules
22 | add_subdirectory(t_ApplyThreshold)
23 | ## NEXT_MODULE
24 |
25 | #-----------------------------------------------------------------------------
26 | include(${Slicer_EXTENSION_GENERATE_CONFIG})
27 | include(${Slicer_EXTENSION_CPACK})
28 |
--------------------------------------------------------------------------------
/02__Interact_Debug_&_Pipeline/README.md:
--------------------------------------------------------------------------------
1 | # 3D Slicer Extension Tutorial: Step by Step
2 |
3 |
4 |
5 |
6 |
7 | ## Step 02: Interact, Debug & Pipeline
8 | [](https://www.youtube.com/watch?v=Gyd-q4NAI7U&list=PLTuWbByD80TORd1R-J7j7nVQ9fot3C2fK)
9 |
10 | #### YouTube Video Tutorial for Step_02
11 |
12 | #### Bilibili Video Tutorial for Step_02
13 |
14 |
15 |
--------------------------------------------------------------------------------
/02__Interact_Debug_&_Pipeline/SL_Tutorials.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SenonETS/3DSlicerTutorial_ExtensionModuleDevelopment/c2ed7fd7e94f79953ec38019b16ca90019817bbc/02__Interact_Debug_&_Pipeline/SL_Tutorials.png
--------------------------------------------------------------------------------
/02__Interact_Debug_&_Pipeline/sl_02__Summary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SenonETS/3DSlicerTutorial_ExtensionModuleDevelopment/c2ed7fd7e94f79953ec38019b16ca90019817bbc/02__Interact_Debug_&_Pipeline/sl_02__Summary.png
--------------------------------------------------------------------------------
/02__Interact_Debug_&_Pipeline/t_ApplyThreshold/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | #-----------------------------------------------------------------------------
2 | set(MODULE_NAME t_ApplyThreshold)
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()
32 |
--------------------------------------------------------------------------------
/02__Interact_Debug_&_Pipeline/t_ApplyThreshold/Resources/Icons/t_ApplyThreshold.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SenonETS/3DSlicerTutorial_ExtensionModuleDevelopment/c2ed7fd7e94f79953ec38019b16ca90019817bbc/02__Interact_Debug_&_Pipeline/t_ApplyThreshold/Resources/Icons/t_ApplyThreshold.png
--------------------------------------------------------------------------------
/02__Interact_Debug_&_Pipeline/t_ApplyThreshold/Resources/UI/t_ApplyThreshold.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | t_ApplyThreshold
4 |
5 |
6 |
7 | 0
8 | 0
9 | 279
10 | 286
11 |
12 |
13 |
14 | -
15 |
16 |
17 | Inputs
18 |
19 |
20 |
-
21 |
22 |
23 | Input volume:
24 |
25 |
26 |
27 | -
28 |
29 |
30 | Pick the input to the algorithm.
31 |
32 |
33 |
34 | vtkMRMLScalarVolumeNode
35 |
36 |
37 |
38 | false
39 |
40 |
41 | false
42 |
43 |
44 | false
45 |
46 |
47 |
48 | -
49 |
50 |
51 | Image threshold:
52 |
53 |
54 |
55 | -
56 |
57 |
58 | Set threshold value for computing the output image. Voxels that have intensities lower than this value will set to zero.
59 |
60 |
61 | 0.100000000000000
62 |
63 |
64 | -100.000000000000000
65 |
66 |
67 | 500.000000000000000
68 |
69 |
70 | 0.500000000000000
71 |
72 |
73 |
74 |
75 |
76 |
77 | -
78 |
79 |
80 | Outputs
81 |
82 |
83 |
-
84 |
85 |
86 | Thresholded volume:
87 |
88 |
89 |
90 | -
91 |
92 |
93 | Pick the output to the algorithm.
94 |
95 |
96 |
97 | vtkMRMLScalarVolumeNode
98 |
99 |
100 |
101 | false
102 |
103 |
104 | true
105 |
106 |
107 | true
108 |
109 |
110 | true
111 |
112 |
113 |
114 | -
115 |
116 |
117 | Inverted volume:
118 |
119 |
120 |
121 | -
122 |
123 |
124 | Result with inverted threshold will be written into this volume
125 |
126 |
127 |
128 | vtkMRMLScalarVolumeNode
129 |
130 |
131 |
132 | false
133 |
134 |
135 | true
136 |
137 |
138 | true
139 |
140 |
141 | true
142 |
143 |
144 |
145 |
146 |
147 |
148 | -
149 |
150 |
151 | Advanced
152 |
153 |
154 | true
155 |
156 |
157 |
-
158 |
159 |
160 | Invert threshold:
161 |
162 |
163 |
164 | -
165 |
166 |
167 | If checked, values above threshold are set to 0. If unchecked, values below are set to 0.
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 | -
178 |
179 |
180 | false
181 |
182 |
183 | Run the algorithm.
184 |
185 |
186 | Apply
187 |
188 |
189 |
190 | -
191 |
192 |
193 | Qt::Vertical
194 |
195 |
196 |
197 | 20
198 | 40
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 | ctkCollapsibleButton
208 | QWidget
209 |
210 | 1
211 |
212 |
213 | ctkSliderWidget
214 | QWidget
215 |
216 |
217 |
218 | qMRMLNodeComboBox
219 | QWidget
220 |
221 |
222 |
223 | qMRMLWidget
224 | QWidget
225 |
226 | 1
227 |
228 |
229 |
230 |
231 |
232 | t_ApplyThreshold
233 | mrmlSceneChanged(vtkMRMLScene*)
234 | inputSelector
235 | setMRMLScene(vtkMRMLScene*)
236 |
237 |
238 | 122
239 | 132
240 |
241 |
242 | 248
243 | 61
244 |
245 |
246 |
247 |
248 | t_ApplyThreshold
249 | mrmlSceneChanged(vtkMRMLScene*)
250 | outputSelector
251 | setMRMLScene(vtkMRMLScene*)
252 |
253 |
254 | 82
255 | 135
256 |
257 |
258 | 220
259 | 161
260 |
261 |
262 |
263 |
264 | t_ApplyThreshold
265 | mrmlSceneChanged(vtkMRMLScene*)
266 | invertedOutputSelector
267 | setMRMLScene(vtkMRMLScene*)
268 |
269 |
270 | 161
271 | 8
272 |
273 |
274 | 173
275 | 176
276 |
277 |
278 |
279 |
280 |
281 |
--------------------------------------------------------------------------------
/02__Interact_Debug_&_Pipeline/t_ApplyThreshold/Testing/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | add_subdirectory(Python)
2 |
--------------------------------------------------------------------------------
/02__Interact_Debug_&_Pipeline/t_ApplyThreshold/Testing/Python/CMakeLists.txt:
--------------------------------------------------------------------------------
1 |
2 | #slicer_add_python_unittest(SCRIPT ${MODULE_NAME}ModuleTest.py)
3 |
--------------------------------------------------------------------------------
/02__Interact_Debug_&_Pipeline/t_ApplyThreshold/t_ApplyThreshold.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 |
4 | import vtk
5 |
6 | import slicer
7 | from slicer.ScriptedLoadableModule import *
8 | from slicer.util import VTKObservationMixin
9 |
10 | '''=================================================================================================================='''
11 | '''=================================================================================================================='''
12 | #
13 | # t_ApplyThreshold
14 | #
15 | class t_ApplyThreshold(ScriptedLoadableModule):
16 |
17 | def __init__(self, parent):
18 | ScriptedLoadableModule.__init__(self, parent)
19 | self.parent.title = "t_ApplyThreshold"
20 | self.parent.categories = ["SL_Tutorials"]
21 | self.parent.dependencies = [] # TODO: add here list of module names that this module requires
22 | self.parent.contributors = ["Sen Li (École de Technologie Supérieure)"]
23 | # TODO: update with short description of the module and a link to online module documentation
24 | self.parent.helpText = """This is t_ApplyThreshold ! """
25 | # TODO: replace with organization, grant and thanks
26 | self.parent.acknowledgementText = """
27 | This file was originally developed by Jean-Christophe Fillion-Robin, Kitware Inc., Andras Lasso, PerkLab,
28 | and Steve Pieper, Isomics, Inc. and was partially funded by NIH grant 3P41RR013218-12S1.
29 | """
30 |
31 | # Additional initialization step after application startup is complete
32 | slicer.app.connect("startupCompleted()", registerSampleData)
33 |
34 | print("t_ApplyThreshold(ScriptedLoadableModule): __init__(self, parent)")
35 |
36 | '''=================================================================================================================='''
37 | '''=================================================================================================================='''
38 | #
39 | # t_ApplyThresholdWidget
40 | #
41 | class t_ApplyThresholdWidget(ScriptedLoadableModuleWidget, VTKObservationMixin):
42 |
43 | def __init__(self, parent=None):
44 | """ Called when the user opens the module the first time and the widget is initialized. """
45 | ScriptedLoadableModuleWidget.__init__(self, parent)
46 | VTKObservationMixin.__init__(self) # needed for parameter node observation
47 | self.logic = None
48 | self._parameterNode = None # SingleTon initialized through self.setParameterNode(self.logic.getParameterNode())
49 | self._updatingGUIFromParameterNode = False
50 | print("**Widget.__init__(self, parent)")
51 |
52 | # ------------------------------------------------------------------------------------------------------------------
53 | def setup(self):
54 | print("**Widget.setup(self), \tSL_Developer")
55 |
56 | """ 00. Called when the user opens the module the first time and the widget is initialized. """
57 | ScriptedLoadableModuleWidget.setup(self)
58 |
59 | # 01. Load widget from .ui file (created by Qt Designer).
60 | # Additional widgets can be instantiated manually and added to self.layout.
61 | uiWidget = slicer.util.loadUI(self.resourcePath('UI/t_ApplyThreshold.ui'))
62 | self.layout.addWidget(uiWidget)
63 | self.ui = slicer.util.childWidgetVariables(uiWidget)
64 |
65 | # 02. Set scene in MRML widgets. Make sure that in Qt designer the
66 | # top-level qMRMLWidget's "mrmlSceneChanged(vtkMRMLScene*)" signal in is connected to
67 | # each MRML widget's "setMRMLScene(vtkMRMLScene*)" slot.
68 | uiWidget.setMRMLScene(slicer.mrmlScene)
69 |
70 | # 03. Create logic class. Logic implements all computations that should be possible to run
71 | # in batch mode, without a graphical user interface.
72 | self.logic = t_ApplyThresholdLogic()
73 |
74 | # 04. Connections, ensure that we update parameter node when scene is closed
75 | self.addObserver(slicer.mrmlScene, slicer.mrmlScene.StartCloseEvent, self.onSceneStartClose)
76 | self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndCloseEvent, self.onSceneEndClose)
77 |
78 | # 05. SL_Developer. Connect Signal-Slot, ensure that whenever user changes some settings on the GUI,
79 | # that is saved in the MRML scene (in the selected parameter node).
80 | self.ui.inputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI)
81 | self.ui.outputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI)
82 | self.ui.imageThresholdSliderWidget.connect("valueChanged(double)", self.updateParameterNodeFromGUI)
83 | self.ui.invertOutputCheckBox.connect("toggled(bool)", self.updateParameterNodeFromGUI)
84 | self.ui.invertedOutputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI)
85 |
86 | self.ui.applyButton.connect('clicked(bool)', self.onApplyButton)
87 |
88 | # 06. Needed for programmer-friendly Module-Reload where the Module had already been enter(self)-ed;
89 | # Otherwise, will initial through function enter(self)
90 | if self.parent.isEntered:
91 | self.initializeParameterNode()
92 |
93 | # ------------------------------------------------------------------------------------------------------------------
94 | def cleanup(self):
95 | """ Called when the application closes and the module widget is destroyed. """
96 | print("**Widget.cleanup(self)")
97 | self.removeObservers()
98 |
99 | # ------------------------------------------------------------------------------------------------------------------
100 | def enter(self):
101 | """ Called each time the user opens this module. """
102 | print("\n**Widget.enter(self)")
103 |
104 | # 01. Slicer. SL__Note: Every-Module own a SingleTon ParameterNode that can be identified by
105 | # self._parameterNode.GetAttribute('ModuleName')! Need to initial every Entry!
106 | self.initializeParameterNode()
107 |
108 | # ------------------------------------------------------------------------------------------------------------------
109 | def exit(self):
110 | """ Called each time the user opens a different module. """
111 | print("**Widget.exit(self)")
112 | # Slicer. Do not react to parameter node changes (GUI will be updated when the user enters into the module)
113 | self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode)
114 |
115 | # ------------------------------------------------------------------------------------------------------------------
116 | def onSceneStartClose(self, caller, event):
117 | """ Called just before the scene is closed. """
118 | print("**Widget.onSceneStartClose(self, caller, event)")
119 |
120 | # Slicer. Parameter node will be reset, do not use it anymore
121 | self.setParameterNode(None)
122 |
123 | # ------------------------------------------------------------------------------------------------------------------
124 | def onSceneEndClose(self, caller, event):
125 | """ Called just after the scene is closed. """''''''
126 | print("**Widget.onSceneEndClose(self, caller, event)")
127 | # If this module is shown while the scene is closed then recreate a new parameter node immediately
128 | if self.parent.isEntered:
129 | self.initializeParameterNode()
130 |
131 | # ------------------------------------------------------------------------------------------------------------------
132 | def initializeParameterNode(self):
133 | """ Ensure parameter node exists and observed. """''''''
134 | print("\t**Widget.initializeParameterNode(self), \t SL_Developer")
135 | # 01. Slicer-Initial: the SingleTon ParameterNode stores all user choices in param-values, node selections...
136 | # so that when the scene is saved and reloaded, these settings are restored.
137 | self.setParameterNode(self.logic.getParameterNode())
138 |
139 | # 02. SL_Developer. Select default input nodes if nothing is selected yet to save a few clicks for the user
140 | if not self._parameterNode.GetNodeReference("InputVolume"):
141 | firstVolumeNode = slicer.mrmlScene.GetFirstNodeByClass("vtkMRMLScalarVolumeNode")
142 | if firstVolumeNode:
143 | self._parameterNode.SetNodeReferenceID("InputVolume", firstVolumeNode.GetID())
144 |
145 | # ------------------------------------------------------------------------------------------------------------------
146 | def setParameterNode(self, inputParameterNode):
147 | """ SL_Notes: Set and observe the SingleTon ParameterNode.
148 | Observation is needed because when ParameteNode is changed then the GUI must be updated immediately.
149 | """
150 | print("\t\t**Widget.setParameterNode(self, inputParameterNode)")
151 | if inputParameterNode:
152 | if not inputParameterNode.IsSingleton():
153 | raise ValueError(f'SL__Allert! \tinputParameterNode = \n{inputParameterNode.__str__()}')
154 | self.logic.setDefaultParameters(inputParameterNode)
155 |
156 | # 01. Unobserve previously selected SingleTon ParameterNode
157 | if self._parameterNode is not None:
158 | self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode)
159 | # 02. Set new SingleTon ParameterNode and Add an observer to the newly selected -> immediate GUI reflection
160 | self._parameterNode = inputParameterNode
161 | if self._parameterNode is not None:
162 | self.addObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode)
163 | # 03. Initial GUI update; need to do this GUI update whenever there is a change from the SingleTon ParameterNode
164 | self.updateGUIFromParameterNode()
165 |
166 | # ------------------------------------------------------------------------------------------------------------------
167 | def updateGUIFromParameterNode(self, caller=None, event=None):
168 | """ This method is called whenever parameter node is changed.
169 | The module GUI is updated to show the current state of the parameter node. """
170 | # 00. Check self._updatingGUIFromParameterNode to prevent from GUI changes
171 | # (it could cause infinite loop: GUI change -> UpdateParamNode -> Update GUI -> UpdateParamNode)
172 | if self._parameterNode is None or self._updatingGUIFromParameterNode:
173 | return
174 |
175 | # I. Open-Brace: Make sure GUI changes do not call updateParameterNodeFromGUI__ (it could cause infinite loop)
176 | self._updatingGUIFromParameterNode = True
177 | # --------------------------------------------------------------------------------------------------------------
178 | # II. SL_Developer. In-Brace, Update UI widgets ()
179 | print("**Widget.updateGUIFromParameterNode(self, caller=None, event=None), \tSL_Developer")
180 | # II-01. Update node selectors and sliders
181 | self.ui.inputSelector.setCurrentNode(self._parameterNode.GetNodeReference("InputVolume"))
182 | self.ui.outputSelector.setCurrentNode(self._parameterNode.GetNodeReference("OutputVolume"))
183 | self.ui.invertedOutputSelector.setCurrentNode(self._parameterNode.GetNodeReference("OutputVolumeInverse"))
184 | self.ui.imageThresholdSliderWidget.value = float(self._parameterNode.GetParameter("Threshold"))
185 | self.ui.invertOutputCheckBox.checked = (self._parameterNode.GetParameter("Invert") == "true")
186 | # II-02. Update buttons states and tooltips
187 | if self._parameterNode.GetNodeReference("InputVolume") and self._parameterNode.GetNodeReference("OutputVolume"):
188 | self.ui.applyButton.toolTip = "Compute output volume"
189 | self.ui.applyButton.enabled = True
190 | else:
191 | self.ui.applyButton.toolTip = "Select input and output volume nodes"
192 | self.ui.applyButton.enabled = False
193 | # --------------------------------------------------------------------------------------------------------------
194 | # III. Close-Brace: All the GUI updates are done
195 | self._updatingGUIFromParameterNode = False
196 |
197 | # ------------------------------------------------------------------------------------------------------------------
198 | # ------------------------------------------------------------------------------------------------------------------
199 | def updateParameterNodeFromGUI(self, caller=None, event=None):
200 | """ Read GUI Method: Method updateParameterNodeFromGUI__ is called when users makes any change in the GUI.
201 | Changes are saved into the parameter node (so that they are restored when the scene is saved and loaded).
202 | """
203 | print(f"**Widget.updateParameterNodeFromGUI(self, caller=None, event=None), \t SL_Developer")
204 | if self._parameterNode is None or self._updatingGUIFromParameterNode:
205 | return
206 |
207 | # I. Before updating the SingleTon ParameterNode; Disable Modify events, e.g., vtk.vtkCommand.ModifiedEvent
208 | wasModified = self._parameterNode.StartModify() # Modify all properties in a single batch
209 |
210 | # II. Update the SingleTon ParameterNode; No updateGUIFromParameterNode triggered in this step
211 | self._parameterNode.SetNodeReferenceID("InputVolume", self.ui.inputSelector.currentNodeID)
212 | self._parameterNode.SetNodeReferenceID("OutputVolume", self.ui.outputSelector.currentNodeID)
213 | self._parameterNode.SetParameter("Threshold", str(self.ui.imageThresholdSliderWidget.value))
214 | self._parameterNode.SetParameter("Invert", "true" if self.ui.invertOutputCheckBox.checked else "false")
215 | self._parameterNode.SetNodeReferenceID("OutputVolumeInverse", self.ui.invertedOutputSelector.currentNodeID)
216 |
217 | # III. After updating the SingleTon ParameterNode; Enable Modify events, e.g., vtk.vtkCommand.ModifiedEvent
218 | self._parameterNode.EndModify(wasModified)
219 |
220 | # ------------------------------------------------------------------------------------------------------------------
221 | def onApplyButton(self):
222 | """ SL_Developer. Run processing when user clicks "Apply" button. """
223 | with slicer.util.tryWithErrorDisplay("Failed to compute results.", waitCursor=True):
224 |
225 | # Compute output
226 | self.logic.process(self.ui.inputSelector.currentNode(), self.ui.outputSelector.currentNode(),
227 | self.ui.imageThresholdSliderWidget.value, self.ui.invertOutputCheckBox.checked)
228 |
229 | # Compute inverted output (if needed)
230 | if self.ui.invertedOutputSelector.currentNode():
231 | # If additional output volume is selected then result with inverted threshold is written there
232 | self.logic.process(self.ui.inputSelector.currentNode(), self.ui.invertedOutputSelector.currentNode(),
233 | self.ui.imageThresholdSliderWidget.value, not self.ui.invertOutputCheckBox.checked, showResult=False)
234 |
235 |
236 | '''=================================================================================================================='''
237 | '''=================================================================================================================='''
238 | #
239 | # t_ApplyThresholdLogic
240 | #
241 | class t_ApplyThresholdLogic(ScriptedLoadableModuleLogic):
242 | """ The Logic class is : to facilitate dynamic reloading of the module without restarting the application.
243 | This class should implement all the actual computation done by your module.
244 | The interface should be such that other python code can import this class
245 | and make use of the functionality without requiring an instance of the Widget.
246 | Uses ScriptedLoadableModuleLogic base class, available at:
247 | https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
248 | """''''''
249 |
250 | def __init__(self):
251 | """ Called when the logic class is instantiated. Can be used for initializing member variables. """
252 | ScriptedLoadableModuleLogic.__init__(self)
253 | print("**Logic.__init__(self)")
254 |
255 | # ------------------------------------------------------------------------------------------------------------------
256 | def setDefaultParameters(self, parameterNode):
257 | """ SL_Developer: Initialize parameter node, Re-Enter, Re-Load. """''''''
258 | print("\t\t\t**Logic.setDefaultParameters(self, parameterNode), \tSL_Developer");
259 |
260 | """ Initialize parameter node with default settings. """
261 | if not parameterNode.GetParameter("Threshold"):
262 | parameterNode.SetParameter("Threshold", "100.0")
263 | if not parameterNode.GetParameter("Invert"):
264 | parameterNode.SetParameter("Invert", "false")
265 |
266 | # ------------------------------------------------------------------------------------------------------------------
267 | def process(self, inputVolume, outputVolume, imageThreshold, invert=False, showResult=True):
268 | """
269 | Run the processing algorithm.
270 | Can be used without GUI widget.
271 | :param inputVolume: volume to be thresholded
272 | :param outputVolume: thresholding result
273 | :param imageThreshold: values above/below this threshold will be set to 0
274 | :param invert: if True then values above the threshold will be set to 0, otherwise values below are set to 0
275 | :param showResult: show output volume in slice viewers
276 | """
277 |
278 | if not inputVolume or not outputVolume:
279 | raise ValueError("Input or output volume is invalid")
280 |
281 | import time
282 | startTime = time.time()
283 | logging.info('Processing started')
284 |
285 | # Compute the thresholded output volume using the "Threshold Scalar Volume" CLI module
286 | cliParams = {
287 | 'InputVolume': inputVolume.GetID(),
288 | 'OutputVolume': outputVolume.GetID(),
289 | 'ThresholdValue': imageThreshold,
290 | 'ThresholdType': 'Above' if invert else 'Below'
291 | }
292 | cliNode = slicer.cli.run(slicer.modules.thresholdscalarvolume, None, cliParams, wait_for_completion=True, update_display=showResult)
293 | # We don't need the CLI module node anymore, remove it to not clutter the scene with it
294 | slicer.mrmlScene.RemoveNode(cliNode)
295 |
296 | stopTime = time.time()
297 | logging.info(f'Processing completed in {stopTime-startTime:.2f} seconds')
298 |
299 | ''' ================================================================================================================='''
300 | ''' ================================================================================================================='''
301 | #
302 | # t_ApplyThresholdTest
303 | #
304 | class t_ApplyThresholdTest(ScriptedLoadableModuleTest):
305 | """ This is the test case for your scripted module.
306 | Uses ScriptedLoadableModuleTest base class, available at:
307 | https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py """
308 |
309 | def setUp(self):
310 | """ Do whatever is needed to reset the state - typically a scene clear will be enough. """
311 | slicer.mrmlScene.Clear()
312 |
313 | # ------------------------------------------------------------------------------------------------------------------
314 | def runTest(self):
315 | """Run as few or as many tests as needed here.
316 | """
317 | self.setUp()
318 | self.test_t_ApplyThreshold1()
319 |
320 | # ------------------------------------------------------------------------------------------------------------------
321 | def test_t_ApplyThreshold1(self):
322 | """ Ideally you should have several levels of tests. At the lowest level
323 | tests should exercise the functionality of the logic with different inputs
324 | (both valid and invalid). At higher levels your tests should emulate the
325 | way the user would interact with your code and confirm that it still works
326 | the way you intended.
327 | One of the most important features of the tests is that it should alert other
328 | developers when their changes will have an impact on the behavior of your
329 | module. For example, if a developer removes a feature that you depend on,
330 | your test should break so they know that the feature is needed.
331 | """
332 |
333 | self.delayDisplay("Starting the test")
334 |
335 | # Get/create input data
336 |
337 | import SampleData
338 | registerSampleData()
339 | inputVolume = SampleData.downloadSample('t_ApplyThreshold1')
340 | self.delayDisplay('Loaded test data set')
341 |
342 | inputScalarRange = inputVolume.GetImageData().GetScalarRange()
343 | self.assertEqual(inputScalarRange[0], 0)
344 | self.assertEqual(inputScalarRange[1], 695)
345 |
346 | outputVolume = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode")
347 | threshold = 100
348 |
349 | # Test the module logic
350 |
351 | logic = t_ApplyThresholdLogic()
352 |
353 | # Test algorithm with non-inverted threshold
354 | logic.process(inputVolume, outputVolume, threshold, True)
355 | outputScalarRange = outputVolume.GetImageData().GetScalarRange()
356 | self.assertEqual(outputScalarRange[0], inputScalarRange[0])
357 | self.assertEqual(outputScalarRange[1], threshold)
358 |
359 | # Test algorithm with inverted threshold
360 | logic.process(inputVolume, outputVolume, threshold, False)
361 | outputScalarRange = outputVolume.GetImageData().GetScalarRange()
362 | self.assertEqual(outputScalarRange[0], inputScalarRange[0])
363 | self.assertEqual(outputScalarRange[1], inputScalarRange[1])
364 |
365 | self.delayDisplay('Test passed')
366 |
367 | # ======================================================================================================================
368 | # ======================================================================================================================
369 | # ------------ Unit-Test: Load Data -----------------------------------------------------------------------
370 |
371 | #
372 | # Register sample data sets in Sample Data module
373 | #
374 | def registerSampleData():
375 | """
376 | Add data sets to Sample Data module.
377 | """
378 | # It is always recommended to provide sample data for users to make it easy to try the module,
379 | # but if no sample data is available then this method (and associated startupCompeted signal connection) can be removed.
380 |
381 | import SampleData
382 | iconsPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons')
383 |
384 | # To ensure that the source code repository remains small (can be downloaded and installed quickly)
385 | # it is recommended to store data sets that are larger than a few MB in a Github release.
386 |
387 | # t_ApplyThreshold1
388 | SampleData.SampleDataLogic.registerCustomSampleDataSource(
389 | # Category and sample name displayed in Sample Data module
390 | category='t_ApplyThreshold',
391 | sampleName='t_ApplyThreshold1',
392 | # Thumbnail should have size of approximately 260x280 pixels and stored in Resources/Icons folder.
393 | # It can be created by Screen Capture module, "Capture all views" option enabled, "Number of images" set to "Single".
394 | thumbnailFileName=os.path.join(iconsPath, 't_ApplyThreshold1.png'),
395 | # Download URL and target file name
396 | uris="https://github.com/Slicer/SlicerTestingData/releases/download/SHA256/998cb522173839c78657f4bc0ea907cea09fd04e44601f17c82ea27927937b95",
397 | fileNames='t_ApplyThreshold1.nrrd',
398 | # Checksum to ensure file integrity. Can be computed by this command:
399 | # import hashlib; print(hashlib.sha256(open(filename, "rb").read()).hexdigest())
400 | checksums='SHA256:998cb522173839c78657f4bc0ea907cea09fd04e44601f17c82ea27927937b95',
401 | # This node name will be used when the data set is loaded
402 | nodeNames='t_ApplyThreshold1'
403 | )
404 |
405 | # t_ApplyThreshold2
406 | SampleData.SampleDataLogic.registerCustomSampleDataSource(
407 | # Category and sample name displayed in Sample Data module
408 | category='t_ApplyThreshold',
409 | sampleName='t_ApplyThreshold2',
410 | thumbnailFileName=os.path.join(iconsPath, 't_ApplyThreshold2.png'),
411 | # Download URL and target file name
412 | uris="https://github.com/Slicer/SlicerTestingData/releases/download/SHA256/1a64f3f422eb3d1c9b093d1a18da354b13bcf307907c66317e2463ee530b7a97",
413 | fileNames='t_ApplyThreshold2.nrrd',
414 | checksums='SHA256:1a64f3f422eb3d1c9b093d1a18da354b13bcf307907c66317e2463ee530b7a97',
415 | # This node name will be used when the data set is loaded
416 | nodeNames='t_ApplyThreshold2'
417 | )
418 |
419 |
--------------------------------------------------------------------------------
/03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.16.3...3.19.7 FATAL_ERROR)
2 |
3 | project(SL_Tutorials)
4 |
5 | #-----------------------------------------------------------------------------
6 | # Extension meta-information
7 | set(EXTENSION_HOMEPAGE "https://www.slicer.org/wiki/Documentation/Nightly/Extensions/SL_Tutorials")
8 | set(EXTENSION_CATEGORY "Tutorials")
9 | set(EXTENSION_CONTRIBUTORS "Sen Li (ETS)")
10 | set(EXTENSION_DESCRIPTION "This is a series of tutorial for 3D Slicer Extension module development.")
11 | set(EXTENSION_ICONURL "https://www.example.com/Slicer/Extensions/SL_Tutorials.png")
12 | set(EXTENSION_SCREENSHOTURLS "https://www.example.com/Slicer/Extensions/SL_Tutorials/Screenshots/1.png")
13 | set(EXTENSION_DEPENDS "NA") # Specified as a list or "NA" if no dependencies
14 |
15 | #-----------------------------------------------------------------------------
16 | # Extension dependencies
17 | find_package(Slicer REQUIRED)
18 | include(${Slicer_USE_FILE})
19 |
20 | #-----------------------------------------------------------------------------
21 | # Extension modules
22 | add_subdirectory(sl__GUI_HelloWorld)
23 | ## NEXT_MODULE
24 |
25 | #-----------------------------------------------------------------------------
26 | include(${Slicer_EXTENSION_GENERATE_CONFIG})
27 | include(${Slicer_EXTENSION_CPACK})
28 |
--------------------------------------------------------------------------------
/03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld/README.md:
--------------------------------------------------------------------------------
1 | # 3D Slicer Extension Tutorial: Step by Step
2 |
3 |
4 |
5 |
6 |
7 | ## Step 03: Developer Foci, Neat Blanks & GUI Hello World
8 |
9 | [](https://www.youtube.com/watch?v=tOldfUkSecI&list=PLTuWbByD80TORd1R-J7j7nVQ9fot3C2fK)
10 |
11 | #### YouTube Video Tutorial for Step_03
12 |
13 | #### Bilibili Video Tutorial for Step_03
14 |
15 |
16 |
--------------------------------------------------------------------------------
/03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld/SL_Tutorials.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SenonETS/3DSlicerTutorial_ExtensionModuleDevelopment/c2ed7fd7e94f79953ec38019b16ca90019817bbc/03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld/SL_Tutorials.png
--------------------------------------------------------------------------------
/03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld/sl_03__Summary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SenonETS/3DSlicerTutorial_ExtensionModuleDevelopment/c2ed7fd7e94f79953ec38019b16ca90019817bbc/03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld/sl_03__Summary.png
--------------------------------------------------------------------------------
/03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld/sl__GUI_HelloWorld/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | #-----------------------------------------------------------------------------
2 | set(MODULE_NAME sl__GUI_HelloWorld)
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()
32 |
--------------------------------------------------------------------------------
/03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld/sl__GUI_HelloWorld/Resources/Icons/sl__GUI_HelloWorld.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SenonETS/3DSlicerTutorial_ExtensionModuleDevelopment/c2ed7fd7e94f79953ec38019b16ca90019817bbc/03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld/sl__GUI_HelloWorld/Resources/Icons/sl__GUI_HelloWorld.png
--------------------------------------------------------------------------------
/03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld/sl__GUI_HelloWorld/Resources/UI/sl__GUI_HelloWorld.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | t_ApplyThreshold
4 |
5 |
6 |
7 | 0
8 | 0
9 | 314
10 | 331
11 |
12 |
13 |
14 | -
15 |
16 |
17 | QPushButton#pushButton_HelloWorld {
18 | background-color: none;
19 | border-style: outset;
20 | border-width: 1.2px;
21 | border-radius: 10px;
22 | border-color: black;
23 | font: bold;
24 | min-width: 8em;
25 | padding: 3px;
26 | }
27 | QPushButton#pushButton_HelloWorld:pressed {
28 | background-color: rgb(110, 125, 0);
29 | border-style: outset;
30 | border-width: 1.2px;
31 | border-radius: 10px;
32 | border-color: black;
33 | font: bold;
34 | min-width: 8em;
35 | padding: 3px;
36 | }
37 | QPushButton#pushButton_HelloWorld:hover {
38 | background-color: rgb(225, 255, 0);
39 | border-style: outset;
40 | border-width: 1.2px;
41 | border-radius: 10px;
42 | border-color: black;
43 | font: bold;
44 | min-width: 8em;
45 | padding: 3px;
46 | }
47 |
48 | QPushButton#pushButton_HelloWorld:hover:pressed{
49 | background-color: rgb(110, 125, 0);
50 | border-style: outset;
51 | border-width: 1.2px;
52 | border-radius: 10px;
53 | border-color: black;
54 | font: bold;
55 | min-width: 8em;
56 | padding: 3px;
57 | }
58 |
59 |
60 |
61 | Say hello by clicking QPushButton
62 |
63 |
64 |
65 | -
66 |
67 |
68 | Qt::Vertical
69 |
70 |
71 |
72 | 20
73 | 40
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | qMRMLWidget
83 | QWidget
84 |
85 | 1
86 |
87 |
88 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld/sl__GUI_HelloWorld/Testing/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | add_subdirectory(Python)
2 |
--------------------------------------------------------------------------------
/03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld/sl__GUI_HelloWorld/Testing/Python/CMakeLists.txt:
--------------------------------------------------------------------------------
1 |
2 | #slicer_add_python_unittest(SCRIPT ${MODULE_NAME}ModuleTest.py)
3 |
--------------------------------------------------------------------------------
/03__DeveloperFoci_NeatBlanks_&_GUI_HelloWorld/sl__GUI_HelloWorld/sl__GUI_HelloWorld.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 |
4 | import vtk
5 |
6 | import slicer, qt
7 | from slicer.ScriptedLoadableModule import *
8 | from slicer.util import VTKObservationMixin
9 |
10 | '''=================================================================================================================='''
11 | '''=================================================================================================================='''
12 | #
13 | # sl__GUI_HelloWorld
14 | #
15 | class sl__GUI_HelloWorld(ScriptedLoadableModule):
16 |
17 | def __init__(self, parent):
18 | ScriptedLoadableModule.__init__(self, parent)
19 | self.parent.title = "sl__GUI_HelloWorld"
20 | self.parent.categories = ["SL_Tutorials"]
21 | self.parent.dependencies = [] # TODO: add here list of module names that this module requires
22 | self.parent.contributors = ["Sen Li (École de Technologie Supérieure)"]
23 | # TODO: update with short description of the module and a link to online module documentation
24 | self.parent.helpText = """This is sl__GUI_HelloWorld ! """
25 | self.parent.acknowledgementText = 'Step-by-step tutorial on 3D Slicer extension development. ' \
26 | '\nThis file was originally developed by Sen Li, LATIS, École de techonologie supérieure. ' \
27 | '\nSen.Li.1@ens.etsmtl.ca'
28 |
29 | print("sl__GUI_HelloWorld(ScriptedLoadableModule): __init__(self, parent)")
30 |
31 | '''=================================================================================================================='''
32 | '''=================================================================================================================='''
33 | #
34 | # sl__GUI_HelloWorldWidget
35 | #
36 | class sl__GUI_HelloWorldWidget(ScriptedLoadableModuleWidget, VTKObservationMixin):
37 |
38 | def __init__(self, parent=None):
39 | """ Called when the user opens the module the first time and the widget is initialized. """
40 | ScriptedLoadableModuleWidget.__init__(self, parent)
41 | VTKObservationMixin.__init__(self) # needed for parameter node observation
42 | self.logic = None
43 | self._parameterNode = None # SingleTon initialized through self.setParameterNode(self.logic.getParameterNode())
44 | self._updatingGUIFromParameterNode = False
45 | print("**Widget.__init__(self, parent)")
46 |
47 | # ------------------------------------------------------------------------------------------------------------------
48 | def setup(self):
49 | print("**Widget.setup(self), \tSL_Developer")
50 |
51 | """ 00. Called when the user opens the module the first time and the widget is initialized. """
52 | ScriptedLoadableModuleWidget.setup(self)
53 |
54 | # 01. Load widget from .ui file (created by Qt Designer).
55 | # Additional widgets can be instantiated manually and added to self.layout.
56 | uiWidget = slicer.util.loadUI(self.resourcePath('UI/sl__GUI_HelloWorld.ui'))
57 | self.layout.addWidget(uiWidget)
58 | self.ui = slicer.util.childWidgetVariables(uiWidget)
59 |
60 | # 02. Set scene in MRML widgets. Make sure that in Qt designer the
61 | # top-level qMRMLWidget's "mrmlSceneChanged(vtkMRMLScene*)" signal in is connected to
62 | # each MRML widget's "setMRMLScene(vtkMRMLScene*)" slot.
63 | uiWidget.setMRMLScene(slicer.mrmlScene)
64 |
65 | # 03. Create logic class. Logic implements all computations that should be possible to run
66 | # in batch mode, without a graphical user interface.
67 | self.logic = sl__GUI_HelloWorldLogic()
68 |
69 | # 04. Connections, ensure that we update parameter node when scene is closed
70 | self.addObserver(slicer.mrmlScene, slicer.mrmlScene.StartCloseEvent, self.onSceneStartClose)
71 | self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndCloseEvent, self.onSceneEndClose)
72 |
73 | # 05. SL_Developer. Connect Signal-Slot, ensure that whenever user changes some settings on the GUI,
74 | # that is saved in the MRML scene (in the selected parameter node).
75 | self.ui.pushButton_HelloWorld.clicked.connect(self.onButtonHelloWorld_Clicked)
76 |
77 | # 06. Needed for programmer-friendly Module-Reload where the Module had already been enter(self)-ed;
78 | # Otherwise, will initial through function enter(self)
79 | if self.parent.isEntered:
80 | self.initializeParameterNode()
81 |
82 | # ------------------------------------------------------------------------------------------------------------------
83 | def cleanup(self):
84 | """ Called when the application closes and the module widget is destroyed. """
85 | print("**Widget.cleanup(self)")
86 | self.removeObservers()
87 |
88 | # ------------------------------------------------------------------------------------------------------------------
89 | def enter(self):
90 | """ Called each time the user opens this module. """
91 | print("\n**Widget.enter(self)")
92 |
93 | # 01. Slicer. SL__Note: Every-Module own a SingleTon ParameterNode that can be identified by
94 | # self._parameterNode.GetAttribute('ModuleName')! Need to initial every Entry!
95 | self.initializeParameterNode()
96 |
97 | # ------------------------------------------------------------------------------------------------------------------
98 | def exit(self):
99 | """ Called each time the user opens a different module. """
100 | print("**Widget.exit(self)")
101 | # Slicer. Do not react to parameter node changes (GUI will be updated when the user enters into the module)
102 | self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode)
103 |
104 | # ------------------------------------------------------------------------------------------------------------------
105 | def onSceneStartClose(self, caller, event):
106 | """ Called just before the scene is closed. """
107 | print("**Widget.onSceneStartClose(self, caller, event)")
108 |
109 | # Slicer. Parameter node will be reset, do not use it anymore
110 | self.setParameterNode(None)
111 |
112 | # ------------------------------------------------------------------------------------------------------------------
113 | def onSceneEndClose(self, caller, event):
114 | """ Called just after the scene is closed. """''''''
115 | print("**Widget.onSceneEndClose(self, caller, event)")
116 | # If this module is shown while the scene is closed then recreate a new parameter node immediately
117 | if self.parent.isEntered:
118 | self.initializeParameterNode()
119 |
120 | # ------------------------------------------------------------------------------------------------------------------
121 | def initializeParameterNode(self):
122 | """ Ensure parameter node exists and observed. """''''''
123 | print("\t**Widget.initializeParameterNode(self), \t SL_Developer")
124 | # 01. Slicer-Initial: the SingleTon ParameterNode stores all user choices in param-values, node selections...
125 | # so that when the scene is saved and reloaded, these settings are restored.
126 | self.setParameterNode(self.logic.getParameterNode())
127 |
128 | # 02. SL_Developer. To update ParameterNode and attach observers
129 | pass
130 |
131 | # ------------------------------------------------------------------------------------------------------------------
132 | def setParameterNode(self, inputParameterNode):
133 | """ SL_Notes: Set and observe the SingleTon ParameterNode.
134 | Observation is needed because when ParameteNode is changed then the GUI must be updated immediately.
135 | """
136 | print("\t\t**Widget.setParameterNode(self, inputParameterNode)")
137 | if inputParameterNode:
138 | if not inputParameterNode.IsSingleton():
139 | raise ValueError(f'SL__Allert! \tinputParameterNode = \n{inputParameterNode.__str__()}')
140 | self.logic.setDefaultParameters(inputParameterNode)
141 |
142 | # 01. Unobserve previously selected SingleTon ParameterNode
143 | if self._parameterNode is not None:
144 | self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode)
145 | # 02. Set new SingleTon ParameterNode and Add an observer to the newly selected -> immediate GUI reflection
146 | self._parameterNode = inputParameterNode
147 | if self._parameterNode is not None:
148 | self.addObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode)
149 | # 03. Initial GUI update; need to do this GUI update whenever there is a change from the SingleTon ParameterNode
150 | self.updateGUIFromParameterNode()
151 |
152 | # ------------------------------------------------------------------------------------------------------------------
153 | def updateGUIFromParameterNode(self, caller=None, event=None):
154 | """ This method is called whenever parameter node is changed.
155 | The module GUI is updated to show the current state of the parameter node. """
156 | # 00. Check self._updatingGUIFromParameterNode to prevent from GUI changes
157 | # (it could cause infinite loop: GUI change -> UpdateParamNode -> Update GUI -> UpdateParamNode)
158 | if self._parameterNode is None or self._updatingGUIFromParameterNode:
159 | return
160 |
161 | # I. Open-Brace: Make sure GUI changes do not call updateParameterNodeFromGUI__ (it could cause infinite loop)
162 | self._updatingGUIFromParameterNode = True
163 | # --------------------------------------------------------------------------------------------------------------
164 | # II. SL_Developer. In-Brace, Update UI widgets ()
165 | print("**Widget.updateGUIFromParameterNode(self, caller=None, event=None), \tSL_Developer")
166 | # II-01. Update buttons states and tooltips
167 | self.ui.pushButton_HelloWorld.toolTip = "Click Me and join SL Slicer Tutorial World"
168 | self.ui.pushButton_HelloWorld.enabled = True
169 | # --------------------------------------------------------------------------------------------------------------
170 | # III. Close-Brace: All the GUI updates are done
171 | self._updatingGUIFromParameterNode = False
172 |
173 | # ------------------------------------------------------------------------------------------------------------------
174 | # ------------------------------------------------------------------------------------------------------------------
175 | def updateParameterNodeFromGUI(self, caller=None, event=None):
176 | """ Read GUI Method: Method updateParameterNodeFromGUI__ is called when users makes any change in the GUI.
177 | Changes are saved into the parameter node (so that they are restored when the scene is saved and loaded).
178 | """
179 | print(f"**Widget.updateParameterNodeFromGUI(self, caller=None, event=None), \t SL_Developer")
180 | if self._parameterNode is None or self._updatingGUIFromParameterNode:
181 | return
182 |
183 | # I. Before updating the SingleTon ParameterNode; Disable Modify events, e.g., vtk.vtkCommand.ModifiedEvent
184 | wasModified = self._parameterNode.StartModify() # Modify all properties in a single batch
185 |
186 | # II. SL_Developer. Update the Singleton ParameterNode; No updateGUIFromParameterNode triggered in this step
187 | # self._parameterNode.SetNodeReferenceID("InputVolume", self.ui.inputSelector.currentNodeID)
188 |
189 | # III. After updating the SingleTon ParameterNode; Enable Modify events, e.g., vtk.vtkCommand.ModifiedEvent
190 | self._parameterNode.EndModify(wasModified)
191 |
192 | # ------------------------------------------------------------------------------------------------------------------
193 | def onButtonHelloWorld_Clicked(self):
194 | """ SL_Developer. Run processing when user clicks "Hello World" button. """
195 | with slicer.util.tryWithErrorDisplay("Failed to compute results.", waitCursor=True):
196 |
197 | str_Result = self.logic.obtainStr_HelloWorld()
198 | qt.QMessageBox.information(slicer.util.mainWindow(), 'SL Tutorial: GUI HelloWorld', str_Result)
199 |
200 |
201 | '''=================================================================================================================='''
202 | '''=================================================================================================================='''
203 | #
204 | # sl__GUI_HelloWorldLogic
205 | #
206 | class sl__GUI_HelloWorldLogic(ScriptedLoadableModuleLogic):
207 | """ The Logic class is : to facilitate dynamic reloading of the module without restarting the application.
208 | This class should implement all the actual computation done by your module.
209 | The interface should be such that other python code can import this class
210 | and make use of the functionality without requiring an instance of the Widget.
211 | Uses ScriptedLoadableModuleLogic base class, available at:
212 | https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
213 | """''''''
214 |
215 | def __init__(self):
216 | """ Called when the logic class is instantiated. Can be used for initializing member variables. """
217 | ScriptedLoadableModuleLogic.__init__(self)
218 | print("**Logic.__init__(self)")
219 |
220 | # ------------------------------------------------------------------------------------------------------------------
221 | def setDefaultParameters(self, parameterNode):
222 | """ SL_Developer: Initialize parameter node, Re-Enter, Re-Load. """''''''
223 | print("\t\t\t**Logic.setDefaultParameters(self, parameterNode), \tSL_Developer");
224 |
225 | # To initial ModuleSingleton ParameterNode
226 | pass
227 |
228 | # ------------------------------------------------------------------------------------------------------------------
229 | def obtainStr_HelloWorld(self):
230 | """ To be called without GUI **Widget. """
231 | return "SL: Hello World!"
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 | ''' ================================================================================================================='''
247 | ''' ================================================================================================================='''
248 | ''' ================================================================================================================='''
249 | #
250 | # sl__GUI_HelloWorldTest
251 | #
252 | class sl__GUI_HelloWorldTest(ScriptedLoadableModuleTest):
253 | """ This is the test case for your scripted module.
254 | Uses ScriptedLoadableModuleTest base class, available at:
255 | https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py """
256 |
257 | def setUp(self):
258 | """ Do whatever is needed to reset the state - typically a scene clear will be enough. """
259 | slicer.mrmlScene.Clear()
260 |
261 | # ------------------------------------------------------------------------------------------------------------------
262 | def runTest(self):
263 | """Run as few or as many tests as needed here.
264 | """
265 | self.setUp()
266 | self.test_sl__GUI_HelloWorld1()
267 |
268 | # ------------------------------------------------------------------------------------------------------------------
269 | def test_sl__GUI_HelloWorld1(self):
270 | """ Ideally you should have several levels of tests. At the lowest level
271 | tests should exercise the functionality of the logic with different inputs
272 | (both valid and invalid). At higher levels your tests should emulate the
273 | way the user would interact with your code and confirm that it still works
274 | the way you intended.
275 | One of the most important features of the tests is that it should alert other
276 | developers when their changes will have an impact on the behavior of your
277 | module. For example, if a developer removes a feature that you depend on,
278 | your test should break so they know that the feature is needed.
279 | """
280 |
281 | self.delayDisplay("Starting the test")
282 |
283 | # Test the module logic
284 | logic = sl__GUI_HelloWorldLogic()
285 | str_Result = logic.obtainStr_HelloWorld()
286 | self.assertIsNotNone(str_Result)
287 | self.delayDisplay('Test passed')
288 |
--------------------------------------------------------------------------------
/04__CodeStyle_MethodGroups_&_US_SeqViewer/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.16.3...3.19.7 FATAL_ERROR)
2 |
3 | project(SL_Tutorials)
4 |
5 | #-----------------------------------------------------------------------------
6 | # Extension meta-information
7 | set(EXTENSION_HOMEPAGE "https://www.slicer.org/wiki/Documentation/Nightly/Extensions/SL_Tutorials")
8 | set(EXTENSION_CATEGORY "Tutorials")
9 | set(EXTENSION_CONTRIBUTORS "Sen Li (ETS)")
10 | set(EXTENSION_DESCRIPTION "This is a series of tutorial for 3D Slicer Extension module development.")
11 | set(EXTENSION_ICONURL "https://www.example.com/Slicer/Extensions/SL_Tutorials.png")
12 | set(EXTENSION_SCREENSHOTURLS "https://www.example.com/Slicer/Extensions/SL_Tutorials/Screenshots/1.png")
13 | set(EXTENSION_DEPENDS "NA") # Specified as a list or "NA" if no dependencies
14 |
15 | #-----------------------------------------------------------------------------
16 | # Extension dependencies
17 | find_package(Slicer REQUIRED)
18 | include(${Slicer_USE_FILE})
19 |
20 | #-----------------------------------------------------------------------------
21 | # Extension modules
22 | add_subdirectory(sl__US_SeqViewer)
23 | ## NEXT_MODULE
24 |
25 | #-----------------------------------------------------------------------------
26 | include(${Slicer_EXTENSION_GENERATE_CONFIG})
27 | include(${Slicer_EXTENSION_CPACK})
28 |
--------------------------------------------------------------------------------
/04__CodeStyle_MethodGroups_&_US_SeqViewer/README.md:
--------------------------------------------------------------------------------
1 | # 3D Slicer Extension Tutorial: Step by Step
2 |
3 |
4 |
5 |
6 | ## Step 04: Index SequenceBrowser
7 |
8 | [](https://www.youtube.com/watch?v=f_gsm0GJ4_8&list=PLTuWbByD80TORd1R-J7j7nVQ9fot3C2fK)
9 |
10 |
11 | #### YouTube Video Tutorial for Step_04
12 |
13 | #### Bilibili Video Tutorial for Step_04
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/04__CodeStyle_MethodGroups_&_US_SeqViewer/SL_Tutorials.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SenonETS/3DSlicerTutorial_ExtensionModuleDevelopment/c2ed7fd7e94f79953ec38019b16ca90019817bbc/04__CodeStyle_MethodGroups_&_US_SeqViewer/SL_Tutorials.png
--------------------------------------------------------------------------------
/04__CodeStyle_MethodGroups_&_US_SeqViewer/sl_04__Summary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SenonETS/3DSlicerTutorial_ExtensionModuleDevelopment/c2ed7fd7e94f79953ec38019b16ca90019817bbc/04__CodeStyle_MethodGroups_&_US_SeqViewer/sl_04__Summary.png
--------------------------------------------------------------------------------
/04__CodeStyle_MethodGroups_&_US_SeqViewer/sl__US_SeqViewer/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | #-----------------------------------------------------------------------------
2 | set(MODULE_NAME sl__US_SeqViewer)
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()
32 |
--------------------------------------------------------------------------------
/04__CodeStyle_MethodGroups_&_US_SeqViewer/sl__US_SeqViewer/Resources/Icons/sl__US_SeqViewer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SenonETS/3DSlicerTutorial_ExtensionModuleDevelopment/c2ed7fd7e94f79953ec38019b16ca90019817bbc/04__CodeStyle_MethodGroups_&_US_SeqViewer/sl__US_SeqViewer/Resources/Icons/sl__US_SeqViewer.png
--------------------------------------------------------------------------------
/04__CodeStyle_MethodGroups_&_US_SeqViewer/sl__US_SeqViewer/Resources/UI/sl__US_SeqViewer.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | sl__US_SeqViewer
4 |
5 |
6 |
7 | 0
8 | 0
9 | 485
10 | 556
11 |
12 |
13 |
14 | -
15 |
16 |
17 |
18 | 14
19 | 75
20 | true
21 |
22 |
23 |
24 | Ultrasound Sequence Viewer
25 |
26 |
27 |
28 | -
29 |
30 |
31 | Qt::Vertical
32 |
33 |
34 | QSizePolicy::Fixed
35 |
36 |
37 |
38 | 20
39 | 10
40 |
41 |
42 |
43 |
44 | -
45 |
46 |
-
47 |
48 |
49 | Sequence browser:
50 |
51 |
52 |
53 | -
54 |
55 |
56 |
57 | vtkMRMLSequenceBrowserNode
58 |
59 |
60 |
61 | false
62 |
63 |
64 | false
65 |
66 |
67 | false
68 |
69 |
70 | true
71 |
72 |
73 |
74 |
75 |
76 | -
77 |
78 |
-
79 |
80 |
81 | N/A
82 |
83 |
84 |
85 | -
86 |
87 |
88 |
89 | 0
90 | 0
91 |
92 |
93 |
94 | 1
95 |
96 |
97 | 99
98 |
99 |
100 | 50
101 |
102 |
103 | Qt::Horizontal
104 |
105 |
106 |
107 | -
108 |
109 |
110 | FrameIndex (from 1):
111 |
112 |
113 |
114 |
115 |
116 | -
117 |
118 |
119 | Qt::Vertical
120 |
121 |
122 |
123 | 20
124 | 40
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 | qMRMLNodeComboBox
134 | QWidget
135 |
136 |
137 |
138 | qMRMLWidget
139 | QWidget
140 |
141 | 1
142 |
143 |
144 |
145 |
146 |
147 | sl__US_SeqViewer
148 | mrmlSceneChanged(vtkMRMLScene*)
149 | sequenceSelector
150 | setMRMLScene(vtkMRMLScene*)
151 |
152 |
153 | 289
154 | 439
155 |
156 |
157 | 338
158 | 19
159 |
160 |
161 |
162 |
163 | slider_SeqFrame
164 | valueChanged(int)
165 | label_FrameIndex
166 | setNum(int)
167 |
168 |
169 | 333
170 | 45
171 |
172 |
173 | 559
174 | 45
175 |
176 |
177 |
178 |
179 |
180 |
--------------------------------------------------------------------------------
/04__CodeStyle_MethodGroups_&_US_SeqViewer/sl__US_SeqViewer/Testing/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | add_subdirectory(Python)
2 |
--------------------------------------------------------------------------------
/04__CodeStyle_MethodGroups_&_US_SeqViewer/sl__US_SeqViewer/Testing/Python/CMakeLists.txt:
--------------------------------------------------------------------------------
1 |
2 | #slicer_add_python_unittest(SCRIPT ${MODULE_NAME}ModuleTest.py)
3 |
--------------------------------------------------------------------------------
/04__CodeStyle_MethodGroups_&_US_SeqViewer/sl__US_SeqViewer/sl__US_SeqViewer.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import vtk, qt
3 |
4 | import slicer
5 | from slicer.ScriptedLoadableModule import *
6 | from slicer.util import VTKObservationMixin
7 |
8 | import numpy as np
9 |
10 | '''=================================================================================================================='''
11 | '''=================================================================================================================='''
12 | '''------------------------- STRING Macro of sl__US_SeqViewer ------------------------------------------------------'''
13 | '''------------------------------------------------------------------------------------------------------------------'''
14 | INT_SliderFrameIndex_Min = 1 # StartingValue of slider_FrameIndex, increase from 1
15 | INT_FRAME_INDEX_SLIDER_DEFAULT = 50 # Default slider_FrameIndex value
16 | INT_FRAME_INDEX_SLIDER_DEFAULT_MAX = 99 # Default slider_FrameIndex maximum
17 |
18 | # ReferenceRole
19 | STR_SeqBrowserNode_RefRole_Selected = 'SeqBrowser_Ref_CurSelected'
20 |
21 |
22 |
23 | '''=================================================================================================================='''
24 | #
25 | # sl__US_SeqViewer
26 | #
27 | class sl__US_SeqViewer(ScriptedLoadableModule):
28 |
29 | def __init__(self, parent):
30 | ScriptedLoadableModule.__init__(self, parent)
31 | self.parent.title = "sl__US_SeqViewer"
32 | self.parent.categories = ["SL_Tutorials"] # Set categories (the module shows up in the module selector)
33 | self.parent.dependencies = ["Markups"] # Add here list of module names that this module requires
34 | self.parent.contributors = ["Sen Li (École de Technologie Supérieure)"]
35 | # TODO: 10. update with a link to online module Tutorial
36 | self.parent.helpText = """This is sl__US_SeqViewer ! """
37 | self.parent.helpText += self.getDefaultModuleDocumentationLink()
38 | self.parent.acknowledgementText = 'Step-by-step tutorial on 3D Slicer extension development. ' \
39 | '\nThis file was originally developed by Sen Li, LATIS, École de techonologie supérieure. ' \
40 | '\nSen.Li.1@ens.etsmtl.ca'
41 |
42 | print("sl__US_SeqViewer(ScriptedLoadableModule): __init__(self, parent)")
43 |
44 | '''=================================================================================================================='''
45 | class sl__US_SeqViewerWidget(ScriptedLoadableModuleWidget, VTKObservationMixin):
46 |
47 | def __init__(self, parent=None):
48 | """ Called when the user opens the module the first time and the widget is initialized. """
49 | ScriptedLoadableModuleWidget.__init__(self, parent)
50 | VTKObservationMixin.__init__(self) # needed for parameter node observation
51 | self.logic = None
52 | self._parameterNode = None # Singleton initialized through self.setParameterNode(self.logic.getParameterNode())
53 | self._updatingGUIFromParameterNode = False
54 |
55 | print("**Widget.__init__(self, parent)")
56 |
57 | def setup(self):
58 | print("**Widget.setup(self), \tSL_Developer")
59 | """ 00. Called when the user opens the module the first time and the widget is initialized. """
60 | ScriptedLoadableModuleWidget.setup(self)
61 |
62 | # 01. Load widget from .ui file (created by Qt Designer).
63 | # Additional widgets can be instantiated manually and added to self.layout.
64 | uiWidget = slicer.util.loadUI(self.resourcePath('UI/sl__US_SeqViewer.ui'))
65 | self.layout.addWidget(uiWidget)
66 | self.ui = slicer.util.childWidgetVariables(uiWidget)
67 |
68 | # 02. Set scene in MRML widgets. Make sure that in Qt designer the
69 | # top-level qMRMLWidget's "mrmlSceneChanged(vtkMRMLScene*)" signal in is connected to
70 | # each MRML widget's "setMRMLScene(vtkMRMLScene*)" slot.
71 | uiWidget.setMRMLScene(slicer.mrmlScene)
72 |
73 | # 03. Create logic class. Logic implements all computations that should be possible to run
74 | # in batch mode, without a graphical user interface.
75 | self.logic = sl__US_SeqViewerLogic()
76 |
77 | # 04. Connections, ensure that we update parameter node when scene is closed
78 | self.addObserver(slicer.mrmlScene, slicer.mrmlScene.StartCloseEvent, self.onSceneStartClose)
79 | self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndCloseEvent, self.onSceneEndClose)
80 |
81 | # 05. SL_Developer. Connect Signal-Slot, ensure that whenever user changes some settings on the GUI,
82 | # that is saved in the MRML scene (in the selected parameter node).
83 | self.ui.sequenceSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onSelectedNodeChanged)
84 | slicer.modules.sequences.toolBar().activeBrowserNodeChanged.connect(self.onSelectedNodeChanged)
85 |
86 | self.ui.slider_SeqFrame.connect("valueChanged(int)", self.onSliderFrameIndex_ValueChanged)
87 |
88 |
89 | # 06. Needed for programmer-friendly Module-Reload where the Module had already been enter(self)-ed;
90 | # Otherwise, will initial through function enter(self)
91 | if self.parent.isEntered:
92 | self.initializeParameterNode() # Every-Module own a Singleton ParameterNode track by **Logic.moduleName!
93 |
94 | # ------------------------------------------------------------------------------------------------------------------
95 | def cleanup(self):
96 | """ Called when the application closes and the module widget is destroyed. """
97 | print("**Widget.cleanup(self)")
98 | self.removeObservers()
99 |
100 | # ------------------------------------------------------------------------------------------------------------------
101 | def enter(self):
102 | """ Called each time the user opens this module. """
103 | print("**Widget.enter(self)")
104 |
105 | # 01. Slicer. SL__Note: Every-Module own a Singleton ParameterNode that can be identified by
106 | # self._parameterNode.GetAttribute('ModuleName')! Need to initial every Entry!
107 | self.initializeParameterNode()
108 |
109 | # ------------------------------------------------------------------------------------------------------------------
110 | def exit(self):
111 | """ Called each time the user opens a different module. """
112 | print("**Widget.exit(self)")
113 | # Slicer. Do not react to parameter node changes (GUI will be updated when the user enters into the module)
114 | self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode)
115 |
116 | # ------------------------------------------------------------------------------------------------------------------
117 | def onSceneStartClose(self, caller, event):
118 | """ Called just before the scene is closed. """
119 | print("**Widget.onSceneStartClose(self, caller, event)")
120 |
121 | # Slicer. Parameter node will be reset, do not use it anymore
122 | self.setParameterNode(None)
123 |
124 | # ------------------------------------------------------------------------------------------------------------------
125 | def onSceneEndClose(self, caller, event):
126 | """ Called just after the scene is closed. """
127 | print("**Widget.onSceneEndClose(self, caller, event)")
128 | # If this module is shown while the scene is closed then recreate a new parameter node immediately
129 | if self.parent.isEntered:
130 | self.initializeParameterNode()
131 |
132 | # ------------------------------------------------------------------------------------------------------------------
133 | def initializeParameterNode(self):
134 | """ Ensure parameter node exists and observed. """
135 | # 01. Slicer-Initial: the Singleton ParameterNode stores all user choices in param-values, node selections...
136 | # so that when the scene is saved and reloaded, these settings are restored.
137 | self.setParameterNode(self.logic.getParameterNode())
138 |
139 | # 02. SL_Developer. To update ParameterNode and attach observers
140 | pass
141 |
142 |
143 | # ------------------------------------------------------------------------------------------------------------------
144 | def setParameterNode(self, inputParameterNode):
145 | """ SL_Notes: Set and observe the Singleton ParameterNode.
146 | Observation is needed because when ParameterNode is changed then the GUI must be updated immediately.
147 | """
148 | print("**Widget.setParameterNode(self, inputParameterNode)")
149 | if inputParameterNode:
150 | if not inputParameterNode.IsSingleton():
151 | raise ValueError(f'SL__Allert! \tinputParameterNode = \n{inputParameterNode.__str__()}')
152 | self.logic.setDefaultParameters(inputParameterNode)
153 |
154 | # 01. Unobserve previously selected Singleton ParameterNode;
155 | if self._parameterNode is not None:
156 | self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode)
157 | # 02. Set new Singleton ParameterNode and Add an observer to the newly selected
158 | self._parameterNode = inputParameterNode
159 | if self._parameterNode is not None:
160 | self.addObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode)
161 | # 03. Initial GUI update; need to do this GUI update whenever there is a change from the Singleton ParameterNode
162 | self.updateGUIFromParameterNode()
163 |
164 | # ==================================================================================================================
165 | # ==================================================================================================================
166 | # =========== SL_Developer, Section I: get, set, obtain ================================
167 | # ------------------------------------------------------------------------------------------------------------------
168 | def getSelectedItemNumber_FromGUI_Slider(self):
169 | # Slider FrameIndex starts from 1, but idx_SelectedItemNumber starts 0.
170 | idx_CurSeqBrowser_SelectedItemNumber = self.ui.slider_SeqFrame.value - INT_SliderFrameIndex_Min
171 | return idx_CurSeqBrowser_SelectedItemNumber
172 |
173 | # ------------------------------------------------------------------------------------------------------------------
174 | # ==================================================================================================================
175 | # ==================================================================================================================
176 | # =========== SL_Developer, Section II-A: updateGUIFromParameterNode__ & Slots that call uiUpdate =
177 | # ------------------------------------------------------------------------------------------------------------------
178 | def updateGUIFromParameterNode(self, caller=None, event=None):
179 | """ This method is called whenever parameter node is changed.
180 | The module GUI is updated to show the current state of the parameter node. """
181 | # 00. Check self._updatingGUIFromParameterNode to prevent from GUI changes
182 | # (it could cause infinite loop: GUI change -> UpdateParamNode -> Update GUI -> UpdateParamNode)
183 | if self._parameterNode is None or self._updatingGUIFromParameterNode:
184 | return
185 |
186 | # I. Open-Brace: Make sure GUI changes do not call updateParameterNodeFromGUI__ (it could cause infinite loop)
187 | self._updatingGUIFromParameterNode = True
188 | # --------------------------------------------------------------------------------------------------------------
189 | # II. SL_Developer, C: In-Brace, Update UI widgets ()
190 | print("**Widget.updateGUIFromParameterNode(self, caller=None, event=None), \tSL_Developer")
191 | # II-01. Update Values of Node-Selectors (qMRMLNodeComboBox)
192 | nodeSeqBrowser_Selected = self._parameterNode.GetNodeReference(STR_SeqBrowserNode_RefRole_Selected)
193 | self.ui.sequenceSelector.setCurrentNode(nodeSeqBrowser_Selected)
194 | # II-02. Update Status of slider_SeqFrame, and label_FrameIndex: QLabel, Sliders (ctkSliderWidget)
195 | self.uiUpdate_Slider_SeqFrame__by__nodeSeqBrowser_Selected(nodeSeqBrowser_Selected)
196 |
197 | # --------------------------------------------------------------------------------------------------------------
198 | # III. Close-Brace: All the GUI updates are done;
199 | self._updatingGUIFromParameterNode = False
200 |
201 | # ------------------------------------------------------------------------------------------------------------------
202 | # ------------------------------------------------------------------------------------------------------------------
203 | def onSliderFrameIndex_ValueChanged(self, caller=None, event=None):
204 | ''' SL_Notes: Not UserOnly function, can be called when a target_ControlPoint is selected! ''' ''''''
205 | # 00. Check Singleton ParameterNode: in case of enter() or onSceneStartClose()
206 | if self._parameterNode is None or self._updatingGUIFromParameterNode:
207 | return
208 |
209 | # 01. LogicUpdate: nodeSeqBrowser's Current-SelectedItemNumber
210 | idx_CurFrame = self.getSelectedItemNumber_FromGUI_Slider()
211 | self.logic.logicUpdate_SwitchSelection_SelectedSeqBrowser_ChangeFrameIndex(idx_CurFrame)
212 | print(f'\t**Widget.onSliderFrameIndex_ValueChanged,\tidx_CurFrame = {idx_CurFrame}')
213 |
214 | # 02. uiUpdate: LandmarkPositionLabels
215 | self._updatingGUIFromParameterNode = True # I. Open-Brace: Avoid updateParameterNodeFromGUI__ (infinite loop)
216 | self.uiUpdate_SwitchSelection_SelectedSeqBrowser_ChangeFrameIndex() # II. In-Brace: uiUpdate
217 | self._updatingGUIFromParameterNode = False # III. Close-Brace: All the GUI updates are done;
218 |
219 | # ------------------------------------------------------------------------------------------------------------------
220 | def onSelectedNodeChanged(self, node_NewActiveBrowser=None, event=None):
221 | ''' SL_Notes: Not UserOnly function, can be called when a target_ControlPoint is selected! ''' ''''''
222 | print(f"\nBeginning of **Widget.onSelectedNodeChanged(): \tnode_NewActiveBrowser ="
223 | f" {node_NewActiveBrowser.GetID() if node_NewActiveBrowser else type(node_NewActiveBrowser)}")
224 | # 00-A. Check Singleton ParameterNode: important test for every NodeChange Slot, in case of onSceneStartClose()
225 | # Check _updatingGUIFromParameterNode: avoid bugs introduced by Slicer (PointAdded, PointPositionDefined)
226 | if self._parameterNode is None or self._updatingGUIFromParameterNode:
227 | return
228 | # 00-B. Check the validity of node_NewActiveBrowser
229 | if not node_NewActiveBrowser:
230 | return
231 |
232 | # 01. LogicUpdate
233 | self.updateParameterNodeFromGUI__Set_RefRoleNodeID(STR_SeqBrowserNode_RefRole_Selected, node_NewActiveBrowser.GetID())
234 |
235 | # 02. uiUpdate: update slider_SeqFrame
236 | if self.parent.isEntered:
237 | # I. Open-Brace: Make sure GUI changes do not call updateParameterNodeFromGUI__ (it could cause infinite loop)
238 | self._updatingGUIFromParameterNode = True
239 | # --------------------------------------------------------------------------------------------------------------
240 | # II-02-A. Re-Set sequenceSelector, just in case the Signal sender is Sequences.toolBar()
241 | self.ui.sequenceSelector.setCurrentNode(node_NewActiveBrowser)
242 | # II-02-B. Re-Set modules.sequences active SeqBrowser, just in case the Signal sender is Laminae-Labeling
243 | slicer.modules.sequences.widgetRepresentation().setActiveBrowserNode(node_NewActiveBrowser)
244 | # II-02-C. Push Slicer Screen refresh before uiUpdate
245 | self.uiUpdate_PushSlicerScreenUpdate_by_ShakeTargetSeqBrowser(node_NewActiveBrowser)
246 | # II-02-D. Start uiUpdate
247 | self.uiUpdate_SwitchSelection_ChangeSeqBrowser_RemainFrameIndex(node_NewActiveBrowser)
248 | # --------------------------------------------------------------------------------------------------------------
249 | # III. Close-Brace: All the GUI updates are done;
250 | self._updatingGUIFromParameterNode = False
251 |
252 | # ------------------------------------------------------------------------------------------------------------------
253 | # ------------------------------------------------------------------------------------------------------------------
254 | # ==================================================================================================================
255 | # ----------- Section II-B: Sub-Functions called by updateGUIFromParameterNode__ or Slot functions ---
256 | # ----- 1. All sub-functions starts with uiUpdate ----------------------------------------------------
257 | # ----- 2. All uiUpdate functions canNOT set self._updatingGUIFromParameterNode ----
258 | # ----- 3. The superior function who call uiUpdate function MUST set self._updatingGUIFromParameterNode ----
259 | # ------------------------------------------------------------------------------------------------------------------
260 | # ------------------------------------------------------------------------------------------------------------------
261 | def uiUpdate_Slider_SeqFrame__by__nodeSeqBrowser_Selected(self, nodeSeqBrowser_Selected):
262 | ''' **Widget.uiUpdate_Slider_SeqFrame__by__nodeSeqBrowser_Selected(self, nodeSeqBrowser_Selected) ''' ''''''
263 | if nodeSeqBrowser_Selected:
264 | str_CurSeqBrowser_ID = 'nodeSeqBrowser_Selected.GetID() = ' + nodeSeqBrowser_Selected.GetID()
265 | str_NumberOfItems = '.GetNumberOfItems() = ' + str(nodeSeqBrowser_Selected.GetNumberOfItems())
266 | str_idxFrame = f', \tidxFrame = {self.logic.obtain_idxSliderCurFrame_from_TargetSeqBrowser(nodeSeqBrowser_Selected)}'
267 | else:
268 | str_CurSeqBrowser_ID = 'CurSeqBrowser.GetID() = ' + str(type(nodeSeqBrowser_Selected))
269 | str_NumberOfItems = ''
270 | str_idxFrame = ''
271 | print(f"\t**Widget.uiUpdate_Slider_SeqFrame__by__nodeSeqBrowser_Selected(), {str_CurSeqBrowser_ID}, {str_NumberOfItems}{str_idxFrame}")
272 |
273 | if nodeSeqBrowser_Selected and nodeSeqBrowser_Selected.GetNumberOfItems() > 0:
274 | self.ui.slider_SeqFrame.enabled = True
275 | self.ui.slider_SeqFrame.minimum = INT_SliderFrameIndex_Min
276 | self.ui.slider_SeqFrame.maximum = nodeSeqBrowser_Selected.GetNumberOfItems()
277 | self.ui.slider_SeqFrame.value = self.logic.obtain_idxSliderCurFrame_from_TargetSeqBrowser(nodeSeqBrowser_Selected)
278 | self.ui.label_FrameIndex.setText(str(self.ui.slider_SeqFrame.value))
279 | else:
280 | # No SequenceBrowser_Node available, so we disable the slider_SeqFrame, and set label_FrameIndex 'N/A'
281 | self.ui.slider_SeqFrame.enabled = False
282 | self.ui.slider_SeqFrame.minimum = INT_SliderFrameIndex_Min
283 | self.ui.slider_SeqFrame.maximum = INT_FRAME_INDEX_SLIDER_DEFAULT_MAX
284 | self.ui.slider_SeqFrame.value = INT_FRAME_INDEX_SLIDER_DEFAULT
285 | self.ui.label_FrameIndex.setText('N/A')
286 |
287 | # ------------------------------------------------------------------------------------------------------------------
288 | # ==================================================================================================================
289 | # ------------------------------------------------------------------------------------------------------------------
290 | def uiUpdate_SwitchSelection_ChangeSeqBrowser_RemainFrameIndex(self, node_NewActiveBrowser):
291 | ''' **Widget.uiUpdate_SwitchSelection_ChangeSeqBrowser_RemainFrameIndex(self, nodeTarget_SeqBrowser) ''' ''''''
292 | # 00-A. Check if the module isEntered
293 | if not self.parent.isEntered: return
294 | # 00-B. Check the validity of nodeTarget_SeqBrowser
295 | if not node_NewActiveBrowser: return
296 |
297 | # 01. Update slider_SeqFrame
298 | if node_NewActiveBrowser:
299 | str_CurSeqBrowser_ID = 'node_NewActiveBrowser.GetID() = ' + node_NewActiveBrowser.GetID()
300 | str_NumberOfItems = 'idx_SeqBrowserSelectedItem = ' + str(node_NewActiveBrowser.GetNumberOfItems())
301 | else:
302 | str_CurSeqBrowser_ID = 'node_NewActiveBrowser.GetID() = ' + str(type(node_NewActiveBrowser))
303 | str_NumberOfItems = ''
304 | print(f"\t**Widget.uiUpdate_SwitchSelection_ChangeSeqBrowser_RemainFrameIndex(), {str_CurSeqBrowser_ID}, {str_NumberOfItems}")
305 | self.uiUpdate_Slider_SeqFrame__by__nodeSeqBrowser_Selected(node_NewActiveBrowser)
306 |
307 |
308 | # ------------------------------------------------------------------------------------------------------------------
309 | def uiUpdate_SwitchSelection_SelectedSeqBrowser_ChangeFrameIndex(self):
310 | ''' **Widget.uiUpdate_SwitchSelection_SelectedSeqBrowser_ChangeFrameIndex(self)
311 | There are two modes to trigger this uiUpdate: UI modified / Non-UI (node) modified.
312 | To guarantee the Non-UI mode, we will update all UI widgets (including the possible TriggerMan UI widget).
313 | All uiUpdate can be done by logicUpdated nodeSeqBrowser_Selected, thus argument idx_TargetFrame NotRequired.
314 | ''' ''''''
315 | # 00-A. Check if the module isEntered
316 | if not self.parent.isEntered: return
317 | # 00-B. Check the validity of nodeSeqBrowser_Selected
318 | nodeSeqBrowser_Selected = self._parameterNode.GetNodeReference(STR_SeqBrowserNode_RefRole_Selected)
319 | if not nodeSeqBrowser_Selected: return
320 |
321 | # 01. Update the uiSlider
322 | self.uiUpdate_Slider_SeqFrame__by__nodeSeqBrowser_Selected(nodeSeqBrowser_Selected)
323 |
324 |
325 | # ------------------------------------------------------------------------------------------------------------------
326 | def uiUpdate_PushSlicerScreenUpdate_by_ShakeTargetSeqBrowser(self, nodeTarget_SeqBrowser):
327 | print(f' **Widget.uiUpdate_PushSlicerScreenUpdate_by_ShakeTargetSeqBrowser()')
328 | if nodeTarget_SeqBrowser:
329 | # Let's push Slicer to update by Setting current selected frame back and forth
330 | idx_curFrame = nodeTarget_SeqBrowser.GetSelectedItemNumber()
331 |
332 | nodeTarget_SeqBrowser.SetSelectedItemNumber(max(idx_curFrame - 1, 0))
333 | nodeTarget_SeqBrowser.SetSelectedItemNumber(min(idx_curFrame + 1, nodeTarget_SeqBrowser.GetNumberOfItems() - 1))
334 | nodeTarget_SeqBrowser.SetSelectedItemNumber(idx_curFrame)
335 |
336 | # ==================================================================================================================
337 | # ==================================================================================================================
338 | # =========== SL_Developer, Section IV: updateParameterNodeFromGUI__ ==============================
339 | # ------------------------------------------------------------------------------------------------------------------
340 | def updateParameterNodeFromGUI__Set_RefRoleNodeID(self, STR_RefRole, str_NodeID):
341 | """ Read GUI Method: Method updateParameterNodeFromGUI__ is called when users makes any change in the GUI.
342 | Changes are saved into the parameter node (so that they are restored when the scene is saved and loaded).
343 | **Widget.updateParameterNodeFromGUI__Set_RefRoleNodeID(self, STR_RefRole, str_NodeID) """
344 | if self._parameterNode is None or self._updatingGUIFromParameterNode:
345 | return
346 |
347 | # I. Before updating the Singleton ParameterNode; Disable Modify events, e.g., vtk.vtkCommand.ModifiedEvent
348 | wasModified = self._parameterNode.StartModify() # Modify all properties in a single batch
349 |
350 | # II. Update the Singleton ParameterNode; No updateGUIFromParameterNode triggered in this step
351 | node_BeforeChange = self._parameterNode.GetNodeReference(STR_RefRole)
352 | if node_BeforeChange: str_NodeBeforeChange = self._parameterNode.GetNodeReference(STR_RefRole).GetID()
353 | else: str_NodeBeforeChange = ""
354 | print(f'\tBefore Update: {str_NodeBeforeChange}')
355 | self._parameterNode.SetNodeReferenceID(STR_RefRole, str_NodeID)
356 | print(f'\tAfter Update: {self._parameterNode.GetNodeReference(STR_RefRole).GetID()}')
357 |
358 | # III. After updating the Singleton ParameterNode; Enable Modify events, e.g., vtk.vtkCommand.ModifiedEvent
359 | self._parameterNode.EndModify(wasModified)
360 |
361 |
362 | # ------------------------------------------------------------------------------------------------------------------
363 | # ------------------------------------------------------------------------------------------------------------------
364 | ''' ================================================================================================================='''
365 | #
366 | # sl__US_SeqViewerLogic
367 | #
368 | class sl__US_SeqViewerLogic(ScriptedLoadableModuleLogic):
369 | """ The Logic class is : to facilitate dynamic reloading of the module without restarting the application.
370 | This class should implement all the actual computation done by your module.
371 | The interface should be such that other python code can import this class
372 | and make use of the functionality without requiring an instance of the Widget.
373 | Uses ScriptedLoadableModuleLogic base class, available at:
374 | https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
375 | """
376 |
377 | def __init__(self):
378 | """ Called when the logic class is instantiated. Can be used for initializing member variables. """
379 | ScriptedLoadableModuleLogic.__init__(self)
380 |
381 | self._isSwitchingSeqBrowser = False
382 |
383 | # ==================================================================================================================
384 | # ==================================================================================================================
385 | # =========== SL_Developer, Section VI: get, set, obtain, & createNewNode =====================
386 | # ------------------------------------------------------------------------------------------------------------------
387 | def obtain_idxSliderCurFrame_from_TargetSeqBrowser(self, nodeTarget_SeqBrowser):
388 | ''' **Logic.obtain_idxSliderCurFrame_from_TargetSeqBrowser(self, nodeTarget_SeqBrowser) ''' ''''''
389 | idx_SliderCurFrame = nodeTarget_SeqBrowser.GetSelectedItemNumber() + INT_SliderFrameIndex_Min
390 | return idx_SliderCurFrame
391 |
392 |
393 | # ------------------------------------------------------------------------------------------------------------------
394 | # ==================================================================================================================
395 | # ==================================================================================================================
396 | # =========== SL_Developer, Section VII-A: logicUpdate & Functions that call paramNodeUpdate ====
397 | # ------------------------------------------------------------------------------------------------------------------
398 | def setDefaultParameters(self, parameterNode):
399 | """ SL_Developer, B: Initialize parameter node, Re-Enter, Re-Load. """
400 | print("**Logic.setDefaultParameters(self, parameterNode), \tSL_Developer, B");
401 | # I. Before updating the Singleton ParameterNode; Disable Modify events, e.g., vtk.vtkCommand.ModifiedEvent
402 | wasModified = parameterNode.StartModify() # Modify all properties in a single batch
403 | # --------------------------------------------------------------------------------------------------------------
404 | # II. Update the Singleton ParameterNode; No updateGUIFromParameterNode triggered in this step
405 | # II-01. Set NodeRef for curSelected SeqBrowser, select the first if not selected
406 | if not parameterNode.GetNodeReference(STR_SeqBrowserNode_RefRole_Selected):
407 | node_SeqBrowser_First = slicer.mrmlScene.GetFirstNodeByClass("vtkMRMLSequenceBrowserNode")
408 | if node_SeqBrowser_First:
409 | # II-01-A. Set NodeRefID for paramNode
410 | parameterNode.SetNodeReferenceID(STR_SeqBrowserNode_RefRole_Selected, node_SeqBrowser_First.GetID())
411 | # II-01-B. Synchronize with modules.sequences's SequenceBrowser active node
412 | slicer.modules.sequences.widgetRepresentation().setActiveBrowserNode(node_SeqBrowser_First)
413 | else:
414 | # II-01-C. Already got NodeRefID for paramNode, we only need to Synchronize with modules.sequences
415 | nodeSeqBrowser_Selected = parameterNode.GetNodeReference(STR_SeqBrowserNode_RefRole_Selected)
416 | slicer.modules.sequences.widgetRepresentation().setActiveBrowserNode(nodeSeqBrowser_Selected)
417 |
418 | # --------------------------------------------------------------------------------------------------------------
419 | # III. After updating the Singleton ParameterNode; Enable Modify events, e.g., vtk.vtkCommand.ModifiedEvent
420 | parameterNode.EndModify(wasModified)
421 |
422 |
423 | # ------------------------------------------------------------------------------------------------------------------
424 | # ------------------------------------------------------------------------------------------------------------------
425 | # ----------- Section VII-B: Sub-Functions with prefix/surfix paramNodeUpdate ----------------------
426 | # ----- 1. All sub-functions prefix/surfix with paramNodeUpdate; --------------------------------------
427 | # ------2. All paramNodeUpdate functions canNOT self.getParameterNode().StartModify() ---
428 | # ------3. The superior function who call paramNodeUpdate function MUST self.getParameterNode().StartModify() ---
429 | # ------------------------------------------------------------------------------------------------------------------
430 |
431 |
432 | # ------------------------------------------------------------------------------------------------------------------
433 | # ==================================================================================================================
434 | # ==================================================================================================================
435 | # =========== SL_Developer, Section VIII: Other Logic Functions ===============================
436 | # ------------------------------------------------------------------------------------------------------------------
437 | # --------------- Section VIII-01: Boolean (is_) Functions ---------------------------------------
438 | # ------------------------------------------------------------------------------------------------------------------
439 | def isValid_idxTargetFrame(self, nodeSeqBrowser, idx_TargetFrame):
440 | ''' **Logic.isValid_idxTargetFrame(self, nodeSeqBrowser, idx_TargetFrame) ''' ''''''
441 | if nodeSeqBrowser and idx_TargetFrame >=0 and idx_TargetFrame < nodeSeqBrowser.GetNumberOfItems():
442 | return True;
443 | else:
444 | return False
445 |
446 |
447 | # ------------------------------------------------------------------------------------------------------------------
448 | # --------------- Section VIII-02: Set / Update Functions ---------------------------------------
449 | # ------------------------------------------------------------------------------------------------------------------
450 | # ------------------------------------------------------------------------------------------------------------------
451 | # ------------------------------------------------------------------------------------------------------------------
452 | def logicUpdate_SwitchSelection_SelectedSeqBrowser_ChangeFrameIndex(self, idx_TargetFrame):
453 | ''' **Logic.logicUpdate_SwitchSelection_SelectedSeqBrowser_ChangeFrameIndex(self, idx_TargetFrame) ''' ''''''
454 | # 00-A. Check the validity of nodeSeqBrowser_Selected and idx_TargetFrame
455 | nodeSeqBrowser_Selected = self.getParameterNode().GetNodeReference(STR_SeqBrowserNode_RefRole_Selected)
456 | if not nodeSeqBrowser_Selected: return
457 | # 00-B. Check the validity of idx_TargetFrame
458 | if not self.isValid_idxTargetFrame(nodeSeqBrowser_Selected, idx_TargetFrame):
459 | raise ValueError(f'SL_Alert! Invalid idx_TargetFrame = {idx_TargetFrame}'); return
460 |
461 | # 01. Update nodeSeqBrowser along with its the Current-SelectedItemNumber
462 | nodeSeqBrowser_Selected.SetSelectedItemNumber(idx_TargetFrame)
463 | print(f"\t\t**Logic.logicUpdate_SwitchSelection_SelectedSeqBrowser_ChangeFrameIndex()\t"
464 | f"nodeSeqBrowser_Selected.GetID() = {nodeSeqBrowser_Selected.GetID()}, idx_TargetFrame = {idx_TargetFrame}")
465 |
466 |
467 |
468 |
469 | ''' ================================================================================================================='''
470 | ''' ================================================================================================================='''
471 | ''' ================================================================================================================='''
472 | #
473 | # sl__US_SeqViewerTest
474 | #
475 | class sl__US_SeqViewerTest(ScriptedLoadableModuleTest):
476 | """ This is the test case for your scripted module.
477 | Uses ScriptedLoadableModuleTest base class, available at:
478 | https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py """
479 | def setUp(self):
480 | """ Do whatever is needed to reset the state - typically a scene clear will be enough. """
481 | slicer.mrmlScene.Clear()
482 |
483 | def runTest(self):
484 | """Run as few or as many tests as needed here. """
485 | self.setUp()
486 | self.test_sl__US_SeqViewer1()
487 |
488 | def test_sl__US_SeqViewer1(self):
489 | """ Ideally you should have several levels of tests. At the lowest level
490 | tests should exercise the functionality of the logic with different inputs
491 | (both valid and invalid). At higher levels your tests should emulate the
492 | way the user would interact with your code and confirm that it still works
493 | the way you intended.
494 | One of the most important features of the tests is that it should alert other
495 | developers when their changes will have an impact on the behavior of your
496 | module. For example, if a developer removes a feature that you depend on,
497 | your test should break so they know that the feature is needed.
498 | """
499 |
500 | self.delayDisplay("Starting the test")
501 |
502 | pass
503 |
504 | self.delayDisplay('Test passed')
505 |
506 |
507 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Step-by-step tutorial of 3D Slicer Extension Module Development
2 |
3 | [](https://www.youtube.com/watch?v=rr0Q9GUuz7E&list=PLTuWbByD80TORd1R-J7j7nVQ9fot3C2fK)
4 |
5 |
6 |
7 |
8 |
9 | ## Step 02: Interact, Debug & Pipeline
10 |
11 |
12 |
13 | #### YouTube Video Tutorial for Step_02
14 |
15 | #### Bilibili Video Tutorial for Step_02
16 |
17 |
18 |
19 |
20 |
21 |
22 | ## Step 03: Developer Foci, Neat Blanks & GUI Hello World
23 |
24 |
25 |
26 | #### YouTube Video Tutorial for Step_03
27 |
28 | #### Bilibili Video Tutorial for Step_03
29 |
30 |
31 |
32 |
33 |
34 | ## Step 04: Code Style, Method Groups & US Sequence Viewer
35 |
36 |
37 |
38 | #### YouTube Video Tutorial for Step_04
39 |
40 | #### Bilibili Video Tutorial for Step_04
41 |
--------------------------------------------------------------------------------