Coverage for python/lsst/afw/display/interface.py: 28%
378 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-19 04:04 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-19 04:04 -0700
1# This file is part of afw.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22__all__ = [
23 "WHITE", "BLACK", "RED", "GREEN", "BLUE", "CYAN", "MAGENTA", "YELLOW", "ORANGE", "IGNORE",
24 "Display", "Event", "noop_callback", "h_callback",
25 "setDefaultBackend", "getDefaultBackend",
26 "setDefaultFrame", "getDefaultFrame", "incrDefaultFrame",
27 "setDefaultMaskTransparency", "setDefaultMaskPlaneColor",
28 "getDisplay", "delAllDisplays",
29]
31import re
32import sys
33import importlib
34import lsst.afw.geom as afwGeom
35import lsst.afw.image as afwImage
36import lsst.log
38logger = lsst.log.Log.getLogger(__name__)
40# Symbolic names for mask/line colors. N.b. ds9 supports any X11 color for masks
41WHITE = "white"
42BLACK = "black"
43RED = "red"
44GREEN = "green"
45BLUE = "blue"
46CYAN = "cyan"
47MAGENTA = "magenta"
48YELLOW = "yellow"
49ORANGE = "orange"
50IGNORE = "ignore"
53def _makeDisplayImpl(display, backend, *args, **kwargs):
54 """Return the ``DisplayImpl`` for the named backend
56 Parameters
57 ----------
58 display : `str`
59 Name of device. Should be importable, either absolutely or relative to lsst.display
60 backend : `str`
61 The desired backend
62 *args
63 Arguments passed to DisplayImpl.__init__
64 *kwargs
65 Keywords arguments passed to DisplayImpl.__init__
67 Examples
68 --------
69 E.g.
71 .. code-block:: py
73 import lsst.afw.display as afwDisplay
74 display = afwDisplay.Display(backend="ds9")
76 would call
78 .. code-block:: py
80 _makeDisplayImpl(..., "ds9", 1)
82 and import the ds9 implementation of ``DisplayImpl`` from `lsst.display.ds9`
83 """
84 _disp = None
85 exc = None
86 candidateBackends = (f"lsst.display.{backend}", backend, f".{backend}", f"lsst.afw.display.{backend}")
87 for dt in candidateBackends:
88 exc = None
89 # only specify the root package if we are not doing an absolute import
90 impargs = {}
91 if dt.startswith("."):
92 impargs["package"] = "lsst.display"
93 try:
94 _disp = importlib.import_module(dt, **impargs)
95 # If _disp doesn't have a DisplayImpl attribute, we probably
96 # picked up an irrelevant module due to a name collision
97 if hasattr(_disp, "DisplayImpl"):
98 break
99 else:
100 _disp = None
101 except (ImportError, SystemError) as e:
102 # SystemError can be raised in Python 3.5 if a relative import
103 # is attempted when the root package, lsst.display, does not exist.
104 # Copy the exception into outer scope
105 exc = e
107 if not _disp or not hasattr(_disp.DisplayImpl, "_show"): 107 ↛ 116line 107 didn't jump to line 116, because the condition on line 107 was never false
108 # If available, re-use the final exception from above
109 e = ImportError(f"Could not load the requested backend: {backend} "
110 f"(tried {', '.join(candidateBackends)}, but none worked).")
111 if exc is not None: 111 ↛ 114line 111 didn't jump to line 114, because the condition on line 111 was never false
112 raise e from exc
113 else:
114 raise e
116 if display:
117 _impl = _disp.DisplayImpl(display, *args, **kwargs)
118 if not hasattr(_impl, "frame"):
119 _impl.frame = display.frame
121 return _impl
122 else:
123 return True
126class Display:
127 """Create an object able to display images and overplot glyphs.
129 Parameters
130 ----------
131 frame
132 An identifier for the display.
133 backend : `str`
134 The backend to use (defaults to value set by setDefaultBackend()).
135 **kwargs
136 Arguments to pass to the backend.
137 """
138 _displays = {}
139 _defaultBackend = None
140 _defaultFrame = 0
141 _defaultMaskPlaneColor = dict(
142 BAD=RED,
143 CR=MAGENTA,
144 EDGE=YELLOW,
145 INTERPOLATED=GREEN,
146 SATURATED=GREEN,
147 DETECTED=BLUE,
148 DETECTED_NEGATIVE=CYAN,
149 SUSPECT=YELLOW,
150 NO_DATA=ORANGE,
151 # deprecated names
152 INTRP=GREEN,
153 SAT=GREEN,
154 )
155 _defaultMaskTransparency = {}
156 _defaultImageColormap = "gray"
158 def __init__(self, frame=None, backend=None, **kwargs):
159 if frame is None:
160 frame = getDefaultFrame()
162 if backend is None:
163 if Display._defaultBackend is None:
164 try:
165 setDefaultBackend("ds9")
166 except RuntimeError:
167 setDefaultBackend("virtualDevice")
169 backend = Display._defaultBackend
171 self.frame = frame
172 self._impl = _makeDisplayImpl(self, backend, **kwargs)
173 self.name = backend
175 self._xy0 = None # displayed data's XY0
176 self.setMaskTransparency(Display._defaultMaskTransparency)
177 self._maskPlaneColors = {}
178 self.setMaskPlaneColor(Display._defaultMaskPlaneColor)
179 self.setImageColormap(Display._defaultImageColormap)
181 self._callbacks = {}
183 for ik in range(ord('a'), ord('z') + 1):
184 k = f"{ik:c}"
185 self.setCallback(k, noRaise=True)
186 self.setCallback(k.upper(), noRaise=True)
188 for k in ('Return', 'Shift_L', 'Shift_R'):
189 self.setCallback(k)
191 for k in ('q', 'Escape'):
192 self.setCallback(k, lambda k, x, y: True)
194 def _h_callback(k, x, y):
195 h_callback(k, x, y)
197 for k in sorted(self._callbacks.keys()):
198 doc = self._callbacks[k].__doc__
199 print(" %-6s %s" % (k, doc.split("\n")[0] if doc else "???"))
201 self.setCallback('h', _h_callback)
203 Display._displays[frame] = self
205 def __enter__(self):
206 """Support for python's with statement.
207 """
208 return self
210 def __exit__(self, *args):
211 """Support for python's with statement.
212 """
213 self.close()
215 def __del__(self):
216 self.close()
218 def __getattr__(self, name):
219 """Return the attribute of ``self._impl``, or ``._impl`` if it is
220 requested.
222 Parameters:
223 -----------
224 name : `str`
225 name of the attribute requested.
227 Returns:
228 --------
229 attribute : `object`
230 the attribute of self._impl for the requested name.
231 """
233 if name == '_impl':
234 return object.__getattr__(self, name)
236 if not (hasattr(self, "_impl") and self._impl):
237 raise AttributeError("Device has no _impl attached")
239 try:
240 return getattr(self._impl, name)
241 except AttributeError:
242 raise AttributeError(
243 f"Device {self.name} has no attribute \"{name}\"")
245 def close(self):
246 if getattr(self, "_impl", None) is not None:
247 self._impl._close()
248 del self._impl
249 self._impl = None
251 if self.frame in Display._displays:
252 del Display._displays[self.frame]
254 @property
255 def verbose(self):
256 """The backend's verbosity.
257 """
258 return self._impl.verbose
260 @verbose.setter
261 def verbose(self, value):
262 if self._impl:
263 self._impl.verbose = value
265 def __str__(self):
266 return f"Display[{self.frame}]"
268 # Handle Displays, including the default one (the frame to use when a user specifies None)
270 @staticmethod
271 def setDefaultBackend(backend):
272 try:
273 _makeDisplayImpl(None, backend)
274 except Exception as e:
275 raise RuntimeError(
276 f"Unable to set backend to {backend}: \"{e}\"")
278 Display._defaultBackend = backend
280 @staticmethod
281 def getDefaultBackend():
282 return Display._defaultBackend
284 @staticmethod
285 def setDefaultFrame(frame=0):
286 """Set the default frame for display.
287 """
288 Display._defaultFrame = frame
290 @staticmethod
291 def getDefaultFrame():
292 """Get the default frame for display.
293 """
294 return Display._defaultFrame
296 @staticmethod
297 def incrDefaultFrame():
298 """Increment the default frame for display.
299 """
300 Display._defaultFrame += 1
301 return Display._defaultFrame
303 @staticmethod
304 def setDefaultMaskTransparency(maskPlaneTransparency={}):
305 if hasattr(maskPlaneTransparency, "copy"): 305 ↛ 306line 305 didn't jump to line 306, because the condition on line 305 was never true
306 maskPlaneTransparency = maskPlaneTransparency.copy()
308 Display._defaultMaskTransparency = maskPlaneTransparency
310 @staticmethod
311 def setDefaultMaskPlaneColor(name=None, color=None):
312 """Set the default mapping from mask plane names to colors.
314 Parameters
315 ----------
316 name : `str` or `dict`
317 Name of mask plane, or a dict mapping names to colors
318 If name is `None`, use the hard-coded default dictionary.
319 color
320 Desired color, or `None` if name is a dict.
321 """
323 if name is None:
324 name = Display._defaultMaskPlaneColor
326 if isinstance(name, dict):
327 assert color is None
328 for k, v in name.items():
329 setDefaultMaskPlaneColor(k, v)
330 return
331 # Set the individual color values
332 Display._defaultMaskPlaneColor[name] = color
334 @staticmethod
335 def setDefaultImageColormap(cmap):
336 """Set the default colormap for images.
338 Parameters
339 ----------
340 cmap : `str`
341 Name of colormap, as interpreted by the backend.
343 Notes
344 -----
345 The only colormaps that all backends are required to honor
346 (if they pay any attention to setImageColormap) are "gray" and "grey".
347 """
349 Display._defaultImageColormap = cmap
351 def setImageColormap(self, cmap):
352 """Set the colormap to use for images.
354 Parameters
355 ----------
356 cmap : `str`
357 Name of colormap, as interpreted by the backend.
359 Notes
360 -----
361 The only colormaps that all backends are required to honor
362 (if they pay any attention to setImageColormap) are "gray" and "grey".
363 """
365 self._impl._setImageColormap(cmap)
367 @staticmethod
368 def getDisplay(frame=None, backend=None, create=True, verbose=False, **kwargs):
369 """Return a specific `Display`, creating it if need be.
371 Parameters
372 ----------
373 frame
374 The desired frame (`None` => use defaultFrame
375 (see `~Display.setDefaultFrame`)).
376 backend : `str`
377 create the specified frame using this backend (or the default if
378 `None`) if it doesn't already exist. If ``backend == ""``, it's an
379 error to specify a non-existent ``frame``.
380 create : `bool`
381 create the display if it doesn't already exist.
382 verbose : `bool`
383 Allow backend to be chatty.
384 **kwargs
385 keyword arguments passed to `Display` constructor.
386 """
388 if frame is None:
389 frame = Display._defaultFrame
391 if frame not in Display._displays:
392 if backend == "":
393 raise RuntimeError(f"Frame {frame} does not exist")
395 Display._displays[frame] = Display(
396 frame, backend, verbose=verbose, **kwargs)
398 Display._displays[frame].verbose = verbose
399 return Display._displays[frame]
401 @staticmethod
402 def delAllDisplays():
403 """Delete and close all known displays.
404 """
405 for disp in list(Display._displays.values()):
406 disp.close()
407 Display._displays = {}
409 def maskColorGenerator(self, omitBW=True):
410 """A generator for "standard" colors.
412 Parameters
413 ----------
414 omitBW : `bool`
415 Don't include `BLACK` and `WHITE`.
417 Examples
418 --------
420 .. code-block:: py
422 colorGenerator = interface.maskColorGenerator(omitBW=True)
423 for p in planeList:
424 print(p, next(colorGenerator))
425 """
426 _maskColors = [WHITE, BLACK, RED, GREEN,
427 BLUE, CYAN, MAGENTA, YELLOW, ORANGE]
429 i = -1
430 while True:
431 i += 1
432 color = _maskColors[i%len(_maskColors)]
433 if omitBW and color in (BLACK, WHITE):
434 continue
436 yield color
438 def setMaskPlaneColor(self, name, color=None):
439 """Request that mask plane name be displayed as color.
441 Parameters
442 ----------
443 name : `str` or `dict`
444 Name of mask plane or a dictionary of name -> colorName.
445 color : `str`
446 The name of the color to use (must be `None` if ``name`` is a
447 `dict`).
449 Colors may be specified as any X11-compliant string (e.g.
450 `"orchid"`), or by one of the following constants in
451 `lsst.afw.display` : `BLACK`, `WHITE`, `RED`, `BLUE`,
452 `GREEN`, `CYAN`, `MAGENTA`, `YELLOW`.
454 If the color is "ignore" (or `IGNORE`) then that mask plane is not
455 displayed.
457 The advantage of using the symbolic names is that the python
458 interpreter can detect typos.
459 """
460 if isinstance(name, dict):
461 assert color is None
462 for k, v in name.items():
463 self.setMaskPlaneColor(k, v)
464 return
466 self._maskPlaneColors[name] = color
468 def getMaskPlaneColor(self, name=None):
469 """Return the color associated with the specified mask plane name.
471 Parameters
472 ----------
473 name : `str`
474 Desired mask plane; if `None`, return entire dict.
475 """
476 if name is None:
477 return self._maskPlaneColors
478 else:
479 color = self._maskPlaneColors.get(name)
481 if color is None:
482 color = self._defaultMaskPlaneColor.get(name)
484 return color
486 def setMaskTransparency(self, transparency=None, name=None):
487 """Specify display's mask transparency (percent); or `None` to not set
488 it when loading masks.
489 """
490 if isinstance(transparency, dict):
491 assert name is None
492 for k, v in transparency.items():
493 self.setMaskTransparency(v, k)
494 return
496 if transparency is not None and (transparency < 0 or transparency > 100):
497 print(
498 "Mask transparency should be in the range [0, 100]; clipping", file=sys.stderr)
499 if transparency < 0:
500 transparency = 0
501 else:
502 transparency = 100
504 if transparency is not None:
505 self._impl._setMaskTransparency(transparency, name)
507 def getMaskTransparency(self, name=None):
508 """Return the current display's mask transparency.
509 """
510 return self._impl._getMaskTransparency(name)
512 def show(self):
513 """Uniconify and Raise display.
515 Notes
516 -----
517 Throws an exception if frame doesn't exit.
518 """
519 return self._impl._show()
521 def __addMissingMaskPlanes(self, mask):
522 """Assign colours to any missing mask planes found in mask.
523 """
524 maskPlanes = mask.getMaskPlaneDict()
525 nMaskPlanes = max(maskPlanes.values()) + 1
527 # Build inverse dictionary from mask plane index to name.
528 planes = {}
529 for key in maskPlanes:
530 planes[maskPlanes[key]] = key
532 colorGenerator = self.display.maskColorGenerator(omitBW=True)
533 for p in range(nMaskPlanes):
534 name = planes[p] # ordered by plane index
535 if name not in self._defaultMaskPlaneColor:
536 self.setDefaultMaskPlaneColor(name, next(colorGenerator))
538 def image(self, data, title="", wcs=None):
539 """Display an image on a display, with semi-transparent masks
540 overlaid, if available.
542 Parameters
543 ----------
544 data : `lsst.afw.image.Exposure` or `lsst.afw.image.MaskedImage` or `lsst.afw.image.Image`
545 Image to display; Exposure and MaskedImage will show transparent
546 mask planes.
547 title : `str`, optional
548 Title for the display window.
549 wcs : `lsst.afw.geom.SkyWcs`, optional
550 World Coordinate System to align an `~lsst.afw.image.MaskedImage`
551 or `~lsst.afw.image.Image` to; raise an exception if ``data``
552 is an `~lsst.afw.image.Exposure`.
554 Raises
555 ------
556 RuntimeError
557 Raised if an Exposure is passed with a non-None wcs when the
558 ``wcs`` kwarg is also non-None.
559 TypeError
560 Raised if data is an incompatible type.
561 """
562 if hasattr(data, "getXY0"):
563 self._xy0 = data.getXY0()
564 else:
565 self._xy0 = None
567 # It's an Exposure; display the MaskedImage with the WCS
568 if isinstance(data, afwImage.Exposure):
569 if wcs:
570 raise RuntimeError("You may not specify a wcs with an Exposure")
571 data, wcs = data.getMaskedImage(), data.wcs
572 # it's a DecoratedImage; display it
573 elif isinstance(data, afwImage.DecoratedImage):
574 try:
575 wcs = afwGeom.makeSkyWcs(data.getMetadata())
576 except TypeError:
577 wcs = None
578 data = data.image
580 self._xy0 = data.getXY0() # DecoratedImage doesn't have getXY0()
582 if isinstance(data, afwImage.Image): # it's an Image; display it
583 self._impl._mtv(data, None, wcs, title)
584 # It's a Mask; display it, bitplane by bitplane.
585 elif isinstance(data, afwImage.Mask):
586 self.__addMissingMaskPlanes(data)
587 # Some displays can't display a Mask without an image; so display
588 # an Image too, with pixel values set to the mask.
589 self._impl._mtv(afwImage.ImageI(data.array), data, wcs, title)
590 # It's a MaskedImage; display Image and overlay Mask.
591 elif isinstance(data, afwImage.MaskedImage):
592 self.__addMissingMaskPlanes(data.mask)
593 self._impl._mtv(data.image, data.mask, wcs, title)
594 else:
595 raise TypeError(f"Unsupported type {data!r}")
597 def mtv(self, data, title="", wcs=None):
598 """Display an image on a display, with semi-transparent masks
599 overlaid, if available.
601 Notes
602 -----
603 Historical note: the name "mtv" comes from Jim Gunn's forth imageprocessing
604 system, Mirella (named after Mirella Freni); The "m" stands for Mirella.
605 """
606 self.image(data, title, wcs)
608 class _Buffering:
609 """Context manager for buffering repeated display commands.
610 """
611 def __init__(self, _impl):
612 self._impl = _impl
614 def __enter__(self):
615 self._impl._buffer(True)
617 def __exit__(self, *args):
618 self._impl._buffer(False)
619 self._impl._flush()
621 def Buffering(self):
622 """Return a context manager that will buffer repeated display
623 commands, to e.g. speed up displaying points.
625 Examples
626 --------
627 .. code-block:: py
629 with display.Buffering():
630 display.dot("+", xc, yc)
631 """
632 return self._Buffering(self._impl)
634 def flush(self):
635 """Flush any buffering that may be provided by the backend.
636 """
637 self._impl._flush()
639 def erase(self):
640 """Erase the specified display frame.
641 """
642 self._impl._erase()
644 def centroids(self, catalog, *, symbol="o", **kwargs):
645 """Draw the sources from a catalog at their pixel centroid positions
646 as given by `~lsst.afw.table.Catalog.getX()` and
647 `~lsst.afw.table.Catalog.getY()`.
649 See `dot` for an explanation of ``symbol`` and available args/kwargs,
650 which are passed to `dot`.
652 Parameters
653 ----------
654 catalog : `lsst.afw.table.Catalog`
655 Catalog to display centroids for. Must have valid `slot_Centroid`.
656 """
657 if not catalog.getCentroidSlot().isValid():
658 raise RuntimeError("Catalog must have a valid `slot_Centroid` defined to get X/Y positions.")
660 with self.Buffering():
661 for pt in catalog:
662 self.dot(symbol, pt.getX(), pt.getY(), **kwargs)
664 def dot(self, symb, c, r, size=2, ctype=None, origin=afwImage.PARENT, **kwargs):
665 """Draw a symbol onto the specified display frame.
667 Parameters
668 ----------
669 symb
670 Possible values are:
672 ``"+"``
673 Draw a +
674 ``"x"``
675 Draw an x
676 ``"*"``
677 Draw a *
678 ``"o"``
679 Draw a circle
680 ``"@:Mxx,Mxy,Myy"``
681 Draw an ellipse with moments (Mxx, Mxy, Myy) (argument size is ignored)
682 `lsst.afw.geom.ellipses.BaseCore`
683 Draw the ellipse (argument size is ignored). N.b. objects
684 derived from `~lsst.afw.geom.ellipses.BaseCore` include
685 `~lsst.afw.geom.ellipses.Axes` and `~lsst.afw.geom.ellipses.Quadrupole`.
686 Any other value
687 Interpreted as a string to be drawn.
688 c, r : `float`
689 The column and row where the symbol is drawn [0-based coordinates].
690 size : `int`
691 Size of symbol, in pixels.
692 ctype : `str`
693 The desired color, either e.g. `lsst.afw.display.RED` or a color name known to X11
694 origin : `lsst.afw.image.ImageOrigin`
695 Coordinate system for the given positions.
696 **kwargs
697 Extra keyword arguments to backend.
698 """
699 if isinstance(symb, int):
700 symb = f"{symb:d}"
702 if origin == afwImage.PARENT and self._xy0 is not None:
703 x0, y0 = self._xy0
704 r -= y0
705 c -= x0
707 if isinstance(symb, afwGeom.ellipses.BaseCore) or re.search(r"^@:", symb):
708 try:
709 mat = re.search(r"^@:([^,]+),([^,]+),([^,]+)", symb)
710 except TypeError:
711 pass
712 else:
713 if mat:
714 mxx, mxy, myy = [float(_) for _ in mat.groups()]
715 symb = afwGeom.Quadrupole(mxx, myy, mxy)
717 symb = afwGeom.ellipses.Axes(symb)
719 self._impl._dot(symb, c, r, size, ctype, **kwargs)
721 def line(self, points, origin=afwImage.PARENT, symbs=False, ctype=None, size=0.5):
722 """Draw a set of symbols or connect points
724 Parameters
725 ----------
726 points : `list`
727 A list of (col, row)
728 origin : `lsst.afw.image.ImageOrigin`
729 Coordinate system for the given positions.
730 symbs : `bool` or sequence
731 If ``symbs`` is `True`, draw points at the specified points using
732 the desired symbol, otherwise connect the dots.
734 If ``symbs`` supports indexing (which includes a string -- caveat
735 emptor) the elements are used to label the points.
736 ctype : `str`
737 ``ctype`` is the name of a color (e.g. 'red').
738 size : `float`
739 Size of points to create if `symbs` is passed.
740 """
741 if symbs:
742 try:
743 symbs[1]
744 except TypeError:
745 symbs = len(points)*list(symbs)
747 for i, xy in enumerate(points):
748 self.dot(symbs[i], *xy, size=size, ctype=ctype)
749 else:
750 if len(points) > 0:
751 if origin == afwImage.PARENT and self._xy0 is not None:
752 x0, y0 = self._xy0
753 _points = list(points) # make a mutable copy
754 for i, p in enumerate(points):
755 _points[i] = (p[0] - x0, p[1] - y0)
756 points = _points
758 self._impl._drawLines(points, ctype)
760 def scale(self, algorithm, min, max=None, unit=None, **kwargs):
761 """Set the range of the scaling from DN in the image to the image
762 display.
764 Parameters
765 ----------
766 algorithm : `str`
767 Desired scaling (e.g. "linear" or "asinh").
768 min
769 Minimum value, or "minmax" or "zscale".
770 max
771 Maximum value (must be `None` for minmax|zscale).
772 unit
773 Units for min and max (e.g. Percent, Absolute, Sigma; `None` if
774 min==minmax|zscale).
775 **kwargs
776 Optional keyword arguments to the backend.
777 """
778 if min in ("minmax", "zscale"):
779 assert max is None, f"You may not specify \"{min}\" and max"
780 assert unit is None, f"You may not specify \"{min}\" and unit"
781 elif max is None:
782 raise RuntimeError("Please specify max")
784 self._impl._scale(algorithm, min, max, unit, **kwargs)
786 def zoom(self, zoomfac=None, colc=None, rowc=None, origin=afwImage.PARENT):
787 """Zoom frame by specified amount, optionally panning also
788 """
789 if (rowc and colc is None) or (colc and rowc is None):
790 raise RuntimeError(
791 "Please specify row and column center to pan about")
793 if rowc is not None:
794 if origin == afwImage.PARENT and self._xy0 is not None:
795 x0, y0 = self._xy0
796 colc -= x0
797 rowc -= y0
799 self._impl._pan(colc, rowc)
801 if zoomfac is None and rowc is None:
802 zoomfac = 2
804 if zoomfac is not None:
805 self._impl._zoom(zoomfac)
807 def pan(self, colc=None, rowc=None, origin=afwImage.PARENT):
808 """Pan to a location.
810 Parameters
811 ----------
812 colc, rowc
813 Coordinates to pan to.
814 origin : `lsst.afw.image.ImageOrigin`
815 Coordinate system for the given positions.
817 See also
818 --------
819 Display.zoom
820 """
821 self.zoom(None, colc, rowc, origin)
823 def interact(self):
824 """Enter an interactive loop, listening for key presses or equivalent
825 UI actions in the display and firing callbacks.
827 Exit with ``q``, ``CR``, ``ESC``, or any equivalent UI action provided
828 in the display. The loop may also be exited by returning `True` from a
829 user-provided callback function.
830 """
831 interactFinished = False
833 while not interactFinished:
834 ev = self._impl._getEvent()
835 if not ev:
836 continue
837 k, x, y = ev.k, ev.x, ev.y # for now
839 if k not in self._callbacks:
840 logger.warn("No callback registered for {0}".format(k))
841 else:
842 try:
843 interactFinished = self._callbacks[k](k, x, y)
844 except Exception as e:
845 logger.error(
846 "Display._callbacks['{0}']({0},{1},{2}) failed: {3}".format(k, x, y, e))
848 def setCallback(self, k, func=None, noRaise=False):
849 """Set the callback for a key.
851 Backend displays may provide an equivalent graphical UI action, but
852 must make the associated key letter visible in the UI in some way.
854 Parameters
855 ----------
856 k : `str`
857 The key to assign the callback to.
858 func : callable
859 The callback assigned to ``k``.
860 noRaise : `bool`
861 Do not raise if ``k`` is already in use.
863 Returns
864 -------
865 oldFunc : callable
866 The callback previously assigned to ``k``.
867 """
869 if k in "f":
870 if noRaise:
871 return
872 raise RuntimeError(
873 f"Key '{k}' is already in use by display, so I can't add a callback for it")
875 ofunc = self._callbacks.get(k)
876 self._callbacks[k] = func if func else noop_callback
878 self._impl._setCallback(k, self._callbacks[k])
880 return ofunc
882 def getActiveCallbackKeys(self, onlyActive=True):
883 """Return all callback keys
885 Parameters
886 ----------
887 onlyActive : `bool`
888 If `True` only return keys that do something
889 """
890 return sorted([k for k, func in self._callbacks.items() if
891 not (onlyActive and func == noop_callback)])
894# Callbacks for display events
897class Event:
898 """A class to handle events such as key presses in image display windows.
899 """
901 def __init__(self, k, x=float('nan'), y=float('nan')):
902 self.k = k
903 self.x = x
904 self.y = y
906 def __str__(self):
907 return f"{self.k} ({self.x:.2f}, {self.y:.2f}"
910# Default fallback function
913def noop_callback(k, x, y):
914 """Callback function
916 Parameters
917 ----------
918 key
919 x
920 y
921 """
922 return False
925def h_callback(k, x, y):
926 print("Enter q or <ESC> to leave interactive mode, h for this help, or a letter to fire a callback")
927 return False
929# Handle Displays, including the default one (the frame to use when a user specifies None)
930# If the default frame is None, image display is disabled
933def setDefaultBackend(backend):
934 Display.setDefaultBackend(backend)
937def getDefaultBackend():
938 return Display.getDefaultBackend()
941def setDefaultFrame(frame=0):
942 return Display.setDefaultFrame(frame)
945def getDefaultFrame():
946 """Get the default frame for display.
947 """
948 return Display.getDefaultFrame()
951def incrDefaultFrame():
952 """Increment the default frame for display.
953 """
954 return Display.incrDefaultFrame()
957def setDefaultMaskTransparency(maskPlaneTransparency={}):
958 return Display.setDefaultMaskTransparency(maskPlaneTransparency)
961def setDefaultMaskPlaneColor(name=None, color=None):
962 """Set the default mapping from mask plane names to colors.
964 Parameters
965 ----------
966 name : `str` or `dict`
967 Name of mask plane, or a dict mapping names to colors.
968 If ``name`` is `None`, use the hard-coded default dictionary.
969 color : `str`
970 Desired color, or `None` if ``name`` is a dict.
971 """
973 return Display.setDefaultMaskPlaneColor(name, color)
976def getDisplay(frame=None, backend=None, create=True, verbose=False, **kwargs):
977 """Return a specific `Display`, creating it if need be.
979 Parameters
980 ----------
981 frame
982 Desired frame (`None` => use defaultFrame (see `setDefaultFrame`)).
983 backend : `str`
984 Create the specified frame using this backend (or the default if
985 `None`) if it doesn't already exist. If ``backend == ""``, it's an
986 error to specify a non-existent ``frame``.
987 create : `bool`
988 Create the display if it doesn't already exist.
989 verbose : `bool`
990 Allow backend to be chatty.
991 **kwargs
992 Keyword arguments passed to `Display` constructor.
994 See also
995 --------
996 Display.getDisplay
997 """
999 return Display.getDisplay(frame, backend, create, verbose, **kwargs)
1002def delAllDisplays():
1003 """Delete and close all known displays.
1004 """
1005 return Display.delAllDisplays()