Coverage for python/lsst/display/matplotlib/matplotlib.py: 13%
397 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-25 00:05 -0700
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-25 00:05 -0700
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#
23#
24# \file
25# \brief Definitions to talk to matplotlib from python using the "afwDisplay"
26# interface
28import math
29import sys
30import unicodedata
31import warnings
33import matplotlib.pyplot as pyplot
34import matplotlib.cbook
35import matplotlib.colors as mpColors
36from matplotlib.blocking_input import BlockingInput
37from mpl_toolkits.axes_grid1 import make_axes_locatable
39import numpy as np
40import numpy.ma as ma
42import lsst.afw.display as afwDisplay
43import lsst.afw.math as afwMath
44import lsst.afw.display.rgb as afwRgb
45import lsst.afw.display.interface as interface
46import lsst.afw.display.virtualDevice as virtualDevice
47import lsst.afw.display.ds9Regions as ds9Regions
48import lsst.afw.image as afwImage
50import lsst.afw.geom as afwGeom
51import lsst.geom as geom
53#
54# Set the list of backends which support _getEvent and thus interact()
55#
56try:
57 interactiveBackends
58except NameError:
59 # List of backends that support `interact`
60 interactiveBackends = [
61 "Qt4Agg",
62 "Qt5Agg",
63 ]
65try:
66 matplotlibCtypes
67except NameError:
68 matplotlibCtypes = {
69 afwDisplay.GREEN: "#00FF00",
70 }
72 def mapCtype(ctype):
73 """Map the ctype to a potentially different ctype
75 Specifically, if matplotlibCtypes[ctype] exists, use it instead
77 This is used e.g. to map "green" to a brighter shade
78 """
79 return matplotlibCtypes[ctype] if ctype in matplotlibCtypes else ctype
82class DisplayImpl(virtualDevice.DisplayImpl):
83 """Provide a matplotlib backend for afwDisplay
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
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
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)
122 The `frame` argument to `Display` may be a matplotlib figure; this
123 permits code such as
124 fig, axes = plt.subplots(1, 2)
126 disp = afwDisplay.Display(fig)
127 disp.scale('asinh', 'zscale', Q=0.5)
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
138 virtualDevice.DisplayImpl.__init__(self, display, verbose)
140 if reopenPlot:
141 pyplot.close(display.frame)
143 if figure is not None:
144 self._figure = figure
145 else:
146 self._figure = pyplot.figure(display.frame, dpi=dpi)
147 self._figure.clf()
149 self._display = display
150 self._maskTransparency = {None: 0.7}
151 self._interpretMaskBits = interpretMaskBits # interpret mask bits in mtv
152 self._fastMaskDisplay = fastMaskDisplay
153 self._useSexagesimal = [useSexagesimal] # use an array so we can modify the value in format_coord
154 self._mtvOrigin = mtvOrigin
155 self._mappable_ax = None
156 self._colorbar_ax = None
157 self._image_colormap = pyplot.cm.gray
158 #
159 self.__alpha = unicodedata.lookup("GREEK SMALL LETTER alpha") # used in cursor display string
160 self.__delta = unicodedata.lookup("GREEK SMALL LETTER delta") # used in cursor display string
161 #
162 # Support self._scale()
163 #
164 self._scaleArgs = dict()
165 self._normalize = None
166 #
167 # Support self._erase(), reporting pixel/mask values, and
168 # zscale/minmax; set in mtv
169 #
170 self._i_setImage(None)
171 #
172 # Ignore warnings due to BlockingKeyInput
173 #
174 if not verbose:
175 warnings.filterwarnings("ignore", category=matplotlib.cbook.mplDeprecation)
177 def _close(self):
178 """!Close the display, cleaning up any allocated resources"""
179 self._image = None
180 self._mask = None
181 self._wcs = None
182 self._figure.gca().format_coord = lambda x, y: None # keeps a copy of _wcs
184 def _show(self):
185 """Put the plot at the top of the window stacking order"""
187 try:
188 self._figure.canvas._tkcanvas._root().lift() # tk
189 except AttributeError:
190 pass
192 try:
193 self._figure.canvas.manager.window.raise_() # os/x
194 except AttributeError:
195 pass
197 try:
198 self._figure.canvas.raise_() # qt[45]
199 except AttributeError:
200 pass
202 #
203 # Extensions to the API
204 #
205 def savefig(self, *args, **kwargs):
206 """Defer to figure.savefig()
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.savefig(*args, **kwargs)
217 def show_colorbar(self, show=True, where="right", axSize="5%", axPad=None, **kwargs):
218 """Show (or hide) the colour bar
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()
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.
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:
244 if self._colorbar_ax is None:
245 orientationDict = dict(right="vertical", bottom="horizontal")
247 mappable, ax = self._mappable_ax
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())}")
255 if axPad is None:
256 axPad = 0.1 if orientation == "vertical" else 0.3
258 divider = make_axes_locatable(ax)
259 self._colorbar_ax = divider.append_axes(where, size=axSize, pad=axPad)
261 self._figure.colorbar(mappable, cax=self._colorbar_ax, orientation=orientation, **kwargs)
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 is not None:
269 self._colorbar_ax.remove()
270 self._colorbar_ax = None
272 def useSexagesimal(self, useSexagesimal):
273 """Control the formatting coordinates as HH:MM:SS.ss
275 Parameters
276 ----------
277 useSexagesimal : `bool`
278 Print coordinates as e.g. HH:MM:SS.ss iff True
280 N.b. can also be set in Display's ctor
281 """
283 """Are we formatting coordinates as HH:MM:SS.ss?"""
284 self._useSexagesimal[0] = useSexagesimal
286 def wait(self, prompt="[c(ontinue) p(db)] :", allowPdb=True):
287 """Wait for keyboard input
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
296 Returns the string you entered
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
309 return s
310 #
311 # Defined API
312 #
314 def _setMaskTransparency(self, transparency, maskplane):
315 """Specify mask transparency (percent)"""
317 self._maskTransparency[maskplane] = 0.01*transparency
319 def _getMaskTransparency(self, maskplane=None):
320 """Return the current mask transparency"""
321 return self._maskTransparency[maskplane if maskplane in self._maskTransparency else None]
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 ""
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(image, mask, wcs)
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.get('algorithm'): # someone called self.scale()
337 self._i_scale(self._scaleArgs['algorithm'], self._scaleArgs['minval'], self._scaleArgs['maxval'],
338 self._scaleArgs['unit'], *self._scaleArgs['args'], **self._scaleArgs['kwargs'])
340 ax = self._figure.gca()
341 ax.cla()
343 self._i_mtv(image, wcs, title, False)
345 if mask:
346 self._i_mtv(mask, wcs, title, True)
348 self.show_colorbar()
350 if title:
351 ax.set_title(title)
353 self._title = title
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):
359 fmt = '(%1.2f, %1.2f)'
360 if self._mtvOrigin == afwImage.PARENT:
361 msg = fmt % (x, y)
362 else:
363 msg = (fmt + "L") % (x - x0, y - y0)
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()
373 if _useSexagesimal[0]:
374 from astropy import units as u
375 from astropy.coordinates import Angle as apAngle
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
384 msg += r" (%s, %s): (%s, %s)" % (self.__alpha, self.__delta, ra, dec)
386 msg += ' %1.3f' % (self._image[col, row])
387 if self._mask:
388 val = self._mask[col, row]
389 if self._interpretMaskBits:
390 msg += " [%s]" % self._mask.interpret(val)
391 else:
392 msg += " 0x%x" % val
394 return msg
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
402 # using tight_layout() is too tight and clips the axes
403 self._figure.canvas.draw_idle()
405 def _i_mtv(self, data, wcs, title, isMask):
406 """Internal routine to display an Image or Mask on a DS9 display"""
408 title = str(title) if title else ""
409 dataArr = data.getArray()
411 if isMask:
412 maskPlanes = data.getMaskPlaneDict()
413 nMaskPlanes = max(maskPlanes.values()) + 1
415 planes = {} # build inverse dictionary
416 for key in maskPlanes:
417 planes[maskPlanes[key]] = key
419 planeList = range(nMaskPlanes)
421 maskArr = np.zeros_like(dataArr, dtype=np.int32)
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
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
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(planes[p] if p in planes else None)
451 colors[i + 1][alphaChannel] = alpha
453 cmap = mpColors.ListedColormap(colors)
454 norm = mpColors.NoNorm()
455 else:
456 cmap = self._image_colormap
457 norm = self._normalize
459 ax = self._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)
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
470 bitIsSet = (dataArr & (1 << p)) != 0
471 if bitIsSet.sum() == 0:
472 continue
474 maskArr[bitIsSet] = i + 1 # + 1 as we set colorNames[0] to black
476 if not self._fastMaskDisplay: # we draw each bitplane separately
477 ax.imshow(maskArr, origin='lower', interpolation='nearest',
478 extent=extent, cmap=cmap, norm=norm)
479 maskArr[:] = 0
481 if self._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 and self._mappable_ax[1] != self._figure.gca():
489 self._colorbar_ax = None
491 mappable = ax.imshow(dataArr, origin='lower', interpolation='nearest',
492 extent=extent, cmap=cmap, norm=norm)
493 self._mappable_ax = (mappable, ax)
495 self._figure.canvas.draw_idle()
497 def _i_setImage(self, image, mask=None, wcs=None):
498 """Save the current image, mask, wcs, and XY0"""
499 self._image = image
500 self._mask = mask
501 self._wcs = wcs
502 self._xy0 = self._image.getXY0() if self._image else (0, 0)
504 self._zoomfac = None
505 if self._image is None:
506 self._width, self._height = 0, 0
507 else:
508 self._width, self._height = self._image.getDimensions()
510 self._xcen = 0.5*self._width
511 self._ycen = 0.5*self._height
513 def _setImageColormap(self, cmap):
514 """Set the colormap used for the image
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)
519 """
520 if not isinstance(cmap, mpColors.Colormap):
521 cmap = getattr(pyplot.cm, cmap)
523 self._image_colormap = cmap
525 #
526 # Graphics commands
527 #
529 def _buffer(self, enable=True):
530 if enable:
531 pyplot.ioff()
532 else:
533 pyplot.ion()
534 self._figure.show()
536 def _flush(self):
537 pass
539 def _erase(self):
540 """Erase the display"""
542 for axis in self._figure.axes:
543 axis.lines = []
544 axis.texts = []
546 self._figure.canvas.draw_idle()
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)
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
569 axis = self._figure.gca()
570 x0, y0 = self._xy0
572 if isinstance(symb, afwGeom.ellipses.Axes):
573 from matplotlib.patches import Ellipse
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
584 axis.add_artist(Circle((c + x0, r + y0), radius=size, color=mapCtype(ctype), fill=False))
585 else:
586 from matplotlib.lines import Line2D
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()
592 cmd, args = cmd[0], cmd[1:]
594 if cmd == "line":
595 args = np.array(args).astype(float) - 1.0
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]
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)
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')"""
615 from matplotlib.lines import Line2D
617 if not ctype:
618 ctype = afwDisplay.GREEN
620 points = np.array(points)
621 x = points[:, 0] + self._xy0[0]
622 y = points[:, 1] + self._xy0[1]
624 self._figure.gca().add_line(Line2D(x, y, color=mapCtype(ctype)))
626 def _scale(self, algorithm, minval, maxval, unit, *args, **kwargs):
627 """
628 Set gray scale
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
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
648 def _i_scale(self, algorithm, minval, maxval, unit, *args, **kwargs):
650 maskedPixels = kwargs.get("maskedPixels", [])
651 if isinstance(maskedPixels, str):
652 maskedPixels = [maskedPixels]
653 bitmask = afwImage.Mask.getPlaneBitMask(maskedPixels)
655 sctrl = afwMath.StatisticsControl()
656 sctrl.setAndMask(bitmask)
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")
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")
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")
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")
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 #
697 def _zoom(self, zoomfac):
698 """Zoom by specified amount"""
700 self._zoomfac = zoomfac
702 if zoomfac is None:
703 return
705 x0, y0 = self._xy0
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])
713 ax = self._figure.gca()
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
719 ax.set_xlim(xmin, xmax)
720 ax.set_ylim(ymin, ymax)
721 ax.set_aspect('equal', 'datalim')
723 self._figure.canvas.draw_idle()
725 def _pan(self, colc, rowc):
726 """Pan to (colc, rowc)"""
728 self._xcen = colc
729 self._ycen = rowc
731 self._zoom(self._zoomfac)
733 def _getEvent(self, timeout=-1):
734 """Listen for a key press, returning (key, x, y)"""
736 if timeout < 0:
737 timeout = 24*3600 # -1 generates complaints in QTimer::singleShot. A day is a long time
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')
745 blocking_input = BlockingKeyInput(self._figure)
746 return blocking_input(timeout=timeout)
748# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
751class BlockingKeyInput(BlockingInput):
752 """
753 Callable class to retrieve a single keyboard click
754 """
755 def __init__(self, fig):
756 """Create a BlockingKeyInput
758 @param fig The figure to monitor for keyboard events
759 """
760 BlockingInput.__init__(self, fig=fig, eventslist=('key_press_event',))
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.ev = None
771 else:
772 self.ev = interface.Event(event.key, event.xdata, event.ydata)
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.ev = None
781 BlockingInput.__call__(self, n=1, timeout=timeout)
783 return self.ev
785# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
788class Normalize(mpColors.Normalize):
789 """Class to support stretches for mtv()"""
791 def __call__(self, value, clip=None):
792 """
793 Return a MaskedArray with value mapped to [0, 255]
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
802 data = data - self.mapping.minimum[0]
803 return ma.array(data*self.mapping.mapIntensityToUint8(data)/255.0)
806class AsinhNormalize(Normalize):
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
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)
816 See Lupton et al., PASP 116, 133
817 """
818 # The object used to perform the desired mapping
819 self.mapping = afwRgb.AsinhMapping(minimum, dataRange, Q)
821 vmin, vmax = self._getMinMaxQ()[0:2]
822 if vmax*Q > vmin:
823 vmax *= Q
824 super().__init__(vmin, vmax)
826 def _getMinMaxQ(self):
827 """Return an asinh mapping's minimum and maximum value, and Q
829 Regrettably this information is not preserved by AsinhMapping
830 so we have to reverse engineer it
831 """
833 frac = 0.1 # magic number in AsinhMapping
834 Q = np.sinh((frac*self.mapping._uint8Max)/self.mapping._slope)/frac
835 dataRange = Q/self.mapping._soften
837 vmin = self.mapping.minimum[0]
838 return vmin, vmin + dataRange, Q
841class AsinhZScaleNormalize(AsinhNormalize):
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
846 @param image image to use estimate minimum and dataRange using zscale
847 (see AsinhNormalize)
848 @param Q Softening parameter (default: 8)
850 See Lupton et al., PASP 116, 133
851 """
853 # The object used to perform the desired mapping
854 self.mapping = afwRgb.AsinhZScaleMapping(image, Q)
856 vmin, vmax = self._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)
862class ZScaleNormalize(Normalize):
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
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 """
873 # The object used to perform the desired mapping
874 self.mapping = afwRgb.ZScaleMapping(image, nSamples, contrast)
876 super().__init__(self.mapping.minimum[0], self.mapping.maximum)
879class LinearNormalize(Normalize):
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
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.mapping = afwRgb.LinearMapping(minimum, maximum)
890 super().__init__(self.mapping.minimum[0], self.mapping.maximum)