1+ from contextlib import contextmanager
12from pathlib import Path
23
34import numpy as np
@@ -157,19 +158,41 @@ def _set_scale_aspect_ratio_to_match_viewer(self,
157158 self ._scales [reset_scale ].max = frozen_width [reset_scale ] * scale_factor
158159 self .center = current_center
159160
161+ @contextmanager
162+ def _hold_all_sync (self ):
163+ """
164+ Batch trait sync messages for the image mark, its color scale and
165+ both axis scales so that the front end receives a single state
166+ update per widget, and hence redraws once, instead of redrawing
167+ after every trait assignment.
168+
169+ These are separate widgets, so each still syncs its own message and
170+ the front end redraws on each. On exit the contexts release -- and
171+ each widget flushes -- in reverse of the order entered here, so the
172+ entry order is chosen to make the image mark flush first, then the
173+ color scale, then the scales. That way the front end never draws
174+ the old image against the new scales (a "refit" flash) nor recolors
175+ the old image before the new data arrives.
176+ """
177+ with self ._scales ['y' ].hold_sync (), self ._scales ['x' ].hold_sync (), \
178+ self ._image .scales ['image' ].hold_sync (), \
179+ self ._image .hold_sync ():
180+ yield
181+
160182 def set_data (self , image_data , reset_view = True ):
161183 self ._image_shape = image_data .shape
162184
163- if reset_view :
164- self .reset_scale_to_fit_image ()
185+ with self ._hold_all_sync ():
186+ if reset_view :
187+ self .reset_scale_to_fit_image ()
165188
166- # Set the image data and map it to the bqplot figure so that
167- # cursor location corresponds to the underlying array index.
168- # The offset follows the convention that the index corresponds
169- # to the center of the pixel.
170- self ._image .image = image_data
171- self ._image .x = [- 0.5 , self ._image_shape [1 ] - 0.5 ]
172- self ._image .y = [- 0.5 , self ._image_shape [0 ] - 0.5 ]
189+ # Set the image data and map it to the bqplot figure so that
190+ # cursor location corresponds to the underlying array index.
191+ # The offset follows the convention that the index corresponds
192+ # to the center of the pixel.
193+ self ._image .image = image_data
194+ self ._image .x = [- 0.5 , self ._image_shape [1 ] - 0.5 ]
195+ self ._image .y = [- 0.5 , self ._image_shape [0 ] - 0.5 ]
173196
174197 @property
175198 def scale_widths (self ):
@@ -364,8 +387,15 @@ def __init__(self, *args, display_width=500, display_aspect_ratio=1):
364387
365388 self ._astro_im = _AstroImage (display_width = display_width ,
366389 viewer_aspect_ratio = display_aspect_ratio )
367- self ._default_cuts = apviz .MinMaxInterval ()
390+ # Cut out the sky background at the bottom and clip only the
391+ # brightest pixels at the top.
392+ self ._default_cuts = apviz .AsymmetricPercentileInterval (30 , 96 )
368393 self ._default_stretch = None
394+ self ._default_colormap = 'Greys_r'
395+ # Store the default through the API layer (which keeps settings
396+ # for the None label even before a load) so that get_colormap
397+ # reports it; set_colormap also applies it to the front end.
398+ self .set_colormap (self ._default_colormap )
369399
370400 self ._data = None
371401 self ._wcs = None
@@ -377,12 +407,14 @@ def __init__(self, *args, display_width=500, display_aspect_ratio=1):
377407 # Guards re-entrancy while we programmatically update the viewport.
378408 self ._updating_viewport = False
379409
410+ # While True, _refresh_display does nothing; set by _defer_refresh.
411+ self ._refresh_deferred = False
412+
380413 # Provide an Output widget to which prints can be directed for
381414 # debugging.
382415 self ._print_out = ipw .Output ()
383416
384417 self .marker = {'color' : 'red' , 'radius' : 20 , 'type' : 'square' }
385- self ._cuts = apviz .AsymmetricPercentileInterval (1 , 99 )
386418
387419 self ._cursor = ipw .HTML ('Coordinates show up here' )
388420
@@ -539,6 +571,31 @@ def _send_data(self, reset_view=True, stretch=None, cuts=None):
539571 self ._astro_im .set_data (self ._interval_and_stretch (stretch = stretch , cuts = cuts ),
540572 reset_view = reset_view )
541573
574+ def _refresh_display (self , image_label = None , reset_view = False ):
575+ """
576+ Recompute the displayed array from the cuts and stretch stored
577+ for the image and send it to the viewer.
578+ """
579+ if self ._data is None or self ._refresh_deferred :
580+ return
581+
582+ self ._send_data (cuts = self .get_cuts (image_label = image_label ),
583+ stretch = self .get_stretch (image_label = image_label ),
584+ reset_view = reset_view )
585+
586+ @contextmanager
587+ def _defer_refresh (self ):
588+ """
589+ Make _refresh_display a no-op inside the block so that several
590+ settings can be stored with a single recomputation of the
591+ displayed array. The caller refreshes after the block.
592+ """
593+ self ._refresh_deferred = True
594+ try :
595+ yield
596+ finally :
597+ self ._refresh_deferred = False
598+
542599 @property
543600 def _current_image_label (self ):
544601 """
@@ -550,26 +607,68 @@ def set_stretch(self, value, image_label=None, **kwargs):
550607 super ().set_stretch (value , image_label = image_label , ** kwargs )
551608 # Changing the stretch only affects the color mapping, so leave the
552609 # current viewport (zoom/pan) untouched.
553- self ._send_data ( stretch = value , reset_view = False )
610+ self ._refresh_display ( image_label = image_label )
554611
555612 def set_cuts (self , value , image_label = None , ** kwargs ):
556613 super ().set_cuts (value , image_label = image_label , ** kwargs )
557614 # Changing the cuts only affects the color mapping, so leave the
558615 # current viewport (zoom/pan) untouched.
559- self ._send_data (cuts = self .get_cuts (image_label = image_label ),
560- reset_view = False )
616+ self ._refresh_display (image_label = image_label )
561617
562618 @property
563619 def viewer (self ):
564620 return self ._astro_im
565621
566622 # The methods, grouped loosely by purpose
567623 def load_image (self , image , image_label = None , ** kwargs ):
568- super ().load_image (image , image_label = image_label , ** kwargs )
569- data = self .get_image (image_label = image_label )
624+ # A newly loaded image is always displayed with the current cuts,
625+ # stretch and colormap; only the viewport resets. Capture the
626+ # current settings before the API layer replaces them with its own
627+ # defaults during the load.
628+ if image_label is not None and image_label in self ._images :
629+ # Re-loading an existing image: its own settings are current.
630+ settings_label = image_label
631+ have_current = True
632+ elif self ._data is not None :
633+ # A new image: carry forward from the displayed image. Its label
634+ # is legitimately None when the caller never passed one, so guard
635+ # on whether an image is displayed, not on the label's value.
636+ settings_label = self ._current_image_label
637+ have_current = True
638+ else :
639+ # Nothing has been displayed yet.
640+ have_current = False
570641
571- self ._data = data .data if isinstance (data , NDData ) else data
572- self ._send_data ()
642+ if have_current :
643+ current_cuts = self .get_cuts (image_label = settings_label )
644+ current_stretch = self .get_stretch (image_label = settings_label )
645+ current_colormap = self .get_colormap (image_label = settings_label )
646+ else :
647+ current_cuts = self ._default_cuts
648+ # Leave the stretch the API layer stores on load (linear).
649+ current_stretch = self ._default_stretch
650+ current_colormap = self ._default_colormap
651+
652+ # Hold the sync so the scale changes from the viewport
653+ # initialization in the API layer arrive at the front end in the
654+ # same batch as the new image data, avoiding flicker from
655+ # intermediate redraws.
656+ with self ._astro_im ._hold_all_sync ():
657+ # Store the settings for the new image so the get_* methods
658+ # report what is displayed. Defer the refresh each setter
659+ # triggers; one refresh at the end displays the image.
660+ with self ._defer_refresh ():
661+ super ().load_image (image , image_label = image_label , ** kwargs )
662+
663+ self .set_cuts (current_cuts , image_label = image_label )
664+ if current_stretch is not None :
665+ self .set_stretch (current_stretch , image_label = image_label )
666+ self .set_colormap (current_colormap , image_label = image_label )
667+
668+ data = self .get_image (image_label = image_label )
669+ self ._data = data .data if isinstance (data , NDData ) else data
670+
671+ self ._refresh_display (image_label = image_label , reset_view = True )
573672
574673 # Saving contents of the view and accessing the view
575674 def save (self , filename , overwrite = False , ** kwargs ):
0 commit comments