Coverage for python/lsst/afw/display/interface.py: 25%
366 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-28 02:45 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-28 02:45 -0800
2# This file is part of afw.
3#
4# Developed for the LSST Data Management System.
5# This product includes software developed by the LSST Project
6# (https://www.lsst.org).
7# See the COPYRIGHT file at the top-level directory of this distribution
8# for details of code ownership.
9#
10# This program is free software: you can redistribute it and/or modify
11# it under the terms of the GNU General Public License as published by
12# the Free Software Foundation, either version 3 of the License, or
13# (at your option) any later version.
14#
15# This program is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License
21# along with this program. If not, see <https://www.gnu.org/licenses/>.
23__all__ = [
24 "WHITE", "BLACK", "RED", "GREEN", "BLUE", "CYAN", "MAGENTA", "YELLOW", "ORANGE", "IGNORE",
25 "Display", "Event", "noop_callback", "h_callback",
26 "setDefaultBackend", "getDefaultBackend",
27 "setDefaultFrame", "getDefaultFrame", "incrDefaultFrame",
28 "setDefaultMaskTransparency", "setDefaultMaskPlaneColor",
29 "getDisplay", "delAllDisplays",
30]
32import re
33import sys
34import importlib
35import lsst.afw.geom as afwGeom
36import lsst.afw.image as afwImage
37import lsst.log
39logger = lsst.log.Log.getLogger(__name__)
41#
42# Symbolic names for mask/line colors. N.b. ds9 supports any X11 color for masks
43#
44WHITE = "white"
45BLACK = "black"
46RED = "red"
47GREEN = "green"
48BLUE = "blue"
49CYAN = "cyan"
50MAGENTA = "magenta"
51YELLOW = "yellow"
52ORANGE = "orange"
53IGNORE = "ignore"
56def _makeDisplayImpl(display, backend, *args, **kwargs):
57 """Return the ``DisplayImpl`` for the named backend
59 Parameters
60 ----------
61 display : `str`
62 Name of device. Should be importable, either absolutely or relative to lsst.display
63 backend : `str`
64 The desired backend
65 *args
66 Arguments passed to DisplayImpl.__init__
67 *kwargs
68 Keywords arguments passed to DisplayImpl.__init__
70 Examples
71 --------
72 E.g.
73 .. code-block:: py
75 import lsst.afw.display as afwDisplay
76 display = afwDisplay.Display(display=1, backend="ds9")
77 would call
78 .. code-block:: py
80 _makeDisplayImpl(..., "ds9", 1)
81 and import the ds9 implementation of ``DisplayImpl`` from `lsst.display.ds9`
82 """
83 _disp = None
84 exc = None
85 for dt in (f"lsst.display.{backend}", backend, f".{backend}", f"lsst.afw.display.{backend}"):
86 exc = None
87 # only specify the root package if we are not doing an absolute import
88 impargs = {}
89 if dt.startswith("."):
90 impargs["package"] = "lsst.display"
91 try:
92 _disp = importlib.import_module(dt, **impargs)
93 break
94 except (ImportError, SystemError) as e:
95 # SystemError can be raised in Python 3.5 if a relative import
96 # is attempted when the root package, lsst.display, does not exist.
97 # Copy the exception into outer scope
98 exc = e
100 if not _disp or not hasattr(_disp.DisplayImpl, "_show"): 100 ↛ 108line 100 didn't jump to line 108, because the condition on line 100 was never false
101 if exc is not None: 101 ↛ 105line 101 didn't jump to line 105, because the condition on line 101 was never false
102 # re-raise the final exception
103 raise exc
104 else:
105 raise ImportError(
106 "Could not load the requested backend: {}".format(backend))
108 if display:
109 _impl = _disp.DisplayImpl(display, *args, **kwargs)
110 if not hasattr(_impl, "frame"):
111 _impl.frame = display.frame
113 return _impl
114 else:
115 return True
118class Display:
119 """Create an object able to display images and overplot glyphs
121 Parameters
122 ----------
123 frame
124 An identifier for the display
125 backend : `str`
126 The backend to use (defaults to value set by setDefaultBackend())
127 *args
128 Arguments to pass to the backend
129 **kwargs
130 Arguments to pass to the backend
131 """
132 _displays = {}
133 _defaultBackend = None
134 _defaultFrame = 0
135 _defaultMaskPlaneColor = dict(
136 BAD=RED,
137 CR=MAGENTA,
138 EDGE=YELLOW,
139 INTERPOLATED=GREEN,
140 SATURATED=GREEN,
141 DETECTED=BLUE,
142 DETECTED_NEGATIVE=CYAN,
143 SUSPECT=YELLOW,
144 NO_DATA=ORANGE,
145 # deprecated names
146 INTRP=GREEN,
147 SAT=GREEN,
148 )
149 _defaultMaskTransparency = {}
150 _defaultImageColormap = "gray"
152 def __init__(self, frame=None, backend=None, *args, **kwargs):
153 if frame is None:
154 frame = getDefaultFrame()
156 if backend is None:
157 if Display._defaultBackend is None:
158 try:
159 setDefaultBackend("ds9")
160 except RuntimeError:
161 setDefaultBackend("virtualDevice")
163 backend = Display._defaultBackend
165 self.frame = frame
166 self._impl = _makeDisplayImpl(self, backend, *args, **kwargs)
167 self.name = backend
169 self._xy0 = None # displayed data's XY0
170 self.setMaskTransparency(Display._defaultMaskTransparency)
171 self._maskPlaneColors = {}
172 self.setMaskPlaneColor(Display._defaultMaskPlaneColor)
173 self.setImageColormap(Display._defaultImageColormap)
175 self._callbacks = {}
177 for ik in range(ord('a'), ord('z') + 1):
178 k = f"{ik:c}"
179 self.setCallback(k, noRaise=True)
180 self.setCallback(k.upper(), noRaise=True)
182 for k in ('Return', 'Shift_L', 'Shift_R'):
183 self.setCallback(k)
185 for k in ('q', 'Escape'):
186 self.setCallback(k, lambda k, x, y: True)
188 def _h_callback(k, x, y):
189 h_callback(k, x, y)
191 for k in sorted(self._callbacks.keys()):
192 doc = self._callbacks[k].__doc__
193 print(" %-6s %s" % (k, doc.split("\n")[0] if doc else "???"))
195 self.setCallback('h', _h_callback)
197 Display._displays[frame] = self
199 def __enter__(self):
200 """Support for python's with statement
201 """
202 return self
204 def __exit__(self, *args):
205 """Support for python's with statement
206 """
207 self.close()
209 def __del__(self):
210 self.close()
212 def __getattr__(self, name):
213 """Return the attribute of ``self._impl``, or ``._impl`` if it is requested
215 Parameters:
216 -----------
217 name : `str`
218 name of the attribute requested
220 Returns:
221 --------
222 attribute : `object`
223 the attribute of self._impl for the requested name
224 """
226 if name == '_impl':
227 return object.__getattr__(self, name)
229 if not (hasattr(self, "_impl") and self._impl):
230 raise AttributeError("Device has no _impl attached")
232 try:
233 return getattr(self._impl, name)
234 except AttributeError:
235 raise AttributeError(
236 f"Device {self.name} has no attribute \"{name}\"")
238 def close(self):
239 if getattr(self, "_impl", None) is not None:
240 self._impl._close()
241 del self._impl
242 self._impl = None
244 if self.frame in Display._displays:
245 del Display._displays[self.frame]
247 @property
248 def verbose(self):
249 """The backend's verbosity
250 """
251 return self._impl.verbose
253 @verbose.setter
254 def verbose(self, value):
255 if self._impl:
256 self._impl.verbose = value
258 def __str__(self):
259 return f"Display[{self.frame}]"
261 #
262 # Handle Displays, including the default one (the frame to use when a user specifies None)
263 #
264 @staticmethod
265 def setDefaultBackend(backend):
266 try:
267 _makeDisplayImpl(None, backend)
268 except Exception as e:
269 raise RuntimeError(
270 f"Unable to set backend to {backend}: \"{e}\"")
272 Display._defaultBackend = backend
274 @staticmethod
275 def getDefaultBackend():
276 return Display._defaultBackend
278 @staticmethod
279 def setDefaultFrame(frame=0):
280 """Set the default frame for display
281 """
282 Display._defaultFrame = frame
284 @staticmethod
285 def getDefaultFrame():
286 """Get the default frame for display
287 """
288 return Display._defaultFrame
290 @staticmethod
291 def incrDefaultFrame():
292 """Increment the default frame for display
293 """
294 Display._defaultFrame += 1
295 return Display._defaultFrame
297 @staticmethod
298 def setDefaultMaskTransparency(maskPlaneTransparency={}):
299 if hasattr(maskPlaneTransparency, "copy"): 299 ↛ 300line 299 didn't jump to line 300, because the condition on line 299 was never true
300 maskPlaneTransparency = maskPlaneTransparency.copy()
302 Display._defaultMaskTransparency = maskPlaneTransparency
304 @staticmethod
305 def setDefaultMaskPlaneColor(name=None, color=None):
306 """Set the default mapping from mask plane names to colors
308 Parameters
309 ----------
310 name : `str` or `dict`
311 name of mask plane, or a dict mapping names to colors
312 If name is `None`, use the hard-coded default dictionary
313 color
314 Desired color, or `None` if name is a dict
315 """
317 if name is None:
318 name = Display._defaultMaskPlaneColor
320 if isinstance(name, dict):
321 assert color is None
322 for k, v in name.items():
323 setDefaultMaskPlaneColor(k, v)
324 return
325 #
326 # Set the individual color values
327 #
328 Display._defaultMaskPlaneColor[name] = color
330 @staticmethod
331 def setDefaultImageColormap(cmap):
332 """Set the default colormap for images
334 Parameters
335 ----------
336 cmap : `str`
337 Name of colormap, as interpreted by the backend
339 Notes
340 -----
341 The only colormaps that all backends are required to honor
342 (if they pay any attention to setImageColormap) are "gray" and "grey"
343 """
345 Display._defaultImageColormap = cmap
347 def setImageColormap(self, cmap):
348 """Set the colormap to use for images
350 Parameters
351 ----------
352 cmap : `str`
353 Name of colormap, as interpreted by the backend
355 Notes
356 -----
357 The only colormaps that all backends are required to honor
358 (if they pay any attention to setImageColormap) are "gray" and "grey"
359 """
361 self._impl._setImageColormap(cmap)
363 @staticmethod
364 def getDisplay(frame=None, backend=None, create=True, verbose=False, *args, **kwargs):
365 """Return a specific `Display`, creating it if need be
367 Parameters
368 ----------
369 frame
370 The desired frame (`None` => use defaultFrame (see `~Display.setDefaultFrame`))
371 backend : `str`
372 create the specified frame using this backend (or the default if
373 `None`) if it doesn't already exist. If ``backend == ""``, it's an
374 error to specify a non-existent ``frame``.
375 create : `bool`
376 create the display if it doesn't already exist.
377 verbose : `bool`
378 Allow backend to be chatty
379 *args
380 arguments passed to `Display` constructor
381 **kwargs
382 keyword arguments passed to `Display` constructor
383 """
385 if frame is None:
386 frame = Display._defaultFrame
388 if frame not in Display._displays:
389 if backend == "":
390 raise RuntimeError(f"Frame {frame} does not exist")
392 Display._displays[frame] = Display(
393 frame, backend, verbose=verbose, *args, **kwargs)
395 Display._displays[frame].verbose = verbose
396 return Display._displays[frame]
398 @staticmethod
399 def delAllDisplays():
400 """Delete and close all known displays
401 """
402 for disp in list(Display._displays.values()):
403 disp.close()
404 Display._displays = {}
406 def maskColorGenerator(self, omitBW=True):
407 """A generator for "standard" colors
409 Parameters
410 ----------
411 omitBW : `bool`
412 Don't include `BLACK` and `WHITE`
414 Examples
415 --------
417 .. code-block:: py
419 colorGenerator = interface.maskColorGenerator(omitBW=True)
420 for p in planeList:
421 print p, next(colorGenerator)
422 """
423 _maskColors = [WHITE, BLACK, RED, GREEN,
424 BLUE, CYAN, MAGENTA, YELLOW, ORANGE]
426 i = -1
427 while True:
428 i += 1
429 color = _maskColors[i%len(_maskColors)]
430 if omitBW and color in (BLACK, WHITE):
431 continue
433 yield color
435 def setMaskPlaneColor(self, name, color=None):
436 """Request that mask plane name be displayed as color
438 Parameters
439 ----------
440 name : `str` or `dict`
441 Name of mask plane or a dictionary of name -> colorName
442 color : `str`
443 The name of the color to use (must be `None` if ``name`` is a `dict`)
445 Colors may be specified as any X11-compliant string (e.g. `"orchid"`), or by one
446 of the following constants in `lsst.afw.display` : `BLACK`, `WHITE`, `RED`, `BLUE`,
447 `GREEN`, `CYAN`, `MAGENTA`, `YELLOW`.
449 If the color is "ignore" (or `IGNORE`) then that mask plane is not displayed
451 The advantage of using the symbolic names is that the python interpreter can detect typos.
452 """
454 if isinstance(name, dict):
455 assert color is None
456 for k, v in name.items():
457 self.setMaskPlaneColor(k, v)
458 return
460 self._maskPlaneColors[name] = color
462 def getMaskPlaneColor(self, name=None):
463 """Return the color associated with the specified mask plane name
465 Parameters
466 ----------
467 name : `str`
468 Desired mask plane; if `None`, return entire dict
469 """
471 if name is None:
472 return self._maskPlaneColors
473 else:
474 color = self._maskPlaneColors.get(name)
476 if color is None:
477 color = self._defaultMaskPlaneColor.get(name)
479 return color
481 def setMaskTransparency(self, transparency=None, name=None):
482 """Specify display's mask transparency (percent); or `None` to not set it when loading masks
483 """
485 if isinstance(transparency, dict):
486 assert name is None
487 for k, v in transparency.items():
488 self.setMaskTransparency(v, k)
489 return
491 if transparency is not None and (transparency < 0 or transparency > 100):
492 print(
493 "Mask transparency should be in the range [0, 100]; clipping", file=sys.stderr)
494 if transparency < 0:
495 transparency = 0
496 else:
497 transparency = 100
499 if transparency is not None:
500 self._impl._setMaskTransparency(transparency, name)
502 def getMaskTransparency(self, name=None):
503 """Return the current display's mask transparency
504 """
506 return self._impl._getMaskTransparency(name)
508 def show(self):
509 """Uniconify and Raise display.
511 Notes
512 -----
513 Throws an exception if frame doesn't exit
514 """
515 return self._impl._show()
517 def __addMissingMaskPlanes(self, mask):
518 """Assign colours to any missing mask planes found in mask"""
520 maskPlanes = mask.getMaskPlaneDict()
521 nMaskPlanes = max(maskPlanes.values()) + 1
523 planes = {} # build inverse dictionary from mask plane index to name
524 for key in maskPlanes:
525 planes[maskPlanes[key]] = key
527 colorGenerator = self.display.maskColorGenerator(omitBW=True)
528 for p in range(nMaskPlanes):
529 name = planes[p] # ordered by plane index
530 if name not in self._defaultMaskPlaneColor:
531 self.setDefaultMaskPlaneColor(name, next(colorGenerator))
533 def mtv(self, data, title="", wcs=None):
534 """Display an `~lsst.afw.image.Image` or `~lsst.afw.image.Mask` on a display
536 Notes
537 -----
538 Historical note: the name "mtv" comes from Jim Gunn's forth imageprocessing
539 system, Mirella (named after Mirella Freni); The "m" stands for Mirella.
540 """
541 if hasattr(data, "getXY0"):
542 self._xy0 = data.getXY0()
543 else:
544 self._xy0 = None
546 # it's an Exposure; display the MaskedImage with the WCS
547 if isinstance(data, afwImage.Exposure):
548 if wcs:
549 raise RuntimeError(
550 "You may not specify a wcs with an Exposure")
551 data, wcs = data.getMaskedImage(), data.getWcs()
552 elif isinstance(data, afwImage.DecoratedImage): # it's a DecoratedImage; display it
553 try:
554 wcs = afwGeom.makeSkyWcs(data.getMetadata())
555 except TypeError:
556 wcs = None
557 data = data.image
559 self._xy0 = data.getXY0() # DecoratedImage doesn't have getXY0()
561 if isinstance(data, afwImage.Image): # it's an Image; display it
562 self._impl._mtv(data, None, wcs, title)
563 # it's a Mask; display it, bitplane by bitplane
564 elif isinstance(data, afwImage.Mask):
565 self.__addMissingMaskPlanes(data)
566 #
567 # Some displays can't display a Mask without an image; so display an Image too,
568 # with pixel values set to the mask
569 #
570 self._impl._mtv(afwImage.ImageI(data.getArray()), data, wcs, title)
571 # it's a MaskedImage; display Image and overlay Mask
572 elif isinstance(data, afwImage.MaskedImage):
573 self.__addMissingMaskPlanes(data.mask)
574 self._impl._mtv(data.getImage(), data.getMask(), wcs, title)
575 else:
576 raise RuntimeError(f"Unsupported type {data!r}")
577 #
578 # Graphics commands
579 #
581 class _Buffering:
582 """A class intended to be used with python's with statement
583 """
585 def __init__(self, _impl):
586 self._impl = _impl
588 def __enter__(self):
589 self._impl._buffer(True)
591 def __exit__(self, *args):
592 self._impl._buffer(False)
593 self._impl._flush()
595 def Buffering(self):
596 """Return a class intended to be used with python's with statement
598 Examples
599 --------
600 .. code-block:: py
602 with display.Buffering():
603 display.dot("+", xc, yc)
604 """
605 return self._Buffering(self._impl)
607 def flush(self):
608 """Flush the buffers
609 """
610 self._impl._flush()
612 def erase(self):
613 """Erase the specified display frame
614 """
615 self._impl._erase()
617 def dot(self, symb, c, r, size=2, ctype=None, origin=afwImage.PARENT, *args, **kwargs):
618 """Draw a symbol onto the specified display frame
620 Parameters
621 ----------
622 symb
623 Possible values are:
625 ``"+"``
626 Draw a +
627 ``"x"``
628 Draw an x
629 ``"*"``
630 Draw a *
631 ``"o"``
632 Draw a circle
633 ``"@:Mxx,Mxy,Myy"``
634 Draw an ellipse with moments (Mxx, Mxy, Myy) (argument size is ignored)
635 `lsst.afw.geom.ellipses.BaseCore`
636 Draw the ellipse (argument size is ignored). N.b. objects
637 derived from `~lsst.afw.geom.ellipses.BaseCore` include
638 `~lsst.afw.geom.ellipses.Axes` and `~lsst.afw.geom.ellipses.Quadrupole`.
639 Any other value
640 Interpreted as a string to be drawn.
641 c, r
642 The column and row where the symbol is drawn [0-based coordinates]
643 size : `int`
644 Size of symbol, in pixels
645 ctype : `str`
646 The desired color, either e.g. `lsst.afw.display.RED` or a color name known to X11
647 origin : `lsst.afw.image.ImageOrigin`
648 Coordinate system for the given positions.
649 *args
650 Extra arguments to backend
651 **kwargs
652 Extra keyword arguments to backend
653 """
654 if isinstance(symb, int):
655 symb = f"{symb:d}"
657 if origin == afwImage.PARENT and self._xy0 is not None:
658 x0, y0 = self._xy0
659 r -= y0
660 c -= x0
662 if isinstance(symb, afwGeom.ellipses.BaseCore) or re.search(r"^@:", symb):
663 try:
664 mat = re.search(r"^@:([^,]+),([^,]+),([^,]+)", symb)
665 except TypeError:
666 pass
667 else:
668 if mat:
669 mxx, mxy, myy = [float(_) for _ in mat.groups()]
670 symb = afwGeom.Quadrupole(mxx, myy, mxy)
672 symb = afwGeom.ellipses.Axes(symb)
674 self._impl._dot(symb, c, r, size, ctype, **kwargs)
676 def line(self, points, origin=afwImage.PARENT, symbs=False, ctype=None, size=0.5):
677 """Draw a set of symbols or connect points
679 Parameters
680 ----------
681 points : `list`
682 a list of (col, row)
683 origin : `lsst.afw.image.ImageOrigin`
684 Coordinate system for the given positions.
685 symbs : `bool` or sequence
686 If ``symbs`` is `True`, draw points at the specified points using the desired symbol,
687 otherwise connect the dots.
689 If ``symbs`` supports indexing (which includes a string -- caveat emptor) the
690 elements are used to label the points
691 ctype : `str`
692 ``ctype`` is the name of a color (e.g. 'red')
693 size : `float`
694 """
695 if symbs:
696 try:
697 symbs[1]
698 except TypeError:
699 symbs = len(points)*list(symbs)
701 for i, xy in enumerate(points):
702 self.dot(symbs[i], *xy, size=size, ctype=ctype)
703 else:
704 if len(points) > 0:
705 if origin == afwImage.PARENT and self._xy0 is not None:
706 x0, y0 = self._xy0
707 _points = list(points) # make a mutable copy
708 for i, p in enumerate(points):
709 _points[i] = (p[0] - x0, p[1] - y0)
710 points = _points
712 self._impl._drawLines(points, ctype)
713 #
714 # Set gray scale
715 #
717 def scale(self, algorithm, min, max=None, unit=None, *args, **kwargs):
718 """Set the range of the scaling from DN in the image to the image display
720 Parameters
721 ----------
722 algorithm : `str`
723 Desired scaling (e.g. "linear" or "asinh")
724 min
725 Minimum value, or "minmax" or "zscale"
726 max
727 Maximum value (must be `None` for minmax|zscale)
728 unit
729 Units for min and max (e.g. Percent, Absolute, Sigma; `None` if min==minmax|zscale)
730 *args
731 Optional arguments to the backend
732 **kwargs
733 Optional keyword arguments to the backend
734 """
735 if min in ("minmax", "zscale"):
736 assert max is None, f"You may not specify \"{min}\" and max"
737 assert unit is None, f"You may not specify \"{min}\" and unit"
738 elif max is None:
739 raise RuntimeError("Please specify max")
741 self._impl._scale(algorithm, min, max, unit, *args, **kwargs)
742 #
743 # Zoom and Pan
744 #
746 def zoom(self, zoomfac=None, colc=None, rowc=None, origin=afwImage.PARENT):
747 """Zoom frame by specified amount, optionally panning also
748 """
750 if (rowc and colc is None) or (colc and rowc is None):
751 raise RuntimeError(
752 "Please specify row and column center to pan about")
754 if rowc is not None:
755 if origin == afwImage.PARENT and self._xy0 is not None:
756 x0, y0 = self._xy0
757 colc -= x0
758 rowc -= y0
760 self._impl._pan(colc, rowc)
762 if zoomfac is None and rowc is None:
763 zoomfac = 2
765 if zoomfac is not None:
766 self._impl._zoom(zoomfac)
768 def pan(self, colc=None, rowc=None, origin=afwImage.PARENT):
769 """Pan to a location
771 Parameters
772 ----------
773 colc, rowc
774 the coordinates to pan to
775 origin : `lsst.afw.image.ImageOrigin`
776 Coordinate system for the given positions.
778 See also
779 --------
780 Display.zoom
781 """
783 self.zoom(None, colc, rowc, origin)
785 def interact(self):
786 """Enter an interactive loop, listening for key presses in display and firing callbacks.
788 Exit with ``q``, ``CR``, ``ESC``, or any other callback function that returns a `True` value.
789 """
790 interactFinished = False
792 while not interactFinished:
793 ev = self._impl._getEvent()
794 if not ev:
795 continue
796 k, x, y = ev.k, ev.x, ev.y # for now
798 if k not in self._callbacks:
799 logger.warn("No callback registered for {0}".format(k))
800 else:
801 try:
802 interactFinished = self._callbacks[k](k, x, y)
803 except Exception as e:
804 logger.error(
805 "Display._callbacks['{0}']({0},{1},{2}) failed: {3}".format(k, x, y, e))
807 def setCallback(self, k, func=None, noRaise=False):
808 """Set the callback for a key
810 Parameters
811 ----------
812 k
813 The key to assign the callback to
814 func : callable
815 The callback assigned to ``k``
816 noRaise : `bool`
818 Returns
819 -------
820 oldFunc : callable
821 The callback previously assigned to ``k``.
822 """
824 if k in "f":
825 if noRaise:
826 return
827 raise RuntimeError(
828 f"Key '{k}' is already in use by display, so I can't add a callback for it")
830 ofunc = self._callbacks.get(k)
831 self._callbacks[k] = func if func else noop_callback
833 self._impl._setCallback(k, self._callbacks[k])
835 return ofunc
837 def getActiveCallbackKeys(self, onlyActive=True):
838 """Return all callback keys
840 Parameters
841 ----------
842 onlyActive : `bool`
843 If `True` only return keys that do something
844 """
846 return sorted([k for k, func in self._callbacks.items() if
847 not (onlyActive and func == noop_callback)])
849#
850# Callbacks for display events
851#
854class Event:
855 """A class to handle events such as key presses in image display windows
856 """
858 def __init__(self, k, x=float('nan'), y=float('nan')):
859 self.k = k
860 self.x = x
861 self.y = y
863 def __str__(self):
864 return f"{self.k} ({self.x:.2f}, {self.y:.2f}"
865#
866# Default fallback function
867#
870def noop_callback(k, x, y):
871 """Callback function
873 Parameters
874 ----------
875 key
876 x
877 y
878 """
879 return False
882def h_callback(k, x, y):
883 print("Enter q or <ESC> to leave interactive mode, h for this help, or a letter to fire a callback")
884 return False
886#
887# Handle Displays, including the default one (the frame to use when a user specifies None)
888#
889# If the default frame is None, image display is disabled
890#
893def setDefaultBackend(backend):
894 Display.setDefaultBackend(backend)
897def getDefaultBackend():
898 return Display.getDefaultBackend()
901def setDefaultFrame(frame=0):
902 return Display.setDefaultFrame(frame)
905def getDefaultFrame():
906 """Get the default frame for display
907 """
908 return Display.getDefaultFrame()
911def incrDefaultFrame():
912 """Increment the default frame for display
913 """
914 return Display.incrDefaultFrame()
917def setDefaultMaskTransparency(maskPlaneTransparency={}):
918 return Display.setDefaultMaskTransparency(maskPlaneTransparency)
921def setDefaultMaskPlaneColor(name=None, color=None):
922 """Set the default mapping from mask plane names to colors
924 Parameters
925 ----------
926 name : `str` or `dict`
927 name of mask plane, or a dict mapping names to colors.
928 If ``name`` is `None`, use the hard-coded default dictionary
929 color : `str`
930 Desired color, or `None` if ``name`` is a dict
931 """
933 return Display.setDefaultMaskPlaneColor(name, color)
936def getDisplay(frame=None, backend=None, create=True, verbose=False, *args, **kwargs):
937 """Return a specific `Display`, creating it if need be
939 Parameters
940 ----------
941 frame
942 The desired frame (`None` => use defaultFrame (see `setDefaultFrame`))
943 backend : `str`
944 Create the specified frame using this backend (or the default if
945 `None`) if it doesn't already exist. If ``backend == ""``, it's an
946 error to specify a non-existent ``frame``.
947 create : `bool`
948 Create the display if it doesn't already exist.
949 verbose : `bool`
950 Allow backend to be chatty
951 *args
952 arguments passed to `Display` constructor
953 **kwargs
954 keyword arguments passed to `Display` constructor
956 See also
957 --------
958 Display.getDisplay
959 """
961 return Display.getDisplay(frame, backend, create, verbose, *args, **kwargs)
964def delAllDisplays():
965 """Delete and close all known displays
966 """
967 return Display.delAllDisplays()