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