LSST Applications g070148d5b3+33e5256705,g0d53e28543+25c8b88941,g0da5cf3356+2dd1178308,g1081da9e2a+62d12e78cb,g17e5ecfddb+7e422d6136,g1c76d35bf8+ede3a706f7,g295839609d+225697d880,g2e2c1a68ba+cc1f6f037e,g2ffcdf413f+853cd4dcde,g38293774b4+62d12e78cb,g3b44f30a73+d953f1ac34,g48ccf36440+885b902d19,g4b2f1765b6+7dedbde6d2,g5320a0a9f6+0c5d6105b6,g56b687f8c9+ede3a706f7,g5c4744a4d9+ef6ac23297,g5ffd174ac0+0c5d6105b6,g6075d09f38+66af417445,g667d525e37+2ced63db88,g670421136f+2ced63db88,g71f27ac40c+2ced63db88,g774830318a+463cbe8d1f,g7876bc68e5+1d137996f1,g7985c39107+62d12e78cb,g7fdac2220c+0fd8241c05,g96f01af41f+368e6903a7,g9ca82378b8+2ced63db88,g9d27549199+ef6ac23297,gabe93b2c52+e3573e3735,gb065e2a02a+3dfbe639da,gbc3249ced9+0c5d6105b6,gbec6a3398f+0c5d6105b6,gc9534b9d65+35b9f25267,gd01420fc67+0c5d6105b6,geee7ff78d7+a14128c129,gf63283c776+ede3a706f7,gfed783d017+0c5d6105b6,w.2022.47
LSST Data Management Base Package
Loading...
Searching...
No Matches
firefly.py
Go to the documentation of this file.
2# LSST Data Management System
3# Copyright 2008, 2009, 2010, 2015 LSST Corporation.
4#
5# This product includes software developed by the
6# LSST Project (http://www.lsst.org/).
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the LSST License Statement and
19# the GNU General Public License along with this program. If not,
20# see <http://www.lsstcorp.org/LegalNotices/>.
21#
22
23from io import BytesIO
24from socket import gaierror
25import tempfile
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.display as afwDisplay
31import lsst.afw.math as afwMath
32import lsst.log
33
34from .footprints import createFootprintsTable
35
36try:
37 import firefly_client
38 _fireflyClient = None
39except ImportError as e:
40 raise RuntimeError("Cannot import firefly_client: %s" % (e))
41from ws4py.client import HandshakeError
42
43
44class FireflyError(Exception):
45
46 def __init__(self, str):
47 Exception.__init__(self, str)
48
49
51 """Return the version of firefly_client in use, as a string"""
52 return(firefly_client.__version__)
53
54
55class DisplayImpl(virtualDevice.DisplayImpl):
56 """Device to talk to a firefly display"""
57
58 @staticmethod
59 def __handleCallbacks(event):
60 if 'type' in event['data']:
61 if event['data']['type'] == 'AREA_SELECT':
62 lsst.log.debug('*************area select')
63 pParams = {'URL': 'http://web.ipac.caltech.edu/staff/roby/demo/wise-m51-band2.fits',
64 'ColorTable': '9'}
65 plot_id = 3
66 global _fireflyClient
67 _fireflyClient.show_fits(fileOnServer=None, plot_id=plot_id, additionalParams=pParams)
68
69 lsst.log.debug("Callback event info: {}".format(event))
70 return
71 data = dict((_.split('=') for _ in event.get('data', {}).split('&')))
72 if data.get('type') == "POINT":
73 lsst.log.debug("Event Received: %s" % data.get('id'))
74
75 def __init__(self, display, verbose=False, url=None,
76 name=None, *args, **kwargs):
77 virtualDevice.DisplayImpl.__init__(self, display, verbose)
78
79 if self.verbose:
80 print("Opening firefly device %s" % (self.display.frame if self.display else "[None]"))
81
82 global _fireflyClient
83 if not _fireflyClient:
84 import os
85 start_tab = None
86 html_file = kwargs.get('html_file',
87 os.environ.get('FIREFLY_HTML', 'slate.html'))
88 if url is None:
89 if (('fireflyLabExtension' in os.environ) and
90 ('fireflyURLLab' in os.environ)):
91 url = os.environ['fireflyURLLab']
92 start_tab = kwargs.get('start_tab', True)
93 start_browser_tab = kwargs.get('start_browser_tab', False)
94 if (name is None) and ('fireflyChannelLab' in os.environ):
95 name = os.environ['fireflyChannelLab']
96 elif 'FIREFLY_URL' in os.environ:
97 url = os.environ['FIREFLY_URL']
98 else:
99 raise RuntimeError('Cannot determine url from environment; you must pass url')
100
101 token = kwargs.get('token',
102 os.environ.get('ACCESS_TOKEN', None))
103
104 try:
105 if start_tab:
106 if verbose:
107 print('Starting Jupyterlab client')
108 _fireflyClient = firefly_client.FireflyClient.make_lab_client(
109 start_tab=True, start_browser_tab=start_browser_tab,
110 html_file=kwargs.get('html_file'), verbose=verbose,
111 token=token)
112
113 else:
114 if verbose:
115 print('Starting vanilla client')
116 _fireflyClient = firefly_client.FireflyClient.make_client(
117 url=url, html_file=html_file, launch_browser=True,
118 channel_override=name, verbose=verbose,
119 token=token)
120
121 except (HandshakeError, gaierror) as e:
122 raise RuntimeError("Unable to connect to %s: %s" % (url or '', e))
123
124 try:
125 _fireflyClient.add_listener(self.__handleCallbacks)
126 except Exception as e:
127 raise RuntimeError("Cannot add listener. Browser must be connected" +
128 "to %s: %s" %
129 (_fireflyClient.get_firefly_url(), e))
130
131 self._isBuffered = False
132 self._regions = []
134 self._fireflyFitsID = None
135 self._fireflyMaskOnServer = None
136 self._client = _fireflyClient
137 self._channel = _fireflyClient.channel
138 self._url = _fireflyClient.get_firefly_url()
139 self._maskIds = []
140 self._maskDict = {}
141 self._maskPlaneColors = {}
142 self._maskTransparencies = {}
143 self._lastZoom = None
144 self._lastPan = None
145 self._lastStretch = None
146
147 def _getRegionLayerId(self):
148 return "lsstRegions%s" % self.display.frame if self.display else "None"
149
150 def _clearImage(self):
151 """Delete the current image in the Firefly viewer
152 """
153 self._client.dispatch(action_type='ImagePlotCntlr.deletePlotView',
154 payload=dict(plotId=str(self.display.frame)))
155
156 def _mtv(self, image, mask=None, wcs=None, title=""):
157 """Display an Image and/or Mask on a Firefly display
158 """
159 if title == "":
160 title = str(self.display.frame)
161 if image:
162 if self.verbose:
163 print('displaying image')
164 self._erase()
165
166 with tempfile.NamedTemporaryFile() as fd:
167 afwDisplay.writeFitsImage(fd.name, image, wcs, title)
168 fd.flush()
169 fd.seek(0, 0)
170 self._fireflyFitsID = _fireflyClient.upload_data(fd, 'FITS')
171
172 try:
173 viewer_id = ('image-' + str(_fireflyClient.render_tree_id) + '-' +
174 str(self.frame))
175 except AttributeError:
176 viewer_id = 'image-' + str(self.frame)
177 extraParams = dict(Title=title,
178 MultiImageIdx=0,
179 PredefinedOverlayIds=' ',
180 viewer_id=viewer_id)
181 # Firefly's Javascript API requires a space for parameters;
182 # otherwise the parameter will be ignored
183
184 if self._lastZoom:
185 extraParams['InitZoomLevel'] = self._lastZoom
186 extraParams['ZoomType'] = 'LEVEL'
187 if self._lastPan:
188 extraParams['InitialCenterPosition'] = '{0:.3f};{1:.3f};PIXEL'.format(
189 self._lastPan[0], self._lastPan[1])
190 if self._lastStretch:
191 extraParams['RangeValues'] = self._lastStretch
192
193 ret = _fireflyClient.show_fits(self._fireflyFitsID, plot_id=str(self.display.frame),
194 **extraParams)
195
196 if not ret["success"]:
197 raise RuntimeError("Display of image failed")
198
199 if mask:
200 if self.verbose:
201 print('displaying mask')
202 with tempfile.NamedTemporaryFile() as fdm:
203 afwDisplay.writeFitsImage(fdm.name, mask, wcs, title)
204 fdm.flush()
205 fdm.seek(0, 0)
206 self._fireflyMaskOnServer = _fireflyClient.upload_data(fdm, 'FITS')
207
208 maskPlaneDict = mask.getMaskPlaneDict()
209 for k, v in maskPlaneDict.items():
210 self._maskDict[k] = v
211 self._maskPlaneColors[k] = self.display.getMaskPlaneColor(k)
212 usedPlanes = int(afwMath.makeStatistics(mask, afwMath.SUM).getValue())
213 for k in self._maskDict:
214 if (((1 << self._maskDict[k]) & usedPlanes) and
215 (k in self._maskPlaneColors) and
216 (self._maskPlaneColors[k] is not None) and
217 (self._maskPlaneColors[k].lower() != 'ignore')):
218 _fireflyClient.add_mask(bit_number=self._maskDict[k],
219 image_number=0,
220 plot_id=str(self.display.frame),
221 mask_id=k,
222 title=k + ' - bit %d'%self._maskDict[k],
223 color=self._maskPlaneColors[k],
224 file_on_server=self._fireflyMaskOnServer)
225 if k in self._maskTransparencies:
227 self._maskIds.append(k)
228
229 def _remove_masks(self):
230 """Remove mask layers"""
231 for k in self._maskIds:
232 _fireflyClient.remove_mask(plot_id=str(self.display.frame), mask_id=k)
233 self._maskIds = []
234
235 def _buffer(self, enable=True):
236 """!Enable or disable buffering of writes to the display
237 param enable True or False, as appropriate
238 """
239 self._isBuffered = enable
240
241 def _flush(self):
242 """!Flush any I/O buffers
243 """
244 if not self._regions:
245 return
246
247 if self.verbose:
248 print("Flushing %d regions" % len(self._regions))
249 print(self._regions)
250
252 _fireflyClient.add_region_data(region_data=self._regions, plot_id=str(self.display.frame),
253 region_layer_id=self._regionLayerId)
254 self._regions = []
255
256 def _uploadTextData(self, regions):
257 self._regions += regions
258
259 if not self._isBuffered:
260 self._flush()
261
262 def _close(self):
263 """Called when the device is closed"""
264 if self.verbose:
265 print("Closing firefly device %s" % (self.display.frame if self.display else "[None]"))
266 if _fireflyClient is not None:
267 _fireflyClient.disconnect()
268 _fireflyClient.session.close()
269
270 def _dot(self, symb, c, r, size, ctype, fontFamily="helvetica", textAngle=None):
271 """Draw a symbol onto the specified DS9 frame at (col,row) = (c,r) [0-based coordinates]
272 Possible values are:
273 + Draw a +
274 x Draw an x
275 * Draw a *
276 o Draw a circle
277 @:Mxx,Mxy,Myy Draw an ellipse with moments (Mxx, Mxy, Myy) (argument size is ignored)
278 An object derived from afwGeom.ellipses.BaseCore Draw the ellipse (argument size is ignored)
279 Any other value is interpreted as a string to be drawn. Strings obey the fontFamily (which may be extended
280 with other characteristics, e.g. "times bold italic". Text will be drawn rotated by textAngle (textAngle
281 is ignored otherwise).
282
283 N.b. objects derived from BaseCore include Axes and Quadrupole.
284 """
285 self._uploadTextData(ds9Regions.dot(symb, c, r, size, ctype, fontFamily, textAngle))
286
287 def _drawLines(self, points, ctype):
288 """Connect the points, a list of (col,row)
289 Ctype is the name of a colour (e.g. 'red')"""
290
291 self._uploadTextData(ds9Regions.drawLines(points, ctype))
292
293 def _erase(self):
294 """Erase all overlays on the image"""
295 if self.verbose:
296 print('region layer id is {}'.format(self._regionLayerId))
297 if self._regionLayerId:
298 _fireflyClient.delete_region_layer(self._regionLayerId, plot_id=str(self.display.frame))
299
300 def _setCallback(self, what, func):
301 if func != interface.noop_callback:
302 try:
303 status = _fireflyClient.add_extension('POINT' if False else 'AREA_SELECT', title=what,
304 plot_id=str(self.display.frame),
305 extension_id=what)
306 if not status['success']:
307 pass
308 except Exception as e:
309 raise RuntimeError("Cannot set callback. Browser must be (re)opened " +
310 "to %s%s : %s" %
311 (_fireflyClient.url_bw,
312 _fireflyClient.channel, e))
313
314 def _getEvent(self):
315 """Return an event generated by a keypress or mouse click
316 """
317 ev = interface.Event("q")
318
319 if self.verbose:
320 print("virtual[%s]._getEvent() -> %s" % (self.display.frame, ev))
321
322 return ev
323 #
324 # Set gray scale
325 #
326
327 def _scale(self, algorithm, min, max, unit=None, *args, **kwargs):
328 """Scale the image stretch and limits
329
330 Parameters:
331 -----------
332 algorithm : `str`
333 stretch algorithm, e.g. 'linear', 'log', 'loglog', 'equal', 'squared',
334 'sqrt', 'asinh', powerlaw_gamma'
335 min : `float` or `str`
336 lower limit, or 'minmax' for full range, or 'zscale'
337 max : `float` or `str`
338 upper limit; overrriden if min is 'minmax' or 'zscale'
339 unit : `str`
340 unit for min and max. 'percent', 'absolute', 'sigma'.
341 if not specified, min and max are presumed to be in 'absolute' units.
342
343 *args, **kwargs : additional position and keyword arguments.
344 The options are shown below:
345
346 **Q** : `float`, optional
347 The asinh softening parameter for asinh stretch.
348 Use Q=0 for linear stretch, increase Q to make brighter features visible.
349 When not specified or None, Q is calculated by Firefly to use full color range.
350 **gamma**
351 The gamma value for power law gamma stretch (default 2.0)
352 **zscale_contrast** : `int`, optional
353 Contrast parameter in percent for zscale algorithm (default 25)
354 **zscale_samples** : `int`, optional
355 Number of samples for zscale algorithm (default 600)
356 **zscale_samples_perline** : `int`, optional
357 Number of samples per line for zscale algorithm (default 120)
358 """
359 stretch_algorithms = ('linear', 'log', 'loglog', 'equal', 'squared', 'sqrt',
360 'asinh', 'powerlaw_gamma')
361 interval_methods = ('percent', 'maxmin', 'absolute', 'zscale', 'sigma')
362 #
363 #
364 # Normalise algorithm's case
365 #
366 if algorithm:
367 algorithm = dict((a.lower(), a) for a in stretch_algorithms).get(algorithm.lower(), algorithm)
368
369 if algorithm not in stretch_algorithms:
370 raise FireflyError('Algorithm %s is invalid; please choose one of "%s"' %
371 (algorithm, '", "'.join(stretch_algorithms)))
372 self._stretchAlgorithm = algorithm
373 else:
374 algorithm = 'linear'
375
376 # Translate parameters for asinh and powerlaw_gamma stretches
377 if 'Q' in kwargs:
378 kwargs['asinh_q_value'] = kwargs['Q']
379 del kwargs['Q']
380
381 if 'gamma' in kwargs:
382 kwargs['gamma_value'] = kwargs['gamma']
383 del kwargs['gamma']
384
385 if min == 'minmax':
386 interval_type = 'percent'
387 unit = 'percent'
388 min, max = 0, 100
389 elif min == 'zscale':
390 interval_type = 'zscale'
391 else:
392 interval_type = None
393
394 if not unit:
395 unit = 'absolute'
396
397 units = ('percent', 'absolute', 'sigma')
398 if unit not in units:
399 raise FireflyError('Unit %s is invalid; please choose one of "%s"' % (unit, '", "'.join(units)))
400
401 if unit == 'sigma':
402 interval_type = 'sigma'
403 elif unit == 'absolute' and interval_type is None:
404 interval_type = 'absolute'
405 elif unit == 'percent':
406 interval_type = 'percent'
407
408 self._stretchMin = min
409 self._stretchMax = max
410 self._stretchUnit = unit
411
412 if interval_type not in interval_methods:
413 raise FireflyError('Interval method %s is invalid' % interval_type)
414
415 rval = {}
416 if interval_type != 'zscale':
417 rval = _fireflyClient.set_stretch(str(self.display.frame), stype=interval_type,
418 algorithm=algorithm, lower_value=min,
419 upper_value=max, **kwargs)
420 else:
421 if 'zscale_contrast' not in kwargs:
422 kwargs['zscale_contrast'] = 25
423 if 'zscale_samples' not in kwargs:
424 kwargs['zscale_samples'] = 600
425 if 'zscale_samples_perline' not in kwargs:
426 kwargs['zscale_samples_perline'] = 120
427 rval = _fireflyClient.set_stretch(str(self.display.frame), stype='zscale',
428 algorithm=algorithm, **kwargs)
429
430 if 'rv_string' in rval:
431 self._lastStretch = rval['rv_string']
432
433 def _setMaskTransparency(self, transparency, maskName):
434 """Specify mask transparency (percent); or None to not set it when loading masks"""
435 if maskName is not None:
436 masklist = [maskName]
437 else:
438 masklist = set(self._maskIds + list(self.display._defaultMaskPlaneColor.keys()))
439 for k in masklist:
440 self._maskTransparencies[k] = transparency
441 _fireflyClient.dispatch(action_type='ImagePlotCntlr.overlayPlotChangeAttributes',
442 payload={'plotId': str(self.display.frame),
443 'imageOverlayId': k,
444 'attributes': {'opacity': 1.0 - transparency/100.},
445 'doReplot': False})
446
447 def _getMaskTransparency(self, maskName):
448 """Return the current mask's transparency"""
449 transparency = None
450 if maskName in self._maskTransparencies:
451 transparency = self._maskTransparencies[maskName]
452 return transparency
453
454 def _setMaskPlaneColor(self, maskName, color):
455 """Specify mask color """
456 _fireflyClient.remove_mask(plot_id=str(self.display.frame),
457 mask_id=maskName)
458 self._maskPlaneColors[maskName] = color
459 if (color.lower() != 'ignore'):
460 _fireflyClient.add_mask(bit_number=self._maskDict[maskName],
461 image_number=1,
462 plot_id=str(self.display.frame),
463 mask_id=maskName,
464 color=self.display.getMaskPlaneColor(maskName),
465 file_on_server=self._fireflyFitsID)
466
467 def _show(self):
468 """Show the requested window"""
469 if self._client.render_tree_id is not None:
470 # we are using Jupyterlab
471 self._client.dispatch(self._client.ACTION_DICT['StartLabWindow'],
472 {})
473 else:
474 localbrowser, url = _fireflyClient.launch_browser(verbose=self.verbose)
475 if not localbrowser and not self.verbose:
476 _fireflyClient.display_url()
477
478 #
479 # Zoom and Pan
480 #
481
482 def _zoom(self, zoomfac):
483 """Zoom display by specified amount
484
485 Parameters:
486 -----------
487 zoomfac: `float`
488 zoom level in screen pixels per image pixel
489 """
490 self._lastZoom = zoomfac
491 _fireflyClient.set_zoom(plot_id=str(self.display.frame), factor=zoomfac)
492
493 def _pan(self, colc, rowc):
494 """Pan to specified pixel coordinates
495
496 Parameters:
497 -----------
498 colc, rowc : `float`
499 column and row in units of pixels (zero-based convention,
500 with the xy0 already subtracted off)
501 """
502 self._lastPan = [colc+0.5, rowc+0.5] # saved for future use in _mtv
503 # Firefly's internal convention is first pixel is (0.5, 0.5)
504 _fireflyClient.set_pan(plot_id=str(self.display.frame), x=colc, y=rowc)
505
506 # Extensions to the API that are specific to using the Firefly backend
507
508 def getClient(self):
509 """Get the instance of FireflyClient for this display
510
511 Returns:
512 --------
513 `firefly_client.FireflyClient`
514 Instance of FireflyClient used by this display
515 """
516 return self._client
517
518 def clearViewer(self):
519 """Reinitialize the viewer
520 """
521 self._client.reinit_viewer()
522
523 def resetLayout(self):
524 """Reset the layout of the Firefly Slate browser
525
526 Clears the display and adds Slate cells to display image in upper left,
527 plot area in upper right, and plots stretch across the bottom
528 """
529 self.clearViewer()
530 try:
531 tables_cell_id = 'tables-' + str(_fireflyClient.render_tree_id)
532 except AttributeError:
533 tables_cell_id = 'tables'
534 self._client.add_cell(row=2, col=0, width=4, height=2, element_type='tables',
535 cell_id=tables_cell_id)
536 try:
537 image_cell_id = ('image-' + str(_fireflyClient.render_tree_id) + '-' +
538 str(self.frame))
539 except AttributeError:
540 image_cell_id = 'image-' + str(self.frame)
541 self._client.add_cell(row=0, col=0, width=2, height=3, element_type='images',
542 cell_id=image_cell_id)
543 try:
544 plots_cell_id = 'plots-' + str(_fireflyClient.render_tree_id)
545 except AttributeError:
546 plots_cell_id = 'plots'
547 self._client.add_cell(row=0, col=2, width=2, height=3, element_type='xyPlots',
548 cell_id=plots_cell_id)
549
550 def overlayFootprints(self, catalog, color='rgba(74,144,226,0.60)',
551 highlightColor='cyan', selectColor='orange',
552 style='fill', layerString='detection footprints ',
553 titleString='catalog footprints '):
554 """Overlay outlines of footprints from a catalog
555
556 Overlay outlines of LSST footprints from the input catalog. The colors
557 and style can be specified as parameters, and the base color and style
558 can be changed in the Firefly browser user interface.
559
560 Parameters:
561 -----------
563 Source catalog from which to display footprints.
564 color : `str`
565 Color for footprints overlay. Colors can be specified as a name
566 like 'cyan' or afwDisplay.RED; as an rgb value such as
567 'rgb(80,100,220)'; or as rgb plus alpha (transparency) such
568 as 'rgba('74,144,226,0.60)'.
569 highlightColor : `str`
570 Color for highlighted footprints
571 selectColor : `str`
572 Color for selected footprints
573 style : {'fill', 'outline'}
574 Style of footprints display, filled or outline
575 insertColumn : `int`
576 Column at which to insert the "family_id" and "category" columns
577 layerString: `str`
578 Name of footprints layer string, to concatenate with the frame
579 Re-using the layer_string will overwrite the previous table and
580 footprints
581 titleString: `str`
582 Title of catalog, to concatenate with the frame
583 """
584 footprintTable = createFootprintsTable(catalog)
585 with BytesIO() as fd:
586 footprintTable.to_xml(fd)
587 tableval = self._client.upload_data(fd, 'UNKNOWN')
588 self._client.overlay_footprints(footprint_file=tableval,
589 title=titleString + str(self.display.frame),
590 footprint_layer_id=layerString + str(self.display.frame),
591 plot_id=str(self.display.frame),
592 color=color,
593 highlightColor=highlightColor,
594 selectColor=selectColor,
595 style=style)
def __init__(self, display, verbose=False, url=None, name=None, *args, **kwargs)
Definition: firefly.py:76
def _setMaskTransparency(self, transparency, maskName)
Definition: firefly.py:433
def overlayFootprints(self, catalog, color='rgba(74, 144, 226, 0.60)', highlightColor='cyan', selectColor='orange', style='fill', layerString='detection footprints ', titleString='catalog footprints ')
Definition: firefly.py:553
daf::base::PropertyList * list
Definition: fits.cc:928
daf::base::PropertySet * set
Definition: fits.cc:927
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)
Definition: Statistics.h:361
Definition: Log.h:717