From 6f1dbcec50badd99efd488a14046bedc5666ad40 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Sun, 15 Apr 2018 22:17:06 +0100 Subject: [PATCH 01/17] Started refactoring PV slicer --- glue/app/qt/application.py | 3 + glue/plugins/tools/pv_slicer/qt/__init__.py | 1 + glue/plugins/tools/pv_slicer/qt/pv_slicer.py | 184 +++++++------------ 3 files changed, 70 insertions(+), 118 deletions(-) diff --git a/glue/app/qt/application.py b/glue/app/qt/application.py index e7466e165..66a55a160 100644 --- a/glue/app/qt/application.py +++ b/glue/app/qt/application.py @@ -487,6 +487,9 @@ def _artihmetic_dialog(self, *event): dialog = ArithmeticEditorWidget(self.data_collection) dialog.exec_() + def selected_layers(self): + return self._layer_widget.selected_layers() + def _on_data_collection_change(self, *event): self._button_save_data.setEnabled(len(self.data_collection) > 0) self._button_link_data.setEnabled(len(self.data_collection) > 1) diff --git a/glue/plugins/tools/pv_slicer/qt/__init__.py b/glue/plugins/tools/pv_slicer/qt/__init__.py index 3ebb8e8d5..c029e0fa9 100644 --- a/glue/plugins/tools/pv_slicer/qt/__init__.py +++ b/glue/plugins/tools/pv_slicer/qt/__init__.py @@ -5,3 +5,4 @@ def setup(): from glue.viewers.image.qt import ImageViewer from glue.plugins.tools.pv_slicer.qt import PVSlicerMode # noqa ImageViewer.tools.append('slice') + ImageViewer.tools.append('pv:crosshair') diff --git a/glue/plugins/tools/pv_slicer/qt/pv_slicer.py b/glue/plugins/tools/pv_slicer/qt/pv_slicer.py index 76df4b647..245593e38 100644 --- a/glue/plugins/tools/pv_slicer/qt/pv_slicer.py +++ b/glue/plugins/tools/pv_slicer/qt/pv_slicer.py @@ -1,12 +1,20 @@ import numpy as np -from glue.viewers.matplotlib.toolbar_mode import PathMode -from glue.viewers.image.qt import StandaloneImageViewer +from glue.core import Data +from glue.core.coordinates import coordinates_from_wcs +from glue.viewers.common.qt.toolbar_mode import PathMode from glue.config import viewer_tool -from glue.utils import defer_draw +from glue.viewers.common.qt.toolbar_mode import ToolbarModeBase from glue.core.coordinate_helpers import axis_label +class PVSliceData(Data): + parent_data = None + parent_data_x = None + parent_data_y = None + parent_viewer = None + + @viewer_tool class PVSlicerMode(PathMode): @@ -26,6 +34,7 @@ def __init__(self, viewer, **kwargs): self.viewer.state.add_callback('reference_data', self._on_reference_data_change) def _on_reference_data_change(self, reference_data): + print(reference_data, reference_data.ndim) if reference_data is not None: self.enabled = reference_data.ndim == 3 @@ -37,147 +46,86 @@ def _extract_callback(self, mode): """ Extract a PV-like slice, given a path traced on the widget """ + vx, vy = mode.roi().to_polygon() - self._build_from_vertices(vx, vy) - def _build_from_vertices(self, vx, vy): pv_slice, x, y, wcs = _slice_from_path(vx, vy, self.viewer.state.reference_data, self.viewer.state.layers[0].attribute, self.viewer.state.wcsaxes_slice[::-1]) - if self._slice_widget is None: - self._slice_widget = PVSliceWidget(image=pv_slice, wcs=wcs, - image_viewer=self.viewer, - x=x, y=y, interpolation='nearest') - self.viewer._session.application.add_widget(self._slice_widget, - label='Custom Slice') - self._slice_widget.window_closed.connect(self._clear_path) - else: - self._slice_widget.set_image(image=pv_slice, wcs=wcs, - x=x, y=y, interpolation='nearest') - result = self._slice_widget - result.axes.set_xlabel("Position along path") + xlabel = "Position along path" if wcs is None: - result.axes.set_ylabel("Cube slice index") + ylabel = "Cube slice index" else: - result.axes.set_ylabel(_slice_label(self.viewer.state.reference_data, - self.viewer.state.wcsaxes_slice[::-1])) + ylabel = _slice_label(self.viewer.state.reference_data, + self.viewer.state.wcsaxes_slice[::-1]) - result.show() + wcs.wcs.ctype = [xlabel, ylabel] - def close(self): - if self._slice_widget: - self._slice_widget.close() - return super(PVSlicerMode, self).close() + data = PVSliceData(label=self.viewer.state.reference_data.label + " [slice]") + data.coords = coordinates_from_wcs(wcs) + data[self.viewer.state.layers[0].attribute] = pv_slice + # TODO: use weak references + data.parent_data = self.viewer.state.reference_data + data.parent_data_x = x + data.parent_data_y = y + data.parent_viewer = self.viewer -class PVSliceWidget(StandaloneImageViewer): + selected = self.viewer.session.application.selected_layers() - """ A standalone image widget with extra interactivity for PV slices """ + print(selected) - def __init__(self, image=None, wcs=None, image_viewer=None, - x=None, y=None, **kwargs): - """ - :param image: 2D Numpy array representing the PV Slice - :param wcs: WCS for the PV slice - :param image_viewer: Parent ImageViewer this was extracted from - :param kwargs: Extra keywords are passed to imshow - """ - self._crosshairs = None - self._parent = image_viewer - super(PVSliceWidget, self).__init__(image=image, wcs=wcs, **kwargs) - conn = self.axes.figure.canvas.mpl_connect - self._down_id = conn('button_press_event', self._on_click) - self._move_id = conn('motion_notify_event', self._on_move) - self.axes.format_coord = self._format_coord - self._x = x - self._y = y - self._parent.state.add_callback('x_att', self.reset) - self._parent.state.add_callback('y_att', self.reset) - - def _format_coord(self, x, y): - """ - Return a formatted location label for the taskbar + if len(selected) == 1 and isinstance(selected[0], PVSliceData): + selected[0].update_values_from_data(data) + else: + self.viewer.session.data_collection.append(data) - :param x: x pixel location in slice array - :param y: y pixel location in slice array - """ - # xy -> xyz in image view - pix = self._pos_in_parent(xdata=x, ydata=y) - - # xyz -> data pixel coords - # accounts for fact that image might be shown transposed/rotated - s = list(self._slc) - idx = _slice_index(self._parent.state.reference_data, self._slc) - s[s.index('x')] = pix[0] - s[s.index('y')] = pix[1] - s[idx] = pix[2] - - # labels = self._parent.coordinate_labels(s) - # return ' '.join(labels) - return '' - - def set_image(self, image=None, wcs=None, x=None, y=None, **kwargs): - super(PVSliceWidget, self).set_image(image=image, wcs=wcs, **kwargs) - self._axes.set_aspect('auto') - self._axes.set_xlim(-0.5, image.shape[1] - 0.5) - self._axes.set_ylim(-0.5, image.shape[0] - 0.5) - self._slc = self._parent.state.wcsaxes_slice[::-1] - self._x = x - self._y = y - - @defer_draw - def _sync_slice(self, event): - s = list(self._slc) - # XXX breaks if display_data changes - _, _, z = self._pos_in_parent(event) - s[_slice_index(self._parent.state.reference_data, s)] = int(z) - self._parent.state.slices = tuple(s) - - @defer_draw - def _draw_crosshairs(self, event): - x, y, _ = self._pos_in_parent(event) - self._parent.show_crosshairs(x, y) - - @defer_draw - def _on_move(self, event): - if not event.button: - return - - if not event.inaxes or event.canvas.toolbar.mode != '': - return - - self._sync_slice(event) - self._draw_crosshairs(event) - - def _pos_in_parent(self, event=None, xdata=None, ydata=None): - - if event is not None: - xdata = event.xdata - ydata = event.ydata +@viewer_tool +class PVLinkCursorMode(ToolbarModeBase): + """ + Selects pixel under mouse cursor. + """ + + icon = "glue_point" + tool_id = 'pv:crosshair' + + _pressed = False + + def __init__(self, *args, **kwargs): + super(PVLinkCursorMode, self).__init__(*args, **kwargs) + self._move_callback = self._on_move + self._press_callback = self._on_move + self.viewer.state.add_callback('reference_data', self._on_reference_data_change) + + def _on_reference_data_change(self, reference_data): + self.enabled = isinstance(reference_data, PVSliceData) + self.data = reference_data + + def _on_move(self, mode): + + # Find position of click in the image viewer + xdata, ydata = self._event_xdata, self._event_ydata + + # TODO: Make this robust in case the axes have been swapped # Find position slice where cursor is - ind = int(round(np.clip(xdata, 0, self._im_array.shape[1] - 1))) + ind = int(round(np.clip(xdata, 0, self.data.shape[1] - 1))) # Find pixel coordinate in input image for this slice - x = self._x[ind] - y = self._y[ind] + x = self.data.parent_data_x[ind] + y = self.data.parent_data_y[ind] # The 3-rd coordinate in the input WCS is simply the second # coordinate in the PV slice. z = ydata - return x, y, z - - def _on_click(self, event): - if not event.inaxes or event.canvas.toolbar.mode != '': - return - self._sync_slice(event) - self._draw_crosshairs(event) + self.data.parent_viewer.show_crosshairs(x, y) - def reset(self, *args): - self.close() + s = list(self.data.parent_viewer.state.wcsaxes_slice[::-1]) + s[_slice_index(self.data.parent_viewer.state.reference_data, s)] = int(z) + self.data.parent_viewer.state.slices = tuple(s) def _slice_from_path(x, y, data, attribute, slc): From 29dd04759ea8c1ac03da68b5dfa5d5c4bfbd2158 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Sun, 15 Apr 2018 22:22:43 +0100 Subject: [PATCH 02/17] Remove StandaloneImageViewer [ci skip] --- CHANGES.md | 2 + .../image/qt/standalone_image_viewer.py | 168 ------------------ 2 files changed, 2 insertions(+), 168 deletions(-) delete mode 100644 glue/viewers/image/qt/standalone_image_viewer.py diff --git a/CHANGES.md b/CHANGES.md index 100b7bed0..ba16cb33d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -574,6 +574,8 @@ v0.13.0 (2018-04-27) subsets to define the areas in which to compute profiles rather than custom ROIs. [#1635] +* Remove ``StandaloneImageViewer`` class. [#1663] + * Added support for PySide2 and remove support for PyQt4 and PySide. [#1662] diff --git a/glue/viewers/image/qt/standalone_image_viewer.py b/glue/viewers/image/qt/standalone_image_viewer.py deleted file mode 100644 index 740ad1446..000000000 --- a/glue/viewers/image/qt/standalone_image_viewer.py +++ /dev/null @@ -1,168 +0,0 @@ -import numpy as np - -from qtpy import QtCore, QtWidgets - -from glue.config import colormaps -from glue.viewers.common.qt.toolbar import BasicToolbar -from glue.viewers.matplotlib.qt.widget import MplWidget -from glue.viewers.image.composite_array import CompositeArray -from glue.external.modest_image import imshow -from glue.utils import defer_draw - -from glue.viewers.matplotlib.mpl_axes import init_mpl - -# Import the mouse mode to make sure it gets registered -from glue.viewers.image.qt.contrast_mouse_mode import ContrastBiasMode # noqa - -__all__ = ['StandaloneImageViewer'] - - -class StandaloneImageViewer(QtWidgets.QMainWindow): - """ - A simplified image viewer, without any brushing or linking, - but with the ability to adjust contrast and resample. - """ - window_closed = QtCore.Signal() - _toolbar_cls = BasicToolbar - tools = ['image:contrast', 'image:colormap'] - - def __init__(self, image=None, wcs=None, parent=None, **kwargs): - """ - :param image: Image to display (2D numpy array) - :param parent: Parent widget (optional) - - :param kwargs: Extra keywords to pass to imshow - """ - super(StandaloneImageViewer, self).__init__(parent) - - self.central_widget = MplWidget() - self.setCentralWidget(self.central_widget) - self._setup_axes() - - self._composite = CompositeArray() - self._composite.allocate('image') - - self._im = None - - self.initialize_toolbar() - - if image is not None: - self.set_image(image=image, wcs=wcs, **kwargs) - - def _setup_axes(self): - _, self._axes = init_mpl(self.central_widget.canvas.fig, axes=None, wcs=True) - self._axes.set_aspect('equal', adjustable='datalim') - - @defer_draw - def set_image(self, image=None, wcs=None, **kwargs): - """ - Update the image shown in the widget - """ - if self._im is not None: - self._im.remove() - self._im = None - - kwargs.setdefault('origin', 'upper') - - self._composite.set('image', array=image, color=colormaps.members[0][1]) - self._im = imshow(self._axes, self._composite, **kwargs) - self._im_array = image - self._set_norm(self._contrast_mode) - - if 'extent' in kwargs: - self.axes.set_xlim(kwargs['extent'][:2]) - self.axes.set_ylim(kwargs['extent'][2:]) - else: - ny, nx = image.shape - self.axes.set_xlim(-0.5, nx - 0.5) - self.axes.set_ylim(-0.5, ny - 0.5) - - # FIXME: for a reason I don't quite understand, dataLim doesn't - # get updated immediately here, which means that there are then - # issues in the first draw of the image (the limits are such that - # only part of the image is shown). We just set dataLim manually - # to avoid this issue. This is also done in ImageViewer. - self.axes.dataLim.intervalx = self.axes.get_xlim() - self.axes.dataLim.intervaly = self.axes.get_ylim() - - self._redraw() - - @property - def axes(self): - """ - The Matplotlib axes object for this figure - """ - return self._axes - - def show(self): - super(StandaloneImageViewer, self).show() - self._redraw() - - def _redraw(self): - self.central_widget.canvas.draw_idle() - - def set_cmap(self, cmap): - self._composite.set('image', color=cmap) - self._im.invalidate_cache() - self._redraw() - - def mdi_wrap(self): - """ - Embed this widget in a GlueMdiSubWindow - """ - from glue.app.qt.mdi_area import GlueMdiSubWindow - sub = GlueMdiSubWindow() - sub.setWidget(self) - self.destroyed.connect(sub.close) - self.window_closed.connect(sub.close) - sub.resize(self.size()) - self._mdi_wrapper = sub - return sub - - def closeEvent(self, event): - if self._im is not None: - self._im.remove() - self._im = None - self.window_closed.emit() - return super(StandaloneImageViewer, self).closeEvent(event) - - def _set_norm(self, mode): - """ - Use the `ContrastMouseMode` to adjust the transfer function - """ - - pmin, pmax = mode.get_clip_percentile() - - if pmin is None: - clim = mode.get_vmin_vmax() - else: - clim = (np.nanpercentile(self._im_array, pmin), - np.nanpercentile(self._im_array, pmax)) - - stretch = mode.stretch - self._composite.set('image', clim=clim, stretch=stretch, - bias=mode.bias, contrast=mode.contrast) - - self._im.invalidate_cache() - self._redraw() - - def initialize_toolbar(self): - - from glue.config import viewer_tool - - self.toolbar = self._toolbar_cls(self) - - for tool_id in self.tools: - mode_cls = viewer_tool.members[tool_id] - if tool_id == 'image:contrast': - mode = mode_cls(self, move_callback=self._set_norm) - self._contrast_mode = mode - else: - mode = mode_cls(self) - self.toolbar.add_tool(mode) - - self.addToolBar(self.toolbar) - - def set_status(self, message): - sb = self.statusBar() - sb.showMessage(message) From 11924d4a79d5cffad16d81416f54457965a6d038 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Mon, 16 Apr 2018 11:09:15 +0100 Subject: [PATCH 03/17] Open new viewer --- glue/plugins/tools/pv_slicer/qt/pv_slicer.py | 20 +++++++++++++++++--- glue/viewers/image/qt/__init__.py | 1 - 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/glue/plugins/tools/pv_slicer/qt/pv_slicer.py b/glue/plugins/tools/pv_slicer/qt/pv_slicer.py index 245593e38..6f392388c 100644 --- a/glue/plugins/tools/pv_slicer/qt/pv_slicer.py +++ b/glue/plugins/tools/pv_slicer/qt/pv_slicer.py @@ -2,10 +2,11 @@ from glue.core import Data from glue.core.coordinates import coordinates_from_wcs +from glue.core.coordinate_helpers import axis_label from glue.viewers.common.qt.toolbar_mode import PathMode from glue.config import viewer_tool from glue.viewers.common.qt.toolbar_mode import ToolbarModeBase -from glue.core.coordinate_helpers import axis_label +from glue.viewers.image.qt import ImageViewer class PVSliceData(Data): @@ -74,12 +75,25 @@ def _extract_callback(self, mode): selected = self.viewer.session.application.selected_layers() - print(selected) - if len(selected) == 1 and isinstance(selected[0], PVSliceData): selected[0].update_values_from_data(data) + data = selected[0] + for tab in self.viewer.session.application.viewers: + for viewer in tab: + if data in viewer._layer_artist_container: + open_viewer = False + break + if not open_viewer: + break + else: + open_viewer = True else: self.viewer.session.data_collection.append(data) + open_viewer = True + + print("OPEN VIEWER", open_viewer) + if open_viewer: + self.viewer.session.application.new_data_viewer(ImageViewer, data=data) @viewer_tool diff --git a/glue/viewers/image/qt/__init__.py b/glue/viewers/image/qt/__init__.py index 63a0b2170..9f21af648 100644 --- a/glue/viewers/image/qt/__init__.py +++ b/glue/viewers/image/qt/__init__.py @@ -1,5 +1,4 @@ from .data_viewer import ImageViewer # noqa -from .standalone_image_viewer import StandaloneImageViewer # noqa def setup(): From f51047ff7587afd40f5ea5cc3191745477fd3a03 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Sun, 12 Aug 2018 10:50:23 +0100 Subject: [PATCH 04/17] More work on refactoring of PV slicer --- glue/plugins/tools/pv_slicer/qt/pv_slicer.py | 22 +-- .../pv_slicer/qt/tests/test_pv_slicer.py | 135 ++++++------------ glue/viewers/image/qt/data_viewer.py | 3 +- glue/viewers/image/qt/profile_viewer_tool.py | 9 ++ 4 files changed, 61 insertions(+), 108 deletions(-) diff --git a/glue/plugins/tools/pv_slicer/qt/pv_slicer.py b/glue/plugins/tools/pv_slicer/qt/pv_slicer.py index 6f392388c..b1e3842ec 100644 --- a/glue/plugins/tools/pv_slicer/qt/pv_slicer.py +++ b/glue/plugins/tools/pv_slicer/qt/pv_slicer.py @@ -35,7 +35,6 @@ def __init__(self, viewer, **kwargs): self.viewer.state.add_callback('reference_data', self._on_reference_data_change) def _on_reference_data_change(self, reference_data): - print(reference_data, reference_data.ndim) if reference_data is not None: self.enabled = reference_data.ndim == 3 @@ -67,17 +66,12 @@ def _extract_callback(self, mode): data.coords = coordinates_from_wcs(wcs) data[self.viewer.state.layers[0].attribute] = pv_slice - # TODO: use weak references - data.parent_data = self.viewer.state.reference_data - data.parent_data_x = x - data.parent_data_y = y - data.parent_viewer = self.viewer - selected = self.viewer.session.application.selected_layers() if len(selected) == 1 and isinstance(selected[0], PVSliceData): selected[0].update_values_from_data(data) data = selected[0] + open_viewer = True for tab in self.viewer.session.application.viewers: for viewer in tab: if data in viewer._layer_artist_container: @@ -85,15 +79,21 @@ def _extract_callback(self, mode): break if not open_viewer: break - else: - open_viewer = True else: self.viewer.session.data_collection.append(data) open_viewer = True - print("OPEN VIEWER", open_viewer) + # TODO: use weak references + data.parent_data = self.viewer.state.reference_data + data.parent_data_x = x + data.parent_data_y = y + data.parent_viewer = self.viewer + if open_viewer: - self.viewer.session.application.new_data_viewer(ImageViewer, data=data) + viewer = self.viewer.session.application.new_data_viewer(ImageViewer, data=data) + + viewer.state.aspect = 'auto' + viewer.state.reset_limits() @viewer_tool diff --git a/glue/plugins/tools/pv_slicer/qt/tests/test_pv_slicer.py b/glue/plugins/tools/pv_slicer/qt/tests/test_pv_slicer.py index 2dc6d0364..911ed8540 100644 --- a/glue/plugins/tools/pv_slicer/qt/tests/test_pv_slicer.py +++ b/glue/plugins/tools/pv_slicer/qt/tests/test_pv_slicer.py @@ -3,13 +3,48 @@ from numpy.testing import assert_allclose from glue.core import Data -from glue.core.coordinates import IdentityCoordinates -from glue.viewers.image.qt import StandaloneImageViewer, ImageViewer -from glue.tests.helpers import requires_astropy, requires_scipy from glue.app.qt import GlueApplication -from glue.utils.qt import process_events +from glue.tests.helpers import requires_astropy, requires_scipy +from glue.viewers.image.qt.data_viewer import ImageViewer + +from ..pv_slicer import _slice_from_path, _slice_label, _slice_index + + +class TestPVSlicerMode(object): + + def setup_method(self, method): + self.app = GlueApplication() + self.data_collection = self.app.data_collection + self.data = Data(x=np.arange(6000).reshape((10, 20, 30))) + self.data_collection.append(self.data) + self.viewer = self.app.new_data_viewer(ImageViewer, data=self.data) + + def teardown_method(self, method): + self.viewer = None + self.app.close() + self.app = None + + def test_plain(self): + + self.viewer.toolbar.active_tool = 'slice' + tool = self.viewer.toolbar.active_tool # should now be set to PVSlicerMode + + roi = MagicMock() + roi.to_polygon.return_value = [1, 10, 12], [2, 13, 14] -from ..pv_slicer import _slice_from_path, _slice_label, _slice_index, PVSliceWidget + mode = MagicMock() + mode.roi.return_value = roi + + tool._extract_callback(mode) + + assert len(self.data_collection) == 2 + assert self.data_collection[1].shape == (10, 16) + + # If we do it again it should just update the existing slice dataset in-place + + roi.to_polygon.return_value = [1, 10, 12], [2, 13, 14] + + tool._extract_callback(mode) @requires_astropy @@ -62,93 +97,3 @@ def test_slice_index(): d = Data(x=np.zeros((2, 3, 4))) assert _slice_index(d, (0, 'y', 'x')) == 0 assert _slice_index(d, ('y', 0, 'x')) == 1 - - -class TestStandaloneImageViewer(object): - - def setup_method(self, method): - im = np.random.random((3, 3)) - self.w = StandaloneImageViewer(im) - - def teardown_method(self, method): - self.w.close() - - def test_set_cmap(self): - cm_mode = self.w.toolbar.tools['image:colormap'] - act = cm_mode.menu_actions()[1] - act.trigger() - assert self.w._composite.layers['image']['color'] is act.cmap - - def test_double_set_image(self): - assert len(self.w._axes.images) == 1 - self.w.set_image(np.zeros((3, 3))) - assert len(self.w._axes.images) == 1 - - -class MockImageViewer(object): - - def __init__(self, slice, data): - self.slice = slice - self.data = data - self.wcs = None - self.state = MagicMock() - - -class TestPVSliceWidget(object): - - def setup_method(self, method): - - self.d = Data(x=np.zeros((2, 3, 4))) - self.slc = (0, 'y', 'x') - self.image = MockImageViewer(self.slc, self.d) - self.w = PVSliceWidget(image=np.zeros((3, 4)), wcs=None, image_viewer=self.image) - - def teardown_method(self, method): - self.w.close() - - def test_basic(self): - pass - - -class TestPVSliceTool(object): - - def setup_method(self, method): - self.cube = Data(label='cube', x=np.arange(1000).reshape((5, 10, 20))) - self.application = GlueApplication() - self.application.data_collection.append(self.cube) - self.viewer = self.application.new_data_viewer(ImageViewer) - self.viewer.add_data(self.cube) - - def teardown_method(self, method): - self.viewer.close() - self.viewer = None - self.application.close() - self.application = None - - @requires_astropy - @requires_scipy - def test_basic(self): - - self.viewer.toolbar.active_tool = 'slice' - - self.viewer.axes.figure.canvas.draw() - process_events() - - x, y = self.viewer.axes.transData.transform([[0.9, 4]])[0] - self.viewer.axes.figure.canvas.button_press_event(x, y, 1) - x, y = self.viewer.axes.transData.transform([[7.2, 6.6]])[0] - self.viewer.axes.figure.canvas.button_press_event(x, y, 1) - - process_events() - - assert len(self.application.tab().subWindowList()) == 1 - - self.viewer.axes.figure.canvas.key_press_event('enter') - - process_events() - - assert len(self.application.tab().subWindowList()) == 2 - - pv_widget = self.application.tab().subWindowList()[1].widget() - assert pv_widget._x.shape == (6,) - assert pv_widget._y.shape == (6,) diff --git a/glue/viewers/image/qt/data_viewer.py b/glue/viewers/image/qt/data_viewer.py index c78a443f0..1dd3b3c45 100644 --- a/glue/viewers/image/qt/data_viewer.py +++ b/glue/viewers/image/qt/data_viewer.py @@ -36,8 +36,7 @@ class ImageViewer(MatplotlibImageMixin, MatplotlibDataViewer): # we override get_data_layer_artist and get_subset_layer_artist for # more advanced logic. - tools = ['select:rectangle', 'select:xrange', - 'select:yrange', 'select:circle', + tools = ['select:rectangle', 'select:circle', 'select:polygon', 'image:point_selection', 'image:contrast_bias', 'profile-viewer'] diff --git a/glue/viewers/image/qt/profile_viewer_tool.py b/glue/viewers/image/qt/profile_viewer_tool.py index 48b110aab..18abeb5c7 100644 --- a/glue/viewers/image/qt/profile_viewer_tool.py +++ b/glue/viewers/image/qt/profile_viewer_tool.py @@ -10,6 +10,15 @@ class ProfileViewerTool(Tool): icon = 'glue_spectrum' tool_id = 'profile-viewer' + def __init__(self, viewer): + super(ProfileViewerTool, self).__init__(viewer) + self.profile_viewers = [] + self.viewer.state.add_callback('reference_data', self._on_reference_data_change) + + def _on_reference_data_change(self, reference_data): + if reference_data is not None: + self.enabled = reference_data.ndim == 3 + @property def profile_viewers_exist(self): from glue.viewers.profile.qt import ProfileViewer From 8c5fac8e4fa52bd10e2996fb60e5c88f850dc5f9 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Fri, 31 Jan 2020 17:22:01 +0000 Subject: [PATCH 05/17] Fixed imports --- glue/plugins/tools/pv_slicer/qt/pv_slicer.py | 4 ++-- glue/plugins/tools/pv_slicer/qt/tests/test_pv_slicer.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/glue/plugins/tools/pv_slicer/qt/pv_slicer.py b/glue/plugins/tools/pv_slicer/qt/pv_slicer.py index b1e3842ec..e488ad1cc 100644 --- a/glue/plugins/tools/pv_slicer/qt/pv_slicer.py +++ b/glue/plugins/tools/pv_slicer/qt/pv_slicer.py @@ -3,9 +3,9 @@ from glue.core import Data from glue.core.coordinates import coordinates_from_wcs from glue.core.coordinate_helpers import axis_label -from glue.viewers.common.qt.toolbar_mode import PathMode +from glue.viewers.matplotlib.toolbar_mode import PathMode from glue.config import viewer_tool -from glue.viewers.common.qt.toolbar_mode import ToolbarModeBase +from glue.viewers.matplotlib.toolbar_mode import ToolbarModeBase from glue.viewers.image.qt import ImageViewer diff --git a/glue/plugins/tools/pv_slicer/qt/tests/test_pv_slicer.py b/glue/plugins/tools/pv_slicer/qt/tests/test_pv_slicer.py index 911ed8540..a0d7e4076 100644 --- a/glue/plugins/tools/pv_slicer/qt/tests/test_pv_slicer.py +++ b/glue/plugins/tools/pv_slicer/qt/tests/test_pv_slicer.py @@ -3,6 +3,7 @@ from numpy.testing import assert_allclose from glue.core import Data +from glue.core.coordinates import IdentityCoordinates from glue.app.qt import GlueApplication from glue.tests.helpers import requires_astropy, requires_scipy from glue.viewers.image.qt.data_viewer import ImageViewer From 39e78f5fa99742eb62fdf068451a5ac122586d18 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Fri, 31 Jan 2020 18:58:32 +0000 Subject: [PATCH 06/17] Added PVSlicedData, which is a DerivedData sub-class, and make it functional --- glue/core/data.py | 2 +- .../plugins/tools/pv_slicer/pv_sliced_data.py | 140 ++++++++++++++++++ glue/plugins/tools/pv_slicer/qt/pv_slicer.py | 120 +++------------ 3 files changed, 158 insertions(+), 104 deletions(-) create mode 100644 glue/plugins/tools/pv_slicer/pv_sliced_data.py diff --git a/glue/core/data.py b/glue/core/data.py index 92e7bdd4e..48b2b780b 100644 --- a/glue/core/data.py +++ b/glue/core/data.py @@ -528,7 +528,7 @@ def compute_histogram(self, cids, weights=None, range=None, bins=None, log=None, raise NotImplementedError() def compute_fixed_resolution_buffer(self, bounds, target_data=None, target_cid=None, - subset_state=None, broadcast=True): + subset_state=None, broadcast=True, cache_id=None): """ Get a fixed-resolution buffer. diff --git a/glue/plugins/tools/pv_slicer/pv_sliced_data.py b/glue/plugins/tools/pv_slicer/pv_sliced_data.py new file mode 100644 index 000000000..7e6ff3b25 --- /dev/null +++ b/glue/plugins/tools/pv_slicer/pv_sliced_data.py @@ -0,0 +1,140 @@ +import numpy as np + +from glue.core.data_derived import DerivedData + +__all__ = ['PVSlicedData'] + + +def sample_points(x, y, spacing=1): + + # Code adapted from pvextractor + + # Find the distance interval between all pairs of points + dx = np.diff(x) + dy = np.diff(y) + dd = np.hypot(dx, dy) + + # Find the total displacement along the broken curve + d = np.hstack([0., np.cumsum(dd)]) + + # Figure out the number of points to sample, and stop short of the + # last point. + n_points = int(np.floor(d[-1] / spacing)) + + if n_points == 0: + raise ValueError("Path is shorter than spacing") + + d_sampled = np.linspace(0., n_points * spacing, n_points + 1) + + x_sampled = np.interp(d_sampled, d, x) + y_sampled = np.interp(d_sampled, d, y) + + return x_sampled, y_sampled + + +class PVSlicedData(DerivedData): + """ + A dataset where two dimensions have been replaced with one using a path. + + The extra dimension is added as the last dimension + """ + + def __init__(self, original_data, cid_x, x, cid_y, y, label=''): + super(DerivedData, self).__init__() + self.original_data = original_data + self.cid_x = cid_x + self.cid_y = cid_y + self.set_xy(x, y) + self.sliced_dims = (cid_x.axis, cid_y.axis) + self._label = label + + def set_xy(self, x, y): + x, y = sample_points(x, y) + self.x = x + self.y = y + + @property + def label(self): + return self._label + + def _without_sliced(self, iterable): + return [x for ix, x in enumerate(iterable) if ix not in self.sliced_dims] + + @property + def shape(self): + return self._without_sliced(self.original_data.shape) + [len(self.x)] + + @property + def main_components(self): + return self.original_data.main_components + + def get_kind(self, cid): + return self.original_data.get_kind(cid) + + def get_data(self, cid, view=None): + + if cid in self.pixel_component_ids: + return super().get_data(cid, view) + + pix_coords = [] + + advanced_indexing = view is not None and isinstance(view[0], np.ndarray) + + idim_current = -1 + + for idim in range(self.original_data.ndim): + + if idim == self.cid_x.axis: + pix = self.x + idim_current = self.ndim - 1 + elif idim == self.cid_y.axis: + pix = self.y + idim_current = self.ndim - 1 + else: + pix = np.arange(self.original_data.shape[idim]) + idim_current += 1 + + if view is not None and len(view) > idim_current: + pix = pix[view[idim_current]] + print("DONE") + + print(idim, idim_current, pix.shape) + + pix_coords.append(pix) + + if not advanced_indexing: + pix_coords = np.meshgrid(*pix_coords, indexing='ij', copy=False) + + print(pix_coords[0].shape) + + shape = pix_coords[0].shape + + keep = np.ones(shape, dtype=bool) + for idim in range(self.original_data.ndim): + keep &= (pix_coords[idim] >= 0) & (pix_coords[idim] < self.original_data.shape[idim]) + + pix_coords = [x[keep].astype(int) for x in pix_coords] + + result = np.zeros(shape) + + result[keep] = self.original_data.get_data(cid, view=pix_coords) + + return result + + def get_mask(self, subset_state, view=None): + # Optimize by getting pixel coordinates of original data in new + # frame of reference and getting the mask for these indices + if view is None: + view = Ellipsis + return self.callable(self.original_data.get_mask(subset_state))[view] + + def compute_statistic(self, *args, **kwargs): + return self.original_data.compute_statistic(*args, **kwargs) + + def compute_histogram(self, *args, **kwargs): + return self.original_data.compute_histogram(*args, **kwargs) + + def compute_fixed_resolution_buffer(self, *args, **kwargs): + from glue.core.fixed_resolution_buffer import compute_fixed_resolution_buffer + print(args, kwargs) + return compute_fixed_resolution_buffer(self, *args, **kwargs) diff --git a/glue/plugins/tools/pv_slicer/qt/pv_slicer.py b/glue/plugins/tools/pv_slicer/qt/pv_slicer.py index e488ad1cc..727fcfd7b 100644 --- a/glue/plugins/tools/pv_slicer/qt/pv_slicer.py +++ b/glue/plugins/tools/pv_slicer/qt/pv_slicer.py @@ -7,13 +7,7 @@ from glue.config import viewer_tool from glue.viewers.matplotlib.toolbar_mode import ToolbarModeBase from glue.viewers.image.qt import ImageViewer - - -class PVSliceData(Data): - parent_data = None - parent_data_x = None - parent_data_y = None - parent_viewer = None +from glue.plugins.tools.pv_slicer.pv_sliced_data import PVSlicedData @viewer_tool @@ -49,28 +43,14 @@ def _extract_callback(self, mode): vx, vy = mode.roi().to_polygon() - pv_slice, x, y, wcs = _slice_from_path(vx, vy, self.viewer.state.reference_data, - self.viewer.state.layers[0].attribute, - self.viewer.state.wcsaxes_slice[::-1]) - - xlabel = "Position along path" - if wcs is None: - ylabel = "Cube slice index" - else: - ylabel = _slice_label(self.viewer.state.reference_data, - self.viewer.state.wcsaxes_slice[::-1]) - - wcs.wcs.ctype = [xlabel, ylabel] - - data = PVSliceData(label=self.viewer.state.reference_data.label + " [slice]") - data.coords = coordinates_from_wcs(wcs) - data[self.viewer.state.layers[0].attribute] = pv_slice - selected = self.viewer.session.application.selected_layers() - if len(selected) == 1 and isinstance(selected[0], PVSliceData): - selected[0].update_values_from_data(data) + if len(selected) == 1 and isinstance(selected[0], PVSlicedData): data = selected[0] + data.original_data = self.viewer.state.reference_data + data.x_att = self.viewer.state.x_att + data.y_att = self.viewer.state.y_att + data.set_xy(vx, vy) open_viewer = True for tab in self.viewer.session.application.viewers: for viewer in tab: @@ -80,15 +60,14 @@ def _extract_callback(self, mode): if not open_viewer: break else: + data = PVSlicedData(self.viewer.state.reference_data, + self.viewer.state.x_att, vx, + self.viewer.state.y_att, vy, + label=self.viewer.state.reference_data.label + " [slice]") + data.parent_viewer = self.viewer self.viewer.session.data_collection.append(data) open_viewer = True - # TODO: use weak references - data.parent_data = self.viewer.state.reference_data - data.parent_data_x = x - data.parent_data_y = y - data.parent_viewer = self.viewer - if open_viewer: viewer = self.viewer.session.application.new_data_viewer(ImageViewer, data=data) @@ -114,7 +93,7 @@ def __init__(self, *args, **kwargs): self.viewer.state.add_callback('reference_data', self._on_reference_data_change) def _on_reference_data_change(self, reference_data): - self.enabled = isinstance(reference_data, PVSliceData) + self.enabled = isinstance(reference_data, PVSlicedData) self.data = reference_data def _on_move(self, mode): @@ -122,14 +101,17 @@ def _on_move(self, mode): # Find position of click in the image viewer xdata, ydata = self._event_xdata, self._event_ydata + if xdata is None or ydata is None: + return + # TODO: Make this robust in case the axes have been swapped # Find position slice where cursor is ind = int(round(np.clip(xdata, 0, self.data.shape[1] - 1))) # Find pixel coordinate in input image for this slice - x = self.data.parent_data_x[ind] - y = self.data.parent_data_y[ind] + x = self.data.x[ind] + y = self.data.y[ind] # The 3-rd coordinate in the input WCS is simply the second # coordinate in the PV slice. @@ -142,74 +124,6 @@ def _on_move(self, mode): self.data.parent_viewer.state.slices = tuple(s) -def _slice_from_path(x, y, data, attribute, slc): - """ - Extract a PV-like slice from a cube - - :param x: An array of x values to extract (pixel units) - :param y: An array of y values to extract (pixel units) - :param data: :class:`~glue.core.data.Data` - :param attribute: :claass:`~glue.core.data.Component` - :param slc: orientation of the image widget that `pts` are defined on - - :returns: (slice, x, y) - slice is a 2D Numpy array, corresponding to a "PV ribbon" - cutout from the cube - x and y are the resampled points along which the - ribbon is extracted - - :note: For >3D cubes, the "V-axis" of the PV slice is the longest - cube axis ignoring the x/y axes of `slc` - """ - from pvextractor import Path, extract_pv_slice - p = Path(list(zip(x, y))) - - cube = data[attribute] - dims = list(range(data.ndim)) - s = list(slc) - ind = _slice_index(data, slc) - - from astropy.wcs import WCS - - if isinstance(data.coords, WCS): - cube_wcs = data.coords - else: - cube_wcs = None - - # transpose cube to (z, y, x, ) - def _swap(x, s, i, j): - x[i], x[j] = x[j], x[i] - s[i], s[j] = s[j], s[i] - - _swap(dims, s, ind, 0) - _swap(dims, s, s.index('y'), 1) - _swap(dims, s, s.index('x'), 2) - - cube = cube.transpose(dims) - - if cube_wcs is not None: - cube_wcs = cube_wcs.sub([data.ndim - nx for nx in dims[::-1]]) - - # slice down from >3D to 3D if needed - s = tuple([slice(None)] * 3 + [slc[d] for d in dims[3:]]) - cube = cube[s] - - # sample cube - spacing = 1 # pixel - x, y = [np.round(_x).astype(int) for _x in p.sample_points(spacing)] - - try: - result = extract_pv_slice(cube, path=p, wcs=cube_wcs, order=0) - wcs = WCS(result.header) - except Exception: # sometimes pvextractor complains due to wcs. Try to recover - result = extract_pv_slice(cube, path=p, wcs=None, order=0) - wcs = None - - data = result.data - - return data, x, y, wcs - - def _slice_index(data, slc): """ The axis over which to extract PV slices From b7e26cbefce9e82c380dd5f5786aca12d5ce03bd Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Wed, 5 Feb 2020 18:58:55 +0000 Subject: [PATCH 07/17] Fixed test --- glue/plugins/tools/pv_slicer/pv_sliced_data.py | 2 +- glue/plugins/tools/pv_slicer/qt/tests/test_pv_slicer.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/glue/plugins/tools/pv_slicer/pv_sliced_data.py b/glue/plugins/tools/pv_slicer/pv_sliced_data.py index 7e6ff3b25..34b7ba965 100644 --- a/glue/plugins/tools/pv_slicer/pv_sliced_data.py +++ b/glue/plugins/tools/pv_slicer/pv_sliced_data.py @@ -62,7 +62,7 @@ def _without_sliced(self, iterable): @property def shape(self): - return self._without_sliced(self.original_data.shape) + [len(self.x)] + return tuple(self._without_sliced(self.original_data.shape) + [len(self.x)]) @property def main_components(self): diff --git a/glue/plugins/tools/pv_slicer/qt/tests/test_pv_slicer.py b/glue/plugins/tools/pv_slicer/qt/tests/test_pv_slicer.py index a0d7e4076..2ad9aa746 100644 --- a/glue/plugins/tools/pv_slicer/qt/tests/test_pv_slicer.py +++ b/glue/plugins/tools/pv_slicer/qt/tests/test_pv_slicer.py @@ -39,7 +39,7 @@ def test_plain(self): tool._extract_callback(mode) assert len(self.data_collection) == 2 - assert self.data_collection[1].shape == (10, 16) + assert self.data_collection[1].shape == (10, 17) # If we do it again it should just update the existing slice dataset in-place From f990363b0f46c68f7aeb5bdf759e4dde4291c43a Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 6 Feb 2020 19:24:14 +0000 Subject: [PATCH 08/17] Added new icon for path/cursor drawing and update icon for making the slice --- glue/icons/glue_path.png | Bin 0 -> 3944 bytes glue/icons/glue_path.svg | 101 ++++++++++++++++++++++++++++++++++++++ glue/icons/glue_slice.png | Bin 1514 -> 3463 bytes glue/icons/glue_slice.svg | 61 +++++++++++++++-------- 4 files changed, 142 insertions(+), 20 deletions(-) create mode 100644 glue/icons/glue_path.png create mode 100644 glue/icons/glue_path.svg diff --git a/glue/icons/glue_path.png b/glue/icons/glue_path.png new file mode 100644 index 0000000000000000000000000000000000000000..af18638f0132cbd8056bc1d240605e53300936f6 GIT binary patch literal 3944 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 literal 0 HcmV?d00001 diff --git a/glue/icons/glue_path.svg b/glue/icons/glue_path.svg new file mode 100644 index 000000000..c96ee0e26 --- /dev/null +++ b/glue/icons/glue_path.svg @@ -0,0 +1,101 @@ + +image/svg+xml diff --git a/glue/icons/glue_slice.png b/glue/icons/glue_slice.png index f1714443a653c8edaa150a42830edd5def303efd..7d047fe51c90dfbccb9762fd76e28672769e2be6 100644 GIT binary patch delta 3453 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 delta 1489 zcmV;?1upuB8|n*?D}OE{001r{0eGc9b^rhX8FWQhbW?9;ba!ELWdK2BZ(?O2No`?g zWm08fWO;GPWjp`?1$RkAK~#9!?43<++eR2bCr*lNf|dwSpg@a2IY0pC1fli?VlTT2 zeF95pmjx1^!14*CoFMKAr0lwrfgB)ec5N3{7A*<{sXLQ?0)J$hj5yMeoEg3c41!p; z`1AOEGaSwgl>z_&00000000000D$O7bQ$Y%MX@fgatI= z+lg;<`CXS^%YI=G;ZgGRb06*@ynE~+9O(#0I>KY*P5{C$bQ#vMBR}5%_Stt|+|BYo z|Lfn67eD{`+kemlKjs{rhAzD)9SAnU`?@S$@5uDQBO^O&a+_NSe;#_I?@NRyo(Lb8 zZL|OGjSs?Y&Xt#SI>M`y)+xLx=?Je*R=4mbq$Au;_&D@iHiUa9yiVx|ug-}mCVW@vDZ(oU#@s^qQrbF`x^`&WI^DK2q07Q;0%IloLV1Qc$qY4i zUoI`O!HW&wHFcTZQ`KO1VTV{W8R53+9pUb2e9(q%r1x+Xws2SX=@5``q_@XJwKxtB z>Fw}RwSPG966qbp^=Q~(lN}5BaqAw9SD$uCx)DBWYUjVdci-GiWVySG8L=F9dSB%J zpG+S-O3)gy^!~dyMNyriHU5TNKH8ZS*#YVKXn%^<_@%$jM>_R-_afbJfZB+q>&D!o zpk61%M;_^h^L(U};^UY8k`&)D={m(3?M@zK7ztrJoi2EJJa`(xnxjf5LZ+ z;&vH%FvAM|0efUKH)Un4;Yr%MV|EeT?+$jAD#-)7hkpnRhV0 zduuO2`t=mQj4j&rzON=hIyr_|2Sye}`)P<>vwEKgBHUS@LzL1>qCE*I%}Y8r>*zXP z5Pvw@A<$a<3w5(=cA!t>K>)&2{m)xf{QO|K`0&pR>E#s7i62WH>8o&av6jtgH?kkj z{(k=&>E+QrQ#Tjd`?M5@Z{p2!0$rw`se(@Q@ir#)pNe_D4(j+uj}%9j{hEs!*-Yx{Rp zhmff0OAGn9B&C|jPbKY|^!kgrP+Ue_(aVB_i9~p<)0jIYef=I@OPxqdxlBMJpMPbN zj%N~u=5}cznZp%E@{#_|^CphJlhW7f`6Dg!Orm~MN>8N>u=$bRe&h4$J1;#K`9{D{?wg-%aRF4~DsW+E=KsyOSK|B90nG`m(=OuWBa0?GwSrP>-4d*TQ`2} z^T8lJ0O3e)?mVqt^kXA@5JK8Qdb#kqrSL3-w1xCQgd@G)^VG8xez~o1ZhzVQpwU-( z(K*vG;YjbGaHO}(fwF(~_+!<=+Zk!dxj9d^oi1~Wiv#JNo~KrYlivMirfPVV7t;M@ zqFQ`+8|l6I^ga&hV35AM^xB2H?vZ - - image/svg+xml \ No newline at end of file + style="fill:none;stroke:#c1343e;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1" /> From 0653861cd24957081f54a80afd98cdff42aa67c7 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 6 Feb 2020 20:55:18 +0000 Subject: [PATCH 09/17] Updated icon conversion script --- glue/icons/convert.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glue/icons/convert.sh b/glue/icons/convert.sh index 3aa416053..e8b3f5797 100755 --- a/glue/icons/convert.sh +++ b/glue/icons/convert.sh @@ -3,7 +3,7 @@ for input in *.svg do output=${input/svg/png} - inkscape --without-gui --export-png=$PWD/$output --export-width=125 --export-height=125 $PWD/$input + inkscape --without-gui --export-file=$PWD/$output --export-type=png --export-width=125 --export-height=125 $PWD/$input done convert -flop playback_forw.png playback_back.png From 58ea625c2a80798daa969311b3f6193253dfd342 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 6 Feb 2020 20:56:32 +0000 Subject: [PATCH 10/17] Added support for showing subsets in PV slices, and fixed implementation of compute_statistic --- .../plugins/tools/pv_slicer/pv_sliced_data.py | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/glue/plugins/tools/pv_slicer/pv_sliced_data.py b/glue/plugins/tools/pv_slicer/pv_sliced_data.py index 34b7ba965..d3d6681b8 100644 --- a/glue/plugins/tools/pv_slicer/pv_sliced_data.py +++ b/glue/plugins/tools/pv_slicer/pv_sliced_data.py @@ -71,10 +71,7 @@ def main_components(self): def get_kind(self, cid): return self.original_data.get_kind(cid) - def get_data(self, cid, view=None): - - if cid in self.pixel_component_ids: - return super().get_data(cid, view) + def _get_pix_coords(self, view=None): pix_coords = [] @@ -96,17 +93,12 @@ def get_data(self, cid, view=None): if view is not None and len(view) > idim_current: pix = pix[view[idim_current]] - print("DONE") - - print(idim, idim_current, pix.shape) pix_coords.append(pix) if not advanced_indexing: pix_coords = np.meshgrid(*pix_coords, indexing='ij', copy=False) - print(pix_coords[0].shape) - shape = pix_coords[0].shape keep = np.ones(shape, dtype=bool) @@ -115,21 +107,33 @@ def get_data(self, cid, view=None): pix_coords = [x[keep].astype(int) for x in pix_coords] - result = np.zeros(shape) + return pix_coords, keep, shape + + def get_data(self, cid, view=None): + if cid in self.pixel_component_ids: + return super().get_data(cid, view) + + pix_coords, keep, shape = self._get_pix_coords(view=view) + result = np.zeros(shape) result[keep] = self.original_data.get_data(cid, view=pix_coords) return result def get_mask(self, subset_state, view=None): - # Optimize by getting pixel coordinates of original data in new - # frame of reference and getting the mask for these indices + if view is None: view = Ellipsis - return self.callable(self.original_data.get_mask(subset_state))[view] - def compute_statistic(self, *args, **kwargs): - return self.original_data.compute_statistic(*args, **kwargs) + pix_coords, keep, shape = self._get_pix_coords(view=view) + result = np.zeros(shape) + result[keep] = self.original_data.get_mask(subset_state, view=pix_coords) + + return result + + def compute_statistic(self, *args, view=None, **kwargs): + pix_coords, _, _ = self._get_pix_coords(view=view) + return self.original_data.compute_statistic(*args, view=pix_coords, **kwargs) def compute_histogram(self, *args, **kwargs): return self.original_data.compute_histogram(*args, **kwargs) From ceb6b563bc84c60689435a79e8d10fcd2b0b3fe9 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 6 Feb 2020 20:57:01 +0000 Subject: [PATCH 11/17] Add behavior and appearance of tools --- glue/plugins/tools/pv_slicer/qt/pv_slicer.py | 40 ++++++++++++++++++-- glue/viewers/matplotlib/mouse_mode.py | 2 +- glue/viewers/matplotlib/toolbar_mode.py | 14 +++++-- 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/glue/plugins/tools/pv_slicer/qt/pv_slicer.py b/glue/plugins/tools/pv_slicer/qt/pv_slicer.py index 727fcfd7b..affd619e6 100644 --- a/glue/plugins/tools/pv_slicer/qt/pv_slicer.py +++ b/glue/plugins/tools/pv_slicer/qt/pv_slicer.py @@ -1,5 +1,7 @@ import numpy as np +from matplotlib.lines import Line2D + from glue.core import Data from glue.core.coordinates import coordinates_from_wcs from glue.core.coordinate_helpers import axis_label @@ -81,23 +83,53 @@ class PVLinkCursorMode(ToolbarModeBase): Selects pixel under mouse cursor. """ - icon = "glue_point" + icon = "glue_path" tool_id = 'pv:crosshair' + action_text = 'Show position on original path' + tool_tip = 'Click and drag to show position of cursor on original slice.' + status_tip = 'Click and drag to show position of cursor on original slice.' _pressed = False def __init__(self, *args, **kwargs): super(PVLinkCursorMode, self).__init__(*args, **kwargs) self._move_callback = self._on_move - self._press_callback = self._on_move + self._press_callback = self._on_press + self._release_callback = self._on_release + self._active = False self.viewer.state.add_callback('reference_data', self._on_reference_data_change) def _on_reference_data_change(self, reference_data): self.enabled = isinstance(reference_data, PVSlicedData) self.data = reference_data + def activate(self): + self._line = Line2D(self.data.x, self.data.y, zorder=1000, color='#669dff', + alpha=0.6, lw=2) + self.data.parent_viewer.axes.add_line(self._line) + self._crosshair = self.data.parent_viewer.axes.plot([], [], '+', ms=12, + mfc='none', mec='#669dff', + mew=1, zorder=100)[0] + self.data.parent_viewer.figure.canvas.draw() + super().activate() + + def deactivate(self): + self._line.remove() + self._crosshair.remove() + self.data.parent_viewer.figure.canvas.draw() + super().deactivate() + + def _on_press(self, mode): + self._active = True + + def _on_release(self, mode): + self._active = False + def _on_move(self, mode): + if not self._active: + return + # Find position of click in the image viewer xdata, ydata = self._event_xdata, self._event_ydata @@ -117,7 +149,9 @@ def _on_move(self, mode): # coordinate in the PV slice. z = ydata - self.data.parent_viewer.show_crosshairs(x, y) + self._crosshair.set_xdata([x]) + self._crosshair.set_ydata([y]) + self.data.parent_viewer.figure.canvas.draw() s = list(self.data.parent_viewer.state.wcsaxes_slice[::-1]) s[_slice_index(self.data.parent_viewer.state.reference_data, s)] = int(z) diff --git a/glue/viewers/matplotlib/mouse_mode.py b/glue/viewers/matplotlib/mouse_mode.py index 332cff52e..37b133c0a 100644 --- a/glue/viewers/matplotlib/mouse_mode.py +++ b/glue/viewers/matplotlib/mouse_mode.py @@ -135,4 +135,4 @@ def deactivate(self): """ for connection in self._connections: self._canvas.mpl_disconnect(connection) - self._connections = [] + self._connections[:] = [] diff --git a/glue/viewers/matplotlib/toolbar_mode.py b/glue/viewers/matplotlib/toolbar_mode.py index 31926bd2c..ec9d9c5fb 100644 --- a/glue/viewers/matplotlib/toolbar_mode.py +++ b/glue/viewers/matplotlib/toolbar_mode.py @@ -81,6 +81,12 @@ def activate(self): self._roi_tool._sync_patch() super(RoiModeBase, self).activate() + def deactivate(self): + self._roi_tool.reset() + self.clear() + self.viewer.figure.canvas.draw() + super(RoiModeBase, self).deactivate() + def roi(self): """ The ROI defined by this mouse mode @@ -200,7 +206,7 @@ def move(self, event): if event.button is not None and self._roi_tool.active(): self._roi_tool.update_selection(event) self._last_event = event - super(ClickRoiMode, self).move(event) + super(ClickRoiMode, self).move(event) def key(self, event): if event.key == 'enter': @@ -245,10 +251,10 @@ def __init__(self, viewer, **kwargs): super(PathMode, self).__init__(viewer, **kwargs) self._roi_tool = roi.MplPathROI(self._axes) - self._roi_tool.plot_opts.update(color='#de2d26', + self._roi_tool.plot_opts.update(color='#669dff', fill=False, - linewidth=3, - alpha=0.4) + linewidth=2, + alpha=0.6) @viewer_tool From 1228dd43fa15cd928a24581dea98bda1afe79496 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Fri, 7 Feb 2020 18:15:35 +0000 Subject: [PATCH 12/17] Improve behavior of PV slicing tool, and fix implementation of PVSlicedData.compute.fixed_resolution_buffer to ensure that multiple sliced datasets can be shown at the same time --- .../plugins/tools/pv_slicer/pv_sliced_data.py | 69 ++++++++++++++++++- glue/plugins/tools/pv_slicer/qt/pv_slicer.py | 57 ++++++++------- .../pv_slicer/tests/test_pv_sliced_data.py | 48 +++++++++++++ 3 files changed, 148 insertions(+), 26 deletions(-) create mode 100644 glue/plugins/tools/pv_slicer/tests/test_pv_sliced_data.py diff --git a/glue/plugins/tools/pv_slicer/pv_sliced_data.py b/glue/plugins/tools/pv_slicer/pv_sliced_data.py index d3d6681b8..3980a6614 100644 --- a/glue/plugins/tools/pv_slicer/pv_sliced_data.py +++ b/glue/plugins/tools/pv_slicer/pv_sliced_data.py @@ -138,7 +138,70 @@ def compute_statistic(self, *args, view=None, **kwargs): def compute_histogram(self, *args, **kwargs): return self.original_data.compute_histogram(*args, **kwargs) - def compute_fixed_resolution_buffer(self, *args, **kwargs): + def compute_fixed_resolution_buffer(self, bounds, target_data=None, target_cid=None, + subset_state=None, broadcast=True, cache_id=None): + from glue.core.fixed_resolution_buffer import compute_fixed_resolution_buffer - print(args, kwargs) - return compute_fixed_resolution_buffer(self, *args, **kwargs) + + # First check that the target data is also a PVSlicedData + # TODO: also check it's actually for the same path + + if not isinstance(target_data, PVSlicedData): + raise TypeError('target_data has to be a PVSlicedData') + + if len(bounds) != len(self.shape): + raise ValueError('bounds should have {0} elements'.format(len(self.shape))) + + # Now translate the bounds so that we replace the path with the + # pixel coordinates in the target dataset + + + # The last item of bounds is the pixel offset in the target PV slice + path_pixel_offset_target = np.linspace(*bounds[-1]) + + # Translate this to a relative offset along the path + path_pixel_offset_target_relative = path_pixel_offset_target / self.shape[-1] + + # Find the pixel coordinates in the current dataset + x = np.interp(path_pixel_offset_target_relative, + np.linspace(0., 1., len(self.x)), + self.x) + y = np.interp(path_pixel_offset_target_relative, + np.linspace(0., 1., len(self.y)), + self.y) + + # Create new bouds list + + new_bounds = [] + idim_current = 0 + + slices = [] + + for idim in range(self.original_data.ndim): + + if idim == self.cid_x.axis: + ixmax = np.ceil(np.max(x)) + bound = (0, ixmax, ixmax + 1) + slices.append(np.round(x).astype(int)) + elif idim == self.cid_y.axis: + iymax = np.ceil(np.max(y)) + bound = (0, iymax, iymax + 1) + slices.append(np.round(x).astype(int)) + else: + bound = bounds[idim_current] + idim_current += 1 + slices.append(slice(None)) + + new_bounds.append(bound) + + # TODO: For now we extract a cube and then slice it, but it would be + # more efficient if bounds could include 1-d arrays. + + # Now compute the fixed resolution buffer using the original datasets + result = compute_fixed_resolution_buffer(self.original_data, new_bounds, + target_data=target_data.original_data, + target_cid=target_cid) + + result = result[slices] + + return result diff --git a/glue/plugins/tools/pv_slicer/qt/pv_slicer.py b/glue/plugins/tools/pv_slicer/qt/pv_slicer.py index affd619e6..35529692d 100644 --- a/glue/plugins/tools/pv_slicer/qt/pv_slicer.py +++ b/glue/plugins/tools/pv_slicer/qt/pv_slicer.py @@ -29,6 +29,7 @@ def __init__(self, viewer, **kwargs): self._roi_callback = self._extract_callback self._slice_widget = None self.viewer.state.add_callback('reference_data', self._on_reference_data_change) + # self._sliced_data = [] def _on_reference_data_change(self, reference_data): if reference_data is not None: @@ -47,31 +48,41 @@ def _extract_callback(self, mode): selected = self.viewer.session.application.selected_layers() - if len(selected) == 1 and isinstance(selected[0], PVSlicedData): - data = selected[0] - data.original_data = self.viewer.state.reference_data - data.x_att = self.viewer.state.x_att - data.y_att = self.viewer.state.y_att - data.set_xy(vx, vy) - open_viewer = True - for tab in self.viewer.session.application.viewers: - for viewer in tab: - if data in viewer._layer_artist_container: - open_viewer = False - break - if not open_viewer: - break - else: - data = PVSlicedData(self.viewer.state.reference_data, - self.viewer.state.x_att, vx, - self.viewer.state.y_att, vy, - label=self.viewer.state.reference_data.label + " [slice]") - data.parent_viewer = self.viewer - self.viewer.session.data_collection.append(data) - open_viewer = True + open_viewer = False + + all_pvdata = [] + + for data in self.viewer.state.layers_data: + if isinstance(data, Data): + + for pvdata in self.viewer.session.data_collection: + if isinstance(pvdata, PVSlicedData): + if pvdata.original_data is data: + break + else: + pvdata = None + + if pvdata is None: + pvdata = PVSlicedData(data, + self.viewer.state.x_att, vx, + self.viewer.state.y_att, vy, + label=data.label + " [slice]") + data.parent_viewer = self.viewer + self.viewer.session.data_collection.append(pvdata) + open_viewer = True + else: + data = pvdata + data.original_data = self.viewer.state.reference_data + data.x_att = self.viewer.state.x_att + data.y_att = self.viewer.state.y_att + data.set_xy(vx, vy) + + all_pvdata.append(pvdata) if open_viewer: - viewer = self.viewer.session.application.new_data_viewer(ImageViewer, data=data) + viewer = self.viewer.session.application.new_data_viewer(ImageViewer) + for pvdata in all_pvdata: + viewer.add_data(pvdata) viewer.state.aspect = 'auto' viewer.state.reset_limits() diff --git a/glue/plugins/tools/pv_slicer/tests/test_pv_sliced_data.py b/glue/plugins/tools/pv_slicer/tests/test_pv_sliced_data.py new file mode 100644 index 000000000..8b1771c71 --- /dev/null +++ b/glue/plugins/tools/pv_slicer/tests/test_pv_sliced_data.py @@ -0,0 +1,48 @@ +import numpy as np +from glue.core import Data, DataCollection +from glue.core.coordinates import AffineCoordinates, IdentityCoordinates +from glue.plugins.tools.pv_slicer.pv_sliced_data import PVSlicedData +from glue.core.link_helpers import LinkSame + + +class TestPVSlicedData: + + def setup_method(self, method): + matrix = np.array([[2, 0, 0, 0], [0, 2, 0, 0], [0, 0, 2, 0], [0, 0, 0, 1]]) + + self.data1 = Data(x=np.arange(120).reshape((6, 5, 4)), coords=AffineCoordinates(matrix)) + self.data2 = Data(y=np.arange(120).reshape((6, 5, 4)), coords=IdentityCoordinates(n_dim=3)) + + self.dc = DataCollection([self.data1, self.data2]) + + self.dc.add_link(LinkSame(self.data1.world_component_ids[0], + self.data2.world_component_ids[0])) + + self.dc.add_link(LinkSame(self.data1.world_component_ids[1], + self.data2.world_component_ids[1])) + + self.dc.add_link(LinkSame(self.data1.world_component_ids[2], + self.data2.world_component_ids[2])) + + # TODO: the paths in the next two PVSlicedData objects are meant to + # be the same conceptually. We should make sure we formalize this with + # a UUID. Also should use proper links. + + x1 = [0, 2, 5] + y1 = [1, 2, 3] + + self.pvdata1 = PVSlicedData(self.data1, + self.data1.pixel_component_ids[1], y1, + self.data1.pixel_component_ids[2], x1) + + x2, y2, _ = self.data2.coords.world_to_pixel_values(*self.data1.coords.pixel_to_world_values(x1, y1, 0)) + + self.pvdata2 = PVSlicedData(self.data2, + self.data2.pixel_component_ids[1], y2, + self.data2.pixel_component_ids[2], x2) + + def test_fixed_resolution_buffer_linked(self): + result = self.pvdata1.compute_fixed_resolution_buffer(bounds=[(0, 5, 15), (0, 6, 20)], + target_data=self.pvdata2, + target_cid=self.data1.id['x']) + assert result.shape == (15, 20) From 7c085c6f92fb43038b41ad6ecf7f9f8baa3c7fe4 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Mon, 10 Feb 2020 18:33:23 +0000 Subject: [PATCH 13/17] More fixes to UI, including a long-standing bug with tools that are disabled by default --- .../plugins/tools/pv_slicer/pv_sliced_data.py | 8 +++++-- glue/plugins/tools/pv_slicer/qt/pv_slicer.py | 21 ++++++++++--------- glue/viewers/common/qt/toolbar.py | 1 + glue/viewers/matplotlib/toolbar_mode.py | 3 ++- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/glue/plugins/tools/pv_slicer/pv_sliced_data.py b/glue/plugins/tools/pv_slicer/pv_sliced_data.py index 3980a6614..b3cd4ec96 100644 --- a/glue/plugins/tools/pv_slicer/pv_sliced_data.py +++ b/glue/plugins/tools/pv_slicer/pv_sliced_data.py @@ -1,6 +1,7 @@ import numpy as np from glue.core.data_derived import DerivedData +from glue.core.message import NumericalDataChangedMessage __all__ = ['PVSlicedData'] @@ -52,6 +53,9 @@ def set_xy(self, x, y): x, y = sample_points(x, y) self.x = x self.y = y + if self.original_data.hub: + msg = NumericalDataChangedMessage(self) + self.original_data.hub.broadcast(msg) @property def label(self): @@ -186,7 +190,7 @@ def compute_fixed_resolution_buffer(self, bounds, target_data=None, target_cid=N elif idim == self.cid_y.axis: iymax = np.ceil(np.max(y)) bound = (0, iymax, iymax + 1) - slices.append(np.round(x).astype(int)) + slices.append(np.round(y).astype(int)) else: bound = bounds[idim_current] idim_current += 1 @@ -202,6 +206,6 @@ def compute_fixed_resolution_buffer(self, bounds, target_data=None, target_cid=N target_data=target_data.original_data, target_cid=target_cid) - result = result[slices] + result = result[tuple(slices)] return result diff --git a/glue/plugins/tools/pv_slicer/qt/pv_slicer.py b/glue/plugins/tools/pv_slicer/qt/pv_slicer.py index 35529692d..004a29a6b 100644 --- a/glue/plugins/tools/pv_slicer/qt/pv_slicer.py +++ b/glue/plugins/tools/pv_slicer/qt/pv_slicer.py @@ -29,11 +29,11 @@ def __init__(self, viewer, **kwargs): self._roi_callback = self._extract_callback self._slice_widget = None self.viewer.state.add_callback('reference_data', self._on_reference_data_change) - # self._sliced_data = [] + self._on_reference_data_change() - def _on_reference_data_change(self, reference_data): - if reference_data is not None: - self.enabled = reference_data.ndim == 3 + def _on_reference_data_change(self, *args): + if self.viewer.state.reference_data is not None: + self.enabled = self.viewer.state.reference_data.ndim == 3 def _clear_path(self): self.viewer.hide_crosshairs() @@ -67,7 +67,7 @@ def _extract_callback(self, mode): self.viewer.state.x_att, vx, self.viewer.state.y_att, vy, label=data.label + " [slice]") - data.parent_viewer = self.viewer + pvdata.parent_viewer = self.viewer self.viewer.session.data_collection.append(pvdata) open_viewer = True else: @@ -84,8 +84,8 @@ def _extract_callback(self, mode): for pvdata in all_pvdata: viewer.add_data(pvdata) - viewer.state.aspect = 'auto' - viewer.state.reset_limits() + viewer.state.aspect = 'auto' + viewer.state.reset_limits() @viewer_tool @@ -109,10 +109,11 @@ def __init__(self, *args, **kwargs): self._release_callback = self._on_release self._active = False self.viewer.state.add_callback('reference_data', self._on_reference_data_change) + self._on_reference_data_change() - def _on_reference_data_change(self, reference_data): - self.enabled = isinstance(reference_data, PVSlicedData) - self.data = reference_data + def _on_reference_data_change(self, *args): + self.enabled = isinstance(self.viewer.state.reference_data, PVSlicedData) + self.data = self.viewer.state.reference_data def activate(self): self._line = Line2D(self.data.x, self.data.y, zorder=1000, color='#669dff', diff --git a/glue/viewers/common/qt/toolbar.py b/glue/viewers/common/qt/toolbar.py index e7c51b124..3b85a3120 100644 --- a/glue/viewers/common/qt/toolbar.py +++ b/glue/viewers/common/qt/toolbar.py @@ -201,6 +201,7 @@ def toggle(state): action.setEnabled(state) add_callback(tool, 'enabled', toggle) + toggle(tool.enabled) self.tools[tool.tool_id] = tool diff --git a/glue/viewers/matplotlib/toolbar_mode.py b/glue/viewers/matplotlib/toolbar_mode.py index ec9d9c5fb..1b39effa3 100644 --- a/glue/viewers/matplotlib/toolbar_mode.py +++ b/glue/viewers/matplotlib/toolbar_mode.py @@ -84,7 +84,8 @@ def activate(self): def deactivate(self): self._roi_tool.reset() self.clear() - self.viewer.figure.canvas.draw() + if self.viewer is not None: + self.viewer.figure.canvas.draw() super(RoiModeBase, self).deactivate() def roi(self): From b5fc6e6203616e7e18eb495aa69427932c88616b Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 10 Feb 2022 10:44:53 +0000 Subject: [PATCH 14/17] Updated changelog entry and fixed tests --- CHANGES.md | 4 +-- .../plugins/tools/pv_slicer/pv_sliced_data.py | 4 +-- .../pv_slicer/qt/tests/test_pv_slicer.py | 34 +------------------ 3 files changed, 5 insertions(+), 37 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ba16cb33d..38b305194 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,8 @@ v1.3.0 (unreleased) * Remove bundled version of pvextractor and include it as a proper dependency. [#2252] +* Remove ``StandaloneImageViewer`` class. [#1663] + v1.2.4 (2022-01-27) ------------------- @@ -574,8 +576,6 @@ v0.13.0 (2018-04-27) subsets to define the areas in which to compute profiles rather than custom ROIs. [#1635] -* Remove ``StandaloneImageViewer`` class. [#1663] - * Added support for PySide2 and remove support for PyQt4 and PySide. [#1662] diff --git a/glue/plugins/tools/pv_slicer/pv_sliced_data.py b/glue/plugins/tools/pv_slicer/pv_sliced_data.py index b3cd4ec96..0abc6ba1d 100644 --- a/glue/plugins/tools/pv_slicer/pv_sliced_data.py +++ b/glue/plugins/tools/pv_slicer/pv_sliced_data.py @@ -184,11 +184,11 @@ def compute_fixed_resolution_buffer(self, bounds, target_data=None, target_cid=N for idim in range(self.original_data.ndim): if idim == self.cid_x.axis: - ixmax = np.ceil(np.max(x)) + ixmax = int(np.ceil(np.max(x))) bound = (0, ixmax, ixmax + 1) slices.append(np.round(x).astype(int)) elif idim == self.cid_y.axis: - iymax = np.ceil(np.max(y)) + iymax = int(np.ceil(np.max(y))) bound = (0, iymax, iymax + 1) slices.append(np.round(y).astype(int)) else: diff --git a/glue/plugins/tools/pv_slicer/qt/tests/test_pv_slicer.py b/glue/plugins/tools/pv_slicer/qt/tests/test_pv_slicer.py index 2ad9aa746..05ced141b 100644 --- a/glue/plugins/tools/pv_slicer/qt/tests/test_pv_slicer.py +++ b/glue/plugins/tools/pv_slicer/qt/tests/test_pv_slicer.py @@ -8,7 +8,7 @@ from glue.tests.helpers import requires_astropy, requires_scipy from glue.viewers.image.qt.data_viewer import ImageViewer -from ..pv_slicer import _slice_from_path, _slice_label, _slice_index +from ..pv_slicer import _slice_label, _slice_index class TestPVSlicerMode(object): @@ -48,38 +48,6 @@ def test_plain(self): tool._extract_callback(mode) -@requires_astropy -@requires_scipy -class TestSliceExtraction(object): - - def setup_method(self, method): - self.x = np.random.random((2, 3, 4)) - self.d = Data(x=self.x) - - def test_constant_y(self): - - slc = (0, 'y', 'x') - x = [-0.5, 3.5] - y = [0, 0] - s = _slice_from_path(x, y, self.d, 'x', slc)[0] - assert_allclose(s, self.x[:, 0, :]) - - def test_constant_x(self): - - slc = (0, 'y', 'x') - y = [-0.5, 2.5] - x = [0, 0] - s = _slice_from_path(x, y, self.d, 'x', slc)[0] - assert_allclose(s, self.x[:, :, 0]) - - def test_transpose(self): - slc = (0, 'x', 'y') - y = [-0.5, 3.5] - x = [0, 0] - s = _slice_from_path(x, y, self.d, 'x', slc)[0] - assert_allclose(s, self.x[:, 0, :]) - - def test_slice_label(): d = Data(x=np.zeros((2, 3, 4)), coords=IdentityCoordinates(n_dim=3)) assert _slice_label(d, (0, 'y', 'x')) == 'World 0' From 7ee3bd83bb03cb55da33eb4dff1bd96172468bc8 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 10 Feb 2022 11:00:59 +0000 Subject: [PATCH 15/17] Fix slicnig of subsets --- glue/plugins/tools/pv_slicer/pv_sliced_data.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/glue/plugins/tools/pv_slicer/pv_sliced_data.py b/glue/plugins/tools/pv_slicer/pv_sliced_data.py index 0abc6ba1d..7b3cbad2b 100644 --- a/glue/plugins/tools/pv_slicer/pv_sliced_data.py +++ b/glue/plugins/tools/pv_slicer/pv_sliced_data.py @@ -204,7 +204,8 @@ def compute_fixed_resolution_buffer(self, bounds, target_data=None, target_cid=N # Now compute the fixed resolution buffer using the original datasets result = compute_fixed_resolution_buffer(self.original_data, new_bounds, target_data=target_data.original_data, - target_cid=target_cid) + target_cid=target_cid, + subset_state=subset_state) result = result[tuple(slices)] From babc29d4467fbeec935a1cd2989ea541537511eb Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 10 Feb 2022 14:36:42 +0000 Subject: [PATCH 16/17] Copy over layer settings --- .../plugins/tools/pv_slicer/pv_sliced_data.py | 3 +- glue/plugins/tools/pv_slicer/qt/pv_slicer.py | 66 ++++++++++++------- 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/glue/plugins/tools/pv_slicer/pv_sliced_data.py b/glue/plugins/tools/pv_slicer/pv_sliced_data.py index 7b3cbad2b..b237ceae5 100644 --- a/glue/plugins/tools/pv_slicer/pv_sliced_data.py +++ b/glue/plugins/tools/pv_slicer/pv_sliced_data.py @@ -205,7 +205,8 @@ def compute_fixed_resolution_buffer(self, bounds, target_data=None, target_cid=N result = compute_fixed_resolution_buffer(self.original_data, new_bounds, target_data=target_data.original_data, target_cid=target_cid, - subset_state=subset_state) + subset_state=subset_state, + cache_id=cache_id) result = result[tuple(slices)] diff --git a/glue/plugins/tools/pv_slicer/qt/pv_slicer.py b/glue/plugins/tools/pv_slicer/qt/pv_slicer.py index 004a29a6b..e3a7c51ca 100644 --- a/glue/plugins/tools/pv_slicer/qt/pv_slicer.py +++ b/glue/plugins/tools/pv_slicer/qt/pv_slicer.py @@ -29,6 +29,7 @@ def __init__(self, viewer, **kwargs): self._roi_callback = self._extract_callback self._slice_widget = None self.viewer.state.add_callback('reference_data', self._on_reference_data_change) + self.open_viewer = True self._on_reference_data_change() def _on_reference_data_change(self, *args): @@ -46,48 +47,67 @@ def _extract_callback(self, mode): vx, vy = mode.roi().to_polygon() - selected = self.viewer.session.application.selected_layers() + if self.open_viewer: + viewer = self.viewer.session.application.new_data_viewer(ImageViewer) + self.open_viewer = False + else: + viewer = None - open_viewer = False + for layer_state in self.viewer.state.layers: - all_pvdata = [] + data = layer_state.layer - for data in self.viewer.state.layers_data: if isinstance(data, Data): + # TODO: need to generalize this for non-xy cuts + for pvdata in self.viewer.session.data_collection: if isinstance(pvdata, PVSlicedData): if pvdata.original_data is data: + pvdata.original_data = self.viewer.state.reference_data + pvdata.x_att = self.viewer.state.x_att + pvdata.y_att = self.viewer.state.y_att + pvdata.set_xy(vx, vy) break else: - pvdata = None - - if pvdata is None: - pvdata = PVSlicedData(data, - self.viewer.state.x_att, vx, - self.viewer.state.y_att, vy, - label=data.label + " [slice]") - pvdata.parent_viewer = self.viewer - self.viewer.session.data_collection.append(pvdata) - open_viewer = True + pvdata = PVSlicedData(data, + self.viewer.state.x_att, vx, + self.viewer.state.y_att, vy, + label=data.label + " [slice]") + pvdata.parent_viewer = self.viewer + self.viewer.session.data_collection.append(pvdata) + else: - data = pvdata - data.original_data = self.viewer.state.reference_data - data.x_att = self.viewer.state.x_att - data.y_att = self.viewer.state.y_att - data.set_xy(vx, vy) - all_pvdata.append(pvdata) + # For now don't do anything with the subsets, adding the data + # will automatically add all subsets. In future we could try + # and only add subsets that were shown in the original viewer. + continue + + if viewer is not None: - if open_viewer: - viewer = self.viewer.session.application.new_data_viewer(ImageViewer) - for pvdata in all_pvdata: viewer.add_data(pvdata) + # Copy over visual state from original layer, such as color, + # attribute and so on. + pvstate = layer_state.as_dict() + pvstate.pop('layer') + + # Find layer state to update in new viewer - this might not be + # the last layer if subsets are present + for new_layer_state in viewer.state.layers[::-1]: + if new_layer_state.layer is pvdata: + new_layer_state.update_from_dict(pvstate) + break + + if viewer is not None: + viewer.state.aspect = 'auto' + viewer.state.color_mode = self.viewer.state.color_mode viewer.state.reset_limits() + @viewer_tool class PVLinkCursorMode(ToolbarModeBase): """ From a61a71c6154af6bffa27ba73b4925fa60195ff31 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 10 Feb 2022 20:50:44 +0000 Subject: [PATCH 17/17] Fix bug that caused infinite loop --- glue/plugins/tools/pv_slicer/qt/pv_slicer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glue/plugins/tools/pv_slicer/qt/pv_slicer.py b/glue/plugins/tools/pv_slicer/qt/pv_slicer.py index e3a7c51ca..b33c79617 100644 --- a/glue/plugins/tools/pv_slicer/qt/pv_slicer.py +++ b/glue/plugins/tools/pv_slicer/qt/pv_slicer.py @@ -64,7 +64,7 @@ def _extract_callback(self, mode): for pvdata in self.viewer.session.data_collection: if isinstance(pvdata, PVSlicedData): if pvdata.original_data is data: - pvdata.original_data = self.viewer.state.reference_data + pvdata.original_data = data pvdata.x_att = self.viewer.state.x_att pvdata.y_att = self.viewer.state.y_att pvdata.set_xy(vx, vy)