Source code for silx.gui.plot.items.roi

# /*##########################################################################
#
# Copyright (c) 2018-2022 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ###########################################################################*/
"""This module provides ROI item for the :class:`~silx.gui.plot.PlotWidget`.

.. inheritance-diagram::
   silx.gui.plot.items.roi
   :parts: 1
"""

__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "28/06/2018"


import logging
import numpy
from typing import Tuple

from ... import utils
from .. import items
from ...colors import rgba
from silx.image.shapes import Polygon
from silx.image._boundingbox import _BoundingBox
from ....utils.proxy import docstring
from ..utils.intersections import segments_intersection
from ._roi_base import _RegionOfInterestBase

# He following imports have to be exposed by this module
from ._roi_base import RegionOfInterest
from ._roi_base import HandleBasedROI
from ._arc_roi import ArcROI  # noqa
from ._band_roi import BandROI  # noqa
from ._roi_base import InteractionModeMixIn  # noqa
from ._roi_base import RoiInteractionMode  # noqa


logger = logging.getLogger(__name__)


[docs] class PointROI(RegionOfInterest, items.SymbolMixIn): """A ROI identifying a point in a 2D plot.""" ICON = "add-shape-point" NAME = "point markers" SHORT_NAME = "point" """Metadata for this kind of ROI""" _plotShape = "point" """Plot shape which is used for the first interaction""" _DEFAULT_SYMBOL = "+" """Default symbol of the PointROI It overwrite the `SymbolMixIn` class attribte. """ def __init__(self, parent=None): RegionOfInterest.__init__(self, parent=parent) items.SymbolMixIn.__init__(self) self._marker = items.Marker() self._marker.sigItemChanged.connect(self._pointPositionChanged) self._marker.setSymbol(self._DEFAULT_SYMBOL) self._marker.sigDragStarted.connect(self._editingStarted) self._marker.sigDragFinished.connect(self._editingFinished) self.addItem(self._marker)
[docs] def setFirstShapePoints(self, points): self.setPosition(points[0])
def _updated(self, event=None, checkVisibility=True): if event == items.ItemChangedType.EDITABLE: self._marker._setDraggable(self.isEditable()) elif event in [items.ItemChangedType.VISIBLE, items.ItemChangedType.SELECTABLE]: self._updateItemProperty(event, self, self._marker) super(PointROI, self)._updated(event, checkVisibility) def _updateText(self, text: str): self._marker.setText(text) def _updatedStyle(self, event, style): self._marker.setColor(style.getColor())
[docs] def getPosition(self) -> Tuple[float, float]: """Returns the position of this ROI""" return self._marker.getPosition()
[docs] def setPosition(self, pos): """Set the position of this ROI :param pos: 2d-coordinate of this point """ self._marker.setPosition(*pos)
[docs] @docstring(_RegionOfInterestBase) def contains(self, position): roiPos = self.getPosition() return position[0] == roiPos[0] and position[1] == roiPos[1]
def _pointPositionChanged(self, event): """Handle position changed events of the marker""" if event is items.ItemChangedType.POSITION: self.sigRegionChanged.emit() def __str__(self): params = "%f %f" % self.getPosition() return "%s(%s)" % (self.__class__.__name__, params)
[docs] class CrossROI(HandleBasedROI, items.LineMixIn): """A ROI identifying a point in a 2D plot and displayed as a cross""" ICON = "add-shape-cross" NAME = "cross marker" SHORT_NAME = "cross" """Metadata for this kind of ROI""" _plotShape = "point" """Plot shape which is used for the first interaction""" def __init__(self, parent=None): HandleBasedROI.__init__(self, parent=parent) items.LineMixIn.__init__(self) self._handle = self.addHandle() self._handle.sigItemChanged.connect(self._handlePositionChanged) self._handleLabel = self.addLabelHandle() self._vmarker = self.addUserHandle(items.YMarker()) self._vmarker._setSelectable(False) self._vmarker._setDraggable(False) self._vmarker.setPosition(*self.getPosition()) self._hmarker = self.addUserHandle(items.XMarker()) self._hmarker._setSelectable(False) self._hmarker._setDraggable(False) self._hmarker.setPosition(*self.getPosition()) def _updated(self, event=None, checkVisibility=True): if event in [items.ItemChangedType.VISIBLE]: markers = (self._vmarker, self._hmarker) self._updateItemProperty(event, self, markers) super(CrossROI, self)._updated(event, checkVisibility) def _updateText(self, text): self._handleLabel.setText(text) def _updatedStyle(self, event, style): super(CrossROI, self)._updatedStyle(event, style) for marker in [self._vmarker, self._hmarker]: marker.setColor(style.getColor()) marker.setLineStyle(style.getLineStyle()) marker.setLineWidth(style.getLineWidth())
[docs] def setFirstShapePoints(self, points): pos = points[0] self.setPosition(pos)
[docs] def getPosition(self) -> Tuple[float, float]: """Returns the position of this ROI""" return self._handle.getPosition()
[docs] def setPosition(self, pos: Tuple[float, float]): """Set the position of this ROI :param pos: 2d-coordinate of this point """ self._handle.setPosition(*pos)
def _handlePositionChanged(self, event): """Handle center marker position updates""" if event is items.ItemChangedType.POSITION: position = self.getPosition() self._handleLabel.setPosition(*position) self._vmarker.setPosition(*position) self._hmarker.setPosition(*position) self.sigRegionChanged.emit()
[docs] @docstring(HandleBasedROI) def contains(self, position): roiPos = self.getPosition() return position[0] == roiPos[0] or position[1] == roiPos[1]
[docs] class LineROI(HandleBasedROI, items.LineMixIn): """A ROI identifying a line in a 2D plot. This ROI provides 1 anchor for each boundary of the line, plus an center in the center to translate the full ROI. """ ICON = "add-shape-diagonal" NAME = "line ROI" SHORT_NAME = "line" """Metadata for this kind of ROI""" _plotShape = "line" """Plot shape which is used for the first interaction""" def __init__(self, parent=None): HandleBasedROI.__init__(self, parent=parent) items.LineMixIn.__init__(self) self._handleStart = self.addHandle() self._handleEnd = self.addHandle() self._handleCenter = self.addTranslateHandle() self._handleLabel = self.addLabelHandle() shape = items.Shape("polylines") shape.setPoints([[0, 0], [0, 0]]) shape.setColor(rgba(self.getColor())) shape.setFill(False) shape.setOverlay(True) shape.setLineStyle(self.getLineStyle()) shape.setLineWidth(self.getLineWidth()) self.__shape = shape self.addItem(shape) def _updated(self, event=None, checkVisibility=True): if event == items.ItemChangedType.VISIBLE: self._updateItemProperty(event, self, self.__shape) super(LineROI, self)._updated(event, checkVisibility) def _updatedStyle(self, event, style: items.CurveStyle): super(LineROI, self)._updatedStyle(event, style) self.__shape.setColor(style.getColor()) self.__shape.setLineStyle(style.getLineStyle()) self.__shape.setLineWidth(style.getLineWidth()) self.__shape.setLineGapColor(style.getLineGapColor())
[docs] def setFirstShapePoints(self, points): assert len(points) == 2 self.setEndPoints(points[0], points[1])
def _updateText(self, text): self._handleLabel.setText(text)
[docs] def setEndPoints(self, startPoint: numpy.ndarray, endPoint: numpy.ndarray): """Set this line location using the ending points :param numpy.ndarray startPoint: Staring bounding point of the line :param numpy.ndarray endPoint: Ending bounding point of the line """ if not numpy.array_equal((startPoint, endPoint), self.getEndPoints()): self.__updateEndPoints(startPoint, endPoint)
def __updateEndPoints(self, startPoint: numpy.ndarray, endPoint: numpy.ndarray): """Update marker and shape to match given end points :param numpy.ndarray startPoint: Staring bounding point of the line :param numpy.ndarray endPoint: Ending bounding point of the line """ startPoint = numpy.array(startPoint) endPoint = numpy.array(endPoint) center = (startPoint + endPoint) * 0.5 with utils.blockSignals(self._handleStart): self._handleStart.setPosition(startPoint[0], startPoint[1]) with utils.blockSignals(self._handleEnd): self._handleEnd.setPosition(endPoint[0], endPoint[1]) with utils.blockSignals(self._handleCenter): self._handleCenter.setPosition(center[0], center[1]) with utils.blockSignals(self._handleLabel): self._handleLabel.setPosition(center[0], center[1]) line = numpy.array((startPoint, endPoint)) self.__shape.setPoints(line) self.sigRegionChanged.emit()
[docs] def getEndPoints(self): """Returns bounding points of this ROI. :rtype: Tuple(numpy.ndarray,numpy.ndarray) """ startPoint = numpy.array(self._handleStart.getPosition()) endPoint = numpy.array(self._handleEnd.getPosition()) return (startPoint, endPoint)
[docs] def handleDragUpdated(self, handle, origin, previous, current): if handle is self._handleStart: _start, end = self.getEndPoints() self.__updateEndPoints(current, end) elif handle is self._handleEnd: start, _end = self.getEndPoints() self.__updateEndPoints(start, current) elif handle is self._handleCenter: start, end = self.getEndPoints() delta = current - previous start += delta end += delta self.setEndPoints(start, end)
[docs] @docstring(_RegionOfInterestBase) def contains(self, position): bottom_left = position[0], position[1] bottom_right = position[0] + 1, position[1] top_left = position[0], position[1] + 1 top_right = position[0] + 1, position[1] + 1 points = self.__shape.getPoints() line_pt1 = points[0] line_pt2 = points[1] bb1 = _BoundingBox.from_points(points) if not bb1.contains(position): return False return ( segments_intersection( seg1_start_pt=line_pt1, seg1_end_pt=line_pt2, seg2_start_pt=bottom_left, seg2_end_pt=bottom_right, ) or segments_intersection( seg1_start_pt=line_pt1, seg1_end_pt=line_pt2, seg2_start_pt=bottom_right, seg2_end_pt=top_right, ) or segments_intersection( seg1_start_pt=line_pt1, seg1_end_pt=line_pt2, seg2_start_pt=top_right, seg2_end_pt=top_left, ) or segments_intersection( seg1_start_pt=line_pt1, seg1_end_pt=line_pt2, seg2_start_pt=top_left, seg2_end_pt=bottom_left, ) ) is not None
def __str__(self): start, end = self.getEndPoints() params = start[0], start[1], end[0], end[1] params = "start: %f %f; end: %f %f" % params return "%s(%s)" % (self.__class__.__name__, params)
[docs] class HorizontalLineROI(RegionOfInterest, items.LineMixIn): """A ROI identifying an horizontal line in a 2D plot.""" ICON = "add-shape-horizontal" NAME = "horizontal line ROI" SHORT_NAME = "hline" """Metadata for this kind of ROI""" _plotShape = "hline" """Plot shape which is used for the first interaction""" def __init__(self, parent=None): RegionOfInterest.__init__(self, parent=parent) items.LineMixIn.__init__(self) self._marker = items.YMarker() self._marker.sigItemChanged.connect(self._linePositionChanged) self._marker.sigDragStarted.connect(self._editingStarted) self._marker.sigDragFinished.connect(self._editingFinished) self.addItem(self._marker) def _updated(self, event=None, checkVisibility=True): if event == items.ItemChangedType.EDITABLE: self._marker._setDraggable(self.isEditable()) elif event in [items.ItemChangedType.VISIBLE, items.ItemChangedType.SELECTABLE]: self._updateItemProperty(event, self, self._marker) super(HorizontalLineROI, self)._updated(event, checkVisibility) def _updateText(self, text: str): self._marker.setText(text) def _updatedStyle(self, event, style): self._marker.setColor(style.getColor()) self._marker.setLineStyle(style.getLineStyle()) self._marker.setLineWidth(style.getLineWidth())
[docs] def setFirstShapePoints(self, points): pos = points[0, 1] if pos == self.getPosition(): return self.setPosition(pos)
[docs] def getPosition(self) -> float: """Returns the position of this line if the horizontal axis""" pos = self._marker.getPosition() return pos[1]
[docs] def setPosition(self, pos: float): """Set the position of this ROI :param pos: Horizontal position of this line """ self._marker.setPosition(0, pos)
[docs] @docstring(_RegionOfInterestBase) def contains(self, position): return position[1] == self.getPosition()
def _linePositionChanged(self, event): """Handle position changed events of the marker""" if event is items.ItemChangedType.POSITION: self.sigRegionChanged.emit() def __str__(self): params = "y: %f" % self.getPosition() return "%s(%s)" % (self.__class__.__name__, params)
[docs] class VerticalLineROI(RegionOfInterest, items.LineMixIn): """A ROI identifying a vertical line in a 2D plot.""" ICON = "add-shape-vertical" NAME = "vertical line ROI" SHORT_NAME = "vline" """Metadata for this kind of ROI""" _plotShape = "vline" """Plot shape which is used for the first interaction""" def __init__(self, parent=None): RegionOfInterest.__init__(self, parent=parent) items.LineMixIn.__init__(self) self._marker = items.XMarker() self._marker.sigItemChanged.connect(self._linePositionChanged) self._marker.sigDragStarted.connect(self._editingStarted) self._marker.sigDragFinished.connect(self._editingFinished) self.addItem(self._marker) def _updated(self, event=None, checkVisibility=True): if event == items.ItemChangedType.EDITABLE: self._marker._setDraggable(self.isEditable()) elif event in [items.ItemChangedType.VISIBLE, items.ItemChangedType.SELECTABLE]: self._updateItemProperty(event, self, self._marker) super(VerticalLineROI, self)._updated(event, checkVisibility) def _updateText(self, text: str): self._marker.setText(text) def _updatedStyle(self, event, style): self._marker.setColor(style.getColor()) self._marker.setLineStyle(style.getLineStyle()) self._marker.setLineWidth(style.getLineWidth())
[docs] def setFirstShapePoints(self, points): pos = points[0, 0] self.setPosition(pos)
[docs] def getPosition(self) -> float: """Returns the position of this line if the horizontal axis""" pos = self._marker.getPosition() return pos[0]
[docs] def setPosition(self, pos: float): """Set the position of this ROI :param float pos: Horizontal position of this line """ self._marker.setPosition(pos, 0)
[docs] @docstring(RegionOfInterest) def contains(self, position): return position[0] == self.getPosition()
def _linePositionChanged(self, event): """Handle position changed events of the marker""" if event is items.ItemChangedType.POSITION: self.sigRegionChanged.emit() def __str__(self): params = "x: %f" % self.getPosition() return "%s(%s)" % (self.__class__.__name__, params)
[docs] class RectangleROI(HandleBasedROI, items.LineMixIn): """A ROI identifying a rectangle in a 2D plot. This ROI provides 1 anchor for each corner, plus an anchor in the center to translate the full ROI. """ ICON = "add-shape-rectangle" NAME = "rectangle ROI" SHORT_NAME = "rectangle" """Metadata for this kind of ROI""" _plotShape = "rectangle" """Plot shape which is used for the first interaction""" def __init__(self, parent=None): HandleBasedROI.__init__(self, parent=parent) items.LineMixIn.__init__(self) self._handleTopLeft = self.addHandle() self._handleTopRight = self.addHandle() self._handleBottomLeft = self.addHandle() self._handleBottomRight = self.addHandle() self._handleCenter = self.addTranslateHandle() self._handleLabel = self.addLabelHandle() shape = items.Shape("rectangle") shape.setPoints([[0, 0], [0, 0]]) shape.setFill(False) shape.setOverlay(True) shape.setLineStyle(self.getLineStyle()) shape.setLineWidth(self.getLineWidth()) shape.setColor(rgba(self.getColor())) self.__shape = shape self.addItem(shape) def _updated(self, event=None, checkVisibility=True): if event in [items.ItemChangedType.VISIBLE]: self._updateItemProperty(event, self, self.__shape) super(RectangleROI, self)._updated(event, checkVisibility) def _updatedStyle(self, event, style): super(RectangleROI, self)._updatedStyle(event, style) self.__shape.setColor(style.getColor()) self.__shape.setLineStyle(style.getLineStyle()) self.__shape.setLineWidth(style.getLineWidth()) self.__shape.setLineGapColor(style.getLineGapColor())
[docs] def setFirstShapePoints(self, points): assert len(points) == 2 self._setBound(points)
def _setBound(self, points): """Initialize the rectangle from a bunch of points""" top = max(points[:, 1]) bottom = min(points[:, 1]) left = min(points[:, 0]) right = max(points[:, 0]) size = right - left, top - bottom self._updateGeometry(origin=(left, bottom), size=size) def _updateText(self, text): self._handleLabel.setText(text)
[docs] def getCenter(self): """Returns the central point of this rectangle :rtype: numpy.ndarray([float,float]) """ pos = self._handleCenter.getPosition() return numpy.array(pos)
[docs] def getOrigin(self): """Returns the corner point with the smaller coordinates :rtype: numpy.ndarray([float,float]) """ pos = self._handleBottomLeft.getPosition() return numpy.array(pos)
[docs] def getSize(self): """Returns the size of this rectangle :rtype: numpy.ndarray([float,float]) """ vmin = self._handleBottomLeft.getPosition() vmax = self._handleTopRight.getPosition() vmin, vmax = numpy.array(vmin), numpy.array(vmax) return vmax - vmin
[docs] def setOrigin(self, position): """Set the origin position of this ROI :param numpy.ndarray position: Location of the smaller corner of the ROI """ size = self.getSize() self.setGeometry(origin=position, size=size)
[docs] def setSize(self, size): """Set the size of this ROI :param numpy.ndarray size: Size of the center of the ROI """ origin = self.getOrigin() self.setGeometry(origin=origin, size=size)
[docs] def setCenter(self, position): """Set the size of this ROI :param numpy.ndarray position: Location of the center of the ROI """ size = self.getSize() self.setGeometry(center=position, size=size)
[docs] def setGeometry(self, origin=None, size=None, center=None): """Set the geometry of the ROI""" if ( (origin is None or numpy.array_equal(origin, self.getOrigin())) and (center is None or numpy.array_equal(center, self.getCenter())) and numpy.array_equal(size, self.getSize()) ): return # Nothing has changed self._updateGeometry(origin, size, center)
def _updateGeometry(self, origin=None, size=None, center=None): """Forced update of the geometry of the ROI""" if origin is not None: origin = numpy.array(origin) size = numpy.array(size) points = numpy.array([origin, origin + size]) center = origin + size * 0.5 elif center is not None: center = numpy.array(center) size = numpy.array(size) points = numpy.array([center - size * 0.5, center + size * 0.5]) else: raise ValueError("Origin or center expected") with utils.blockSignals(self._handleBottomLeft): self._handleBottomLeft.setPosition(points[0, 0], points[0, 1]) with utils.blockSignals(self._handleBottomRight): self._handleBottomRight.setPosition(points[1, 0], points[0, 1]) with utils.blockSignals(self._handleTopLeft): self._handleTopLeft.setPosition(points[0, 0], points[1, 1]) with utils.blockSignals(self._handleTopRight): self._handleTopRight.setPosition(points[1, 0], points[1, 1]) with utils.blockSignals(self._handleCenter): self._handleCenter.setPosition(center[0], center[1]) with utils.blockSignals(self._handleLabel): self._handleLabel.setPosition(points[0, 0], points[0, 1]) self.__shape.setPoints(points) self.sigRegionChanged.emit()
[docs] @docstring(HandleBasedROI) def contains(self, position): assert isinstance(position, (tuple, list, numpy.array)) points = self.__shape.getPoints() bb1 = _BoundingBox.from_points(points) return bb1.contains(position)
[docs] def handleDragUpdated(self, handle, origin, previous, current): if handle is self._handleCenter: # It is the center anchor size = self.getSize() self._updateGeometry(center=current, size=size) else: opposed = { self._handleBottomLeft: self._handleTopRight, self._handleTopRight: self._handleBottomLeft, self._handleBottomRight: self._handleTopLeft, self._handleTopLeft: self._handleBottomRight, } handle2 = opposed[handle] current2 = handle2.getPosition() points = numpy.array([current, current2]) # Switch handles if they were crossed by interaction if ( self._handleBottomLeft.getXPosition() > self._handleBottomRight.getXPosition() ): self._handleBottomLeft, self._handleBottomRight = ( self._handleBottomRight, self._handleBottomLeft, ) if self._handleTopLeft.getXPosition() > self._handleTopRight.getXPosition(): self._handleTopLeft, self._handleTopRight = ( self._handleTopRight, self._handleTopLeft, ) if ( self._handleBottomLeft.getYPosition() > self._handleTopLeft.getYPosition() ): self._handleBottomLeft, self._handleTopLeft = ( self._handleTopLeft, self._handleBottomLeft, ) if ( self._handleBottomRight.getYPosition() > self._handleTopRight.getYPosition() ): self._handleBottomRight, self._handleTopRight = ( self._handleTopRight, self._handleBottomRight, ) self._setBound(points)
def __str__(self): origin = self.getOrigin() w, h = self.getSize() params = origin[0], origin[1], w, h params = "origin: %f %f; width: %f; height: %f" % params return "%s(%s)" % (self.__class__.__name__, params)
[docs] class CircleROI(HandleBasedROI, items.LineMixIn): """A ROI identifying a circle in a 2D plot. This ROI provides 1 anchor at the center to translate the circle, and one anchor on the perimeter to change the radius. """ ICON = "add-shape-circle" NAME = "circle ROI" SHORT_NAME = "circle" """Metadata for this kind of ROI""" _kind = "Circle" """Label for this kind of ROI""" _plotShape = "line" """Plot shape which is used for the first interaction""" def __init__(self, parent=None): items.LineMixIn.__init__(self) HandleBasedROI.__init__(self, parent=parent) self._handlePerimeter = self.addHandle() self._handleCenter = self.addTranslateHandle() self._handleCenter.sigItemChanged.connect(self._centerPositionChanged) self._handleLabel = self.addLabelHandle() shape = items.Shape("polygon") shape.setPoints([[0, 0], [0, 0]]) shape.setColor(rgba(self.getColor())) shape.setFill(False) shape.setOverlay(True) shape.setLineStyle(self.getLineStyle()) shape.setLineWidth(self.getLineWidth()) self.__shape = shape self.addItem(shape) self.__radius = 0 def _updated(self, event=None, checkVisibility=True): if event == items.ItemChangedType.VISIBLE: self._updateItemProperty(event, self, self.__shape) super(CircleROI, self)._updated(event, checkVisibility) def _updatedStyle(self, event, style): super(CircleROI, self)._updatedStyle(event, style) self.__shape.setColor(style.getColor()) self.__shape.setLineStyle(style.getLineStyle()) self.__shape.setLineWidth(style.getLineWidth()) self.__shape.setLineGapColor(style.getLineGapColor())
[docs] def setFirstShapePoints(self, points): assert len(points) == 2 self._setRay(points)
def _setRay(self, points): """Initialize the circle from the center point and a perimeter point.""" center = points[0] radius = numpy.linalg.norm(points[0] - points[1]) self.setGeometry(center=center, radius=radius) def _updateText(self, text): self._handleLabel.setText(text)
[docs] def getCenter(self): """Returns the central point of this rectangle :rtype: numpy.ndarray([float,float]) """ pos = self._handleCenter.getPosition() return numpy.array(pos)
[docs] def getRadius(self): """Returns the radius of this circle :rtype: float """ return self.__radius
[docs] def setCenter(self, position): """Set the center point of this ROI :param numpy.ndarray position: Location of the center of the circle """ self._handleCenter.setPosition(*position)
[docs] def setRadius(self, radius): """Set the size of this ROI :param float size: Radius of the circle """ radius = float(radius) if radius != self.__radius: self.__radius = radius self._updateGeometry()
[docs] def setGeometry(self, center, radius): """Set the geometry of the ROI""" if numpy.array_equal(center, self.getCenter()): self.setRadius(radius) else: self.__radius = float(radius) # Update radius directly self.setCenter(center) # Calls _updateGeometry
def _updateGeometry(self): """Update the handles and shape according to given parameters""" center = self.getCenter() perimeter_point = numpy.array([center[0] + self.__radius, center[1]]) self._handlePerimeter.setPosition(perimeter_point[0], perimeter_point[1]) self._handleLabel.setPosition(center[0], center[1]) nbpoints = 27 angles = numpy.arange(nbpoints) * 2.0 * numpy.pi / nbpoints circleShape = numpy.array( (numpy.cos(angles) * self.__radius, numpy.sin(angles) * self.__radius) ).T circleShape += center self.__shape.setPoints(circleShape) self.sigRegionChanged.emit() def _centerPositionChanged(self, event): """Handle position changed events of the center marker""" if event is items.ItemChangedType.POSITION: self._updateGeometry()
[docs] def handleDragUpdated(self, handle, origin, previous, current): if handle is self._handlePerimeter: center = self.getCenter() self.setRadius(numpy.linalg.norm(center - current))
[docs] @docstring(HandleBasedROI) def contains(self, position): return numpy.linalg.norm(self.getCenter() - position) <= self.getRadius()
def __str__(self): center = self.getCenter() radius = self.getRadius() params = center[0], center[1], radius params = "center: %f %f; radius: %f;" % params return "%s(%s)" % (self.__class__.__name__, params)
[docs] class EllipseROI(HandleBasedROI, items.LineMixIn): """A ROI identifying an oriented ellipse in a 2D plot. This ROI provides 1 anchor at the center to translate the circle, and two anchors on the perimeter to modify the major-radius and minor-radius. These two anchors also allow to change the orientation. """ ICON = "add-shape-ellipse" NAME = "ellipse ROI" SHORT_NAME = "ellipse" """Metadata for this kind of ROI""" _plotShape = "line" """Plot shape which is used for the first interaction""" def __init__(self, parent=None): items.LineMixIn.__init__(self) HandleBasedROI.__init__(self, parent=parent) self._handleAxis0 = self.addHandle() self._handleAxis1 = self.addHandle() self._handleCenter = self.addTranslateHandle() self._handleCenter.sigItemChanged.connect(self._centerPositionChanged) self._handleLabel = self.addLabelHandle() shape = items.Shape("polygon") shape.setPoints([[0, 0], [0, 0]]) shape.setColor(rgba(self.getColor())) shape.setFill(False) shape.setOverlay(True) shape.setLineStyle(self.getLineStyle()) shape.setLineWidth(self.getLineWidth()) self.__shape = shape self.addItem(shape) self._radius = 0.0, 0.0 self._orientation = ( 0.0 # angle in radians between the X-axis and the _handleAxis0 ) def _updated(self, event=None, checkVisibility=True): if event == items.ItemChangedType.VISIBLE: self._updateItemProperty(event, self, self.__shape) super(EllipseROI, self)._updated(event, checkVisibility) def _updatedStyle(self, event, style): super(EllipseROI, self)._updatedStyle(event, style) self.__shape.setColor(style.getColor()) self.__shape.setLineStyle(style.getLineStyle()) self.__shape.setLineWidth(style.getLineWidth()) self.__shape.setLineGapColor(style.getLineGapColor())
[docs] def setFirstShapePoints(self, points): assert len(points) == 2 self._setRay(points)
@staticmethod def _calculateOrientation(p0, p1): """return angle in radians between the vector p0-p1 and the X axis :param p0: first point coordinates (x, y) :param p1: second point coordinates :return: """ vector = (p1[0] - p0[0], p1[1] - p0[1]) x_unit_vector = (1, 0) norm = numpy.linalg.norm(vector) if norm != 0: theta = numpy.arccos(numpy.dot(vector, x_unit_vector) / norm) else: theta = 0 if vector[1] < 0: # arccos always returns values in range [0, pi] theta = 2 * numpy.pi - theta return theta def _setRay(self, points): """Initialize the circle from the center point and a perimeter point.""" center = points[0] radius = numpy.linalg.norm(points[0] - points[1]) orientation = self._calculateOrientation(points[0], points[1]) self.setGeometry( center=center, radius=(radius, radius), orientation=orientation ) def _updateText(self, text): self._handleLabel.setText(text)
[docs] def getCenter(self): """Returns the central point of this rectangle :rtype: numpy.ndarray([float,float]) """ pos = self._handleCenter.getPosition() return numpy.array(pos)
[docs] def getMajorRadius(self): """Returns the half-diameter of the major axis. :rtype: float """ return max(self._radius)
[docs] def getMinorRadius(self): """Returns the half-diameter of the minor axis. :rtype: float """ return min(self._radius)
[docs] def getOrientation(self): """Return angle in radians between the horizontal (X) axis and the major axis of the ellipse in [0, 2*pi[ :rtype: float: """ return self._orientation
[docs] def setCenter(self, center): """Set the center point of this ROI :param numpy.ndarray position: Coordinates (X, Y) of the center of the ellipse """ self._handleCenter.setPosition(*center)
[docs] def setMajorRadius(self, radius): """Set the half-diameter of the major axis of the ellipse. :param float radius: Major radius of the ellipsis. Must be a positive value. """ if self._radius[0] > self._radius[1]: newRadius = radius, self._radius[1] else: newRadius = self._radius[0], radius self.setGeometry(radius=newRadius)
[docs] def setMinorRadius(self, radius): """Set the half-diameter of the minor axis of the ellipse. :param float radius: Minor radius of the ellipsis. Must be a positive value. """ if self._radius[0] > self._radius[1]: newRadius = self._radius[0], radius else: newRadius = radius, self._radius[1] self.setGeometry(radius=newRadius)
[docs] def setOrientation(self, orientation): """Rotate the ellipse :param float orientation: Angle in radians between the horizontal and the major axis. :return: """ self.setGeometry(orientation=orientation)
[docs] def setGeometry(self, center=None, radius=None, orientation=None): """ :param center: (X, Y) coordinates :param float majorRadius: :param float minorRadius: :param float orientation: angle in radians between the major axis and the horizontal :return: """ if center is None: center = self.getCenter() if radius is None: radius = self._radius else: radius = float(radius[0]), float(radius[1]) if orientation is None: orientation = self._orientation else: # ensure that we store the orientation in range [0, 2*pi orientation = numpy.mod(orientation, 2 * numpy.pi) if ( numpy.array_equal(center, self.getCenter()) or radius != self._radius or orientation != self._orientation ): # Update parameters directly self._radius = radius self._orientation = orientation if numpy.array_equal(center, self.getCenter()): self._updateGeometry() else: # This will call _updateGeometry self.setCenter(center)
def _updateGeometry(self): """Update shape and markers""" center = self.getCenter() orientation = self.getOrientation() if self._radius[1] > self._radius[0]: # _handleAxis1 is the major axis orientation -= numpy.pi / 2 point0 = numpy.array( [ center[0] + self._radius[0] * numpy.cos(orientation), center[1] + self._radius[0] * numpy.sin(orientation), ] ) point1 = numpy.array( [ center[0] - self._radius[1] * numpy.sin(orientation), center[1] + self._radius[1] * numpy.cos(orientation), ] ) with utils.blockSignals(self._handleAxis0): self._handleAxis0.setPosition(*point0) with utils.blockSignals(self._handleAxis1): self._handleAxis1.setPosition(*point1) with utils.blockSignals(self._handleLabel): self._handleLabel.setPosition(*center) nbpoints = 27 angles = numpy.arange(nbpoints) * 2.0 * numpy.pi / nbpoints X = self._radius[0] * numpy.cos(angles) * numpy.cos(orientation) - self._radius[ 1 ] * numpy.sin(angles) * numpy.sin(orientation) Y = self._radius[0] * numpy.cos(angles) * numpy.sin(orientation) + self._radius[ 1 ] * numpy.sin(angles) * numpy.cos(orientation) ellipseShape = numpy.array((X, Y)).T ellipseShape += center self.__shape.setPoints(ellipseShape) self.sigRegionChanged.emit()
[docs] def handleDragUpdated(self, handle, origin, previous, current): if handle in (self._handleAxis0, self._handleAxis1): center = self.getCenter() orientation = self._calculateOrientation(center, current) distance = numpy.linalg.norm(center - current) if handle is self._handleAxis1: if self._radius[0] > distance: # _handleAxis1 is not the major axis, rotate -90 degrees orientation -= numpy.pi / 2 radius = self._radius[0], distance else: # _handleAxis0 if self._radius[1] > distance: # _handleAxis0 is not the major axis, rotate +90 degrees orientation += numpy.pi / 2 radius = distance, self._radius[1] self.setGeometry(radius=radius, orientation=orientation)
def _centerPositionChanged(self, event): """Handle position changed events of the center marker""" if event is items.ItemChangedType.POSITION: self._updateGeometry()
[docs] @docstring(HandleBasedROI) def contains(self, position): major, minor = self.getMajorRadius(), self.getMinorRadius() delta = self.getOrientation() x, y = position - self.getCenter() return ( (x * numpy.cos(delta) + y * numpy.sin(delta)) ** 2 / major**2 + (x * numpy.sin(delta) - y * numpy.cos(delta)) ** 2 / minor**2 ) <= 1
def __str__(self): center = self.getCenter() major = self.getMajorRadius() minor = self.getMinorRadius() orientation = self.getOrientation() params = center[0], center[1], major, minor, orientation params = ( "center: %f %f; major radius: %f: minor radius: %f; orientation: %f" % params ) return "%s(%s)" % (self.__class__.__name__, params)
[docs] class PolygonROI(HandleBasedROI, items.LineMixIn): """A ROI identifying a closed polygon in a 2D plot. This ROI provides 1 anchor for each point of the polygon. """ ICON = "add-shape-polygon" NAME = "polygon ROI" SHORT_NAME = "polygon" """Metadata for this kind of ROI""" _plotShape = "polygon" """Plot shape which is used for the first interaction""" def __init__(self, parent=None): HandleBasedROI.__init__(self, parent=parent) items.LineMixIn.__init__(self) self._handleLabel = self.addLabelHandle() self._handleCenter = self.addTranslateHandle() self._handlePoints = [] self._points = numpy.empty((0, 2)) self._handleClose = None self._polygon_shape = None shape = self.__createShape() self.__shape = shape self.addItem(shape) def _updated(self, event=None, checkVisibility=True): if event in [items.ItemChangedType.VISIBLE]: self._updateItemProperty(event, self, self.__shape) super(PolygonROI, self)._updated(event, checkVisibility) def _updatedStyle(self, event, style): super(PolygonROI, self)._updatedStyle(event, style) self.__shape.setColor(style.getColor()) self.__shape.setLineStyle(style.getLineStyle()) self.__shape.setLineWidth(style.getLineWidth()) self.__shape.setLineGapColor(style.getLineGapColor()) if self._handleClose is not None: color = self._computeHandleColor(style.getColor()) self._handleClose.setColor(color) def __createShape(self, interaction=False): kind = "polygon" if not interaction else "polylines" shape = items.Shape(kind) shape.setPoints([[0, 0], [0, 0]]) shape.setFill(False) shape.setOverlay(True) style = self.getCurrentStyle() shape.setLineStyle(style.getLineStyle()) shape.setLineWidth(style.getLineWidth()) shape.setColor(rgba(style.getColor())) return shape
[docs] def setFirstShapePoints(self, points): if self._handleClose is not None: self._handleClose.setPosition(*points[0]) self.setPoints(points)
[docs] def creationStarted(self): """Called when the ROI creation interaction was started.""" # Handle to see where to close the polygon self._handleClose = self.addUserHandle() self._handleClose.setSymbol("o") color = self._computeHandleColor(rgba(self.getColor())) self._handleClose.setColor(color) # Hide the center while creating the first shape self._handleCenter.setSymbol("") # In interaction replace the polygon by a line, to display something unclosed self.removeItem(self.__shape) self.__shape = self.__createShape(interaction=True) self.__shape.setPoints(self._points) self.addItem(self.__shape)
[docs] def isBeingCreated(self): """Returns true if the ROI is in creation step""" return self._handleClose is not None
[docs] def creationFinalized(self): """Called when the ROI creation interaction was finalized.""" self.removeHandle(self._handleClose) self._handleClose = None self.removeItem(self.__shape) self.__shape = self.__createShape() self.__shape.setPoints(self._points) self.addItem(self.__shape) # Hide the center while creating the first shape self._handleCenter.setSymbol("+") for handle in self._handlePoints: handle.setSymbol("s")
def _updateText(self, text): self._handleLabel.setText(text)
[docs] def getPoints(self): """Returns the list of the points of this polygon. :rtype: numpy.ndarray """ return self._points.copy()
[docs] def setPoints(self, points): """Set the position of this ROI :param numpy.ndarray pos: 2d-coordinate of this point """ assert len(points.shape) == 2 and points.shape[1] == 2 if numpy.array_equal(points, self._points): return # Nothing has changed self._polygon_shape = None # Update the needed handles while len(self._handlePoints) != len(points): if len(self._handlePoints) < len(points): handle = self.addHandle() self._handlePoints.append(handle) if self.isBeingCreated(): handle.setSymbol("") else: handle = self._handlePoints.pop(-1) self.removeHandle(handle) for handle, position in zip(self._handlePoints, points): with utils.blockSignals(handle): handle.setPosition(position[0], position[1]) if len(points) > 0: if not self.isHandleBeingDragged(): vmin = numpy.min(points, axis=0) vmax = numpy.max(points, axis=0) center = (vmax + vmin) * 0.5 with utils.blockSignals(self._handleCenter): self._handleCenter.setPosition(center[0], center[1]) num = numpy.argmin(points[:, 1]) pos = points[num] with utils.blockSignals(self._handleLabel): self._handleLabel.setPosition(pos[0], pos[1]) if len(points) == 0: self._points = numpy.empty((0, 2)) else: self._points = points self.__shape.setPoints(self._points) self.sigRegionChanged.emit()
def translate(self, x, y): points = self.getPoints() delta = numpy.array([x, y]) self.setPoints(points) self.setPoints(points + delta)
[docs] def handleDragUpdated(self, handle, origin, previous, current): if handle is self._handleCenter: delta = current - previous self.translate(delta[0], delta[1]) else: points = self.getPoints() num = self._handlePoints.index(handle) points[num] = current self.setPoints(points)
[docs] def handleDragFinished(self, handle, origin, current): points = self._points if len(points) > 0: # Only update the center at the end # To avoid to disturb the interaction vmin = numpy.min(points, axis=0) vmax = numpy.max(points, axis=0) center = (vmax + vmin) * 0.5 with utils.blockSignals(self._handleCenter): self._handleCenter.setPosition(center[0], center[1])
def __str__(self): points = self._points params = "; ".join("%f %f" % (pt[0], pt[1]) for pt in points) return "%s(%s)" % (self.__class__.__name__, params)
[docs] @docstring(HandleBasedROI) def contains(self, position): bb1 = _BoundingBox.from_points(self.getPoints()) if bb1.contains(position) is False: return False if self._polygon_shape is None: self._polygon_shape = Polygon(vertices=self.getPoints()) # warning: both the polygon and the value are inverted return self._polygon_shape.is_inside(row=position[0], col=position[1])
def _setControlPoints(self, points): RegionOfInterest._setControlPoints(self, points=points) self._polygon_shape = None
[docs] class HorizontalRangeROI(RegionOfInterest, items.LineMixIn): """A ROI identifying an horizontal range in a 1D plot.""" ICON = "add-range-horizontal" NAME = "horizontal range ROI" SHORT_NAME = "hrange" _plotShape = "line" """Plot shape which is used for the first interaction""" def __init__(self, parent=None): RegionOfInterest.__init__(self, parent=parent) items.LineMixIn.__init__(self) self._markerMin = items.XMarker() self._markerMax = items.XMarker() self._markerCen = items.XMarker() self._markerCen.setLineStyle(" ") self._markerMin._setConstraint(self.__positionMinConstraint) self._markerMax._setConstraint(self.__positionMaxConstraint) self._markerMin.sigDragStarted.connect(self._editingStarted) self._markerMin.sigDragFinished.connect(self._editingFinished) self._markerMax.sigDragStarted.connect(self._editingStarted) self._markerMax.sigDragFinished.connect(self._editingFinished) self._markerCen.sigDragStarted.connect(self._editingStarted) self._markerCen.sigDragFinished.connect(self._editingFinished) self.addItem(self._markerCen) self.addItem(self._markerMin) self.addItem(self._markerMax) self.__filterReentrant = utils.LockReentrant()
[docs] def setFirstShapePoints(self, points): vmin = min(points[:, 0]) vmax = max(points[:, 0]) self._updatePos(vmin, vmax)
def _updated(self, event=None, checkVisibility=True): if event == items.ItemChangedType.EDITABLE: self._updateEditable() self._updateText(self.getText()) elif event == items.ItemChangedType.LINE_STYLE: markers = [self._markerMin, self._markerMax] self._updateItemProperty(event, self, markers) elif event in [items.ItemChangedType.VISIBLE, items.ItemChangedType.SELECTABLE]: markers = [self._markerMin, self._markerMax, self._markerCen] self._updateItemProperty(event, self, markers) super(HorizontalRangeROI, self)._updated(event, checkVisibility) def _updatedStyle(self, event, style): markers = [self._markerMin, self._markerMax, self._markerCen] for m in markers: m.setColor(style.getColor()) m.setLineWidth(style.getLineWidth()) def _updateText(self, text: str): if self.isEditable(): self._markerMin.setText("") self._markerCen.setText(text) else: self._markerMin.setText(text) self._markerCen.setText("") def _updateEditable(self): editable = self.isEditable() self._markerMin._setDraggable(editable) self._markerMax._setDraggable(editable) self._markerCen._setDraggable(editable) if self.isEditable(): self._markerMin.sigItemChanged.connect(self._minPositionChanged) self._markerMax.sigItemChanged.connect(self._maxPositionChanged) self._markerCen.sigItemChanged.connect(self._cenPositionChanged) self._markerCen.setLineStyle(":") else: self._markerMin.sigItemChanged.disconnect(self._minPositionChanged) self._markerMax.sigItemChanged.disconnect(self._maxPositionChanged) self._markerCen.sigItemChanged.disconnect(self._cenPositionChanged) self._markerCen.setLineStyle(" ") def _updatePos(self, vmin, vmax, force=False): """Update marker position and emit signal. :param float vmin: :param float vmax: :param bool force: True to update even if already at the right position. """ if not force and numpy.array_equal((vmin, vmax), self.getRange()): return # Nothing has changed center = (vmin + vmax) * 0.5 with self.__filterReentrant: with utils.blockSignals(self._markerMin): self._markerMin.setPosition(vmin, 0) with utils.blockSignals(self._markerCen): self._markerCen.setPosition(center, 0) with utils.blockSignals(self._markerMax): self._markerMax.setPosition(vmax, 0) self.sigRegionChanged.emit()
[docs] def setRange(self, vmin, vmax): """Set the range of this ROI. :param float vmin: Staring location of the range :param float vmax: Ending location of the range """ if vmin is None or vmax is None: err = "Can't set vmin or vmax to None" raise ValueError(err) if vmin > vmax: err = ( "Can't set vmin and vmax because vmin >= vmax " "vmin = %s, vmax = %s" % (vmin, vmax) ) raise ValueError(err) self._updatePos(vmin, vmax)
[docs] def getRange(self): """Returns the range of this ROI. :rtype: Tuple[float,float] """ vmin = self.getMin() vmax = self.getMax() return vmin, vmax
[docs] def setMin(self, vmin): """Set the min of this ROI. :param float vmin: New min """ vmax = self.getMax() self._updatePos(vmin, vmax)
[docs] def getMin(self): """Returns the min value of this ROI. :rtype: float """ return self._markerMin.getPosition()[0]
[docs] def setMax(self, vmax): """Set the max of this ROI. :param float vmax: New max """ vmin = self.getMin() self._updatePos(vmin, vmax)
[docs] def getMax(self): """Returns the max value of this ROI. :rtype: float """ return self._markerMax.getPosition()[0]
[docs] def setCenter(self, center): """Set the center of this ROI. :param float center: New center """ vmin, vmax = self.getRange() previousCenter = (vmin + vmax) * 0.5 delta = center - previousCenter self._updatePos(vmin + delta, vmax + delta)
[docs] def getCenter(self): """Returns the center location of this ROI. :rtype: float """ vmin, vmax = self.getRange() return (vmin + vmax) * 0.5
def __positionMinConstraint(self, x, y): """Constraint of the min marker""" if self.__filterReentrant.locked(): # Ignore the constraint when we set an explicit value return x, y vmax = self.getMax() if vmax is None: return x, y return min(x, vmax), y def __positionMaxConstraint(self, x, y): """Constraint of the max marker""" if self.__filterReentrant.locked(): # Ignore the constraint when we set an explicit value return x, y vmin = self.getMin() if vmin is None: return x, y return max(x, vmin), y def _minPositionChanged(self, event): """Handle position changed events of the marker""" if event is items.ItemChangedType.POSITION: marker = self.sender() self._updatePos(marker.getXPosition(), self.getMax(), force=True) def _maxPositionChanged(self, event): """Handle position changed events of the marker""" if event is items.ItemChangedType.POSITION: marker = self.sender() self._updatePos(self.getMin(), marker.getXPosition(), force=True) def _cenPositionChanged(self, event): """Handle position changed events of the marker""" if event is items.ItemChangedType.POSITION: marker = self.sender() self.setCenter(marker.getXPosition())
[docs] @docstring(HandleBasedROI) def contains(self, position): return self.getMin() <= position[0] <= self.getMax()
def __str__(self): vrange = self.getRange() params = "min: %f; max: %f" % vrange return "%s(%s)" % (self.__class__.__name__, params)