From 5d052011e7738c0ff7c257532a2c76538beed994 Mon Sep 17 00:00:00 2001 From: Andras Lasso Date: Tue, 31 Oct 2023 11:11:12 -0400 Subject: [PATCH] ENH: Make Grow from seeds effect input requirements more clear Grow from seeds effect requires at least 2 visible segments as input if no editable region is specified, and at least 1 visible segment if editable region is specified. When the requirement was not fulfilled then the user did not know what was wrong, because there was no visible notification on the GUI (only in the application log), see for example https://discourse.slicer.org/t/grow-from-seeds-does-not-work-if-painting-only-one-segment/32452/6 This commit adds display of the error in a popup window if there are not enough input segments. A notification on the status bar is added if segmentation is canceled due to removal of an input segment. --- ...ScriptedSegmentEditorAutoCompleteEffect.py | 43 ++++++++++++++----- .../SegmentEditorGrowFromSeedsEffect.py | 1 + 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorEffects/AbstractScriptedSegmentEditorAutoCompleteEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorEffects/AbstractScriptedSegmentEditorAutoCompleteEffect.py index a8e3fc31974..278b488c3c9 100644 --- a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorEffects/AbstractScriptedSegmentEditorAutoCompleteEffect.py +++ b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorEffects/AbstractScriptedSegmentEditorAutoCompleteEffect.py @@ -31,7 +31,10 @@ def __init__(self, scriptedEffect): scriptedEffect.perSegment = False AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect) + # Number of segments required when editable area is not specified self.minimumNumberOfSegments = 1 + # Number of segments required when editable area is specified + self.minimumNumberOfSegmentsWithEditableArea = 1 self.clippedMasterImageDataRequired = False self.clippedMaskImageDataRequired = False @@ -168,8 +171,9 @@ def onSegmentationModified(self, caller, event): segment = segmentation.GetSegment(segmentID) if not segment: # selected segment was deleted, cancel segmentation - logging.debug("Segmentation cancelled because an input segment was deleted") + logging.debug("Segmentation operation is cancelled because an input segment was deleted") self.onCancel() + slicer.util.showStatusMessage(_("Segmentation operation is cancelled because an input segment was deleted."), 3000) return segmentLabelmap = segment.GetRepresentation(vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName()) if segmentID in self.selectedSegmentModifiedTimes \ @@ -228,7 +232,9 @@ def observeSegmentation(self, observationEnabled): def getPreviewNode(self): previewNode = self.scriptedEffect.parameterSetNode().GetNodeReference(ResultPreviewNodeReferenceRole) - if previewNode and self.scriptedEffect.parameter("SegmentationResultPreviewOwnerEffect") != self.scriptedEffect.name: + if (previewNode + and self.scriptedEffect.parameterDefined("SegmentationResultPreviewOwnerEffect") + and self.scriptedEffect.parameter("SegmentationResultPreviewOwnerEffect") != self.scriptedEffect.name): # another effect owns this preview node return None return previewNode @@ -279,13 +285,10 @@ def onPreview(self): slicer.util.showStatusMessage(_("Running {effectName} auto-complete...").format(effectName=self.scriptedEffect.name), 2000) try: - # This can be a long operation - indicate it to the user - qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) - self.preview() + with slicer.util.tryWithErrorDisplay(_("Segmentation operation failed:"), waitCursor=True): + self.preview() finally: - qt.QApplication.restoreOverrideCursor() - - self.previewComputationInProgress = False + self.previewComputationInProgress = False def reset(self): self.delayedAutoUpdateTimer.stop() @@ -436,10 +439,28 @@ def preview(self): if self.selectedSegmentIds is None: self.selectedSegmentIds = vtk.vtkStringArray() segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(self.selectedSegmentIds) - if self.selectedSegmentIds.GetNumberOfValues() < self.minimumNumberOfSegments: - logging.error(f"Auto-complete operation skipped: at least {self.minimumNumberOfSegments} visible segments are required") + + if self.minimumNumberOfSegments != self.minimumNumberOfSegmentsWithEditableArea: + editableAreaSpecified = ( + self.scriptedEffect.parameterSetNode().GetSourceVolumeIntensityMask() + or self.scriptedEffect.parameterSetNode().GetMaskMode() != slicer.vtkMRMLSegmentationNode.EditAllowedEverywhere) + if editableAreaSpecified and self.selectedSegmentIds.GetNumberOfValues() < self.minimumNumberOfSegmentsWithEditableArea: + logging.error(f"Auto-complete operation failed: at least {self.minimumNumberOfSegmentsWithEditableArea} visible segments are required when editable area is defined") + raise RuntimeError( + _("Minimum {minimumNumberOfSegments} visible segments are required.").format( + minimumNumberOfSegments=self.minimumNumberOfSegmentsWithEditableArea)) + elif (not editableAreaSpecified) and self.selectedSegmentIds.GetNumberOfValues() < self.minimumNumberOfSegments: + logging.error(f"Auto-complete operation skipped: at least {self.minimumNumberOfSegmentsWithEditableArea} visible segments or setting of editable area is required") + raise RuntimeError( + _("Minimum {minimumNumberOfSegments} visible segments (or specification of editable area or intensity range) is required.").format( + minimumNumberOfSegments=self.minimumNumberOfSegments)) + elif self.selectedSegmentIds.GetNumberOfValues() < self.minimumNumberOfSegments: + # Same number of input segments required regardless of editable area + logging.error(f"Auto-complete operation failed: at least {self.minimumNumberOfSegments} visible segments are required") self.selectedSegmentIds = None - return + raise RuntimeError( + _("Minimum {minimumNumberOfSegments} visible segments are required.").format( + minimumNumberOfSegments=self.minimumNumberOfSegments)) # Compute merged labelmap extent (effective extent slightly expanded) if not self.mergedLabelmapGeometryImage: diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorEffects/SegmentEditorGrowFromSeedsEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorEffects/SegmentEditorGrowFromSeedsEffect.py index e2ec5382250..fbd06e09287 100644 --- a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorEffects/SegmentEditorGrowFromSeedsEffect.py +++ b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorEffects/SegmentEditorGrowFromSeedsEffect.py @@ -22,6 +22,7 @@ def __init__(self, scriptedEffect): scriptedEffect.name = 'Grow from seeds' # no tr (don't translate it because modules find effects by name) scriptedEffect.title = _('Grow from seeds') self.minimumNumberOfSegments = 2 + self.minimumNumberOfSegmentsWithEditableArea = 1 # if mask is specified then one input segment is sufficient self.clippedMasterImageDataRequired = True # source volume intensities are used by this effect self.clippedMaskImageDataRequired = True # masking is used self.growCutFilter = None