diff --git a/glue/core/roi.py b/glue/core/roi.py index 23629d206..572f9cc5b 100644 --- a/glue/core/roi.py +++ b/glue/core/roi.py @@ -1,4 +1,5 @@ import copy +import warnings import numpy as np from matplotlib.patches import Ellipse, Polygon, Rectangle, Path as MplPath, PathPatch @@ -487,17 +488,20 @@ def contains(self, x, y): y = np.asarray(y) return (x - self.xc) ** 2 + (y - self.yc) ** 2 < self.radius ** 2 - def set_center(self, x, y): + def set_center(self, x, y): # pragma: no cover """Set the center of the circular region""" - self.xc = x - self.yc = y + warnings.warn("set_center is deprecated and may be removed " + "in a future release, use move_to", DeprecationWarning) + self.move_to(x, y) def set_radius(self, radius): """Set the radius of the circular region""" self.radius = radius - def get_center(self): - return self.xc, self.yc + def get_center(self): # pragma: no cover + warnings.warn("get_center is deprecated and may be removed " + "in a future release, use center", DeprecationWarning) + return self.center() def get_radius(self): return self.radius @@ -509,8 +513,8 @@ def reset(self): self.radius = 0. def defined(self): - return self.xc is not None and \ - self.yc is not None and self.radius is not None + return (self.xc is not None and + self.yc is not None and self.radius is not None) def to_polygon(self): if not self.defined(): @@ -523,9 +527,12 @@ def to_polygon(self): def transformed(self, xfunc=None, yfunc=None): return PolygonalROI(*self.to_polygon()).transformed(xfunc=xfunc, yfunc=yfunc) - def move_to(self, xdelta, ydelta): - self.xc += xdelta - self.yc += ydelta + 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), @@ -556,8 +563,8 @@ class EllipticalROI(Roi): Notes ----- - The `radius_x`, `radius_y` properties refer to the semiaxes along the `x` and `y` - axes *before* any rotation is applied. + The `radius_x`, `radius_y` properties refer to the semiaxes along the `x` and `y` + axes *before* any rotation is applied. """ def __init__(self, xc=None, yc=None, radius_x=None, radius_y=None, theta=None): @@ -619,8 +626,10 @@ def defined(self): self.radius_x is not None and self.radius_y is not None) - def get_center(self): - return self.xc, self.yc + def get_center(self): # pragma: no cover + warnings.warn("get_center is deprecated and may be removed " + "in a future release, use center", DeprecationWarning) + return self.center() def to_polygon(self): if not self.defined(): @@ -647,9 +656,12 @@ def bounds(self): def transformed(self, xfunc=None, yfunc=None): return PolygonalROI(*self.to_polygon()).transformed(xfunc=xfunc, yfunc=yfunc) - def move_to(self, xdelta, ydelta): - self.xc += xdelta - self.yc += ydelta + def center(self): + return self.xc, self.yc + + def move_to(self, x, y): + self.xc = x + self.yc = y def rotate_to(self, theta): self.theta = 0 if theta is None else theta @@ -865,7 +877,18 @@ def centroid(self): return np.dot(xs, dxy) * scl + x0, np.dot(ys, dxy) * scl + y0 - def move_to(self, xdelta, ydelta): + def center(self): + # centroid is more robust than mean, but + # for linear (1D) "polygons" centroid is not defined. + if self.area() == 0: + return self.mean() + else: + return self.centroid() + + def move_to(self, new_x, new_y): + xcen, ycen = self.center() + xdelta = new_x - xcen + ydelta = new_y - ycen self.vx = list(map(lambda x: x + xdelta, self.vx)) self.vy = list(map(lambda y: y + ydelta, self.vy)) @@ -884,12 +907,7 @@ def rotate_to(self, theta, center=None): """ theta = 0 if theta is None else theta - # For linear (1D) "polygons" centroid is not defined. - if center is None: - if self.area() == 0: - center = self.mean() - else: - center = self.centroid() + center = self.center() if center is None else center dtheta = theta - self.theta if self.defined() and not np.isclose(dtheta % np.pi, 0.0, atol=1e-9): @@ -1485,7 +1503,7 @@ def __init__(self, axes, data_space=True): def _sync_patch(self): if self._roi.defined(): - xy = self._roi.get_center() + xy = self._roi.center() r = self._roi.get_radius() self._patch.center = xy self._patch.width = 2. * r @@ -1518,12 +1536,12 @@ def start_selection(self, event): if event.key == SCRUBBING_KEY: self._scrubbing = True - (xc, yc) = self._roi.get_center() + (xc, yc) = self._roi.center() self._dx = xc - xi self._dy = yc - yi else: self.reset() - self._roi.set_center(xi, yi) + self._roi.move_to(xi, yi) self._roi.set_radius(0.) self._xi = xi self._yi = yi @@ -1547,7 +1565,7 @@ def update_selection(self, event): return False if self._scrubbing: - self._roi.set_center(xi + self._dx, yi + self._dy) + self._roi.move_to(xi + self._dx, yi + self._dy) else: dx = xy[0, 0] - self._xi dy = xy[0, 1] - self._yi @@ -1561,7 +1579,7 @@ def roi(self): return PolygonalROI() # Get the circular ROI parameters in pixel units - xy_center = self._roi.get_center() + xy_center = self._roi.center() rad = self._roi.get_radius() # At this point, if one of the axes is not linear, we convert to a polygon @@ -1688,8 +1706,10 @@ def update_selection(self, event): xval, yval = axes_trans.transform([event.x, event.y]) if self._scrubbing: - self._roi.move_to(xval - self._cx, - yval - self._cy) + old_x, old_y = self._roi.center() + new_x = old_x + xval - self._cx + new_y = old_y + yval - self._cy + self._roi.move_to(new_x, new_y) self._cx = xval self._cy = yval else: diff --git a/glue/core/subset.py b/glue/core/subset.py index 080603128..6d968455b 100644 --- a/glue/core/subset.py +++ b/glue/core/subset.py @@ -441,6 +441,14 @@ def attributes(self): def subset_state(self): # convenience method, mimic interface of Subset return self + def center(self): + """Return center of underlying ROI, if any.""" + return # None until explicitly implemented by subclass + + def move_to(self, *args): + """Move any underlying ROI to the new given center.""" + pass # no-op until explicitly implemented by subclass + @contract(data='isinstance(Data)') def to_index_list(self, data): return np.where(data.get_mask(self.subset_state).flat)[0] @@ -538,6 +546,12 @@ def pretransform(self, value): def attributes(self): return tuple(self._atts) + def center(self): + return self._roi.center() + + def move_to(self, *args): + self._roi.move_to(*args) + @contract(data='isinstance(Data)', view='array_view') def to_mask(self, data, view=None): @@ -1079,6 +1093,28 @@ def __init__(self, state1, state2=None): def copy(self): return type(self)(self.state1, self.state2) + def center(self): + cen = self.state1.center() + if cen is None and self.state2: + cen = self.state2.center() + return cen + + def move_to(self, *args): + """Move any underlying ROI to the new given center.""" + if self.state2: + cen1 = self.state1.center() + cen2 = self.state2.center() + if cen2 is not None and cen1 is not None: + offset = np.asarray(cen2) - np.asarray(cen1) + if np.isscalar(offset): + mt_args = (args[0] + offset, ) + else: + mt_args = tuple(map(operator.add, args, offset)) + else: + mt_args = args + self.state2.move_to(*mt_args) + self.state1.move_to(*args) + @property def attributes(self): att = self.state1.attributes diff --git a/glue/core/tests/test_roi.py b/glue/core/tests/test_roi.py index e5aeac2f7..a0ae1dd07 100644 --- a/glue/core/tests/test_roi.py +++ b/glue/core/tests/test_roi.py @@ -273,16 +273,16 @@ def test_contains_on_undefined_contains_raises(self): self.roi.contains(1, 1) def test_set_center(self): - self.roi.set_center(0, 0) + self.roi.move_to(0, 0) self.roi.set_radius(1) assert self.roi.contains(0, 0) assert not self.roi.contains(2, 2) - self.roi.set_center(2, 2) + self.roi.move_to(2, 2) assert not self.roi.contains(0, 0) assert self.roi.contains(2, 2) def test_set_radius(self): - self.roi.set_center(0, 0) + self.roi.move_to(0, 0) self.roi.set_radius(1) assert not self.roi.contains(1.5, 0) self.roi.set_radius(5) @@ -291,14 +291,14 @@ def test_set_radius(self): def test_contains_many(self): x = [0, 0, 0, 0, 0] y = [0, 0, 0, 0, 0] - self.roi.set_center(0, 0) + self.roi.move_to(0, 0) self.roi.set_radius(1) assert all(self.roi.contains(x, y)) assert all(self.roi.contains(np.asarray(x), np.asarray(y))) assert not any(self.roi.contains(np.asarray(x) + 10, y)) def test_poly(self): - self.roi.set_center(0, 0) + self.roi.move_to(0, 0) self.roi.set_radius(1) x, y = self.roi.to_polygon() poly = PolygonalROI(vx=x, vy=y) @@ -312,7 +312,7 @@ def test_poly_undefined(self): def test_reset(self): assert not self.roi.defined() - self.roi.set_center(0, 0) + self.roi.move_to(0, 0) assert not self.roi.defined() self.roi.set_radius(2) assert self.roi.defined() @@ -320,7 +320,7 @@ def test_reset(self): assert not self.roi.defined() def test_multidim(self): - self.roi.set_center(0, 0) + self.roi.move_to(0, 0) self.roi.set_radius(1) x = np.array([.1, .2, .3, .4]).reshape(2, 2) y = np.array([-.1, -.2, -.3, -.4]).reshape(2, 2) @@ -329,7 +329,7 @@ def test_multidim(self): assert self.roi.contains(x, y).shape == (2, 2) def test_serialization(self): - self.roi.set_center(3, 4) + self.roi.move_to(3, 4) self.roi.set_radius(5) new_roi = roundtrip_roi(self.roi) assert_almost_equal(new_roi.xc, 3) diff --git a/glue/core/tests/test_subset.py b/glue/core/tests/test_subset.py index c2f1c87ad..c9c1a9072 100644 --- a/glue/core/tests/test_subset.py +++ b/glue/core/tests/test_subset.py @@ -14,7 +14,7 @@ from .. import DataCollection, ComponentLink from ...config import settings from ..data import Data, Component -from ..roi import CategoricalROI, RectangularROI, Projected3dROI, CircularROI +from ..roi import CategoricalROI, RectangularROI, Projected3dROI, CircularROI, EllipticalROI, RangeROI from ..message import SubsetDeleteMessage from ..registry import Registry from ..link_helpers import LinkSame @@ -221,8 +221,8 @@ def test_visual_attributes(self): s = d.new_subset(**visual_attributes) for attr, value in visual_attributes.items(): if attr == 'preferred_cmap': - from matplotlib.cm import get_cmap - assert s.style.preferred_cmap == get_cmap(visual_attributes[attr]) + import matplotlib + assert s.style.preferred_cmap == matplotlib.colormaps[visual_attributes[attr]] else: assert getattr(s.style, attr, None) == value @@ -933,6 +933,43 @@ def test_roi_subset_state_rotated(self): roi=roi) assert_equal(data_clone.subsets[1].to_mask(), [0, 0, 0, 1]) + # Also test move_to for simple subset state + theta = roi.theta + assert subset.subset_state.center() == roi.center() + subset.subset_state.move_to(2, 3) + assert_allclose(subset.subset_state.center(), (2, 3)) + assert_allclose(subset.subset_state._roi.center(), (2, 3)) + assert_allclose(subset.subset_state._roi.theta, theta) + + def test_roi_subset_state_composite_move_to(self): + xatt = self.data.id['a'] + yatt = self.data.id['c'] + roi_1 = RectangularROI(xmin=0, xmax=4, ymin=1, ymax=5) # xc=2, yc=3 + roi_2 = CircularROI(xc=1, yc=1, radius=2) + roi_3 = EllipticalROI(xc=3, yc=4, radius_x=2, radius_y=3, theta=0.785) + subset_state = AndState( + AndState(RoiSubsetState(xatt=xatt, yatt=yatt, roi=roi_1), + RoiSubsetState(xatt=xatt, yatt=yatt, roi=roi_2)), + InvertState(RoiSubsetState(xatt=xatt, yatt=yatt, roi=roi_3))) + assert_allclose(subset_state.center(), (2, 3)) + + subset_state.move_to(20, 30) + assert_allclose(subset_state.center(), (20, 30)) + assert_allclose(subset_state.state1.state1.center(), (20, 30)) # RectangularROI + assert_allclose(subset_state.state1.state2.center(), (19, 28)) # CircularROI + assert_allclose(subset_state.state2.state1.center(), (21, 31)) # EllipticalROI + assert_allclose(subset_state.state2.state1._roi.theta, 0.785) + assert not subset_state.state2.state2 + + def test_roi_subset_state_1d_move_to(self): + roi = RangeROI('x', min=0, max=5) + subset_state = RoiSubsetState(xatt=self.data.id['a'], yatt=self.data.id['c'], roi=roi) + assert_allclose(subset_state.center(), 2.5) + + subset_state.move_to(3) + assert_allclose(subset_state.center(), 3) + assert_allclose((subset_state._roi.min, subset_state._roi.max), (0.5, 5.5)) + @requires_scipy def test_floodfill_subset_state(): diff --git a/glue/utils/geometry.py b/glue/utils/geometry.py index 1c60a2dde..2b6ae1dba 100644 --- a/glue/utils/geometry.py +++ b/glue/utils/geometry.py @@ -156,7 +156,7 @@ def polygon_line_intersections(px, py, xval=None, yval=None): def floodfill(data, start_coords, threshold): - from scipy.ndimage.measurements import label + from scipy.ndimage import label # Determine value at the starting coordinates value = data[start_coords]