Coverage for python/lsst/ip/isr/isrFunctions.py : 9%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#
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
24from deprecated.sphinx import deprecated
26import lsst.geom
27import lsst.afw.image as afwImage
28import lsst.afw.detection as afwDetection
29import lsst.afw.math as afwMath
30import lsst.meas.algorithms as measAlg
31import lsst.afw.cameraGeom as camGeom
33from lsst.afw.geom.wcsUtils import makeDistortedTanWcs
34from lsst.meas.algorithms.detection import SourceDetectionTask
36from contextlib import contextmanager
38from .overscan import OverscanCorrectionTask, OverscanCorrectionTaskConfig
41def createPsf(fwhm):
42 """Make a double Gaussian PSF.
44 Parameters
45 ----------
46 fwhm : scalar
47 FWHM of double Gaussian smoothing kernel.
49 Returns
50 -------
51 psf : `lsst.meas.algorithms.DoubleGaussianPsf`
52 The created smoothing kernel.
53 """
54 ksize = 4*int(fwhm) + 1
55 return measAlg.DoubleGaussianPsf(ksize, ksize, fwhm/(2*math.sqrt(2*math.log(2))))
58def transposeMaskedImage(maskedImage):
59 """Make a transposed copy of a masked image.
61 Parameters
62 ----------
63 maskedImage : `lsst.afw.image.MaskedImage`
64 Image to process.
66 Returns
67 -------
68 transposed : `lsst.afw.image.MaskedImage`
69 The transposed copy of the input image.
70 """
71 transposed = maskedImage.Factory(lsst.geom.Extent2I(maskedImage.getHeight(), maskedImage.getWidth()))
72 transposed.getImage().getArray()[:] = maskedImage.getImage().getArray().T
73 transposed.getMask().getArray()[:] = maskedImage.getMask().getArray().T
74 transposed.getVariance().getArray()[:] = maskedImage.getVariance().getArray().T
75 return transposed
78def interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=None):
79 """Interpolate over defects specified in a defect list.
81 Parameters
82 ----------
83 maskedImage : `lsst.afw.image.MaskedImage`
84 Image to process.
85 defectList : `lsst.meas.algorithms.Defects`
86 List of defects to interpolate over.
87 fwhm : scalar
88 FWHM of double Gaussian smoothing kernel.
89 fallbackValue : scalar, optional
90 Fallback value if an interpolated value cannot be determined.
91 If None, then the clipped mean of the image is used.
92 """
93 psf = createPsf(fwhm)
94 if fallbackValue is None:
95 fallbackValue = afwMath.makeStatistics(maskedImage.getImage(), afwMath.MEANCLIP).getValue()
96 if 'INTRP' not in maskedImage.getMask().getMaskPlaneDict():
97 maskedImage.getMask().addMaskPlane('INTRP')
98 measAlg.interpolateOverDefects(maskedImage, psf, defectList, fallbackValue, True)
99 return maskedImage
102def makeThresholdMask(maskedImage, threshold, growFootprints=1, maskName='SAT'):
103 """Mask pixels based on threshold detection.
105 Parameters
106 ----------
107 maskedImage : `lsst.afw.image.MaskedImage`
108 Image to process. Only the mask plane is updated.
109 threshold : scalar
110 Detection threshold.
111 growFootprints : scalar, optional
112 Number of pixels to grow footprints of detected regions.
113 maskName : str, optional
114 Mask plane name, or list of names to convert
116 Returns
117 -------
118 defectList : `lsst.meas.algorithms.Defects`
119 Defect list constructed from pixels above the threshold.
120 """
121 # find saturated regions
122 thresh = afwDetection.Threshold(threshold)
123 fs = afwDetection.FootprintSet(maskedImage, thresh)
125 if growFootprints > 0:
126 fs = afwDetection.FootprintSet(fs, rGrow=growFootprints, isotropic=False)
127 fpList = fs.getFootprints()
129 # set mask
130 mask = maskedImage.getMask()
131 bitmask = mask.getPlaneBitMask(maskName)
132 afwDetection.setMaskFromFootprintList(mask, fpList, bitmask)
134 return measAlg.Defects.fromFootprintList(fpList)
137def growMasks(mask, radius=0, maskNameList=['BAD'], maskValue="BAD"):
138 """Grow a mask by an amount and add to the requested plane.
140 Parameters
141 ----------
142 mask : `lsst.afw.image.Mask`
143 Mask image to process.
144 radius : scalar
145 Amount to grow the mask.
146 maskNameList : `str` or `list` [`str`]
147 Mask names that should be grown.
148 maskValue : `str`
149 Mask plane to assign the newly masked pixels to.
150 """
151 if radius > 0:
152 thresh = afwDetection.Threshold(mask.getPlaneBitMask(maskNameList), afwDetection.Threshold.BITMASK)
153 fpSet = afwDetection.FootprintSet(mask, thresh)
154 fpSet = afwDetection.FootprintSet(fpSet, rGrow=radius, isotropic=False)
155 fpSet.setMask(mask, maskValue)
158def interpolateFromMask(maskedImage, fwhm, growSaturatedFootprints=1,
159 maskNameList=['SAT'], fallbackValue=None):
160 """Interpolate over defects identified by a particular set of mask planes.
162 Parameters
163 ----------
164 maskedImage : `lsst.afw.image.MaskedImage`
165 Image to process.
166 fwhm : scalar
167 FWHM of double Gaussian smoothing kernel.
168 growSaturatedFootprints : scalar, optional
169 Number of pixels to grow footprints for saturated pixels.
170 maskNameList : `List` of `str`, optional
171 Mask plane name.
172 fallbackValue : scalar, optional
173 Value of last resort for interpolation.
174 """
175 mask = maskedImage.getMask()
177 if growSaturatedFootprints > 0 and "SAT" in maskNameList:
178 # If we are interpolating over an area larger than the original masked region, we need
179 # to expand the original mask bit to the full area to explain why we interpolated there.
180 growMasks(mask, radius=growSaturatedFootprints, maskNameList=['SAT'], maskValue="SAT")
182 thresh = afwDetection.Threshold(mask.getPlaneBitMask(maskNameList), afwDetection.Threshold.BITMASK)
183 fpSet = afwDetection.FootprintSet(mask, thresh)
184 defectList = measAlg.Defects.fromFootprintList(fpSet.getFootprints())
186 interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=fallbackValue)
188 return maskedImage
191def saturationCorrection(maskedImage, saturation, fwhm, growFootprints=1, interpolate=True, maskName='SAT',
192 fallbackValue=None):
193 """Mark saturated pixels and optionally interpolate over them
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)
221 return maskedImage
224def trimToMatchCalibBBox(rawMaskedImage, calibMaskedImage):
225 """Compute number of edge trim pixels to match the calibration data.
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.
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.
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.")
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
268 return replacementMaskedImage
271def biasCorrection(maskedImage, biasMaskedImage, trimToFit=False):
272 """Apply bias correction in place.
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.
284 Raises
285 ------
286 RuntimeError
287 Raised if ``maskedImage`` and ``biasMaskedImage`` do not have
288 the same size.
290 """
291 if trimToFit:
292 maskedImage = trimToMatchCalibBBox(maskedImage, biasMaskedImage)
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
300def darkCorrection(maskedImage, darkMaskedImage, expScale, darkScale, invert=False, trimToFit=False):
301 """Apply dark correction in place.
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.
319 Raises
320 ------
321 RuntimeError
322 Raised if ``maskedImage`` and ``darkMaskedImage`` do not have
323 the same size.
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)
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)))
337 scale = expScale / darkScale
338 if not invert:
339 maskedImage.scaledMinus(scale, darkMaskedImage)
340 else:
341 maskedImage.scaledPlus(scale, darkMaskedImage)
344def updateVariance(maskedImage, gain, readNoise):
345 """Set the variance plane based on the image plane.
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
362def flatCorrection(maskedImage, flatMaskedImage, scalingType, userScale=1.0, invert=False, trimToFit=False):
363 """Apply flat correction in place.
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.
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)
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)))
395 # Figure out scale from the data
396 # Ideally the flats are normalized by the calibration product pipeline, but this allows some flexibility
397 # in the case that the flat is created by some other mechanism.
398 if scalingType in ('MEAN', 'MEDIAN'):
399 scalingType = afwMath.stringToStatisticsProperty(scalingType)
400 flatScale = afwMath.makeStatistics(flatMaskedImage.image, scalingType).getValue()
401 elif scalingType == 'USER':
402 flatScale = userScale
403 else:
404 raise RuntimeError('%s : %s not implemented' % ("flatCorrection", scalingType))
406 if not invert:
407 maskedImage.scaledDivides(1.0/flatScale, flatMaskedImage)
408 else:
409 maskedImage.scaledMultiplies(1.0/flatScale, flatMaskedImage)
412def illuminationCorrection(maskedImage, illumMaskedImage, illumScale, trimToFit=True):
413 """Apply illumination correction in place.
415 Parameters
416 ----------
417 maskedImage : `lsst.afw.image.MaskedImage`
418 Image to process. The image is modified.
419 illumMaskedImage : `lsst.afw.image.MaskedImage`
420 Illumination correction image of the same size as ``maskedImage``.
421 illumScale : scalar
422 Scale factor for the illumination correction.
423 trimToFit : `Bool`, optional
424 If True, raw data is symmetrically trimmed to match
425 calibration size.
427 Raises
428 ------
429 RuntimeError
430 Raised if ``maskedImage`` and ``illumMaskedImage`` do not have
431 the same size.
432 """
433 if trimToFit:
434 maskedImage = trimToMatchCalibBBox(maskedImage, illumMaskedImage)
436 if maskedImage.getBBox(afwImage.LOCAL) != illumMaskedImage.getBBox(afwImage.LOCAL):
437 raise RuntimeError("maskedImage bbox %s != illumMaskedImage bbox %s" %
438 (maskedImage.getBBox(afwImage.LOCAL), illumMaskedImage.getBBox(afwImage.LOCAL)))
440 maskedImage.scaledDivides(1.0/illumScale, illumMaskedImage)
443def overscanCorrection(ampMaskedImage, overscanImage, fitType='MEDIAN', order=1, collapseRej=3.0,
444 statControl=None, overscanIsInt=True):
445 """Apply overscan correction in place.
447 Parameters
448 ----------
449 ampMaskedImage : `lsst.afw.image.MaskedImage`
450 Image of amplifier to correct; modified.
451 overscanImage : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
452 Image of overscan; modified.
453 fitType : `str`
454 Type of fit for overscan correction. May be one of:
456 - ``MEAN``: use mean of overscan.
457 - ``MEANCLIP``: use clipped mean of overscan.
458 - ``MEDIAN``: use median of overscan.
459 - ``MEDIAN_PER_ROW``: use median per row of overscan.
460 - ``POLY``: fit with ordinary polynomial.
461 - ``CHEB``: fit with Chebyshev polynomial.
462 - ``LEG``: fit with Legendre polynomial.
463 - ``NATURAL_SPLINE``: fit with natural spline.
464 - ``CUBIC_SPLINE``: fit with cubic spline.
465 - ``AKIMA_SPLINE``: fit with Akima spline.
467 order : `int`
468 Polynomial order or number of spline knots; ignored unless
469 ``fitType`` indicates a polynomial or spline.
470 statControl : `lsst.afw.math.StatisticsControl`
471 Statistics control object. In particular, we pay attention to numSigmaClip
472 overscanIsInt : `bool`
473 Treat the overscan region as consisting of integers, even if it's been
474 converted to float. E.g. handle ties properly.
476 Returns
477 -------
478 result : `lsst.pipe.base.Struct`
479 Result struct with components:
481 - ``imageFit``: Value(s) removed from image (scalar or
482 `lsst.afw.image.Image`)
483 - ``overscanFit``: Value(s) removed from overscan (scalar or
484 `lsst.afw.image.Image`)
485 - ``overscanImage``: Overscan corrected overscan region
486 (`lsst.afw.image.Image`)
487 Raises
488 ------
489 pexExcept.Exception
490 Raised if ``fitType`` is not an allowed value.
492 Notes
493 -----
494 The ``ampMaskedImage`` and ``overscanImage`` are modified, with the fit
495 subtracted. Note that the ``overscanImage`` should not be a subimage of
496 the ``ampMaskedImage``, to avoid being subtracted twice.
498 Debug plots are available for the SPLINE fitTypes by setting the
499 `debug.display` for `name` == "lsst.ip.isr.isrFunctions". These
500 plots show the scatter plot of the overscan data (collapsed along
501 the perpendicular dimension) as a function of position on the CCD
502 (normalized between +/-1).
503 """
504 ampImage = ampMaskedImage.getImage()
506 config = OverscanCorrectionTaskConfig()
507 if fitType:
508 config.fitType = fitType
509 if order:
510 config.order = order
511 if collapseRej:
512 config.numSigmaClip = collapseRej
513 if overscanIsInt:
514 config.overscanIsInt = True
516 overscanTask = OverscanCorrectionTask(config=config)
517 return overscanTask.run(ampImage, overscanImage)
520def brighterFatterCorrection(exposure, kernel, maxIter, threshold, applyGain, gains=None):
521 """Apply brighter fatter correction in place for the image.
523 Parameters
524 ----------
525 exposure : `lsst.afw.image.Exposure`
526 Exposure to have brighter-fatter correction applied. Modified
527 by this method.
528 kernel : `numpy.ndarray`
529 Brighter-fatter kernel to apply.
530 maxIter : scalar
531 Number of correction iterations to run.
532 threshold : scalar
533 Convergence threshold in terms of the sum of absolute
534 deviations between an iteration and the previous one.
535 applyGain : `Bool`
536 If True, then the exposure values are scaled by the gain prior
537 to correction.
538 gains : `dict` [`str`, `float`]
539 A dictionary, keyed by amplifier name, of the gains to use.
540 If gains is None, the nominal gains in the amplifier object are used.
542 Returns
543 -------
544 diff : `float`
545 Final difference between iterations achieved in correction.
546 iteration : `int`
547 Number of iterations used to calculate correction.
549 Notes
550 -----
551 This correction takes a kernel that has been derived from flat
552 field images to redistribute the charge. The gradient of the
553 kernel is the deflection field due to the accumulated charge.
555 Given the original image I(x) and the kernel K(x) we can compute
556 the corrected image Ic(x) using the following equation:
558 Ic(x) = I(x) + 0.5*d/dx(I(x)*d/dx(int( dy*K(x-y)*I(y))))
560 To evaluate the derivative term we expand it as follows:
562 0.5 * ( d/dx(I(x))*d/dx(int(dy*K(x-y)*I(y))) + I(x)*d^2/dx^2(int(dy* K(x-y)*I(y))) )
564 Because we use the measured counts instead of the incident counts
565 we apply the correction iteratively to reconstruct the original
566 counts and the correction. We stop iterating when the summed
567 difference between the current corrected image and the one from
568 the previous iteration is below the threshold. We do not require
569 convergence because the number of iterations is too large a
570 computational cost. How we define the threshold still needs to be
571 evaluated, the current default was shown to work reasonably well
572 on a small set of images. For more information on the method see
573 DocuShare Document-19407.
575 The edges as defined by the kernel are not corrected because they
576 have spurious values due to the convolution.
577 """
578 image = exposure.getMaskedImage().getImage()
580 # The image needs to be units of electrons/holes
581 with gainContext(exposure, image, applyGain, gains):
583 kLx = numpy.shape(kernel)[0]
584 kLy = numpy.shape(kernel)[1]
585 kernelImage = afwImage.ImageD(kLx, kLy)
586 kernelImage.getArray()[:, :] = kernel
587 tempImage = image.clone()
589 nanIndex = numpy.isnan(tempImage.getArray())
590 tempImage.getArray()[nanIndex] = 0.
592 outImage = afwImage.ImageF(image.getDimensions())
593 corr = numpy.zeros_like(image.getArray())
594 prev_image = numpy.zeros_like(image.getArray())
595 convCntrl = afwMath.ConvolutionControl(False, True, 1)
596 fixedKernel = afwMath.FixedKernel(kernelImage)
598 # Define boundary by convolution region. The region that the correction will be
599 # calculated for is one fewer in each dimension because of the second derivative terms.
600 # NOTE: these need to use integer math, as we're using start:end as numpy index ranges.
601 startX = kLx//2
602 endX = -kLx//2
603 startY = kLy//2
604 endY = -kLy//2
606 for iteration in range(maxIter):
608 afwMath.convolve(outImage, tempImage, fixedKernel, convCntrl)
609 tmpArray = tempImage.getArray()
610 outArray = outImage.getArray()
612 with numpy.errstate(invalid="ignore", over="ignore"):
613 # First derivative term
614 gradTmp = numpy.gradient(tmpArray[startY:endY, startX:endX])
615 gradOut = numpy.gradient(outArray[startY:endY, startX:endX])
616 first = (gradTmp[0]*gradOut[0] + gradTmp[1]*gradOut[1])[1:-1, 1:-1]
618 # Second derivative term
619 diffOut20 = numpy.diff(outArray, 2, 0)[startY:endY, startX + 1:endX - 1]
620 diffOut21 = numpy.diff(outArray, 2, 1)[startY + 1:endY - 1, startX:endX]
621 second = tmpArray[startY + 1:endY - 1, startX + 1:endX - 1]*(diffOut20 + diffOut21)
623 corr[startY + 1:endY - 1, startX + 1:endX - 1] = 0.5*(first + second)
625 tmpArray[:, :] = image.getArray()[:, :]
626 tmpArray[nanIndex] = 0.
627 tmpArray[startY:endY, startX:endX] += corr[startY:endY, startX:endX]
629 if iteration > 0:
630 diff = numpy.sum(numpy.abs(prev_image - tmpArray))
632 if diff < threshold:
633 break
634 prev_image[:, :] = tmpArray[:, :]
636 image.getArray()[startY + 1:endY - 1, startX + 1:endX - 1] += \
637 corr[startY + 1:endY - 1, startX + 1:endX - 1]
639 return diff, iteration
642@contextmanager
643def gainContext(exp, image, apply, gains=None):
644 """Context manager that applies and removes gain.
646 Parameters
647 ----------
648 exp : `lsst.afw.image.Exposure`
649 Exposure to apply/remove gain.
650 image : `lsst.afw.image.Image`
651 Image to apply/remove gain.
652 apply : `Bool`
653 If True, apply and remove the amplifier gain.
654 gains : `dict` [`str`, `float`]
655 A dictionary, keyed by amplifier name, of the gains to use.
656 If gains is None, the nominal gains in the amplifier object are used.
658 Yields
659 ------
660 exp : `lsst.afw.image.Exposure`
661 Exposure with the gain applied.
662 """
663 # check we have all of them if provided because mixing and matching would
664 # be a real mess
665 if gains and apply is True:
666 ampNames = [amp.getName() for amp in exp.getDetector()]
667 for ampName in ampNames:
668 if ampName not in gains.keys():
669 raise RuntimeError(f"Gains provided to gain context, but no entry found for amp {ampName}")
671 if apply:
672 ccd = exp.getDetector()
673 for amp in ccd:
674 sim = image.Factory(image, amp.getBBox())
675 if gains:
676 gain = gains[amp.getName()]
677 else:
678 gain = amp.getGain()
679 sim *= gain
681 try:
682 yield exp
683 finally:
684 if apply:
685 ccd = exp.getDetector()
686 for amp in ccd:
687 sim = image.Factory(image, amp.getBBox())
688 if gains:
689 gain = gains[amp.getName()]
690 else:
691 gain = amp.getGain()
692 sim /= gain
695def attachTransmissionCurve(exposure, opticsTransmission=None, filterTransmission=None,
696 sensorTransmission=None, atmosphereTransmission=None):
697 """Attach a TransmissionCurve to an Exposure, given separate curves for
698 different components.
700 Parameters
701 ----------
702 exposure : `lsst.afw.image.Exposure`
703 Exposure object to modify by attaching the product of all given
704 ``TransmissionCurves`` in post-assembly trimmed detector coordinates.
705 Must have a valid ``Detector`` attached that matches the detector
706 associated with sensorTransmission.
707 opticsTransmission : `lsst.afw.image.TransmissionCurve`
708 A ``TransmissionCurve`` that represents the throughput of the optics,
709 to be evaluated in focal-plane coordinates.
710 filterTransmission : `lsst.afw.image.TransmissionCurve`
711 A ``TransmissionCurve`` that represents the throughput of the filter
712 itself, to be evaluated in focal-plane coordinates.
713 sensorTransmission : `lsst.afw.image.TransmissionCurve`
714 A ``TransmissionCurve`` that represents the throughput of the sensor
715 itself, to be evaluated in post-assembly trimmed detector coordinates.
716 atmosphereTransmission : `lsst.afw.image.TransmissionCurve`
717 A ``TransmissionCurve`` that represents the throughput of the
718 atmosphere, assumed to be spatially constant.
720 Returns
721 -------
722 combined : `lsst.afw.image.TransmissionCurve`
723 The TransmissionCurve attached to the exposure.
725 Notes
726 -----
727 All ``TransmissionCurve`` arguments are optional; if none are provided, the
728 attached ``TransmissionCurve`` will have unit transmission everywhere.
729 """
730 combined = afwImage.TransmissionCurve.makeIdentity()
731 if atmosphereTransmission is not None:
732 combined *= atmosphereTransmission
733 if opticsTransmission is not None:
734 combined *= opticsTransmission
735 if filterTransmission is not None:
736 combined *= filterTransmission
737 detector = exposure.getDetector()
738 fpToPix = detector.getTransform(fromSys=camGeom.FOCAL_PLANE,
739 toSys=camGeom.PIXELS)
740 combined = combined.transformedBy(fpToPix)
741 if sensorTransmission is not None:
742 combined *= sensorTransmission
743 exposure.getInfo().setTransmissionCurve(combined)
744 return combined
747@deprecated(reason="Camera geometry-based SkyWcs are now set when reading raws. To be removed after v19.",
748 category=FutureWarning)
749def addDistortionModel(exposure, camera):
750 """!Update the WCS in exposure with a distortion model based on camera
751 geometry.
753 Parameters
754 ----------
755 exposure : `lsst.afw.image.Exposure`
756 Exposure to process. Must contain a Detector and WCS. The
757 exposure is modified.
758 camera : `lsst.afw.cameraGeom.Camera`
759 Camera geometry.
761 Raises
762 ------
763 RuntimeError
764 Raised if ``exposure`` is lacking a Detector or WCS, or if
765 ``camera`` is None.
766 Notes
767 -----
768 Add a model for optical distortion based on geometry found in ``camera``
769 and the ``exposure``'s detector. The raw input exposure is assumed
770 have a TAN WCS that has no compensation for optical distortion.
771 Two other possibilities are:
772 - The raw input exposure already has a model for optical distortion,
773 as is the case for raw DECam data.
774 In that case you should set config.doAddDistortionModel False.
775 - The raw input exposure has a model for distortion, but it has known
776 deficiencies severe enough to be worth fixing (e.g. because they
777 cause problems for fitting a better WCS). In that case you should
778 override this method with a version suitable for your raw data.
780 """
781 wcs = exposure.getWcs()
782 if wcs is None:
783 raise RuntimeError("exposure has no WCS")
784 if camera is None:
785 raise RuntimeError("camera is None")
786 detector = exposure.getDetector()
787 if detector is None:
788 raise RuntimeError("exposure has no Detector")
789 pixelToFocalPlane = detector.getTransform(camGeom.PIXELS, camGeom.FOCAL_PLANE)
790 focalPlaneToFieldAngle = camera.getTransformMap().getTransform(camGeom.FOCAL_PLANE,
791 camGeom.FIELD_ANGLE)
792 distortedWcs = makeDistortedTanWcs(wcs, pixelToFocalPlane, focalPlaneToFieldAngle)
793 exposure.setWcs(distortedWcs)
796def applyGains(exposure, normalizeGains=False):
797 """Scale an exposure by the amplifier gains.
799 Parameters
800 ----------
801 exposure : `lsst.afw.image.Exposure`
802 Exposure to process. The image is modified.
803 normalizeGains : `Bool`, optional
804 If True, then amplifiers are scaled to force the median of
805 each amplifier to equal the median of those medians.
806 """
807 ccd = exposure.getDetector()
808 ccdImage = exposure.getMaskedImage()
810 medians = []
811 for amp in ccd:
812 sim = ccdImage.Factory(ccdImage, amp.getBBox())
813 sim *= amp.getGain()
815 if normalizeGains:
816 medians.append(numpy.median(sim.getImage().getArray()))
818 if normalizeGains:
819 median = numpy.median(numpy.array(medians))
820 for index, amp in enumerate(ccd):
821 sim = ccdImage.Factory(ccdImage, amp.getBBox())
822 if medians[index] != 0.0:
823 sim *= median/medians[index]
826def widenSaturationTrails(mask):
827 """Grow the saturation trails by an amount dependent on the width of the trail.
829 Parameters
830 ----------
831 mask : `lsst.afw.image.Mask`
832 Mask which will have the saturated areas grown.
833 """
835 extraGrowDict = {}
836 for i in range(1, 6):
837 extraGrowDict[i] = 0
838 for i in range(6, 8):
839 extraGrowDict[i] = 1
840 for i in range(8, 10):
841 extraGrowDict[i] = 3
842 extraGrowMax = 4
844 if extraGrowMax <= 0:
845 return
847 saturatedBit = mask.getPlaneBitMask("SAT")
849 xmin, ymin = mask.getBBox().getMin()
850 width = mask.getWidth()
852 thresh = afwDetection.Threshold(saturatedBit, afwDetection.Threshold.BITMASK)
853 fpList = afwDetection.FootprintSet(mask, thresh).getFootprints()
855 for fp in fpList:
856 for s in fp.getSpans():
857 x0, x1 = s.getX0(), s.getX1()
859 extraGrow = extraGrowDict.get(x1 - x0 + 1, extraGrowMax)
860 if extraGrow > 0:
861 y = s.getY() - ymin
862 x0 -= xmin + extraGrow
863 x1 -= xmin - extraGrow
865 if x0 < 0:
866 x0 = 0
867 if x1 >= width - 1:
868 x1 = width - 1
870 mask.array[y, x0:x1+1] |= saturatedBit
873def setBadRegions(exposure, badStatistic="MEDIAN"):
874 """Set all BAD areas of the chip to the average of the rest of the exposure
876 Parameters
877 ----------
878 exposure : `lsst.afw.image.Exposure`
879 Exposure to mask. The exposure mask is modified.
880 badStatistic : `str`, optional
881 Statistic to use to generate the replacement value from the
882 image data. Allowed values are 'MEDIAN' or 'MEANCLIP'.
884 Returns
885 -------
886 badPixelCount : scalar
887 Number of bad pixels masked.
888 badPixelValue : scalar
889 Value substituted for bad pixels.
891 Raises
892 ------
893 RuntimeError
894 Raised if `badStatistic` is not an allowed value.
895 """
896 if badStatistic == "MEDIAN":
897 statistic = afwMath.MEDIAN
898 elif badStatistic == "MEANCLIP":
899 statistic = afwMath.MEANCLIP
900 else:
901 raise RuntimeError("Impossible method %s of bad region correction" % badStatistic)
903 mi = exposure.getMaskedImage()
904 mask = mi.getMask()
905 BAD = mask.getPlaneBitMask("BAD")
906 INTRP = mask.getPlaneBitMask("INTRP")
908 sctrl = afwMath.StatisticsControl()
909 sctrl.setAndMask(BAD)
910 value = afwMath.makeStatistics(mi, statistic, sctrl).getValue()
912 maskArray = mask.getArray()
913 imageArray = mi.getImage().getArray()
914 badPixels = numpy.logical_and((maskArray & BAD) > 0, (maskArray & INTRP) == 0)
915 imageArray[:] = numpy.where(badPixels, value, imageArray)
917 return badPixels.sum(), value