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

385 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-09 02:01 -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 

38from ._images_compat import normalize_lsst_images 

39 

40logger = logging.getLogger(__name__) 

41 

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

43WHITE = "white" 

44BLACK = "black" 

45RED = "red" 

46GREEN = "green" 

47BLUE = "blue" 

48CYAN = "cyan" 

49MAGENTA = "magenta" 

50YELLOW = "yellow" 

51ORANGE = "orange" 

52IGNORE = "ignore" 

53 

54 

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

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

57 

58 Parameters 

59 ---------- 

60 display : `str` 

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

62 backend : `str` 

63 The desired backend 

64 *args 

65 Arguments passed to DisplayImpl.__init__ 

66 *kwargs 

67 Keywords arguments passed to DisplayImpl.__init__ 

68 

69 Examples 

70 -------- 

71 E.g. 

72 

73 .. code-block:: py 

74 

75 import lsst.afw.display as afwDisplay 

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

77 

78 would call 

79 

80 .. code-block:: py 

81 

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

83 

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

85 """ 

86 _disp = None 

87 exc = None 

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

89 for dt in candidateBackends: 

90 exc = None 

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

92 impargs = {} 

93 if dt.startswith("."): 

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

95 try: 

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

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

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

99 if hasattr(_disp, "DisplayImpl"): 99 ↛ 102line 99 didn't jump to line 102 because the condition on line 99 was always true

100 break 

101 else: 

102 _disp = None 

103 except (ImportError, SystemError) as e: 

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

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

106 # Copy the exception into outer scope 

107 exc = e 

108 

109 if not _disp or not hasattr(_disp.DisplayImpl, "_show"): 

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

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

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

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

114 raise e from exc 

115 else: 

116 raise e 

117 

118 if display: 

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

120 if not hasattr(_impl, "frame"): 120 ↛ 123line 120 didn't jump to line 123 because the condition on line 120 was always true

121 _impl.frame = display.frame 

122 

123 return _impl 

124 else: 

125 return True 

126 

127 

128class Display: 

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

130 

131 Parameters 

132 ---------- 

133 frame 

134 An identifier for the display. 

135 backend : `str` 

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

137 **kwargs 

138 Arguments to pass to the backend. 

139 """ 

140 _displays = {} 

141 _defaultBackend = None 

142 _defaultFrame = 0 

143 _defaultMaskPlaneColor = dict( 

144 BAD=RED, 

145 CR=MAGENTA, 

146 COSMIC_RAY=MAGENTA, 

147 EDGE=YELLOW, 

148 DETECTION_EDGE=YELLOW, 

149 INTERPOLATED=GREEN, 

150 SATURATED=GREEN, 

151 DETECTED=BLUE, 

152 DETECTED_NEGATIVE=CYAN, 

153 SUSPECT=YELLOW, 

154 NO_DATA=ORANGE, 

155 # deprecated names 

156 INTRP=GREEN, 

157 SAT=GREEN, 

158 ) 

159 _defaultMaskTransparency = {} 

160 _defaultImageColormap = "gray" 

161 

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

163 if frame is None: 163 ↛ 164line 163 didn't jump to line 164 because the condition on line 163 was never true

164 frame = getDefaultFrame() 

165 

166 if backend is None: 

167 if Display._defaultBackend is None: 167 ↛ 168line 167 didn't jump to line 168 because the condition on line 167 was never true

168 try: 

169 setDefaultBackend("ds9") 

170 except RuntimeError: 

171 setDefaultBackend("virtualDevice") 

172 

173 backend = Display._defaultBackend 

174 

175 self.frame = frame 

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

177 self.name = backend 

178 

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

180 self.setMaskTransparency(Display._defaultMaskTransparency) 

181 self._maskPlaneColors = {} 

182 self.setMaskPlaneColor(Display._defaultMaskPlaneColor) 

183 self.setImageColormap(Display._defaultImageColormap) 

184 

185 self._callbacks = {} 

186 

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

188 k = f"{ik:c}" 

189 self.setCallback(k, noRaise=True) 

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

191 

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

193 self.setCallback(k) 

194 

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

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

197 

198 def _h_callback(k, x, y): 

199 h_callback(k, x, y) 

200 

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

202 doc = self._callbacks[k].__doc__ 

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

204 

205 self.setCallback('h', _h_callback) 

206 

207 Display._displays[frame] = self 

208 

209 def __enter__(self): 

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

211 """ 

212 return self 

213 

214 def __exit__(self, *args): 

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

216 """ 

217 self.close() 

218 

219 def __del__(self): 

220 self.close() 

221 

222 def __getattr__(self, name): 

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

224 requested. 

225 

226 Parameters: 

227 ----------- 

228 name : `str` 

229 name of the attribute requested. 

230 

231 Returns: 

232 -------- 

233 attribute : `object` 

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

235 """ 

236 

237 if name == '_impl': 237 ↛ 238line 237 didn't jump to line 238 because the condition on line 237 was never true

238 return object.__getattr__(self, name) 

239 

240 if not (hasattr(self, "_impl") and self._impl): 240 ↛ 241line 240 didn't jump to line 241 because the condition on line 240 was never true

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

242 

243 try: 

244 return getattr(self._impl, name) 

245 except AttributeError: 

246 raise AttributeError( 

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

248 

249 def close(self): 

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

251 self._impl._close() 

252 del self._impl 

253 self._impl = None 

254 

255 if self.frame in Display._displays: 

256 del Display._displays[self.frame] 

257 

258 @property 

259 def verbose(self): 

260 """The backend's verbosity. 

261 """ 

262 return self._impl.verbose 

263 

264 @verbose.setter 

265 def verbose(self, value): 

266 if self._impl: 266 ↛ exitline 266 didn't return from function 'verbose' because the condition on line 266 was always true

267 self._impl.verbose = value 

268 

269 def __str__(self): 

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

271 

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

273 

274 @staticmethod 

275 def setDefaultBackend(backend): 

276 try: 

277 _makeDisplayImpl(None, backend) 

278 except Exception as e: 

279 raise RuntimeError( 

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

281 

282 Display._defaultBackend = backend 

283 

284 @staticmethod 

285 def getDefaultBackend(): 

286 return Display._defaultBackend 

287 

288 @staticmethod 

289 def setDefaultFrame(frame=0): 

290 """Set the default frame for display. 

291 """ 

292 Display._defaultFrame = frame 

293 

294 @staticmethod 

295 def getDefaultFrame(): 

296 """Get the default frame for display. 

297 """ 

298 return Display._defaultFrame 

299 

300 @staticmethod 

301 def incrDefaultFrame(): 

302 """Increment the default frame for display. 

303 """ 

304 Display._defaultFrame += 1 

305 return Display._defaultFrame 

306 

307 @staticmethod 

308 def setDefaultMaskTransparency(maskPlaneTransparency={}): 

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

310 maskPlaneTransparency = maskPlaneTransparency.copy() 

311 

312 Display._defaultMaskTransparency = maskPlaneTransparency 

313 

314 @staticmethod 

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

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

317 

318 Parameters 

319 ---------- 

320 name : `str` or `dict` 

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

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

323 color 

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

325 """ 

326 

327 if name is None: 327 ↛ 328line 327 didn't jump to line 328 because the condition on line 327 was never true

328 name = Display._defaultMaskPlaneColor 

329 

330 if isinstance(name, dict): 330 ↛ 331line 330 didn't jump to line 331 because the condition on line 330 was never true

331 assert color is None 

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

333 setDefaultMaskPlaneColor(k, v) 

334 return 

335 # Set the individual color values 

336 Display._defaultMaskPlaneColor[name] = color 

337 

338 @staticmethod 

339 def setDefaultImageColormap(cmap): 

340 """Set the default colormap for images. 

341 

342 Parameters 

343 ---------- 

344 cmap : `str` 

345 Name of colormap, as interpreted by the backend. 

346 

347 Notes 

348 ----- 

349 The only colormaps that all backends are required to honor 

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

351 """ 

352 

353 Display._defaultImageColormap = cmap 

354 

355 def setImageColormap(self, cmap): 

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

357 

358 Parameters 

359 ---------- 

360 cmap : `str` 

361 Name of colormap, as interpreted by the backend. 

362 

363 Notes 

364 ----- 

365 The only colormaps that all backends are required to honor 

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

367 """ 

368 

369 self._impl._setImageColormap(cmap) 

370 

371 @staticmethod 

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

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

374 

375 Parameters 

376 ---------- 

377 frame 

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

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

380 backend : `str` 

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

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

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

384 create : `bool` 

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

386 verbose : `bool` 

387 Allow backend to be chatty. 

388 **kwargs 

389 keyword arguments passed to `Display` constructor. 

390 """ 

391 

392 if frame is None: 

393 frame = Display._defaultFrame 

394 

395 if frame not in Display._displays: 

396 if backend == "": 

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

398 

399 Display._displays[frame] = Display( 

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

401 

402 Display._displays[frame].verbose = verbose 

403 return Display._displays[frame] 

404 

405 @staticmethod 

406 def delAllDisplays(): 

407 """Delete and close all known displays. 

408 """ 

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

410 disp.close() 

411 Display._displays = {} 

412 

413 def maskColorGenerator(self, omitBW=True): 

414 """A generator for "standard" colors. 

415 

416 Parameters 

417 ---------- 

418 omitBW : `bool` 

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

420 

421 Examples 

422 -------- 

423 

424 .. code-block:: py 

425 

426 colorGenerator = interface.maskColorGenerator(omitBW=True) 

427 for p in planeList: 

428 print(p, next(colorGenerator)) 

429 """ 

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

431 BLUE, CYAN, MAGENTA, YELLOW, ORANGE] 

432 

433 i = -1 

434 while True: 

435 i += 1 

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

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

438 continue 

439 

440 yield color 

441 

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

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

444 

445 Parameters 

446 ---------- 

447 name : `str` or `dict` 

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

449 color : `str` 

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

451 `dict`). 

452 

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

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

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

456 `GREEN`, `CYAN`, `MAGENTA`, `YELLOW`. 

457 

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

459 displayed. 

460 

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

462 interpreter can detect typos. 

463 """ 

464 if isinstance(name, dict): 

465 assert color is None 

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

467 self.setMaskPlaneColor(k, v) 

468 return 

469 

470 self._maskPlaneColors[name] = color 

471 

472 def getMaskPlaneColor(self, name=None): 

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

474 

475 Parameters 

476 ---------- 

477 name : `str` 

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

479 """ 

480 if name is None: 

481 return self._maskPlaneColors 

482 else: 

483 color = self._maskPlaneColors.get(name) 

484 

485 if color is None: 485 ↛ 486line 485 didn't jump to line 486 because the condition on line 485 was never true

486 color = self._defaultMaskPlaneColor.get(name) 

487 

488 return color 

489 

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

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

492 it when loading masks. 

493 """ 

494 if isinstance(transparency, dict): 494 ↛ 495line 494 didn't jump to line 495 because the condition on line 494 was never true

495 assert name is None 

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

497 self.setMaskTransparency(v, k) 

498 return 

499 

500 if transparency is not None and (transparency < 0 or transparency > 100): 500 ↛ 501line 500 didn't jump to line 501 because the condition on line 500 was never true

501 print( 

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

503 if transparency < 0: 

504 transparency = 0 

505 else: 

506 transparency = 100 

507 

508 if transparency is not None: 508 ↛ exitline 508 didn't return from function 'setMaskTransparency' because the condition on line 508 was always true

509 self._impl._setMaskTransparency(transparency, name) 

510 

511 def getMaskTransparency(self, name=None): 

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

513 """ 

514 return self._impl._getMaskTransparency(name) 

515 

516 def show(self): 

517 """Uniconify and Raise display. 

518 

519 Notes 

520 ----- 

521 Throws an exception if frame doesn't exit. 

522 """ 

523 return self._impl._show() 

524 

525 def __addMissingMaskPlanes(self, mask): 

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

527 """ 

528 maskPlanes = mask.getMaskPlaneDict() 

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

530 

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

532 planes = {} 

533 for key in maskPlanes: 

534 planes[maskPlanes[key]] = key 

535 

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

537 for p in range(nMaskPlanes): 

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

539 if name not in self._defaultMaskPlaneColor: 

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

541 

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

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

544 overlaid, if available. 

545 

546 Parameters 

547 ---------- 

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

549 Image to display; Exposure and MaskedImage will show transparent 

550 mask planes. `lsst.images` objects (`~lsst.images.MaskedImage` 

551 and its subclasses such as `~lsst.images.VisitImage` and 

552 `~lsst.images.cells.CellCoadd`, `~lsst.images.Image`, and 

553 `~lsst.images.Mask`) are converted to the equivalent afw types, 

554 using any attached ``sky_projection`` as the WCS. 

555 title : `str`, optional 

556 Title for the display window. 

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

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

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

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

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

562 Additional FITS metadata to be sent to display. 

563 

564 Raises 

565 ------ 

566 RuntimeError 

567 Raised if an Exposure, or an ``lsst.images`` object with its own 

568 ``sky_projection``, is passed when the ``wcs`` kwarg is also 

569 non-None. 

570 TypeError 

571 Raised if data is an incompatible type. 

572 """ 

573 data, wcs = normalize_lsst_images(data, wcs) 

574 

575 if hasattr(data, "getXY0"): 

576 self._xy0 = data.getXY0() 

577 else: 

578 self._xy0 = None 

579 

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

581 if isinstance(data, afwImage.Exposure): 

582 if wcs: 582 ↛ 583line 582 didn't jump to line 583 because the condition on line 582 was never true

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

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

585 # it's a DecoratedImage; display it 

586 elif isinstance(data, afwImage.DecoratedImage): 

587 try: 

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

589 except TypeError: 

590 wcs = None 

591 data = data.image 

592 

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

594 

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

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

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

598 elif isinstance(data, afwImage.Mask): 

599 self.__addMissingMaskPlanes(data) 

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

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

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

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

604 elif isinstance(data, afwImage.MaskedImage): 604 ↛ 608line 604 didn't jump to line 608 because the condition on line 604 was always true

605 self.__addMissingMaskPlanes(data.mask) 

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

607 else: 

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

609 

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

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

612 overlaid, if available. 

613 

614 Notes 

615 ----- 

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

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

618 """ 

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

620 

621 class _Buffering: 

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

623 """ 

624 def __init__(self, _impl): 

625 self._impl = _impl 

626 

627 def __enter__(self): 

628 self._impl._buffer(True) 

629 

630 def __exit__(self, *args): 

631 self._impl._buffer(False) 

632 self._impl._flush() 

633 

634 def Buffering(self): 

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

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

637 

638 Examples 

639 -------- 

640 .. code-block:: py 

641 

642 with display.Buffering(): 

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

644 """ 

645 return self._Buffering(self._impl) 

646 

647 def flush(self): 

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

649 """ 

650 self._impl._flush() 

651 

652 def erase(self): 

653 """Erase the specified display frame. 

654 """ 

655 self._impl._erase() 

656 

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

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

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

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

661 

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

663 which are passed to `dot`. 

664 

665 Parameters 

666 ---------- 

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

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

669 """ 

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

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

672 

673 with self.Buffering(): 

674 for pt in catalog: 

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

676 

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

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

679 

680 Parameters 

681 ---------- 

682 symb 

683 Possible values are: 

684 

685 ``"+"`` 

686 Draw a + 

687 ``"x"`` 

688 Draw an x 

689 ``"*"`` 

690 Draw a * 

691 ``"o"`` 

692 Draw a circle 

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

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

695 `lsst.afw.geom.ellipses.BaseCore` 

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

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

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

699 Any other value 

700 Interpreted as a string to be drawn. 

701 c, r : `float` 

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

703 size : `int` 

704 Size of symbol, in pixels. 

705 ctype : `str` 

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

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

708 Coordinate system for the given positions. 

709 **kwargs 

710 Extra keyword arguments to backend. 

711 """ 

712 if isinstance(symb, int): 712 ↛ 713line 712 didn't jump to line 713 because the condition on line 712 was never true

713 symb = f"{symb:d}" 

714 

715 if origin == afwImage.PARENT and self._xy0 is not None: 715 ↛ 720line 715 didn't jump to line 720 because the condition on line 715 was always true

716 x0, y0 = self._xy0 

717 r -= y0 

718 c -= x0 

719 

720 if isinstance(symb, afwGeom.ellipses.BaseCore) or re.search(r"^@:", symb): 720 ↛ 721line 720 didn't jump to line 721 because the condition on line 720 was never true

721 try: 

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

723 except TypeError: 

724 pass 

725 else: 

726 if mat: 

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

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

729 

730 symb = afwGeom.ellipses.Axes(symb) 

731 

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

733 

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

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

736 

737 Parameters 

738 ---------- 

739 points : `list` 

740 A list of (col, row) 

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

742 Coordinate system for the given positions. 

743 symbs : `bool` or sequence 

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

745 the desired symbol, otherwise connect the dots. 

746 

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

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

749 ctype : `str` 

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

751 size : `float` 

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

753 """ 

754 if symbs: 

755 try: 

756 symbs[1] 

757 except TypeError: 

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

759 

760 for i, xy in enumerate(points): 

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

762 else: 

763 if len(points) > 0: 763 ↛ exitline 763 didn't return from function 'line' because the condition on line 763 was always true

764 if origin == afwImage.PARENT and self._xy0 is not None: 764 ↛ 771line 764 didn't jump to line 771 because the condition on line 764 was always true

765 x0, y0 = self._xy0 

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

767 for i, p in enumerate(points): 

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

769 points = _points 

770 

771 self._impl._drawLines(points, ctype) 

772 

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

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

775 display. 

776 

777 Parameters 

778 ---------- 

779 algorithm : `str` 

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

781 min 

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

783 max 

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

785 unit 

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

787 min==minmax|zscale). 

788 **kwargs 

789 Optional keyword arguments to the backend. 

790 """ 

791 if min in ("minmax", "zscale"): 791 ↛ 794line 791 didn't jump to line 794 because the condition on line 791 was always true

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

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

794 elif max is None: 

795 raise RuntimeError("Please specify max") 

796 

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

798 

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

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

801 """ 

802 if (rowc and colc is None) or (colc and rowc is None): 802 ↛ 803line 802 didn't jump to line 803 because the condition on line 802 was never true

803 raise RuntimeError( 

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

805 

806 if rowc is not None: 

807 if origin == afwImage.PARENT and self._xy0 is not None: 807 ↛ 808line 807 didn't jump to line 808 because the condition on line 807 was never true

808 x0, y0 = self._xy0 

809 colc -= x0 

810 rowc -= y0 

811 

812 self._impl._pan(colc, rowc) 

813 

814 if zoomfac is None and rowc is None: 814 ↛ 815line 814 didn't jump to line 815 because the condition on line 814 was never true

815 zoomfac = 2 

816 

817 if zoomfac is not None: 

818 self._impl._zoom(zoomfac) 

819 

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

821 """Pan to a location. 

822 

823 Parameters 

824 ---------- 

825 colc, rowc 

826 Coordinates to pan to. 

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

828 Coordinate system for the given positions. 

829 

830 See also 

831 -------- 

832 Display.zoom 

833 """ 

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

835 

836 def interact(self): 

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

838 UI actions in the display and firing callbacks. 

839 

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

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

842 user-provided callback function. 

843 """ 

844 interactFinished = False 

845 

846 while not interactFinished: 

847 ev = self._impl._getEvent() 

848 if not ev: 848 ↛ 849line 848 didn't jump to line 849 because the condition on line 848 was never true

849 continue 

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

851 

852 if k not in self._callbacks: 852 ↛ 853line 852 didn't jump to line 853 because the condition on line 852 was never true

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

854 else: 

855 try: 

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

857 except Exception: 

858 logger.exception( 

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

860 

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

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

863 displayed. 

864 

865 Parameters 

866 ---------- 

867 match_type : `str`, optional 

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

869 docs for what values to use. 

870 lock_match : `bool`, optional 

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

872 preserve the alignment in other images. 

873 """ 

874 try: 

875 self._impl._alignImages(match_type, lock_match) 

876 except AttributeError: 

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

878 

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

880 """Set the callback for a key. 

881 

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

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

884 

885 Parameters 

886 ---------- 

887 k : `str` 

888 The key to assign the callback to. 

889 func : callable 

890 The callback assigned to ``k``. 

891 noRaise : `bool` 

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

893 

894 Returns 

895 ------- 

896 oldFunc : callable 

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

898 """ 

899 

900 if k in "f": 

901 if noRaise: 901 ↛ 903line 901 didn't jump to line 903 because the condition on line 901 was always true

902 return 

903 raise RuntimeError( 

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

905 

906 ofunc = self._callbacks.get(k) 

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

908 

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

910 

911 return ofunc 

912 

913 def getActiveCallbackKeys(self, onlyActive=True): 

914 """Return all callback keys 

915 

916 Parameters 

917 ---------- 

918 onlyActive : `bool` 

919 If `True` only return keys that do something 

920 """ 

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

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

923 

924 

925# Callbacks for display events 

926 

927 

928class Event: 

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

930 """ 

931 

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

933 self.k = k 

934 self.x = x 

935 self.y = y 

936 

937 def __str__(self): 

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

939 

940 

941# Default fallback function 

942 

943 

944def noop_callback(k, x, y): 

945 """Callback function 

946 

947 Parameters 

948 ---------- 

949 key 

950 x 

951 y 

952 """ 

953 return False 

954 

955 

956def h_callback(k, x, y): 

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

958 return False 

959 

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

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

962 

963 

964def setDefaultBackend(backend): 

965 Display.setDefaultBackend(backend) 

966 

967 

968def getDefaultBackend(): 

969 return Display.getDefaultBackend() 

970 

971 

972def setDefaultFrame(frame=0): 

973 return Display.setDefaultFrame(frame) 

974 

975 

976def getDefaultFrame(): 

977 """Get the default frame for display. 

978 """ 

979 return Display.getDefaultFrame() 

980 

981 

982def incrDefaultFrame(): 

983 """Increment the default frame for display. 

984 """ 

985 return Display.incrDefaultFrame() 

986 

987 

988def setDefaultMaskTransparency(maskPlaneTransparency={}): 

989 return Display.setDefaultMaskTransparency(maskPlaneTransparency) 

990 

991 

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

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

994 

995 Parameters 

996 ---------- 

997 name : `str` or `dict` 

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

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

1000 color : `str` 

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

1002 """ 

1003 

1004 return Display.setDefaultMaskPlaneColor(name, color) 

1005 

1006 

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

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

1009 

1010 Parameters 

1011 ---------- 

1012 frame 

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

1014 backend : `str` 

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

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

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

1018 create : `bool` 

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

1020 verbose : `bool` 

1021 Allow backend to be chatty. 

1022 **kwargs 

1023 Keyword arguments passed to `Display` constructor. 

1024 

1025 See also 

1026 -------- 

1027 Display.getDisplay 

1028 """ 

1029 

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

1031 

1032 

1033def delAllDisplays(): 

1034 """Delete and close all known displays. 

1035 """ 

1036 return Display.delAllDisplays()