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