34from contextlib
import contextmanager
36from .defects
import Defects
40 """Make a double Gaussian PSF.
45 FWHM of double Gaussian smoothing kernel.
50 The created smoothing kernel.
52 ksize = 4*int(fwhm) + 1
53 return measAlg.DoubleGaussianPsf(ksize, ksize, fwhm/(2*math.sqrt(2*math.log(2))))
57 """Make a transposed copy of a masked image.
67 The transposed copy of the input image.
69 transposed = maskedImage.Factory(lsst.geom.Extent2I(maskedImage.getHeight(), maskedImage.getWidth()))
70 transposed.getImage().getArray()[:] = maskedImage.getImage().getArray().T
71 transposed.getMask().getArray()[:] = maskedImage.getMask().getArray().T
72 transposed.getVariance().getArray()[:] = maskedImage.getVariance().getArray().T
77 """Interpolate over defects specified in a defect list.
83 defectList : `lsst.meas.algorithms.Defects`
84 List of defects to interpolate over.
86 FWHM of double Gaussian smoothing kernel.
87 fallbackValue : scalar, optional
88 Fallback value if an interpolated value cannot be determined.
89 If
None, then the clipped mean of the image
is used.
92 if fallbackValue
is None:
93 fallbackValue = afwMath.makeStatistics(maskedImage.getImage(), afwMath.MEANCLIP).getValue()
94 if 'INTRP' not in maskedImage.getMask().getMaskPlaneDict():
95 maskedImage.getMask().addMaskPlane(
'INTRP')
96 measAlg.interpolateOverDefects(maskedImage, psf, defectList, fallbackValue,
True)
101 """Mask pixels based on threshold detection.
106 Image to process. Only the mask plane is updated.
109 growFootprints : scalar, optional
110 Number of pixels to grow footprints of detected regions.
111 maskName : str, optional
112 Mask plane name,
or list of names to convert
116 defectList : `lsst.meas.algorithms.Defects`
117 Defect list constructed
from pixels above the threshold.
120 thresh = afwDetection.Threshold(threshold)
121 fs = afwDetection.FootprintSet(maskedImage, thresh)
123 if growFootprints > 0:
124 fs = afwDetection.FootprintSet(fs, rGrow=growFootprints, isotropic=
False)
125 fpList = fs.getFootprints()
128 mask = maskedImage.getMask()
129 bitmask = mask.getPlaneBitMask(maskName)
130 afwDetection.setMaskFromFootprintList(mask, fpList, bitmask)
132 return Defects.fromFootprintList(fpList)
135def growMasks(mask, radius=0, maskNameList=['BAD'], maskValue="BAD"):
136 """Grow a mask by an amount and add to the requested plane.
141 Mask image to process.
143 Amount to grow the mask.
144 maskNameList : `str` or `list` [`str`]
145 Mask names that should be grown.
147 Mask plane to assign the newly masked pixels to.
150 thresh = afwDetection.Threshold(mask.getPlaneBitMask(maskNameList), afwDetection.Threshold.BITMASK)
151 fpSet = afwDetection.FootprintSet(mask, thresh)
152 fpSet = afwDetection.FootprintSet(fpSet, rGrow=radius, isotropic=
False)
153 fpSet.setMask(mask, maskValue)
157 maskNameList=['SAT'], fallbackValue=None):
158 """Interpolate over defects identified by a particular set of mask planes.
165 FWHM of double Gaussian smoothing kernel.
166 growSaturatedFootprints : scalar, optional
167 Number of pixels to grow footprints for saturated pixels.
168 maskNameList : `List` of `str`, optional
170 fallbackValue : scalar, optional
171 Value of last resort
for interpolation.
173 mask = maskedImage.getMask()
175 if growSaturatedFootprints > 0
and "SAT" in maskNameList:
179 growMasks(mask, radius=growSaturatedFootprints, maskNameList=[
'SAT'], maskValue=
"SAT")
181 thresh = afwDetection.Threshold(mask.getPlaneBitMask(maskNameList), afwDetection.Threshold.BITMASK)
182 fpSet = afwDetection.FootprintSet(mask, thresh)
183 defectList = Defects.fromFootprintList(fpSet.getFootprints())
192 """Mark saturated pixels and optionally interpolate over them
199 Saturation level used as the detection threshold.
201 FWHM of double Gaussian smoothing kernel.
202 growFootprints : scalar, optional
203 Number of pixels to grow footprints of detected regions.
204 interpolate : Bool, optional
205 If
True, saturated pixels are interpolated over.
206 maskName : str, optional
208 fallbackValue : scalar, optional
209 Value of last resort
for interpolation.
212 maskedImage=maskedImage,
213 threshold=saturation,
214 growFootprints=growFootprints,
224 """Compute number of edge trim pixels to match the calibration data.
226 Use the dimension difference between the raw exposure and the
227 calibration exposure to compute the edge trim pixels. This trim
228 is applied symmetrically,
with the same number of pixels masked on
236 Calibration image to draw new bounding box
from.
241 ``rawMaskedImage`` trimmed to the appropriate size
245 Rasied
if ``rawMaskedImage`` cannot be symmetrically trimmed to
246 match ``calibMaskedImage``.
248 nx, ny = rawMaskedImage.getBBox().getDimensions() - calibMaskedImage.getBBox().getDimensions()
250 raise RuntimeError(
"Raw and calib maskedImages are trimmed differently in X and Y.")
252 raise RuntimeError(
"Calibration maskedImage is trimmed unevenly in X.")
254 raise RuntimeError(
"Calibration maskedImage is larger than raw data.")
258 replacementMaskedImage = rawMaskedImage[nEdge:-nEdge, nEdge:-nEdge, afwImage.LOCAL]
259 SourceDetectionTask.setEdgeBits(
261 replacementMaskedImage.getBBox(),
262 rawMaskedImage.getMask().getPlaneBitMask(
"EDGE")
265 replacementMaskedImage = rawMaskedImage
267 return replacementMaskedImage
271 """Apply bias correction in place.
276 Image to process. The image is modified by this method.
278 Bias image of the same size
as ``maskedImage``
279 trimToFit : `Bool`, optional
280 If
True, raw data
is symmetrically trimmed to match
286 Raised
if ``maskedImage``
and ``biasMaskedImage`` do
not have
293 if maskedImage.getBBox(afwImage.LOCAL) != biasMaskedImage.getBBox(afwImage.LOCAL):
294 raise RuntimeError(
"maskedImage bbox %s != biasMaskedImage bbox %s" %
295 (maskedImage.getBBox(afwImage.LOCAL), biasMaskedImage.getBBox(afwImage.LOCAL)))
296 maskedImage -= biasMaskedImage
299def darkCorrection(maskedImage, darkMaskedImage, expScale, darkScale, invert=False, trimToFit=False):
300 """Apply dark correction in place.
305 Image to process. The image is modified by this method.
307 Dark image of the same size
as ``maskedImage``.
309 Dark exposure time
for ``maskedImage``.
311 Dark exposure time
for ``darkMaskedImage``.
312 invert : `Bool`, optional
313 If
True, re-add the dark to an already corrected image.
314 trimToFit : `Bool`, optional
315 If
True, raw data
is symmetrically trimmed to match
321 Raised
if ``maskedImage``
and ``darkMaskedImage`` do
not have
326 The dark correction
is applied by calculating:
327 maskedImage -= dark * expScaling / darkScaling
332 if maskedImage.getBBox(afwImage.LOCAL) != darkMaskedImage.getBBox(afwImage.LOCAL):
333 raise RuntimeError(
"maskedImage bbox %s != darkMaskedImage bbox %s" %
334 (maskedImage.getBBox(afwImage.LOCAL), darkMaskedImage.getBBox(afwImage.LOCAL)))
336 scale = expScale / darkScale
338 maskedImage.scaledMinus(scale, darkMaskedImage)
340 maskedImage.scaledPlus(scale, darkMaskedImage)
344 """Set the variance plane based on the image plane.
349 Image to process. The variance plane is modified.
351 The amplifier gain
in electrons/ADU.
353 The amplifier read nmoise
in ADU/pixel.
355 var = maskedImage.getVariance()
356 var[:] = maskedImage.getImage()
361def flatCorrection(maskedImage, flatMaskedImage, scalingType, userScale=1.0, invert=False, trimToFit=False):
362 """Apply flat correction in place.
367 Image to process. The image is modified.
369 Flat image of the same size
as ``maskedImage``
371 Flat scale computation method. Allowed values are
'MEAN',
373 userScale : scalar, optional
374 Scale to use
if ``scalingType``=
'USER'.
375 invert : `Bool`, optional
376 If
True, unflatten an already flattened image.
377 trimToFit : `Bool`, optional
378 If
True, raw data
is symmetrically trimmed to match
384 Raised
if ``maskedImage``
and ``flatMaskedImage`` do
not have
385 the same size
or if ``scalingType``
is not an allowed value.
390 if maskedImage.getBBox(afwImage.LOCAL) != flatMaskedImage.getBBox(afwImage.LOCAL):
391 raise RuntimeError(
"maskedImage bbox %s != flatMaskedImage bbox %s" %
392 (maskedImage.getBBox(afwImage.LOCAL), flatMaskedImage.getBBox(afwImage.LOCAL)))
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
404 raise RuntimeError(
'%s : %s not implemented' % (
"flatCorrection", scalingType))
407 maskedImage.scaledDivides(1.0/flatScale, flatMaskedImage)
409 maskedImage.scaledMultiplies(1.0/flatScale, flatMaskedImage)
413 """Apply illumination correction in place.
418 Image to process. The image is modified.
420 Illumination correction image of the same size
as ``maskedImage``.
422 Scale factor
for the illumination correction.
423 trimToFit : `Bool`, optional
424 If
True, raw data
is symmetrically trimmed to match
430 Raised
if ``maskedImage``
and ``illumMaskedImage`` do
not have
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)
444 """Apply brighter fatter correction in place for the image.
449 Exposure to have brighter-fatter correction applied. Modified
451 kernel : `numpy.ndarray`
452 Brighter-fatter kernel to apply.
454 Number of correction iterations to run.
456 Convergence threshold in terms of the sum of absolute
457 deviations between an iteration
and the previous one.
459 If
True, then the exposure values are scaled by the gain prior
461 gains : `dict` [`str`, `float`]
462 A dictionary, keyed by amplifier name, of the gains to use.
463 If gains
is None, the nominal gains
in the amplifier object are used.
468 Final difference between iterations achieved
in correction.
470 Number of iterations used to calculate correction.
474 This correction takes a kernel that has been derived
from flat
475 field images to redistribute the charge. The gradient of the
476 kernel
is the deflection field due to the accumulated charge.
478 Given the original image I(x)
and the kernel K(x) we can compute
479 the corrected image Ic(x) using the following equation:
481 Ic(x) = I(x) + 0.5*d/dx(I(x)*d/dx(int( dy*K(x-y)*I(y))))
483 To evaluate the derivative term we expand it
as follows:
485 0.5 * ( d/dx(I(x))*d/dx(int(dy*K(x-y)*I(y)))
486 + I(x)*d^2/dx^2(int(dy* K(x-y)*I(y))) )
488 Because we use the measured counts instead of the incident counts
489 we apply the correction iteratively to reconstruct the original
490 counts
and the correction. We stop iterating when the summed
491 difference between the current corrected image
and the one
from
492 the previous iteration
is below the threshold. We do
not require
493 convergence because the number of iterations
is too large a
494 computational cost. How we define the threshold still needs to be
495 evaluated, the current default was shown to work reasonably well
496 on a small set of images. For more information on the method see
497 DocuShare Document-19407.
499 The edges
as defined by the kernel are
not corrected because they
500 have spurious values due to the convolution.
502 image = exposure.getMaskedImage().getImage()
505 with gainContext(exposure, image, applyGain, gains):
507 kLx = numpy.shape(kernel)[0]
508 kLy = numpy.shape(kernel)[1]
509 kernelImage = afwImage.ImageD(kLx, kLy)
510 kernelImage.getArray()[:, :] = kernel
511 tempImage = image.clone()
513 nanIndex = numpy.isnan(tempImage.getArray())
514 tempImage.getArray()[nanIndex] = 0.
516 outImage = afwImage.ImageF(image.getDimensions())
517 corr = numpy.zeros_like(image.getArray())
518 prev_image = numpy.zeros_like(image.getArray())
519 convCntrl = afwMath.ConvolutionControl(
False,
True, 1)
520 fixedKernel = afwMath.FixedKernel(kernelImage)
532 for iteration
in range(maxIter):
534 afwMath.convolve(outImage, tempImage, fixedKernel, convCntrl)
535 tmpArray = tempImage.getArray()
536 outArray = outImage.getArray()
538 with numpy.errstate(invalid=
"ignore", over=
"ignore"):
540 gradTmp = numpy.gradient(tmpArray[startY:endY, startX:endX])
541 gradOut = numpy.gradient(outArray[startY:endY, startX:endX])
542 first = (gradTmp[0]*gradOut[0] + gradTmp[1]*gradOut[1])[1:-1, 1:-1]
545 diffOut20 = numpy.diff(outArray, 2, 0)[startY:endY, startX + 1:endX - 1]
546 diffOut21 = numpy.diff(outArray, 2, 1)[startY + 1:endY - 1, startX:endX]
547 second = tmpArray[startY + 1:endY - 1, startX + 1:endX - 1]*(diffOut20 + diffOut21)
549 corr[startY + 1:endY - 1, startX + 1:endX - 1] = 0.5*(first + second)
551 tmpArray[:, :] = image.getArray()[:, :]
552 tmpArray[nanIndex] = 0.
553 tmpArray[startY:endY, startX:endX] += corr[startY:endY, startX:endX]
556 diff = numpy.sum(numpy.abs(prev_image - tmpArray))
560 prev_image[:, :] = tmpArray[:, :]
562 image.getArray()[startY + 1:endY - 1, startX + 1:endX - 1] += \
563 corr[startY + 1:endY - 1, startX + 1:endX - 1]
565 return diff, iteration
570 """Context manager that applies and removes gain.
575 Exposure to apply/remove gain.
577 Image to apply/remove gain.
579 If True, apply
and remove the amplifier gain.
580 gains : `dict` [`str`, `float`]
581 A dictionary, keyed by amplifier name, of the gains to use.
582 If gains
is None, the nominal gains
in the amplifier object are used.
587 Exposure
with the gain applied.
591 if gains
and apply
is True:
592 ampNames = [amp.getName()
for amp
in exp.getDetector()]
593 for ampName
in ampNames:
594 if ampName
not in gains.keys():
595 raise RuntimeError(f
"Gains provided to gain context, but no entry found for amp {ampName}")
598 ccd = exp.getDetector()
600 sim = image.Factory(image, amp.getBBox())
602 gain = gains[amp.getName()]
611 ccd = exp.getDetector()
613 sim = image.Factory(image, amp.getBBox())
615 gain = gains[amp.getName()]
622 sensorTransmission=None, atmosphereTransmission=None):
623 """Attach a TransmissionCurve to an Exposure, given separate curves for
624 different components.
629 Exposure object to modify by attaching the product of all given
630 ``TransmissionCurves`` in post-assembly trimmed detector coordinates.
631 Must have a valid ``Detector`` attached that matches the detector
632 associated
with sensorTransmission.
634 A ``TransmissionCurve`` that represents the throughput of the optics,
635 to be evaluated
in focal-plane coordinates.
637 A ``TransmissionCurve`` that represents the throughput of the filter
638 itself, to be evaluated
in focal-plane coordinates.
640 A ``TransmissionCurve`` that represents the throughput of the sensor
641 itself, to be evaluated
in post-assembly trimmed detector coordinates.
643 A ``TransmissionCurve`` that represents the throughput of the
644 atmosphere, assumed to be spatially constant.
649 The TransmissionCurve attached to the exposure.
653 All ``TransmissionCurve`` arguments are optional;
if none are provided, the
654 attached ``TransmissionCurve`` will have unit transmission everywhere.
656 combined = afwImage.TransmissionCurve.makeIdentity()
657 if atmosphereTransmission
is not None:
658 combined *= atmosphereTransmission
659 if opticsTransmission
is not None:
660 combined *= opticsTransmission
661 if filterTransmission
is not None:
662 combined *= filterTransmission
663 detector = exposure.getDetector()
664 fpToPix = detector.getTransform(fromSys=camGeom.FOCAL_PLANE,
665 toSys=camGeom.PIXELS)
666 combined = combined.transformedBy(fpToPix)
667 if sensorTransmission
is not None:
668 combined *= sensorTransmission
669 exposure.getInfo().setTransmissionCurve(combined)
673def applyGains(exposure, normalizeGains=False, ptcGains=None):
674 """Scale an exposure by the amplifier gains.
679 Exposure to process. The image is modified.
680 normalizeGains : `Bool`, optional
681 If
True, then amplifiers are scaled to force the median of
682 each amplifier to equal the median of those medians.
683 ptcGains : `dict`[`str`], optional
684 Dictionary keyed by amp name containing the PTC gains.
686 ccd = exposure.getDetector()
687 ccdImage = exposure.getMaskedImage()
691 sim = ccdImage.Factory(ccdImage, amp.getBBox())
693 sim *= ptcGains[amp.getName()]
698 medians.append(numpy.median(sim.getImage().getArray()))
701 median = numpy.median(numpy.array(medians))
702 for index, amp
in enumerate(ccd):
703 sim = ccdImage.Factory(ccdImage, amp.getBBox())
704 if medians[index] != 0.0:
705 sim *= median/medians[index]
709 """Grow the saturation trails by an amount dependent on the width of the
715 Mask which will have the saturated areas grown.
719 for i
in range(1, 6):
721 for i
in range(6, 8):
723 for i
in range(8, 10):
727 if extraGrowMax <= 0:
730 saturatedBit = mask.getPlaneBitMask(
"SAT")
732 xmin, ymin = mask.getBBox().getMin()
733 width = mask.getWidth()
735 thresh = afwDetection.Threshold(saturatedBit, afwDetection.Threshold.BITMASK)
736 fpList = afwDetection.FootprintSet(mask, thresh).getFootprints()
739 for s
in fp.getSpans():
740 x0, x1 = s.getX0(), s.getX1()
742 extraGrow = extraGrowDict.get(x1 - x0 + 1, extraGrowMax)
745 x0 -= xmin + extraGrow
746 x1 -= xmin - extraGrow
753 mask.array[y, x0:x1+1] |= saturatedBit
757 """Set all BAD areas of the chip to the average of the rest of the exposure
762 Exposure to mask. The exposure mask is modified.
763 badStatistic : `str`, optional
764 Statistic to use to generate the replacement value
from the
765 image data. Allowed values are
'MEDIAN' or 'MEANCLIP'.
769 badPixelCount : scalar
770 Number of bad pixels masked.
771 badPixelValue : scalar
772 Value substituted
for bad pixels.
777 Raised
if `badStatistic`
is not an allowed value.
779 if badStatistic ==
"MEDIAN":
780 statistic = afwMath.MEDIAN
781 elif badStatistic ==
"MEANCLIP":
782 statistic = afwMath.MEANCLIP
784 raise RuntimeError(
"Impossible method %s of bad region correction" % badStatistic)
786 mi = exposure.getMaskedImage()
788 BAD = mask.getPlaneBitMask(
"BAD")
789 INTRP = mask.getPlaneBitMask(
"INTRP")
791 sctrl = afwMath.StatisticsControl()
792 sctrl.setAndMask(BAD)
793 value = afwMath.makeStatistics(mi, statistic, sctrl).getValue()
795 maskArray = mask.getArray()
796 imageArray = mi.getImage().getArray()
797 badPixels = numpy.logical_and((maskArray & BAD) > 0, (maskArray & INTRP) == 0)
798 imageArray[:] = numpy.where(badPixels, value, imageArray)
800 return badPixels.sum(), value
804 """Check to see if an exposure is in a filter specified by a list.
806 The goal of this is to provide a unified filter checking interface
807 for all filter dependent stages.
813 filterList : `list` [`str`]
814 List of physical_filter names to check.
815 log : `logging.Logger`
816 Logger to handle messages.
821 True if the exposure
's filter is contained in the list.
823 if len(filterList) == 0:
825 thisFilter = exposure.getFilter()
826 if thisFilter
is None:
827 log.warning(
"No FilterLabel attached to this exposure!")
831 if thisPhysicalFilter
in filterList:
833 elif thisFilter.bandLabel
in filterList:
835 log.warning(
"Physical filter (%s) should be used instead of band %s for filter configurations"
836 " (%s)", thisPhysicalFilter, thisFilter.bandLabel, filterList)
843 """Get the physical filter label associated with the given filterLabel.
845 If ``filterLabel`` is `
None`
or there
is no physicalLabel attribute
846 associated
with the given ``filterLabel``, the returned label will be
853 physical filter label.
854 log : `logging.Logger`
855 Logger to handle messages.
859 physicalFilter : `str`
860 The value returned by the physicalLabel attribute of ``filterLabel``
if
861 it exists, otherwise set to \
"Unknown\".
863 if filterLabel
is None:
864 physicalFilter =
"Unknown"
865 log.warning(
"filterLabel is None. Setting physicalFilter to \"Unknown\".")
868 physicalFilter = filterLabel.physicalLabel
870 log.warning(
"filterLabel has no physicalLabel attribute. Setting physicalFilter to \"Unknown\".")
871 physicalFilter =
"Unknown"
872 return physicalFilter
def biasCorrection(maskedImage, biasMaskedImage, trimToFit=False)
def growMasks(mask, radius=0, maskNameList=['BAD'], maskValue="BAD")
def darkCorrection(maskedImage, darkMaskedImage, expScale, darkScale, invert=False, trimToFit=False)
def saturationCorrection(maskedImage, saturation, fwhm, growFootprints=1, interpolate=True, maskName='SAT', fallbackValue=None)
def interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=None)
def illuminationCorrection(maskedImage, illumMaskedImage, illumScale, trimToFit=True)
def 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 interpolateFromMask(maskedImage, fwhm, growSaturatedFootprints=1, maskNameList=['SAT'], fallbackValue=None)
def transposeMaskedImage(maskedImage)
def flatCorrection(maskedImage, flatMaskedImage, scalingType, userScale=1.0, invert=False, trimToFit=False)
def attachTransmissionCurve(exposure, opticsTransmission=None, filterTransmission=None, sensorTransmission=None, atmosphereTransmission=None)
def widenSaturationTrails(mask)