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

440 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-28 03:10 -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.butler.Butler` or `None` 

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

406 the instrument was specified during butler construction or is included 

407 in the ``kwargs`` parameter. 

408 type : `str` 

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

410 isTrimmed : `bool` 

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

412 verbose : `bool` 

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

414 background : `float` 

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

416 callback : callable 

417 A function called with (image, detector, butler) for every image, which 

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

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

420 *args : `list` 

421 Passed to the base class constructor. 

422 **kwargs : `dict` 

423 Passed to the butler. 

424 

425 Notes 

426 ----- 

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

428 

429 def callback(im, ccd, imageSource): 

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

431 """ 

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

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

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

435 super().__init__(*args) 

436 self.isTrimmed = isTrimmed 

437 self.type = type 

438 self.butler = butler 

439 self.kwargs = kwargs 

440 self.isRaw = False 

441 self.background = background 

442 self.verbose = verbose 

443 self.callback = callback 

444 

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

446 if binSize > 1: 

447 im = afwMath.binImage(im, binSize) 

448 

449 if allowRotate: 

450 im = afwMath.rotateImageBy90( 

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

452 

453 return im 

454 

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

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

457 

458 log = _LOG.getChild("ButlerImage") 

459 

460 if self.isTrimmed: 

461 bbox = ccd.getBBox() 

462 else: 

463 bbox = calcRawCcdBBox(ccd) 

464 

465 im = None 

466 if self.butler is not None: 

467 err = None 

468 try: 

469 im = self.butler.get(self.type, detector=ccd.getId(), **self.kwargs) 

470 except FitsError as e: 

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

472 except Exception as e: # try a different dataId 

473 err = e 

474 else: 

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

476 

477 if im: 

478 if asMaskedImage: 

479 im = im.getMaskedImage() 

480 else: 

481 im = im.getMaskedImage().getImage() 

482 else: 

483 if self.verbose: 

484 # Lost by jupyterlab. 

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

486 

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

488 

489 if im is None: 

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

491 

492 if self.type == "raw": 

493 if hasattr(im, 'convertF'): 

494 im = im.convertF() 

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

496 self.callback = rawCallback 

497 

498 allowRotate = True 

499 if self.callback: 

500 try: 

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

502 except Exception as e: 

503 if self.verbose: 

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

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

506 else: 

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

508 

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

510 

511 

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

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

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

515 a raw image. 

516 

517 Parameters 

518 ---------- 

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

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

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

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

523 imageSource : `FakeImageDataSource` or `None` 

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

525 correctGain : `bool` 

526 Correct each amplifier for its gain? 

527 subtractBias : `bool` 

528 Subtract the bias from each amplifier? 

529 convertToFloat : `bool` 

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

531 obeyNQuarter : `bool` 

532 Obey nQuarter from the Detector (default: True) 

533 

534 Returns 

535 ------- 

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

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

538 

539 Notes 

540 ----- 

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

542 """ 

543 if ccd is None: 

544 ccd = im.getDetector() 

545 if hasattr(im, "getMaskedImage"): 

546 im = im.getMaskedImage() 

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

548 im = im.convertF() 

549 

550 isTrimmed = imageSource.isTrimmed 

551 if isTrimmed: 

552 bbox = ccd.getBBox() 

553 else: 

554 bbox = calcRawCcdBBox(ccd) 

555 

556 ampImages = [] 

557 for a in ccd: 

558 if isTrimmed: 

559 data = im[a.getRawDataBBox()] 

560 else: 

561 data = im 

562 

563 if subtractBias: 

564 bias = im[a.getRawHorizontalOverscanBBox()] 

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

566 if correctGain: 

567 data *= a.getGain() 

568 

569 ampImages.append(data) 

570 

571 ccdImage = im.Factory(bbox) 

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

573 if isTrimmed: 

574 assembleAmplifierImage(ccdImage, ampImage, amp) 

575 else: 

576 assembleAmplifierRawImage(ccdImage, ampImage, amp) 

577 

578 if obeyNQuarter: 

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

580 ccdImage = afwMath.rotateImageBy90(ccdImage, nQuarter) 

581 

582 return ccdImage 

583 

584 

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

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

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

588 

589 Parameters 

590 ---------- 

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

592 Detector to iterate for the amp bounding boxes. 

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

594 Bounding box of the un-trimmed Detector. 

595 nQuarter : `int` 

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

597 isTrimmed : `bool` 

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

599 ccdOrigin : `tuple` of `float` 

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

601 display : `lsst.afw.display.Display` 

602 Image display to display on. 

603 binSize : `int` 

604 Bin the image by this factor in both dimensions. 

605 

606 Notes 

607 ----- 

608 The colours are: 

609 - Entire detector GREEN 

610 - All data for amp GREEN 

611 - HorizontalPrescan YELLOW 

612 - HorizontalOverscan RED 

613 - Data BLUE 

614 - VerticalOverscan MAGENTA 

615 - VerticalOverscan MAGENTA 

616 """ 

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

618 raise RuntimeError("Please specify a display") 

619 

620 if untrimmedCcdBbox is None: 

621 if isTrimmed: 

622 untrimmedCcdBbox = ccd.getBBox() 

623 else: 

624 untrimmedCcdBbox = lsst.geom.Box2I() 

625 for a in ccd.getAmplifiers(): 

626 bbox = a.getRawBBox() 

627 untrimmedCcdBbox.include(bbox) 

628 

629 with display.Buffering(): 

630 ccdDim = untrimmedCcdBbox.getDimensions() 

631 ccdBbox = rotateBBoxBy90(untrimmedCcdBbox, nQuarter, ccdDim) 

632 for amp in ccd: 

633 if isTrimmed: 

634 ampbbox = amp.getBBox() 

635 else: 

636 ampbbox = amp.getRawBBox() 

637 if nQuarter != 0: 

638 ampbbox = rotateBBoxBy90(ampbbox, nQuarter, ccdDim) 

639 

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

641 display=display, bin=binSize) 

642 

643 if not isTrimmed: 

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

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

646 (amp.getRawVerticalOverscanBBox(), 

647 afwDisplay.MAGENTA), 

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

649 if nQuarter != 0: 

650 bbox = rotateBBoxBy90(bbox, nQuarter, ccdDim) 

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

652 display=display, bin=binSize) 

653 # Label each Amp 

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

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

656 # 

657 # Rotate the amp labels too 

658 # 

659 if nQuarter == 0: 

660 c, s = 1, 0 

661 elif nQuarter == 1: 

662 c, s = 0, -1 

663 elif nQuarter == 2: 

664 c, s = -1, 0 

665 elif nQuarter == 3: 

666 c, s = 0, 1 

667 c, s = 1, 0 

668 ccdHeight = ccdBbox.getHeight() 

669 ccdWidth = ccdBbox.getWidth() 

670 xc -= 0.5*ccdHeight 

671 yc -= 0.5*ccdWidth 

672 

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

674 

675 if ccdOrigin: 

676 xc += ccdOrigin[0] 

677 yc += ccdOrigin[1] 

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

679 yc/binSize, textAngle=nQuarter*90) 

680 

681 displayUtils.drawBBox(ccdBbox, origin=ccdOrigin, 

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

683 

684 

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

686 imageFactory=afwImage.ImageU): 

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

688 

689 Parameters 

690 ---------- 

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

692 Amp record to use in display. 

693 imageSource : `FakeImageDataSource` or `None` 

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

695 display : `lsst.afw.display.Display` 

696 Image display to use. 

697 overlay : `bool` 

698 Overlay bounding boxes? 

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

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

701 """ 

702 if not display: 

703 display = _getDisplayFromDisplayOrFrame(display) 

704 

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

706 ampImSize = ampImage.getDimensions() 

707 title = amp.getName() 

708 display.mtv(ampImage, title=title) 

709 if overlay: 

710 with display.Buffering(): 

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

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

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

714 bboxes.append( 

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

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

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

718 0.49, afwDisplay.YELLOW)) 

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

720 0.49, afwDisplay.MAGENTA)) 

721 else: 

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

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

724 

725 for bbox, borderWidth, ctype in bboxes: 

726 if bbox.isEmpty(): 

727 continue 

728 bbox = lsst.geom.Box2I(bbox) 

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

730 displayUtils.drawBBox( 

731 bbox, borderWidth=borderWidth, ctype=ctype, display=display) 

732 

733 

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

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

736 """Show a CCD on display. 

737 

738 Parameters 

739 ---------- 

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

741 Detector to use in display. 

742 imageSource : `FakeImageDataSource` or `None` 

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

744 display : `lsst.afw.display.Display` 

745 image display to use. 

746 overlay : `bool` 

747 Show amp bounding boxes on the displayed image? 

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

749 The image factory to use in generating the images. 

750 binSize : `int` 

751 Bin the image by this factor in both dimensions. 

752 inCameraCoords : `bool` 

753 Show the Detector in camera coordinates? 

754 """ 

755 display = _getDisplayFromDisplayOrFrame(display) 

756 

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

758 nQuarter = 0 

759 ccdImage, ccd = imageSource.getCcdImage( 

760 ccd, imageFactory=imageFactory, binSize=binSize) 

761 

762 ccdBbox = ccdImage.getBBox() 

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

764 isTrimmed = True 

765 else: 

766 isTrimmed = False 

767 

768 if inCameraCoords: 

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

770 ccdImage = afwMath.rotateImageBy90(ccdImage, nQuarter) 

771 title = ccd.getName() 

772 if isTrimmed: 

773 title += "(trimmed)" 

774 

775 if display: 

776 display.mtv(ccdImage, title=title) 

777 

778 if overlay: 

779 overlayCcdBoxes(ccd, ccdBbox, nQuarter, isTrimmed, 

780 ccdOrigin, display, binSize) 

781 

782 return ccdImage 

783 

784 

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

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

787 

788 Parameters 

789 ---------- 

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

791 List of Detector. 

792 binSize : `int` 

793 Bin the image by this factor in both dimensions. 

794 pixelSize_o : `float` 

795 Size of the pixel in mm. 

796 origin : `int` 

797 Origin of the camera pixel grid in pixels. 

798 

799 Returns 

800 ------- 

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

802 A list of bounding boxes in camera pixel coordinates. 

803 """ 

804 boxList = [] 

805 for ccd in ccdList: 

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

807 raise RuntimeError( 

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

809 

810 dbbox = lsst.geom.Box2D() 

811 for corner in ccd.getCorners(FOCAL_PLANE): 

812 dbbox.include(corner) 

813 llc = dbbox.getMin() 

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

815 cbbox = ccd.getBBox() 

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

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

818 bbox = lsst.geom.Box2I( 

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

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

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

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

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

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

825 boxList.append(bbox) 

826 return boxList 

827 

828 

829def getCameraImageBBox(camBbox, pixelSize, bufferSize): 

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

831 

832 Parameters 

833 ---------- 

834 camBbox : `lsst.geom.Box2D` 

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

836 pixelSize : `float` 

837 Size of a detector pixel in mm. 

838 bufferSize : `int` 

839 Buffer around edge of image in pixels. 

840 

841 Returns 

842 ------- 

843 box : `lsst.geom.Box2I` 

844 The resulting bounding box. 

845 """ 

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

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

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

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

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

851 retBox.grow(bufferSize) 

852 return retBox 

853 

854 

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

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

857 """Make an Image of a Camera. 

858 

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

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

861 original bboxes. 

862 

863 Parameters 

864 ---------- 

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

866 Camera object to use to make the image. 

867 detectorNameList : `list` [`str`] 

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

869 Use all Detectors if `None`. 

870 background : `float` 

871 Value to use where there is no Detector. 

872 bufferSize : `int` 

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

874 imageSource : `FakeImageDataSource` or `None` 

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

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

877 Type of image to build. 

878 binSize : `int` 

879 Bin the image by this factor in both dimensions. 

880 

881 Returns 

882 ------- 

883 image : `lsst.afw.image.Image` 

884 Image of the entire camera. 

885 """ 

886 log = _LOG.getChild("makeImageFromCamera") 

887 

888 if detectorNameList is None: 

889 ccdList = camera 

890 else: 

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

892 

893 if detectorNameList is None: 

894 camBbox = camera.getFpBBox() 

895 else: 

896 camBbox = lsst.geom.Box2D() 

897 for detName in detectorNameList: 

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

899 camBbox.include(corner) 

900 

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

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

903 origin = camBbox.getMin() 

904 

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

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

907 camIm[:] = imageSource.background 

908 

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

910 

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

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

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

914 if im is None: 

915 continue 

916 

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

918 try: 

919 imView[:] = im 

920 except pexExceptions.LengthError as e: 

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

922 det.getName(), e) 

923 

924 return camIm 

925 

926 

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

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

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

930 **kwargs): 

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

932 

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

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

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

936 containing the LLC of the image. 

937 

938 Parameters 

939 ---------- 

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

941 Camera object to use to make the image. 

942 imageSource : `FakeImageDataSource` or `None` 

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

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

945 Type of image to make 

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

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

948 Use all Detectors if `None`. 

949 binSize : `int` 

950 Bin the image by this factor in both dimensions. 

951 bufferSize : `int` 

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

953 overlay : `bool` 

954 Overlay Detector IDs and boundaries? 

955 title : `str` 

956 Title to use in display. 

957 showWcs : `bool` 

958 Include a WCS in the display? 

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

960 Color to use when drawing Detector boundaries. 

961 textSize : `float` 

962 Size of detector labels 

963 originAtCenter : `bool` 

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

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

966 display : `lsst.afw.display` 

967 Image display on which to display. 

968 **kwargs : 

969 All remaining keyword arguments are passed to makeImageFromCamera 

970 

971 Returns 

972 ------- 

973 image : `lsst.afw.image.Image` 

974 The mosaic image. 

975 """ 

976 display = _getDisplayFromDisplayOrFrame(display) 

977 

978 if binSize < 1: 

979 binSize = 1 

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

981 imageSource=imageSource, imageFactory=imageFactory, binSize=binSize, 

982 **kwargs) 

983 

984 if detectorNameList is None: 

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

986 else: 

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

988 

989 if detectorNameList is None: 

990 camBbox = camera.getFpBBox() 

991 else: 

992 camBbox = lsst.geom.Box2D() 

993 for detName in detectorNameList: 

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

995 camBbox.include(corner) 

996 pixelSize = ccdList[0].getPixelSize() 

997 

998 if showWcs: 

999 if originAtCenter: 

1000 wcsReferencePixel = lsst.geom.Box2D( 

1001 cameraImage.getBBox()).getCenter() 

1002 else: 

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

1004 wcs = makeFocalPlaneWcs(pixelSize*binSize, wcsReferencePixel) 

1005 else: 

1006 wcs = None 

1007 

1008 if display: 

1009 if title == "": 

1010 title = camera.getName() 

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

1012 

1013 if overlay: 

1014 with display.Buffering(): 

1015 camBbox = getCameraImageBBox( 

1016 camBbox, pixelSize, bufferSize*binSize) 

1017 bboxList = getCcdInCamBBoxList( 

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

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

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

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

1022 # pixel 

1023 displayUtils.drawBBox( 

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

1025 dims = bbox.getDimensions() 

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

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

1028 

1029 return cameraImage 

1030 

1031 

1032def makeFocalPlaneWcs(pixelSize, referencePixel): 

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

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

1035 

1036 Parameters 

1037 ---------- 

1038 pixelSize : `float` 

1039 Size of the image pixels in physical units 

1040 referencePixel : `lsst.geom.Point2D` 

1041 Pixel for origin of WCS 

1042 

1043 Returns 

1044 ------- 

1045 `lsst.afw.geom.Wcs` 

1046 Wcs object for mapping between pixels and focal plane. 

1047 """ 

1048 md = dafBase.PropertySet() 

1049 if referencePixel is None: 

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

1051 for i in range(2): 

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

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

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

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

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

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

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

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

1060 

1061 return afwGeom.makeSkyWcs(md) 

1062 

1063 

1064def findAmp(ccd, pixelPosition): 

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

1066 

1067 Parameters 

1068 ---------- 

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

1070 Detector to look in. 

1071 pixelPosition : `lsst.geom.Point2I` 

1072 The pixel position to find the amp for. 

1073 

1074 Returns 

1075 ------- 

1076 `lsst.afw.table.AmpInfoCatalog` 

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

1078 """ 

1079 for amp in ccd: 

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

1081 return amp 

1082 

1083 return None