LSSTApplications  8.0.0.0+107,8.0.0.1+13,9.1+18,9.2,master-g084aeec0a4,master-g0aced2eed8+6,master-g15627eb03c,master-g28afc54ef9,master-g3391ba5ea0,master-g3d0fb8ae5f,master-g4432ae2e89+36,master-g5c3c32f3ec+17,master-g60f1e072bb+1,master-g6a3ac32d1b,master-g76a88a4307+1,master-g7bce1f4e06+57,master-g8ff4092549+31,master-g98e65bf68e,master-ga6b77976b1+53,master-gae20e2b580+3,master-gb584cd3397+53,master-gc5448b162b+1,master-gc54cf9771d,master-gc69578ece6+1,master-gcbf758c456+22,master-gcec1da163f+63,master-gcf15f11bcc,master-gd167108223,master-gf44c96c709
LSSTDataManagementBasePackage
ds9.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008, 2009, 2010 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 ##
24 ## \file
25 ## \brief Definitions to talk to ds9 from python
26 
27 from __future__ import with_statement
28 
29 import os, re, math, sys, time
30 
31 try:
32  import xpa
33 except ImportError, e:
34  print >> sys.stderr, "Cannot import xpa: %s" % e
35 
36 import displayLib
37 import lsst.afw.geom as afwGeom
38 import lsst.afw.image as afwImage
39 import lsst.afw.math as afwMath
40 
41 try:
42  needShow
43 except NameError:
44  needShow = True; # Used to avoid a bug in ds9 5.4
45 
46 ## An error talking to ds9
47 class Ds9Error(IOError):
48  """Some problem talking to ds9"""
49 
50 try:
51  type(_frame0)
52 except NameError:
53  _currentFrame = None
54 
55  def selectFrame(frame):
56  global _currentFrame
57  if frame != _currentFrame:
58  ds9Cmd(flush=True)
59  _currentFrame = frame
60 
61  return "frame %d" % (frame + _frame0)
62 
63  def setFrame0(frame0):
64  """Add frame0 to all frame specifications"""
65  global _frame0
66  _frame0 = frame0
67 
68  setFrame0(0)
69 
70 try:
71  type(_defaultFrame)
72 except NameError:
73  def setDefaultFrame(frame=0):
74  """Set the default frame for ds9"""
75  global _defaultFrame
76  _defaultFrame = frame
77 
79  """Get the default frame for ds9"""
80  return _defaultFrame
81 
83  """Increment the default frame for ds9"""
84  global _defaultFrame
85  _defaultFrame += 1
86  return _defaultFrame
87 
89 #
90 # Symbolic names for mask/line colours. N.b. ds9 5.3+ supports any X11 colour for masks
91 #
92 WHITE = "white"
93 BLACK = "black"
94 RED = "red"
95 GREEN = "green"
96 BLUE = "blue"
97 CYAN = "cyan"
98 MAGENTA = "magenta"
99 YELLOW = "yellow"
100 _maskColors = [WHITE, BLACK, RED, GREEN, BLUE, CYAN, MAGENTA, YELLOW]
101 
102 def setMaskPlaneColor(name, color=None):
103  """Request that mask plane name be displayed as color; name may be a dictionary
104  (in which case color should be omitted"""
105 
106  if isinstance(name, dict):
107  assert color == None
108  for k in name.keys():
109  setMaskPlaneColor(k, name[k])
110  return
111 
112  global _maskPlaneColors
113  try:
114  type(_maskPlaneColors)
115  except:
116  _maskPlaneColors = {}
117 
118  _maskPlaneColors[name] = color
119 
120 #
121 # Default mapping from mask plane names to colours
122 #
124  "BAD": RED,
125  "CR" : MAGENTA,
126  "EDGE": YELLOW,
127  "INTERPOLATED" : GREEN,
128  "SATURATED" : GREEN,
129  "DETECTED" : BLUE,
130  "DETECTED_NEGATIVE" : CYAN,
131  "SUSPECT" : YELLOW,
132  # deprecated names
133  "INTRP" : GREEN,
134  "SAT" : GREEN,
135  })
136 
138  """Return the colour associated with the specified mask plane name"""
139 
140  if _maskPlaneColors.has_key(name):
141  return _maskPlaneColors[name]
142  else:
143  return None
144 
145 def setMaskPlaneVisibility(name, show=True):
146  """Specify the visibility of a given mask plane;
147  name may be a dictionary (in which case show will be ignored)"""
148 
149  global _maskPlaneVisibility
150  try:
151  type(_maskPlaneVisibility)
152  except NameError, e:
153  _maskPlaneVisibility = {}
154 
155  if isinstance(name, dict):
156  for k in name.keys():
157  setMaskPlaneVisibility(k, name[k])
158  return
159 
160  _maskPlaneVisibility[name] = show
161 
163 
165  """Should we display the specified mask plane name?"""
166 
167  if _maskPlaneVisibility.has_key(name):
168  return _maskPlaneVisibility[name]
169  else:
170  return True
171 
172 try:
173  _maskTransparency
174 except NameError:
175  _maskTransparency = None
176 
177 def setMaskTransparency(transparency=None, frame=None):
178  """Specify ds9's mask transparency (percent); or None to not set it when loading masks"""
179 
180  global _maskTransparency
181  if transparency is not None and (transparency < 0 or transparency > 100):
182  print >> sys.stderr, "Mask transparency should be in the range [0, 100]; clipping"
183  if transparency < 0:
184  transparency = 0
185  else:
186  transparency = 100
187 
188  _maskTransparency = transparency
189 
190  if transparency is not None:
191  ds9Cmd("mask transparency %d" % transparency, frame=frame)
192 
194  """Return requested ds9's mask transparency"""
195 
196  return _maskTransparency
197 
198 def getMaskTransparency(frame=None):
199  """Return the current ds9's mask transparency"""
200 
201  return float(ds9Cmd("mask transparency", get=True))
202 
203 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
204 
206  """Parse XPA_PORT and send return an identifier to send ds9 commands there, instead of "ds9"
207  If you don't have XPA_PORT set, the usual xpans tricks will be played when we return "ds9".
208  """
209  xpa_port = os.environ.get("XPA_PORT")
210  if xpa_port:
211  mat = re.search(r"^DS9:ds9\s+(\d+)\s+(\d+)", xpa_port)
212  if mat:
213  port1, port2 = mat.groups()
214 
215  return "127.0.0.1:%s" % (port1)
216  else:
217  print >> sys.stderr, "Failed to parse XPA_PORT=%s" % xpa_port
218 
219  return "ds9"
220 
222  """Return the version of ds9 in use, as a string"""
223  try:
224  v = ds9Cmd("about", get=True)
225  return v.splitlines()[1].split()[1]
226  except Exception, e:
227  print >> sys.stderr, "Error reading version: %s" % e
228  return "0.0.0"
229 
230 try:
231  cmdBuffer
232 except NameError:
233  XPA_SZ_LINE = 4096 - 100 # internal buffersize in xpa. Sigh; esp. as the 100 is some needed slop
234 
235  class Buffer(object):
236  """Control buffering the sending of commands to ds9;
237 annoying but necessary for anything resembling performance
238 
239 The usual usage pattern (from a module importing this file, ds9.py) is probably:
240 
241  with ds9.Buffering():
242  # bunches of ds9.{dot,line} commands
243  ds9.flush()
244  # bunches more ds9.{dot,line} commands
245 
246 or (if you don't like "with")
247  ds9.buffer()
248  # bunches of ds9.{dot,line} commands
249  ds9.flush()
250  # bunches more ds9.{dot,line} commands
251  ds9.buffer(False)
252 
253 or (the old idiom):
254 
255  ds9.cmdBuffer.pushSize()
256  # bunches of ds9.{dot,line} commands
257  ds9.cmdBuffer.flush()
258  # bunches more ds9.{dot,line} commands
259  ds9.cmdBuffer.popSize()
260  """
261 
262  def __init__(self, size=0):
263  """Create a command buffer, with a maximum depth of size"""
264  self._commands = "" # list of pending commands
265  self._lenCommands = len(self._commands)
266  self._bufsize = [] # stack of bufsizes
267 
268  self._bufsize.append(size) # don't call self.size() as ds9Cmd isn't defined yet
269 
270  def set(self, size, silent=True):
271  """Set the ds9 buffer size to size"""
272  if size < 0:
273  size = XPA_SZ_LINE - 5
274 
275  if size > XPA_SZ_LINE:
276  print >> sys.stderr, \
277  "xpa silently hardcodes a limit of %d for buffer sizes (you asked for %d) " % \
278  (XPA_SZ_LINE, size)
279  self.set(-1) # use max buffersize
280  return
281 
282  if self._bufsize:
283  self._bufsize[-1] = size # change current value
284  else:
285  self._bufsize.append(size) # there is no current value; set one
286 
287  self.flush(silent=silent)
288 
289  def _getSize(self):
290  """Get the current ds9 buffer size"""
291  return self._bufsize[-1]
292 
293  def pushSize(self, size=-1):
294  """Replace current ds9 command buffer size with size (see also popSize)
295  @param: Size of buffer (-1: largest possible given bugs in xpa)"""
296  self.flush(silent=True)
297  self._bufsize.append(0)
298  self.set(size, silent=True)
299 
300  def popSize(self):
301  """Switch back to the previous command buffer size (see also pushSize)"""
302  self.flush(silent=True)
303 
304  if len(self._bufsize) > 1:
305  self._bufsize.pop()
306 
307  def flush(self, silent=True):
308  """Flush the pending commands"""
309  ds9Cmd(flush=True, silent=silent)
310 
311  cmdBuffer = Buffer(0)
312 
313 def ds9Cmd(cmd=None, trap=True, flush=False, silent=True, frame=None, get=False):
314  """Issue a ds9 command, raising errors as appropriate"""
315 
316  if getDefaultFrame() is None:
317  return
318 
319  global cmdBuffer
320  if cmd:
321  if frame is not None:
322  cmd = "%s;" % selectFrame(frame) + cmd
323 
324  if get:
325  return xpa.get(None, getXpaAccessPoint(), cmd, "").strip()
326 
327  # Work around xpa's habit of silently truncating long lines
328  if cmdBuffer._lenCommands + len(cmd) > XPA_SZ_LINE - 5: # 5 to handle newlines and such like
329  ds9Cmd(flush=True, silent=silent)
330 
331  cmdBuffer._commands += ";" + cmd
332  cmdBuffer._lenCommands += 1 + len(cmd)
333 
334  if flush or cmdBuffer._lenCommands >= cmdBuffer._getSize():
335  cmd = cmdBuffer._commands + "\n"
336  cmdBuffer._commands = ""
337  cmdBuffer._lenCommands = 0
338  else:
339  return
340 
341  cmd = cmd.rstrip()
342  if not cmd:
343  return
344 
345  try:
346  ret = xpa.set(None, getXpaAccessPoint(), cmd, "", "", 0)
347  if ret:
348  raise IOError(ret)
349  except IOError, e:
350  if not trap:
351  raise Ds9Error, "XPA: %s, (%s)" % (e, cmd)
352  elif not silent:
353  print >> sys.stderr, "Caught ds9 exception processing command \"%s\": %s" % (cmd, e)
354 
355 def initDS9(execDs9=True):
356  try:
357  xpa.reset()
358  ds9Cmd("iconify no; raise", False)
359  ds9Cmd("wcs wcsa", False) # include the pixel coordinates WCS (WCSA)
360 
361  v0, v1 = ds9Version().split('.')[0:2]
362  global needShow
363  needShow = False
364  try:
365  if int(v0) == 5:
366  needShow = (int(v1) <= 4)
367  except:
368  pass
369  except Ds9Error, e:
370  if execDs9:
371  print "ds9 doesn't appear to be running (%s), I'll exec it for you" % e
372  if not re.search('xpa', os.environ['PATH']):
373  raise Ds9Error, 'You need the xpa binaries in your path to use ds9 with python'
374 
375  os.system('ds9 &')
376  for i in range(10):
377  try:
378  ds9Cmd(selectFrame(1), False)
379  break
380  except Ds9Error:
381  print "waiting for ds9...\r",
382  sys.stdout.flush()
383  time.sleep(0.5)
384  else:
385  print " \r",
386  break
387 
388  sys.stdout.flush()
389 
390  raise Ds9Error
391 
392 def show(frame=None):
393  """Uniconify and Raise ds9. N.b. throws an exception if frame doesn't exit"""
394  if frame is None:
395  frame = getDefaultFrame()
396 
397  if frame is None:
398  return
399 
400  ds9Cmd("raise", trap=False, frame=frame)
401 
402 def setMaskColor(color=GREEN):
403  """Set the ds9 mask colour to; eg. setMaskColor(RED)"""
404  ds9Cmd("mask color %s" % color)
405 
406 
407 def mtv(data, frame=None, init=True, wcs=None, isMask=False, lowOrderBits=False, title=None, settings=None):
408  """Display an Image or Mask on a DS9 display
409 
410  If lowOrderBits is True, give low-order-bits priority in display (i.e.
411  overlay them last)
412 
413  Historical note: the name "mtv" comes from Jim Gunn's forth imageprocessing
414  system, Mirella (named after Mirella Freni); The "m" stands for Mirella.
415  """
416 
417  if frame is None:
418  frame = getDefaultFrame()
419 
420  if frame is None:
421  return
422 
423  if init:
424  for i in range(3):
425  try:
426  initDS9(i == 0)
427  except Ds9Error:
428  print "waiting for ds9...\r",
429  sys.stdout.flush()
430  time.sleep(0.5)
431  else:
432  if i > 0:
433  print " \r",
434  sys.stdout.flush()
435  break
436 
437  ds9Cmd(selectFrame(frame))
438  ds9Cmd("smooth no")
439  erase(frame)
440 
441  if settings:
442  for setting in settings:
443  ds9Cmd("%s %s" % (setting, settings[setting]))
444 
445  if re.search("::DecoratedImage<", repr(data)): # it's a DecorateImage; display it
446  _mtv(data.getImage(), wcs, title, False)
447  elif re.search("::MaskedImage<", repr(data)): # it's a MaskedImage; display Image and overlay Mask
448  _mtv(data.getImage(), wcs, title, False)
449  mask = data.getMask(True)
450  if mask:
451  mtv(mask, frame, False, wcs, True, lowOrderBits=lowOrderBits, title=title, settings=settings)
452  if getDesiredMaskTransparency() is not None:
453  ds9Cmd("mask transparency %d" % getDesiredMaskTransparency())
454 
455  elif re.search("::Exposure<", repr(data)): # it's an Exposure; display the MaskedImage with the WCS
456  if wcs:
457  raise RuntimeError, "You may not specify a wcs with an Exposure"
458 
459  mtv(data.getMaskedImage(), frame, False, data.getWcs(),
460  False, lowOrderBits=lowOrderBits, title=title, settings=settings)
461 
462  elif re.search("::Mask<", repr(data)): # it's a Mask; display it, bitplane by bitplane
463  maskPlanes = data.getMaskPlaneDict()
464  nMaskPlanes = max(maskPlanes.values()) + 1
465 
466  planes = {} # build inverse dictionary
467  for key in maskPlanes.keys():
468  planes[maskPlanes[key]] = key
469 
470  colorIndex = 0 # index into maskColors
471 
472  if lowOrderBits:
473  planeList = range(nMaskPlanes - 1, -1, -1)
474  else:
475  planeList = range(nMaskPlanes)
476 
477  usedPlanes = long(afwMath.makeStatistics(data, afwMath.SUM).getValue())
478  mask = data.Factory(data.getDimensions())
479  #
480  # ds9 can't display a Mask without an image; so display an Image first
481  #
482  if not isMask:
483  im = afwImage.ImageU(data.getDimensions())
484  mtv(im, frame=frame)
485 
486  for p in planeList:
487  if planes.get(p):
488  pname = planes[p]
489  if not getMaskPlaneVisibility(pname):
490  continue
491  else:
492  pname = "unknown"
493 
494  if not getMaskPlaneVisibility(pname):
495  continue
496 
497  if not ((1 << p) & usedPlanes): # no pixels have this bitplane set
498  continue
499 
500  mask <<= data
501  mask &= (1 << p)
502 
503  color = getMaskPlaneColor(pname)
504 
505  if not color: # none was specified
506  while True:
507  color = _maskColors[colorIndex % len(_maskColors)]
508  colorIndex += 1
509  if color != WHITE and color != BLACK:
510  break
511 
512  setMaskColor(color)
513  _mtv(mask, wcs, title, True)
514  return
515  elif re.search("::Image<", repr(data)): # it's an Image; display it
516  _mtv(data, wcs, title, False)
517  else:
518  raise RuntimeError, "Unsupported type %s" % repr(data)
519 
520 try:
521  haveGzip
522 except NameError:
523  haveGzip = not os.system("gzip < /dev/null > /dev/null 2>&1") # does gzip work?
524 
525 def _mtv(data, wcs, title, isMask):
526  """Internal routine to display an Image or Mask on a DS9 display"""
527 
528  title = str(title) if title else ""
529 
530  if True:
531  if isMask:
532  xpa_cmd = "xpaset %s fits mask" % getXpaAccessPoint()
533  if re.search(r"unsigned short|boost::uint16_t", data.__str__()):
534  data |= 0x8000 # Hack. ds9 mis-handles BZERO/BSCALE in masks. This is a copy we're modifying
535  else:
536  xpa_cmd = "xpaset %s fits" % getXpaAccessPoint()
537 
538  if haveGzip:
539  xpa_cmd = "gzip | " + xpa_cmd
540 
541  pfd = os.popen(xpa_cmd, "w")
542  else:
543  pfd = file("foo.fits", "w")
544 
545  try:
546  displayLib.writeFitsImage(pfd.fileno(), data, wcs, title)
547  except Exception, e:
548  try:
549  pfd.close()
550  except:
551  pass
552 
553  raise e
554 
555  try:
556  pfd.close()
557  except:
558  pass
559 #
560 # Graphics commands
561 #
562 def buffer(enable=True):
563  if enable:
564  cmdBuffer.pushSize()
565  else:
566  cmdBuffer.popSize()
567 
568 flush = lambda : cmdBuffer.flush(silent=True)
569 
570 class Buffering(object):
571  """A class intended to be used with python's with statement:
572 E.g.
573  with ds9.Buffering():
574  ds9.dot("+", xc, yc)
575  """
576  def __enter__(self):
577  buffer(True)
578  def __exit__(self, *args):
579  buffer(False)
580 
581 def erase(frame=None):
582  """Erase the specified DS9 frame"""
583  if frame is None:
584  frame = getDefaultFrame()
585 
586  if frame is None:
587  return
588 
589  ds9Cmd("regions delete all", flush=True, frame=frame)
590 
591 def dot(symb, c, r, frame=None, size=2, ctype=None, fontFamily="helvetica", silent=True, textAngle=None):
592  """Draw a symbol onto the specified DS9 frame at (col,row) = (c,r) [0-based coordinates]
593 Possible values are:
594  + Draw a +
595  x Draw an x
596  * Draw a *
597  o Draw a circle
598  @:Mxx,Mxy,Myy Draw an ellipse with moments (Mxx, Mxy, Myy) (argument size is ignored)
599  An object derived from afwGeom.ellipses.BaseCore Draw the ellipse (argument size is ignored)
600 Any other value is interpreted as a string to be drawn. Strings obey the fontFamily (which may be extended
601 with other characteristics, e.g. "times bold italic". Text will be drawn rotated by textAngle (textAngle is
602 ignored otherwise).
603 
604 N.b. objects derived from BaseCore include Axes and Quadrupole.
605 """
606  if frame is None:
607  frame = getDefaultFrame()
608 
609  if frame is None:
610  return
611 
612  if isinstance(symb, int):
613  symb = "%d" % (symb)
614 
615  if ctype == None:
616  color = "" # the default
617  else:
618  color = ' # color=%s' % ctype
619 
620  cmd = selectFrame(frame) + "; "
621  r += 1
622  c += 1 # ds9 uses 1-based coordinates
623  if isinstance(symb, afwGeom.ellipses.BaseCore) or re.search(r"^@:", symb):
624  try:
625  mat = re.search(r"^@:([^,]+),([^,]+),([^,]+)", symb)
626  except TypeError:
627  pass
628  else:
629  if mat:
630  mxx, mxy, myy = [float(_) for _ in mat.groups()]
631  symb = afwGeom.ellipses.Quadrupole(mxx, myy, mxy)
632 
633  symb = afwGeom.ellipses.Axes(symb)
634  cmd += 'regions command {ellipse %g %g %g %g %g%s}; ' % (c, r, symb.getA(), symb.getB(),
635  math.degrees(symb.getTheta()), color)
636  elif symb == '+':
637  cmd += 'regions command {line %g %g %g %g%s}; ' % (c, r+size, c, r-size, color)
638  cmd += 'regions command {line %g %g %g %g%s}; ' % (c-size, r, c+size, r, color)
639  elif symb == 'x':
640  size = size/math.sqrt(2)
641  cmd += 'regions command {line %g %g %g %g%s}; ' % (c+size, r+size, c-size, r-size, color)
642  cmd += 'regions command {line %g %g %g %g%s}; ' % (c-size, r+size, c+size, r-size, color)
643  elif symb == '*':
644  size30 = 0.5*size
645  size60 = 0.5*math.sqrt(3)*size
646  cmd += 'regions command {line %g %g %g %g%s}; ' % (c+size, r, c-size, r, color)
647  cmd += 'regions command {line %g %g %g %g%s}; ' % (c-size30, r+size60, c+size30, r-size60, color)
648  cmd += 'regions command {line %g %g %g %g%s}; ' % (c+size30, r+size60, c-size30, r-size60, color)
649  elif symb == 'o':
650  cmd += 'regions command {circle %g %g %g%s}; ' % (c, r, size, color)
651  else:
652  try:
653  # We have to check for the frame's existance with show() as the text command crashed ds9 5.4
654  # if it doesn't
655  if needShow:
656  show(frame)
657 
658  color = re.sub("^ # ", "", color) # skip the leading " # "
659 
660  angle = ""
661  if textAngle is not None:
662  angle += " textangle=%.1f"%(textAngle)
663 
664  font = ""
665  if size != 2 or fontFamily != "helvetica":
666  fontFamily = fontFamily.split()
667  font += ' font="%s %d' % (fontFamily.pop(0), int(10*size/2.0 + 0.5))
668  if fontFamily:
669  font += " %s" % " ".join(fontFamily)
670  font += '"'
671  extra = ""
672  if color or angle or font:
673  extra = " # "
674  extra += color
675  extra += angle
676  extra += font
677 
678  cmd += 'regions command {text %g %g \"%s\"%s };' % (c, r, symb, extra)
679  except Exception, e:
680  print >> sys.stderr, ("Ds9 frame %d doesn't exist" % frame), e
681 
682  ds9Cmd(cmd, silent=silent)
683 
684 def line(points, frame=None, symbs=False, ctype=None):
685  """Draw a set of symbols or connect the points, a list of (col,row)
686 If symbs is True, draw points at the specified points using the desired symbol,
687 otherwise connect the dots. Ctype is the name of a colour (e.g. 'red')"""
688 
689  if frame is None:
690  frame = getDefaultFrame()
691 
692  if frame is None:
693  return
694 
695  if symbs:
696  for (c, r) in points:
697  dot(symbs, r, c, frame=frame, size=0.5, ctype=ctype)
698  else:
699  if ctype == None: # default
700  color = ""
701  else:
702  color = "# color=%s" % ctype
703 
704  if len(points) > 0:
705  cmd = selectFrame(frame) + "; "
706 
707  c0, r0 = points[0]
708  r0 += 1
709  c0 += 1 # ds9 uses 1-based coordinates
710  for (c, r) in points[1:]:
711  r += 1
712  c += 1 # ds9 uses 1-based coordinates
713  cmd += 'regions command { line %g %g %g %g %s};' % (c0, r0, c, r, color)
714  c0, r0 = c, r
715 
716  ds9Cmd(cmd)
717 #
718 # Set gray scale
719 #
720 def scale(min=None, max=None, type=None, frame=None):
721  if type is None:
722  if min is None and max is None:
723  raise Ds9Error("Please specify min and max, or a stretch type, or both")
724  else:
725  ds9Cmd("scale %s" % type, frame=frame)
726 
727  if min is None:
728  if max is not None:
729  raise Ds9Error("Please specify min")
730  else:
731  if max is None:
732  raise Ds9Error("Please specify max")
733  ds9Cmd("scale limits %g %g" % (min, max), frame=frame)
734 
735 #
736 # Zoom and Pan
737 #
738 def zoom(zoomfac=None, colc=None, rowc=None, frame=None):
739  """Zoom frame by specified amount, optionally panning also"""
740 
741  if frame < 0:
742  frame = getDefaultFrame()
743 
744  if frame is None:
745  return
746 
747  if frame is None:
748  frame = getDefaultFrame()
749 
750  if frame is None:
751  return
752 
753  if (rowc and colc is None) or (colc and rowc is None):
754  raise Ds9Error, "Please specify row and column center to pan about"
755 
756  if zoomfac == None and rowc == None:
757  zoomfac = 2
758 
759  cmd = selectFrame(frame) + "; "
760  if zoomfac != None:
761  cmd += "zoom to %d; " % zoomfac
762 
763  if rowc != None:
764  cmd += "pan to %g %g physical; " % (colc + 1, rowc + 1) # ds9 is 1-indexed. Grrr
765 
766  ds9Cmd(cmd, flush=True)
767 
768 def pan(colc=None, rowc=None, frame=None):
769  """Pan to (rowc, colc); see also zoom"""
770 
771  if frame is None:
772  frame = getDefaultFrame()
773 
774  if frame is None:
775  return
776 
777  zoom(None, colc, rowc, frame)
778 
779 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
780 
781 def interact():
782  """Enter an interactive loop, listening for key presses in ds9 and firing callbacks.
783 
784  Exit with q, <carriage return>, or <escape>
785 """
786 
787  while True:
788  vals = ds9Cmd("imexam key coordinate", get=True).split()
789  if vals[0] == "XPA$ERROR":
790  if vals[1:4] == ['unknown', 'option', '"-state"']:
791  pass # a ds9 bug --- you get this by hitting TAB
792  else:
793  print >> sys.stderr, "Error return from imexam:", " ".join(vals)
794  continue
795 
796  k = vals.pop(0)
797  try:
798  x = float(vals[0]); y = float(vals[1])
799  except:
800  x = float("NaN"); y = float("NaN")
801 
802  try:
803  if callbacks[k](k, x, y):
804  break
805  except KeyError:
806  print >> sys.stderr, "No callback is registered for %s" % k
807  except Exception, e:
808  print >> sys.stderr, "ds9.callbacks[%s](%s, %s, %s) failed: %s" % \
809  (k, k, x, y, e)
810 
811 #
812 # Default fallback function
813 #
814 def noop_callback(k, x, y):
815  """Callback function: arguments key, x, y"""
816  return False
817 
818 def setCallback(k, func=noop_callback, noRaise=False):
819  """Set the callback for key k to be func, returning the old callback
820  """
821 
822  if k in "f":
823  if noRaise:
824  return
825  raise RuntimeError("Key '%s' is already in use by ds9, so I can't add a callback for it" % k)
826 
827  ofunc = callbacks.get(k)
828  callbacks[k] = func
829 
830  return ofunc
831 
832 def getActiveCallbackKeys(onlyActive=True):
833  """!Return all callback keys
834 \param onlyActive If true only return keys that do something
835  """
836 
837  return sorted([k for k, func in callbacks.items() if not (onlyActive and func == noop_callback)])
838 
839 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
840 
841 try:
842  callbacks
843 except NameError:
844  callbacks = {}
845 
846  for ik in range(ord('a'), ord('z') + 1):
847  k = "%c" % ik
848  setCallback(k, noRaise=True)
849  setCallback(k.upper(), noRaise=True)
850 
851  for k in ('Return', 'XPA$ERROR', 'Shift_L', 'Shift_R'):
852  setCallback(k)
853 
854  for k in ('q', 'Escape'):
855  setCallback(k, lambda k, x, y: True)
856 
857  def _h_callback(k, x, y):
858  print "Enter q or <ESC> to leave interactive mode, h for this help, or a letter to fire a callback"
859  return False
860 
861  setCallback('h', _h_callback)
862 
863 
def setMaskPlaneVisibility
Definition: ds9.py:145
def getActiveCallbackKeys
Return all callback keys.
Definition: ds9.py:832
def getMaskPlaneVisibility
Definition: ds9.py:164
def setMaskPlaneColor
Definition: ds9.py:102
def setDefaultFrame
Definition: ds9.py:73
def getMaskPlaneColor
Definition: ds9.py:137
def getXpaAccessPoint
Definition: ds9.py:205
double max
Definition: attributes.cc:218
def getDefaultFrame
Definition: ds9.py:78
An error talking to ds9.
Definition: ds9.py:47
def incrDefaultFrame
Definition: ds9.py:82
def getDesiredMaskTransparency
Definition: ds9.py:193
def getMaskTransparency
Definition: ds9.py:198
Statistics makeStatistics(afwImage::Mask< afwImage::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl)
Specialization to handle Masks.
Definition: Statistics.cc:1023
def setMaskTransparency
Definition: ds9.py:177