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