summaryrefslogtreecommitdiff
path: root/silx/gui/plot/MaskToolsWidget.py
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot/MaskToolsWidget.py')
-rw-r--r--silx/gui/plot/MaskToolsWidget.py187
1 files changed, 146 insertions, 41 deletions
diff --git a/silx/gui/plot/MaskToolsWidget.py b/silx/gui/plot/MaskToolsWidget.py
index 8ff8641..1ec1e7f 100644
--- a/silx/gui/plot/MaskToolsWidget.py
+++ b/silx/gui/plot/MaskToolsWidget.py
@@ -32,11 +32,9 @@ This widget is meant to work with :class:`silx.gui.plot.PlotWidget`.
"""
from __future__ import division
-
__authors__ = ["T. Vincent", "P. Knobel"]
__license__ = "MIT"
-__date__ = "15/02/2019"
-
+__date__ = "08/12/2020"
import os
import sys
@@ -53,16 +51,15 @@ from ._BaseMaskToolsWidget import BaseMask, BaseMaskToolsWidget, BaseMaskToolsDo
from . import items
from ..colors import cursorColorForColormap, rgba
from .. import qt
+from ..utils import LockReentrant
from silx.third_party.EdfFile import EdfFile
from silx.third_party.TiffIO import TiffIO
import fabio
-
_logger = logging.getLogger(__name__)
-
_HDF5_EXT_STR = ' '.join(['*' + ext for ext in NEXUS_HDF5_EXT])
@@ -91,6 +88,7 @@ class ImageMask(BaseMask):
This is meant for internal use by :class:`MaskToolsWidget`.
"""
+
def __init__(self, image=None):
"""
@@ -193,7 +191,7 @@ class ImageMask(BaseMask):
selection = self._mask[max(0, row):row + height + 1,
max(0, col):col + width + 1]
if mask:
- selection[:, :] = level
+ selection[:,:] = level
else:
selection[selection == level] = 0
self._notify()
@@ -289,6 +287,38 @@ class MaskToolsWidget(BaseMaskToolsWidget):
self._z = 1 # Mask layer in plot
self._data = numpy.zeros((0, 0), dtype=numpy.uint8) # Store image
+ self.__itemMaskUpdatedLock = LockReentrant()
+ self.__itemMaskUpdated = False
+
+ def __maskStateChanged(self) -> None:
+ """Handle mask commit to update item mask"""
+ item = self._mask.getDataItem()
+ if item is not None:
+ with self.__itemMaskUpdatedLock:
+ item.setMaskData(self._mask.getMask(copy=True), copy=False)
+
+ def setItemMaskUpdated(self, enabled: bool) -> None:
+ """Toggle item mask and mask tool synchronisation.
+
+ :param bool enabled: True to synchronise. Default: False
+ """
+ enabled = bool(enabled)
+ if enabled != self.__itemMaskUpdated:
+ if self.__itemMaskUpdated:
+ self._mask.sigStateChanged.disconnect(self.__maskStateChanged)
+ self.__itemMaskUpdated = enabled
+ if self.__itemMaskUpdated:
+ # Synchronize item and tool mask
+ self._setMaskedImage(self._mask.getDataItem())
+ self._mask.sigStateChanged.connect(self.__maskStateChanged)
+
+ def isItemMaskUpdated(self) -> bool:
+ """Returns whether or not item and mask tool masks are synchronised.
+
+ :rtype: bool
+ """
+ return self.__itemMaskUpdated
+
def setSelectionMask(self, mask, copy=True):
"""Set the mask to a new array.
@@ -319,13 +349,6 @@ class MaskToolsWidget(BaseMaskToolsWidget):
if numpy.array_equal(mask, self.getSelectionMask()):
return mask.shape
- # ensure all mask attributes are synchronized with the active image
- # and connect listener
- activeImage = self.plot.getActiveImage()
- if activeImage is not None and activeImage.getName() != self._maskName:
- self._activeImageChanged()
- self.plot.sigActiveImageChanged.connect(self._activeImageChanged)
-
if self._data.shape[0:2] == (0, 0) or mask.shape == self._data.shape[0:2]:
self._mask.setMask(mask, copy=copy)
self._mask.commit()
@@ -339,7 +362,7 @@ class MaskToolsWidget(BaseMaskToolsWidget):
dtype=numpy.uint8)
height = min(self._data.shape[0], mask.shape[0])
width = min(self._data.shape[1], mask.shape[1])
- resizedMask[:height, :width] = mask[:height, :width]
+ resizedMask[:height,:width] = mask[:height,:width]
self._mask.setMask(resizedMask, copy=False)
self._mask.commit()
return resizedMask.shape
@@ -374,7 +397,9 @@ class MaskToolsWidget(BaseMaskToolsWidget):
self._activeImageChangedAfterCare)
except (RuntimeError, TypeError):
pass
- self._activeImageChanged() # Init mask + enable/disable widget
+
+ # Sync with current active image
+ self._setMaskedImage(self.plot.getActiveImage())
self.plot.sigActiveImageChanged.connect(self._activeImageChanged)
def hideEvent(self, event):
@@ -383,14 +408,41 @@ class MaskToolsWidget(BaseMaskToolsWidget):
self._activeImageChanged)
except (RuntimeError, TypeError):
pass
+
+ image = self.getMaskedItem()
+ if image is not None:
+ try:
+ image.sigItemChanged.disconnect(self.__imageChanged)
+ except (RuntimeError, TypeError):
+ pass # TODO should not happen
+
if self.isMaskInteractionActivated():
# Disable drawing tool
self.browseAction.trigger()
- if self.getSelectionMask(copy=False) is not None:
+ if self.isItemMaskUpdated(): # No "after-care"
+ self._data = numpy.zeros((0, 0), dtype=numpy.uint8)
+ self._mask.setDataItem(None)
+ self._mask.reset()
+
+ if self.plot.getImage(self._maskName):
+ self.plot.remove(self._maskName, kind='image')
+
+ elif self.getSelectionMask(copy=False) is not None:
self.plot.sigActiveImageChanged.connect(
self._activeImageChangedAfterCare)
+ def _activeImageChanged(self, previous, current):
+ """Reacts upon active image change.
+
+ Only handle change of active image items here.
+ """
+ if previous != current:
+ image = self.plot.getActiveImage()
+ if image is not None and image.getName() == self._maskName:
+ image = None # Active image is the mask
+ self._setMaskedImage(image)
+
def _setOverlayColorForImage(self, image):
"""Set the color of overlay adapted to image
@@ -443,41 +495,93 @@ class MaskToolsWidget(BaseMaskToolsWidget):
self._mask.setDataItem(activeImage)
self._updatePlotMask()
- def _activeImageChanged(self, *args):
- """Update widget and mask according to active image changes"""
- activeImage = self.plot.getActiveImage()
- if (activeImage is None or activeImage.getName() == self._maskName or
- activeImage.getData(copy=False).size == 0):
- # No active image or active image is the mask or image has no data...
+ def _setMaskedImage(self, image):
+ """Change the image that is used a reference to author the mask"""
+ previous = self.getMaskedItem()
+ if previous is not None and self.isVisible():
+ # Disconnect from previous image
+ try:
+ previous.sigItemChanged.disconnect(self.__imageChanged)
+ except TypeError:
+ pass # TODO fixme should not happen
+
+ # Set the image
+ self._mask.setDataItem(image)
+
+ if image is None: # No image, disable mask
self.setEnabled(False)
self._data = numpy.zeros((0, 0), dtype=numpy.uint8)
self._mask.reset()
self._mask.commit()
- else: # There is an active image
- self.setEnabled(True)
+ self._updateInteractiveMode()
+
+ else: # Update and connect to image's sigItemChanged
+ if self.isItemMaskUpdated():
+ if image.getMaskData(copy=False) is None:
+ # Image item has no mask: use current mask from the tool
+ image.setMaskData(
+ self.getSelectionMask(copy=False), copy=True)
+ else: # Image item has a mask: set it in tool
+ self.setSelectionMask(
+ image.getMaskData(copy=False), copy=True)
+ self._mask.resetHistory()
+ self.__imageUpdated()
+ if self.isVisible():
+ image.sigItemChanged.connect(self.__imageChanged)
+
+ def __imageChanged(self, event):
+ """Reacts upon image item changes"""
+ image = self._mask.getDataItem()
+ if image is None:
+ _logger.error("Mask is not attached to an image")
+ return
- self._setOverlayColorForImage(activeImage)
+ if event in (items.ItemChangedType.COLORMAP,
+ items.ItemChangedType.DATA,
+ items.ItemChangedType.POSITION,
+ items.ItemChangedType.SCALE,
+ items.ItemChangedType.VISIBLE,
+ items.ItemChangedType.ZVALUE):
+ self.__imageUpdated()
+
+ elif (event == items.ItemChangedType.MASK and
+ self.isItemMaskUpdated() and
+ not self.__itemMaskUpdatedLock.locked()):
+ # Update mask from the image item unless mask tool is updating it
+ self.setSelectionMask(image.getMaskData(copy=False), copy=True)
+
+ def __imageUpdated(self):
+ """Synchronize mask with current state of the image"""
+ image = self._mask.getDataItem()
+ if image is None:
+ _logger.error("No active image while expecting one")
+ return
- self._setMaskColors(self.levelSpinBox.value(),
- self.transparencySlider.value() /
- self.transparencySlider.maximum())
+ self._setOverlayColorForImage(image)
- self._origin = activeImage.getOrigin()
- self._scale = activeImage.getScale()
- self._z = activeImage.getZValue() + 1
- self._data = activeImage.getData(copy=False)
- self._mask.setDataItem(activeImage)
- if self._data.shape[:2] != self._mask.getMask(copy=False).shape:
- self._mask.reset(self._data.shape[:2])
- self._mask.commit()
- else:
- # Refresh in case origin, scale, z changed
- self._updatePlotMask()
+ self._setMaskColors(self.levelSpinBox.value(),
+ self.transparencySlider.value() /
+ self.transparencySlider.maximum())
+
+ self._origin = image.getOrigin()
+ self._scale = image.getScale()
+ self._z = image.getZValue() + 1
+ self._data = image.getData(copy=False)
+ self._mask.setDataItem(image)
+ if self._data.shape[:2] != self._mask.getMask(copy=False).shape:
+ self._mask.reset(self._data.shape[:2])
+ self._mask.commit()
+ else:
+ # Refresh in case origin, scale, z changed
+ self._updatePlotMask()
+
+ # Visible and with data
+ self.setEnabled(image.isVisible() and self._data.size != 0)
- # Threshold tools only available for data with colormap
- self.thresholdGroup.setEnabled(self._data.ndim == 2)
+ # Threshold tools only available for data with colormap
+ self.thresholdGroup.setEnabled(self._data.ndim == 2)
self._updateInteractiveMode()
@@ -809,6 +913,7 @@ class MaskToolsDockWidget(BaseMaskToolsDockWidget):
:param plot: The PlotWidget this widget is operating on
:paran str name: The title of this widget
"""
+
def __init__(self, parent=None, plot=None, name='Mask'):
widget = MaskToolsWidget(plot=plot)
super(MaskToolsDockWidget, self).__init__(parent, name, widget)