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 .cameraGeomLib 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 not amp.getHasRawInfo(): 

72 raise RuntimeError("Cannot modify wcs without raw amp information") 

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 if not amp.getHasRawInfo(): 

214 raise RuntimeError( 

215 "Can't create a raw amp image without raw amp information") 

216 bbox = amp.getRawBBox() 

217 dbbox = amp.getRawDataBBox() 

218 img = imageFactory(bbox) 

219 if imValue is None: 

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

221 else: 

222 img.set(imValue) 

223 # Set the first pixel read to a different value 

224 markbbox = lsst.geom.Box2I() 

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

226 markbbox.include(dbbox.getMin()) 

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

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

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

230 markbbox.include(cornerPoint) 

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

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

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

234 markbbox.include(cornerPoint) 

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

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

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

238 markbbox.include(cornerPoint) 

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

240 else: 

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

242 mimg = imageFactory(img, markbbox) 

243 mimg.set(markValue) 

244 return img 

245 

246 

247def calcRawCcdBBox(ccd): 

248 """Calculate the raw ccd bounding box. 

249 

250 Parameters 

251 ---------- 

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

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

254 

255 Returns 

256 ------- 

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

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

259 information to calculate raw BBox. 

260 """ 

261 bbox = lsst.geom.Box2I() 

262 for amp in ccd: 

263 if not amp.getHasRawInfo(): 

264 return None 

265 tbbox = amp.getRawBBox() 

266 tbbox.shift(amp.getRawXYOffset()) 

267 bbox.include(tbbox) 

268 return bbox 

269 

270 

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

272 binSize=1): 

273 """Make an Image of a CCD. 

274 

275 Parameters 

276 ---------- 

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

278 Detector to use in making the image. 

279 isTrimmed : `bool` 

280 Assemble a trimmed Detector image. 

281 showAmpGain : `bool` 

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

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

284 Image type to generate. 

285 rcMarkSize : `float` 

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

287 binSize : `int` 

288 Bin the image by this factor in both dimensions. 

289 

290 Returns 

291 ------- 

292 image : `lsst.afw.image.Image` 

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

294 """ 

295 ampImages = [] 

296 index = 0 

297 if isTrimmed: 

298 bbox = ccd.getBBox() 

299 else: 

300 bbox = calcRawCcdBBox(ccd) 

301 for amp in ccd: 

302 if amp.getHasRawInfo(): 

303 if showAmpGain: 

304 ampImages.append(makeImageFromAmp( 

305 amp, imageFactory=imageFactory, markSize=rcMarkSize)) 

306 else: 

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

308 imageFactory=imageFactory, markSize=rcMarkSize)) 

309 index += 1 

310 

311 if len(ampImages) > 0: 

312 ccdImage = imageFactory(bbox) 

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

314 if isTrimmed: 

315 assembleAmplifierImage(ccdImage, ampImage, amp) 

316 else: 

317 assembleAmplifierRawImage(ccdImage, ampImage, amp) 

318 else: 

319 if not isTrimmed: 

320 raise RuntimeError( 

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

322 ccdImage = imageFactory(ccd.getBBox()) 

323 ccdImage = afwMath.binImage(ccdImage, binSize) 

324 return ccdImage 

325 

326 

327class FakeImageDataSource: 

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

329 

330 Parameters 

331 ---------- 

332 isTrimmed : `bool` 

333 Should amps be trimmed? 

334 verbose : `bool` 

335 Be chatty? 

336 background : `float` 

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

338 showAmpGain : `bool` 

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

340 markSize : `float` 

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

342 markValue : `float` 

343 Value to assign the read corner mark. 

344 ampImValue : `float` or `None` 

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

346 scaleGain : callable 

347 Function to scale the gain by. 

348 """ 

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

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

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

352 self.isTrimmed = isTrimmed 

353 self.verbose = verbose 

354 self.background = background 

355 self.showAmpGain = showAmpGain 

356 self.markSize = markSize 

357 self.markValue = markValue 

358 self.ampImValue = ampImValue 

359 self.scaleGain = scaleGain 

360 

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

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

363 

364 Parameters 

365 ---------- 

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

367 Detector to use for making the image. 

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

369 Image constructor for making the image. 

370 binSize : `int` 

371 Bin the image by this factor in both dimensions. 

372 

373 Returns 

374 ------- 

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

376 The constructed image. 

377 """ 

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

379 imageFactory=imageFactory, binSize=binSize) 

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

381 

382 def getAmpImage(self, amp, imageFactory): 

383 """Return an amp segment image. 

384 

385 Parameters 

386 ---------- 

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

388 AmpInfoTable for this amp. 

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

390 Image constructor for making the image. 

391 

392 Returns 

393 ------- 

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

395 The constructed image. 

396 """ 

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

398 markSize=self.markSize, markValue=self.markValue, 

399 scaleGain=self.scaleGain) 

400 if self.isTrimmed: 

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

402 return ampImage 

403 

404 

405class ButlerImage(FakeImageDataSource): 

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

407 

408 Parameters 

409 ---------- 

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

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

412 type : `str` 

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

414 isTrimmed : `bool` 

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

416 verbose : `bool` 

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

418 background : `float` 

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

420 callback : callable 

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

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

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

424 *args : `list` 

425 Passed to the butler. 

426 **kwargs : `dict` 

427 Passed to the butler. 

428 

429 Notes 

430 ----- 

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

432 

433 def callback(im, ccd, imageSource): 

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

435 """ 

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

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

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

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

440 self.isTrimmed = isTrimmed 

441 self.type = type 

442 self.butler = butler 

443 self.kwargs = kwargs 

444 self.isRaw = False 

445 self.background = background 

446 self.verbose = verbose 

447 self.callback = callback 

448 

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

450 if binSize > 1: 

451 im = afwMath.binImage(im, binSize) 

452 

453 if allowRotate: 

454 im = afwMath.rotateImageBy90( 

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

456 

457 return im 

458 

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

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

461 

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

463 

464 if self.isTrimmed: 

465 bbox = ccd.getBBox() 

466 else: 

467 bbox = calcRawCcdBBox(ccd) 

468 

469 im = None 

470 if self.butler is not None: 

471 err = None 

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

473 try: 

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

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

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

477 break 

478 except Exception as e: # try a different dataId 

479 if err is None: 

480 err = e 

481 continue 

482 else: 

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

484 break 

485 

486 if im: 

487 if asMaskedImage: 

488 im = im.getMaskedImage() 

489 else: 

490 im = im.getMaskedImage().getImage() 

491 else: 

492 if self.verbose: 

493 # Lost by jupyterlab. 

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

495 

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

497 

498 if im is None: 

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

500 

501 if self.type == "raw": 

502 if hasattr(im, 'convertF'): 

503 im = im.convertF() 

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

505 self.callback = rawCallback 

506 

507 allowRotate = True 

508 if self.callback: 

509 try: 

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

511 except Exception as e: 

512 if self.verbose: 

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

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

515 else: 

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

517 

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

519 

520 

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

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

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

524 a raw image. 

525 

526 Parameters 

527 ---------- 

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

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

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

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

532 imageSource : `FakeImageDataSource` or `None` 

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

534 correctGain : `bool` 

535 Correct each amplifier for its gain? 

536 subtractBias : `bool` 

537 Subtract the bias from each amplifier? 

538 convertToFloat : `bool` 

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

540 obeyNQuarter : `bool` 

541 Obey nQuarter from the Detector (default: True) 

542 

543 Returns 

544 ------- 

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

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

547 

548 Notes 

549 ----- 

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

551 """ 

552 if ccd is None: 

553 ccd = im.getDetector() 

554 if hasattr(im, "getMaskedImage"): 

555 im = im.getMaskedImage() 

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

557 im = im.convertF() 

558 

559 isTrimmed = imageSource.isTrimmed 

560 if isTrimmed: 

561 bbox = ccd.getBBox() 

562 else: 

563 bbox = calcRawCcdBBox(ccd) 

564 

565 ampImages = [] 

566 for a in ccd: 

567 if isTrimmed: 

568 data = im[a.getRawDataBBox()] 

569 else: 

570 data = im 

571 

572 if subtractBias: 

573 bias = im[a.getRawHorizontalOverscanBBox()] 

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

575 if correctGain: 

576 data *= a.getGain() 

577 

578 ampImages.append(data) 

579 

580 ccdImage = im.Factory(bbox) 

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

582 if isTrimmed: 

583 assembleAmplifierImage(ccdImage, ampImage, amp) 

584 else: 

585 assembleAmplifierRawImage(ccdImage, ampImage, amp) 

586 

587 if obeyNQuarter: 

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

589 ccdImage = afwMath.rotateImageBy90(ccdImage, nQuarter) 

590 

591 return ccdImage 

592 

593 

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

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

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

597 

598 Parameters 

599 ---------- 

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

601 Detector to iterate for the amp bounding boxes. 

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

603 Bounding box of the un-trimmed Detector. 

604 nQuarter : `int` 

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

606 isTrimmed : `bool` 

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

608 ccdOrigin : `tuple` of `float` 

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

610 display : `lsst.afw.display.Display` 

611 Image display to display on. 

612 binSize : `int` 

613 Bin the image by this factor in both dimensions. 

614 

615 Notes 

616 ----- 

617 The colours are: 

618 - Entire detector GREEN 

619 - All data for amp GREEN 

620 - HorizontalPrescan YELLOW 

621 - HorizontalOverscan RED 

622 - Data BLUE 

623 - VerticalOverscan MAGENTA 

624 - VerticalOverscan MAGENTA 

625 """ 

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

627 raise RuntimeError("Please specify a display") 

628 

629 if untrimmedCcdBbox is None: 

630 if isTrimmed: 

631 untrimmedCcdBbox = ccd.getBBox() 

632 else: 

633 untrimmedCcdBbox = lsst.geom.Box2I() 

634 for a in ccd.getAmplifiers(): 

635 bbox = a.getRawBBox() 

636 untrimmedCcdBbox.include(bbox) 

637 

638 with display.Buffering(): 

639 ccdDim = untrimmedCcdBbox.getDimensions() 

640 ccdBbox = rotateBBoxBy90(untrimmedCcdBbox, nQuarter, ccdDim) 

641 for amp in ccd: 

642 if isTrimmed: 

643 ampbbox = amp.getBBox() 

644 else: 

645 ampbbox = amp.getRawBBox() 

646 if nQuarter != 0: 

647 ampbbox = rotateBBoxBy90(ampbbox, nQuarter, ccdDim) 

648 

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

650 display=display, bin=binSize) 

651 

652 if not isTrimmed and amp.getHasRawInfo(): 

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

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

655 (amp.getRawVerticalOverscanBBox(), 

656 afwDisplay.MAGENTA), 

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

658 if nQuarter != 0: 

659 bbox = rotateBBoxBy90(bbox, nQuarter, ccdDim) 

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

661 display=display, bin=binSize) 

662 # Label each Amp 

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

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

665 # 

666 # Rotate the amp labels too 

667 # 

668 if nQuarter == 0: 

669 c, s = 1, 0 

670 elif nQuarter == 1: 

671 c, s = 0, -1 

672 elif nQuarter == 2: 

673 c, s = -1, 0 

674 elif nQuarter == 3: 

675 c, s = 0, 1 

676 c, s = 1, 0 

677 ccdHeight = ccdBbox.getHeight() 

678 ccdWidth = ccdBbox.getWidth() 

679 xc -= 0.5*ccdHeight 

680 yc -= 0.5*ccdWidth 

681 

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

683 

684 if ccdOrigin: 

685 xc += ccdOrigin[0] 

686 yc += ccdOrigin[1] 

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

688 yc/binSize, textAngle=nQuarter*90) 

689 

690 displayUtils.drawBBox(ccdBbox, origin=ccdOrigin, 

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

692 

693 

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

695 imageFactory=afwImage.ImageU): 

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

697 

698 Parameters 

699 ---------- 

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

701 Amp record to use in display. 

702 imageSource : `FakeImageDataSource` or `None` 

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

704 display : `lsst.afw.display.Display` 

705 Image display to use. 

706 overlay : `bool` 

707 Overlay bounding boxes? 

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

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

710 """ 

711 if not display: 

712 display = _getDisplayFromDisplayOrFrame(display) 

713 

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

715 ampImSize = ampImage.getDimensions() 

716 title = amp.getName() 

717 display.mtv(ampImage, title=title) 

718 if overlay: 

719 with display.Buffering(): 

720 if amp.getHasRawInfo() and ampImSize == amp.getRawBBox().getDimensions(): 

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

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

723 bboxes.append( 

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

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

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

727 0.49, afwDisplay.YELLOW)) 

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

729 0.49, afwDisplay.MAGENTA)) 

730 else: 

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

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

733 

734 for bbox, borderWidth, ctype in bboxes: 

735 if bbox.isEmpty(): 

736 continue 

737 bbox = lsst.geom.Box2I(bbox) 

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

739 displayUtils.drawBBox( 

740 bbox, borderWidth=borderWidth, ctype=ctype, display=display) 

741 

742 

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

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

745 """Show a CCD on display. 

746 

747 Parameters 

748 ---------- 

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

750 Detector to use in display. 

751 imageSource : `FakeImageDataSource` or `None` 

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

753 display : `lsst.afw.display.Display` 

754 image display to use. 

755 overlay : `bool` 

756 Show amp bounding boxes on the displayed image? 

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

758 The image factory to use in generating the images. 

759 binSize : `int` 

760 Bin the image by this factor in both dimensions. 

761 inCameraCoords : `bool` 

762 Show the Detector in camera coordinates? 

763 """ 

764 display = _getDisplayFromDisplayOrFrame(display) 

765 

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

767 nQuarter = 0 

768 ccdImage, ccd = imageSource.getCcdImage( 

769 ccd, imageFactory=imageFactory, binSize=binSize) 

770 

771 ccdBbox = ccdImage.getBBox() 

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

773 isTrimmed = True 

774 else: 

775 isTrimmed = False 

776 

777 if inCameraCoords: 

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

779 ccdImage = afwMath.rotateImageBy90(ccdImage, nQuarter) 

780 title = ccd.getName() 

781 if isTrimmed: 

782 title += "(trimmed)" 

783 

784 if display: 

785 display.mtv(ccdImage, title=title) 

786 

787 if overlay: 

788 overlayCcdBoxes(ccd, ccdBbox, nQuarter, isTrimmed, 

789 ccdOrigin, display, binSize) 

790 

791 return ccdImage 

792 

793 

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

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

796 

797 Parameters 

798 ---------- 

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

800 List of Detector. 

801 binSize : `int` 

802 Bin the image by this factor in both dimensions. 

803 pixelSize_o : `float` 

804 Size of the pixel in mm. 

805 origin : `int` 

806 Origin of the camera pixel grid in pixels. 

807 

808 Returns 

809 ------- 

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

811 A list of bounding boxes in camera pixel coordinates. 

812 """ 

813 boxList = [] 

814 for ccd in ccdList: 

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

816 raise RuntimeError( 

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

818 

819 dbbox = lsst.geom.Box2D() 

820 for corner in ccd.getCorners(FOCAL_PLANE): 

821 dbbox.include(corner) 

822 llc = dbbox.getMin() 

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

824 cbbox = ccd.getBBox() 

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

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

827 bbox = lsst.geom.Box2I( 

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

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

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

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

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

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

834 boxList.append(bbox) 

835 return boxList 

836 

837 

838def getCameraImageBBox(camBbox, pixelSize, bufferSize): 

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

840 

841 Parameters 

842 ---------- 

843 camBbox : `lsst.geom.Box2D` 

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

845 pixelSize : `float` 

846 Size of a detector pixel in mm. 

847 bufferSize : `int` 

848 Buffer around edge of image in pixels. 

849 

850 Returns 

851 ------- 

852 box : `lsst.geom.Box2I` 

853 The resulting bounding box. 

854 """ 

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

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

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

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

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

860 retBox.grow(bufferSize) 

861 return retBox 

862 

863 

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

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

866 """Make an Image of a Camera. 

867 

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

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

870 original bboxes. 

871 

872 Parameters 

873 ---------- 

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

875 Camera object to use to make the image. 

876 detectorNameList : `list` [`str`] 

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

878 Use all Detectors if `None`. 

879 background : `float` 

880 Value to use where there is no Detector. 

881 bufferSize : `int` 

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

883 imageSource : `FakeImageDataSource` or `None` 

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

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

886 Type of image to build. 

887 binSize : `int` 

888 Bin the image by this factor in both dimensions. 

889 

890 Returns 

891 ------- 

892 image : `lsst.afw.image.Image` 

893 Image of the entire camera. 

894 """ 

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

896 

897 if detectorNameList is None: 

898 ccdList = camera 

899 else: 

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

901 

902 if detectorNameList is None: 

903 camBbox = camera.getFpBBox() 

904 else: 

905 camBbox = lsst.geom.Box2D() 

906 for detName in detectorNameList: 

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

908 camBbox.include(corner) 

909 

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

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

912 origin = camBbox.getMin() 

913 

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

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

916 camIm[:] = imageSource.background 

917 

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

919 

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

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

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

923 if im is None: 

924 continue 

925 

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

927 try: 

928 imView[:] = im 

929 except pexExceptions.LengthError as e: 

930 log.error(f"Unable to fit image for detector \"{det.getName()}\" into image of camera: {e}") 

931 

932 return camIm 

933 

934 

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

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

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

938 **kwargs): 

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

940 

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

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

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

944 containing the LLC of the image. 

945 

946 Parameters 

947 ---------- 

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

949 Camera object to use to make the image. 

950 imageSource : `FakeImageDataSource` or `None` 

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

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

953 Type of image to make 

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

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

956 Use all Detectors if `None`. 

957 binSize : `int` 

958 Bin the image by this factor in both dimensions. 

959 bufferSize : `int` 

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

961 overlay : `bool` 

962 Overlay Detector IDs and boundaries? 

963 title : `str` 

964 Title to use in display. 

965 showWcs : `bool` 

966 Include a WCS in the display? 

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

968 Color to use when drawing Detector boundaries. 

969 textSize : `float` 

970 Size of detector labels 

971 originAtCenter : `bool` 

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

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

974 display : `lsst.afw.display` 

975 Image display on which to display. 

976 **kwargs : 

977 All remaining keyword arguments are passed to makeImageFromCamera 

978 

979 Returns 

980 ------- 

981 image : `lsst.afw.image.Image` 

982 The mosaic image. 

983 """ 

984 display = _getDisplayFromDisplayOrFrame(display) 

985 

986 if binSize < 1: 

987 binSize = 1 

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

989 imageSource=imageSource, imageFactory=imageFactory, binSize=binSize, 

990 **kwargs) 

991 

992 if detectorNameList is None: 

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

994 else: 

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

996 

997 if detectorNameList is None: 

998 camBbox = camera.getFpBBox() 

999 else: 

1000 camBbox = lsst.geom.Box2D() 

1001 for detName in detectorNameList: 

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

1003 camBbox.include(corner) 

1004 pixelSize = ccdList[0].getPixelSize() 

1005 

1006 if showWcs: 

1007 if originAtCenter: 

1008 wcsReferencePixel = lsst.geom.Box2D( 

1009 cameraImage.getBBox()).getCenter() 

1010 else: 

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

1012 wcs = makeFocalPlaneWcs(pixelSize*binSize, wcsReferencePixel) 

1013 else: 

1014 wcs = None 

1015 

1016 if display: 

1017 if title == "": 

1018 title = camera.getName() 

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

1020 

1021 if overlay: 

1022 with display.Buffering(): 

1023 camBbox = getCameraImageBBox( 

1024 camBbox, pixelSize, bufferSize*binSize) 

1025 bboxList = getCcdInCamBBoxList( 

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

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

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

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

1030 # pixel 

1031 displayUtils.drawBBox( 

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

1033 dims = bbox.getDimensions() 

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

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

1036 

1037 return cameraImage 

1038 

1039 

1040def makeFocalPlaneWcs(pixelSize, referencePixel): 

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

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

1043 

1044 Parameters 

1045 ---------- 

1046 pixelSize : `float` 

1047 Size of the image pixels in physical units 

1048 referencePixel : `lsst.geom.Point2D` 

1049 Pixel for origin of WCS 

1050 

1051 Returns 

1052 ------- 

1053 `lsst.afw.geom.Wcs` 

1054 Wcs object for mapping between pixels and focal plane. 

1055 """ 

1056 md = dafBase.PropertySet() 

1057 if referencePixel is None: 

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

1059 for i in range(2): 

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

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

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

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

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

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

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

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

1068 

1069 return afwGeom.makeSkyWcs(md) 

1070 

1071 

1072def findAmp(ccd, pixelPosition): 

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

1074 

1075 Parameters 

1076 ---------- 

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

1078 Detector to look in. 

1079 pixelPosition : `lsst.geom.Point2I` 

1080 The pixel position to find the amp for. 

1081 

1082 Returns 

1083 ------- 

1084 `lsst.afw.table.AmpInfoCatalog` 

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

1086 """ 

1087 for amp in ccd: 

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

1089 return amp 

1090 

1091 return None