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