Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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 

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

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

56 

57 Parameters 

58 ---------- 

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

60 The WCS object to start from. 

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

62 Amp object to use 

63 isTrimmed : `bool` 

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

65 

66 Returns 

67 ------- 

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

69 The modified WCS. 

70 """ 

71 if isTrimmed: 

72 ampBox = amp.getRawDataBBox() 

73 else: 

74 ampBox = amp.getRawBBox() 

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

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

77 # Shift WCS for trimming 

78 if isTrimmed: 

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

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

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

82 offset = amp.getRawXYOffset() 

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

84 

85 

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

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

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

89 the field of view. 

90 

91 Parameters 

92 ---------- 

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

94 A camera object 

95 fieldSizeDeg_x : `float` 

96 Amount of the field to sample in x in degrees 

97 fieldSizeDeg_y : `float` or `None` 

98 Amount of the field to sample in y in degrees 

99 dx : `float` 

100 Spacing of sample points in x in degrees 

101 dy : `float` 

102 Spacing of sample points in y in degrees 

103 figsize : `tuple` containing two `float` 

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

105 useIds : `bool` 

106 Label detectors by name, not id? 

107 showFig : `bool` 

108 Display the figure on the screen? 

109 savePath : `str` or `None` 

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

111 """ 

112 try: 

113 from matplotlib.patches import Polygon 

114 from matplotlib.collections import PatchCollection 

115 import matplotlib.pyplot as plt 

116 except ImportError: 

117 raise ImportError( 

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

119 

120 if fieldSizeDeg_x: 

121 if fieldSizeDeg_y is None: 

122 fieldSizeDeg_y = fieldSizeDeg_x 

123 

124 field_gridx, field_gridy = numpy.meshgrid( 

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

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

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

128 else: 

129 field_gridx, field_gridy = [], [] 

130 

131 xs = [] 

132 ys = [] 

133 pcolors = [] 

134 

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

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

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

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

139 for posFocalPlane in posFocalPlaneList: 

140 xs.append(posFocalPlane.getX()) 

141 ys.append(posFocalPlane.getY()) 

142 dets = camera.findDetectors(posFocalPlane, FOCAL_PLANE) 

143 if len(dets) > 0: 

144 pcolors.append('w') 

145 else: 

146 pcolors.append('k') 

147 

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

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

150 

151 patches = [] 

152 colors = [] 

153 plt.figure(figsize=figsize) 

154 ax = plt.gca() 

155 xvals = [] 

156 yvals = [] 

157 for det in camera: 

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

159 for corner in corners: 

160 xvals.append(corner[0]) 

161 yvals.append(corner[1]) 

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

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

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

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

166 horizontalalignment='center', size=6) 

167 

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

169 ax.add_collection(patchCollection) 

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

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

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

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

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

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

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

177 if savePath is not None: 

178 plt.savefig(savePath) 

179 if showFig: 

180 plt.show() 

181 

182 

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

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

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

186 

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

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

189 The scaling algorithm is assignable. 

190 

191 Parameters 

192 ---------- 

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

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

195 imValue : `float` or `None` 

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

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

198 Type of image to construct. 

199 markSize : `float` 

200 Size of mark at read corner in pixels. 

201 markValue : `float` 

202 Value of pixels in the read corner mark. 

203 scaleGain : callable 

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

205 

206 Returns 

207 ------- 

208 ampImage : `lsst.afw.image` 

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

210 """ 

211 bbox = amp.getRawBBox() 

212 dbbox = amp.getRawDataBBox() 

213 img = imageFactory(bbox) 

214 if imValue is None: 

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

216 else: 

217 img.set(imValue) 

218 # Set the first pixel read to a different value 

219 markbbox = lsst.geom.Box2I() 

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

221 markbbox.include(dbbox.getMin()) 

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

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

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

225 markbbox.include(cornerPoint) 

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

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

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

229 markbbox.include(cornerPoint) 

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

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

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

233 markbbox.include(cornerPoint) 

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

235 else: 

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

237 mimg = imageFactory(img, markbbox) 

238 mimg.set(markValue) 

239 return img 

240 

241 

242def calcRawCcdBBox(ccd): 

243 """Calculate the raw ccd bounding box. 

244 

245 Parameters 

246 ---------- 

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

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

249 

250 Returns 

251 ------- 

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

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

254 information to calculate raw BBox. 

255 """ 

256 bbox = lsst.geom.Box2I() 

257 for amp in ccd: 

258 tbbox = amp.getRawBBox() 

259 tbbox.shift(amp.getRawXYOffset()) 

260 bbox.include(tbbox) 

261 return bbox 

262 

263 

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

265 binSize=1): 

266 """Make an Image of a CCD. 

267 

268 Parameters 

269 ---------- 

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

271 Detector to use in making the image. 

272 isTrimmed : `bool` 

273 Assemble a trimmed Detector image. 

274 showAmpGain : `bool` 

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

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

277 Image type to generate. 

278 rcMarkSize : `float` 

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

280 binSize : `int` 

281 Bin the image by this factor in both dimensions. 

282 

283 Returns 

284 ------- 

285 image : `lsst.afw.image.Image` 

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

287 """ 

288 ampImages = [] 

289 index = 0 

290 if isTrimmed: 

291 bbox = ccd.getBBox() 

292 else: 

293 bbox = calcRawCcdBBox(ccd) 

294 for amp in ccd: 

295 if showAmpGain: 

296 ampImages.append(makeImageFromAmp( 

297 amp, imageFactory=imageFactory, markSize=rcMarkSize)) 

298 else: 

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

300 imageFactory=imageFactory, markSize=rcMarkSize)) 

301 index += 1 

302 

303 if len(ampImages) > 0: 

304 ccdImage = imageFactory(bbox) 

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

306 if isTrimmed: 

307 assembleAmplifierImage(ccdImage, ampImage, amp) 

308 else: 

309 assembleAmplifierRawImage(ccdImage, ampImage, amp) 

310 else: 

311 if not isTrimmed: 

312 raise RuntimeError( 

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

314 ccdImage = imageFactory(ccd.getBBox()) 

315 ccdImage = afwMath.binImage(ccdImage, binSize) 

316 return ccdImage 

317 

318 

319class FakeImageDataSource: 

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

321 

322 Parameters 

323 ---------- 

324 isTrimmed : `bool` 

325 Should amps be trimmed? 

326 verbose : `bool` 

327 Be chatty? 

328 background : `float` 

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

330 showAmpGain : `bool` 

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

332 markSize : `float` 

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

334 markValue : `float` 

335 Value to assign the read corner mark. 

336 ampImValue : `float` or `None` 

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

338 scaleGain : callable 

339 Function to scale the gain by. 

340 """ 

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

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

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

344 self.isTrimmed = isTrimmed 

345 self.verbose = verbose 

346 self.background = background 

347 self.showAmpGain = showAmpGain 

348 self.markSize = markSize 

349 self.markValue = markValue 

350 self.ampImValue = ampImValue 

351 self.scaleGain = scaleGain 

352 

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

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

355 

356 Parameters 

357 ---------- 

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

359 Detector to use for making the image. 

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

361 Image constructor for making the image. 

362 binSize : `int` 

363 Bin the image by this factor in both dimensions. 

364 

365 Returns 

366 ------- 

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

368 The constructed image. 

369 """ 

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

371 imageFactory=imageFactory, binSize=binSize) 

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

373 

374 def getAmpImage(self, amp, imageFactory): 

375 """Return an amp segment image. 

376 

377 Parameters 

378 ---------- 

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

380 AmpInfoTable for this amp. 

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

382 Image constructor for making the image. 

383 

384 Returns 

385 ------- 

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

387 The constructed image. 

388 """ 

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

390 markSize=self.markSize, markValue=self.markValue, 

391 scaleGain=self.scaleGain) 

392 if self.isTrimmed: 

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

394 return ampImage 

395 

396 

397class ButlerImage(FakeImageDataSource): 

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

399 

400 Parameters 

401 ---------- 

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

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

404 type : `str` 

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

406 isTrimmed : `bool` 

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

408 verbose : `bool` 

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

410 background : `float` 

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

412 callback : callable 

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

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

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

416 *args : `list` 

417 Passed to the butler. 

418 **kwargs : `dict` 

419 Passed to the butler. 

420 

421 Notes 

422 ----- 

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

424 

425 def callback(im, ccd, imageSource): 

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

427 """ 

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

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

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

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

432 self.isTrimmed = isTrimmed 

433 self.type = type 

434 self.butler = butler 

435 self.kwargs = kwargs 

436 self.isRaw = False 

437 self.background = background 

438 self.verbose = verbose 

439 self.callback = callback 

440 

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

442 if binSize > 1: 

443 im = afwMath.binImage(im, binSize) 

444 

445 if allowRotate: 

446 im = afwMath.rotateImageBy90( 

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

448 

449 return im 

450 

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

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

453 

454 log = lsst.log.Log.getLogger("afw.cameraGeom.utils.ButlerImage") 

455 

456 if self.isTrimmed: 

457 bbox = ccd.getBBox() 

458 else: 

459 bbox = calcRawCcdBBox(ccd) 

460 

461 im = None 

462 if self.butler is not None: 

463 err = None 

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

465 try: 

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

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

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

469 break 

470 except Exception as e: # try a different dataId 

471 if err is None: 

472 err = e 

473 continue 

474 else: 

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

476 break 

477 

478 if im: 

479 if asMaskedImage: 

480 im = im.getMaskedImage() 

481 else: 

482 im = im.getMaskedImage().getImage() 

483 else: 

484 if self.verbose: 

485 # Lost by jupyterlab. 

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

487 

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

489 

490 if im is None: 

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

492 

493 if self.type == "raw": 

494 if hasattr(im, 'convertF'): 

495 im = im.convertF() 

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

497 self.callback = rawCallback 

498 

499 allowRotate = True 

500 if self.callback: 

501 try: 

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

503 except Exception as e: 

504 if self.verbose: 

505 log.error(f"callback failed: {e}") 

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

507 else: 

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

509 

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

511 

512 

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

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

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

516 a raw image. 

517 

518 Parameters 

519 ---------- 

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

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

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

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

524 imageSource : `FakeImageDataSource` or `None` 

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

526 correctGain : `bool` 

527 Correct each amplifier for its gain? 

528 subtractBias : `bool` 

529 Subtract the bias from each amplifier? 

530 convertToFloat : `bool` 

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

532 obeyNQuarter : `bool` 

533 Obey nQuarter from the Detector (default: True) 

534 

535 Returns 

536 ------- 

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

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

539 

540 Notes 

541 ----- 

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

543 """ 

544 if ccd is None: 

545 ccd = im.getDetector() 

546 if hasattr(im, "getMaskedImage"): 

547 im = im.getMaskedImage() 

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

549 im = im.convertF() 

550 

551 isTrimmed = imageSource.isTrimmed 

552 if isTrimmed: 

553 bbox = ccd.getBBox() 

554 else: 

555 bbox = calcRawCcdBBox(ccd) 

556 

557 ampImages = [] 

558 for a in ccd: 

559 if isTrimmed: 

560 data = im[a.getRawDataBBox()] 

561 else: 

562 data = im 

563 

564 if subtractBias: 

565 bias = im[a.getRawHorizontalOverscanBBox()] 

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

567 if correctGain: 

568 data *= a.getGain() 

569 

570 ampImages.append(data) 

571 

572 ccdImage = im.Factory(bbox) 

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

574 if isTrimmed: 

575 assembleAmplifierImage(ccdImage, ampImage, amp) 

576 else: 

577 assembleAmplifierRawImage(ccdImage, ampImage, amp) 

578 

579 if obeyNQuarter: 

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

581 ccdImage = afwMath.rotateImageBy90(ccdImage, nQuarter) 

582 

583 return ccdImage 

584 

585 

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

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

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

589 

590 Parameters 

591 ---------- 

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

593 Detector to iterate for the amp bounding boxes. 

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

595 Bounding box of the un-trimmed Detector. 

596 nQuarter : `int` 

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

598 isTrimmed : `bool` 

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

600 ccdOrigin : `tuple` of `float` 

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

602 display : `lsst.afw.display.Display` 

603 Image display to display on. 

604 binSize : `int` 

605 Bin the image by this factor in both dimensions. 

606 

607 Notes 

608 ----- 

609 The colours are: 

610 - Entire detector GREEN 

611 - All data for amp GREEN 

612 - HorizontalPrescan YELLOW 

613 - HorizontalOverscan RED 

614 - Data BLUE 

615 - VerticalOverscan MAGENTA 

616 - VerticalOverscan MAGENTA 

617 """ 

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

619 raise RuntimeError("Please specify a display") 

620 

621 if untrimmedCcdBbox is None: 

622 if isTrimmed: 

623 untrimmedCcdBbox = ccd.getBBox() 

624 else: 

625 untrimmedCcdBbox = lsst.geom.Box2I() 

626 for a in ccd.getAmplifiers(): 

627 bbox = a.getRawBBox() 

628 untrimmedCcdBbox.include(bbox) 

629 

630 with display.Buffering(): 

631 ccdDim = untrimmedCcdBbox.getDimensions() 

632 ccdBbox = rotateBBoxBy90(untrimmedCcdBbox, nQuarter, ccdDim) 

633 for amp in ccd: 

634 if isTrimmed: 

635 ampbbox = amp.getBBox() 

636 else: 

637 ampbbox = amp.getRawBBox() 

638 if nQuarter != 0: 

639 ampbbox = rotateBBoxBy90(ampbbox, nQuarter, ccdDim) 

640 

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

642 display=display, bin=binSize) 

643 

644 if not isTrimmed: 

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

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

647 (amp.getRawVerticalOverscanBBox(), 

648 afwDisplay.MAGENTA), 

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

650 if nQuarter != 0: 

651 bbox = rotateBBoxBy90(bbox, nQuarter, ccdDim) 

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

653 display=display, bin=binSize) 

654 # Label each Amp 

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

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

657 # 

658 # Rotate the amp labels too 

659 # 

660 if nQuarter == 0: 

661 c, s = 1, 0 

662 elif nQuarter == 1: 

663 c, s = 0, -1 

664 elif nQuarter == 2: 

665 c, s = -1, 0 

666 elif nQuarter == 3: 

667 c, s = 0, 1 

668 c, s = 1, 0 

669 ccdHeight = ccdBbox.getHeight() 

670 ccdWidth = ccdBbox.getWidth() 

671 xc -= 0.5*ccdHeight 

672 yc -= 0.5*ccdWidth 

673 

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

675 

676 if ccdOrigin: 

677 xc += ccdOrigin[0] 

678 yc += ccdOrigin[1] 

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

680 yc/binSize, textAngle=nQuarter*90) 

681 

682 displayUtils.drawBBox(ccdBbox, origin=ccdOrigin, 

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

684 

685 

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

687 imageFactory=afwImage.ImageU): 

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

689 

690 Parameters 

691 ---------- 

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

693 Amp record to use in display. 

694 imageSource : `FakeImageDataSource` or `None` 

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

696 display : `lsst.afw.display.Display` 

697 Image display to use. 

698 overlay : `bool` 

699 Overlay bounding boxes? 

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

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

702 """ 

703 if not display: 

704 display = _getDisplayFromDisplayOrFrame(display) 

705 

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

707 ampImSize = ampImage.getDimensions() 

708 title = amp.getName() 

709 display.mtv(ampImage, title=title) 

710 if overlay: 

711 with display.Buffering(): 

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

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

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

715 bboxes.append( 

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

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

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

719 0.49, afwDisplay.YELLOW)) 

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

721 0.49, afwDisplay.MAGENTA)) 

722 else: 

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

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

725 

726 for bbox, borderWidth, ctype in bboxes: 

727 if bbox.isEmpty(): 

728 continue 

729 bbox = lsst.geom.Box2I(bbox) 

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

731 displayUtils.drawBBox( 

732 bbox, borderWidth=borderWidth, ctype=ctype, display=display) 

733 

734 

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

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

737 """Show a CCD on display. 

738 

739 Parameters 

740 ---------- 

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

742 Detector to use in display. 

743 imageSource : `FakeImageDataSource` or `None` 

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

745 display : `lsst.afw.display.Display` 

746 image display to use. 

747 overlay : `bool` 

748 Show amp bounding boxes on the displayed image? 

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

750 The image factory to use in generating the images. 

751 binSize : `int` 

752 Bin the image by this factor in both dimensions. 

753 inCameraCoords : `bool` 

754 Show the Detector in camera coordinates? 

755 """ 

756 display = _getDisplayFromDisplayOrFrame(display) 

757 

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

759 nQuarter = 0 

760 ccdImage, ccd = imageSource.getCcdImage( 

761 ccd, imageFactory=imageFactory, binSize=binSize) 

762 

763 ccdBbox = ccdImage.getBBox() 

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

765 isTrimmed = True 

766 else: 

767 isTrimmed = False 

768 

769 if inCameraCoords: 

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

771 ccdImage = afwMath.rotateImageBy90(ccdImage, nQuarter) 

772 title = ccd.getName() 

773 if isTrimmed: 

774 title += "(trimmed)" 

775 

776 if display: 

777 display.mtv(ccdImage, title=title) 

778 

779 if overlay: 

780 overlayCcdBoxes(ccd, ccdBbox, nQuarter, isTrimmed, 

781 ccdOrigin, display, binSize) 

782 

783 return ccdImage 

784 

785 

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

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

788 

789 Parameters 

790 ---------- 

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

792 List of Detector. 

793 binSize : `int` 

794 Bin the image by this factor in both dimensions. 

795 pixelSize_o : `float` 

796 Size of the pixel in mm. 

797 origin : `int` 

798 Origin of the camera pixel grid in pixels. 

799 

800 Returns 

801 ------- 

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

803 A list of bounding boxes in camera pixel coordinates. 

804 """ 

805 boxList = [] 

806 for ccd in ccdList: 

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

808 raise RuntimeError( 

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

810 

811 dbbox = lsst.geom.Box2D() 

812 for corner in ccd.getCorners(FOCAL_PLANE): 

813 dbbox.include(corner) 

814 llc = dbbox.getMin() 

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

816 cbbox = ccd.getBBox() 

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

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

819 bbox = lsst.geom.Box2I( 

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

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

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

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

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

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

826 boxList.append(bbox) 

827 return boxList 

828 

829 

830def getCameraImageBBox(camBbox, pixelSize, bufferSize): 

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

832 

833 Parameters 

834 ---------- 

835 camBbox : `lsst.geom.Box2D` 

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

837 pixelSize : `float` 

838 Size of a detector pixel in mm. 

839 bufferSize : `int` 

840 Buffer around edge of image in pixels. 

841 

842 Returns 

843 ------- 

844 box : `lsst.geom.Box2I` 

845 The resulting bounding box. 

846 """ 

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

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

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

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

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

852 retBox.grow(bufferSize) 

853 return retBox 

854 

855 

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

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

858 """Make an Image of a Camera. 

859 

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

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

862 original bboxes. 

863 

864 Parameters 

865 ---------- 

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

867 Camera object to use to make the image. 

868 detectorNameList : `list` [`str`] 

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

870 Use all Detectors if `None`. 

871 background : `float` 

872 Value to use where there is no Detector. 

873 bufferSize : `int` 

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

875 imageSource : `FakeImageDataSource` or `None` 

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

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

878 Type of image to build. 

879 binSize : `int` 

880 Bin the image by this factor in both dimensions. 

881 

882 Returns 

883 ------- 

884 image : `lsst.afw.image.Image` 

885 Image of the entire camera. 

886 """ 

887 log = lsst.log.Log.getLogger("afw.cameraGeom.utils.makeImageFromCamera") 

888 

889 if detectorNameList is None: 

890 ccdList = camera 

891 else: 

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

893 

894 if detectorNameList is None: 

895 camBbox = camera.getFpBBox() 

896 else: 

897 camBbox = lsst.geom.Box2D() 

898 for detName in detectorNameList: 

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

900 camBbox.include(corner) 

901 

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

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

904 origin = camBbox.getMin() 

905 

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

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

908 camIm[:] = imageSource.background 

909 

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

911 

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

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

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

915 if im is None: 

916 continue 

917 

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

919 try: 

920 imView[:] = im 

921 except pexExceptions.LengthError as e: 

922 log.error(f"Unable to fit image for detector \"{det.getName()}\" into image of camera: {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