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