summaryrefslogtreecommitdiff
path: root/silx/gui/plot/Profile.py
diff options
context:
space:
mode:
authorPicca Frédéric-Emmanuel <picca@synchrotron-soleil.fr>2017-08-18 14:48:52 +0200
committerPicca Frédéric-Emmanuel <picca@synchrotron-soleil.fr>2017-08-18 14:48:52 +0200
commitf7bdc2acff3c13a6d632c28c4569690ab106eed7 (patch)
tree9d67cdb7152ee4e711379e03fe0546c7c3b97303 /silx/gui/plot/Profile.py
Import Upstream version 0.5.0+dfsg
Diffstat (limited to 'silx/gui/plot/Profile.py')
-rw-r--r--silx/gui/plot/Profile.py741
1 files changed, 741 insertions, 0 deletions
diff --git a/silx/gui/plot/Profile.py b/silx/gui/plot/Profile.py
new file mode 100644
index 0000000..a11b3f0
--- /dev/null
+++ b/silx/gui/plot/Profile.py
@@ -0,0 +1,741 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2004-2017 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__ = "24/04/2017"
+
+
+import numpy
+
+from silx.image.bilinear import BilinearImage
+
+from .. import icons
+from .. import qt
+from . import items
+from .Colors import cursorColorForColormap
+from .PlotActions import PlotAction
+from .PlotToolButtons import ProfileToolButton
+from .ProfileMainWindow import ProfileMainWindow
+
+from silx.utils.decorators import deprecated
+
+
+def _alignedFullProfile(data, origin, scale, position, roiWidth, axis):
+ """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.
+ :return: profile image + effective ROI area corners in plot coords
+ """
+ assert axis in (0, 1)
+ assert len(data.shape) == 3
+
+ # 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:
+ profile = data[:, max(0, start):min(end, height), :].mean(
+ axis=1, dtype=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):
+ """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.
+ :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]
+
+ 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)
+
+ imgProfile = numpy.mean(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):
+ """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
+ :return: `profile, area, profileName, xLabel`, where:
+ - 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 is a string describing the meaning of the X axis on the
+ profile plot ("rows", "columns", "distance")
+
+ :rtype: tuple(ndarray, (ndarray, ndarray), str, 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)
+
+ yMin, yMax = min(area[1]), max(area[1]) - 1
+ if roiWidth <= 1:
+ profileName = 'Y = %g' % yMin
+ else:
+ profileName = 'Y = [%g, %g]' % (yMin, yMax)
+ xLabel = 'Columns'
+
+ elif lineProjectionMode == 'Y': # Vertical profile on the whole image
+ profile, area = _alignedFullProfile(currentData3D,
+ origin, scale,
+ roiStart[0], roiWidth,
+ axis=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 = 'Rows'
+
+ 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)
+
+ 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)
+
+ # 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))
+ 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
+ startPt = startPt[0] - 0.5 * dRow, startPt[1] - 0.5 * dCol
+ endPt = 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((startPt[1] - 0.5 * roiWidth * dCol,
+ startPt[1] + 0.5 * roiWidth * dCol,
+ endPt[1] + 0.5 * roiWidth * dCol,
+ endPt[1] - 0.5 * roiWidth * dCol),
+ dtype=numpy.float32) * scale[0] + origin[0],
+ numpy.array((startPt[0] - 0.5 * roiWidth * dRow,
+ startPt[0] + 0.5 * roiWidth * dRow,
+ endPt[0] + 0.5 * roiWidth * dRow,
+ endPt[0] - 0.5 * roiWidth * dRow),
+ dtype=numpy.float32) * scale[1] + origin[1])
+
+ y0, x0 = startPt
+ y1, x1 = endPt
+ if x1 == x0 or y1 == y0:
+ profileName = 'From (%g, %g) to (%g, %g)' % (x0, y0, x1, y1)
+ else:
+ m = (y1 - y0) / (x1 - x0)
+ b = y0 - m * x0
+ profileName = 'y = %g * x %+g ; width=%d' % (m, b, roiWidth)
+ xLabel = 'Distance'
+
+ return profile, area, profileName, xLabel
+
+
+# ProfileToolBar ##############################################################
+
+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'
+
+ def __init__(self, parent=None, plot=None, profileWindow=None,
+ title='Profile Selection'):
+ super(ProfileToolBar, self).__init__(title, parent)
+ assert plot is not None
+ self.plot = plot
+
+ self._overlayColor = None
+ self._defaultOverlayColor = 'red' # update when active image change
+
+ 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:
+ self._profileMainWindow = ProfileMainWindow(self)
+
+ # Actions
+ self.browseAction = qt.QAction(
+ icons.getQIcon('normal'),
+ 'Browsing Mode', None)
+ self.browseAction.setToolTip(
+ 'Enables zooming interaction mode')
+ self.browseAction.setCheckable(True)
+ self.browseAction.triggered[bool].connect(self._browseActionTriggered)
+
+ self.hLineAction = qt.QAction(
+ icons.getQIcon('shape-horizontal'),
+ 'Horizontal Profile Mode', None)
+ 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', None)
+ 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', None)
+ 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', None)
+ 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)
+
+ self.browseAction.setChecked(True)
+
+ # 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(0, 1000)
+ self.lineWidthSpinBox.setValue(1)
+ self.lineWidthSpinBox.valueChanged[int].connect(
+ self._lineWidthSpinBoxValueChangedSlot)
+ self.addWidget(self.lineWidthSpinBox)
+
+ 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
+ @deprecated(replacement="getProfilePlot", since_version="0.5.0")
+ def profileWindow(self):
+ return self.getProfilePlot()
+
+ 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
+
+ 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"""
+ self.setEnabled(legend is not None)
+ if legend is not None:
+ # Update default profile color
+ activeImage = self.plot.getActiveImage()
+ 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.browseAction.setChecked(True)
+
+ 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 _browseActionTriggered(self, checked):
+ """Handle browse action mode triggered by user."""
+ if checked:
+ self.clearProfile()
+ self.plot.setInteractiveMode('zoom', source=self)
+ if self.getProfileMainWindow() is not None:
+ self.getProfileMainWindow().hide()
+
+ 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()
+
+ def clearProfile(self):
+ """Remove profile curve and profile area."""
+ self._roiInfo = None
+ self.updateProfile()
+
+ 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().setGraphXLabel('X')
+ self.getProfilePlot().setGraphYLabel('Y')
+
+ self._createProfile(currentData=image.getData(copy=False),
+ origin=image.getOrigin(),
+ scale=image.getScale(),
+ colormap=None, # Not used for 2D data
+ z=image.getZValue())
+
+ def _createProfile(self, currentData, origin, scale, colormap, z):
+ """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
+
+ profile, area, profileName, xLabel = createProfile(
+ roiInfo=self._roiInfo,
+ currentData=currentData,
+ origin=origin,
+ scale=scale,
+ lineWidth=self.lineWidthSpinBox.value())
+
+ self.getProfilePlot().setGraphTitle(profileName)
+
+ dataIs3D = len(currentData.shape) > 2
+ if dataIs3D:
+ self.getProfilePlot().addImage(profile,
+ legend=profileName,
+ xlabel=xLabel,
+ ylabel="Frame index (depth)",
+ colormap=colormap)
+ else:
+ coords = numpy.arange(len(profile[0]), dtype=numpy.float32)
+ self.getProfilePlot().addCurve(coords,
+ profile[0],
+ legend=profileName,
+ xlabel=xLabel,
+ 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 or
+ spaceOnRightSide > spaceOnLeftSide):
+ # Place profile on the right
+ profileMainWindow.move(winGeom.right(), winGeom.top())
+ else:
+ # Not enough place on the right, place profile on the left
+ profileMainWindow.move(
+ max(0, winGeom.left() - profileWindowWidth), winGeom.top())
+
+ profileMainWindow.show()
+ else:
+ self.getProfilePlot().show()
+
+ 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()
+
+
+class Profile3DToolBar(ProfileToolBar):
+ def __init__(self, parent=None, plot=None, title='Profile Selection'):
+ """QToolBar providing profile tools for an image or a stack of images.
+
+ :param parent: the parent QWidget
+ :param plot: :class:`PlotWindow` 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=plot,
+ title=title)
+
+ 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)
+
+ 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()
+
+ 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.plot.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().setGraphXLabel('X')
+ self.getProfilePlot().setGraphYLabel('Y')
+
+ self._createProfile(currentData=stackData[0],
+ origin=stackData[1]['origin'],
+ scale=stackData[1]['scale'],
+ colormap=stackData[1]['colormap'],
+ z=stackData[1]['z'])
+ else:
+ raise ValueError(
+ "Profile type must be 1D or 2D, not %s" % self._profileType)