Source code for silx.gui.plot.Profile
# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2004-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.
#
# ###########################################################################*/
"""Utility functions, toolbars and actions  to create profile on images
and stacks of images"""
__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel", "H. Payno"]
__license__ = "MIT"
__date__ = "12/04/2019"
import weakref
import numpy
from silx.image.bilinear import BilinearImage
from .. import icons
from .. import qt
from . import items
from ..colors import cursorColorForColormap
from . import actions
from .PlotToolButtons import ProfileToolButton, ProfileOptionToolButton
from .ProfileMainWindow import ProfileMainWindow
from silx.utils.deprecation import deprecated
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'
    :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')
    # 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 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'
    :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)
        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 = 'Y = %g' % yMin
        else:
            profileName = 'Y = [%g, %g]' % (yMin, yMax)
        xLabel = 'X'
    elif lineProjectionMode == 'Y':  # Vertical profile on the whole image
        profile, area = _alignedFullProfile(currentData3D,
                                            origin, scale,
                                            roiStart[0], roiWidth,
                                            axis=1,
                                            method=method)
        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 = 'X = %g' % xMin
        else:
            profileName = 'X = [%g, %g]' % (xMin, xMax)
        xLabel = 'Y'
    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
                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))
                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
            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 = 'X = %g; Y = [%g, %g]' % (x0, y0, y1)
            coords = numpy.arange(len(profile[0]), dtype=numpy.float32)
            coords = coords * scale[1] + y0
            xLabel = 'Y'
        elif startPt[0] == endPt[0]:
            profileName = 'Y = %g; X = [%g, %g]' % (y0, x0, x1)
            coords = numpy.arange(len(profile[0]), dtype=numpy.float32)
            coords = coords * scale[0] + x0
            xLabel = 'X'
        else:
            m = (y1 - y0) / (x1 - x0)
            b = y0 - m * x0
            profileName = 'y = %g * x %+g ; width=%d' % (m, b, roiWidth)
            coords = numpy.linspace(x0, x1, len(profile[0]),
                                    endpoint=True,
                                    dtype=numpy.float32)
            xLabel = 'X'
    return coords, profile, area, profileName, xLabel
# ProfileToolBar ##############################################################
[docs]class ProfileToolBar(qt.QToolBar):
    """QToolBar providing profile tools operating on a :class:`PlotWindow`.
    Attributes:
    - plot: Associated :class:`PlotWindow` on which the profile line is drawn.
    - actionGroup: :class:`QActionGroup` of available actions.
    To run the following sample code, a QApplication must be initialized.
    First, create a PlotWindow and add a :class:`ProfileToolBar`.
    >>> from silx.gui.plot import PlotWindow
    >>> from silx.gui.plot.Profile import ProfileToolBar
    >>> plot = PlotWindow()  # Create a PlotWindow
    >>> toolBar = ProfileToolBar(plot=plot)  # Create a profile toolbar
    >>> plot.addToolBar(toolBar)  # Add it to plot
    >>> plot.show()  # To display the PlotWindow with the profile toolbar
    :param plot: :class:`PlotWindow` instance on which to operate.
    :param profileWindow: Plot widget instance where to
                          display the profile curve or None to create one.
    :param str title: See :class:`QToolBar`.
    :param parent: See :class:`QToolBar`.
    """
    # TODO Make it a QActionGroup instead of a QToolBar
    _POLYGON_LEGEND = '__ProfileToolBar_ROI_Polygon'
    DEFAULT_PROF_METHOD = 'mean'
    def __init__(self, parent=None, plot=None, profileWindow=None,
                 title='Profile Selection'):
        super(ProfileToolBar, self).__init__(title, parent)
        assert plot is not None
        self._plotRef = weakref.ref(plot)
        self._overlayColor = None
        self._defaultOverlayColor = 'red'  # update when active image change
        self._method = self.DEFAULT_PROF_METHOD
        self._roiInfo = None  # Store start and end points and type of ROI
        self._profileWindow = profileWindow
        """User provided plot widget in which the profile curve is plotted.
        None if no custom profile plot was provided."""
        self._profileMainWindow = None
        """Main window providing 2 profile plot widgets for 1D or 2D profiles.
        The window provides two public methods
            - :meth:`setProfileDimensions`
            - :meth:`getPlot`: return handle on the actual plot widget
              currently being used
        None if the user specified a custom profile plot window.
        """
        if self._profileWindow is None:
            backend = type(plot._backend)
            self._profileMainWindow = ProfileMainWindow(self, backend=backend)
        # Actions
        self._browseAction = actions.mode.ZoomModeAction(self.plot, parent=self)
        self._browseAction.setVisible(False)
        self.hLineAction = qt.QAction(icons.getQIcon('shape-horizontal'),
                                      'Horizontal Profile Mode',
                                      self)
        self.hLineAction.setToolTip(
            'Enables horizontal profile selection mode')
        self.hLineAction.setCheckable(True)
        self.hLineAction.toggled[bool].connect(self._hLineActionToggled)
        self.vLineAction = qt.QAction(icons.getQIcon('shape-vertical'),
                                      'Vertical Profile Mode',
                                      self)
        self.vLineAction.setToolTip(
            'Enables vertical profile selection mode')
        self.vLineAction.setCheckable(True)
        self.vLineAction.toggled[bool].connect(self._vLineActionToggled)
        self.lineAction = qt.QAction(icons.getQIcon('shape-diagonal'),
                                     'Free Line Profile Mode',
                                     self)
        self.lineAction.setToolTip(
            'Enables line profile selection mode')
        self.lineAction.setCheckable(True)
        self.lineAction.toggled[bool].connect(self._lineActionToggled)
        self.clearAction = qt.QAction(icons.getQIcon('profile-clear'),
                                      'Clear Profile',
                                      self)
        self.clearAction.setToolTip(
            'Clear the profile Region of interest')
        self.clearAction.setCheckable(False)
        self.clearAction.triggered.connect(self.clearProfile)
        # ActionGroup
        self.actionGroup = qt.QActionGroup(self)
        self.actionGroup.addAction(self._browseAction)
        self.actionGroup.addAction(self.hLineAction)
        self.actionGroup.addAction(self.vLineAction)
        self.actionGroup.addAction(self.lineAction)
        # Add actions to ToolBar
        self.addAction(self._browseAction)
        self.addAction(self.hLineAction)
        self.addAction(self.vLineAction)
        self.addAction(self.lineAction)
        self.addAction(self.clearAction)
        # Add width spin box to toolbar
        self.addWidget(qt.QLabel('W:'))
        self.lineWidthSpinBox = qt.QSpinBox(self)
        self.lineWidthSpinBox.setRange(1, 1000)
        self.lineWidthSpinBox.setValue(1)
        self.lineWidthSpinBox.valueChanged[int].connect(
            self._lineWidthSpinBoxValueChangedSlot)
        self.addWidget(self.lineWidthSpinBox)
        self.methodsButton = ProfileOptionToolButton(parent=self, plot=self)
        self.__profileOptionToolAction = self.addWidget(self.methodsButton)
        # TODO: add connection with the signal
        self.methodsButton.sigMethodChanged.connect(self.setProfileMethod)
        self.plot.sigInteractiveModeChanged.connect(
            self._interactiveModeChanged)
        # Enable toolbar only if there is an active image
        self.setEnabled(self.plot.getActiveImage(just_legend=True) is not None)
        self.plot.sigActiveImageChanged.connect(
            self._activeImageChanged)
        # listen to the profile window signals to clear profile polygon on close
        if self.getProfileMainWindow() is not None:
            self.getProfileMainWindow().sigClose.connect(self.clearProfile)
    @property
    def plot(self):
        """The :class:`.PlotWidget` associated to the toolbar."""
        return self._plotRef()
    @property
    @deprecated(since_version="0.6.0")
    def browseAction(self):
        return self._browseAction
    @property
    @deprecated(replacement="getProfilePlot", since_version="0.5.0")
    def profileWindow(self):
        return self.getProfilePlot()
[docs]    def getProfilePlot(self):
        """Return plot widget in which the profile curve or the
        profile image is plotted.
        """
        if self.getProfileMainWindow() is not None:
            return self.getProfileMainWindow().getPlot()
        # in case the user provided a custom plot for profiles
        return self._profileWindow
[docs]    def getProfileMainWindow(self):
        """Return window containing the profile curve widget.
        This can return *None* if a custom profile plot window was
        specified in the constructor.
        """
        return self._profileMainWindow
    def _activeImageChanged(self, previous, legend):
        """Handle active image change: toggle enabled toolbar, update curve"""
        if legend is None:
            self.setEnabled(False)
        else:
            activeImage = self.plot.getActiveImage()
            # Disable for empty image
            self.setEnabled(activeImage.getData(copy=False).size > 0)
            # Update default profile color
            if isinstance(activeImage, items.ColormapMixIn):
                self._defaultOverlayColor = cursorColorForColormap(
                    activeImage.getColormap()['name'])
            else:
                self._defaultOverlayColor = 'black'
            self.updateProfile()
    def _lineWidthSpinBoxValueChangedSlot(self, value):
        """Listen to ROI width widget to refresh ROI and profile"""
        self.updateProfile()
    def _interactiveModeChanged(self, source):
        """Handle plot interactive mode changed:
        If changed from elsewhere, disable drawing tool
        """
        if source is not self:
            self.clearProfile()
            # Uncheck all drawing profile modes
            self.hLineAction.setChecked(False)
            self.vLineAction.setChecked(False)
            self.lineAction.setChecked(False)
            if self.getProfileMainWindow() is not None:
                self.getProfileMainWindow().hide()
    def _hLineActionToggled(self, checked):
        """Handle horizontal line profile action toggle"""
        if checked:
            self.plot.setInteractiveMode('draw', shape='hline',
                                         color=None, source=self)
            self.plot.sigPlotSignal.connect(self._plotWindowSlot)
        else:
            self.plot.sigPlotSignal.disconnect(self._plotWindowSlot)
    def _vLineActionToggled(self, checked):
        """Handle vertical line profile action toggle"""
        if checked:
            self.plot.setInteractiveMode('draw', shape='vline',
                                         color=None, source=self)
            self.plot.sigPlotSignal.connect(self._plotWindowSlot)
        else:
            self.plot.sigPlotSignal.disconnect(self._plotWindowSlot)
    def _lineActionToggled(self, checked):
        """Handle line profile action toggle"""
        if checked:
            self.plot.setInteractiveMode('draw', shape='line',
                                         color=None, source=self)
            self.plot.sigPlotSignal.connect(self._plotWindowSlot)
        else:
            self.plot.sigPlotSignal.disconnect(self._plotWindowSlot)
    def _plotWindowSlot(self, event):
        """Listen to Plot to handle drawing events to refresh ROI and profile.
        """
        if event['event'] not in ('drawingProgress', 'drawingFinished'):
            return
        checkedAction = self.actionGroup.checkedAction()
        if checkedAction == self.hLineAction:
            lineProjectionMode = 'X'
        elif checkedAction == self.vLineAction:
            lineProjectionMode = 'Y'
        elif checkedAction == self.lineAction:
            lineProjectionMode = 'D'
        else:
            return
        roiStart, roiEnd = event['points'][0], event['points'][1]
        self._roiInfo = roiStart, roiEnd, lineProjectionMode
        self.updateProfile()
    @property
    def overlayColor(self):
        """The color to use for the ROI.
        If set to None (the default), the overlay color is adapted to the
        active image colormap and changes if the active image colormap changes.
        """
        return self._overlayColor or self._defaultOverlayColor
    @overlayColor.setter
    def overlayColor(self, color):
        self._overlayColor = color
        self.updateProfile()
[docs]    def clearProfile(self):
        """Remove profile curve and profile area."""
        self._roiInfo = None
        self.updateProfile()
[docs]    def updateProfile(self):
        """Update the displayed profile and profile ROI.
        This uses the current active image of the plot and the current ROI.
        """
        image = self.plot.getActiveImage()
        if image is None:
            return
        # Clean previous profile area, and previous curve
        self.plot.remove(self._POLYGON_LEGEND, kind='item')
        self.getProfilePlot().clear()
        self.getProfilePlot().setGraphTitle('')
        self.getProfilePlot().getXAxis().setLabel('X')
        self.getProfilePlot().getYAxis().setLabel('Y')
        self._createProfile(currentData=image.getData(copy=False),
                            origin=image.getOrigin(),
                            scale=image.getScale(),
                            colormap=None,  # Not used for 2D data
                            z=image.getZValue(),
                            method=self.getProfileMethod())
    def _createProfile(self, currentData, origin, scale, colormap, z, method):
        """Create the profile line for the the given image.
        :param numpy.ndarray currentData: the image or the 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 dict colormap: The colormap to use
        :param int z: The z layer of the image
        """
        if self._roiInfo is None:
            return
        coords, profile, area, profileName, xLabel = createProfile(
            roiInfo=self._roiInfo,
            currentData=currentData,
            origin=origin,
            scale=scale,
            lineWidth=self.lineWidthSpinBox.value(),
            method=method)
        profilePlot = self.getProfilePlot()
        profilePlot.setGraphTitle(profileName)
        profilePlot.getXAxis().setLabel(xLabel)
        dataIs3D = len(currentData.shape) > 2
        if dataIs3D:
            profileScale = (coords[-1] - coords[0]) / profile.shape[1], 1
            profilePlot.addImage(profile,
                                 legend=profileName,
                                 colormap=colormap,
                                 origin=(coords[0], 0),
                                 scale=profileScale)
            profilePlot.getYAxis().setLabel("Frame index (depth)")
        else:
            profilePlot.addCurve(coords,
                                 profile[0],
                                 legend=profileName,
                                 color=self.overlayColor)
        self.plot.addItem(area[0], area[1],
                          legend=self._POLYGON_LEGEND,
                          color=self.overlayColor,
                          shape='polygon', fill=True,
                          replace=False, z=z + 1)
        self._showProfileMainWindow()
    def _showProfileMainWindow(self):
        """If profile window was created by this toolbar,
        try to avoid overlapping with the toolbar's parent window.
        """
        profileMainWindow = self.getProfileMainWindow()
        if profileMainWindow is not None:
            winGeom = self.window().frameGeometry()
            qapp = qt.QApplication.instance()
            screenGeom = qapp.desktop().availableGeometry(self)
            spaceOnLeftSide = winGeom.left()
            spaceOnRightSide = screenGeom.width() - winGeom.right()
            profileWindowWidth = profileMainWindow.frameGeometry().width()
            if (profileWindowWidth < spaceOnRightSide):
                # Place profile on the right
                profileMainWindow.move(winGeom.right(), winGeom.top())
            elif(profileWindowWidth < spaceOnLeftSide):
                # Place profile on the left
                profileMainWindow.move(
                    max(0, winGeom.left() - profileWindowWidth), winGeom.top())
            profileMainWindow.show()
            profileMainWindow.raise_()
        else:
            self.getProfilePlot().show()
            self.getProfilePlot().raise_()
[docs]    def hideProfileWindow(self):
        """Hide profile window.
        """
        # this method is currently only used by StackView when the perspective
        # is changed
        if self.getProfileMainWindow() is not None:
            self.getProfileMainWindow().hide()
    def setProfileMethod(self, method):
        assert method in ('sum', 'mean')
        self._method = method
        self.updateProfile()
    def getProfileMethod(self):
        return self._method
    def getProfileOptionToolAction(self):
        return self.__profileOptionToolAction
[docs]class Profile3DToolBar(ProfileToolBar):
    def __init__(self, parent=None, stackview=None,
                 title='Profile Selection'):
        """QToolBar providing profile tools for an image or a stack of images.
        :param parent: the parent QWidget
        :param stackview: :class:`StackView` instance on which to operate.
        :param str title: See :class:`QToolBar`.
        :param parent: See :class:`QToolBar`.
        """
        # TODO: add param profileWindow (specify the plot used for profiles)
        super(Profile3DToolBar, self).__init__(parent=parent,
                                               plot=stackview.getPlot(),
                                               title=title)
        self.stackView = stackview
        """:class:`StackView` instance"""
        self.profile3dAction = ProfileToolButton(
            parent=self, plot=self.plot)
        self.profile3dAction.computeProfileIn2D()
        self.profile3dAction.setVisible(True)
        self.addWidget(self.profile3dAction)
        self.profile3dAction.sigDimensionChanged.connect(self._setProfileType)
        # create the 3D toolbar
        self._profileType = None
        self._setProfileType(2)
        self._method3D = 'sum'
    def _setProfileType(self, dimensions):
        """Set the profile type: "1D" for a curve (profile on a single image)
        or "2D" for an image (profile on a stack of images).
        :param int dimensions: 1 for a "1D" profile or 2 for a "2D" profile
        """
        # fixme this assumes that we created _profileMainWindow
        self._profileType = "1D" if dimensions == 1 else "2D"
        self.getProfileMainWindow().setProfileType(self._profileType)
        self.updateProfile()
[docs]    def updateProfile(self):
        """Method overloaded from :class:`ProfileToolBar`,
        to pass the stack of images instead of just the active image.
        In 1D profile mode, use the regular parent method.
        """
        if self._profileType == "1D":
            super(Profile3DToolBar, self).updateProfile()
        elif self._profileType == "2D":
            stackData = self.stackView.getCurrentView(copy=False,
                                                      returnNumpyArray=True)
            if stackData is None:
                return
            self.plot.remove(self._POLYGON_LEGEND, kind='item')
            self.getProfilePlot().clear()
            self.getProfilePlot().setGraphTitle('')
            self.getProfilePlot().getXAxis().setLabel('X')
            self.getProfilePlot().getYAxis().setLabel('Y')
            self._createProfile(currentData=stackData[0],
                                origin=stackData[1]['origin'],
                                scale=stackData[1]['scale'],
                                colormap=stackData[1]['colormap'],
                                z=stackData[1]['z'],
                                method=self.getProfileMethod())
        else:
            raise ValueError(
                    "Profile type must be 1D or 2D, not %s" % self._profileType)
    def setProfileMethod(self, method):
        assert method in ('sum', 'mean')
        self._method3D = method
        self.updateProfile()
    def getProfileMethod(self):
        return self._method3D