From 8ff1f66994951af10451ea1230f4aa3ca3735780 Mon Sep 17 00:00:00 2001 From: Robert Lupton the Good Date: Wed, 4 Jul 2018 11:12:37 -0600 Subject: [PATCH 01/13] Accumulated changes --- examples/display_matplotlib.ipynb | 50 ++++++++++++++++ python/lsst/display/matplotlib/matplotlib.py | 60 ++++++++++++++------ 2 files changed, 92 insertions(+), 18 deletions(-) diff --git a/examples/display_matplotlib.ipynb b/examples/display_matplotlib.ipynb index 31699a9..e215646 100644 --- a/examples/display_matplotlib.ipynb +++ b/examples/display_matplotlib.ipynb @@ -166,6 +166,56 @@ "print(\"Interacting with display 0; hit q to quit; 1, 2, 4, 8, 9 to zoom\")\n", "disp.interact()" ] + }, + { + "cell_type": "code", + "execution_count": 64, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "OK or No\n" + ] + } + ], + "source": [ + "dd = dict(accept = 'OK')\n", + "\n", + "if dd.get('accept'):\n", + " if dd['accept'] == 1 or dd['accept'] == 3 or dd['accept'] == 3:\n", + " print \"XXX\"" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "if dd.get('accept') and (dd['accept'] == 1 or \n", + " dd['accept'] == 2 or \n", + " dd['accept'] == 3):\n", + " print \"XXX\"" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "accept = dd.get('accept')\n", + "if accept and accept == 1 or accept == 2 or accept == 3:\n", + " print \"XXX\"" + ] } ], "metadata": { diff --git a/python/lsst/display/matplotlib/matplotlib.py b/python/lsst/display/matplotlib/matplotlib.py index 7b54a72..a89c0d3 100644 --- a/python/lsst/display/matplotlib/matplotlib.py +++ b/python/lsst/display/matplotlib/matplotlib.py @@ -62,6 +62,7 @@ ## List of backends that support `interact` interactiveBackends = [ "Qt4Agg", + "Qt5Agg", ] class DisplayImpl(virtualDevice.DisplayImpl): @@ -80,11 +81,13 @@ class DisplayImpl(virtualDevice.DisplayImpl): Apparently only qt supports Display.interact(); the list of interactive backends is given by lsst.display.matplotlib.interactiveBackends """ - def __init__(self, display, verbose=False, interpretMaskBits=True, mtvOrigin=afwImage.PARENT, + def __init__(self, display, verbose=False, + interpretMaskBits=True, mtvOrigin=afwImage.PARENT, fastMaskDisplay=True, *args, **kwargs): """ Initialise a matplotlib display + @param fastMaskDisplay XXX @param interpretMaskBits Interpret the mask value under the cursor @param mtvOrigin Display pixel coordinates with LOCAL origin (bottom left == 0,0 not XY0) @@ -95,6 +98,7 @@ def __init__(self, display, verbose=False, interpretMaskBits=True, mtvOrigin=afw self._display = display self._maskTransparency = {None : 0.7} self._interpretMaskBits = interpretMaskBits # interpret mask bits in mtv + self._fastMaskDisplay = fastMaskDisplay self._mtvOrigin = mtvOrigin self._mappable = None self._image_colormap = pyplot.cm.gray @@ -144,6 +148,10 @@ def _show(self): # # Extensions to the API # + def savefig(self, *args, **kwargs): + """Defer to figure.savefig()""" + self._figure.savefig(*args, **kwargs) + def show_colorbar(self, show=True): """Show (or hide) the colour bar""" if show: @@ -279,13 +287,6 @@ def _i_mtv(self, data, wcs, title, isMask): colorNames.append(color) # - # Set the maskArr image to be an index into our colour map (cmap; see below) - # - for i, p in enumerate(planeList): - color = colorNames[i] - maskArr[(dataArr & (1 << p)) != 0] += i + 1 # + 1 as we set colorNames[0] to black - - # # Convert those colours to RGBA so we can have per-mask-plane transparency # and build a colour map # @@ -299,7 +300,6 @@ def _i_mtv(self, data, wcs, title, isMask): colors[i + 1][3] = alpha - dataArr = maskArr cmap = mpColors.ListedColormap(colors) norm = mpColors.NoNorm() else: @@ -308,13 +308,32 @@ def _i_mtv(self, data, wcs, title, isMask): ax = self._figure.gca() bbox = data.getBBox() - mappable = ax.imshow(dataArr, origin='lower', interpolation='nearest', - extent=(bbox.getBeginX() - 0.5, bbox.getEndX() - 0.5, - bbox.getBeginY() - 0.5, bbox.getEndY() - 0.5), - cmap=cmap, norm=norm) - - if not isMask: - self._mappable = mappable + extent = (bbox.getBeginX() - 0.5, bbox.getEndX() - 0.5, + bbox.getBeginY() - 0.5, bbox.getEndY() - 0.5) + + with pyplot.rc_context(dict(interactive=False)): + if isMask: + if self._fastMaskDisplay: + for p in reversed(planeList): + if colors[p + 1][3] == 0: + continue + maskArr[(dataArr & (1 << p)) != 0] = p + 1 # + 1 as we set colorNames[0] to black + + ax.imshow(maskArr, origin='lower', interpolation='nearest', + extent=extent, cmap=cmap, norm=norm) + else: + for i, p in enumerate(planeList): + if colors[i + 1][3] == 0: + continue + maskArr[:] = 0 + maskArr[(dataArr & (1 << p)) != 0] = i + 1 # + 1 as we set colorNames[0] to black + + ax.imshow(maskArr, origin='lower', interpolation='nearest', + extent=extent, cmap=cmap, norm=norm) + else: + mappable = ax.imshow(dataArr, origin='lower', interpolation='nearest', + extent=extent, cmap=cmap, norm=norm) + self._mappable = mappable self._figure.canvas.draw_idle() @@ -338,7 +357,11 @@ def _i_setImage(self, image, mask=None, wcs=None): # Graphics commands # def _buffer(self, enable=True): - pass + if enable: + self._figure.draw() + pyplot.ion() + else: + pyplot.ioff() def _flush(self): pass @@ -514,7 +537,8 @@ def _zoom(self, zoomfac): ax.set_xlim(xmin, xmax) ax.set_ylim(ymin, ymax) - + ax.set_aspect('equal', 'datalim'); + self._figure.canvas.draw_idle() def _pan(self, colc, rowc): From 9c2761c15b5b353bae0af26428d8213938e7e471 Mon Sep 17 00:00:00 2001 From: Robert Lupton the Good Date: Fri, 2 Nov 2018 16:05:17 -0400 Subject: [PATCH 02/13] Fix drawing ellipses --- python/lsst/display/matplotlib/matplotlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/lsst/display/matplotlib/matplotlib.py b/python/lsst/display/matplotlib/matplotlib.py index a89c0d3..7302404 100644 --- a/python/lsst/display/matplotlib/matplotlib.py +++ b/python/lsst/display/matplotlib/matplotlib.py @@ -416,7 +416,7 @@ def _dot(self, symb, c, r, size, ctype, # Following matplotlib.patches.Ellipse documentation 'width' and 'height' are diameters while # 'angle' is rotation in degrees (anti-clockwise) axis.add_artist(Ellipse((c + x0, r + y0), height=2*symb.getA(), width=2*symb.getB(), - angle=90.+math.degrees(symb.getTheta()), + angle=90.0 + math.degrees(symb.getTheta()), edgecolor=ctype, facecolor='none')) elif symb == 'o': from matplotlib.patches import CirclePolygon as Circle From 3deb44b975ca968ea6aad033513aa890d993b424 Mon Sep 17 00:00:00 2001 From: Robert Lupton the Good Date: Fri, 2 Nov 2018 16:06:26 -0400 Subject: [PATCH 03/13] Allow user to modify our definitions of e.g. GREEN Useful as matplotlib's GREEN is very dark --- python/lsst/display/matplotlib/matplotlib.py | 26 ++++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/python/lsst/display/matplotlib/matplotlib.py b/python/lsst/display/matplotlib/matplotlib.py index 7302404..59fd89c 100644 --- a/python/lsst/display/matplotlib/matplotlib.py +++ b/python/lsst/display/matplotlib/matplotlib.py @@ -65,6 +65,22 @@ "Qt5Agg", ] +try: + matplotlibCtypes +except NameError: + matplotlibCtypes = { + afwDisplay.GREEN : "#00FF00", + } + + def mapCtype(ctype): + """Map the ctype to a potentially different ctype + + Specifically, if matplotlibCtypes[ctype] exists, use it instead + + This is used e.g. to map "green" to a brighter shade + """ + return matplotlibCtypes[ctype] if ctype in matplotlibCtypes else ctype + class DisplayImpl(virtualDevice.DisplayImpl): """Provide a matplotlib backend for afwDisplay @@ -417,11 +433,11 @@ def _dot(self, symb, c, r, size, ctype, # 'angle' is rotation in degrees (anti-clockwise) axis.add_artist(Ellipse((c + x0, r + y0), height=2*symb.getA(), width=2*symb.getB(), angle=90.0 + math.degrees(symb.getTheta()), - edgecolor=ctype, facecolor='none')) + edgecolor=mapCtype(ctype), facecolor='none')) elif symb == 'o': from matplotlib.patches import CirclePolygon as Circle - axis.add_artist(Circle((c + x0, r + y0), radius=size, color=ctype, fill=False)) + axis.add_artist(Circle((c + x0, r + y0), radius=size, color=mapCtype(ctype), fill=False)) else: from matplotlib.lines import Line2D @@ -441,10 +457,10 @@ def _dot(self, symb, c, r, size, ctype, x = args[i%2 == 0] y = args[i%2 == 1] - axis.add_line(Line2D(x, y, color=ctype)) + axis.add_line(Line2D(x, y, color=mapCtype(ctype))) elif cmd == "text": x, y = np.array(args[0:2]).astype(float) - 1.0 - axis.text(x, y, symb, color=ctype, + axis.text(x, y, symb, color=mapCtype(ctype), horizontalalignment='center', verticalalignment='center') else: raise RuntimeError(ds9Cmd) @@ -462,7 +478,7 @@ def _drawLines(self, points, ctype): x = points[:, 0] + self._xy0[0] y = points[:, 1] + self._xy0[1] - self._figure.gca().add_line(Line2D(x, y, color=ctype)) + self._figure.gca().add_line(Line2D(x, y, color=mapCtype(ctype))) # # Set gray scale # From 9e9b943f4a3f9aef0f6ae8244c526e583a37e9c4 Mon Sep 17 00:00:00 2001 From: Robert Lupton the Good Date: Fri, 2 Nov 2018 16:07:47 -0400 Subject: [PATCH 04/13] Support reopenPlot argument to the Display() constructor Useful with e.g. %ipympl --- python/lsst/display/matplotlib/matplotlib.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/python/lsst/display/matplotlib/matplotlib.py b/python/lsst/display/matplotlib/matplotlib.py index 59fd89c..da7c74d 100644 --- a/python/lsst/display/matplotlib/matplotlib.py +++ b/python/lsst/display/matplotlib/matplotlib.py @@ -99,7 +99,7 @@ class DisplayImpl(virtualDevice.DisplayImpl): """ def __init__(self, display, verbose=False, interpretMaskBits=True, mtvOrigin=afwImage.PARENT, fastMaskDisplay=True, - *args, **kwargs): + reopenPlot=False, *args, **kwargs): """ Initialise a matplotlib display @@ -107,9 +107,13 @@ def __init__(self, display, verbose=False, @param interpretMaskBits Interpret the mask value under the cursor @param mtvOrigin Display pixel coordinates with LOCAL origin (bottom left == 0,0 not XY0) + @param reopenPlot If true, close the plot before opening it. + (useful with e.g. %ipympl) """ virtualDevice.DisplayImpl.__init__(self, display, verbose) + if reopenPlot: + pyplot.close(display.frame) self._figure = pyplot.figure(display.frame) self._display = display self._maskTransparency = {None : 0.7} From 6a5971633971f171296c3fda744f57fc46e7fb99 Mon Sep 17 00:00:00 2001 From: Robert Lupton the Good Date: Fri, 2 Nov 2018 16:09:32 -0400 Subject: [PATCH 05/13] Fix fastMaskDisplay overlay code I'm not sure how it ever worked. Calling `imshow` with an empty mask appears to erase all mask display (matplotlib bug?) --- python/lsst/display/matplotlib/matplotlib.py | 30 +++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/python/lsst/display/matplotlib/matplotlib.py b/python/lsst/display/matplotlib/matplotlib.py index da7c74d..547ef81 100644 --- a/python/lsst/display/matplotlib/matplotlib.py +++ b/python/lsst/display/matplotlib/matplotlib.py @@ -103,7 +103,8 @@ def __init__(self, display, verbose=False, """ Initialise a matplotlib display - @param fastMaskDisplay XXX + @param fastMaskDisplay If True, only show the first bitplane that's set + Not really what we want, but a bit faster @param interpretMaskBits Interpret the mask value under the cursor @param mtvOrigin Display pixel coordinates with LOCAL origin (bottom left == 0,0 not XY0) @@ -333,23 +334,24 @@ def _i_mtv(self, data, wcs, title, isMask): with pyplot.rc_context(dict(interactive=False)): if isMask: - if self._fastMaskDisplay: - for p in reversed(planeList): - if colors[p + 1][3] == 0: - continue - maskArr[(dataArr & (1 << p)) != 0] = p + 1 # + 1 as we set colorNames[0] to black + for i, p in reversed(list(enumerate(planeList))): + if colors[i + 1][3] == 0: + continue - ax.imshow(maskArr, origin='lower', interpolation='nearest', - extent=extent, cmap=cmap, norm=norm) - else: - for i, p in enumerate(planeList): - if colors[i + 1][3] == 0: - continue - maskArr[:] = 0 - maskArr[(dataArr & (1 << p)) != 0] = i + 1 # + 1 as we set colorNames[0] to black + bitIsSet = (dataArr & (1 << p)) != 0 + if bitIsSet.sum() == 0: + continue + + maskArr[bitIsSet] = i + 1 # + 1 as we set colorNames[0] to black + if not self._fastMaskDisplay: # we draw each bitplane separately ax.imshow(maskArr, origin='lower', interpolation='nearest', extent=extent, cmap=cmap, norm=norm) + maskArr[:] = 0 + + if self._fastMaskDisplay: # we only draw the lowest bitplane + ax.imshow(maskArr, origin='lower', interpolation='nearest', + extent=extent, cmap=cmap, norm=norm) else: mappable = ax.imshow(dataArr, origin='lower', interpolation='nearest', extent=extent, cmap=cmap, norm=norm) From b7a7ae04bcf8b0edf3a5366a4aebd3c9cb6b3988 Mon Sep 17 00:00:00 2001 From: Robert Lupton the Good Date: Fri, 2 Nov 2018 16:11:00 -0400 Subject: [PATCH 06/13] Added support for wait() Should be added to afwDisplay's API --- python/lsst/display/matplotlib/matplotlib.py | 24 ++++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/python/lsst/display/matplotlib/matplotlib.py b/python/lsst/display/matplotlib/matplotlib.py index 547ef81..da5a687 100644 --- a/python/lsst/display/matplotlib/matplotlib.py +++ b/python/lsst/display/matplotlib/matplotlib.py @@ -191,16 +191,26 @@ def _setImageColormap(self, cmap): self._image_colormap = cmap - # This will be moved into Display (DM-15218) - def setImageColormap(self, cmap): - """Set the colormap used for the image + def wait(self, prompt="[c(ontinue) p(db)] :", allowPdb=True): + """Wait for keyboard input + + @param prompt `str` + The prompt string. + @param allowPdb `bool` + If true, entering a 'p' or 'pdb' puts you into pdb + + Returns the string you entered - cmap is a string to be interpreted by the backend; where possible - a string such as "gray" will be honoured, but backend - specific values are also permitted + Useful when plotting from a programme that exits such as a processCcd + Any key except 'p' continues; 'p' puts you into pdb (unless allowPdb is False) """ - self._setImageColormap(cmap) + while True: + s = input(prompt) + if allowPdb and s in ("p", "pdb"): + import pdb; pdb.set_trace() + continue + return s # # Defined API # From 34227ba27a4b4a7a8077ba3c952cb25df720ab03 Mon Sep 17 00:00:00 2001 From: Robert Lupton the Good Date: Fri, 2 Nov 2018 16:12:08 -0400 Subject: [PATCH 07/13] Support afwDisplay's setImageColormap() --- python/lsst/display/matplotlib/matplotlib.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/python/lsst/display/matplotlib/matplotlib.py b/python/lsst/display/matplotlib/matplotlib.py index da5a687..1660403 100644 --- a/python/lsst/display/matplotlib/matplotlib.py +++ b/python/lsst/display/matplotlib/matplotlib.py @@ -385,6 +385,17 @@ def _i_setImage(self, image, mask=None, wcs=None): self._xcen = 0.5*self._width self._ycen = 0.5*self._height + def _setImageColormap(self, cmap): + """Set the colormap used for the image + + cmap should be either the name of an attribute of pyplot.cm or an mpColors.Colormap + (e.g. "gray" or pyplot.cm.gray) + + """ + if not isinstance(cmap, mpColors.Colormap): + cmap = getattr(pyplot.cm, cmap) + + self._image_colormap = cmap # # Graphics commands # From 44d361db5bc59a3a519a1398c934314650d731ae Mon Sep 17 00:00:00 2001 From: Robert Lupton the Good Date: Fri, 2 Nov 2018 16:12:37 -0400 Subject: [PATCH 08/13] Fix Buffering --- python/lsst/display/matplotlib/matplotlib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/lsst/display/matplotlib/matplotlib.py b/python/lsst/display/matplotlib/matplotlib.py index 1660403..fc83e42 100644 --- a/python/lsst/display/matplotlib/matplotlib.py +++ b/python/lsst/display/matplotlib/matplotlib.py @@ -401,10 +401,10 @@ def _setImageColormap(self, cmap): # def _buffer(self, enable=True): if enable: - self._figure.draw() - pyplot.ion() - else: pyplot.ioff() + else: + pyplot.ion() + self._figure.show() def _flush(self): pass From 044c709a80cd514833a9ad45dde0a3e095c8ac3a Mon Sep 17 00:00:00 2001 From: Robert Lupton the Good Date: Fri, 2 Nov 2018 15:33:11 -0500 Subject: [PATCH 09/13] Stopped flake8 from whining --- python/lsst/display/matplotlib/matplotlib.py | 117 ++++++++++--------- 1 file changed, 63 insertions(+), 54 deletions(-) diff --git a/python/lsst/display/matplotlib/matplotlib.py b/python/lsst/display/matplotlib/matplotlib.py index fc83e42..b2b34b5 100644 --- a/python/lsst/display/matplotlib/matplotlib.py +++ b/python/lsst/display/matplotlib/matplotlib.py @@ -20,17 +20,14 @@ # see . # -## -## \file -## \brief Definitions to talk to matplotlib from python using the "afwDisplay" interface +# +# \file +# \brief Definitions to talk to matplotlib from python using the "afwDisplay" interface from __future__ import absolute_import, division, print_function import math -import os -import re import sys -import time import unicodedata import warnings @@ -52,14 +49,14 @@ import lsst.afw.geom as afwGeom -#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- # # Set the list of backends which support _getEvent and thus interact() # try: interactiveBackends except NameError: - ## List of backends that support `interact` + # List of backends that support `interact` interactiveBackends = [ "Qt4Agg", "Qt5Agg", @@ -69,7 +66,7 @@ matplotlibCtypes except NameError: matplotlibCtypes = { - afwDisplay.GREEN : "#00FF00", + afwDisplay.GREEN : "#00FF00", # noqa: ignore=E203 } def mapCtype(ctype): @@ -81,6 +78,7 @@ def mapCtype(ctype): """ return matplotlibCtypes[ctype] if ctype in matplotlibCtypes else ctype + class DisplayImpl(virtualDevice.DisplayImpl): """Provide a matplotlib backend for afwDisplay @@ -91,7 +89,7 @@ class DisplayImpl(virtualDevice.DisplayImpl): %gui qt or %matplotlib inline - or + or %matplotlib osx Apparently only qt supports Display.interact(); the list of interactive backends @@ -117,15 +115,15 @@ def __init__(self, display, verbose=False, pyplot.close(display.frame) self._figure = pyplot.figure(display.frame) self._display = display - self._maskTransparency = {None : 0.7} - self._interpretMaskBits = interpretMaskBits # interpret mask bits in mtv + self._maskTransparency = {None : 0.7} # noqa: ignore=E203 + self._interpretMaskBits = interpretMaskBits # interpret mask bits in mtv self._fastMaskDisplay = fastMaskDisplay self._mtvOrigin = mtvOrigin self._mappable = None self._image_colormap = pyplot.cm.gray # - self.__alpha = unicodedata.lookup("GREEK SMALL LETTER alpha") # used in cursor display string - self.__delta = unicodedata.lookup("GREEK SMALL LETTER delta") # used in cursor display string + self.__alpha = unicodedata.lookup("GREEK SMALL LETTER alpha") # used in cursor display string + self.__delta = unicodedata.lookup("GREEK SMALL LETTER delta") # used in cursor display string # # Support self._scale() # @@ -139,30 +137,30 @@ def __init__(self, display, verbose=False, # Ignore warnings due to BlockingKeyInput # if not verbose: - warnings.filterwarnings("ignore",category=matplotlib.cbook.mplDeprecation) + warnings.filterwarnings("ignore", category=matplotlib.cbook.mplDeprecation) def _close(self): """!Close the display, cleaning up any allocated resources""" self._image = None self._mask = None self._wcs = None - self._figure.gca().format_coord = None # keeps a copy of _wcs + self._figure.gca().format_coord = None # keeps a copy of _wcs def _show(self): """Put the plot at the top of the window stacking order""" try: - self._figure.canvas._tkcanvas._root().lift() # tk + self._figure.canvas._tkcanvas._root().lift() # tk except AttributeError: pass try: - self._figure.canvas.manager.window.raise_() # os/x + self._figure.canvas.manager.window.raise_() # os/x except AttributeError: pass try: - self._figure.canvas.raise_() # qt[45] + self._figure.canvas.raise_() # qt[45] except AttributeError: pass @@ -207,13 +205,15 @@ def wait(self, prompt="[c(ontinue) p(db)] :", allowPdb=True): while True: s = input(prompt) if allowPdb and s in ("p", "pdb"): - import pdb; pdb.set_trace() + import pdb + pdb.set_trace() continue return s # # Defined API # + def _setMaskTransparency(self, transparency, maskplane): """Specify mask transparency (percent)""" @@ -233,9 +233,9 @@ def _mtv(self, image, mask=None, wcs=None, title=""): # and minmax/zscale stretches. We also save XY0 # self._i_setImage(image, mask, wcs) - - # We need to know the pixel values to support e.g. 'zscale' and 'minmax', so do the scaling now - if self._scaleArgs.get('algorithm'): # someone called self.scale() + + # We need to know the pixel values to support e.g. 'zscale' and 'minmax', so do the scaling now + if self._scaleArgs.get('algorithm'): # someone called self.scale() self._i_scale(self._scaleArgs['algorithm'], self._scaleArgs['minval'], self._scaleArgs['maxval'], self._scaleArgs['unit'], *self._scaleArgs['args'], **self._scaleArgs['kwargs']) @@ -246,16 +246,16 @@ def _mtv(self, image, mask=None, wcs=None, title=""): if mask: self._i_mtv(mask, wcs, title, True) - + if title: ax.set_title(title) self._title = title - # + def format_coord(x, y, wcs=self._wcs, x0=self._xy0[0], y0=self._xy0[1], origin=afwImage.PARENT, bbox=self._image.getBBox(afwImage.PARENT)): - fmt = '(%1.2f, %1.2f)' + fmt = '(%1.2f, %1.2f)' if self._mtvOrigin == afwImage.PARENT: msg = fmt % (x, y) else: @@ -283,7 +283,7 @@ def format_coord(x, y, wcs=self._wcs, x0=self._xy0[0], y0=self._xy0[1], from matplotlib.image import AxesImage for a in ax.mouseover_set: if isinstance(a, AxesImage): - a.get_cursor_data = lambda ev: None # disabled + a.get_cursor_data = lambda ev: None # disabled self._figure.tight_layout() self._figure.canvas.draw_idle() @@ -315,7 +315,7 @@ def _i_mtv(self, data, wcs, title, isMask): color = next(colorGenerator) elif color.lower() == afwDisplay.IGNORE: color = 'black' # we'll set alpha = 0 anyway - + colorNames.append(color) # # Convert those colours to RGBA so we can have per-mask-plane transparency @@ -352,14 +352,14 @@ def _i_mtv(self, data, wcs, title, isMask): if bitIsSet.sum() == 0: continue - maskArr[bitIsSet] = i + 1 # + 1 as we set colorNames[0] to black + maskArr[bitIsSet] = i + 1 # + 1 as we set colorNames[0] to black - if not self._fastMaskDisplay: # we draw each bitplane separately + if not self._fastMaskDisplay: # we draw each bitplane separately ax.imshow(maskArr, origin='lower', interpolation='nearest', extent=extent, cmap=cmap, norm=norm) maskArr[:] = 0 - if self._fastMaskDisplay: # we only draw the lowest bitplane + if self._fastMaskDisplay: # we only draw the lowest bitplane ax.imshow(maskArr, origin='lower', interpolation='nearest', extent=extent, cmap=cmap, norm=norm) else: @@ -396,9 +396,11 @@ def _setImageColormap(self, cmap): cmap = getattr(pyplot.cm, cmap) self._image_colormap = cmap + # # Graphics commands # + def _buffer(self, enable=True): if enable: pyplot.ioff() @@ -442,8 +444,8 @@ def _dot(self, symb, c, r, size, ctype, @:Mxx,Mxy,Myy Draw an ellipse with moments (Mxx, Mxy, Myy) (argument size is ignored) An object derived from afwGeom.ellipses.BaseCore Draw the ellipse (argument size is ignored) Any other value is interpreted as a string to be drawn. Strings obey the fontFamily (which may be extended - with other characteristics, e.g. "times bold italic". Text will be drawn rotated by textAngle (textAngle is - ignored otherwise). + with other characteristics, e.g. "times bold italic". Text will be drawn rotated by textAngle + (textAngle is ignored otherwise). N.b. objects derived from BaseCore include Axes and Quadrupole. """ @@ -452,14 +454,14 @@ def _dot(self, symb, c, r, size, ctype, axis = self._figure.gca() x0, y0 = self._xy0 - + if isinstance(symb, afwGeom.ellipses.BaseCore): from matplotlib.patches import Ellipse - # Following matplotlib.patches.Ellipse documentation 'width' and 'height' are diameters while + # Following matplotlib.patches.Ellipse documentation 'width' and 'height' are diameters while # 'angle' is rotation in degrees (anti-clockwise) axis.add_artist(Ellipse((c + x0, r + y0), height=2*symb.getA(), width=2*symb.getB(), - angle=90.0 + math.degrees(symb.getTheta()), + angle=90.0 + math.degrees(symb.getTheta()), edgecolor=mapCtype(ctype), facecolor='none')) elif symb == 'o': from matplotlib.patches import CirclePolygon as Circle @@ -471,10 +473,10 @@ def _dot(self, symb, c, r, size, ctype, for ds9Cmd in ds9Regions.dot(symb, c + x0, r + y0, size, fontFamily="helvetica", textAngle=None): tmp = ds9Cmd.split('#') cmd = tmp.pop(0).split() - comment = tmp.pop(0) if tmp else "" + comment = tmp.pop(0) if tmp else "" # noqa: ignore=F581 cmd, args = cmd[0], cmd[1:] - + if cmd == "line": args = np.array(args).astype(float) - 1.0 @@ -506,10 +508,11 @@ def _drawLines(self, points, ctype): y = points[:, 1] + self._xy0[1] self._figure.gca().add_line(Line2D(x, y, color=mapCtype(ctype))) - # - # Set gray scale - # + def _scale(self, algorithm, minval, maxval, unit, *args, **kwargs): + """ + Set gray scale + """ self._scaleArgs['algorithm'] = algorithm self._scaleArgs['minval'] = minval self._scaleArgs['maxval'] = maxval @@ -558,13 +561,13 @@ def _i_scale(self, algorithm, minval, maxval, unit, *args, **kwargs): # # Zoom and Pan # + def _zoom(self, zoomfac): """Zoom by specified amount""" self._zoomfac = zoomfac x0, y0 = self._xy0 - x1, y1 = x0 + self._width, y0 + self._height size = min(self._width, self._height) if size < self._zoomfac: # avoid min == max @@ -580,7 +583,7 @@ def _zoom(self, zoomfac): ax.set_xlim(xmin, xmax) ax.set_ylim(ymin, ymax) - ax.set_aspect('equal', 'datalim'); + ax.set_aspect('equal', 'datalim') self._figure.canvas.draw_idle() @@ -590,28 +593,29 @@ def _pan(self, colc, rowc): self._xcen = colc self._ycen = rowc - self._zoom(self._zoomfac) + self._zoom(self._zoomfac) def _getEvent(self, timeout=-1): """Listen for a key press, returning (key, x, y)""" mpBackend = matplotlib.get_backend() if mpBackend not in interactiveBackends: - print("The %s matplotlib backend doesn't support display._getEvent()" % + print("The %s matplotlib backend doesn't support display._getEvent()" % (matplotlib.get_backend(),), file=sys.stderr) return interface.Event('q') - + blocking_input = BlockingKeyInput(self._figure) return blocking_input(timeout=timeout) -#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + class BlockingKeyInput(BlockingInput): """ Callable class to retrieve a single keyboard click """ def __init__(self, fig): - """Create a BlockingKeyInput + r"""Create a BlockingKeyInput \param fig The figure to monitor for keyboard events """ @@ -624,7 +628,7 @@ def post_event(self): try: event = self.events[-1] except IndexError: - ## details of the event to pass back to the display + # details of the event to pass back to the display self.ev = None else: self.ev = interface.Event(event.key, event.xdata, event.ydata) @@ -640,7 +644,8 @@ def __call__(self, timeout=-1): return self.ev -#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + class Normalize(mpColors.Normalize): """Class to support stretches for mtv()""" @@ -659,6 +664,7 @@ def __call__(self, value, clip=None): data = data - self.mapping.minimum[0] return ma.array(data*self.mapping.mapIntensityToUint8(data)/255.0) + class AsinhNormalize(Normalize): """Provide an asinh stretch for mtv()""" def __init__(self, minimum=0, dataRange=1, Q=8): @@ -672,9 +678,10 @@ def __init__(self, minimum=0, dataRange=1, Q=8): """ Normalize.__init__(self) - ## The object used to perform the desired mapping + # The object used to perform the desired mapping self.mapping = afwRgb.AsinhMapping(minimum, dataRange, Q) + class AsinhZScaleNormalize(Normalize): """Provide an asinh stretch using zscale to set limits for mtv()""" def __init__(self, image=None, Q=8): @@ -687,9 +694,10 @@ def __init__(self, image=None, Q=8): """ Normalize.__init__(self) - ## The object used to perform the desired mapping + # The object used to perform the desired mapping self.mapping = afwRgb.AsinhZScaleMapping(image, Q) + class ZScaleNormalize(Normalize): """Provide a zscale stretch for mtv()""" def __init__(self, image=None, nSamples=1000, contrast=0.25): @@ -702,9 +710,10 @@ def __init__(self, image=None, nSamples=1000, contrast=0.25): Normalize.__init__(self) - ## The object used to perform the desired mapping + # The object used to perform the desired mapping self.mapping = afwRgb.ZScaleMapping(image, nSamples, contrast) + class LinearNormalize(Normalize): """Provide a linear stretch for mtv()""" def __init__(self, minimum=0, maximum=1): @@ -716,5 +725,5 @@ def __init__(self, minimum=0, maximum=1): Normalize.__init__(self) - ## The object used to perform the desired mapping + # The object used to perform the desired mapping self.mapping = afwRgb.LinearMapping(minimum, maximum) From aa01af0f3e0579bc97c26e5a0ba6df06aadd1077 Mon Sep 17 00:00:00 2001 From: Robert Lupton the Good Date: Sat, 3 Nov 2018 16:26:48 -0500 Subject: [PATCH 10/13] afwDisplay.dot() always converts ellipses.BaseCore to Axes for us Also clarify the code a little post-review --- python/lsst/display/matplotlib/matplotlib.py | 21 ++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/python/lsst/display/matplotlib/matplotlib.py b/python/lsst/display/matplotlib/matplotlib.py index b2b34b5..86a057b 100644 --- a/python/lsst/display/matplotlib/matplotlib.py +++ b/python/lsst/display/matplotlib/matplotlib.py @@ -84,6 +84,8 @@ class DisplayImpl(virtualDevice.DisplayImpl): Recommended backends in notebooks are: %matplotlib notebook + or + %matplotlib ipympl or %matplotlib qt %gui qt @@ -101,7 +103,8 @@ def __init__(self, display, verbose=False, """ Initialise a matplotlib display - @param fastMaskDisplay If True, only show the first bitplane that's set + @param fastMaskDisplay If True, only show the first bitplane that's set in each pixel + (e.g. if (SATURATED & DETECTED), ignore DETECTED) Not really what we want, but a bit faster @param interpretMaskBits Interpret the mask value under the cursor @param mtvOrigin Display pixel coordinates with LOCAL origin @@ -321,15 +324,19 @@ def _i_mtv(self, data, wcs, title, isMask): # Convert those colours to RGBA so we can have per-mask-plane transparency # and build a colour map # + # Pixels equal to 0 don't get set (as no bits are set), so leave them transparent + # and start our colours at [1] -- hence "i + 1" below + # colors = mpColors.to_rgba_array(colorNames) - colors[0][3] = 0.0 # it's black anyway + alphaChannel = 3 # the alpha channel; the A in RGBA + colors[0][alphaChannel] = 0.0 # it's black anyway for i, p in enumerate(planeList): if colorNames[i + 1] == 'black': alpha = 0.0 else: alpha = 1 - self._getMaskTransparency(planes[p] if p in planes else None) - colors[i + 1][3] = alpha + colors[i + 1][alphaChannel] = alpha cmap = mpColors.ListedColormap(colors) norm = mpColors.NoNorm() @@ -345,7 +352,7 @@ def _i_mtv(self, data, wcs, title, isMask): with pyplot.rc_context(dict(interactive=False)): if isMask: for i, p in reversed(list(enumerate(planeList))): - if colors[i + 1][3] == 0: + if colors[i + 1][alphaChannel] == 0: # colors[0] is reserved continue bitIsSet = (dataArr & (1 << p)) != 0 @@ -442,12 +449,10 @@ def _dot(self, symb, c, r, size, ctype, * Draw a * o Draw a circle @:Mxx,Mxy,Myy Draw an ellipse with moments (Mxx, Mxy, Myy) (argument size is ignored) - An object derived from afwGeom.ellipses.BaseCore Draw the ellipse (argument size is ignored) + An afwGeom.ellipses.Axes Draw the ellipse (argument size is ignored) Any other value is interpreted as a string to be drawn. Strings obey the fontFamily (which may be extended with other characteristics, e.g. "times bold italic". Text will be drawn rotated by textAngle (textAngle is ignored otherwise). - - N.b. objects derived from BaseCore include Axes and Quadrupole. """ if not ctype: ctype = afwDisplay.GREEN @@ -455,7 +460,7 @@ def _dot(self, symb, c, r, size, ctype, axis = self._figure.gca() x0, y0 = self._xy0 - if isinstance(symb, afwGeom.ellipses.BaseCore): + if isinstance(symb, afwGeom.ellipses.Axes): from matplotlib.patches import Ellipse # Following matplotlib.patches.Ellipse documentation 'width' and 'height' are diameters while From fe3033ec92eb7b25c577f05712f373c4ed08660b Mon Sep 17 00:00:00 2001 From: Robert Lupton the Good Date: Fri, 9 Nov 2018 15:38:33 -0600 Subject: [PATCH 11/13] Fixed some array subscripting which no longer works the same way --- examples/display_matplotlib.ipynb | 159 +++++++++++++++++------------- 1 file changed, 88 insertions(+), 71 deletions(-) diff --git a/examples/display_matplotlib.ipynb b/examples/display_matplotlib.ipynb index e215646..c743b37 100644 --- a/examples/display_matplotlib.ipynb +++ b/examples/display_matplotlib.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": { "collapsed": false }, @@ -13,7 +13,7 @@ "\n", "#%matplotlib osx\n", "\n", - "%matplotlib notebook\n", + "%matplotlib ipympl\n", "#%config InlineBackend.figure_format = 'retina'\n", "\n", "import lsst.afw.display as afwDisplay\n", @@ -22,26 +22,41 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": { "collapsed": false, "scrolled": false }, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "FigureCanvasNbAgg()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "import lsst.afw.geom as afwGeom\n", "import lsst.afw.image as afwImage\n", "import lsst.afw.math as afwMath\n", "\n", - "disp = afwDisplay.Display()\n", + "disp = afwDisplay.Display(0)\n", "\n", "im = afwImage.MaskedImageF(100, 50)\n", "afwMath.randomGaussianImage(im.image, afwMath.Random())\n", "im.setXY0(10, 5)\n", - "im[0, 0] = 20\n", - "im.mask[0, 0] = im.mask.getPlaneBitMask(\"EDGE\")\n", - "im[-1, -1] = 30\n", - "im.image[50, 22:24] = 40\n", + "im.image[0, 0, afwImage.LOCAL] = 20\n", + "im.mask[10, 5] = im.mask.getPlaneBitMask(\"EDGE\")\n", + "im.image[-1, -1, afwImage.LOCAL] = 30\n", + "im.image[afwGeom.BoxI(afwGeom.PointI(50, 22), afwGeom.ExtentI(1, 2))] = 40\n", "im.mask[50, 22] = 0x5\n", "\n", "disp.scale(\"asinh\", \"zscale\", Q=8)\n", @@ -52,14 +67,29 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": { "collapsed": false }, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "096f5b9e684b4737962072f3fcf31326", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "FigureCanvasNbAgg()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "if True:\n", - " disp = afwDisplay.Display()\n", + " disp = afwDisplay.Display(reopenPlot=True)\n", "\n", "if True:\n", " disp.scale(\"asinh\", \"zscale\", Q=8)\n", @@ -70,7 +100,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": { "collapsed": false, "scrolled": false @@ -84,7 +114,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": { "collapsed": false, "scrolled": false @@ -99,7 +129,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": { "collapsed": false }, @@ -110,7 +140,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": { "collapsed": false }, @@ -121,18 +151,33 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": { "collapsed": false }, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "7be077d6b5634de3a895ddedcec7e6ea", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "FigureCanvasNbAgg()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ - "disp2 = afwDisplay.Display(2)" + "disp2 = afwDisplay.Display(2, reopenPlot=True)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": { "collapsed": true }, @@ -144,11 +189,26 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": { "collapsed": false }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Interacting with display 0; hit q to quit; 1, 2, 4, 8, 9 to zoom\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "The module://ipympl.backend_nbagg matplotlib backend doesn't support display._getEvent()\n" + ] + } + ], "source": [ "def pan(k, x, y):\n", " disp.pan(x, y)\n", @@ -169,61 +229,18 @@ }, { "cell_type": "code", - "execution_count": 64, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "OK or No\n" - ] - } - ], - "source": [ - "dd = dict(accept = 'OK')\n", - "\n", - "if dd.get('accept'):\n", - " if dd['accept'] == 1 or dd['accept'] == 3 or dd['accept'] == 3:\n", - " print \"XXX\"" - ] - }, - { - "cell_type": "code", - "execution_count": 71, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "if dd.get('accept') and (dd['accept'] == 1 or \n", - " dd['accept'] == 2 or \n", - " dd['accept'] == 3):\n", - " print \"XXX\"" - ] - }, - { - "cell_type": "code", - "execution_count": 65, - "metadata": { - "collapsed": true - }, + "execution_count": null, + "metadata": {}, "outputs": [], - "source": [ - "accept = dd.get('accept')\n", - "if accept and accept == 1 or accept == 2 or accept == 3:\n", - " print \"XXX\"" - ] + "source": [] } ], "metadata": { "anaconda-cloud": {}, "kernelspec": { - "display_name": "Python [default]", + "display_name": "LSST", "language": "python", - "name": "python2" + "name": "lsst" }, "language_info": { "codemirror_mode": { @@ -239,5 +256,5 @@ } }, "nbformat": 4, - "nbformat_minor": 1 + "nbformat_minor": 2 } From 39bb55ce71d85d2b97857d29a8d04f73153d52f1 Mon Sep 17 00:00:00 2001 From: Robert Lupton the Good Date: Fri, 9 Nov 2018 15:47:30 -0600 Subject: [PATCH 12/13] Removed duplicate of _setImageColormap --- python/lsst/display/matplotlib/matplotlib.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/python/lsst/display/matplotlib/matplotlib.py b/python/lsst/display/matplotlib/matplotlib.py index 86a057b..62e1bbd 100644 --- a/python/lsst/display/matplotlib/matplotlib.py +++ b/python/lsst/display/matplotlib/matplotlib.py @@ -180,18 +180,6 @@ def show_colorbar(self, show=True): if self._mappable: self._figure.colorbar(self._mappable) - def _setImageColormap(self, cmap): - """Set the colormap used for the image - - cmap should be either the name of an attribute of pyplot.cm or an mpColors.Colormap - (e.g. "gray" or pyplot.cm.gray) - - """ - if not isinstance(cmap, mpColors.Colormap): - cmap = getattr(pyplot.cm, cmap) - - self._image_colormap = cmap - def wait(self, prompt="[c(ontinue) p(db)] :", allowPdb=True): """Wait for keyboard input From a3fc192d699fa54f6caedaecc84f50f8fee2497a Mon Sep 17 00:00:00 2001 From: Robert Lupton the Good Date: Fri, 9 Nov 2018 15:47:39 -0600 Subject: [PATCH 13/13] flake8 --- tests/test_display_matplotlib.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_display_matplotlib.py b/tests/test_display_matplotlib.py index 05e882a..f41449e 100644 --- a/tests/test_display_matplotlib.py +++ b/tests/test_display_matplotlib.py @@ -26,6 +26,7 @@ import lsst.utils.tests import lsst.afw.display as afwDisplay + class DisplayMatplotlibTestCase(unittest.TestCase): def setUp(self): @@ -49,6 +50,7 @@ def testSetImageColormap(self): """ pass + class TestMemory(lsst.utils.tests.MemoryTestCase): pass