Skip to content

Commit

Permalink
Merge pull request #2403 from pllim/annulus-eclipse-of-my-heart
Browse files Browse the repository at this point in the history
FEAT: New CircularAnnulusROI class to represent circular annulus ROI
  • Loading branch information
astrofrog authored May 15, 2023
2 parents d197670 + 7fd9894 commit 8969a97
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 38 deletions.
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

0 comments on commit 8969a97

Please sign in to comment.