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

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/>.
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 .cameraGeomLib 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 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))
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.
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")
122 if fieldSizeDeg_x:
123 if fieldSizeDeg_y is None:
124 fieldSizeDeg_y = fieldSizeDeg_x
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 = [], []
133 xs = []
134 ys = []
135 pcolors = []
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')
150 colorMap = {DetectorType.SCIENCE: 'b', DetectorType.FOCUS: 'y',
151 DetectorType.GUIDER: 'g', DetectorType.WAVEFRONT: 'r'}
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)
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()
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.
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.
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).
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
247def calcRawCcdBBox(ccd):
248 """Calculate the raw ccd bounding box.
250 Parameters
251 ----------
252 ccd : `lsst.afw.cameraGeom.Detector`
253 Detector for which to calculate the un-trimmed bounding box.
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
271def makeImageFromCcd(ccd, isTrimmed=True, showAmpGain=True, imageFactory=afwImage.ImageU, rcMarkSize=10,
272 binSize=1):
273 """Make an Image of a CCD.
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.
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
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
327class FakeImageDataSource:
328 """A class to retrieve synthetic images for display by the show* methods
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
361 def getCcdImage(self, det, imageFactory, binSize):
362 """Return a CCD image for the detector and the (possibly updated) Detector.
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.
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
382 def getAmpImage(self, amp, imageFactory):
383 """Return an amp segment image.
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.
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
405class ButlerImage(FakeImageDataSource):
406 """A class to return an Image of a given Ccd using the butler.
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.
429 Notes
430 -----
431 You can define a short named function as a callback::
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
449 def _prepareImage(self, ccd, im, binSize, allowRotate=True):
450 if binSize > 1:
451 im = afwMath.binImage(im, binSize)
453 if allowRotate:
454 im = afwMath.rotateImageBy90(
455 im, ccd.getOrientation().getNQuarter())
457 return im
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"""
462 log = lsst.log.Log.getLogger("afw.cameraGeom.utils.ButlerImage")
464 if self.isTrimmed:
465 bbox = ccd.getBBox()
466 else:
467 bbox = calcRawCcdBBox(ccd)
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
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}")
496 log.warn(f"Reading {ccd.getId()}: {err}")
498 if im is None:
499 return self._prepareImage(ccd, imageFactory(*bbox.getDimensions()), binSize), ccd
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
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
518 return self._prepareImage(ccd, im, binSize, allowRotate=allowRotate), ccd
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.
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)
543 Returns
544 -------
545 image : `lsst.afw.image.Image` like
546 The constructed image (type returned by ``im.Factory``).
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()
559 isTrimmed = imageSource.isTrimmed
560 if isTrimmed:
561 bbox = ccd.getBBox()
562 else:
563 bbox = calcRawCcdBBox(ccd)
565 ampImages = []
566 for a in ccd:
567 if isTrimmed:
568 data = im[a.getRawDataBBox()]
569 else:
570 data = im
572 if subtractBias:
573 bias = im[a.getRawHorizontalOverscanBBox()]
574 data -= afwMath.makeStatistics(bias, afwMath.MEANCLIP).getValue()
575 if correctGain:
576 data *= a.getGain()
578 ampImages.append(data)
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)
587 if obeyNQuarter:
588 nQuarter = ccd.getOrientation().getNQuarter()
589 ccdImage = afwMath.rotateImageBy90(ccdImage, nQuarter)
591 return ccdImage
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.
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.
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")
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)
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)
649 displayUtils.drawBBox(ampbbox, origin=ccdOrigin, borderWidth=0.49,
650 display=display, bin=binSize)
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
682 xc, yc = 0.5*ccdHeight + c*xc + s*yc, 0.5*ccdWidth + -s*xc + c*yc
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)
690 displayUtils.drawBBox(ccdBbox, origin=ccdOrigin,
691 borderWidth=0.49, ctype=afwDisplay.MAGENTA, display=display, bin=binSize)
694def showAmp(amp, imageSource=FakeImageDataSource(isTrimmed=False), display=None, overlay=True,
695 imageFactory=afwImage.ImageU):
696 """Show an amp in an image display.
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)
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()
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)
743def showCcd(ccd, imageSource=FakeImageDataSource(), display=None, overlay=True,
744 imageFactory=afwImage.ImageF, binSize=1, inCameraCoords=False):
745 """Show a CCD on display.
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)
766 ccdOrigin = lsst.geom.Point2I(0, 0)
767 nQuarter = 0
768 ccdImage, ccd = imageSource.getCcdImage(
769 ccd, imageFactory=imageFactory, binSize=binSize)
771 ccdBbox = ccdImage.getBBox()
772 if ccdBbox.getDimensions() == ccd.getBBox().getDimensions():
773 isTrimmed = True
774 else:
775 isTrimmed = False
777 if inCameraCoords:
778 nQuarter = ccd.getOrientation().getNQuarter()
779 ccdImage = afwMath.rotateImageBy90(ccdImage, nQuarter)
780 title = ccd.getName()
781 if isTrimmed:
782 title += "(trimmed)"
784 if display:
785 display.mtv(ccdImage, title=title)
787 if overlay:
788 overlayCcdBoxes(ccd, ccdBbox, nQuarter, isTrimmed,
789 ccdOrigin, display, binSize)
791 return ccdImage
794def getCcdInCamBBoxList(ccdList, binSize, pixelSize_o, origin):
795 """Get the bounding boxes of a list of Detectors within a camera sized pixel grid
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.
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")
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
838def getCameraImageBBox(camBbox, pixelSize, bufferSize):
839 """Get the bounding box of a camera sized image in pixels
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.
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
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.
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.
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.
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")
897 if detectorNameList is None:
898 ccdList = camera
899 else:
900 ccdList = [camera[name] for name in detectorNameList]
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)
910 pixelSize_o = camera[next(camera.getNameIter())].getPixelSize()
911 camBbox = getCameraImageBBox(camBbox, pixelSize_o, bufferSize*binSize)
912 origin = camBbox.getMin()
914 camIm = imageFactory(int(math.ceil(camBbox.getDimensions().getX()/binSize)),
915 int(math.ceil(camBbox.getDimensions().getY()/binSize)))
916 camIm[:] = imageSource.background
918 assert imageSource.isTrimmed, "isTrimmed is False isn't supported by getCcdInCamBBoxList"
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
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}")
932 return camIm
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.
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.
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
979 Returns
980 -------
981 image : `lsst.afw.image.Image`
982 The mosaic image.
983 """
984 display = _getDisplayFromDisplayOrFrame(display)
986 if binSize < 1:
987 binSize = 1
988 cameraImage = makeImageFromCamera(camera, detectorNameList=detectorNameList, bufferSize=bufferSize,
989 imageSource=imageSource, imageFactory=imageFactory, binSize=binSize,
990 **kwargs)
992 if detectorNameList is None:
993 ccdList = [camera[name] for name in camera.getNameIter()]
994 else:
995 ccdList = [camera[name] for name in detectorNameList]
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()
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
1016 if display:
1017 if title == "":
1018 title = camera.getName()
1019 display.mtv(cameraImage, title=title, wcs=wcs)
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)
1037 return cameraImage
1040def makeFocalPlaneWcs(pixelSize, referencePixel):
1041 """Make a WCS for the focal plane geometry
1042 (i.e. one that returns positions in "mm")
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
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")
1069 return afwGeom.makeSkyWcs(md)
1072def findAmp(ccd, pixelPosition):
1073 """Find the Amp with the specified pixel position within the composite
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.
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
1091 return None