diff options
Diffstat (limited to 'silx/gui/plot/tools/profile')
-rw-r--r-- | silx/gui/plot/tools/profile/ScatterProfileToolBar.py | 54 | ||||
-rw-r--r-- | silx/gui/plot/tools/profile/__init__.py | 38 | ||||
-rw-r--r-- | silx/gui/plot/tools/profile/core.py | 525 | ||||
-rw-r--r-- | silx/gui/plot/tools/profile/editors.py | 307 | ||||
-rw-r--r-- | silx/gui/plot/tools/profile/manager.py | 1076 | ||||
-rw-r--r-- | silx/gui/plot/tools/profile/rois.py | 1156 | ||||
-rw-r--r-- | silx/gui/plot/tools/profile/toolbar.py | 172 |
7 files changed, 0 insertions, 3328 deletions
diff --git a/silx/gui/plot/tools/profile/ScatterProfileToolBar.py b/silx/gui/plot/tools/profile/ScatterProfileToolBar.py deleted file mode 100644 index 44187ef..0000000 --- a/silx/gui/plot/tools/profile/ScatterProfileToolBar.py +++ /dev/null @@ -1,54 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018-2019 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. -# -# ###########################################################################*/ -"""This module profile tools for scatter plots. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "28/06/2018" - - -from silx.utils import deprecation -from . import toolbar - - -class ScatterProfileToolBar(toolbar.ProfileToolBar): - """QToolBar providing scatter plot profiling tools - - :param parent: See :class:`QToolBar`. - :param plot: :class:`~silx.gui.plot.PlotWidget` on which to operate. - :param str title: See :class:`QToolBar`. - """ - - def __init__(self, parent=None, plot=None, title=None): - super(ScatterProfileToolBar, self).__init__(parent, plot) - if title is not None: - deprecation.deprecated_warning("Attribute", - name="title", - reason="removed", - since_version="0.13.0", - only_once=True, - skip_backtrace_count=1) - self.setScheme("scatter") diff --git a/silx/gui/plot/tools/profile/__init__.py b/silx/gui/plot/tools/profile/__init__.py deleted file mode 100644 index d91191e..0000000 --- a/silx/gui/plot/tools/profile/__init__.py +++ /dev/null @@ -1,38 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018 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. -# -# ###########################################################################*/ -"""This module provides tools to get profiles on plot data. - -It provides: - -- :class:`ScatterProfileToolBar`: a QToolBar to handle profile on scatter data - -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "07/06/2018" - - -from .ScatterProfileToolBar import ScatterProfileToolBar # noqa diff --git a/silx/gui/plot/tools/profile/core.py b/silx/gui/plot/tools/profile/core.py deleted file mode 100644 index 200f5cf..0000000 --- a/silx/gui/plot/tools/profile/core.py +++ /dev/null @@ -1,525 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018-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. -# -# ###########################################################################*/ -"""This module define core objects for profile tools. -""" - -__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel", "H. Payno", "V. Valls"] -__license__ = "MIT" -__date__ = "17/04/2020" - -import collections -import numpy -import weakref - -from silx.image.bilinear import BilinearImage -from silx.gui import qt - - -CurveProfileData = collections.namedtuple( - 'CurveProfileData', [ - "coords", - "profile", - "title", - "xLabel", - "yLabel", - ]) - -RgbaProfileData = collections.namedtuple( - 'RgbaProfileData', [ - "coords", - "profile", - "profile_r", - "profile_g", - "profile_b", - "profile_a", - "title", - "xLabel", - "yLabel", - ]) - -ImageProfileData = collections.namedtuple( - 'ImageProfileData', [ - 'coords', - 'profile', - 'title', - 'xLabel', - 'yLabel', - 'colormap', - ]) - - -class ProfileRoiMixIn: - """Base mix-in for ROI which can be used to select a profile. - - This mix-in have to be applied to a :class:`~silx.gui.plot.items.roi.RegionOfInterest` - in order to be usable by a :class:`~silx.gui.plot.tools.profile.manager.ProfileManager`. - """ - - ITEM_KIND = None - """Define the plot item which can be used with this profile ROI""" - - sigProfilePropertyChanged = qt.Signal() - """Emitted when a property of this profile have changed""" - - sigPlotItemChanged = qt.Signal() - """Emitted when the plot item linked to this profile have changed""" - - def __init__(self, parent=None): - self.__profileWindow = None - self.__profileManager = None - self.__plotItem = None - self.setName("Profile") - self.setEditable(True) - self.setSelectable(True) - - def invalidateProfile(self): - """Must be called by the implementation when the profile have to be - recomputed.""" - profileManager = self.getProfileManager() - if profileManager is not None: - profileManager.requestUpdateProfile(self) - - def invalidateProperties(self): - """Must be called when a property of the profile have changed.""" - self.sigProfilePropertyChanged.emit() - - def _setPlotItem(self, plotItem): - """Specify the plot item to use with this profile - - :param `~silx.gui.plot.items.item.Item` plotItem: A plot item - """ - previousPlotItem = self.getPlotItem() - if previousPlotItem is plotItem: - return - self.__plotItem = weakref.ref(plotItem) - self.sigPlotItemChanged.emit() - - def getPlotItem(self): - """Returns the plot item used by this profile - - :rtype: `~silx.gui.plot.items.item.Item` - """ - if self.__plotItem is None: - return None - plotItem = self.__plotItem() - if plotItem is None: - self.__plotItem = None - return plotItem - - def _setProfileManager(self, profileManager): - self.__profileManager = profileManager - - def getProfileManager(self): - """ - Returns the profile manager connected to this ROI. - - :rtype: ~silx.gui.plot.tools.profile.manager.ProfileManager - """ - return self.__profileManager - - def getProfileWindow(self): - """ - Returns the windows associated to this ROI, else None. - - :rtype: ProfileWindow - """ - return self.__profileWindow - - def setProfileWindow(self, profileWindow): - """ - Associate a window to this ROI. Can be None. - - :param ProfileWindow profileWindow: A main window - to display the profile. - """ - if profileWindow is self.__profileWindow: - return - if self.__profileWindow is not None: - self.__profileWindow.sigClose.disconnect(self.__profileWindowAboutToClose) - self.__profileWindow.setRoiProfile(None) - self.__profileWindow = profileWindow - if self.__profileWindow is not None: - self.__profileWindow.sigClose.connect(self.__profileWindowAboutToClose) - self.__profileWindow.setRoiProfile(self) - - def __profileWindowAboutToClose(self): - profileManager = self.getProfileManager() - roiManager = profileManager.getRoiManager() - try: - roiManager.removeRoi(self) - except ValueError: - pass - - def computeProfile(self, item): - """ - Compute the profile which will be displayed. - - This method is not called from the main Qt thread, but from a thread - pool. - - :param ~silx.gui.plot.items.Item item: A plot item - :rtype: Union[CurveProfileData,ImageProfileData] - """ - raise NotImplementedError() - - -def _alignedFullProfile(data, origin, scale, position, roiWidth, axis, method): - """Get a profile along one axis on a stack of images - - :param numpy.ndarray data: 3D volume (stack of 2D images) - The first dimension is the image index. - :param origin: Origin of image in plot (ox, oy) - :param scale: Scale of image in plot (sx, sy) - :param float position: Position of profile line in plot coords - on the axis orthogonal to the profile direction. - :param int roiWidth: Width of the profile in image pixels. - :param int axis: 0 for horizontal profile, 1 for vertical. - :param str method: method to compute the profile. Can be 'mean' or 'sum' or - 'none' - :return: profile image + effective ROI area corners in plot coords - """ - assert axis in (0, 1) - assert len(data.shape) == 3 - assert method in ('mean', 'sum', 'none') - - # Convert from plot to image coords - imgPos = int((position - origin[1 - axis]) / scale[1 - axis]) - - if axis == 1: # Vertical profile - # Transpose image to always do a horizontal profile - data = numpy.transpose(data, (0, 2, 1)) - - nimages, height, width = data.shape - - roiWidth = min(height, roiWidth) # Clip roi width to image size - - # Get [start, end[ coords of the roi in the data - start = int(int(imgPos) + 0.5 - roiWidth / 2.) - start = min(max(0, start), height - roiWidth) - end = start + roiWidth - - if method == 'none': - profile = None - else: - if start < height and end > 0: - if method == 'mean': - fct = numpy.mean - elif method == 'sum': - fct = numpy.sum - else: - raise ValueError('method not managed') - profile = fct(data[:, max(0, start):min(end, height), :], axis=1).astype(numpy.float32) - else: - profile = numpy.zeros((nimages, width), dtype=numpy.float32) - - # Compute effective ROI in plot coords - profileBounds = numpy.array( - (0, width, width, 0), - dtype=numpy.float32) * scale[axis] + origin[axis] - roiBounds = numpy.array( - (start, start, end, end), - dtype=numpy.float32) * scale[1 - axis] + origin[1 - axis] - - if axis == 0: # Horizontal profile - area = profileBounds, roiBounds - else: # vertical profile - area = roiBounds, profileBounds - - return profile, area - - -def _alignedPartialProfile(data, rowRange, colRange, axis, method): - """Mean of a rectangular region (ROI) of a stack of images - along a given axis. - - Returned values and all parameters are in image coordinates. - - :param numpy.ndarray data: 3D volume (stack of 2D images) - The first dimension is the image index. - :param rowRange: [min, max[ of ROI rows (upper bound excluded). - :type rowRange: 2-tuple of int (min, max) with min < max - :param colRange: [min, max[ of ROI columns (upper bound excluded). - :type colRange: 2-tuple of int (min, max) with min < max - :param int axis: The axis along which to take the profile of the ROI. - 0: Sum rows along columns. - 1: Sum columns along rows. - :param str method: method to compute the profile. Can be 'mean' or 'sum' - :return: Profile image along the ROI as the mean of the intersection - of the ROI and the image. - """ - assert axis in (0, 1) - assert len(data.shape) == 3 - assert rowRange[0] < rowRange[1] - assert colRange[0] < colRange[1] - assert method in ('mean', 'sum') - - nimages, height, width = data.shape - - # Range aligned with the integration direction - profileRange = colRange if axis == 0 else rowRange - - profileLength = abs(profileRange[1] - profileRange[0]) - - # Subset of the image to use as intersection of ROI and image - rowStart = min(max(0, rowRange[0]), height) - rowEnd = min(max(0, rowRange[1]), height) - colStart = min(max(0, colRange[0]), width) - colEnd = min(max(0, colRange[1]), width) - - if method == 'mean': - _fct = numpy.mean - elif method == 'sum': - _fct = numpy.sum - else: - raise ValueError('method not managed') - - imgProfile = _fct(data[:, rowStart:rowEnd, colStart:colEnd], axis=axis + 1, - dtype=numpy.float32) - - # Profile including out of bound area - profile = numpy.zeros((nimages, profileLength), dtype=numpy.float32) - - # Place imgProfile in full profile - offset = - min(0, profileRange[0]) - profile[:, offset:offset + imgProfile.shape[1]] = imgProfile - - return profile - - -def createProfile(roiInfo, currentData, origin, scale, lineWidth, method): - """Create the profile line for the the given image. - - :param roiInfo: information about the ROI: start point, end point and - type ("X", "Y", "D") - :param numpy.ndarray currentData: the 2D image or the 3D stack of images - on which we compute the profile. - :param origin: (ox, oy) the offset from origin - :type origin: 2-tuple of float - :param scale: (sx, sy) the scale to use - :type scale: 2-tuple of float - :param int lineWidth: width of the profile line - :param str method: method to compute the profile. Can be 'mean' or 'sum' - or 'none': to compute everything except the profile - :return: `coords, profile, area, profileName, xLabel`, where: - - coords is the X coordinate to use to display the profile - - profile is a 2D array of the profiles of the stack of images. - For a single image, the profile is a curve, so this parameter - has a shape *(1, len(curve))* - - area is a tuple of two 1D arrays with 4 values each. They represent - the effective ROI area corners in plot coords. - - profileName is a string describing the ROI, meant to be used as - title of the profile plot - - xLabel the label for X in the profile window - - :rtype: tuple(ndarray,ndarray,(ndarray,ndarray),str) - """ - if currentData is None or roiInfo is None or lineWidth is None: - raise ValueError("createProfile called with invalide arguments") - - # force 3D data (stack of images) - if len(currentData.shape) == 2: - currentData3D = currentData.reshape((1,) + currentData.shape) - elif len(currentData.shape) == 3: - currentData3D = currentData - - roiWidth = max(1, lineWidth) - roiStart, roiEnd, lineProjectionMode = roiInfo - - if lineProjectionMode == 'X': # Horizontal profile on the whole image - profile, area = _alignedFullProfile(currentData3D, - origin, scale, - roiStart[1], roiWidth, - axis=0, - method=method) - - if method == 'none': - coords = None - else: - coords = numpy.arange(len(profile[0]), dtype=numpy.float32) - coords = coords * scale[0] + origin[0] - - yMin, yMax = min(area[1]), max(area[1]) - 1 - if roiWidth <= 1: - profileName = '{ylabel} = %g' % yMin - else: - profileName = '{ylabel} = [%g, %g]' % (yMin, yMax) - xLabel = '{xlabel}' - - elif lineProjectionMode == 'Y': # Vertical profile on the whole image - profile, area = _alignedFullProfile(currentData3D, - origin, scale, - roiStart[0], roiWidth, - axis=1, - method=method) - - if method == 'none': - coords = None - else: - coords = numpy.arange(len(profile[0]), dtype=numpy.float32) - coords = coords * scale[1] + origin[1] - - xMin, xMax = min(area[0]), max(area[0]) - 1 - if roiWidth <= 1: - profileName = '{xlabel} = %g' % xMin - else: - profileName = '{xlabel} = [%g, %g]' % (xMin, xMax) - xLabel = '{ylabel}' - - else: # Free line profile - - # Convert start and end points in image coords as (row, col) - startPt = ((roiStart[1] - origin[1]) / scale[1], - (roiStart[0] - origin[0]) / scale[0]) - endPt = ((roiEnd[1] - origin[1]) / scale[1], - (roiEnd[0] - origin[0]) / scale[0]) - - if (int(startPt[0]) == int(endPt[0]) or - int(startPt[1]) == int(endPt[1])): - # Profile is aligned with one of the axes - - # Convert to int - startPt = int(startPt[0]), int(startPt[1]) - endPt = int(endPt[0]), int(endPt[1]) - - # Ensure startPt <= endPt - if startPt[0] > endPt[0] or startPt[1] > endPt[1]: - startPt, endPt = endPt, startPt - - if startPt[0] == endPt[0]: # Row aligned - rowRange = (int(startPt[0] + 0.5 - 0.5 * roiWidth), - int(startPt[0] + 0.5 + 0.5 * roiWidth)) - colRange = startPt[1], endPt[1] + 1 - if method == 'none': - profile = None - else: - profile = _alignedPartialProfile(currentData3D, - rowRange, colRange, - axis=0, - method=method) - - else: # Column aligned - rowRange = startPt[0], endPt[0] + 1 - colRange = (int(startPt[1] + 0.5 - 0.5 * roiWidth), - int(startPt[1] + 0.5 + 0.5 * roiWidth)) - if method == 'none': - profile = None - else: - profile = _alignedPartialProfile(currentData3D, - rowRange, colRange, - axis=1, - method=method) - # Convert ranges to plot coords to draw ROI area - area = ( - numpy.array( - (colRange[0], colRange[1], colRange[1], colRange[0]), - dtype=numpy.float32) * scale[0] + origin[0], - numpy.array( - (rowRange[0], rowRange[0], rowRange[1], rowRange[1]), - dtype=numpy.float32) * scale[1] + origin[1]) - - else: # General case: use bilinear interpolation - - # Ensure startPt <= endPt - if (startPt[1] > endPt[1] or ( - startPt[1] == endPt[1] and startPt[0] > endPt[0])): - startPt, endPt = endPt, startPt - - if method == 'none': - profile = None - else: - profile = [] - for slice_idx in range(currentData3D.shape[0]): - bilinear = BilinearImage(currentData3D[slice_idx, :, :]) - - profile.append(bilinear.profile_line( - (startPt[0] - 0.5, startPt[1] - 0.5), - (endPt[0] - 0.5, endPt[1] - 0.5), - roiWidth, - method=method)) - profile = numpy.array(profile) - - # Extend ROI with half a pixel on each end, and - # Convert back to plot coords (x, y) - length = numpy.sqrt((endPt[0] - startPt[0]) ** 2 + - (endPt[1] - startPt[1]) ** 2) - dRow = (endPt[0] - startPt[0]) / length - dCol = (endPt[1] - startPt[1]) / length - - # Extend ROI with half a pixel on each end - roiStartPt = startPt[0] - 0.5 * dRow, startPt[1] - 0.5 * dCol - roiEndPt = endPt[0] + 0.5 * dRow, endPt[1] + 0.5 * dCol - - # Rotate deltas by 90 degrees to apply line width - dRow, dCol = dCol, -dRow - - area = ( - numpy.array((roiStartPt[1] - 0.5 * roiWidth * dCol, - roiStartPt[1] + 0.5 * roiWidth * dCol, - roiEndPt[1] + 0.5 * roiWidth * dCol, - roiEndPt[1] - 0.5 * roiWidth * dCol), - dtype=numpy.float32) * scale[0] + origin[0], - numpy.array((roiStartPt[0] - 0.5 * roiWidth * dRow, - roiStartPt[0] + 0.5 * roiWidth * dRow, - roiEndPt[0] + 0.5 * roiWidth * dRow, - roiEndPt[0] - 0.5 * roiWidth * dRow), - dtype=numpy.float32) * scale[1] + origin[1]) - - # Convert start and end points back to plot coords - y0 = startPt[0] * scale[1] + origin[1] - x0 = startPt[1] * scale[0] + origin[0] - y1 = endPt[0] * scale[1] + origin[1] - x1 = endPt[1] * scale[0] + origin[0] - - if startPt[1] == endPt[1]: - profileName = '{xlabel} = %g; {ylabel} = [%g, %g]' % (x0, y0, y1) - if method == 'none': - coords = None - else: - coords = numpy.arange(len(profile[0]), dtype=numpy.float32) - coords = coords * scale[1] + y0 - xLabel = '{ylabel}' - - elif startPt[0] == endPt[0]: - profileName = '{ylabel} = %g; {xlabel} = [%g, %g]' % (y0, x0, x1) - if method == 'none': - coords = None - else: - coords = numpy.arange(len(profile[0]), dtype=numpy.float32) - coords = coords * scale[0] + x0 - xLabel = '{xlabel}' - - else: - m = (y1 - y0) / (x1 - x0) - b = y0 - m * x0 - profileName = '{ylabel} = %g * {xlabel} %+g' % (m, b) - if method == 'none': - coords = None - else: - coords = numpy.linspace(x0, x1, len(profile[0]), - endpoint=True, - dtype=numpy.float32) - xLabel = '{xlabel}' - - return coords, profile, area, profileName, xLabel diff --git a/silx/gui/plot/tools/profile/editors.py b/silx/gui/plot/tools/profile/editors.py deleted file mode 100644 index 80e0452..0000000 --- a/silx/gui/plot/tools/profile/editors.py +++ /dev/null @@ -1,307 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018-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. -# -# ###########################################################################*/ -"""This module provides editors which are used to custom profile ROI properties. -""" - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "28/06/2018" - -import logging - -from silx.gui import qt - -from silx.gui.utils import blockSignals -from silx.gui.plot.PlotToolButtons import ProfileOptionToolButton -from silx.gui.plot.PlotToolButtons import ProfileToolButton -from . import rois -from . import core - - -_logger = logging.getLogger(__name__) - - -class _NoProfileRoiEditor(qt.QWidget): - - sigDataCommited = qt.Signal() - - def setEditorData(self, roi): - pass - - def setRoiData(self, roi): - pass - - -class _DefaultImageProfileRoiEditor(qt.QWidget): - - sigDataCommited = qt.Signal() - - def __init__(self, parent=None): - qt.QWidget.__init__(self, parent=parent) - layout = qt.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - self._initLayout(layout) - - def _initLayout(self, layout): - self._lineWidth = qt.QSpinBox(self) - self._lineWidth.setRange(1, 1000) - self._lineWidth.setValue(1) - self._lineWidth.valueChanged[int].connect(self._widgetChanged) - - self._methodsButton = ProfileOptionToolButton(parent=self, plot=None) - self._methodsButton.sigMethodChanged.connect(self._widgetChanged) - - label = qt.QLabel('W:') - label.setToolTip("Line width in pixels") - layout.addWidget(label) - layout.addWidget(self._lineWidth) - layout.addWidget(self._methodsButton) - - def _widgetChanged(self, value=None): - self.commitData() - - def commitData(self): - self.sigDataCommited.emit() - - def setEditorData(self, roi): - with blockSignals(self._lineWidth): - self._lineWidth.setValue(roi.getProfileLineWidth()) - with blockSignals(self._methodsButton): - method = roi.getProfileMethod() - self._methodsButton.setMethod(method) - - def setRoiData(self, roi): - lineWidth = self._lineWidth.value() - roi.setProfileLineWidth(lineWidth) - method = self._methodsButton.getMethod() - roi.setProfileMethod(method) - - -class _DefaultImageStackProfileRoiEditor(_DefaultImageProfileRoiEditor): - - def _initLayout(self, layout): - super(_DefaultImageStackProfileRoiEditor, self)._initLayout(layout) - self._profileDim = ProfileToolButton(parent=self, plot=None) - self._profileDim.sigDimensionChanged.connect(self._widgetChanged) - layout.addWidget(self._profileDim) - - def setEditorData(self, roi): - super(_DefaultImageStackProfileRoiEditor, self).setEditorData(roi) - with blockSignals(self._profileDim): - kind = roi.getProfileType() - dim = {"1D": 1, "2D": 2}[kind] - self._profileDim.setDimension(dim) - - def setRoiData(self, roi): - super(_DefaultImageStackProfileRoiEditor, self).setRoiData(roi) - dim = self._profileDim.getDimension() - kind = {1: "1D", 2: "2D"}[dim] - roi.setProfileType(kind) - - -class _DefaultScatterProfileRoiEditor(qt.QWidget): - - sigDataCommited = qt.Signal() - - def __init__(self, parent=None): - qt.QWidget.__init__(self, parent=parent) - - self._nPoints = qt.QSpinBox(self) - self._nPoints.setRange(1, 9999) - self._nPoints.setValue(1024) - self._nPoints.valueChanged[int].connect(self.__widgetChanged) - - layout = qt.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - label = qt.QLabel('Samples:') - label.setToolTip("Number of sample points of the profile") - layout.addWidget(label) - layout.addWidget(self._nPoints) - - def __widgetChanged(self, value=None): - self.commitData() - - def commitData(self): - self.sigDataCommited.emit() - - def setEditorData(self, roi): - with blockSignals(self._nPoints): - self._nPoints.setValue(roi.getNPoints()) - - def setRoiData(self, roi): - nPoints = self._nPoints.value() - roi.setNPoints(nPoints) - - -class ProfileRoiEditorAction(qt.QWidgetAction): - """ - Action displaying GUI to edit the selected ROI. - - :param qt.QWidget parent: Parent widget - """ - def __init__(self, parent=None): - super(ProfileRoiEditorAction, self).__init__(parent) - self.__roiManager = None - self.__roi = None - self.__inhibiteReentance = None - - def createWidget(self, parent): - """Inherit the method to create a new editor""" - widget = qt.QWidget(parent) - layout = qt.QHBoxLayout(widget) - if isinstance(parent, qt.QMenu): - margins = layout.contentsMargins() - layout.setContentsMargins(margins.left(), 0, margins.right(), 0) - else: - layout.setContentsMargins(0, 0, 0, 0) - - editorClass = self.getEditorClass(self.__roi) - editor = editorClass(parent) - editor.setEditorData(self.__roi) - self.__setEditor(widget, editor) - return widget - - def deleteWidget(self, widget): - """Inherit the method to delete an editor""" - self.__setEditor(widget, None) - return qt.QWidgetAction.deleteWidget(self, widget) - - def _getEditor(self, widget): - """Returns the editor contained in the widget holder""" - layout = widget.layout() - if layout.count() == 0: - return None - return layout.itemAt(0).widget() - - def setRoiManager(self, roiManager): - """ - Connect this action to a ROI manager. - - :param RegionOfInterestManager roiManager: A ROI manager - """ - if self.__roiManager is roiManager: - return - if self.__roiManager is not None: - self.__roiManager.sigCurrentRoiChanged.disconnect(self.__currentRoiChanged) - self.__roiManager = roiManager - if self.__roiManager is not None: - self.__roiManager.sigCurrentRoiChanged.connect(self.__currentRoiChanged) - self.__currentRoiChanged(roiManager.getCurrentRoi()) - - def __currentRoiChanged(self, roi): - """Handle changes of the selected ROI""" - if roi is not None and not isinstance(roi, core.ProfileRoiMixIn): - return - self.setProfileRoi(roi) - - def setProfileRoi(self, roi): - """Set a profile ROI to edit. - - :param ProfileRoiMixIn roi: A profile ROI - """ - if self.__roi is roi: - return - if self.__roi is not None: - self.__roi.sigProfilePropertyChanged.disconnect(self.__roiPropertyChanged) - self.__roi = roi - if self.__roi is not None: - self.__roi.sigProfilePropertyChanged.connect(self.__roiPropertyChanged) - self._updateWidgets() - - def getRoiProfile(self): - """Returns the edited profile ROI. - - :rtype: ProfileRoiMixIn - """ - return self.__roi - - def __roiPropertyChanged(self): - """Handle changes on the property defining the ROI. - """ - self._updateWidgetValues() - - def __setEditor(self, widget, editor): - """Set the editor to display. - - :param qt.QWidget editor: The editor to display - """ - previousEditor = self._getEditor(widget) - if previousEditor is editor: - return - layout = widget.layout() - if previousEditor is not None: - previousEditor.sigDataCommited.disconnect(self._editorDataCommited) - layout.removeWidget(previousEditor) - previousEditor.deleteLater() - if editor is not None: - editor.sigDataCommited.connect(self._editorDataCommited) - layout.addWidget(editor) - - def getEditorClass(self, roi): - """Returns the editor class to use according to the ROI.""" - if roi is None: - editorClass = _NoProfileRoiEditor - elif isinstance(roi, (rois._DefaultImageStackProfileRoiMixIn, - rois.ProfileImageStackCrossROI)): - # Must be done before the default image ROI - # Cause ImageStack ROIs inherit from Image ROIs - editorClass = _DefaultImageStackProfileRoiEditor - elif isinstance(roi, (rois._DefaultImageProfileRoiMixIn, - rois.ProfileImageCrossROI)): - editorClass = _DefaultImageProfileRoiEditor - elif isinstance(roi, (rois._DefaultScatterProfileRoiMixIn, - rois.ProfileScatterCrossROI)): - editorClass = _DefaultScatterProfileRoiEditor - else: - # Unsupported - editorClass = _NoProfileRoiEditor - return editorClass - - def _updateWidgets(self): - """Update the kind of editor to display, according to the selected - profile ROI.""" - parent = self.parent() - editorClass = self.getEditorClass(self.__roi) - for widget in self.createdWidgets(): - editor = editorClass(parent) - editor.setEditorData(self.__roi) - self.__setEditor(widget, editor) - - def _updateWidgetValues(self): - """Update the content of the displayed editor, according to the - selected profile ROI.""" - for widget in self.createdWidgets(): - editor = self._getEditor(widget) - if self.__inhibiteReentance is editor: - continue - editor.setEditorData(self.__roi) - - def _editorDataCommited(self): - """Handle changes from the editor.""" - editor = self.sender() - if self.__roi is not None: - self.__inhibiteReentance = editor - editor.setRoiData(self.__roi) - self.__inhibiteReentance = None diff --git a/silx/gui/plot/tools/profile/manager.py b/silx/gui/plot/tools/profile/manager.py deleted file mode 100644 index 68db9a6..0000000 --- a/silx/gui/plot/tools/profile/manager.py +++ /dev/null @@ -1,1076 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018-2021 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. -# -# ###########################################################################*/ -"""This module provides a manager to compute and display profiles. -""" - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "28/06/2018" - -import logging -import weakref - -from silx.gui import qt -from silx.gui import colors -from silx.gui import utils - -from silx.utils.weakref import WeakMethodProxy -from silx.gui import icons -from silx.gui.plot import PlotWidget -from silx.gui.plot.tools.roi import RegionOfInterestManager -from silx.gui.plot.tools.roi import CreateRoiModeAction -from silx.gui.plot import items -from silx.gui.qt import silxGlobalThreadPool -from silx.gui.qt import inspect -from . import rois -from . import core -from . import editors - - -_logger = logging.getLogger(__name__) - - -class _RunnableComputeProfile(qt.QRunnable): - """Runner to process profiles - - :param qt.QThreadPool threadPool: The thread which will be used to - execute this runner. It is used to update the used signals - :param ~silx.gui.plot.items.Item item: Item in which the profile is - computed - :param ~silx.gui.plot.tools.profile.core.ProfileRoiMixIn roi: ROI - defining the profile shape and other characteristics - """ - - class _Signals(qt.QObject): - """Signal holder""" - resultReady = qt.Signal(object, object) - runnerFinished = qt.Signal(object) - - def __init__(self, threadPool, item, roi): - """Constructor - """ - super(_RunnableComputeProfile, self).__init__() - self._signals = self._Signals() - self._signals.moveToThread(threadPool.thread()) - self._item = item - self._roi = roi - self._cancelled = False - - def _lazyCancel(self): - """Cancel the runner if it is not yet started. - - The threadpool will still execute the runner, but this will process - nothing. - - This is only used with Qt<5.9 where QThreadPool.tryTake is not available. - """ - self._cancelled = True - - def autoDelete(self): - return False - - def getRoi(self): - """Returns the ROI in which the runner will compute a profile. - - :rtype: ~silx.gui.plot.tools.profile.core.ProfileRoiMixIn - """ - return self._roi - - @property - def resultReady(self): - """Signal emitted when the result of the computation is available. - - This signal provides 2 values: The ROI, and the computation result. - """ - return self._signals.resultReady - - @property - def runnerFinished(self): - """Signal emitted when runner have finished. - - This signal provides a single value: the runner itself. - """ - return self._signals.runnerFinished - - def run(self): - """Process the profile computation. - """ - if not self._cancelled: - try: - profileData = self._roi.computeProfile(self._item) - except Exception: - _logger.error("Error while computing profile", exc_info=True) - else: - self.resultReady.emit(self._roi, profileData) - self.runnerFinished.emit(self) - - -class ProfileWindow(qt.QMainWindow): - """ - Display a computed profile. - - The content can be described using :meth:`setRoiProfile` if the source of - the profile is a profile ROI, and :meth:`setProfile` for the data content. - """ - - sigClose = qt.Signal() - """Emitted by :meth:`closeEvent` (e.g. when the window is closed - through the window manager's close icon).""" - - def __init__(self, parent=None, backend=None): - qt.QMainWindow.__init__(self, parent=parent, flags=qt.Qt.Dialog) - - self.setWindowTitle('Profile window') - self._plot1D = None - self._plot2D = None - self._backend = backend - self._data = None - - widget = qt.QWidget() - self._layout = qt.QStackedLayout(widget) - self._layout.setContentsMargins(0, 0, 0, 0) - self.setCentralWidget(widget) - - def prepareWidget(self, roi): - """Called before the show to prepare the window to use with - a specific ROI.""" - if isinstance(roi, rois._DefaultImageStackProfileRoiMixIn): - profileType = roi.getProfileType() - else: - profileType = "1D" - if profileType == "1D": - self.getPlot1D() - elif profileType == "2D": - self.getPlot2D() - - def createPlot1D(self, parent, backend): - """Inherit this function to create your own plot to render 1D - profiles. The default value is a `Plot1D`. - - :param parent: The parent of this widget or None. - :param backend: The backend to use for the plot. - See :class:`PlotWidget` for the list of supported backend. - :rtype: PlotWidget - """ - # import here to avoid circular import - from ...PlotWindow import Plot1D - plot = Plot1D(parent=parent, backend=backend) - plot.setDataMargins(yMinMargin=0.1, yMaxMargin=0.1) - plot.setGraphYLabel('Profile') - plot.setGraphXLabel('') - return plot - - def createPlot2D(self, parent, backend): - """Inherit this function to create your own plot to render 2D - profiles. The default value is a `Plot2D`. - - :param parent: The parent of this widget or None. - :param backend: The backend to use for the plot. - See :class:`PlotWidget` for the list of supported backend. - :rtype: PlotWidget - """ - # import here to avoid circular import - from ...PlotWindow import Plot2D - return Plot2D(parent=parent, backend=backend) - - def getPlot1D(self, init=True): - """Return the current plot used to display curves and create it if it - does not yet exists and `init` is True. Else returns None.""" - if not init: - return self._plot1D - if self._plot1D is None: - self._plot1D = self.createPlot1D(self, self._backend) - self._layout.addWidget(self._plot1D) - return self._plot1D - - def _showPlot1D(self): - plot = self.getPlot1D() - self._layout.setCurrentWidget(plot) - - def getPlot2D(self, init=True): - """Return the current plot used to display image and create it if it - does not yet exists and `init` is True. Else returns None.""" - if not init: - return self._plot2D - if self._plot2D is None: - self._plot2D = self.createPlot2D(parent=self, backend=self._backend) - self._layout.addWidget(self._plot2D) - return self._plot2D - - def _showPlot2D(self): - plot = self.getPlot2D() - self._layout.setCurrentWidget(plot) - - def getCurrentPlotWidget(self): - return self._layout.currentWidget() - - def closeEvent(self, qCloseEvent): - self.sigClose.emit() - qCloseEvent.accept() - - def setRoiProfile(self, roi): - """Set the profile ROI which it the source of the following data - to display. - - :param ProfileRoiMixIn roi: The profile ROI data source - """ - if roi is None: - return - self.__color = colors.rgba(roi.getColor()) - - def _setImageProfile(self, data): - """ - Setup the window to display a new profile data which is represented - by an image. - - :param core.ImageProfileData data: Computed data profile - """ - plot = self.getPlot2D() - - plot.clear() - plot.setGraphTitle(data.title) - plot.getXAxis().setLabel(data.xLabel) - - - coords = data.coords - colormap = data.colormap - profileScale = (coords[-1] - coords[0]) / data.profile.shape[1], 1 - plot.addImage(data.profile, - legend="profile", - colormap=colormap, - origin=(coords[0], 0), - scale=profileScale) - plot.getYAxis().setLabel("Frame index (depth)") - - self._showPlot2D() - - def _setCurveProfile(self, data): - """ - Setup the window to display a new profile data which is represented - by a curve. - - :param core.CurveProfileData data: Computed data profile - """ - plot = self.getPlot1D() - - plot.clear() - plot.setGraphTitle(data.title) - plot.getXAxis().setLabel(data.xLabel) - plot.getYAxis().setLabel(data.yLabel) - - plot.addCurve(data.coords, - data.profile, - legend="level", - color=self.__color) - - self._showPlot1D() - - def _setRgbaProfile(self, data): - """ - Setup the window to display a new profile data which is represented - by a curve. - - :param core.RgbaProfileData data: Computed data profile - """ - plot = self.getPlot1D() - - plot.clear() - plot.setGraphTitle(data.title) - plot.getXAxis().setLabel(data.xLabel) - plot.getYAxis().setLabel(data.yLabel) - - self._showPlot1D() - - plot.addCurve(data.coords, data.profile, - legend="level", color="black") - plot.addCurve(data.coords, data.profile_r, - legend="red", color="red") - plot.addCurve(data.coords, data.profile_g, - legend="green", color="green") - plot.addCurve(data.coords, data.profile_b, - legend="blue", color="blue") - if data.profile_a is not None: - plot.addCurve(data.coords, data.profile_a, legend="alpha", color="gray") - - def clear(self): - """Clear the window profile""" - plot = self.getPlot1D(init=False) - if plot is not None: - plot.clear() - plot = self.getPlot2D(init=False) - if plot is not None: - plot.clear() - - def getProfile(self): - """Returns the profile data which is displayed""" - return self.__data - - def setProfile(self, data): - """ - Setup the window to display a new profile data. - - This method dispatch the result to a specific method according to the - data type. - - :param data: Computed data profile - """ - self.__data = data - if data is None: - self.clear() - elif isinstance(data, core.ImageProfileData): - self._setImageProfile(data) - elif isinstance(data, core.RgbaProfileData): - self._setRgbaProfile(data) - elif isinstance(data, core.CurveProfileData): - self._setCurveProfile(data) - else: - raise TypeError("Unsupported type %s" % type(data)) - - -class _ClearAction(qt.QAction): - """Action to clear the profile manager - - The action is only enabled if something can be cleaned up. - """ - - def __init__(self, parent, profileManager): - super(_ClearAction, self).__init__(parent) - self.__profileManager = weakref.ref(profileManager) - icon = icons.getQIcon('profile-clear') - self.setIcon(icon) - self.setText('Clear profile') - self.setToolTip('Clear the profiles') - self.setCheckable(False) - self.setEnabled(False) - self.triggered.connect(profileManager.clearProfile) - plot = profileManager.getPlotWidget() - roiManager = profileManager.getRoiManager() - plot.sigInteractiveModeChanged.connect(self.__modeUpdated) - roiManager.sigRoiChanged.connect(self.__roiListUpdated) - - def getProfileManager(self): - return self.__profileManager() - - def __roiListUpdated(self): - self.__update() - - def __modeUpdated(self, source): - self.__update() - - def __update(self): - profileManager = self.getProfileManager() - if profileManager is None: - return - roiManager = profileManager.getRoiManager() - if roiManager is None: - return - enabled = roiManager.isStarted() or len(roiManager.getRois()) > 0 - self.setEnabled(enabled) - - -class _StoreLastParamBehavior(qt.QObject): - """This object allow to store and restore the properties of the ROI - profiles""" - - def __init__(self, parent): - assert isinstance(parent, ProfileManager) - super(_StoreLastParamBehavior, self).__init__(parent=parent) - self.__properties = {} - self.__profileRoi = None - self.__filter = utils.LockReentrant() - - def _roi(self): - """Return the spied ROI""" - if self.__profileRoi is None: - return None - roi = self.__profileRoi() - if roi is None: - self.__profileRoi = None - return roi - - def setProfileRoi(self, roi): - """Set a profile ROI to spy. - - :param ProfileRoiMixIn roi: A profile ROI - """ - previousRoi = self._roi() - if previousRoi is roi: - return - if previousRoi is not None: - previousRoi.sigProfilePropertyChanged.disconnect(self._profilePropertyChanged) - self.__profileRoi = None if roi is None else weakref.ref(roi) - if roi is not None: - roi.sigProfilePropertyChanged.connect(self._profilePropertyChanged) - - def _profilePropertyChanged(self): - """Handle changes on the properties defining the profile ROI. - """ - if self.__filter.locked(): - return - roi = self.sender() - self.storeProperties(roi) - - def storeProperties(self, roi): - if isinstance(roi, (rois._DefaultImageStackProfileRoiMixIn, - rois.ProfileImageStackCrossROI)): - self.__properties["method"] = roi.getProfileMethod() - self.__properties["line-width"] = roi.getProfileLineWidth() - self.__properties["type"] = roi.getProfileType() - elif isinstance(roi, (rois._DefaultImageProfileRoiMixIn, - rois.ProfileImageCrossROI)): - self.__properties["method"] = roi.getProfileMethod() - self.__properties["line-width"] = roi.getProfileLineWidth() - elif isinstance(roi, (rois._DefaultScatterProfileRoiMixIn, - rois.ProfileScatterCrossROI)): - self.__properties["npoints"] = roi.getNPoints() - - def restoreProperties(self, roi): - with self.__filter: - if isinstance(roi, (rois._DefaultImageStackProfileRoiMixIn, - rois.ProfileImageStackCrossROI)): - value = self.__properties.get("method", None) - if value is not None: - roi.setProfileMethod(value) - value = self.__properties.get("line-width", None) - if value is not None: - roi.setProfileLineWidth(value) - value = self.__properties.get("type", None) - if value is not None: - roi.setProfileType(value) - elif isinstance(roi, (rois._DefaultImageProfileRoiMixIn, - rois.ProfileImageCrossROI)): - value = self.__properties.get("method", None) - if value is not None: - roi.setProfileMethod(value) - value = self.__properties.get("line-width", None) - if value is not None: - roi.setProfileLineWidth(value) - elif isinstance(roi, (rois._DefaultScatterProfileRoiMixIn, - rois.ProfileScatterCrossROI)): - value = self.__properties.get("npoints", None) - if value is not None: - roi.setNPoints(value) - - -class ProfileManager(qt.QObject): - """Base class for profile management tools - - :param plot: :class:`~silx.gui.plot.PlotWidget` on which to operate. - :param plot: :class:`~silx.gui.plot.tools.roi.RegionOfInterestManager` - on which to operate. - """ - def __init__(self, parent=None, plot=None, roiManager=None): - super(ProfileManager, self).__init__(parent) - - assert isinstance(plot, PlotWidget) - self._plotRef = weakref.ref( - plot, WeakMethodProxy(self.__plotDestroyed)) - - # Set-up interaction manager - if roiManager is None: - roiManager = RegionOfInterestManager(plot) - - self._roiManagerRef = weakref.ref(roiManager) - self._rois = [] - self._pendingRunners = [] - """List of ROIs which have to be updated""" - - self.__reentrantResults = {} - """Store reentrant result to avoid to skip some of them - cause the implementation uses a QEventLoop.""" - - self._profileWindowClass = ProfileWindow - """Class used to display the profile results""" - - self._computedProfiles = 0 - """Statistics for tests""" - - self.__itemTypes = [] - """Kind of items to use""" - - self.__tracking = False - """Is the plot active items are tracked""" - - self.__useColorFromCursor = True - """If true, force the ROI color with the colormap marker color""" - - self._item = None - """The selected item""" - - self.__singleProfileAtATime = True - """When it's true, only a single profile is displayed at a time.""" - - self._previousWindowGeometry = [] - - self._storeProperties = _StoreLastParamBehavior(self) - """If defined the profile properties of the last ROI are reused to the - new created ones""" - - # Listen to plot limits changed - plot.getXAxis().sigLimitsChanged.connect(self.requestUpdateAllProfile) - plot.getYAxis().sigLimitsChanged.connect(self.requestUpdateAllProfile) - - roiManager.sigInteractiveModeFinished.connect(self.__interactionFinished) - roiManager.sigInteractiveRoiCreated.connect(self.__roiCreated) - roiManager.sigRoiAdded.connect(self.__roiAdded) - roiManager.sigRoiAboutToBeRemoved.connect(self.__roiRemoved) - - def setSingleProfile(self, enable): - """ - Enable or disable the single profile mode. - - In single mode, the manager enforce a single ROI at the same - time. A new one will remove the previous one. - - If this mode is not enabled, many ROIs can be created, and many - profile windows will be displayed. - """ - self.__singleProfileAtATime = enable - - def isSingleProfile(self): - """ - Returns true if the manager is in a single profile mode. - - :rtype: bool - """ - return self.__singleProfileAtATime - - def __interactionFinished(self): - """Handle end of interactive mode""" - pass - - def __roiAdded(self, roi): - """Handle new ROI""" - # Filter out non profile ROIs - if not isinstance(roi, core.ProfileRoiMixIn): - return - self.__addProfile(roi) - - def __roiRemoved(self, roi): - """Handle removed ROI""" - # Filter out non profile ROIs - if not isinstance(roi, core.ProfileRoiMixIn): - return - self.__removeProfile(roi) - - def createProfileAction(self, profileRoiClass, parent=None): - """Create an action from a class of ProfileRoi - - :param core.ProfileRoiMixIn profileRoiClass: A class of a profile ROI - :param qt.QObject parent: The parent of the created action. - :rtype: qt.QAction - """ - if not issubclass(profileRoiClass, core.ProfileRoiMixIn): - raise TypeError("Type %s not expected" % type(profileRoiClass)) - roiManager = self.getRoiManager() - action = CreateRoiModeAction(parent, roiManager, profileRoiClass) - if hasattr(profileRoiClass, "ICON"): - action.setIcon(icons.getQIcon(profileRoiClass.ICON)) - if hasattr(profileRoiClass, "NAME"): - def articulify(word): - """Add an an/a article in the front of the word""" - first = word[1] if word[0] == 'h' else word[0] - if first in "aeiou": - return "an " + word - return "a " + word - action.setText('Define %s' % articulify(profileRoiClass.NAME)) - action.setToolTip('Enables %s selection mode' % profileRoiClass.NAME) - action.setSingleShot(True) - return action - - def createClearAction(self, parent): - """Create an action to clean up the plot from the profile ROIs. - - :param qt.QObject parent: The parent of the created action. - :rtype: qt.QAction - """ - action = _ClearAction(parent, self) - return action - - def createImageActions(self, parent): - """Create actions designed for image items. This actions created - new ROIs. - - :param qt.QObject parent: The parent of the created action. - :rtype: List[qt.QAction] - """ - profileClasses = [ - rois.ProfileImageHorizontalLineROI, - rois.ProfileImageVerticalLineROI, - rois.ProfileImageLineROI, - rois.ProfileImageDirectedLineROI, - rois.ProfileImageCrossROI, - ] - return [self.createProfileAction(pc, parent=parent) for pc in profileClasses] - - def createScatterActions(self, parent): - """Create actions designed for scatter items. This actions created - new ROIs. - - :param qt.QObject parent: The parent of the created action. - :rtype: List[qt.QAction] - """ - profileClasses = [ - rois.ProfileScatterHorizontalLineROI, - rois.ProfileScatterVerticalLineROI, - rois.ProfileScatterLineROI, - rois.ProfileScatterCrossROI, - ] - return [self.createProfileAction(pc, parent=parent) for pc in profileClasses] - - def createScatterSliceActions(self, parent): - """Create actions designed for regular scatter items. This actions - created new ROIs. - - This ROIs was designed to use the input data without interpolation, - like you could do with an image. - - :param qt.QObject parent: The parent of the created action. - :rtype: List[qt.QAction] - """ - profileClasses = [ - rois.ProfileScatterHorizontalSliceROI, - rois.ProfileScatterVerticalSliceROI, - rois.ProfileScatterCrossSliceROI, - ] - return [self.createProfileAction(pc, parent=parent) for pc in profileClasses] - - def createImageStackActions(self, parent): - """Create actions designed for stack image items. This actions - created new ROIs. - - This ROIs was designed to create both profile on the displayed image - and profile on the full stack (2D result). - - :param qt.QObject parent: The parent of the created action. - :rtype: List[qt.QAction] - """ - profileClasses = [ - rois.ProfileImageStackHorizontalLineROI, - rois.ProfileImageStackVerticalLineROI, - rois.ProfileImageStackLineROI, - rois.ProfileImageStackCrossROI, - ] - return [self.createProfileAction(pc, parent=parent) for pc in profileClasses] - - def createEditorAction(self, parent): - """Create an action containing GUI to edit the selected profile ROI. - - :param qt.QObject parent: The parent of the created action. - :rtype: qt.QAction - """ - action = editors.ProfileRoiEditorAction(parent) - action.setRoiManager(self.getRoiManager()) - return action - - def setItemType(self, image=False, scatter=False): - """Set the item type to use and select the active one. - - :param bool image: Image item are allowed - :param bool scatter: Scatter item are allowed - """ - self.__itemTypes = [] - plot = self.getPlotWidget() - item = None - if image: - self.__itemTypes.append("image") - item = plot.getActiveImage() - if scatter: - self.__itemTypes.append("scatter") - if item is None: - item = plot.getActiveScatter() - self.setPlotItem(item) - - def setProfileWindowClass(self, profileWindowClass): - """Set the class which will be instantiated to display profile result. - """ - self._profileWindowClass = profileWindowClass - - def setActiveItemTracking(self, tracking): - """Enable/disable the tracking of the active item of the plot. - - :param bool tracking: Tracking mode - """ - if self.__tracking == tracking: - return - plot = self.getPlotWidget() - if self.__tracking: - plot.sigActiveImageChanged.disconnect(self._activeImageChanged) - plot.sigActiveScatterChanged.disconnect(self._activeScatterChanged) - self.__tracking = tracking - if self.__tracking: - plot.sigActiveImageChanged.connect(self.__activeImageChanged) - plot.sigActiveScatterChanged.connect(self.__activeScatterChanged) - - def setDefaultColorFromCursorColor(self, enabled): - """Enabled/disable the use of the colormap cursor color to display the - ROIs. - - If set, the manager will update the color of the profile ROIs using the - current colormap cursor color from the selected item. - """ - self.__useColorFromCursor = enabled - - def __activeImageChanged(self, previous, legend): - """Handle plot item selection""" - if "image" in self.__itemTypes: - plot = self.getPlotWidget() - item = plot.getImage(legend) - self.setPlotItem(item) - - def __activeScatterChanged(self, previous, legend): - """Handle plot item selection""" - if "scatter" in self.__itemTypes: - plot = self.getPlotWidget() - item = plot.getScatter(legend) - self.setPlotItem(item) - - def __roiCreated(self, roi): - """Handle ROI creation""" - # Filter out non profile ROIs - if isinstance(roi, core.ProfileRoiMixIn): - if self._storeProperties is not None: - # Initialize the properties with the previous ones - self._storeProperties.restoreProperties(roi) - - def __addProfile(self, profileRoi): - """Add a new ROI to the manager.""" - if profileRoi.getFocusProxy() is None: - if self._storeProperties is not None: - # Follow changes on properties - self._storeProperties.setProfileRoi(profileRoi) - if self.__singleProfileAtATime: - # FIXME: It would be good to reuse the windows to avoid blinking - self.clearProfile() - - profileRoi._setProfileManager(self) - self._updateRoiColor(profileRoi) - self._rois.append(profileRoi) - self.requestUpdateProfile(profileRoi) - - def __removeProfile(self, profileRoi): - """Remove a ROI from the manager.""" - window = self._disconnectProfileWindow(profileRoi) - if window is not None: - geometry = window.geometry() - if not geometry.isEmpty(): - self._previousWindowGeometry.append(geometry) - self.clearProfileWindow(window) - if profileRoi in self._rois: - self._rois.remove(profileRoi) - - def _disconnectProfileWindow(self, profileRoi): - """Handle profile window close.""" - window = profileRoi.getProfileWindow() - profileRoi.setProfileWindow(None) - return window - - def clearProfile(self): - """Clear the associated ROI profile""" - roiManager = self.getRoiManager() - for roi in list(self._rois): - if roi.getFocusProxy() is not None: - # Skip sub ROIs, it will be removed by their parents - continue - roiManager.removeRoi(roi) - - if not roiManager.isDrawing(): - # Clean the selected mode - roiManager.stop() - - def hasPendingOperations(self): - """Returns true if a thread is still computing or displaying a profile. - - :rtype: bool - """ - return len(self.__reentrantResults) > 0 or len(self._pendingRunners) > 0 - - def requestUpdateAllProfile(self): - """Request to update the profile of all the managed ROIs. - """ - for roi in self._rois: - self.requestUpdateProfile(roi) - - def requestUpdateProfile(self, profileRoi): - """Request to update a specific profile ROI. - - :param ~core.ProfileRoiMixIn profileRoi: - """ - if profileRoi.computeProfile is None: - return - threadPool = silxGlobalThreadPool() - - # Clean up deprecated runners - for runner in list(self._pendingRunners): - if not inspect.isValid(runner): - self._pendingRunners.remove(runner) - continue - if runner.getRoi() is profileRoi: - if hasattr(threadPool, "tryTake"): - if threadPool.tryTake(runner): - self._pendingRunners.remove(runner) - else: # Support Qt<5.9 - runner._lazyCancel() - - item = self.getPlotItem() - if item is None or not isinstance(item, profileRoi.ITEM_KIND): - # This item is not compatible with this profile - profileRoi._setPlotItem(None) - profileWindow = profileRoi.getProfileWindow() - if profileWindow is not None: - profileWindow.setProfile(None) - return - - profileRoi._setPlotItem(item) - runner = _RunnableComputeProfile(threadPool, item, profileRoi) - runner.runnerFinished.connect(self.__cleanUpRunner) - runner.resultReady.connect(self.__displayResult) - self._pendingRunners.append(runner) - threadPool.start(runner) - - def __cleanUpRunner(self, runner): - """Remove a thread pool runner from the list of hold tasks. - - Called at the termination of the runner. - """ - if runner in self._pendingRunners: - self._pendingRunners.remove(runner) - - def __displayResult(self, roi, profileData): - """Display the result of a ROI. - - :param ~core.ProfileRoiMixIn profileRoi: A managed ROI - :param ~core.CurveProfileData profileData: Computed data profile - """ - if roi in self.__reentrantResults: - # Store the data to process it in the main loop - # And not a sub loop created by initProfileWindow - # This also remove the duplicated requested - self.__reentrantResults[roi] = profileData - return - - self.__reentrantResults[roi] = profileData - self._computedProfiles = self._computedProfiles + 1 - window = roi.getProfileWindow() - if window is None: - plot = self.getPlotWidget() - window = self.createProfileWindow(plot, roi) - # roi.profileWindow have to be set before initializing the window - # Cause the initialization is using QEventLoop - roi.setProfileWindow(window) - self.initProfileWindow(window, roi) - window.show() - - lastData = self.__reentrantResults.pop(roi) - window.setProfile(lastData) - - def __plotDestroyed(self, ref): - """Handle finalization of PlotWidget - - :param ref: weakref to the plot - """ - self._plotRef = None - self._roiManagerRef = None - self._pendingRunners = [] - - def setPlotItem(self, item): - """Set the plot item focused by the profile manager. - - :param ~silx.gui.plot.items.Item item: A plot item - """ - previous = self.getPlotItem() - if previous is item: - return - if item is None: - self._item = None - else: - item.sigItemChanged.connect(self.__itemChanged) - self._item = weakref.ref(item) - self._updateRoiColors() - self.requestUpdateAllProfile() - - def getDefaultColor(self, item): - """Returns the default ROI color to use according to the given item. - - :param ~silx.gui.plot.items.item.Item item: AN item - :rtype: qt.QColor - """ - color = 'pink' - if isinstance(item, items.ColormapMixIn): - colormap = item.getColormap() - name = colormap.getName() - if name is not None: - color = colors.cursorColorForColormap(name) - color = colors.asQColor(color) - return color - - def _updateRoiColors(self): - """Update ROI color according to the item selection""" - if not self.__useColorFromCursor: - return - item = self.getPlotItem() - color = self.getDefaultColor(item) - for roi in self._rois: - roi.setColor(color) - - def _updateRoiColor(self, roi): - """Update a specific ROI according to the current selected item. - - :param RegionOfInterest roi: The ROI to update - """ - if not self.__useColorFromCursor: - return - item = self.getPlotItem() - color = self.getDefaultColor(item) - roi.setColor(color) - - def __itemChanged(self, changeType): - """Handle item changes. - """ - if changeType in (items.ItemChangedType.DATA, - items.ItemChangedType.MASK, - items.ItemChangedType.POSITION, - items.ItemChangedType.SCALE): - self.requestUpdateAllProfile() - elif changeType == (items.ItemChangedType.COLORMAP): - self._updateRoiColors() - - def getPlotItem(self): - """Returns the item focused by the profile manager. - - :rtype: ~silx.gui.plot.items.Item - """ - if self._item is None: - return None - item = self._item() - if item is None: - self._item = None - return item - - def getPlotWidget(self): - """The plot associated to the profile manager. - - :rtype: ~silx.gui.plot.PlotWidget - """ - if self._plotRef is None: - return None - plot = self._plotRef() - if plot is None: - self._plotRef = None - return plot - - def getCurrentRoi(self): - """Returns the currently selected ROI, else None. - - :rtype: core.ProfileRoiMixIn - """ - roiManager = self.getRoiManager() - if roiManager is None: - return None - roi = roiManager.getCurrentRoi() - if not isinstance(roi, core.ProfileRoiMixIn): - return None - return roi - - def getRoiManager(self): - """Returns the used ROI manager - - :rtype: RegionOfInterestManager - """ - return self._roiManagerRef() - - def createProfileWindow(self, plot, roi): - """Create a new profile window. - - :param ~core.ProfileRoiMixIn roi: The plot containing the raw data - :param ~core.ProfileRoiMixIn roi: A managed ROI - :rtype: ~ProfileWindow - """ - return self._profileWindowClass(plot) - - def initProfileWindow(self, profileWindow, roi): - """This function is called just after the profile window creation in - order to initialize the window location. - - :param ~ProfileWindow profileWindow: - The profile window to initialize. - """ - # Enforce the use of one of the widgets - # To have the correct window size - profileWindow.prepareWidget(roi) - profileWindow.adjustSize() - - # Trick to avoid blinking while retrieving the right window size - # Display the window, hide it and wait for some event loops - profileWindow.show() - profileWindow.hide() - eventLoop = qt.QEventLoop(self) - for _ in range(10): - if not eventLoop.processEvents(): - break - - profileWindow.show() - if len(self._previousWindowGeometry) > 0: - geometry = self._previousWindowGeometry.pop() - profileWindow.setGeometry(geometry) - return - - window = self.getPlotWidget().window() - winGeom = window.frameGeometry() - qapp = qt.QApplication.instance() - desktop = qapp.desktop() - screenGeom = desktop.availableGeometry(window) - spaceOnLeftSide = winGeom.left() - spaceOnRightSide = screenGeom.width() - winGeom.right() - - profileGeom = profileWindow.frameGeometry() - profileWidth = profileGeom.width() - - # Align vertically to the center of the window - top = winGeom.top() + (winGeom.height() - profileGeom.height()) // 2 - - margin = 5 - if profileWidth < spaceOnRightSide: - # Place profile on the right - left = winGeom.right() + margin - elif profileWidth < spaceOnLeftSide: - # Place profile on the left - left = max(0, winGeom.left() - profileWidth - margin) - else: - # Move it as much as possible where there is more space - if spaceOnLeftSide > spaceOnRightSide: - left = 0 - else: - left = screenGeom.width() - profileGeom.width() - profileWindow.move(left, top) - - - def clearProfileWindow(self, profileWindow): - """Called when a profile window is not anymore needed. - - By default the window will be closed. But it can be - inherited to change this behavior. - """ - profileWindow.deleteLater() diff --git a/silx/gui/plot/tools/profile/rois.py b/silx/gui/plot/tools/profile/rois.py deleted file mode 100644 index eb7e975..0000000 --- a/silx/gui/plot/tools/profile/rois.py +++ /dev/null @@ -1,1156 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018-2021 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. -# -# ###########################################################################*/ -"""This module define ROIs for profile tools. - -.. inheritance-diagram:: - silx.gui.plot.tools.profile.rois - :top-classes: silx.gui.plot.tools.profile.core.ProfileRoiMixIn, silx.gui.plot.items.roi.RegionOfInterest - :parts: 1 - :private-bases: -""" - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "01/12/2020" - -import numpy -import weakref -from concurrent.futures import CancelledError - -from silx.gui import colors - -from silx.gui.plot import items -from silx.gui.plot.items import roi as roi_items -from . import core -from silx.gui import utils -from .....utils.proxy import docstring - - -def _relabelAxes(plot, text): - """Relabel {xlabel} and {ylabel} from this text using the corresponding - plot axis label. If the axis label is empty, label it with "X" and "Y". - - :rtype: str - """ - xLabel = plot.getXAxis().getLabel() - if not xLabel: - xLabel = "X" - yLabel = plot.getYAxis().getLabel() - if not yLabel: - yLabel = "Y" - return text.format(xlabel=xLabel, ylabel=yLabel) - - -def _lineProfileTitle(x0, y0, x1, y1): - """Compute corresponding plot title - - This can be overridden to change title behavior. - - :param float x0: Profile start point X coord - :param float y0: Profile start point Y coord - :param float x1: Profile end point X coord - :param float y1: Profile end point Y coord - :return: Title to use - :rtype: str - """ - if x0 == x1: - title = '{xlabel} = %g; {ylabel} = [%g, %g]' % (x0, y0, y1) - elif y0 == y1: - title = '{ylabel} = %g; {xlabel} = [%g, %g]' % (y0, x0, x1) - else: - m = (y1 - y0) / (x1 - x0) - b = y0 - m * x0 - title = '{ylabel} = %g * {xlabel} %+g' % (m, b) - - return title - - -class _ImageProfileArea(items.Shape): - """This shape displays the location of pixels used to compute the - profile.""" - - def __init__(self, parentRoi): - items.Shape.__init__(self, "polygon") - color = colors.rgba(parentRoi.getColor()) - self.setColor(color) - self.setFill(True) - self.setOverlay(True) - self.setPoints([[0, 0], [0, 0]]) # Else it segfault - - self.__parentRoi = weakref.ref(parentRoi) - parentRoi.sigItemChanged.connect(self._updateAreaProperty) - parentRoi.sigRegionChanged.connect(self._updateArea) - parentRoi.sigProfilePropertyChanged.connect(self._updateArea) - parentRoi.sigPlotItemChanged.connect(self._updateArea) - - def getParentRoi(self): - if self.__parentRoi is None: - return None - parentRoi = self.__parentRoi() - if parentRoi is None: - self.__parentRoi = None - return parentRoi - - def _updateAreaProperty(self, event=None, checkVisibility=True): - parentRoi = self.sender() - if event == items.ItemChangedType.COLOR: - parentRoi._updateItemProperty(event, parentRoi, self) - elif event == items.ItemChangedType.VISIBLE: - if self.getPlotItem() is not None: - parentRoi._updateItemProperty(event, parentRoi, self) - - def _updateArea(self): - roi = self.getParentRoi() - item = roi.getPlotItem() - if item is None: - self.setVisible(False) - return - polygon = self._computePolygon(item) - self.setVisible(True) - polygon = numpy.array(polygon).T - self.setLineStyle("--") - self.setPoints(polygon, copy=False) - - def _computePolygon(self, item): - if not isinstance(item, items.ImageBase): - raise TypeError("Unexpected class %s" % type(item)) - - currentData = item.getValueData(copy=False) - - roi = self.getParentRoi() - origin = item.getOrigin() - scale = item.getScale() - _coords, _profile, area, _profileName, _xLabel = core.createProfile( - roiInfo=roi._getRoiInfo(), - currentData=currentData, - origin=origin, - scale=scale, - lineWidth=roi.getProfileLineWidth(), - method="none") - return area - - -class _SliceProfileArea(items.Shape): - """This shape displays the location a profile in a scatter. - - Each point used to compute the slice are linked together. - """ - - def __init__(self, parentRoi): - items.Shape.__init__(self, "polygon") - color = colors.rgba(parentRoi.getColor()) - self.setColor(color) - self.setFill(True) - self.setOverlay(True) - self.setPoints([[0, 0], [0, 0]]) # Else it segfault - - self.__parentRoi = weakref.ref(parentRoi) - parentRoi.sigItemChanged.connect(self._updateAreaProperty) - parentRoi.sigRegionChanged.connect(self._updateArea) - parentRoi.sigProfilePropertyChanged.connect(self._updateArea) - parentRoi.sigPlotItemChanged.connect(self._updateArea) - - def getParentRoi(self): - if self.__parentRoi is None: - return None - parentRoi = self.__parentRoi() - if parentRoi is None: - self.__parentRoi = None - return parentRoi - - def _updateAreaProperty(self, event=None, checkVisibility=True): - parentRoi = self.sender() - if event == items.ItemChangedType.COLOR: - parentRoi._updateItemProperty(event, parentRoi, self) - elif event == items.ItemChangedType.VISIBLE: - if self.getPlotItem() is not None: - parentRoi._updateItemProperty(event, parentRoi, self) - - def _updateArea(self): - roi = self.getParentRoi() - item = roi.getPlotItem() - if item is None: - self.setVisible(False) - return - polylines = self._computePolylines(roi, item) - if polylines is None: - self.setVisible(False) - return - self.setVisible(True) - self.setLineStyle("--") - self.setPoints(polylines, copy=False) - - def _computePolylines(self, roi, item): - slicing = roi._getSlice(item) - if slicing is None: - return None - xx, yy, _values, _xx_error, _yy_error = item.getData(copy=False) - xx, yy = xx[slicing], yy[slicing] - polylines = numpy.array((xx, yy)).T - if len(polylines) == 0: - return None - return polylines - - -class _DefaultImageProfileRoiMixIn(core.ProfileRoiMixIn): - """Provide common behavior for silx default image profile ROI. - """ - - ITEM_KIND = items.ImageBase - - def __init__(self, parent=None): - core.ProfileRoiMixIn.__init__(self, parent=parent) - self.__method = "mean" - self.__width = 1 - self.sigRegionChanged.connect(self.__regionChanged) - self.sigPlotItemChanged.connect(self.__updateArea) - self.__area = _ImageProfileArea(self) - self.addItem(self.__area) - - def __regionChanged(self): - self.invalidateProfile() - self.__updateArea() - - def setProfileMethod(self, method): - """ - :param str method: method to compute the profile. Can be 'mean' or 'sum' - """ - if self.__method == method: - return - self.__method = method - self.invalidateProperties() - self.invalidateProfile() - - def getProfileMethod(self): - return self.__method - - def setProfileLineWidth(self, width): - if self.__width == width: - return - self.__width = width - self.__updateArea() - self.invalidateProperties() - self.invalidateProfile() - - def getProfileLineWidth(self): - return self.__width - - def __updateArea(self): - plotItem = self.getPlotItem() - if plotItem is None: - self.setLineStyle("-") - else: - self.setLineStyle("--") - - def _getRoiInfo(self): - """Wrapper to allow to reuse the previous Profile code. - - It would be good to remove it at one point. - """ - if isinstance(self, roi_items.HorizontalLineROI): - lineProjectionMode = 'X' - y = self.getPosition() - roiStart = (0, y) - roiEnd = (1, y) - elif isinstance(self, roi_items.VerticalLineROI): - lineProjectionMode = 'Y' - x = self.getPosition() - roiStart = (x, 0) - roiEnd = (x, 1) - elif isinstance(self, roi_items.LineROI): - lineProjectionMode = 'D' - roiStart, roiEnd = self.getEndPoints() - else: - assert False - - return roiStart, roiEnd, lineProjectionMode - - def computeProfile(self, item): - if not isinstance(item, items.ImageBase): - raise TypeError("Unexpected class %s" % type(item)) - - origin = item.getOrigin() - scale = item.getScale() - method = self.getProfileMethod() - lineWidth = self.getProfileLineWidth() - - def createProfile2(currentData): - coords, profile, _area, profileName, xLabel = core.createProfile( - roiInfo=self._getRoiInfo(), - currentData=currentData, - origin=origin, - scale=scale, - lineWidth=lineWidth, - method=method) - return coords, profile, profileName, xLabel - - currentData = item.getValueData(copy=False) - - yLabel = "%s" % str(method).capitalize() - coords, profile, title, xLabel = createProfile2(currentData) - title = title + "; width = %d" % lineWidth - - # Use the axis names from the original plot - profileManager = self.getProfileManager() - plot = profileManager.getPlotWidget() - title = _relabelAxes(plot, title) - xLabel = _relabelAxes(plot, xLabel) - - if isinstance(item, items.ImageRgba): - rgba = item.getData(copy=False) - _coords, r, _profileName, _xLabel = createProfile2(rgba[..., 0]) - _coords, g, _profileName, _xLabel = createProfile2(rgba[..., 1]) - _coords, b, _profileName, _xLabel = createProfile2(rgba[..., 2]) - if rgba.shape[-1] == 4: - _coords, a, _profileName, _xLabel = createProfile2(rgba[..., 3]) - else: - a = [None] - data = core.RgbaProfileData( - coords=coords, - profile=profile[0], - profile_r=r[0], - profile_g=g[0], - profile_b=b[0], - profile_a=a[0], - title=title, - xLabel=xLabel, - yLabel=yLabel, - ) - else: - data = core.CurveProfileData( - coords=coords, - profile=profile[0], - title=title, - xLabel=xLabel, - yLabel=yLabel, - ) - return data - - -class ProfileImageHorizontalLineROI(roi_items.HorizontalLineROI, - _DefaultImageProfileRoiMixIn): - """ROI for an horizontal profile at a location of an image""" - - ICON = 'shape-horizontal' - NAME = 'horizontal line profile' - - def __init__(self, parent=None): - roi_items.HorizontalLineROI.__init__(self, parent=parent) - _DefaultImageProfileRoiMixIn.__init__(self, parent=parent) - - -class ProfileImageVerticalLineROI(roi_items.VerticalLineROI, - _DefaultImageProfileRoiMixIn): - """ROI for a vertical profile at a location of an image""" - - ICON = 'shape-vertical' - NAME = 'vertical line profile' - - def __init__(self, parent=None): - roi_items.VerticalLineROI.__init__(self, parent=parent) - _DefaultImageProfileRoiMixIn.__init__(self, parent=parent) - - -class ProfileImageLineROI(roi_items.LineROI, - _DefaultImageProfileRoiMixIn): - """ROI for an image profile between 2 points. - - The X profile of this ROI is the projecting into one of the x/y axes, - using its scale and its orientation. - """ - - ICON = 'shape-diagonal' - NAME = 'line profile' - - def __init__(self, parent=None): - roi_items.LineROI.__init__(self, parent=parent) - _DefaultImageProfileRoiMixIn.__init__(self, parent=parent) - - -class ProfileImageDirectedLineROI(roi_items.LineROI, - _DefaultImageProfileRoiMixIn): - """ROI for an image profile between 2 points. - - The X profile of the line is displayed projected into the line itself, - using its scale and its orientation. It's the distance from the origin. - """ - - ICON = 'shape-diagonal-directed' - NAME = 'directed line profile' - - def __init__(self, parent=None): - roi_items.LineROI.__init__(self, parent=parent) - _DefaultImageProfileRoiMixIn.__init__(self, parent=parent) - self._handleStart.setSymbol('o') - - def computeProfile(self, item): - if not isinstance(item, items.ImageBase): - raise TypeError("Unexpected class %s" % type(item)) - - from silx.image.bilinear import BilinearImage - - origin = item.getOrigin() - scale = item.getScale() - method = self.getProfileMethod() - lineWidth = self.getProfileLineWidth() - currentData = item.getValueData(copy=False) - - roiInfo = self._getRoiInfo() - roiStart, roiEnd, _lineProjectionMode = roiInfo - - startPt = ((roiStart[1] - origin[1]) / scale[1], - (roiStart[0] - origin[0]) / scale[0]) - endPt = ((roiEnd[1] - origin[1]) / scale[1], - (roiEnd[0] - origin[0]) / scale[0]) - - if numpy.array_equal(startPt, endPt): - return None - - bilinear = BilinearImage(currentData) - profile = bilinear.profile_line( - (startPt[0] - 0.5, startPt[1] - 0.5), - (endPt[0] - 0.5, endPt[1] - 0.5), - lineWidth, - method=method) - - # Compute the line size - lineSize = numpy.sqrt((roiEnd[1] - roiStart[1]) ** 2 + - (roiEnd[0] - roiStart[0]) ** 2) - coords = numpy.linspace(0, lineSize, len(profile), - endpoint=True, - dtype=numpy.float32) - - title = _lineProfileTitle(*roiStart, *roiEnd) - title = title + "; width = %d" % lineWidth - xLabel = "√({xlabel}²+{ylabel}²)" - yLabel = str(method).capitalize() - - # Use the axis names from the original plot - profileManager = self.getProfileManager() - plot = profileManager.getPlotWidget() - xLabel = _relabelAxes(plot, xLabel) - title = _relabelAxes(plot, title) - - data = core.CurveProfileData( - coords=coords, - profile=profile, - title=title, - xLabel=xLabel, - yLabel=yLabel, - ) - return data - - -class _ProfileCrossROI(roi_items.HandleBasedROI, core.ProfileRoiMixIn): - - """ROI to manage a cross of profiles - - It is managed using 2 sub ROIs for vertical and horizontal. - """ - - _kind = "Cross" - """Label for this kind of ROI""" - - _plotShape = "point" - """Plot shape which is used for the first interaction""" - - def __init__(self, parent=None): - roi_items.HandleBasedROI.__init__(self, parent=parent) - core.ProfileRoiMixIn.__init__(self, parent=parent) - self.sigRegionChanged.connect(self.__regionChanged) - self.sigAboutToBeRemoved.connect(self.__aboutToBeRemoved) - self.__position = 0, 0 - self.__vline = None - self.__hline = None - self.__handle = self.addHandle() - self.__handleLabel = self.addLabelHandle() - self.__handleLabel.setText(self.getName()) - self.__inhibitReentance = utils.LockReentrant() - self.computeProfile = None - self.sigItemChanged.connect(self.__updateLineProperty) - - # Make sure the marker is over the ROIs - self.__handle.setZValue(1) - # Create the vline and the hline - self._createSubRois() - - @docstring(roi_items.HandleBasedROI) - def contains(self, position): - roiPos = self.getPosition() - return position[0] == roiPos[0] or position[1] == roiPos[1] - - def setFirstShapePoints(self, points): - pos = points[0] - self.setPosition(pos) - - def getPosition(self): - """Returns the position of this ROI - - :rtype: numpy.ndarray - """ - return self.__position - - def setPosition(self, pos): - """Set the position of this ROI - - :param numpy.ndarray pos: 2d-coordinate of this point - """ - self.__position = pos - with utils.blockSignals(self.__handle): - self.__handle.setPosition(*pos) - with utils.blockSignals(self.__handleLabel): - self.__handleLabel.setPosition(*pos) - self.sigRegionChanged.emit() - - def handleDragUpdated(self, handle, origin, previous, current): - if handle is self.__handle: - self.setPosition(current) - - def __updateLineProperty(self, event=None, checkVisibility=True): - if event == items.ItemChangedType.NAME: - self.__handleLabel.setText(self.getName()) - elif event in [items.ItemChangedType.COLOR, - items.ItemChangedType.VISIBLE]: - lines = [] - if self.__vline: - lines.append(self.__vline) - if self.__hline: - lines.append(self.__hline) - self._updateItemProperty(event, self, lines) - - def _createLines(self, parent): - """Inherit this function to return 2 ROI objects for respectivly - the horizontal, and the vertical lines.""" - raise NotImplementedError() - - def _setProfileManager(self, profileManager): - core.ProfileRoiMixIn._setProfileManager(self, profileManager) - # Connecting the vline and the hline - roiManager = profileManager.getRoiManager() - roiManager.addRoi(self.__vline) - roiManager.addRoi(self.__hline) - - def _createSubRois(self): - hline, vline = self._createLines(parent=None) - for i, line in enumerate([vline, hline]): - line.setPosition(self.__position[i]) - line.setEditable(True) - line.setSelectable(True) - line.setFocusProxy(self) - line.setName("") - self.__vline = vline - self.__hline = hline - vline.sigAboutToBeRemoved.connect(self.__vlineRemoved) - vline.sigRegionChanged.connect(self.__vlineRegionChanged) - hline.sigAboutToBeRemoved.connect(self.__hlineRemoved) - hline.sigRegionChanged.connect(self.__hlineRegionChanged) - - def _getLines(self): - return self.__hline, self.__vline - - def __regionChanged(self): - if self.__inhibitReentance.locked(): - return - x, y = self.getPosition() - hline, vline = self._getLines() - if hline is None: - return - with self.__inhibitReentance: - hline.setPosition(y) - vline.setPosition(x) - - def __vlineRegionChanged(self): - if self.__inhibitReentance.locked(): - return - pos = self.getPosition() - vline = self.__vline - pos = vline.getPosition(), pos[1] - with self.__inhibitReentance: - self.setPosition(pos) - - def __hlineRegionChanged(self): - if self.__inhibitReentance.locked(): - return - pos = self.getPosition() - hline = self.__hline - pos = pos[0], hline.getPosition() - with self.__inhibitReentance: - self.setPosition(pos) - - def __aboutToBeRemoved(self): - vline = self.__vline - hline = self.__hline - # Avoid side remove signals - if hline is not None: - hline.sigAboutToBeRemoved.disconnect(self.__hlineRemoved) - hline.sigRegionChanged.disconnect(self.__hlineRegionChanged) - if vline is not None: - vline.sigAboutToBeRemoved.disconnect(self.__vlineRemoved) - vline.sigRegionChanged.disconnect(self.__vlineRegionChanged) - # Clean up the child - profileManager = self.getProfileManager() - roiManager = profileManager.getRoiManager() - if hline is not None: - roiManager.removeRoi(hline) - self.__hline = None - if vline is not None: - roiManager.removeRoi(vline) - self.__vline = None - - def __hlineRemoved(self): - self.__lineRemoved(isHline=True) - - def __vlineRemoved(self): - self.__lineRemoved(isHline=False) - - def __lineRemoved(self, isHline): - """If any of the lines is removed: disconnect this objects, and let the - other one persist""" - hline, vline = self._getLines() - - hline.sigAboutToBeRemoved.disconnect(self.__hlineRemoved) - vline.sigAboutToBeRemoved.disconnect(self.__vlineRemoved) - hline.sigRegionChanged.disconnect(self.__hlineRegionChanged) - vline.sigRegionChanged.disconnect(self.__vlineRegionChanged) - - self.__hline = None - self.__vline = None - profileManager = self.getProfileManager() - roiManager = profileManager.getRoiManager() - if isHline: - self.__releaseLine(vline) - else: - self.__releaseLine(hline) - roiManager.removeRoi(self) - - def __releaseLine(self, line): - """Release the line in order to make it independent""" - line.setFocusProxy(None) - line.setName(self.getName()) - line.setEditable(self.isEditable()) - line.setSelectable(self.isSelectable()) - - -class ProfileImageCrossROI(_ProfileCrossROI): - """ROI to manage a cross of profiles - - It is managed using 2 sub ROIs for vertical and horizontal. - """ - - ICON = 'shape-cross' - NAME = 'cross profile' - ITEM_KIND = items.ImageBase - - def _createLines(self, parent): - vline = ProfileImageVerticalLineROI(parent=parent) - hline = ProfileImageHorizontalLineROI(parent=parent) - return hline, vline - - def setProfileMethod(self, method): - """ - :param str method: method to compute the profile. Can be 'mean' or 'sum' - """ - hline, vline = self._getLines() - hline.setProfileMethod(method) - vline.setProfileMethod(method) - self.invalidateProperties() - - def getProfileMethod(self): - hline, _vline = self._getLines() - return hline.getProfileMethod() - - def setProfileLineWidth(self, width): - hline, vline = self._getLines() - hline.setProfileLineWidth(width) - vline.setProfileLineWidth(width) - self.invalidateProperties() - - def getProfileLineWidth(self): - hline, _vline = self._getLines() - return hline.getProfileLineWidth() - - -class _DefaultScatterProfileRoiMixIn(core.ProfileRoiMixIn): - """Provide common behavior for silx default scatter profile ROI. - """ - - ITEM_KIND = items.Scatter - - def __init__(self, parent=None): - core.ProfileRoiMixIn.__init__(self, parent=parent) - self.__nPoints = 1024 - self.sigRegionChanged.connect(self.__regionChanged) - - def __regionChanged(self): - self.invalidateProfile() - - # Number of points - - def getNPoints(self): - """Returns the number of points of the profiles - - :rtype: int - """ - return self.__nPoints - - def setNPoints(self, npoints): - """Set the number of points of the profiles - - :param int npoints: - """ - npoints = int(npoints) - if npoints < 1: - raise ValueError("Unsupported number of points: %d" % npoints) - elif npoints != self.__nPoints: - self.__nPoints = npoints - self.invalidateProperties() - self.invalidateProfile() - - def _computeProfile(self, scatter, x0, y0, x1, y1): - """Compute corresponding profile - - :param float x0: Profile start point X coord - :param float y0: Profile start point Y coord - :param float x1: Profile end point X coord - :param float y1: Profile end point Y coord - :return: (points, values) profile data or None - """ - future = scatter._getInterpolator() - try: - interpolator = future.result() - except CancelledError: - return None - if interpolator is None: - return None # Cannot init an interpolator - - nPoints = self.getNPoints() - points = numpy.transpose(( - numpy.linspace(x0, x1, nPoints, endpoint=True), - numpy.linspace(y0, y1, nPoints, endpoint=True))) - - values = interpolator(points) - - if not numpy.any(numpy.isfinite(values)): - return None # Profile outside convex hull - - return points, values - - def computeProfile(self, item): - """Update profile according to current ROI""" - if not isinstance(item, items.Scatter): - raise TypeError("Unexpected class %s" % type(item)) - - # Get end points - if isinstance(self, roi_items.LineROI): - points = self.getEndPoints() - x0, y0 = points[0] - x1, y1 = points[1] - elif isinstance(self, (roi_items.VerticalLineROI, roi_items.HorizontalLineROI)): - profileManager = self.getProfileManager() - plot = profileManager.getPlotWidget() - - if isinstance(self, roi_items.HorizontalLineROI): - x0, x1 = plot.getXAxis().getLimits() - y0 = y1 = self.getPosition() - - elif isinstance(self, roi_items.VerticalLineROI): - x0 = x1 = self.getPosition() - y0, y1 = plot.getYAxis().getLimits() - else: - raise RuntimeError('Unsupported ROI for profile: {}'.format(self.__class__)) - - if x1 < x0 or (x1 == x0 and y1 < y0): - # Invert points - x0, y0, x1, y1 = x1, y1, x0, y0 - - profile = self._computeProfile(item, x0, y0, x1, y1) - if profile is None: - return None - - title = _lineProfileTitle(x0, y0, x1, y1) - points = profile[0] - values = profile[1] - - if (numpy.abs(points[-1, 0] - points[0, 0]) > - numpy.abs(points[-1, 1] - points[0, 1])): - xProfile = points[:, 0] - xLabel = '{xlabel}' - else: - xProfile = points[:, 1] - xLabel = '{ylabel}' - - # Use the axis names from the original - profileManager = self.getProfileManager() - plot = profileManager.getPlotWidget() - title = _relabelAxes(plot, title) - xLabel = _relabelAxes(plot, xLabel) - - data = core.CurveProfileData( - coords=xProfile, - profile=values, - title=title, - xLabel=xLabel, - yLabel='Profile', - ) - return data - - -class ProfileScatterHorizontalLineROI(roi_items.HorizontalLineROI, - _DefaultScatterProfileRoiMixIn): - """ROI for an horizontal profile at a location of a scatter""" - - ICON = 'shape-horizontal' - NAME = 'horizontal line profile' - - def __init__(self, parent=None): - roi_items.HorizontalLineROI.__init__(self, parent=parent) - _DefaultScatterProfileRoiMixIn.__init__(self, parent=parent) - - -class ProfileScatterVerticalLineROI(roi_items.VerticalLineROI, - _DefaultScatterProfileRoiMixIn): - """ROI for an horizontal profile at a location of a scatter""" - - ICON = 'shape-vertical' - NAME = 'vertical line profile' - - def __init__(self, parent=None): - roi_items.VerticalLineROI.__init__(self, parent=parent) - _DefaultScatterProfileRoiMixIn.__init__(self, parent=parent) - - -class ProfileScatterLineROI(roi_items.LineROI, - _DefaultScatterProfileRoiMixIn): - """ROI for an horizontal profile at a location of a scatter""" - - ICON = 'shape-diagonal' - NAME = 'line profile' - - def __init__(self, parent=None): - roi_items.LineROI.__init__(self, parent=parent) - _DefaultScatterProfileRoiMixIn.__init__(self, parent=parent) - - -class ProfileScatterCrossROI(_ProfileCrossROI): - """ROI to manage a cross of profiles for scatters. - """ - - ICON = 'shape-cross' - NAME = 'cross profile' - ITEM_KIND = items.Scatter - - def _createLines(self, parent): - vline = ProfileScatterVerticalLineROI(parent=parent) - hline = ProfileScatterHorizontalLineROI(parent=parent) - return hline, vline - - def getNPoints(self): - """Returns the number of points of the profiles - - :rtype: int - """ - hline, _vline = self._getLines() - return hline.getNPoints() - - def setNPoints(self, npoints): - """Set the number of points of the profiles - - :param int npoints: - """ - hline, vline = self._getLines() - hline.setNPoints(npoints) - vline.setNPoints(npoints) - self.invalidateProperties() - - -class _DefaultScatterProfileSliceRoiMixIn(core.ProfileRoiMixIn): - """Default ROI to allow to slice in the scatter data.""" - - ITEM_KIND = items.Scatter - - def __init__(self, parent=None): - core.ProfileRoiMixIn.__init__(self, parent=parent) - self.__area = _SliceProfileArea(self) - self.addItem(self.__area) - self.sigRegionChanged.connect(self._regionChanged) - self.sigPlotItemChanged.connect(self._updateArea) - - def _regionChanged(self): - self.invalidateProfile() - self._updateArea() - - def _updateArea(self): - plotItem = self.getPlotItem() - if plotItem is None: - self.setLineStyle("-") - else: - self.setLineStyle("--") - - def _getSlice(self, item): - position = self.getPosition() - bounds = item.getCurrentVisualizationParameter(items.Scatter.VisualizationParameter.GRID_BOUNDS) - if isinstance(self, roi_items.HorizontalLineROI): - axis = 1 - elif isinstance(self, roi_items.VerticalLineROI): - axis = 0 - else: - assert False - if position < bounds[0][axis] or position > bounds[1][axis]: - # ROI outside of the scatter bound - return None - - major_order = item.getCurrentVisualizationParameter(items.Scatter.VisualizationParameter.GRID_MAJOR_ORDER) - assert major_order == 'row' - max_grid_yy, max_grid_xx = item.getCurrentVisualizationParameter(items.Scatter.VisualizationParameter.GRID_SHAPE) - - xx, yy, _values, _xx_error, _yy_error = item.getData(copy=False) - if isinstance(self, roi_items.HorizontalLineROI): - axis = yy - max_grid_first = max_grid_yy - max_grid_second = max_grid_xx - major_axis = major_order == 'column' - elif isinstance(self, roi_items.VerticalLineROI): - axis = xx - max_grid_first = max_grid_xx - max_grid_second = max_grid_yy - major_axis = major_order == 'row' - else: - assert False - - def argnearest(array, value): - array = numpy.abs(array - value) - return numpy.argmin(array) - - if major_axis: - # slice in the middle of the scatter - start = max_grid_second // 2 * max_grid_first - vslice = axis[start:start + max_grid_second] - index = argnearest(vslice, position) - slicing = slice(index, None, max_grid_first) - else: - # slice in the middle of the scatter - vslice = axis[max_grid_second // 2::max_grid_second] - index = argnearest(vslice, position) - start = index * max_grid_second - slicing = slice(start, start + max_grid_second) - - return slicing - - def computeProfile(self, item): - if not isinstance(item, items.Scatter): - raise TypeError("Unsupported %s item" % type(item)) - - slicing = self._getSlice(item) - if slicing is None: - # ROI out of bounds - return None - - _xx, _yy, values, _xx_error, _yy_error = item.getData(copy=False) - profile = values[slicing] - - if isinstance(self, roi_items.HorizontalLineROI): - title = "Horizontal slice" - xLabel = "{xlabel} index" - elif isinstance(self, roi_items.VerticalLineROI): - title = "Vertical slice" - xLabel = "{ylabel} index" - else: - assert False - - # Use the axis names from the original plot - profileManager = self.getProfileManager() - plot = profileManager.getPlotWidget() - xLabel = _relabelAxes(plot, xLabel) - - data = core.CurveProfileData( - coords=numpy.arange(len(profile)), - profile=profile, - title=title, - xLabel=xLabel, - yLabel="Profile", - ) - return data - - -class ProfileScatterHorizontalSliceROI(roi_items.HorizontalLineROI, - _DefaultScatterProfileSliceRoiMixIn): - """ROI for an horizontal profile at a location of a scatter - using data slicing. - """ - - ICON = 'slice-horizontal' - NAME = 'horizontal data slice profile' - - def __init__(self, parent=None): - roi_items.HorizontalLineROI.__init__(self, parent=parent) - _DefaultScatterProfileSliceRoiMixIn.__init__(self, parent=parent) - - -class ProfileScatterVerticalSliceROI(roi_items.VerticalLineROI, - _DefaultScatterProfileSliceRoiMixIn): - """ROI for a vertical profile at a location of a scatter - using data slicing. - """ - - ICON = 'slice-vertical' - NAME = 'vertical data slice profile' - - def __init__(self, parent=None): - roi_items.VerticalLineROI.__init__(self, parent=parent) - _DefaultScatterProfileSliceRoiMixIn.__init__(self, parent=parent) - - -class ProfileScatterCrossSliceROI(_ProfileCrossROI): - """ROI to manage a cross of slicing profiles on scatters. - """ - - ICON = 'slice-cross' - NAME = 'cross data slice profile' - ITEM_KIND = items.Scatter - - def _createLines(self, parent): - vline = ProfileScatterVerticalSliceROI(parent=parent) - hline = ProfileScatterHorizontalSliceROI(parent=parent) - return hline, vline - - -class _DefaultImageStackProfileRoiMixIn(_DefaultImageProfileRoiMixIn): - - ITEM_KIND = items.ImageStack - - def __init__(self, parent=None): - super(_DefaultImageStackProfileRoiMixIn, self).__init__(parent=parent) - self.__profileType = "1D" - """Kind of profile""" - - def getProfileType(self): - return self.__profileType - - def setProfileType(self, kind): - assert kind in ["1D", "2D"] - if self.__profileType == kind: - return - self.__profileType = kind - self.invalidateProperties() - self.invalidateProfile() - - def computeProfile(self, item): - if not isinstance(item, items.ImageStack): - raise TypeError("Unexpected class %s" % type(item)) - - kind = self.getProfileType() - if kind == "1D": - result = _DefaultImageProfileRoiMixIn.computeProfile(self, item) - # z = item.getStackPosition() - return result - - assert kind == "2D" - - def createProfile2(currentData): - coords, profile, _area, profileName, xLabel = core.createProfile( - roiInfo=self._getRoiInfo(), - currentData=currentData, - origin=origin, - scale=scale, - lineWidth=self.getProfileLineWidth(), - method=method) - return coords, profile, profileName, xLabel - - currentData = numpy.array(item.getStackData(copy=False)) - origin = item.getOrigin() - scale = item.getScale() - colormap = item.getColormap() - method = self.getProfileMethod() - - coords, profile, profileName, xLabel = createProfile2(currentData) - - data = core.ImageProfileData( - coords=coords, - profile=profile, - title=profileName, - xLabel=xLabel, - yLabel="Profile", - colormap=colormap, - ) - return data - - -class ProfileImageStackHorizontalLineROI(roi_items.HorizontalLineROI, - _DefaultImageStackProfileRoiMixIn): - """ROI for an horizontal profile at a location of a stack of images""" - - ICON = 'shape-horizontal' - NAME = 'horizontal line profile' - - def __init__(self, parent=None): - roi_items.HorizontalLineROI.__init__(self, parent=parent) - _DefaultImageStackProfileRoiMixIn.__init__(self, parent=parent) - - -class ProfileImageStackVerticalLineROI(roi_items.VerticalLineROI, - _DefaultImageStackProfileRoiMixIn): - """ROI for an vertical profile at a location of a stack of images""" - - ICON = 'shape-vertical' - NAME = 'vertical line profile' - - def __init__(self, parent=None): - roi_items.VerticalLineROI.__init__(self, parent=parent) - _DefaultImageStackProfileRoiMixIn.__init__(self, parent=parent) - - -class ProfileImageStackLineROI(roi_items.LineROI, - _DefaultImageStackProfileRoiMixIn): - """ROI for an vertical profile at a location of a stack of images""" - - ICON = 'shape-diagonal' - NAME = 'line profile' - - def __init__(self, parent=None): - roi_items.LineROI.__init__(self, parent=parent) - _DefaultImageStackProfileRoiMixIn.__init__(self, parent=parent) - - -class ProfileImageStackCrossROI(ProfileImageCrossROI): - """ROI for an vertical profile at a location of a stack of images""" - - ICON = 'shape-cross' - NAME = 'cross profile' - ITEM_KIND = items.ImageStack - - def _createLines(self, parent): - vline = ProfileImageStackVerticalLineROI(parent=parent) - hline = ProfileImageStackHorizontalLineROI(parent=parent) - return hline, vline - - def getProfileType(self): - hline, _vline = self._getLines() - return hline.getProfileType() - - def setProfileType(self, kind): - hline, vline = self._getLines() - hline.setProfileType(kind) - vline.setProfileType(kind) - self.invalidateProperties() diff --git a/silx/gui/plot/tools/profile/toolbar.py b/silx/gui/plot/tools/profile/toolbar.py deleted file mode 100644 index 4a9a195..0000000 --- a/silx/gui/plot/tools/profile/toolbar.py +++ /dev/null @@ -1,172 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018-2019 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. -# -# ###########################################################################*/ -"""This module provides tool bar helper. -""" - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "28/06/2018" - - -import logging -import weakref - -from silx.gui import qt -from silx.gui.widgets.MultiModeAction import MultiModeAction -from . import manager -from .. import roi as roi_mdl -from silx.gui.plot import items - - -_logger = logging.getLogger(__name__) - - -class ProfileToolBar(qt.QToolBar): - """Tool bar to provide profile for a plot. - - It is an helper class. For a dedicated application it would be better to - use an own tool bar in order in order have more flexibility. - """ - def __init__(self, parent=None, plot=None): - super(ProfileToolBar, self).__init__(parent=parent) - self.__scheme = None - self.__manager = None - self.__plot = weakref.ref(plot) - self.__multiAction = None - - def getPlotWidget(self): - """The :class:`~silx.gui.plot.PlotWidget` associated to the toolbar. - - :rtype: Union[~silx.gui.plot.PlotWidget,None] - """ - if self.__plot is None: - return None - plot = self.__plot() - if self.__plot is None: - self.__plot = None - return plot - - def setScheme(self, scheme): - """Initialize the tool bar using a configuration scheme. - - It have to be done once and only once. - - :param str scheme: One of "scatter", "image", "imagestack" - """ - assert self.__scheme is None - self.__scheme = scheme - - plot = self.getPlotWidget() - self.__manager = manager.ProfileManager(self, plot) - - if scheme == "image": - self.__manager.setItemType(image=True) - self.__manager.setActiveItemTracking(True) - - multiAction = MultiModeAction(self) - self.addAction(multiAction) - for action in self.__manager.createImageActions(self): - multiAction.addAction(action) - self.__multiAction = multiAction - - cleanAction = self.__manager.createClearAction(self) - self.addAction(cleanAction) - editorAction = self.__manager.createEditorAction(self) - self.addAction(editorAction) - - plot.sigActiveImageChanged.connect(self._activeImageChanged) - self._activeImageChanged() - - elif scheme == "scatter": - self.__manager.setItemType(scatter=True) - self.__manager.setActiveItemTracking(True) - - multiAction = MultiModeAction(self) - self.addAction(multiAction) - for action in self.__manager.createScatterActions(self): - multiAction.addAction(action) - for action in self.__manager.createScatterSliceActions(self): - multiAction.addAction(action) - self.__multiAction = multiAction - - cleanAction = self.__manager.createClearAction(self) - self.addAction(cleanAction) - editorAction = self.__manager.createEditorAction(self) - self.addAction(editorAction) - - plot.sigActiveScatterChanged.connect(self._activeScatterChanged) - self._activeScatterChanged() - - elif scheme == "imagestack": - self.__manager.setItemType(image=True) - self.__manager.setActiveItemTracking(True) - - multiAction = MultiModeAction(self) - self.addAction(multiAction) - for action in self.__manager.createImageStackActions(self): - multiAction.addAction(action) - self.__multiAction = multiAction - - cleanAction = self.__manager.createClearAction(self) - self.addAction(cleanAction) - editorAction = self.__manager.createEditorAction(self) - self.addAction(editorAction) - - plot.sigActiveImageChanged.connect(self._activeImageChanged) - self._activeImageChanged() - - else: - raise ValueError("Toolbar scheme %s unsupported" % scheme) - - def _setRoiActionEnabled(self, itemKind, enabled): - for action in self.__multiAction.getMenu().actions(): - if not isinstance(action, roi_mdl.CreateRoiModeAction): - continue - roiClass = action.getRoiClass() - if issubclass(itemKind, roiClass.ITEM_KIND): - action.setEnabled(enabled) - - def _activeImageChanged(self, previous=None, legend=None): - """Handle active image change to toggle actions""" - if legend is None: - self._setRoiActionEnabled(items.ImageStack, False) - self._setRoiActionEnabled(items.ImageBase, False) - else: - plot = self.getPlotWidget() - image = plot.getActiveImage() - # Disable for empty image - enabled = image.getData(copy=False).size > 0 - self._setRoiActionEnabled(type(image), enabled) - - def _activeScatterChanged(self, previous=None, legend=None): - """Handle active scatter change to toggle actions""" - if legend is None: - self._setRoiActionEnabled(items.Scatter, False) - else: - plot = self.getPlotWidget() - scatter = plot.getActiveScatter() - # Disable for empty image - enabled = scatter.getValueData(copy=False).size > 0 - self._setRoiActionEnabled(type(scatter), enabled) |