Coverage for python/lsst/afw/cameraGeom/utils.py: 8%
Shortcuts 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
Shortcuts 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/>.
22"""
23Support for displaying cameraGeom objects.
24"""
26__all__ = ['prepareWcsData', 'plotFocalPlane', 'makeImageFromAmp', 'calcRawCcdBBox', 'makeImageFromCcd',
27 'FakeImageDataSource', 'ButlerImage', 'rawCallback', 'overlayCcdBoxes',
28 'showAmp', 'showCcd', 'getCcdInCamBBoxList', 'getCameraImageBBox',
29 'makeImageFromCamera', 'showCamera', 'makeFocalPlaneWcs', 'findAmp']
31import math
32import numpy
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
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
50import lsst.afw.display as afwDisplay
51import lsst.afw.display.utils as displayUtils
54def prepareWcsData(wcs, amp, isTrimmed=True):
55 """Put Wcs from an Amp image into CCD coordinates
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?
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))
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.
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")
120 if fieldSizeDeg_x:
121 if fieldSizeDeg_y is None:
122 fieldSizeDeg_y = fieldSizeDeg_x
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 = [], []
131 xs = []
132 ys = []
133 pcolors = []
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')
148 colorMap = {DetectorType.SCIENCE: 'b', DetectorType.FOCUS: 'y',
149 DetectorType.GUIDER: 'g', DetectorType.WAVEFRONT: 'r'}
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)
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()
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.
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.
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).
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
242def calcRawCcdBBox(ccd):
243 """Calculate the raw ccd bounding box.
245 Parameters
246 ----------
247 ccd : `lsst.afw.cameraGeom.Detector`
248 Detector for which to calculate the un-trimmed bounding box.
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
264def makeImageFromCcd(ccd, isTrimmed=True, showAmpGain=True, imageFactory=afwImage.ImageU, rcMarkSize=10,
265 binSize=1):
266 """Make an Image of a CCD.
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.
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
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
319class FakeImageDataSource:
320 """A class to retrieve synthetic images for display by the show* methods
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
353 def getCcdImage(self, det, imageFactory, binSize):
354 """Return a CCD image for the detector and the (possibly updated) Detector.
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.
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
374 def getAmpImage(self, amp, imageFactory):
375 """Return an amp segment image.
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.
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
397class ButlerImage(FakeImageDataSource):
398 """A class to return an Image of a given Ccd using the butler.
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.
421 Notes
422 -----
423 You can define a short named function as a callback::
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
441 def _prepareImage(self, ccd, im, binSize, allowRotate=True):
442 if binSize > 1:
443 im = afwMath.binImage(im, binSize)
445 if allowRotate:
446 im = afwMath.rotateImageBy90(
447 im, ccd.getOrientation().getNQuarter())
449 return im
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"""
454 log = lsst.log.Log.getLogger("afw.cameraGeom.utils.ButlerImage")
456 if self.isTrimmed:
457 bbox = ccd.getBBox()
458 else:
459 bbox = calcRawCcdBBox(ccd)
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
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}")
488 log.warn(f"Reading {ccd.getId()}: {err}")
490 if im is None:
491 return self._prepareImage(ccd, imageFactory(*bbox.getDimensions()), binSize), ccd
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
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
510 return self._prepareImage(ccd, im, binSize, allowRotate=allowRotate), ccd
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.
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)
535 Returns
536 -------
537 image : `lsst.afw.image.Image` like
538 The constructed image (type returned by ``im.Factory``).
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()
551 isTrimmed = imageSource.isTrimmed
552 if isTrimmed:
553 bbox = ccd.getBBox()
554 else:
555 bbox = calcRawCcdBBox(ccd)
557 ampImages = []
558 for a in ccd:
559 if isTrimmed:
560 data = im[a.getRawDataBBox()]
561 else:
562 data = im
564 if subtractBias:
565 bias = im[a.getRawHorizontalOverscanBBox()]
566 data -= afwMath.makeStatistics(bias, afwMath.MEANCLIP).getValue()
567 if correctGain:
568 data *= a.getGain()
570 ampImages.append(data)
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)
579 if obeyNQuarter:
580 nQuarter = ccd.getOrientation().getNQuarter()
581 ccdImage = afwMath.rotateImageBy90(ccdImage, nQuarter)
583 return ccdImage
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.
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.
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")
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)
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)
641 displayUtils.drawBBox(ampbbox, origin=ccdOrigin, borderWidth=0.49,
642 display=display, bin=binSize)
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
674 xc, yc = 0.5*ccdHeight + c*xc + s*yc, 0.5*ccdWidth + -s*xc + c*yc
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)
682 displayUtils.drawBBox(ccdBbox, origin=ccdOrigin,
683 borderWidth=0.49, ctype=afwDisplay.MAGENTA, display=display, bin=binSize)
686def showAmp(amp, imageSource=FakeImageDataSource(isTrimmed=False), display=None, overlay=True,
687 imageFactory=afwImage.ImageU):
688 """Show an amp in an image display.
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)
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()
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)
735def showCcd(ccd, imageSource=FakeImageDataSource(), display=None, overlay=True,
736 imageFactory=afwImage.ImageF, binSize=1, inCameraCoords=False):
737 """Show a CCD on display.
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)
758 ccdOrigin = lsst.geom.Point2I(0, 0)
759 nQuarter = 0
760 ccdImage, ccd = imageSource.getCcdImage(
761 ccd, imageFactory=imageFactory, binSize=binSize)
763 ccdBbox = ccdImage.getBBox()
764 if ccdBbox.getDimensions() == ccd.getBBox().getDimensions():
765 isTrimmed = True
766 else:
767 isTrimmed = False
769 if inCameraCoords:
770 nQuarter = ccd.getOrientation().getNQuarter()
771 ccdImage = afwMath.rotateImageBy90(ccdImage, nQuarter)
772 title = ccd.getName()
773 if isTrimmed:
774 title += "(trimmed)"
776 if display:
777 display.mtv(ccdImage, title=title)
779 if overlay:
780 overlayCcdBoxes(ccd, ccdBbox, nQuarter, isTrimmed,
781 ccdOrigin, display, binSize)
783 return ccdImage
786def getCcdInCamBBoxList(ccdList, binSize, pixelSize_o, origin):
787 """Get the bounding boxes of a list of Detectors within a camera sized pixel grid
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.
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")
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
830def getCameraImageBBox(camBbox, pixelSize, bufferSize):
831 """Get the bounding box of a camera sized image in pixels
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.
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
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.
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.
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.
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")
889 if detectorNameList is None:
890 ccdList = camera
891 else:
892 ccdList = [camera[name] for name in detectorNameList]
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)
902 pixelSize_o = camera[next(camera.getNameIter())].getPixelSize()
903 camBbox = getCameraImageBBox(camBbox, pixelSize_o, bufferSize*binSize)
904 origin = camBbox.getMin()
906 camIm = imageFactory(int(math.ceil(camBbox.getDimensions().getX()/binSize)),
907 int(math.ceil(camBbox.getDimensions().getY()/binSize)))
908 camIm[:] = imageSource.background
910 assert imageSource.isTrimmed, "isTrimmed is False isn't supported by getCcdInCamBBoxList"
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
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}")
924 return camIm
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.
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.
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
971 Returns
972 -------
973 image : `lsst.afw.image.Image`
974 The mosaic image.
975 """
976 display = _getDisplayFromDisplayOrFrame(display)
978 if binSize < 1:
979 binSize = 1
980 cameraImage = makeImageFromCamera(camera, detectorNameList=detectorNameList, bufferSize=bufferSize,
981 imageSource=imageSource, imageFactory=imageFactory, binSize=binSize,
982 **kwargs)
984 if detectorNameList is None:
985 ccdList = [camera[name] for name in camera.getNameIter()]
986 else:
987 ccdList = [camera[name] for name in detectorNameList]
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()
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
1008 if display:
1009 if title == "":
1010 title = camera.getName()
1011 display.mtv(cameraImage, title=title, wcs=wcs)
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)
1029 return cameraImage
1032def makeFocalPlaneWcs(pixelSize, referencePixel):
1033 """Make a WCS for the focal plane geometry
1034 (i.e. one that returns positions in "mm")
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
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")
1061 return afwGeom.makeSkyWcs(md)
1064def findAmp(ccd, pixelPosition):
1065 """Find the Amp with the specified pixel position within the composite
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.
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
1083 return None