From ca29542fec5466e48eba0ce734729a9da78aeb66 Mon Sep 17 00:00:00 2001 From: Derek Homeier Date: Tue, 18 Jul 2023 19:03:35 +0200 Subject: [PATCH 1/4] Add strict_circle option and `BqplotTrueCircleMode` class --- glue_jupyter/bqplot/common/tools.py | 40 +++++++++++++++++++++++------ glue_jupyter/bqplot/image/viewer.py | 3 ++- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/glue_jupyter/bqplot/common/tools.py b/glue_jupyter/bqplot/common/tools.py index 8f7c21d5..9200fffe 100644 --- a/glue_jupyter/bqplot/common/tools.py +++ b/glue_jupyter/bqplot/common/tools.py @@ -275,7 +275,7 @@ class BqplotCircleMode(BqplotSelectionTool): action_text = 'Circular ROI' tool_tip = 'Define a circular region of interest' - def __init__(self, viewer, roi=None, finalize_callback=None, **kwargs): + def __init__(self, viewer, roi=None, finalize_callback=None, strict_circle=False, **kwargs): super().__init__(viewer, **kwargs) @@ -299,6 +299,7 @@ def __init__(self, viewer, roi=None, finalize_callback=None, **kwargs): self.interact.observe(self.on_selection_change, "selected_x") self.interact.observe(self.on_selection_change, "selected_y") self.finalize_callback = finalize_callback + self._strict_circle = strict_circle def update_selection(self, *args): if self.interact.brushing: @@ -309,17 +310,19 @@ def update_selection(self, *args): y = self.interact.selected_y # similar to https://github.com/glue-viz/glue/blob/b14ccffac6a5 # 271c2869ead9a562a2e66232e397/glue/core/roi.py#L1275-L1297 - # We should now check if the radius in data coordinates is the same - # along x and y, as if so then we can return a circle, otherwise we - # should return an ellipse. + # If _strict_circle set, enforce returning a circle; otherwise check + # if the radius in data coordinates is (nearly) the same along x and y, + # to return a circle as well, else we should return an ellipse. xc = x.mean() yc = y.mean() rx = abs(x[1] - x[0])/2 ry = abs(y[1] - y[0])/2 # We use a tolerance of 1e-2 below to match the tolerance set in glue-core # https://github.com/glue-viz/glue/blob/6b968b352bc5ad68b95ad5e3bb25550782a69ee8/glue/viewers/matplotlib/state.py#L198 - if np.allclose(rx, ry, rtol=1e-2): - roi = CircularROI(xc=xc, yc=yc, radius=rx) + if self._strict_circle: + roi = CircularROI(xc=xc, yc=yc, radius=np.sqrt((rx**2 + ry**2) * 0.5)) + elif np.allclose(rx, ry, rtol=1e-2): + roi = CircularROI(xc=xc, yc=yc, radius=(rx + ry) * 0.5) else: roi = EllipticalROI(xc=xc, yc=yc, radius_x=rx, radius_y=ry) self.viewer.apply_roi(roi) @@ -330,7 +333,10 @@ def update_from_roi(self, roi): if isinstance(roi, CircularROI): rx = ry = roi.radius elif isinstance(roi, EllipticalROI): - rx, ry = roi.radius_x, roi.radius_y + if self._strict_circle: + rx, ry = np.sqrt((roi.radius_x ** 2 + roi.radius_y ** 2) * 0.5) + else: + rx, ry = roi.radius_x, roi.radius_y else: raise TypeError(f'Cannot initialize a BqplotCircleMode from a {type(roi)}') self.interact.selected_x = [roi.xc - rx, roi.xc + rx] @@ -348,6 +354,21 @@ def activate(self): super().activate() +@viewer_tool +class BqplotTrueCircleMode(BqplotCircleMode): + + tool_id = 'bqplot:truecircle' + tool_tip = 'Define a strictly circular region of interest' + + def __init__(self, viewer, roi=None, finalize_callback=None, **kwargs): + + super().__init__(viewer, **kwargs) + + if not kwargs.pop('strict_circle', True): + raise ValueError('BqplotTrueCircleMode cannot have strict_circle=False') + self._strict_circle = True + + @viewer_tool class BqplotEllipseMode(BqplotCircleMode): @@ -582,7 +603,10 @@ def press(self, x, y): if layer.visible and isinstance(subset_state, RoiSubsetState): roi = subset_state.roi if roi.defined() and roi.contains(x, y): - if isinstance(roi, (EllipticalROI, CircularROI)): + if isinstance(roi, EllipticalROI): + self._active_tool = BqplotEllipseMode( + self.viewer, roi=roi, finalize_callback=self.release) + elif isinstance(roi, CircularROI): self._active_tool = BqplotCircleMode( self.viewer, roi=roi, finalize_callback=self.release) elif isinstance(roi, (PolygonalROI, RectangularROI)): diff --git a/glue_jupyter/bqplot/image/viewer.py b/glue_jupyter/bqplot/image/viewer.py index 4fa6afc1..4b79a7ef 100644 --- a/glue_jupyter/bqplot/image/viewer.py +++ b/glue_jupyter/bqplot/image/viewer.py @@ -30,7 +30,8 @@ class BqplotImageView(BqplotBaseView): _state_cls = BqplotImageViewerState _options_cls = ImageViewerStateWidget - tools = ['bqplot:home', 'bqplot:panzoom', 'bqplot:rectangle', 'bqplot:circle', 'bqplot:polygon'] + tools = ['bqplot:home', 'bqplot:panzoom', 'bqplot:rectangle', 'bqplot:circle', 'bqplot:polygon', + 'bqplot:ellipse', 'bqplot:truecircle'] def __init__(self, session): From fa1d4241d79dfc175886a4c222a7cce035cf82e7 Mon Sep 17 00:00:00 2001 From: Derek Homeier Date: Tue, 18 Jul 2023 19:04:49 +0200 Subject: [PATCH 2/4] Tests for 'bqplot:truecircle' and 'bqplot:ellipse' --- glue_jupyter/bqplot/tests/test_bqplot.py | 36 +++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/glue_jupyter/bqplot/tests/test_bqplot.py b/glue_jupyter/bqplot/tests/test_bqplot.py index 4e68712c..2d5adee7 100644 --- a/glue_jupyter/bqplot/tests/test_bqplot.py +++ b/glue_jupyter/bqplot/tests/test_bqplot.py @@ -4,7 +4,7 @@ from numpy.testing import assert_allclose from nbconvert.preprocessors import ExecutePreprocessor from glue.core import Data -from glue.core.roi import EllipticalROI +from glue.core.roi import CircularROI, EllipticalROI DATA = os.path.join(os.path.dirname(__file__), 'data') @@ -270,6 +270,40 @@ def test_imshow_circular_brush(app, data_image): assert_allclose(roi.radius_y, 273.25) +def test_imshow_true_circular_brush(app, data_image): + + v = app.imshow(data=data_image) + v.state.aspect = 'auto' + + tool = v.toolbar.tools['bqplot:truecircle'] + tool.activate() + tool.interact.brushing = True + tool.interact.selected = [(1.5, 3.5), (300.5, 550)] + tool.interact.brushing = False + + roi = data_image.subsets[0].subset_state.roi + assert isinstance(roi, CircularROI) + assert_allclose(roi.xc, 151.00) + assert_allclose(roi.yc, 276.75) + assert_allclose(roi.radius, 220.2451) + + +def test_imshow_elliptical_brush(app, data_image): + v = app.imshow(data=data_image) + v.state.aspect = 'auto' + + tool = v.toolbar.tools['bqplot:ellipse'] + tool.activate() + tool.interact.brushing = True + tool.interact.selected = [(1.5, 3.5), (300.5, 550)] + tool.interact.brushing = False + + roi = data_image.subsets[0].subset_state.roi + assert isinstance(roi, EllipticalROI) + assert_allclose(roi.xc, 151.00) + assert_allclose(roi.yc, 276.75) + + def test_imshow_equal_aspect(app, data_image): data = Data(array=np.random.random((100, 5))) app.data_collection.append(data) From 06c7ea7c81ed70558eefb40a75d1b3992a96b303 Mon Sep 17 00:00:00 2001 From: Derek Homeier Date: Wed, 9 Aug 2023 15:52:49 +0200 Subject: [PATCH 3/4] Create `TrueCircularROI` --- glue_jupyter/bqplot/common/tools.py | 14 ++++++++++---- glue_jupyter/bqplot/tests/test_bqplot.py | 5 +++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/glue_jupyter/bqplot/common/tools.py b/glue_jupyter/bqplot/common/tools.py index 9200fffe..a6485d33 100644 --- a/glue_jupyter/bqplot/common/tools.py +++ b/glue_jupyter/bqplot/common/tools.py @@ -27,6 +27,10 @@ ICONS_DIR = os.path.join(os.path.dirname(__file__), '..', '..', 'icons') +class TrueCircularROI(CircularROI): + pass + + class InteractCheckableTool(CheckableTool): def __init__(self, viewer): @@ -275,7 +279,7 @@ class BqplotCircleMode(BqplotSelectionTool): action_text = 'Circular ROI' tool_tip = 'Define a circular region of interest' - def __init__(self, viewer, roi=None, finalize_callback=None, strict_circle=False, **kwargs): + def __init__(self, viewer, roi=None, finalize_callback=None, strict_circle=None, **kwargs): super().__init__(viewer, **kwargs) @@ -291,6 +295,7 @@ def __init__(self, viewer, roi=None, finalize_callback=None, strict_circle=False border_style['stroke'] = INTERACT_COLOR self.interact.style = style self.interact.border_style = border_style + self._strict_circle = strict_circle if roi is not None: self.update_from_roi(roi) @@ -299,7 +304,6 @@ def __init__(self, viewer, roi=None, finalize_callback=None, strict_circle=False self.interact.observe(self.on_selection_change, "selected_x") self.interact.observe(self.on_selection_change, "selected_y") self.finalize_callback = finalize_callback - self._strict_circle = strict_circle def update_selection(self, *args): if self.interact.brushing: @@ -320,7 +324,7 @@ def update_selection(self, *args): # We use a tolerance of 1e-2 below to match the tolerance set in glue-core # https://github.com/glue-viz/glue/blob/6b968b352bc5ad68b95ad5e3bb25550782a69ee8/glue/viewers/matplotlib/state.py#L198 if self._strict_circle: - roi = CircularROI(xc=xc, yc=yc, radius=np.sqrt((rx**2 + ry**2) * 0.5)) + roi = TrueCircularROI(xc=xc, yc=yc, radius=np.sqrt((rx**2 + ry**2) * 0.5)) elif np.allclose(rx, ry, rtol=1e-2): roi = CircularROI(xc=xc, yc=yc, radius=(rx + ry) * 0.5) else: @@ -332,6 +336,8 @@ def update_selection(self, *args): def update_from_roi(self, roi): if isinstance(roi, CircularROI): rx = ry = roi.radius + if isinstance(roi, TrueCircularROI) and self._strict_circle is not False: + self._strict_circle = True elif isinstance(roi, EllipticalROI): if self._strict_circle: rx, ry = np.sqrt((roi.radius_x ** 2 + roi.radius_y ** 2) * 0.5) @@ -364,7 +370,7 @@ def __init__(self, viewer, roi=None, finalize_callback=None, **kwargs): super().__init__(viewer, **kwargs) - if not kwargs.pop('strict_circle', True): + if kwargs.pop('strict_circle', True) is False: raise ValueError('BqplotTrueCircleMode cannot have strict_circle=False') self._strict_circle = True diff --git a/glue_jupyter/bqplot/tests/test_bqplot.py b/glue_jupyter/bqplot/tests/test_bqplot.py index 2d5adee7..008e6438 100644 --- a/glue_jupyter/bqplot/tests/test_bqplot.py +++ b/glue_jupyter/bqplot/tests/test_bqplot.py @@ -4,7 +4,8 @@ from numpy.testing import assert_allclose from nbconvert.preprocessors import ExecutePreprocessor from glue.core import Data -from glue.core.roi import CircularROI, EllipticalROI +from glue.core.roi import EllipticalROI +from ..common.tools import TrueCircularROI DATA = os.path.join(os.path.dirname(__file__), 'data') @@ -282,7 +283,7 @@ def test_imshow_true_circular_brush(app, data_image): tool.interact.brushing = False roi = data_image.subsets[0].subset_state.roi - assert isinstance(roi, CircularROI) + assert isinstance(roi, TrueCircularROI) assert_allclose(roi.xc, 151.00) assert_allclose(roi.yc, 276.75) assert_allclose(roi.radius, 220.2451) From 449592f03c20b6cdf4d919229ff670710b5853b5 Mon Sep 17 00:00:00 2001 From: Derek Homeier Date: Thu, 10 Aug 2023 15:52:36 +0200 Subject: [PATCH 4/4] Remove `strict_circle` kwarg --- glue_jupyter/bqplot/common/tools.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/glue_jupyter/bqplot/common/tools.py b/glue_jupyter/bqplot/common/tools.py index a6485d33..6b20eb0c 100644 --- a/glue_jupyter/bqplot/common/tools.py +++ b/glue_jupyter/bqplot/common/tools.py @@ -279,7 +279,7 @@ class BqplotCircleMode(BqplotSelectionTool): action_text = 'Circular ROI' tool_tip = 'Define a circular region of interest' - def __init__(self, viewer, roi=None, finalize_callback=None, strict_circle=None, **kwargs): + def __init__(self, viewer, roi=None, finalize_callback=None, **kwargs): super().__init__(viewer, **kwargs) @@ -295,7 +295,7 @@ def __init__(self, viewer, roi=None, finalize_callback=None, strict_circle=None, border_style['stroke'] = INTERACT_COLOR self.interact.style = style self.interact.border_style = border_style - self._strict_circle = strict_circle + self._strict_circle = False if roi is not None: self.update_from_roi(roi) @@ -336,7 +336,7 @@ def update_selection(self, *args): def update_from_roi(self, roi): if isinstance(roi, CircularROI): rx = ry = roi.radius - if isinstance(roi, TrueCircularROI) and self._strict_circle is not False: + if isinstance(roi, TrueCircularROI): self._strict_circle = True elif isinstance(roi, EllipticalROI): if self._strict_circle: @@ -369,9 +369,6 @@ class BqplotTrueCircleMode(BqplotCircleMode): def __init__(self, viewer, roi=None, finalize_callback=None, **kwargs): super().__init__(viewer, **kwargs) - - if kwargs.pop('strict_circle', True) is False: - raise ValueError('BqplotTrueCircleMode cannot have strict_circle=False') self._strict_circle = True