Coverage for python/lsst/afw/cameraGeom/utils.py: 9%

445 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-19 04:54 -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""" 

23Support for displaying cameraGeom objects. 

24""" 

25 

26__all__ = ['prepareWcsData', 'plotFocalPlane', 'makeImageFromAmp', 'calcRawCcdBBox', 'makeImageFromCcd', 

27 'FakeImageDataSource', 'ButlerImage', 'rawCallback', 'overlayCcdBoxes', 

28 'showAmp', 'showCcd', 'getCcdInCamBBoxList', 'getCameraImageBBox', 

29 'makeImageFromCamera', 'showCamera', 'makeFocalPlaneWcs', 'findAmp'] 

30 

31import math 

32import numpy 

33 

34import lsst.geom 

35from lsst.afw.fits import FitsError 

36import lsst.afw.geom as afwGeom 

37import lsst.afw.image as afwImage 

38import lsst.afw.math as afwMath 

39import lsst.afw.cameraGeom as afwCameraGeom 

40import lsst.daf.base as dafBase 

41import lsst.log 

42import lsst.pex.exceptions as pexExceptions 

43 

44from ._rotateBBoxBy90 import rotateBBoxBy90 

45from ._assembleImage import assembleAmplifierImage, assembleAmplifierRawImage 

46from ._cameraGeom import FIELD_ANGLE, FOCAL_PLANE 

47from lsst.afw.display.utils import _getDisplayFromDisplayOrFrame 

48from lsst.afw.cameraGeom import DetectorType 

49 

50import lsst.afw.display as afwDisplay 

51import lsst.afw.display.utils as displayUtils 

52 

53_LOG = lsst.log.Log.getLogger(__name__) 

54 

55 

56def prepareWcsData(wcs, amp, isTrimmed=True): 

57 """Put Wcs from an Amp image into CCD coordinates 

58 

59 Parameters 

60 ---------- 

61 wcs : `lsst.afw.geom.SkyWcs` 

62 The WCS object to start from. 

63 amp : `lsst.afw.table.AmpInfoRecord` 

64 Amp object to use 

65 isTrimmed : `bool` 

66 Is the image to which the WCS refers trimmed of non-imaging pixels? 

67 

68 Returns 

69 ------- 

70 ampWcs : `lsst.afw.geom.SkyWcs` 

71 The modified WCS. 

72 """ 

73 if isTrimmed: 

74 ampBox = amp.getRawDataBBox() 

75 else: 

76 ampBox = amp.getRawBBox() 

77 ampCenter = lsst.geom.Point2D(ampBox.getDimensions()/2.0) 

78 wcs = afwGeom.makeFlippedWcs(wcs, amp.getRawFlipX(), amp.getRawFlipY(), ampCenter) 

79 # Shift WCS for trimming 

80 if isTrimmed: 

81 trim_shift = ampBox.getMin() - amp.getBBox().getMin() 

82 wcs = wcs.copyAtShiftedPixelOrigin(lsst.geom.Extent2D(-trim_shift.getX(), -trim_shift.getY())) 

83 # Account for shift of amp data in larger ccd matrix 

84 offset = amp.getRawXYOffset() 

85 return wcs.copyAtShiftedPixelOrigin(lsst.geom.Extent2D(offset)) 

86 

87 

88def plotFocalPlane(camera, fieldSizeDeg_x=0, fieldSizeDeg_y=None, dx=0.1, dy=0.1, figsize=(10., 10.), 

89 useIds=False, showFig=True, savePath=None): 

90 """Make a plot of the focal plane along with a set points that sample 

91 the field of view. 

92 

93 Parameters 

94 ---------- 

95 camera : `lsst.afw.cameraGeom.Camera` 

96 A camera object 

97 fieldSizeDeg_x : `float` 

98 Amount of the field to sample in x in degrees 

99 fieldSizeDeg_y : `float` or `None` 

100 Amount of the field to sample in y in degrees 

101 dx : `float` 

102 Spacing of sample points in x in degrees 

103 dy : `float` 

104 Spacing of sample points in y in degrees 

105 figsize : `tuple` containing two `float` 

106 Matplotlib style tuple indicating the size of the figure in inches 

107 useIds : `bool` 

108 Label detectors by name, not id? 

109 showFig : `bool` 

110 Display the figure on the screen? 

111 savePath : `str` or `None` 

112 If not `None`, save a copy of the figure to this name. 

113 """ 

114 try: 

115 from matplotlib.patches import Polygon 

116 from matplotlib.collections import PatchCollection 

117 import matplotlib.pyplot as plt 

118 except ImportError: 

119 raise ImportError( 

120 "Can't run plotFocalPlane: matplotlib has not been set up") 

121 

122 if fieldSizeDeg_x: 

123 if fieldSizeDeg_y is None: 

124 fieldSizeDeg_y = fieldSizeDeg_x 

125 

126 field_gridx, field_gridy = numpy.meshgrid( 

127 numpy.arange(0., fieldSizeDeg_x + dx, dx) - fieldSizeDeg_x/2., 

128 numpy.arange(0., fieldSizeDeg_y + dy, dy) - fieldSizeDeg_y/2.) 

129 field_gridx, field_gridy = field_gridx.flatten(), field_gridy.flatten() 

130 else: 

131 field_gridx, field_gridy = [], [] 

132 

133 xs = [] 

134 ys = [] 

135 pcolors = [] 

136 

137 # compute focal plane positions corresponding to field angles field_gridx, field_gridy 

138 posFieldAngleList = [lsst.geom.Point2D(x*lsst.geom.radians, y*lsst.geom.radians) 

139 for x, y in zip(field_gridx, field_gridy)] 

140 posFocalPlaneList = camera.transform(posFieldAngleList, FIELD_ANGLE, FOCAL_PLANE) 

141 for posFocalPlane in posFocalPlaneList: 

142 xs.append(posFocalPlane.getX()) 

143 ys.append(posFocalPlane.getY()) 

144 dets = camera.findDetectors(posFocalPlane, FOCAL_PLANE) 

145 if len(dets) > 0: 

146 pcolors.append('w') 

147 else: 

148 pcolors.append('k') 

149 

150 colorMap = {DetectorType.SCIENCE: 'b', DetectorType.FOCUS: 'y', 

151 DetectorType.GUIDER: 'g', DetectorType.WAVEFRONT: 'r'} 

152 

153 patches = [] 

154 colors = [] 

155 plt.figure(figsize=figsize) 

156 ax = plt.gca() 

157 xvals = [] 

158 yvals = [] 

159 for det in camera: 

160 corners = [(c.getX(), c.getY()) for c in det.getCorners(FOCAL_PLANE)] 

161 for corner in corners: 

162 xvals.append(corner[0]) 

163 yvals.append(corner[1]) 

164 colors.append(colorMap[det.getType()]) 

165 patches.append(Polygon(corners, True)) 

166 center = det.getOrientation().getFpPosition() 

167 ax.text(center.getX(), center.getY(), det.getId() if useIds else det.getName(), 

168 horizontalalignment='center', size=6) 

169 

170 patchCollection = PatchCollection(patches, alpha=0.6, facecolor=colors) 

171 ax.add_collection(patchCollection) 

172 ax.scatter(xs, ys, s=10, alpha=.7, linewidths=0., c=pcolors) 

173 ax.set_xlim(min(xvals) - abs(0.1*min(xvals)), 

174 max(xvals) + abs(0.1*max(xvals))) 

175 ax.set_ylim(min(yvals) - abs(0.1*min(yvals)), 

176 max(yvals) + abs(0.1*max(yvals))) 

177 ax.set_xlabel('Focal Plane X (mm)') 

178 ax.set_ylabel('Focal Plane Y (mm)') 

179 if savePath is not None: 

180 plt.savefig(savePath) 

181 if showFig: 

182 plt.show() 

183 

184 

185def makeImageFromAmp(amp, imValue=None, imageFactory=afwImage.ImageU, markSize=10, markValue=0, 185 ↛ exitline 185 didn't jump to the function exit

186 scaleGain=lambda gain: (gain*1000)//10): 

187 """Make an image from an amp object. 

188 

189 Since images are integer images by default, the gain needs to be scaled to 

190 give enough dynamic range to see variation from amp to amp. 

191 The scaling algorithm is assignable. 

192 

193 Parameters 

194 ---------- 

195 amp : `lsst.afw.table.AmpInfoRecord` 

196 Amp record to use for constructing the raw amp image. 

197 imValue : `float` or `None` 

198 Value to assign to the constructed image, or scaleGain(gain) if `None`. 

199 imageFactory : callable like `lsst.afw.image.Image` 

200 Type of image to construct. 

201 markSize : `float` 

202 Size of mark at read corner in pixels. 

203 markValue : `float` 

204 Value of pixels in the read corner mark. 

205 scaleGain : callable 

206 The function by which to scale the gain (must take a single argument). 

207 

208 Returns 

209 ------- 

210 ampImage : `lsst.afw.image` 

211 An untrimmed amp image, of the type produced by ``imageFactory``. 

212 """ 

213 bbox = amp.getRawBBox() 

214 dbbox = amp.getRawDataBBox() 

215 img = imageFactory(bbox) 

216 if imValue is None: 

217 img.set(int(scaleGain(amp.getGain()))) 

218 else: 

219 img.set(imValue) 

220 # Set the first pixel read to a different value 

221 markbbox = lsst.geom.Box2I() 

222 if amp.getReadoutCorner() == afwCameraGeom.ReadoutCorner.LL: 

223 markbbox.include(dbbox.getMin()) 

224 markbbox.include(dbbox.getMin() + lsst.geom.Extent2I(markSize, markSize)) 

225 elif amp.getReadoutCorner() == afwCameraGeom.ReadoutCorner.LR: 

226 cornerPoint = lsst.geom.Point2I(dbbox.getMaxX(), dbbox.getMinY()) 

227 markbbox.include(cornerPoint) 

228 markbbox.include(cornerPoint + lsst.geom.Extent2I(-markSize, markSize)) 

229 elif amp.getReadoutCorner() == afwCameraGeom.ReadoutCorner.UR: 

230 cornerPoint = lsst.geom.Point2I(dbbox.getMax()) 

231 markbbox.include(cornerPoint) 

232 markbbox.include(cornerPoint + lsst.geom.Extent2I(-markSize, -markSize)) 

233 elif amp.getReadoutCorner() == afwCameraGeom.ReadoutCorner.UL: 

234 cornerPoint = lsst.geom.Point2I(dbbox.getMinX(), dbbox.getMaxY()) 

235 markbbox.include(cornerPoint) 

236 markbbox.include(cornerPoint + lsst.geom.Extent2I(markSize, -markSize)) 

237 else: 

238 raise RuntimeError("Could not set readout corner") 

239 mimg = imageFactory(img, markbbox) 

240 mimg.set(markValue) 

241 return img 

242 

243 

244def calcRawCcdBBox(ccd): 

245 """Calculate the raw ccd bounding box. 

246 

247 Parameters 

248 ---------- 

249 ccd : `lsst.afw.cameraGeom.Detector` 

250 Detector for which to calculate the un-trimmed bounding box. 

251 

252 Returns 

253 ------- 

254 bbox : `lsst.geom.Box2I` or `None` 

255 Bounding box of the un-trimmed Detector, or `None` if there is not enough 

256 information to calculate raw BBox. 

257 """ 

258 bbox = lsst.geom.Box2I() 

259 for amp in ccd: 

260 tbbox = amp.getRawBBox() 

261 tbbox.shift(amp.getRawXYOffset()) 

262 bbox.include(tbbox) 

263 return bbox 

264 

265 

266def makeImageFromCcd(ccd, isTrimmed=True, showAmpGain=True, imageFactory=afwImage.ImageU, rcMarkSize=10, 

267 binSize=1): 

268 """Make an Image of a CCD. 

269 

270 Parameters 

271 ---------- 

272 ccd : `lsst.afw.cameraGeom.Detector` 

273 Detector to use in making the image. 

274 isTrimmed : `bool` 

275 Assemble a trimmed Detector image. 

276 showAmpGain : `bool` 

277 Use the per-amp gain to color the pixels in the image? 

278 imageFactory : callable like `lsst.afw.image.Image` 

279 Image type to generate. 

280 rcMarkSize : `float` 

281 Size of the mark to make in the amp images at the read corner. 

282 binSize : `int` 

283 Bin the image by this factor in both dimensions. 

284 

285 Returns 

286 ------- 

287 image : `lsst.afw.image.Image` 

288 Image of the Detector (type returned by ``imageFactory``). 

289 """ 

290 ampImages = [] 

291 index = 0 

292 if isTrimmed: 

293 bbox = ccd.getBBox() 

294 else: 

295 bbox = calcRawCcdBBox(ccd) 

296 for amp in ccd: 

297 if showAmpGain: 

298 ampImages.append(makeImageFromAmp( 

299 amp, imageFactory=imageFactory, markSize=rcMarkSize)) 

300 else: 

301 ampImages.append(makeImageFromAmp(amp, imValue=(index + 1)*1000, 

302 imageFactory=imageFactory, markSize=rcMarkSize)) 

303 index += 1 

304 

305 if len(ampImages) > 0: 

306 ccdImage = imageFactory(bbox) 

307 for ampImage, amp in zip(ampImages, ccd): 

308 if isTrimmed: 

309 assembleAmplifierImage(ccdImage, ampImage, amp) 

310 else: 

311 assembleAmplifierRawImage(ccdImage, ampImage, amp) 

312 else: 

313 if not isTrimmed: 

314 raise RuntimeError( 

315 "Cannot create untrimmed CCD without amps with raw information") 

316 ccdImage = imageFactory(ccd.getBBox()) 

317 ccdImage = afwMath.binImage(ccdImage, binSize) 

318 return ccdImage 

319 

320 

321class FakeImageDataSource: 

322 """A class to retrieve synthetic images for display by the show* methods 

323 

324 Parameters 

325 ---------- 

326 isTrimmed : `bool` 

327 Should amps be trimmed? 

328 verbose : `bool` 

329 Be chatty? 

330 background : `float` 

331 The value of any pixels that lie outside the CCDs. 

332 showAmpGain : `bool` 

333 Color the amp segments with the gain of the amp? 

334 markSize : `float` 

335 Size of the side of the box used to mark the read corner. 

336 markValue : `float` 

337 Value to assign the read corner mark. 

338 ampImValue : `float` or `None` 

339 Value to assign to amps; scaleGain(gain) is used if `None`. 

340 scaleGain : callable 

341 Function to scale the gain by. 

342 """ 

343 def __init__(self, isTrimmed=True, verbose=False, background=numpy.nan, 343 ↛ exitline 343 didn't jump to the function exit

344 showAmpGain=True, markSize=10, markValue=0, 

345 ampImValue=None, scaleGain=lambda gain: (gain*1000)//10): 

346 self.isTrimmed = isTrimmed 

347 self.verbose = verbose 

348 self.background = background 

349 self.showAmpGain = showAmpGain 

350 self.markSize = markSize 

351 self.markValue = markValue 

352 self.ampImValue = ampImValue 

353 self.scaleGain = scaleGain 

354 

355 def getCcdImage(self, det, imageFactory, binSize): 

356 """Return a CCD image for the detector and the (possibly updated) Detector. 

357 

358 Parameters 

359 ---------- 

360 det : `lsst.afw.cameraGeom.Detector` 

361 Detector to use for making the image. 

362 imageFactory : callable like `lsst.afw.image.Image` 

363 Image constructor for making the image. 

364 binSize : `int` 

365 Bin the image by this factor in both dimensions. 

366 

367 Returns 

368 ------- 

369 ccdImage : `lsst.afw.image.Image` 

370 The constructed image. 

371 """ 

372 ccdImage = makeImageFromCcd(det, isTrimmed=self.isTrimmed, showAmpGain=self.showAmpGain, 

373 imageFactory=imageFactory, binSize=binSize) 

374 return afwMath.rotateImageBy90(ccdImage, det.getOrientation().getNQuarter()), det 

375 

376 def getAmpImage(self, amp, imageFactory): 

377 """Return an amp segment image. 

378 

379 Parameters 

380 ---------- 

381 amp : `lsst.afw.table.AmpInfoTable` 

382 AmpInfoTable for this amp. 

383 imageFactory : callable like `lsst.afw.image.Image` 

384 Image constructor for making the image. 

385 

386 Returns 

387 ------- 

388 ampImage : `lsst.afw.image.Image` 

389 The constructed image. 

390 """ 

391 ampImage = makeImageFromAmp(amp, imValue=self.ampImValue, imageFactory=imageFactory, 

392 markSize=self.markSize, markValue=self.markValue, 

393 scaleGain=self.scaleGain) 

394 if self.isTrimmed: 

395 ampImage = ampImage.Factory(ampImage, amp.getRawDataBBox()) 

396 return ampImage 

397 

398 

399class ButlerImage(FakeImageDataSource): 

400 """A class to return an Image of a given Ccd using the butler. 

401 

402 Parameters 

403 ---------- 

404 butler : `lsst.daf.persistence.Butler` or `None` 

405 The butler to use. If `None`, an empty image is returned. 

406 type : `str` 

407 The type of image to read (e.g. raw, bias, flat, calexp). 

408 isTrimmed : `bool` 

409 If true, the showCamera command expects to be given trimmed images. 

410 verbose : `bool` 

411 Be chatty (in particular, log any error messages from the butler)? 

412 background : `float` 

413 The value of any pixels that lie outside the CCDs. 

414 callback : callable 

415 A function called with (image, ccd, butler) for every image, which 

416 returns the image to be displayed (e.g. rawCallback). The image must 

417 be of the correct size, allowing for the value of isTrimmed. 

418 *args : `list` 

419 Passed to the butler. 

420 **kwargs : `dict` 

421 Passed to the butler. 

422 

423 Notes 

424 ----- 

425 You can define a short named function as a callback:: 

426 

427 def callback(im, ccd, imageSource): 

428 return cameraGeom.utils.rawCallback(im, ccd, imageSource, correctGain=True) 

429 """ 

430 def __init__(self, butler=None, type="raw", 

431 isTrimmed=True, verbose=False, background=numpy.nan, 

432 callback=None, *args, **kwargs): 

433 super(ButlerImage, self).__init__(*args) 

434 self.isTrimmed = isTrimmed 

435 self.type = type 

436 self.butler = butler 

437 self.kwargs = kwargs 

438 self.isRaw = False 

439 self.background = background 

440 self.verbose = verbose 

441 self.callback = callback 

442 

443 def _prepareImage(self, ccd, im, binSize, allowRotate=True): 

444 if binSize > 1: 

445 im = afwMath.binImage(im, binSize) 

446 

447 if allowRotate: 

448 im = afwMath.rotateImageBy90( 

449 im, ccd.getOrientation().getNQuarter()) 

450 

451 return im 

452 

453 def getCcdImage(self, ccd, imageFactory=afwImage.ImageF, binSize=1, asMaskedImage=False): 

454 """Return an image of the specified ccd, and also the (possibly updated) ccd""" 

455 

456 log = _LOG.getChild("ButlerImage") 

457 

458 if self.isTrimmed: 

459 bbox = ccd.getBBox() 

460 else: 

461 bbox = calcRawCcdBBox(ccd) 

462 

463 im = None 

464 if self.butler is not None: 

465 err = None 

466 for dataId in [dict(detector=ccd.getId()), dict(ccd=ccd.getId()), dict(ccd=ccd.getName())]: 

467 try: 

468 im = self.butler.get(self.type, dataId, **self.kwargs) 

469 except FitsError as e: # no point trying another dataId 

470 err = IOError(e.args[0].split('\n')[0]) # It's a very chatty error 

471 break 

472 except Exception as e: # try a different dataId 

473 if err is None: 

474 err = e 

475 continue 

476 else: 

477 ccd = im.getDetector() # possibly modified by assembleCcdTask 

478 break 

479 

480 if im: 

481 if asMaskedImage: 

482 im = im.getMaskedImage() 

483 else: 

484 im = im.getMaskedImage().getImage() 

485 else: 

486 if self.verbose: 

487 # Lost by jupyterlab. 

488 print(f"Reading {ccd.getId()}: {err}") 

489 

490 log.warning("Reading %s: %s", ccd.getId(), err) 

491 

492 if im is None: 

493 return self._prepareImage(ccd, imageFactory(*bbox.getDimensions()), binSize), ccd 

494 

495 if self.type == "raw": 

496 if hasattr(im, 'convertF'): 

497 im = im.convertF() 

498 if False and self.callback is None: # we need to trim the raw image 

499 self.callback = rawCallback 

500 

501 allowRotate = True 

502 if self.callback: 

503 try: 

504 im = self.callback(im, ccd, imageSource=self) 

505 except Exception as e: 

506 if self.verbose: 

507 log.error("callback failed: %s", e) 

508 im = imageFactory(*bbox.getDimensions()) 

509 else: 

510 allowRotate = False # the callback was responsible for any rotations 

511 

512 return self._prepareImage(ccd, im, binSize, allowRotate=allowRotate), ccd 

513 

514 

515def rawCallback(im, ccd=None, imageSource=None, 

516 correctGain=False, subtractBias=False, convertToFloat=False, obeyNQuarter=True): 

517 """A callback function that may or may not subtract bias/correct gain/trim 

518 a raw image. 

519 

520 Parameters 

521 ---------- 

522 im : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage` or `lsst.afw.image.Exposure` 

523 An image of a chip, ready to be binned and maybe rotated. 

524 ccd : `lsst.afw.cameraGeom.Detector` or `None` 

525 The Detector; if `None` assume that im is an exposure and extract its Detector. 

526 imageSource : `FakeImageDataSource` or `None` 

527 Source to get ccd images. Must have a `getCcdImage()` method. 

528 correctGain : `bool` 

529 Correct each amplifier for its gain? 

530 subtractBias : `bool` 

531 Subtract the bias from each amplifier? 

532 convertToFloat : `bool` 

533 Convert ``im`` to floating point if possible. 

534 obeyNQuarter : `bool` 

535 Obey nQuarter from the Detector (default: True) 

536 

537 Returns 

538 ------- 

539 image : `lsst.afw.image.Image` like 

540 The constructed image (type returned by ``im.Factory``). 

541 

542 Notes 

543 ----- 

544 If imageSource is derived from ButlerImage, imageSource.butler is available. 

545 """ 

546 if ccd is None: 

547 ccd = im.getDetector() 

548 if hasattr(im, "getMaskedImage"): 

549 im = im.getMaskedImage() 

550 if convertToFloat and hasattr(im, "convertF"): 

551 im = im.convertF() 

552 

553 isTrimmed = imageSource.isTrimmed 

554 if isTrimmed: 

555 bbox = ccd.getBBox() 

556 else: 

557 bbox = calcRawCcdBBox(ccd) 

558 

559 ampImages = [] 

560 for a in ccd: 

561 if isTrimmed: 

562 data = im[a.getRawDataBBox()] 

563 else: 

564 data = im 

565 

566 if subtractBias: 

567 bias = im[a.getRawHorizontalOverscanBBox()] 

568 data -= afwMath.makeStatistics(bias, afwMath.MEANCLIP).getValue() 

569 if correctGain: 

570 data *= a.getGain() 

571 

572 ampImages.append(data) 

573 

574 ccdImage = im.Factory(bbox) 

575 for ampImage, amp in zip(ampImages, ccd): 

576 if isTrimmed: 

577 assembleAmplifierImage(ccdImage, ampImage, amp) 

578 else: 

579 assembleAmplifierRawImage(ccdImage, ampImage, amp) 

580 

581 if obeyNQuarter: 

582 nQuarter = ccd.getOrientation().getNQuarter() 

583 ccdImage = afwMath.rotateImageBy90(ccdImage, nQuarter) 

584 

585 return ccdImage 

586 

587 

588def overlayCcdBoxes(ccd, untrimmedCcdBbox=None, nQuarter=0, 

589 isTrimmed=False, ccdOrigin=(0, 0), display=None, binSize=1): 

590 """Overlay bounding boxes on an image display. 

591 

592 Parameters 

593 ---------- 

594 ccd : `lsst.afw.cameraGeom.Detector` 

595 Detector to iterate for the amp bounding boxes. 

596 untrimmedCcdBbox : `lsst.geom.Box2I` or `None` 

597 Bounding box of the un-trimmed Detector. 

598 nQuarter : `int` 

599 number of 90 degree rotations to apply to the bounding boxes (used for rotated chips). 

600 isTrimmed : `bool` 

601 Is the Detector image over which the boxes are layed trimmed? 

602 ccdOrigin : `tuple` of `float` 

603 Detector origin relative to the parent origin if in a larger pixel grid. 

604 display : `lsst.afw.display.Display` 

605 Image display to display on. 

606 binSize : `int` 

607 Bin the image by this factor in both dimensions. 

608 

609 Notes 

610 ----- 

611 The colours are: 

612 - Entire detector GREEN 

613 - All data for amp GREEN 

614 - HorizontalPrescan YELLOW 

615 - HorizontalOverscan RED 

616 - Data BLUE 

617 - VerticalOverscan MAGENTA 

618 - VerticalOverscan MAGENTA 

619 """ 

620 if not display: # should be second parameter, and not defaulted!! 

621 raise RuntimeError("Please specify a display") 

622 

623 if untrimmedCcdBbox is None: 

624 if isTrimmed: 

625 untrimmedCcdBbox = ccd.getBBox() 

626 else: 

627 untrimmedCcdBbox = lsst.geom.Box2I() 

628 for a in ccd.getAmplifiers(): 

629 bbox = a.getRawBBox() 

630 untrimmedCcdBbox.include(bbox) 

631 

632 with display.Buffering(): 

633 ccdDim = untrimmedCcdBbox.getDimensions() 

634 ccdBbox = rotateBBoxBy90(untrimmedCcdBbox, nQuarter, ccdDim) 

635 for amp in ccd: 

636 if isTrimmed: 

637 ampbbox = amp.getBBox() 

638 else: 

639 ampbbox = amp.getRawBBox() 

640 if nQuarter != 0: 

641 ampbbox = rotateBBoxBy90(ampbbox, nQuarter, ccdDim) 

642 

643 displayUtils.drawBBox(ampbbox, origin=ccdOrigin, borderWidth=0.49, 

644 display=display, bin=binSize) 

645 

646 if not isTrimmed: 

647 for bbox, ctype in ((amp.getRawHorizontalOverscanBBox(), afwDisplay.RED), 

648 (amp.getRawDataBBox(), afwDisplay.BLUE), 

649 (amp.getRawVerticalOverscanBBox(), 

650 afwDisplay.MAGENTA), 

651 (amp.getRawPrescanBBox(), afwDisplay.YELLOW)): 

652 if nQuarter != 0: 

653 bbox = rotateBBoxBy90(bbox, nQuarter, ccdDim) 

654 displayUtils.drawBBox(bbox, origin=ccdOrigin, borderWidth=0.49, ctype=ctype, 

655 display=display, bin=binSize) 

656 # Label each Amp 

657 xc, yc = ((ampbbox.getMin()[0] + ampbbox.getMax()[0])//2, 

658 (ampbbox.getMin()[1] + ampbbox.getMax()[1])//2) 

659 # 

660 # Rotate the amp labels too 

661 # 

662 if nQuarter == 0: 

663 c, s = 1, 0 

664 elif nQuarter == 1: 

665 c, s = 0, -1 

666 elif nQuarter == 2: 

667 c, s = -1, 0 

668 elif nQuarter == 3: 

669 c, s = 0, 1 

670 c, s = 1, 0 

671 ccdHeight = ccdBbox.getHeight() 

672 ccdWidth = ccdBbox.getWidth() 

673 xc -= 0.5*ccdHeight 

674 yc -= 0.5*ccdWidth 

675 

676 xc, yc = 0.5*ccdHeight + c*xc + s*yc, 0.5*ccdWidth + -s*xc + c*yc 

677 

678 if ccdOrigin: 

679 xc += ccdOrigin[0] 

680 yc += ccdOrigin[1] 

681 display.dot(str(amp.getName()), xc/binSize, 

682 yc/binSize, textAngle=nQuarter*90) 

683 

684 displayUtils.drawBBox(ccdBbox, origin=ccdOrigin, 

685 borderWidth=0.49, ctype=afwDisplay.MAGENTA, display=display, bin=binSize) 

686 

687 

688def showAmp(amp, imageSource=FakeImageDataSource(isTrimmed=False), display=None, overlay=True, 

689 imageFactory=afwImage.ImageU): 

690 """Show an amp in an image display. 

691 

692 Parameters 

693 ---------- 

694 amp : `lsst.afw.tables.AmpInfoRecord` 

695 Amp record to use in display. 

696 imageSource : `FakeImageDataSource` or `None` 

697 Source for getting the amp image. Must have a ``getAmpImage()`` method. 

698 display : `lsst.afw.display.Display` 

699 Image display to use. 

700 overlay : `bool` 

701 Overlay bounding boxes? 

702 imageFactory : callable like `lsst.afw.image.Image` 

703 Type of image to display (only used if ampImage is `None`). 

704 """ 

705 if not display: 

706 display = _getDisplayFromDisplayOrFrame(display) 

707 

708 ampImage = imageSource.getAmpImage(amp, imageFactory=imageFactory) 

709 ampImSize = ampImage.getDimensions() 

710 title = amp.getName() 

711 display.mtv(ampImage, title=title) 

712 if overlay: 

713 with display.Buffering(): 

714 if ampImSize == amp.getRawBBox().getDimensions(): 

715 bboxes = [(amp.getRawBBox(), 0.49, afwDisplay.GREEN), ] 

716 xy0 = bboxes[0][0].getMin() 

717 bboxes.append( 

718 (amp.getRawHorizontalOverscanBBox(), 0.49, afwDisplay.RED)) 

719 bboxes.append((amp.getRawDataBBox(), 0.49, afwDisplay.BLUE)) 

720 bboxes.append((amp.getRawPrescanBBox(), 

721 0.49, afwDisplay.YELLOW)) 

722 bboxes.append((amp.getRawVerticalOverscanBBox(), 

723 0.49, afwDisplay.MAGENTA)) 

724 else: 

725 bboxes = [(amp.getBBox(), 0.49, None), ] 

726 xy0 = bboxes[0][0].getMin() 

727 

728 for bbox, borderWidth, ctype in bboxes: 

729 if bbox.isEmpty(): 

730 continue 

731 bbox = lsst.geom.Box2I(bbox) 

732 bbox.shift(-lsst.geom.ExtentI(xy0)) 

733 displayUtils.drawBBox( 

734 bbox, borderWidth=borderWidth, ctype=ctype, display=display) 

735 

736 

737def showCcd(ccd, imageSource=FakeImageDataSource(), display=None, overlay=True, 

738 imageFactory=afwImage.ImageF, binSize=1, inCameraCoords=False): 

739 """Show a CCD on display. 

740 

741 Parameters 

742 ---------- 

743 ccd : `lsst.afw.cameraGeom.Detector` 

744 Detector to use in display. 

745 imageSource : `FakeImageDataSource` or `None` 

746 Source to get ccd images. Must have a ``getCcdImage()`` method. 

747 display : `lsst.afw.display.Display` 

748 image display to use. 

749 overlay : `bool` 

750 Show amp bounding boxes on the displayed image? 

751 imageFactory : callable like `lsst.afw.image.Image` 

752 The image factory to use in generating the images. 

753 binSize : `int` 

754 Bin the image by this factor in both dimensions. 

755 inCameraCoords : `bool` 

756 Show the Detector in camera coordinates? 

757 """ 

758 display = _getDisplayFromDisplayOrFrame(display) 

759 

760 ccdOrigin = lsst.geom.Point2I(0, 0) 

761 nQuarter = 0 

762 ccdImage, ccd = imageSource.getCcdImage( 

763 ccd, imageFactory=imageFactory, binSize=binSize) 

764 

765 ccdBbox = ccdImage.getBBox() 

766 if ccdBbox.getDimensions() == ccd.getBBox().getDimensions(): 

767 isTrimmed = True 

768 else: 

769 isTrimmed = False 

770 

771 if inCameraCoords: 

772 nQuarter = ccd.getOrientation().getNQuarter() 

773 ccdImage = afwMath.rotateImageBy90(ccdImage, nQuarter) 

774 title = ccd.getName() 

775 if isTrimmed: 

776 title += "(trimmed)" 

777 

778 if display: 

779 display.mtv(ccdImage, title=title) 

780 

781 if overlay: 

782 overlayCcdBoxes(ccd, ccdBbox, nQuarter, isTrimmed, 

783 ccdOrigin, display, binSize) 

784 

785 return ccdImage 

786 

787 

788def getCcdInCamBBoxList(ccdList, binSize, pixelSize_o, origin): 

789 """Get the bounding boxes of a list of Detectors within a camera sized pixel grid 

790 

791 Parameters 

792 ---------- 

793 ccdList : `lsst.afw.cameraGeom.Detector` 

794 List of Detector. 

795 binSize : `int` 

796 Bin the image by this factor in both dimensions. 

797 pixelSize_o : `float` 

798 Size of the pixel in mm. 

799 origin : `int` 

800 Origin of the camera pixel grid in pixels. 

801 

802 Returns 

803 ------- 

804 boxList : `list` [`lsst.geom.Box2I`] 

805 A list of bounding boxes in camera pixel coordinates. 

806 """ 

807 boxList = [] 

808 for ccd in ccdList: 

809 if not pixelSize_o == ccd.getPixelSize(): 

810 raise RuntimeError( 

811 "Cameras with detectors with different pixel scales are not currently supported") 

812 

813 dbbox = lsst.geom.Box2D() 

814 for corner in ccd.getCorners(FOCAL_PLANE): 

815 dbbox.include(corner) 

816 llc = dbbox.getMin() 

817 nQuarter = ccd.getOrientation().getNQuarter() 

818 cbbox = ccd.getBBox() 

819 ex = cbbox.getDimensions().getX()//binSize 

820 ey = cbbox.getDimensions().getY()//binSize 

821 bbox = lsst.geom.Box2I( 

822 cbbox.getMin(), lsst.geom.Extent2I(int(ex), int(ey))) 

823 bbox = rotateBBoxBy90(bbox, nQuarter, bbox.getDimensions()) 

824 bbox.shift(lsst.geom.Extent2I(int(llc.getX()//pixelSize_o.getX()/binSize), 

825 int(llc.getY()//pixelSize_o.getY()/binSize))) 

826 bbox.shift(lsst.geom.Extent2I(-int(origin.getX()//binSize), 

827 -int(origin.getY())//binSize)) 

828 boxList.append(bbox) 

829 return boxList 

830 

831 

832def getCameraImageBBox(camBbox, pixelSize, bufferSize): 

833 """Get the bounding box of a camera sized image in pixels 

834 

835 Parameters 

836 ---------- 

837 camBbox : `lsst.geom.Box2D` 

838 Camera bounding box in focal plane coordinates (mm). 

839 pixelSize : `float` 

840 Size of a detector pixel in mm. 

841 bufferSize : `int` 

842 Buffer around edge of image in pixels. 

843 

844 Returns 

845 ------- 

846 box : `lsst.geom.Box2I` 

847 The resulting bounding box. 

848 """ 

849 pixMin = lsst.geom.Point2I(int(camBbox.getMinX()//pixelSize.getX()), 

850 int(camBbox.getMinY()//pixelSize.getY())) 

851 pixMax = lsst.geom.Point2I(int(camBbox.getMaxX()//pixelSize.getX()), 

852 int(camBbox.getMaxY()//pixelSize.getY())) 

853 retBox = lsst.geom.Box2I(pixMin, pixMax) 

854 retBox.grow(bufferSize) 

855 return retBox 

856 

857 

858def makeImageFromCamera(camera, detectorNameList=None, background=numpy.nan, bufferSize=10, 

859 imageSource=FakeImageDataSource(), imageFactory=afwImage.ImageU, binSize=1): 

860 """Make an Image of a Camera. 

861 

862 Put each detector's image in the correct location and orientation on the 

863 focal plane. The input images can be binned to an integer fraction of their 

864 original bboxes. 

865 

866 Parameters 

867 ---------- 

868 camera : `lsst.afw.cameraGeom.Camera` 

869 Camera object to use to make the image. 

870 detectorNameList : `list` [`str`] 

871 List of detector names from ``camera`` to use in building the image. 

872 Use all Detectors if `None`. 

873 background : `float` 

874 Value to use where there is no Detector. 

875 bufferSize : `int` 

876 Size of border in binned pixels to make around the camera image. 

877 imageSource : `FakeImageDataSource` or `None` 

878 Source to get ccd images. Must have a ``getCcdImage()`` method. 

879 imageFactory : callable like `lsst.afw.image.Image` 

880 Type of image to build. 

881 binSize : `int` 

882 Bin the image by this factor in both dimensions. 

883 

884 Returns 

885 ------- 

886 image : `lsst.afw.image.Image` 

887 Image of the entire camera. 

888 """ 

889 log = _LOG.getChild("makeImageFromCamera") 

890 

891 if detectorNameList is None: 

892 ccdList = camera 

893 else: 

894 ccdList = [camera[name] for name in detectorNameList] 

895 

896 if detectorNameList is None: 

897 camBbox = camera.getFpBBox() 

898 else: 

899 camBbox = lsst.geom.Box2D() 

900 for detName in detectorNameList: 

901 for corner in camera[detName].getCorners(FOCAL_PLANE): 

902 camBbox.include(corner) 

903 

904 pixelSize_o = camera[next(camera.getNameIter())].getPixelSize() 

905 camBbox = getCameraImageBBox(camBbox, pixelSize_o, bufferSize*binSize) 

906 origin = camBbox.getMin() 

907 

908 camIm = imageFactory(int(math.ceil(camBbox.getDimensions().getX()/binSize)), 

909 int(math.ceil(camBbox.getDimensions().getY()/binSize))) 

910 camIm[:] = imageSource.background 

911 

912 assert imageSource.isTrimmed, "isTrimmed is False isn't supported by getCcdInCamBBoxList" 

913 

914 boxList = getCcdInCamBBoxList(ccdList, binSize, pixelSize_o, origin) 

915 for det, bbox in zip(ccdList, boxList): 

916 im = imageSource.getCcdImage(det, imageFactory, binSize)[0] 

917 if im is None: 

918 continue 

919 

920 imView = camIm.Factory(camIm, bbox, afwImage.LOCAL) 

921 try: 

922 imView[:] = im 

923 except pexExceptions.LengthError as e: 

924 log.error("Unable to fit image for detector \"%s\" into image of camera: %s", 

925 det.getName(), e) 

926 

927 return camIm 

928 

929 

930def showCamera(camera, imageSource=FakeImageDataSource(), imageFactory=afwImage.ImageF, 

931 detectorNameList=None, binSize=10, bufferSize=10, overlay=True, title="", 

932 showWcs=None, ctype=afwDisplay.GREEN, textSize=1.25, originAtCenter=True, display=None, 

933 **kwargs): 

934 """Show a Camera on display, with the specified display. 

935 

936 The rotation of the sensors is snapped to the nearest multiple of 90 deg. 

937 Also note that the pixel size is constant over the image array. The lower 

938 left corner (LLC) of each sensor amp is snapped to the LLC of the pixel 

939 containing the LLC of the image. 

940 

941 Parameters 

942 ---------- 

943 camera : `lsst.afw.cameraGeom.Camera` 

944 Camera object to use to make the image. 

945 imageSource : `FakeImageDataSource` or `None` 

946 Source to get ccd images. Must have a ``getCcdImage()`` method. 

947 imageFactory : `lsst.afw.image.Image` 

948 Type of image to make 

949 detectorNameList : `list` [`str`] or `None` 

950 List of detector names from `camera` to use in building the image. 

951 Use all Detectors if `None`. 

952 binSize : `int` 

953 Bin the image by this factor in both dimensions. 

954 bufferSize : `int` 

955 Size of border in binned pixels to make around the camera image. 

956 overlay : `bool` 

957 Overlay Detector IDs and boundaries? 

958 title : `str` 

959 Title to use in display. 

960 showWcs : `bool` 

961 Include a WCS in the display? 

962 ctype : `lsst.afw.display.COLOR` or `str` 

963 Color to use when drawing Detector boundaries. 

964 textSize : `float` 

965 Size of detector labels 

966 originAtCenter : `bool` 

967 Put origin of the camera WCS at the center of the image? 

968 If `False`, the origin will be at the lower left. 

969 display : `lsst.afw.display` 

970 Image display on which to display. 

971 **kwargs : 

972 All remaining keyword arguments are passed to makeImageFromCamera 

973 

974 Returns 

975 ------- 

976 image : `lsst.afw.image.Image` 

977 The mosaic image. 

978 """ 

979 display = _getDisplayFromDisplayOrFrame(display) 

980 

981 if binSize < 1: 

982 binSize = 1 

983 cameraImage = makeImageFromCamera(camera, detectorNameList=detectorNameList, bufferSize=bufferSize, 

984 imageSource=imageSource, imageFactory=imageFactory, binSize=binSize, 

985 **kwargs) 

986 

987 if detectorNameList is None: 

988 ccdList = [camera[name] for name in camera.getNameIter()] 

989 else: 

990 ccdList = [camera[name] for name in detectorNameList] 

991 

992 if detectorNameList is None: 

993 camBbox = camera.getFpBBox() 

994 else: 

995 camBbox = lsst.geom.Box2D() 

996 for detName in detectorNameList: 

997 for corner in camera[detName].getCorners(FOCAL_PLANE): 

998 camBbox.include(corner) 

999 pixelSize = ccdList[0].getPixelSize() 

1000 

1001 if showWcs: 

1002 if originAtCenter: 

1003 wcsReferencePixel = lsst.geom.Box2D( 

1004 cameraImage.getBBox()).getCenter() 

1005 else: 

1006 wcsReferencePixel = lsst.geom.Point2I(0, 0) 

1007 wcs = makeFocalPlaneWcs(pixelSize*binSize, wcsReferencePixel) 

1008 else: 

1009 wcs = None 

1010 

1011 if display: 

1012 if title == "": 

1013 title = camera.getName() 

1014 display.mtv(cameraImage, title=title, wcs=wcs) 

1015 

1016 if overlay: 

1017 with display.Buffering(): 

1018 camBbox = getCameraImageBBox( 

1019 camBbox, pixelSize, bufferSize*binSize) 

1020 bboxList = getCcdInCamBBoxList( 

1021 ccdList, binSize, pixelSize, camBbox.getMin()) 

1022 for bbox, ccd in zip(bboxList, ccdList): 

1023 nQuarter = ccd.getOrientation().getNQuarter() 

1024 # borderWidth to 0.5 to align with the outside edge of the 

1025 # pixel 

1026 displayUtils.drawBBox( 

1027 bbox, borderWidth=0.5, ctype=ctype, display=display) 

1028 dims = bbox.getDimensions() 

1029 display.dot(ccd.getName(), bbox.getMinX() + dims.getX()/2, bbox.getMinY() + dims.getY()/2, 

1030 ctype=ctype, size=textSize, textAngle=nQuarter*90) 

1031 

1032 return cameraImage 

1033 

1034 

1035def makeFocalPlaneWcs(pixelSize, referencePixel): 

1036 """Make a WCS for the focal plane geometry 

1037 (i.e. one that returns positions in "mm") 

1038 

1039 Parameters 

1040 ---------- 

1041 pixelSize : `float` 

1042 Size of the image pixels in physical units 

1043 referencePixel : `lsst.geom.Point2D` 

1044 Pixel for origin of WCS 

1045 

1046 Returns 

1047 ------- 

1048 `lsst.afw.geom.Wcs` 

1049 Wcs object for mapping between pixels and focal plane. 

1050 """ 

1051 md = dafBase.PropertySet() 

1052 if referencePixel is None: 

1053 referencePixel = lsst.geom.PointD(0, 0) 

1054 for i in range(2): 

1055 md.set("CRPIX%d"%(i + 1), referencePixel[i]) 

1056 md.set("CRVAL%d"%(i + 1), 0.) 

1057 md.set("CDELT1", pixelSize[0]) 

1058 md.set("CDELT2", pixelSize[1]) 

1059 md.set("CTYPE1", "CAMERA_X") 

1060 md.set("CTYPE2", "CAMERA_Y") 

1061 md.set("CUNIT1", "mm") 

1062 md.set("CUNIT2", "mm") 

1063 

1064 return afwGeom.makeSkyWcs(md) 

1065 

1066 

1067def findAmp(ccd, pixelPosition): 

1068 """Find the Amp with the specified pixel position within the composite 

1069 

1070 Parameters 

1071 ---------- 

1072 ccd : `lsst.afw.cameraGeom.Detector` 

1073 Detector to look in. 

1074 pixelPosition : `lsst.geom.Point2I` 

1075 The pixel position to find the amp for. 

1076 

1077 Returns 

1078 ------- 

1079 `lsst.afw.table.AmpInfoCatalog` 

1080 Amp record in which ``pixelPosition`` falls or `None` if no Amp found. 

1081 """ 

1082 for amp in ccd: 

1083 if amp.getBBox().contains(pixelPosition): 

1084 return amp 

1085 

1086 return None