Coverage for python / lsst / display / astrowidgets / astrowidgets.py: 22%

193 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-07 08:20 +0000

1# This file is part of display_astrowidgets. 

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__ = ["AstroWidgetsVersion", "DisplayImpl"] 

23 

24import sys 

25import tempfile 

26import warnings 

27from astropy.table import Table 

28from astropy.io.fits.verify import VerifyWarning 

29from astropy.wcs import FITSFixedWarning 

30 

31import lsst.afw.display.interface as interface 

32import lsst.afw.display.virtualDevice as virtualDevice 

33import lsst.afw.display.ds9Regions as ds9Regions 

34from lsst.afw.display import writeFitsImage 

35 

36try: 

37 from ginga.misc.log import get_logger 

38 from ginga.util.io import io_fits 

39 haveGinga = True 

40except ImportError: 

41 import logging 

42 logging.getLogger("lsst.afw.display.astrowidgets").warning("Cannot import ginga libraries.") 

43 haveGinga = False 

44 

45 

46try: 

47 import astrowidgets 

48 haveAstrowidgets = True 

49except ImportError: 

50 haveAstrowidgets = False 

51 

52try: 

53 _maskTransparency 

54except NameError: 

55 _maskTransparency = None 

56 

57 

58def AstroWidgetsVersion(): 

59 """Get the version of Astrowidgets in use. 

60 

61 Returns 

62 ------- 

63 version : `str` 

64 Version of DS9 in use. 

65 """ 

66 return astrowidgets.__version__ 

67 

68 

69class AstroWidgetsEvent(interface.Event): 

70 """An event generated by a mouse or key click on the display""" 

71 

72 def __int__(self, k, x, y): 

73 interface.Event.__init__(self, k, x, y) 

74 

75 

76class DisplayImpl(virtualDevice.DisplayImpl): 

77 """Virtual device display implementation. 

78 

79 Parameters 

80 ---------- 

81 display : `lsst.afw.display.virtualDevice.DisplayImpl` 

82 Display object to connect to. 

83 dims : `tuple` [`int`, `int`], optional 

84 Dimensions of the viewer window. 

85 use_opencv : `bool`, optional 

86 Should openCV be used to speed drawing? 

87 verbose : `bool`, optional 

88 Increase log verbosity? 

89 """ 

90 markerDict = {'+': 'plus', 'x': 'cross', '.': 'circle', '*': 'circle', 'o': 'circle'} 

91 

92 def __init__(self, display, dims=None, use_opencv=False, verbose=False, *args, **kwargs): 

93 virtualDevice.DisplayImpl.__init__(self, display, verbose) 

94 if dims is None: 

95 width, height = 1024, 768 

96 else: 

97 width, height = dims 

98 if haveGinga: 

99 self.logger = get_logger("ginga", log_stderr=True, level=40) 

100 else: 

101 self.logger = None 

102 self._viewer = astrowidgets.ImageWidget(image_width=width, image_height=height, 

103 use_opencv=use_opencv, logger=self.logger) 

104 self._defaultMarkTagName = 'all' 

105 self._callbackDict = dict() 

106 

107 # We want to display the IW, but ginga has all the handles 

108 self._gingaViewer = self._viewer._viewer 

109 

110 bd = self._gingaViewer.get_bindings() 

111 bd.enable_all(True) 

112 self._canvas = self._viewer.canvas 

113 self._canvas.enable_draw(False) 

114 self._maskTransparency = 0.8 

115 self._redraw = True 

116 

117 def embed(self): 

118 """Attach this display to the output of the current cell.""" 

119 return self._viewer 

120 

121 def get_viewer(self): 

122 """Return the ginga viewer""" 

123 return self._viewer 

124 

125 def show_color_bar(self, show=True): 

126 """Show (or hide) the colour bar. 

127 

128 Parameters 

129 ---------- 

130 show : `bool`, optional 

131 Should the color bar be shown? 

132 """ 

133 self._gingaViewer.show_color_bar(show) 

134 

135 def show_pan_mark(self, show=True, color='red'): 

136 """Show (or hide) the pan mark. 

137 

138 Parameters 

139 ---------- 

140 show : `bool`, optional 

141 Should the pan marker be shown? 

142 color : `str`, optional 

143 What color should the pan mark be? 

144 """ 

145 self._gingaViewer.show_pan_mark(show, color) 

146 

147 def _setMaskTransparency(self, transparency, maskplane=None): 

148 """Specify mask transparency (percent); or None to not set it when loading masks. 

149 

150 Parameters 

151 ---------- 

152 transparency : `float` 

153 Transparency of the masks in percent (0-100). 

154 maskplane : `str`, optional 

155 Unsupported option to only change the transparency of 

156 certain masks. 

157 """ 

158 if maskplane is not None: 

159 print("display_astrowidgets is not yet able to set transparency for individual maskplanes" % maskplane, # noqa E501 

160 file=sys.stderr) 

161 return 

162 

163 self._maskTransparency = 0.01*transparency 

164 

165 def _getMaskTransparency(self, maskplane=None): 

166 """Return the current mask transparency.""" 

167 return self._maskTransparency 

168 

169 def _mtv(self, image, mask=None, wcs=None, title="", metadata=None): 

170 """Display an Image and/or Mask on a ginga display 

171 

172 Parameters 

173 ---------- 

174 image : `lsst.afw.image.Image` or `lsst.afw.image.Exposure` 

175 Image to display. 

176 mask : `lsst.afw.image.Mask`, optional 

177 Mask to use, if the input does not contain one. 

178 wcs : `ginga.util.wcsmod.wcs_astropy` 

179 WCS to use, if the input does not contain one. 

180 title : `str`, optional 

181 Title that will be stored in OBJECT header of FITS file. 

182 metadata : `lsst.daf.base.PropertyList`, optional 

183 FITS header information that might be used. 

184 """ 

185 self._erase() 

186 self._canvas.delete_all_objects() 

187 self._buffer() 

188 if haveGinga: 

189 with tempfile.NamedTemporaryFile() as fd: 

190 writeFitsImage(fd.name, image, wcs, title, metadata=metadata) 

191 fd.flush() 

192 # Astropy complains a lot about things we do not care about. 

193 with warnings.catch_warnings(): 

194 warnings.simplefilter("ignore", VerifyWarning) 

195 warnings.simplefilter("ignore", FITSFixedWarning) 

196 Aimage = io_fits.load_file(fd.name) 

197 self._gingaViewer.set_image(Aimage) 

198 

199 if mask: 

200 maskColorFromName = {'BAD': 'red', 

201 'SAT': 'green', 

202 'INTRP': 'green', 

203 'CR': 'magenta', 

204 'EDGE': 'yellow', 

205 'DETECTED': 'blue', 

206 'DETECTED_NEGATIVE': 'cyan', 

207 'SUSPECT': 'yellow', 

208 'NO_DATA': 'orange', 

209 'CROSSTALK': None, 

210 'UNMASKEDNAN': None} 

211 maskDict = dict() 

212 for plane, bit in mask.getMaskPlaneDict().items(): 

213 color = maskColorFromName.get(plane, None) 

214 if color: 

215 maskDict[1 << bit] = color 

216 # This value of 0.9 is pretty thick for the alpha. 

217 self.overlay_mask(mask, maskDict, 

218 self._maskTransparency) 

219 self._buffer(enable=False) 

220 self._flush() 

221 

222 def overlay_mask(self, maskImage, maskDict, maskAlpha): 

223 """Draw mask onto the image display. 

224 

225 Parameters 

226 ---------- 

227 maskImage : `lsst.afw.image.Mask` 

228 Mask to display. 

229 maskDict : `dict` [`str`, `str`] 

230 Dictionary of mask plane names to colors. 

231 maskAlpha : `float` 

232 Transparency to display the mask. 

233 """ 

234 import numpy as np 

235 from ginga.RGBImage import RGBImage 

236 from ginga import colors 

237 

238 maskArray = maskImage.getArray() 

239 height, width = maskArray.shape 

240 maskRGBA = np.zeros((height, width, 4), dtype=np.uint8) 

241 nSet = np.zeros_like(maskArray, dtype=np.uint8) 

242 

243 for maskValue, maskColor in maskDict.items(): 

244 r, g, b = colors.lookup_color(maskColor) 

245 isSet = (maskArray & maskValue) != 0 

246 if (isSet == 0).all(): 

247 continue 

248 

249 maskRGBA[:, :, 0][isSet] = 255 * r 

250 maskRGBA[:, :, 1][isSet] = 255 * g 

251 maskRGBA[:, :, 2][isSet] = 255 * b 

252 

253 nSet[isSet] += 1 

254 

255 maskRGBA[:, :, 3][nSet == 0] = 0 

256 maskRGBA[:, :, 3][nSet != 0] = 255 * maskAlpha 

257 

258 nSet[nSet == 0] = 1 

259 for C in (0, 1, 2): 

260 maskRGBA[:, :, C] //= nSet 

261 

262 rgb_img = RGBImage(data_np=maskRGBA) 

263 Image = self._viewer.canvas.get_draw_class('image') 

264 maskImageRGBA = Image(0, 0, rgb_img) 

265 

266 if "mask_overlay" in self._gingaViewer.canvas.get_tags(): 

267 self._gingaViewer.canvas.delete_object_by_tag("mask_overlay") 

268 self._gingaViewer.canvas.add(maskImageRGBA, tag="mask_overlay") 

269 

270 def _buffer(self, enable=True): 

271 self._redraw = not enable 

272 

273 def _flush(self): 

274 self._gingaViewer.redraw(whence=3) 

275 

276 def _erase(self): 

277 """Erase the display""" 

278 self._canvas.delete_all_objects() 

279 

280 def _dot(self, symb, c, r, size, ctype, fontFamily="helvetica", textAngle=None, label='_dot'): 

281 """Draw a symbol at (col,row) = (c,r) [0-based coordinates]. 

282 

283 Parameters 

284 ---------- 

285 symb : `str` 

286 Symbol to draw. Should be one of '+', 'x', '*', 'o', '.'. 

287 c : `int` 

288 Image column for dot center (0-based coordinates). 

289 r : `int` 

290 Image row for dot center (0-based coordinate). 

291 size : `int` 

292 Size of dot. 

293 fontFamily : `str`, optional 

294 Font to use for text symbols. 

295 textAngle : `float`, optional 

296 Text rotation angle. 

297 label : `str`, optional 

298 Label to store this dot in the internal list. 

299 """ 

300 dataTable = Table([{'x': c, 'y': r}]) 

301 if symb in '+x*.o': 

302 self._viewer.marker = {'type': self.markerDict[symb], 'color': ctype, 'radius': size} 

303 self._viewer.add_markers(dataTable, marker_name=label) 

304 self._flush() 

305 else: 

306 Line = self._canvas.get_draw_class('line') 

307 Text = self._canvas.get_draw_class('text') 

308 

309 for ds9Cmd in ds9Regions.dot(symb, c, r, size, fontFamily="helvetica", textAngle=None): 

310 tmp = ds9Cmd.split('#') 

311 cmd = tmp.pop(0).split() 

312 comment = tmp.pop(0) if tmp else "" 

313 

314 cmd, args = cmd[0], cmd[1:] 

315 if cmd == "line": 

316 self._gingaViewer.canvas.add(Line(*[float(p) - 1 for p in args], color=ctype), 

317 redraw=self._redraw) 

318 elif cmd == "text": 

319 x, y = [float(p) - 1 for p in args[0:2]] 

320 self._gingaViewer.canvas.add(Text(x, y, symb, color=ctype), redraw=self._redraw) 

321 else: 

322 raise RuntimeError(ds9Cmd) 

323 if comment: 

324 print(comment) 

325 

326 def _drawLines(self, points, ctype): 

327 """Connect the points, a list of (col,row). 

328 

329 Parameters 

330 ---------- 

331 points : `list` [`tuple` [`int`, `int`]] 

332 Points to connect with lines. 

333 ctype : `str` 

334 Color to use. 

335 """ 

336 Line = self._gingaViewer.canvas.get_draw_class('line') 

337 p0 = points[0] 

338 for p in points[1:]: 

339 self._gingaViewer.canvas.add(Line(p0[0], p0[1], p[0], p[1], color=ctype), redraw=self._redraw) 

340 p0 = p 

341 

342 def beginMarking(self, symb='+', ctype='cyan', size=10, label='interactive'): 

343 """Begin interactive mark adding. 

344 

345 Parameters 

346 ---------- 

347 symb : `str`, optional 

348 Symbol to use. Should be one of '+', 'x', '*', 'o', '.'. 

349 ctype : `str`, optional 

350 Color of markers. 

351 size : `float`, optional 

352 Size of marker. 

353 label : `str` 

354 Label to store this marker in the internal list. 

355 """ 

356 self._viewer.start_marking(marker_name=label, 

357 marker={'type': self.markerDict[symb], 'color': ctype, 'radius': size}) 

358 

359 def endMarking(self): 

360 """End interactive mark adding.""" 

361 self._viewer.stop_marking() 

362 

363 def getMarkers(self, label='interactive'): 

364 """Get list of markers. 

365 

366 Parameters 

367 ---------- 

368 label : `str`, optional 

369 Marker label to return. 

370 

371 Returns 

372 ------- 

373 table : `astropy.table.Table` 

374 Table of markers with the given label. 

375 """ 

376 return self._viewer.get_markers(marker_name=label) 

377 

378 def clearMarkers(self, label=None): 

379 """Clear markers. 

380 

381 Parameters 

382 ---------- 

383 label : `str`, optional 

384 Marker label to clear. If None, all markers are cleared. 

385 """ 

386 if label: 

387 self._viewer.remove_markers(label) 

388 else: 

389 self._viewer.reset_markers() 

390 

391 def linkMarkers(self, ctype='brown', label='interactive'): 

392 """Connect markers with lines. 

393 

394 Parameters 

395 ---------- 

396 ctype : `str`, optional 

397 Color to draw the lines. 

398 label : `str`, optional 

399 Marker label to connect. Lines are drawn in the order 

400 found in the table. 

401 """ 

402 Line = self._gingaViewer.canvas.get_draw_class('line') 

403 table = self._viewer.get_markers(marker_name=label) 

404 

405 x0, y0 = (0, 0) 

406 for rowCount, (x, y) in enumerate(table.iterrows('x', 'y')): 

407 if rowCount != 0: 

408 self._gingaViewer.canvas.add(Line(x0, y0, x, y, color=ctype), redraw=self._redraw) 

409 x0 = x 

410 y0 = y 

411 

412 def clearLines(self): 

413 """Remove all lines from the display.""" 

414 self._gingaViewer.canvas.deleteObjects(list(self._gingaViewer.canvas.get_objects_by_kind('line'))) 

415 

416 def _scale(self, algorithm, min, max, unit, *args, **kwargs): 

417 """Set greyscale values. 

418 

419 Parameters 

420 ---------- 

421 algorithm : `str` 

422 Image scaling algorithm to use. 

423 min : `float` or `str` 

424 Minimum value to set to black. If a string, should be one of 'zscale' or 'minmax'. 

425 max : `float` 

426 Maximum value to set to white. 

427 unit : `str` 

428 Scaling units. This is ignored. 

429 """ 

430 self._gingaViewer.set_color_map('gray') 

431 self._gingaViewer.set_color_algorithm(algorithm) 

432 

433 if min == "zscale": 

434 self._gingaViewer.set_autocut_params('zscale', contrast=0.25) 

435 self._gingaViewer.auto_levels() 

436 elif min == "minmax": 

437 self._gingaViewer.set_autocut_params('minmax') 

438 self._gingaViewer.auto_levels() 

439 else: 

440 if unit: 

441 print("ginga: ignoring scale unit %s" % unit, file=sys.stderr) 

442 

443 self._gingaViewer.cut_levels(min, max) 

444 

445 def _show(self): 

446 """Show the requested display. 

447 

448 In this case, embed it in the notebook (equivalent to 

449 Display.get_viewer().show(); see also 

450 Display.get_viewer().embed() N.b. These command *must* be the 

451 last entry in their cell 

452 """ 

453 return self._gingaViewer.show() 

454 

455 # 

456 # Zoom and Pan 

457 # 

458 def _zoom(self, zoomfac): 

459 """Zoom by specified amount 

460 

461 Parameters 

462 ---------- 

463 zoomfac : `float` 

464 Zoom factor to use. 

465 """ 

466 self._gingaViewer.scale_to(zoomfac, zoomfac) 

467 

468 def _pan(self, colc, rowc): 

469 """Pan to (colc, rowc) 

470 

471 Parameters 

472 ---------- 

473 colc : `int` 

474 Column to center in viewer (0-based coordinate). 

475 rowc : `int` 

476 Row to center in viewer (0-based coordinate). 

477 """ 

478 self._gingaViewer.set_pan(colc, rowc) 

479 

480 def _getEvent(self): 

481 """Listen for a key press on a frame in DS9 and return an event. 

482 

483 Returns 

484 ------- 

485 event : `Ds9Event` 

486 Event with (key, x, y). 

487 """ 

488 pass