25 "attachTransmissionCurve",
27 "brighterFatterCorrection",
35 "illuminationCorrection",
36 "interpolateDefectList",
37 "interpolateFromMask",
39 "saturationCorrection",
41 "transposeMaskedImage",
42 "trimToMatchCalibBBox",
44 "widenSaturationTrails",
59from contextlib
import contextmanager
61from .defects
import Defects
65 """Make a double Gaussian PSF.
70 FWHM of double Gaussian smoothing kernel.
75 The created smoothing kernel.
77 ksize = 4*int(fwhm) + 1
78 return measAlg.DoubleGaussianPsf(ksize, ksize, fwhm/(2*math.sqrt(2*math.log(2))))
82 """Make a transposed copy of a masked image.
92 The transposed copy of the input image.
94 transposed = maskedImage.Factory(lsst.geom.Extent2I(maskedImage.getHeight(), maskedImage.getWidth()))
95 transposed.getImage().getArray()[:] = maskedImage.getImage().getArray().T
96 transposed.getMask().getArray()[:] = maskedImage.getMask().getArray().T
97 transposed.getVariance().getArray()[:] = maskedImage.getVariance().getArray().T
102 """Interpolate over defects specified in a defect list.
108 defectList : `lsst.meas.algorithms.Defects`
109 List of defects to interpolate over.
111 FWHM of double Gaussian smoothing kernel.
112 fallbackValue : scalar, optional
113 Fallback value if an interpolated value cannot be determined.
114 If
None, then the clipped mean of the image
is used.
117 if fallbackValue
is None:
118 fallbackValue = afwMath.makeStatistics(maskedImage.getImage(), afwMath.MEANCLIP).getValue()
119 if 'INTRP' not in maskedImage.getMask().getMaskPlaneDict():
120 maskedImage.getMask().addMaskPlane(
'INTRP')
121 measAlg.interpolateOverDefects(maskedImage, psf, defectList, fallbackValue,
True)
126 """Mask pixels based on threshold detection.
131 Image to process. Only the mask plane is updated.
134 growFootprints : scalar, optional
135 Number of pixels to grow footprints of detected regions.
136 maskName : str, optional
137 Mask plane name,
or list of names to convert
141 defectList : `lsst.meas.algorithms.Defects`
142 Defect list constructed
from pixels above the threshold.
145 thresh = afwDetection.Threshold(threshold)
146 fs = afwDetection.FootprintSet(maskedImage, thresh)
148 if growFootprints > 0:
149 fs = afwDetection.FootprintSet(fs, rGrow=growFootprints, isotropic=
False)
150 fpList = fs.getFootprints()
153 mask = maskedImage.getMask()
154 bitmask = mask.getPlaneBitMask(maskName)
155 afwDetection.setMaskFromFootprintList(mask, fpList, bitmask)
157 return Defects.fromFootprintList(fpList)
160def growMasks(mask, radius=0, maskNameList=['BAD'], maskValue="BAD"):
161 """Grow a mask by an amount and add to the requested plane.
166 Mask image to process.
168 Amount to grow the mask.
169 maskNameList : `str` or `list` [`str`]
170 Mask names that should be grown.
172 Mask plane to assign the newly masked pixels to.
175 thresh = afwDetection.Threshold(mask.getPlaneBitMask(maskNameList), afwDetection.Threshold.BITMASK)
176 fpSet = afwDetection.FootprintSet(mask, thresh)
177 fpSet = afwDetection.FootprintSet(fpSet, rGrow=radius, isotropic=
False)
178 fpSet.setMask(mask, maskValue)
182 maskNameList=['SAT'], fallbackValue=None):
183 """Interpolate over defects identified by a particular set of mask planes.
190 FWHM of double Gaussian smoothing kernel.
191 growSaturatedFootprints : scalar, optional
192 Number of pixels to grow footprints for saturated pixels.
193 maskNameList : `List` of `str`, optional
195 fallbackValue : scalar, optional
196 Value of last resort
for interpolation.
198 mask = maskedImage.getMask()
200 if growSaturatedFootprints > 0
and "SAT" in maskNameList:
204 growMasks(mask, radius=growSaturatedFootprints, maskNameList=[
'SAT'], maskValue=
"SAT")
206 thresh = afwDetection.Threshold(mask.getPlaneBitMask(maskNameList), afwDetection.Threshold.BITMASK)
207 fpSet = afwDetection.FootprintSet(mask, thresh)
208 defectList = Defects.fromFootprintList(fpSet.getFootprints())
217 """Mark saturated pixels and optionally interpolate over them
224 Saturation level used as the detection threshold.
226 FWHM of double Gaussian smoothing kernel.
227 growFootprints : scalar, optional
228 Number of pixels to grow footprints of detected regions.
229 interpolate : Bool, optional
230 If
True, saturated pixels are interpolated over.
231 maskName : str, optional
233 fallbackValue : scalar, optional
234 Value of last resort
for interpolation.
237 maskedImage=maskedImage,
238 threshold=saturation,
239 growFootprints=growFootprints,
249 """Compute number of edge trim pixels to match the calibration data.
251 Use the dimension difference between the raw exposure and the
252 calibration exposure to compute the edge trim pixels. This trim
253 is applied symmetrically,
with the same number of pixels masked on
261 Calibration image to draw new bounding box
from.
266 ``rawMaskedImage`` trimmed to the appropriate size.
271 Raised
if ``rawMaskedImage`` cannot be symmetrically trimmed to
272 match ``calibMaskedImage``.
274 nx, ny = rawMaskedImage.getBBox().getDimensions() - calibMaskedImage.getBBox().getDimensions()
276 raise RuntimeError(
"Raw and calib maskedImages are trimmed differently in X and Y.")
278 raise RuntimeError(
"Calibration maskedImage is trimmed unevenly in X.")
280 raise RuntimeError(
"Calibration maskedImage is larger than raw data.")
284 replacementMaskedImage = rawMaskedImage[nEdge:-nEdge, nEdge:-nEdge, afwImage.LOCAL]
285 SourceDetectionTask.setEdgeBits(
287 replacementMaskedImage.getBBox(),
288 rawMaskedImage.getMask().getPlaneBitMask(
"EDGE")
291 replacementMaskedImage = rawMaskedImage
293 return replacementMaskedImage
297 """Apply bias correction in place.
302 Image to process. The image is modified by this method.
304 Bias image of the same size
as ``maskedImage``
305 trimToFit : `Bool`, optional
306 If
True, raw data
is symmetrically trimmed to match
312 Raised
if ``maskedImage``
and ``biasMaskedImage`` do
not have
319 if maskedImage.getBBox(afwImage.LOCAL) != biasMaskedImage.getBBox(afwImage.LOCAL):
320 raise RuntimeError(
"maskedImage bbox %s != biasMaskedImage bbox %s" %
321 (maskedImage.getBBox(afwImage.LOCAL), biasMaskedImage.getBBox(afwImage.LOCAL)))
322 maskedImage -= biasMaskedImage
325def darkCorrection(maskedImage, darkMaskedImage, expScale, darkScale, invert=False, trimToFit=False):
326 """Apply dark correction in place.
331 Image to process. The image is modified by this method.
333 Dark image of the same size
as ``maskedImage``.
335 Dark exposure time
for ``maskedImage``.
337 Dark exposure time
for ``darkMaskedImage``.
338 invert : `Bool`, optional
339 If
True, re-add the dark to an already corrected image.
340 trimToFit : `Bool`, optional
341 If
True, raw data
is symmetrically trimmed to match
347 Raised
if ``maskedImage``
and ``darkMaskedImage`` do
not have
352 The dark correction
is applied by calculating:
353 maskedImage -= dark * expScaling / darkScaling
358 if maskedImage.getBBox(afwImage.LOCAL) != darkMaskedImage.getBBox(afwImage.LOCAL):
359 raise RuntimeError(
"maskedImage bbox %s != darkMaskedImage bbox %s" %
360 (maskedImage.getBBox(afwImage.LOCAL), darkMaskedImage.getBBox(afwImage.LOCAL)))
362 scale = expScale / darkScale
364 maskedImage.scaledMinus(scale, darkMaskedImage)
366 maskedImage.scaledPlus(scale, darkMaskedImage)
370 """Set the variance plane based on the image plane.
375 Image to process. The variance plane is modified.
377 The amplifier gain
in electrons/ADU.
379 The amplifier read nmoise
in ADU/pixel.
381 var = maskedImage.getVariance()
382 var[:] = maskedImage.getImage()
387def flatCorrection(maskedImage, flatMaskedImage, scalingType, userScale=1.0, invert=False, trimToFit=False):
388 """Apply flat correction in place.
393 Image to process. The image is modified.
395 Flat image of the same size
as ``maskedImage``
397 Flat scale computation method. Allowed values are
'MEAN',
399 userScale : scalar, optional
400 Scale to use
if ``scalingType=
'USER'``.
401 invert : `Bool`, optional
402 If
True, unflatten an already flattened image.
403 trimToFit : `Bool`, optional
404 If
True, raw data
is symmetrically trimmed to match
410 Raised
if ``maskedImage``
and ``flatMaskedImage`` do
not have
411 the same size
or if ``scalingType``
is not an allowed value.
416 if maskedImage.getBBox(afwImage.LOCAL) != flatMaskedImage.getBBox(afwImage.LOCAL):
417 raise RuntimeError(
"maskedImage bbox %s != flatMaskedImage bbox %s" %
418 (maskedImage.getBBox(afwImage.LOCAL), flatMaskedImage.getBBox(afwImage.LOCAL)))
424 if scalingType
in (
'MEAN',
'MEDIAN'):
425 scalingType = afwMath.stringToStatisticsProperty(scalingType)
426 flatScale = afwMath.makeStatistics(flatMaskedImage.image, scalingType).getValue()
427 elif scalingType ==
'USER':
428 flatScale = userScale
430 raise RuntimeError(
'%s : %s not implemented' % (
"flatCorrection", scalingType))
433 maskedImage.scaledDivides(1.0/flatScale, flatMaskedImage)
435 maskedImage.scaledMultiplies(1.0/flatScale, flatMaskedImage)
439 """Apply illumination correction in place.
444 Image to process. The image is modified.
446 Illumination correction image of the same size
as ``maskedImage``.
448 Scale factor
for the illumination correction.
449 trimToFit : `Bool`, optional
450 If
True, raw data
is symmetrically trimmed to match
456 Raised
if ``maskedImage``
and ``illumMaskedImage`` do
not have
462 if maskedImage.getBBox(afwImage.LOCAL) != illumMaskedImage.getBBox(afwImage.LOCAL):
463 raise RuntimeError(
"maskedImage bbox %s != illumMaskedImage bbox %s" %
464 (maskedImage.getBBox(afwImage.LOCAL), illumMaskedImage.getBBox(afwImage.LOCAL)))
466 maskedImage.scaledDivides(1.0/illumScale, illumMaskedImage)
470 """Apply brighter fatter correction in place for the image.
475 Exposure to have brighter-fatter correction applied. Modified
477 kernel : `numpy.ndarray`
478 Brighter-fatter kernel to apply.
480 Number of correction iterations to run.
482 Convergence threshold in terms of the sum of absolute
483 deviations between an iteration
and the previous one.
485 If
True, then the exposure values are scaled by the gain prior
487 gains : `dict` [`str`, `float`]
488 A dictionary, keyed by amplifier name, of the gains to use.
489 If gains
is None, the nominal gains
in the amplifier object are used.
494 Final difference between iterations achieved
in correction.
496 Number of iterations used to calculate correction.
500 This correction takes a kernel that has been derived
from flat
501 field images to redistribute the charge. The gradient of the
502 kernel
is the deflection field due to the accumulated charge.
504 Given the original image I(x)
and the kernel K(x) we can compute
505 the corrected image Ic(x) using the following equation:
507 Ic(x) = I(x) + 0.5*d/dx(I(x)*d/dx(int( dy*K(x-y)*I(y))))
509 To evaluate the derivative term we expand it
as follows:
511 0.5 * ( d/dx(I(x))*d/dx(int(dy*K(x-y)*I(y)))
512 + I(x)*d^2/dx^2(int(dy* K(x-y)*I(y))) )
514 Because we use the measured counts instead of the incident counts
515 we apply the correction iteratively to reconstruct the original
516 counts
and the correction. We stop iterating when the summed
517 difference between the current corrected image
and the one
from
518 the previous iteration
is below the threshold. We do
not require
519 convergence because the number of iterations
is too large a
520 computational cost. How we define the threshold still needs to be
521 evaluated, the current default was shown to work reasonably well
522 on a small set of images. For more information on the method see
523 DocuShare Document-19407.
525 The edges
as defined by the kernel are
not corrected because they
526 have spurious values due to the convolution.
528 image = exposure.getMaskedImage().getImage()
531 with gainContext(exposure, image, applyGain, gains):
533 kLx = numpy.shape(kernel)[0]
534 kLy = numpy.shape(kernel)[1]
535 kernelImage = afwImage.ImageD(kLx, kLy)
536 kernelImage.getArray()[:, :] = kernel
537 tempImage = image.clone()
539 nanIndex = numpy.isnan(tempImage.getArray())
540 tempImage.getArray()[nanIndex] = 0.
542 outImage = afwImage.ImageF(image.getDimensions())
543 corr = numpy.zeros_like(image.getArray())
544 prev_image = numpy.zeros_like(image.getArray())
545 convCntrl = afwMath.ConvolutionControl(
False,
True, 1)
546 fixedKernel = afwMath.FixedKernel(kernelImage)
558 for iteration
in range(maxIter):
560 afwMath.convolve(outImage, tempImage, fixedKernel, convCntrl)
561 tmpArray = tempImage.getArray()
562 outArray = outImage.getArray()
564 with numpy.errstate(invalid=
"ignore", over=
"ignore"):
566 gradTmp = numpy.gradient(tmpArray[startY:endY, startX:endX])
567 gradOut = numpy.gradient(outArray[startY:endY, startX:endX])
568 first = (gradTmp[0]*gradOut[0] + gradTmp[1]*gradOut[1])[1:-1, 1:-1]
571 diffOut20 = numpy.diff(outArray, 2, 0)[startY:endY, startX + 1:endX - 1]
572 diffOut21 = numpy.diff(outArray, 2, 1)[startY + 1:endY - 1, startX:endX]
573 second = tmpArray[startY + 1:endY - 1, startX + 1:endX - 1]*(diffOut20 + diffOut21)
575 corr[startY + 1:endY - 1, startX + 1:endX - 1] = 0.5*(first + second)
577 tmpArray[:, :] = image.getArray()[:, :]
578 tmpArray[nanIndex] = 0.
579 tmpArray[startY:endY, startX:endX] += corr[startY:endY, startX:endX]
582 diff = numpy.sum(numpy.abs(prev_image - tmpArray))
586 prev_image[:, :] = tmpArray[:, :]
588 image.getArray()[startY + 1:endY - 1, startX + 1:endX - 1] += \
589 corr[startY + 1:endY - 1, startX + 1:endX - 1]
591 return diff, iteration
595def gainContext(exp, image, apply, gains=None):
596 """Context manager that applies and removes gain.
601 Exposure to apply/remove gain.
603 Image to apply/remove gain.
605 If True, apply
and remove the amplifier gain.
606 gains : `dict` [`str`, `float`]
607 A dictionary, keyed by amplifier name, of the gains to use.
608 If gains
is None, the nominal gains
in the amplifier object are used.
613 Exposure
with the gain applied.
617 if gains
and apply
is True:
618 ampNames = [amp.getName()
for amp
in exp.getDetector()]
619 for ampName
in ampNames:
620 if ampName
not in gains.keys():
621 raise RuntimeError(f
"Gains provided to gain context, but no entry found for amp {ampName}")
624 ccd = exp.getDetector()
626 sim = image.Factory(image, amp.getBBox())
628 gain = gains[amp.getName()]
637 ccd = exp.getDetector()
639 sim = image.Factory(image, amp.getBBox())
641 gain = gains[amp.getName()]
648 sensorTransmission=None, atmosphereTransmission=None):
649 """Attach a TransmissionCurve to an Exposure, given separate curves for
650 different components.
655 Exposure object to modify by attaching the product of all given
656 ``TransmissionCurves`` in post-assembly trimmed detector coordinates.
657 Must have a valid ``Detector`` attached that matches the detector
658 associated
with sensorTransmission.
660 A ``TransmissionCurve`` that represents the throughput of the optics,
661 to be evaluated
in focal-plane coordinates.
663 A ``TransmissionCurve`` that represents the throughput of the filter
664 itself, to be evaluated
in focal-plane coordinates.
666 A ``TransmissionCurve`` that represents the throughput of the sensor
667 itself, to be evaluated
in post-assembly trimmed detector coordinates.
669 A ``TransmissionCurve`` that represents the throughput of the
670 atmosphere, assumed to be spatially constant.
675 The TransmissionCurve attached to the exposure.
679 All ``TransmissionCurve`` arguments are optional;
if none are provided, the
680 attached ``TransmissionCurve`` will have unit transmission everywhere.
682 combined = afwImage.TransmissionCurve.makeIdentity()
683 if atmosphereTransmission
is not None:
684 combined *= atmosphereTransmission
685 if opticsTransmission
is not None:
686 combined *= opticsTransmission
687 if filterTransmission
is not None:
688 combined *= filterTransmission
689 detector = exposure.getDetector()
690 fpToPix = detector.getTransform(fromSys=camGeom.FOCAL_PLANE,
691 toSys=camGeom.PIXELS)
692 combined = combined.transformedBy(fpToPix)
693 if sensorTransmission
is not None:
694 combined *= sensorTransmission
695 exposure.getInfo().setTransmissionCurve(combined)
699def applyGains(exposure, normalizeGains=False, ptcGains=None):
700 """Scale an exposure by the amplifier gains.
705 Exposure to process. The image is modified.
706 normalizeGains : `Bool`, optional
707 If
True, then amplifiers are scaled to force the median of
708 each amplifier to equal the median of those medians.
709 ptcGains : `dict`[`str`], optional
710 Dictionary keyed by amp name containing the PTC gains.
712 ccd = exposure.getDetector()
713 ccdImage = exposure.getMaskedImage()
717 sim = ccdImage.Factory(ccdImage, amp.getBBox())
719 sim *= ptcGains[amp.getName()]
724 medians.append(numpy.median(sim.getImage().getArray()))
727 median = numpy.median(numpy.array(medians))
728 for index, amp
in enumerate(ccd):
729 sim = ccdImage.Factory(ccdImage, amp.getBBox())
730 if medians[index] != 0.0:
731 sim *= median/medians[index]
735 """Grow the saturation trails by an amount dependent on the width of the
741 Mask which will have the saturated areas grown.
745 for i
in range(1, 6):
747 for i
in range(6, 8):
749 for i
in range(8, 10):
753 if extraGrowMax <= 0:
756 saturatedBit = mask.getPlaneBitMask(
"SAT")
758 xmin, ymin = mask.getBBox().getMin()
759 width = mask.getWidth()
761 thresh = afwDetection.Threshold(saturatedBit, afwDetection.Threshold.BITMASK)
762 fpList = afwDetection.FootprintSet(mask, thresh).getFootprints()
765 for s
in fp.getSpans():
766 x0, x1 = s.getX0(), s.getX1()
768 extraGrow = extraGrowDict.get(x1 - x0 + 1, extraGrowMax)
771 x0 -= xmin + extraGrow
772 x1 -= xmin - extraGrow
779 mask.array[y, x0:x1+1] |= saturatedBit
783 """Set all BAD areas of the chip to the average of the rest of the exposure
788 Exposure to mask. The exposure mask is modified.
789 badStatistic : `str`, optional
790 Statistic to use to generate the replacement value
from the
791 image data. Allowed values are
'MEDIAN' or 'MEANCLIP'.
795 badPixelCount : scalar
796 Number of bad pixels masked.
797 badPixelValue : scalar
798 Value substituted
for bad pixels.
803 Raised
if `badStatistic`
is not an allowed value.
805 if badStatistic ==
"MEDIAN":
806 statistic = afwMath.MEDIAN
807 elif badStatistic ==
"MEANCLIP":
808 statistic = afwMath.MEANCLIP
810 raise RuntimeError(
"Impossible method %s of bad region correction" % badStatistic)
812 mi = exposure.getMaskedImage()
814 BAD = mask.getPlaneBitMask(
"BAD")
815 INTRP = mask.getPlaneBitMask(
"INTRP")
817 sctrl = afwMath.StatisticsControl()
818 sctrl.setAndMask(BAD)
819 value = afwMath.makeStatistics(mi, statistic, sctrl).getValue()
821 maskArray = mask.getArray()
822 imageArray = mi.getImage().getArray()
823 badPixels = numpy.logical_and((maskArray & BAD) > 0, (maskArray & INTRP) == 0)
824 imageArray[:] = numpy.where(badPixels, value, imageArray)
826 return badPixels.sum(), value
829def checkFilter(exposure, filterList, log):
830 """Check to see if an exposure is in a filter specified by a list.
832 The goal of this is to provide a unified filter checking interface
833 for all filter dependent stages.
839 filterList : `list` [`str`]
840 List of physical_filter names to check.
841 log : `logging.Logger`
842 Logger to handle messages.
847 True if the exposure
's filter is contained in the list.
849 if len(filterList) == 0:
851 thisFilter = exposure.getFilter()
852 if thisFilter
is None:
853 log.warning(
"No FilterLabel attached to this exposure!")
857 if thisPhysicalFilter
in filterList:
859 elif thisFilter.bandLabel
in filterList:
861 log.warning(
"Physical filter (%s) should be used instead of band %s for filter configurations"
862 " (%s)", thisPhysicalFilter, thisFilter.bandLabel, filterList)
869 """Get the physical filter label associated with the given filterLabel.
871 If ``filterLabel`` is `
None`
or there
is no physicalLabel attribute
872 associated
with the given ``filterLabel``, the returned label will be
879 physical filter label.
880 log : `logging.Logger`
881 Logger to handle messages.
885 physicalFilter : `str`
886 The value returned by the physicalLabel attribute of ``filterLabel``
if
887 it exists, otherwise set to \
"Unknown\".
889 if filterLabel
is None:
890 physicalFilter =
"Unknown"
891 log.warning(
"filterLabel is None. Setting physicalFilter to \"Unknown\".")
894 physicalFilter = filterLabel.physicalLabel
896 log.warning(
"filterLabel has no physicalLabel attribute. Setting physicalFilter to \"Unknown\".")
897 physicalFilter =
"Unknown"
898 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 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)