Coverage for python/lsst/afw/display/interface.py: 26%

349 statements  

« prev     ^ index     » next       coverage.py v7.1.0, created at 2023-02-05 17:50 -0800

1 

2# This file is part of afw. 

3# 

4# Developed for the LSST Data Management System. 

5# This product includes software developed by the LSST Project 

6# (https://www.lsst.org). 

7# See the COPYRIGHT file at the top-level directory of this distribution 

8# for details of code ownership. 

9# 

10# This program is free software: you can redistribute it and/or modify 

11# it under the terms of the GNU General Public License as published by 

12# the Free Software Foundation, either version 3 of the License, or 

13# (at your option) any later version. 

14# 

15# This program is distributed in the hope that it will be useful, 

16# but WITHOUT ANY WARRANTY; without even the implied warranty of 

17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

18# GNU General Public License for more details. 

19# 

20# You should have received a copy of the GNU General Public License 

21# along with this program. If not, see <https://www.gnu.org/licenses/>. 

22 

23__all__ = [ 

24 "WHITE", "BLACK", "RED", "GREEN", "BLUE", "CYAN", "MAGENTA", "YELLOW", "ORANGE", "IGNORE", 

25 "Display", "Event", "noop_callback", "h_callback", 

26 "setDefaultBackend", "getDefaultBackend", 

27 "setDefaultFrame", "getDefaultFrame", "incrDefaultFrame", 

28 "setDefaultMaskTransparency", "setDefaultMaskPlaneColor", 

29 "getDisplay", "delAllDisplays", 

30] 

31 

32import re 

33import sys 

34import importlib 

35import lsst.afw.geom as afwGeom 

36import lsst.afw.image as afwImage 

37import lsst.log 

38 

39logger = lsst.log.Log.getLogger("afw.display.interface") 

40 

41# 

42# Symbolic names for mask/line colors. N.b. ds9 supports any X11 color for masks 

43# 

44WHITE = "white" 

45BLACK = "black" 

46RED = "red" 

47GREEN = "green" 

48BLUE = "blue" 

49CYAN = "cyan" 

50MAGENTA = "magenta" 

51YELLOW = "yellow" 

52ORANGE = "orange" 

53IGNORE = "ignore" 

54 

55 

56def _makeDisplayImpl(display, backend, *args, **kwargs): 

57 """Return the ``DisplayImpl`` for the named backend 

58 

59 Parameters 

60 ---------- 

61 display : `str` 

62 Name of device. Should be importable, either absolutely or relative to lsst.display 

63 backend : `str` 

64 The desired backend 

65 *args 

66 Arguments passed to DisplayImpl.__init__ 

67 *kwargs 

68 Keywords arguments passed to DisplayImpl.__init__ 

69 

70 Examples 

71 -------- 

72 E.g. 

73 .. code-block:: py 

74 

75 import lsst.afw.display as afwDisplay 

76 display = afwDisplay.Display(display=1, backend="ds9") 

77 would call 

78 .. code-block:: py 

79 

80 _makeDisplayImpl(..., "ds9", 1) 

81 and import the ds9 implementation of ``DisplayImpl`` from `lsst.display.ds9` 

82 """ 

83 _disp = None 

84 exc = None 

85 for dt in (f"lsst.display.{backend}", backend, f".{backend}", f"lsst.afw.display.{backend}"): 

86 exc = None 

87 # only specify the root package if we are not doing an absolute import 

88 impargs = {} 

89 if dt.startswith("."): 

90 impargs["package"] = "lsst.display" 

91 try: 

92 _disp = importlib.import_module(dt, **impargs) 

93 break 

94 except (ImportError, SystemError) as e: 

95 # SystemError can be raised in Python 3.5 if a relative import 

96 # is attempted when the root package, lsst.display, does not exist. 

97 # Copy the exception into outer scope 

98 exc = e 

99 

100 if not _disp or not hasattr(_disp.DisplayImpl, "_show"): 100 ↛ 108line 100 didn't jump to line 108, because the condition on line 100 was never false

101 if exc is not None: 101 ↛ 105line 101 didn't jump to line 105, because the condition on line 101 was never false

102 # re-raise the final exception 

103 raise exc 

104 else: 

105 raise ImportError( 

106 "Could not load the requested backend: {}".format(backend)) 

107 

108 if display: 

109 _impl = _disp.DisplayImpl(display, *args, **kwargs) 

110 if not hasattr(_impl, "frame"): 

111 _impl.frame = display.frame 

112 

113 return _impl 

114 else: 

115 return True 

116 

117 

118class Display: 

119 """Create an object able to display images and overplot glyphs 

120 

121 Parameters 

122 ---------- 

123 frame 

124 An identifier for the display 

125 backend : `str` 

126 The backend to use (defaults to value set by setDefaultBackend()) 

127 *args 

128 Arguments to pass to the backend 

129 **kwargs 

130 Arguments to pass to the backend 

131 """ 

132 _displays = {} 

133 _defaultBackend = None 

134 _defaultFrame = 0 

135 _defaultMaskPlaneColor = dict( 

136 BAD=RED, 

137 CR=MAGENTA, 

138 EDGE=YELLOW, 

139 INTERPOLATED=GREEN, 

140 SATURATED=GREEN, 

141 DETECTED=BLUE, 

142 DETECTED_NEGATIVE=CYAN, 

143 SUSPECT=YELLOW, 

144 NO_DATA=ORANGE, 

145 # deprecated names 

146 INTRP=GREEN, 

147 SAT=GREEN, 

148 ) 

149 _defaultMaskTransparency = {} 

150 _defaultImageColormap = "gray" 

151 

152 def __init__(self, frame=None, backend=None, *args, **kwargs): 

153 if frame is None: 

154 frame = getDefaultFrame() 

155 

156 if backend is None: 

157 if Display._defaultBackend is None: 

158 try: 

159 setDefaultBackend("ds9") 

160 except RuntimeError: 

161 setDefaultBackend("virtualDevice") 

162 

163 backend = Display._defaultBackend 

164 

165 self.frame = frame 

166 self._impl = _makeDisplayImpl(self, backend, *args, **kwargs) 

167 self.name = backend 

168 

169 self._xy0 = None # displayed data's XY0 

170 self.setMaskTransparency(Display._defaultMaskTransparency) 

171 self._maskPlaneColors = {} 

172 self.setMaskPlaneColor(Display._defaultMaskPlaneColor) 

173 self.setImageColormap(Display._defaultImageColormap) 

174 

175 self._callbacks = {} 

176 

177 for ik in range(ord('a'), ord('z') + 1): 

178 k = f"{ik:c}" 

179 self.setCallback(k, noRaise=True) 

180 self.setCallback(k.upper(), noRaise=True) 

181 

182 for k in ('Return', 'Shift_L', 'Shift_R'): 

183 self.setCallback(k) 

184 

185 for k in ('q', 'Escape'): 

186 self.setCallback(k, lambda k, x, y: True) 

187 

188 def _h_callback(k, x, y): 

189 h_callback(k, x, y) 

190 

191 for k in sorted(self._callbacks.keys()): 

192 doc = self._callbacks[k].__doc__ 

193 print(" %-6s %s" % (k, doc.split("\n")[0] if doc else "???")) 

194 

195 self.setCallback('h', _h_callback) 

196 

197 Display._displays[frame] = self 

198 

199 def __enter__(self): 

200 """Support for python's with statement 

201 """ 

202 return self 

203 

204 def __exit__(self, *args): 

205 """Support for python's with statement 

206 """ 

207 self.close() 

208 

209 def __del__(self): 

210 self.close() 

211 

212 def __getattr__(self, name): 

213 """Return the attribute of ``self._impl``, or ``._impl`` if it is requested 

214 

215 Parameters: 

216 ----------- 

217 name : `str` 

218 name of the attribute requested 

219 

220 Returns: 

221 -------- 

222 attribute : `object` 

223 the attribute of self._impl for the requested name 

224 """ 

225 

226 if name == '_impl': 

227 return object.__getattr__(self, name) 

228 

229 if not (hasattr(self, "_impl") and self._impl): 

230 raise AttributeError("Device has no _impl attached") 

231 

232 try: 

233 return getattr(self._impl, name) 

234 except AttributeError: 

235 raise AttributeError( 

236 f"Device {self.name} has no attribute \"{name}\"") 

237 

238 def close(self): 

239 if getattr(self, "_impl", None) is not None: 

240 self._impl._close() 

241 del self._impl 

242 self._impl = None 

243 

244 if self.frame in Display._displays: 

245 del Display._displays[self.frame] 

246 

247 @property 

248 def verbose(self): 

249 """The backend's verbosity 

250 """ 

251 return self._impl.verbose 

252 

253 @verbose.setter 

254 def verbose(self, value): 

255 if self._impl: 

256 self._impl.verbose = value 

257 

258 def __str__(self): 

259 return f"Display[{self.frame}]" 

260 

261 # 

262 # Handle Displays, including the default one (the frame to use when a user specifies None) 

263 # 

264 @staticmethod 

265 def setDefaultBackend(backend): 

266 try: 

267 _makeDisplayImpl(None, backend) 

268 except Exception as e: 

269 raise RuntimeError( 

270 f"Unable to set backend to {backend}: \"{e}\"") 

271 

272 Display._defaultBackend = backend 

273 

274 @staticmethod 

275 def getDefaultBackend(): 

276 return Display._defaultBackend 

277 

278 @staticmethod 

279 def setDefaultFrame(frame=0): 

280 """Set the default frame for display 

281 """ 

282 Display._defaultFrame = frame 

283 

284 @staticmethod 

285 def getDefaultFrame(): 

286 """Get the default frame for display 

287 """ 

288 return Display._defaultFrame 

289 

290 @staticmethod 

291 def incrDefaultFrame(): 

292 """Increment the default frame for display 

293 """ 

294 Display._defaultFrame += 1 

295 return Display._defaultFrame 

296 

297 @staticmethod 

298 def setDefaultMaskTransparency(maskPlaneTransparency={}): 

299 if hasattr(maskPlaneTransparency, "copy"): 299 ↛ 300line 299 didn't jump to line 300, because the condition on line 299 was never true

300 maskPlaneTransparency = maskPlaneTransparency.copy() 

301 

302 Display._defaultMaskTransparency = maskPlaneTransparency 

303 

304 @staticmethod 

305 def setDefaultMaskPlaneColor(name=None, color=None): 

306 """Set the default mapping from mask plane names to colors 

307 

308 Parameters 

309 ---------- 

310 name : `str` or `dict` 

311 name of mask plane, or a dict mapping names to colors 

312 If name is `None`, use the hard-coded default dictionary 

313 color 

314 Desired color, or `None` if name is a dict 

315 """ 

316 

317 if name is None: 

318 name = Display._defaultMaskPlaneColor 

319 

320 if isinstance(name, dict): 

321 assert color is None 

322 for k, v in name.items(): 

323 setDefaultMaskPlaneColor(k, v) 

324 return 

325 # 

326 # Set the individual color values 

327 # 

328 Display._defaultMaskPlaneColor[name] = color 

329 

330 @staticmethod 

331 def setDefaultImageColormap(cmap): 

332 """Set the default colormap for images 

333 

334 Parameters 

335 ---------- 

336 cmap : `str` 

337 Name of colormap, as interpreted by the backend 

338 

339 Notes 

340 ----- 

341 The only colormaps that all backends are required to honor 

342 (if they pay any attention to setImageColormap) are "gray" and "grey" 

343 """ 

344 

345 Display._defaultImageColormap = cmap 

346 

347 def setImageColormap(self, cmap): 

348 """Set the colormap to use for images 

349 

350 Parameters 

351 ---------- 

352 cmap : `str` 

353 Name of colormap, as interpreted by the backend 

354 

355 Notes 

356 ----- 

357 The only colormaps that all backends are required to honor 

358 (if they pay any attention to setImageColormap) are "gray" and "grey" 

359 """ 

360 

361 self._impl._setImageColormap(cmap) 

362 

363 @staticmethod 

364 def getDisplay(frame=None, backend=None, create=True, verbose=False, *args, **kwargs): 

365 """Return a specific `Display`, creating it if need be 

366 

367 Parameters 

368 ---------- 

369 frame 

370 The desired frame (`None` => use defaultFrame (see `~Display.setDefaultFrame`)) 

371 backend : `str` 

372 create the specified frame using this backend (or the default if 

373 `None`) if it doesn't already exist. If ``backend == ""``, it's an 

374 error to specify a non-existent ``frame``. 

375 create : `bool` 

376 create the display if it doesn't already exist. 

377 verbose : `bool` 

378 Allow backend to be chatty 

379 *args 

380 arguments passed to `Display` constructor 

381 **kwargs 

382 keyword arguments passed to `Display` constructor 

383 """ 

384 

385 if frame is None: 

386 frame = Display._defaultFrame 

387 

388 if frame not in Display._displays: 

389 if backend == "": 

390 raise RuntimeError(f"Frame {frame} does not exist") 

391 

392 Display._displays[frame] = Display( 

393 frame, backend, verbose=verbose, *args, **kwargs) 

394 

395 Display._displays[frame].verbose = verbose 

396 return Display._displays[frame] 

397 

398 @staticmethod 

399 def delAllDisplays(): 

400 """Delete and close all known displays 

401 """ 

402 for disp in list(Display._displays.values()): 

403 disp.close() 

404 Display._displays = {} 

405 

406 def maskColorGenerator(self, omitBW=True): 

407 """A generator for "standard" colors 

408 

409 Parameters 

410 ---------- 

411 omitBW : `bool` 

412 Don't include `BLACK` and `WHITE` 

413 

414 Examples 

415 -------- 

416 

417 .. code-block:: py 

418 

419 colorGenerator = interface.maskColorGenerator(omitBW=True) 

420 for p in planeList: 

421 print p, next(colorGenerator) 

422 """ 

423 _maskColors = [WHITE, BLACK, RED, GREEN, 

424 BLUE, CYAN, MAGENTA, YELLOW, ORANGE] 

425 

426 i = -1 

427 while True: 

428 i += 1 

429 color = _maskColors[i%len(_maskColors)] 

430 if omitBW and color in (BLACK, WHITE): 

431 continue 

432 

433 yield color 

434 

435 def setMaskPlaneColor(self, name, color=None): 

436 """Request that mask plane name be displayed as color 

437 

438 Parameters 

439 ---------- 

440 name : `str` or `dict` 

441 Name of mask plane or a dictionary of name -> colorName 

442 color : `str` 

443 The name of the color to use (must be `None` if ``name`` is a `dict`) 

444 

445 Colors may be specified as any X11-compliant string (e.g. `"orchid"`), or by one 

446 of the following constants in `lsst.afw.display` : `BLACK`, `WHITE`, `RED`, `BLUE`, 

447 `GREEN`, `CYAN`, `MAGENTA`, `YELLOW`. 

448 

449 If the color is "ignore" (or `IGNORE`) then that mask plane is not displayed 

450 

451 The advantage of using the symbolic names is that the python interpreter can detect typos. 

452 """ 

453 

454 if isinstance(name, dict): 

455 assert color is None 

456 for k, v in name.items(): 

457 self.setMaskPlaneColor(k, v) 

458 return 

459 

460 self._maskPlaneColors[name] = color 

461 

462 def getMaskPlaneColor(self, name=None): 

463 """Return the color associated with the specified mask plane name 

464 

465 Parameters 

466 ---------- 

467 name : `str` 

468 Desired mask plane; if `None`, return entire dict 

469 """ 

470 

471 if name is None: 

472 return self._maskPlaneColors 

473 else: 

474 return self._maskPlaneColors.get(name) 

475 

476 def setMaskTransparency(self, transparency=None, name=None): 

477 """Specify display's mask transparency (percent); or `None` to not set it when loading masks 

478 """ 

479 

480 if isinstance(transparency, dict): 

481 assert name is None 

482 for k, v in transparency.items(): 

483 self.setMaskTransparency(v, k) 

484 return 

485 

486 if transparency is not None and (transparency < 0 or transparency > 100): 

487 print( 

488 "Mask transparency should be in the range [0, 100]; clipping", file=sys.stderr) 

489 if transparency < 0: 

490 transparency = 0 

491 else: 

492 transparency = 100 

493 

494 if transparency is not None: 

495 self._impl._setMaskTransparency(transparency, name) 

496 

497 def getMaskTransparency(self, name=None): 

498 """Return the current display's mask transparency 

499 """ 

500 

501 return self._impl._getMaskTransparency(name) 

502 

503 def show(self): 

504 """Uniconify and Raise display. 

505 

506 Notes 

507 ----- 

508 Throws an exception if frame doesn't exit 

509 """ 

510 return self._impl._show() 

511 

512 def mtv(self, data, title="", wcs=None): 

513 """Display an `~lsst.afw.image.Image` or `~lsst.afw.image.Mask` on a display 

514 

515 Notes 

516 ----- 

517 Historical note: the name "mtv" comes from Jim Gunn's forth imageprocessing 

518 system, Mirella (named after Mirella Freni); The "m" stands for Mirella. 

519 """ 

520 if hasattr(data, "getXY0"): 

521 self._xy0 = data.getXY0() 

522 else: 

523 self._xy0 = None 

524 

525 # it's an Exposure; display the MaskedImage with the WCS 

526 if isinstance(data, afwImage.Exposure): 

527 if wcs: 

528 raise RuntimeError( 

529 "You may not specify a wcs with an Exposure") 

530 data, wcs = data.getMaskedImage(), data.getWcs() 

531 elif isinstance(data, afwImage.DecoratedImage): # it's a DecoratedImage; display it 

532 try: 

533 wcs = afwGeom.makeSkyWcs(data.getMetadata()) 

534 except TypeError: 

535 wcs = None 

536 data = data.image 

537 

538 self._xy0 = data.getXY0() # DecoratedImage doesn't have getXY0() 

539 

540 if isinstance(data, afwImage.Image): # it's an Image; display it 

541 self._impl._mtv(data, None, wcs, title) 

542 # it's a Mask; display it, bitplane by bitplane 

543 elif isinstance(data, afwImage.Mask): 

544 # 

545 # Some displays can't display a Mask without an image; so display an Image too, 

546 # with pixel values set to the mask 

547 # 

548 self._impl._mtv(afwImage.ImageI(data.getArray()), data, wcs, title) 

549 # it's a MaskedImage; display Image and overlay Mask 

550 elif isinstance(data, afwImage.MaskedImage): 

551 self._impl._mtv(data.getImage(), data.getMask(), wcs, title) 

552 else: 

553 raise RuntimeError(f"Unsupported type {data!r}") 

554 # 

555 # Graphics commands 

556 # 

557 

558 class _Buffering: 

559 """A class intended to be used with python's with statement 

560 """ 

561 

562 def __init__(self, _impl): 

563 self._impl = _impl 

564 

565 def __enter__(self): 

566 self._impl._buffer(True) 

567 

568 def __exit__(self, *args): 

569 self._impl._buffer(False) 

570 self._impl._flush() 

571 

572 def Buffering(self): 

573 """Return a class intended to be used with python's with statement 

574 

575 Examples 

576 -------- 

577 .. code-block:: py 

578 

579 with display.Buffering(): 

580 display.dot("+", xc, yc) 

581 """ 

582 return self._Buffering(self._impl) 

583 

584 def flush(self): 

585 """Flush the buffers 

586 """ 

587 self._impl._flush() 

588 

589 def erase(self): 

590 """Erase the specified display frame 

591 """ 

592 self._impl._erase() 

593 

594 def dot(self, symb, c, r, size=2, ctype=None, origin=afwImage.PARENT, *args, **kwargs): 

595 """Draw a symbol onto the specified display frame 

596 

597 Parameters 

598 ---------- 

599 symb 

600 Possible values are: 

601 

602 ``"+"`` 

603 Draw a + 

604 ``"x"`` 

605 Draw an x 

606 ``"*"`` 

607 Draw a * 

608 ``"o"`` 

609 Draw a circle 

610 ``"@:Mxx,Mxy,Myy"`` 

611 Draw an ellipse with moments (Mxx, Mxy, Myy) (argument size is ignored) 

612 `lsst.afw.geom.ellipses.BaseCore` 

613 Draw the ellipse (argument size is ignored). N.b. objects 

614 derived from `~lsst.afw.geom.ellipses.BaseCore` include 

615 `~lsst.afw.geom.ellipses.Axes` and `~lsst.afw.geom.ellipses.Quadrupole`. 

616 Any other value 

617 Interpreted as a string to be drawn. 

618 c, r 

619 The column and row where the symbol is drawn [0-based coordinates] 

620 size : `int` 

621 Size of symbol, in pixels 

622 ctype : `str` 

623 The desired color, either e.g. `lsst.afw.display.RED` or a color name known to X11 

624 origin : `lsst.afw.image.ImageOrigin` 

625 Coordinate system for the given positions. 

626 *args 

627 Extra arguments to backend 

628 **kwargs 

629 Extra keyword arguments to backend 

630 """ 

631 if isinstance(symb, int): 

632 symb = f"{symb:d}" 

633 

634 if origin == afwImage.PARENT and self._xy0 is not None: 

635 x0, y0 = self._xy0 

636 r -= y0 

637 c -= x0 

638 

639 if isinstance(symb, afwGeom.ellipses.BaseCore) or re.search(r"^@:", symb): 

640 try: 

641 mat = re.search(r"^@:([^,]+),([^,]+),([^,]+)", symb) 

642 except TypeError: 

643 pass 

644 else: 

645 if mat: 

646 mxx, mxy, myy = [float(_) for _ in mat.groups()] 

647 symb = afwGeom.Quadrupole(mxx, myy, mxy) 

648 

649 symb = afwGeom.ellipses.Axes(symb) 

650 

651 self._impl._dot(symb, c, r, size, ctype, **kwargs) 

652 

653 def line(self, points, origin=afwImage.PARENT, symbs=False, ctype=None, size=0.5): 

654 """Draw a set of symbols or connect points 

655 

656 Parameters 

657 ---------- 

658 points : `list` 

659 a list of (col, row) 

660 origin : `lsst.afw.image.ImageOrigin` 

661 Coordinate system for the given positions. 

662 symbs : `bool` or sequence 

663 If ``symbs`` is `True`, draw points at the specified points using the desired symbol, 

664 otherwise connect the dots. 

665 

666 If ``symbs`` supports indexing (which includes a string -- caveat emptor) the 

667 elements are used to label the points 

668 ctype : `str` 

669 ``ctype`` is the name of a color (e.g. 'red') 

670 size : `float` 

671 """ 

672 if symbs: 

673 try: 

674 symbs[1] 

675 except TypeError: 

676 symbs = len(points)*list(symbs) 

677 

678 for i, xy in enumerate(points): 

679 self.dot(symbs[i], *xy, size=size, ctype=ctype) 

680 else: 

681 if len(points) > 0: 

682 if origin == afwImage.PARENT and self._xy0 is not None: 

683 x0, y0 = self._xy0 

684 _points = list(points) # make a mutable copy 

685 for i, p in enumerate(points): 

686 _points[i] = (p[0] - x0, p[1] - y0) 

687 points = _points 

688 

689 self._impl._drawLines(points, ctype) 

690 # 

691 # Set gray scale 

692 # 

693 

694 def scale(self, algorithm, min, max=None, unit=None, *args, **kwargs): 

695 """Set the range of the scaling from DN in the image to the image display 

696 

697 Parameters 

698 ---------- 

699 algorithm : `str` 

700 Desired scaling (e.g. "linear" or "asinh") 

701 min 

702 Minimum value, or "minmax" or "zscale" 

703 max 

704 Maximum value (must be `None` for minmax|zscale) 

705 unit 

706 Units for min and max (e.g. Percent, Absolute, Sigma; `None` if min==minmax|zscale) 

707 *args 

708 Optional arguments to the backend 

709 **kwargs 

710 Optional keyword arguments to the backend 

711 """ 

712 if min in ("minmax", "zscale"): 

713 assert max is None, f"You may not specify \"{min}\" and max" 

714 assert unit is None, f"You may not specify \"{min}\" and unit" 

715 elif max is None: 

716 raise RuntimeError("Please specify max") 

717 

718 self._impl._scale(algorithm, min, max, unit, *args, **kwargs) 

719 # 

720 # Zoom and Pan 

721 # 

722 

723 def zoom(self, zoomfac=None, colc=None, rowc=None, origin=afwImage.PARENT): 

724 """Zoom frame by specified amount, optionally panning also 

725 """ 

726 

727 if (rowc and colc is None) or (colc and rowc is None): 

728 raise RuntimeError( 

729 "Please specify row and column center to pan about") 

730 

731 if rowc is not None: 

732 if origin == afwImage.PARENT and self._xy0 is not None: 

733 x0, y0 = self._xy0 

734 colc -= x0 

735 rowc -= y0 

736 

737 self._impl._pan(colc, rowc) 

738 

739 if zoomfac is None and rowc is None: 

740 zoomfac = 2 

741 

742 if zoomfac is not None: 

743 self._impl._zoom(zoomfac) 

744 

745 def pan(self, colc=None, rowc=None, origin=afwImage.PARENT): 

746 """Pan to a location 

747 

748 Parameters 

749 ---------- 

750 colc, rowc 

751 the coordinates to pan to 

752 origin : `lsst.afw.image.ImageOrigin` 

753 Coordinate system for the given positions. 

754 

755 See also 

756 -------- 

757 Display.zoom 

758 """ 

759 

760 self.zoom(None, colc, rowc, origin) 

761 

762 def interact(self): 

763 """Enter an interactive loop, listening for key presses in display and firing callbacks. 

764 

765 Exit with ``q``, ``CR``, ``ESC``, or any other callback function that returns a `True` value. 

766 """ 

767 interactFinished = False 

768 

769 while not interactFinished: 

770 ev = self._impl._getEvent() 

771 if not ev: 

772 continue 

773 k, x, y = ev.k, ev.x, ev.y # for now 

774 

775 if k not in self._callbacks: 

776 logger.warn("No callback registered for {0}".format(k)) 

777 else: 

778 try: 

779 interactFinished = self._callbacks[k](k, x, y) 

780 except Exception as e: 

781 logger.error( 

782 "Display._callbacks['{0}']({0},{1},{2}) failed: {3}".format(k, x, y, e)) 

783 

784 def setCallback(self, k, func=None, noRaise=False): 

785 """Set the callback for a key 

786 

787 Parameters 

788 ---------- 

789 k 

790 The key to assign the callback to 

791 func : callable 

792 The callback assigned to ``k`` 

793 noRaise : `bool` 

794 

795 Returns 

796 ------- 

797 oldFunc : callable 

798 The callback previously assigned to ``k``. 

799 """ 

800 

801 if k in "f": 

802 if noRaise: 

803 return 

804 raise RuntimeError( 

805 f"Key '{k}' is already in use by display, so I can't add a callback for it") 

806 

807 ofunc = self._callbacks.get(k) 

808 self._callbacks[k] = func if func else noop_callback 

809 

810 self._impl._setCallback(k, self._callbacks[k]) 

811 

812 return ofunc 

813 

814 def getActiveCallbackKeys(self, onlyActive=True): 

815 """Return all callback keys 

816 

817 Parameters 

818 ---------- 

819 onlyActive : `bool` 

820 If `True` only return keys that do something 

821 """ 

822 

823 return sorted([k for k, func in self._callbacks.items() if 

824 not (onlyActive and func == noop_callback)]) 

825 

826# 

827# Callbacks for display events 

828# 

829 

830 

831class Event: 

832 """A class to handle events such as key presses in image display windows 

833 """ 

834 

835 def __init__(self, k, x=float('nan'), y=float('nan')): 

836 self.k = k 

837 self.x = x 

838 self.y = y 

839 

840 def __str__(self): 

841 return f"{self.k} ({self.x:.2f}, {self.y:.2f}" 

842# 

843# Default fallback function 

844# 

845 

846 

847def noop_callback(k, x, y): 

848 """Callback function 

849 

850 Parameters 

851 ---------- 

852 key 

853 x 

854 y 

855 """ 

856 return False 

857 

858 

859def h_callback(k, x, y): 

860 print("Enter q or <ESC> to leave interactive mode, h for this help, or a letter to fire a callback") 

861 return False 

862 

863# 

864# Handle Displays, including the default one (the frame to use when a user specifies None) 

865# 

866# If the default frame is None, image display is disabled 

867# 

868 

869 

870def setDefaultBackend(backend): 

871 Display.setDefaultBackend(backend) 

872 

873 

874def getDefaultBackend(): 

875 return Display.getDefaultBackend() 

876 

877 

878def setDefaultFrame(frame=0): 

879 return Display.setDefaultFrame(frame) 

880 

881 

882def getDefaultFrame(): 

883 """Get the default frame for display 

884 """ 

885 return Display.getDefaultFrame() 

886 

887 

888def incrDefaultFrame(): 

889 """Increment the default frame for display 

890 """ 

891 return Display.incrDefaultFrame() 

892 

893 

894def setDefaultMaskTransparency(maskPlaneTransparency={}): 

895 return Display.setDefaultMaskTransparency(maskPlaneTransparency) 

896 

897 

898def setDefaultMaskPlaneColor(name=None, color=None): 

899 """Set the default mapping from mask plane names to colors 

900 

901 Parameters 

902 ---------- 

903 name : `str` or `dict` 

904 name of mask plane, or a dict mapping names to colors. 

905 If ``name`` is `None`, use the hard-coded default dictionary 

906 color : `str` 

907 Desired color, or `None` if ``name`` is a dict 

908 """ 

909 

910 return Display.setDefaultMaskPlaneColor(name, color) 

911 

912 

913def getDisplay(frame=None, backend=None, create=True, verbose=False, *args, **kwargs): 

914 """Return a specific `Display`, creating it if need be 

915 

916 Parameters 

917 ---------- 

918 frame 

919 The desired frame (`None` => use defaultFrame (see `setDefaultFrame`)) 

920 backend : `str` 

921 Create the specified frame using this backend (or the default if 

922 `None`) if it doesn't already exist. If ``backend == ""``, it's an 

923 error to specify a non-existent ``frame``. 

924 create : `bool` 

925 Create the display if it doesn't already exist. 

926 verbose : `bool` 

927 Allow backend to be chatty 

928 *args 

929 arguments passed to `Display` constructor 

930 **kwargs 

931 keyword arguments passed to `Display` constructor 

932 

933 See also 

934 -------- 

935 Display.getDisplay 

936 """ 

937 

938 return Display.getDisplay(frame, backend, create, verbose, *args, **kwargs) 

939 

940 

941def delAllDisplays(): 

942 """Delete and close all known displays 

943 """ 

944 return Display.delAllDisplays()