LSST Applications g063fba187b+cac8b7c890,g0f08755f38+6aee506743,g1653933729+a8ce1bb630,g168dd56ebc+a8ce1bb630,g1a2382251a+b4475c5878,g1dcb35cd9c+8f9bc1652e,g20f6ffc8e0+6aee506743,g217e2c1bcf+73dee94bd0,g28da252d5a+1f19c529b9,g2bbee38e9b+3f2625acfc,g2bc492864f+3f2625acfc,g3156d2b45e+6e55a43351,g32e5bea42b+1bb94961c2,g347aa1857d+3f2625acfc,g35bb328faa+a8ce1bb630,g3a166c0a6a+3f2625acfc,g3e281a1b8c+c5dd892a6c,g3e8969e208+a8ce1bb630,g414038480c+5927e1bc1e,g41af890bb2+8a9e676b2a,g7af13505b9+809c143d88,g80478fca09+6ef8b1810f,g82479be7b0+f568feb641,g858d7b2824+6aee506743,g89c8672015+f4add4ffd5,g9125e01d80+a8ce1bb630,ga5288a1d22+2903d499ea,gb58c049af0+d64f4d3760,gc28159a63d+3f2625acfc,gcab2d0539d+b12535109e,gcf0d15dbbd+46a3f46ba9,gda6a2b7d83+46a3f46ba9,gdaeeff99f8+1711a396fd,ge79ae78c31+3f2625acfc,gef2f8181fd+0a71e47438,gf0baf85859+c1f95f4921,gfa517265be+6aee506743,gfa999e8aa5+17cd334064,w.2024.51
LSST Data Management Base Package
Loading...
Searching...
No Matches
isrFunctions.py
Go to the documentation of this file.
2# LSST Data Management System
3# Copyright 2008, 2009, 2010 LSST Corporation.
4#
5# This product includes software developed by the
6# LSST Project (http://www.lsst.org/).
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the LSST License Statement and
19# the GNU General Public License along with this program. If not,
20# see <http://www.lsstcorp.org/LegalNotices/>.
21#
22
23__all__ = [
24 "applyGains",
25 "attachTransmissionCurve",
26 "biasCorrection",
27 "brighterFatterCorrection",
28 "checkFilter",
29 "countMaskedPixels",
30 "createPsf",
31 "darkCorrection",
32 "flatCorrection",
33 "fluxConservingBrighterFatterCorrection",
34 "gainContext",
35 "getPhysicalFilter",
36 "growMasks",
37 "illuminationCorrection",
38 "interpolateDefectList",
39 "interpolateFromMask",
40 "makeThresholdMask",
41 "saturationCorrection",
42 "setBadRegions",
43 "transferFlux",
44 "transposeMaskedImage",
45 "trimToMatchCalibBBox",
46 "updateVariance",
47 "widenSaturationTrails",
48 "getExposureGains",
49 "getExposureReadNoises",
50]
51
52import math
53import numpy
54
55import lsst.geom
56import lsst.afw.image as afwImage
57import lsst.afw.detection as afwDetection
58import lsst.afw.math as afwMath
59import lsst.meas.algorithms as measAlg
60import lsst.afw.cameraGeom as camGeom
61
62from lsst.meas.algorithms.detection import SourceDetectionTask
63
64from contextlib import contextmanager
65
66from .defects import Defects
67
68
69def createPsf(fwhm):
70 """Make a double Gaussian PSF.
71
72 Parameters
73 ----------
74 fwhm : scalar
75 FWHM of double Gaussian smoothing kernel.
76
77 Returns
78 -------
79 psf : `lsst.meas.algorithms.DoubleGaussianPsf`
80 The created smoothing kernel.
81 """
82 ksize = 4*int(fwhm) + 1
83 return measAlg.DoubleGaussianPsf(ksize, ksize, fwhm/(2*math.sqrt(2*math.log(2))))
84
85
86def transposeMaskedImage(maskedImage):
87 """Make a transposed copy of a masked image.
88
89 Parameters
90 ----------
91 maskedImage : `lsst.afw.image.MaskedImage`
92 Image to process.
93
94 Returns
95 -------
96 transposed : `lsst.afw.image.MaskedImage`
97 The transposed copy of the input image.
98 """
99 transposed = maskedImage.Factory(lsst.geom.Extent2I(maskedImage.getHeight(), maskedImage.getWidth()))
100 transposed.getImage().getArray()[:] = maskedImage.getImage().getArray().T
101 transposed.getMask().getArray()[:] = maskedImage.getMask().getArray().T
102 transposed.getVariance().getArray()[:] = maskedImage.getVariance().getArray().T
103 return transposed
104
105
106def interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=None,
107 maskNameList=None, useLegacyInterp=True):
108 """Interpolate over defects specified in a defect list.
109
110 Parameters
111 ----------
112 maskedImage : `lsst.afw.image.MaskedImage`
113 Image to process.
114 defectList : `lsst.meas.algorithms.Defects`
115 List of defects to interpolate over.
116 fwhm : `float`
117 FWHM of double Gaussian smoothing kernel.
118 fallbackValue : scalar, optional
119 Fallback value if an interpolated value cannot be determined.
120 If None, then the clipped mean of the image is used.
121 maskNameList : `list [string]`
122 List of the defects to interpolate over (used for GP interpolator).
123 useLegacyInterp : `bool`
124 Use the legacy interpolation (polynomial interpolation) if True. Use
125 Gaussian Process interpolation if False.
126
127 Notes
128 -----
129 The ``fwhm`` parameter is used to create a PSF, but the underlying
130 interpolation code (`lsst.meas.algorithms.interpolateOverDefects`) does
131 not currently make use of this information in legacy Interpolation, but use
132 if for the Gaussian Process as an estimation of the correlation lenght.
133 """
134 psf = createPsf(fwhm)
135 if fallbackValue is None:
136 fallbackValue = afwMath.makeStatistics(maskedImage.getImage(), afwMath.MEANCLIP).getValue()
137 if 'INTRP' not in maskedImage.getMask().getMaskPlaneDict():
138 maskedImage.getMask().addMaskPlane('INTRP')
139
140 # Hardcoded fwhm value. PSF estimated latter in step1,
141 # not in ISR.
142 if useLegacyInterp:
143 kwargs = {}
144 fwhm = fwhm
145 else:
146 # tested on a dozens of images and looks a good set of
147 # hyperparameters, but cannot guarrenty this is optimal,
148 # need further testing.
149 kwargs = {"bin_spacing": 20,
150 "threshold_dynamic_binning": 2000,
151 "threshold_subdivide": 20000}
152 fwhm = 15
153
154 measAlg.interpolateOverDefects(maskedImage, psf, defectList,
155 fallbackValue=fallbackValue,
156 useFallbackValueAtEdge=True,
157 fwhm=fwhm,
158 useLegacyInterp=useLegacyInterp,
159 maskNameList=maskNameList, **kwargs)
160 return maskedImage
161
162
163def makeThresholdMask(maskedImage, threshold, growFootprints=1, maskName='SAT'):
164 """Mask pixels based on threshold detection.
165
166 Parameters
167 ----------
168 maskedImage : `lsst.afw.image.MaskedImage`
169 Image to process. Only the mask plane is updated.
170 threshold : scalar
171 Detection threshold.
172 growFootprints : scalar, optional
173 Number of pixels to grow footprints of detected regions.
174 maskName : str, optional
175 Mask plane name, or list of names to convert
176
177 Returns
178 -------
179 defectList : `lsst.meas.algorithms.Defects`
180 Defect list constructed from pixels above the threshold.
181 """
182 # find saturated regions
183 thresh = afwDetection.Threshold(threshold)
184 fs = afwDetection.FootprintSet(maskedImage, thresh)
185
186 if growFootprints > 0:
187 fs = afwDetection.FootprintSet(fs, rGrow=growFootprints, isotropic=False)
188 fpList = fs.getFootprints()
189
190 # set mask
191 mask = maskedImage.getMask()
192 bitmask = mask.getPlaneBitMask(maskName)
193 afwDetection.setMaskFromFootprintList(mask, fpList, bitmask)
194
195 return Defects.fromFootprintList(fpList)
196
197
198def growMasks(mask, radius=0, maskNameList=['BAD'], maskValue="BAD"):
199 """Grow a mask by an amount and add to the requested plane.
200
201 Parameters
202 ----------
203 mask : `lsst.afw.image.Mask`
204 Mask image to process.
205 radius : scalar
206 Amount to grow the mask.
207 maskNameList : `str` or `list` [`str`]
208 Mask names that should be grown.
209 maskValue : `str`
210 Mask plane to assign the newly masked pixels to.
211 """
212 if radius > 0:
213 thresh = afwDetection.Threshold(mask.getPlaneBitMask(maskNameList), afwDetection.Threshold.BITMASK)
214 fpSet = afwDetection.FootprintSet(mask, thresh)
215 fpSet = afwDetection.FootprintSet(fpSet, rGrow=radius, isotropic=False)
216 fpSet.setMask(mask, maskValue)
217
218
219def interpolateFromMask(maskedImage, fwhm, growSaturatedFootprints=1,
220 maskNameList=['SAT'], fallbackValue=None, useLegacyInterp=True):
221 """Interpolate over defects identified by a particular set of mask planes.
222
223 Parameters
224 ----------
225 maskedImage : `lsst.afw.image.MaskedImage`
226 Image to process.
227 fwhm : `float`
228 FWHM of double Gaussian smoothing kernel.
229 growSaturatedFootprints : scalar, optional
230 Number of pixels to grow footprints for saturated pixels.
231 maskNameList : `List` of `str`, optional
232 Mask plane name.
233 fallbackValue : scalar, optional
234 Value of last resort for interpolation.
235
236 Notes
237 -----
238 The ``fwhm`` parameter is used to create a PSF, but the underlying
239 interpolation code (`lsst.meas.algorithms.interpolateOverDefects`) does
240 not currently make use of this information.
241 """
242 mask = maskedImage.getMask()
243
244 if growSaturatedFootprints > 0 and "SAT" in maskNameList:
245 # If we are interpolating over an area larger than the original masked
246 # region, we need to expand the original mask bit to the full area to
247 # explain why we interpolated there.
248 growMasks(mask, radius=growSaturatedFootprints, maskNameList=['SAT'], maskValue="SAT")
249
250 thresh = afwDetection.Threshold(mask.getPlaneBitMask(maskNameList), afwDetection.Threshold.BITMASK)
251 fpSet = afwDetection.FootprintSet(mask, thresh)
252 defectList = Defects.fromFootprintList(fpSet.getFootprints())
253
254 interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=fallbackValue,
255 maskNameList=maskNameList, useLegacyInterp=useLegacyInterp)
256
257 return maskedImage
258
259
260def saturationCorrection(maskedImage, saturation, fwhm, growFootprints=1, interpolate=True, maskName='SAT',
261 fallbackValue=None, useLegacyInterp=True):
262 """Mark saturated pixels and optionally interpolate over them
263
264 Parameters
265 ----------
266 maskedImage : `lsst.afw.image.MaskedImage`
267 Image to process.
268 saturation : scalar
269 Saturation level used as the detection threshold.
270 fwhm : `float`
271 FWHM of double Gaussian smoothing kernel.
272 growFootprints : scalar, optional
273 Number of pixels to grow footprints of detected regions.
274 interpolate : Bool, optional
275 If True, saturated pixels are interpolated over.
276 maskName : str, optional
277 Mask plane name.
278 fallbackValue : scalar, optional
279 Value of last resort for interpolation.
280
281 Notes
282 -----
283 The ``fwhm`` parameter is used to create a PSF, but the underlying
284 interpolation code (`lsst.meas.algorithms.interpolateOverDefects`) does
285 not currently make use of this information.
286 """
287 defectList = makeThresholdMask(
288 maskedImage=maskedImage,
289 threshold=saturation,
290 growFootprints=growFootprints,
291 maskName=maskName,
292 )
293 if interpolate:
294 interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=fallbackValue,
295 maskNameList=[maskName], useLegacyInterp=useLegacyInterp)
296
297 return maskedImage
298
299
300def trimToMatchCalibBBox(rawMaskedImage, calibMaskedImage):
301 """Compute number of edge trim pixels to match the calibration data.
302
303 Use the dimension difference between the raw exposure and the
304 calibration exposure to compute the edge trim pixels. This trim
305 is applied symmetrically, with the same number of pixels masked on
306 each side.
307
308 Parameters
309 ----------
310 rawMaskedImage : `lsst.afw.image.MaskedImage`
311 Image to trim.
312 calibMaskedImage : `lsst.afw.image.MaskedImage`
313 Calibration image to draw new bounding box from.
314
315 Returns
316 -------
317 replacementMaskedImage : `lsst.afw.image.MaskedImage`
318 ``rawMaskedImage`` trimmed to the appropriate size.
319
320 Raises
321 ------
322 RuntimeError
323 Raised if ``rawMaskedImage`` cannot be symmetrically trimmed to
324 match ``calibMaskedImage``.
325 """
326 nx, ny = rawMaskedImage.getBBox().getDimensions() - calibMaskedImage.getBBox().getDimensions()
327 if nx != ny:
328 raise RuntimeError("Raw and calib maskedImages are trimmed differently in X and Y.")
329 if nx % 2 != 0:
330 raise RuntimeError("Calibration maskedImage is trimmed unevenly in X.")
331 if nx < 0:
332 raise RuntimeError("Calibration maskedImage is larger than raw data.")
333
334 nEdge = nx//2
335 if nEdge > 0:
336 replacementMaskedImage = rawMaskedImage[nEdge:-nEdge, nEdge:-nEdge, afwImage.LOCAL]
337 SourceDetectionTask.setEdgeBits(
338 rawMaskedImage,
339 replacementMaskedImage.getBBox(),
340 rawMaskedImage.getMask().getPlaneBitMask("EDGE")
341 )
342 else:
343 replacementMaskedImage = rawMaskedImage
344
345 return replacementMaskedImage
346
347
348def biasCorrection(maskedImage, biasMaskedImage, trimToFit=False):
349 """Apply bias correction in place.
350
351 Parameters
352 ----------
353 maskedImage : `lsst.afw.image.MaskedImage`
354 Image to process. The image is modified by this method.
355 biasMaskedImage : `lsst.afw.image.MaskedImage`
356 Bias image of the same size as ``maskedImage``
357 trimToFit : `Bool`, optional
358 If True, raw data is symmetrically trimmed to match
359 calibration size.
360
361 Raises
362 ------
363 RuntimeError
364 Raised if ``maskedImage`` and ``biasMaskedImage`` do not have
365 the same size.
366
367 """
368 if trimToFit:
369 maskedImage = trimToMatchCalibBBox(maskedImage, biasMaskedImage)
370
371 if maskedImage.getBBox(afwImage.LOCAL) != biasMaskedImage.getBBox(afwImage.LOCAL):
372 raise RuntimeError("maskedImage bbox %s != biasMaskedImage bbox %s" %
373 (maskedImage.getBBox(afwImage.LOCAL), biasMaskedImage.getBBox(afwImage.LOCAL)))
374 maskedImage -= biasMaskedImage
375
376
377def darkCorrection(maskedImage, darkMaskedImage, expScale, darkScale, invert=False, trimToFit=False):
378 """Apply dark correction in place.
379
380 Parameters
381 ----------
382 maskedImage : `lsst.afw.image.MaskedImage`
383 Image to process. The image is modified by this method.
384 darkMaskedImage : `lsst.afw.image.MaskedImage`
385 Dark image of the same size as ``maskedImage``.
386 expScale : scalar
387 Dark exposure time for ``maskedImage``.
388 darkScale : scalar
389 Dark exposure time for ``darkMaskedImage``.
390 invert : `Bool`, optional
391 If True, re-add the dark to an already corrected image.
392 trimToFit : `Bool`, optional
393 If True, raw data is symmetrically trimmed to match
394 calibration size.
395
396 Raises
397 ------
398 RuntimeError
399 Raised if ``maskedImage`` and ``darkMaskedImage`` do not have
400 the same size.
401
402 Notes
403 -----
404 The dark correction is applied by calculating:
405 maskedImage -= dark * expScaling / darkScaling
406 """
407 if trimToFit:
408 maskedImage = trimToMatchCalibBBox(maskedImage, darkMaskedImage)
409
410 if maskedImage.getBBox(afwImage.LOCAL) != darkMaskedImage.getBBox(afwImage.LOCAL):
411 raise RuntimeError("maskedImage bbox %s != darkMaskedImage bbox %s" %
412 (maskedImage.getBBox(afwImage.LOCAL), darkMaskedImage.getBBox(afwImage.LOCAL)))
413
414 scale = expScale / darkScale
415 if not invert:
416 maskedImage.scaledMinus(scale, darkMaskedImage)
417 else:
418 maskedImage.scaledPlus(scale, darkMaskedImage)
419
420
421def updateVariance(maskedImage, gain, readNoise):
422 """Set the variance plane based on the image plane.
423
424 The maskedImage must have units of `adu` (if gain != 1.0) or
425 electron (if gain == 1.0). This routine will always produce a
426 variance plane in the same units as the image.
427
428 Parameters
429 ----------
430 maskedImage : `lsst.afw.image.MaskedImage`
431 Image to process. The variance plane is modified.
432 gain : scalar
433 The amplifier gain in electron/adu.
434 readNoise : scalar
435 The amplifier read noise in electron/pixel.
436 """
437 var = maskedImage.getVariance()
438 var[:] = maskedImage.getImage()
439 var /= gain
440 var += (readNoise/gain)**2
441
442
443def flatCorrection(maskedImage, flatMaskedImage, scalingType, userScale=1.0, invert=False, trimToFit=False):
444 """Apply flat correction in place.
445
446 Parameters
447 ----------
448 maskedImage : `lsst.afw.image.MaskedImage`
449 Image to process. The image is modified.
450 flatMaskedImage : `lsst.afw.image.MaskedImage`
451 Flat image of the same size as ``maskedImage``
452 scalingType : str
453 Flat scale computation method. Allowed values are 'MEAN',
454 'MEDIAN', or 'USER'.
455 userScale : scalar, optional
456 Scale to use if ``scalingType='USER'``.
457 invert : `Bool`, optional
458 If True, unflatten an already flattened image.
459 trimToFit : `Bool`, optional
460 If True, raw data is symmetrically trimmed to match
461 calibration size.
462
463 Raises
464 ------
465 RuntimeError
466 Raised if ``maskedImage`` and ``flatMaskedImage`` do not have
467 the same size or if ``scalingType`` is not an allowed value.
468 """
469 if trimToFit:
470 maskedImage = trimToMatchCalibBBox(maskedImage, flatMaskedImage)
471
472 if maskedImage.getBBox(afwImage.LOCAL) != flatMaskedImage.getBBox(afwImage.LOCAL):
473 raise RuntimeError("maskedImage bbox %s != flatMaskedImage bbox %s" %
474 (maskedImage.getBBox(afwImage.LOCAL), flatMaskedImage.getBBox(afwImage.LOCAL)))
475
476 # Figure out scale from the data
477 # Ideally the flats are normalized by the calibration product pipeline,
478 # but this allows some flexibility in the case that the flat is created by
479 # some other mechanism.
480 if scalingType in ('MEAN', 'MEDIAN'):
481 scalingType = afwMath.stringToStatisticsProperty(scalingType)
482 flatScale = afwMath.makeStatistics(flatMaskedImage.image, scalingType).getValue()
483 elif scalingType == 'USER':
484 flatScale = userScale
485 else:
486 raise RuntimeError('%s : %s not implemented' % ("flatCorrection", scalingType))
487
488 if not invert:
489 maskedImage.scaledDivides(1.0/flatScale, flatMaskedImage)
490 else:
491 maskedImage.scaledMultiplies(1.0/flatScale, flatMaskedImage)
492
493
494def illuminationCorrection(maskedImage, illumMaskedImage, illumScale, trimToFit=True):
495 """Apply illumination correction in place.
496
497 Parameters
498 ----------
499 maskedImage : `lsst.afw.image.MaskedImage`
500 Image to process. The image is modified.
501 illumMaskedImage : `lsst.afw.image.MaskedImage`
502 Illumination correction image of the same size as ``maskedImage``.
503 illumScale : scalar
504 Scale factor for the illumination correction.
505 trimToFit : `Bool`, optional
506 If True, raw data is symmetrically trimmed to match
507 calibration size.
508
509 Raises
510 ------
511 RuntimeError
512 Raised if ``maskedImage`` and ``illumMaskedImage`` do not have
513 the same size.
514 """
515 if trimToFit:
516 maskedImage = trimToMatchCalibBBox(maskedImage, illumMaskedImage)
517
518 if maskedImage.getBBox(afwImage.LOCAL) != illumMaskedImage.getBBox(afwImage.LOCAL):
519 raise RuntimeError("maskedImage bbox %s != illumMaskedImage bbox %s" %
520 (maskedImage.getBBox(afwImage.LOCAL), illumMaskedImage.getBBox(afwImage.LOCAL)))
521
522 maskedImage.scaledDivides(1.0/illumScale, illumMaskedImage)
523
524
525def brighterFatterCorrection(exposure, kernel, maxIter, threshold, applyGain, gains=None):
526 """Apply brighter fatter correction in place for the image.
527
528 Parameters
529 ----------
530 exposure : `lsst.afw.image.Exposure`
531 Exposure to have brighter-fatter correction applied. Modified
532 by this method.
533 kernel : `numpy.ndarray`
534 Brighter-fatter kernel to apply.
535 maxIter : scalar
536 Number of correction iterations to run.
537 threshold : scalar
538 Convergence threshold in terms of the sum of absolute
539 deviations between an iteration and the previous one.
540 applyGain : `Bool`
541 If True, then the exposure values are scaled by the gain prior
542 to correction.
543 gains : `dict` [`str`, `float`]
544 A dictionary, keyed by amplifier name, of the gains to use.
545 If gains is None, the nominal gains in the amplifier object are used.
546
547 Returns
548 -------
549 diff : `float`
550 Final difference between iterations achieved in correction.
551 iteration : `int`
552 Number of iterations used to calculate correction.
553
554 Notes
555 -----
556 This correction takes a kernel that has been derived from flat
557 field images to redistribute the charge. The gradient of the
558 kernel is the deflection field due to the accumulated charge.
559
560 Given the original image I(x) and the kernel K(x) we can compute
561 the corrected image Ic(x) using the following equation:
562
563 Ic(x) = I(x) + 0.5*d/dx(I(x)*d/dx(int( dy*K(x-y)*I(y))))
564
565 To evaluate the derivative term we expand it as follows:
566
567 0.5 * ( d/dx(I(x))*d/dx(int(dy*K(x-y)*I(y)))
568 + I(x)*d^2/dx^2(int(dy* K(x-y)*I(y))) )
569
570 Because we use the measured counts instead of the incident counts
571 we apply the correction iteratively to reconstruct the original
572 counts and the correction. We stop iterating when the summed
573 difference between the current corrected image and the one from
574 the previous iteration is below the threshold. We do not require
575 convergence because the number of iterations is too large a
576 computational cost. How we define the threshold still needs to be
577 evaluated, the current default was shown to work reasonably well
578 on a small set of images. For more information on the method see
579 DocuShare Document-19407.
580
581 The edges as defined by the kernel are not corrected because they
582 have spurious values due to the convolution.
583 """
584 image = exposure.getMaskedImage().getImage()
585
586 # The image needs to be units of electrons/holes
587 with gainContext(exposure, image, applyGain, gains):
588
589 kLx = numpy.shape(kernel)[0]
590 kLy = numpy.shape(kernel)[1]
591 kernelImage = afwImage.ImageD(kLx, kLy)
592 kernelImage.getArray()[:, :] = kernel
593 tempImage = image.clone()
594
595 nanIndex = numpy.isnan(tempImage.getArray())
596 tempImage.getArray()[nanIndex] = 0.
597
598 outImage = afwImage.ImageF(image.getDimensions())
599 corr = numpy.zeros_like(image.getArray())
600 prev_image = numpy.zeros_like(image.getArray())
601 convCntrl = afwMath.ConvolutionControl(False, True, 1)
602 fixedKernel = afwMath.FixedKernel(kernelImage)
603
604 # Define boundary by convolution region. The region that the
605 # correction will be calculated for is one fewer in each dimension
606 # because of the second derivative terms.
607 # NOTE: these need to use integer math, as we're using start:end as
608 # numpy index ranges.
609 startX = kLx//2
610 endX = -kLx//2
611 startY = kLy//2
612 endY = -kLy//2
613
614 for iteration in range(maxIter):
615
616 afwMath.convolve(outImage, tempImage, fixedKernel, convCntrl)
617 tmpArray = tempImage.getArray()
618 outArray = outImage.getArray()
619
620 with numpy.errstate(invalid="ignore", over="ignore"):
621 # First derivative term
622 gradTmp = numpy.gradient(tmpArray[startY:endY, startX:endX])
623 gradOut = numpy.gradient(outArray[startY:endY, startX:endX])
624 first = (gradTmp[0]*gradOut[0] + gradTmp[1]*gradOut[1])[1:-1, 1:-1]
625
626 # Second derivative term
627 diffOut20 = numpy.diff(outArray, 2, 0)[startY:endY, startX + 1:endX - 1]
628 diffOut21 = numpy.diff(outArray, 2, 1)[startY + 1:endY - 1, startX:endX]
629 second = tmpArray[startY + 1:endY - 1, startX + 1:endX - 1]*(diffOut20 + diffOut21)
630
631 corr[startY + 1:endY - 1, startX + 1:endX - 1] = 0.5*(first + second)
632
633 tmpArray[:, :] = image.getArray()[:, :]
634 tmpArray[nanIndex] = 0.
635 tmpArray[startY:endY, startX:endX] += corr[startY:endY, startX:endX]
636
637 if iteration > 0:
638 diff = numpy.sum(numpy.abs(prev_image - tmpArray), dtype=numpy.float64)
639
640 if diff < threshold:
641 break
642 prev_image[:, :] = tmpArray[:, :]
643
644 image.getArray()[startY + 1:endY - 1, startX + 1:endX - 1] += \
645 corr[startY + 1:endY - 1, startX + 1:endX - 1]
646
647 return diff, iteration
648
649
650def transferFlux(cFunc, fStep, correctionMode=True):
651 """Take the input convolved deflection potential and the flux array
652 to compute and apply the flux transfer into the correction array.
653
654 Parameters
655 ----------
656 cFunc: `numpy.array`
657 Deflection potential, being the convolution of the flux F with the
658 kernel K.
659 fStep: `numpy.array`
660 The array of flux values which act as the source of the flux transfer.
661 correctionMode: `bool`
662 Defines if applying correction (True) or generating sims (False).
663
664 Returns
665 -------
666 corr:
667 BFE correction array
668 """
669
670 if cFunc.shape != fStep.shape:
671 raise RuntimeError(f'transferFlux: array shapes do not match: {cFunc.shape}, {fStep.shape}')
672
673 # set the sign of the correction and set its value for the
674 # time averaged solution
675 if correctionMode:
676 # negative sign if applying BFE correction
677 factor = -0.5
678 else:
679 # positive sign if generating BFE simulations
680 factor = 0.5
681
682 # initialise the BFE correction image to zero
683 corr = numpy.zeros_like(cFunc)
684
685 # Generate a 2D mesh of x,y coordinates
686 yDim, xDim = cFunc.shape
687 y = numpy.arange(yDim, dtype=int)
688 x = numpy.arange(xDim, dtype=int)
689 xc, yc = numpy.meshgrid(x, y)
690
691 # process each axis in turn
692 for ax in [0, 1]:
693
694 # gradient of phi on right/upper edge of pixel
695 diff = numpy.diff(cFunc, axis=ax)
696
697 # expand array back to full size with zero gradient at the end
698 gx = numpy.zeros_like(cFunc)
699 yDiff, xDiff = diff.shape
700 gx[:yDiff, :xDiff] += diff
701
702 # select pixels with either positive gradients on the right edge,
703 # flux flowing to the right/up
704 # or negative gradients, flux flowing to the left/down
705 for i, sel in enumerate([gx > 0, gx < 0]):
706 xSelPixels = xc[sel]
707 ySelPixels = yc[sel]
708 # and add the flux into the pixel to the right or top
709 # depending on which axis we are handling
710 if ax == 0:
711 xPix = xSelPixels
712 yPix = ySelPixels+1
713 else:
714 xPix = xSelPixels+1
715 yPix = ySelPixels
716 # define flux as the either current pixel value or pixel
717 # above/right
718 # depending on whether positive or negative gradient
719 if i == 0:
720 # positive gradients, flux flowing to higher coordinate values
721 flux = factor * fStep[sel]*gx[sel]
722 else:
723 # negative gradients, flux flowing to lower coordinate values
724 flux = factor * fStep[yPix, xPix]*gx[sel]
725 # change the fluxes of the donor and receiving pixels
726 # such that flux is conserved
727 corr[sel] -= flux
728 corr[yPix, xPix] += flux
729
730 # return correction array
731 return corr
732
733
734def fluxConservingBrighterFatterCorrection(exposure, kernel, maxIter, threshold, applyGain,
735 gains=None, correctionMode=True):
736 """Apply brighter fatter correction in place for the image.
737
738 This version presents a modified version of the algorithm
739 found in ``lsst.ip.isr.isrFunctions.brighterFatterCorrection``
740 which conserves the image flux, resulting in improved
741 correction of the cores of stars. The convolution has also been
742 modified to mitigate edge effects.
743
744 Parameters
745 ----------
746 exposure : `lsst.afw.image.Exposure`
747 Exposure to have brighter-fatter correction applied. Modified
748 by this method.
749 kernel : `numpy.ndarray`
750 Brighter-fatter kernel to apply.
751 maxIter : scalar
752 Number of correction iterations to run.
753 threshold : scalar
754 Convergence threshold in terms of the sum of absolute
755 deviations between an iteration and the previous one.
756 applyGain : `Bool`
757 If True, then the exposure values are scaled by the gain prior
758 to correction.
759 gains : `dict` [`str`, `float`]
760 A dictionary, keyed by amplifier name, of the gains to use.
761 If gains is None, the nominal gains in the amplifier object are used.
762 correctionMode : `Bool`
763 If True (default) the function applies correction for BFE. If False,
764 the code can instead be used to generate a simulation of BFE (sign
765 change in the direction of the effect)
766
767 Returns
768 -------
769 diff : `float`
770 Final difference between iterations achieved in correction.
771 iteration : `int`
772 Number of iterations used to calculate correction.
773
774 Notes
775 -----
776 Modified version of ``lsst.ip.isr.isrFunctions.brighterFatterCorrection``.
777
778 This correction takes a kernel that has been derived from flat
779 field images to redistribute the charge. The gradient of the
780 kernel is the deflection field due to the accumulated charge.
781
782 Given the original image I(x) and the kernel K(x) we can compute
783 the corrected image Ic(x) using the following equation:
784
785 Ic(x) = I(x) + 0.5*d/dx(I(x)*d/dx(int( dy*K(x-y)*I(y))))
786
787 Improved algorithm at this step applies the divergence theorem to
788 obtain a pixelised correction.
789
790 Because we use the measured counts instead of the incident counts
791 we apply the correction iteratively to reconstruct the original
792 counts and the correction. We stop iterating when the summed
793 difference between the current corrected image and the one from
794 the previous iteration is below the threshold. We do not require
795 convergence because the number of iterations is too large a
796 computational cost. How we define the threshold still needs to be
797 evaluated, the current default was shown to work reasonably well
798 on a small set of images.
799
800 Edges are handled in the convolution by padding. This is still not
801 a physical model for the edge, but avoids discontinuity in the correction.
802
803 Author of modified version: Lance.Miller@physics.ox.ac.uk
804 (see DM-38555).
805 """
806 image = exposure.getMaskedImage().getImage()
807
808 # The image needs to be units of electrons/holes
809 with gainContext(exposure, image, applyGain, gains):
810
811 # get kernel and its shape
812 kLy, kLx = kernel.shape
813 kernelImage = afwImage.ImageD(kLx, kLy)
814 kernelImage.getArray()[:, :] = kernel
815 tempImage = image.clone()
816
817 nanIndex = numpy.isnan(tempImage.getArray())
818 tempImage.getArray()[nanIndex] = 0.
819
820 outImage = afwImage.ImageF(image.getDimensions())
821 corr = numpy.zeros_like(image.getArray())
822 prevImage = numpy.zeros_like(image.getArray())
823 convCntrl = afwMath.ConvolutionControl(False, False, 1)
824 fixedKernel = afwMath.FixedKernel(kernelImage)
825
826 # set the padding amount
827 # ensure we pad by an even amount larger than the kernel
828 kLy = 2 * ((1+kLy)//2)
829 kLx = 2 * ((1+kLx)//2)
830
831 # The deflection potential only depends on the gradient of
832 # the convolution, so we can subtract the mean, which then
833 # allows us to pad the image with zeros and avoid wrap-around effects
834 # (although still not handling the image edges with a physical model)
835 # This wouldn't be great if there were a strong image gradient.
836 imYdimension, imXdimension = tempImage.array.shape
837 imean = numpy.mean(tempImage.getArray()[~nanIndex])
838 # subtract mean from image
839 tempImage -= imean
840 tempImage.array[nanIndex] = 0.
841 padArray = numpy.pad(tempImage.getArray(), ((0, kLy), (0, kLx)))
842 outImage = afwImage.ImageF(numpy.pad(outImage.getArray(), ((0, kLy), (0, kLx))))
843 # Convert array to afw image so afwMath.convolve works
844 padImage = afwImage.ImageF(padArray.shape[1], padArray.shape[0])
845 padImage.array[:] = padArray
846
847 for iteration in range(maxIter):
848
849 # create deflection potential, convolution of flux with kernel
850 # using padded counts array
851 afwMath.convolve(outImage, padImage, fixedKernel, convCntrl)
852 tmpArray = tempImage.getArray()
853 outArray = outImage.getArray()
854
855 # trim convolution output back to original shape
856 outArray = outArray[:imYdimension, :imXdimension]
857
858 # generate the correction array, with correctionMode set as input
859 corr[...] = transferFlux(outArray, tmpArray, correctionMode=correctionMode)
860
861 # update the arrays for the next iteration
862 tmpArray[:, :] = image.getArray()[:, :]
863 tmpArray += corr
864 tmpArray[nanIndex] = 0.
865 # update padded array
866 # subtract mean
867 tmpArray -= imean
868 tempImage.array[nanIndex] = 0.
869 padArray = numpy.pad(tempImage.getArray(), ((0, kLy), (0, kLx)))
870
871 if iteration > 0:
872 diff = numpy.sum(numpy.abs(prevImage - tmpArray))
873
874 if diff < threshold:
875 break
876 prevImage[:, :] = tmpArray[:, :]
877
878 image.getArray()[:] += corr[:]
879
880 return diff, iteration
881
882
883@contextmanager
884def gainContext(exp, image, apply, gains=None, invert=False, isTrimmed=True):
885 """Context manager that applies and removes gain.
886
887 Parameters
888 ----------
889 exp : `lsst.afw.image.Exposure`
890 Exposure to apply/remove gain.
891 image : `lsst.afw.image.Image`
892 Image to apply/remove gain.
893 apply : `bool`
894 If True, apply and remove the amplifier gain.
895 gains : `dict` [`str`, `float`], optional
896 A dictionary, keyed by amplifier name, of the gains to use.
897 If gains is None, the nominal gains in the amplifier object are used.
898 invert : `bool`, optional
899 Invert the gains (e.g. convert electrons to adu temporarily)?
900 isTrimmed : `bool`, optional
901 Is this a trimmed exposure?
902
903 Yields
904 ------
905 exp : `lsst.afw.image.Exposure`
906 Exposure with the gain applied.
907 """
908 # check we have all of them if provided because mixing and matching would
909 # be a real mess
910 if gains and apply is True:
911 ampNames = [amp.getName() for amp in exp.getDetector()]
912 for ampName in ampNames:
913 if ampName not in gains.keys():
914 raise RuntimeError(f"Gains provided to gain context, but no entry found for amp {ampName}")
915
916 if apply:
917 ccd = exp.getDetector()
918 for amp in ccd:
919 sim = image.Factory(image, amp.getBBox() if isTrimmed else amp.getRawBBox())
920 if gains:
921 gain = gains[amp.getName()]
922 else:
923 gain = amp.getGain()
924 if invert:
925 sim /= gain
926 else:
927 sim *= gain
928
929 try:
930 yield exp
931 finally:
932 if apply:
933 ccd = exp.getDetector()
934 for amp in ccd:
935 sim = image.Factory(image, amp.getBBox() if isTrimmed else amp.getRawBBox())
936 if gains:
937 gain = gains[amp.getName()]
938 else:
939 gain = amp.getGain()
940 if invert:
941 sim *= gain
942 else:
943 sim /= gain
944
945
946def attachTransmissionCurve(exposure, opticsTransmission=None, filterTransmission=None,
947 sensorTransmission=None, atmosphereTransmission=None):
948 """Attach a TransmissionCurve to an Exposure, given separate curves for
949 different components.
950
951 Parameters
952 ----------
953 exposure : `lsst.afw.image.Exposure`
954 Exposure object to modify by attaching the product of all given
955 ``TransmissionCurves`` in post-assembly trimmed detector coordinates.
956 Must have a valid ``Detector`` attached that matches the detector
957 associated with sensorTransmission.
958 opticsTransmission : `lsst.afw.image.TransmissionCurve`
959 A ``TransmissionCurve`` that represents the throughput of the optics,
960 to be evaluated in focal-plane coordinates.
961 filterTransmission : `lsst.afw.image.TransmissionCurve`
962 A ``TransmissionCurve`` that represents the throughput of the filter
963 itself, to be evaluated in focal-plane coordinates.
964 sensorTransmission : `lsst.afw.image.TransmissionCurve`
965 A ``TransmissionCurve`` that represents the throughput of the sensor
966 itself, to be evaluated in post-assembly trimmed detector coordinates.
967 atmosphereTransmission : `lsst.afw.image.TransmissionCurve`
968 A ``TransmissionCurve`` that represents the throughput of the
969 atmosphere, assumed to be spatially constant.
970
971 Returns
972 -------
973 combined : `lsst.afw.image.TransmissionCurve`
974 The TransmissionCurve attached to the exposure.
975
976 Notes
977 -----
978 All ``TransmissionCurve`` arguments are optional; if none are provided, the
979 attached ``TransmissionCurve`` will have unit transmission everywhere.
980 """
981 combined = afwImage.TransmissionCurve.makeIdentity()
982 if atmosphereTransmission is not None:
983 combined *= atmosphereTransmission
984 if opticsTransmission is not None:
985 combined *= opticsTransmission
986 if filterTransmission is not None:
987 combined *= filterTransmission
988 detector = exposure.getDetector()
989 fpToPix = detector.getTransform(fromSys=camGeom.FOCAL_PLANE,
990 toSys=camGeom.PIXELS)
991 combined = combined.transformedBy(fpToPix)
992 if sensorTransmission is not None:
993 combined *= sensorTransmission
994 exposure.getInfo().setTransmissionCurve(combined)
995 return combined
996
997
998def applyGains(exposure, normalizeGains=False, ptcGains=None, isTrimmed=True):
999 """Scale an exposure by the amplifier gains.
1000
1001 Parameters
1002 ----------
1003 exposure : `lsst.afw.image.Exposure`
1004 Exposure to process. The image is modified.
1005 normalizeGains : `Bool`, optional
1006 If True, then amplifiers are scaled to force the median of
1007 each amplifier to equal the median of those medians.
1008 ptcGains : `dict`[`str`], optional
1009 Dictionary keyed by amp name containing the PTC gains.
1010 isTrimmed : `bool`, optional
1011 Is the input image trimmed?
1012 """
1013 ccd = exposure.getDetector()
1014 ccdImage = exposure.getMaskedImage()
1015
1016 medians = []
1017 for amp in ccd:
1018 if isTrimmed:
1019 sim = ccdImage.Factory(ccdImage, amp.getBBox())
1020 else:
1021 sim = ccdImage.Factory(ccdImage, amp.getRawBBox())
1022 if ptcGains:
1023 sim *= ptcGains[amp.getName()]
1024 else:
1025 sim *= amp.getGain()
1026
1027 if normalizeGains:
1028 medians.append(numpy.median(sim.getImage().getArray()))
1029
1030 if normalizeGains:
1031 median = numpy.median(numpy.array(medians))
1032 for index, amp in enumerate(ccd):
1033 if isTrimmed:
1034 sim = ccdImage.Factory(ccdImage, amp.getBBox())
1035 else:
1036 sim = ccdImage.Factory(ccdImage, amp.getRawBBox())
1037 if medians[index] != 0.0:
1038 sim *= median/medians[index]
1039
1040
1042 """Grow the saturation trails by an amount dependent on the width of the
1043 trail.
1044
1045 Parameters
1046 ----------
1047 mask : `lsst.afw.image.Mask`
1048 Mask which will have the saturated areas grown.
1049 """
1050
1051 extraGrowDict = {}
1052 for i in range(1, 6):
1053 extraGrowDict[i] = 0
1054 for i in range(6, 8):
1055 extraGrowDict[i] = 1
1056 for i in range(8, 10):
1057 extraGrowDict[i] = 3
1058 extraGrowMax = 4
1059
1060 if extraGrowMax <= 0:
1061 return
1062
1063 saturatedBit = mask.getPlaneBitMask("SAT")
1064
1065 xmin, ymin = mask.getBBox().getMin()
1066 width = mask.getWidth()
1067
1068 thresh = afwDetection.Threshold(saturatedBit, afwDetection.Threshold.BITMASK)
1069 fpList = afwDetection.FootprintSet(mask, thresh).getFootprints()
1070
1071 for fp in fpList:
1072 for s in fp.getSpans():
1073 x0, x1 = s.getX0(), s.getX1()
1074
1075 extraGrow = extraGrowDict.get(x1 - x0 + 1, extraGrowMax)
1076 if extraGrow > 0:
1077 y = s.getY() - ymin
1078 x0 -= xmin + extraGrow
1079 x1 -= xmin - extraGrow
1080
1081 if x0 < 0:
1082 x0 = 0
1083 if x1 >= width - 1:
1084 x1 = width - 1
1085
1086 mask.array[y, x0:x1+1] |= saturatedBit
1087
1088
1089def setBadRegions(exposure, badStatistic="MEDIAN"):
1090 """Set all BAD areas of the chip to the average of the rest of the exposure
1091
1092 Parameters
1093 ----------
1094 exposure : `lsst.afw.image.Exposure`
1095 Exposure to mask. The exposure mask is modified.
1096 badStatistic : `str`, optional
1097 Statistic to use to generate the replacement value from the
1098 image data. Allowed values are 'MEDIAN' or 'MEANCLIP'.
1099
1100 Returns
1101 -------
1102 badPixelCount : scalar
1103 Number of bad pixels masked.
1104 badPixelValue : scalar
1105 Value substituted for bad pixels.
1106
1107 Raises
1108 ------
1109 RuntimeError
1110 Raised if `badStatistic` is not an allowed value.
1111 """
1112 if badStatistic == "MEDIAN":
1113 statistic = afwMath.MEDIAN
1114 elif badStatistic == "MEANCLIP":
1115 statistic = afwMath.MEANCLIP
1116 else:
1117 raise RuntimeError("Impossible method %s of bad region correction" % badStatistic)
1118
1119 mi = exposure.getMaskedImage()
1120 mask = mi.getMask()
1121 BAD = mask.getPlaneBitMask("BAD")
1122 INTRP = mask.getPlaneBitMask("INTRP")
1123
1125 sctrl.setAndMask(BAD)
1126 value = afwMath.makeStatistics(mi, statistic, sctrl).getValue()
1127
1128 maskArray = mask.getArray()
1129 imageArray = mi.getImage().getArray()
1130 badPixels = numpy.logical_and((maskArray & BAD) > 0, (maskArray & INTRP) == 0)
1131 imageArray[:] = numpy.where(badPixels, value, imageArray)
1132
1133 return badPixels.sum(), value
1134
1135
1136def checkFilter(exposure, filterList, log):
1137 """Check to see if an exposure is in a filter specified by a list.
1138
1139 The goal of this is to provide a unified filter checking interface
1140 for all filter dependent stages.
1141
1142 Parameters
1143 ----------
1144 exposure : `lsst.afw.image.Exposure`
1145 Exposure to examine.
1146 filterList : `list` [`str`]
1147 List of physical_filter names to check.
1148 log : `logging.Logger`
1149 Logger to handle messages.
1150
1151 Returns
1152 -------
1153 result : `bool`
1154 True if the exposure's filter is contained in the list.
1155 """
1156 if len(filterList) == 0:
1157 return False
1158 thisFilter = exposure.getFilter()
1159 if thisFilter is None:
1160 log.warning("No FilterLabel attached to this exposure!")
1161 return False
1162
1163 thisPhysicalFilter = getPhysicalFilter(thisFilter, log)
1164 if thisPhysicalFilter in filterList:
1165 return True
1166 elif thisFilter.bandLabel in filterList:
1167 if log:
1168 log.warning("Physical filter (%s) should be used instead of band %s for filter configurations"
1169 " (%s)", thisPhysicalFilter, thisFilter.bandLabel, filterList)
1170 return True
1171 else:
1172 return False
1173
1174
1175def getPhysicalFilter(filterLabel, log):
1176 """Get the physical filter label associated with the given filterLabel.
1177
1178 If ``filterLabel`` is `None` or there is no physicalLabel attribute
1179 associated with the given ``filterLabel``, the returned label will be
1180 "Unknown".
1181
1182 Parameters
1183 ----------
1184 filterLabel : `lsst.afw.image.FilterLabel`
1185 The `lsst.afw.image.FilterLabel` object from which to derive the
1186 physical filter label.
1187 log : `logging.Logger`
1188 Logger to handle messages.
1189
1190 Returns
1191 -------
1192 physicalFilter : `str`
1193 The value returned by the physicalLabel attribute of ``filterLabel`` if
1194 it exists, otherwise set to \"Unknown\".
1195 """
1196 if filterLabel is None:
1197 physicalFilter = "Unknown"
1198 log.warning("filterLabel is None. Setting physicalFilter to \"Unknown\".")
1199 else:
1200 try:
1201 physicalFilter = filterLabel.physicalLabel
1202 except RuntimeError:
1203 log.warning("filterLabel has no physicalLabel attribute. Setting physicalFilter to \"Unknown\".")
1204 physicalFilter = "Unknown"
1205 return physicalFilter
1206
1207
1208def countMaskedPixels(maskedIm, maskPlane):
1209 """Count the number of pixels in a given mask plane.
1210
1211 Parameters
1212 ----------
1213 maskedIm : `~lsst.afw.image.MaskedImage`
1214 Masked image to examine.
1215 maskPlane : `str`
1216 Name of the mask plane to examine.
1217
1218 Returns
1219 -------
1220 nPix : `int`
1221 Number of pixels in the requested mask plane.
1222 """
1223 maskBit = maskedIm.mask.getPlaneBitMask(maskPlane)
1224 nPix = numpy.where(numpy.bitwise_and(maskedIm.mask.array, maskBit))[0].flatten().size
1225 return nPix
1226
1227
1228def getExposureGains(exposure):
1229 """Get the per-amplifier gains used for this exposure.
1230
1231 Parameters
1232 ----------
1233 exposure : `lsst.afw.image.Exposure`
1234 The exposure to find gains for.
1235
1236 Returns
1237 -------
1238 gains : `dict` [`str` `float`]
1239 Dictionary of gain values, keyed by amplifier name.
1240 Returns empty dict when detector is None.
1241 """
1242 det = exposure.getDetector()
1243 if det is None:
1244 return dict()
1245
1246 metadata = exposure.getMetadata()
1247 gains = {}
1248 for amp in det:
1249 ampName = amp.getName()
1250 # The key may use the new LSST ISR or the old LSST prefix
1251 if (key1 := f"LSST ISR GAIN {ampName}") in metadata:
1252 gains[ampName] = metadata[key1]
1253 elif (key2 := f"LSST GAIN {ampName}") in metadata:
1254 gains[ampName] = metadata[key2]
1255 else:
1256 gains[ampName] = amp.getGain()
1257 return gains
1258
1259
1261 """Get the per-amplifier read noise used for this exposure.
1262
1263 Parameters
1264 ----------
1265 exposure : `lsst.afw.image.Exposure`
1266 The exposure to find read noise for.
1267
1268 Returns
1269 -------
1270 readnoises : `dict` [`str` `float`]
1271 Dictionary of read noise values, keyed by amplifier name.
1272 Returns empty dict when detector is None.
1273 """
1274 det = exposure.getDetector()
1275 if det is None:
1276 return dict()
1277
1278 metadata = exposure.getMetadata()
1279 readnoises = {}
1280 for amp in det:
1281 ampName = amp.getName()
1282 # The key may use the new LSST ISR or the old LSST prefix
1283 if (key1 := f"LSST ISR READNOISE {ampName}") in metadata:
1284 readnoises[ampName] = metadata[key1]
1285 elif (key2 := f"LSST READNOISE {ampName}") in metadata:
1286 readnoises[ampName] = metadata[key2]
1287 else:
1288 readnoises[ampName] = amp.getReadNoise()
1289 return readnoises
1290
1291
1292def isTrimmedExposure(exposure):
1293 """Check if the unused pixels (pre-/over-scan pixels) have
1294 been trimmed from an exposure.
1295
1296 Parameters
1297 ----------
1298 exposure : `lsst.afw.image.Exposure`
1299 The exposure to check.
1300
1301 Returns
1302 -------
1303 result : `bool`
1304 True if the image is trimmed, else False.
1305 """
1306 return exposure.getDetector().getBBox() == exposure.getBBox()
1307
1308
1309def isTrimmedImage(image, detector):
1310 """Check if the unused pixels (pre-/over-scan pixels) have
1311 been trimmed from an image
1312
1313 Parameters
1314 ----------
1315 image : `lsst.afw.image.Image`
1316 The image to check.
1317 detector : `lsst.afw.cameraGeom.Detector`
1318 The detector associated with the image.
1319
1320 Returns
1321 -------
1322 result : `bool`
1323 True if the image is trimmed, else False.
1324 """
1325 return detector.getBBox() == image.getBBox()
Parameters to control convolution.
A kernel created from an Image.
Definition Kernel.h:471
Pass parameters to a Statistics object.
Definition Statistics.h:83
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
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, ConvolutionControl const &convolutionControl=ConvolutionControl())
Convolve an Image or MaskedImage with a Kernel, setting pixels of an existing output image.
Property stringToStatisticsProperty(std::string const property)
Conversion function to switch a string to a Property (see Statistics.h)
gainContext(exp, image, apply, gains=None, invert=False, isTrimmed=True)
interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=None, maskNameList=None, useLegacyInterp=True)
setBadRegions(exposure, badStatistic="MEDIAN")
countMaskedPixels(maskedIm, maskPlane)
attachTransmissionCurve(exposure, opticsTransmission=None, filterTransmission=None, sensorTransmission=None, atmosphereTransmission=None)
updateVariance(maskedImage, gain, readNoise)
flatCorrection(maskedImage, flatMaskedImage, scalingType, userScale=1.0, invert=False, trimToFit=False)
illuminationCorrection(maskedImage, illumMaskedImage, illumScale, trimToFit=True)
growMasks(mask, radius=0, maskNameList=['BAD'], maskValue="BAD")
transferFlux(cFunc, fStep, correctionMode=True)
biasCorrection(maskedImage, biasMaskedImage, trimToFit=False)
checkFilter(exposure, filterList, log)
darkCorrection(maskedImage, darkMaskedImage, expScale, darkScale, invert=False, trimToFit=False)
fluxConservingBrighterFatterCorrection(exposure, kernel, maxIter, threshold, applyGain, gains=None, correctionMode=True)
brighterFatterCorrection(exposure, kernel, maxIter, threshold, applyGain, gains=None)
makeThresholdMask(maskedImage, threshold, growFootprints=1, maskName='SAT')
transposeMaskedImage(maskedImage)
interpolateFromMask(maskedImage, fwhm, growSaturatedFootprints=1, maskNameList=['SAT'], fallbackValue=None, useLegacyInterp=True)
isTrimmedImage(image, detector)
applyGains(exposure, normalizeGains=False, ptcGains=None, isTrimmed=True)
getPhysicalFilter(filterLabel, log)
saturationCorrection(maskedImage, saturation, fwhm, growFootprints=1, interpolate=True, maskName='SAT', fallbackValue=None, useLegacyInterp=True)
trimToMatchCalibBBox(rawMaskedImage, calibMaskedImage)