LSST Applications g070148d5b3+33e5256705,g0d53e28543+25c8b88941,g0da5cf3356+2dd1178308,g1081da9e2a+62d12e78cb,g17e5ecfddb+7e422d6136,g1c76d35bf8+ede3a706f7,g295839609d+225697d880,g2e2c1a68ba+cc1f6f037e,g2ffcdf413f+853cd4dcde,g38293774b4+62d12e78cb,g3b44f30a73+d953f1ac34,g48ccf36440+885b902d19,g4b2f1765b6+7dedbde6d2,g5320a0a9f6+0c5d6105b6,g56b687f8c9+ede3a706f7,g5c4744a4d9+ef6ac23297,g5ffd174ac0+0c5d6105b6,g6075d09f38+66af417445,g667d525e37+2ced63db88,g670421136f+2ced63db88,g71f27ac40c+2ced63db88,g774830318a+463cbe8d1f,g7876bc68e5+1d137996f1,g7985c39107+62d12e78cb,g7fdac2220c+0fd8241c05,g96f01af41f+368e6903a7,g9ca82378b8+2ced63db88,g9d27549199+ef6ac23297,gabe93b2c52+e3573e3735,gb065e2a02a+3dfbe639da,gbc3249ced9+0c5d6105b6,gbec6a3398f+0c5d6105b6,gc9534b9d65+35b9f25267,gd01420fc67+0c5d6105b6,geee7ff78d7+a14128c129,gf63283c776+ede3a706f7,gfed783d017+0c5d6105b6,w.2022.47
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
31import warnings
32
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
38
39import numpy as np
40import numpy.ma as ma
41
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
49
50import lsst.afw.geom as afwGeom
51import lsst.geom as geom
52
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 ]
64
65try:
66 matplotlibCtypes
67except 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
82class 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
145 else:
146 self._figure = pyplot.figure(display.frame, dpi=dpi)
147 self._figure.clf()
148
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)
176
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
183
184 def _show(self):
185 """Put the plot at the top of the window stacking order"""
186
187 try:
188 self._figure.canvas._tkcanvas._root().lift() # tk
189 except AttributeError:
190 pass
191
192 try:
193 self._figure.canvas.manager.window.raise_() # os/x
194 except AttributeError:
195 pass
196
197 try:
198 self._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.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:
244 if self._colorbar_ax is None:
245 orientationDict = dict(right="vertical", bottom="horizontal")
246
247 mappable, ax = self._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 = divider.append_axes(where, size=axSize, pad=axPad)
260
261 self._figure.colorbar(mappable, cax=self._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 is not None:
269 self._colorbar_ax.remove()
270 self._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[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[maskplane] = 0.01*transparency
318
319 def _getMaskTransparency(self, maskplane=None):
320 """Return the current mask transparency"""
321 return self._maskTransparency[maskplane if maskplane in self._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(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.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'])
339
340 ax = self._figure.gca()
341 ax.cla()
342
343 self._i_mtv(image, wcs, title, False)
344
345 if mask:
346 self._i_mtv(mask, wcs, title, True)
347
348 self.show_colorbar()
349
350 if title:
351 ax.set_title(title)
352
353 self._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):
358
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)
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, self.__delta, ra, dec)
385
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
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.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(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
457 norm = self._normalize
458
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)
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: # 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: # 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
490
491 mappable = ax.imshow(dataArr, origin='lower', interpolation='nearest',
492 extent=extent, cmap=cmap, norm=norm)
493 self._mappable_ax = (mappable, ax)
494
495 self._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
500 self._mask = mask
501 self._wcs = wcs
502 self._xy0 = self._image.getXY0() if self._image else (0, 0)
503
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()
509
510 self._xcen = 0.5*self._width
511 self._ycen = 0.5*self._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 = 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.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
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
751class 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.ev = None
771 else:
772 self.ev = 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.ev = None
780
781 BlockingInput.__call__(self, n=1, timeout=timeout)
782
783 return self.ev
784
785# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
786
787
788class 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.mapping = afwRgb.AsinhMapping(minimum, dataRange, Q)
820
821 vmin, vmax = self._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.mapping._uint8Max)/self.mapping._slope)/frac
835 dataRange = Q/self.mapping._soften
836
837 vmin = self.mapping.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.mappingmapping = afwRgb.AsinhZScaleMapping(image, Q)
855
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)
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.mapping = afwRgb.ZScaleMapping(image, nSamples, contrast)
875
876 super().__init__(self.mapping.minimum[0], self.mapping.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.mapping = afwRgb.LinearMapping(minimum, maximum)
889
890 super().__init__(self.mapping.minimum[0], self.mapping.maximum)
int min
int max
table::Key< int > to
table::Key< int > a
Pass parameters to a Statistics object.
Definition: Statistics.h:83
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:928
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:1241
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:361