|
a |
|
b/Scripted/MusculoskeletalAnalysis/MusculoskeletalAnalysis.py |
|
|
1 |
import logging |
|
|
2 |
import os |
|
|
3 |
import time |
|
|
4 |
import vtk |
|
|
5 |
import importlib |
|
|
6 |
|
|
|
7 |
import slicer |
|
|
8 |
from slicer.ScriptedLoadableModule import * |
|
|
9 |
from slicer.util import VTKObservationMixin |
|
|
10 |
|
|
|
11 |
|
|
|
12 |
# |
|
|
13 |
# MusculoskeletalAnalysis |
|
|
14 |
# |
|
|
15 |
|
|
|
16 |
class MusculoskeletalAnalysis(ScriptedLoadableModule): |
|
|
17 |
"""Uses ScriptedLoadableModule base class, available at: |
|
|
18 |
https://github.com/Slicer/Slicer/blob/main/Base/Python/slicer/ScriptedLoadableModule.py |
|
|
19 |
""" |
|
|
20 |
|
|
|
21 |
def __init__(self, parent): |
|
|
22 |
ScriptedLoadableModule.__init__(self, parent) |
|
|
23 |
self.parent.title = "Musculoskeletal Analysis" |
|
|
24 |
self.parent.categories = ["Quantification"] |
|
|
25 |
self.parent.dependencies = ["CorticalAnalysis", "CancellousAnalysis", "DensityAnalysis", "IntervertebralAnalysis"] # TODO: add here list of module names that this module requires |
|
|
26 |
self.parent.contributors = ["Joseph Szatkowski (Washington University in St. Louis)"] |
|
|
27 |
# TODO: update with short description of the module and a link to online module documentation |
|
|
28 |
self.parent.helpText = """ |
|
|
29 |
1. Select a volume containing the image to analyze.\n |
|
|
30 |
2. Select a segmentation representing the area to analyze. For cortical analysis exclude the medullary cavity. For cancellous analysis exclude the cortical bone.\n |
|
|
31 |
3. Use the threshold slider to select a threshold identifying the bone.\n |
|
|
32 |
4. Select the function to perform. See <a href="https://github.com/WashUMusculoskeletalCore/Slicer-MusculoskeletalAnalysis/blob/main/README.md">here</a> for more information.\n |
|
|
33 |
5. Select the directory to send the output files to. If files already exist they will be appended to.\n |
|
|
34 |
6. Click "Apply"\n |
|
|
35 |
ADVANCED\n |
|
|
36 |
7. If the image volume is not the original DICOM, select the original DICOM node to get DICOM tags from. |
|
|
37 |
""" |
|
|
38 |
# TODO: replace with organization, grant and thanks |
|
|
39 |
self.parent.acknowledgementText = """ |
|
|
40 |
Developed by the Washington University in St. Louis Musculoskeletal Reseach Center with the assistance of Michael Brodt, Anish Jagannathan, Matthew Silva, and Simon Tang.\n |
|
|
41 |
This file was partially funded by NIH grant P30 AR074992. |
|
|
42 |
""" |
|
|
43 |
|
|
|
44 |
# Additional initialization step after application startup is complete |
|
|
45 |
slicer.app.connect("startupCompleted()", registerSampleData) |
|
|
46 |
|
|
|
47 |
|
|
|
48 |
# |
|
|
49 |
# Register sample data sets in Sample Data module |
|
|
50 |
# |
|
|
51 |
|
|
|
52 |
def registerSampleData(): |
|
|
53 |
""" |
|
|
54 |
Add data sets to Sample Data module. |
|
|
55 |
""" |
|
|
56 |
# It is always recommended to provide sample data for users to make it easy to try the module, |
|
|
57 |
# but if no sample data is available then this method (and associated startupCompeted signal connection) can be removed. |
|
|
58 |
|
|
|
59 |
import SampleData |
|
|
60 |
iconsPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons') |
|
|
61 |
|
|
|
62 |
# To ensure that the source code repository remains small (can be downloaded and installed quickly) |
|
|
63 |
# it is recommended to store data sets that are larger than a few MB in a Github release. |
|
|
64 |
|
|
|
65 |
SampleData.SampleDataLogic.registerCustomSampleDataSource( |
|
|
66 |
# Category and sample name displayed in Sample Data module |
|
|
67 |
category='MusculoskeletalAnalysis', |
|
|
68 |
sampleName='Cortical1', |
|
|
69 |
# Thumbnail should have size of approximately 260x280 pixels and stored in Resources/Icons folder. |
|
|
70 |
# It can be created by Screen Capture module, "Capture all views" option enabled, "Number of images" set to "Single". |
|
|
71 |
thumbnailFileName=os.path.join(iconsPath, 'Cortical1.png'), |
|
|
72 |
# Download URL and target file name |
|
|
73 |
uris="https://github.com/WashUMusculoskeletalCore/Slicer-MusculoskeletalAnalysis/releases/download/v1.1-assets/CorticalSample1.nrrd", |
|
|
74 |
fileNames='CorticalSample1.nrrd', |
|
|
75 |
# Checksum to ensure file integrity. Can be computed by this command: |
|
|
76 |
# import hashlib; print(hashlib.sha256(open(filename, "rb").read()).hexdigest()) |
|
|
77 |
checksums='SHA256:740fcdbe9c7341ffeb2fb44e68e25098077281e11d541163379bf301db4b65b9', |
|
|
78 |
# This node name will be used when the data set is loaded |
|
|
79 |
nodeNames='Cortical1' |
|
|
80 |
) |
|
|
81 |
|
|
|
82 |
|
|
|
83 |
SampleData.SampleDataLogic.registerCustomSampleDataSource( |
|
|
84 |
# Category and sample name displayed in Sample Data module |
|
|
85 |
category='MusculoskeletalAnalysis', |
|
|
86 |
sampleName='CorticalMask1', |
|
|
87 |
# Thumbnail should have size of approximately 260x280 pixels and stored in Resources/Icons folder. |
|
|
88 |
# It can be created by Screen Capture module, "Capture all views" option enabled, "Number of images" set to "Single". |
|
|
89 |
thumbnailFileName=os.path.join(iconsPath, 'CorticalMask1.png'), |
|
|
90 |
# Download URL and target file name |
|
|
91 |
uris="https://github.com/WashUMusculoskeletalCore/Slicer-MusculoskeletalAnalysis/releases/download/v1.1-assets/CorticalMaskSample1.seg.nrrd", |
|
|
92 |
fileNames='CorticalMaskSample1.seg.nrrd', |
|
|
93 |
loadFileType='SegmentationFile', |
|
|
94 |
# Checksum to ensure file integrity. Can be computed by this command: |
|
|
95 |
# import hashlib; print(hashlib.sha256(open(filename, "rb").read()).hexdigest()) |
|
|
96 |
checksums='SHA256:1cdd2ea240d848d3d5241eddffe2467f5bf49f80dab50937c8127eb511fa3b9a', |
|
|
97 |
# This node name will be used when the data set is loaded |
|
|
98 |
nodeNames='CorticalMask1' |
|
|
99 |
) |
|
|
100 |
|
|
|
101 |
SampleData.SampleDataLogic.registerCustomSampleDataSource( |
|
|
102 |
# Category and sample name displayed in Sample Data module |
|
|
103 |
category='MusculoskeletalAnalysis', |
|
|
104 |
sampleName='Cortical2', |
|
|
105 |
# Thumbnail should have size of approximately 260x280 pixels and stored in Resources/Icons folder. |
|
|
106 |
# It can be created by Screen Capture module, "Capture all views" option enabled, "Number of images" set to "Single". |
|
|
107 |
thumbnailFileName=os.path.join(iconsPath, 'Cortical2.png'), |
|
|
108 |
# Download URL and target file name |
|
|
109 |
uris="https://github.com/WashUMusculoskeletalCore/Slicer-MusculoskeletalAnalysis/releases/download/v1.1-assets/CorticalSample2.nrrd", |
|
|
110 |
fileNames='CorticalSample2.nrrd', |
|
|
111 |
# Checksum to ensure file integrity. Can be computed by this command: |
|
|
112 |
# import hashlib; print(hashlib.sha256(open(filename, "rb").read()).hexdigest()) |
|
|
113 |
checksums='SHA256:8e0869839abc008000d32d8ffef923a89ee7fd10fd974d63baa1daecb47154f5', |
|
|
114 |
# This node name will be used when the data set is loaded |
|
|
115 |
nodeNames='Cortical2' |
|
|
116 |
) |
|
|
117 |
|
|
|
118 |
SampleData.SampleDataLogic.registerCustomSampleDataSource( |
|
|
119 |
# Category and sample name displayed in Sample Data module |
|
|
120 |
category='MusculoskeletalAnalysis', |
|
|
121 |
sampleName='CorticalMask2', |
|
|
122 |
# Thumbnail should have size of approximately 260x280 pixels and stored in Resources/Icons folder. |
|
|
123 |
# It can be created by Screen Capture module, "Capture all views" option enabled, "Number of images" set to "Single". |
|
|
124 |
thumbnailFileName=os.path.join(iconsPath, 'CorticalMask2.png'), |
|
|
125 |
# Download URL and target file name |
|
|
126 |
uris="https://github.com/WashUMusculoskeletalCore/Slicer-MusculoskeletalAnalysis/releases/download/v1.1-assets/CorticalMaskSample2.seg.nrrd", |
|
|
127 |
fileNames='CorticalMaskSample2.seg.nrrd', |
|
|
128 |
loadFileType='SegmentationFile', |
|
|
129 |
# Checksum to ensure file integrity. Can be computed by this command: |
|
|
130 |
# import hashlib; print(hashlib.sha256(open(filename, "rb").read()).hexdigest()) |
|
|
131 |
checksums='SHA256:509b7e5b16a838bf743ec083677ac7a1a2b1f7f3d2fb8ad93e75b38619150d7e', |
|
|
132 |
# This node name will be used when the data set is loaded |
|
|
133 |
nodeNames='CorticalMask2' |
|
|
134 |
) |
|
|
135 |
|
|
|
136 |
SampleData.SampleDataLogic.registerCustomSampleDataSource( |
|
|
137 |
# Category and sample name displayed in Sample Data module |
|
|
138 |
category='MusculoskeletalAnalysis', |
|
|
139 |
sampleName='Cancellous1', |
|
|
140 |
thumbnailFileName=os.path.join(iconsPath, 'Cancellous1.png'), |
|
|
141 |
# Download URL and target file name |
|
|
142 |
uris="https://github.com/WashUMusculoskeletalCore/Slicer-MusculoskeletalAnalysis/releases/download/v1.1-assets/CancellousSample1.nrrd", |
|
|
143 |
fileNames='CancellousSample1.nrrd', |
|
|
144 |
# Checksum to ensure file integrity. Can be computed by this command: |
|
|
145 |
# import hashlib; print(hashlib.sha256(open(filename, "rb").read()).hexdigest()) |
|
|
146 |
checksums='SHA256:cb47e20fd9d4caf210a256db8317f2553f409399834b0bc15b28e57daf46ba89', |
|
|
147 |
# This node name will be used when the data set is loaded |
|
|
148 |
nodeNames='Cancellous1' |
|
|
149 |
) |
|
|
150 |
|
|
|
151 |
|
|
|
152 |
SampleData.SampleDataLogic.registerCustomSampleDataSource( |
|
|
153 |
# Category and sample name displayed in Sample Data module |
|
|
154 |
category='MusculoskeletalAnalysis', |
|
|
155 |
sampleName='CancellousMask1', |
|
|
156 |
thumbnailFileName=os.path.join(iconsPath, 'CancellousMask1.png'), |
|
|
157 |
# Download URL and target file name |
|
|
158 |
uris="https://github.com/WashUMusculoskeletalCore/Slicer-MusculoskeletalAnalysis/releases/download/v1.1-assets/CancellousMaskSample1.seg.nrrd", |
|
|
159 |
fileNames='CancellousMaskSample1.seg.nrrd', |
|
|
160 |
loadFileType='SegmentationFile', |
|
|
161 |
# Checksum to ensure file integrity. Can be computed by this command: |
|
|
162 |
# import hashlib; print(hashlib.sha256(open(filename, "rb").read()).hexdigest()) |
|
|
163 |
checksums='SHA256:1d632557a415b0d84dfaca4bf743aa9319e5a5e805e7a997d76c6f7bd9e75160', |
|
|
164 |
# This node name will be used when the data set is loaded |
|
|
165 |
nodeNames='CancellousMask1' |
|
|
166 |
) |
|
|
167 |
|
|
|
168 |
SampleData.SampleDataLogic.registerCustomSampleDataSource( |
|
|
169 |
# Category and sample name displayed in Sample Data module |
|
|
170 |
category='MusculoskeletalAnalysis', |
|
|
171 |
sampleName='Cancellous2', |
|
|
172 |
thumbnailFileName=os.path.join(iconsPath, 'Cancellous2.png'), |
|
|
173 |
# Download URL and target file name |
|
|
174 |
uris="https://github.com/WashUMusculoskeletalCore/Slicer-MusculoskeletalAnalysis/releases/download/v1.1-assets/CancellousSample2.nrrd", |
|
|
175 |
fileNames='CancellousSample2.nrrd', |
|
|
176 |
# Checksum to ensure file integrity. Can be computed by this command: |
|
|
177 |
# import hashlib; print(hashlib.sha256(open(filename, "rb").read()).hexdigest()) |
|
|
178 |
checksums='SHA256:fd594a087700afcb3a1fc6c0ee4fa087f263eda86f93f0cfc317927876bda813', |
|
|
179 |
# This node name will be used when the data set is loaded |
|
|
180 |
nodeNames='Cancellous2' |
|
|
181 |
) |
|
|
182 |
|
|
|
183 |
|
|
|
184 |
SampleData.SampleDataLogic.registerCustomSampleDataSource( |
|
|
185 |
# Category and sample name displayed in Sample Data module |
|
|
186 |
category='MusculoskeletalAnalysis', |
|
|
187 |
sampleName='CancellousMask2', |
|
|
188 |
thumbnailFileName=os.path.join(iconsPath, 'CancellousMask2.png'), |
|
|
189 |
# Download URL and target file name |
|
|
190 |
uris="https://github.com/WashUMusculoskeletalCore/Slicer-MusculoskeletalAnalysis/releases/download/v1.1-assets/CancellousMaskSample2.seg.nrrd", |
|
|
191 |
fileNames='CancellousMaskSample2.seg.nrrd', |
|
|
192 |
loadFileType='SegmentationFile', |
|
|
193 |
# Checksum to ensure file integrity. Can be computed by this command: |
|
|
194 |
# import hashlib; print(hashlib.sha256(open(filename, "rb").read()).hexdigest()) |
|
|
195 |
checksums='SHA256:473cb8d1a2ccc8973eb7f0b69eb297d2d4119155b1a9940cb5e11d2ccd4e1315', |
|
|
196 |
# This node name will be used when the data set is loaded |
|
|
197 |
nodeNames='CancellousMask2' |
|
|
198 |
) |
|
|
199 |
|
|
|
200 |
SampleData.SampleDataLogic.registerCustomSampleDataSource( |
|
|
201 |
# Category and sample name displayed in Sample Data module |
|
|
202 |
category='MusculoskeletalAnalysis', |
|
|
203 |
sampleName='Intervertebral1', |
|
|
204 |
thumbnailFileName=os.path.join(iconsPath, 'Intervertebral1.png'), |
|
|
205 |
# Download URL and target file name |
|
|
206 |
uris="https://github.com/WashUMusculoskeletalCore/Slicer-MusculoskeletalAnalysis/releases/download/v1.1-assets/IntervertebralSample1.nrrd", |
|
|
207 |
fileNames='Intervertebral1.nrrd', |
|
|
208 |
# Checksum to ensure file integrity. Can be computed by this command: |
|
|
209 |
# import hashlib; print(hashlib.sha256(open(filename, "rb").read()).hexdigest()) |
|
|
210 |
checksums='SHA256:26706cec367bded189182e8b3e02c804bdad5267cee25f95202ee240823841a9', |
|
|
211 |
# This node name will be used when the data set is loaded |
|
|
212 |
nodeNames='Intervertebral1' |
|
|
213 |
) |
|
|
214 |
|
|
|
215 |
|
|
|
216 |
SampleData.SampleDataLogic.registerCustomSampleDataSource( |
|
|
217 |
# Category and sample name displayed in Sample Data module |
|
|
218 |
category='MusculoskeletalAnalysis', |
|
|
219 |
sampleName='IntervertebralMask1', |
|
|
220 |
thumbnailFileName=os.path.join(iconsPath, 'IntervertebralMask1.png'), |
|
|
221 |
# Download URL and target file name |
|
|
222 |
uris="https://github.com/WashUMusculoskeletalCore/Slicer-MusculoskeletalAnalysis/releases/download/v1.1-assets/IntervertebralMaskSample1.seg.nrrd", |
|
|
223 |
fileNames='IntervertebralMaskSample1.seg.nrrd', |
|
|
224 |
loadFileType='SegmentationFile', |
|
|
225 |
# Checksum to ensure file integrity. Can be computed by this command: |
|
|
226 |
# import hashlib; print(hashlib.sha256(open(filename, "rb").read()).hexdigest()) |
|
|
227 |
checksums='SHA256:7c140faa9924dd47d14df1208d5dc130479a8c64bfba6b741e02362465f7183f', |
|
|
228 |
# This node name will be used when the data set is loaded |
|
|
229 |
nodeNames='IntervertebralMask1' |
|
|
230 |
) |
|
|
231 |
|
|
|
232 |
# |
|
|
233 |
# MusculoskeletalAnalysisWidget |
|
|
234 |
# |
|
|
235 |
|
|
|
236 |
class MusculoskeletalAnalysisWidget(ScriptedLoadableModuleWidget, VTKObservationMixin): |
|
|
237 |
"""Uses ScriptedLoadableModuleWidget base class, available at: |
|
|
238 |
https://github.com/Slicer/Slicer/blob/main/Base/Python/slicer/ScriptedLoadableModule.py |
|
|
239 |
""" |
|
|
240 |
|
|
|
241 |
def __init__(self, parent=None): |
|
|
242 |
""" |
|
|
243 |
Called when the user opens the module the first time and the widget is initialized. |
|
|
244 |
""" |
|
|
245 |
ScriptedLoadableModuleWidget.__init__(self, parent) |
|
|
246 |
VTKObservationMixin.__init__(self) # needed for parameter node observation |
|
|
247 |
self.logic = None |
|
|
248 |
self._parameterNode = None |
|
|
249 |
self._updatingGUIFromParameterNode = False |
|
|
250 |
|
|
|
251 |
def setup(self): |
|
|
252 |
""" |
|
|
253 |
Called when the user opens the module the first time and the widget is initialized. |
|
|
254 |
""" |
|
|
255 |
ScriptedLoadableModuleWidget.setup(self) |
|
|
256 |
|
|
|
257 |
# Load widget from .ui file (created by Qt Designer). |
|
|
258 |
# Additional widgets can be instantiated manually and added to self.layout. |
|
|
259 |
uiWidget = slicer.util.loadUI(self.resourcePath('UI/MusculoskeletalAnalysis.ui')) |
|
|
260 |
self.layout.addWidget(uiWidget) |
|
|
261 |
self.ui = slicer.util.childWidgetVariables(uiWidget) |
|
|
262 |
|
|
|
263 |
# Set scene in MRML widgets. Make sure that in Qt designer the top-level qMRMLWidget's |
|
|
264 |
# "mrmlSceneChanged(vtkMRMLScene*)" signal in is connected to each MRML widget's. |
|
|
265 |
# "setMRMLScene(vtkMRMLScene*)" slot. |
|
|
266 |
uiWidget.setMRMLScene(slicer.mrmlScene) |
|
|
267 |
|
|
|
268 |
# Create logic class. Logic implements all computations that should be possible to run |
|
|
269 |
# in batch mode, without a graphical user interface. |
|
|
270 |
self.logic = MusculoskeletalAnalysisLogic() |
|
|
271 |
|
|
|
272 |
# Connections |
|
|
273 |
|
|
|
274 |
# These connections ensure that we update parameter node when scene is closed |
|
|
275 |
self.addObserver(slicer.mrmlScene, slicer.mrmlScene.StartCloseEvent, self.onSceneStartClose) |
|
|
276 |
self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndCloseEvent, self.onSceneEndClose) |
|
|
277 |
|
|
|
278 |
# These connections ensure that whenever user changes some settings on the GUI, that is saved in the MRML scene |
|
|
279 |
# (in the selected parameter node). |
|
|
280 |
self.ui.inputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.inputVolumeChanged) |
|
|
281 |
self.ui.segmentSelector.connect("currentNodeChanged(vtkMRMLNode*)",self.segmentNodeChanged) |
|
|
282 |
self.ui.segmentSelector.connect("currentSegmentChanged(QString)",self.segmentChanged) |
|
|
283 |
self.ui.segmentSelector.connect("segmentSelectionChanged(QStringList)", self.segmentChanged) |
|
|
284 |
self.ui.thresholdSelector.connect("thresholdValuesChanged(double, double)", self.updateParameterNodeFromGUI) |
|
|
285 |
self.ui.analysisSelector.connect("currentTextChanged(const QString)", self.updateParameterNodeFromGUI) |
|
|
286 |
self.ui.DICOMOptions.connect("buttonClicked(QAbstractButton*)", self.updateParameterNodeFromGUI) |
|
|
287 |
self.ui.DICOMSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.DICOMSeriesChanged) |
|
|
288 |
self.ui.voxelSizeLineEdit.connect("editingFinished()", self.updateParameterNodeFromGUI) |
|
|
289 |
self.ui.scalingLineEdit.connect("editingFinished()", self.updateParameterNodeFromGUI) |
|
|
290 |
self.ui.densitySlopeLineEdit.connect("editingFinished()", self.updateParameterNodeFromGUI) |
|
|
291 |
self.ui.densityInterceptLineEdit.connect("editingFinished()", self.updateParameterNodeFromGUI) |
|
|
292 |
self.ui.rescaleSlopeLineEdit.connect("editingFinished()", self.updateParameterNodeFromGUI) |
|
|
293 |
self.ui.rescaleInterceptLineEdit.connect("editingFinished()", self.updateParameterNodeFromGUI) |
|
|
294 |
self.ui.outputDirectorySelector.connect("currentPathChanged(const QString)", self.updateParameterNodeFromGUI) |
|
|
295 |
|
|
|
296 |
# Buttons |
|
|
297 |
self.ui.applyButton.connect('clicked(bool)', self.onApplyButton) |
|
|
298 |
|
|
|
299 |
# Hidden elements |
|
|
300 |
self.ui.AnalysisProgress.hide() |
|
|
301 |
# Make sure parameter node is initialized (needed for module reload) |
|
|
302 |
self.initializeParameterNode() |
|
|
303 |
|
|
|
304 |
|
|
|
305 |
|
|
|
306 |
|
|
|
307 |
def cleanup(self): |
|
|
308 |
""" |
|
|
309 |
Called when the application closes and the module widget is destroyed. |
|
|
310 |
""" |
|
|
311 |
self.removeObservers() |
|
|
312 |
|
|
|
313 |
def enter(self): |
|
|
314 |
""" |
|
|
315 |
Called each time the user opens this module. |
|
|
316 |
""" |
|
|
317 |
# Make sure parameter node exists and observed |
|
|
318 |
self.initializeParameterNode() |
|
|
319 |
|
|
|
320 |
def exit(self): |
|
|
321 |
""" |
|
|
322 |
Called each time the user opens a different module. |
|
|
323 |
""" |
|
|
324 |
# Do not react to parameter node changes (GUI wlil be updated when the user enters into the module) |
|
|
325 |
self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) |
|
|
326 |
|
|
|
327 |
def onSceneStartClose(self, caller, event): |
|
|
328 |
""" |
|
|
329 |
Called just before the scene is closed. |
|
|
330 |
""" |
|
|
331 |
# Parameter node will be reset, do not use it anymore |
|
|
332 |
self.setParameterNode(None) |
|
|
333 |
|
|
|
334 |
def onSceneEndClose(self, caller, event): |
|
|
335 |
""" |
|
|
336 |
Called just after the scene is closed. |
|
|
337 |
""" |
|
|
338 |
# If this module is shown while the scene is closed then recreate a new parameter node immediately |
|
|
339 |
if self.parent.isEntered: |
|
|
340 |
self.initializeParameterNode() |
|
|
341 |
|
|
|
342 |
def initializeParameterNode(self): |
|
|
343 |
""" |
|
|
344 |
Ensure parameter node exists and observed. |
|
|
345 |
""" |
|
|
346 |
# Parameter node stores all user choices in parameter values, node selections, etc. |
|
|
347 |
# so that when the scene is saved and reloaded, these settings are restored. |
|
|
348 |
|
|
|
349 |
self.setParameterNode(self.logic.getParameterNode()) |
|
|
350 |
|
|
|
351 |
# Select default input nodes if nothing is selected yet to save a few clicks for the user |
|
|
352 |
if not self._parameterNode.GetNodeReference("InputVolume"): |
|
|
353 |
firstVolumeNode = slicer.mrmlScene.GetFirstNodeByClass("vtkMRMLScalarVolumeNode") |
|
|
354 |
if firstVolumeNode: |
|
|
355 |
self._parameterNode.SetNodeReferenceID("InputVolume", firstVolumeNode.GetID()) |
|
|
356 |
|
|
|
357 |
if not self._parameterNode.GetNodeReference("SegmentNode"): |
|
|
358 |
firstSegmentNode = slicer.mrmlScene.GetFirstNodeByClass("vtkMRMLSegmentationNode") |
|
|
359 |
if firstSegmentNode: |
|
|
360 |
self._parameterNode.SetNodeReferenceID("SegmentNode", firstSegmentNode.GetID()) |
|
|
361 |
self.ui.segmentSelector.setCurrentNode(firstSegmentNode) |
|
|
362 |
if not self._parameterNode.GetParameter("SegmentID"): |
|
|
363 |
if self._parameterNode.GetNodeReference("SegmentNode"): |
|
|
364 |
segmentation = self._parameterNode.GetNodeReference("SegmentNode").GetSegmentation() |
|
|
365 |
self._parameterNode.SetParameter("SegmentID", segmentation.GetNthSegmentID(0)) |
|
|
366 |
|
|
|
367 |
|
|
|
368 |
|
|
|
369 |
if not self._parameterNode.GetNodeReference("DICOMNode"): |
|
|
370 |
firstVolumeNode = slicer.mrmlScene.GetFirstNodeByClass("vtkMRMLScalarVolumeNode") |
|
|
371 |
if firstVolumeNode: |
|
|
372 |
self._parameterNode.SetNodeReferenceID("DICOMNode", firstVolumeNode.GetID()) |
|
|
373 |
|
|
|
374 |
# Set default state for flags |
|
|
375 |
self._parameterNode.SetParameter("Analyzing", "False") |
|
|
376 |
|
|
|
377 |
|
|
|
378 |
|
|
|
379 |
|
|
|
380 |
|
|
|
381 |
|
|
|
382 |
def setParameterNode(self, inputParameterNode): |
|
|
383 |
""" |
|
|
384 |
Set and observe parameter node. |
|
|
385 |
Observation is needed because when the parameter node is changed then the GUI must be updated immediately. |
|
|
386 |
""" |
|
|
387 |
|
|
|
388 |
if inputParameterNode: |
|
|
389 |
self.logic.setDefaultParameters(inputParameterNode) |
|
|
390 |
|
|
|
391 |
# Unobserve previously selected parameter node and add an observer to the newly selected. |
|
|
392 |
# Changes of parameter node are observed so that whenever parameters are changed by a script or any other module |
|
|
393 |
# those are reflected immediately in the GUI. |
|
|
394 |
if self._parameterNode is not None: |
|
|
395 |
self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) |
|
|
396 |
self._parameterNode = inputParameterNode |
|
|
397 |
if self._parameterNode is not None: |
|
|
398 |
self.addObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) |
|
|
399 |
|
|
|
400 |
# Initial GUI update |
|
|
401 |
self.updateGUIFromParameterNode() |
|
|
402 |
|
|
|
403 |
def updateGUIFromParameterNode(self, caller=None, event=None): |
|
|
404 |
""" |
|
|
405 |
This method is called whenever parameter node is changed. |
|
|
406 |
The module GUI is updated to show the current state of the parameter node. |
|
|
407 |
""" |
|
|
408 |
|
|
|
409 |
if self._parameterNode is None or self._updatingGUIFromParameterNode: |
|
|
410 |
return |
|
|
411 |
|
|
|
412 |
# Make sure GUI changes do not call updateParameterNodeFromGUI (it could cause infinite loop) |
|
|
413 |
self._updatingGUIFromParameterNode = True |
|
|
414 |
|
|
|
415 |
# Update node selectors and sliders |
|
|
416 |
|
|
|
417 |
self.ui.thresholdSelector.setMRMLVolumeNode(self._parameterNode.GetNodeReference("InputVolume")) |
|
|
418 |
if self._parameterNode.GetParameter("LowerThreshold") and self._parameterNode.GetParameter("UpperThreshold"): |
|
|
419 |
self.ui.thresholdSelector.setLowerThreshold(float(self._parameterNode.GetParameter("LowerThreshold"))) |
|
|
420 |
self.ui.thresholdSelector.setUpperThreshold(float(self._parameterNode.GetParameter("UpperThreshold"))) |
|
|
421 |
self.ui.analysisSelector.setCurrentText(str(self._parameterNode.GetParameter("Analysis"))) |
|
|
422 |
self.ui.AlternateDICOMCheckBox.setChecked(self._parameterNode.GetParameter("UseAlt")=="True") |
|
|
423 |
self.ui.ManualDICOMCheckBox.setChecked(self._parameterNode.GetParameter("UseMan")=="True") |
|
|
424 |
self.ui.DICOMSelector.setCurrentNode(self._parameterNode.GetNodeReference("DICOMNode")) |
|
|
425 |
self.ui.voxelSizeLineEdit.setText(self._parameterNode.GetParameter("0018,0050")) |
|
|
426 |
self.ui.scalingLineEdit.setText(self._parameterNode.GetParameter("0029,1000")) |
|
|
427 |
self.ui.densitySlopeLineEdit.setText(self._parameterNode.GetParameter("0029,1004")) |
|
|
428 |
self.ui.densityInterceptLineEdit.setText(self._parameterNode.GetParameter("0029,1005")) |
|
|
429 |
self.ui.rescaleSlopeLineEdit.setText(self._parameterNode.GetParameter("0028,1053")) |
|
|
430 |
self.ui.rescaleInterceptLineEdit.setText(self._parameterNode.GetParameter("0028,1052")) |
|
|
431 |
self.ui.outputDirectorySelector.setCurrentPath(str(self._parameterNode.GetParameter("OutputDirectory"))) |
|
|
432 |
|
|
|
433 |
|
|
|
434 |
# Update buttons states and tooltips |
|
|
435 |
if self._parameterNode.GetNodeReference("InputVolume"): |
|
|
436 |
self.ui.thresholdSelector.enabled = True |
|
|
437 |
else: |
|
|
438 |
self.ui.thresholdSelector.enabled = False |
|
|
439 |
|
|
|
440 |
if self._parameterNode.GetParameter("MultiSelect") == 'True': |
|
|
441 |
self.ui.segmentSelector.multiSelection = True |
|
|
442 |
self.ui.thresholdSelector.enabled = False |
|
|
443 |
self.ui.thresholdSelector.setVisible(False) |
|
|
444 |
else: |
|
|
445 |
self.ui.segmentSelector.multiSelection = False |
|
|
446 |
self.ui.thresholdSelector.enabled = True |
|
|
447 |
self.ui.thresholdSelector.setVisible(True) |
|
|
448 |
|
|
|
449 |
# Update advanced options |
|
|
450 |
self.ui.DICOMSelector.enabled = (self._parameterNode.GetParameter("UseAlt")=="True") |
|
|
451 |
manual = (self._parameterNode.GetParameter("UseMan")=="True") |
|
|
452 |
self.ui.voxelSizeLineEdit.enabled=manual |
|
|
453 |
self.ui.scalingLineEdit.enabled=manual |
|
|
454 |
self.ui.densitySlopeLineEdit.enabled=manual |
|
|
455 |
self.ui.densityInterceptLineEdit.enabled=manual |
|
|
456 |
self.ui.rescaleSlopeLineEdit.enabled=manual |
|
|
457 |
self.ui.rescaleInterceptLineEdit.enabled=manual |
|
|
458 |
self.ui.voxelSizeLabel.enabled=manual |
|
|
459 |
self.ui.scalingLabel.enabled=manual |
|
|
460 |
self.ui.densitySlopeLabel.enabled=manual |
|
|
461 |
self.ui.densityInterceptLabel.enabled=manual |
|
|
462 |
self.ui.rescaleSlopeLabel.enabled=manual |
|
|
463 |
self.ui.rescaleInterceptLabel.enabled=manual |
|
|
464 |
# Update Apply Button |
|
|
465 |
if self._parameterNode.GetParameter("Analyzing")=="True": |
|
|
466 |
self.ui.applyButton.toolTip = "Currently running analysis" |
|
|
467 |
self.ui.applyButton.enabled = False |
|
|
468 |
elif self._parameterNode.GetNodeReference("InputVolume") and self._parameterNode.GetParameter("SegmentID") and (self._parameterNode.GetParameter("UseDICOM")=="False" or self._parameterNode.GetNodeReferenceID("DICOMNode")): |
|
|
469 |
self.ui.applyButton.toolTip = "Perform the selected analysis" |
|
|
470 |
self.ui.applyButton.enabled = True |
|
|
471 |
else: |
|
|
472 |
self.ui.applyButton.toolTip = "Select input volume node, input segment, and output directory" |
|
|
473 |
self.ui.applyButton.enabled = False |
|
|
474 |
|
|
|
475 |
# All the GUI updates are done |
|
|
476 |
self._updatingGUIFromParameterNode = False |
|
|
477 |
|
|
|
478 |
def inputVolumeChanged(self, event): |
|
|
479 |
""" |
|
|
480 |
Called when the input volume is changed in the selector. |
|
|
481 |
Passes the caller information to updateParameterNode |
|
|
482 |
""" |
|
|
483 |
self.updateParameterNodeFromGUI(event, "InputVolume") |
|
|
484 |
|
|
|
485 |
def segmentNodeChanged(self, event): |
|
|
486 |
""" |
|
|
487 |
Called when the segment node is changed in the selector. |
|
|
488 |
Passes the caller information to updateParameterNode |
|
|
489 |
""" |
|
|
490 |
self.updateParameterNodeFromGUI(event, "SegmentNode") |
|
|
491 |
|
|
|
492 |
|
|
|
493 |
def segmentChanged(self, event): |
|
|
494 |
""" |
|
|
495 |
Called when the segment is changed in the selector. |
|
|
496 |
Passes the caller information to updateParameterNode |
|
|
497 |
""" |
|
|
498 |
self.updateParameterNodeFromGUI(event, "Segment") |
|
|
499 |
|
|
|
500 |
def DICOMSeriesChanged(self, event): |
|
|
501 |
self.updateParameterNodeFromGUI(event, "DICOM") |
|
|
502 |
|
|
|
503 |
def updateParameterNodeFromGUI(self, event=None, caller=None): |
|
|
504 |
""" |
|
|
505 |
This method is called when the user makes any change in the GUI. |
|
|
506 |
The changes are saved into the parameter node (so that they are restored when the scene is saved and loaded). |
|
|
507 |
""" |
|
|
508 |
if self._parameterNode is None or self._updatingGUIFromParameterNode: |
|
|
509 |
return |
|
|
510 |
|
|
|
511 |
wasModified = self._parameterNode.StartModify() # Modify all properties in a single batch |
|
|
512 |
self._parameterNode.SetNodeReferenceID("InputVolume", self.ui.inputSelector.currentNodeID) |
|
|
513 |
if caller == "InputVolume": |
|
|
514 |
if event is None: |
|
|
515 |
self._parameterNode.SetNodeReferenceID("InputVolume", None) |
|
|
516 |
else: |
|
|
517 |
self._parameterNode.SetNodeReferenceID("InputVolume", event.GetID()) |
|
|
518 |
elif caller == 'SegmentNode': |
|
|
519 |
if event is None: |
|
|
520 |
self._parameterNode.SetNodeReferenceID("SegmentNode", None) |
|
|
521 |
else: |
|
|
522 |
self._parameterNode.SetNodeReferenceID("SegmentNode", event.GetID()) |
|
|
523 |
elif caller == 'Segment': |
|
|
524 |
self._parameterNode.SetParameter("SegmentID", str(event)) |
|
|
525 |
elif caller == 'DICOM' and event is not None: |
|
|
526 |
self._parameterNode.SetNodeReferenceID("DICOMNode", event.GetID()) |
|
|
527 |
self._parameterNode.SetParameter("LowerThreshold", str(self.ui.thresholdSelector.lowerThreshold)) |
|
|
528 |
self._parameterNode.SetParameter("UpperThreshold", str(self.ui.thresholdSelector.upperThreshold)) |
|
|
529 |
self._parameterNode.SetParameter("Analysis", str(self.ui.analysisSelector.currentText)) |
|
|
530 |
self._parameterNode.SetParameter("UseAlt", str(self.ui.AlternateDICOMCheckBox.checked)) |
|
|
531 |
self._parameterNode.SetParameter("UseMan", str(self.ui.ManualDICOMCheckBox.checked)) |
|
|
532 |
self.setNumParameter("0018,0050", str(self.ui.voxelSizeLineEdit.text)) |
|
|
533 |
self.setNumParameter("0029,1000", str(self.ui.scalingLineEdit.text)) |
|
|
534 |
self.setNumParameter("0029,1004", str(self.ui.densitySlopeLineEdit.text)) |
|
|
535 |
self.setNumParameter("0029,1005", str(self.ui.densityInterceptLineEdit.text)) |
|
|
536 |
self.setNumParameter("0028,1053", str(self.ui.rescaleSlopeLineEdit.text)) |
|
|
537 |
self.setNumParameter("0028,1052", str(self.ui.rescaleInterceptLineEdit.text)) |
|
|
538 |
self._parameterNode.SetParameter("OutputDirectory", str(self.ui.outputDirectorySelector.currentPath)) |
|
|
539 |
|
|
|
540 |
if self._parameterNode.GetParameter("Analysis") == 'Intervertebral Disc': |
|
|
541 |
self._parameterNode.SetParameter("MultiSelect", 'True') |
|
|
542 |
else: |
|
|
543 |
self._parameterNode.SetParameter("MultiSelect", 'False') |
|
|
544 |
|
|
|
545 |
self._parameterNode.EndModify(wasModified) |
|
|
546 |
self.updateGUIFromParameterNode() |
|
|
547 |
|
|
|
548 |
|
|
|
549 |
|
|
|
550 |
# Sets a parameter to a value if the value can be converted to a float, otherwises sets it to blank |
|
|
551 |
def setNumParameter(self, parameter, value): |
|
|
552 |
try: |
|
|
553 |
float(value) |
|
|
554 |
self._parameterNode.SetParameter(parameter, value) |
|
|
555 |
except ValueError: |
|
|
556 |
self._parameterNode.SetParameter(parameter, "") |
|
|
557 |
|
|
|
558 |
|
|
|
559 |
def onApplyButton(self): |
|
|
560 |
""" |
|
|
561 |
Run processing when user clicks "Apply" button. |
|
|
562 |
""" |
|
|
563 |
with slicer.util.tryWithErrorDisplay("Failed to compute results.", waitCursor=True): |
|
|
564 |
# Compute output |
|
|
565 |
self.logic.process(self._parameterNode.GetNodeReference("InputVolume"), self._parameterNode.GetNodeReference("SegmentNode"), self._parameterNode.GetParameter("SegmentID"), |
|
|
566 |
self.ui.thresholdSelector.lowerThreshold, self.ui.thresholdSelector.upperThreshold, self.ui.analysisSelector.currentText, self.ui.outputDirectorySelector.currentPath, |
|
|
567 |
self.ui.AlternateDICOMCheckBox.checked, self._parameterNode.GetNodeReference("DICOMNode"), self.ui.ManualDICOMCheckBox.checked, |
|
|
568 |
{'0018,0050':self.ui.voxelSizeLineEdit.text, '0029,1000':self.ui.scalingLineEdit.text, '0029,1004':self.ui.densitySlopeLineEdit.text, '0029,1005':self.ui.densityInterceptLineEdit.text, '0028,1053':self.ui.rescaleSlopeLineEdit.text, '0028,1052':self.ui.rescaleInterceptLineEdit.text}, self) |
|
|
569 |
self.updateGUIFromParameterNode() |
|
|
570 |
|
|
|
571 |
|
|
|
572 |
|
|
|
573 |
|
|
|
574 |
# Updates |
|
|
575 |
def analysisUpdate(self, cliNode, event): |
|
|
576 |
if cliNode.GetStatus() & cliNode.Completed: |
|
|
577 |
self.ui.AnalysisProgress.setValue(100) |
|
|
578 |
self.ui.AnalysisProgress.hide() |
|
|
579 |
if cliNode.GetStatus() & cliNode.ErrorsMask: |
|
|
580 |
# error |
|
|
581 |
errorText = cliNode.GetErrorText() |
|
|
582 |
print("CLI execution failed: " + errorText) |
|
|
583 |
else: |
|
|
584 |
# success |
|
|
585 |
print("CLI execution succeeded.") |
|
|
586 |
startTime=float(self._parameterNode.GetParameter("startTime")) |
|
|
587 |
stopTime = time.time() |
|
|
588 |
logging.info(f'Processing completed in {stopTime-startTime:.2f} seconds') |
|
|
589 |
# Clean up temp nodes |
|
|
590 |
slicer.mrmlScene.RemoveNode(self._parameterNode.GetNodeReference("labelmapNode")) |
|
|
591 |
slicer.mrmlScene.RemoveNode(self._parameterNode.GetNodeReference("labelmapNode2")) |
|
|
592 |
slicer.mrmlScene.RemoveNode(cliNode) |
|
|
593 |
self._parameterNode.SetParameter("Analyzing", "False") |
|
|
594 |
self.updateGUIFromParameterNode() |
|
|
595 |
else: |
|
|
596 |
self.ui.AnalysisProgress.setValue(cliNode.GetProgress()) |
|
|
597 |
|
|
|
598 |
|
|
|
599 |
|
|
|
600 |
|
|
|
601 |
|
|
|
602 |
# |
|
|
603 |
# MusculoskeletalAnalysisLogic |
|
|
604 |
# |
|
|
605 |
|
|
|
606 |
class MusculoskeletalAnalysisLogic(ScriptedLoadableModuleLogic): |
|
|
607 |
"""This class should implement all the actual |
|
|
608 |
computation done by your module. The interface |
|
|
609 |
should be such that other python code can import |
|
|
610 |
this class and make use of the functionality without |
|
|
611 |
requiring an instance of the Widget. |
|
|
612 |
Uses ScriptedLoadableModuleLogic base class, available at: |
|
|
613 |
https://github.com/Slicer/Slicer/blob/main/Base/Python/slicer/ScriptedLoadableModule.py |
|
|
614 |
""" |
|
|
615 |
|
|
|
616 |
def __init__(self): |
|
|
617 |
""" |
|
|
618 |
Called when the logic class is instantiated. Can be used for initializing member variables. |
|
|
619 |
""" |
|
|
620 |
ScriptedLoadableModuleLogic.__init__(self) |
|
|
621 |
|
|
|
622 |
def setDefaultParameters(self, parameterNode): |
|
|
623 |
""" |
|
|
624 |
Initialize parameter node with default settings. |
|
|
625 |
""" |
|
|
626 |
if not parameterNode.GetParameter("Analysis"): |
|
|
627 |
parameterNode.SetParameter("Analysis", "Cortical Bone") |
|
|
628 |
|
|
|
629 |
def process(self, inputVolume, mask, maskLabel, lowerThreshold, upperThreshold, analysis, outputDirectory, altDICOM=False, DICOMNode=None, manDICOM=False, DICOMOptions=None, source=None, wait=False): |
|
|
630 |
""" |
|
|
631 |
Run the processing algorithm. |
|
|
632 |
Can be used without GUI widget. |
|
|
633 |
:param InputVolume: volume to be thresholded |
|
|
634 |
:param Analysis: analysis function to perform |
|
|
635 |
:param OutputDirectory: directory to write output files to |
|
|
636 |
""" |
|
|
637 |
|
|
|
638 |
# Check inputs |
|
|
639 |
if not inputVolume: |
|
|
640 |
raise ValueError("Input volume is invalid") |
|
|
641 |
if not mask or not maskLabel: |
|
|
642 |
raise ValueError("Segment is invalid") |
|
|
643 |
if altDICOM and not DICOMNode: |
|
|
644 |
raise ValueError("No DICOM source") |
|
|
645 |
if manDICOM and not all(DICOMOptions): |
|
|
646 |
raise ValueError("Not all DICOM options are selected") |
|
|
647 |
|
|
|
648 |
if not os.access(outputDirectory, os.W_OK): |
|
|
649 |
if not os.access(outputDirectory, os.F_OK): |
|
|
650 |
# If directory doesn't exist try to create it |
|
|
651 |
try: |
|
|
652 |
os.makedirs(outputDirectory) |
|
|
653 |
if not os.access(outputDirectory, os.W_OK): |
|
|
654 |
raise ValueError("Output Directory is invalid") |
|
|
655 |
except: |
|
|
656 |
raise ValueError("Output Directory is invalid") |
|
|
657 |
else: |
|
|
658 |
# If directory is not writable for other reason |
|
|
659 |
raise ValueError("Output Directory is invalid") |
|
|
660 |
|
|
|
661 |
startTime = time.time() |
|
|
662 |
logging.info('Processing started') |
|
|
663 |
|
|
|
664 |
# Get mask segments |
|
|
665 |
if analysis == 'Intervertebral Disc': |
|
|
666 |
maskLabel = maskLabel.strip('()') |
|
|
667 |
labels = maskLabel.split(', ') |
|
|
668 |
maskID = mask.GetSegmentation().GetSegmentIdBySegmentName(labels[0].strip('\'')) |
|
|
669 |
maskArray = vtk.vtkStringArray() |
|
|
670 |
maskArray.InsertNextValue(maskID) |
|
|
671 |
labelmap = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLLabelMapVolumeNode") |
|
|
672 |
slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentsToLabelmapNode(mask, maskArray, labelmap, inputVolume) |
|
|
673 |
|
|
|
674 |
maskID2 = mask.GetSegmentation().GetSegmentIdBySegmentName(labels[1].strip('\'')) |
|
|
675 |
maskArray2 = vtk.vtkStringArray() |
|
|
676 |
maskArray2.InsertNextValue(maskID2) |
|
|
677 |
labelmap2 = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLLabelMapVolumeNode") |
|
|
678 |
slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentsToLabelmapNode(mask, maskArray2, labelmap2, inputVolume) |
|
|
679 |
else: |
|
|
680 |
maskID = mask.GetSegmentation().GetSegmentIdBySegmentName(maskLabel) |
|
|
681 |
maskArray = vtk.vtkStringArray() |
|
|
682 |
maskArray.InsertNextValue(maskID) |
|
|
683 |
labelmap = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLLabelMapVolumeNode") |
|
|
684 |
slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentsToLabelmapNode(mask, maskArray, labelmap, inputVolume) |
|
|
685 |
|
|
|
686 |
|
|
|
687 |
# Get DICOM source |
|
|
688 |
if altDICOM: |
|
|
689 |
dSource = DICOMNode |
|
|
690 |
elif manDICOM: |
|
|
691 |
dSource = DICOMOptions |
|
|
692 |
else: |
|
|
693 |
dSource = inputVolume |
|
|
694 |
if analysis != 'Intervertebral Disc': |
|
|
695 |
# Get Density info |
|
|
696 |
slope=float(self.getDICOMTag(dSource, '0029,1004'))/(float(self.getDICOMTag(dSource, '0029,1000'))*float(self.getDICOMTag(dSource, '0028,1053'))) |
|
|
697 |
intercept=float(self.getDICOMTag(dSource, '0029,1005'))-(float(self.getDICOMTag(dSource, '0028,1052'))*slope) |
|
|
698 |
|
|
|
699 |
|
|
|
700 |
voxelSize = self.getDICOMTag(dSource, '0018,0050') |
|
|
701 |
|
|
|
702 |
# Prepare parameters for the selected function |
|
|
703 |
if analysis == "Cortical Bone": |
|
|
704 |
parameters = {"image":inputVolume, "mask":labelmap, "lowerThreshold":lowerThreshold, "upperThreshold":upperThreshold, "voxelSize":voxelSize, "slope":slope, "intercept":intercept, "inputName":inputVolume.GetName(), "output":outputDirectory} |
|
|
705 |
module=slicer.modules.corticalanalysis |
|
|
706 |
requirements = [('scipy', 'scipy'), ('skimage', 'scikit-image'), ('nrrd', 'pynrrd')] |
|
|
707 |
elif analysis == "Cancellous Bone": |
|
|
708 |
parameters = {"image":inputVolume, "mask":labelmap, "lowerThreshold":lowerThreshold, "upperThreshold":upperThreshold, "voxelSize":voxelSize, "slope":slope, "intercept":intercept, "inputName":inputVolume.GetName(), "output":outputDirectory} |
|
|
709 |
module=slicer.modules.cancellousanalysis |
|
|
710 |
requirements = [('scipy', 'scipy'), ('skimage', 'scikit-image'), ('nrrd', 'pynrrd'), ('trimesh', 'trimesh')] |
|
|
711 |
elif analysis == "Bone Density": |
|
|
712 |
parameters = {"image":inputVolume, "mask":labelmap, "voxelSize":voxelSize, "slope":slope, "intercept":intercept, "inputName":inputVolume.GetName(), "output":outputDirectory} |
|
|
713 |
module=slicer.modules.densityanalysis |
|
|
714 |
requirements = [('nrrd', 'pynrrd')] |
|
|
715 |
elif analysis == 'Intervertebral Disc': |
|
|
716 |
parameters = {"image":inputVolume, "mask1":labelmap, "mask2":labelmap2, "voxelSize":voxelSize, "inputName":inputVolume.GetName(), "output":outputDirectory} |
|
|
717 |
module = slicer.modules.intervertebralanalysis |
|
|
718 |
requirements = [('scipy', 'scipy'), ('nrrd', 'pynrrd')] |
|
|
719 |
|
|
|
720 |
# Install required python modules |
|
|
721 |
self.importRequest(requirements) |
|
|
722 |
|
|
|
723 |
node = slicer.cli.createNode(module, parameters=parameters) |
|
|
724 |
# Set up source before running to avoid race conditions |
|
|
725 |
if source: |
|
|
726 |
source.ui.AnalysisProgress.setValue(0) |
|
|
727 |
source.ui.AnalysisProgress.show() |
|
|
728 |
source._parameterNode.SetParameter("Analyzing", "True") |
|
|
729 |
source._parameterNode.SetNodeReferenceID("labelmapNode", labelmap.GetID()) |
|
|
730 |
if 'labelmap2' in locals(): |
|
|
731 |
source._parameterNode.SetNodeReferenceID("labelmapNode2", labelmap2.GetID()) |
|
|
732 |
source._parameterNode.SetParameter("startTime", str(startTime)) |
|
|
733 |
node.AddObserver('ModifiedEvent', source.analysisUpdate) |
|
|
734 |
slicer.cli.run(module=module, node=node, wait_for_completion=wait) |
|
|
735 |
|
|
|
736 |
|
|
|
737 |
# Used to get dicom metadata from the volume |
|
|
738 |
# source: the volume node or DICOM dict |
|
|
739 |
# tag: The DICOM tag number as a string ('####,####') |
|
|
740 |
def getDICOMTag(self, source, tag): |
|
|
741 |
if type(source) is dict: |
|
|
742 |
data=source[tag] |
|
|
743 |
else: |
|
|
744 |
shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) |
|
|
745 |
volumeItemId = shNode.GetItemByDataNode(source) |
|
|
746 |
seriesInstanceUID = shNode.GetItemUID(volumeItemId, 'DICOM') |
|
|
747 |
|
|
|
748 |
db = slicer.dicomDatabase |
|
|
749 |
instanceList = db.instancesForSeries(seriesInstanceUID) |
|
|
750 |
data = db.instanceValue(instanceList[0], tag) |
|
|
751 |
return data |
|
|
752 |
|
|
|
753 |
# Check a list of modules to see if they are installed, request permission to install any missing. |
|
|
754 |
# requested: A list of tuples in the format (moduleName, pipName) |
|
|
755 |
def importRequest(self, requested): |
|
|
756 |
missing = [] |
|
|
757 |
for r in requested: |
|
|
758 |
if not importlib.util.find_spec(r[0]): |
|
|
759 |
missing.append(r) |
|
|
760 |
if len(missing) > 0: |
|
|
761 |
names, pips = zip(*missing) |
|
|
762 |
if slicer.util.confirmOkCancelDisplay("The following python modules are required: " + ", ".join(names) + ". Do you want to install them?"): |
|
|
763 |
for p in pips: |
|
|
764 |
slicer.util.pip_install(p) |
|
|
765 |
#pass |
|
|
766 |
else: |
|
|
767 |
raise ImportError("Required python modules do not have permission to install") |
|
|
768 |
|
|
|
769 |
|
|
|
770 |
|
|
|
771 |
# |
|
|
772 |
# MusculoskeletalAnalysisTest |
|
|
773 |
# |
|
|
774 |
|
|
|
775 |
class MusculoskeletalAnalysisTest(ScriptedLoadableModuleTest): |
|
|
776 |
""" |
|
|
777 |
This is the test case for your scripted module. |
|
|
778 |
Uses ScriptedLoadableModuleTest base class, available at: |
|
|
779 |
https://github.com/Slicer/Slicer/blob/main/Base/Python/slicer/ScriptedLoadableModule.py |
|
|
780 |
""" |
|
|
781 |
|
|
|
782 |
def setUp(self): |
|
|
783 |
""" Do whatever is needed to reset the state - typically a scene clear will be enough. |
|
|
784 |
""" |
|
|
785 |
slicer.mrmlScene.Clear() |
|
|
786 |
|
|
|
787 |
def runTest(self): |
|
|
788 |
"""Run as few or as many tests as needed here. |
|
|
789 |
""" |
|
|
790 |
self.setUp() |
|
|
791 |
self.test_MusculoskeletalAnalysis1() |
|
|
792 |
|
|
|
793 |
def test_MusculoskeletalAnalysis1(self): |
|
|
794 |
""" Ideally you should have several levels of tests. At the lowest level |
|
|
795 |
tests should exercise the functionality of the logic with different inputs |
|
|
796 |
(both valid and invalid). At higher levels your tests should emulate the |
|
|
797 |
way the user would interact with your code and confirm that it still works |
|
|
798 |
the way you intended. |
|
|
799 |
One of the most important features of the tests is that it should alert other |
|
|
800 |
developers when their changes will have an impact on the behavior of your |
|
|
801 |
module. For example, if a developer removes a feature that you depend on, |
|
|
802 |
your test should break so they know that the feature is needed. |
|
|
803 |
""" |
|
|
804 |
|
|
|
805 |
self.delayDisplay("Starting the test") |
|
|
806 |
|
|
|
807 |
|
|
|
808 |
# Get/create input data |
|
|
809 |
from datetime import date |
|
|
810 |
import SampleData |
|
|
811 |
# Make sure the scene is clear before starting |
|
|
812 |
slicer.mrmlScene.Clear() |
|
|
813 |
registerSampleData() |
|
|
814 |
|
|
|
815 |
|
|
|
816 |
try: |
|
|
817 |
# Set test parameters |
|
|
818 |
inputVolume = SampleData.downloadSample('Cortical1') |
|
|
819 |
mask = SampleData.downloadSample('CorticalMask1') |
|
|
820 |
maskLabel = "Segment_1" |
|
|
821 |
lowerThreshold = 4000 |
|
|
822 |
upperThreshold = 10000 |
|
|
823 |
analysis="Cortical Bone" |
|
|
824 |
options = {"0018,0050":0.0073996, "0028,1052":-1000, "0028,1053":0.4943119, "0029,1000":4096,"0029,1004":365.712, "0029,1005":-199.725998, } |
|
|
825 |
outputDirectory = os.path.expanduser("~\\Documents\\MusculoskeletalAnalysisTest") |
|
|
826 |
|
|
|
827 |
self.delayDisplay('Loaded test data set') |
|
|
828 |
# Test the module logic |
|
|
829 |
|
|
|
830 |
logic = MusculoskeletalAnalysisLogic() |
|
|
831 |
|
|
|
832 |
|
|
|
833 |
# Test cortical analysis |
|
|
834 |
logic.process(inputVolume, mask, maskLabel, lowerThreshold, upperThreshold, analysis, outputDirectory, manDICOM=True, DICOMOptions=options, wait=True) |
|
|
835 |
self.testFile(os.path.join(outputDirectory, "cortical.txt"), [str(date.today()), "Cortical1", 0.21890675924688482, 0.06119903498019812, 1094.0291917644427, 0.08353005741605146, 1.6847633350543425, 0.9198784024224288, 0.7648849326319137, 0.47030180777724473, 0.0073996]) |
|
|
836 |
finally: |
|
|
837 |
# Clean up nodes |
|
|
838 |
slicer.mrmlScene.Clear() |
|
|
839 |
|
|
|
840 |
try: |
|
|
841 |
# Set test parameters |
|
|
842 |
inputVolume = SampleData.downloadSample('Cancellous1') |
|
|
843 |
mask = SampleData.downloadSample('CancellousMask1') |
|
|
844 |
maskLabel = "Segment_1" |
|
|
845 |
lowerThreshold = 1500 |
|
|
846 |
upperThreshold = 10000 |
|
|
847 |
analysis="Cancellous Bone" |
|
|
848 |
self.delayDisplay('Loaded test data set') |
|
|
849 |
# Test cancellous analysis |
|
|
850 |
logic.process(inputVolume, mask, maskLabel, lowerThreshold, upperThreshold, analysis, outputDirectory, manDICOM=True, DICOMOptions=options, wait=True) |
|
|
851 |
self.testFile(os.path.join(outputDirectory, "cancellous.txt"), [str(date.today()), "Cancellous1", 1.333489381819056, 0.27215499848346064, 0.20409236263449296, 0.05182980579223419, 0.01560752819180971, 0.16437754493704732, 0.06443102087549339, 6.083556001417202, 1.6664854524662474, 365.2072574703726, 589.9997317002255, 0.0073996, 1500.0, 10000.0]) |
|
|
852 |
|
|
|
853 |
# Reuses cancellous parameters |
|
|
854 |
analysis="Bone Density" |
|
|
855 |
self.delayDisplay('Loaded test data set') |
|
|
856 |
# Test density analysis |
|
|
857 |
logic.process(inputVolume, mask, maskLabel, lowerThreshold, upperThreshold, analysis, outputDirectory, manDICOM=True, DICOMOptions=options, wait=True) |
|
|
858 |
self.testFile(os.path.join(outputDirectory, "density.txt"), ["", "", 2.32469398135312, 147.00866960658377, 309.78804391297064, -255.03709872604347, 1198.6849313585226, "", "", "", "", "", "", "", ""]) |
|
|
859 |
finally: |
|
|
860 |
# Clean up nodes |
|
|
861 |
slicer.mrmlScene.Clear() |
|
|
862 |
|
|
|
863 |
try: |
|
|
864 |
# Set test parameters |
|
|
865 |
inputVolume = SampleData.downloadSample('Intervertebral1') |
|
|
866 |
mask = SampleData.downloadSample('IntervertebralMask1') |
|
|
867 |
maskLabel = "'Segment_1, Segment_2'" |
|
|
868 |
lowerThreshold = 0 |
|
|
869 |
upperThreshold = 0 |
|
|
870 |
analysis="Intervertebral Disc" |
|
|
871 |
self.delayDisplay('Loaded test data set') |
|
|
872 |
# Test cancellous analysis |
|
|
873 |
logic.process(inputVolume, mask, maskLabel, lowerThreshold, upperThreshold, analysis, outputDirectory, manDICOM=True, DICOMOptions=options, wait=True) |
|
|
874 |
self.testFile(os.path.join(outputDirectory, "intervertebral.txt"), [str(date.today()), "Intervertebral1", 0.28008754758301957, 0.0694562859207484, 0.24798062791478137, 1.4070918728304844, 1.0306450189585161, 0.1775904, 0.12621094857350137, 0.0073996]) |
|
|
875 |
|
|
|
876 |
finally: |
|
|
877 |
# Clean up nodes |
|
|
878 |
slicer.mrmlScene.Clear() |
|
|
879 |
|
|
|
880 |
|
|
|
881 |
self.delayDisplay('Test passed') |
|
|
882 |
|
|
|
883 |
# testFile |
|
|
884 |
# Tests that the newest line of an output file matches a specified input |
|
|
885 |
# fileName: The path to the output file. Output file should be a tsv file |
|
|
886 |
# data: A list of strings to compare the file to |
|
|
887 |
# Throws an assertion error if the data does not match |
|
|
888 |
def testFile(self, fileName, data): |
|
|
889 |
# Confirm that file exist |
|
|
890 |
assert os.path.exists(fileName), "Filename "+fileName+" does not exist." |
|
|
891 |
# Get the header from the first line of the file and the most recent data as the last line |
|
|
892 |
with open(fileName) as f: |
|
|
893 |
lines = f.read().splitlines() |
|
|
894 |
firstLine = lines[0] |
|
|
895 |
lastLine = lines[-1] |
|
|
896 |
header = firstLine.split("\t") |
|
|
897 |
testData = lastLine.split("\t") |
|
|
898 |
# Check that the number of rows is correct |
|
|
899 |
assert len(data) == len(testData), "Expected "+ str(len(data)) + " lines, got " + str(len(testData)) + " instead." |
|
|
900 |
for i in range(len(data)): |
|
|
901 |
try: |
|
|
902 |
# Checks that numeric data is within 5% |
|
|
903 |
float(testData[i]) # If the data is not convertable to a float, throws a value error and compares data as a string |
|
|
904 |
assert (float(data[i]) > float(testData[i])*.95 and float(data[i]) < float(testData[i])*1.05) or (float(data[i]) < float(testData[i])*.95 and float(data[i]) > float(testData[i])*1.05), "Value for " + header[i] + ", " + testData[i] + " outside of range of expected value " + str(data[i]) + "." |
|
|
905 |
except ValueError: |
|
|
906 |
# Checks that strings match |
|
|
907 |
assert data[i] == testData[i], "Value for " + header[i] + ", " + testData[i] + ", doesn't match expected value " + str(data[i]) + "." |
|
|
908 |
|