Skip to content

Commit

Permalink
Merge pull request #2476 from dhomeier/slice-attribute-limits
Browse files Browse the repository at this point in the history
  • Loading branch information
astrofrog authored Apr 19, 2024
2 parents 4bf43cd + 467db11 commit 6bd0021
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 10 deletions.
25 changes: 17 additions & 8 deletions glue/core/state_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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):

Expand Down
22 changes: 22 additions & 0 deletions glue/core/tests/test_state_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():

Expand Down
30 changes: 28 additions & 2 deletions glue/viewers/image/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -492,15 +492,18 @@ 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')
cmap = DDCProperty(docstring='The colormap used to render the layer')
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):

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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.
Expand Down
40 changes: 40 additions & 0 deletions glue/viewers/image/tests/test_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 6bd0021

Please sign in to comment.