LSST Applications  21.0.0-1-g8760c09+3ec9fa6d59,21.0.0-1-ga51b5d4+1a3c9f8315,21.0.0-13-g5daad6e+2e8edea047,21.0.0-15-g543e7c3+e4ac1ef71f,21.0.0-15-g5a7caf0+08c1890463,21.0.0-15-ge3888ae+d5f2eb71b7,21.0.0-15-gfb99f82+9e2f6ff707,21.0.0-18-g546cdbd+4e75b35dac,21.0.0-2-g103fe59+b77ac56db3,21.0.0-2-g45278ab+3ec9fa6d59,21.0.0-2-g5242d73+8a658b4ab3,21.0.0-2-g7f82c8f+e76196b80b,21.0.0-2-g8faa9b5+a13d88a6a5,21.0.0-2-ga326454+e76196b80b,21.0.0-2-gde069b7+135055f2fa,21.0.0-2-gecfae73+345e77691a,21.0.0-2-gfc62afb+8a658b4ab3,21.0.0-20-g35aa1e9+3afe4fe733,21.0.0-20-g8cd22d88+eb66f50d9f,21.0.0-3-g357aad2+16e3f8303a,21.0.0-3-g4a4ce7f+8a658b4ab3,21.0.0-3-g4be5c26+8a658b4ab3,21.0.0-3-g65f322c+7c7224af56,21.0.0-3-ge02ed75+9e2f6ff707,21.0.0-30-gf21da11f8+33ac405c47,21.0.0-4-g591bb35+9e2f6ff707,21.0.0-4-g65b4814+4e75b35dac,21.0.0-4-ge8a399c+22b5d34500,21.0.0-5-g8c1d971+75b22be884,21.0.0-5-gcb07a25+7075ac2890,21.0.0-5-gcc89fd6+a13d88a6a5,21.0.0-5-gd00fb1e+5ce235a660,21.0.0-6-gc675373+8a658b4ab3,21.0.0-7-gdf92d54+3ec9fa6d59,21.0.0-72-gffa78901+7685cf5dd5,21.0.0-8-g5674e7b+3979a46207,21.0.0-8-g72414d6+11243696b5,master-gac4afde19b+9e2f6ff707,w.2021.17
LSST Data Management Base Package
ds9.py
Go to the documentation of this file.
1 # This file is part of display_ds9.
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__ = ["Ds9Error", "getXpaAccessPoint", "ds9Version", "Buffer",
23  "selectFrame", "ds9Cmd", "initDS9", "Ds9Event", "DisplayImpl"]
24 
25 import os
26 import re
27 import sys
28 import time
29 
30 import numpy as np
31 
32 import lsst.afw.display.interface as interface
33 import lsst.afw.display.virtualDevice as virtualDevice
34 import lsst.afw.display.ds9Regions as ds9Regions
35 
36 try:
37  from . import xpa as xpa
38 except ImportError as e:
39  print("Cannot import xpa: %s" % (e), file=sys.stderr)
40 
41 import lsst.afw.display as afwDisplay
42 import lsst.afw.math as afwMath
43 
44 try:
45  needShow
46 except NameError:
47  needShow = True # Used to avoid a bug in ds9 5.4
48 
49 
50 class Ds9Error(IOError):
51  """Represents an error communicating with DS9.
52  """
53 
54 
55 try:
56  _maskTransparency
57 except NameError:
58  _maskTransparency = None
59 
60 
62  """Parse XPA_PORT if set and return an identifier to send DS9 commands.
63 
64  Returns
65  -------
66 
67  xpaAccessPoint : `str`
68  Either a reference to the local host with the configured port, or the
69  string ``"ds9"``.
70 
71  Notes
72  -----
73  If you don't have XPA_PORT set, the usual xpans tricks will be played
74  when we return ``"ds9"``.
75  """
76  xpa_port = os.environ.get("XPA_PORT")
77  if xpa_port:
78  mat = re.search(r"^DS9:ds9\s+(\d+)\s+(\d+)", xpa_port)
79  if mat:
80  port1, port2 = mat.groups()
81 
82  return "127.0.0.1:%s" % (port1)
83  else:
84  print("Failed to parse XPA_PORT=%s" % xpa_port, file=sys.stderr)
85 
86  return "ds9"
87 
88 
89 def ds9Version():
90  """Get the version of DS9 in use.
91 
92  Returns
93  -------
94  version : `str`
95  Version of DS9 in use.
96  """
97  try:
98  v = ds9Cmd("about", get=True)
99  return v.splitlines()[1].split()[1]
100  except Exception as e:
101  print("Error reading version: %s" % e, file=sys.stderr)
102  return "0.0.0"
103 
104 
105 try:
106  cmdBuffer
107 except NameError:
108  # internal buffersize in xpa. Sigh; esp. as the 100 is some needed slop
109  XPA_SZ_LINE = 4096 - 100
110 
111  class Buffer(object):
112  """Buffer to control sending commands to DS9.
113 
114  Notes
115  -----
116  The usual usage pattern is:
117 
118  >>> with ds9.Buffering():
119  ... # bunches of ds9.{dot,line} commands
120  ... ds9.flush()
121  ... # bunches more ds9.{dot,line} commands
122  """
123 
124  def __init__(self, size=0):
125  self._commands_commands = "" # list of pending commands
126  self._lenCommands_lenCommands = len(self._commands_commands)
127  self._bufsize_bufsize = [] # stack of bufsizes
128 
129  self._bufsize_bufsize.append(size) # don't call self.size() as ds9Cmd isn't defined yet
130 
131  def set(self, size, silent=True):
132  """Set the ds9 buffer size to size.
133 
134  Parameters
135  ----------
136  size : `int`
137  Size of buffer. Requesting a negative size provides the
138  largest possible buffer given bugs in xpa.
139  silent : `bool`, optional
140  Do not print error messages (default `True`).
141  """
142  if size < 0:
143  size = XPA_SZ_LINE - 5
144 
145  if size > XPA_SZ_LINE:
146  print("xpa silently hardcodes a limit of %d for buffer sizes (you asked for %d) " %
147  (XPA_SZ_LINE, size), file=sys.stderr)
148  self.setset(-1) # use max buffersize
149  return
150 
151  if self._bufsize_bufsize:
152  self._bufsize_bufsize[-1] = size # change current value
153  else:
154  self._bufsize_bufsize.append(size) # there is no current value; set one
155 
156  self.flushflush(silent=silent)
157 
158  def _getSize(self):
159  """Get the current DS9 buffer size.
160 
161  Returns
162  -------
163  size : `int`
164  Size of buffer.
165  """
166  return self._bufsize_bufsize[-1]
167 
168  def pushSize(self, size=-1):
169  """Replace current DS9 command buffer size.
170 
171  Parameters
172  ----------
173  size : `int`, optional
174  Size of buffer. A negative value sets the largest possible
175  buffer.
176 
177  Notes
178  -----
179  See also `popSize`.
180  """
181  self.flushflush(silent=True)
182  self._bufsize_bufsize.append(0)
183  self.setset(size, silent=True)
184 
185  def popSize(self):
186  """Switch back to the previous command buffer size.
187 
188  Notes
189  -----
190  See also `pushSize`.
191  """
192  self.flushflush(silent=True)
193 
194  if len(self._bufsize_bufsize) > 1:
195  self._bufsize_bufsize.pop()
196 
197  def flush(self, silent=True):
198  """Flush the pending commands.
199 
200  Parameters
201  ----------
202  silent : `bool`, optional
203  Do not print error messages.
204  """
205  ds9Cmd(flush=True, silent=silent)
206 
207  cmdBuffer = Buffer(0)
208 
209 
210 def selectFrame(frame):
211  """Convert integer frame number to DS9 command syntax.
212 
213  Parameters
214  ----------
215  frame : `int`
216  Frame number
217 
218  Returns
219  -------
220  frameString : `str`
221  """
222  return "frame %d" % (frame)
223 
224 
225 def ds9Cmd(cmd=None, trap=True, flush=False, silent=True, frame=None, get=False):
226  """Issue a DS9 command, raising errors as appropriate.
227 
228  Parameters
229  ----------
230  cmd : `str`, optional
231  Command to execute.
232  trap : `bool`, optional
233  Trap errors.
234  flush : `bool`, optional
235  Flush the output.
236  silent : `bool`, optional
237  Do not print trapped error messages.
238  frame : `int`, optional
239  Frame number on which to execute command.
240  get : `bool`, optional
241  Return xpa response.
242  """
243 
244  global cmdBuffer
245  if cmd:
246  if frame is not None:
247  cmd = "%s;" % selectFrame(frame) + cmd
248 
249  if get:
250  return xpa.get(None, getXpaAccessPoint(), cmd, "").strip()
251 
252  # Work around xpa's habit of silently truncating long lines; the value
253  # ``5`` provides some margin to handle new lines and the like.
254  if cmdBuffer._lenCommands + len(cmd) > XPA_SZ_LINE - 5:
255  ds9Cmd(flush=True, silent=silent)
256 
257  cmdBuffer._commands += ";" + cmd
258  cmdBuffer._lenCommands += 1 + len(cmd)
259 
260  if flush or cmdBuffer._lenCommands >= cmdBuffer._getSize():
261  cmd = (cmdBuffer._commands + "\n")
262  cmdBuffer._commands = ""
263  cmdBuffer._lenCommands = 0
264  else:
265  return
266 
267  cmd = cmd.rstrip()
268  if not cmd:
269  return
270 
271  try:
272  ret = xpa.set(None, getXpaAccessPoint(), cmd, "", "", 0)
273  if ret:
274  raise IOError(ret)
275  except IOError as e:
276  if not trap:
277  raise Ds9Error("XPA: %s, (%s)" % (e, cmd))
278  elif not silent:
279  print("Caught ds9 exception processing command \"%s\": %s" % (cmd, e), file=sys.stderr)
280 
281 
282 def initDS9(execDs9=True):
283  """Initialize DS9.
284 
285  Parameters
286  ----------
287  execDs9 : `bool`, optional
288  If DS9 is not running, attempt to execute it.
289  """
290  try:
291  xpa.reset()
292  ds9Cmd("iconify no; raise", False)
293  ds9Cmd("wcs wcsa", False) # include the pixel coordinates WCS (WCSA)
294 
295  v0, v1 = ds9Version().split('.')[0:2]
296  global needShow
297  needShow = False
298  try:
299  if int(v0) == 5:
300  needShow = (int(v1) <= 4)
301  except Exception:
302  pass
303  except Ds9Error as e:
304  if not re.search('xpa', os.environ['PATH']):
305  raise Ds9Error('You need the xpa binaries in your path to use ds9 with python')
306 
307  if not execDs9:
308  raise Ds9Error
309 
310  import distutils.spawn
311  if not distutils.spawn.find_executable("ds9"):
312  raise NameError("ds9 doesn't appear to be on your path")
313  if "DISPLAY" not in os.environ:
314  raise RuntimeError("$DISPLAY isn't set, so I won't be able to start ds9 for you")
315 
316  print("ds9 doesn't appear to be running (%s), I'll try to exec it for you" % e)
317 
318  os.system('ds9 &')
319  for i in range(10):
320  try:
321  ds9Cmd(selectFrame(1), False)
322  break
323  except Ds9Error:
324  print("waiting for ds9...\r", end="")
325  sys.stdout.flush()
326  time.sleep(0.5)
327  else:
328  print(" \r", end="")
329  break
330 
331  sys.stdout.flush()
332 
333  raise Ds9Error
334 
335 
336 class Ds9Event(interface.Event):
337  """An event generated by a mouse or key click on the display.
338  """
339 
340  def __init__(self, k, x, y):
341  interface.Event.__init__(self, k, x, y)
342 
343 
344 class DisplayImpl(virtualDevice.DisplayImpl):
345  """Virtual device display implementation.
346  """
347 
348  def __init__(self, display, verbose=False, *args, **kwargs):
349  virtualDevice.DisplayImpl.__init__(self, display, verbose)
350 
351  def _close(self):
352  """Called when the device is closed.
353  """
354  pass
355 
356  def _setMaskTransparency(self, transparency, maskplane):
357  """Specify DS9's mask transparency.
358 
359  Parameters
360  ----------
361  transparency : `int`
362  Percent transparency.
363  maskplane : `NoneType`
364  If `None`, transparency is enabled. Otherwise, this parameter is
365  ignored.
366  """
367  if maskplane is not None:
368  print("ds9 is unable to set transparency for individual maskplanes" % maskplane,
369  file=sys.stderr)
370  return
371  ds9Cmd("mask transparency %d" % transparency, frame=self.display.frame)
372 
373  def _getMaskTransparency(self, maskplane):
374  """Return the current DS9's mask transparency.
375 
376  Parameters
377  ----------
378  maskplane : unused
379  This parameter does nothing.
380  """
381  selectFrame(self.display.frame)
382  return float(ds9Cmd("mask transparency", get=True))
383 
384  def _show(self):
385  """Uniconify and raise DS9.
386 
387  Notes
388  -----
389  Raises if ``self.display.frame`` doesn't exist.
390  """
391  ds9Cmd("raise", trap=False, frame=self.display.frame)
392 
393  def _mtv(self, image, mask=None, wcs=None, title=""):
394  """Display an Image and/or Mask on a DS9 display.
395 
396  Parameters
397  ----------
398  image : subclass of `lsst.afw.image.Image`
399  Image to display.
400  mask : subclass of `lsst.afw.image.Mask`, optional
401  Mask.
402  wcs : `lsst.afw.geom.SkyWcs`, optional
403  WCS of data
404  title : `str`, optional
405  Title of image.
406  """
407 
408  for i in range(3):
409  try:
410  initDS9(i == 0)
411  except Ds9Error:
412  print("waiting for ds9...\r", end="")
413  sys.stdout.flush()
414  time.sleep(0.5)
415  else:
416  if i > 0:
417  print(" \r", end="")
418  sys.stdout.flush()
419  break
420 
421  ds9Cmd(selectFrame(self.display.frame))
422  ds9Cmd("smooth no")
423  self._erase()
424 
425  if image:
426  _i_mtv(image, wcs, title, False)
427 
428  if mask:
429  maskPlanes = mask.getMaskPlaneDict()
430  nMaskPlanes = max(maskPlanes.values()) + 1
431 
432  planes = {} # build inverse dictionary
433  for key in maskPlanes:
434  planes[maskPlanes[key]] = key
435 
436  planeList = range(nMaskPlanes)
437  usedPlanes = int(afwMath.makeStatistics(mask, afwMath.SUM).getValue())
438  mask1 = mask.Factory(mask.getDimensions()) # Mask containing just one bitplane
439 
440  colorGenerator = self.display.maskColorGenerator(omitBW=True)
441  for p in planeList:
442  if planes.get(p):
443  pname = planes[p]
444 
445  if not ((1 << p) & usedPlanes): # no pixels have this bitplane set
446  continue
447 
448  mask1[:] = mask
449  mask1 &= (1 << p)
450 
451  color = self.display.getMaskPlaneColor(pname)
452 
453  if not color: # none was specified
454  color = next(colorGenerator)
455  elif color.lower() == "ignore":
456  continue
457 
458  ds9Cmd("mask color %s" % color)
459  _i_mtv(mask1, wcs, title, True)
460  #
461  # Graphics commands
462  #
463 
464  def _buffer(self, enable=True):
465  """Push and pop buffer size.
466 
467  Parameters
468  ----------
469  enable : `bool`, optional
470  If `True` (default), push size; else pop it.
471  """
472  if enable:
473  cmdBuffer.pushSize()
474  else:
475  cmdBuffer.popSize()
476 
477  def _flush(self):
478  """Flush buffer.
479  """
480  cmdBuffer.flush()
481 
482  def _erase(self):
483  """Erase all regions in current frame.
484  """
485  ds9Cmd("regions delete all", flush=True, frame=self.display.frame)
486 
487  def _dot(self, symb, c, r, size, ctype, fontFamily="helvetica", textAngle=None):
488  """Draw a symbol onto the specified DS9 frame.
489 
490  Parameters
491  ----------
492  symb : `str`, or subclass of `lsst.afw.geom.ellipses.BaseCore`
493  Symbol to be drawn. Possible values are:
494 
495  - ``"+"``: Draw a "+"
496  - ``"x"``: Draw an "x"
497  - ``"*"``: Draw a "*"
498  - ``"o"``: Draw a circle
499  - ``"@:Mxx,Mxy,Myy"``: Draw an ellipse with moments (Mxx, Mxy,
500  Myy);(the ``size`` parameter is ignored)
501  - An object derived from `lsst.afw.geom.ellipses.BaseCore`: Draw
502  the ellipse (argument size is ignored)
503 
504  Any other value is interpreted as a string to be drawn.
505  c : `int`
506  Column to draw symbol [0-based coordinates].
507  r : `int`
508  Row to draw symbol [0-based coordinates].
509  size : `float`
510  Size of symbol.
511  ctype : `str`
512  the name of a colour (e.g. ``"red"``)
513  fontFamily : `str`, optional
514  String font. May be extended with other characteristics,
515  e.g. ``"times bold italic"``.
516  textAngle: `float`, optional
517  Text will be drawn rotated by ``textAngle``.
518 
519  Notes
520  -----
521  Objects derived from `lsst.afw.geom.ellipses.BaseCore` include
522  `~lsst.afw.geom.ellipses.Axes` and `lsst.afw.geom.ellipses.Quadrupole`.
523  """
524  cmd = selectFrame(self.display.frame) + "; "
525  for region in ds9Regions.dot(symb, c, r, size, ctype, fontFamily, textAngle):
526  cmd += 'regions command {%s}; ' % region
527 
528  ds9Cmd(cmd, silent=True)
529 
530  def _drawLines(self, points, ctype):
531  """Connect the points.
532 
533  Parameters
534  -----------
535  points : `list` of (`int`, `int`)
536  A list of points specified as (col, row).
537  ctype : `str`
538  The name of a colour (e.g. ``"red"``).
539  """
540  cmd = selectFrame(self.display.frame) + "; "
541  for region in ds9Regions.drawLines(points, ctype):
542  cmd += 'regions command {%s}; ' % region
543 
544  ds9Cmd(cmd)
545 
546  def _scale(self, algorithm, min, max, unit, *args, **kwargs):
547  """Set image color scale.
548 
549  Parameters
550  ----------
551  algorithm : {``"linear"``, ``"log"``, ``"pow"``, ``"sqrt"``, ``"squared"``, ``"asinh"``, ``"sinh"``, ``"histequ"``} # noqa: E501
552  Scaling algorithm. May be any value supported by DS9.
553  min : `float`
554  Minimum value for scale.
555  max : `float`
556  Maximum value for scale.
557  unit : `str`
558  Ignored.
559  *args
560  Ignored.
561  **kwargs
562  Ignored
563  """
564  if algorithm:
565  ds9Cmd("scale %s" % algorithm, frame=self.display.frame)
566 
567  if min in ("minmax", "zscale"):
568  ds9Cmd("scale mode %s" % (min))
569  else:
570  if unit:
571  print("ds9: ignoring scale unit %s" % unit)
572 
573  ds9Cmd("scale limits %g %g" % (min, max), frame=self.display.frame)
574  #
575  # Zoom and Pan
576  #
577 
578  def _zoom(self, zoomfac):
579  """Zoom frame by specified amount.
580 
581  Parameters
582  ----------
583  zoomfac : `int`
584  DS9 zoom factor.
585  """
586  cmd = selectFrame(self.display.frame) + "; "
587  cmd += "zoom to %d; " % zoomfac
588 
589  ds9Cmd(cmd, flush=True)
590 
591  def _pan(self, colc, rowc):
592  """Pan frame.
593 
594  Parameters
595  ----------
596  colc : `int`
597  Physical column to which to pan.
598  rowc : `int`
599  Physical row to which to pan.
600  """
601  cmd = selectFrame(self.display.frame) + "; "
602  # ds9 is 1-indexed. Grrr
603  cmd += "pan to %g %g physical; " % (colc + 1, rowc + 1)
604 
605  ds9Cmd(cmd, flush=True)
606 
607  def _getEvent(self):
608  """Listen for a key press on a frame in DS9 and return an event.
609 
610  Returns
611  -------
612  event : `Ds9Event`
613  Event with (key, x, y).
614  """
615  vals = ds9Cmd("imexam key coordinate", get=True).split()
616  if vals[0] == "XPA$ERROR":
617  if vals[1:4] == ['unknown', 'option', '"-state"']:
618  pass # a ds9 bug --- you get this by hitting TAB
619  else:
620  print("Error return from imexam:", " ".join(vals), file=sys.stderr)
621  return None
622 
623  k = vals.pop(0)
624  try:
625  x = float(vals[0])
626  y = float(vals[1])
627  except Exception:
628  x = float("NaN")
629  y = float("NaN")
630 
631  return Ds9Event(k, x, y)
632 
633 
634 try:
635  haveGzip
636 except NameError:
637  # does gzip work?
638  haveGzip = not os.system("gzip < /dev/null > /dev/null 2>&1")
639 
640 
641 def _i_mtv(data, wcs, title, isMask):
642  """Internal routine to display an image or a mask on a DS9 display.
643 
644  Parameters
645  ----------
646  data : Subclass of `lsst.afw.image.Image` or `lsst.afw.image.Mask`
647  Data to display.
648  wcs : `lsst.afw.geom.SkyWcs`
649  WCS of data.
650  title : `str`
651  Title of display.
652  isMask : `bool`
653  Is ``data`` a mask?
654  """
655  title = str(title) if title else ""
656 
657  if True:
658  if isMask:
659  xpa_cmd = "xpaset %s fits mask" % getXpaAccessPoint()
660  # ds9 mis-handles BZERO/BSCALE in uint16 data.
661  # The following hack works around this.
662  # This is a copy we're modifying
663  if data.getArray().dtype == np.uint16:
664  data |= 0x8000
665  else:
666  xpa_cmd = "xpaset %s fits" % getXpaAccessPoint()
667 
668  if haveGzip:
669  xpa_cmd = "gzip | " + xpa_cmd
670 
671  pfd = os.popen(xpa_cmd, "w")
672  else:
673  pfd = open("foo.fits", "w")
674 
675  ds9Cmd(flush=True, silent=True)
676 
677  try:
678  afwDisplay.writeFitsImage(pfd.fileno(), data, wcs, title)
679  except Exception as e:
680  try:
681  pfd.close()
682  except Exception:
683  pass
684 
685  raise e
686 
687  try:
688  pfd.close()
689  except Exception:
690  pass
691 
692 
693 if False:
694  try:
695  definedCallbacks
696  except NameError:
697  definedCallbacks = True
698 
699  for k in ('XPA$ERROR',):
700  interface.setCallback(k)
int max
def __init__(self, size=0)
Definition: ds9.py:124
def set(self, size, silent=True)
Definition: ds9.py:131
def pushSize(self, size=-1)
Definition: ds9.py:168
def flush(self, silent=True)
Definition: ds9.py:197
def __init__(self, display, verbose=False, *args, **kwargs)
Definition: ds9.py:348
def __init__(self, k, x, y)
Definition: ds9.py:340
bool strip
Definition: fits.cc:911
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
Definition: functional.cc:33
def getMaskPlaneColor(name, frame=None)
Definition: ds9.py:76
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:354
def initDS9(execDs9=True)
Definition: ds9.py:282
def selectFrame(frame)
Definition: ds9.py:210
def getXpaAccessPoint()
Definition: ds9.py:61
def ds9Cmd(cmd=None, trap=True, flush=False, silent=True, frame=None, get=False)
Definition: ds9.py:225
def ds9Version()
Definition: ds9.py:89