LSST Applications g0afe43252f+b86e4b8053,g0e535e6de9+ad13c1b82d,g11f7dcd041+017865fdd3,g1ce3e0751c+f991eae79d,g25e82e299b+da9c58a417,g28da252d5a+9a4345bd89,g2bbee38e9b+b6588ad223,g2bc492864f+b6588ad223,g2cdde0e794+8523d0dbb4,g347aa1857d+b6588ad223,g35bb328faa+b86e4b8053,g3a166c0a6a+b6588ad223,g461a3dce89+b86e4b8053,g52b1c1532d+b86e4b8053,g7d11c3e888+3343bff751,g80478fca09+f22f659b46,g8543c18506+fadcf71ecf,g858d7b2824+c704b22fcc,g8cd86fa7b1+497903d09f,g965a9036f2+c704b22fcc,g979bb04a14+43c1c4070b,g9ddcbc5298+f24b38b85a,gae0086650b+b86e4b8053,gb781091699+0f9934ae6f,gbb886bcc26+486567bf06,gc28159a63d+b6588ad223,gc30aee3386+a2f0f6cab9,gc9f6898e11+66e322ebb3,gcaf7e4fdec+c704b22fcc,gcd45df26be+c704b22fcc,gcdd4ae20e8+02080e1d2d,gcf0d15dbbd+02080e1d2d,gdaeeff99f8+006e14e809,gdbce86181e+42405ade82,ge3d4d395c2+224150c836,ge79ae78c31+b6588ad223,gf048a9a2f4+69e41b07e6,gf0baf85859+b4cca3d10f,w.2024.30
LSST Data Management Base Package
Loading...
Searching...
No Matches
utils.py
Go to the documentation of this file.
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 logging
32import math
33import numpy
34
35import lsst.geom
36from lsst.afw.fits import FitsError
37import lsst.afw.geom as afwGeom
38import lsst.afw.image as afwImage
39import lsst.afw.math as afwMath
40import lsst.afw.cameraGeom as afwCameraGeom
41import lsst.daf.base as dafBase
42import lsst.pex.exceptions as pexExceptions
43
44from ._rotateBBoxBy90 import rotateBBoxBy90
45from ._assembleImage import assembleAmplifierImage, assembleAmplifierRawImage
46from ._cameraGeom import FIELD_ANGLE, FOCAL_PLANE
47from lsst.afw.display.utils import _getDisplayFromDisplayOrFrame
48from lsst.afw.cameraGeom import DetectorType
49
50import lsst.afw.display as afwDisplay
51import lsst.afw.display.utils as displayUtils
52
53_LOG = logging.getLogger(__name__)
54
55
56def prepareWcsData(wcs, amp, isTrimmed=True):
57 """Put Wcs from an Amp image into CCD coordinates
58
59 Parameters
60 ----------
61 wcs : `lsst.afw.geom.SkyWcs`
62 The WCS object to start from.
63 amp : `lsst.afw.table.AmpInfoRecord`
64 Amp object to use
65 isTrimmed : `bool`
66 Is the image to which the WCS refers trimmed of non-imaging pixels?
67
68 Returns
69 -------
70 ampWcs : `lsst.afw.geom.SkyWcs`
71 The modified WCS.
72 """
73 if isTrimmed:
74 ampBox = amp.getRawDataBBox()
75 else:
76 ampBox = amp.getRawBBox()
77 ampCenter = lsst.geom.Point2D(ampBox.getDimensions()/2.0)
78 wcs = afwGeom.makeFlippedWcs(wcs, amp.getRawFlipX(), amp.getRawFlipY(), ampCenter)
79 # Shift WCS for trimming
80 if isTrimmed:
81 trim_shift = ampBox.getMin() - amp.getBBox().getMin()
82 wcs = wcs.copyAtShiftedPixelOrigin(lsst.geom.Extent2D(-trim_shift.getX(), -trim_shift.getY()))
83 # Account for shift of amp data in larger ccd matrix
84 offset = amp.getRawXYOffset()
85 return wcs.copyAtShiftedPixelOrigin(lsst.geom.Extent2D(offset))
86
87
88def plotFocalPlane(camera, fieldSizeDeg_x=0, fieldSizeDeg_y=None, dx=0.1, dy=0.1, figsize=(10., 10.),
89 useIds=False, showFig=True, savePath=None):
90 """Make a plot of the focal plane along with a set points that sample
91 the field of view.
92
93 Parameters
94 ----------
95 camera : `lsst.afw.cameraGeom.Camera`
96 A camera object
97 fieldSizeDeg_x : `float`
98 Amount of the field to sample in x in degrees
99 fieldSizeDeg_y : `float` or `None`
100 Amount of the field to sample in y in degrees
101 dx : `float`
102 Spacing of sample points in x in degrees
103 dy : `float`
104 Spacing of sample points in y in degrees
105 figsize : `tuple` containing two `float`
106 Matplotlib style tuple indicating the size of the figure in inches
107 useIds : `bool`
108 Label detectors by name, not id?
109 showFig : `bool`
110 Display the figure on the screen?
111 savePath : `str` or `None`
112 If not `None`, save a copy of the figure to this name.
113 """
114 try:
115 from matplotlib.patches import Polygon
116 from matplotlib.collections import PatchCollection
117 import matplotlib.pyplot as plt
118 except ImportError:
119 raise ImportError(
120 "Can't run plotFocalPlane: matplotlib has not been set up")
121
122 if fieldSizeDeg_x:
123 if fieldSizeDeg_y is None:
124 fieldSizeDeg_y = fieldSizeDeg_x
125
126 field_gridx, field_gridy = numpy.meshgrid(
127 numpy.arange(0., fieldSizeDeg_x + dx, dx) - fieldSizeDeg_x/2.,
128 numpy.arange(0., fieldSizeDeg_y + dy, dy) - fieldSizeDeg_y/2.)
129 field_gridx, field_gridy = field_gridx.flatten(), field_gridy.flatten()
130 else:
131 field_gridx, field_gridy = [], []
132
133 xs = []
134 ys = []
135 pcolors = []
136
137 # compute focal plane positions corresponding to field angles field_gridx, field_gridy
138 posFieldAngleList = [lsst.geom.Point2D(x*lsst.geom.radians, y*lsst.geom.radians)
139 for x, y in zip(field_gridx, field_gridy)]
140 posFocalPlaneList = camera.transform(posFieldAngleList, FIELD_ANGLE, FOCAL_PLANE)
141 for posFocalPlane in posFocalPlaneList:
142 xs.append(posFocalPlane.getX())
143 ys.append(posFocalPlane.getY())
144 dets = camera.findDetectors(posFocalPlane, FOCAL_PLANE)
145 if len(dets) > 0:
146 pcolors.append('w')
147 else:
148 pcolors.append('k')
149
150 colorMap = {DetectorType.SCIENCE: 'b', DetectorType.FOCUS: 'y',
151 DetectorType.GUIDER: 'g', DetectorType.WAVEFRONT: 'r'}
152
153 patches = []
154 colors = []
155 plt.figure(figsize=figsize)
156 ax = plt.gca()
157 xvals = []
158 yvals = []
159 for det in camera:
160 corners = [(c.getX(), c.getY()) for c in det.getCorners(FOCAL_PLANE)]
161 for corner in corners:
162 xvals.append(corner[0])
163 yvals.append(corner[1])
164 colors.append(colorMap[det.getType()])
165 patches.append(Polygon(corners, True))
166 center = det.getOrientation().getFpPosition()
167 ax.text(center.getX(), center.getY(), det.getId() if useIds else det.getName(),
168 horizontalalignment='center', size=6)
169
170 patchCollection = PatchCollection(patches, alpha=0.6, facecolor=colors)
171 ax.add_collection(patchCollection)
172 ax.scatter(xs, ys, s=10, alpha=.7, linewidths=0., c=pcolors)
173 ax.set_xlim(min(xvals) - abs(0.1*min(xvals)),
174 max(xvals) + abs(0.1*max(xvals)))
175 ax.set_ylim(min(yvals) - abs(0.1*min(yvals)),
176 max(yvals) + abs(0.1*max(yvals)))
177 ax.set_xlabel('Focal Plane X (mm)')
178 ax.set_ylabel('Focal Plane Y (mm)')
179 if savePath is not None:
180 plt.savefig(savePath)
181 if showFig:
182 plt.show()
183
184
185def makeImageFromAmp(amp, imValue=None, imageFactory=afwImage.ImageU, markSize=10, markValue=0,
186 scaleGain=lambda gain: (gain*1000)//10):
187 """Make an image from an amp object.
188
189 Since images are integer images by default, the gain needs to be scaled to
190 give enough dynamic range to see variation from amp to amp.
191 The scaling algorithm is assignable.
192
193 Parameters
194 ----------
195 amp : `lsst.afw.table.AmpInfoRecord`
196 Amp record to use for constructing the raw amp image.
197 imValue : `float` or `None`
198 Value to assign to the constructed image, or scaleGain(gain) if `None`.
199 imageFactory : callable like `lsst.afw.image.Image`
200 Type of image to construct.
201 markSize : `float`
202 Size of mark at read corner in pixels.
203 markValue : `float`
204 Value of pixels in the read corner mark.
205 scaleGain : callable
206 The function by which to scale the gain (must take a single argument).
207
208 Returns
209 -------
210 ampImage : `lsst.afw.image`
211 An untrimmed amp image, of the type produced by ``imageFactory``.
212 """
213 bbox = amp.getRawBBox()
214 dbbox = amp.getRawDataBBox()
215 img = imageFactory(bbox)
216 if imValue is None:
217 img.set(int(scaleGain(amp.getGain())))
218 else:
219 img.set(imValue)
220 # Set the first pixel read to a different value
221 markbbox = lsst.geom.Box2I()
222 if amp.getReadoutCorner() == afwCameraGeom.ReadoutCorner.LL:
223 markbbox.include(dbbox.getMin())
224 markbbox.include(dbbox.getMin() + lsst.geom.Extent2I(markSize, markSize))
225 elif amp.getReadoutCorner() == afwCameraGeom.ReadoutCorner.LR:
226 cornerPoint = lsst.geom.Point2I(dbbox.getMaxX(), dbbox.getMinY())
227 markbbox.include(cornerPoint)
228 markbbox.include(cornerPoint + lsst.geom.Extent2I(-markSize, markSize))
229 elif amp.getReadoutCorner() == afwCameraGeom.ReadoutCorner.UR:
230 cornerPoint = lsst.geom.Point2I(dbbox.getMax())
231 markbbox.include(cornerPoint)
232 markbbox.include(cornerPoint + lsst.geom.Extent2I(-markSize, -markSize))
233 elif amp.getReadoutCorner() == afwCameraGeom.ReadoutCorner.UL:
234 cornerPoint = lsst.geom.Point2I(dbbox.getMinX(), dbbox.getMaxY())
235 markbbox.include(cornerPoint)
236 markbbox.include(cornerPoint + lsst.geom.Extent2I(markSize, -markSize))
237 else:
238 raise RuntimeError("Could not set readout corner")
239 mimg = imageFactory(img, markbbox)
240 mimg.set(markValue)
241 return img
242
243
245 """Calculate the raw ccd bounding box.
246
247 Parameters
248 ----------
249 ccd : `lsst.afw.cameraGeom.Detector`
250 Detector for which to calculate the un-trimmed bounding box.
251
252 Returns
253 -------
254 bbox : `lsst.geom.Box2I` or `None`
255 Bounding box of the un-trimmed Detector, or `None` if there is not enough
256 information to calculate raw BBox.
257 """
258 bbox = lsst.geom.Box2I()
259 for amp in ccd:
260 tbbox = amp.getRawBBox()
261 tbbox.shift(amp.getRawXYOffset())
262 bbox.include(tbbox)
263 return bbox
264
265
266def makeImageFromCcd(ccd, isTrimmed=True, showAmpGain=True, imageFactory=afwImage.ImageU, rcMarkSize=10,
267 binSize=1):
268 """Make an Image of a CCD.
269
270 Parameters
271 ----------
272 ccd : `lsst.afw.cameraGeom.Detector`
273 Detector to use in making the image.
274 isTrimmed : `bool`
275 Assemble a trimmed Detector image.
276 showAmpGain : `bool`
277 Use the per-amp gain to color the pixels in the image?
278 imageFactory : callable like `lsst.afw.image.Image`
279 Image type to generate.
280 rcMarkSize : `float`
281 Size of the mark to make in the amp images at the read corner.
282 binSize : `int`
283 Bin the image by this factor in both dimensions.
284
285 Returns
286 -------
287 image : `lsst.afw.image.Image`
288 Image of the Detector (type returned by ``imageFactory``).
289 """
290 ampImages = []
291 index = 0
292 if isTrimmed:
293 bbox = ccd.getBBox()
294 else:
295 bbox = calcRawCcdBBox(ccd)
296 for amp in ccd:
297 if showAmpGain:
298 ampImages.append(makeImageFromAmp(
299 amp, imageFactory=imageFactory, markSize=rcMarkSize))
300 else:
301 ampImages.append(makeImageFromAmp(amp, imValue=(index + 1)*1000,
302 imageFactory=imageFactory, markSize=rcMarkSize))
303 index += 1
304
305 if len(ampImages) > 0:
306 ccdImage = imageFactory(bbox)
307 for ampImage, amp in zip(ampImages, ccd):
308 if isTrimmed:
309 assembleAmplifierImage(ccdImage, ampImage, amp)
310 else:
311 assembleAmplifierRawImage(ccdImage, ampImage, amp)
312 else:
313 if not isTrimmed:
314 raise RuntimeError(
315 "Cannot create untrimmed CCD without amps with raw information")
316 ccdImage = imageFactory(ccd.getBBox())
317 ccdImage = afwMath.binImage(ccdImage, binSize)
318 return ccdImage
319
320
322 """A class to retrieve synthetic images for display by the show* methods
323
324 Parameters
325 ----------
326 isTrimmed : `bool`
327 Should amps be trimmed?
328 verbose : `bool`
329 Be chatty?
330 background : `float`
331 The value of any pixels that lie outside the CCDs.
332 showAmpGain : `bool`
333 Color the amp segments with the gain of the amp?
334 markSize : `float`
335 Size of the side of the box used to mark the read corner.
336 markValue : `float`
337 Value to assign the read corner mark.
338 ampImValue : `float` or `None`
339 Value to assign to amps; scaleGain(gain) is used if `None`.
340 scaleGain : callable
341 Function to scale the gain by.
342 """
343 def __init__(self, isTrimmed=True, verbose=False, background=numpy.nan,
344 showAmpGain=True, markSize=10, markValue=0,
345 ampImValue=None, scaleGain=lambda gain: (gain*1000)//10):
346 self.isTrimmed = isTrimmed
347 self.verbose = verbose
348 self.background = background
349 self.showAmpGain = showAmpGain
350 self.markSize = markSize
351 self.markValue = markValue
352 self.ampImValue = ampImValue
353 self.scaleGain = scaleGain
354
355 def getCcdImage(self, det, imageFactory, binSize):
356 """Return a CCD image for the detector and the (possibly updated) Detector.
357
358 Parameters
359 ----------
360 det : `lsst.afw.cameraGeom.Detector`
361 Detector to use for making the image.
362 imageFactory : callable like `lsst.afw.image.Image`
363 Image constructor for making the image.
364 binSize : `int`
365 Bin the image by this factor in both dimensions.
366
367 Returns
368 -------
369 ccdImage : `lsst.afw.image.Image`
370 The constructed image.
371 """
372 ccdImage = makeImageFromCcd(det, isTrimmed=self.isTrimmed, showAmpGain=self.showAmpGain,
373 imageFactory=imageFactory, binSize=binSize)
374 return afwMath.rotateImageBy90(ccdImage, det.getOrientation().getNQuarter()), det
375
376 def getAmpImage(self, amp, imageFactory):
377 """Return an amp segment image.
378
379 Parameters
380 ----------
381 amp : `lsst.afw.table.AmpInfoTable`
382 AmpInfoTable for this amp.
383 imageFactory : callable like `lsst.afw.image.Image`
384 Image constructor for making the image.
385
386 Returns
387 -------
388 ampImage : `lsst.afw.image.Image`
389 The constructed image.
390 """
391 ampImage = makeImageFromAmp(amp, imValue=self.ampImValue, imageFactory=imageFactory,
392 markSize=self.markSize, markValue=self.markValue,
393 scaleGain=self.scaleGain)
394 if self.isTrimmed:
395 ampImage = ampImage.Factory(ampImage, amp.getRawDataBBox())
396 return ampImage
397
398
400 """A class to return an Image of a given Ccd using the butler.
401
402 Parameters
403 ----------
404 butler : `lsst.daf.butler.Butler` or `None`
405 The butler to use. If `None`, an empty image is returned. Assumes that
406 the instrument was specified during butler construction or is included
407 in the ``kwargs`` parameter.
408 type : `str`
409 The type of image to read (e.g. raw, bias, flat, calexp).
410 isTrimmed : `bool`
411 If true, the showCamera command expects to be given trimmed images.
412 verbose : `bool`
413 Be chatty (in particular, log any error messages from the butler)?
414 background : `float`
415 The value of any pixels that lie outside the CCDs.
416 callback : callable
417 A function called with (image, detector, butler) for every image, which
418 returns the image to be displayed (e.g. rawCallback). The image must
419 be of the correct size, allowing for the value of isTrimmed.
420 *args : `list`
421 Passed to the base class constructor.
422 **kwargs : `dict`
423 Passed to the butler.
424
425 Notes
426 -----
427 You can define a short named function as a callback::
428
429 def callback(im, ccd, imageSource):
430 return cameraGeom.utils.rawCallback(im, ccd, imageSource, correctGain=True)
431 """
432 def __init__(self, butler=None, type="raw",
433 isTrimmed=True, verbose=False, background=numpy.nan,
434 callback=None, *args, **kwargs):
435 super().__init__(*args)
436 self.isTrimmedisTrimmed = isTrimmed
437 self.type = type
438 self.butler = butler
439 self.kwargs = kwargs
440 self.isRaw = False
441 self.backgroundbackground = background
442 self.verboseverbose = verbose
443 self.callback = callback
444
445 def _prepareImage(self, ccd, im, binSize, allowRotate=True):
446 if binSize > 1:
447 im = afwMath.binImage(im, binSize)
448
449 if allowRotate:
451 im, ccd.getOrientation().getNQuarter())
452
453 return im
454
455 def getCcdImage(self, ccd, imageFactory=afwImage.ImageF, binSize=1, asMaskedImage=False):
456 """Return an image of the specified ccd, and also the (possibly updated) ccd"""
457
458 log = _LOG.getChild("ButlerImage")
459
460 if self.isTrimmedisTrimmed:
461 bbox = ccd.getBBox()
462 else:
463 bbox = calcRawCcdBBox(ccd)
464
465 im = None
466 if self.butler is not None:
467 err = None
468 try:
469 im = self.butler.get(self.type, detector=ccd.getId(), **self.kwargs)
470 except FitsError as e:
471 err = IOError(e.args[0].split('\n')[0]) # It's a very chatty error
472 except Exception as e: # try a different dataId
473 err = e
474 else:
475 ccd = im.getDetector() # possibly modified by assembleCcdTask
476
477 if im:
478 if asMaskedImage:
479 im = im.getMaskedImage()
480 else:
481 im = im.getMaskedImage().getImage()
482 else:
483 if self.verboseverbose:
484 # Lost by jupyterlab.
485 print(f"Reading {ccd.getId()}: {err}")
486
487 log.warning("Reading %s: %s", ccd.getId(), err)
488
489 if im is None:
490 return self._prepareImage(ccd, imageFactory(*bbox.getDimensions()), binSize), ccd
491
492 if self.type == "raw":
493 if hasattr(im, 'convertF'):
494 im = im.convertF()
495 if False and self.callback is None: # we need to trim the raw image
496 self.callback = rawCallback
497
498 allowRotate = True
499 if self.callback:
500 try:
501 im = self.callback(im, ccd, imageSource=self)
502 except Exception:
503 if self.verboseverbose:
504 log.exception("callback failed.")
505 im = imageFactory(*bbox.getDimensions())
506 else:
507 allowRotate = False # the callback was responsible for any rotations
508
509 return self._prepareImage(ccd, im, binSize, allowRotate=allowRotate), ccd
510
511
512def rawCallback(im, ccd=None, imageSource=None,
513 correctGain=False, subtractBias=False, convertToFloat=False, obeyNQuarter=True):
514 """A callback function that may or may not subtract bias/correct gain/trim
515 a raw image.
516
517 Parameters
518 ----------
519 im : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage` or `lsst.afw.image.Exposure`
520 An image of a chip, ready to be binned and maybe rotated.
521 ccd : `lsst.afw.cameraGeom.Detector` or `None`
522 The Detector; if `None` assume that im is an exposure and extract its Detector.
523 imageSource : `FakeImageDataSource` or `None`
524 Source to get ccd images. Must have a `getCcdImage()` method.
525 correctGain : `bool`
526 Correct each amplifier for its gain?
527 subtractBias : `bool`
528 Subtract the bias from each amplifier?
529 convertToFloat : `bool`
530 Convert ``im`` to floating point if possible.
531 obeyNQuarter : `bool`
532 Obey nQuarter from the Detector (default: True)
533
534 Returns
535 -------
536 image : `lsst.afw.image.Image` like
537 The constructed image (type returned by ``im.Factory``).
538
539 Notes
540 -----
541 If imageSource is derived from ButlerImage, imageSource.butler is available.
542 """
543 if ccd is None:
544 ccd = im.getDetector()
545 if hasattr(im, "getMaskedImage"):
546 im = im.getMaskedImage()
547 if convertToFloat and hasattr(im, "convertF"):
548 im = im.convertF()
549
550 isTrimmed = imageSource.isTrimmed
551 if isTrimmed:
552 bbox = ccd.getBBox()
553 else:
554 bbox = calcRawCcdBBox(ccd)
555
556 ampImages = []
557 for a in ccd:
558 if isTrimmed:
559 data = im[a.getRawDataBBox()]
560 else:
561 data = im
562
563 if subtractBias:
564 bias = im[a.getRawHorizontalOverscanBBox()]
565 data -= afwMath.makeStatistics(bias, afwMath.MEANCLIP).getValue()
566 if correctGain:
567 data *= a.getGain()
568
569 ampImages.append(data)
570
571 ccdImage = im.Factory(bbox)
572 for ampImage, amp in zip(ampImages, ccd):
573 if isTrimmed:
574 assembleAmplifierImage(ccdImage, ampImage, amp)
575 else:
576 assembleAmplifierRawImage(ccdImage, ampImage, amp)
577
578 if obeyNQuarter:
579 nQuarter = ccd.getOrientation().getNQuarter()
580 ccdImage = afwMath.rotateImageBy90(ccdImage, nQuarter)
581
582 return ccdImage
583
584
585def overlayCcdBoxes(ccd, untrimmedCcdBbox=None, nQuarter=0,
586 isTrimmed=False, ccdOrigin=(0, 0), display=None, binSize=1):
587 """Overlay bounding boxes on an image display.
588
589 Parameters
590 ----------
591 ccd : `lsst.afw.cameraGeom.Detector`
592 Detector to iterate for the amp bounding boxes.
593 untrimmedCcdBbox : `lsst.geom.Box2I` or `None`
594 Bounding box of the un-trimmed Detector.
595 nQuarter : `int`
596 number of 90 degree rotations to apply to the bounding boxes (used for rotated chips).
597 isTrimmed : `bool`
598 Is the Detector image over which the boxes are layed trimmed?
599 ccdOrigin : `tuple` of `float`
600 Detector origin relative to the parent origin if in a larger pixel grid.
601 display : `lsst.afw.display.Display`
602 Image display to display on.
603 binSize : `int`
604 Bin the image by this factor in both dimensions.
605
606 Notes
607 -----
608 The colours are:
609 - Entire detector GREEN
610 - All data for amp GREEN
611 - HorizontalPrescan YELLOW
612 - HorizontalOverscan RED
613 - Data BLUE
614 - VerticalOverscan MAGENTA
615 - VerticalOverscan MAGENTA
616 """
617 if not display: # should be second parameter, and not defaulted!!
618 raise RuntimeError("Please specify a display")
619
620 if untrimmedCcdBbox is None:
621 if isTrimmed:
622 untrimmedCcdBbox = ccd.getBBox()
623 else:
624 untrimmedCcdBbox = lsst.geom.Box2I()
625 for a in ccd.getAmplifiers():
626 bbox = a.getRawBBox()
627 untrimmedCcdBbox.include(bbox)
628
629 with display.Buffering():
630 ccdDim = untrimmedCcdBbox.getDimensions()
631 ccdBbox = rotateBBoxBy90(untrimmedCcdBbox, nQuarter, ccdDim)
632 for amp in ccd:
633 if isTrimmed:
634 ampbbox = amp.getBBox()
635 else:
636 ampbbox = amp.getRawBBox()
637 if nQuarter != 0:
638 ampbbox = rotateBBoxBy90(ampbbox, nQuarter, ccdDim)
639
640 displayUtils.drawBBox(ampbbox, origin=ccdOrigin, borderWidth=0.49,
641 display=display, bin=binSize)
642
643 if not isTrimmed:
644 for bbox, ctype in ((amp.getRawHorizontalOverscanBBox(), afwDisplay.RED),
645 (amp.getRawDataBBox(), afwDisplay.BLUE),
646 (amp.getRawVerticalOverscanBBox(),
647 afwDisplay.MAGENTA),
648 (amp.getRawPrescanBBox(), afwDisplay.YELLOW)):
649 if nQuarter != 0:
650 bbox = rotateBBoxBy90(bbox, nQuarter, ccdDim)
651 displayUtils.drawBBox(bbox, origin=ccdOrigin, borderWidth=0.49, ctype=ctype,
652 display=display, bin=binSize)
653 # Label each Amp
654 xc, yc = ((ampbbox.getMin()[0] + ampbbox.getMax()[0])//2,
655 (ampbbox.getMin()[1] + ampbbox.getMax()[1])//2)
656 #
657 # Rotate the amp labels too
658 #
659 if nQuarter == 0:
660 c, s = 1, 0
661 elif nQuarter == 1:
662 c, s = 0, -1
663 elif nQuarter == 2:
664 c, s = -1, 0
665 elif nQuarter == 3:
666 c, s = 0, 1
667 c, s = 1, 0
668 ccdHeight = ccdBbox.getHeight()
669 ccdWidth = ccdBbox.getWidth()
670 xc -= 0.5*ccdHeight
671 yc -= 0.5*ccdWidth
672
673 xc, yc = 0.5*ccdHeight + c*xc + s*yc, 0.5*ccdWidth + -s*xc + c*yc
674
675 if ccdOrigin:
676 xc += ccdOrigin[0]
677 yc += ccdOrigin[1]
678 display.dot(str(amp.getName()), xc/binSize,
679 yc/binSize, textAngle=nQuarter*90)
680
681 displayUtils.drawBBox(ccdBbox, origin=ccdOrigin,
682 borderWidth=0.49, ctype=afwDisplay.MAGENTA, display=display, bin=binSize)
683
684
685def showAmp(amp, imageSource=FakeImageDataSource(isTrimmed=False), display=None, overlay=True,
686 imageFactory=afwImage.ImageU):
687 """Show an amp in an image display.
688
689 Parameters
690 ----------
691 amp : `lsst.afw.tables.AmpInfoRecord`
692 Amp record to use in display.
693 imageSource : `FakeImageDataSource` or `None`
694 Source for getting the amp image. Must have a ``getAmpImage()`` method.
695 display : `lsst.afw.display.Display`
696 Image display to use.
697 overlay : `bool`
698 Overlay bounding boxes?
699 imageFactory : callable like `lsst.afw.image.Image`
700 Type of image to display (only used if ampImage is `None`).
701 """
702 if not display:
703 display = _getDisplayFromDisplayOrFrame(display)
704
705 ampImage = imageSource.getAmpImage(amp, imageFactory=imageFactory)
706 ampImSize = ampImage.getDimensions()
707 title = amp.getName()
708 display.mtv(ampImage, title=title)
709 if overlay:
710 with display.Buffering():
711 if ampImSize == amp.getRawBBox().getDimensions():
712 bboxes = [(amp.getRawBBox(), 0.49, afwDisplay.GREEN), ]
713 xy0 = bboxes[0][0].getMin()
714 bboxes.append(
715 (amp.getRawHorizontalOverscanBBox(), 0.49, afwDisplay.RED))
716 bboxes.append((amp.getRawDataBBox(), 0.49, afwDisplay.BLUE))
717 bboxes.append((amp.getRawPrescanBBox(),
718 0.49, afwDisplay.YELLOW))
719 bboxes.append((amp.getRawVerticalOverscanBBox(),
720 0.49, afwDisplay.MAGENTA))
721 else:
722 bboxes = [(amp.getBBox(), 0.49, None), ]
723 xy0 = bboxes[0][0].getMin()
724
725 for bbox, borderWidth, ctype in bboxes:
726 if bbox.isEmpty():
727 continue
728 bbox = lsst.geom.Box2I(bbox)
729 bbox.shift(-lsst.geom.ExtentI(xy0))
730 displayUtils.drawBBox(
731 bbox, borderWidth=borderWidth, ctype=ctype, display=display)
732
733
734def showCcd(ccd, imageSource=FakeImageDataSource(), display=None, overlay=True,
735 imageFactory=afwImage.ImageF, binSize=1, inCameraCoords=False):
736 """Show a CCD on display.
737
738 Parameters
739 ----------
740 ccd : `lsst.afw.cameraGeom.Detector`
741 Detector to use in display.
742 imageSource : `FakeImageDataSource` or `None`
743 Source to get ccd images. Must have a ``getCcdImage()`` method.
744 display : `lsst.afw.display.Display`
745 image display to use.
746 overlay : `bool`
747 Show amp bounding boxes on the displayed image?
748 imageFactory : callable like `lsst.afw.image.Image`
749 The image factory to use in generating the images.
750 binSize : `int`
751 Bin the image by this factor in both dimensions.
752 inCameraCoords : `bool`
753 Show the Detector in camera coordinates?
754 """
755 display = _getDisplayFromDisplayOrFrame(display)
756
757 ccdOrigin = lsst.geom.Point2I(0, 0)
758 nQuarter = 0
759 ccdImage, ccd = imageSource.getCcdImage(
760 ccd, imageFactory=imageFactory, binSize=binSize)
761
762 ccdBbox = ccdImage.getBBox()
763 if ccdBbox.getDimensions() == ccd.getBBox().getDimensions():
764 isTrimmed = True
765 else:
766 isTrimmed = False
767
768 if inCameraCoords:
769 nQuarter = ccd.getOrientation().getNQuarter()
770 ccdImage = afwMath.rotateImageBy90(ccdImage, nQuarter)
771 title = ccd.getName()
772 if isTrimmed:
773 title += "(trimmed)"
774
775 if display:
776 display.mtv(ccdImage, title=title)
777
778 if overlay:
779 overlayCcdBoxes(ccd, ccdBbox, nQuarter, isTrimmed,
780 ccdOrigin, display, binSize)
781
782 return ccdImage
783
784
785def getCcdInCamBBoxList(ccdList, binSize, pixelSize_o, origin):
786 """Get the bounding boxes of a list of Detectors within a camera sized pixel grid
787
788 Parameters
789 ----------
790 ccdList : `lsst.afw.cameraGeom.Detector`
791 List of Detector.
792 binSize : `int`
793 Bin the image by this factor in both dimensions.
794 pixelSize_o : `float`
795 Size of the pixel in mm.
796 origin : `int`
797 Origin of the camera pixel grid in pixels.
798
799 Returns
800 -------
801 boxList : `list` [`lsst.geom.Box2I`]
802 A list of bounding boxes in camera pixel coordinates.
803 """
804 boxList = []
805 for ccd in ccdList:
806 if not pixelSize_o == ccd.getPixelSize():
807 raise RuntimeError(
808 "Cameras with detectors with different pixel scales are not currently supported")
809
810 dbbox = lsst.geom.Box2D()
811 for corner in ccd.getCorners(FOCAL_PLANE):
812 dbbox.include(corner)
813 llc = dbbox.getMin()
814 nQuarter = ccd.getOrientation().getNQuarter()
815 cbbox = ccd.getBBox()
816 ex = cbbox.getDimensions().getX()//binSize
817 ey = cbbox.getDimensions().getY()//binSize
818 bbox = lsst.geom.Box2I(
819 cbbox.getMin(), lsst.geom.Extent2I(int(ex), int(ey)))
820 bbox = rotateBBoxBy90(bbox, nQuarter, bbox.getDimensions())
821 bbox.shift(lsst.geom.Extent2I(int(llc.getX()//pixelSize_o.getX()/binSize),
822 int(llc.getY()//pixelSize_o.getY()/binSize)))
823 bbox.shift(lsst.geom.Extent2I(-int(origin.getX()//binSize),
824 -int(origin.getY())//binSize))
825 boxList.append(bbox)
826 return boxList
827
828
829def getCameraImageBBox(camBbox, pixelSize, bufferSize):
830 """Get the bounding box of a camera sized image in pixels
831
832 Parameters
833 ----------
834 camBbox : `lsst.geom.Box2D`
835 Camera bounding box in focal plane coordinates (mm).
836 pixelSize : `float`
837 Size of a detector pixel in mm.
838 bufferSize : `int`
839 Buffer around edge of image in pixels.
840
841 Returns
842 -------
843 box : `lsst.geom.Box2I`
844 The resulting bounding box.
845 """
846 pixMin = lsst.geom.Point2I(int(camBbox.getMinX()//pixelSize.getX()),
847 int(camBbox.getMinY()//pixelSize.getY()))
848 pixMax = lsst.geom.Point2I(int(camBbox.getMaxX()//pixelSize.getX()),
849 int(camBbox.getMaxY()//pixelSize.getY()))
850 retBox = lsst.geom.Box2I(pixMin, pixMax)
851 retBox.grow(bufferSize)
852 return retBox
853
854
855def makeImageFromCamera(camera, detectorNameList=None, background=numpy.nan, bufferSize=10,
856 imageSource=FakeImageDataSource(), imageFactory=afwImage.ImageU, binSize=1):
857 """Make an Image of a Camera.
858
859 Put each detector's image in the correct location and orientation on the
860 focal plane. The input images can be binned to an integer fraction of their
861 original bboxes.
862
863 Parameters
864 ----------
865 camera : `lsst.afw.cameraGeom.Camera`
866 Camera object to use to make the image.
867 detectorNameList : `list` [`str`]
868 List of detector names from ``camera`` to use in building the image.
869 Use all Detectors if `None`.
870 background : `float`
871 Value to use where there is no Detector.
872 bufferSize : `int`
873 Size of border in binned pixels to make around the camera image.
874 imageSource : `FakeImageDataSource` or `None`
875 Source to get ccd images. Must have a ``getCcdImage()`` method.
876 imageFactory : callable like `lsst.afw.image.Image`
877 Type of image to build.
878 binSize : `int`
879 Bin the image by this factor in both dimensions.
880
881 Returns
882 -------
883 image : `lsst.afw.image.Image`
884 Image of the entire camera.
885 """
886 log = _LOG.getChild("makeImageFromCamera")
887
888 if detectorNameList is None:
889 ccdList = camera
890 else:
891 ccdList = [camera[name] for name in detectorNameList]
892
893 if detectorNameList is None:
894 camBbox = camera.getFpBBox()
895 else:
896 camBbox = lsst.geom.Box2D()
897 for detName in detectorNameList:
898 for corner in camera[detName].getCorners(FOCAL_PLANE):
899 camBbox.include(corner)
900
901 pixelSize_o = camera[next(camera.getNameIter())].getPixelSize()
902 camBbox = getCameraImageBBox(camBbox, pixelSize_o, bufferSize*binSize)
903 origin = camBbox.getMin()
904
905 camIm = imageFactory(int(math.ceil(camBbox.getDimensions().getX()/binSize)),
906 int(math.ceil(camBbox.getDimensions().getY()/binSize)))
907 camIm[:] = imageSource.background
908
909 assert imageSource.isTrimmed, "isTrimmed is False isn't supported by getCcdInCamBBoxList"
910
911 boxList = getCcdInCamBBoxList(ccdList, binSize, pixelSize_o, origin)
912 for det, bbox in zip(ccdList, boxList):
913 im = imageSource.getCcdImage(det, imageFactory, binSize)[0]
914 if im is None:
915 continue
916
917 imView = camIm.Factory(camIm, bbox, afwImage.LOCAL)
918 try:
919 imView[:] = im
921 log.exception("Unable to fit image for detector \"%s\" into image of camera.", det.getName())
922
923 return camIm
924
925
926def showCamera(camera, imageSource=FakeImageDataSource(), imageFactory=afwImage.ImageF,
927 detectorNameList=None, binSize=10, bufferSize=10, overlay=True, title="",
928 showWcs=None, ctype=afwDisplay.GREEN, textSize=1.25, originAtCenter=True, display=None,
929 **kwargs):
930 """Show a Camera on display, with the specified display.
931
932 The rotation of the sensors is snapped to the nearest multiple of 90 deg.
933 Also note that the pixel size is constant over the image array. The lower
934 left corner (LLC) of each sensor amp is snapped to the LLC of the pixel
935 containing the LLC of the image.
936
937 Parameters
938 ----------
939 camera : `lsst.afw.cameraGeom.Camera`
940 Camera object to use to make the image.
941 imageSource : `FakeImageDataSource` or `None`
942 Source to get ccd images. Must have a ``getCcdImage()`` method.
943 imageFactory : `lsst.afw.image.Image`
944 Type of image to make
945 detectorNameList : `list` [`str`] or `None`
946 List of detector names from `camera` to use in building the image.
947 Use all Detectors if `None`.
948 binSize : `int`
949 Bin the image by this factor in both dimensions.
950 bufferSize : `int`
951 Size of border in binned pixels to make around the camera image.
952 overlay : `bool`
953 Overlay Detector IDs and boundaries?
954 title : `str`
955 Title to use in display.
956 showWcs : `bool`
957 Include a WCS in the display?
958 ctype : `lsst.afw.display.COLOR` or `str`
959 Color to use when drawing Detector boundaries.
960 textSize : `float`
961 Size of detector labels
962 originAtCenter : `bool`
963 Put origin of the camera WCS at the center of the image?
964 If `False`, the origin will be at the lower left.
965 display : `lsst.afw.display`
966 Image display on which to display.
967 **kwargs :
968 All remaining keyword arguments are passed to makeImageFromCamera
969
970 Returns
971 -------
972 image : `lsst.afw.image.Image`
973 The mosaic image.
974 """
975 display = _getDisplayFromDisplayOrFrame(display)
976
977 if binSize < 1:
978 binSize = 1
979 cameraImage = makeImageFromCamera(camera, detectorNameList=detectorNameList, bufferSize=bufferSize,
980 imageSource=imageSource, imageFactory=imageFactory, binSize=binSize,
981 **kwargs)
982
983 if detectorNameList is None:
984 ccdList = [camera[name] for name in camera.getNameIter()]
985 else:
986 ccdList = [camera[name] for name in detectorNameList]
987
988 if detectorNameList is None:
989 camBbox = camera.getFpBBox()
990 else:
991 camBbox = lsst.geom.Box2D()
992 for detName in detectorNameList:
993 for corner in camera[detName].getCorners(FOCAL_PLANE):
994 camBbox.include(corner)
995 pixelSize = ccdList[0].getPixelSize()
996
997 if showWcs:
998 if originAtCenter:
999 wcsReferencePixel = lsst.geom.Box2D(
1000 cameraImage.getBBox()).getCenter()
1001 else:
1002 wcsReferencePixel = lsst.geom.Point2I(0, 0)
1003 wcs = makeFocalPlaneWcs(pixelSize*binSize, wcsReferencePixel)
1004 else:
1005 wcs = None
1006
1007 if display:
1008 if title == "":
1009 title = camera.getName()
1010 display.mtv(cameraImage, title=title, wcs=wcs)
1011
1012 if overlay:
1013 with display.Buffering():
1014 camBbox = getCameraImageBBox(
1015 camBbox, pixelSize, bufferSize*binSize)
1016 bboxList = getCcdInCamBBoxList(
1017 ccdList, binSize, pixelSize, camBbox.getMin())
1018 for bbox, ccd in zip(bboxList, ccdList):
1019 nQuarter = ccd.getOrientation().getNQuarter()
1020 # borderWidth to 0.5 to align with the outside edge of the
1021 # pixel
1022 displayUtils.drawBBox(
1023 bbox, borderWidth=0.5, ctype=ctype, display=display)
1024 dims = bbox.getDimensions()
1025 display.dot(ccd.getName(), bbox.getMinX() + dims.getX()/2, bbox.getMinY() + dims.getY()/2,
1026 ctype=ctype, size=textSize, textAngle=nQuarter*90)
1027
1028 return cameraImage
1029
1030
1031def makeFocalPlaneWcs(pixelSize, referencePixel):
1032 """Make a WCS for the focal plane geometry
1033 (i.e. one that returns positions in "mm")
1034
1035 Parameters
1036 ----------
1037 pixelSize : `float`
1038 Size of the image pixels in physical units
1039 referencePixel : `lsst.geom.Point2D`
1040 Pixel for origin of WCS
1041
1042 Returns
1043 -------
1044 `lsst.afw.geom.Wcs`
1045 Wcs object for mapping between pixels and focal plane.
1046 """
1047 md = dafBase.PropertySet()
1048 if referencePixel is None:
1049 referencePixel = lsst.geom.PointD(0, 0)
1050 for i in range(2):
1051 md.set("CRPIX%d"%(i + 1), referencePixel[i])
1052 md.set("CRVAL%d"%(i + 1), 0.)
1053 md.set("CDELT1", pixelSize[0])
1054 md.set("CDELT2", pixelSize[1])
1055 md.set("CTYPE1", "CAMERA_X")
1056 md.set("CTYPE2", "CAMERA_Y")
1057 md.set("CUNIT1", "mm")
1058 md.set("CUNIT2", "mm")
1059
1060 return afwGeom.makeSkyWcs(md)
1061
1062
1063def findAmp(ccd, pixelPosition):
1064 """Find the Amp with the specified pixel position within the composite
1065
1066 Parameters
1067 ----------
1068 ccd : `lsst.afw.cameraGeom.Detector`
1069 Detector to look in.
1070 pixelPosition : `lsst.geom.Point2I`
1071 The pixel position to find the amp for.
1072
1073 Returns
1074 -------
1075 `lsst.afw.table.AmpInfoCatalog`
1076 Amp record in which ``pixelPosition`` falls or `None` if no Amp found.
1077 """
1078 for amp in ccd:
1079 if amp.getBBox().contains(pixelPosition):
1080 return amp
1081
1082 return None
int min
int max
_prepareImage(self, ccd, im, binSize, allowRotate=True)
Definition utils.py:445
__init__(self, butler=None, type="raw", isTrimmed=True, verbose=False, background=numpy.nan, callback=None, *args, **kwargs)
Definition utils.py:434
getCcdImage(self, ccd, imageFactory=afwImage.ImageF, binSize=1, asMaskedImage=False)
Definition utils.py:455
__init__(self, isTrimmed=True, verbose=False, background=numpy.nan, showAmpGain=True, markSize=10, markValue=0, ampImValue=None, scaleGain=lambda gain:(gain *1000)//10)
Definition utils.py:345
getCcdImage(self, det, imageFactory, binSize)
Definition utils.py:355
Class for storing generic metadata.
Definition PropertySet.h:66
A floating-point coordinate rectangle geometry.
Definition Box.h:413
An integer coordinate rectangle.
Definition Box.h:55
Reports attempts to exceed implementation-defined length limits for some classes.
Definition Runtime.h:76
findAmp(ccd, pixelPosition)
Definition utils.py:1063
overlayCcdBoxes(ccd, untrimmedCcdBbox=None, nQuarter=0, isTrimmed=False, ccdOrigin=(0, 0), display=None, binSize=1)
Definition utils.py:586
getCcdInCamBBoxList(ccdList, binSize, pixelSize_o, origin)
Definition utils.py:785
showAmp(amp, imageSource=FakeImageDataSource(isTrimmed=False), display=None, overlay=True, imageFactory=afwImage.ImageU)
Definition utils.py:686
showCcd(ccd, imageSource=FakeImageDataSource(), display=None, overlay=True, imageFactory=afwImage.ImageF, binSize=1, inCameraCoords=False)
Definition utils.py:735
makeImageFromCcd(ccd, isTrimmed=True, showAmpGain=True, imageFactory=afwImage.ImageU, rcMarkSize=10, binSize=1)
Definition utils.py:267
plotFocalPlane(camera, fieldSizeDeg_x=0, fieldSizeDeg_y=None, dx=0.1, dy=0.1, figsize=(10., 10.), useIds=False, showFig=True, savePath=None)
Definition utils.py:89
rawCallback(im, ccd=None, imageSource=None, correctGain=False, subtractBias=False, convertToFloat=False, obeyNQuarter=True)
Definition utils.py:513
prepareWcsData(wcs, amp, isTrimmed=True)
Definition utils.py:56
getCameraImageBBox(camBbox, pixelSize, bufferSize)
Definition utils.py:829
makeImageFromCamera(camera, detectorNameList=None, background=numpy.nan, bufferSize=10, imageSource=FakeImageDataSource(), imageFactory=afwImage.ImageU, binSize=1)
Definition utils.py:856
showCamera(camera, imageSource=FakeImageDataSource(), imageFactory=afwImage.ImageF, detectorNameList=None, binSize=10, bufferSize=10, overlay=True, title="", showWcs=None, ctype=afwDisplay.GREEN, textSize=1.25, originAtCenter=True, display=None, **kwargs)
Definition utils.py:929
makeFocalPlaneWcs(pixelSize, referencePixel)
Definition utils.py:1031
makeImageFromAmp(amp, imValue=None, imageFactory=afwImage.ImageU, markSize=10, markValue=0, scaleGain=lambda gain:(gain *1000)//10)
Definition utils.py:186
std::shared_ptr< SkyWcs > makeSkyWcs(daf::base::PropertySet &metadata, bool strip=false)
Construct a SkyWcs from FITS keywords.
Definition SkyWcs.cc:521
std::shared_ptr< SkyWcs > makeFlippedWcs(SkyWcs const &wcs, bool flipLR, bool flipTB, lsst::geom::Point2D const &center)
Return a copy of a FITS-WCS with pixel positions flipped around a specified center.
Definition SkyWcs.cc:467
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)
Definition Statistics.h:361
std::shared_ptr< ImageT > rotateImageBy90(ImageT const &image, int nQuarter)
Rotate an image by an integral number of quarter turns.
std::shared_ptr< ImageT > binImage(ImageT const &inImage, int const binX, int const binY, lsst::afw::math::Property const flags=lsst::afw::math::MEAN)
Definition binImage.cc:44