Skip to content

Commit

Permalink
Refactoring rotation[_matrix_2d]; docstring updates
Browse files Browse the repository at this point in the history
  • Loading branch information
dhomeier committed Sep 30, 2021
1 parent 87f4f43 commit 0da63cd
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 70 deletions.
126 changes: 57 additions & 69 deletions glue/core/roi.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from glue.core.component import CategoricalComponent
from glue.core.exceptions import UndefinedROI
from glue.utils import points_inside_poly, iterate_chunks
from glue.utils import points_inside_poly, iterate_chunks, rotation_matrix_2d


np.seterr(all='ignore')
Expand Down Expand Up @@ -55,12 +55,6 @@ def pixel_to_axes(axes, x, y):
return axes.transAxes.inverted().transform(xy)


def rotation(alpha):
"""Return rotation matrix for angle alpha (increasing anticlockwise) around origin.
"""
return np.array([[np.cos(alpha), -np.sin(alpha)], [np.sin(alpha), np.cos(alpha)]])


class Roi(object): # pragma: no cover

"""
Expand Down Expand Up @@ -127,11 +121,11 @@ def to_polygon(self):
raise NotImplementedError()

def rotate_to(self, theta):
"""Set rotation angle of ROI around center to theta"""
"""Set rotation angle of ROI around center to theta (radian)"""
raise NotImplementedError()

def rotate_by(self, dtheta):
"""Rotate the ROI around center by angle dtheta"""
"""Rotate the ROI around center by angle dtheta (radian)"""
self.rotate_to(getattr(self, 'theta', 0.0) + dtheta)

def copy(self):
Expand Down Expand Up @@ -180,12 +174,12 @@ class RectangularROI(Roi):
Parameters
----------
xmin, xmax : float, optional
x coordinates of left and right border
ymin, ymax : float, optional
y coordinates of lower and upper border
xmin, xmax : float, optional
x coordinates of left and right border.
ymin, ymax : float, optional
y coordinates of lower and upper border.
theta : float, optional
Angle of anticlockwise rotation around center
Angle of anticlockwise rotation around center in radian.
"""

def __init__(self, xmin=None, xmax=None, ymin=None, ymax=None, theta=None):
Expand All @@ -195,11 +189,6 @@ def __init__(self, xmin=None, xmax=None, ymin=None, ymax=None, theta=None):
self.ymin = ymin
self.ymax = ymax
self.theta = 0 if theta is None else theta
if np.isclose(self.theta % np.pi, 0.0, atol=1e-9):
self.rotation = np.identity(2)
else:
self.rotation = rotation(self.theta)
self.invrot = self.rotation * [[1, -1], [-1, 1]]

def __str__(self):
if self.defined() and self.theta == 0:
Expand All @@ -226,11 +215,6 @@ def move_to(self, x, y):
def rotate_to(self, theta):
""" Rotate anticlockwise around center to position angle theta (radian) """
self.theta = 0 if theta is None else theta
if np.isclose(self.theta % np.pi, 0.0, atol=1e-9):
self.rotation = np.identity(2)
else:
self.rotation = rotation(self.theta)
self.invrot = self.rotation * [[1, -1], [-1, 1]]

def transpose(self, copy=True):
if copy:
Expand All @@ -253,16 +237,20 @@ def height(self):

def contains(self, x, y):
"""
Test whether a set of (x,y) points falls within
the region of interest
Test whether a set of (x,y) points falls within the region of interest
:param x: A scalar or iterable of x coordinates
:param y: A scalar or iterable of y coordinates
Parameters
----------
x : float or array-like
x coordinate(s) of point(s).
y : float or array-like
y coordinate(s) of point(s).
*Returns*
Returns
-------
A list of True/False values, for whether each (x,y)
point falls within the ROI
inside : bool or `numpy.ndarray`
An iterable of True/False values, for whether each (x,y) point falls within the ROI
"""

if not self.defined():
Expand All @@ -289,7 +277,7 @@ def contains(self, x, y):
x = x[keep] - xc
y = y[keep] - yc
shape = (2,) + x.shape
x, y = (self.invrot @ [x.flatten(), y.flatten()]).reshape(shape)
x, y = (rotation_matrix_2d(-self.theta) @ [x.flatten(), y.flatten()]).reshape(shape)
inside[keep] = (abs(x) <= self.width() / 2) & (abs(y) <= self.height() / 2)
return inside

Expand Down Expand Up @@ -323,7 +311,8 @@ def to_polygon(self):
else:
corners = (np.array([-1, 1, 1, -1, -1]) * self.width() / 2,
np.array([-1, -1, 1, 1, -1]) * self.height() / 2)
return tuple((self.rotation @ corners) + np.array(self.center()).reshape((2, 1)))
return tuple((rotation_matrix_2d(self.theta) @ corners) +
np.array(self.center()).reshape((2, 1)))
else:
return [], []

Expand Down Expand Up @@ -544,16 +533,16 @@ class EllipticalROI(Roi):
Parameters
----------
xc : float, optional
x coordinate of center
yc : float, optional
y coordinate of center
radius_x : float, optional
Semiaxis along x axis
radius_y : float, optional
Semiaxis along y axis
xc : float, optional
x coordinate of center.
yc : float, optional
y coordinate of center.
radius_x : float, optional
Semiaxis along x axis.
radius_y : float, optional
Semiaxis along y axis.
theta : float, optional
Angle of anticlockwise rotation around (xc, yc)
Angle of anticlockwise rotation around (xc, yc) in radian.
"""

def __init__(self, xc=None, yc=None, radius_x=None, radius_y=None, theta=None):
Expand All @@ -563,11 +552,6 @@ def __init__(self, xc=None, yc=None, radius_x=None, radius_y=None, theta=None):
self.radius_x = radius_x
self.radius_y = radius_y
self.theta = 0 if theta is None else theta
if np.isclose(self.theta % np.pi, 0.0, atol=1e-9):
self.rotation = np.identity(2)
else:
self.rotation = rotation(self.theta)
self.invrot = self.rotation * [[1, -1], [-1, 1]]

def __str__(self):
if self.defined():
Expand All @@ -582,14 +566,18 @@ def contains(self, x, y):
Test whether a set of (x,y) points falls within
the region of interest
:param x: A scalar or iterable of x coordinates
:param y: A scalar or iterable of y coordinates
*Returns*
Parameters
----------
x : float or array-like
x coordinate(s) of point(s).
y : float or array-like
y coordinate(s) of point(s).
A list of True/False values, for whether each (x,y)
point falls within the ROI
Returns
-------
inside : bool or `numpy.ndarray`
An iterable of True/False values, for whether each (x,y) point falls within the ROI
"""
if not self.defined():
raise UndefinedROI
Expand All @@ -616,7 +604,7 @@ def contains(self, x, y):
x = x[keep] - self.xc
y = y[keep] - self.yc
shape = (2,) + x.shape
x, y = (self.invrot @ [x.flatten(), y.flatten()]).reshape(shape)
x, y = (rotation_matrix_2d(-self.theta) @ [x.flatten(), y.flatten()]).reshape(shape)
inside[keep] = ((x ** 2 / self.radius_x ** 2 + y ** 2 / self.radius_y ** 2) < 1.)
return inside

Expand Down Expand Up @@ -646,7 +634,7 @@ def to_polygon(self):
theta = np.linspace(0, 2 * np.pi, num=20)
x = self.radius_x * np.cos(theta)
y = self.radius_y * np.sin(theta)
x, y = self.rotation @ (x, y)
x, y = rotation_matrix_2d(self.theta) @ (x, y)
return x + self.xc, y + self.yc

def bounds(self):
Expand All @@ -672,11 +660,6 @@ def move_to(self, xdelta, ydelta):
def rotate_to(self, theta):
""" Rotate anticlockwise around center to position angle theta (radian) """
self.theta = 0 if theta is None else theta
if np.isclose(self.theta % np.pi, 0.0, atol=1e-9):
self.rotation = np.identity(2)
else:
self.rotation = rotation(self.theta)
self.invrot = self.rotation * [[1, -1], [-1, 1]]

def __gluestate__(self, context):
return dict(xc=context.do(self.xc),
Expand Down Expand Up @@ -792,18 +775,22 @@ def __str__(self):

def contains(self, x, y):
"""
Test whether a set of (x,y) points falls within
the region of interest
:param x: A list of x points
:param y: A list of y points
Test whether a set of (x,y) points falls within the region of interest
*Returns*
Parameters
----------
x : float or array-like
x coordinate(s) of point(s).
y : float or array-like
y coordinate(s) of point(s).
A list of True/False values, for whether each (x,y)
point falls within the ROI
Returns
-------
inside : bool or `numpy.ndarray`
An iterable of True/False values, for whether each (x,y) point falls within the ROI
"""

if not self.defined():
raise UndefinedROI
if not isinstance(x, np.ndarray):
Expand Down Expand Up @@ -834,7 +821,8 @@ def rotate_to(self, theta):
dtheta = theta - self.theta
if self.defined() and not np.isclose(dtheta % np.pi, 0.0, atol=1e-9):
dx, dy = np.array([self.vx, self.vy]) - np.array(self.center()).reshape(2, 1)
self.vx, self.vy = rotation(dtheta) @ (dx, dy) + np.array(self.center()).reshape(2, 1)
self.vx, self.vy = (rotation_matrix_2d(dtheta) @ (dx, dy) +
np.array(self.center()).reshape(2, 1))
self.theta = theta


Expand Down
18 changes: 17 additions & 1 deletion glue/utils/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,23 @@

from glue.utils import unbroadcast, broadcast_to

__all__ = ['points_inside_poly', 'polygon_line_intersections', 'floodfill']
__all__ = ['points_inside_poly', 'polygon_line_intersections', 'floodfill', 'rotation_matrix_2d']


def rotation_matrix_2d(alpha):
"""
Return rotation matrix for angle alpha around origin.
Parameters
----------
alpha : float
Rotation angle in radian, increasing for anticlockwise rotation.
"""
if np.asarray(alpha).ndim > 0:
# In principle this works on an array as well; would have to return matrix.T then
raise ValueError("Only scalar input for angle accepted")

return np.array([[np.cos(alpha), -np.sin(alpha)], [np.sin(alpha), np.cos(alpha)]])


def points_inside_poly(x, y, vx, vy):
Expand Down

0 comments on commit 0da63cd

Please sign in to comment.