LSST Applications g0265f82a02+c6dfa2ddaf,g2079a07aa2+1b2e822518,g2ab2c8e58b+dcaebcd53c,g2bbee38e9b+c6dfa2ddaf,g337abbeb29+c6dfa2ddaf,g3ddfee87b4+971473b56f,g420f1b9425+9c5d1f27f4,g4770a20bdc+7962a82c67,g50ff169b8f+2eb0e556e8,g52b1c1532d+90ebb246c7,g555ede804d+971473b56f,g591dd9f2cf+601419b17e,g5ec818987f+105adce70b,g858d7b2824+dcaebcd53c,g876c692160+5450b3d607,g8a8a8dda67+90ebb246c7,g8cdfe0ae6a+4fd9e222a8,g99cad8db69+ed556ab06a,g9ddcbc5298+a1346535a5,ga1e77700b3+df8f93165b,ga8c6da7877+e280585b77,gae46bcf261+c6dfa2ddaf,gb0e22166c9+8634eb87fb,gb3f2274832+5f6d78177c,gba4ed39666+1ac82b564f,gbb8dafda3b+d1938d02c0,gbeb006f7da+2258dca5ef,gc28159a63d+c6dfa2ddaf,gc86a011abf+dcaebcd53c,gcf0d15dbbd+971473b56f,gd2a12a3803+6772067a4c,gdaeeff99f8+1cafcb7cd4,ge79ae78c31+c6dfa2ddaf,gee10cc3b42+90ebb246c7,gf1cff7945b+dcaebcd53c,w.2024.14
LSST Data Management Base Package
Loading...
Searching...
No Matches
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
25import os
26import re
27import shutil
28import sys
29import time
30
31import numpy as np
32
33import lsst.afw.display.interface as interface
34import lsst.afw.display.virtualDevice as virtualDevice
35import lsst.afw.display.ds9Regions as ds9Regions
36
37try:
38 from . import xpa as xpa
39except ImportError as e:
40 print(f"Cannot import xpa: {e}", file=sys.stderr)
41
42import lsst.afw.display as afwDisplay
43import lsst.afw.math as afwMath
44
45try:
46 needShow
47except NameError:
48 needShow = True # Used to avoid a bug in ds9 5.4
49
50
51class Ds9Error(IOError):
52 """Represents an error communicating with DS9.
53 """
54
55
56try:
57 _maskTransparency
58except NameError:
59 _maskTransparency = None
60
61
63 """Parse XPA_PORT if set and return an identifier to send DS9 commands.
64
65 Returns
66 -------
67
68 xpaAccessPoint : `str`
69 Either a reference to the local host with the configured port, or the
70 string ``"ds9"``.
71
72 Notes
73 -----
74 If you don't have XPA_PORT set, the usual xpans tricks will be played
75 when we return ``"ds9"``.
76 """
77 xpa_port = os.environ.get("XPA_PORT")
78 if xpa_port:
79 mat = re.search(r"^DS9:ds9\s+(\d+)\s+(\d+)", xpa_port)
80 if mat:
81 port1, port2 = mat.groups()
82
83 return f"127.0.0.1:{port1}"
84 else:
85 print(f"Failed to parse XPA_PORT={xpa_port}", file=sys.stderr)
86
87 return "ds9"
88
89
91 """Get the version of DS9 in use.
92
93 Returns
94 -------
95 version : `str`
96 Version of DS9 in use.
97 """
98 try:
99 v = ds9Cmd("about", get=True)
100 return v.splitlines()[1].split()[1]
101 except Exception as e:
102 print(f"Error reading version: {e}", file=sys.stderr)
103 return "0.0.0"
104
105
106try:
107 cmdBuffer
108except NameError:
109 # internal buffersize in xpa. Sigh; esp. as the 100 is some needed slop
110 XPA_SZ_LINE = 4096 - 100
111
112 class Buffer:
113 """Buffer to control sending commands to DS9.
114
115 Notes
116 -----
117 The usual usage pattern is:
118
119 >>> with ds9.Buffering():
120 ... # bunches of ds9.{dot,line} commands
121 ... ds9.flush()
122 ... # bunches more ds9.{dot,line} commands
123 """
124
125 def __init__(self, size=0):
126 self._commands = "" # list of pending commands
127 self._lenCommands = len(self._commands)
128 self._bufsize = [] # stack of bufsizes
129
130 self._bufsize.append(size) # don't call self.size() as ds9Cmd isn't defined yet
131
132 def set(self, size, silent=True):
133 """Set the ds9 buffer size to size.
134
135 Parameters
136 ----------
137 size : `int`
138 Size of buffer. Requesting a negative size provides the
139 largest possible buffer given bugs in xpa.
140 silent : `bool`, optional
141 Do not print error messages (default `True`).
142 """
143 if size < 0:
144 size = XPA_SZ_LINE - 5
145
146 if size > XPA_SZ_LINE:
147 print("xpa silently hardcodes a limit of %d for buffer sizes (you asked for %d) " %
148 (XPA_SZ_LINE, size), file=sys.stderr)
149 self.set(-1) # use max buffersize
150 return
151
152 if self._bufsize:
153 self._bufsize[-1] = size # change current value
154 else:
155 self._bufsize.append(size) # there is no current value; set one
156
157 self.flush(silent=silent)
158
159 def _getSize(self):
160 """Get the current DS9 buffer size.
161
162 Returns
163 -------
164 size : `int`
165 Size of buffer.
166 """
167 return self._bufsize[-1]
168
169 def pushSize(self, size=-1):
170 """Replace current DS9 command buffer size.
171
172 Parameters
173 ----------
174 size : `int`, optional
175 Size of buffer. A negative value sets the largest possible
176 buffer.
177
178 Notes
179 -----
180 See also `popSize`.
181 """
182 self.flush(silent=True)
183 self._bufsize.append(0)
184 self.set(size, silent=True)
185
186 def popSize(self):
187 """Switch back to the previous command buffer size.
188
189 Notes
190 -----
191 See also `pushSize`.
192 """
193 self.flush(silent=True)
194
195 if len(self._bufsize) > 1:
196 self._bufsize.pop()
197
198 def flush(self, silent=True):
199 """Flush the pending commands.
200
201 Parameters
202 ----------
203 silent : `bool`, optional
204 Do not print error messages.
205 """
206 ds9Cmd(flush=True, silent=silent)
207
208 cmdBuffer = Buffer(0)
209
210
211def selectFrame(frame):
212 """Convert integer frame number to DS9 command syntax.
213
214 Parameters
215 ----------
216 frame : `int`
217 Frame number
218
219 Returns
220 -------
221 frameString : `str`
222 """
223 return f"frame {frame}"
224
225
226def ds9Cmd(cmd=None, trap=True, flush=False, silent=True, frame=None, get=False):
227 """Issue a DS9 command, raising errors as appropriate.
228
229 Parameters
230 ----------
231 cmd : `str`, optional
232 Command to execute.
233 trap : `bool`, optional
234 Trap errors.
235 flush : `bool`, optional
236 Flush the output.
237 silent : `bool`, optional
238 Do not print trapped error messages.
239 frame : `int`, optional
240 Frame number on which to execute command.
241 get : `bool`, optional
242 Return xpa response.
243 """
244
245 global cmdBuffer
246 if cmd:
247 if frame is not None:
248 cmd = f"{selectFrame(frame)};{cmd}"
249
250 if get:
251 return xpa.get(None, getXpaAccessPoint(), cmd, "").strip()
252
253 # Work around xpa's habit of silently truncating long lines; the value
254 # ``5`` provides some margin to handle new lines and the like.
255 if cmdBuffer._lenCommands + len(cmd) > XPA_SZ_LINE - 5:
256 ds9Cmd(flush=True, silent=silent)
257
258 cmdBuffer._commands += ";" + cmd
259 cmdBuffer._lenCommands += 1 + len(cmd)
260
261 if flush or cmdBuffer._lenCommands >= cmdBuffer._getSize():
262 cmd = (cmdBuffer._commands + "\n")
263 cmdBuffer._commands = ""
264 cmdBuffer._lenCommands = 0
265 else:
266 return
267
268 cmd = cmd.rstrip()
269 if not cmd:
270 return
271
272 try:
273 ret = xpa.set(None, getXpaAccessPoint(), cmd, "", "", 0)
274 if ret:
275 raise OSError(ret)
276 except OSError as e:
277 if not trap:
278 raise Ds9Error(f"XPA: {e}, ({cmd})")
279 elif not silent:
280 print(f"Caught ds9 exception processing command \"{cmd}\": {e}", file=sys.stderr)
281
282
283def initDS9(execDs9=True):
284 """Initialize DS9.
285
286 Parameters
287 ----------
288 execDs9 : `bool`, optional
289 If DS9 is not running, attempt to execute it.
290 """
291 try:
292 xpa.reset()
293 ds9Cmd("iconify no; raise", False)
294 ds9Cmd("wcs wcsa", False) # include the pixel coordinates WCS (WCSA)
295
296 v0, v1 = ds9Version().split('.')[0:2]
297 global needShow
298 needShow = False
299 try:
300 if int(v0) == 5:
301 needShow = (int(v1) <= 4)
302 except Exception:
303 pass
304 except Ds9Error as e:
305 if not re.search('xpa', os.environ['PATH']):
306 raise Ds9Error('You need the xpa binaries in your path to use ds9 with python')
307
308 if not execDs9:
309 raise Ds9Error
310
311 if not shutil.which("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(f"ds9 doesn't appear to be running ({e}), I'll try to exec it for you")
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
336class 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
344class 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(f"ds9 is unable to set transparency for individual maskplanes ({maskplane})",
369 file=sys.stderr)
370 return
371 ds9Cmd(f"mask transparency {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.getBBox()) # 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(f"mask color {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 += f'regions command {{{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 += f'regions command {{{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(f"scale {algorithm}", frame=self.display.frame)
566
567 if min in ("minmax", "zscale"):
568 ds9Cmd(f"scale mode {min}")
569 else:
570 if unit:
571 print(f"ds9: ignoring scale unit {unit}")
572
573 ds9Cmd(f"scale limits {min:g} {max:g}", 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 += f"zoom to {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 += f"pan to {colc + 1:g} {rowc + 1:g} physical; "
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
634try:
635 haveGzip
636except NameError:
637 # does gzip work?
638 haveGzip = not os.system("gzip < /dev/null > /dev/null 2>&1")
639
640
641def _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 isMask:
658 xpa_cmd = f"xpaset {getXpaAccessPoint()} fits mask"
659 # ds9 mis-handles BZERO/BSCALE in uint16 data.
660 # The following hack works around this.
661 # This is a copy we're modifying
662 if data.getArray().dtype == np.uint16:
663 data |= 0x8000
664 else:
665 xpa_cmd = f"xpaset {getXpaAccessPoint()} fits"
666
667 if haveGzip:
668 xpa_cmd = "gzip | " + xpa_cmd
669
670 pfd = os.popen(xpa_cmd, "w")
671
672 ds9Cmd(flush=True, silent=True)
673
674 try:
675 afwDisplay.writeFitsImage(pfd.fileno(), data, wcs, title)
676 except Exception as e:
677 try:
678 pfd.close()
679 except Exception:
680 pass
681
682 raise e
683
684 try:
685 pfd.close()
686 except Exception:
687 pass
int max
__init__(self, size=0)
Definition ds9.py:125
flush(self, silent=True)
Definition ds9.py:198
pushSize(self, size=-1)
Definition ds9.py:169
set(self, size, silent=True)
Definition ds9.py:132
_getMaskTransparency(self, maskplane)
Definition ds9.py:373
__init__(self, display, verbose=False, *args, **kwargs)
Definition ds9.py:348
_drawLines(self, points, ctype)
Definition ds9.py:530
_scale(self, algorithm, min, max, unit, *args, **kwargs)
Definition ds9.py:546
_dot(self, symb, c, r, size, ctype, fontFamily="helvetica", textAngle=None)
Definition ds9.py:487
_setMaskTransparency(self, transparency, maskplane)
Definition ds9.py:356
_pan(self, colc, rowc)
Definition ds9.py:591
_mtv(self, image, mask=None, wcs=None, title="")
Definition ds9.py:393
__init__(self, k, x, y)
Definition ds9.py:340
daf::base::PropertySet * set
Definition fits.cc:931
bool strip
Definition fits.cc:930
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:361
initDS9(execDs9=True)
Definition ds9.py:283
ds9Cmd(cmd=None, trap=True, flush=False, silent=True, frame=None, get=False)
Definition ds9.py:226
selectFrame(frame)
Definition ds9.py:211
_i_mtv(data, wcs, title, isMask)
Definition ds9.py:641