Coverage for python/lsst/afw/display/utils.py: 7%

188 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2022-09-30 02: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# @file 

23# @brief Utilities to use with displaying images 

24 

25import lsst.geom 

26import lsst.afw.image as afwImage 

27 

28__all__ = ( 

29 "Mosaic", 

30 "drawBBox", "drawFootprint", "drawCoaddInputs", 

31) 

32 

33 

34def _getDisplayFromDisplayOrFrame(display, frame=None): 

35 """Return an `lsst.afw.display.Display` given either a display or a frame ID. 

36 

37 Notes 

38 ----- 

39 If the two arguments are consistent, return the desired display; if they are not, 

40 raise a `RuntimeError` exception. 

41 

42 If the desired display is `None`, return `None`; 

43 if ``(display, frame) == ("deferToFrame", None)``, return the default display 

44 """ 

45 

46 # import locally to allow this file to be imported by __init__ 

47 import lsst.afw.display as afwDisplay 

48 

49 if display in ("deferToFrame", None): 

50 if display is None and frame is None: 

51 return None 

52 

53 # "deferToFrame" is the default value, and means "obey frame" 

54 display = None 

55 

56 if display and not hasattr(display, "frame"): 

57 raise RuntimeError(f"display == {display} doesn't support .frame") 

58 

59 if frame and display and display.frame != frame: 

60 raise RuntimeError("Please specify display *or* frame") 

61 

62 if display: 

63 frame = display.frame 

64 

65 display = afwDisplay.getDisplay(frame, create=True) 

66 

67 return display 

68 

69 

70class Mosaic: 

71 """A class to handle mosaics of one or more identically-sized images 

72 (or `~lsst.afw.image.Mask` or `~lsst.afw.image.MaskedImage`) 

73 

74 Notes 

75 ----- 

76 Note that this mosaic is a patchwork of the input images; if you want to 

77 make a mosaic of a set images of the sky, you probably want to use the coadd code 

78 

79 Examples 

80 -------- 

81 

82 .. code-block:: py 

83 

84 m = Mosaic() 

85 m.setGutter(5) 

86 m.setBackground(10) 

87 m.setMode("square") # the default; other options are "x" or "y" 

88 

89 mosaic = m.makeMosaic(im1, im2, im3) # build the mosaic 

90 display = afwDisplay.getDisplay() 

91 display.mtv(mosaic) # display it 

92 m.drawLabels(["Label 1", "Label 2", "Label 3"], display) # label the panels 

93 

94 # alternative way to build a mosaic 

95 images = [im1, im2, im3] 

96 labels = ["Label 1", "Label 2", "Label 3"] 

97 

98 mosaic = m.makeMosaic(images) 

99 display.mtv(mosaic) 

100 m.drawLabels(labels, display) 

101 

102 # Yet another way to build a mosaic (no need to build the images/labels lists) 

103 for i in range(len(images)): 

104 m.append(images[i], labels[i]) 

105 # You may optionally include a colour, e.g. afwDisplay.YELLOW, as a third argument 

106 

107 mosaic = m.makeMosaic() 

108 display.mtv(mosaic) 

109 m.drawLabels(display=display) 

110 

111 Or simply: 

112 

113 .. code-block:: py 

114 

115 mosaic = m.makeMosaic(display=display) 

116 

117 You can return the (ix, iy)th (or nth) bounding box (in pixels) with `getBBox()` 

118 """ 

119 

120 def __init__(self, gutter=3, background=0, mode="square"): 

121 self.gutter = gutter # number of pixels between panels in a mosaic 

122 self.background = background # value in gutters 

123 self.setMode(mode) # mosaicing mode 

124 self.xsize = 0 # column size of panels 

125 self.ysize = 0 # row size of panels 

126 

127 self.reset() 

128 

129 def reset(self): 

130 """Reset the list of images to be mosaiced""" 

131 self.images = [] # images to mosaic together 

132 self.labels = [] # labels for images 

133 

134 def append(self, image, label=None, ctype=None): 

135 """Add an image to the list of images to be mosaiced 

136 

137 Returns 

138 ------- 

139 index 

140 the index of this image (may be passed to `getBBox()`) 

141 

142 Notes 

143 ----- 

144 Set may be cleared with ``Mosaic.reset()`` 

145 """ 

146 if not self.xsize: 

147 self.xsize = image.getWidth() 

148 self.ysize = image.getHeight() 

149 

150 self.images.append(image) 

151 self.labels.append((label, ctype)) 

152 

153 return len(self.images) 

154 

155 def makeMosaic(self, images=None, display="deferToFrame", mode=None, 

156 background=None, title=""): 

157 """Return a mosaic of all the images provided. 

158 

159 If none are specified, use the list accumulated with `Mosaic.append()`. 

160 

161 If display is specified, display the mosaic 

162 """ 

163 

164 if images: 

165 if self.images: 

166 raise RuntimeError( 

167 f"You have already appended {len(self.images)} images to this Mosaic") 

168 

169 try: 

170 len(images) # check that it quacks like a list 

171 except TypeError: 

172 images = [images] 

173 

174 self.images = images 

175 else: 

176 images = self.images 

177 

178 if self.nImage == 0: 

179 raise RuntimeError("You must provide at least one image") 

180 

181 self.xsize, self.ysize = 0, 0 

182 for im in images: 

183 w, h = im.getWidth(), im.getHeight() 

184 if w > self.xsize: 

185 self.xsize = w 

186 if h > self.ysize: 

187 self.ysize = h 

188 

189 if background is None: 

190 background = self.background 

191 if mode is None: 

192 mode = self.mode 

193 

194 if mode == "square": 

195 nx, ny = 1, self.nImage 

196 while nx*im.getWidth() < ny*im.getHeight(): 

197 nx += 1 

198 ny = self.nImage//nx 

199 

200 if nx*ny < self.nImage: 

201 ny += 1 

202 if nx*ny < self.nImage: 

203 nx += 1 

204 

205 if nx > self.nImage: 

206 nx = self.nImage 

207 

208 assert(nx*ny >= self.nImage) 

209 elif mode == "x": 

210 nx, ny = self.nImage, 1 

211 elif mode == "y": 

212 nx, ny = 1, self.nImage 

213 elif isinstance(mode, int): 

214 nx = mode 

215 ny = self.nImage//nx 

216 if nx*ny < self.nImage: 

217 ny += 1 

218 else: 

219 raise RuntimeError(f"Unknown mosaicing mode: {mode}") 

220 

221 self.nx, self.ny = nx, ny 

222 

223 mosaic = images[0].Factory( 

224 lsst.geom.Extent2I(nx*self.xsize + (nx - 1)*self.gutter, 

225 ny*self.ysize + (ny - 1)*self.gutter) 

226 ) 

227 try: 

228 mosaic.set(self.background) 

229 except AttributeError: 

230 raise RuntimeError(f"Attempt to mosaic images of type {type(mosaic)} which don't support set") 

231 

232 for i in range(len(images)): 

233 smosaic = mosaic.Factory( 

234 mosaic, self.getBBox(i%nx, i//nx), afwImage.LOCAL) 

235 im = images[i] 

236 

237 if smosaic.getDimensions() != im.getDimensions(): # im is smaller than smosaic 

238 llc = lsst.geom.PointI((smosaic.getWidth() - im.getWidth())//2, 

239 (smosaic.getHeight() - im.getHeight())//2) 

240 smosaic = smosaic.Factory(smosaic, lsst.geom.Box2I( 

241 llc, im.getDimensions()), afwImage.LOCAL) 

242 

243 smosaic[:] = im 

244 

245 display = _getDisplayFromDisplayOrFrame(display) 

246 if display: 

247 display.mtv(mosaic, title=title) 

248 

249 if images == self.images: 

250 self.drawLabels(display=display) 

251 

252 return mosaic 

253 

254 def setGutter(self, gutter): 

255 """Set the number of pixels between panels in a mosaic 

256 """ 

257 self.gutter = gutter 

258 

259 def setBackground(self, background): 

260 """Set the value in the gutters 

261 """ 

262 self.background = background 

263 

264 def setMode(self, mode): 

265 """Set mosaicing mode. 

266 

267 Parameters 

268 ---------- 

269 mode : {"square", "x", "y"} 

270 Valid options: 

271 

272 square 

273 Make mosaic as square as possible 

274 x 

275 Make mosaic one image high 

276 y 

277 Make mosaic one image wide 

278 """ 

279 

280 if mode not in ("square", "x", "y"): 

281 raise RuntimeError(f"Unknown mosaicing mode: {mode}") 

282 

283 self.mode = mode 

284 

285 def getBBox(self, ix, iy=None): 

286 """Get the BBox for a panel 

287 

288 Parameters 

289 ---------- 

290 ix : `int` 

291 If ``iy`` is not `None`, this is the x coordinate of the panel. 

292 If ``iy`` is `None`, this is the number of the panel. 

293 iy : `int`, optional 

294 The y coordinate of the panel. 

295 """ 

296 

297 if iy is None: 

298 ix, iy = ix % self.nx, ix//self.nx 

299 

300 return lsst.geom.Box2I(lsst.geom.PointI(ix*(self.xsize + self.gutter), iy*(self.ysize + self.gutter)), 

301 lsst.geom.ExtentI(self.xsize, self.ysize)) 

302 

303 def drawLabels(self, labels=None, display="deferToFrame", frame=None): 

304 """Draw the list labels at the corners of each panel. 

305 

306 Notes 

307 ----- 

308 If labels is None, use the ones specified by ``Mosaic.append()`` 

309 """ 

310 

311 if not labels: 

312 labels = self.labels 

313 

314 if not labels: 

315 return 

316 

317 if len(labels) != self.nImage: 

318 raise RuntimeError(f"You provided {len(labels)} labels for {self.nImage} panels") 

319 

320 display = _getDisplayFromDisplayOrFrame(display, frame) 

321 if not display: 

322 return 

323 

324 with display.Buffering(): 

325 for i in range(len(labels)): 

326 if labels[i]: 

327 label, ctype = labels[i], None 

328 try: 

329 label, ctype = label 

330 except Exception: 

331 pass 

332 

333 if not label: 

334 continue 

335 

336 display.dot(str(label), self.getBBox(i).getMinX(), 

337 self.getBBox(i).getMinY(), ctype=ctype) 

338 

339 @property 

340 def nImage(self): 

341 """Number of images 

342 """ 

343 return len(self.images) 

344 

345 

346def drawBBox(bbox, borderWidth=0.0, origin=None, display="deferToFrame", ctype=None, bin=1, frame=None): 

347 """Draw a bounding box on a display frame with the specified ctype. 

348 

349 Parameters 

350 ---------- 

351 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D` 

352 The box to draw 

353 borderWidth : `float` 

354 Include this many pixels 

355 origin 

356 If specified, the box is shifted by ``origin`` 

357 display : `str` 

358 ctype : `str` 

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

360 bin : `int` 

361 All BBox coordinates are divided by bin, as is right and proper for overlaying on a binned image 

362 frame 

363 """ 

364 x0, y0 = bbox.getMinX(), bbox.getMinY() 

365 x1, y1 = bbox.getMaxX(), bbox.getMaxY() 

366 

367 if origin: 

368 x0 += origin[0] 

369 x1 += origin[0] 

370 y0 += origin[1] 

371 y1 += origin[1] 

372 

373 x0 /= bin 

374 y0 /= bin 

375 x1 /= bin 

376 y1 /= bin 

377 borderWidth /= bin 

378 

379 display = _getDisplayFromDisplayOrFrame(display, frame) 

380 display.line([(x0 - borderWidth, y0 - borderWidth), 

381 (x0 - borderWidth, y1 + borderWidth), 

382 (x1 + borderWidth, y1 + borderWidth), 

383 (x1 + borderWidth, y0 - borderWidth), 

384 (x0 - borderWidth, y0 - borderWidth), 

385 ], ctype=ctype) 

386 

387 

388def drawFootprint(foot, borderWidth=0.5, origin=None, XY0=None, frame=None, ctype=None, bin=1, 

389 peaks=False, symb="+", size=0.4, ctypePeak=None, display="deferToFrame"): 

390 """Draw an `lsst.afw.detection.Footprint` on a display frame with the specified ctype. 

391 

392 Parameters 

393 ---------- 

394 foot : `lsst.afw.detection.Footprint` 

395 borderWidth : `float` 

396 Include an extra borderWidth pixels 

397 origin 

398 If ``origin`` is present, it's arithmetically added to the Footprint 

399 XY0 

400 if ``XY0`` is present is subtracted from the Footprint 

401 frame 

402 ctype : `str` 

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

404 bin : `int` 

405 All Footprint coordinates are divided by bin, as is right and proper 

406 for overlaying on a binned image 

407 peaks : `bool` 

408 If peaks is `True`, also show the object's Peaks using the specified 

409 ``symb`` and ``size`` and ``ctypePeak`` 

410 symb : `str` 

411 size : `float` 

412 ctypePeak : `str` 

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

414 display : `str` 

415 """ 

416 

417 if XY0: 

418 if origin: 

419 raise RuntimeError("You may not specify both origin and XY0") 

420 origin = (-XY0[0], -XY0[1]) 

421 

422 display = _getDisplayFromDisplayOrFrame(display, frame) 

423 with display.Buffering(): 

424 borderWidth /= bin 

425 for s in foot.getSpans(): 

426 y, x0, x1 = s.getY(), s.getX0(), s.getX1() 

427 

428 if origin: 

429 x0 += origin[0] 

430 x1 += origin[0] 

431 y += origin[1] 

432 

433 x0 /= bin 

434 x1 /= bin 

435 y /= bin 

436 

437 display.line([(x0 - borderWidth, y - borderWidth), 

438 (x0 - borderWidth, y + borderWidth), 

439 (x1 + borderWidth, y + borderWidth), 

440 (x1 + borderWidth, y - borderWidth), 

441 (x0 - borderWidth, y - borderWidth), 

442 ], ctype=ctype) 

443 

444 if peaks: 

445 for p in foot.getPeaks(): 

446 x, y = p.getIx(), p.getIy() 

447 

448 if origin: 

449 x += origin[0] 

450 y += origin[1] 

451 

452 x /= bin 

453 y /= bin 

454 

455 display.dot(symb, x, y, size=size, ctype=ctypePeak) 

456 

457 

458def drawCoaddInputs(exposure, frame=None, ctype=None, bin=1, display="deferToFrame"): 

459 """Draw the bounding boxes of input exposures to a coadd on a display 

460 frame with the specified ctype, assuming ``display.mtv()`` has already been 

461 called on the given exposure on this frame. 

462 

463 All coordinates are divided by ``bin``, as is right and proper for overlaying on a binned image 

464 """ 

465 coaddWcs = exposure.getWcs() 

466 catalog = exposure.getInfo().getCoaddInputs().ccds 

467 

468 offset = lsst.geom.PointD() - lsst.geom.PointD(exposure.getXY0()) 

469 

470 display = _getDisplayFromDisplayOrFrame(display, frame) 

471 

472 with display.Buffering(): 

473 for record in catalog: 

474 ccdBox = lsst.geom.Box2D(record.getBBox()) 

475 ccdCorners = ccdBox.getCorners() 

476 coaddCorners = [coaddWcs.skyToPixel(record.getWcs().pixelToSky(point)) + offset 

477 for point in ccdCorners] 

478 display.line([(coaddCorners[i].getX()/bin, coaddCorners[i].getY()/bin) 

479 for i in range(-1, 4)], ctype=ctype)