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

Make ROI.move_to() behave consistently and expose it at subset_state level #2391

Merged
merged 6 commits into from
May 2, 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
82 changes: 51 additions & 31 deletions glue/core/roi.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import copy
import warnings

import numpy as np
from matplotlib.patches import Ellipse, Polygon, Rectangle, Path as MplPath, PathPatch
Expand Down Expand Up @@ -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
Expand All @@ -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():
Expand All @@ -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),
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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():
Expand All @@ -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
Expand Down Expand Up @@ -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))

Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
36 changes: 36 additions & 0 deletions glue/core/subset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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):

Expand Down Expand Up @@ -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
Expand Down
16 changes: 8 additions & 8 deletions glue/core/tests/test_roi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -312,15 +312,15 @@ 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()
self.roi.reset()
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)
Expand All @@ -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)
Expand Down
43 changes: 40 additions & 3 deletions glue/core/tests/test_subset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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():
Expand Down
2 changes: 1 addition & 1 deletion glue/utils/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down