LSST Applications g070148d5b3+33e5256705,g0d53e28543+25c8b88941,g0da5cf3356+2dd1178308,g1081da9e2a+62d12e78cb,g17e5ecfddb+7e422d6136,g1c76d35bf8+ede3a706f7,g295839609d+225697d880,g2e2c1a68ba+cc1f6f037e,g2ffcdf413f+853cd4dcde,g38293774b4+62d12e78cb,g3b44f30a73+d953f1ac34,g48ccf36440+885b902d19,g4b2f1765b6+7dedbde6d2,g5320a0a9f6+0c5d6105b6,g56b687f8c9+ede3a706f7,g5c4744a4d9+ef6ac23297,g5ffd174ac0+0c5d6105b6,g6075d09f38+66af417445,g667d525e37+2ced63db88,g670421136f+2ced63db88,g71f27ac40c+2ced63db88,g774830318a+463cbe8d1f,g7876bc68e5+1d137996f1,g7985c39107+62d12e78cb,g7fdac2220c+0fd8241c05,g96f01af41f+368e6903a7,g9ca82378b8+2ced63db88,g9d27549199+ef6ac23297,gabe93b2c52+e3573e3735,gb065e2a02a+3dfbe639da,gbc3249ced9+0c5d6105b6,gbec6a3398f+0c5d6105b6,gc9534b9d65+35b9f25267,gd01420fc67+0c5d6105b6,geee7ff78d7+a14128c129,gf63283c776+ede3a706f7,gfed783d017+0c5d6105b6,w.2022.47
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 "gainContext",
34 "getPhysicalFilter",
35 "growMasks",
36 "illuminationCorrection",
37 "interpolateDefectList",
38 "interpolateFromMask",
39 "makeThresholdMask",
40 "saturationCorrection",
41 "setBadRegions",
42 "transposeMaskedImage",
43 "trimToMatchCalibBBox",
44 "updateVariance",
45 "widenSaturationTrails",
46]
47
48import math
49import numpy
50
51import lsst.geom
52import lsst.afw.image as afwImage
53import lsst.afw.detection as afwDetection
54import lsst.afw.math as afwMath
55import lsst.meas.algorithms as measAlg
56import lsst.afw.cameraGeom as camGeom
57
58from lsst.meas.algorithms.detection import SourceDetectionTask
59
60from contextlib import contextmanager
61
62from .defects import Defects
63
64
65def createPsf(fwhm):
66 """Make a double Gaussian PSF.
67
68 Parameters
69 ----------
70 fwhm : scalar
71 FWHM of double Gaussian smoothing kernel.
72
73 Returns
74 -------
76 The created smoothing kernel.
77 """
78 ksize = 4*int(fwhm) + 1
79 return measAlg.DoubleGaussianPsf(ksize, ksize, fwhm/(2*math.sqrt(2*math.log(2))))
80
81
82def transposeMaskedImage(maskedImage):
83 """Make a transposed copy of a masked image.
84
85 Parameters
86 ----------
87 maskedImage : `lsst.afw.image.MaskedImage`
88 Image to process.
89
90 Returns
91 -------
92 transposed : `lsst.afw.image.MaskedImage`
93 The transposed copy of the input image.
94 """
95 transposed = maskedImage.Factory(lsst.geom.Extent2I(maskedImage.getHeight(), maskedImage.getWidth()))
96 transposed.getImage().getArray()[:] = maskedImage.getImage().getArray().T
97 transposed.getMask().getArray()[:] = maskedImage.getMask().getArray().T
98 transposed.getVariance().getArray()[:] = maskedImage.getVariance().getArray().T
99 return transposed
100
101
102def interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=None):
103 """Interpolate over defects specified in a defect list.
104
105 Parameters
106 ----------
107 maskedImage : `lsst.afw.image.MaskedImage`
108 Image to process.
109 defectList : `lsst.meas.algorithms.Defects`
110 List of defects to interpolate over.
111 fwhm : scalar
112 FWHM of double Gaussian smoothing kernel.
113 fallbackValue : scalar, optional
114 Fallback value if an interpolated value cannot be determined.
115 If None, then the clipped mean of the image is used.
116 """
117 psf = createPsf(fwhm)
118 if fallbackValue is None:
119 fallbackValue = afwMath.makeStatistics(maskedImage.getImage(), afwMath.MEANCLIP).getValue()
120 if 'INTRP' not in maskedImage.getMask().getMaskPlaneDict():
121 maskedImage.getMask().addMaskPlane('INTRP')
122 measAlg.interpolateOverDefects(maskedImage, psf, defectList, fallbackValue, True)
123 return maskedImage
124
125
126def makeThresholdMask(maskedImage, threshold, growFootprints=1, maskName='SAT'):
127 """Mask pixels based on threshold detection.
128
129 Parameters
130 ----------
131 maskedImage : `lsst.afw.image.MaskedImage`
132 Image to process. Only the mask plane is updated.
133 threshold : scalar
134 Detection threshold.
135 growFootprints : scalar, optional
136 Number of pixels to grow footprints of detected regions.
137 maskName : str, optional
138 Mask plane name, or list of names to convert
139
140 Returns
141 -------
142 defectList : `lsst.meas.algorithms.Defects`
143 Defect list constructed from pixels above the threshold.
144 """
145 # find saturated regions
146 thresh = afwDetection.Threshold(threshold)
147 fs = afwDetection.FootprintSet(maskedImage, thresh)
148
149 if growFootprints > 0:
150 fs = afwDetection.FootprintSet(fs, rGrow=growFootprints, isotropic=False)
151 fpList = fs.getFootprints()
152
153 # set mask
154 mask = maskedImage.getMask()
155 bitmask = mask.getPlaneBitMask(maskName)
156 afwDetection.setMaskFromFootprintList(mask, fpList, bitmask)
157
158 return Defects.fromFootprintList(fpList)
159
160
161def growMasks(mask, radius=0, maskNameList=['BAD'], maskValue="BAD"):
162 """Grow a mask by an amount and add to the requested plane.
163
164 Parameters
165 ----------
166 mask : `lsst.afw.image.Mask`
167 Mask image to process.
168 radius : scalar
169 Amount to grow the mask.
170 maskNameList : `str` or `list` [`str`]
171 Mask names that should be grown.
172 maskValue : `str`
173 Mask plane to assign the newly masked pixels to.
174 """
175 if radius > 0:
176 thresh = afwDetection.Threshold(mask.getPlaneBitMask(maskNameList), afwDetection.Threshold.BITMASK)
177 fpSet = afwDetection.FootprintSet(mask, thresh)
178 fpSet = afwDetection.FootprintSet(fpSet, rGrow=radius, isotropic=False)
179 fpSet.setMask(mask, maskValue)
180
181
182def interpolateFromMask(maskedImage, fwhm, growSaturatedFootprints=1,
183 maskNameList=['SAT'], fallbackValue=None):
184 """Interpolate over defects identified by a particular set of mask planes.
185
186 Parameters
187 ----------
188 maskedImage : `lsst.afw.image.MaskedImage`
189 Image to process.
190 fwhm : scalar
191 FWHM of double Gaussian smoothing kernel.
192 growSaturatedFootprints : scalar, optional
193 Number of pixels to grow footprints for saturated pixels.
194 maskNameList : `List` of `str`, optional
195 Mask plane name.
196 fallbackValue : scalar, optional
197 Value of last resort for interpolation.
198 """
199 mask = maskedImage.getMask()
200
201 if growSaturatedFootprints > 0 and "SAT" in maskNameList:
202 # If we are interpolating over an area larger than the original masked
203 # region, we need to expand the original mask bit to the full area to
204 # explain why we interpolated there.
205 growMasks(mask, radius=growSaturatedFootprints, maskNameList=['SAT'], maskValue="SAT")
206
207 thresh = afwDetection.Threshold(mask.getPlaneBitMask(maskNameList), afwDetection.Threshold.BITMASK)
208 fpSet = afwDetection.FootprintSet(mask, thresh)
209 defectList = Defects.fromFootprintList(fpSet.getFootprints())
210
211 interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=fallbackValue)
212
213 return maskedImage
214
215
216def saturationCorrection(maskedImage, saturation, fwhm, growFootprints=1, interpolate=True, maskName='SAT',
217 fallbackValue=None):
218 """Mark saturated pixels and optionally interpolate over them
219
220 Parameters
221 ----------
222 maskedImage : `lsst.afw.image.MaskedImage`
223 Image to process.
224 saturation : scalar
225 Saturation level used as the detection threshold.
226 fwhm : scalar
227 FWHM of double Gaussian smoothing kernel.
228 growFootprints : scalar, optional
229 Number of pixels to grow footprints of detected regions.
230 interpolate : Bool, optional
231 If True, saturated pixels are interpolated over.
232 maskName : str, optional
233 Mask plane name.
234 fallbackValue : scalar, optional
235 Value of last resort for interpolation.
236 """
237 defectList = makeThresholdMask(
238 maskedImage=maskedImage,
239 threshold=saturation,
240 growFootprints=growFootprints,
241 maskName=maskName,
242 )
243 if interpolate:
244 interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=fallbackValue)
245
246 return maskedImage
247
248
249def trimToMatchCalibBBox(rawMaskedImage, calibMaskedImage):
250 """Compute number of edge trim pixels to match the calibration data.
251
252 Use the dimension difference between the raw exposure and the
253 calibration exposure to compute the edge trim pixels. This trim
254 is applied symmetrically, with the same number of pixels masked on
255 each side.
256
257 Parameters
258 ----------
259 rawMaskedImage : `lsst.afw.image.MaskedImage`
260 Image to trim.
261 calibMaskedImage : `lsst.afw.image.MaskedImage`
262 Calibration image to draw new bounding box from.
263
264 Returns
265 -------
266 replacementMaskedImage : `lsst.afw.image.MaskedImage`
267 ``rawMaskedImage`` trimmed to the appropriate size.
268
269 Raises
270 ------
271 RuntimeError
272 Raised if ``rawMaskedImage`` cannot be symmetrically trimmed to
273 match ``calibMaskedImage``.
274 """
275 nx, ny = rawMaskedImage.getBBox().getDimensions() - calibMaskedImage.getBBox().getDimensions()
276 if nx != ny:
277 raise RuntimeError("Raw and calib maskedImages are trimmed differently in X and Y.")
278 if nx % 2 != 0:
279 raise RuntimeError("Calibration maskedImage is trimmed unevenly in X.")
280 if nx < 0:
281 raise RuntimeError("Calibration maskedImage is larger than raw data.")
282
283 nEdge = nx//2
284 if nEdge > 0:
285 replacementMaskedImage = rawMaskedImage[nEdge:-nEdge, nEdge:-nEdge, afwImage.LOCAL]
286 SourceDetectionTask.setEdgeBits(
287 rawMaskedImage,
288 replacementMaskedImage.getBBox(),
289 rawMaskedImage.getMask().getPlaneBitMask("EDGE")
290 )
291 else:
292 replacementMaskedImage = rawMaskedImage
293
294 return replacementMaskedImage
295
296
297def biasCorrection(maskedImage, biasMaskedImage, trimToFit=False):
298 """Apply bias correction in place.
299
300 Parameters
301 ----------
302 maskedImage : `lsst.afw.image.MaskedImage`
303 Image to process. The image is modified by this method.
304 biasMaskedImage : `lsst.afw.image.MaskedImage`
305 Bias image of the same size as ``maskedImage``
306 trimToFit : `Bool`, optional
307 If True, raw data is symmetrically trimmed to match
308 calibration size.
309
310 Raises
311 ------
312 RuntimeError
313 Raised if ``maskedImage`` and ``biasMaskedImage`` do not have
314 the same size.
315
316 """
317 if trimToFit:
318 maskedImage = trimToMatchCalibBBox(maskedImage, biasMaskedImage)
319
320 if maskedImage.getBBox(afwImage.LOCAL) != biasMaskedImage.getBBox(afwImage.LOCAL):
321 raise RuntimeError("maskedImage bbox %s != biasMaskedImage bbox %s" %
322 (maskedImage.getBBox(afwImage.LOCAL), biasMaskedImage.getBBox(afwImage.LOCAL)))
323 maskedImage -= biasMaskedImage
324
325
326def darkCorrection(maskedImage, darkMaskedImage, expScale, darkScale, invert=False, trimToFit=False):
327 """Apply dark correction in place.
328
329 Parameters
330 ----------
331 maskedImage : `lsst.afw.image.MaskedImage`
332 Image to process. The image is modified by this method.
333 darkMaskedImage : `lsst.afw.image.MaskedImage`
334 Dark image of the same size as ``maskedImage``.
335 expScale : scalar
336 Dark exposure time for ``maskedImage``.
337 darkScale : scalar
338 Dark exposure time for ``darkMaskedImage``.
339 invert : `Bool`, optional
340 If True, re-add the dark to an already corrected image.
341 trimToFit : `Bool`, optional
342 If True, raw data is symmetrically trimmed to match
343 calibration size.
344
345 Raises
346 ------
347 RuntimeError
348 Raised if ``maskedImage`` and ``darkMaskedImage`` do not have
349 the same size.
350
351 Notes
352 -----
353 The dark correction is applied by calculating:
354 maskedImage -= dark * expScaling / darkScaling
355 """
356 if trimToFit:
357 maskedImage = trimToMatchCalibBBox(maskedImage, darkMaskedImage)
358
359 if maskedImage.getBBox(afwImage.LOCAL) != darkMaskedImage.getBBox(afwImage.LOCAL):
360 raise RuntimeError("maskedImage bbox %s != darkMaskedImage bbox %s" %
361 (maskedImage.getBBox(afwImage.LOCAL), darkMaskedImage.getBBox(afwImage.LOCAL)))
362
363 scale = expScale / darkScale
364 if not invert:
365 maskedImage.scaledMinus(scale, darkMaskedImage)
366 else:
367 maskedImage.scaledPlus(scale, darkMaskedImage)
368
369
370def updateVariance(maskedImage, gain, readNoise):
371 """Set the variance plane based on the image plane.
372
373 Parameters
374 ----------
375 maskedImage : `lsst.afw.image.MaskedImage`
376 Image to process. The variance plane is modified.
377 gain : scalar
378 The amplifier gain in electrons/ADU.
379 readNoise : scalar
380 The amplifier read nmoise in ADU/pixel.
381 """
382 var = maskedImage.getVariance()
383 var[:] = maskedImage.getImage()
384 var /= gain
385 var += readNoise**2
386
387
388def flatCorrection(maskedImage, flatMaskedImage, scalingType, userScale=1.0, invert=False, trimToFit=False):
389 """Apply flat correction in place.
390
391 Parameters
392 ----------
393 maskedImage : `lsst.afw.image.MaskedImage`
394 Image to process. The image is modified.
395 flatMaskedImage : `lsst.afw.image.MaskedImage`
396 Flat image of the same size as ``maskedImage``
397 scalingType : str
398 Flat scale computation method. Allowed values are 'MEAN',
399 'MEDIAN', or 'USER'.
400 userScale : scalar, optional
401 Scale to use if ``scalingType='USER'``.
402 invert : `Bool`, optional
403 If True, unflatten an already flattened image.
404 trimToFit : `Bool`, optional
405 If True, raw data is symmetrically trimmed to match
406 calibration size.
407
408 Raises
409 ------
410 RuntimeError
411 Raised if ``maskedImage`` and ``flatMaskedImage`` do not have
412 the same size or if ``scalingType`` is not an allowed value.
413 """
414 if trimToFit:
415 maskedImage = trimToMatchCalibBBox(maskedImage, flatMaskedImage)
416
417 if maskedImage.getBBox(afwImage.LOCAL) != flatMaskedImage.getBBox(afwImage.LOCAL):
418 raise RuntimeError("maskedImage bbox %s != flatMaskedImage bbox %s" %
419 (maskedImage.getBBox(afwImage.LOCAL), flatMaskedImage.getBBox(afwImage.LOCAL)))
420
421 # Figure out scale from the data
422 # Ideally the flats are normalized by the calibration product pipeline,
423 # but this allows some flexibility in the case that the flat is created by
424 # some other mechanism.
425 if scalingType in ('MEAN', 'MEDIAN'):
426 scalingType = afwMath.stringToStatisticsProperty(scalingType)
427 flatScale = afwMath.makeStatistics(flatMaskedImage.image, scalingType).getValue()
428 elif scalingType == 'USER':
429 flatScale = userScale
430 else:
431 raise RuntimeError('%s : %s not implemented' % ("flatCorrection", scalingType))
432
433 if not invert:
434 maskedImage.scaledDivides(1.0/flatScale, flatMaskedImage)
435 else:
436 maskedImage.scaledMultiplies(1.0/flatScale, flatMaskedImage)
437
438
439def illuminationCorrection(maskedImage, illumMaskedImage, illumScale, trimToFit=True):
440 """Apply illumination correction in place.
441
442 Parameters
443 ----------
444 maskedImage : `lsst.afw.image.MaskedImage`
445 Image to process. The image is modified.
446 illumMaskedImage : `lsst.afw.image.MaskedImage`
447 Illumination correction image of the same size as ``maskedImage``.
448 illumScale : scalar
449 Scale factor for the illumination correction.
450 trimToFit : `Bool`, optional
451 If True, raw data is symmetrically trimmed to match
452 calibration size.
453
454 Raises
455 ------
456 RuntimeError
457 Raised if ``maskedImage`` and ``illumMaskedImage`` do not have
458 the same size.
459 """
460 if trimToFit:
461 maskedImage = trimToMatchCalibBBox(maskedImage, illumMaskedImage)
462
463 if maskedImage.getBBox(afwImage.LOCAL) != illumMaskedImage.getBBox(afwImage.LOCAL):
464 raise RuntimeError("maskedImage bbox %s != illumMaskedImage bbox %s" %
465 (maskedImage.getBBox(afwImage.LOCAL), illumMaskedImage.getBBox(afwImage.LOCAL)))
466
467 maskedImage.scaledDivides(1.0/illumScale, illumMaskedImage)
468
469
470def brighterFatterCorrection(exposure, kernel, maxIter, threshold, applyGain, gains=None):
471 """Apply brighter fatter correction in place for the image.
472
473 Parameters
474 ----------
475 exposure : `lsst.afw.image.Exposure`
476 Exposure to have brighter-fatter correction applied. Modified
477 by this method.
478 kernel : `numpy.ndarray`
479 Brighter-fatter kernel to apply.
480 maxIter : scalar
481 Number of correction iterations to run.
482 threshold : scalar
483 Convergence threshold in terms of the sum of absolute
484 deviations between an iteration and the previous one.
485 applyGain : `Bool`
486 If True, then the exposure values are scaled by the gain prior
487 to correction.
488 gains : `dict` [`str`, `float`]
489 A dictionary, keyed by amplifier name, of the gains to use.
490 If gains is None, the nominal gains in the amplifier object are used.
491
492 Returns
493 -------
494 diff : `float`
495 Final difference between iterations achieved in correction.
496 iteration : `int`
497 Number of iterations used to calculate correction.
498
499 Notes
500 -----
501 This correction takes a kernel that has been derived from flat
502 field images to redistribute the charge. The gradient of the
503 kernel is the deflection field due to the accumulated charge.
504
505 Given the original image I(x) and the kernel K(x) we can compute
506 the corrected image Ic(x) using the following equation:
507
508 Ic(x) = I(x) + 0.5*d/dx(I(x)*d/dx(int( dy*K(x-y)*I(y))))
509
510 To evaluate the derivative term we expand it as follows:
511
512 0.5 * ( d/dx(I(x))*d/dx(int(dy*K(x-y)*I(y)))
513 + I(x)*d^2/dx^2(int(dy* K(x-y)*I(y))) )
514
515 Because we use the measured counts instead of the incident counts
516 we apply the correction iteratively to reconstruct the original
517 counts and the correction. We stop iterating when the summed
518 difference between the current corrected image and the one from
519 the previous iteration is below the threshold. We do not require
520 convergence because the number of iterations is too large a
521 computational cost. How we define the threshold still needs to be
522 evaluated, the current default was shown to work reasonably well
523 on a small set of images. For more information on the method see
524 DocuShare Document-19407.
525
526 The edges as defined by the kernel are not corrected because they
527 have spurious values due to the convolution.
528 """
529 image = exposure.getMaskedImage().getImage()
530
531 # The image needs to be units of electrons/holes
532 with gainContext(exposure, image, applyGain, gains):
533
534 kLx = numpy.shape(kernel)[0]
535 kLy = numpy.shape(kernel)[1]
536 kernelImage = afwImage.ImageD(kLx, kLy)
537 kernelImage.getArray()[:, :] = kernel
538 tempImage = image.clone()
539
540 nanIndex = numpy.isnan(tempImage.getArray())
541 tempImage.getArray()[nanIndex] = 0.
542
543 outImage = afwImage.ImageF(image.getDimensions())
544 corr = numpy.zeros_like(image.getArray())
545 prev_image = numpy.zeros_like(image.getArray())
546 convCntrl = afwMath.ConvolutionControl(False, True, 1)
547 fixedKernel = afwMath.FixedKernel(kernelImage)
548
549 # Define boundary by convolution region. The region that the
550 # correction will be calculated for is one fewer in each dimension
551 # because of the second derivative terms.
552 # NOTE: these need to use integer math, as we're using start:end as
553 # numpy index ranges.
554 startX = kLx//2
555 endX = -kLx//2
556 startY = kLy//2
557 endY = -kLy//2
558
559 for iteration in range(maxIter):
560
561 afwMath.convolve(outImage, tempImage, fixedKernel, convCntrl)
562 tmpArray = tempImage.getArray()
563 outArray = outImage.getArray()
564
565 with numpy.errstate(invalid="ignore", over="ignore"):
566 # First derivative term
567 gradTmp = numpy.gradient(tmpArray[startY:endY, startX:endX])
568 gradOut = numpy.gradient(outArray[startY:endY, startX:endX])
569 first = (gradTmp[0]*gradOut[0] + gradTmp[1]*gradOut[1])[1:-1, 1:-1]
570
571 # Second derivative term
572 diffOut20 = numpy.diff(outArray, 2, 0)[startY:endY, startX + 1:endX - 1]
573 diffOut21 = numpy.diff(outArray, 2, 1)[startY + 1:endY - 1, startX:endX]
574 second = tmpArray[startY + 1:endY - 1, startX + 1:endX - 1]*(diffOut20 + diffOut21)
575
576 corr[startY + 1:endY - 1, startX + 1:endX - 1] = 0.5*(first + second)
577
578 tmpArray[:, :] = image.getArray()[:, :]
579 tmpArray[nanIndex] = 0.
580 tmpArray[startY:endY, startX:endX] += corr[startY:endY, startX:endX]
581
582 if iteration > 0:
583 diff = numpy.sum(numpy.abs(prev_image - tmpArray))
584
585 if diff < threshold:
586 break
587 prev_image[:, :] = tmpArray[:, :]
588
589 image.getArray()[startY + 1:endY - 1, startX + 1:endX - 1] += \
590 corr[startY + 1:endY - 1, startX + 1:endX - 1]
591
592 return diff, iteration
593
594
595@contextmanager
596def gainContext(exp, image, apply, gains=None):
597 """Context manager that applies and removes gain.
598
599 Parameters
600 ----------
602 Exposure to apply/remove gain.
603 image : `lsst.afw.image.Image`
604 Image to apply/remove gain.
605 apply : `Bool`
606 If True, apply and remove the amplifier gain.
607 gains : `dict` [`str`, `float`]
608 A dictionary, keyed by amplifier name, of the gains to use.
609 If gains is None, the nominal gains in the amplifier object are used.
610
611 Yields
612 ------
614 Exposure with the gain applied.
615 """
616 # check we have all of them if provided because mixing and matching would
617 # be a real mess
618 if gains and apply is True:
619 ampNames = [amp.getName() for amp in exp.getDetector()]
620 for ampName in ampNames:
621 if ampName not in gains.keys():
622 raise RuntimeError(f"Gains provided to gain context, but no entry found for amp {ampName}")
623
624 if apply:
625 ccd = exp.getDetector()
626 for amp in ccd:
627 sim = image.Factory(image, amp.getBBox())
628 if gains:
629 gain = gains[amp.getName()]
630 else:
631 gain = amp.getGain()
632 sim *= gain
633
634 try:
635 yield exp
636 finally:
637 if apply:
638 ccd = exp.getDetector()
639 for amp in ccd:
640 sim = image.Factory(image, amp.getBBox())
641 if gains:
642 gain = gains[amp.getName()]
643 else:
644 gain = amp.getGain()
645 sim /= gain
646
647
648def attachTransmissionCurve(exposure, opticsTransmission=None, filterTransmission=None,
649 sensorTransmission=None, atmosphereTransmission=None):
650 """Attach a TransmissionCurve to an Exposure, given separate curves for
651 different components.
652
653 Parameters
654 ----------
655 exposure : `lsst.afw.image.Exposure`
656 Exposure object to modify by attaching the product of all given
657 ``TransmissionCurves`` in post-assembly trimmed detector coordinates.
658 Must have a valid ``Detector`` attached that matches the detector
659 associated with sensorTransmission.
660 opticsTransmission : `lsst.afw.image.TransmissionCurve`
661 A ``TransmissionCurve`` that represents the throughput of the optics,
662 to be evaluated in focal-plane coordinates.
663 filterTransmission : `lsst.afw.image.TransmissionCurve`
664 A ``TransmissionCurve`` that represents the throughput of the filter
665 itself, to be evaluated in focal-plane coordinates.
666 sensorTransmission : `lsst.afw.image.TransmissionCurve`
667 A ``TransmissionCurve`` that represents the throughput of the sensor
668 itself, to be evaluated in post-assembly trimmed detector coordinates.
669 atmosphereTransmission : `lsst.afw.image.TransmissionCurve`
670 A ``TransmissionCurve`` that represents the throughput of the
671 atmosphere, assumed to be spatially constant.
672
673 Returns
674 -------
676 The TransmissionCurve attached to the exposure.
677
678 Notes
679 -----
680 All ``TransmissionCurve`` arguments are optional; if none are provided, the
681 attached ``TransmissionCurve`` will have unit transmission everywhere.
682 """
683 combined = afwImage.TransmissionCurve.makeIdentity()
684 if atmosphereTransmission is not None:
685 combined *= atmosphereTransmission
686 if opticsTransmission is not None:
687 combined *= opticsTransmission
688 if filterTransmission is not None:
689 combined *= filterTransmission
690 detector = exposure.getDetector()
691 fpToPix = detector.getTransform(fromSys=camGeom.FOCAL_PLANE,
692 toSys=camGeom.PIXELS)
693 combined = combined.transformedBy(fpToPix)
694 if sensorTransmission is not None:
695 combined *= sensorTransmission
696 exposure.getInfo().setTransmissionCurve(combined)
697 return combined
698
699
700def applyGains(exposure, normalizeGains=False, ptcGains=None):
701 """Scale an exposure by the amplifier gains.
702
703 Parameters
704 ----------
705 exposure : `lsst.afw.image.Exposure`
706 Exposure to process. The image is modified.
707 normalizeGains : `Bool`, optional
708 If True, then amplifiers are scaled to force the median of
709 each amplifier to equal the median of those medians.
710 ptcGains : `dict`[`str`], optional
711 Dictionary keyed by amp name containing the PTC gains.
712 """
713 ccd = exposure.getDetector()
714 ccdImage = exposure.getMaskedImage()
715
716 medians = []
717 for amp in ccd:
718 sim = ccdImage.Factory(ccdImage, amp.getBBox())
719 if ptcGains:
720 sim *= ptcGains[amp.getName()]
721 else:
722 sim *= amp.getGain()
723
724 if normalizeGains:
725 medians.append(numpy.median(sim.getImage().getArray()))
726
727 if normalizeGains:
728 median = numpy.median(numpy.array(medians))
729 for index, amp in enumerate(ccd):
730 sim = ccdImage.Factory(ccdImage, amp.getBBox())
731 if medians[index] != 0.0:
732 sim *= median/medians[index]
733
734
736 """Grow the saturation trails by an amount dependent on the width of the
737 trail.
738
739 Parameters
740 ----------
741 mask : `lsst.afw.image.Mask`
742 Mask which will have the saturated areas grown.
743 """
744
745 extraGrowDict = {}
746 for i in range(1, 6):
747 extraGrowDict[i] = 0
748 for i in range(6, 8):
749 extraGrowDict[i] = 1
750 for i in range(8, 10):
751 extraGrowDict[i] = 3
752 extraGrowMax = 4
753
754 if extraGrowMax <= 0:
755 return
756
757 saturatedBit = mask.getPlaneBitMask("SAT")
758
759 xmin, ymin = mask.getBBox().getMin()
760 width = mask.getWidth()
761
762 thresh = afwDetection.Threshold(saturatedBit, afwDetection.Threshold.BITMASK)
763 fpList = afwDetection.FootprintSet(mask, thresh).getFootprints()
764
765 for fp in fpList:
766 for s in fp.getSpans():
767 x0, x1 = s.getX0(), s.getX1()
768
769 extraGrow = extraGrowDict.get(x1 - x0 + 1, extraGrowMax)
770 if extraGrow > 0:
771 y = s.getY() - ymin
772 x0 -= xmin + extraGrow
773 x1 -= xmin - extraGrow
774
775 if x0 < 0:
776 x0 = 0
777 if x1 >= width - 1:
778 x1 = width - 1
779
780 mask.array[y, x0:x1+1] |= saturatedBit
781
782
783def setBadRegions(exposure, badStatistic="MEDIAN"):
784 """Set all BAD areas of the chip to the average of the rest of the exposure
785
786 Parameters
787 ----------
788 exposure : `lsst.afw.image.Exposure`
789 Exposure to mask. The exposure mask is modified.
790 badStatistic : `str`, optional
791 Statistic to use to generate the replacement value from the
792 image data. Allowed values are 'MEDIAN' or 'MEANCLIP'.
793
794 Returns
795 -------
796 badPixelCount : scalar
797 Number of bad pixels masked.
798 badPixelValue : scalar
799 Value substituted for bad pixels.
800
801 Raises
802 ------
803 RuntimeError
804 Raised if `badStatistic` is not an allowed value.
805 """
806 if badStatistic == "MEDIAN":
807 statistic = afwMath.MEDIAN
808 elif badStatistic == "MEANCLIP":
809 statistic = afwMath.MEANCLIP
810 else:
811 raise RuntimeError("Impossible method %s of bad region correction" % badStatistic)
812
813 mi = exposure.getMaskedImage()
814 mask = mi.getMask()
815 BAD = mask.getPlaneBitMask("BAD")
816 INTRP = mask.getPlaneBitMask("INTRP")
817
819 sctrl.setAndMask(BAD)
820 value = afwMath.makeStatistics(mi, statistic, sctrl).getValue()
821
822 maskArray = mask.getArray()
823 imageArray = mi.getImage().getArray()
824 badPixels = numpy.logical_and((maskArray & BAD) > 0, (maskArray & INTRP) == 0)
825 imageArray[:] = numpy.where(badPixels, value, imageArray)
826
827 return badPixels.sum(), value
828
829
830def checkFilter(exposure, filterList, log):
831 """Check to see if an exposure is in a filter specified by a list.
832
833 The goal of this is to provide a unified filter checking interface
834 for all filter dependent stages.
835
836 Parameters
837 ----------
838 exposure : `lsst.afw.image.Exposure`
839 Exposure to examine.
840 filterList : `list` [`str`]
841 List of physical_filter names to check.
842 log : `logging.Logger`
843 Logger to handle messages.
844
845 Returns
846 -------
847 result : `bool`
848 True if the exposure's filter is contained in the list.
849 """
850 if len(filterList) == 0:
851 return False
852 thisFilter = exposure.getFilter()
853 if thisFilter is None:
854 log.warning("No FilterLabel attached to this exposure!")
855 return False
856
857 thisPhysicalFilter = getPhysicalFilter(thisFilter, log)
858 if thisPhysicalFilter in filterList:
859 return True
860 elif thisFilter.bandLabel in filterList:
861 if log:
862 log.warning("Physical filter (%s) should be used instead of band %s for filter configurations"
863 " (%s)", thisPhysicalFilter, thisFilter.bandLabel, filterList)
864 return True
865 else:
866 return False
867
868
869def getPhysicalFilter(filterLabel, log):
870 """Get the physical filter label associated with the given filterLabel.
871
872 If ``filterLabel`` is `None` or there is no physicalLabel attribute
873 associated with the given ``filterLabel``, the returned label will be
874 "Unknown".
875
876 Parameters
877 ----------
878 filterLabel : `lsst.afw.image.FilterLabel`
879 The `lsst.afw.image.FilterLabel` object from which to derive the
880 physical filter label.
881 log : `logging.Logger`
882 Logger to handle messages.
883
884 Returns
885 -------
886 physicalFilter : `str`
887 The value returned by the physicalLabel attribute of ``filterLabel`` if
888 it exists, otherwise set to \"Unknown\".
889 """
890 if filterLabel is None:
891 physicalFilter = "Unknown"
892 log.warning("filterLabel is None. Setting physicalFilter to \"Unknown\".")
893 else:
894 try:
895 physicalFilter = filterLabel.physicalLabel
896 except RuntimeError:
897 log.warning("filterLabel has no physicalLabel attribute. Setting physicalFilter to \"Unknown\".")
898 physicalFilter = "Unknown"
899 return physicalFilter
900
901
902def countMaskedPixels(maskedIm, maskPlane):
903 """Count the number of pixels in a given mask plane.
904
905 Parameters
906 ----------
907 maskedIm : `~lsst.afw.image.MaskedImage`
908 Masked image to examine.
909 maskPlane : `str`
910 Name of the mask plane to examine.
911
912 Returns
913 -------
914 nPix : `int`
915 Number of pixels in the requested mask plane.
916 """
917 maskBit = maskedIm.mask.getPlaneBitMask(maskPlane)
918 nPix = numpy.where(numpy.bitwise_and(maskedIm.mask.array, maskBit))[0].flatten().size
919 return nPix
A class to contain the data, WCS, and other information needed to describe an image of the sky.
Definition: Exposure.h:72
A group of labels for a filter in an exposure or coadd.
Definition: FilterLabel.h:58
A class to represent a 2-dimensional array of pixels.
Definition: Image.h:51
Represent a 2-dimensional array of bitmask pixels.
Definition: Mask.h:77
A class to manipulate images, masks, and variance as a single object.
Definition: MaskedImage.h:74
A spatially-varying transmission curve as a function of wavelength.
Parameters to control convolution.
Definition: ConvolveImage.h:50
A kernel created from an Image.
Definition: Kernel.h:471
Pass parameters to a Statistics object.
Definition: Statistics.h:83
Represent a Psf as a circularly symmetrical double Gaussian.
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)
Definition: Statistics.cc:762
def biasCorrection(maskedImage, biasMaskedImage, trimToFit=False)
def growMasks(mask, radius=0, maskNameList=['BAD'], maskValue="BAD")
def darkCorrection(maskedImage, darkMaskedImage, expScale, darkScale, invert=False, trimToFit=False)
def saturationCorrection(maskedImage, saturation, fwhm, growFootprints=1, interpolate=True, maskName='SAT', fallbackValue=None)
def interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=None)
def illuminationCorrection(maskedImage, illumMaskedImage, illumScale, trimToFit=True)
def brighterFatterCorrection(exposure, kernel, maxIter, threshold, applyGain, gains=None)
def setBadRegions(exposure, badStatistic="MEDIAN")
def applyGains(exposure, normalizeGains=False, ptcGains=None)
def getPhysicalFilter(filterLabel, log)
def trimToMatchCalibBBox(rawMaskedImage, calibMaskedImage)
def updateVariance(maskedImage, gain, readNoise)
def interpolateFromMask(maskedImage, fwhm, growSaturatedFootprints=1, maskNameList=['SAT'], fallbackValue=None)
def transposeMaskedImage(maskedImage)
Definition: isrFunctions.py:82
def flatCorrection(maskedImage, flatMaskedImage, scalingType, userScale=1.0, invert=False, trimToFit=False)
def attachTransmissionCurve(exposure, opticsTransmission=None, filterTransmission=None, sensorTransmission=None, atmosphereTransmission=None)