Slicer 5.9
Slicer is a multi-platform, free and open source software package for visualization and medical image computing
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
SegmentEditorThresholdEffect.py
Go to the documentation of this file.
1import logging
2import os
3import weakref
4
5import ctk
6import vtk
7import qt
8
9import slicer
10from SegmentEditorEffects import *
11
12from slicer.i18n import tr as _
13
14
16 """ThresholdEffect is an Effect implementing the global threshold
17 operation in the segment editor
18
19 This is also an example for scripted effects, and some methods have no
20 function. The methods that are not needed (i.e. the default implementation in
21 qSlicerSegmentEditorAbstractEffect is satisfactory) can simply be omitted.
22 """
23
24 def __init__(self, scriptedEffect):
25 AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
26 scriptedEffect.name = "Threshold" # no tr (don't translate it because modules find effects by name)
27 scriptedEffect.title = _("Threshold")
28
31
32 # Effect-specific members
33 import vtkITK
34
35 self.autoThresholdCalculator = vtkITK.vtkITKImageThresholdCalculator()
36
37 self.timer = qt.QTimer()
38 self.previewState = 0
39 self.previewStep = 1
40 self.previewSteps = 5
41 self.timer.connect("timeout()", self.preview)
42
45
46 # Histogram stencil setup
47 self.stencil = vtk.vtkPolyDataToImageStencil()
48
49 # Histogram reslice setup
50 self.reslice = vtk.vtkImageReslice()
51 self.reslice.AutoCropOutputOff()
52 self.reslice.SetOptimization(1)
53 self.reslice.SetOutputOrigin(0, 0, 0)
54 self.reslice.SetOutputSpacing(1, 1, 1)
55 self.reslice.SetOutputDimensionality(3)
56 self.reslice.GenerateStencilOutputOn()
57
58 self.imageAccumulate = vtk.vtkImageAccumulate()
59 self.imageAccumulate.SetInputConnection(0, self.reslice.GetOutputPort())
60 self.imageAccumulate.SetInputConnection(1, self.stencil.GetOutputPort())
61
64
65 # Threshold range can be set by clicking in slice views. When the effect (or derived effects, such
66 # as Local Threshold) is used programmatically in a custom module then it may be desirable to show
67 # the threshold preview glow, but not let the user modify the preset threshold by view interactions.
68 # In such cases, enableViewInteractions can be set to False.
70
71 def cleanup(self):
72 # Disconnect the timer signal to allow proper garbage collection.
73 #
74 # This prevents lingering signal/slot connections from keeping
75 # the object alive. For more details, see the parent class
76 # cleanup() docstring or the following issue:
77 # https://github.com/Slicer/Slicer/issues/7392
78 self.timer.disconnect("timeout()", self.preview)
79
80 def clone(self):
81 import qSlicerSegmentationsEditorEffectsPythonQt as effects
82
83 clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
84 clonedEffect.setPythonSource(__file__.replace("\\", "/"))
85 return clonedEffect
86
87 def icon(self):
88 iconPath = os.path.join(os.path.dirname(__file__), "Resources/Icons/Threshold.png")
89 if os.path.exists(iconPath):
90 return qt.QIcon(iconPath)
91 return qt.QIcon()
92
93 def helpText(self):
94 return "<html>" + _("""Fill segment based on source volume intensity range<br>. Options:<p>
95<ul style="margin: 0">
96<li><b>Use for masking:</b> set the selected intensity range as <dfn>Editable intensity range</dfn> and switch to Paint effect.
97<li><b>Apply:</b> set the previewed segmentation in the selected segment. Previous contents of the segment is overwritten.
98</ul><p>""")
99
100 def activate(self):
101 # Update intensity range
103
104 # Start preview pulse
105 self.timer.start(200)
106
107 def deactivate(self):
108 # Stop preview pulse
109 self.timer.stop()
110
111 # Clear preview pipeline
114
115 def setupOptionsFrame(self):
116 self.thresholdSliderLabel = qt.QLabel(_("Threshold Range:"))
117 self.thresholdSliderLabel.setToolTip(_("Set the range of the background values that should be labeled."))
118 self.scriptedEffect.addOptionsWidget(self.thresholdSliderLabel)
119
120 self.thresholdSlider = ctk.ctkRangeWidget()
121 self.thresholdSlider.spinBoxAlignment = qt.Qt.AlignTop
122 self.thresholdSlider.singleStep = 0.01
123 self.scriptedEffect.addOptionsWidget(self.thresholdSlider)
124
126 self.autoThresholdModeSelectorComboBox.addItem(_("threshold above"), MODE_SET_LOWER_MAX)
127 self.autoThresholdModeSelectorComboBox.addItem(_("threshold below"), MODE_SET_MIN_UPPER)
128 self.autoThresholdModeSelectorComboBox.addItem(_("set as lower value"), MODE_SET_LOWER)
129 self.autoThresholdModeSelectorComboBox.addItem(_("set as upper value"), MODE_SET_UPPER)
130 self.autoThresholdModeSelectorComboBox.setToolTip(_("How to set lower and upper values of the threshold range."
131 " Threshold above/below: sets the range from the computed value to maximum/minimum."
132 " Set as lower/upper value: only modifies one side of the threshold range."))
133
135 self.autoThresholdMethodSelectorComboBox.addItem(_("Otsu"), METHOD_OTSU)
136 self.autoThresholdMethodSelectorComboBox.addItem(_("Huang"), METHOD_HUANG)
137 self.autoThresholdMethodSelectorComboBox.addItem(_("IsoData"), METHOD_ISO_DATA)
138 # Kittler-Illingworth sometimes fails with an exception, but it does not cause any major issue,
139 # it just logs an error message and does not compute a new threshold value
140 self.autoThresholdMethodSelectorComboBox.addItem(_("Kittler-Illingworth"), METHOD_KITTLER_ILLINGWORTH)
141 # Li sometimes crashes (index out of range error in
142 # ITK/Modules/Filtering/Thresholding/include/itkLiThresholdCalculator.hxx#L94)
143 # We can add this method back when issue is fixed in ITK.
144 # self.autoThresholdMethodSelectorComboBox.addItem("Li", METHOD_LI)
145 self.autoThresholdMethodSelectorComboBox.addItem(_("Maximum entropy"), METHOD_MAXIMUM_ENTROPY)
146 self.autoThresholdMethodSelectorComboBox.addItem(_("Moments"), METHOD_MOMENTS)
147 self.autoThresholdMethodSelectorComboBox.addItem(_("Renyi entropy"), METHOD_RENYI_ENTROPY)
148 self.autoThresholdMethodSelectorComboBox.addItem(_("Shanbhag"), METHOD_SHANBHAG)
149 self.autoThresholdMethodSelectorComboBox.addItem(_("Triangle"), METHOD_TRIANGLE)
150 self.autoThresholdMethodSelectorComboBox.addItem(_("Yen"), METHOD_YEN)
151 self.autoThresholdMethodSelectorComboBox.setToolTip(_("Select method to compute threshold value automatically."))
152
153 self.selectPreviousAutoThresholdButton = qt.QToolButton()
155 self.selectPreviousAutoThresholdButton.setToolTip(_("Select previous thresholding method and set thresholds."
156 " Useful for iterating through all available methods."))
157
158 self.selectNextAutoThresholdButton = qt.QToolButton()
159 self.selectNextAutoThresholdButton.text = ">"
160 self.selectNextAutoThresholdButton.setToolTip(_("Select next thresholding method and set thresholds."
161 " Useful for iterating through all available methods."))
162
163 self.setAutoThresholdButton = qt.QPushButton(_("Set"))
164 self.setAutoThresholdButton.setToolTip(_("Set threshold using selected method."))
165 # qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding)
166 # fails on some systems, therefore set the policies using separate method calls
167 qSize = qt.QSizePolicy()
168 qSize.setHorizontalPolicy(qt.QSizePolicy.Expanding)
169 self.setAutoThresholdButton.setSizePolicy(qSize)
170
171 autoThresholdFrame = qt.QGridLayout()
172 autoThresholdFrame.addWidget(self.autoThresholdMethodSelectorComboBox, 0, 0, 1, 1)
173 autoThresholdFrame.addWidget(self.selectPreviousAutoThresholdButton, 0, 1, 1, 1)
174 autoThresholdFrame.addWidget(self.selectNextAutoThresholdButton, 0, 2, 1, 1)
175 autoThresholdFrame.addWidget(self.autoThresholdModeSelectorComboBox, 1, 0, 1, 3)
176 autoThresholdFrame.addWidget(self.setAutoThresholdButton, 2, 0, 1, 3)
177
178 autoThresholdGroupBox = ctk.ctkCollapsibleGroupBox()
179 autoThresholdGroupBox.setTitle(_("Automatic threshold"))
180 autoThresholdGroupBox.setLayout(autoThresholdFrame)
181 autoThresholdGroupBox.collapsed = True
182 self.scriptedEffect.addOptionsWidget(autoThresholdGroupBox)
183
184 histogramFrame = qt.QVBoxLayout()
185
186 histogramBrushFrame = qt.QHBoxLayout()
187 histogramFrame.addLayout(histogramBrushFrame)
188
189 self.regionLabel = qt.QLabel(_("Region shape:"))
190 histogramBrushFrame.addWidget(self.regionLabel)
191
192 self.histogramBrushButtonGroup = qt.QButtonGroup()
193 self.histogramBrushButtonGroup.setExclusive(True)
194
195 self.boxROIButton = qt.QToolButton()
196 self.boxROIButton.setText(_("Box"))
197 self.boxROIButton.setCheckable(True)
198 self.boxROIButton.clicked.connect(self.updateMRMLFromGUI)
199 histogramBrushFrame.addWidget(self.boxROIButton)
200 self.histogramBrushButtonGroup.addButton(self.boxROIButton)
201
202 self.circleROIButton = qt.QToolButton()
203 self.circleROIButton.setText(_("Circle"))
204 self.circleROIButton.setCheckable(True)
205 self.circleROIButton.clicked.connect(self.updateMRMLFromGUI)
206 histogramBrushFrame.addWidget(self.circleROIButton)
207 self.histogramBrushButtonGroup.addButton(self.circleROIButton)
208
209 self.drawROIButton = qt.QToolButton()
210 self.drawROIButton.setText(_("Draw"))
211 self.drawROIButton.setCheckable(True)
212 self.drawROIButton.clicked.connect(self.updateMRMLFromGUI)
213 histogramBrushFrame.addWidget(self.drawROIButton)
214 self.histogramBrushButtonGroup.addButton(self.drawROIButton)
215
216 self.lineROIButton = qt.QToolButton()
217 self.lineROIButton.setText(_("Line"))
218 self.lineROIButton.setCheckable(True)
219 self.lineROIButton.clicked.connect(self.updateMRMLFromGUI)
220 histogramBrushFrame.addWidget(self.lineROIButton)
221 self.histogramBrushButtonGroup.addButton(self.lineROIButton)
222
223 histogramBrushFrame.addStretch()
224
225 self.histogramView = ctk.ctkTransferFunctionView()
226 self.histogramView = self.histogramView
227 histogramFrame.addWidget(self.histogramView)
228 scene = self.histogramView.scene()
229
230 self.histogramFunction = vtk.vtkPiecewiseFunction()
231 self.histogramFunctionContainer = ctk.ctkVTKPiecewiseFunction(self.scriptedEffect)
232 self.histogramFunctionContainer.setPiecewiseFunction(self.histogramFunction)
233 self.histogramFunctionItem = ctk.ctkTransferFunctionBarsItem(self.histogramFunctionContainer)
234 self.histogramFunctionItem.barWidth = 1.0
235 self.histogramFunctionItem.logMode = ctk.ctkTransferFunctionBarsItem.NoLog
236 self.histogramFunctionItem.setZValue(1)
237 scene.addItem(self.histogramFunctionItem)
238
240 self.histogramEventFilter.setThresholdEffect(self)
241 self.histogramFunctionItem.installEventFilter(self.histogramEventFilter)
242
243 self.minMaxFunction = vtk.vtkPiecewiseFunction()
244 self.minMaxFunctionContainer = ctk.ctkVTKPiecewiseFunction(self.scriptedEffect)
245 self.minMaxFunctionContainer.setPiecewiseFunction(self.minMaxFunction)
246 self.minMaxFunctionItem = ctk.ctkTransferFunctionBarsItem(self.minMaxFunctionContainer)
247 self.minMaxFunctionItem.barWidth = 0.03
248 self.minMaxFunctionItem.logMode = ctk.ctkTransferFunctionBarsItem.NoLog
249 self.minMaxFunctionItem.barColor = qt.QColor(200, 0, 0)
250 self.minMaxFunctionItem.setZValue(0)
251 scene.addItem(self.minMaxFunctionItem)
252
253 self.averageFunction = vtk.vtkPiecewiseFunction()
254 self.averageFunctionContainer = ctk.ctkVTKPiecewiseFunction(self.scriptedEffect)
255 self.averageFunctionContainer.setPiecewiseFunction(self.averageFunction)
256 self.averageFunctionItem = ctk.ctkTransferFunctionBarsItem(self.averageFunctionContainer)
257 self.averageFunctionItem.barWidth = 0.03
258 self.averageFunctionItem.logMode = ctk.ctkTransferFunctionBarsItem.NoLog
259 self.averageFunctionItem.barColor = qt.QColor(225, 150, 0)
260 self.averageFunctionItem.setZValue(-1)
261 scene.addItem(self.averageFunctionItem)
262
263 # Window level gradient
264 self.backgroundColor = [1.0, 1.0, 0.7]
265 self.backgroundFunction = vtk.vtkColorTransferFunction()
266 self.backgroundFunctionContainer = ctk.ctkVTKColorTransferFunction(self.scriptedEffect)
267 self.backgroundFunctionContainer.setColorTransferFunction(self.backgroundFunction)
268 self.backgroundFunctionItem = ctk.ctkTransferFunctionGradientItem(self.backgroundFunctionContainer)
269 self.backgroundFunctionItem.setZValue(-2)
270 scene.addItem(self.backgroundFunctionItem)
271
272 histogramItemFrame = qt.QHBoxLayout()
273 histogramFrame.addLayout(histogramItemFrame)
274
275
277
278 lowerGroupBox = qt.QGroupBox(_("Lower"))
279 lowerHistogramLayout = qt.QHBoxLayout()
280 lowerHistogramLayout.setContentsMargins(0, 3, 0, 3)
281 lowerGroupBox.setLayout(lowerHistogramLayout)
282 histogramItemFrame.addWidget(lowerGroupBox)
283 self.histogramLowerMethodButtonGroup = qt.QButtonGroup()
284 self.histogramLowerMethodButtonGroup.setExclusive(True)
285
287 self.histogramLowerThresholdMinimumButton.setText(_("Min"))
288 self.histogramLowerThresholdMinimumButton.setToolTip(_("Minimum"))
289 self.histogramLowerThresholdMinimumButton.setCheckable(True)
291 lowerHistogramLayout.addWidget(self.histogramLowerThresholdMinimumButton)
293
295 self.histogramLowerThresholdLowerButton.setText(_("Lower"))
296 self.histogramLowerThresholdLowerButton.setCheckable(True)
298 lowerHistogramLayout.addWidget(self.histogramLowerThresholdLowerButton)
300
302 self.histogramLowerThresholdAverageButton.setText(_("Mean"))
303 self.histogramLowerThresholdAverageButton.setCheckable(True)
305 lowerHistogramLayout.addWidget(self.histogramLowerThresholdAverageButton)
307
308
310
311 upperGroupBox = qt.QGroupBox(_("Upper"))
312 upperHistogramLayout = qt.QHBoxLayout()
313 upperHistogramLayout.setContentsMargins(0, 3, 0, 3)
314 upperGroupBox.setLayout(upperHistogramLayout)
315 histogramItemFrame.addWidget(upperGroupBox)
316 self.histogramUpperMethodButtonGroup = qt.QButtonGroup()
317 self.histogramUpperMethodButtonGroup.setExclusive(True)
318
320 self.histogramUpperThresholdAverageButton.setText(_("Mean"))
321 self.histogramUpperThresholdAverageButton.setCheckable(True)
323 upperHistogramLayout.addWidget(self.histogramUpperThresholdAverageButton)
325
327 self.histogramUpperThresholdUpperButton.setText(_("Upper"))
328 self.histogramUpperThresholdUpperButton.setCheckable(True)
330 upperHistogramLayout.addWidget(self.histogramUpperThresholdUpperButton)
332
334 self.histogramUpperThresholdMaximumButton.setText(_("Max"))
335 self.histogramUpperThresholdMaximumButton.setToolTip(_("Maximum"))
336 self.histogramUpperThresholdMaximumButton.setCheckable(True)
338 upperHistogramLayout.addWidget(self.histogramUpperThresholdMaximumButton)
340
341 histogramGroupBox = ctk.ctkCollapsibleGroupBox()
342 histogramGroupBox.setTitle(_("Local histogram"))
343 histogramGroupBox.setLayout(histogramFrame)
344 histogramGroupBox.collapsed = True
345 self.scriptedEffect.addOptionsWidget(histogramGroupBox)
346
347 self.useForPaintButton = qt.QPushButton(_("Use for masking"))
348 self.useForPaintButton.setToolTip(_("Use specified intensity range for masking and switch to Paint effect."))
349 self.scriptedEffect.addOptionsWidget(self.useForPaintButton)
350
351 self.applyButton = qt.QPushButton(_("Apply"))
352 self.applyButton.objectName = self.__class__.__name__ + "Apply"
353 self.applyButton.setToolTip(_("Fill selected segment in regions that are in the specified intensity range."))
354 self.scriptedEffect.addOptionsWidget(self.applyButton)
355
356 self.useForPaintButton.connect("clicked()", self.onUseForPaint)
357 self.thresholdSlider.connect("valuesChanged(double,double)", self.onThresholdValuesChanged)
359 self.autoThresholdModeSelectorComboBox.connect("activated(int)", self.onSelectedAutoThresholdMethod)
362 self.setAutoThresholdButton.connect("clicked()", self.onAutoThreshold)
363 self.applyButton.connect("clicked()", self.onApply)
364
365 def sourceVolumeNodeChanged(self):
366 # Set scalar range of source volume image data to threshold slider
367 masterImageData = self.scriptedEffect.sourceVolumeImageData()
368 if masterImageData:
369 lo, hi = masterImageData.GetScalarRange()
370 self.thresholdSlider.setRange(lo, hi)
371 self.thresholdSlider.singleStep = (hi - lo) / 1000.
372 if (self.scriptedEffect.doubleParameter("MinimumThreshold") == self.scriptedEffect.doubleParameter("MaximumThreshold")):
373 # has not been initialized yet
374 self.scriptedEffect.setParameter("MinimumThreshold", lo + (hi - lo) * 0.25)
375 self.scriptedEffect.setParameter("MaximumThreshold", hi)
376
377 def layoutChanged(self):
379
380 def setMRMLDefaults(self):
381 self.scriptedEffect.setParameterDefault("MinimumThreshold", 0.0)
382 self.scriptedEffect.setParameterDefault("MaximumThreshold", 0)
383 self.scriptedEffect.setParameterDefault("AutoThresholdMethod", METHOD_OTSU)
384 self.scriptedEffect.setParameterDefault("AutoThresholdMode", MODE_SET_LOWER_MAX)
385 self.scriptedEffect.setParameterDefault(HISTOGRAM_BRUSH_TYPE_PARAMETER_NAME, HISTOGRAM_BRUSH_TYPE_CIRCLE)
386 self.scriptedEffect.setParameterDefault(HISTOGRAM_SET_LOWER_PARAMETER_NAME, HISTOGRAM_SET_LOWER)
387 self.scriptedEffect.setParameterDefault(HISTOGRAM_SET_UPPER_PARAMETER_NAME, HISTOGRAM_SET_UPPER)
388
389 def updateGUIFromMRML(self):
390 self.thresholdSlider.blockSignals(True)
391 self.thresholdSlider.setMinimumValue(self.scriptedEffect.doubleParameter("MinimumThreshold"))
392 self.thresholdSlider.setMaximumValue(self.scriptedEffect.doubleParameter("MaximumThreshold"))
393 self.thresholdSlider.blockSignals(False)
394
395 autoThresholdMethod = self.autoThresholdMethodSelectorComboBox.findData(self.scriptedEffect.parameter("AutoThresholdMethod"))
396 wasBlocked = self.autoThresholdMethodSelectorComboBox.blockSignals(True)
397 self.autoThresholdMethodSelectorComboBox.setCurrentIndex(autoThresholdMethod)
398 self.autoThresholdMethodSelectorComboBox.blockSignals(wasBlocked)
399
400 autoThresholdMode = self.autoThresholdModeSelectorComboBox.findData(self.scriptedEffect.parameter("AutoThresholdMode"))
401 wasBlocked = self.autoThresholdModeSelectorComboBox.blockSignals(True)
402 self.autoThresholdModeSelectorComboBox.setCurrentIndex(autoThresholdMode)
403 self.autoThresholdModeSelectorComboBox.blockSignals(wasBlocked)
404
405 histogramBrushType = self.scriptedEffect.parameter(HISTOGRAM_BRUSH_TYPE_PARAMETER_NAME)
406 self.boxROIButton.checked = histogramBrushType == HISTOGRAM_BRUSH_TYPE_BOX
407 self.circleROIButton.checked = histogramBrushType == HISTOGRAM_BRUSH_TYPE_CIRCLE
408 self.drawROIButton.checked = histogramBrushType == HISTOGRAM_BRUSH_TYPE_DRAW
409 self.lineROIButton.checked = histogramBrushType == HISTOGRAM_BRUSH_TYPE_LINE
410
411 histogramSetModeLower = self.scriptedEffect.parameter(HISTOGRAM_SET_LOWER_PARAMETER_NAME)
412 self.histogramLowerThresholdMinimumButton.checked = histogramSetModeLower == HISTOGRAM_SET_MINIMUM
413 self.histogramLowerThresholdLowerButton.checked = histogramSetModeLower == HISTOGRAM_SET_LOWER
414 self.histogramLowerThresholdAverageButton.checked = histogramSetModeLower == HISTOGRAM_SET_AVERAGE
415
416 histogramSetModeUpper = self.scriptedEffect.parameter(HISTOGRAM_SET_UPPER_PARAMETER_NAME)
417 self.histogramUpperThresholdAverageButton.checked = histogramSetModeUpper == HISTOGRAM_SET_AVERAGE
418 self.histogramUpperThresholdUpperButton.checked = histogramSetModeUpper == HISTOGRAM_SET_UPPER
419 self.histogramUpperThresholdMaximumButton.checked = histogramSetModeUpper == HISTOGRAM_SET_MAXIMUM
420
422
423 def updateMRMLFromGUI(self):
424 with slicer.util.NodeModify(self.scriptedEffect.parameterSetNode()):
425 self.scriptedEffect.setParameter("MinimumThreshold", self.thresholdSlider.minimumValue)
426 self.scriptedEffect.setParameter("MaximumThreshold", self.thresholdSlider.maximumValue)
427
428 methodIndex = self.autoThresholdMethodSelectorComboBox.currentIndex
429 autoThresholdMethod = self.autoThresholdMethodSelectorComboBox.itemData(methodIndex)
430 self.scriptedEffect.setParameter("AutoThresholdMethod", autoThresholdMethod)
431
432 modeIndex = self.autoThresholdModeSelectorComboBox.currentIndex
433 autoThresholdMode = self.autoThresholdModeSelectorComboBox.itemData(modeIndex)
434 self.scriptedEffect.setParameter("AutoThresholdMode", autoThresholdMode)
435
436 histogramParameterChanged = False
437
438 histogramBrushType = HISTOGRAM_BRUSH_TYPE_CIRCLE
439 if self.boxROIButton.checked:
440 histogramBrushType = HISTOGRAM_BRUSH_TYPE_BOX
441 elif self.circleROIButton.checked:
442 histogramBrushType = HISTOGRAM_BRUSH_TYPE_CIRCLE
443 elif self.drawROIButton.checked:
444 histogramBrushType = HISTOGRAM_BRUSH_TYPE_DRAW
445 elif self.lineROIButton.checked:
446 histogramBrushType = HISTOGRAM_BRUSH_TYPE_LINE
447
448 if histogramBrushType != self.scriptedEffect.parameter(HISTOGRAM_BRUSH_TYPE_PARAMETER_NAME):
449 self.scriptedEffect.setParameter(HISTOGRAM_BRUSH_TYPE_PARAMETER_NAME, histogramBrushType)
450 histogramParameterChanged = True
451
452 histogramSetModeLower = HISTOGRAM_SET_LOWER
454 histogramSetModeLower = HISTOGRAM_SET_MINIMUM
455 elif self.histogramLowerThresholdLowerButton.checked:
456 histogramSetModeLower = HISTOGRAM_SET_LOWER
457 elif self.histogramLowerThresholdAverageButton.checked:
458 histogramSetModeLower = HISTOGRAM_SET_AVERAGE
459 if histogramSetModeLower != self.scriptedEffect.parameter(HISTOGRAM_SET_LOWER_PARAMETER_NAME):
460 self.scriptedEffect.setParameter(HISTOGRAM_SET_LOWER_PARAMETER_NAME, histogramSetModeLower)
461 histogramParameterChanged = True
462
463 histogramSetModeUpper = HISTOGRAM_SET_UPPER
465 histogramSetModeUpper = HISTOGRAM_SET_AVERAGE
466 elif self.histogramUpperThresholdUpperButton.checked:
467 histogramSetModeUpper = HISTOGRAM_SET_UPPER
468 elif self.histogramUpperThresholdMaximumButton.checked:
469 histogramSetModeUpper = HISTOGRAM_SET_MAXIMUM
470 if histogramSetModeUpper != self.scriptedEffect.parameter(HISTOGRAM_SET_UPPER_PARAMETER_NAME):
471 self.scriptedEffect.setParameter(HISTOGRAM_SET_UPPER_PARAMETER_NAME, histogramSetModeUpper)
472 histogramParameterChanged = True
473
474 if histogramParameterChanged:
475 self.updateHistogram()
476
477 #
478 # Effect specific methods (the above ones are the API methods to override)
479 #
480 def onThresholdValuesChanged(self, min, max):
481 self.scriptedEffect.updateMRMLFromGUI()
482
483 def onUseForPaint(self):
484 parameterSetNode = self.scriptedEffect.parameterSetNode()
485 parameterSetNode.SourceVolumeIntensityMaskOn()
486 parameterSetNode.SetSourceVolumeIntensityMaskRange(self.thresholdSlider.minimumValue, self.thresholdSlider.maximumValue)
487 # Switch to paint effect
488 self.scriptedEffect.selectEffect("Paint")
489
490 def onSelectPreviousAutoThresholdMethod(self):
491 self.autoThresholdMethodSelectorComboBox.currentIndex = (self.autoThresholdMethodSelectorComboBox.currentIndex - 1) \
494
495 def onSelectNextAutoThresholdMethod(self):
496 self.autoThresholdMethodSelectorComboBox.currentIndex = (self.autoThresholdMethodSelectorComboBox.currentIndex + 1) \
499
500 def onSelectedAutoThresholdMethod(self):
501 self.updateMRMLFromGUI()
502 self.onAutoThreshold()
503 self.updateGUIFromMRML()
504
505 def onAutoThreshold(self):
506 autoThresholdMethod = self.scriptedEffect.parameter("AutoThresholdMethod")
507 autoThresholdMode = self.scriptedEffect.parameter("AutoThresholdMode")
508 self.autoThreshold(autoThresholdMethod, autoThresholdMode)
509
510 def autoThreshold(self, autoThresholdMethod, autoThresholdMode):
511 if autoThresholdMethod == METHOD_HUANG:
512 self.autoThresholdCalculator.SetMethodToHuang()
513 elif autoThresholdMethod == METHOD_INTERMODES:
514 self.autoThresholdCalculator.SetMethodToIntermodes()
515 elif autoThresholdMethod == METHOD_ISO_DATA:
516 self.autoThresholdCalculator.SetMethodToIsoData()
517 elif autoThresholdMethod == METHOD_KITTLER_ILLINGWORTH:
518 self.autoThresholdCalculator.SetMethodToKittlerIllingworth()
519 elif autoThresholdMethod == METHOD_LI:
520 self.autoThresholdCalculator.SetMethodToLi()
521 elif autoThresholdMethod == METHOD_MAXIMUM_ENTROPY:
522 self.autoThresholdCalculator.SetMethodToMaximumEntropy()
523 elif autoThresholdMethod == METHOD_MOMENTS:
524 self.autoThresholdCalculator.SetMethodToMoments()
525 elif autoThresholdMethod == METHOD_OTSU:
526 self.autoThresholdCalculator.SetMethodToOtsu()
527 elif autoThresholdMethod == METHOD_RENYI_ENTROPY:
528 self.autoThresholdCalculator.SetMethodToRenyiEntropy()
529 elif autoThresholdMethod == METHOD_SHANBHAG:
530 self.autoThresholdCalculator.SetMethodToShanbhag()
531 elif autoThresholdMethod == METHOD_TRIANGLE:
532 self.autoThresholdCalculator.SetMethodToTriangle()
533 elif autoThresholdMethod == METHOD_YEN:
534 self.autoThresholdCalculator.SetMethodToYen()
535 else:
536 logging.error(f"Unknown AutoThresholdMethod {autoThresholdMethod}")
537
538 masterImageData = self.scriptedEffect.sourceVolumeImageData()
539 self.autoThresholdCalculator.SetInputData(masterImageData)
540
541 self.autoThresholdCalculator.Update()
542 computedThreshold = self.autoThresholdCalculator.GetThreshold()
543
544 sourceVolumeMin, sourceVolumeMax = masterImageData.GetScalarRange()
545
546 if autoThresholdMode == MODE_SET_UPPER:
547 self.scriptedEffect.setParameter("MaximumThreshold", computedThreshold)
548 elif autoThresholdMode == MODE_SET_LOWER:
549 self.scriptedEffect.setParameter("MinimumThreshold", computedThreshold)
550 elif autoThresholdMode == MODE_SET_MIN_UPPER:
551 self.scriptedEffect.setParameter("MinimumThreshold", sourceVolumeMin)
552 self.scriptedEffect.setParameter("MaximumThreshold", computedThreshold)
553 elif autoThresholdMode == MODE_SET_LOWER_MAX:
554 self.scriptedEffect.setParameter("MinimumThreshold", computedThreshold)
555 self.scriptedEffect.setParameter("MaximumThreshold", sourceVolumeMax)
556 else:
557 logging.error(f"Unknown AutoThresholdMode {autoThresholdMode}")
558
559 def onApply(self):
560 if not self.scriptedEffect.confirmCurrentSegmentVisible():
561 return
562
563 try:
564 # Get source volume image data
565 masterImageData = self.scriptedEffect.sourceVolumeImageData()
566 # Get modifier labelmap
567 modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
568 originalImageToWorldMatrix = vtk.vtkMatrix4x4()
569 modifierLabelmap.GetImageToWorldMatrix(originalImageToWorldMatrix)
570 # Get parameters
571 min = self.scriptedEffect.doubleParameter("MinimumThreshold")
572 max = self.scriptedEffect.doubleParameter("MaximumThreshold")
573
574 self.scriptedEffect.saveStateForUndo()
575
576 # Perform thresholding
577 thresh = vtk.vtkImageThreshold()
578 thresh.SetInputData(masterImageData)
579 thresh.ThresholdBetween(min, max)
580 thresh.SetInValue(1)
581 thresh.SetOutValue(0)
582 thresh.SetOutputScalarType(modifierLabelmap.GetScalarType())
583 thresh.Update()
584 modifierLabelmap.DeepCopy(thresh.GetOutput())
585 except IndexError:
586 logging.error("apply: Failed to threshold source volume!")
587 pass
588
589 # Apply changes
590 self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
591
592 # De-select effect
593 self.scriptedEffect.selectEffect("")
594
595 def clearPreviewDisplayPipelines(self):
596 for sliceWidget, pipeline in self.previewPipelines.items():
597 self.scriptedEffect.removeActor2D(sliceWidget, pipeline.actor)
598 segmentationDisplayableManager = sliceWidget.sliceView().displayableManagerByClassName("vtkMRMLSegmentationsDisplayableManager2D")
599 segmentationDisplayableManager.RemoveCustomSegmentRenderer(pipeline.customRendererTag)
600
601 self.previewPipelines = {}
603 self.previewedSegmentID = None
604
605 def clearHistogramDisplay(self):
606 if self.histogramPipeline is None:
607 return
608 self.histogramPipeline.removeActors()
609 self.histogramPipeline = None
610
611 def updatePreviewDisplayPipelines(self):
612 # Clear previous pipelines before setting up the new ones
613 layoutManager = slicer.app.layoutManager()
614 sliceViewNames = layoutManager.sliceViewNames() if self.previewedSegmentationDisplayNode and layoutManager else []
615
616 # Add a pipeline for each 2D slice view
617 sliceWidgetPipelinesToKeep = []
618 for sliceViewName in sliceViewNames:
619
620 sliceWidget = layoutManager.sliceWidget(sliceViewName)
621 if not self.scriptedEffect.segmentationDisplayableInView(sliceWidget.mrmlSliceNode()):
622 # No need to add pipeline in this widget
623 continue
624
625 pipeline = self.previewPipelines.get(sliceWidget)
626
627 # Check if a custom segment renderer is already registered for this segment by someone else (e.g., another segment editor widget)
628 segmentationDisplayableManager = sliceWidget.sliceView().displayableManagerByClassName("vtkMRMLSegmentationsDisplayableManager2D")
629 existingSegmentRendererTag = segmentationDisplayableManager.GetCustomSegmentRendererTag(
631 if existingSegmentRendererTag != 0:
632 if (not pipeline) or (pipeline.customRendererTag != existingSegmentRendererTag):
633 # Another segment editor widget is already displaying this segment with a custom renderer
634 continue
635
636 sliceWidgetPipelinesToKeep.append(sliceWidget)
637 if pipeline:
638 # Pipeline is already present
639 continue
640
641 # Add pipeline
642 renderer = self.scriptedEffect.renderer(sliceWidget)
643 if renderer is None:
644 logging.error("updatePreviewDisplayPipelines: Failed to get renderer!")
645 continue
646 pipeline = PreviewPipeline()
647 self.previewPipelines[sliceWidget] = pipeline
648 self.scriptedEffect.addActor2D(sliceWidget, pipeline.actor)
649 pipeline.customRendererTag = segmentationDisplayableManager.AddCustomSegmentRenderer(
651
652 # Removed unused pipelines
653 for sliceWidget, pipeline in self.previewPipelines.items():
654 if sliceWidget in sliceWidgetPipelinesToKeep:
655 continue
656 self.scriptedEffect.removeActor2D(sliceWidget, pipeline.actor)
657 segmentationDisplayableManager = sliceWidget.sliceView().displayableManagerByClassName("vtkMRMLSegmentationsDisplayableManager2D")
658 segmentationDisplayableManager.RemoveCustomSegmentRenderer(pipeline.customRendererTag)
659 self.previewPipelines.pop(sliceWidget)
660
661 def preview(self):
662 # Make sure we keep the currently selected segment hidden
663 # (the user may have changed selected segmentation or segment)
664
665 segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
666 displayNode = segmentationNode.GetDisplayNode() if segmentationNode else None
667 segmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID()
668
669 if (self.previewedSegmentationDisplayNode != displayNode) or (segmentID != self.previewedSegmentID):
670 # Previewed segmentation or segment changed, remove any previous custom rendering of another previewed segment
672 self.previewedSegmentationDisplayNode = displayNode
673 self.previewedSegmentID = segmentID
674
675 # Update preview display pipelines
680
682 return
683
684 opacity = 0.5 + self.previewState / (2.0 * self.previewSteps)
685 min = self.scriptedEffect.doubleParameter("MinimumThreshold")
686 max = self.scriptedEffect.doubleParameter("MaximumThreshold")
687
688 # Get color of edited segment
689 segmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID()
690
691 r, g, b = self.previewedSegmentationDisplayNode.GetSegmentColor(segmentID)
692
693 # Set values to pipelines
694 for sliceWidget in self.previewPipelines:
695 pipeline = self.previewPipelines[sliceWidget]
696 pipeline.lookupTable.SetTableValue(1, r, g, b, opacity)
697 layerLogic = self.getSourceVolumeLayerLogic(sliceWidget)
698 pipeline.thresholdFilter.SetInputConnection(layerLogic.GetReslice().GetOutputPort())
699 pipeline.thresholdFilter.ThresholdBetween(min, max)
700 pipeline.actor.VisibilityOn()
701 sliceWidget.sliceView().scheduleRender()
702
703 self.previewState += self.previewStep
704 if self.previewState >= self.previewSteps:
705 self.previewStep = -1
706 if self.previewState <= 0:
707 self.previewStep = 1
708
709 def processInteractionEvents(self, callerInteractor, eventId, viewWidget):
710 abortEvent = False
711
712 if viewWidget not in self.previewPipelines:
713 # In this view, this effect instance does not display threshold preview pipeline,
714 # therefore prevent it from displaying the local histogram pipeline, too.
715 return abortEvent
716
717 # Note that if multiple segment editor widgets are active then only one (the first that gets the interaction events)
718 # will be notified. In all the other segment editor widgets the local histogram will not be updated.
719 # The behavior could be made more deterministic by storing a preferred segment editor node in the selection node
720 # and not using the same segment editor node in multiple segment editor widgets.
721
722 if not self.enableViewInteractions:
723 return abortEvent
724
725 masterImageData = self.scriptedEffect.sourceVolumeImageData()
726 if masterImageData is None:
727 return abortEvent
728
729 # Only allow for slice views
730 if viewWidget.className() != "qMRMLSliceWidget":
731 return abortEvent
732
733 anyModifierKeyPressed = callerInteractor.GetShiftKey() or callerInteractor.GetControlKey() or callerInteractor.GetAltKey()
734
735 # Clicking in a view should remove all previous pipelines
736 if eventId == vtk.vtkCommand.LeftButtonPressEvent and not anyModifierKeyPressed:
738
739 if self.histogramPipeline is None:
740 self.createHistogramPipeline(viewWidget)
741
742 xy = callerInteractor.GetEventPosition()
743 ras = self.xyToRas(xy, viewWidget)
744
745 if eventId == vtk.vtkCommand.LeftButtonPressEvent and not anyModifierKeyPressed:
746 self.histogramPipeline.state = HISTOGRAM_STATE_MOVING
747 self.histogramPipeline.addPoint(ras)
748 self.updateHistogram()
749 abortEvent = True
750 elif eventId == vtk.vtkCommand.LeftButtonReleaseEvent:
751 if self.histogramPipeline.state == HISTOGRAM_STATE_MOVING:
752 self.histogramPipeline.state = HISTOGRAM_STATE_PLACED
753 abortEvent = True
754 elif eventId == vtk.vtkCommand.MouseMoveEvent:
755 if self.histogramPipeline.state == HISTOGRAM_STATE_MOVING:
756 self.histogramPipeline.addPoint(ras)
757 self.updateHistogram()
758 return abortEvent
759
760 def createHistogramPipeline(self, sliceWidget):
761 brushType = HISTOGRAM_BRUSH_TYPE_CIRCLE
762 if self.boxROIButton.checked:
763 brushType = HISTOGRAM_BRUSH_TYPE_BOX
764 elif self.drawROIButton.checked:
765 brushType = HISTOGRAM_BRUSH_TYPE_DRAW
766 elif self.lineROIButton.checked:
767 brushType = HISTOGRAM_BRUSH_TYPE_LINE
768 pipeline = HistogramPipeline(self, self.scriptedEffect, sliceWidget, brushType)
769 self.histogramPipeline = pipeline
770
771 def processViewNodeEvents(self, callerViewNode, eventId, viewWidget):
772 if self.histogramPipeline is not None:
773 self.histogramPipeline.updateBrushModel()
774
775 def onHistogramMouseClick(self, pos, button):
776 self.selectionStartPosition = pos
777 self.selectionEndPosition = pos
778 if button == qt.Qt.RightButton:
779 self.selectionStartPosition = None
780 self.selectionEndPosition = None
781 self.minMaxFunction.RemoveAllPoints()
782 self.averageFunction.RemoveAllPoints()
783 self.updateHistogram()
784
785 def onHistogramMouseMove(self, pos, button):
786 self.selectionEndPosition = pos
787 if button == qt.Qt.RightButton:
788 return
789 self.updateHistogram()
790
791 def onHistogramMouseRelease(self, pos, button):
792 self.selectionEndPosition = pos
793 if button == qt.Qt.RightButton:
794 return
795 self.updateHistogram()
796
797 def getSourceVolumeLayerLogic(self, sliceWidget):
798 sourceVolumeNode = self.scriptedEffect.parameterSetNode().GetSourceVolumeNode()
799 sliceLogic = sliceWidget.sliceLogic()
800
801 backgroundLogic = sliceLogic.GetBackgroundLayer()
802 backgroundVolumeNode = backgroundLogic.GetVolumeNode()
803 if sourceVolumeNode == backgroundVolumeNode:
804 return backgroundLogic
805
806 foregroundLogic = sliceLogic.GetForegroundLayer()
807 foregroundVolumeNode = foregroundLogic.GetVolumeNode()
808 if sourceVolumeNode == foregroundVolumeNode:
809 return foregroundLogic
810
811 logging.warning("Source volume is not set as either the foreground or background")
812
813 foregroundOpacity = 0.0
814 if foregroundVolumeNode:
815 compositeNode = sliceLogic.GetSliceCompositeNode()
816 foregroundOpacity = compositeNode.GetForegroundOpacity()
817
818 if foregroundOpacity > 0.5:
819 return foregroundLogic
820
821 return backgroundLogic
822
823 def updateHistogram(self):
824 masterImageData = self.scriptedEffect.sourceVolumeImageData()
825 if masterImageData is None or self.histogramPipeline is None:
826 self.histogramFunction.RemoveAllPoints()
827 return
828
829 # Ensure that the brush is in the correct location
830 self.histogramPipeline.updateBrushModel()
831
832 self.stencil.SetInputConnection(self.histogramPipeline.worldToSliceTransformer.GetOutputPort())
833
834 self.histogramPipeline.worldToSliceTransformer.Update()
835 brushPolydata = self.histogramPipeline.worldToSliceTransformer.GetOutput()
836 brushBounds = brushPolydata.GetBounds()
837 brushExtent = [0, -1, 0, -1, 0, -1]
838 for i in range(3):
839 brushExtent[2 * i] = vtk.vtkMath.Floor(brushBounds[2 * i])
840 brushExtent[2 * i + 1] = vtk.vtkMath.Ceil(brushBounds[2 * i + 1])
841 if brushExtent[0] > brushExtent[1] or brushExtent[2] > brushExtent[3] or brushExtent[4] > brushExtent[5]:
842 self.histogramFunction.RemoveAllPoints()
843 return
844
845 layerLogic = self.getSourceVolumeLayerLogic(self.histogramPipeline.sliceWidget)
846 self.reslice.SetInputConnection(layerLogic.GetReslice().GetInputConnection(0, 0))
847 self.reslice.SetResliceTransform(layerLogic.GetReslice().GetResliceTransform())
848 self.reslice.SetInterpolationMode(layerLogic.GetReslice().GetInterpolationMode())
849 self.reslice.SetOutputExtent(brushExtent)
850
851 maxNumberOfBins = 1000
852 masterImageData = self.scriptedEffect.sourceVolumeImageData()
853 scalarRange = masterImageData.GetScalarRange()
854 scalarType = masterImageData.GetScalarType()
855 if scalarType == vtk.VTK_FLOAT or scalarType == vtk.VTK_DOUBLE:
856 numberOfBins = maxNumberOfBins
857 else:
858 numberOfBins = int(scalarRange[1] - scalarRange[0]) + 1
859 numberOfBins = min(numberOfBins, maxNumberOfBins)
860 binSpacing = (scalarRange[1] - scalarRange[0] + 1) / numberOfBins
861
862 self.imageAccumulate.SetComponentExtent(0, numberOfBins - 1, 0, 0, 0, 0)
863 self.imageAccumulate.SetComponentSpacing(binSpacing, binSpacing, binSpacing)
864 self.imageAccumulate.SetComponentOrigin(scalarRange[0], scalarRange[0], scalarRange[0])
865
866 self.imageAccumulate.Update()
867
868 self.histogramFunction.RemoveAllPoints()
869 tableSize = self.imageAccumulate.GetOutput().GetPointData().GetScalars().GetNumberOfTuples()
870 for i in range(tableSize):
871 value = self.imageAccumulate.GetOutput().GetPointData().GetScalars().GetTuple1(i)
872 self.histogramFunction.AddPoint(binSpacing * i + scalarRange[0], value)
873 self.histogramFunction.AdjustRange(scalarRange)
874
875 lower = self.imageAccumulate.GetMin()[0]
876 average = self.imageAccumulate.GetMean()[0]
877 upper = self.imageAccumulate.GetMax()[0]
878
879 # If there is a selection, then set the threshold based on that
880 if self.selectionStartPosition is not None and self.selectionEndPosition is not None:
881 # Clamp selection based on scalar range
882 startX = min(scalarRange[1], max(scalarRange[0], self.selectionStartPosition[0]))
883 endX = min(scalarRange[1], max(scalarRange[0], self.selectionEndPosition[0]))
884
885 lower = min(startX, endX)
886 average = (startX + endX) / 2.0
887 upper = max(startX, endX)
888
889 epsilon = 0.00001
890 self.minMaxFunction.RemoveAllPoints()
891 self.minMaxFunction.AddPoint(lower - epsilon, 0.0)
892 self.minMaxFunction.AddPoint(lower, 1.0)
893 self.minMaxFunction.AddPoint(lower + epsilon, 0.0)
894 self.minMaxFunction.AddPoint(upper - epsilon, 0.0)
895 self.minMaxFunction.AddPoint(upper, 1.0)
896 self.minMaxFunction.AddPoint(upper + epsilon, 0.0)
897 self.minMaxFunction.AdjustRange(scalarRange)
898
899 self.averageFunction.RemoveAllPoints()
900 self.averageFunction.AddPoint(average - epsilon, 0.0)
901 self.averageFunction.AddPoint(average, 1.0)
902 self.averageFunction.AddPoint(average + epsilon, 0.0)
903 self.averageFunction.AdjustRange(scalarRange)
904
905 minimumThreshold = lower
906 maximumThreshold = upper
907
908 histogramSetModeLower = self.scriptedEffect.parameter(HISTOGRAM_SET_LOWER_PARAMETER_NAME)
909 if histogramSetModeLower == HISTOGRAM_SET_MINIMUM:
910 minimumThreshold = scalarRange[0]
911 elif histogramSetModeLower == HISTOGRAM_SET_LOWER:
912 minimumThreshold = lower
913 elif histogramSetModeLower == HISTOGRAM_SET_AVERAGE:
914 minimumThreshold = average
915
916 histogramSetModeUpper = self.scriptedEffect.parameter(HISTOGRAM_SET_UPPER_PARAMETER_NAME)
917 if histogramSetModeUpper == HISTOGRAM_SET_AVERAGE:
918 maximumThreshold = average
919 elif histogramSetModeUpper == HISTOGRAM_SET_UPPER:
920 maximumThreshold = upper
921 elif histogramSetModeUpper == HISTOGRAM_SET_MAXIMUM:
922 maximumThreshold = scalarRange[1]
923
924 self.scriptedEffect.setParameter("MinimumThreshold", minimumThreshold)
925 self.scriptedEffect.setParameter("MaximumThreshold", maximumThreshold)
926
927 def updateHistogramBackground(self):
928 self.backgroundFunction.RemoveAllPoints()
929
930 masterImageData = self.scriptedEffect.sourceVolumeImageData()
931 if masterImageData is None:
932 return
933
934 scalarRange = masterImageData.GetScalarRange()
935
936 epsilon = 0.00001
937 low = self.scriptedEffect.doubleParameter("MinimumThreshold")
938 upper = self.scriptedEffect.doubleParameter("MaximumThreshold")
939 low = max(scalarRange[0] + epsilon, low)
940 upper = min(scalarRange[1] - epsilon, upper)
941
942 self.backgroundFunction.AddRGBPoint(scalarRange[0], 1, 1, 1)
943 self.backgroundFunction.AddRGBPoint(low - epsilon, 1, 1, 1)
944 self.backgroundFunction.AddRGBPoint(low, self.backgroundColor[0], self.backgroundColor[1], self.backgroundColor[2])
945 self.backgroundFunction.AddRGBPoint(upper, self.backgroundColor[0], self.backgroundColor[1], self.backgroundColor[2])
946 self.backgroundFunction.AddRGBPoint(upper + epsilon, 1, 1, 1)
947 self.backgroundFunction.AddRGBPoint(scalarRange[1], 1, 1, 1)
948 self.backgroundFunction.SetAlpha(1.0)
949 self.backgroundFunction.Build()
950
951
952#
953# PreviewPipeline
954#
956 """Visualization objects and pipeline for each slice view for threshold preview"""
957
958 def __init__(self):
959 self.lookupTable = vtk.vtkLookupTable()
960 self.lookupTable.SetRampToLinear()
961 self.lookupTable.SetNumberOfTableValues(2)
962 self.lookupTable.SetTableRange(0, 1)
963 self.lookupTable.SetTableValue(0, 0, 0, 0, 0)
964 self.colorMapper = vtk.vtkImageMapToRGBA()
965 self.colorMapper.SetOutputFormatToRGBA()
966 self.colorMapper.SetLookupTable(self.lookupTable)
967 self.thresholdFilter = vtk.vtkImageThreshold()
968 self.thresholdFilter.SetInValue(1)
969 self.thresholdFilter.SetOutValue(0)
970 self.thresholdFilter.SetOutputScalarTypeToUnsignedChar()
971
972 # Feedback actor
973 self.mapper = vtk.vtkImageMapper()
974 self.dummyImage = vtk.vtkImageData()
975 self.dummyImage.AllocateScalars(vtk.VTK_UNSIGNED_INT, 1)
976 self.mapper.SetInputData(self.dummyImage)
977 self.actor = vtk.vtkActor2D()
978 self.actor.VisibilityOff()
979 self.actor.SetMapper(self.mapper)
980 self.mapper.SetColorWindow(255)
981 self.mapper.SetColorLevel(128)
982
983 # Setup pipeline
984 self.colorMapper.SetInputConnection(self.thresholdFilter.GetOutputPort())
985 self.mapper.SetInputConnection(self.colorMapper.GetOutputPort())
986
987 # Custom renderer tag that is provided by the segmentation displayable manager
988 # when we add our custom segment renderer.
990
991
995class HistogramEventFilter(qt.QObject):
996
997 def __init__(self, *args, **kwargs):
998 qt.QObject.__init__(self, *args, **kwargs)
1000
1001 @property
1003 return self.thresholdEffectWeakRef() if self.thresholdEffectWeakRef else None
1004
1005 def setThresholdEffect(self, thresholdEffect):
1006 self.thresholdEffectWeakRef = weakref.ref(thresholdEffect)
1007
1008 def eventFilter(self, object, event):
1009 if self.thresholdEffect is None:
1010 return
1011
1012 if (
1013 event.type() == qt.QEvent.GraphicsSceneMousePress
1014 or event.type() == qt.QEvent.GraphicsSceneMouseMove
1015 or event.type() == qt.QEvent.GraphicsSceneMouseRelease
1016 ):
1017 transferFunction = object.transferFunction()
1018 if transferFunction is None:
1019 return
1020
1021 representation = transferFunction.representation()
1022 x = representation.mapXFromScene(event.pos().x())
1023 y = representation.mapYFromScene(event.pos().y())
1024 position = (x, y)
1025
1026 if event.type() == qt.QEvent.GraphicsSceneMousePress:
1027 self.thresholdEffect.onHistogramMouseClick(position, event.button())
1028 elif event.type() == qt.QEvent.GraphicsSceneMouseMove:
1029 self.thresholdEffect.onHistogramMouseMove(position, event.button())
1030 elif event.type() == qt.QEvent.GraphicsSceneMouseRelease:
1031 self.thresholdEffect.onHistogramMouseRelease(position, event.button())
1032 return True
1033 return False
1034
1035
1037 def __init__(self, thresholdEffect, scriptedEffect, sliceWidget, brushMode):
1038 self.thresholdEffect = thresholdEffect
1039 self.scriptedEffect = scriptedEffect
1040 self.sliceWidget = sliceWidget
1041 self.brushMode = brushMode
1042 self.state = HISTOGRAM_STATE_OFF
1043
1044 self.point1 = None
1045 self.point2 = None
1046
1047 # Actor setup
1048 self.brushCylinderSource = vtk.vtkCylinderSource()
1049 self.brushCylinderSource.SetResolution(32)
1050
1051 self.brushCubeSource = vtk.vtkCubeSource()
1052
1053 self.brushLineSource = vtk.vtkLineSource()
1054 self.brushTubeSource = vtk.vtkTubeFilter()
1055 self.brushTubeSource.SetInputConnection(self.brushLineSource.GetOutputPort())
1056 self.brushTubeSource.SetNumberOfSides(50)
1057 self.brushTubeSource.SetCapping(True)
1058
1059 self.brushToWorldOriginTransform = vtk.vtkTransform()
1060 self.brushToWorldOriginTransformer = vtk.vtkTransformPolyDataFilter()
1062 self.brushToWorldOriginTransformer.SetInputConnection(self.brushCylinderSource.GetOutputPort())
1063
1064 self.normalFilter = vtk.vtkPolyDataNormals()
1065 self.normalFilter.AutoOrientNormalsOn()
1066 self.normalFilter.SetInputConnection(self.brushToWorldOriginTransformer.GetOutputPort())
1067
1068 # Brush to RAS transform
1069 self.worldOriginToWorldTransform = vtk.vtkTransform()
1070 self.worldOriginToWorldTransformer = vtk.vtkTransformPolyDataFilter()
1072 self.worldOriginToWorldTransformer.SetInputConnection(self.normalFilter.GetOutputPort())
1073
1074 # RAS to XY transform
1075 self.worldToSliceTransform = vtk.vtkTransform()
1076 self.worldToSliceTransformer = vtk.vtkTransformPolyDataFilter()
1077 self.worldToSliceTransformer.SetTransform(self.worldToSliceTransform)
1078 self.worldToSliceTransformer.SetInputConnection(self.worldOriginToWorldTransformer.GetOutputPort())
1079
1080 # Cutting takes place in XY coordinates
1081 self.slicePlane = vtk.vtkPlane()
1082 self.slicePlane.SetNormal(0, 0, 1)
1083 self.slicePlane.SetOrigin(0, 0, 0)
1084 self.cutter = vtk.vtkCutter()
1085 self.cutter.SetCutFunction(self.slicePlane)
1086 self.cutter.SetInputConnection(self.worldToSliceTransformer.GetOutputPort())
1087
1088 self.rasPoints = vtk.vtkPoints()
1089 lines = vtk.vtkCellArray()
1090 self.polyData = vtk.vtkPolyData()
1091 self.polyData.SetPoints(self.rasPoints)
1092 self.polyData.SetLines(lines)
1093
1094 # Thin line
1095 self.thinRASPoints = vtk.vtkPoints()
1096 thinLines = vtk.vtkCellArray()
1097 self.thinPolyData = vtk.vtkPolyData()
1098 self.thinPolyData.SetPoints(self.rasPoints)
1099 self.thinPolyData.SetLines(thinLines)
1100
1101 self.mapper = vtk.vtkPolyDataMapper2D()
1102 self.mapper.SetInputConnection(self.cutter.GetOutputPort())
1103
1104 # Add actor
1105 self.actor = vtk.vtkActor2D()
1106 self.actor.SetMapper(self.mapper)
1107 actorProperty = self.actor.GetProperty()
1108 actorProperty.SetColor(1, 1, 0)
1109 actorProperty.SetLineWidth(2)
1110 renderer = self.scriptedEffect.renderer(sliceWidget)
1111 if renderer is None:
1112 logging.error("pipelineForWidget: Failed to get renderer!")
1113 return None
1114 self.scriptedEffect.addActor2D(sliceWidget, self.actor)
1115
1116 self.thinActor = None
1117 if self.brushMode == HISTOGRAM_BRUSH_TYPE_DRAW:
1118 self.worldToSliceTransformer.SetInputData(self.polyData)
1119 self.mapper.SetInputConnection(self.worldToSliceTransformer.GetOutputPort())
1120
1121 self.thinWorldToSliceTransformer = vtk.vtkTransformPolyDataFilter()
1122 self.thinWorldToSliceTransformer.SetInputData(self.thinPolyData)
1124
1125 self.thinMapper = vtk.vtkPolyDataMapper2D()
1126 self.thinMapper.SetInputConnection(self.thinWorldToSliceTransformer.GetOutputPort())
1127
1128 self.thinActor = vtk.vtkActor2D()
1129 self.thinActor.SetMapper(self.thinMapper)
1130 thinActorProperty = self.thinActor.GetProperty()
1131 thinActorProperty.SetColor(1, 1, 0)
1132 thinActorProperty.SetLineWidth(1)
1133 self.scriptedEffect.addActor2D(sliceWidget, self.thinActor)
1134 elif self.brushMode == HISTOGRAM_BRUSH_TYPE_LINE:
1135 self.worldToSliceTransformer.SetInputConnection(self.brushTubeSource.GetOutputPort())
1136
1137 def removeActors(self):
1138 if self.actor is not None:
1139 self.scriptedEffect.removeActor2D(self.sliceWidget, self.actor)
1140 if self.thinActor is not None:
1141 self.scriptedEffect.removeActor2D(self.sliceWidget, self.thinActor)
1142
1143 def setPoint1(self, ras):
1144 self.point1 = ras
1145 self.updateBrushModel()
1146
1147 def setPoint2(self, ras):
1148 self.point2 = ras
1149 self.updateBrushModel()
1150
1151 def addPoint(self, ras):
1152 if self.brushMode == HISTOGRAM_BRUSH_TYPE_DRAW:
1153 newPointIndex = self.rasPoints.InsertNextPoint(ras)
1154 previousPointIndex = newPointIndex - 1
1155 if previousPointIndex >= 0:
1156 idList = vtk.vtkIdList()
1157 idList.InsertNextId(previousPointIndex)
1158 idList.InsertNextId(newPointIndex)
1159 self.polyData.InsertNextCell(vtk.VTK_LINE, idList)
1160
1161 thinLines = self.thinPolyData.GetLines()
1162 thinLines.Initialize()
1163 idList = vtk.vtkIdList()
1164 idList.InsertNextId(newPointIndex)
1165 idList.InsertNextId(0)
1166 self.thinPolyData.InsertNextCell(vtk.VTK_LINE, idList)
1167
1168 else:
1169 if self.point1 is None:
1170 self.setPoint1(ras)
1171 self.setPoint2(ras)
1172
1174 if self.brushMode != HISTOGRAM_BRUSH_TYPE_DRAW and (self.point1 is None or self.point2 is None):
1175 return
1176
1177 # Update slice cutting plane position and orientation
1178 sliceXyToRas = self.sliceWidget.sliceLogic().GetSliceNode().GetXYToRAS()
1179 rasToSliceXy = vtk.vtkMatrix4x4()
1180 vtk.vtkMatrix4x4.Invert(sliceXyToRas, rasToSliceXy)
1181 self.worldToSliceTransform.SetMatrix(rasToSliceXy)
1182
1183 # brush is rotated to the slice widget plane
1184 brushToWorldOriginTransformMatrix = vtk.vtkMatrix4x4()
1185 brushToWorldOriginTransformMatrix.DeepCopy(self.sliceWidget.sliceLogic().GetSliceNode().GetSliceToRAS())
1186 brushToWorldOriginTransformMatrix.SetElement(0, 3, 0)
1187 brushToWorldOriginTransformMatrix.SetElement(1, 3, 0)
1188 brushToWorldOriginTransformMatrix.SetElement(2, 3, 0)
1189
1190 self.brushToWorldOriginTransform.Identity()
1191 self.brushToWorldOriginTransform.Concatenate(brushToWorldOriginTransformMatrix)
1192 self.brushToWorldOriginTransform.RotateX(90) # cylinder's long axis is the Y axis, we need to rotate it to Z axis
1193
1194 sliceSpacingMm = self.scriptedEffect.sliceSpacing(self.sliceWidget)
1195
1196 center = [0, 0, 0]
1197 if self.brushMode == HISTOGRAM_BRUSH_TYPE_CIRCLE:
1198 center = self.point1
1199
1200 point1ToPoint2 = [0, 0, 0]
1201 vtk.vtkMath.Subtract(self.point1, self.point2, point1ToPoint2)
1202 radius = vtk.vtkMath.Normalize(point1ToPoint2)
1203
1204 self.brushToWorldOriginTransformer.SetInputConnection(self.brushCylinderSource.GetOutputPort())
1205 self.brushCylinderSource.SetRadius(radius)
1206 self.brushCylinderSource.SetHeight(sliceSpacingMm)
1207
1208 elif self.brushMode == HISTOGRAM_BRUSH_TYPE_BOX:
1209 self.brushToWorldOriginTransformer.SetInputConnection(self.brushCubeSource.GetOutputPort())
1210
1211 length = [0, 0, 0]
1212 for i in range(3):
1213 center[i] = (self.point1[i] + self.point2[i]) / 2.0
1214 length[i] = abs(self.point1[i] - self.point2[i])
1215
1216 xVector = [1, 0, 0, 0]
1217 self.brushToWorldOriginTransform.MultiplyPoint(xVector, xVector)
1218 xLength = abs(vtk.vtkMath.Dot(xVector[:3], length))
1219 self.brushCubeSource.SetXLength(xLength)
1220
1221 zVector = [0, 0, 1, 0]
1222 self.brushToWorldOriginTransform.MultiplyPoint(zVector, zVector)
1223 zLength = abs(vtk.vtkMath.Dot(zVector[:3], length))
1224 self.brushCubeSource.SetZLength(zLength)
1225 self.brushCubeSource.SetYLength(sliceSpacingMm)
1226
1227 elif self.brushMode == HISTOGRAM_BRUSH_TYPE_LINE:
1228 self.brushLineSource.SetPoint1(self.point1)
1229 self.brushLineSource.SetPoint2(self.point2)
1230 self.brushTubeSource.SetRadius(sliceSpacingMm)
1231
1232 self.worldOriginToWorldTransform.Identity()
1233 self.worldOriginToWorldTransform.Translate(center)
1234
1235 self.sliceWidget.sliceView().scheduleRender()
1236
1237
1238HISTOGRAM_BRUSH_TYPE_PARAMETER_NAME = "BrushType"
1239
1240HISTOGRAM_BRUSH_TYPE_BOX = "BOX"
1241HISTOGRAM_BRUSH_TYPE_CIRCLE = "CIRCLE"
1242HISTOGRAM_BRUSH_TYPE_DRAW = "DRAW"
1243HISTOGRAM_BRUSH_TYPE_LINE = "LINE"
1244
1245HISTOGRAM_STATE_OFF = "OFF"
1246HISTOGRAM_STATE_MOVING = "MOVING"
1247HISTOGRAM_STATE_PLACED = "PLACED"
1248
1249HISTOGRAM_SET_LOWER_PARAMETER_NAME = "HistogramSetLower"
1250HISTOGRAM_SET_UPPER_PARAMETER_NAME = "HistogramSetUpper"
1251
1252HISTOGRAM_SET_MINIMUM = "MINIMUM"
1253HISTOGRAM_SET_LOWER = "LOWER"
1254HISTOGRAM_SET_AVERAGE = "AVERAGE"
1255HISTOGRAM_SET_UPPER = "UPPER"
1256HISTOGRAM_SET_MAXIMUM = "MAXIMUM"
1257
1258
1259
1260METHOD_HUANG = "HUANG"
1261METHOD_INTERMODES = "INTERMODES"
1262METHOD_ISO_DATA = "ISO_DATA"
1263METHOD_KITTLER_ILLINGWORTH = "KITTLER_ILLINGWORTH"
1264METHOD_LI = "LI"
1265METHOD_MAXIMUM_ENTROPY = "MAXIMUM_ENTROPY"
1266METHOD_MOMENTS = "MOMENTS"
1267METHOD_OTSU = "OTSU"
1268METHOD_RENYI_ENTROPY = "RENYI_ENTROPY"
1269METHOD_SHANBHAG = "SHANBHAG"
1270METHOD_TRIANGLE = "TRIANGLE"
1271METHOD_YEN = "YEN"
1272
1273MODE_SET_UPPER = "SET_UPPER"
1274MODE_SET_LOWER = "SET_LOWER"
1275MODE_SET_MIN_UPPER = "SET_MIN_UPPER"
1276MODE_SET_LOWER_MAX = "SET_LOWER_MAX"
__init__(self, thresholdEffect, scriptedEffect, sliceWidget, brushMode)