Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New CircularAnnulusROI class to represent circular annulus ROI #2403

Merged
merged 4 commits into from
May 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 111 additions & 21 deletions glue/core/roi.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
np.seterr(all='ignore')


__all__ = ['Roi', 'RectangularROI', 'CircularROI', 'PolygonalROI',
__all__ = ['Roi', 'RectangularROI', 'CircularROI', 'CircularAnnulusROI', 'PolygonalROI',
'AbstractMplRoi', 'MplRectangularROI', 'MplCircularROI',
'MplPolygonalROI', 'MplXRangeROI', 'MplYRangeROI',
'XRangeROI', 'RangeROI', 'YRangeROI', 'VertexROIBase',
Expand Down Expand Up @@ -54,7 +54,7 @@ def pixel_to_axes(axes, x, y):
return axes.transAxes.inverted().transform(xy)


class Roi(object): # pragma: no cover
class Roi: # pragma: no cover

"""
A geometrical 2D region of interest.
Expand Down Expand Up @@ -215,7 +215,7 @@ class RectangularROI(Roi):
"""

def __init__(self, xmin=None, xmax=None, ymin=None, ymax=None, theta=None):
super(RectangularROI, self).__init__()
super().__init__()
self.xmin = xmin
self.xmax = xmax
self.ymin = ymin
Expand Down Expand Up @@ -364,7 +364,7 @@ class RangeROI(Roi):
End value of the range.
"""
def __init__(self, orientation, min=None, max=None):
super(RangeROI, self).__init__()
super().__init__()

self.min = min
self.max = max
Expand Down Expand Up @@ -448,17 +448,16 @@ def __setgluestate__(cls, rec, context):
class XRangeROI(RangeROI):

def __init__(self, min=None, max=None):
super(XRangeROI, self).__init__('x', min=min, max=max)
super().__init__('x', min=min, max=max)


class YRangeROI(RangeROI):

def __init__(self, min=None, max=None):
super(YRangeROI, self).__init__('y', min=min, max=max)
super().__init__('y', min=min, max=max)


class CircularROI(Roi):

"""
A 2D circular region of interest.

Expand All @@ -473,7 +472,7 @@ class CircularROI(Roi):
"""

def __init__(self, xc=None, yc=None, radius=None):
super(CircularROI, self).__init__()
super().__init__()
self.xc = xc
self.yc = yc
self.radius = radius
Expand Down Expand Up @@ -519,7 +518,7 @@ def defined(self):
def to_polygon(self):
if not self.defined():
return [], []
theta = np.linspace(0, 2 * np.pi, num=20)
theta = np.linspace(0, 2 * np.pi, num=100)
x = self.xc + self.radius * np.cos(theta)
y = self.yc + self.radius * np.sin(theta)
return x, y
Expand All @@ -544,6 +543,97 @@ def __setgluestate__(cls, rec, context):
return cls(xc=rec['xc'], yc=rec['yc'], radius=rec['radius'])


class CircularAnnulusROI(Roi):
"""
A 2D circular annulus region of interest.

Parameters
----------
xc : float, optional
`x` coordinate of center.
yc : float, optional
`y` coordinate of center.
inner_radius : float, optional
Inner radius of the circular annulus.
outer_radius : float, optional
Outer radius of the circular annulus.
"""
def __init__(self, xc=None, yc=None, inner_radius=None, outer_radius=None):
super().__init__()
self.xc = xc
self.yc = yc
self.inner_radius = inner_radius
self.outer_radius = outer_radius

def contains(self, x, y):
if not self.defined():
raise UndefinedROI

if not isinstance(x, np.ndarray):
x = np.asarray(x)
if not isinstance(y, np.ndarray):
y = np.asarray(y)

dx = x - self.xc
dy = y - self.yc
r = np.sqrt((dx * dx) + (dy * dy))
# Python likes to do inclusive for min limit and exclusive for max limit, so why not.
return (r >= self.inner_radius) & (r < self.outer_radius)

def reset(self):
"""Reset the circular annulus region."""
self.xc = None
self.yc = None
self.inner_radius = None
self.outer_radius = None

def defined(self):
number = (float, int)
if (isinstance(self.xc, number) and isinstance(self.yc, number) and
isinstance(self.inner_radius, number) and isinstance(self.outer_radius, number) and
(self.inner_radius > 0) and (self.outer_radius > self.inner_radius)):
status = True
else:
status = False
return status

def to_polygon(self):
if not self.defined():
return [], []
theta = np.linspace(0, 2 * np.pi, num=100)
x_inner = self.xc + self.inner_radius * np.cos(theta)
y_inner = self.yc + self.inner_radius * np.sin(theta)
x_outer = self.xc + self.outer_radius * np.cos(theta)
y_outer = self.yc + self.outer_radius * np.sin(theta)
# theta=2pi=360deg=0deg --> x=r, y=0
x = np.concatenate((x_inner, x_outer[::-1], x_inner[0]), axis=None)
y = np.concatenate((y_inner, y_outer[::-1], y_inner[0]), axis=None)
return x, y

def transformed(self, xfunc=None, yfunc=None):
return PolygonalROI(*self.to_polygon()).transformed(xfunc=xfunc, yfunc=yfunc)

def center(self):
return self.xc, self.yc

def move_to(self, x, y):
self.xc = x
self.yc = y

def __gluestate__(self, context):
return dict(xc=context.do(self.xc),
yc=context.do(self.yc),
inner_radius=context.do(self.inner_radius),
outer_radius=context.do(self.outer_radius))

@classmethod
def __setgluestate__(cls, rec, context):
return cls(xc=rec['xc'],
yc=rec['yc'],
inner_radius=rec['inner_radius'],
outer_radius=rec['outer_radius'])


class EllipticalROI(Roi):
"""
A 2D elliptical region of interest with semimajor/minor axes `radius_[xy]`.
Expand All @@ -568,7 +658,7 @@ class EllipticalROI(Roi):
"""

def __init__(self, xc=None, yc=None, radius_x=None, radius_y=None, theta=None):
super(EllipticalROI, self).__init__()
super().__init__()
self.xc = xc
self.yc = yc
self.radius_x = radius_x
Expand Down Expand Up @@ -634,7 +724,7 @@ def get_center(self): # pragma: no cover
def to_polygon(self):
if not self.defined():
return [], []
theta = np.linspace(0, 2 * np.pi, num=20)
theta = np.linspace(0, 2 * np.pi, num=100)
x = self.radius_x * np.cos(theta)
y = self.radius_y * np.sin(theta)
x, y = rotation_matrix_2d(self.theta) @ (x, y)
Expand Down Expand Up @@ -698,7 +788,7 @@ class VertexROIBase(Roi):
"""

def __init__(self, vx=None, vy=None):
super(VertexROIBase, self).__init__()
super().__init__()
self.vx = [] if vx is None else list(vx)
self.vy = [] if vy is None else list(vy)
self.theta = 0
Expand Down Expand Up @@ -935,7 +1025,7 @@ class Projected3dROI(Roi):
"""

def __init__(self, roi_2d=None, projection_matrix=None):
super(Projected3dROI, self).__init__()
super().__init__()
self.roi_2d = roi_2d
self.projection_matrix = np.asarray(projection_matrix)

Expand Down Expand Up @@ -1013,7 +1103,7 @@ def __str__(self):
return result


class AbstractMplRoi(object):
class AbstractMplRoi:
"""
Base class for objects which use Matplotlib user events to edit/display ROIs.

Expand Down Expand Up @@ -1153,7 +1243,7 @@ class MplRectangularROI(AbstractMplRoi):

def __init__(self, axes, data_space=True):

super(MplRectangularROI, self).__init__(axes, data_space=data_space)
super().__init__(axes, data_space=data_space)

self._xi = None
self._yi = None
Expand Down Expand Up @@ -1270,7 +1360,7 @@ class MplXRangeROI(AbstractMplRoi):

def __init__(self, axes, data_space=True):

super(MplXRangeROI, self).__init__(axes, data_space=data_space)
super().__init__(axes, data_space=data_space)

self._xi = None

Expand Down Expand Up @@ -1376,7 +1466,7 @@ class MplYRangeROI(AbstractMplRoi):

def __init__(self, axes, data_space=True):

super(MplYRangeROI, self).__init__(axes, data_space=data_space)
super().__init__(axes, data_space=data_space)

self._yi = None

Expand Down Expand Up @@ -1488,7 +1578,7 @@ class MplCircularROI(AbstractMplRoi):

def __init__(self, axes, data_space=True):

super(MplCircularROI, self).__init__(axes, data_space=data_space)
super().__init__(axes, data_space=data_space)

self.plot_opts = {'edgecolor': PATCH_COLOR,
'facecolor': PATCH_COLOR,
Expand Down Expand Up @@ -1632,7 +1722,7 @@ class MplPolygonalROI(AbstractMplRoi):

def __init__(self, axes, roi=None, data_space=True):

super(MplPolygonalROI, self).__init__(axes, roi=roi, data_space=data_space)
super().__init__(axes, roi=roi, data_space=data_space)

self.plot_opts = {'edgecolor': PATCH_COLOR,
'facecolor': PATCH_COLOR,
Expand Down Expand Up @@ -1739,7 +1829,7 @@ class MplPathROI(MplPolygonalROI):

def __init__(self, axes, roi=None):

super(MplPolygonalROI, self).__init__(axes)
super().__init__(axes)

self.plot_opts = {'edgecolor': PATCH_COLOR,
'facecolor': PATCH_COLOR,
Expand All @@ -1756,7 +1846,7 @@ def start_selection(self, event):
self._background_cache = None
self._axes.figure.canvas.draw()

super(MplPathROI, self).start_selection(event)
super().start_selection(event)

def _sync_patch(self):

Expand Down
6 changes: 4 additions & 2 deletions glue/core/subset.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
import numpy as np

from glue.core.roi import (PolygonalROI, CategoricalROI, RangeROI, XRangeROI,
YRangeROI, RectangularROI, CircularROI, EllipticalROI, Projected3dROI)
YRangeROI, RectangularROI, CircularROI, EllipticalROI, CircularAnnulusROI,
Projected3dROI)
from glue.core.contracts import contract
from glue.core.util import split_component_view
from glue.core.registry import Registry
Expand Down Expand Up @@ -2018,7 +2019,8 @@ def roi_to_subset_state(roi, x_att=None, y_att=None, x_categories=None, y_catego

# The selection is polygon-like or requires a pretransform and components are numerical

if not isinstance(roi, (PolygonalROI, RectangularROI, CircularROI, EllipticalROI, RangeROI)):
if not isinstance(roi, (PolygonalROI, RectangularROI, CircularROI, EllipticalROI, RangeROI,
CircularAnnulusROI)):
roi = PolygonalROI(*roi.to_polygon())

subset_state = RoiSubsetState()
Expand Down
Loading