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
60 interactiveBackends = [
69 afwDisplay.GREEN:
"#00FF00",
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
79 return matplotlibCtypes[ctype]
if ctype
in matplotlibCtypes
else ctype
83 """Provide a matplotlib backend for afwDisplay
85 Recommended backends in notebooks are:
97 Apparently only qt supports Display.interact(); the list of interactive
98 backends
is given by lsst.display.matplotlib.interactiveBackends
101 interpretMaskBits=True, mtvOrigin=afwImage.PARENT, fastMaskDisplay=False,
102 reopenPlot=False, useSexagesimal=False, dpi=None, *args, **kwargs):
104 Initialise a matplotlib display
106 @param fastMaskDisplay If
True only show the first bitplane that
's
108 (e.g.
if (SATURATED & 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
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):
133 if hasattr(display.frame,
"number"):
134 figure = display.frame
138 virtualDevice.DisplayImpl.__init__(self, display, verbose)
141 pyplot.close(display.frame)
143 if figure
is not None:
146 self.
_figure = pyplot.figure(display.frame, dpi=dpi)
159 self.
__alpha = unicodedata.lookup(
"GREEK SMALL LETTER alpha")
160 self.
__delta = unicodedata.lookup(
"GREEK SMALL LETTER delta")
175 warnings.filterwarnings(
"ignore", category=matplotlib.cbook.mplDeprecation)
178 """!Close the display, cleaning up any allocated resources"""
182 self.
_figure.gca().format_coord =
lambda x, y:
None
185 """Put the plot at the top of the window stacking order"""
188 self.
_figure.canvas._tkcanvas._root().lift()
189 except AttributeError:
193 self.
_figure.canvas.manager.window.raise_()
194 except AttributeError:
199 except AttributeError:
206 """Defer to figure.savefig()
211 Passed through to figure.savefig()
213 Passed through to figure.savefig()
217 def show_colorbar(self, show=True, where="right", axSize="5%", axPad=None, **kwargs):
218 """Show (or hide) the colour bar
223 Should I show the colour bar?
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
231 Passed through to colorbar()
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.
245 orientationDict = dict(right=
"vertical", bottom=
"horizontal")
249 if where
in orientationDict:
250 orientation = orientationDict[where]
252 print(f
"Unknown location {where}; "
253 f
"please use one of {', '.join(orientationDict.keys())}")
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)
273 """Control the formatting coordinates as HH:MM:SS.ss
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
283 """Are we formatting coordinates as HH:MM:SS.ss?"""
286 def wait(self, prompt="[c(ontinue) p(db)] :
", allowPdb=True):
287 """Wait for keyboard input
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
304 if allowPdb
and s
in (
"p",
"pdb"):
314 def _setMaskTransparency(self, transparency, maskplane):
315 """Specify mask transparency (percent)"""
319 def _getMaskTransparency(self, maskplane=None):
320 """Return the current mask transparency"""
323 def _mtv(self, image, mask=None, wcs=None, title=""):
324 """Display an Image and/or Mask on a matplotlib display
326 title = str(title) if title
else ""
343 self.
_i_mtv(image, wcs, title,
False)
346 self.
_i_mtv(mask, wcs, title,
True)
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),
359 fmt =
'(%1.2f, %1.2f)'
363 msg = (fmt +
"L") % (x - x0, y - y0)
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)
384 msg +=
r" (%s, %s): (%s, %s)" % (self.
__alpha, self.
__delta, ra, dec)
386 msg +=
' %1.3f' % (self.
_image[col, row])
388 val = self.
_mask[col, row]
390 msg +=
" [%s]" % self.
_mask.interpret(val)
396 ax.format_coord = format_coord
399 for a
in ax.get_images():
400 a.get_cursor_data =
lambda ev:
None
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()
412 maskPlanes = data.getMaskPlaneDict()
413 nMaskPlanes =
max(maskPlanes.values()) + 1
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)
426 color = self.display.getMaskPlaneColor(planes[p])
if p
in planes
else None
429 color = next(colorGenerator)
430 elif color.lower() == afwDisplay.IGNORE:
433 colorNames.append(color)
442 colors = mpColors.to_rgba_array(colorNames)
444 colors[0][alphaChannel] = 0.0
445 for i, p
in enumerate(planeList):
446 if colorNames[i + 1] ==
'black':
451 colors[i + 1][alphaChannel] = alpha
453 cmap = mpColors.ListedColormap(colors)
454 norm = mpColors.NoNorm()
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)):
466 for i, p
in reversed(
list(enumerate(planeList))):
467 if colors[i + 1][alphaChannel] == 0:
470 bitIsSet = (dataArr & (1 << p)) != 0
471 if bitIsSet.sum() == 0:
474 maskArr[bitIsSet] = i + 1
477 ax.imshow(maskArr, origin=
'lower', interpolation=
'nearest',
478 extent=extent, cmap=cmap, norm=norm)
482 ax.imshow(maskArr, origin=
'lower', interpolation=
'nearest',
483 extent=extent, cmap=cmap, norm=norm)
491 mappable = ax.imshow(dataArr, origin=
'lower', interpolation=
'nearest',
492 extent=extent, cmap=cmap, norm=norm)
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"""
506 self._width, self.
_height = 0, 0
510 self.
_xcen = 0.5*self._width
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)
520 if not isinstance(cmap, mpColors.Colormap):
521 cmap = getattr(pyplot.cm, cmap)
529 def _buffer(self, enable=True):
540 """Erase the display"""
542 for axis
in self._figure.axes:
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]
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
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).
567 ctype = afwDisplay.GREEN
569 axis = self._figure.gca()
572 if isinstance(symb, afwGeom.ellipses.Axes):
573 from matplotlib.patches
import Ellipse
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'))
582 from matplotlib.patches
import CirclePolygon
as Circle
584 axis.add_artist(Circle((c + x0, r + y0), radius=size, color=
mapCtype(ctype), fill=
False))
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:]
595 args = np.array(args).astype(float) - 1.0
597 x = np.empty(len(args)//2)
599 i = np.arange(len(args), dtype=int)
603 axis.add_line(Line2D(x, y, color=
mapCtype(ctype)))
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')
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
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):
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
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
643 self._i_scale(algorithm, minval, maxval, unit, *args, **kwargs)
644 except (AttributeError, RuntimeError):
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)
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")
664 minval = stats.getValue(afwMath.MIN)
665 maxval = stats.getValue(afwMath.MAX)
666 elif minval ==
"zscale":
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))
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))
690 self._normalize = LinearNormalize(minimum=minval, maximum=maxval)
692 raise RuntimeError(
"Unsupported stretch algorithm \"%s\"" % algorithm)
697 def _zoom(self, zoomfac):
698 """Zoom by specified amount"""
700 self._zoomfac = zoomfac
707 size =
min(self._width, self._height)
708 if 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
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)"""
731 self._zoom(self._zoomfac)
733 def _getEvent(self, timeout=-1):
734 """Listen for a key press, returning (key, x, y)"""
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)
753 Callable class to retrieve
a single keyboard click
756 """Create a BlockingKeyInput
758 @param fig The figure to monitor
for keyboard events
760 BlockingInput.__init__(self, fig=fig, eventslist=('key_press_event',))
764 Return the event containing the key and (x, y)
767 event = self.events[-1]
772 self.
ev = interface.Event(event.key, event.xdata, event.ydata)
776 Blocking call to retrieve a single key click
777 Returns key or None if timeout (-1: never timeout)
781 BlockingInput.__call__(self, n=1, timeout=timeout)
789 """Class to support stretches for mtv()"""
793 Return a MaskedArray with value mapped to [0, 255]
795 @param value Input pixel value
or array to be mapped
797 if isinstance(value, np.ndarray):
802 data = data - self.mapping.minimum[0]
803 return ma.array(data*self.mapping.mapIntensityToUint8(data)/255.0)
807 """Provide an asinh stretch for mtv()"""
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
819 self.
mapping = afwRgb.AsinhMapping(minimum, dataRange, Q)
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
834 Q = np.sinh((frac*self.
mapping._uint8Max)/self.
mapping._slope)/frac
835 dataRange = Q/self.
mapping._soften
838 return vmin, vmin + dataRange, Q
842 """Provide an asinh stretch using zscale to set limits for mtv()"""
844 """Initialise an object able to carry out an asinh mapping
846 @param image image to use estimate minimum
and dataRange using zscale
848 @param Q Softening parameter (default: 8)
850 See Lupton et al., PASP 116, 133
859 Normalize.__init__(self, vmin, vmax)
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)
874 self.
mapping = afwRgb.ZScaleMapping(image, nSamples, contrast)
880 """Provide a linear stretch for mtv()"""
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
888 self.
mapping = afwRgb.LinearMapping(minimum, maximum)
Pass parameters to a Statistics object.
def __init__(self, minimum=0, dataRange=1, Q=8)
def __init__(self, image=None, Q=8)
def wait(self, prompt="[c(ontinue) p(db)] :", allowPdb=True)
def savefig(self, *args, **kwargs)
def _i_scale(self, algorithm, minval, maxval, unit, *args, **kwargs)
def useSexagesimal(self, useSexagesimal)
def _getMaskTransparency(self, maskplane=None)
def _i_mtv(self, data, wcs, title, isMask)
def __init__(self, display, verbose=False, interpretMaskBits=True, mtvOrigin=afwImage.PARENT, fastMaskDisplay=False, reopenPlot=False, useSexagesimal=False, dpi=None, *args, **kwargs)
def show_colorbar(self, show=True, where="right", axSize="5%", axPad=None, **kwargs)
def _i_setImage(self, image, mask=None, wcs=None)
def __init__(self, minimum=0, maximum=1)
def __call__(self, value, clip=None)
def __init__(self, image=None, nSamples=1000, contrast=0.25)
daf::base::PropertyList * list
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.
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)