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