Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BUG: Fix segment disappearing when sharing segment editor node between widgets #7577

Merged
merged 1 commit into from Feb 9, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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