LSSTApplications  16.0-11-g09ed895+2,16.0-11-g12e47bd,16.0-11-g9bb73b2+6,16.0-12-g5c924a4+6,16.0-14-g9a974b3+1,16.0-15-g1417920+1,16.0-15-gdd5ca33+1,16.0-16-gf0259e2,16.0-17-g31abd91+7,16.0-17-g7d7456e+7,16.0-17-ga3d2e9f+13,16.0-18-ga4d4bcb+1,16.0-18-gd06566c+1,16.0-2-g0febb12+21,16.0-2-g9d5294e+69,16.0-2-ga8830df+6,16.0-20-g21842373+7,16.0-24-g3eae5ec,16.0-28-gfc9ea6c+4,16.0-29-ge8801f9,16.0-3-ge00e371+34,16.0-4-g18f3627+13,16.0-4-g5f3a788+20,16.0-4-ga3eb747+10,16.0-4-gabf74b7+29,16.0-4-gb13d127+6,16.0-49-g42e581f7+6,16.0-5-g27fb78a+7,16.0-5-g6a53317+34,16.0-5-gb3f8a4b+87,16.0-6-g9321be7+4,16.0-6-gcbc7b31+42,16.0-6-gf49912c+29,16.0-7-gd2eeba5+51,16.0-71-ge89f8615e,16.0-8-g21fd5fe+29,16.0-8-g3a9f023+20,16.0-8-g4734f7a+1,16.0-8-g5858431+3,16.0-9-gf5c1f43+8,master-gd73dc1d098+1,w.2019.01
LSSTDataManagementBasePackage
firefly.py
Go to the documentation of this file.
1 #
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 
23 from __future__ import absolute_import, division, print_function
24 from past.builtins import long
25 
26 from io import BytesIO
27 from socket import gaierror
28 import tempfile
29 
30 import lsst.afw.display.interface as interface
31 import lsst.afw.display.virtualDevice as virtualDevice
32 import lsst.afw.display.ds9Regions as ds9Regions
33 import lsst.afw.display.displayLib as displayLib
34 import lsst.afw.math as afwMath
35 import lsst.log
36 
37 from .footprints import createFootprintsTable
38 
39 try:
40  import firefly_client
41  _fireflyClient = None
42 except ImportError as e:
43  raise RuntimeError("Cannot import firefly_client: %s" % (e))
44 from ws4py.client import HandshakeError
45 
46 
47 class FireflyError(Exception):
48 
49  def __init__(self, str):
50  Exception.__init__(self, str)
51 
52 
54  """Return the version of firefly_client in use, as a string"""
55  return(firefly_client.__version__)
56 
57 
58 class DisplayImpl(virtualDevice.DisplayImpl):
59  """Device to talk to a firefly display"""
60 
61  @staticmethod
62  def __handleCallbacks(event):
63  if 'type' in event['data']:
64  if event['data']['type'] == 'AREA_SELECT':
65  lsst.log.debug('*************area select')
66  pParams = {'URL': 'http://web.ipac.caltech.edu/staff/roby/demo/wise-m51-band2.fits',
67  'ColorTable': '9'}
68  plot_id = 3
69  global _fireflyClient
70  _fireflyClient.show_fits(fileOnServer=None, plot_id=plot_id, additionalParams=pParams)
71 
72  lsst.log.debug("Callback event info: {}".format(event))
73  return
74  data = dict((_.split('=') for _ in event.get('data', {}).split('&')))
75  if data.get('type') == "POINT":
76  lsst.log.debug("Event Received: %s" % data.get('id'))
77 
78  def __init__(self, display, verbose=False, url=None,
79  name=None, *args, **kwargs):
80  virtualDevice.DisplayImpl.__init__(self, display, verbose)
81 
82  if self.verbose:
83  print("Opening firefly device %s" % (self.display.frame if self.display else "[None]"))
84 
85  global _fireflyClient
86  if not _fireflyClient:
87  import os
88  if ('html_file' not in kwargs) and ('FIREFLY_HTML' not in os.environ):
89  kwargs['html_file'] = 'slate.html'
90  try:
91  if url is None:
92  _fireflyClient = firefly_client.FireflyClient(channel=name, **kwargs)
93  else:
94  _fireflyClient = firefly_client.FireflyClient(url,
95  channel=name, **kwargs)
96  except (HandshakeError, gaierror) as e:
97  raise RuntimeError("Unable to connect to %s: %s" % (url or '', e))
98 
99  global localbrowser
100  localbrowser, browser_url = _fireflyClient.launch_browser(verbose=self.verbose)
101  if not localbrowser and not self.verbose:
102  _fireflyClient.display_url()
103  if self.verbose:
104  print('localbrowser: ', localbrowser, ' browser_url: ', browser_url)
105  try:
106  _fireflyClient.add_listener(self.__handleCallbacks)
107  except Exception as e:
108  raise RuntimeError("Cannot add listener. Browser must be connected" +
109  "to %s: %s" %
110  (_fireflyClient.get_firefly_url(), e))
111 
112  self._isBuffered = False
113  self._regions = []
114  self._regionLayerId = self._getRegionLayerId()
115  self._fireflyFitsID = None
116  self._fireflyMaskOnServer = None
117  self._client = _fireflyClient
118  self._channel = _fireflyClient.channel
119  self._url = _fireflyClient.get_firefly_url()
120  self._localbrowser = localbrowser
121  self._maskIds = []
122  self._maskDict = {}
123  self._maskPlaneColors = {}
124  self._maskTransparencies = {}
125  self._lastZoom = None
126  self._lastPan = None
127  self._lastStretch = None
128 
129  def _getRegionLayerId(self):
130  return "lsstRegions%s" % self.display.frame if self.display else "None"
131 
132  def _clearImage(self):
133  """Delete the current image in the Firefly viewer
134  """
135  self._client.dispatch_remote_action(channel=self._client.channel,
136  action_type='ImagePlotCntlr.deletePlotView',
137  payload=dict(plotId=str(self.display.frame)))
138 
139  def _mtv(self, image, mask=None, wcs=None, title=""):
140  """Display an Image and/or Mask on a Firefly display
141  """
142  if title == "":
143  title = str(self.display.frame)
144  if image:
145  if self.verbose:
146  print('displaying image')
147  self._erase()
148 
149  with tempfile.NamedTemporaryFile() as fd:
150  displayLib.writeFitsImage(fd.name, image, wcs, title)
151  fd.flush()
152  fd.seek(0, 0)
153  self._fireflyFitsID = _fireflyClient.upload_data(fd, 'FITS')
154 
155  extraParams = dict(Title=title,
156  MultiImageIdx=0,
157  PredefinedOverlayIds=' ',
158  viewer_id='image-' + str(self.frame))
159  # Firefly's Javascript API requires a space for parameters;
160  # otherwise the parameter will be ignored
161 
162  if self._lastZoom:
163  extraParams['InitZoomLevel'] = self._lastZoom
164  extraParams['ZoomType'] = 'LEVEL'
165  if self._lastPan:
166  extraParams['InitialCenterPosition'] = '{0:.3f};{1:.3f};PIXEL'.format(
167  self._lastPan[0], self._lastPan[1])
168  if self._lastStretch:
169  extraParams['RangeValues'] = self._lastStretch
170 
171  ret = _fireflyClient.show_fits(self._fireflyFitsID, plot_id=str(self.display.frame),
172  **extraParams)
173 
174  if not ret["success"]:
175  raise RuntimeError("Display of image failed")
176 
177  if mask:
178  if self.verbose:
179  print('displaying mask')
180  with tempfile.NamedTemporaryFile() as fdm:
181  displayLib.writeFitsImage(fdm.name, mask, wcs, title)
182  fdm.flush()
183  fdm.seek(0, 0)
184  self._fireflyMaskOnServer = _fireflyClient.upload_data(fdm, 'FITS')
185 
186  maskPlaneDict = mask.getMaskPlaneDict()
187  for k, v in maskPlaneDict.items():
188  self._maskDict[k] = v
189  self._maskPlaneColors[k] = self.display.getMaskPlaneColor(k)
190  usedPlanes = long(afwMath.makeStatistics(mask, afwMath.SUM).getValue())
191  for k in self._maskDict:
192  if (((1 << self._maskDict[k]) & usedPlanes) and
193  (k in self._maskPlaneColors) and
194  (self._maskPlaneColors[k] is not None) and
195  (self._maskPlaneColors[k].lower() != 'ignore')):
196  _fireflyClient.add_mask(bit_number=self._maskDict[k],
197  image_number=0,
198  plot_id=str(self.display.frame),
199  mask_id=k,
200  title=k + ' - bit %d'%self._maskDict[k],
201  color=self._maskPlaneColors[k],
202  file_on_server=self._fireflyMaskOnServer)
203  if k in self._maskTransparencies:
205  self._maskIds.append(k)
206 
207  def _remove_masks(self):
208  """Remove mask layers"""
209  for k in self._maskIds:
210  _fireflyClient.remove_mask(plot_id=str(self.display.frame), mask_id=k)
211  self._maskIds = []
212 
213  def _buffer(self, enable=True):
214  """!Enable or disable buffering of writes to the display
215  param enable True or False, as appropriate
216  """
217  self._isBuffered = enable
218 
219  def _flush(self):
220  """!Flush any I/O buffers
221  """
222  if not self._regions:
223  return
224 
225  if self.verbose:
226  print("Flushing %d regions" % len(self._regions))
227  print(self._regions)
228 
229  self._regionLayerId = self._getRegionLayerId()
230  _fireflyClient.add_region_data(region_data=self._regions, plot_id=str(self.display.frame),
231  region_layer_id=self._regionLayerId)
232  self._regions = []
233 
234  def _uploadTextData(self, regions):
235  self._regions += regions
236 
237  if not self._isBuffered:
238  self._flush()
239 
240  def _close(self):
241  """Called when the device is closed"""
242  if self.verbose:
243  print("Closing firefly device %s" % (self.display.frame if self.display else "[None]"))
244  if _fireflyClient is not None:
245  _fireflyClient.disconnect()
246  _fireflyClient.session.close()
247 
248  def _dot(self, symb, c, r, size, ctype, fontFamily="helvetica", textAngle=None):
249  """Draw a symbol onto the specified DS9 frame at (col,row) = (c,r) [0-based coordinates]
250  Possible values are:
251  + Draw a +
252  x Draw an x
253  * Draw a *
254  o Draw a circle
255  @:Mxx,Mxy,Myy Draw an ellipse with moments (Mxx, Mxy, Myy) (argument size is ignored)
256  An object derived from afwGeom.ellipses.BaseCore Draw the ellipse (argument size is ignored)
257  Any other value is interpreted as a string to be drawn. Strings obey the fontFamily (which may be extended
258  with other characteristics, e.g. "times bold italic". Text will be drawn rotated by textAngle (textAngle
259  is ignored otherwise).
260 
261  N.b. objects derived from BaseCore include Axes and Quadrupole.
262  """
263  self._uploadTextData(ds9Regions.dot(symb, c, r, size, ctype, fontFamily, textAngle))
264 
265  def _drawLines(self, points, ctype):
266  """Connect the points, a list of (col,row)
267  Ctype is the name of a colour (e.g. 'red')"""
268 
269  self._uploadTextData(ds9Regions.drawLines(points, ctype))
270 
271  def _erase(self):
272  """Erase all overlays on the image"""
273  if self.verbose:
274  print('region layer id is {}'.format(self._regionLayerId))
275  if self._regionLayerId:
276  _fireflyClient.delete_region_layer(self._regionLayerId, plot_id=str(self.display.frame))
277 
278  def _setCallback(self, what, func):
279  if func != interface.noop_callback:
280  try:
281  status = _fireflyClient.add_extension('POINT' if False else 'AREA_SELECT', title=what,
282  plot_id=str(self.display.frame),
283  extension_id=what)
284  if not status['success']:
285  pass
286  except Exception as e:
287  raise RuntimeError("Cannot set callback. Browser must be (re)opened " +
288  "to %s%s : %s" %
289  (_fireflyClient.url_bw,
290  _fireflyClient.channel, e))
291 
292  def _getEvent(self):
293  """Return an event generated by a keypress or mouse click
294  """
295  ev = interface.Event("q")
296 
297  if self.verbose:
298  print("virtual[%s]._getEvent() -> %s" % (self.display.frame, ev))
299 
300  return ev
301  #
302  # Set gray scale
303  #
304 
305  def _scale(self, algorithm, min, max, unit=None, *args, **kwargs):
306  """Scale the image stretch and limits
307 
308  Parameters:
309  -----------
310  algorithm : `str`
311  stretch algorithm, e.g. 'linear', 'log', 'loglog', 'equal', 'squared',
312  'sqrt', 'asinh', powerlaw_gamma'
313  min : `float` or `str`
314  lower limit, or 'minmax' for full range, or 'zscale'
315  max : `float` or `str`
316  upper limit; overrriden if min is 'minmax' or 'zscale'
317  unit : `str`
318  unit for min and max. 'percent', 'absolute', 'sigma'.
319  if not specified, min and max are presumed to be in 'absolute' units.
320 
321  *args, **kwargs : additional position and keyword arguments.
322  The options are shown below:
323 
324  **Q** : `float`, optional
325  The asinh softening parameter for asinh stretch.
326  Use Q=0 for linear stretch, increase Q to make brighter features visible.
327  When not specified or None, Q is calculated by Firefly to use full color range.
328  **gamma**
329  The gamma value for power law gamma stretch (default 2.0)
330  **zscale_contrast** : `int`, optional
331  Contrast parameter in percent for zscale algorithm (default 25)
332  **zscale_samples** : `int`, optional
333  Number of samples for zscale algorithm (default 600)
334  **zscale_samples_perline** : `int`, optional
335  Number of samples per line for zscale algorithm (default 120)
336  """
337  stretch_algorithms = ('linear', 'log', 'loglog', 'equal', 'squared', 'sqrt',
338  'asinh', 'powerlaw_gamma')
339  interval_methods = ('percent', 'maxmin', 'absolute', 'zscale', 'sigma')
340  #
341  #
342  # Normalise algorithm's case
343  #
344  if algorithm:
345  algorithm = dict((a.lower(), a) for a in stretch_algorithms).get(algorithm.lower(), algorithm)
346 
347  if algorithm not in stretch_algorithms:
348  raise FireflyError('Algorithm %s is invalid; please choose one of "%s"' %
349  (algorithm, '", "'.join(stretch_algorithms)))
350  self._stretchAlgorithm = algorithm
351  else:
352  algorithm = 'linear'
353 
354  # Translate parameters for asinh and powerlaw_gamma stretches
355  if 'Q' in kwargs:
356  kwargs['asinh_q_value'] = kwargs['Q']
357  del kwargs['Q']
358 
359  if 'gamma' in kwargs:
360  kwargs['gamma_value'] = kwargs['gamma']
361  del kwargs['gamma']
362 
363  if min == 'minmax':
364  interval_type = 'percent'
365  unit = 'percent'
366  min, max = 0, 100
367  elif min == 'zscale':
368  interval_type = 'zscale'
369  else:
370  interval_type = None
371 
372  if not unit:
373  unit = 'absolute'
374 
375  units = ('percent', 'absolute', 'sigma')
376  if unit not in units:
377  raise FireflyError('Unit %s is invalid; please choose one of "%s"' % (unit, '", "'.join(units)))
378 
379  if unit == 'sigma':
380  interval_type = 'sigma'
381  elif unit == 'absolute' and interval_type is None:
382  interval_type = 'absolute'
383  elif unit == 'percent':
384  interval_type = 'percent'
385 
386  self._stretchMin = min
387  self._stretchMax = max
388  self._stretchUnit = unit
389 
390  if interval_type not in interval_methods:
391  raise FireflyError('Interval method %s is invalid' % interval_type)
392 
393  rval = {}
394  if interval_type is not 'zscale':
395  rval = _fireflyClient.set_stretch(str(self.display.frame), stype=interval_type,
396  algorithm=algorithm, lower_value=min,
397  upper_value=max, **kwargs)
398  else:
399  if 'zscale_contrast' not in kwargs:
400  kwargs['zscale_contrast'] = 25
401  if 'zscale_samples' not in kwargs:
402  kwargs['zscale_samples'] = 600
403  if 'zscale_samples_perline' not in kwargs:
404  kwargs['zscale_samples_perline'] = 120
405  rval = _fireflyClient.set_stretch(str(self.display.frame), stype='zscale',
406  algorithm=algorithm, **kwargs)
407 
408  if 'rv_string' in rval:
409  self._lastStretch = rval['rv_string']
410 
411  def _setMaskTransparency(self, transparency, maskName):
412  """Specify mask transparency (percent); or None to not set it when loading masks"""
413  if maskName is not None:
414  masklist = [maskName]
415  else:
416  masklist = set(self._maskIds + list(self.display._defaultMaskPlaneColor.keys()))
417  for k in masklist:
418  self._maskTransparencies[k] = transparency
419  _fireflyClient.dispatch_remote_action(channel=_fireflyClient.channel,
420  action_type='ImagePlotCntlr.overlayPlotChangeAttributes',
421  payload={'plotId': str(self.display.frame),
422  'imageOverlayId': k,
423  'attributes': {'opacity': 1.0 - transparency/100.},
424  'doReplot': False})
425 
426  def _getMaskTransparency(self, maskName):
427  """Return the current mask's transparency"""
428  transparency = None
429  if maskName in self._maskTransparencies:
430  transparency = self._maskTransparencies[maskName]
431  return transparency
432 
433  def _setMaskPlaneColor(self, maskName, color):
434  """Specify mask color """
435  _fireflyClient.remove_mask(plot_id=str(self.display.frame),
436  mask_id=maskName)
437  self._maskPlaneColors[maskName] = color
438  if (color.lower() != 'ignore'):
439  _fireflyClient.add_mask(bit_number=self._maskDict[maskName],
440  image_number=1,
441  plot_id=str(self.display.frame),
442  mask_id=maskName,
443  color=self.display.getMaskPlaneColor(maskName),
444  file_on_server=self._fireflyFitsID)
445 
446  def _show(self):
447  """Show the requested window"""
448  localbrowser, url = _fireflyClient.launch_browser(verbose=self.verbose)
449  if not localbrowser and not self.verbose:
450  _fireflyClient.display_url()
451  #
452  # Zoom and Pan
453  #
454 
455  def _zoom(self, zoomfac):
456  """Zoom display by specified amount
457 
458  Parameters:
459  -----------
460  zoomfac: `float`
461  zoom level in screen pixels per image pixel
462  """
463  self._lastZoom = zoomfac
464  _fireflyClient.set_zoom(plot_id=str(self.display.frame), factor=zoomfac)
465 
466  def _pan(self, colc, rowc):
467  """Pan to specified pixel coordinates
468 
469  Parameters:
470  -----------
471  colc, rowc : `float`
472  column and row in units of pixels (zero-based convention,
473  with the xy0 already subtracted off)
474  """
475  self._lastPan = [colc+0.5, rowc+0.5] # saved for future use in _mtv
476  # Firefly's internal convention is first pixel is (0.5, 0.5)
477  _fireflyClient.set_pan(plot_id=str(self.display.frame), x=colc, y=rowc)
478 
479  # Extensions to the API that are specific to using the Firefly backend
480 
481  def getClient(self):
482  """Get the instance of FireflyClient for this display
483 
484  Returns:
485  --------
486  `firefly_client.FireflyClient`
487  Instance of FireflyClient used by this display
488  """
489  return self._client
490 
491  def clearViewer(self):
492  """Reinitialize the viewer
493  """
494  self._client.reinit_viewer()
495 
496  def resetLayout(self):
497  """Reset the layout of the Firefly Slate browser
498 
499  Clears the display and adds Slate cells to display image in upper left,
500  plot area in upper right, and plots stretch across the bottom
501  """
502  self.clearViewer()
503  self._client.add_cell(row=2, col=0, width=4, height=2, element_type='tables',
504  cell_id='tables')
505  self._client.add_cell(row=0, col=0, width=2, height=3, element_type='images',
506  cell_id='image-%s' % str(self.display.frame))
507  self._client.add_cell(row=0, col=2, width=2, height=3, element_type='xyPlots',
508  cell_id='plots')
509 
510  def overlayFootprints(self, catalog, color='rgba(74,144,226,0.60)',
511  highlightColor='cyan', selectColor='orange',
512  style='fill', layerString='detection footprints ',
513  titleString='catalog footprints '):
514  """Overlay outlines of footprints from a catalog
515 
516  Overlay outlines of LSST footprints from the input catalog. The colors
517  and style can be specified as parameters, and the base color and style
518  can be changed in the Firefly browser user interface.
519 
520  Parameters:
521  -----------
522  catalog : `lsst.afw.table.SourceCatalog`
523  Source catalog from which to display footprints.
524  color : `str`
525  Color for footprints overlay. Colors can be specified as a name
526  like 'cyan' or afwDisplay.RED; as an rgb value such as
527  'rgb(80,100,220)'; or as rgb plus alpha (transparency) such
528  as 'rgba('74,144,226,0.60)'.
529  highlightColor : `str`
530  Color for highlighted footprints
531  selectColor : `str`
532  Color for selected footprints
533  style : {'fill', 'outline'}
534  Style of footprints display, filled or outline
535  insertColumn : `int`
536  Column at which to insert the "family_id" and "category" columns
537  layerString: `str`
538  Name of footprints layer string, to concatenate with the frame
539  Re-using the layer_string will overwrite the previous table and
540  footprints
541  titleString: `str`
542  Title of catalog, to concatenate with the frame
543  """
544  footprintTable = createFootprintsTable(catalog)
545  with BytesIO() as fd:
546  footprintTable.to_xml(fd)
547  tableval = self._client.upload_data(fd, 'UNKNOWN')
548  self._client.overlay_footprints(footprint_file=tableval,
549  title=titleString + str(self.display.frame),
550  footprint_layer_id=layerString + str(self.display.frame),
551  plot_id=str(self.display.frame),
552  color=color,
553  highlightColor=highlightColor,
554  selectColor=selectColor,
555  style=style)
def _setMaskTransparency(self, transparency, maskName)
Definition: firefly.py:411
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
Definition: functional.cc:33
daf::base::PropertySet * set
Definition: fits.cc:832
Definition: Log.h:716
Statistics makeStatistics(lsst::afw::math::MaskedVector< EntryT > const &mv, std::vector< WeightPixel > const &vweights, int const flags, StatisticsControl const &sctrl=StatisticsControl())
The makeStatistics() overload to handle lsst::afw::math::MaskedVector<>
Definition: Statistics.h:520
def _flush(self)
Flush any I/O buffers.
Definition: firefly.py:219
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
Definition: history.py:129
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:513
def getMaskPlaneColor(name, frame=None)
Definition: ds9.py:77
def __init__(self, display, verbose=False, url=None, name=None, args, kwargs)
Definition: firefly.py:79
def createFootprintsTable(catalog, xy0=None, insertColumn=4)
Definition: footprints.py:62
daf::base::PropertyList * list
Definition: fits.cc:833