diff --git a/glue/core/state_objects.py b/glue/core/state_objects.py index 6262964ed..4d1089908 100644 --- a/glue/core/state_objects.py +++ b/glue/core/state_objects.py @@ -8,6 +8,7 @@ from echo import (delay_callback, CallbackProperty, HasCallbackProperties, CallbackList) from glue.core.state import saver, loader +from glue.core.subset import SliceSubsetState from glue.core.component_id import PixelComponentID from glue.core.exceptions import IncompatibleAttribute from glue.core.units import UnitConverter @@ -299,10 +300,9 @@ def __init__(self, state, attribute, random_subset=10000, margin=0, **kwargs): self.margin = margin self.random_subset = random_subset - self.subset_indices = None + self._subset_state = None if self.attribute is not None: - if (self.lower is not None and self.upper is not None and getattr(self, 'percentile', None) is None): # If the lower and upper limits are already set, we need to make # sure we don't override them, so we set the percentile mode to @@ -315,7 +315,7 @@ def __init__(self, state, attribute, random_subset=10000, margin=0, **kwargs): def update_values(self, force=False, use_default_modifiers=False, **properties): - if not force and not any(prop in properties for prop in ('attribute', 'percentile', 'log', 'display_units')): + if not force and not any(prop in properties for prop in ('attribute', ) + self.modifiers_names): self.set(percentile='Custom') return @@ -356,12 +356,9 @@ def update_values(self, force=False, use_default_modifiers=False, **properties): self.set(lower=lower, upper=upper) return - if not force and (percentile == 'Custom' or not hasattr(self, 'data') or self.data is None): - + if percentile == 'Custom' or (not force and getattr(self, 'data', None) is None): self.set(percentile=percentile, log=log) - else: - # Shortcut if the component ID is a pixel component ID if isinstance(self.component_id, PixelComponentID) and percentile == 100 and not log: lower = -0.5 @@ -374,22 +371,25 @@ def update_values(self, force=False, use_default_modifiers=False, **properties): if percentile == 100: lower = self.data.compute_statistic('minimum', cid=self.component_id, finite=True, positive=log, + subset_state=self._subset_state, random_subset=self.random_subset) upper = self.data.compute_statistic('maximum', cid=self.component_id, finite=True, positive=log, + subset_state=self._subset_state, random_subset=self.random_subset) else: lower = self.data.compute_statistic('percentile', cid=self.component_id, percentile=exclude, positive=log, + subset_state=self._subset_state, random_subset=self.random_subset) upper = self.data.compute_statistic('percentile', cid=self.component_id, percentile=100 - exclude, positive=log, + subset_state=self._subset_state, random_subset=self.random_subset) if not isinstance(lower, np.datetime64) and np.isnan(lower): lower, upper = 0, 1 else: - if self.data.get_kind(self.component_id) == 'categorical': lower = np.floor(lower - 0.5) + 0.5 upper = np.ceil(upper + 0.5) - 0.5 @@ -415,6 +415,15 @@ def update_values(self, force=False, use_default_modifiers=False, **properties): def flip_limits(self): self.set(lower=self.upper, upper=self.lower) + def set_slice(self, slices): + """Set subset for compute_statistic to current slice or global""" + + self._subset_state = None if slices is None else SliceSubsetState(self.data, slices) + + # Force update if percentile not set to 'Custom'. + if isinstance(self.percentile, (int, float)): + self.update_values(force=True) + class StateAttributeSingleValueHelper(StateAttributeCacheHelper): diff --git a/glue/core/tests/test_state_objects.py b/glue/core/tests/test_state_objects.py index 163d64f55..de009873e 100644 --- a/glue/core/tests/test_state_objects.py +++ b/glue/core/tests/test_state_objects.py @@ -182,6 +182,28 @@ def test_manual_edit(self): assert self.helper.upper == 234 assert self.helper.log + def test_set_slice(self): + + # Set subset to compute limits from slice + self.helper.set_slice([slice(2000, 8000)]) + + assert self.helper.percentile == 100 + + assert_allclose(self.helper.lower, -59.996) + assert_allclose(self.helper.upper, 59.996) + + self.helper.percentile = 90 + + assert_allclose(self.helper.lower, -53.9964) + assert_allclose(self.helper.upper, 53.9964) + + self.helper.set_slice(None) + + self.helper.percentile = 95 + + assert_allclose(self.helper.lower, -95) + assert_allclose(self.helper.upper, 95) + class TestStateAttributeSingleValueHelper(): diff --git a/glue/viewers/image/state.py b/glue/viewers/image/state.py index a40346893..3a5c6db8b 100644 --- a/glue/viewers/image/state.py +++ b/glue/viewers/image/state.py @@ -492,8 +492,8 @@ class ImageLayerState(BaseImageLayerState, StretchStateMixin): v_min = DDCProperty(docstring='The lower level shown') v_max = DDCProperty(docstring='The upper level shown') attribute_display_unit = DDSCProperty(docstring='The units to use to define the levels') - percentile = DDSCProperty(docstring='The percentile value used to ' - 'automatically calculate levels') + percentile = DDSCProperty(docstring='The percentile value used to automatically ' + 'calculate levels; "Custom" for manually set levels') contrast = DDCProperty(1, docstring='The contrast of the layer') bias = DDCProperty(0.5, docstring='A constant value that is added to the ' 'layer before rendering') @@ -501,6 +501,9 @@ class ImageLayerState(BaseImageLayerState, StretchStateMixin): global_sync = DDCProperty(False, docstring='Whether the color and transparency ' 'should be synced with the global ' 'color and transparency for the data') + stretch_global = DDCProperty(True, docstring='Calculate automatic levels for rendering ' + 'stretch from the full data cube or only the ' + 'current layer (slice)') def __init__(self, layer=None, viewer_state=None, **kwargs): @@ -542,6 +545,7 @@ def format_unit(unit): self.add_callback('global_sync', self._update_syncing) self.add_callback('layer', self._update_attribute) + self.add_callback('stretch_global', self._set_global_stretch, priority=0) self._update_syncing() @@ -581,6 +585,28 @@ def _update_syncing(self, *args): def _get_image(self, view=None): return self.layer[self.attribute, view] + def _set_global_stretch(self, stretch_global=True): + if stretch_global: + self.viewer_state.remove_callback('slices', self._update_slice_subset) + self.attribute_lim_helper.set_slice(None) + else: + self.viewer_state.add_callback('slices', self._update_slice_subset) + self.attribute_lim_helper.set_slice(self.viewer_state.numpy_slice_aggregation_transpose[0]) + + def _update_slice_subset(self, slices): + """ + Select a subset slice for determining image levels. + + Parameters + ---------- + slices : iterable of :class:`slice` or `None` + An iterable containing :class:`slice` objects that can instantiate + a :class:`~glue.core.subset.SliceSubsetState` and has to be consistent + with the shape of `self.data`; `None` to unslice + - will be used via helper property. + """ + self.attribute_lim_helper.set_slice(self.viewer_state.numpy_slice_aggregation_transpose[0]) + def flip_limits(self): """ Flip the image levels. diff --git a/glue/viewers/image/tests/test_state.py b/glue/viewers/image/tests/test_state.py index 055c995c5..ce88efb0c 100644 --- a/glue/viewers/image/tests/test_state.py +++ b/glue/viewers/image/tests/test_state.py @@ -418,3 +418,43 @@ def test_attribute_units(): assert_allclose(layer_state1.v_min, 2.475) assert_allclose(layer_state1.v_max, 50) + + +def test_stretch_global(): + + # Test the option of using global vs per-slice stretch + + viewer_state = ImageViewerState() + + data1 = Data(x=np.arange(1000).reshape((10, 10, 10))) + + layer_state = ImageLayerState(layer=data1, viewer_state=viewer_state) + viewer_state.layers.append(layer_state) + + assert layer_state.stretch_global is True + + assert layer_state.percentile == 100 + assert layer_state.v_min == 0 + assert layer_state.v_max == 999 + + assert viewer_state.slices == (0, 0, 0) + + layer_state.stretch_global = False + + assert layer_state.v_min == 0 + assert layer_state.v_max == 99 + + viewer_state.slices = (9, 0, 0) + + assert layer_state.v_min == 900 + assert layer_state.v_max == 999 + + layer_state.percentile = 90 + + assert layer_state.v_min == 904.95 + assert layer_state.v_max == 994.05 + + layer_state.stretch_global = True + + assert layer_state.v_min == 49.95 + assert layer_state.v_max == 949.05