diff options
Diffstat (limited to 'silx/gui/plot/MaskToolsWidget.py')
-rw-r--r-- | silx/gui/plot/MaskToolsWidget.py | 919 |
1 files changed, 0 insertions, 919 deletions
diff --git a/silx/gui/plot/MaskToolsWidget.py b/silx/gui/plot/MaskToolsWidget.py deleted file mode 100644 index 1ec1e7f..0000000 --- a/silx/gui/plot/MaskToolsWidget.py +++ /dev/null @@ -1,919 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""Widget providing a set of tools to draw masks on a PlotWidget. - -This widget is meant to work with :class:`silx.gui.plot.PlotWidget`. - -- :class:`ImageMask`: Handle mask bitmap update and history -- :class:`MaskToolsWidget`: GUI for :class:`Mask` -- :class:`MaskToolsDockWidget`: DockWidget to integrate in :class:`PlotWindow` -""" -from __future__ import division - -__authors__ = ["T. Vincent", "P. Knobel"] -__license__ = "MIT" -__date__ = "08/12/2020" - -import os -import sys -import numpy -import logging -import collections -import h5py - -from silx.image import shapes -from silx.io.utils import NEXUS_HDF5_EXT, is_dataset -from silx.gui.dialog.DatasetDialog import DatasetDialog - -from ._BaseMaskToolsWidget import BaseMask, BaseMaskToolsWidget, BaseMaskToolsDockWidget -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]) - - -def _selectDataset(filename, mode=DatasetDialog.SaveMode): - """Open a dialog to prompt the user to select a dataset in - a hdf5 file. - - :param str filename: name of an existing HDF5 file - :param mode: DatasetDialog.SaveMode or DatasetDialog.LoadMode - :rtype: str - :return: Name of selected dataset - """ - dialog = DatasetDialog() - dialog.addFile(filename) - dialog.setWindowTitle("Select a 2D dataset") - dialog.setMode(mode) - if not dialog.exec_(): - return None - return dialog.getSelectedDataUrl().data_path() - - -class ImageMask(BaseMask): - """A 2D mask field with update operations. - - Coords follows (row, column) convention and are in mask array coords. - - This is meant for internal use by :class:`MaskToolsWidget`. - """ - - def __init__(self, image=None): - """ - - :param image: :class:`silx.gui.plot.items.ImageBase` instance - """ - BaseMask.__init__(self, image) - self.reset(shape=(0, 0)) # Init the mask with a 2D shape - - def getDataValues(self): - """Return image data as a 2D or 3D array (if it is a RGBA image). - - :rtype: 2D or 3D numpy.ndarray - """ - return self._dataItem.getData(copy=False) - - def save(self, filename, kind): - """Save current mask in a file - - :param str filename: The file where to save to mask - :param str kind: The kind of file to save in 'edf', 'tif', 'npy', 'h5' - or 'msk' (if FabIO is installed) - :raise Exception: Raised if the file writing fail - """ - if kind == 'edf': - edfFile = EdfFile(filename, access="w+") - header = {"program_name": "silx-mask", "masked_value": "nonzero"} - edfFile.WriteImage(header, self.getMask(copy=False), Append=0) - - elif kind == 'tif': - tiffFile = TiffIO(filename, mode='w') - tiffFile.writeImage(self.getMask(copy=False), software='silx') - - elif kind == 'npy': - try: - numpy.save(filename, self.getMask(copy=False)) - except IOError: - raise RuntimeError("Mask file can't be written") - - elif ("." + kind) in NEXUS_HDF5_EXT: - self._saveToHdf5(filename, self.getMask(copy=False)) - - elif kind == 'msk': - try: - data = self.getMask(copy=False) - image = fabio.fabioimage.FabioImage(data=data) - image = image.convert(fabio.fit2dmaskimage.Fit2dMaskImage) - image.save(filename) - except Exception: - _logger.debug("Backtrace", exc_info=True) - raise RuntimeError("Mask file can't be written") - else: - raise ValueError("Format '%s' is not supported" % kind) - - @staticmethod - def _saveToHdf5(filename, mask): - """Save a mask array to a HDF5 file. - - :param str filename: name of an existing HDF5 file - :param numpy.ndarray mask: Mask array. - :returns: True if operation succeeded, False otherwise. - """ - if not os.path.exists(filename): - # create new file - with h5py.File(filename, "w") as _h5f: - pass - dataPath = _selectDataset(filename) - if dataPath is None: - return False - with h5py.File(filename, "a") as h5f: - existing_ds = h5f.get(dataPath) - if existing_ds is not None: - reply = qt.QMessageBox.question( - None, - "Confirm overwrite", - "Do you want to overwrite an existing dataset?", - qt.QMessageBox.Yes | qt.QMessageBox.No) - if reply != qt.QMessageBox.Yes: - return False - del h5f[dataPath] - try: - h5f.create_dataset(dataPath, data=mask) - except Exception: - return False - return True - - # Drawing operations - def updateRectangle(self, level, row, col, height, width, mask=True): - """Mask/Unmask a rectangle of the given mask level. - - :param int level: Mask level to update. - :param int row: Starting row of the rectangle - :param int col: Starting column of the rectangle - :param int height: - :param int width: - :param bool mask: True to mask (default), False to unmask. - """ - assert 0 < level < 256 - if row + height <= 0 or col + width <= 0: - return # Rectangle outside image, avoid negative indices - selection = self._mask[max(0, row):row + height + 1, - max(0, col):col + width + 1] - if mask: - selection[:,:] = level - else: - selection[selection == level] = 0 - self._notify() - - def updatePolygon(self, level, vertices, mask=True): - """Mask/Unmask a polygon of the given mask level. - - :param int level: Mask level to update. - :param vertices: Nx2 array of polygon corners as (row, col) - :param bool mask: True to mask (default), False to unmask. - """ - fill = shapes.polygon_fill_mask(vertices, self._mask.shape) - if mask: - self._mask[fill != 0] = level - else: - self._mask[numpy.logical_and(fill != 0, - self._mask == level)] = 0 - self._notify() - - def updatePoints(self, level, rows, cols, mask=True): - """Mask/Unmask points with given coordinates. - - :param int level: Mask level to update. - :param rows: Rows of selected points - :type rows: 1D numpy.ndarray - :param cols: Columns of selected points - :type cols: 1D numpy.ndarray - :param bool mask: True to mask (default), False to unmask. - """ - valid = numpy.logical_and( - numpy.logical_and(rows >= 0, cols >= 0), - numpy.logical_and(rows < self._mask.shape[0], - cols < self._mask.shape[1])) - rows, cols = rows[valid], cols[valid] - - if mask: - self._mask[rows, cols] = level - else: - inMask = self._mask[rows, cols] == level - self._mask[rows[inMask], cols[inMask]] = 0 - self._notify() - - def updateDisk(self, level, crow, ccol, radius, mask=True): - """Mask/Unmask a disk of the given mask level. - - :param int level: Mask level to update. - :param int crow: Disk center row. - :param int ccol: Disk center column. - :param float radius: Radius of the disk in mask array unit - :param bool mask: True to mask (default), False to unmask. - """ - rows, cols = shapes.circle_fill(crow, ccol, radius) - self.updatePoints(level, rows, cols, mask) - - def updateEllipse(self, level, crow, ccol, radius_r, radius_c, mask=True): - """Mask/Unmask an ellipse of the given mask level. - - :param int level: Mask level to update. - :param int crow: Row of the center of the ellipse - :param int ccol: Column of the center of the ellipse - :param float radius_r: Radius of the ellipse in the row - :param float radius_c: Radius of the ellipse in the column - :param bool mask: True to mask (default), False to unmask. - """ - rows, cols = shapes.ellipse_fill(crow, ccol, radius_r, radius_c) - self.updatePoints(level, rows, cols, mask) - - def updateLine(self, level, row0, col0, row1, col1, width, mask=True): - """Mask/Unmask a line of the given mask level. - - :param int level: Mask level to update. - :param int row0: Row of the starting point. - :param int col0: Column of the starting point. - :param int row1: Row of the end point. - :param int col1: Column of the end point. - :param int width: Width of the line in mask array unit. - :param bool mask: True to mask (default), False to unmask. - """ - rows, cols = shapes.draw_line(row0, col0, row1, col1, width) - self.updatePoints(level, rows, cols, mask) - - -class MaskToolsWidget(BaseMaskToolsWidget): - """Widget with tools for drawing mask on an image in a PlotWidget.""" - - _maxLevelNumber = 255 - - def __init__(self, parent=None, plot=None): - super(MaskToolsWidget, self).__init__(parent, plot, - mask=ImageMask()) - self._origin = (0., 0.) # Mask origin in plot - self._scale = (1., 1.) # Mask scale in plot - 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. - - :param numpy.ndarray mask: - The array to use for the mask or None to reset the mask. - :type mask: numpy.ndarray of uint8 of dimension 2, C-contiguous. - Array of other types are converted. - :param bool copy: True (the default) to copy the array, - False to use it as is if possible. - :return: None if failed, shape of mask as 2-tuple if successful. - The mask can be cropped or padded to fit active image, - the returned shape is that of the active image. - """ - if mask is None: - self.resetSelectionMask() - return self._data.shape[:2] - - mask = numpy.array(mask, copy=False, dtype=numpy.uint8) - if len(mask.shape) != 2: - _logger.error('Not an image, shape: %d', len(mask.shape)) - return None - - # Handle mask with single level - if self.multipleMasks() == 'single': - mask = numpy.array(mask != 0, dtype=numpy.uint8) - - # if mask has not changed, do nothing - if numpy.array_equal(mask, self.getSelectionMask()): - return mask.shape - - 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() - return mask.shape - else: - _logger.warning('Mask has not the same size as current image.' - ' Mask will be cropped or padded to fit image' - ' dimensions. %s != %s', - str(mask.shape), str(self._data.shape)) - resizedMask = numpy.zeros(self._data.shape[0:2], - 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] - self._mask.setMask(resizedMask, copy=False) - self._mask.commit() - return resizedMask.shape - - # Handle mask refresh on the plot - def _updatePlotMask(self): - """Update mask image in plot""" - mask = self.getSelectionMask(copy=False) - if mask is not None: - # get the mask from the plot - maskItem = self.plot.getImage(self._maskName) - mustBeAdded = maskItem is None - if mustBeAdded: - maskItem = items.MaskImageData() - maskItem.setName(self._maskName) - # update the items - maskItem.setData(mask, copy=False) - maskItem.setColormap(self._colormap) - maskItem.setOrigin(self._origin) - maskItem.setScale(self._scale) - maskItem.setZValue(self._z) - - if mustBeAdded: - self.plot.addItem(maskItem) - - elif self.plot.getImage(self._maskName): - self.plot.remove(self._maskName, kind='image') - - def showEvent(self, event): - try: - self.plot.sigActiveImageChanged.disconnect( - self._activeImageChangedAfterCare) - except (RuntimeError, TypeError): - pass - - # Sync with current active image - self._setMaskedImage(self.plot.getActiveImage()) - self.plot.sigActiveImageChanged.connect(self._activeImageChanged) - - def hideEvent(self, event): - try: - self.plot.sigActiveImageChanged.disconnect( - 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.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 - - :param image: :class:`.items.ImageBase` object to set color for. - """ - if isinstance(image, items.ColormapMixIn): - colormap = image.getColormap() - self._defaultOverlayColor = rgba( - cursorColorForColormap(colormap['name'])) - else: - self._defaultOverlayColor = rgba('black') - - def _activeImageChangedAfterCare(self, *args): - """Check synchro of active image and mask when mask widget is hidden. - - If active image has no more the same size as the mask, the mask is - removed, otherwise it is adjusted to origin, scale and z. - """ - activeImage = self.plot.getActiveImage() - if activeImage is None or activeImage.getName() == self._maskName: - # No active image or active image is the mask... - 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') - - self.plot.sigActiveImageChanged.disconnect( - self._activeImageChangedAfterCare) - else: - self._setOverlayColorForImage(activeImage) - self._setMaskColors(self.levelSpinBox.value(), - self.transparencySlider.value() / - self.transparencySlider.maximum()) - - self._origin = activeImage.getOrigin() - self._scale = activeImage.getScale() - self._z = activeImage.getZValue() + 1 - self._data = activeImage.getData(copy=False) - if self._data.shape[:2] != self._mask.getMask(copy=False).shape: - # Image has not the same size, remove mask and stop listening - if self.plot.getImage(self._maskName): - self.plot.remove(self._maskName, kind='image') - - self.plot.sigActiveImageChanged.disconnect( - self._activeImageChangedAfterCare) - else: - # Refresh in case origin, scale, z changed - self._mask.setDataItem(activeImage) - self._updatePlotMask() - - 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() - - 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 - - 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._setOverlayColorForImage(image) - - 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) - - self._updateInteractiveMode() - - # Handle whole mask operations - def load(self, filename): - """Load a mask from an image file. - - :param str filename: File name from which to load the mask - :raise Exception: An exception in case of failure - :raise RuntimeWarning: In case the mask was applied but with some - import changes to notice - """ - _, extension = os.path.splitext(filename) - extension = extension.lower()[1:] - - if extension == "npy": - try: - mask = numpy.load(filename) - except IOError: - _logger.error("Can't load filename '%s'", filename) - _logger.debug("Backtrace", exc_info=True) - raise RuntimeError('File "%s" is not a numpy file.', filename) - elif extension in ["tif", "tiff"]: - try: - image = TiffIO(filename, mode="r") - mask = image.getImage(0) - except Exception as e: - _logger.error("Can't load filename %s", filename) - _logger.debug("Backtrace", exc_info=True) - raise e - elif extension == "edf": - try: - mask = EdfFile(filename, access='r').GetData(0) - except Exception as e: - _logger.error("Can't load filename %s", filename) - _logger.debug("Backtrace", exc_info=True) - raise e - elif extension == "msk": - try: - mask = fabio.open(filename).data - except Exception as e: - _logger.error("Can't load fit2d mask file") - _logger.debug("Backtrace", exc_info=True) - raise e - elif ("." + extension) in NEXUS_HDF5_EXT: - mask = self._loadFromHdf5(filename) - if mask is None: - raise IOError("Could not load mask from HDF5 dataset") - else: - msg = "Extension '%s' is not supported." - raise RuntimeError(msg % extension) - - effectiveMaskShape = self.setSelectionMask(mask, copy=False) - if effectiveMaskShape is None: - return - if mask.shape != effectiveMaskShape: - msg = 'Mask was resized from %s to %s' - msg = msg % (str(mask.shape), str(effectiveMaskShape)) - raise RuntimeWarning(msg) - - def _loadMask(self): - """Open load mask dialog""" - dialog = qt.QFileDialog(self) - dialog.setWindowTitle("Load Mask") - dialog.setModal(1) - - extensions = collections.OrderedDict() - extensions["EDF files"] = "*.edf" - extensions["TIFF files"] = "*.tif *.tiff" - extensions["NumPy binary files"] = "*.npy" - extensions["HDF5 files"] = _HDF5_EXT_STR - # Fit2D mask is displayed anyway fabio is here or not - # to show to the user that the option exists - extensions["Fit2D mask files"] = "*.msk" - - filters = [] - filters.append("All supported files (%s)" % " ".join(extensions.values())) - for name, extension in extensions.items(): - filters.append("%s (%s)" % (name, extension)) - filters.append("All files (*)") - - dialog.setNameFilters(filters) - dialog.setFileMode(qt.QFileDialog.ExistingFile) - dialog.setDirectory(self.maskFileDir) - if not dialog.exec_(): - dialog.close() - return - - filename = dialog.selectedFiles()[0] - dialog.close() - - # Update the directory according to the user selection - self.maskFileDir = os.path.dirname(filename) - - try: - self.load(filename) - except RuntimeWarning as e: - message = e.args[0] - msg = qt.QMessageBox(self) - msg.setIcon(qt.QMessageBox.Warning) - msg.setText("Mask loaded but an operation was applied.\n" + message) - msg.exec_() - except Exception as e: - message = e.args[0] - msg = qt.QMessageBox(self) - msg.setIcon(qt.QMessageBox.Critical) - msg.setText("Cannot load mask from file. " + message) - msg.exec_() - - @staticmethod - def _loadFromHdf5(filename): - """Load a mask array from a HDF5 file. - - :param str filename: name of an existing HDF5 file - :returns: A mask as a numpy array, or None if the interactive dialog - was cancelled - """ - dataPath = _selectDataset(filename, mode=DatasetDialog.LoadMode) - if dataPath is None: - return None - - with h5py.File(filename, "r") as h5f: - dataset = h5f.get(dataPath) - if not is_dataset(dataset): - raise IOError("%s is not a dataset" % dataPath) - mask = dataset[()] - return mask - - def _saveMask(self): - """Open Save mask dialog""" - dialog = qt.QFileDialog(self) - dialog.setWindowTitle("Save Mask") - dialog.setOption(dialog.DontUseNativeDialog) - dialog.setModal(1) - hdf5Filter = 'HDF5 (%s)' % _HDF5_EXT_STR - filters = [ - 'EDF (*.edf)', - 'TIFF (*.tif)', - 'NumPy binary file (*.npy)', - hdf5Filter, - # Fit2D mask is displayed anyway fabio is here or not - # to show to the user that the option exists - 'Fit2D mask (*.msk)', - ] - dialog.setNameFilters(filters) - dialog.setFileMode(qt.QFileDialog.AnyFile) - dialog.setAcceptMode(qt.QFileDialog.AcceptSave) - dialog.setDirectory(self.maskFileDir) - - def onFilterSelection(filt_): - # disable overwrite confirmation for HDF5, - # because we append the data to existing files - if filt_ == hdf5Filter: - dialog.setOption(dialog.DontConfirmOverwrite) - else: - dialog.setOption(dialog.DontConfirmOverwrite, False) - - dialog.filterSelected.connect(onFilterSelection) - if not dialog.exec_(): - dialog.close() - return - - nameFilter = dialog.selectedNameFilter() - filename = dialog.selectedFiles()[0] - dialog.close() - - if "HDF5" in nameFilter: - has_allowed_ext = False - for ext in NEXUS_HDF5_EXT: - if (len(filename) > len(ext) and - filename[-len(ext):].lower() == ext.lower()): - has_allowed_ext = True - extension = ext - if not has_allowed_ext: - extension = ".h5" - filename += ".h5" - else: - # convert filter name to extension name with the . - extension = nameFilter.split()[-1][2:-1] - if not filename.lower().endswith(extension): - filename += extension - - if os.path.exists(filename) and "HDF5" not in nameFilter: - try: - os.remove(filename) - except IOError as e: - msg = qt.QMessageBox(self) - msg.setWindowTitle("Removing existing file") - msg.setIcon(qt.QMessageBox.Critical) - - if hasattr(e, "strerror"): - strerror = e.strerror - else: - strerror = sys.exc_info()[1] - msg.setText("Cannot save.\n" - "Input Output Error: %s" % strerror) - msg.exec_() - return - - # Update the directory according to the user selection - self.maskFileDir = os.path.dirname(filename) - - try: - self.save(filename, extension[1:]) - except Exception as e: - msg = qt.QMessageBox(self) - msg.setWindowTitle("Saving mask file") - msg.setIcon(qt.QMessageBox.Critical) - - if hasattr(e, "strerror"): - strerror = e.strerror - else: - strerror = sys.exc_info()[1] - msg.setText("Cannot save file %s\n%s" % (filename, strerror)) - msg.exec_() - - def resetSelectionMask(self): - """Reset the mask""" - self._mask.reset(shape=self._data.shape[:2]) - self._mask.commit() - - def _plotDrawEvent(self, event): - """Handle draw events from the plot""" - if (self._drawingMode is None or - event['event'] not in ('drawingProgress', 'drawingFinished')): - return - - if not len(self._data): - return - - level = self.levelSpinBox.value() - - if self._drawingMode == 'rectangle': - if event['event'] == 'drawingFinished': - # Convert from plot to array coords - doMask = self._isMasking() - ox, oy = self._origin - sx, sy = self._scale - - height = int(abs(event['height'] / sy)) - width = int(abs(event['width'] / sx)) - - row = int((event['y'] - oy) / sy) - if sy < 0: - row -= height - - col = int((event['x'] - ox) / sx) - if sx < 0: - col -= width - - self._mask.updateRectangle( - level, - row=row, - col=col, - height=height, - width=width, - mask=doMask) - self._mask.commit() - - elif self._drawingMode == 'ellipse': - if event['event'] == 'drawingFinished': - doMask = self._isMasking() - # Convert from plot to array coords - center = (event['points'][0] - self._origin) / self._scale - size = event['points'][1] / self._scale - center = center.astype(numpy.int64) # (row, col) - self._mask.updateEllipse(level, center[1], center[0], size[1], size[0], doMask) - self._mask.commit() - - elif self._drawingMode == 'polygon': - if event['event'] == 'drawingFinished': - doMask = self._isMasking() - # Convert from plot to array coords - vertices = (event['points'] - self._origin) / self._scale - vertices = vertices.astype(numpy.int64)[:, (1, 0)] # (row, col) - self._mask.updatePolygon(level, vertices, doMask) - self._mask.commit() - - elif self._drawingMode == 'pencil': - doMask = self._isMasking() - # convert from plot to array coords - col, row = (event['points'][-1] - self._origin) / self._scale - col, row = int(col), int(row) - brushSize = self._getPencilWidth() - - if self._lastPencilPos != (row, col): - if self._lastPencilPos is not None: - # Draw the line - self._mask.updateLine( - level, - self._lastPencilPos[0], self._lastPencilPos[1], - row, col, - brushSize, - doMask) - - # Draw the very first, or last point - self._mask.updateDisk(level, row, col, brushSize / 2., doMask) - - if event['event'] == 'drawingFinished': - self._mask.commit() - self._lastPencilPos = None - else: - self._lastPencilPos = row, col - else: - _logger.error("Drawing mode %s unsupported", self._drawingMode) - - def _loadRangeFromColormapTriggered(self): - """Set range from active image colormap range""" - activeImage = self.plot.getActiveImage() - if (isinstance(activeImage, items.ColormapMixIn) and - activeImage.getName() != self._maskName): - # Update thresholds according to colormap - colormap = activeImage.getColormap() - if colormap['autoscale']: - min_ = numpy.nanmin(activeImage.getData(copy=False)) - max_ = numpy.nanmax(activeImage.getData(copy=False)) - else: - min_, max_ = colormap['vmin'], colormap['vmax'] - self.minLineEdit.setText(str(min_)) - self.maxLineEdit.setText(str(max_)) - - -class MaskToolsDockWidget(BaseMaskToolsDockWidget): - """:class:`MaskToolsWidget` embedded in a QDockWidget. - - For integration in a :class:`PlotWindow`. - - :param parent: See :class:`QDockWidget` - :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) |