lsst.display.ds9  16.0+4
ds9.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 
26 
27 from __future__ import absolute_import, division, print_function
28 from builtins import str
29 from builtins import next
30 from builtins import range
31 from builtins import object
32 from past.builtins import long
33 
34 __all__ = ["Ds9Error", "getXpaAccessPoint", "ds9Version", "Buffer",
35  "selectFrame", "ds9Cmd", "initDS9", "Ds9Event", "DisplayImpl"]
36 
37 import os
38 import re
39 import sys
40 import time
41 
42 import numpy as np
43 
44 import lsst.afw.display.interface as interface
45 import lsst.afw.display.virtualDevice as virtualDevice
46 import lsst.afw.display.ds9Regions as ds9Regions
47 
48 try:
49  from . import xpa as xpa
50 except ImportError as e:
51  print("Cannot import xpa: %s" % (e), file=sys.stderr)
52 
53 import lsst.afw.display.displayLib as displayLib
54 import lsst.afw.math as afwMath
55 
56 try:
57  needShow
58 except NameError:
59  needShow = True # Used to avoid a bug in ds9 5.4
60 
61 
62 
63 
64 class Ds9Error(IOError):
65  """Some problem talking to ds9"""
66 
67 try:
68  _maskTransparency
69 except NameError:
70  _maskTransparency = None
71 
72 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
73 
74 
76  """Parse XPA_PORT and send return an identifier to send ds9 commands there, instead of "ds9"
77  If you don't have XPA_PORT set, the usual xpans tricks will be played when we return "ds9".
78  """
79  xpa_port = os.environ.get("XPA_PORT")
80  if xpa_port:
81  mat = re.search(r"^DS9:ds9\s+(\d+)\s+(\d+)", xpa_port)
82  if mat:
83  port1, port2 = mat.groups()
84 
85  return "127.0.0.1:%s" % (port1)
86  else:
87  print("Failed to parse XPA_PORT=%s" % xpa_port, file=sys.stderr)
88 
89  return "ds9"
90 
91 
92 def ds9Version():
93  """Return the version of ds9 in use, as a string"""
94  try:
95  v = ds9Cmd("about", get=True)
96  return v.splitlines()[1].split()[1]
97  except Exception as e:
98  print("Error reading version: %s" % e, file=sys.stderr)
99  return "0.0.0"
100 
101 try:
102  cmdBuffer
103 except NameError:
104  XPA_SZ_LINE = 4096 - 100 # internal buffersize in xpa. Sigh; esp. as the 100 is some needed slop
105 
106  class Buffer(object):
107  """Control buffering the sending of commands to ds9;
108  annoying but necessary for anything resembling performance
109 
110  The usual usage pattern (from a module importing this file, ds9.py) is:
111 
112  with ds9.Buffering():
113  # bunches of ds9.{dot,line} commands
114  ds9.flush()
115  # bunches more ds9.{dot,line} commands
116  """
117 
118  def __init__(self, size=0):
119  """Create a command buffer, with a maximum depth of size"""
120  self._commands = "" # list of pending commands
121  self._lenCommands = len(self._commands)
122  self._bufsize = [] # stack of bufsizes
123 
124  self._bufsize.append(size) # don't call self.size() as ds9Cmd isn't defined yet
125 
126  def set(self, size, silent=True):
127  """Set the ds9 buffer size to size"""
128  if size < 0:
129  size = XPA_SZ_LINE - 5
130 
131  if size > XPA_SZ_LINE:
132  print ("xpa silently hardcodes a limit of %d for buffer sizes (you asked for %d) " %
133  (XPA_SZ_LINE, size), file=sys.stderr)
134  self.set(-1) # use max buffersize
135  return
136 
137  if self._bufsize:
138  self._bufsize[-1] = size # change current value
139  else:
140  self._bufsize.append(size) # there is no current value; set one
141 
142  self.flush(silent=silent)
143 
144  def _getSize(self):
145  """Get the current ds9 buffer size"""
146  return self._bufsize[-1]
147 
148  def pushSize(self, size=-1):
149  """Replace current ds9 command buffer size with size (see also popSize)
150  @param: Size of buffer (-1: largest possible given bugs in xpa)"""
151  self.flush(silent=True)
152  self._bufsize.append(0)
153  self.set(size, silent=True)
154 
155  def popSize(self):
156  """Switch back to the previous command buffer size (see also pushSize)"""
157  self.flush(silent=True)
158 
159  if len(self._bufsize) > 1:
160  self._bufsize.pop()
161 
162  def flush(self, silent=True):
163  """Flush the pending commands"""
164  ds9Cmd(flush=True, silent=silent)
165 
166  cmdBuffer = Buffer(0)
167 
168 
169 def selectFrame(frame):
170  return "frame %d" % (frame)
171 
172 
173 def ds9Cmd(cmd=None, trap=True, flush=False, silent=True, frame=None, get=False):
174  """Issue a ds9 command, raising errors as appropriate"""
175 
176  global cmdBuffer
177  if cmd:
178  if frame is not None:
179  cmd = "%s;" % selectFrame(frame) + cmd
180 
181  if get:
182  return xpa.get(None, getXpaAccessPoint(), cmd, "").strip()
183 
184  # Work around xpa's habit of silently truncating long lines
185  if cmdBuffer._lenCommands + len(cmd) > XPA_SZ_LINE - 5: # 5 to handle newlines and such like
186  ds9Cmd(flush=True, silent=silent)
187 
188  cmdBuffer._commands += ";" + cmd
189  cmdBuffer._lenCommands += 1 + len(cmd)
190 
191  if flush or cmdBuffer._lenCommands >= cmdBuffer._getSize():
192  cmd = (cmdBuffer._commands + "\n")
193  cmdBuffer._commands = ""
194  cmdBuffer._lenCommands = 0
195  else:
196  return
197 
198  cmd = cmd.rstrip()
199  if not cmd:
200  return
201 
202  try:
203  ret = xpa.set(None, getXpaAccessPoint(), cmd, "", "", 0)
204  if ret:
205  raise IOError(ret)
206  except IOError as e:
207  if not trap:
208  raise Ds9Error("XPA: %s, (%s)" % (e, cmd))
209  elif not silent:
210  print("Caught ds9 exception processing command \"%s\": %s" % (cmd, e), file=sys.stderr)
211 
212 
213 def initDS9(execDs9=True):
214  try:
215  xpa.reset()
216  ds9Cmd("iconify no; raise", False)
217  ds9Cmd("wcs wcsa", False) # include the pixel coordinates WCS (WCSA)
218 
219  v0, v1 = ds9Version().split('.')[0:2]
220  global needShow
221  needShow = False
222  try:
223  if int(v0) == 5:
224  needShow = (int(v1) <= 4)
225  except:
226  pass
227  except Ds9Error as e:
228  if not re.search('xpa', os.environ['PATH']):
229  raise Ds9Error('You need the xpa binaries in your path to use ds9 with python')
230 
231  if not execDs9:
232  raise Ds9Error
233 
234  import distutils.spawn
235  if not distutils.spawn.find_executable("ds9"):
236  raise NameError("ds9 doesn't appear to be on your path")
237  if not "DISPLAY" in os.environ:
238  raise RuntimeError("$DISPLAY isn't set, so I won't be able to start ds9 for you")
239 
240  print("ds9 doesn't appear to be running (%s), I'll try to exec it for you" % e)
241 
242  os.system('ds9 &')
243  for i in range(10):
244  try:
245  ds9Cmd(selectFrame(1), False)
246  break
247  except Ds9Error:
248  print("waiting for ds9...\r", end="")
249  sys.stdout.flush()
250  time.sleep(0.5)
251  else:
252  print(" \r", end="")
253  break
254 
255  sys.stdout.flush()
256 
257  raise Ds9Error
258 
259 
260 class Ds9Event(interface.Event):
261  """An event generated by a mouse or key click on the display"""
262 
263  def __init__(self, k, x, y):
264  interface.Event.__init__(self, k, x, y)
265 
266 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
267 
268 
269 class DisplayImpl(virtualDevice.DisplayImpl):
270 
271  def __init__(self, display, verbose=False, *args, **kwargs):
272  virtualDevice.DisplayImpl.__init__(self, display, verbose)
273 
274  def _close(self):
275  """Called when the device is closed"""
276  pass
277 
278  def _setMaskTransparency(self, transparency, maskplane):
279  """Specify ds9's mask transparency (percent); or None to not set it when loading masks"""
280  if maskplane != None:
281  print("ds9 is unable to set transparency for individual maskplanes" % maskplane,
282  file=sys.stderr)
283  return
284  ds9Cmd("mask transparency %d" % transparency, frame=self.display.frame)
285 
286  def _getMaskTransparency(self, maskplane):
287  """Return the current ds9's mask transparency"""
288 
289  selectFrame(self.display.frame)
290  return float(ds9Cmd("mask transparency", get=True))
291 
292  def _show(self):
293  """Uniconify and Raise ds9. N.b. throws an exception if frame doesn't exit"""
294  ds9Cmd("raise", trap=False, frame=self.display.frame)
295 
296  def _mtv(self, image, mask=None, wcs=None, title=""):
297  """Display an Image and/or Mask on a DS9 display
298  """
299 
300  for i in range(3):
301  try:
302  initDS9(i == 0)
303  except Ds9Error:
304  print("waiting for ds9...\r", end="")
305  sys.stdout.flush()
306  time.sleep(0.5)
307  else:
308  if i > 0:
309  print(" \r", end="")
310  sys.stdout.flush()
311  break
312 
313  ds9Cmd(selectFrame(self.display.frame))
314  ds9Cmd("smooth no")
315  self._erase()
316 
317  if image:
318  _i_mtv(image, wcs, title, False)
319 
320  if mask:
321  maskPlanes = mask.getMaskPlaneDict()
322  nMaskPlanes = max(maskPlanes.values()) + 1
323 
324  planes = {} # build inverse dictionary
325  for key in maskPlanes:
326  planes[maskPlanes[key]] = key
327 
328  planeList = range(nMaskPlanes)
329  usedPlanes = long(afwMath.makeStatistics(mask, afwMath.SUM).getValue())
330  mask1 = mask.Factory(mask.getDimensions()) # Mask containing just one bitplane
331 
332  colorGenerator = self.display.maskColorGenerator(omitBW=True)
333  for p in planeList:
334  if planes.get(p):
335  pname = planes[p]
336 
337  if not ((1 << p) & usedPlanes): # no pixels have this bitplane set
338  continue
339 
340  mask1[:] = mask
341  mask1 &= (1 << p)
342 
343  color = self.display.getMaskPlaneColor(pname)
344 
345  if not color: # none was specified
346  color = next(colorGenerator)
347  elif color.lower() == "ignore":
348  continue
349 
350  ds9Cmd("mask color %s" % color)
351  _i_mtv(mask1, wcs, title, True)
352  #
353  # Graphics commands
354  #
355 
356  def _buffer(self, enable=True):
357  if enable:
358  cmdBuffer.pushSize()
359  else:
360  cmdBuffer.popSize()
361 
362  def _flush(self):
363  cmdBuffer.flush()
364 
365  def _erase(self):
366  """Erase the specified DS9 frame"""
367  ds9Cmd("regions delete all", flush=True, frame=self.display.frame)
368 
369  def _dot(self, symb, c, r, size, ctype, fontFamily="helvetica", textAngle=None):
370  """Draw a symbol onto the specified DS9 frame at (col,row) = (c,r) [0-based coordinates]
371  Possible values are:
372  + Draw a +
373  x Draw an x
374  * Draw a *
375  o Draw a circle
376  @:Mxx,Mxy,Myy Draw an ellipse with moments (Mxx, Mxy, Myy) (argument size is ignored)
377  An object derived from afwGeom.ellipses.BaseCore Draw the ellipse (argument size is ignored)
378  Any other value is interpreted as a string to be drawn. Strings obey the fontFamily (which may be extended
379  with other characteristics, e.g. "times bold italic". Text will be drawn rotated by textAngle (textAngle is
380  ignored otherwise).
381 
382  N.b. objects derived from BaseCore include Axes and Quadrupole.
383  """
384  cmd = selectFrame(self.display.frame) + "; "
385  for region in ds9Regions.dot(symb, c, r, size, ctype, fontFamily, textAngle):
386  cmd += 'regions command {%s}; ' % region
387 
388  ds9Cmd(cmd, silent=True)
389 
390  def _drawLines(self, points, ctype):
391  """Connect the points, a list of (col,row)
392  Ctype is the name of a colour (e.g. 'red')"""
393 
394  cmd = selectFrame(self.display.frame) + "; "
395  for region in ds9Regions.drawLines(points, ctype):
396  cmd += 'regions command {%s}; ' % region
397 
398  ds9Cmd(cmd)
399  #
400  # Set gray scale
401  #
402 
403  def _scale(self, algorithm, min, max, unit, *args, **kwargs):
404  if algorithm:
405  ds9Cmd("scale %s" % algorithm, frame=self.display.frame)
406 
407  if min in ("minmax", "zscale"):
408  ds9Cmd("scale mode %s" % (min))
409  else:
410  if unit:
411  print("ds9: ignoring scale unit %s" % unit)
412 
413  ds9Cmd("scale limits %g %g" % (min, max), frame=self.display.frame)
414  #
415  # Zoom and Pan
416  #
417 
418  def _zoom(self, zoomfac):
419  """Zoom frame by specified amount"""
420 
421  cmd = selectFrame(self.display.frame) + "; "
422  cmd += "zoom to %d; " % zoomfac
423 
424  ds9Cmd(cmd, flush=True)
425 
426  def _pan(self, colc, rowc):
427  """Pan frame to (colc, rowc)"""
428 
429  cmd = selectFrame(self.display.frame) + "; "
430  cmd += "pan to %g %g physical; " % (colc + 1, rowc + 1) # ds9 is 1-indexed. Grrr
431 
432  ds9Cmd(cmd, flush=True)
433 
434  def _getEvent(self):
435  """Listen for a key press on frame in ds9, returning (key, x, y)"""
436 
437  vals = ds9Cmd("imexam key coordinate", get=True).split()
438  if vals[0] == "XPA$ERROR":
439  if vals[1:4] == ['unknown', 'option', '"-state"']:
440  pass # a ds9 bug --- you get this by hitting TAB
441  else:
442  print("Error return from imexam:", " ".join(vals), file=sys.stderr)
443  return None
444 
445  k = vals.pop(0)
446  try:
447  x = float(vals[0])
448  y = float(vals[1])
449  except:
450  x = float("NaN")
451  y = float("NaN")
452 
453  return Ds9Event(k, x, y)
454 
455 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
456 
457 try:
458  haveGzip
459 except NameError:
460  haveGzip = not os.system("gzip < /dev/null > /dev/null 2>&1") # does gzip work?
461 
462 
463 def _i_mtv(data, wcs, title, isMask):
464  """Internal routine to display an Image or Mask on a DS9 display"""
465 
466  title = str(title) if title else ""
467 
468  if True:
469  if isMask:
470  xpa_cmd = "xpaset %s fits mask" % getXpaAccessPoint()
471  # ds9 mis-handles BZERO/BSCALE in uint16 data. The following hack works around this.
472  # This is a copy we're modifying
473  if data.getArray().dtype == np.uint16:
474  data |= 0x8000
475  else:
476  xpa_cmd = "xpaset %s fits" % getXpaAccessPoint()
477 
478  if haveGzip:
479  xpa_cmd = "gzip | " + xpa_cmd
480 
481  pfd = os.popen(xpa_cmd, "w")
482  else:
483  pfd = file("foo.fits", "w")
484 
485  ds9Cmd(flush=True, silent=True)
486 
487  try:
488  displayLib.writeFitsImage(pfd.fileno(), data, wcs, title)
489  except Exception as e:
490  try:
491  pfd.close()
492  except:
493  pass
494 
495  raise e
496 
497  try:
498  pfd.close()
499  except:
500  pass
501 
502 if False:
503  try:
504  definedCallbacks
505  except NameError:
506  definedCallbacks = True
507 
508  for k in ('XPA$ERROR',):
509  interface.setCallback(k)
An error talking to ds9.
Definition: ds9.py:64
def initDS9(execDs9=True)
Definition: ds9.py:213
def set(self, size, silent=True)
Definition: ds9.py:126
def ds9Version()
Definition: ds9.py:92
def ds9Cmd(cmd=None, trap=True, flush=False, silent=True, frame=None, get=False)
Definition: ds9.py:173
def __init__(self, size=0)
Definition: ds9.py:118
def pushSize(self, size=-1)
Definition: ds9.py:148
def __init__(self, k, x, y)
Definition: ds9.py:263
def selectFrame(frame)
Definition: ds9.py:169
def __init__(self, display, verbose=False, args, kwargs)
Definition: ds9.py:271
def flush(self, silent=True)
Definition: ds9.py:162
def getXpaAccessPoint()
Definition: ds9.py:75