Skip to content

Commit

Permalink
BUG: Fix segment disappearing when sharing segment editor node betwee…
Browse files Browse the repository at this point in the history
…n widgets

When a single vtkMRMLSegmentEditorNode was used by multiple qSlicerSegmentEditorWidget instances then after using Threshold effect, the current segment disappeared from slice views.

The reason was that Threshold effects in every segment editor widgets get activated (via sharing the segment editor node) and they all started preview, saved original segment opacity, and then on "Apply" restored the original segment opacity.
However, the segment opacity that was restored by second, third, etc. effects were not the original anymore, but it was already the opacity that was set to 0 (it has to be set to 0 to prevent the current segment occluding the preview).

The solution could have been to store the original opacity in a shared location (e.g., in the segment editor node), but that would not have solved the slight rendering issue (preview glow was darker) and performance degradation caused by several effects showing the preview glow at the same time.

Fixed it by storing the object ID of the threshold effect that manipulates the segmentation display node in the node's "SegmentEditor.PreviewingEffect" attribute. All other Threshold effects check this attribute and if they find that another effect already provide preview then they don't display the preview.

This commit also fixes disappearing segment when selecting another segmentation node while the Threshold effect is active.

fixes #6874
  • Loading branch information
lassoan authored and pieper committed Feb 9, 2024
1 parent 70ced7c commit 1a89f8e
Showing 1 changed file with 88 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def __init__(self, scriptedEffect):

self.segment2DFillOpacity = None
self.segment2DOutlineOpacity = None
self.previewedSegmentationDisplayNode = None
self.previewedSegmentID = None

# Effect-specific members
Expand All @@ -42,7 +43,6 @@ def __init__(self, scriptedEffect):

self.previewPipelines = {}
self.histogramPipeline = None
self.setupPreviewDisplay()

# Histogram stencil setup
self.stencil = vtk.vtkPolyDataToImageStencil()
Expand Down Expand Up @@ -84,24 +84,24 @@ def helpText(self):
</ul><p>""")

def activate(self):
self.setCurrentSegmentTransparent()

# Update intensity range
self.sourceVolumeNodeChanged()

# Setup and start preview pulse
self.setupPreviewDisplay()
# Start preview pulse
self.timer.start(200)

def deactivate(self):
# Stop preview pulse
self.timer.stop()

self.restorePreviewedSegmentTransparency()

# Clear preview pipeline and stop timer
self.clearPreviewDisplay()
# Clear preview pipeline
self.clearPreviewDisplayPipelines()
self.clearHistogramDisplay()
self.timer.stop()

def setCurrentSegmentTransparent(self):

def updatePreviewedSegmentTransparency(self):
"""Save current segment opacity and set it to zero
to temporarily hide the segment so that threshold preview
can be seen better.
Expand All @@ -110,44 +110,53 @@ def setCurrentSegmentTransparent(self):
opacity.
"""
segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
if not segmentationNode:
return
displayNode = segmentationNode.GetDisplayNode()
if not displayNode:
return
displayNode = segmentationNode.GetDisplayNode() if segmentationNode else None
segmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID()

if segmentID == self.previewedSegmentID:
if (self.previewedSegmentationDisplayNode == displayNode) and (segmentID == self.previewedSegmentID):
# already previewing the current segment
return

# If an other segment was previewed before, restore that.
if self.previewedSegmentID:
self.restorePreviewedSegmentTransparency()

if not segmentID:
# No segment selected, no need to make any segment transparent
return

# To allow previewing results, temporarily change opacity of the edited segment by modifying the display node.
# There may be multiple segment editor widgets that edit the same segmentation display node, therefore
# before we modify it we need to check if it is not used by any other segment editor effect.
currentEffectHash = str(self.__hash__())
previewingEffectHash = displayNode.GetAttribute("SegmentEditor.PreviewingEffect")
if previewingEffectHash and (previewingEffectHash != currentEffectHash):
# Another effect is already using this display node for preview
return

# Indicate in the display node that other segment editor effects should not tart preview now
displayNode.SetAttribute("SegmentEditor.PreviewingEffect", currentEffectHash)

# Make current segment fully transparent
if segmentID:
self.segment2DFillOpacity = displayNode.GetSegmentOpacity2DFill(segmentID)
self.segment2DOutlineOpacity = displayNode.GetSegmentOpacity2DOutline(segmentID)
self.previewedSegmentID = segmentID
displayNode.SetSegmentOpacity2DFill(segmentID, 0)
displayNode.SetSegmentOpacity2DOutline(segmentID, 0)
self.segment2DFillOpacity = displayNode.GetSegmentOpacity2DFill(segmentID)
self.segment2DOutlineOpacity = displayNode.GetSegmentOpacity2DOutline(segmentID)
self.previewedSegmentID = segmentID
self.previewedSegmentationDisplayNode = displayNode
displayNode.SetSegmentOpacity2DFill(segmentID, 0)
displayNode.SetSegmentOpacity2DOutline(segmentID, 0)

def restorePreviewedSegmentTransparency(self):
"""Restore previewed segment's opacity that was temporarily
made transparen by calling setCurrentSegmentTransparent().
made transparent by calling updatePreviewedSegmentTransparency().
"""
segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
if not segmentationNode:
return
displayNode = segmentationNode.GetDisplayNode()
displayNode = self.previewedSegmentationDisplayNode
if not displayNode:
return
if not self.previewedSegmentID:
# already previewing the current segment
return
displayNode.SetSegmentOpacity2DFill(self.previewedSegmentID, self.segment2DFillOpacity)
displayNode.SetSegmentOpacity2DOutline(self.previewedSegmentID, self.segment2DOutlineOpacity)
if self.previewedSegmentID:
displayNode.SetSegmentOpacity2DFill(self.previewedSegmentID, self.segment2DFillOpacity)
displayNode.SetSegmentOpacity2DOutline(self.previewedSegmentID, self.segment2DOutlineOpacity)
displayNode.RemoveAttribute("SegmentEditor.PreviewingEffect")
self.previewedSegmentationDisplayNode = None
self.previewedSegmentID = None

def setupOptionsFrame(self):
Expand Down Expand Up @@ -413,7 +422,7 @@ def sourceVolumeNodeChanged(self):
self.scriptedEffect.setParameter("MaximumThreshold", hi)

def layoutChanged(self):
self.setupPreviewDisplay()
self.updatePreviewDisplayPipelines()

def setMRMLDefaults(self):
self.scriptedEffect.setParameterDefault("MinimumThreshold", 0.0)
Expand Down Expand Up @@ -630,7 +639,7 @@ def onApply(self):
# De-select effect
self.scriptedEffect.selectEffect("")

def clearPreviewDisplay(self):
def clearPreviewDisplayPipelines(self):
for sliceWidget, pipeline in self.previewPipelines.items():
self.scriptedEffect.removeActor2D(sliceWidget, pipeline.actor)
self.previewPipelines = {}
Expand All @@ -641,52 +650,64 @@ def clearHistogramDisplay(self):
self.histogramPipeline.removeActors()
self.histogramPipeline = None

def setupPreviewDisplay(self):
def updatePreviewDisplayPipelines(self):
# Clear previous pipelines before setting up the new ones
self.clearPreviewDisplay()

layoutManager = slicer.app.layoutManager()
if layoutManager is None:
return
sliceViewNames = layoutManager.sliceViewNames() if self.previewedSegmentationDisplayNode and layoutManager else []

# Add a pipeline for each 2D slice view
for sliceViewName in layoutManager.sliceViewNames():
sliceWidgetPipelinesToKeep = []
for sliceViewName in sliceViewNames:

sliceWidget = layoutManager.sliceWidget(sliceViewName)
if not self.scriptedEffect.segmentationDisplayableInView(sliceWidget.mrmlSliceNode()):
# No need to add pipeline in this widget
continue

sliceWidgetPipelinesToKeep.append(sliceWidget)
pipeline = self.previewPipelines.get(sliceWidget)
if pipeline:
# Pipeline is already present
continue

# Add pipeline
renderer = self.scriptedEffect.renderer(sliceWidget)
if renderer is None:
logging.error("setupPreviewDisplay: Failed to get renderer!")
logging.error("updatePreviewDisplayPipelines: Failed to get renderer!")
continue

# Create pipeline
pipeline = PreviewPipeline()
self.previewPipelines[sliceWidget] = pipeline

# Add actor
self.scriptedEffect.addActor2D(sliceWidget, pipeline.actor)

# Removed unused pipelines
for sliceWidget, pipeline in self.previewPipelines.items():
if sliceWidget in sliceWidgetPipelinesToKeep:
continue
self.scriptedEffect.removeActor2D(sliceWidget, pipeline.actor)
self.previewPipelines.pop(sliceWidget)

def preview(self):
# Make sure we keep the currently selected segment hidden
# (the user may have changed selected segmentation or segment)
self.updatePreviewedSegmentTransparency()

# Update preview display pipelines
if self.previewedSegmentationDisplayNode and not self.previewPipelines:
self.updatePreviewDisplayPipelines()
elif not self.previewedSegmentationDisplayNode and self.previewPipelines:
self.clearPreviewDisplayPipelines()

if not self.previewedSegmentationDisplayNode:
return

opacity = 0.5 + self.previewState / (2.0 * self.previewSteps)
min = self.scriptedEffect.doubleParameter("MinimumThreshold")
max = self.scriptedEffect.doubleParameter("MaximumThreshold")

# Get color of edited segment
segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
if not segmentationNode:
# scene was closed while preview was active
return
displayNode = segmentationNode.GetDisplayNode()
if displayNode is None:
logging.error("preview: Invalid segmentation display node!")
color = [0.5, 0.5, 0.5]
segmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID()

# Make sure we keep the currently selected segment hidden (the user may have changed selection)
if segmentID != self.previewedSegmentID:
self.setCurrentSegmentTransparent()

r, g, b = segmentationNode.GetSegmentation().GetSegment(segmentID).GetColor()
r, g, b = self.previewedSegmentationDisplayNode.GetSegmentColor(segmentID)

# Set values to pipelines
for sliceWidget in self.previewPipelines:
Expand All @@ -707,6 +728,16 @@ def preview(self):
def processInteractionEvents(self, callerInteractor, eventId, viewWidget):
abortEvent = False

if viewWidget not in self.previewPipelines:
# In this view, this effect instance does not display threshold preview pipeline,
# therefore prevent it from displaying the local histogram pipeline, too.
return abortEvent

# Note that if multiple segment editor widgets are active then only one (the first that gets the interaction events)
# will be notified. In all the other segment editor widgets the local histogram will not be updated.
# The behavior could be made more deterministic by storing a preferred segment editor node in the selection node
# and not using the same segment editor node in multiple segment editor widgets.

masterImageData = self.scriptedEffect.sourceVolumeImageData()
if masterImageData is None:
return abortEvent
Expand Down

0 comments on commit 1a89f8e

Please sign in to comment.