# 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 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__ = "03/04/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)) if isinstance(item, items.ImageData): currentData = item.getData(copy=False) elif isinstance(item, items.ImageRgba): rgba = item.getData(copy=False) currentData = rgba[..., 0] 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 if isinstance(item, items.ImageData): currentData = item.getData(copy=False) elif isinstance(item, items.ImageRgba): rgba = item.getData(copy=False) is_uint8 = rgba.dtype.type == numpy.uint8 # luminosity if is_uint8: rgba = rgba.astype(numpy.float) currentData = 0.21 * rgba[..., 0] + 0.72 * rgba[..., 1] + 0.07 * rgba[..., 2] 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.getData(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()