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

383 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-21 01:29 -0700

1# This file is part of afw. 

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__ = [ 

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

24 "Display", "Event", "noop_callback", "h_callback", 

25 "setDefaultBackend", "getDefaultBackend", 

26 "setDefaultFrame", "getDefaultFrame", "incrDefaultFrame", 

27 "setDefaultMaskTransparency", "setDefaultMaskPlaneColor", 

28 "getDisplay", "delAllDisplays", 

29] 

30 

31import logging 

32import re 

33import sys 

34import importlib 

35import lsst.afw.geom as afwGeom 

36import lsst.afw.image as afwImage 

37 

38logger = logging.getLogger(__name__) 

39 

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

41WHITE = "white" 

42BLACK = "black" 

43RED = "red" 

44GREEN = "green" 

45BLUE = "blue" 

46CYAN = "cyan" 

47MAGENTA = "magenta" 

48YELLOW = "yellow" 

49ORANGE = "orange" 

50IGNORE = "ignore" 

51 

52 

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

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

55 

56 Parameters 

57 ---------- 

58 display : `str` 

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

60 backend : `str` 

61 The desired backend 

62 *args 

63 Arguments passed to DisplayImpl.__init__ 

64 *kwargs 

65 Keywords arguments passed to DisplayImpl.__init__ 

66 

67 Examples 

68 -------- 

69 E.g. 

70 

71 .. code-block:: py 

72 

73 import lsst.afw.display as afwDisplay 

74 display = afwDisplay.Display(backend="ds9") 

75 

76 would call 

77 

78 .. code-block:: py 

79 

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

81 

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

83 """ 

84 _disp = None 

85 exc = None 

86 candidateBackends = (f"lsst.display.{backend}", backend, f".{backend}", f"lsst.afw.display.{backend}") 

87 for dt in candidateBackends: 

88 exc = None 

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

90 impargs = {} 

91 if dt.startswith("."): 

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

93 try: 

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

95 # If _disp doesn't have a DisplayImpl attribute, we probably 

96 # picked up an irrelevant module due to a name collision 

97 if hasattr(_disp, "DisplayImpl"): 

98 break 

99 else: 

100 _disp = None 

101 except (ImportError, SystemError) as e: 

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

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

104 # Copy the exception into outer scope 

105 exc = e 

106 

107 if not _disp or not hasattr(_disp.DisplayImpl, "_show"): 107 ↛ 116line 107 didn't jump to line 116 because the condition on line 107 was always true

108 # If available, re-use the final exception from above 

109 e = ImportError(f"Could not load the requested backend: {backend} " 

110 f"(tried {', '.join(candidateBackends)}, but none worked).") 

111 if exc is not None: 111 ↛ 114line 111 didn't jump to line 114 because the condition on line 111 was always true

112 raise e from exc 

113 else: 

114 raise e 

115 

116 if display: 

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

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

119 _impl.frame = display.frame 

120 

121 return _impl 

122 else: 

123 return True 

124 

125 

126class Display: 

127 """Create an object able to display images and overplot glyphs. 

128 

129 Parameters 

130 ---------- 

131 frame 

132 An identifier for the display. 

133 backend : `str` 

134 The backend to use (defaults to value set by setDefaultBackend()). 

135 **kwargs 

136 Arguments to pass to the backend. 

137 """ 

138 _displays = {} 

139 _defaultBackend = None 

140 _defaultFrame = 0 

141 _defaultMaskPlaneColor = dict( 

142 BAD=RED, 

143 CR=MAGENTA, 

144 EDGE=YELLOW, 

145 INTERPOLATED=GREEN, 

146 SATURATED=GREEN, 

147 DETECTED=BLUE, 

148 DETECTED_NEGATIVE=CYAN, 

149 SUSPECT=YELLOW, 

150 NO_DATA=ORANGE, 

151 # deprecated names 

152 INTRP=GREEN, 

153 SAT=GREEN, 

154 ) 

155 _defaultMaskTransparency = {} 

156 _defaultImageColormap = "gray" 

157 

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

159 if frame is None: 

160 frame = getDefaultFrame() 

161 

162 if backend is None: 

163 if Display._defaultBackend is None: 

164 try: 

165 setDefaultBackend("ds9") 

166 except RuntimeError: 

167 setDefaultBackend("virtualDevice") 

168 

169 backend = Display._defaultBackend 

170 

171 self.frame = frame 

172 self._impl = _makeDisplayImpl(self, backend, **kwargs) 

173 self.name = backend 

174 

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

176 self.setMaskTransparency(Display._defaultMaskTransparency) 

177 self._maskPlaneColors = {} 

178 self.setMaskPlaneColor(Display._defaultMaskPlaneColor) 

179 self.setImageColormap(Display._defaultImageColormap) 

180 

181 self._callbacks = {} 

182 

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

184 k = f"{ik:c}" 

185 self.setCallback(k, noRaise=True) 

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

187 

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

189 self.setCallback(k) 

190 

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

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

193 

194 def _h_callback(k, x, y): 

195 h_callback(k, x, y) 

196 

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

198 doc = self._callbacks[k].__doc__ 

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

200 

201 self.setCallback('h', _h_callback) 

202 

203 Display._displays[frame] = self 

204 

205 def __enter__(self): 

206 """Support for python's with statement. 

207 """ 

208 return self 

209 

210 def __exit__(self, *args): 

211 """Support for python's with statement. 

212 """ 

213 self.close() 

214 

215 def __del__(self): 

216 self.close() 

217 

218 def __getattr__(self, name): 

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

220 requested. 

221 

222 Parameters: 

223 ----------- 

224 name : `str` 

225 name of the attribute requested. 

226 

227 Returns: 

228 -------- 

229 attribute : `object` 

230 the attribute of self._impl for the requested name. 

231 """ 

232 

233 if name == '_impl': 

234 return object.__getattr__(self, name) 

235 

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

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

238 

239 try: 

240 return getattr(self._impl, name) 

241 except AttributeError: 

242 raise AttributeError( 

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

244 

245 def close(self): 

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

247 self._impl._close() 

248 del self._impl 

249 self._impl = None 

250 

251 if self.frame in Display._displays: 

252 del Display._displays[self.frame] 

253 

254 @property 

255 def verbose(self): 

256 """The backend's verbosity. 

257 """ 

258 return self._impl.verbose 

259 

260 @verbose.setter 

261 def verbose(self, value): 

262 if self._impl: 

263 self._impl.verbose = value 

264 

265 def __str__(self): 

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

267 

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

269 

270 @staticmethod 

271 def setDefaultBackend(backend): 

272 try: 

273 _makeDisplayImpl(None, backend) 

274 except Exception as e: 

275 raise RuntimeError( 

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

277 

278 Display._defaultBackend = backend 

279 

280 @staticmethod 

281 def getDefaultBackend(): 

282 return Display._defaultBackend 

283 

284 @staticmethod 

285 def setDefaultFrame(frame=0): 

286 """Set the default frame for display. 

287 """ 

288 Display._defaultFrame = frame 

289 

290 @staticmethod 

291 def getDefaultFrame(): 

292 """Get the default frame for display. 

293 """ 

294 return Display._defaultFrame 

295 

296 @staticmethod 

297 def incrDefaultFrame(): 

298 """Increment the default frame for display. 

299 """ 

300 Display._defaultFrame += 1 

301 return Display._defaultFrame 

302 

303 @staticmethod 

304 def setDefaultMaskTransparency(maskPlaneTransparency={}): 

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

306 maskPlaneTransparency = maskPlaneTransparency.copy() 

307 

308 Display._defaultMaskTransparency = maskPlaneTransparency 

309 

310 @staticmethod 

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

312 """Set the default mapping from mask plane names to colors. 

313 

314 Parameters 

315 ---------- 

316 name : `str` or `dict` 

317 Name of mask plane, or a dict mapping names to colors 

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

319 color 

320 Desired color, or `None` if name is a dict. 

321 """ 

322 

323 if name is None: 

324 name = Display._defaultMaskPlaneColor 

325 

326 if isinstance(name, dict): 

327 assert color is None 

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

329 setDefaultMaskPlaneColor(k, v) 

330 return 

331 # Set the individual color values 

332 Display._defaultMaskPlaneColor[name] = color 

333 

334 @staticmethod 

335 def setDefaultImageColormap(cmap): 

336 """Set the default colormap for images. 

337 

338 Parameters 

339 ---------- 

340 cmap : `str` 

341 Name of colormap, as interpreted by the backend. 

342 

343 Notes 

344 ----- 

345 The only colormaps that all backends are required to honor 

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

347 """ 

348 

349 Display._defaultImageColormap = cmap 

350 

351 def setImageColormap(self, cmap): 

352 """Set the colormap to use for images. 

353 

354 Parameters 

355 ---------- 

356 cmap : `str` 

357 Name of colormap, as interpreted by the backend. 

358 

359 Notes 

360 ----- 

361 The only colormaps that all backends are required to honor 

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

363 """ 

364 

365 self._impl._setImageColormap(cmap) 

366 

367 @staticmethod 

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

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

370 

371 Parameters 

372 ---------- 

373 frame 

374 The desired frame (`None` => use defaultFrame 

375 (see `~Display.setDefaultFrame`)). 

376 backend : `str` 

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

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

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

380 create : `bool` 

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

382 verbose : `bool` 

383 Allow backend to be chatty. 

384 **kwargs 

385 keyword arguments passed to `Display` constructor. 

386 """ 

387 

388 if frame is None: 

389 frame = Display._defaultFrame 

390 

391 if frame not in Display._displays: 

392 if backend == "": 

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

394 

395 Display._displays[frame] = Display( 

396 frame, backend, verbose=verbose, **kwargs) 

397 

398 Display._displays[frame].verbose = verbose 

399 return Display._displays[frame] 

400 

401 @staticmethod 

402 def delAllDisplays(): 

403 """Delete and close all known displays. 

404 """ 

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

406 disp.close() 

407 Display._displays = {} 

408 

409 def maskColorGenerator(self, omitBW=True): 

410 """A generator for "standard" colors. 

411 

412 Parameters 

413 ---------- 

414 omitBW : `bool` 

415 Don't include `BLACK` and `WHITE`. 

416 

417 Examples 

418 -------- 

419 

420 .. code-block:: py 

421 

422 colorGenerator = interface.maskColorGenerator(omitBW=True) 

423 for p in planeList: 

424 print(p, next(colorGenerator)) 

425 """ 

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

427 BLUE, CYAN, MAGENTA, YELLOW, ORANGE] 

428 

429 i = -1 

430 while True: 

431 i += 1 

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

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

434 continue 

435 

436 yield color 

437 

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

439 """Request that mask plane name be displayed as color. 

440 

441 Parameters 

442 ---------- 

443 name : `str` or `dict` 

444 Name of mask plane or a dictionary of name -> colorName. 

445 color : `str` 

446 The name of the color to use (must be `None` if ``name`` is a 

447 `dict`). 

448 

449 Colors may be specified as any X11-compliant string (e.g. 

450 `"orchid"`), or by one of the following constants in 

451 `lsst.afw.display` : `BLACK`, `WHITE`, `RED`, `BLUE`, 

452 `GREEN`, `CYAN`, `MAGENTA`, `YELLOW`. 

453 

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

455 displayed. 

456 

457 The advantage of using the symbolic names is that the python 

458 interpreter can detect typos. 

459 """ 

460 if isinstance(name, dict): 

461 assert color is None 

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

463 self.setMaskPlaneColor(k, v) 

464 return 

465 

466 self._maskPlaneColors[name] = color 

467 

468 def getMaskPlaneColor(self, name=None): 

469 """Return the color associated with the specified mask plane name. 

470 

471 Parameters 

472 ---------- 

473 name : `str` 

474 Desired mask plane; if `None`, return entire dict. 

475 """ 

476 if name is None: 

477 return self._maskPlaneColors 

478 else: 

479 color = self._maskPlaneColors.get(name) 

480 

481 if color is None: 

482 color = self._defaultMaskPlaneColor.get(name) 

483 

484 return color 

485 

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

487 """Specify display's mask transparency (percent); or `None` to not set 

488 it when loading masks. 

489 """ 

490 if isinstance(transparency, dict): 

491 assert name is None 

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

493 self.setMaskTransparency(v, k) 

494 return 

495 

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

497 print( 

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

499 if transparency < 0: 

500 transparency = 0 

501 else: 

502 transparency = 100 

503 

504 if transparency is not None: 

505 self._impl._setMaskTransparency(transparency, name) 

506 

507 def getMaskTransparency(self, name=None): 

508 """Return the current display's mask transparency. 

509 """ 

510 return self._impl._getMaskTransparency(name) 

511 

512 def show(self): 

513 """Uniconify and Raise display. 

514 

515 Notes 

516 ----- 

517 Throws an exception if frame doesn't exit. 

518 """ 

519 return self._impl._show() 

520 

521 def __addMissingMaskPlanes(self, mask): 

522 """Assign colours to any missing mask planes found in mask. 

523 """ 

524 maskPlanes = mask.getMaskPlaneDict() 

525 nMaskPlanes = max(maskPlanes.values()) + 1 

526 

527 # Build inverse dictionary from mask plane index to name. 

528 planes = {} 

529 for key in maskPlanes: 

530 planes[maskPlanes[key]] = key 

531 

532 colorGenerator = self.display.maskColorGenerator(omitBW=True) 

533 for p in range(nMaskPlanes): 

534 name = planes[p] # ordered by plane index 

535 if name not in self._defaultMaskPlaneColor: 

536 self.setDefaultMaskPlaneColor(name, next(colorGenerator)) 

537 

538 def image(self, data, title="", wcs=None, metadata=None): 

539 """Display an image on a display, with semi-transparent masks 

540 overlaid, if available. 

541 

542 Parameters 

543 ---------- 

544 data : `lsst.afw.image.Exposure` or `lsst.afw.image.MaskedImage` or `lsst.afw.image.Image` 

545 Image to display; Exposure and MaskedImage will show transparent 

546 mask planes. 

547 title : `str`, optional 

548 Title for the display window. 

549 wcs : `lsst.afw.geom.SkyWcs`, optional 

550 World Coordinate System to align an `~lsst.afw.image.MaskedImage` 

551 or `~lsst.afw.image.Image` to; raise an exception if ``data`` 

552 is an `~lsst.afw.image.Exposure`. 

553 metadata : `lsst.daf.base.PropertySet`, optional 

554 Additional FITS metadata to be sent to display. 

555 

556 Raises 

557 ------ 

558 RuntimeError 

559 Raised if an Exposure is passed with a non-None wcs when the 

560 ``wcs`` kwarg is also non-None. 

561 TypeError 

562 Raised if data is an incompatible type. 

563 """ 

564 if hasattr(data, "getXY0"): 

565 self._xy0 = data.getXY0() 

566 else: 

567 self._xy0 = None 

568 

569 # It's an Exposure; display the MaskedImage with the WCS 

570 if isinstance(data, afwImage.Exposure): 

571 if wcs: 

572 raise RuntimeError("You may not specify a wcs with an Exposure") 

573 data, wcs, metadata = data.getMaskedImage(), data.wcs, data.metadata 

574 # it's a DecoratedImage; display it 

575 elif isinstance(data, afwImage.DecoratedImage): 

576 try: 

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

578 except TypeError: 

579 wcs = None 

580 data = data.image 

581 

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

583 

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

585 self._impl._mtv(data, None, wcs, title, metadata) 

586 # It's a Mask; display it, bitplane by bitplane. 

587 elif isinstance(data, afwImage.Mask): 

588 self.__addMissingMaskPlanes(data) 

589 # Some displays can't display a Mask without an image; so display 

590 # an Image too, with pixel values set to the mask. 

591 self._impl._mtv(afwImage.ImageI(data.array), data, wcs, title, metadata) 

592 # It's a MaskedImage; display Image and overlay Mask. 

593 elif isinstance(data, afwImage.MaskedImage): 

594 self.__addMissingMaskPlanes(data.mask) 

595 self._impl._mtv(data.image, data.mask, wcs, title, metadata) 

596 else: 

597 raise TypeError(f"Unsupported type {data!r}") 

598 

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

600 """Display an image on a display, with semi-transparent masks 

601 overlaid, if available. 

602 

603 Notes 

604 ----- 

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

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

607 """ 

608 self.image(data, title, wcs, metadata) 

609 

610 class _Buffering: 

611 """Context manager for buffering repeated display commands. 

612 """ 

613 def __init__(self, _impl): 

614 self._impl = _impl 

615 

616 def __enter__(self): 

617 self._impl._buffer(True) 

618 

619 def __exit__(self, *args): 

620 self._impl._buffer(False) 

621 self._impl._flush() 

622 

623 def Buffering(self): 

624 """Return a context manager that will buffer repeated display 

625 commands, to e.g. speed up displaying points. 

626 

627 Examples 

628 -------- 

629 .. code-block:: py 

630 

631 with display.Buffering(): 

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

633 """ 

634 return self._Buffering(self._impl) 

635 

636 def flush(self): 

637 """Flush any buffering that may be provided by the backend. 

638 """ 

639 self._impl._flush() 

640 

641 def erase(self): 

642 """Erase the specified display frame. 

643 """ 

644 self._impl._erase() 

645 

646 def centroids(self, catalog, *, symbol="o", **kwargs): 

647 """Draw the sources from a catalog at their pixel centroid positions 

648 as given by `~lsst.afw.table.Catalog.getX()` and 

649 `~lsst.afw.table.Catalog.getY()`. 

650 

651 See `dot` for an explanation of ``symbol`` and available args/kwargs, 

652 which are passed to `dot`. 

653 

654 Parameters 

655 ---------- 

656 catalog : `lsst.afw.table.Catalog` 

657 Catalog to display centroids for. Must have valid `slot_Centroid`. 

658 """ 

659 if not catalog.getCentroidSlot().isValid(): 

660 raise RuntimeError("Catalog must have a valid `slot_Centroid` defined to get X/Y positions.") 

661 

662 with self.Buffering(): 

663 for pt in catalog: 

664 self.dot(symbol, pt.getX(), pt.getY(), **kwargs) 

665 

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

667 """Draw a symbol onto the specified display frame. 

668 

669 Parameters 

670 ---------- 

671 symb 

672 Possible values are: 

673 

674 ``"+"`` 

675 Draw a + 

676 ``"x"`` 

677 Draw an x 

678 ``"*"`` 

679 Draw a * 

680 ``"o"`` 

681 Draw a circle 

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

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

684 `lsst.afw.geom.ellipses.BaseCore` 

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

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

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

688 Any other value 

689 Interpreted as a string to be drawn. 

690 c, r : `float` 

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

692 size : `int` 

693 Size of symbol, in pixels. 

694 ctype : `str` 

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

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

697 Coordinate system for the given positions. 

698 **kwargs 

699 Extra keyword arguments to backend. 

700 """ 

701 if isinstance(symb, int): 

702 symb = f"{symb:d}" 

703 

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

705 x0, y0 = self._xy0 

706 r -= y0 

707 c -= x0 

708 

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

710 try: 

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

712 except TypeError: 

713 pass 

714 else: 

715 if mat: 

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

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

718 

719 symb = afwGeom.ellipses.Axes(symb) 

720 

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

722 

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

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

725 

726 Parameters 

727 ---------- 

728 points : `list` 

729 A list of (col, row) 

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

731 Coordinate system for the given positions. 

732 symbs : `bool` or sequence 

733 If ``symbs`` is `True`, draw points at the specified points using 

734 the desired symbol, otherwise connect the dots. 

735 

736 If ``symbs`` supports indexing (which includes a string -- caveat 

737 emptor) the elements are used to label the points. 

738 ctype : `str` 

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

740 size : `float` 

741 Size of points to create if `symbs` is passed. 

742 """ 

743 if symbs: 

744 try: 

745 symbs[1] 

746 except TypeError: 

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

748 

749 for i, xy in enumerate(points): 

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

751 else: 

752 if len(points) > 0: 

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

754 x0, y0 = self._xy0 

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

756 for i, p in enumerate(points): 

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

758 points = _points 

759 

760 self._impl._drawLines(points, ctype) 

761 

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

763 """Set the range of the scaling from DN in the image to the image 

764 display. 

765 

766 Parameters 

767 ---------- 

768 algorithm : `str` 

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

770 min 

771 Minimum value, or "minmax" or "zscale". 

772 max 

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

774 unit 

775 Units for min and max (e.g. Percent, Absolute, Sigma; `None` if 

776 min==minmax|zscale). 

777 **kwargs 

778 Optional keyword arguments to the backend. 

779 """ 

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

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

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

783 elif max is None: 

784 raise RuntimeError("Please specify max") 

785 

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

787 

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

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

790 """ 

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

792 raise RuntimeError( 

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

794 

795 if rowc is not None: 

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

797 x0, y0 = self._xy0 

798 colc -= x0 

799 rowc -= y0 

800 

801 self._impl._pan(colc, rowc) 

802 

803 if zoomfac is None and rowc is None: 

804 zoomfac = 2 

805 

806 if zoomfac is not None: 

807 self._impl._zoom(zoomfac) 

808 

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

810 """Pan to a location. 

811 

812 Parameters 

813 ---------- 

814 colc, rowc 

815 Coordinates to pan to. 

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

817 Coordinate system for the given positions. 

818 

819 See also 

820 -------- 

821 Display.zoom 

822 """ 

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

824 

825 def interact(self): 

826 """Enter an interactive loop, listening for key presses or equivalent 

827 UI actions in the display and firing callbacks. 

828 

829 Exit with ``q``, ``CR``, ``ESC``, or any equivalent UI action provided 

830 in the display. The loop may also be exited by returning `True` from a 

831 user-provided callback function. 

832 """ 

833 interactFinished = False 

834 

835 while not interactFinished: 

836 ev = self._impl._getEvent() 

837 if not ev: 

838 continue 

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

840 

841 if k not in self._callbacks: 

842 logger.warning("No callback registered for %s", k) 

843 else: 

844 try: 

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

846 except Exception: 

847 logger.exception( 

848 "Display._callbacks['%s'](%s,%s,%s) failed.", k, x, y) 

849 

850 def alignImages(self, match_type="", lock_match=True): 

851 """Align and optionally lock the orientation of the images being 

852 displayed. 

853 

854 Parameters 

855 ---------- 

856 match_type : `str`, optional 

857 Match type to use to align the images; see the interface-specific 

858 docs for what values to use. 

859 lock_match : `bool`, optional 

860 Whether to lock the alignment. Panning/zooming in one image will 

861 preserve the alignment in other images. 

862 """ 

863 try: 

864 self._impl._alignImages(match_type, lock_match) 

865 except AttributeError: 

866 raise NotImplementedError("This feature only available on some backends.") 

867 

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

869 """Set the callback for a key. 

870 

871 Backend displays may provide an equivalent graphical UI action, but 

872 must make the associated key letter visible in the UI in some way. 

873 

874 Parameters 

875 ---------- 

876 k : `str` 

877 The key to assign the callback to. 

878 func : callable 

879 The callback assigned to ``k``. 

880 noRaise : `bool` 

881 Do not raise if ``k`` is already in use. 

882 

883 Returns 

884 ------- 

885 oldFunc : callable 

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

887 """ 

888 

889 if k in "f": 

890 if noRaise: 

891 return 

892 raise RuntimeError( 

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

894 

895 ofunc = self._callbacks.get(k) 

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

897 

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

899 

900 return ofunc 

901 

902 def getActiveCallbackKeys(self, onlyActive=True): 

903 """Return all callback keys 

904 

905 Parameters 

906 ---------- 

907 onlyActive : `bool` 

908 If `True` only return keys that do something 

909 """ 

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

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

912 

913 

914# Callbacks for display events 

915 

916 

917class Event: 

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

919 """ 

920 

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

922 self.k = k 

923 self.x = x 

924 self.y = y 

925 

926 def __str__(self): 

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

928 

929 

930# Default fallback function 

931 

932 

933def noop_callback(k, x, y): 

934 """Callback function 

935 

936 Parameters 

937 ---------- 

938 key 

939 x 

940 y 

941 """ 

942 return False 

943 

944 

945def h_callback(k, x, y): 

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

947 return False 

948 

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

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

951 

952 

953def setDefaultBackend(backend): 

954 Display.setDefaultBackend(backend) 

955 

956 

957def getDefaultBackend(): 

958 return Display.getDefaultBackend() 

959 

960 

961def setDefaultFrame(frame=0): 

962 return Display.setDefaultFrame(frame) 

963 

964 

965def getDefaultFrame(): 

966 """Get the default frame for display. 

967 """ 

968 return Display.getDefaultFrame() 

969 

970 

971def incrDefaultFrame(): 

972 """Increment the default frame for display. 

973 """ 

974 return Display.incrDefaultFrame() 

975 

976 

977def setDefaultMaskTransparency(maskPlaneTransparency={}): 

978 return Display.setDefaultMaskTransparency(maskPlaneTransparency) 

979 

980 

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

982 """Set the default mapping from mask plane names to colors. 

983 

984 Parameters 

985 ---------- 

986 name : `str` or `dict` 

987 Name of mask plane, or a dict mapping names to colors. 

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

989 color : `str` 

990 Desired color, or `None` if ``name`` is a dict. 

991 """ 

992 

993 return Display.setDefaultMaskPlaneColor(name, color) 

994 

995 

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

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

998 

999 Parameters 

1000 ---------- 

1001 frame 

1002 Desired frame (`None` => use defaultFrame (see `setDefaultFrame`)). 

1003 backend : `str` 

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

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

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

1007 create : `bool` 

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

1009 verbose : `bool` 

1010 Allow backend to be chatty. 

1011 **kwargs 

1012 Keyword arguments passed to `Display` constructor. 

1013 

1014 See also 

1015 -------- 

1016 Display.getDisplay 

1017 """ 

1018 

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

1020 

1021 

1022def delAllDisplays(): 

1023 """Delete and close all known displays. 

1024 """ 

1025 return Display.delAllDisplays()