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

366 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-11 03:10 -0700

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(__name__) 

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 color = self._maskPlaneColors.get(name) 

475 

476 if color is None: 

477 color = self._defaultMaskPlaneColor.get(name) 

478 

479 return color 

480 

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

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

483 """ 

484 

485 if isinstance(transparency, dict): 

486 assert name is None 

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

488 self.setMaskTransparency(v, k) 

489 return 

490 

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

492 print( 

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

494 if transparency < 0: 

495 transparency = 0 

496 else: 

497 transparency = 100 

498 

499 if transparency is not None: 

500 self._impl._setMaskTransparency(transparency, name) 

501 

502 def getMaskTransparency(self, name=None): 

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

504 """ 

505 

506 return self._impl._getMaskTransparency(name) 

507 

508 def show(self): 

509 """Uniconify and Raise display. 

510 

511 Notes 

512 ----- 

513 Throws an exception if frame doesn't exit 

514 """ 

515 return self._impl._show() 

516 

517 def __addMissingMaskPlanes(self, mask): 

518 """Assign colours to any missing mask planes found in mask""" 

519 

520 maskPlanes = mask.getMaskPlaneDict() 

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

522 

523 planes = {} # build inverse dictionary from mask plane index to name 

524 for key in maskPlanes: 

525 planes[maskPlanes[key]] = key 

526 

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

528 for p in range(nMaskPlanes): 

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

530 if name not in self._defaultMaskPlaneColor: 

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

532 

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

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

535 

536 Notes 

537 ----- 

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

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

540 """ 

541 if hasattr(data, "getXY0"): 

542 self._xy0 = data.getXY0() 

543 else: 

544 self._xy0 = None 

545 

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

547 if isinstance(data, afwImage.Exposure): 

548 if wcs: 

549 raise RuntimeError( 

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

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

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

553 try: 

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

555 except TypeError: 

556 wcs = None 

557 data = data.image 

558 

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

560 

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

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

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

564 elif isinstance(data, afwImage.Mask): 

565 self.__addMissingMaskPlanes(data) 

566 # 

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

568 # with pixel values set to the mask 

569 # 

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

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

572 elif isinstance(data, afwImage.MaskedImage): 

573 self.__addMissingMaskPlanes(data.mask) 

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

575 else: 

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

577 # 

578 # Graphics commands 

579 # 

580 

581 class _Buffering: 

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

583 """ 

584 

585 def __init__(self, _impl): 

586 self._impl = _impl 

587 

588 def __enter__(self): 

589 self._impl._buffer(True) 

590 

591 def __exit__(self, *args): 

592 self._impl._buffer(False) 

593 self._impl._flush() 

594 

595 def Buffering(self): 

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

597 

598 Examples 

599 -------- 

600 .. code-block:: py 

601 

602 with display.Buffering(): 

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

604 """ 

605 return self._Buffering(self._impl) 

606 

607 def flush(self): 

608 """Flush the buffers 

609 """ 

610 self._impl._flush() 

611 

612 def erase(self): 

613 """Erase the specified display frame 

614 """ 

615 self._impl._erase() 

616 

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

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

619 

620 Parameters 

621 ---------- 

622 symb 

623 Possible values are: 

624 

625 ``"+"`` 

626 Draw a + 

627 ``"x"`` 

628 Draw an x 

629 ``"*"`` 

630 Draw a * 

631 ``"o"`` 

632 Draw a circle 

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

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

635 `lsst.afw.geom.ellipses.BaseCore` 

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

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

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

639 Any other value 

640 Interpreted as a string to be drawn. 

641 c, r 

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

643 size : `int` 

644 Size of symbol, in pixels 

645 ctype : `str` 

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

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

648 Coordinate system for the given positions. 

649 *args 

650 Extra arguments to backend 

651 **kwargs 

652 Extra keyword arguments to backend 

653 """ 

654 if isinstance(symb, int): 

655 symb = f"{symb:d}" 

656 

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

658 x0, y0 = self._xy0 

659 r -= y0 

660 c -= x0 

661 

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

663 try: 

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

665 except TypeError: 

666 pass 

667 else: 

668 if mat: 

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

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

671 

672 symb = afwGeom.ellipses.Axes(symb) 

673 

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

675 

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

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

678 

679 Parameters 

680 ---------- 

681 points : `list` 

682 a list of (col, row) 

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

684 Coordinate system for the given positions. 

685 symbs : `bool` or sequence 

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

687 otherwise connect the dots. 

688 

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

690 elements are used to label the points 

691 ctype : `str` 

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

693 size : `float` 

694 """ 

695 if symbs: 

696 try: 

697 symbs[1] 

698 except TypeError: 

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

700 

701 for i, xy in enumerate(points): 

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

703 else: 

704 if len(points) > 0: 

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

706 x0, y0 = self._xy0 

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

708 for i, p in enumerate(points): 

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

710 points = _points 

711 

712 self._impl._drawLines(points, ctype) 

713 # 

714 # Set gray scale 

715 # 

716 

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

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

719 

720 Parameters 

721 ---------- 

722 algorithm : `str` 

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

724 min 

725 Minimum value, or "minmax" or "zscale" 

726 max 

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

728 unit 

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

730 *args 

731 Optional arguments to the backend 

732 **kwargs 

733 Optional keyword arguments to the backend 

734 """ 

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

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

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

738 elif max is None: 

739 raise RuntimeError("Please specify max") 

740 

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

742 # 

743 # Zoom and Pan 

744 # 

745 

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

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

748 """ 

749 

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

751 raise RuntimeError( 

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

753 

754 if rowc is not None: 

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

756 x0, y0 = self._xy0 

757 colc -= x0 

758 rowc -= y0 

759 

760 self._impl._pan(colc, rowc) 

761 

762 if zoomfac is None and rowc is None: 

763 zoomfac = 2 

764 

765 if zoomfac is not None: 

766 self._impl._zoom(zoomfac) 

767 

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

769 """Pan to a location 

770 

771 Parameters 

772 ---------- 

773 colc, rowc 

774 the coordinates to pan to 

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

776 Coordinate system for the given positions. 

777 

778 See also 

779 -------- 

780 Display.zoom 

781 """ 

782 

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

784 

785 def interact(self): 

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

787 

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

789 """ 

790 interactFinished = False 

791 

792 while not interactFinished: 

793 ev = self._impl._getEvent() 

794 if not ev: 

795 continue 

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

797 

798 if k not in self._callbacks: 

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

800 else: 

801 try: 

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

803 except Exception as e: 

804 logger.error( 

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

806 

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

808 """Set the callback for a key 

809 

810 Parameters 

811 ---------- 

812 k 

813 The key to assign the callback to 

814 func : callable 

815 The callback assigned to ``k`` 

816 noRaise : `bool` 

817 

818 Returns 

819 ------- 

820 oldFunc : callable 

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

822 """ 

823 

824 if k in "f": 

825 if noRaise: 

826 return 

827 raise RuntimeError( 

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

829 

830 ofunc = self._callbacks.get(k) 

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

832 

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

834 

835 return ofunc 

836 

837 def getActiveCallbackKeys(self, onlyActive=True): 

838 """Return all callback keys 

839 

840 Parameters 

841 ---------- 

842 onlyActive : `bool` 

843 If `True` only return keys that do something 

844 """ 

845 

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

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

848 

849# 

850# Callbacks for display events 

851# 

852 

853 

854class Event: 

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

856 """ 

857 

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

859 self.k = k 

860 self.x = x 

861 self.y = y 

862 

863 def __str__(self): 

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

865# 

866# Default fallback function 

867# 

868 

869 

870def noop_callback(k, x, y): 

871 """Callback function 

872 

873 Parameters 

874 ---------- 

875 key 

876 x 

877 y 

878 """ 

879 return False 

880 

881 

882def h_callback(k, x, y): 

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

884 return False 

885 

886# 

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

888# 

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

890# 

891 

892 

893def setDefaultBackend(backend): 

894 Display.setDefaultBackend(backend) 

895 

896 

897def getDefaultBackend(): 

898 return Display.getDefaultBackend() 

899 

900 

901def setDefaultFrame(frame=0): 

902 return Display.setDefaultFrame(frame) 

903 

904 

905def getDefaultFrame(): 

906 """Get the default frame for display 

907 """ 

908 return Display.getDefaultFrame() 

909 

910 

911def incrDefaultFrame(): 

912 """Increment the default frame for display 

913 """ 

914 return Display.incrDefaultFrame() 

915 

916 

917def setDefaultMaskTransparency(maskPlaneTransparency={}): 

918 return Display.setDefaultMaskTransparency(maskPlaneTransparency) 

919 

920 

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

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

923 

924 Parameters 

925 ---------- 

926 name : `str` or `dict` 

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

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

929 color : `str` 

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

931 """ 

932 

933 return Display.setDefaultMaskPlaneColor(name, color) 

934 

935 

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

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

938 

939 Parameters 

940 ---------- 

941 frame 

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

943 backend : `str` 

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

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

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

947 create : `bool` 

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

949 verbose : `bool` 

950 Allow backend to be chatty 

951 *args 

952 arguments passed to `Display` constructor 

953 **kwargs 

954 keyword arguments passed to `Display` constructor 

955 

956 See also 

957 -------- 

958 Display.getDisplay 

959 """ 

960 

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

962 

963 

964def delAllDisplays(): 

965 """Delete and close all known displays 

966 """ 

967 return Display.delAllDisplays()