# /*##########################################################################
#
# 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)