LSST Applications  21.0.0-172-gfb10e10a+18fedfabac,22.0.0+297cba6710,22.0.0+80564b0ff1,22.0.0+8d77f4f51a,22.0.0+a28f4c53b1,22.0.0+dcf3732eb2,22.0.1-1-g7d6de66+2a20fdde0d,22.0.1-1-g8e32f31+297cba6710,22.0.1-1-geca5380+7fa3b7d9b6,22.0.1-12-g44dc1dc+2a20fdde0d,22.0.1-15-g6a90155+515f58c32b,22.0.1-16-g9282f48+790f5f2caa,22.0.1-2-g92698f7+dcf3732eb2,22.0.1-2-ga9b0f51+7fa3b7d9b6,22.0.1-2-gd1925c9+bf4f0e694f,22.0.1-24-g1ad7a390+a9625a72a8,22.0.1-25-g5bf6245+3ad8ecd50b,22.0.1-25-gb120d7b+8b5510f75f,22.0.1-27-g97737f7+2a20fdde0d,22.0.1-32-gf62ce7b1+aa4237961e,22.0.1-4-g0b3f228+2a20fdde0d,22.0.1-4-g243d05b+871c1b8305,22.0.1-4-g3a563be+32dcf1063f,22.0.1-4-g44f2e3d+9e4ab0f4fa,22.0.1-42-gca6935d93+ba5e5ca3eb,22.0.1-5-g15c806e+85460ae5f3,22.0.1-5-g58711c4+611d128589,22.0.1-5-g75bb458+99c117b92f,22.0.1-6-g1c63a23+7fa3b7d9b6,22.0.1-6-g50866e6+84ff5a128b,22.0.1-6-g8d3140d+720564cf76,22.0.1-6-gd805d02+cc5644f571,22.0.1-8-ge5750ce+85460ae5f3,master-g6e05de7fdc+babf819c66,master-g99da0e417a+8d77f4f51a,w.2021.48
LSST Data Management Base Package
matplotlib.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008, 2009, 2010, 2015 LSST Corporation.
4 #
5 # This product includes software developed by the
6 # LSST Project (http://www.lsst.org/).
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <http://www.lsstcorp.org/LegalNotices/>.
21 #
22 
23 #
24 # \file
25 # \brief Definitions to talk to matplotlib from python using the "afwDisplay"
26 # interface
27 
28 import math
29 import sys
30 import unicodedata
31 import warnings
32 
33 import matplotlib.pyplot as pyplot
34 import matplotlib.cbook
35 import matplotlib.colors as mpColors
36 from matplotlib.blocking_input import BlockingInput
37 from mpl_toolkits.axes_grid1 import make_axes_locatable
38 
39 import numpy as np
40 import numpy.ma as ma
41 
42 import lsst.afw.display as afwDisplay
43 import lsst.afw.math as afwMath
44 import lsst.afw.display.rgb as afwRgb
45 import lsst.afw.display.interface as interface
46 import lsst.afw.display.virtualDevice as virtualDevice
47 import lsst.afw.display.ds9Regions as ds9Regions
48 import lsst.afw.image as afwImage
49 
50 import lsst.afw.geom as afwGeom
51 import lsst.geom as geom
52 
53 #
54 # Set the list of backends which support _getEvent and thus interact()
55 #
56 try:
57  interactiveBackends
58 except NameError:
59  # List of backends that support `interact`
60  interactiveBackends = [
61  "Qt4Agg",
62  "Qt5Agg",
63  ]
64 
65 try:
66  matplotlibCtypes
67 except NameError:
68  matplotlibCtypes = {
69  afwDisplay.GREEN: "#00FF00",
70  }
71 
72  def mapCtype(ctype):
73  """Map the ctype to a potentially different ctype
74 
75  Specifically, if matplotlibCtypes[ctype] exists, use it instead
76 
77  This is used e.g. to map "green" to a brighter shade
78  """
79  return matplotlibCtypes[ctype] if ctype in matplotlibCtypes else ctype
80 
81 
82 class DisplayImpl(virtualDevice.DisplayImpl):
83  """Provide a matplotlib backend for afwDisplay
84 
85  Recommended backends in notebooks are:
86  %matplotlib notebook
87  or
88  %matplotlib ipympl
89  or
90  %matplotlib qt
91  %gui qt
92  or
93  %matplotlib inline
94  or
95  %matplotlib osx
96 
97  Apparently only qt supports Display.interact(); the list of interactive
98  backends is given by lsst.display.matplotlib.interactiveBackends
99  """
100  def __init__(self, display, verbose=False,
101  interpretMaskBits=True, mtvOrigin=afwImage.PARENT, fastMaskDisplay=False,
102  reopenPlot=False, useSexagesimal=False, dpi=None, *args, **kwargs):
103  """
104  Initialise a matplotlib display
105 
106  @param fastMaskDisplay If True only show the first bitplane that's
107  set in each pixel
108  (e.g. if (SATURATED & DETECTED)
109  ignore DETECTED)
110  Not really what we want, but a bit faster
111  @param interpretMaskBits Interpret the mask value under the cursor
112  @param mtvOrigin Display pixel coordinates with LOCAL origin
113  (bottom left == 0,0 not XY0)
114  @param reopenPlot If true, close the plot before opening it.
115  (useful with e.g. %ipympl)
116  @param useSexagesimal If True, display coordinates in sexagesimal
117  E.g. hh:mm:ss.ss (default:False)
118  May be changed by calling
119  display.useSexagesimal()
120  @param dpi Number of dpi (passed to pyplot.figure)
121 
122  The `frame` argument to `Display` may be a matplotlib figure; this
123  permits code such as
124  fig, axes = plt.subplots(1, 2)
125 
126  disp = afwDisplay.Display(fig)
127  disp.scale('asinh', 'zscale', Q=0.5)
128 
129  for axis, exp in zip(axes, exps):
130  plt.sca(axis) # make axis active
131  disp.mtv(exp)
132  """
133  if hasattr(display.frame, "number"): # the "display" quacks like a matplotlib figure
134  figure = display.frame
135  else:
136  figure = None
137 
138  virtualDevice.DisplayImpl.__init__(self, display, verbose)
139 
140  if reopenPlot:
141  pyplot.close(display.frame)
142 
143  if figure is not None:
144  self._figure_figure = figure
145  else:
146  self._figure_figure = pyplot.figure(display.frame, dpi=dpi)
147  self._figure_figure.clf()
148 
149  self._display_display = display
150  self._maskTransparency_maskTransparency = {None: 0.7}
151  self._interpretMaskBits_interpretMaskBits = interpretMaskBits # interpret mask bits in mtv
152  self._fastMaskDisplay_fastMaskDisplay = fastMaskDisplay
153  self._useSexagesimal_useSexagesimal = [useSexagesimal] # use an array so we can modify the value in format_coord
154  self._mtvOrigin_mtvOrigin = mtvOrigin
155  self._mappable_ax_mappable_ax = None
156  self._colorbar_ax_colorbar_ax = None
157  self._image_colormap_image_colormap = pyplot.cm.gray
158  #
159  self.__alpha__alpha = unicodedata.lookup("GREEK SMALL LETTER alpha") # used in cursor display string
160  self.__delta__delta = unicodedata.lookup("GREEK SMALL LETTER delta") # used in cursor display string
161  #
162  # Support self._scale()
163  #
164  self._scaleArgs_scaleArgs = dict()
165  self._normalize_normalize = None
166  #
167  # Support self._erase(), reporting pixel/mask values, and
168  # zscale/minmax; set in mtv
169  #
170  self._i_setImage_i_setImage(None)
171  #
172  # Ignore warnings due to BlockingKeyInput
173  #
174  if not verbose:
175  warnings.filterwarnings("ignore", category=matplotlib.cbook.mplDeprecation)
176 
177  def _close(self):
178  """!Close the display, cleaning up any allocated resources"""
179  self._image_image = None
180  self._mask_mask = None
181  self._wcs_wcs = None
182  self._figure_figure.gca().format_coord = lambda x, y: None # keeps a copy of _wcs
183 
184  def _show(self):
185  """Put the plot at the top of the window stacking order"""
186 
187  try:
188  self._figure_figure.canvas._tkcanvas._root().lift() # tk
189  except AttributeError:
190  pass
191 
192  try:
193  self._figure_figure.canvas.manager.window.raise_() # os/x
194  except AttributeError:
195  pass
196 
197  try:
198  self._figure_figure.canvas.raise_() # qt[45]
199  except AttributeError:
200  pass
201 
202  #
203  # Extensions to the API
204  #
205  def savefig(self, *args, **kwargs):
206  """Defer to figure.savefig()
207 
208  Parameters
209  ----------
210  args : `list`
211  Passed through to figure.savefig()
212  kwargs : `dict`
213  Passed through to figure.savefig()
214  """
215  self._figure_figure.savefig(*args, **kwargs)
216 
217  def show_colorbar(self, show=True, where="right", axSize="5%", axPad=None, **kwargs):
218  """Show (or hide) the colour bar
219 
220  Parameters
221  ----------
222  show : `bool`
223  Should I show the colour bar?
224  where : `str`
225  Location of colour bar: "right" or "bottom"
226  axSize : `float` or `str`
227  Size of axes to hold the colour bar; fraction of current x-size
228  axPad : `float` or `str`
229  Padding between axes and colour bar; fraction of current x-size
230  args : `list`
231  Passed through to colorbar()
232  kwargs : `dict`
233  Passed through to colorbar()
234 
235  We set the default padding to put the colourbar in a reasonable
236  place for roughly square plots, but you may need to fiddle for
237  plots with extreme axis ratios.
238 
239  You can only configure the colorbar when it isn't yet visible, but
240  as you can easily remove it this is not in practice a difficulty.
241  """
242  if show:
243  if self._mappable_ax_mappable_ax:
244  if self._colorbar_ax_colorbar_ax is None:
245  orientationDict = dict(right="vertical", bottom="horizontal")
246 
247  mappable, ax = self._mappable_ax_mappable_ax
248 
249  if where in orientationDict:
250  orientation = orientationDict[where]
251  else:
252  print(f"Unknown location {where}; "
253  f"please use one of {', '.join(orientationDict.keys())}")
254 
255  if axPad is None:
256  axPad = 0.1 if orientation == "vertical" else 0.3
257 
258  divider = make_axes_locatable(ax)
259  self._colorbar_ax_colorbar_ax = divider.append_axes(where, size=axSize, pad=axPad)
260 
261  self._figure_figure.colorbar(mappable, cax=self._colorbar_ax_colorbar_ax, orientation=orientation, **kwargs)
262 
263  try: # fails with %matplotlib inline
264  pyplot.sca(ax) # make main window active again
265  except ValueError:
266  pass
267  else:
268  if self._colorbar_ax_colorbar_ax is not None:
269  self._colorbar_ax_colorbar_ax.remove()
270  self._colorbar_ax_colorbar_ax = None
271 
272  def useSexagesimal(self, useSexagesimal):
273  """Control the formatting coordinates as HH:MM:SS.ss
274 
275  Parameters
276  ----------
277  useSexagesimal : `bool`
278  Print coordinates as e.g. HH:MM:SS.ss iff True
279 
280  N.b. can also be set in Display's ctor
281  """
282 
283  """Are we formatting coordinates as HH:MM:SS.ss?"""
284  self._useSexagesimal_useSexagesimal[0] = useSexagesimal
285 
286  def wait(self, prompt="[c(ontinue) p(db)] :", allowPdb=True):
287  """Wait for keyboard input
288 
289  Parameters
290  ----------
291  prompt : `str`
292  The prompt string.
293  allowPdb : `bool`
294  If true, entering a 'p' or 'pdb' puts you into pdb
295 
296  Returns the string you entered
297 
298  Useful when plotting from a programme that exits such as a processCcd
299  Any key except 'p' continues; 'p' puts you into pdb (unless
300  allowPdb is False)
301  """
302  while True:
303  s = input(prompt)
304  if allowPdb and s in ("p", "pdb"):
305  import pdb
306  pdb.set_trace()
307  continue
308 
309  return s
310  #
311  # Defined API
312  #
313 
314  def _setMaskTransparency(self, transparency, maskplane):
315  """Specify mask transparency (percent)"""
316 
317  self._maskTransparency_maskTransparency[maskplane] = 0.01*transparency
318 
319  def _getMaskTransparency(self, maskplane=None):
320  """Return the current mask transparency"""
321  return self._maskTransparency_maskTransparency[maskplane if maskplane in self._maskTransparency_maskTransparency else None]
322 
323  def _mtv(self, image, mask=None, wcs=None, title=""):
324  """Display an Image and/or Mask on a matplotlib display
325  """
326  title = str(title) if title else ""
327 
328  #
329  # Save a reference to the image as it makes erase() easy and permits
330  # printing cursor values and minmax/zscale stretches. We also save XY0
331  #
332  self._i_setImage_i_setImage(image, mask, wcs)
333 
334  # We need to know the pixel values to support e.g. 'zscale' and
335  # 'minmax', so do the scaling now
336  if self._scaleArgs_scaleArgs.get('algorithm'): # someone called self.scale()
337  self._i_scale_i_scale(self._scaleArgs_scaleArgs['algorithm'], self._scaleArgs_scaleArgs['minval'], self._scaleArgs_scaleArgs['maxval'],
338  self._scaleArgs_scaleArgs['unit'], *self._scaleArgs_scaleArgs['args'], **self._scaleArgs_scaleArgs['kwargs'])
339 
340  ax = self._figure_figure.gca()
341  ax.cla()
342 
343  self._i_mtv_i_mtv(image, wcs, title, False)
344 
345  if mask:
346  self._i_mtv_i_mtv(mask, wcs, title, True)
347 
348  self.show_colorbarshow_colorbar()
349 
350  if title:
351  ax.set_title(title)
352 
353  self._title_title = title
354 
355  def format_coord(x, y, wcs=self._wcs, x0=self._xy0[0], y0=self._xy0[1],
356  origin=afwImage.PARENT, bbox=self._image.getBBox(afwImage.PARENT),
357  _useSexagesimal=self._useSexagesimal_useSexagesimal):
358 
359  fmt = '(%1.2f, %1.2f)'
360  if self._mtvOrigin_mtvOrigin == afwImage.PARENT:
361  msg = fmt % (x, y)
362  else:
363  msg = (fmt + "L") % (x - x0, y - y0)
364 
365  col = int(x + 0.5)
366  row = int(y + 0.5)
367  if bbox.contains(geom.PointI(col, row)):
368  if wcs is not None:
369  raDec = wcs.pixelToSky(x, y)
370  ra = raDec[0].asDegrees()
371  dec = raDec[1].asDegrees()
372 
373  if _useSexagesimal[0]:
374  from astropy import units as u
375  from astropy.coordinates import Angle as apAngle
376 
377  kwargs = dict(sep=':', pad=True, precision=2)
378  ra = apAngle(ra*u.deg).to_string(unit=u.hour, **kwargs)
379  dec = apAngle(dec*u.deg).to_string(unit=u.deg, **kwargs)
380  else:
381  ra = "%9.4f" % ra
382  dec = "%9.4f" % dec
383 
384  msg += r" (%s, %s): (%s, %s)" % (self.__alpha__alpha, self.__delta__delta, ra, dec)
385 
386  msg += ' %1.3f' % (self._image_image[col, row])
387  if self._mask_mask:
388  val = self._mask_mask[col, row]
389  if self._interpretMaskBits_interpretMaskBits:
390  msg += " [%s]" % self._mask_mask.interpret(val)
391  else:
392  msg += " 0x%x" % val
393 
394  return msg
395 
396  ax.format_coord = format_coord
397  # Stop images from reporting their value as we've already
398  # printed it nicely
399  for a in ax.get_images():
400  a.get_cursor_data = lambda ev: None # disabled
401 
402  # using tight_layout() is too tight and clips the axes
403  self._figure_figure.canvas.draw_idle()
404 
405  def _i_mtv(self, data, wcs, title, isMask):
406  """Internal routine to display an Image or Mask on a DS9 display"""
407 
408  title = str(title) if title else ""
409  dataArr = data.getArray()
410 
411  if isMask:
412  maskPlanes = data.getMaskPlaneDict()
413  nMaskPlanes = max(maskPlanes.values()) + 1
414 
415  planes = {} # build inverse dictionary
416  for key in maskPlanes:
417  planes[maskPlanes[key]] = key
418 
419  planeList = range(nMaskPlanes)
420 
421  maskArr = np.zeros_like(dataArr, dtype=np.int32)
422 
423  colorNames = ['black']
424  colorGenerator = self.display.maskColorGenerator(omitBW=True)
425  for p in planeList:
426  color = self.display.getMaskPlaneColor(planes[p]) if p in planes else None
427 
428  if not color: # none was specified
429  color = next(colorGenerator)
430  elif color.lower() == afwDisplay.IGNORE:
431  color = 'black' # we'll set alpha = 0 anyway
432 
433  colorNames.append(color)
434  #
435  # Convert those colours to RGBA so we can have per-mask-plane
436  # transparency and build a colour map
437  #
438  # Pixels equal to 0 don't get set (as no bits are set), so leave
439  # them transparent and start our colours at [1] --
440  # hence "i + 1" below
441  #
442  colors = mpColors.to_rgba_array(colorNames)
443  alphaChannel = 3 # the alpha channel; the A in RGBA
444  colors[0][alphaChannel] = 0.0 # it's black anyway
445  for i, p in enumerate(planeList):
446  if colorNames[i + 1] == 'black':
447  alpha = 0.0
448  else:
449  alpha = 1 - self._getMaskTransparency_getMaskTransparency(planes[p] if p in planes else None)
450 
451  colors[i + 1][alphaChannel] = alpha
452 
453  cmap = mpColors.ListedColormap(colors)
454  norm = mpColors.NoNorm()
455  else:
456  cmap = self._image_colormap_image_colormap
457  norm = self._normalize_normalize
458 
459  ax = self._figure_figure.gca()
460  bbox = data.getBBox()
461  extent = (bbox.getBeginX() - 0.5, bbox.getEndX() - 0.5,
462  bbox.getBeginY() - 0.5, bbox.getEndY() - 0.5)
463 
464  with pyplot.rc_context(dict(interactive=False)):
465  if isMask:
466  for i, p in reversed(list(enumerate(planeList))):
467  if colors[i + 1][alphaChannel] == 0: # colors[0] is reserved
468  continue
469 
470  bitIsSet = (dataArr & (1 << p)) != 0
471  if bitIsSet.sum() == 0:
472  continue
473 
474  maskArr[bitIsSet] = i + 1 # + 1 as we set colorNames[0] to black
475 
476  if not self._fastMaskDisplay_fastMaskDisplay: # we draw each bitplane separately
477  ax.imshow(maskArr, origin='lower', interpolation='nearest',
478  extent=extent, cmap=cmap, norm=norm)
479  maskArr[:] = 0
480 
481  if self._fastMaskDisplay_fastMaskDisplay: # we only draw the lowest bitplane
482  ax.imshow(maskArr, origin='lower', interpolation='nearest',
483  extent=extent, cmap=cmap, norm=norm)
484  else:
485  # If we're playing with subplots and have reset the axis
486  # the cached colorbar axis belongs to the old one, so set
487  # it to None
488  if self._mappable_ax_mappable_ax and self._mappable_ax_mappable_ax[1] != self._figure_figure.gca():
489  self._colorbar_ax_colorbar_ax = None
490 
491  mappable = ax.imshow(dataArr, origin='lower', interpolation='nearest',
492  extent=extent, cmap=cmap, norm=norm)
493  self._mappable_ax_mappable_ax = (mappable, ax)
494 
495  self._figure_figure.canvas.draw_idle()
496 
497  def _i_setImage(self, image, mask=None, wcs=None):
498  """Save the current image, mask, wcs, and XY0"""
499  self._image_image = image
500  self._mask_mask = mask
501  self._wcs_wcs = wcs
502  self._xy0_xy0 = self._image_image.getXY0() if self._image_image else (0, 0)
503 
504  self._zoomfac_zoomfac = None
505  if self._image_image is None:
506  self._width, self._height_height = 0, 0
507  else:
508  self._width, self._height_height = self._image_image.getDimensions()
509 
510  self._xcen_xcen = 0.5*self._width
511  self._ycen_ycen = 0.5*self._height_height
512 
513  def _setImageColormap(self, cmap):
514  """Set the colormap used for the image
515 
516  cmap should be either the name of an attribute of pyplot.cm or an
517  mpColors.Colormap (e.g. "gray" or pyplot.cm.gray)
518 
519  """
520  if not isinstance(cmap, mpColors.Colormap):
521  cmap = getattr(pyplot.cm, cmap)
522 
523  self._image_colormap_image_colormap = cmap
524 
525  #
526  # Graphics commands
527  #
528 
529  def _buffer(self, enable=True):
530  if enable:
531  pyplot.ioff()
532  else:
533  pyplot.ion()
534  self._figure_figure.show()
535 
536  def _flush(self):
537  pass
538 
539  def _erase(self):
540  """Erase the display"""
541 
542  for axis in self._figure.axes:
543  axis.lines = []
544  axis.texts = []
545 
546  self._figure.canvas.draw_idle()
547 
548  def _dot(self, symb, c, r, size, ctype,
549  fontFamily="helvetica", textAngle=None):
550  """Draw a symbol at (col,row) = (c,r) [0-based coordinates]
551  Possible values are:
552  + Draw a +
553  x Draw an x
554  * Draw a *
555  o Draw a circle
556  @:Mxx,Mxy,Myy Draw an ellipse with moments
557  (Mxx, Mxy, Myy) (argument size is ignored)
558  An afwGeom.ellipses.Axes Draw the ellipse (argument size is
559  ignored)
560 
561  Any other value is interpreted as a string to be drawn. Strings obey the
562  fontFamily (which may be extended with other characteristics, e.g.
563  "times bold italic". Text will be drawn rotated by textAngle
564  (textAngle is ignored otherwise).
565  """
566  if not ctype:
567  ctype = afwDisplay.GREEN
568 
569  axis = self._figure.gca()
570  x0, y0 = self._xy0
571 
572  if isinstance(symb, afwGeom.ellipses.Axes):
573  from matplotlib.patches import Ellipse
574 
575  # Following matplotlib.patches.Ellipse documentation 'width' and
576  # 'height' are diameters while 'angle' is rotation in degrees
577  # (anti-clockwise)
578  axis.add_artist(Ellipse((c + x0, r + y0), height=2*symb.getA(), width=2*symb.getB(),
579  angle=90.0 + math.degrees(symb.getTheta()),
580  edgecolor=mapCtype(ctype), facecolor='none'))
581  elif symb == 'o':
582  from matplotlib.patches import CirclePolygon as Circle
583 
584  axis.add_artist(Circle((c + x0, r + y0), radius=size, color=mapCtype(ctype), fill=False))
585  else:
586  from matplotlib.lines import Line2D
587 
588  for ds9Cmd in ds9Regions.dot(symb, c + x0, r + y0, size, fontFamily="helvetica", textAngle=None):
589  tmp = ds9Cmd.split('#')
590  cmd = tmp.pop(0).split()
591 
592  cmd, args = cmd[0], cmd[1:]
593 
594  if cmd == "line":
595  args = np.array(args).astype(float) - 1.0
596 
597  x = np.empty(len(args)//2)
598  y = np.empty_like(x)
599  i = np.arange(len(args), dtype=int)
600  x = args[i%2 == 0]
601  y = args[i%2 == 1]
602 
603  axis.add_line(Line2D(x, y, color=mapCtype(ctype)))
604  elif cmd == "text":
605  x, y = np.array(args[0:2]).astype(float) - 1.0
606  axis.text(x, y, symb, color=mapCtype(ctype),
607  horizontalalignment='center', verticalalignment='center')
608  else:
609  raise RuntimeError(ds9Cmd)
610 
611  def _drawLines(self, points, ctype):
612  """Connect the points, a list of (col,row)
613  Ctype is the name of a colour (e.g. 'red')"""
614 
615  from matplotlib.lines import Line2D
616 
617  if not ctype:
618  ctype = afwDisplay.GREEN
619 
620  points = np.array(points)
621  x = points[:, 0] + self._xy0[0]
622  y = points[:, 1] + self._xy0[1]
623 
624  self._figure.gca().add_line(Line2D(x, y, color=mapCtype(ctype)))
625 
626  def _scale(self, algorithm, minval, maxval, unit, *args, **kwargs):
627  """
628  Set gray scale
629 
630  N.b. Supports extra arguments:
631  @param maskedPixels List of names of mask bits to ignore
632  E.g. ["BAD", "INTERP"].
633  A single name is also supported
634  """
635  self._scaleArgs['algorithm'] = algorithm
636  self._scaleArgs['minval'] = minval
637  self._scaleArgs['maxval'] = maxval
638  self._scaleArgs['unit'] = unit
639  self._scaleArgs['args'] = args
640  self._scaleArgs['kwargs'] = kwargs
641 
642  try:
643  self._i_scale(algorithm, minval, maxval, unit, *args, **kwargs)
644  except (AttributeError, RuntimeError):
645  # Unable to access self._image; we'll try again when we run mtv
646  pass
647 
648  def _i_scale(self, algorithm, minval, maxval, unit, *args, **kwargs):
649 
650  maskedPixels = kwargs.get("maskedPixels", [])
651  if isinstance(maskedPixels, str):
652  maskedPixels = [maskedPixels]
653  bitmask = afwImage.Mask.getPlaneBitMask(maskedPixels)
654 
655  sctrl = afwMath.StatisticsControl()
656  sctrl.setAndMask(bitmask)
657 
658  if minval == "minmax":
659  if self._image is None:
660  raise RuntimeError("You may only use minmax if an image is loaded into the display")
661 
662  mi = afwImage.makeMaskedImage(self._image, self._mask)
663  stats = afwMath.makeStatistics(mi, afwMath.MIN | afwMath.MAX, sctrl)
664  minval = stats.getValue(afwMath.MIN)
665  maxval = stats.getValue(afwMath.MAX)
666  elif minval == "zscale":
667  if bitmask:
668  print("scale(..., 'zscale', maskedPixels=...) is not yet implemented")
669 
670  if algorithm is None:
671  self._normalize = None
672  elif algorithm == "asinh":
673  if minval == "zscale":
674  if self._image is None:
675  raise RuntimeError("You may only use zscale if an image is loaded into the display")
676 
677  self._normalize = AsinhZScaleNormalize(image=self._image, Q=kwargs.get("Q", 8.0))
678  else:
679  self._normalize = AsinhNormalize(minimum=minval,
680  dataRange=maxval - minval, Q=kwargs.get("Q", 8.0))
681  elif algorithm == "linear":
682  if minval == "zscale":
683  if self._image is None:
684  raise RuntimeError("You may only use zscale if an image is loaded into the display")
685 
686  self._normalize = ZScaleNormalize(image=self._image,
687  nSamples=kwargs.get("nSamples", 1000),
688  contrast=kwargs.get("contrast", 0.25))
689  else:
690  self._normalize = LinearNormalize(minimum=minval, maximum=maxval)
691  else:
692  raise RuntimeError("Unsupported stretch algorithm \"%s\"" % algorithm)
693  #
694  # Zoom and Pan
695  #
696 
697  def _zoom(self, zoomfac):
698  """Zoom by specified amount"""
699 
700  self._zoomfac = zoomfac
701 
702  if zoomfac is None:
703  return
704 
705  x0, y0 = self._xy0
706 
707  size = min(self._width, self._height)
708  if size < self._zoomfac: # avoid min == max
709  size = self._zoomfac
710  xmin, xmax = self._xcen + x0 + size/self._zoomfac*np.array([-1, 1])
711  ymin, ymax = self._ycen + y0 + size/self._zoomfac*np.array([-1, 1])
712 
713  ax = self._figure.gca()
714 
715  tb = self._figure.canvas.toolbar
716  if tb is not None: # It's None for e.g. %matplotlib inline in jupyter
717  tb.push_current() # save the current zoom in the view stack
718 
719  ax.set_xlim(xmin, xmax)
720  ax.set_ylim(ymin, ymax)
721  ax.set_aspect('equal', 'datalim')
722 
723  self._figure.canvas.draw_idle()
724 
725  def _pan(self, colc, rowc):
726  """Pan to (colc, rowc)"""
727 
728  self._xcen = colc
729  self._ycen = rowc
730 
731  self._zoom(self._zoomfac)
732 
733  def _getEvent(self, timeout=-1):
734  """Listen for a key press, returning (key, x, y)"""
735 
736  if timeout < 0:
737  timeout = 24*3600 # -1 generates complaints in QTimer::singleShot. A day is a long time
738 
739  mpBackend = matplotlib.get_backend()
740  if mpBackend not in interactiveBackends:
741  print("The %s matplotlib backend doesn't support display._getEvent()" %
742  (matplotlib.get_backend(),), file=sys.stderr)
743  return interface.Event('q')
744 
745  blocking_input = BlockingKeyInput(self._figure)
746  return blocking_input(timeout=timeout)
747 
748 # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
749 
750 
751 class BlockingKeyInput(BlockingInput):
752  """
753  Callable class to retrieve a single keyboard click
754  """
755  def __init__(self, fig):
756  """Create a BlockingKeyInput
757 
758  @param fig The figure to monitor for keyboard events
759  """
760  BlockingInput.__init__(self, fig=fig, eventslist=('key_press_event',))
761 
762  def post_event(self):
763  """
764  Return the event containing the key and (x, y)
765  """
766  try:
767  event = self.events[-1]
768  except IndexError:
769  # details of the event to pass back to the display
770  self.evev = None
771  else:
772  self.evev = interface.Event(event.key, event.xdata, event.ydata)
773 
774  def __call__(self, timeout=-1):
775  """
776  Blocking call to retrieve a single key click
777  Returns key or None if timeout (-1: never timeout)
778  """
779  self.evev = None
780 
781  BlockingInput.__call__(self, n=1, timeout=timeout)
782 
783  return self.evev
784 
785 # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
786 
787 
788 class Normalize(mpColors.Normalize):
789  """Class to support stretches for mtv()"""
790 
791  def __call__(self, value, clip=None):
792  """
793  Return a MaskedArray with value mapped to [0, 255]
794 
795  @param value Input pixel value or array to be mapped
796  """
797  if isinstance(value, np.ndarray):
798  data = value
799  else:
800  data = value.data
801 
802  data = data - self.mapping.minimum[0]
803  return ma.array(data*self.mapping.mapIntensityToUint8(data)/255.0)
804 
805 
807  """Provide an asinh stretch for mtv()"""
808  def __init__(self, minimum=0, dataRange=1, Q=8):
809  """Initialise an object able to carry out an asinh mapping
810 
811  @param minimum Minimum pixel value (default: 0)
812  @param dataRange Range of values for stretch if Q=0; roughly the
813  linear part (default: 1)
814  @param Q Softening parameter (default: 8)
815 
816  See Lupton et al., PASP 116, 133
817  """
818  # The object used to perform the desired mapping
819  self.mappingmapping = afwRgb.AsinhMapping(minimum, dataRange, Q)
820 
821  vmin, vmax = self._getMinMaxQ_getMinMaxQ()[0:2]
822  if vmax*Q > vmin:
823  vmax *= Q
824  super().__init__(vmin, vmax)
825 
826  def _getMinMaxQ(self):
827  """Return an asinh mapping's minimum and maximum value, and Q
828 
829  Regrettably this information is not preserved by AsinhMapping
830  so we have to reverse engineer it
831  """
832 
833  frac = 0.1 # magic number in AsinhMapping
834  Q = np.sinh((frac*self.mappingmapping._uint8Max)/self.mappingmapping._slope)/frac
835  dataRange = Q/self.mappingmapping._soften
836 
837  vmin = self.mappingmapping.minimum[0]
838  return vmin, vmin + dataRange, Q
839 
840 
842  """Provide an asinh stretch using zscale to set limits for mtv()"""
843  def __init__(self, image=None, Q=8):
844  """Initialise an object able to carry out an asinh mapping
845 
846  @param image image to use estimate minimum and dataRange using zscale
847  (see AsinhNormalize)
848  @param Q Softening parameter (default: 8)
849 
850  See Lupton et al., PASP 116, 133
851  """
852 
853  # The object used to perform the desired mapping
854  self.mappingmappingmapping = afwRgb.AsinhZScaleMapping(image, Q)
855 
856  vmin, vmax = self._getMinMaxQ_getMinMaxQ()[0:2]
857  # n.b. super() would call AsinhNormalize,
858  # and I want to pass min/max to the baseclass
859  Normalize.__init__(self, vmin, vmax)
860 
861 
863  """Provide a zscale stretch for mtv()"""
864  def __init__(self, image=None, nSamples=1000, contrast=0.25):
865  """Initialise an object able to carry out a zscale mapping
866 
867  @param image to be used to estimate the stretch
868  @param nSamples Number of data points to use (default: 1000)
869  @param contrast Control the range of pixels to display around the
870  median (default: 0.25)
871  """
872 
873  # The object used to perform the desired mapping
874  self.mappingmapping = afwRgb.ZScaleMapping(image, nSamples, contrast)
875 
876  super().__init__(self.mappingmapping.minimum[0], self.mappingmapping.maximum)
877 
878 
880  """Provide a linear stretch for mtv()"""
881  def __init__(self, minimum=0, maximum=1):
882  """Initialise an object able to carry out a linear mapping
883 
884  @param minimum Minimum value to display
885  @param maximum Maximum value to display
886  """
887  # The object used to perform the desired mapping
888  self.mappingmapping = afwRgb.LinearMapping(minimum, maximum)
889 
890  super().__init__(self.mappingmapping.minimum[0], self.mappingmapping.maximum)
int min
int max
Pass parameters to a Statistics object.
Definition: Statistics.h:92
def __init__(self, minimum=0, dataRange=1, Q=8)
Definition: matplotlib.py:808
def wait(self, prompt="[c(ontinue) p(db)] :", allowPdb=True)
Definition: matplotlib.py:286
def _i_scale(self, algorithm, minval, maxval, unit, *args, **kwargs)
Definition: matplotlib.py:648
def useSexagesimal(self, useSexagesimal)
Definition: matplotlib.py:272
def _getMaskTransparency(self, maskplane=None)
Definition: matplotlib.py:319
def _i_mtv(self, data, wcs, title, isMask)
Definition: matplotlib.py:405
def __init__(self, display, verbose=False, interpretMaskBits=True, mtvOrigin=afwImage.PARENT, fastMaskDisplay=False, reopenPlot=False, useSexagesimal=False, dpi=None, *args, **kwargs)
Definition: matplotlib.py:102
def show_colorbar(self, show=True, where="right", axSize="5%", axPad=None, **kwargs)
Definition: matplotlib.py:217
def _i_setImage(self, image, mask=None, wcs=None)
Definition: matplotlib.py:497
def __init__(self, minimum=0, maximum=1)
Definition: matplotlib.py:881
def __call__(self, value, clip=None)
Definition: matplotlib.py:791
def __init__(self, image=None, nSamples=1000, contrast=0.25)
Definition: matplotlib.py:864
daf::base::PropertyList * list
Definition: fits.cc:913
def show(frame=None)
Definition: ds9.py:88
def getMaskPlaneColor(name, frame=None)
Definition: ds9.py:76
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.
MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > * makeMaskedImage(typename std::shared_ptr< Image< ImagePixelT >> image, typename std::shared_ptr< Mask< MaskPixelT >> mask=Mask< MaskPixelT >(), typename std::shared_ptr< Image< VariancePixelT >> variance=Image< VariancePixelT >())
A function to return a MaskedImage of the correct type (cf.
Definition: MaskedImage.h:1240
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)
Definition: Statistics.h:359