25 "attachTransmissionCurve",
27 "brighterFatterCorrection",
33 "fluxConservingBrighterFatterCorrection",
40 "illuminationCorrection",
41 "interpolateDefectList",
42 "interpolateFromMask",
44 "saturationCorrection",
47 "transposeMaskedImage",
48 "trimToMatchCalibBBox",
50 "widenSaturationTrails",
52 "getExposureReadNoises",
60import lsst.afw.image
as afwImage
68from contextlib
import contextmanager
70from .defects
import Defects
74 """Make a double Gaussian PSF.
79 FWHM of double Gaussian smoothing kernel.
83 psf : `lsst.meas.algorithms.DoubleGaussianPsf`
84 The created smoothing kernel.
86 ksize = 4*int(fwhm) + 1
87 return measAlg.DoubleGaussianPsf(ksize, ksize, fwhm/(2*math.sqrt(2*math.log(2))))
91 """Make a transposed copy of a masked image.
95 maskedImage : `lsst.afw.image.MaskedImage`
100 transposed : `lsst.afw.image.MaskedImage`
101 The transposed copy of the input image.
103 transposed = maskedImage.Factory(
lsst.geom.Extent2I(maskedImage.getHeight(), maskedImage.getWidth()))
104 transposed.getImage().getArray()[:] = maskedImage.getImage().getArray().T
105 transposed.getMask().getArray()[:] = maskedImage.getMask().getArray().T
106 transposed.getVariance().getArray()[:] = maskedImage.getVariance().getArray().T
111 maskNameList=None, useLegacyInterp=True):
112 """Interpolate over defects specified in a defect list.
116 maskedImage : `lsst.afw.image.MaskedImage`
118 defectList : `lsst.meas.algorithms.Defects`
119 List of defects to interpolate over.
121 FWHM of double Gaussian smoothing kernel.
122 fallbackValue : scalar, optional
123 Fallback value if an interpolated value cannot be determined.
124 If None, then the clipped mean of the image is used.
125 maskNameList : `list [string]`
126 List of the defects to interpolate over (used for GP interpolator).
127 useLegacyInterp : `bool`
128 Use the legacy interpolation (polynomial interpolation) if True. Use
129 Gaussian Process interpolation if False.
133 The ``fwhm`` parameter is used to create a PSF, but the underlying
134 interpolation code (`lsst.meas.algorithms.interpolateOverDefects`) does
135 not currently make use of this information in legacy Interpolation, but use
136 if for the Gaussian Process as an estimation of the correlation lenght.
139 if fallbackValue
is None:
141 if 'INTRP' not in maskedImage.getMask().getMaskPlaneDict():
142 maskedImage.getMask().addMaskPlane(
'INTRP')
153 kwargs = {
"bin_spacing": 20,
154 "threshold_dynamic_binning": 2000,
155 "threshold_subdivide": 20000}
158 measAlg.interpolateOverDefects(maskedImage, psf, defectList,
159 fallbackValue=fallbackValue,
160 useFallbackValueAtEdge=
True,
162 useLegacyInterp=useLegacyInterp,
163 maskNameList=maskNameList, **kwargs)
168 """Mask pixels based on threshold detection.
172 maskedImage : `lsst.afw.image.MaskedImage`
173 Image to process. Only the mask plane is updated.
176 growFootprints : scalar, optional
177 Number of pixels to grow footprints of detected regions.
178 maskName : str, optional
179 Mask plane name, or list of names to convert
183 defectList : `lsst.meas.algorithms.Defects`
184 Defect list constructed from pixels above the threshold.
187 thresh = afwDetection.Threshold(threshold)
188 fs = afwDetection.FootprintSet(maskedImage, thresh)
190 if growFootprints > 0:
191 fs = afwDetection.FootprintSet(fs, rGrow=growFootprints, isotropic=
False)
192 fpList = fs.getFootprints()
195 mask = maskedImage.getMask()
196 bitmask = mask.getPlaneBitMask(maskName)
197 afwDetection.setMaskFromFootprintList(mask, fpList, bitmask)
199 return Defects.fromFootprintList(fpList)
202def growMasks(mask, radius=0, maskNameList=['BAD'], maskValue="BAD"):
203 """Grow a mask by an amount and add to the requested plane.
207 mask : `lsst.afw.image.Mask`
208 Mask image to process.
210 Amount to grow the mask.
211 maskNameList : `str` or `list` [`str`]
212 Mask names that should be grown.
214 Mask plane to assign the newly masked pixels to.
217 thresh = afwDetection.Threshold(mask.getPlaneBitMask(maskNameList), afwDetection.Threshold.BITMASK)
218 fpSet = afwDetection.FootprintSet(mask, thresh)
219 fpSet = afwDetection.FootprintSet(fpSet, rGrow=radius, isotropic=
False)
220 fpSet.setMask(mask, maskValue)
224 fpCore, itlEdgeBleedSatMinArea=10000,
225 itlEdgeBleedSatMaxArea=100000,
226 itlEdgeBleedThreshold=5000.,
227 itlEdgeBleedModelConstant=0.02,
228 saturatedMaskName="SAT", log=None):
229 """Mask edge bleeds in ITL detectors.
233 ccdExposure : `lsst.afw.image.Exposure`
234 Exposure to apply masking to.
235 badAmpDict : `dict` [`str`, `bool`]
236 Dictionary of amplifiers, keyed by name, value is True if
237 amplifier is fully masked.
238 fpCore : `lsst.afw.detection._detection.Footprint`
239 Footprint of saturated core.
240 itlEdgeBleedThreshold : `float`, optional
241 Threshold above median sky background for edge bleed detection
243 itlEdgeBleedModelConstant : `float`, optional
244 Constant in the decaying exponential in the edge bleed masking.
245 saturatedMaskName : `str`, optional
246 Mask name for saturation.
247 log : `logging.Logger`, optional
248 Logger to handle messages.
251 log = log
if log
else logging.getLogger(__name__)
254 satLevel = numpy.nanmedian([ccdExposure.metadata[f
"LSST ISR SATURATION LEVEL {amp.getName()}"]
255 for amp
in ccdExposure.getDetector()
if not badAmpDict[amp.getName()]])
259 xCore, yCore = fpCore.getCentroid()
261 yCoreFP = int(yCore) - fpCore.getBBox().getMinY()
265 checkCoreNbRow = fpCore.getSpans().asArray()[yCoreFP, :]
268 indexSwitchFalse = []
269 if checkCoreNbRow[0]:
273 indexSwitchTrue.append(0)
278 for i, value
in enumerate(checkCoreNbRow):
281 indexSwitchTrue.append(i)
286 indexSwitchFalse.append(i)
294 xEdgesCores.append(int((indexSwitchTrue[1] + indexSwitchFalse[0])/2))
295 xEdgesCores.append(fpCore.getSpans().asArray().shape[1])
297 for i
in range(nbCore):
298 subfp = fpCore.getSpans().asArray()[:, xEdgesCores[i]:xEdgesCores[i+1]]
299 xCoreFP = int(xEdgesCores[i] + numpy.argmax(numpy.sum(subfp, axis=0)))
301 xCore = xCoreFP + fpCore.getBBox().getMinX()
304 if subfp.shape[0] < 200:
305 yCoreFP = int(numpy.argmax(numpy.sum(subfp, axis=1)))
307 yCoreFP = int(numpy.argmax(numpy.sum(subfp[100:-100, :],
309 yCoreFP = 100+yCoreFP
312 widthSat = numpy.sum(subfp[int(yCoreFP), :])
314 subfpArea = numpy.sum(subfp)
315 if subfpArea > itlEdgeBleedSatMinArea
and subfpArea < itlEdgeBleedSatMaxArea:
318 itlEdgeBleedThreshold,
319 itlEdgeBleedModelConstant,
320 saturatedMaskName, log)
324 "Too many (%d) cores in saturated footprint to mask edge bleeds.",
329 xCore, yCore = fpCore.getCentroid()
331 yCoreFP = yCore - fpCore.getBBox().getMinY()
333 widthSat = numpy.sum(fpCore.getSpans().asArray()[int(yCoreFP), :])
335 satLevel, widthSat, itlEdgeBleedThreshold,
336 itlEdgeBleedModelConstant, saturatedMaskName, log)
341 itlEdgeBleedThreshold=5000.,
342 itlEdgeBleedModelConstant=0.03,
343 saturatedMaskName="SAT", log=None):
344 """Apply ITL edge bleed masking model.
348 ccdExposure : `lsst.afw.image.Exposure`
349 Exposure to apply masking to.
351 X coordinate of the saturated core.
353 Minimum saturation level of the detector.
355 Width of the saturated core.
356 itlEdgeBleedThreshold : `float`, optional
357 Threshold above median sky background for edge bleed detection
359 itlEdgeBleedModelConstant : `float`, optional
360 Constant in the decaying exponential in the edge bleed masking.
361 saturatedMaskName : `str`, optional
362 Mask name for saturation.
363 log : `logging.Logger`, optional
364 Logger to handle messages.
366 log = log
if log
else logging.getLogger(__name__)
368 maskedImage = ccdExposure.maskedImage
369 xmax = maskedImage.image.array.shape[1]
370 saturatedBit = maskedImage.mask.getPlaneBitMask(saturatedMaskName)
372 for amp
in ccdExposure.getDetector():
378 yBox = amp.getBBox().getCenter()[1]
379 if amp.getBBox().contains(xCore, yBox):
382 ampName = amp.getName()
392 if ampName[:2] ==
'C1':
393 sliceImage = maskedImage.image.array[:200, :]
394 sliceMask = maskedImage.mask.array[:200, :]
395 elif ampName[:2] ==
'C0':
396 sliceImage = numpy.flipud(maskedImage.image.array[-200:, :])
397 sliceMask = numpy.flipud(maskedImage.mask.array[-200:, :])
409 lowerRangeSmall = int(xCore)-5
410 upperRangeSmall = int(xCore)+5
411 if lowerRangeSmall < 0:
413 if upperRangeSmall > xmax:
414 upperRangeSmall = xmax
415 ampImageBG = numpy.median(maskedImage[amp.getBBox()].image.array)
416 edgeMedian = numpy.median(sliceImage[:50, lowerRangeSmall:upperRangeSmall])
417 if edgeMedian > (ampImageBG + itlEdgeBleedThreshold):
419 log.info(
"Found edge bleed around column %d", xCore)
428 subImageXMin = int(xCore)-250
429 subImageXMax = int(xCore)+250
432 elif subImageXMax > xmax:
435 subImage = sliceImage[:100, subImageXMin:subImageXMax]
436 maxWidthEdgeBleed = numpy.max(numpy.sum(subImage > 0.45*satLevel,
441 edgeBleedHalfWidth = \
442 int(((maxWidthEdgeBleed)*numpy.exp(-itlEdgeBleedModelConstant*y)
444 lowerRange = int(xCore)-edgeBleedHalfWidth
445 upperRange = int(xCore)+edgeBleedHalfWidth
451 if upperRange > xmax:
453 sliceMask[y, lowerRange:upperRange] |= saturatedBit
457 """Mask columns presenting saturation sag in saturated footprints in
462 ccdExposure : `lsst.afw.image.Exposure`
463 Exposure to apply masking to.
464 fpCore : `lsst.afw.detection._detection.Footprint`
465 Footprint of saturated core.
466 saturatedMaskName : `str`, optional
467 Mask name for saturation.
472 maskedImage = ccdExposure.maskedImage
473 saturatedBit = maskedImage.mask.getPlaneBitMask(saturatedMaskName)
475 cc = numpy.sum(fpCore.getSpans().asArray(), axis=0)
478 columnsToMaskFP = numpy.where(cc > fpCore.getSpans().asArray().shape[0]/5.)
480 columnsToMask = [x + int(fpCore.getBBox().getMinX())
for x
in columnsToMaskFP]
481 maskedImage.mask.array[:, columnsToMask] |= saturatedBit
484def maskITLDip(exposure, detectorConfig, maskPlaneNames=["SUSPECT", "ITL_DIP"], log=None):
485 """Add mask bits according to the ITL dip model.
489 exposure : `lsst.afw.image.Exposure`
490 Exposure to do ITL dip masking.
491 detectorConfig : `lsst.ip.isr.overscanAmpConfig.OverscanDetectorConfig`
492 Configuration for this detector.
493 maskPlaneNames : `list [`str`], optional
494 Name of the ITL Dip mask planes.
495 log : `logging.Logger`, optional
496 If not set, a default logger will be used.
498 if detectorConfig.itlDipBackgroundFraction == 0.0:
503 log = logging.getLogger(__name__)
505 thresh = afwDetection.Threshold(
506 exposure.mask.getPlaneBitMask(
"SAT"),
507 afwDetection.Threshold.BITMASK,
509 fpList = afwDetection.FootprintSet(exposure.mask, thresh).getFootprints()
511 heights = numpy.asarray([fp.getBBox().getHeight()
for fp
in fpList])
513 largeHeights, = numpy.where(heights >= detectorConfig.itlDipMinHeight)
515 if len(largeHeights) == 0:
519 approxBackground = numpy.median(exposure.image.array)
520 maskValue = exposure.mask.getPlaneBitMask(maskPlaneNames)
522 maskBak = exposure.mask.array.copy()
525 for index
in largeHeights:
527 center = fp.getCentroid()
529 nSat = numpy.sum(fp.getSpans().asArray(), axis=0)
530 width = numpy.sum(nSat > detectorConfig.itlDipMinHeight)
532 if width < detectorConfig.itlDipMinWidth:
535 width = numpy.clip(width,
None, detectorConfig.itlDipMaxWidth)
537 dipMax = detectorConfig.itlDipBackgroundFraction * approxBackground * width
540 if dipMax < detectorConfig.itlDipMinBackgroundNoiseFraction * numpy.sqrt(approxBackground):
543 minCol = int(center.getX() - (detectorConfig.itlDipWidthScale * width) / 2.)
544 maxCol = int(center.getX() + (detectorConfig.itlDipWidthScale * width) / 2.)
545 minCol = numpy.clip(minCol, 0,
None)
546 maxCol = numpy.clip(maxCol,
None, exposure.mask.array.shape[1] - 1)
549 "Found ITL dip (width %d; bkg %.2f); masking column %d to %d",
556 exposure.mask.array[:, minCol: maxCol + 1] |= maskValue
558 nMaskedCols += (maxCol - minCol + 1)
560 if nMaskedCols > detectorConfig.itlDipMaxColsPerImage:
562 "Too many (%d) columns would be masked on this image from dip masking; restoring original mask.",
565 exposure.mask.array[:, :] = maskBak
569 maskNameList=['SAT'], fallbackValue=None, useLegacyInterp=True):
570 """Interpolate over defects identified by a particular set of mask planes.
574 maskedImage : `lsst.afw.image.MaskedImage`
577 FWHM of double Gaussian smoothing kernel.
578 growSaturatedFootprints : scalar, optional
579 Number of pixels to grow footprints for saturated pixels.
580 maskNameList : `List` of `str`, optional
582 fallbackValue : scalar, optional
583 Value of last resort for interpolation.
587 The ``fwhm`` parameter is used to create a PSF, but the underlying
588 interpolation code (`lsst.meas.algorithms.interpolateOverDefects`) does
589 not currently make use of this information.
591 mask = maskedImage.getMask()
593 if growSaturatedFootprints > 0
and "SAT" in maskNameList:
597 growMasks(mask, radius=growSaturatedFootprints, maskNameList=[
'SAT'], maskValue=
"SAT")
599 thresh = afwDetection.Threshold(mask.getPlaneBitMask(maskNameList), afwDetection.Threshold.BITMASK)
600 fpSet = afwDetection.FootprintSet(mask, thresh)
601 defectList = Defects.fromFootprintList(fpSet.getFootprints())
604 maskNameList=maskNameList, useLegacyInterp=useLegacyInterp)
610 fallbackValue=None, useLegacyInterp=True):
611 """Mark saturated pixels and optionally interpolate over them
615 maskedImage : `lsst.afw.image.MaskedImage`
618 Saturation level used as the detection threshold.
620 FWHM of double Gaussian smoothing kernel.
621 growFootprints : scalar, optional
622 Number of pixels to grow footprints of detected regions.
623 interpolate : Bool, optional
624 If True, saturated pixels are interpolated over.
625 maskName : str, optional
627 fallbackValue : scalar, optional
628 Value of last resort for interpolation.
632 The ``fwhm`` parameter is used to create a PSF, but the underlying
633 interpolation code (`lsst.meas.algorithms.interpolateOverDefects`) does
634 not currently make use of this information.
637 maskedImage=maskedImage,
638 threshold=saturation,
639 growFootprints=growFootprints,
644 maskNameList=[maskName], useLegacyInterp=useLegacyInterp)
650 """Compute number of edge trim pixels to match the calibration data.
652 Use the dimension difference between the raw exposure and the
653 calibration exposure to compute the edge trim pixels. This trim
654 is applied symmetrically, with the same number of pixels masked on
659 rawMaskedImage : `lsst.afw.image.MaskedImage`
661 calibMaskedImage : `lsst.afw.image.MaskedImage`
662 Calibration image to draw new bounding box from.
666 replacementMaskedImage : `lsst.afw.image.MaskedImage`
667 ``rawMaskedImage`` trimmed to the appropriate size.
672 Raised if ``rawMaskedImage`` cannot be symmetrically trimmed to
673 match ``calibMaskedImage``.
675 nx, ny = rawMaskedImage.getBBox().getDimensions() - calibMaskedImage.getBBox().getDimensions()
677 raise RuntimeError(
"Raw and calib maskedImages are trimmed differently in X and Y.")
679 raise RuntimeError(
"Calibration maskedImage is trimmed unevenly in X.")
681 raise RuntimeError(
"Calibration maskedImage is larger than raw data.")
685 replacementMaskedImage = rawMaskedImage[nEdge:-nEdge, nEdge:-nEdge, afwImage.LOCAL]
686 SourceDetectionTask.setEdgeBits(
688 replacementMaskedImage.getBBox(),
689 rawMaskedImage.getMask().getPlaneBitMask(
"EDGE")
692 replacementMaskedImage = rawMaskedImage
694 return replacementMaskedImage
698 """Apply bias correction in place.
702 maskedImage : `lsst.afw.image.MaskedImage`
703 Image to process. The image is modified by this method.
704 biasMaskedImage : `lsst.afw.image.MaskedImage`
705 Bias image of the same size as ``maskedImage``
706 trimToFit : `Bool`, optional
707 If True, raw data is symmetrically trimmed to match
713 Raised if ``maskedImage`` and ``biasMaskedImage`` do not have
720 if maskedImage.getBBox(afwImage.LOCAL) != biasMaskedImage.getBBox(afwImage.LOCAL):
721 raise RuntimeError(
"maskedImage bbox %s != biasMaskedImage bbox %s" %
722 (maskedImage.getBBox(afwImage.LOCAL), biasMaskedImage.getBBox(afwImage.LOCAL)))
723 maskedImage -= biasMaskedImage
726def darkCorrection(maskedImage, darkMaskedImage, expScale, darkScale, invert=False, trimToFit=False):
727 """Apply dark correction in place.
731 maskedImage : `lsst.afw.image.MaskedImage`
732 Image to process. The image is modified by this method.
733 darkMaskedImage : `lsst.afw.image.MaskedImage`
734 Dark image of the same size as ``maskedImage``.
736 Dark exposure time for ``maskedImage``.
738 Dark exposure time for ``darkMaskedImage``.
739 invert : `Bool`, optional
740 If True, re-add the dark to an already corrected image.
741 trimToFit : `Bool`, optional
742 If True, raw data is symmetrically trimmed to match
748 Raised if ``maskedImage`` and ``darkMaskedImage`` do not have
753 The dark correction is applied by calculating:
754 maskedImage -= dark * expScaling / darkScaling
759 if maskedImage.getBBox(afwImage.LOCAL) != darkMaskedImage.getBBox(afwImage.LOCAL):
760 raise RuntimeError(
"maskedImage bbox %s != darkMaskedImage bbox %s" %
761 (maskedImage.getBBox(afwImage.LOCAL), darkMaskedImage.getBBox(afwImage.LOCAL)))
763 scale = expScale / darkScale
765 maskedImage.scaledMinus(scale, darkMaskedImage)
767 maskedImage.scaledPlus(scale, darkMaskedImage)
771 """Set the variance plane based on the image plane.
773 The maskedImage must have units of `adu` (if gain != 1.0) or
774 electron (if gain == 1.0). This routine will always produce a
775 variance plane in the same units as the image.
779 maskedImage : `lsst.afw.image.MaskedImage`
780 Image to process. The variance plane is modified.
782 The amplifier gain in electron/adu.
784 The amplifier read noise in electron/pixel.
785 replace : `bool`, optional
786 Replace the current variance? If False, the image
787 variance will be added to the current variance plane.
789 var = maskedImage.variance
791 var[:, :] = maskedImage.image
793 var[:, :] += maskedImage.image
795 var += (readNoise/gain)**2
798def flatCorrection(maskedImage, flatMaskedImage, scalingType, userScale=1.0, invert=False, trimToFit=False):
799 """Apply flat correction in place.
803 maskedImage : `lsst.afw.image.MaskedImage`
804 Image to process. The image is modified.
805 flatMaskedImage : `lsst.afw.image.MaskedImage`
806 Flat image of the same size as ``maskedImage``
808 Flat scale computation method. Allowed values are 'MEAN',
810 userScale : scalar, optional
811 Scale to use if ``scalingType='USER'``.
812 invert : `Bool`, optional
813 If True, unflatten an already flattened image.
814 trimToFit : `Bool`, optional
815 If True, raw data is symmetrically trimmed to match
821 Raised if ``maskedImage`` and ``flatMaskedImage`` do not have
822 the same size or if ``scalingType`` is not an allowed value.
827 if maskedImage.getBBox(afwImage.LOCAL) != flatMaskedImage.getBBox(afwImage.LOCAL):
828 raise RuntimeError(
"maskedImage bbox %s != flatMaskedImage bbox %s" %
829 (maskedImage.getBBox(afwImage.LOCAL), flatMaskedImage.getBBox(afwImage.LOCAL)))
835 if scalingType
in (
'MEAN',
'MEDIAN'):
838 elif scalingType ==
'USER':
839 flatScale = userScale
841 raise RuntimeError(
'%s : %s not implemented' % (
"flatCorrection", scalingType))
844 maskedImage.scaledDivides(1.0/flatScale, flatMaskedImage)
846 maskedImage.scaledMultiplies(1.0/flatScale, flatMaskedImage)
850 """Apply illumination correction in place.
854 maskedImage : `lsst.afw.image.MaskedImage`
855 Image to process. The image is modified.
856 illumMaskedImage : `lsst.afw.image.MaskedImage`
857 Illumination correction image of the same size as ``maskedImage``.
859 Scale factor for the illumination correction.
860 trimToFit : `Bool`, optional
861 If True, raw data is symmetrically trimmed to match
867 Raised if ``maskedImage`` and ``illumMaskedImage`` do not have
873 if maskedImage.getBBox(afwImage.LOCAL) != illumMaskedImage.getBBox(afwImage.LOCAL):
874 raise RuntimeError(
"maskedImage bbox %s != illumMaskedImage bbox %s" %
875 (maskedImage.getBBox(afwImage.LOCAL), illumMaskedImage.getBBox(afwImage.LOCAL)))
877 maskedImage.scaledDivides(1.0/illumScale, illumMaskedImage)
881 """Apply brighter fatter correction in place for the image.
885 exposure : `lsst.afw.image.Exposure`
886 Exposure to have brighter-fatter correction applied. Modified
888 kernel : `numpy.ndarray`
889 Brighter-fatter kernel to apply.
891 Number of correction iterations to run.
893 Convergence threshold in terms of the sum of absolute
894 deviations between an iteration and the previous one.
896 If True, then the exposure values are scaled by the gain prior
898 gains : `dict` [`str`, `float`]
899 A dictionary, keyed by amplifier name, of the gains to use.
900 If gains is None, the nominal gains in the amplifier object are used.
905 Final difference between iterations achieved in correction.
907 Number of iterations used to calculate correction.
911 This correction takes a kernel that has been derived from flat
912 field images to redistribute the charge. The gradient of the
913 kernel is the deflection field due to the accumulated charge.
915 Given the original image I(x) and the kernel K(x) we can compute
916 the corrected image Ic(x) using the following equation:
918 Ic(x) = I(x) + 0.5*d/dx(I(x)*d/dx(int( dy*K(x-y)*I(y))))
920 To evaluate the derivative term we expand it as follows:
922 0.5 * ( d/dx(I(x))*d/dx(int(dy*K(x-y)*I(y)))
923 + I(x)*d^2/dx^2(int(dy* K(x-y)*I(y))) )
925 Because we use the measured counts instead of the incident counts
926 we apply the correction iteratively to reconstruct the original
927 counts and the correction. We stop iterating when the summed
928 difference between the current corrected image and the one from
929 the previous iteration is below the threshold. We do not require
930 convergence because the number of iterations is too large a
931 computational cost. How we define the threshold still needs to be
932 evaluated, the current default was shown to work reasonably well
933 on a small set of images. For more information on the method see
934 DocuShare Document-19407.
936 The edges as defined by the kernel are not corrected because they
937 have spurious values due to the convolution.
939 image = exposure.getMaskedImage().getImage()
942 with gainContext(exposure, image, applyGain, gains):
944 kLx = numpy.shape(kernel)[0]
945 kLy = numpy.shape(kernel)[1]
946 kernelImage = afwImage.ImageD(kLx, kLy)
947 kernelImage.getArray()[:, :] = kernel
948 tempImage = afwImage.ImageD(image, deep=
True)
950 nanIndex = numpy.isnan(tempImage.getArray())
951 tempImage.getArray()[nanIndex] = 0.
953 outImage = afwImage.ImageD(image.getDimensions())
954 corr = numpy.zeros(image.array.shape, dtype=numpy.float64)
955 prev_image = numpy.zeros(image.array.shape, dtype=numpy.float64)
969 for iteration
in range(maxIter):
972 tmpArray = tempImage.getArray()
973 outArray = outImage.getArray()
975 with numpy.errstate(invalid=
"ignore", over=
"ignore"):
977 gradTmp = numpy.gradient(tmpArray[startY:endY, startX:endX])
978 gradOut = numpy.gradient(outArray[startY:endY, startX:endX])
979 first = (gradTmp[0]*gradOut[0] + gradTmp[1]*gradOut[1])[1:-1, 1:-1]
982 diffOut20 = numpy.diff(outArray, 2, 0)[startY:endY, startX + 1:endX - 1]
983 diffOut21 = numpy.diff(outArray, 2, 1)[startY + 1:endY - 1, startX:endX]
984 second = tmpArray[startY + 1:endY - 1, startX + 1:endX - 1]*(diffOut20 + diffOut21)
986 corr[startY + 1:endY - 1, startX + 1:endX - 1] = 0.5*(first + second)
988 tmpArray[:, :] = image.getArray()[:, :]
989 tmpArray[nanIndex] = 0.
990 tmpArray[startY:endY, startX:endX] += corr[startY:endY, startX:endX]
993 diff = numpy.sum(numpy.abs(prev_image - tmpArray), dtype=numpy.float64)
997 prev_image[:, :] = tmpArray[:, :]
999 image.getArray()[startY + 1:endY - 1, startX + 1:endX - 1] += \
1000 corr[startY + 1:endY - 1, startX + 1:endX - 1]
1002 return diff, iteration
1006 """Take the input convolved deflection potential and the flux array
1007 to compute and apply the flux transfer into the correction array.
1011 cFunc: `numpy.array`
1012 Deflection potential, being the convolution of the flux F with the
1014 fStep: `numpy.array`
1015 The array of flux values which act as the source of the flux transfer.
1016 correctionMode: `bool`
1017 Defines if applying correction (True) or generating sims (False).
1022 BFE correction array
1025 if cFunc.shape != fStep.shape:
1026 raise RuntimeError(f
'transferFlux: array shapes do not match: {cFunc.shape}, {fStep.shape}')
1038 corr = numpy.zeros(cFunc.shape, dtype=numpy.float64)
1041 yDim, xDim = cFunc.shape
1042 y = numpy.arange(yDim, dtype=int)
1043 x = numpy.arange(xDim, dtype=int)
1044 xc, yc = numpy.meshgrid(x, y)
1050 diff = numpy.diff(cFunc, axis=ax)
1053 gx = numpy.zeros(cFunc.shape, dtype=numpy.float64)
1054 yDiff, xDiff = diff.shape
1055 gx[:yDiff, :xDiff] += diff
1060 for i, sel
in enumerate([gx > 0, gx < 0]):
1061 xSelPixels = xc[sel]
1062 ySelPixels = yc[sel]
1076 flux = factor * fStep[sel]*gx[sel]
1079 flux = factor * fStep[yPix, xPix]*gx[sel]
1083 corr[yPix, xPix] += flux
1090 gains=None, correctionMode=True):
1091 """Apply brighter fatter correction in place for the image.
1093 This version presents a modified version of the algorithm
1094 found in ``lsst.ip.isr.isrFunctions.brighterFatterCorrection``
1095 which conserves the image flux, resulting in improved
1096 correction of the cores of stars. The convolution has also been
1097 modified to mitigate edge effects.
1101 exposure : `lsst.afw.image.Exposure`
1102 Exposure to have brighter-fatter correction applied. Modified
1104 kernel : `numpy.ndarray`
1105 Brighter-fatter kernel to apply.
1107 Number of correction iterations to run.
1109 Convergence threshold in terms of the sum of absolute
1110 deviations between an iteration and the previous one.
1112 If True, then the exposure values are scaled by the gain prior
1114 gains : `dict` [`str`, `float`]
1115 A dictionary, keyed by amplifier name, of the gains to use.
1116 If gains is None, the nominal gains in the amplifier object are used.
1117 correctionMode : `Bool`
1118 If True (default) the function applies correction for BFE. If False,
1119 the code can instead be used to generate a simulation of BFE (sign
1120 change in the direction of the effect)
1125 Final difference between iterations achieved in correction.
1127 Number of iterations used to calculate correction.
1131 Modified version of ``lsst.ip.isr.isrFunctions.brighterFatterCorrection``.
1133 This correction takes a kernel that has been derived from flat
1134 field images to redistribute the charge. The gradient of the
1135 kernel is the deflection field due to the accumulated charge.
1137 Given the original image I(x) and the kernel K(x) we can compute
1138 the corrected image Ic(x) using the following equation:
1140 Ic(x) = I(x) + 0.5*d/dx(I(x)*d/dx(int( dy*K(x-y)*I(y))))
1142 Improved algorithm at this step applies the divergence theorem to
1143 obtain a pixelised correction.
1145 Because we use the measured counts instead of the incident counts
1146 we apply the correction iteratively to reconstruct the original
1147 counts and the correction. We stop iterating when the summed
1148 difference between the current corrected image and the one from
1149 the previous iteration is below the threshold. We do not require
1150 convergence because the number of iterations is too large a
1151 computational cost. How we define the threshold still needs to be
1152 evaluated, the current default was shown to work reasonably well
1153 on a small set of images.
1155 Edges are handled in the convolution by padding. This is still not
1156 a physical model for the edge, but avoids discontinuity in the correction.
1158 Author of modified version: Lance.Miller@physics.ox.ac.uk
1161 image = exposure.getMaskedImage().getImage()
1164 with gainContext(exposure, image, applyGain, gains):
1167 kLy, kLx = kernel.shape
1168 kernelImage = afwImage.ImageD(kLx, kLy)
1169 kernelImage.getArray()[:, :] = kernel
1170 tempImage = afwImage.ImageD(image, deep=
True)
1172 nanIndex = numpy.isnan(tempImage.getArray())
1173 tempImage.getArray()[nanIndex] = 0.
1175 outImage = afwImage.ImageD(image.getDimensions())
1176 corr = numpy.zeros(image.array.shape, dtype=numpy.float64)
1177 prevImage = numpy.zeros(image.array.shape, dtype=numpy.float64)
1183 kLy = 2 * ((1+kLy)//2)
1184 kLx = 2 * ((1+kLx)//2)
1191 imYdimension, imXdimension = tempImage.array.shape
1192 imean = numpy.mean(tempImage.getArray()[~nanIndex], dtype=numpy.float64)
1195 tempImage.array[nanIndex] = 0.0
1196 padArray = numpy.pad(tempImage.getArray(), ((0, kLy), (0, kLx)))
1197 outImage = afwImage.ImageD(numpy.pad(outImage.getArray(), ((0, kLy), (0, kLx))))
1199 padImage = afwImage.ImageD(padArray.shape[1], padArray.shape[0])
1200 padImage.array[:] = padArray
1202 for iteration
in range(maxIter):
1207 tmpArray = tempImage.getArray()
1208 outArray = outImage.getArray()
1211 outArray = outArray[:imYdimension, :imXdimension]
1214 corr[...] =
transferFlux(outArray, tmpArray, correctionMode=correctionMode)
1217 tmpArray[:, :] = image.getArray()[:, :]
1219 tmpArray[nanIndex] = 0.
1223 tempImage.array[nanIndex] = 0.
1224 padArray = numpy.pad(tempImage.getArray(), ((0, kLy), (0, kLx)))
1227 diff = numpy.sum(numpy.abs(prevImage - tmpArray), dtype=numpy.float64)
1229 if diff < threshold:
1231 prevImage[:, :] = tmpArray[:, :]
1233 image.getArray()[:] += corr[:]
1235 return diff, iteration
1239def gainContext(exp, image, apply, gains=None, invert=False, isTrimmed=True):
1240 """Context manager that applies and removes gain.
1244 exp : `lsst.afw.image.Exposure`
1245 Exposure to apply/remove gain.
1246 image : `lsst.afw.image.Image`
1247 Image to apply/remove gain.
1249 If True, apply and remove the amplifier gain.
1250 gains : `dict` [`str`, `float`], optional
1251 A dictionary, keyed by amplifier name, of the gains to use.
1252 If gains is None, the nominal gains in the amplifier object are used.
1253 invert : `bool`, optional
1254 Invert the gains (e.g. convert electrons to adu temporarily)?
1255 isTrimmed : `bool`, optional
1256 Is this a trimmed exposure?
1260 exp : `lsst.afw.image.Exposure`
1261 Exposure with the gain applied.
1265 if gains
and apply
is True:
1266 ampNames = [amp.getName()
for amp
in exp.getDetector()]
1267 for ampName
in ampNames:
1268 if ampName
not in gains.keys():
1269 raise RuntimeError(f
"Gains provided to gain context, but no entry found for amp {ampName}")
1272 ccd = exp.getDetector()
1274 sim = image.Factory(image, amp.getBBox()
if isTrimmed
else amp.getRawBBox())
1276 gain = gains[amp.getName()]
1278 gain = amp.getGain()
1288 ccd = exp.getDetector()
1290 sim = image.Factory(image, amp.getBBox()
if isTrimmed
else amp.getRawBBox())
1292 gain = gains[amp.getName()]
1294 gain = amp.getGain()
1302 sensorTransmission=None, atmosphereTransmission=None):
1303 """Attach a TransmissionCurve to an Exposure, given separate curves for
1304 different components.
1308 exposure : `lsst.afw.image.Exposure`
1309 Exposure object to modify by attaching the product of all given
1310 ``TransmissionCurves`` in post-assembly trimmed detector coordinates.
1311 Must have a valid ``Detector`` attached that matches the detector
1312 associated with sensorTransmission.
1313 opticsTransmission : `lsst.afw.image.TransmissionCurve`
1314 A ``TransmissionCurve`` that represents the throughput of the optics,
1315 to be evaluated in focal-plane coordinates.
1316 filterTransmission : `lsst.afw.image.TransmissionCurve`
1317 A ``TransmissionCurve`` that represents the throughput of the filter
1318 itself, to be evaluated in focal-plane coordinates.
1319 sensorTransmission : `lsst.afw.image.TransmissionCurve`
1320 A ``TransmissionCurve`` that represents the throughput of the sensor
1321 itself, to be evaluated in post-assembly trimmed detector coordinates.
1322 atmosphereTransmission : `lsst.afw.image.TransmissionCurve`
1323 A ``TransmissionCurve`` that represents the throughput of the
1324 atmosphere, assumed to be spatially constant.
1328 combined : `lsst.afw.image.TransmissionCurve`
1329 The TransmissionCurve attached to the exposure.
1333 All ``TransmissionCurve`` arguments are optional; if none are provided, the
1334 attached ``TransmissionCurve`` will have unit transmission everywhere.
1336 combined = afwImage.TransmissionCurve.makeIdentity()
1337 if atmosphereTransmission
is not None:
1338 combined *= atmosphereTransmission
1339 if opticsTransmission
is not None:
1340 combined *= opticsTransmission
1341 if filterTransmission
is not None:
1342 combined *= filterTransmission
1343 detector = exposure.getDetector()
1344 fpToPix = detector.getTransform(fromSys=camGeom.FOCAL_PLANE,
1345 toSys=camGeom.PIXELS)
1346 combined = combined.transformedBy(fpToPix)
1347 if sensorTransmission
is not None:
1348 combined *= sensorTransmission
1349 exposure.getInfo().setTransmissionCurve(combined)
1353def applyGains(exposure, normalizeGains=False, ptcGains=None, isTrimmed=True):
1354 """Scale an exposure by the amplifier gains.
1358 exposure : `lsst.afw.image.Exposure`
1359 Exposure to process. The image is modified.
1360 normalizeGains : `Bool`, optional
1361 If True, then amplifiers are scaled to force the median of
1362 each amplifier to equal the median of those medians.
1363 ptcGains : `dict`[`str`], optional
1364 Dictionary keyed by amp name containing the PTC gains.
1365 isTrimmed : `bool`, optional
1366 Is the input image trimmed?
1368 ccd = exposure.getDetector()
1369 ccdImage = exposure.getMaskedImage()
1374 sim = ccdImage.Factory(ccdImage, amp.getBBox())
1376 sim = ccdImage.Factory(ccdImage, amp.getRawBBox())
1378 sim *= ptcGains[amp.getName()]
1380 sim *= amp.getGain()
1383 medians.append(numpy.median(sim.getImage().getArray()))
1386 median = numpy.median(numpy.array(medians))
1387 for index, amp
in enumerate(ccd):
1389 sim = ccdImage.Factory(ccdImage, amp.getBBox())
1391 sim = ccdImage.Factory(ccdImage, amp.getRawBBox())
1392 if medians[index] != 0.0:
1393 sim *= median/medians[index]
1397 """Grow the saturation trails by an amount dependent on the width of the
1402 mask : `lsst.afw.image.Mask`
1403 Mask which will have the saturated areas grown.
1407 for i
in range(1, 6):
1408 extraGrowDict[i] = 0
1409 for i
in range(6, 8):
1410 extraGrowDict[i] = 1
1411 for i
in range(8, 10):
1412 extraGrowDict[i] = 3
1415 if extraGrowMax <= 0:
1418 saturatedBit = mask.getPlaneBitMask(
"SAT")
1420 xmin, ymin = mask.getBBox().getMin()
1421 width = mask.getWidth()
1423 thresh = afwDetection.Threshold(saturatedBit, afwDetection.Threshold.BITMASK)
1424 fpList = afwDetection.FootprintSet(mask, thresh).getFootprints()
1427 for s
in fp.getSpans():
1428 x0, x1 = s.getX0(), s.getX1()
1430 extraGrow = extraGrowDict.get(x1 - x0 + 1, extraGrowMax)
1433 x0 -= xmin + extraGrow
1434 x1 -= xmin - extraGrow
1441 mask.array[y, x0:x1+1] |= saturatedBit
1445 """Set all BAD areas of the chip to the average of the rest of the exposure
1449 exposure : `lsst.afw.image.Exposure`
1450 Exposure to mask. The exposure mask is modified.
1451 badStatistic : `str`, optional
1452 Statistic to use to generate the replacement value from the
1453 image data. Allowed values are 'MEDIAN' or 'MEANCLIP'.
1457 badPixelCount : scalar
1458 Number of bad pixels masked.
1459 badPixelValue : scalar
1460 Value substituted for bad pixels.
1465 Raised if `badStatistic` is not an allowed value.
1467 if badStatistic ==
"MEDIAN":
1468 statistic = afwMath.MEDIAN
1469 elif badStatistic ==
"MEANCLIP":
1470 statistic = afwMath.MEANCLIP
1472 raise RuntimeError(
"Impossible method %s of bad region correction" % badStatistic)
1474 mi = exposure.getMaskedImage()
1476 BAD = mask.getPlaneBitMask(
"BAD")
1477 INTRP = mask.getPlaneBitMask(
"INTRP")
1480 sctrl.setAndMask(BAD)
1483 maskArray = mask.getArray()
1484 imageArray = mi.getImage().getArray()
1485 badPixels = numpy.logical_and((maskArray & BAD) > 0, (maskArray & INTRP) == 0)
1486 imageArray[:] = numpy.where(badPixels, value, imageArray)
1488 return badPixels.sum(), value
1492 """Check to see if an exposure is in a filter specified by a list.
1494 The goal of this is to provide a unified filter checking interface
1495 for all filter dependent stages.
1499 exposure : `lsst.afw.image.Exposure`
1500 Exposure to examine.
1501 filterList : `list` [`str`]
1502 List of physical_filter names to check.
1503 log : `logging.Logger`
1504 Logger to handle messages.
1509 True if the exposure's filter is contained in the list.
1511 if len(filterList) == 0:
1513 thisFilter = exposure.getFilter()
1514 if thisFilter
is None:
1515 log.warning(
"No FilterLabel attached to this exposure!")
1519 if thisPhysicalFilter
in filterList:
1521 elif thisFilter.bandLabel
in filterList:
1523 log.warning(
"Physical filter (%s) should be used instead of band %s for filter configurations"
1524 " (%s)", thisPhysicalFilter, thisFilter.bandLabel, filterList)
1531 """Get the physical filter label associated with the given filterLabel.
1533 If ``filterLabel`` is `None` or there is no physicalLabel attribute
1534 associated with the given ``filterLabel``, the returned label will be
1539 filterLabel : `lsst.afw.image.FilterLabel`
1540 The `lsst.afw.image.FilterLabel` object from which to derive the
1541 physical filter label.
1542 log : `logging.Logger`
1543 Logger to handle messages.
1547 physicalFilter : `str`
1548 The value returned by the physicalLabel attribute of ``filterLabel`` if
1549 it exists, otherwise set to \"Unknown\".
1551 if filterLabel
is None:
1552 physicalFilter =
"Unknown"
1553 log.warning(
"filterLabel is None. Setting physicalFilter to \"Unknown\".")
1556 physicalFilter = filterLabel.physicalLabel
1557 except RuntimeError:
1558 log.warning(
"filterLabel has no physicalLabel attribute. Setting physicalFilter to \"Unknown\".")
1559 physicalFilter =
"Unknown"
1560 return physicalFilter
1564 """Count the number of pixels in a given mask plane.
1568 maskedIm : `~lsst.afw.image.MaskedImage`
1569 Masked image to examine.
1571 Name of the mask plane to examine.
1576 Number of pixels in the requested mask plane.
1578 maskBit = maskedIm.mask.getPlaneBitMask(maskPlane)
1579 nPix = numpy.where(numpy.bitwise_and(maskedIm.mask.array, maskBit))[0].flatten().size
1584 """Get the per-amplifier gains used for this exposure.
1588 exposure : `lsst.afw.image.Exposure`
1589 The exposure to find gains for.
1593 gains : `dict` [`str` `float`]
1594 Dictionary of gain values, keyed by amplifier name.
1595 Returns empty dict when detector is None.
1597 det = exposure.getDetector()
1601 metadata = exposure.getMetadata()
1604 ampName = amp.getName()
1606 if (key1 := f
"LSST ISR GAIN {ampName}")
in metadata:
1607 gains[ampName] = metadata[key1]
1608 elif (key2 := f
"LSST GAIN {ampName}")
in metadata:
1609 gains[ampName] = metadata[key2]
1611 gains[ampName] = amp.getGain()
1616 """Get the per-amplifier read noise used for this exposure.
1620 exposure : `lsst.afw.image.Exposure`
1621 The exposure to find read noise for.
1625 readnoises : `dict` [`str` `float`]
1626 Dictionary of read noise values, keyed by amplifier name.
1627 Returns empty dict when detector is None.
1629 det = exposure.getDetector()
1633 metadata = exposure.getMetadata()
1636 ampName = amp.getName()
1638 if (key1 := f
"LSST ISR READNOISE {ampName}")
in metadata:
1639 readnoises[ampName] = metadata[key1]
1640 elif (key2 := f
"LSST READNOISE {ampName}")
in metadata:
1641 readnoises[ampName] = metadata[key2]
1643 readnoises[ampName] = amp.getReadNoise()
1648 """Check if the unused pixels (pre-/over-scan pixels) have
1649 been trimmed from an exposure.
1653 exposure : `lsst.afw.image.Exposure`
1654 The exposure to check.
1659 True if the image is trimmed, else False.
1661 return exposure.getDetector().getBBox() == exposure.getBBox()
1665 """Check if the unused pixels (pre-/over-scan pixels) have
1666 been trimmed from an image
1670 image : `lsst.afw.image.Image`
1672 detector : `lsst.afw.cameraGeom.Detector`
1673 The detector associated with the image.
1678 True if the image is trimmed, else False.
1680 return detector.getBBox() == image.getBBox()
Parameters to control convolution.
A kernel created from an Image.
Pass parameters to a Statistics object.
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)
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, ConvolutionControl const &convolutionControl=ConvolutionControl())
Convolve an Image or MaskedImage with a Kernel, setting pixels of an existing output image.
Property stringToStatisticsProperty(std::string const property)
Conversion function to switch a string to a Property (see Statistics.h)
gainContext(exp, image, apply, gains=None, invert=False, isTrimmed=True)
getExposureGains(exposure)
interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=None, maskNameList=None, useLegacyInterp=True)
setBadRegions(exposure, badStatistic="MEDIAN")
countMaskedPixels(maskedIm, maskPlane)
maskITLEdgeBleed(ccdExposure, badAmpDict, fpCore, itlEdgeBleedSatMinArea=10000, itlEdgeBleedSatMaxArea=100000, itlEdgeBleedThreshold=5000., itlEdgeBleedModelConstant=0.02, saturatedMaskName="SAT", log=None)
attachTransmissionCurve(exposure, opticsTransmission=None, filterTransmission=None, sensorTransmission=None, atmosphereTransmission=None)
flatCorrection(maskedImage, flatMaskedImage, scalingType, userScale=1.0, invert=False, trimToFit=False)
illuminationCorrection(maskedImage, illumMaskedImage, illumScale, trimToFit=True)
growMasks(mask, radius=0, maskNameList=['BAD'], maskValue="BAD")
maskITLSatSag(ccdExposure, fpCore, saturatedMaskName="SAT")
widenSaturationTrails(mask)
transferFlux(cFunc, fStep, correctionMode=True)
biasCorrection(maskedImage, biasMaskedImage, trimToFit=False)
checkFilter(exposure, filterList, log)
darkCorrection(maskedImage, darkMaskedImage, expScale, darkScale, invert=False, trimToFit=False)
fluxConservingBrighterFatterCorrection(exposure, kernel, maxIter, threshold, applyGain, gains=None, correctionMode=True)
brighterFatterCorrection(exposure, kernel, maxIter, threshold, applyGain, gains=None)
makeThresholdMask(maskedImage, threshold, growFootprints=1, maskName='SAT')
getExposureReadNoises(exposure)
transposeMaskedImage(maskedImage)
interpolateFromMask(maskedImage, fwhm, growSaturatedFootprints=1, maskNameList=['SAT'], fallbackValue=None, useLegacyInterp=True)
isTrimmedImage(image, detector)
maskITLDip(exposure, detectorConfig, maskPlaneNames=["SUSPECT", "ITL_DIP"], log=None)
updateVariance(maskedImage, gain, readNoise, replace=True)
applyGains(exposure, normalizeGains=False, ptcGains=None, isTrimmed=True)
getPhysicalFilter(filterLabel, log)
_applyMaskITLEdgeBleed(ccdExposure, xCore, satLevel, widthSat, itlEdgeBleedThreshold=5000., itlEdgeBleedModelConstant=0.03, saturatedMaskName="SAT", log=None)
saturationCorrection(maskedImage, saturation, fwhm, growFootprints=1, interpolate=True, maskName='SAT', fallbackValue=None, useLegacyInterp=True)
isTrimmedExposure(exposure)
trimToMatchCalibBBox(rawMaskedImage, calibMaskedImage)