Coverage for python/lsst/afw/display/interface.py: 73%
385 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 01:42 -0700
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 01:42 -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 logging
32import re
33import sys
34import importlib
35import lsst.afw.geom as afwGeom
36import lsst.afw.image as afwImage
38from ._images_compat import normalize_lsst_images
40logger = logging.getLogger(__name__)
42# Symbolic names for mask/line colors. N.b. ds9 supports any X11 color for masks
43WHITE = "white"
44BLACK = "black"
45RED = "red"
46GREEN = "green"
47BLUE = "blue"
48CYAN = "cyan"
49MAGENTA = "magenta"
50YELLOW = "yellow"
51ORANGE = "orange"
52IGNORE = "ignore"
55def _makeDisplayImpl(display, backend, *args, **kwargs):
56 """Return the ``DisplayImpl`` for the named backend
58 Parameters
59 ----------
60 display : `str`
61 Name of device. Should be importable, either absolutely or relative to lsst.display
62 backend : `str`
63 The desired backend
64 *args
65 Arguments passed to DisplayImpl.__init__
66 *kwargs
67 Keywords arguments passed to DisplayImpl.__init__
69 Examples
70 --------
71 E.g.
73 .. code-block:: py
75 import lsst.afw.display as afwDisplay
76 display = afwDisplay.Display(backend="ds9")
78 would call
80 .. code-block:: py
82 _makeDisplayImpl(..., "ds9", 1)
84 and import the ds9 implementation of ``DisplayImpl`` from `lsst.display.ds9`
85 """
86 _disp = None
87 exc = None
88 candidateBackends = (f"lsst.display.{backend}", backend, f".{backend}", f"lsst.afw.display.{backend}")
89 for dt in candidateBackends:
90 exc = None
91 # only specify the root package if we are not doing an absolute import
92 impargs = {}
93 if dt.startswith("."):
94 impargs["package"] = "lsst.display"
95 try:
96 _disp = importlib.import_module(dt, **impargs)
97 # If _disp doesn't have a DisplayImpl attribute, we probably
98 # picked up an irrelevant module due to a name collision
99 if hasattr(_disp, "DisplayImpl"): 99 ↛ 102line 99 didn't jump to line 102 because the condition on line 99 was always true
100 break
101 else:
102 _disp = None
103 except (ImportError, SystemError) as e:
104 # SystemError can be raised in Python 3.5 if a relative import
105 # is attempted when the root package, lsst.display, does not exist.
106 # Copy the exception into outer scope
107 exc = e
109 if not _disp or not hasattr(_disp.DisplayImpl, "_show"):
110 # If available, re-use the final exception from above
111 e = ImportError(f"Could not load the requested backend: {backend} "
112 f"(tried {', '.join(candidateBackends)}, but none worked).")
113 if exc is not None: 113 ↛ 116line 113 didn't jump to line 116 because the condition on line 113 was always true
114 raise e from exc
115 else:
116 raise e
118 if display:
119 _impl = _disp.DisplayImpl(display, *args, **kwargs)
120 if not hasattr(_impl, "frame"): 120 ↛ 123line 120 didn't jump to line 123 because the condition on line 120 was always true
121 _impl.frame = display.frame
123 return _impl
124 else:
125 return True
128class Display:
129 """Create an object able to display images and overplot glyphs.
131 Parameters
132 ----------
133 frame
134 An identifier for the display.
135 backend : `str`
136 The backend to use (defaults to value set by setDefaultBackend()).
137 **kwargs
138 Arguments to pass to the backend.
139 """
140 _displays = {}
141 _defaultBackend = None
142 _defaultFrame = 0
143 _defaultMaskPlaneColor = dict(
144 BAD=RED,
145 CR=MAGENTA,
146 COSMIC_RAY=MAGENTA,
147 EDGE=YELLOW,
148 DETECTION_EDGE=YELLOW,
149 INTERPOLATED=GREEN,
150 SATURATED=GREEN,
151 DETECTED=BLUE,
152 DETECTED_NEGATIVE=CYAN,
153 SUSPECT=YELLOW,
154 NO_DATA=ORANGE,
155 # deprecated names
156 INTRP=GREEN,
157 SAT=GREEN,
158 )
159 _defaultMaskTransparency = {}
160 _defaultImageColormap = "gray"
162 def __init__(self, frame=None, backend=None, **kwargs):
163 if frame is None: 163 ↛ 164line 163 didn't jump to line 164 because the condition on line 163 was never true
164 frame = getDefaultFrame()
166 if backend is None:
167 if Display._defaultBackend is None: 167 ↛ 168line 167 didn't jump to line 168 because the condition on line 167 was never true
168 try:
169 setDefaultBackend("ds9")
170 except RuntimeError:
171 setDefaultBackend("virtualDevice")
173 backend = Display._defaultBackend
175 self.frame = frame
176 self._impl = _makeDisplayImpl(self, backend, **kwargs)
177 self.name = backend
179 self._xy0 = None # displayed data's XY0
180 self.setMaskTransparency(Display._defaultMaskTransparency)
181 self._maskPlaneColors = {}
182 self.setMaskPlaneColor(Display._defaultMaskPlaneColor)
183 self.setImageColormap(Display._defaultImageColormap)
185 self._callbacks = {}
187 for ik in range(ord('a'), ord('z') + 1):
188 k = f"{ik:c}"
189 self.setCallback(k, noRaise=True)
190 self.setCallback(k.upper(), noRaise=True)
192 for k in ('Return', 'Shift_L', 'Shift_R'):
193 self.setCallback(k)
195 for k in ('q', 'Escape'):
196 self.setCallback(k, lambda k, x, y: True)
198 def _h_callback(k, x, y):
199 h_callback(k, x, y)
201 for k in sorted(self._callbacks.keys()):
202 doc = self._callbacks[k].__doc__
203 print(" %-6s %s" % (k, doc.split("\n")[0] if doc else "???"))
205 self.setCallback('h', _h_callback)
207 Display._displays[frame] = self
209 def __enter__(self):
210 """Support for python's with statement.
211 """
212 return self
214 def __exit__(self, *args):
215 """Support for python's with statement.
216 """
217 self.close()
219 def __del__(self):
220 self.close()
222 def __getattr__(self, name):
223 """Return the attribute of ``self._impl``, or ``._impl`` if it is
224 requested.
226 Parameters:
227 -----------
228 name : `str`
229 name of the attribute requested.
231 Returns:
232 --------
233 attribute : `object`
234 the attribute of self._impl for the requested name.
235 """
237 if name == '_impl': 237 ↛ 238line 237 didn't jump to line 238 because the condition on line 237 was never true
238 return object.__getattr__(self, name)
240 if not (hasattr(self, "_impl") and self._impl): 240 ↛ 241line 240 didn't jump to line 241 because the condition on line 240 was never true
241 raise AttributeError("Device has no _impl attached")
243 try:
244 return getattr(self._impl, name)
245 except AttributeError:
246 raise AttributeError(
247 f"Device {self.name} has no attribute \"{name}\"")
249 def close(self):
250 if getattr(self, "_impl", None) is not None:
251 self._impl._close()
252 del self._impl
253 self._impl = None
255 if self.frame in Display._displays:
256 del Display._displays[self.frame]
258 @property
259 def verbose(self):
260 """The backend's verbosity.
261 """
262 return self._impl.verbose
264 @verbose.setter
265 def verbose(self, value):
266 if self._impl: 266 ↛ exitline 266 didn't return from function 'verbose' because the condition on line 266 was always true
267 self._impl.verbose = value
269 def __str__(self):
270 return f"Display[{self.frame}]"
272 # Handle Displays, including the default one (the frame to use when a user specifies None)
274 @staticmethod
275 def setDefaultBackend(backend):
276 try:
277 _makeDisplayImpl(None, backend)
278 except Exception as e:
279 raise RuntimeError(
280 f"Unable to set backend to {backend}: \"{e}\"")
282 Display._defaultBackend = backend
284 @staticmethod
285 def getDefaultBackend():
286 return Display._defaultBackend
288 @staticmethod
289 def setDefaultFrame(frame=0):
290 """Set the default frame for display.
291 """
292 Display._defaultFrame = frame
294 @staticmethod
295 def getDefaultFrame():
296 """Get the default frame for display.
297 """
298 return Display._defaultFrame
300 @staticmethod
301 def incrDefaultFrame():
302 """Increment the default frame for display.
303 """
304 Display._defaultFrame += 1
305 return Display._defaultFrame
307 @staticmethod
308 def setDefaultMaskTransparency(maskPlaneTransparency={}):
309 if hasattr(maskPlaneTransparency, "copy"): 309 ↛ 310line 309 didn't jump to line 310 because the condition on line 309 was never true
310 maskPlaneTransparency = maskPlaneTransparency.copy()
312 Display._defaultMaskTransparency = maskPlaneTransparency
314 @staticmethod
315 def setDefaultMaskPlaneColor(name=None, color=None):
316 """Set the default mapping from mask plane names to colors.
318 Parameters
319 ----------
320 name : `str` or `dict`
321 Name of mask plane, or a dict mapping names to colors
322 If name is `None`, use the hard-coded default dictionary.
323 color
324 Desired color, or `None` if name is a dict.
325 """
327 if name is None: 327 ↛ 328line 327 didn't jump to line 328 because the condition on line 327 was never true
328 name = Display._defaultMaskPlaneColor
330 if isinstance(name, dict): 330 ↛ 331line 330 didn't jump to line 331 because the condition on line 330 was never true
331 assert color is None
332 for k, v in name.items():
333 setDefaultMaskPlaneColor(k, v)
334 return
335 # Set the individual color values
336 Display._defaultMaskPlaneColor[name] = color
338 @staticmethod
339 def setDefaultImageColormap(cmap):
340 """Set the default colormap for images.
342 Parameters
343 ----------
344 cmap : `str`
345 Name of colormap, as interpreted by the backend.
347 Notes
348 -----
349 The only colormaps that all backends are required to honor
350 (if they pay any attention to setImageColormap) are "gray" and "grey".
351 """
353 Display._defaultImageColormap = cmap
355 def setImageColormap(self, cmap):
356 """Set the colormap to use for images.
358 Parameters
359 ----------
360 cmap : `str`
361 Name of colormap, as interpreted by the backend.
363 Notes
364 -----
365 The only colormaps that all backends are required to honor
366 (if they pay any attention to setImageColormap) are "gray" and "grey".
367 """
369 self._impl._setImageColormap(cmap)
371 @staticmethod
372 def getDisplay(frame=None, backend=None, create=True, verbose=False, **kwargs):
373 """Return a specific `Display`, creating it if need be.
375 Parameters
376 ----------
377 frame
378 The desired frame (`None` => use defaultFrame
379 (see `~Display.setDefaultFrame`)).
380 backend : `str`
381 create the specified frame using this backend (or the default if
382 `None`) if it doesn't already exist. If ``backend == ""``, it's an
383 error to specify a non-existent ``frame``.
384 create : `bool`
385 create the display if it doesn't already exist.
386 verbose : `bool`
387 Allow backend to be chatty.
388 **kwargs
389 keyword arguments passed to `Display` constructor.
390 """
392 if frame is None:
393 frame = Display._defaultFrame
395 if frame not in Display._displays:
396 if backend == "":
397 raise RuntimeError(f"Frame {frame} does not exist")
399 Display._displays[frame] = Display(
400 frame, backend, verbose=verbose, **kwargs)
402 Display._displays[frame].verbose = verbose
403 return Display._displays[frame]
405 @staticmethod
406 def delAllDisplays():
407 """Delete and close all known displays.
408 """
409 for disp in list(Display._displays.values()):
410 disp.close()
411 Display._displays = {}
413 def maskColorGenerator(self, omitBW=True):
414 """A generator for "standard" colors.
416 Parameters
417 ----------
418 omitBW : `bool`
419 Don't include `BLACK` and `WHITE`.
421 Examples
422 --------
424 .. code-block:: py
426 colorGenerator = interface.maskColorGenerator(omitBW=True)
427 for p in planeList:
428 print(p, next(colorGenerator))
429 """
430 _maskColors = [WHITE, BLACK, RED, GREEN,
431 BLUE, CYAN, MAGENTA, YELLOW, ORANGE]
433 i = -1
434 while True:
435 i += 1
436 color = _maskColors[i%len(_maskColors)]
437 if omitBW and color in (BLACK, WHITE):
438 continue
440 yield color
442 def setMaskPlaneColor(self, name, color=None):
443 """Request that mask plane name be displayed as color.
445 Parameters
446 ----------
447 name : `str` or `dict`
448 Name of mask plane or a dictionary of name -> colorName.
449 color : `str`
450 The name of the color to use (must be `None` if ``name`` is a
451 `dict`).
453 Colors may be specified as any X11-compliant string (e.g.
454 `"orchid"`), or by one of the following constants in
455 `lsst.afw.display` : `BLACK`, `WHITE`, `RED`, `BLUE`,
456 `GREEN`, `CYAN`, `MAGENTA`, `YELLOW`.
458 If the color is "ignore" (or `IGNORE`) then that mask plane is not
459 displayed.
461 The advantage of using the symbolic names is that the python
462 interpreter can detect typos.
463 """
464 if isinstance(name, dict):
465 assert color is None
466 for k, v in name.items():
467 self.setMaskPlaneColor(k, v)
468 return
470 self._maskPlaneColors[name] = color
472 def getMaskPlaneColor(self, name=None):
473 """Return the color associated with the specified mask plane name.
475 Parameters
476 ----------
477 name : `str`
478 Desired mask plane; if `None`, return entire dict.
479 """
480 if name is None:
481 return self._maskPlaneColors
482 else:
483 color = self._maskPlaneColors.get(name)
485 if color is None: 485 ↛ 486line 485 didn't jump to line 486 because the condition on line 485 was never true
486 color = self._defaultMaskPlaneColor.get(name)
488 return color
490 def setMaskTransparency(self, transparency=None, name=None):
491 """Specify display's mask transparency (percent); or `None` to not set
492 it when loading masks.
493 """
494 if isinstance(transparency, dict): 494 ↛ 495line 494 didn't jump to line 495 because the condition on line 494 was never true
495 assert name is None
496 for k, v in transparency.items():
497 self.setMaskTransparency(v, k)
498 return
500 if transparency is not None and (transparency < 0 or transparency > 100): 500 ↛ 501line 500 didn't jump to line 501 because the condition on line 500 was never true
501 print(
502 "Mask transparency should be in the range [0, 100]; clipping", file=sys.stderr)
503 if transparency < 0:
504 transparency = 0
505 else:
506 transparency = 100
508 if transparency is not None: 508 ↛ exitline 508 didn't return from function 'setMaskTransparency' because the condition on line 508 was always true
509 self._impl._setMaskTransparency(transparency, name)
511 def getMaskTransparency(self, name=None):
512 """Return the current display's mask transparency.
513 """
514 return self._impl._getMaskTransparency(name)
516 def show(self):
517 """Uniconify and Raise display.
519 Notes
520 -----
521 Throws an exception if frame doesn't exit.
522 """
523 return self._impl._show()
525 def __addMissingMaskPlanes(self, mask):
526 """Assign colours to any missing mask planes found in mask.
527 """
528 maskPlanes = mask.getMaskPlaneDict()
529 nMaskPlanes = max(maskPlanes.values()) + 1
531 # Build inverse dictionary from mask plane index to name.
532 planes = {}
533 for key in maskPlanes:
534 planes[maskPlanes[key]] = key
536 colorGenerator = self.display.maskColorGenerator(omitBW=True)
537 for p in range(nMaskPlanes):
538 name = planes[p] # ordered by plane index
539 if name not in self._defaultMaskPlaneColor:
540 self.setDefaultMaskPlaneColor(name, next(colorGenerator))
542 def image(self, data, title="", wcs=None, metadata=None):
543 """Display an image on a display, with semi-transparent masks
544 overlaid, if available.
546 Parameters
547 ----------
548 data : `lsst.afw.image.Exposure` or `lsst.afw.image.MaskedImage` or `lsst.afw.image.Image`
549 Image to display; Exposure and MaskedImage will show transparent
550 mask planes. `lsst.images` objects (`~lsst.images.MaskedImage`
551 and its subclasses such as `~lsst.images.VisitImage` and
552 `~lsst.images.cells.CellCoadd`, `~lsst.images.Image`, and
553 `~lsst.images.Mask`) are converted to the equivalent afw types,
554 using any attached ``sky_projection`` as the WCS.
555 title : `str`, optional
556 Title for the display window.
557 wcs : `lsst.afw.geom.SkyWcs`, optional
558 World Coordinate System to align an `~lsst.afw.image.MaskedImage`
559 or `~lsst.afw.image.Image` to; raise an exception if ``data``
560 is an `~lsst.afw.image.Exposure`.
561 metadata : `lsst.daf.base.PropertySet`, optional
562 Additional FITS metadata to be sent to display.
564 Raises
565 ------
566 RuntimeError
567 Raised if an Exposure, or an ``lsst.images`` object with its own
568 ``sky_projection``, is passed when the ``wcs`` kwarg is also
569 non-None.
570 TypeError
571 Raised if data is an incompatible type.
572 """
573 data, wcs = normalize_lsst_images(data, wcs)
575 if hasattr(data, "getXY0"):
576 self._xy0 = data.getXY0()
577 else:
578 self._xy0 = None
580 # It's an Exposure; display the MaskedImage with the WCS
581 if isinstance(data, afwImage.Exposure):
582 if wcs: 582 ↛ 583line 582 didn't jump to line 583 because the condition on line 582 was never true
583 raise RuntimeError("You may not specify a wcs with an Exposure")
584 data, wcs, metadata = data.getMaskedImage(), data.wcs, data.metadata
585 # it's a DecoratedImage; display it
586 elif isinstance(data, afwImage.DecoratedImage):
587 try:
588 wcs = afwGeom.makeSkyWcs(data.getMetadata())
589 except TypeError:
590 wcs = None
591 data = data.image
593 self._xy0 = data.getXY0() # DecoratedImage doesn't have getXY0()
595 if isinstance(data, afwImage.Image): # it's an Image; display it
596 self._impl._mtv(data, None, wcs, title, metadata)
597 # It's a Mask; display it, bitplane by bitplane.
598 elif isinstance(data, afwImage.Mask):
599 self.__addMissingMaskPlanes(data)
600 # Some displays can't display a Mask without an image; so display
601 # an Image too, with pixel values set to the mask.
602 self._impl._mtv(afwImage.ImageI(data.array), data, wcs, title, metadata)
603 # It's a MaskedImage; display Image and overlay Mask.
604 elif isinstance(data, afwImage.MaskedImage): 604 ↛ 608line 604 didn't jump to line 608 because the condition on line 604 was always true
605 self.__addMissingMaskPlanes(data.mask)
606 self._impl._mtv(data.image, data.mask, wcs, title, metadata)
607 else:
608 raise TypeError(f"Unsupported type {data!r}")
610 def mtv(self, data, title="", wcs=None, metadata=None):
611 """Display an image on a display, with semi-transparent masks
612 overlaid, if available.
614 Notes
615 -----
616 Historical note: the name "mtv" comes from Jim Gunn's forth imageprocessing
617 system, Mirella (named after Mirella Freni); The "m" stands for Mirella.
618 """
619 self.image(data, title, wcs, metadata)
621 class _Buffering:
622 """Context manager for buffering repeated display commands.
623 """
624 def __init__(self, _impl):
625 self._impl = _impl
627 def __enter__(self):
628 self._impl._buffer(True)
630 def __exit__(self, *args):
631 self._impl._buffer(False)
632 self._impl._flush()
634 def Buffering(self):
635 """Return a context manager that will buffer repeated display
636 commands, to e.g. speed up displaying points.
638 Examples
639 --------
640 .. code-block:: py
642 with display.Buffering():
643 display.dot("+", xc, yc)
644 """
645 return self._Buffering(self._impl)
647 def flush(self):
648 """Flush any buffering that may be provided by the backend.
649 """
650 self._impl._flush()
652 def erase(self):
653 """Erase the specified display frame.
654 """
655 self._impl._erase()
657 def centroids(self, catalog, *, symbol="o", **kwargs):
658 """Draw the sources from a catalog at their pixel centroid positions
659 as given by `~lsst.afw.table.Catalog.getX()` and
660 `~lsst.afw.table.Catalog.getY()`.
662 See `dot` for an explanation of ``symbol`` and available args/kwargs,
663 which are passed to `dot`.
665 Parameters
666 ----------
667 catalog : `lsst.afw.table.Catalog`
668 Catalog to display centroids for. Must have valid `slot_Centroid`.
669 """
670 if not catalog.getCentroidSlot().isValid():
671 raise RuntimeError("Catalog must have a valid `slot_Centroid` defined to get X/Y positions.")
673 with self.Buffering():
674 for pt in catalog:
675 self.dot(symbol, pt.getX(), pt.getY(), **kwargs)
677 def dot(self, symb, c, r, size=2, ctype=None, origin=afwImage.PARENT, **kwargs):
678 """Draw a symbol onto the specified display frame.
680 Parameters
681 ----------
682 symb
683 Possible values are:
685 ``"+"``
686 Draw a +
687 ``"x"``
688 Draw an x
689 ``"*"``
690 Draw a *
691 ``"o"``
692 Draw a circle
693 ``"@:Mxx,Mxy,Myy"``
694 Draw an ellipse with moments (Mxx, Mxy, Myy) (argument size is ignored)
695 `lsst.afw.geom.ellipses.BaseCore`
696 Draw the ellipse (argument size is ignored). N.b. objects
697 derived from `~lsst.afw.geom.ellipses.BaseCore` include
698 `~lsst.afw.geom.ellipses.Axes` and `~lsst.afw.geom.ellipses.Quadrupole`.
699 Any other value
700 Interpreted as a string to be drawn.
701 c, r : `float`
702 The column and row where the symbol is drawn [0-based coordinates].
703 size : `int`
704 Size of symbol, in pixels.
705 ctype : `str`
706 The desired color, either e.g. `lsst.afw.display.RED` or a color name known to X11
707 origin : `lsst.afw.image.ImageOrigin`
708 Coordinate system for the given positions.
709 **kwargs
710 Extra keyword arguments to backend.
711 """
712 if isinstance(symb, int): 712 ↛ 713line 712 didn't jump to line 713 because the condition on line 712 was never true
713 symb = f"{symb:d}"
715 if origin == afwImage.PARENT and self._xy0 is not None: 715 ↛ 720line 715 didn't jump to line 720 because the condition on line 715 was always true
716 x0, y0 = self._xy0
717 r -= y0
718 c -= x0
720 if isinstance(symb, afwGeom.ellipses.BaseCore) or re.search(r"^@:", symb): 720 ↛ 721line 720 didn't jump to line 721 because the condition on line 720 was never true
721 try:
722 mat = re.search(r"^@:([^,]+),([^,]+),([^,]+)", symb)
723 except TypeError:
724 pass
725 else:
726 if mat:
727 mxx, mxy, myy = [float(_) for _ in mat.groups()]
728 symb = afwGeom.Quadrupole(mxx, myy, mxy)
730 symb = afwGeom.ellipses.Axes(symb)
732 self._impl._dot(symb, c, r, size, ctype, **kwargs)
734 def line(self, points, origin=afwImage.PARENT, symbs=False, ctype=None, size=0.5):
735 """Draw a set of symbols or connect points
737 Parameters
738 ----------
739 points : `list`
740 A list of (col, row)
741 origin : `lsst.afw.image.ImageOrigin`
742 Coordinate system for the given positions.
743 symbs : `bool` or sequence
744 If ``symbs`` is `True`, draw points at the specified points using
745 the desired symbol, otherwise connect the dots.
747 If ``symbs`` supports indexing (which includes a string -- caveat
748 emptor) the elements are used to label the points.
749 ctype : `str`
750 ``ctype`` is the name of a color (e.g. 'red').
751 size : `float`
752 Size of points to create if `symbs` is passed.
753 """
754 if symbs:
755 try:
756 symbs[1]
757 except TypeError:
758 symbs = len(points)*list(symbs)
760 for i, xy in enumerate(points):
761 self.dot(symbs[i], *xy, size=size, ctype=ctype)
762 else:
763 if len(points) > 0: 763 ↛ exitline 763 didn't return from function 'line' because the condition on line 763 was always true
764 if origin == afwImage.PARENT and self._xy0 is not None: 764 ↛ 771line 764 didn't jump to line 771 because the condition on line 764 was always true
765 x0, y0 = self._xy0
766 _points = list(points) # make a mutable copy
767 for i, p in enumerate(points):
768 _points[i] = (p[0] - x0, p[1] - y0)
769 points = _points
771 self._impl._drawLines(points, ctype)
773 def scale(self, algorithm, min, max=None, unit=None, **kwargs):
774 """Set the range of the scaling from DN in the image to the image
775 display.
777 Parameters
778 ----------
779 algorithm : `str`
780 Desired scaling (e.g. "linear" or "asinh").
781 min
782 Minimum value, or "minmax" or "zscale".
783 max
784 Maximum value (must be `None` for minmax|zscale).
785 unit
786 Units for min and max (e.g. Percent, Absolute, Sigma; `None` if
787 min==minmax|zscale).
788 **kwargs
789 Optional keyword arguments to the backend.
790 """
791 if min in ("minmax", "zscale"): 791 ↛ 794line 791 didn't jump to line 794 because the condition on line 791 was always true
792 assert max is None, f"You may not specify \"{min}\" and max"
793 assert unit is None, f"You may not specify \"{min}\" and unit"
794 elif max is None:
795 raise RuntimeError("Please specify max")
797 self._impl._scale(algorithm, min, max, unit, **kwargs)
799 def zoom(self, zoomfac=None, colc=None, rowc=None, origin=afwImage.PARENT):
800 """Zoom frame by specified amount, optionally panning also
801 """
802 if (rowc and colc is None) or (colc and rowc is None): 802 ↛ 803line 802 didn't jump to line 803 because the condition on line 802 was never true
803 raise RuntimeError(
804 "Please specify row and column center to pan about")
806 if rowc is not None:
807 if origin == afwImage.PARENT and self._xy0 is not None: 807 ↛ 808line 807 didn't jump to line 808 because the condition on line 807 was never true
808 x0, y0 = self._xy0
809 colc -= x0
810 rowc -= y0
812 self._impl._pan(colc, rowc)
814 if zoomfac is None and rowc is None: 814 ↛ 815line 814 didn't jump to line 815 because the condition on line 814 was never true
815 zoomfac = 2
817 if zoomfac is not None:
818 self._impl._zoom(zoomfac)
820 def pan(self, colc=None, rowc=None, origin=afwImage.PARENT):
821 """Pan to a location.
823 Parameters
824 ----------
825 colc, rowc
826 Coordinates to pan to.
827 origin : `lsst.afw.image.ImageOrigin`
828 Coordinate system for the given positions.
830 See also
831 --------
832 Display.zoom
833 """
834 self.zoom(None, colc, rowc, origin)
836 def interact(self):
837 """Enter an interactive loop, listening for key presses or equivalent
838 UI actions in the display and firing callbacks.
840 Exit with ``q``, ``CR``, ``ESC``, or any equivalent UI action provided
841 in the display. The loop may also be exited by returning `True` from a
842 user-provided callback function.
843 """
844 interactFinished = False
846 while not interactFinished:
847 ev = self._impl._getEvent()
848 if not ev: 848 ↛ 849line 848 didn't jump to line 849 because the condition on line 848 was never true
849 continue
850 k, x, y = ev.k, ev.x, ev.y # for now
852 if k not in self._callbacks: 852 ↛ 853line 852 didn't jump to line 853 because the condition on line 852 was never true
853 logger.warning("No callback registered for %s", k)
854 else:
855 try:
856 interactFinished = self._callbacks[k](k, x, y)
857 except Exception:
858 logger.exception(
859 "Display._callbacks['%s'](%s,%s,%s) failed.", k, x, y)
861 def alignImages(self, match_type="", lock_match=True):
862 """Align and optionally lock the orientation of the images being
863 displayed.
865 Parameters
866 ----------
867 match_type : `str`, optional
868 Match type to use to align the images; see the interface-specific
869 docs for what values to use.
870 lock_match : `bool`, optional
871 Whether to lock the alignment. Panning/zooming in one image will
872 preserve the alignment in other images.
873 """
874 try:
875 self._impl._alignImages(match_type, lock_match)
876 except AttributeError:
877 raise NotImplementedError("This feature only available on some backends.")
879 def setCallback(self, k, func=None, noRaise=False):
880 """Set the callback for a key.
882 Backend displays may provide an equivalent graphical UI action, but
883 must make the associated key letter visible in the UI in some way.
885 Parameters
886 ----------
887 k : `str`
888 The key to assign the callback to.
889 func : callable
890 The callback assigned to ``k``.
891 noRaise : `bool`
892 Do not raise if ``k`` is already in use.
894 Returns
895 -------
896 oldFunc : callable
897 The callback previously assigned to ``k``.
898 """
900 if k in "f":
901 if noRaise: 901 ↛ 903line 901 didn't jump to line 903 because the condition on line 901 was always true
902 return
903 raise RuntimeError(
904 f"Key '{k}' is already in use by display, so I can't add a callback for it")
906 ofunc = self._callbacks.get(k)
907 self._callbacks[k] = func if func else noop_callback
909 self._impl._setCallback(k, self._callbacks[k])
911 return ofunc
913 def getActiveCallbackKeys(self, onlyActive=True):
914 """Return all callback keys
916 Parameters
917 ----------
918 onlyActive : `bool`
919 If `True` only return keys that do something
920 """
921 return sorted([k for k, func in self._callbacks.items() if
922 not (onlyActive and func == noop_callback)])
925# Callbacks for display events
928class Event:
929 """A class to handle events such as key presses in image display windows.
930 """
932 def __init__(self, k, x=float('nan'), y=float('nan')):
933 self.k = k
934 self.x = x
935 self.y = y
937 def __str__(self):
938 return f"{self.k} ({self.x:.2f}, {self.y:.2f}"
941# Default fallback function
944def noop_callback(k, x, y):
945 """Callback function
947 Parameters
948 ----------
949 key
950 x
951 y
952 """
953 return False
956def h_callback(k, x, y):
957 print("Enter q or <ESC> to leave interactive mode, h for this help, or a letter to fire a callback")
958 return False
960# Handle Displays, including the default one (the frame to use when a user specifies None)
961# If the default frame is None, image display is disabled
964def setDefaultBackend(backend):
965 Display.setDefaultBackend(backend)
968def getDefaultBackend():
969 return Display.getDefaultBackend()
972def setDefaultFrame(frame=0):
973 return Display.setDefaultFrame(frame)
976def getDefaultFrame():
977 """Get the default frame for display.
978 """
979 return Display.getDefaultFrame()
982def incrDefaultFrame():
983 """Increment the default frame for display.
984 """
985 return Display.incrDefaultFrame()
988def setDefaultMaskTransparency(maskPlaneTransparency={}):
989 return Display.setDefaultMaskTransparency(maskPlaneTransparency)
992def setDefaultMaskPlaneColor(name=None, color=None):
993 """Set the default mapping from mask plane names to colors.
995 Parameters
996 ----------
997 name : `str` or `dict`
998 Name of mask plane, or a dict mapping names to colors.
999 If ``name`` is `None`, use the hard-coded default dictionary.
1000 color : `str`
1001 Desired color, or `None` if ``name`` is a dict.
1002 """
1004 return Display.setDefaultMaskPlaneColor(name, color)
1007def getDisplay(frame=None, backend=None, create=True, verbose=False, **kwargs):
1008 """Return a specific `Display`, creating it if need be.
1010 Parameters
1011 ----------
1012 frame
1013 Desired frame (`None` => use defaultFrame (see `setDefaultFrame`)).
1014 backend : `str`
1015 Create the specified frame using this backend (or the default if
1016 `None`) if it doesn't already exist. If ``backend == ""``, it's an
1017 error to specify a non-existent ``frame``.
1018 create : `bool`
1019 Create the display if it doesn't already exist.
1020 verbose : `bool`
1021 Allow backend to be chatty.
1022 **kwargs
1023 Keyword arguments passed to `Display` constructor.
1025 See also
1026 --------
1027 Display.getDisplay
1028 """
1030 return Display.getDisplay(frame, backend, create, verbose, **kwargs)
1033def delAllDisplays():
1034 """Delete and close all known displays.
1035 """
1036 return Display.delAllDisplays()