Source code for pypeit.display.display

"""
Module for ginga routines.  Mainly for debugging

.. include:: ../include/links.rst

"""
import os
import numpy as np
import time
from IPython import embed
import subprocess

# A note from ejeschke on how to use the canvas add command in ginga: https://github.com/ejeschke/ginga/issues/720
# c
# The add() command can add any of the shape types that are defined under ginga.canvas.types. The good part is that if you
# go to that directory in the ginga source tree (ginga/canvas/types) and browse the source, you will find a parameter
# description table at the beginning of each type definition, describing each parameter in the type and what it is for.
# Most of the standard geometric types are in basic.py and there are specialized ones in utils.py, astro.py and layer.py. Looking at
# the classes will also tell you which parameters are positional and which are keyword.

from astropy.io import fits

from ginga.util import grc

from pypeit import msgs
from pypeit import io
from pypeit import utils

[docs]def connect_to_ginga(host='localhost', port=9000, raise_err=False, allow_new=False): """ Connect to a RC Ginga. Args: host (:obj:`str`, optional): Host name. port (:obj:`int`, optional): Probably should remain at 9000 raise_err (:obj:`bool`, optional): Raise an error if no connection is made, otherwise just raise a warning and continue allow_new (:obj:`bool`, optional): Allow a subprocess to be called to execute a new ginga viewer if one is not already running. Returns: `ginga.RemoteClient`_: connection to ginga viewer. """ # Start viewer = grc.RemoteClient(host, port) # Test sh = viewer.shell() try: tmp = sh.get_current_workspace() except: if allow_new: subprocess.Popen(['ginga', '--modules=RC,SlitWavelength']) # NOTE: time.sleep(3) is now insufficient. The loop below # continues to try to connect with the ginga viewer that # was just instantiated for a maximum number of iterations. # If the connection is remains unsuccessful, an error is # thrown stating that the connection timed out. maxiter = int(1e6) for i in range(maxiter): try: viewer = grc.RemoteClient(host, port) sh = viewer.shell() tmp = sh.get_current_workspace() except: continue else: break if i == maxiter-1: msgs.error('Timeout waiting for ginga to start. If window does not appear, type ' '`ginga --modules=RC,SlitWavelength` on the command line. In either case, wait for ' 'the ginga viewer to open and try the pypeit command again.') return viewer if raise_err: raise ValueError else: msgs.warn('Problem connecting to Ginga. Launch an RC Ginga viewer and ' 'then continue: \n ginga --modules=RC,SlitWavelength') # Return return viewer
[docs]def show_image(inp, chname='Image', waveimg=None, mask=None, exten=0, cuts=None, clear=False, wcs_match=False): """ Display an image using Ginga. .. todo:: - implement instrument specific reading - use the `mask` as a boolean mask if `bitmask` is not provided. Args: inp (:obj:`str`, numpy.ndarray): The image to view. If a string is provided, it must be the name of a fits image that can be read by `astropy.io.fits`. chname (:obj:`str`, optional): The name of the ginga channel to use. waveimg (:obj:`numpy.ndarray`, optional): Wavelength image mask (:class:`~pypeit.images.ImageBitMaskArray`, optional): A bitmask array that designates a pixel as being masked. Currently this is only used when displaying the spectral extraction result. exten (:obj:`int`, optional): The extension of the fits file with the image to show. This is only used if the input is a file name. cuts (array-like, optional): Initial cut levels to apply when displaying the image. This object must have a length of 2 with the lower and upper levels, respectively. clear (:obj:`bool`, optional): Clear any existing ginga viewer and its channels. wcs_match(:obj:`bool`, optional): Use this as a reference image for the WCS and match all image in other channels to it. Returns: :obj:`tuple`: The ginga remote client object and the channel object with the displayed image. Raises: ValueError: Raised if `cuts` is provided and does not have two elements. """ # Input checks if cuts is not None and len(cuts) != 2: raise ValueError('Input cuts must only have two elements, the lower and upper cut.') # Instantiate viewer viewer = connect_to_ginga() # Read or set the image data. This will fail if the input is a # string and astropy.io.fits cannot read the image. img = io.fits_open(inp)[exten].data if isinstance(inp, str) else inp if clear: clear_all() ch = viewer.channel(chname) # Header header = {} header['NAXIS1'] = img.shape[1] header['NAXIS2'] = img.shape[0] # Giddy up # waveimg = None if waveimg is not None: sh = viewer.shell() args = [chname, chname, grc.Blob(img.tobytes()), img.shape, img.dtype.name, header, grc.Blob(waveimg.tobytes()), waveimg.dtype.name, {}] sh.call_global_plugin_method('SlitWavelength', 'load_buffer', args, {}) else: ch.load_np(chname, img, 'fits', header) # These commands set up the viewer. They can be found at # ginga/ginga/ImageView.py canvas = viewer.canvas(ch._chname) out = canvas.clear() out = ch.set_color_map('ramp') out = ch.set_intensity_map('ramp') out = ch.set_color_algorithm('linear') out = ch.restore_contrast() out = ch.restore_cmap() if cuts is not None: out = ch.cut_levels(float(cuts[0]), float(cuts[1])) # WCS Match this to other images with this as the reference image? if wcs_match: # After displaying all the images since up the images with WCS_MATCH shell = viewer.shell() out = shell.start_global_plugin('WCSMatch') out = shell.call_global_plugin_method('WCSMatch', 'set_reference_channel', [chname], {}) # TODO: I would prefer to change the color map to indicate these # pixels rather than overplot points. Because for large numbers of # masked pixels, this is super slow. Need to ask ginga folks how to # do that. # If bitmask was passed in, assume this is an extraction qa image # and use the mask to identify why each pixel was masked if mask is not None: # Select pixels on any slit onslit = mask.flagged('OFFSLITS', invert=True) # These are the pixels that were masked by the bpm spec_bpm, spat_bpm = np.where(mask.bpm & onslit) nbpm = len(spec_bpm) # note: must cast numpy floats to regular python floats to pass the remote interface points_bpm = [dict(type='point', args=(float(spat_bpm[i]), float(spec_bpm[i]), 2), kwargs=dict(style='plus', color='magenta')) for i in range(nbpm)] # These are the pixels that were masked by LACOSMICS spec_cr, spat_cr = np.where(mask.cr & onslit) ncr = len(spec_cr) # note: must cast numpy floats to regular python floats to pass the remote interface points_cr = [dict(type='point', args=(float(spat_cr[i]), float(spec_cr[i]), 2), kwargs=dict(style='plus', color='cyan')) for i in range(ncr)] # These are the pixels that were masked by the extraction spec_ext, spat_ext = np.where(mask.extract & onslit) next = len(spec_ext) # note: must cast numpy floats to regular python floats to pass the remote interface points_ext = [dict(type='point', args=(float(spat_ext[i]), float(spec_ext[i]), 2), kwargs=dict(style='plus', color='red')) for i in range(next)] # Get the "rest" of the flags other_flags = list(mask.bit_keys()) other_flags.remove('OFFSLITS') other_flags.remove('BPM') other_flags.remove('CR') other_flags.remove('EXTRACT') # Determine where any of them are flagged other_bpm = mask.flagged(flag=other_flags) # These are the pixels that were masked for any other reason (and on a slit) spec_oth, spat_oth = np.where(other_bpm & onslit) noth = len(spec_oth) # note: must cast numpy floats to regular python floats to pass # the remote interface points_oth = [dict(type='point', args=(float(spat_oth[i]), float(spec_oth[i]), 2), kwargs=dict(style='plus', color='yellow')) for i in range(noth)] nspat = img.shape[1] nspec = img.shape[0] # Labels for the points text_bpm = [dict(type='text', args=(nspat / 2 -40, nspec / 2, 'BPM'), kwargs=dict(color='magenta', fontsize=20))] text_cr = [dict(type='text', args=(nspat / 2 -40, nspec / 2 - 30, 'CR'), kwargs=dict(color='cyan', fontsize=20))] text_ext = [dict(type='text', args=(nspat / 2 -40, nspec / 2 - 60, 'EXTRACT'), kwargs=dict(color='red', fontsize=20))] text_oth = [dict(type='text', args=(nspat / 2 -40, nspec / 2 - 90, 'OTHER'), kwargs=dict(color='yellow', fontsize=20))] canvas_list = points_bpm + points_cr + points_ext + points_oth + text_bpm + text_cr \ + text_ext + text_oth canvas.add('constructedcanvas', canvas_list) return viewer, ch
[docs]def show_points(viewer, ch, spec, spat, color='cyan', legend=None, legend_spec=None, legend_spat=None): """ Plot points in a ginga viewer Parameters ---------- viewer : `ginga.RemoteClient`_ Ginga RC viewer ch : ``ginga.util.grc._channel_proxy`` Ginga channel spec : list List of spectral positions on image to plot spat : list List of spatial positions on image to plot color : str Color for points legend : str Label for a legend legend_spec : float Spectral pixel loation for legend legend_spat : float Pixel loation for legend """ canvas = viewer.canvas(ch._chname) npoints = len(spec) canvas_list = [dict(type='point', args=(float(spat[i]), float(spec[i]), 2), kwargs=dict(style='plus', color=color)) for i in range(npoints)] if legend is not None: spec_label = np.mean(np.array(spec)) if legend_spec is None else legend_spec spat_label = (np.mean(np.array(spat)) + 30) if legend_spat is None else legend_spat text = [dict(type='text', args=(spat_label, spec_label, legend), kwargs=dict(color=color, fontsize=20))] canvas_list += text canvas.add('constructedcanvas', canvas_list)
# TODO: Should we continue to allow rotate as an option?
[docs]def show_slits(viewer, ch, left, right, slit_ids=None, left_ids=None, right_ids=None, maskdef_ids=None, spec_vals=None, rotate=False, pstep=50, clear=False, synced=True): r""" Overplot slits on the image in Ginga in the given channel Args: viewer (`ginga.RemoteClient`_): Ginga RC viewer ch (``ginga.util.grc._channel_proxy``): Ginga channel left (`numpy.ndarray`_): Array with spatial position of left slit edges. Shape must be :math:`(N_{\rm spec},)` or :math:`(N_{\rm spec}, N_{\rm l-edge})`, and can be different from ``right`` unless ``synced`` is True. right (`numpy.ndarray`_): Array with spatial position of right slit edges. Shape must be :math:`(N_{\rm spec},)` or :math:`(N_{\rm spec}, N_{\rm r-edge})`, and can be different from ``left`` unless ``synced`` is True. spec_vals (`numpy.ndarray`_, optional): Array with spectral position of left and right slit edges. Shape must be :math:`(N_{\rm spec},)` or :math:`(N_{\rm spec}, N_{\rm r-edge})`. Currently it is only possible to input a single set of spec_vals for both ``left`` and ``right`` edges, but not possible to pass distinct spec_vals for left and right. If not passed in the default of np.arange(:math:`(N_{\rm spec},)`) will be used. slit_ids (:obj:`int`, array-like, optional): PypeIt ID numbers for the slits. If None, IDs run from -1 to :math:`-N_{\rm slits}`. If not None, shape must be :math:`(N_{\rm slits},)`. These are only used if ``synced`` is True. left_ids (:obj:`int`, array-like, optional): ID numbers for the left edges. If None, IDs run from -1 to :math:`-N_{\rm l-edge}`. If not None, shape must be :math:`(N_{\rm l-edge},)`. These are only used if ``synced`` is False. right_ids (:obj:`int`, array-like, optional): ID numbers for the right edges. If None, IDs run from -1 to :math:`-N_{\rm r-edge}`. If not None, shape must be :math:`(N_{\rm r-edge},)`. These are only used if ``synced`` is False. maskdef_ids (:obj:`int`, array-like, optional): slitmask IDs assigned to each slits. If None, IDs will not be shown. If not None, shape must be :math:`(N_{\rm slits},)`. These are only used if ``synced`` is True. rotate (:obj:`bool`, optional): Rotate the image? pstep (:obj:`bool`, optional): Show every pstep point of the edges as opposed to *every* point, recommended for speed. clear (:obj:`bool`, optional): Clear the canvas? synced (:obj:`bool`, optional): Flag the left and right traces are synced into slits. Otherwise, the edges are treated separately. If True, the number of left and right edges must be the same and ``left_ids`` and ``right_ids`` are ignored. """ # Setup the trace data and IDs _left = left.reshape(-1,1) if left.ndim == 1 else left nleft = _left.shape[1] _right = right.reshape(-1,1) if right.ndim == 1 else right nright = _right.shape[1] nspec = _left.shape[0] if _right.shape[0] != nspec: # TODO: Any reason to remove this restriction? msgs.error('Input left and right edges have different spectral lengths.') # Spectral pixel location if spec_vals is not None: y = spec_vals.reshape(-1,1) if spec_vals.ndim == 1 else spec_vals else: y = np.repeat(np.arange(nspec).astype(float)[:, np.newaxis], nright, axis=1) # Check input if synced: if left.shape != right.shape: msgs.error('Input left and right traces must have the same shape if they have been ' 'synchronized into slits.') if left_ids is not None or right_ids is not None: msgs.warn('For showing synced edges, left and right ID numbers are ignored.') nslits = _left.shape[1] _left_ids = None _right_ids = None _slit_ids = np.arange(nslits) if slit_ids is None else np.atleast_1d(slit_ids) if len(_slit_ids) != nslits: msgs.error('Incorrect number of slit IDs provided.') _slit_id_loc = _left + 0.45*(_right - _left) if maskdef_ids is not None and maskdef_ids.size == nslits: _maskdef_ids = np.atleast_1d(maskdef_ids) else: _maskdef_ids = None else: _left_ids = -np.arange(nleft) if left_ids is None else np.atleast_1d(left_ids) if len(_left_ids) != nleft: msgs.error('Incorrect number of left IDs provided.') _left_id_loc = _left*1.05 _right_ids = -np.arange(nright) if right_ids is None else np.atleast_1d(right_ids) if len(_right_ids) != nright: msgs.error('Incorrect number of right IDs provided.') _right_id_loc = _right*(1-0.05) # Canvas canvas = viewer.canvas(ch._chname) if clear: canvas.clear() # Label positions top = int(2*nspec/3.) bot = int(nspec/2.) # Plot lefts. Points need to be int or float. Use of .tolist() on # each array insures this canvas_list = [dict(type=str('path'), args=(list(zip(y[::pstep, i].tolist(), _left[::pstep,i].tolist())),) if rotate else (list(zip(_left[::pstep,i].tolist(), y[::pstep, i].tolist())),), kwargs=dict(color=str('green'))) for i in range(nleft)] if not synced: # Add text canvas_list += [dict(type='text', args=(float(y[bot, i]), float(_left_id_loc[bot,i]), str('S{0}'.format(_left_ids[i]))) if rotate else (float(_left_id_loc[bot,i]), float(y[bot, i]), str('S{0}'.format(_left_ids[i]))), kwargs=dict(color=str('aquamarine'), fontsize=20., rot_deg=90.)) for i in range(nleft)] # Plot rights. Points need to be int or float. Use of .tolist() on # each array insures this canvas_list += [dict(type=str('path'), args=(list(zip(y[::pstep, i].tolist(), _right[::pstep,i].tolist())),) if rotate else (list(zip(_right[::pstep,i].tolist(), y[::pstep, i].tolist())),), kwargs=dict(color=str('magenta'))) for i in range(nright)] if not synced: # Add text canvas_list += [dict(type='text', args=(float(y[bot, i]), float(_right_id_loc[bot,i]), str('S{0}'.format(_right_ids[i]))) if rotate else (float(_right_id_loc[bot,i]), float(y[bot, i]), str('S{0}'.format(_right_ids[i]))), kwargs=dict(color=str('magenta'), fontsize=20., rot_deg=90.)) for i in range(nright)] canvas.add('constructedcanvas', canvas_list) # Plot slit labels, if synced if synced: # Slit IDs canvas_list += [dict(type='text', args=(float(y[bot, i]), float(_slit_id_loc[bot,i])-400, str('S{0}'.format(_slit_ids[i]))) if rotate else (float(_slit_id_loc[bot,i]), float(y[bot, i])-400, str('S{0}'.format(_slit_ids[i]))), kwargs=dict(color=str('aquamarine'), fontsize=20., rot_deg=90.)) for i in range(nslits)] # maskdef_ids if _maskdef_ids is not None: canvas_list += [dict(type='text', args=(float(y[bot, i]), float(_slit_id_loc[bot,i])-250, str('{0}'.format(_maskdef_ids[i]))) if rotate else (float(_slit_id_loc[bot,i]), float(y[bot, i])-250, str('{0}'.format(_maskdef_ids[i]))), kwargs=dict(color=str('cyan'), fontsize=20., rot_deg=90.)) for i in range(nslits)] canvas.add('constructedcanvas', canvas_list)
[docs]def show_trace(viewer, ch, trace, trc_name=None, maskdef_extr=None, manual_extr=None, clear=False, rotate=False, pstep=3, yval=None, color='blue'): r""" Args: viewer (`ginga.RemoteClient`_): Ginga RC viewer. ch (``ginga.util.grc._channel_proxy``): Ginga channel. trace (`numpy.ndarray`_): Array with spatial position of the object traces on the detector. Shape must be :math:`(N_{\rm spec},)` or :math:`(N_{\rm spec}, N_{\rm trace})`. trc_name (`numpy.ndarray`_, optional): Array with Trace names. Shape must be :math:`(N_{\rm trace},)`. maskdef_extr (`numpy.ndarray`_, optional): Array with the maskdef extraction flags. Shape must be :math:`(N_{\rm trace},)`. manual_extr (`numpy.ndarray`_, optional): Array with the manual extraction flags. Shape must be :math:`(N_{\rm trace},)`. clear (:obj:`bool`, optional): Clear the canvas? rotate (:obj:`bool`, optional): Rotate the image? pstep (:obj:`bool`, optional): Show every pstep point of the edges as opposed to *every* point, recommended for speed. yval (`numpy.ndarray`_, optional): Array with spectral position of the object traces. Shape must be :math:`(N_{\rm spec},)` or :math:`(N_{\rm spec}, N_{\rm trace})`. If not passed in, the default of np.arange(:math:`(N_{\rm spec},)`) will be used. color (str, optional): Color for the trace """ # Canvas canvas = viewer.canvas(ch._chname) if clear: canvas.clear() if trace.ndim == 1: trace = trace.reshape(-1,1) ntrace = trace.shape[1] _maskdef_extr = ntrace*[False] if maskdef_extr is None else maskdef_extr _manual_extr = ntrace*[False] if manual_extr is None else manual_extr # Show if yval is None: y = np.repeat(np.arange(trace.shape[0]).astype(float)[:, None], trace.shape[1], axis=1) else: y = yval.reshape(-1, 1) if yval.ndim == 1 else yval canvas_list = [] for i in range(trace.shape[1]): if _maskdef_extr[i]: _color = '#f0e442' if color is not None else color elif _manual_extr[i]: _color = '#33ccff' if color is not None else color else: _color = 'orange' if color is not None else color canvas_list += [dict(type=str('path'), args=(list(zip(y[::pstep,i].tolist(), trace[::pstep,i].tolist())),) if rotate else (list(zip(trace[::pstep,i].tolist(), y[::pstep,i].tolist())),), kwargs=dict(color=_color))] # Text ohf = len(trace[:,i])//2 # Do it canvas_list += [dict(type='text',args=(float(y[ohf,i]), float(trace[ohf,i]), str(trc_name[i])) if rotate else (float(trace[ohf,i]), float(y[ohf,i]), str(trc_name[i])), kwargs=dict(color=_color, fontsize=17., rot_deg=90.))] canvas.add('constructedcanvas', canvas_list)
[docs]def clear_canvas(cname): """ Clear the ginga canvas Args: cname (str): Channel name """ viewer = connect_to_ginga() ch = viewer.channel(cname) canvas = viewer.canvas(ch._chname) canvas.clear()
[docs]def clear_all(allow_new=False): """ Clear all of the ginga canvasses. Args: allow_new (:obj:`bool`, optional): Allow a subprocess to be called to execute a new ginga viewer if one is not already running. See :func:`connect_to_ginga`. """ viewer = connect_to_ginga(allow_new=allow_new) shell = viewer.shell() chnames = shell.get_channel_names() for ch in chnames: shell.delete_channel(ch)
[docs]def show_tilts(viewer, ch, tilt_traces, yoff=0., xoff=0., points=True, nspec=None, pstep=3, clear_canvas=False): """ Show the arc tilts on the input channel Args: viewer (`ginga.RemoteClient`_): Ginga RC viewer ch (``ginga.util.grc._channel_proxy``): Ginga channel tilt_traces (`astropy.table.Table`_): Table containing the traced and fitted tilts yoff (float, optional): Offset tilts by this amount xoff (float, optional): Offset tilts by this amount points (bool, optional): Plot the Gaussian-weighted tilt centers nspec (int, optional): Number of spectral pixels in the TiltImage pstep (int, optional): Show every pstep point of the tilts as opposed to *every* point, recommended for speed. clear_canvas (bool, optional): Clear the canvas first? """ if tilt_traces is None: return msgs.error('No tilts have been traced or fitted') canvas = viewer.canvas(ch._chname) if clear_canvas: canvas.clear() canvas_list = [] # Plot traced tilts # We just plot the points, so we do not need to loop over each slit/line # This makes the plotting much very slow, this is why we make it optional by using the points keyword if 'goodpix_tilt' in tilt_traces.keys() and tilt_traces['goodpix_tilt'][0].size > 0 and points: # note: must cast numpy floats to regular python floats to pass the remote interface goodpix_spat = tilt_traces['goodpix_spat'][0] + xoff goodpix_tilt = tilt_traces['goodpix_tilt'][0] + yoff canvas_list += [dict(type='squarebox', args=(float(goodpix_spat[i]), float(goodpix_tilt[i]), 1), kwargs=dict(color='cyan', fill=False)) for i in range(goodpix_tilt.size)] # Plot the 2D fitted tilts # loop over each line, this allows to use type='path' and therefore a faster plotting if 'good2dfit_lid' in tilt_traces.keys(): for iline in np.unique(tilt_traces['good2dfit_lid'][0]): # good fit this_line = tilt_traces['good2dfit_lid'][0] == iline if np.any(this_line): good2dfit_spat = tilt_traces['good2dfit_spat'][0][this_line] + xoff good2dfit_tilt = tilt_traces['good2dfit_tilt'][0][this_line] + yoff canvas_list += [dict(type=str('path'), args=(list(zip(good2dfit_spat[::pstep].tolist(), good2dfit_tilt[::pstep].tolist())),), kwargs=dict(color='blue', linewidth=1))] # Now plot the masked traces and the rejected 2D fits # We just plot the points, so we do not need to loop over each slit/line # masked traces if 'badpix_tilt' in tilt_traces.keys() and tilt_traces['badpix_tilt'][0].size > 0: # note: must cast numpy floats to regular python floats to pass the remote interface badpix_spat = tilt_traces['badpix_spat'][0] + xoff badpix_tilt = tilt_traces['badpix_tilt'][0] + yoff canvas_list += [dict(type='squarebox', args=(float(badpix_spat[i]), float(badpix_tilt[i]), 1), kwargs=dict(color='red', fill=False)) for i in range(badpix_tilt.size)] # rejected fit if 'bad2dfit_tilt' in tilt_traces.keys() and tilt_traces['bad2dfit_tilt'][0].size > 0: # note: must cast numpy floats to regular python floats to pass the remote interface bad2dfit_spat = tilt_traces['bad2dfit_spat'][0] + xoff bad2dfit_tilt = tilt_traces['bad2dfit_tilt'][0] + yoff canvas_list += [dict(type='squarebox', args=(float(bad2dfit_spat[i]), float(bad2dfit_tilt[i]), 1), kwargs=dict(color='yellow', fill=False)) for i in range(bad2dfit_tilt.size)] # Add text text_xpos = 20 start_ypos = 20 ypos_step = 0.03*nspec if nspec is not None else 50. text_ypos = [start_ypos, start_ypos + ypos_step, start_ypos + 2*ypos_step] text_str = ['Masked pixel', 'Rejected in fit', 'Good tilt fit'] text_color = ['red', 'yellow', 'blue'] if points: text_ypos += [start_ypos + 3*ypos_step] text_str += ['Good pixel'] text_color += ['cyan'] canvas_list += [dict(type='text', args=(float(text_xpos), float(text_ypos[i]), str(text_str[i])), kwargs=dict(color=text_color[i], fontsize=20)) for i in range(len(text_str))] canvas.add('constructedcanvas', canvas_list)
[docs]def show_scattered_light(image_list, slits=None, wcs_match=True): """ Interface to ginga to show the quality of the Scattered Light subtraction Parameters ---------- image_list : zip A zip of the images to show, their names, and the scales slits : :class:`~pypeit.slittrace.SlitTraceSet`, optional The current slit traces wcs_match : :obj:`bool`, optional Use a reference image for the WCS and match all image in other channels to it. """ connect_to_ginga(raise_err=True, allow_new=True) if slits is not None: left, right, mask = slits.select_edges() gpm = mask == 0 # Loop me clear = True for img, name, cut in image_list: if img is None: continue viewer, ch = show_image(img, chname=name, cuts=cut, wcs_match=wcs_match, clear=clear) if slits is not None: show_slits(viewer, ch, left[:, gpm], right[:, gpm], slit_ids=slits.spat_id[gpm]) # Turn off clear if clear: clear = False