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