LSST Applications g0f08755f38+82efc23009,g12f32b3c4e+e7bdf1200e,g1653933729+a8ce1bb630,g1a0ca8cf93+50eff2b06f,g28da252d5a+52db39f6a5,g2bbee38e9b+37c5a29d61,g2bc492864f+37c5a29d61,g2cdde0e794+c05ff076ad,g3156d2b45e+41e33cbcdc,g347aa1857d+37c5a29d61,g35bb328faa+a8ce1bb630,g3a166c0a6a+37c5a29d61,g3e281a1b8c+fb992f5633,g414038480c+7f03dfc1b0,g41af890bb2+11b950c980,g5fbc88fb19+17cd334064,g6b1c1869cb+12dd639c9a,g781aacb6e4+a8ce1bb630,g80478fca09+72e9651da0,g82479be7b0+04c31367b4,g858d7b2824+82efc23009,g9125e01d80+a8ce1bb630,g9726552aa6+8047e3811d,ga5288a1d22+e532dc0a0b,gae0086650b+a8ce1bb630,gb58c049af0+d64f4d3760,gc28159a63d+37c5a29d61,gcf0d15dbbd+2acd6d4d48,gd7358e8bfb+778a810b6e,gda3e153d99+82efc23009,gda6a2b7d83+2acd6d4d48,gdaeeff99f8+1711a396fd,ge2409df99d+6b12de1076,ge79ae78c31+37c5a29d61,gf0baf85859+d0a5978c5a,gf3967379c6+4954f8c433,gfb92a5be7c+82efc23009,gfec2e1e490+2aaed99252,w.2024.46
LSST Data Management Base Package
Loading...
Searching...
No Matches
astrowidgets.py
Go to the documentation of this file.
1# This file is part of display_astrowidgets.
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/>.
21
22__all__ = ["AstroWidgetsVersion", "DisplayImpl"]
23
24import sys
25from astropy.table import Table
26
27import lsst.afw.display.interface as interface
28import lsst.afw.display.virtualDevice as virtualDevice
29import lsst.afw.display.ds9Regions as ds9Regions
30import lsst.afw.geom as afwGeom
31
32try:
33 from ginga.misc.log import get_logger
34 from ginga.AstroImage import AstroImage
35 from ginga.util.wcsmod.wcs_astropy import AstropyWCS
36 haveGinga = True
37except ImportError:
38 import logging
39 logging.getLogger("lsst.afw.display.astrowidgets").warning("Cannot import ginga libraries.")
40
42 def skyToPixel(*args, **kwargs):
43 pass
44
45 def pixelToSky(*args, **kwargs):
46 pass
47
48 haveGinga = False
49
50
51try:
52 import astrowidgets
53 haveAstrowidgets = True
54except ImportError:
55 haveAstrowidgets = False
56
57try:
58 _maskTransparency
59except NameError:
60 _maskTransparency = None
61
62
64 """Get the version of DS9 in use.
65
66 Returns
67 -------
68 version : `str`
69 Version of DS9 in use.
70 """
71 return astrowidgets.__version__
72
73
74class AstroWidgetsEvent(interface.Event):
75 """An event generated by a mouse or key click on the display"""
76
77 def __int__(self, k, x, y):
78 interface.Event.__init__(self, k, x, y)
79
80
81class DisplayImpl(virtualDevice.DisplayImpl):
82 """Virtual device display implementation.
83
84 Parameters
85 ----------
86 display : `lsst.afw.display.virtualDevice.DisplayImpl`
87 Display object to connect to.
88 dims : `tuple` [`int`, `int`], optional
89 Dimensions of the viewer window.
90 use_opencv : `bool`, optional
91 Should openCV be used to speed drawing?
92 verbose : `bool`, optional
93 Increase log verbosity?
94 """
95 markerDict = {'+': 'plus', 'x': 'cross', '.': 'circle', '*': 'circle', 'o': 'circle'}
96
97 def __init__(self, display, dims=None, use_opencv=False, verbose=False, *args, **kwargs):
98 virtualDevice.DisplayImpl.__init__(self, display, verbose)
99 if dims is None:
100 width, height = 1024, 768
101 else:
102 width, height = dims
103 if haveGinga:
104 self.logger = get_logger("ginga", log_stderr=True, level=40)
105 else:
106 self.logger = None
107 self._viewer = astrowidgets.ImageWidget(image_width=width, image_height=height,
108 use_opencv=use_opencv, logger=self.logger)
110 self._callbackDict = dict()
111
112 # We want to display the IW, but ginga has all the handles
113 self._gingaViewer = self._viewer._viewer
114
115 bd = self._gingaViewer.get_bindings()
116 bd.enable_all(True)
117 self._canvas = self._viewer.canvas
118 self._canvas.enable_draw(False)
120 self._redraw = True
121
122 def embed(self):
123 """Attach this display to the output of the current cell."""
124 return self._viewer
125
126 def get_viewer(self):
127 """Return the ginga viewer"""
128 return self._viewer
129
130 def show_color_bar(self, show=True):
131 """Show (or hide) the colour bar.
132
133 Parameters
134 ----------
135 show : `bool`, optional
136 Should the color bar be shown?
137 """
138 self._gingaViewer.show_color_bar(show)
139
140 def show_pan_mark(self, show=True, color='red'):
141 """Show (or hide) the pan mark.
142
143 Parameters
144 ----------
145 show : `bool`, optional
146 Should the pan marker be shown?
147 color : `str`, optional
148 What color should the pan mark be?
149 """
150 self._gingaViewer.show_pan_mark(show, color)
151
152 def _setMaskTransparency(self, transparency, maskplane=None):
153 """Specify mask transparency (percent); or None to not set it when loading masks.
154
155 Parameters
156 ----------
157 transparency : `float`
158 Transparency of the masks in percent (0-100).
159 maskplane : `str`, optional
160 Unsupported option to only change the transparency of
161 certain masks.
162 """
163 if maskplane is not None:
164 print("display_astrowidgets is not yet able to set transparency for individual maskplanes" % maskplane, # noqa E501
165 file=sys.stderr)
166 return
167
168 self._maskTransparency = 0.01*transparency
169
170 def _getMaskTransparency(self, maskplane=None):
171 """Return the current mask transparency."""
172 return self._maskTransparency
173
174 def _mtv(self, image, mask=None, wcs=None, title=""):
175 """Display an Image and/or Mask on a ginga display
176
177 Parameters
178 ----------
179 image : `lsst.afw.image.Image` or `lsst.afw.image.Exposure`
180 Image to display.
181 mask : `lsst.afw.image.Mask`, optional
182 Mask to use, if the input does not contain one.
183 wcs : `ginga.util.wcsmod.wcs_astropy`
184 WCS to use, if the input does not contain one.
185 title : `str`, optional
186 Unsupported display title.
187 """
188 self._erase()
189 self._canvas.delete_all_objects()
190 self._buffer()
191 if haveGinga:
192 Aimage = AstroImage(inherit_primary_header=True)
193 Aimage.set_data(image.getArray())
194
195 self._gingaViewer.set_image(Aimage)
196
197 if wcs is not None:
198 if haveGinga:
199 _wcs = AstropyWCS(self.logger)
200 Aimage.lsst_wcs = WcsAdaptorForGinga(wcs)
201 _wcs.pixtoradec = Aimage.lsst_wcs.pixtoradec
202 _wcs.pixtosystem = Aimage.lsst_wcs.pixtosystem
203 _wcs.radectopix = Aimage.lsst_wcs.radectopix
204
205 Aimage.set_wcs(_wcs)
206 Aimage.wcs.wcs = Aimage.lsst_wcs
207
208 if mask:
209 maskColorFromName = {'BAD': 'red',
210 'SAT': 'green',
211 'INTRP': 'green',
212 'CR': 'magenta',
213 'EDGE': 'yellow',
214 'DETECTED': 'blue',
215 'DETECTED_NEGATIVE': 'cyan',
216 'SUSPECT': 'yellow',
217 'NO_DATA': 'orange',
218 'CROSSTALK': None,
219 'UNMASKEDNAN': None}
220 maskDict = dict()
221 for plane, bit in mask.getMaskPlaneDict().items():
222 color = maskColorFromName.get(plane, None)
223 if color:
224 maskDict[1 << bit] = color
225 # This value of 0.9 is pretty thick for the alpha.
226 self.overlay_mask(mask, maskDict,
227 self._maskTransparency)
228 self._buffer(enable=False)
229 self._flush()
230
231 def overlay_mask(self, maskImage, maskDict, maskAlpha):
232 """Draw mask onto the image display.
233
234 Parameters
235 ----------
236 maskImage : `lsst.afw.image.Mask`
237 Mask to display.
238 maskDict : `dict` [`str`, `str`]
239 Dictionary of mask plane names to colors.
240 maskAlpha : `float`
241 Transparency to display the mask.
242 """
243 import numpy as np
244 from ginga.RGBImage import RGBImage
245 from ginga import colors
246
247 maskArray = maskImage.getArray()
248 height, width = maskArray.shape
249 maskRGBA = np.zeros((height, width, 4), dtype=np.uint8)
250 nSet = np.zeros_like(maskArray, dtype=np.uint8)
251
252 for maskValue, maskColor in maskDict.items():
253 r, g, b = colors.lookup_color(maskColor)
254 isSet = (maskArray & maskValue) != 0
255 if (isSet == 0).all():
256 continue
257
258 maskRGBA[:, :, 0][isSet] = 255 * r
259 maskRGBA[:, :, 1][isSet] = 255 * g
260 maskRGBA[:, :, 2][isSet] = 255 * b
261
262 nSet[isSet] += 1
263
264 maskRGBA[:, :, 3][nSet == 0] = 0
265 maskRGBA[:, :, 3][nSet != 0] = 255 * maskAlpha
266
267 nSet[nSet == 0] = 1
268 for C in (0, 1, 2):
269 maskRGBA[:, :, C] //= nSet
270
271 rgb_img = RGBImage(data_np=maskRGBA)
272 Image = self._viewer.canvas.get_draw_class('image')
273 maskImageRGBA = Image(0, 0, rgb_img)
274
275 if "mask_overlay" in self._gingaViewer.canvas.get_tags():
276 self._gingaViewer.canvas.delete_object_by_tag("mask_overlay")
277 self._gingaViewer.canvas.add(maskImageRGBA, tag="mask_overlay")
278
279 def _buffer(self, enable=True):
280 self._redraw = not enable
281
282 def _flush(self):
283 self._gingaViewer.redraw(whence=3)
284
285 def _erase(self):
286 """Erase the display"""
287 self._canvas.delete_all_objects()
288
289 def _dot(self, symb, c, r, size, ctype, fontFamily="helvetica", textAngle=None, label='_dot'):
290 """Draw a symbol at (col,row) = (c,r) [0-based coordinates].
291
292 Parameters
293 ----------
294 symb : `str`
295 Symbol to draw. Should be one of '+', 'x', '*', 'o', '.'.
296 c : `int`
297 Image column for dot center (0-based coordinates).
298 r : `int`
299 Image row for dot center (0-based coordinate).
300 size : `int`
301 Size of dot.
302 fontFamily : `str`, optional
303 Font to use for text symbols.
304 textAngle : `float`, optional
305 Text rotation angle.
306 label : `str`, optional
307 Label to store this dot in the internal list.
308 """
309 dataTable = Table([{'x': c, 'y': r}])
310 if symb in '+x*.o':
311 self._viewer.marker = {'type': self.markerDict[symb], 'color': ctype, 'radius': size}
312 self._viewer.add_markers(dataTable, marker_name=label)
313 self._flush()
314 else:
315 Line = self._canvas.get_draw_class('line')
316 Text = self._canvas.get_draw_class('text')
317
318 for ds9Cmd in ds9Regions.dot(symb, c, r, size, fontFamily="helvetica", textAngle=None):
319 tmp = ds9Cmd.split('#')
320 cmd = tmp.pop(0).split()
321 comment = tmp.pop(0) if tmp else ""
322
323 cmd, args = cmd[0], cmd[1:]
324 if cmd == "line":
325 self._gingaViewer.canvas.add(Line(*[float(p) - 1 for p in args], color=ctype),
326 redraw=self._redraw)
327 elif cmd == "text":
328 x, y = [float(p) - 1 for p in args[0:2]]
329 self._gingaViewer.canvas.add(Text(x, y, symb, color=ctype), redraw=self._redraw)
330 else:
331 raise RuntimeError(ds9Cmd)
332 if comment:
333 print(comment)
334
335 def _drawLines(self, points, ctype):
336 """Connect the points, a list of (col,row).
337
338 Parameters
339 ----------
340 points : `list` [`tuple` [`int`, `int`]]
341 Points to connect with lines.
342 ctype : `str`
343 Color to use.
344 """
345 Line = self._gingaViewer.canvas.get_draw_class('line')
346 p0 = points[0]
347 for p in points[1:]:
348 self._gingaViewer.canvas.add(Line(p0[0], p0[1], p[0], p[1], color=ctype), redraw=self._redraw)
349 p0 = p
350
351 def beginMarking(self, symb='+', ctype='cyan', size=10, label='interactive'):
352 """Begin interactive mark adding.
353
354 Parameters
355 ----------
356 symb : `str`, optional
357 Symbol to use. Should be one of '+', 'x', '*', 'o', '.'.
358 ctype : `str`, optional
359 Color of markers.
360 size : `float`, optional
361 Size of marker.
362 label : `str`
363 Label to store this marker in the internal list.
364 """
365 self._viewer.start_marking(marker_name=label,
366 marker={'type': self.markerDict[symb], 'color': ctype, 'radius': size})
367
368 def endMarking(self):
369 """End interactive mark adding."""
370 self._viewer.stop_marking()
371
372 def getMarkers(self, label='interactive'):
373 """Get list of markers.
374
375 Parameters
376 ----------
377 label : `str`, optional
378 Marker label to return.
379
380 Returns
381 -------
382 table : `astropy.table.Table`
383 Table of markers with the given label.
384 """
385 return self._viewer.get_markers(marker_name=label)
386
387 def clearMarkers(self, label=None):
388 """Clear markers.
389
390 Parameters
391 ----------
392 label : `str`, optional
393 Marker label to clear. If None, all markers are cleared.
394 """
395 if label:
396 self._viewer.remove_markers(label)
397 else:
398 self._viewer.reset_markers()
399
400 def linkMarkers(self, ctype='brown', label='interactive'):
401 """Connect markers with lines.
402
403 Parameters
404 ----------
405 ctype : `str`, optional
406 Color to draw the lines.
407 label : `str`, optional
408 Marker label to connect. Lines are drawn in the order
409 found in the table.
410 """
411 Line = self._gingaViewer.canvas.get_draw_class('line')
412 table = self._viewer.get_markers(marker_name=label)
413
414 x0, y0 = (0, 0)
415 for rowCount, (x, y) in enumerate(table.iterrows('x', 'y')):
416 if rowCount != 0:
417 self._gingaViewer.canvas.add(Line(x0, y0, x, y, color=ctype), redraw=self._redraw)
418 x0 = x
419 y0 = y
420
421 def clearLines(self):
422 """Remove all lines from the display."""
423 self._gingaViewer.canvas.deleteObjects(list(self._gingaViewer.canvas.get_objects_by_kind('line')))
424
425 def _scale(self, algorithm, min, max, unit, *args, **kwargs):
426 """Set greyscale values.
427
428 Parameters
429 ----------
430 algorithm : `str`
431 Image scaling algorithm to use.
432 min : `float` or `str`
433 Minimum value to set to black. If a string, should be one of 'zscale' or 'minmax'.
434 max : `float`
435 Maximum value to set to white.
436 unit : `str`
437 Scaling units. This is ignored.
438 """
439 self._gingaViewer.set_color_map('gray')
440 self._gingaViewer.set_color_algorithm(algorithm)
441
442 if min == "zscale":
443 self._gingaViewer.set_autocut_params('zscale', contrast=0.25)
444 self._gingaViewer.auto_levels()
445 elif min == "minmax":
446 self._gingaViewer.set_autocut_params('minmax')
447 self._gingaViewer.auto_levels()
448 else:
449 if unit:
450 print("ginga: ignoring scale unit %s" % unit, file=sys.stderr)
451
452 self._gingaViewer.cut_levels(min, max)
453
454 def _show(self):
455 """Show the requested display.
456
457 In this case, embed it in the notebook (equivalent to
458 Display.get_viewer().show(); see also
459 Display.get_viewer().embed() N.b. These command *must* be the
460 last entry in their cell
461 """
462 return self._gingaViewer.show()
463
464 #
465 # Zoom and Pan
466 #
467 def _zoom(self, zoomfac):
468 """Zoom by specified amount
469
470 Parameters
471 ----------
472 zoomfac : `float`
473 Zoom factor to use.
474 """
475 self._gingaViewer.scale_to(zoomfac, zoomfac)
476
477 def _pan(self, colc, rowc):
478 """Pan to (colc, rowc)
479
480 Parameters
481 ----------
482 colc : `int`
483 Column to center in viewer (0-based coordinate).
484 rowc : `int`
485 Row to center in viewer (0-based coordinate).
486 """
487 self._gingaViewer.set_pan(colc, rowc)
488
489 def _getEvent(self):
490 """Listen for a key press on a frame in DS9 and return an event.
491
492 Returns
493 -------
494 event : `Ds9Event`
495 Event with (key, x, y).
496 """
497 pass
498
499
500# Copy ginga's WCS implementation
501class WcsAdaptorForGinga(AstropyWCS):
502 """A class to adapt the LSST Wcs class for Ginga.
503
504 This was taken largely from the afw.display.ginga package.
505
506 Parameters
507 ----------
508 wcs : `ginga.util.wcsmod.wcs_astropy`
509 WCS to adapt for Ginga.
510 """
511 def __init__(self, wcs):
512 self._wcs = wcs
513
514 def pixtoradec(self, idxs, coords='data'):
515 """Return (ra, dec) in degrees given a position in pixels.
516
517 Parameters
518 ----------
519 idxs : `list` [`tuple` [`float`, `float`]]
520 Pixel locations to convert.
521 coords : `str`, optional
522 This parameter is ignored.
523 Returns
524 -------
525 ra : `list`
526 RA position in degrees.
527 dec : `list`
528 DEC position in degrees.
529 """
530 ra, dec = self._wcs.pixelToSky(*idxs)
531
532 return ra.asDegrees(), dec.asDegrees()
533
534 def pixtosystem(self, idxs, system=None, coords='data'):
535 """Return (ra, dec) in degrees given a position in pixels.
536
537 Parameters
538 ----------
539 idxs : `list` [`tuple` [`float`, `float`]]
540 Pixel locations to convert.
541 system : `str`, optional
542 This parameter is ignored.
543 coords : `str`, optional
544 This parameter is ignored.
545
546 Returns
547 -------
548 ra : `list`
549 RA position in degrees.
550 dec : `list`
551 DEC position in degrees.
552 """
553 return self.pixtoradec(idxs, coords=coords)
554
555 def radectopix(self, ra_deg, dec_deg, coords='data', naxispath=None):
556 """Return (x, y) in pixels given (ra, dec) in degrees
557
558 Parameters
559 ----------
560 ra_deg : `list` [`float`]
561 RA position in degrees.
562 dec_deg : `list` [`float`]
563 DEC position in degrees.
564 coords : `str`, optional
565 This parameter is ignored.
566 naxispath : `str`, optional
567 This parameter is ignored.
568
569 Returns
570 -------
571 out : `tuple` [`list` [`float, `float`]]
572 Image coordates for input positions.
573 """
574 return self._wcs.skyToPixel(ra_deg*afwGeom.degrees, dec_deg*afwGeom.degrees)
575
576 def all_pix2world(self, *args, **kwargs):
577 out = []
578 print(f"{args}")
579 for pos in args[0]:
580 r, d = self.pixtoradec(pos)
581 out.append([r, d])
582 return tuple(out)
583
584 def datapt_to_wcspt(self, *args):
585 return (0.0, 0.0)
586
587 def wcspt_to_datapt(self, *args):
588 return (0.0, 0.0)
__init__(self, display, dims=None, use_opencv=False, verbose=False, *args, **kwargs)