27 from __future__
import with_statement
29 import os, re, math, sys, time
33 except ImportError, e:
34 print >> sys.stderr,
"Cannot import xpa: %s" % e
48 """Some problem talking to ds9"""
57 if frame != _currentFrame:
61 return "frame %d" % (frame + _frame0)
64 """Add frame0 to all frame specifications"""
74 """Set the default frame for ds9"""
79 """Get the default frame for ds9"""
83 """Increment the default frame for ds9"""
100 _maskColors = [WHITE, BLACK, RED, GREEN, BLUE, CYAN, MAGENTA, YELLOW]
103 """Request that mask plane name be displayed as color; name may be a dictionary
104 (in which case color should be omitted"""
106 if isinstance(name, dict):
108 for k
in name.keys():
112 global _maskPlaneColors
114 type(_maskPlaneColors)
116 _maskPlaneColors = {}
118 _maskPlaneColors[name] = color
127 "INTERPOLATED" : GREEN,
130 "DETECTED_NEGATIVE" : CYAN,
138 """Return the colour associated with the specified mask plane name"""
140 if _maskPlaneColors.has_key(name):
141 return _maskPlaneColors[name]
146 """Specify the visibility of a given mask plane;
147 name may be a dictionary (in which case show will be ignored)"""
149 global _maskPlaneVisibility
151 type(_maskPlaneVisibility)
153 _maskPlaneVisibility = {}
155 if isinstance(name, dict):
156 for k
in name.keys():
160 _maskPlaneVisibility[name] = show
165 """Should we display the specified mask plane name?"""
167 if _maskPlaneVisibility.has_key(name):
168 return _maskPlaneVisibility[name]
175 _maskTransparency =
None
178 """Specify ds9's mask transparency (percent); or None to not set it when loading masks"""
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"
188 _maskTransparency = transparency
190 if transparency
is not None:
191 ds9Cmd(
"mask transparency %d" % transparency, frame=frame)
194 """Return requested ds9's mask transparency"""
196 return _maskTransparency
199 """Return the current ds9's mask transparency"""
201 return float(
ds9Cmd(
"mask transparency", get=
True))
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".
209 xpa_port = os.environ.get(
"XPA_PORT")
211 mat = re.search(
r"^DS9:ds9\s+(\d+)\s+(\d+)", xpa_port)
213 port1, port2 = mat.groups()
215 return "127.0.0.1:%s" % (port1)
217 print >> sys.stderr,
"Failed to parse XPA_PORT=%s" % xpa_port
222 """Return the version of ds9 in use, as a string"""
224 v =
ds9Cmd(
"about", get=
True)
225 return v.splitlines()[1].split()[1]
227 print >> sys.stderr,
"Error reading version: %s" % e
233 XPA_SZ_LINE = 4096 - 100
236 """Control buffering the sending of commands to ds9;
237 annoying but necessary for anything resembling performance
239 The usual usage pattern (from a module importing this file, ds9.py) is probably:
241 with ds9.Buffering():
242 # bunches of ds9.{dot,line} commands
244 # bunches more ds9.{dot,line} commands
246 or (if you don't like "with")
248 # bunches of ds9.{dot,line} commands
250 # bunches more ds9.{dot,line} commands
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()
263 """Create a command buffer, with a maximum depth of size"""
268 self._bufsize.append(size)
270 def set(self, size, silent=True):
271 """Set the ds9 buffer size to size"""
273 size = XPA_SZ_LINE - 5
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) " % \
285 self._bufsize.append(size)
287 self.
flush(silent=silent)
290 """Get the current ds9 buffer size"""
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)
301 """Switch back to the previous command buffer size (see also pushSize)"""
302 self.
flush(silent=
True)
308 """Flush the pending commands"""
309 ds9Cmd(flush=
True, silent=silent)
313 def ds9Cmd(cmd=None, trap=True, flush=False, silent=True, frame=None, get=False):
314 """Issue a ds9 command, raising errors as appropriate"""
321 if frame
is not None:
328 if cmdBuffer._lenCommands + len(cmd) > XPA_SZ_LINE - 5:
329 ds9Cmd(flush=
True, silent=silent)
331 cmdBuffer._commands +=
";" + cmd
332 cmdBuffer._lenCommands += 1 + len(cmd)
334 if flush
or cmdBuffer._lenCommands >= cmdBuffer._getSize():
335 cmd = cmdBuffer._commands +
"\n"
336 cmdBuffer._commands =
""
337 cmdBuffer._lenCommands = 0
351 raise Ds9Error,
"XPA: %s, (%s)" % (e, cmd)
353 print >> sys.stderr,
"Caught ds9 exception processing command \"%s\": %s" % (cmd, e)
358 ds9Cmd(
"iconify no; raise",
False)
366 needShow = (int(v1) <= 4)
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'
381 print "waiting for ds9...\r",
393 """Uniconify and Raise ds9. N.b. throws an exception if frame doesn't exit"""
400 ds9Cmd(
"raise", trap=
False, frame=frame)
403 """Set the ds9 mask colour to; eg. setMaskColor(RED)"""
404 ds9Cmd(
"mask color %s" % color)
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
410 If lowOrderBits is True, give low-order-bits priority in display (i.e.
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.
428 print "waiting for ds9...\r",
442 for setting
in settings:
443 ds9Cmd(
"%s %s" % (setting, settings[setting]))
445 if re.search(
"::DecoratedImage<", repr(data)):
446 _mtv(data.getImage(), wcs, title,
False)
447 elif re.search(
"::MaskedImage<", repr(data)):
448 _mtv(data.getImage(), wcs, title,
False)
449 mask = data.getMask(
True)
451 mtv(mask, frame,
False, wcs,
True, lowOrderBits=lowOrderBits, title=title, settings=settings)
455 elif re.search(
"::Exposure<", repr(data)):
457 raise RuntimeError,
"You may not specify a wcs with an Exposure"
459 mtv(data.getMaskedImage(), frame,
False, data.getWcs(),
460 False, lowOrderBits=lowOrderBits, title=title, settings=settings)
462 elif re.search(
"::Mask<", repr(data)):
463 maskPlanes = data.getMaskPlaneDict()
464 nMaskPlanes =
max(maskPlanes.values()) + 1
467 for key
in maskPlanes.keys():
468 planes[maskPlanes[key]] = key
473 planeList = range(nMaskPlanes - 1, -1, -1)
475 planeList = range(nMaskPlanes)
478 mask = data.Factory(data.getDimensions())
483 im = afwImage.ImageU(data.getDimensions())
497 if not ((1 << p) & usedPlanes):
507 color = _maskColors[colorIndex % len(_maskColors)]
509 if color != WHITE
and color != BLACK:
513 _mtv(mask, wcs, title,
True)
515 elif re.search(
"::Image<", repr(data)):
516 _mtv(data, wcs, title,
False)
518 raise RuntimeError,
"Unsupported type %s" % repr(data)
523 haveGzip =
not os.system(
"gzip < /dev/null > /dev/null 2>&1")
525 def _mtv(data, wcs, title, isMask):
526 """Internal routine to display an Image or Mask on a DS9 display"""
528 title = str(title)
if title
else ""
533 if re.search(
r"unsigned short|boost::uint16_t", data.__str__()):
539 xpa_cmd =
"gzip | " + xpa_cmd
541 pfd = os.popen(xpa_cmd,
"w")
543 pfd = file(
"foo.fits",
"w")
546 displayLib.writeFitsImage(pfd.fileno(), data, wcs, title)
568 flush =
lambda : cmdBuffer.flush(silent=
True)
571 """A class intended to be used with python's with statement:
573 with ds9.Buffering():
582 """Erase the specified DS9 frame"""
589 ds9Cmd(
"regions delete all", flush=
True, frame=frame)
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]
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
604 N.b. objects derived from BaseCore include Axes and Quadrupole.
612 if isinstance(symb, int):
618 color =
' # color=%s' % ctype
623 if isinstance(symb, afwGeom.ellipses.BaseCore)
or re.search(
r"^@:", symb):
625 mat = re.search(
r"^@:([^,]+),([^,]+),([^,]+)", symb)
630 mxx, mxy, myy = [float(_)
for _
in mat.groups()]
631 symb = afwGeom.ellipses.Quadrupole(mxx, myy, mxy)
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)
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)
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)
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)
650 cmd +=
'regions command {circle %g %g %g%s}; ' % (c, r, size, color)
658 color = re.sub(
"^ # ",
"", color)
661 if textAngle
is not None:
662 angle +=
" textangle=%.1f"%(textAngle)
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))
669 font +=
" %s" %
" ".join(fontFamily)
672 if color
or angle
or font:
678 cmd +=
'regions command {text %g %g \"%s\"%s };' % (c, r, symb, extra)
680 print >> sys.stderr, (
"Ds9 frame %d doesn't exist" % frame), e
682 ds9Cmd(cmd, silent=silent)
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')"""
696 for (c, r)
in points:
697 dot(symbs, r, c, frame=frame, size=0.5, ctype=ctype)
702 color =
"# color=%s" % ctype
710 for (c, r)
in points[1:]:
713 cmd +=
'regions command { line %g %g %g %g %s};' % (c0, r0, c, r, color)
720 def scale(min=None, max=None, type=None, frame=None):
722 if min
is None and max
is None:
723 raise Ds9Error(
"Please specify min and max, or a stretch type, or both")
725 ds9Cmd(
"scale %s" % type, frame=frame)
729 raise Ds9Error(
"Please specify min")
732 raise Ds9Error(
"Please specify max")
733 ds9Cmd(
"scale limits %g %g" % (min, max), frame=frame)
738 def zoom(zoomfac=None, colc=None, rowc=None, frame=None):
739 """Zoom frame by specified amount, optionally panning also"""
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"
756 if zoomfac ==
None and rowc ==
None:
761 cmd +=
"zoom to %d; " % zoomfac
764 cmd +=
"pan to %g %g physical; " % (colc + 1, rowc + 1)
768 def pan(colc=None, rowc=None, frame=None):
769 """Pan to (rowc, colc); see also zoom"""
777 zoom(
None, colc, rowc, frame)
782 """Enter an interactive loop, listening for key presses in ds9 and firing callbacks.
784 Exit with q, <carriage return>, or <escape>
788 vals =
ds9Cmd(
"imexam key coordinate", get=
True).split()
789 if vals[0] ==
"XPA$ERROR":
790 if vals[1:4] == [
'unknown',
'option',
'"-state"']:
793 print >> sys.stderr,
"Error return from imexam:",
" ".join(vals)
798 x = float(vals[0]); y = float(vals[1])
800 x = float(
"NaN"); y = float(
"NaN")
803 if callbacks[k](k, x, y):
806 print >> sys.stderr,
"No callback is registered for %s" % k
808 print >> sys.stderr,
"ds9.callbacks[%s](%s, %s, %s) failed: %s" % \
815 """Callback function: arguments key, x, y"""
819 """Set the callback for key k to be func, returning the old callback
825 raise RuntimeError(
"Key '%s' is already in use by ds9, so I can't add a callback for it" % k)
827 ofunc = callbacks.get(k)
833 """!Return all callback keys
834 \param onlyActive If true only return keys that do something
837 return sorted([k
for k, func
in callbacks.items()
if not (onlyActive
and func == noop_callback)])
846 for ik
in range(ord(
'a'), ord(
'z') + 1):
851 for k
in (
'Return',
'XPA$ERROR',
'Shift_L',
'Shift_R'):
854 for k
in (
'q',
'Escape'):
858 print "Enter q or <ESC> to leave interactive mode, h for this help, or a letter to fire a callback"
def setMaskPlaneVisibility
def getActiveCallbackKeys
Return all callback keys.
def getMaskPlaneVisibility
def getDesiredMaskTransparency
Statistics makeStatistics(afwImage::Mask< afwImage::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl)
Specialization to handle Masks.