25 "attachTransmissionCurve",
28 "compareCameraKeywords",
41 "illuminationCorrection",
42 "interpolateDefectList",
43 "interpolateFromMask",
45 "saturationCorrection",
47 "transposeMaskedImage",
48 "trimToMatchCalibBBox",
50 "widenSaturationTrails",
52 "getExposureReadNoises",
69from contextlib
import contextmanager
71from .defects
import Defects
75 """Make a double Gaussian PSF.
80 FWHM of double Gaussian smoothing kernel.
84 psf : `lsst.meas.algorithms.DoubleGaussianPsf`
85 The created smoothing kernel.
87 ksize = 4*int(fwhm) + 1
88 return measAlg.DoubleGaussianPsf(ksize, ksize, fwhm/(2*math.sqrt(2*math.log(2))))
92 """Make a transposed copy of a masked image.
96 maskedImage : `lsst.afw.image.MaskedImage`
101 transposed : `lsst.afw.image.MaskedImage`
102 The transposed copy of the input image.
104 transposed = maskedImage.Factory(
lsst.geom.Extent2I(maskedImage.getHeight(), maskedImage.getWidth()))
105 transposed.getImage().getArray()[:] = maskedImage.getImage().getArray().T
106 transposed.getMask().getArray()[:] = maskedImage.getMask().getArray().T
107 transposed.getVariance().getArray()[:] = maskedImage.getVariance().getArray().T
112 maskNameList=None, useLegacyInterp=True):
113 """Interpolate over defects specified in a defect list.
117 maskedImage : `lsst.afw.image.MaskedImage`
119 defectList : `lsst.meas.algorithms.Defects`
120 List of defects to interpolate over.
122 FWHM of double Gaussian smoothing kernel.
123 fallbackValue : scalar, optional
124 Fallback value if an interpolated value cannot be determined.
125 If None, then the clipped mean of the image is used.
126 maskNameList : `list [string]`
127 List of the defects to interpolate over (used for GP interpolator).
128 useLegacyInterp : `bool`
129 Use the legacy interpolation (polynomial interpolation) if True. Use
130 Gaussian Process interpolation if False.
134 The ``fwhm`` parameter is used to create a PSF, but the underlying
135 interpolation code (`lsst.meas.algorithms.interpolateOverDefects`) does
136 not currently make use of this information in legacy Interpolation, but use
137 if for the Gaussian Process as an estimation of the correlation lenght.
140 if fallbackValue
is None:
141 fallbackValue = afwMath.makeStatistics(maskedImage.getImage(), afwMath.MEANCLIP).getValue()
142 if 'INTRP' not in maskedImage.getMask().getMaskPlaneDict():
143 maskedImage.getMask().addMaskPlane(
'INTRP')
154 kwargs = {
"bin_spacing": 20,
155 "threshold_dynamic_binning": 2000,
156 "threshold_subdivide": 20000}
159 measAlg.interpolateOverDefects(maskedImage, psf, defectList,
160 fallbackValue=fallbackValue,
161 useFallbackValueAtEdge=
True,
163 useLegacyInterp=useLegacyInterp,
164 maskNameList=maskNameList, **kwargs)
169 """Mask pixels based on threshold detection.
173 maskedImage : `lsst.afw.image.MaskedImage`
174 Image to process. Only the mask plane is updated.
177 growFootprints : scalar, optional
178 Number of pixels to grow footprints of detected regions.
179 maskName : str, optional
180 Mask plane name, or list of names to convert
184 defectList : `lsst.meas.algorithms.Defects`
185 Defect list constructed from pixels above the threshold.
188 thresh = afwDetection.Threshold(threshold)
189 fs = afwDetection.FootprintSet(maskedImage, thresh)
191 if growFootprints > 0:
192 fs = afwDetection.FootprintSet(fs, rGrow=growFootprints, isotropic=
False)
193 fpList = fs.getFootprints()
196 mask = maskedImage.getMask()
197 bitmask = mask.getPlaneBitMask(maskName)
198 afwDetection.setMaskFromFootprintList(mask, fpList, bitmask)
200 return Defects.fromFootprintList(fpList)
203def growMasks(mask, radius=0, maskNameList=['BAD'], maskValue="BAD"):
204 """Grow a mask by an amount and add to the requested plane.
208 mask : `lsst.afw.image.Mask`
209 Mask image to process.
211 Amount to grow the mask.
212 maskNameList : `str` or `list` [`str`]
213 Mask names that should be grown.
215 Mask plane to assign the newly masked pixels to.
218 spans = SpanSet.fromMask(mask, mask.getPlaneBitMask(maskNameList))
221 spans = spans.dilated(radius, Stencil.MANHATTAN)
222 spans = spans.clippedTo(mask.getBBox())
223 spans.setMask(mask, mask.getPlaneBitMask(maskValue))
227 e2vEdgeBleedSatMaxArea=100000,
228 e2vEdgeBleedYMax=350,
229 saturatedMaskName="SAT", log=None):
230 """Mask edge bleeds in E2V detectors.
234 exposure : `lsst.afw.image.Exposure`
235 Exposure to apply masking to.
236 e2vEdgeBleedSatMinArea : `int`, optional
237 Minimum limit of saturated cores footprint area.
238 e2vEdgeBleedSatMaxArea : `int`, optional
239 Maximum limit of saturated cores footprint area.
240 e2vEdgeBleedYMax: `float`, optional
241 Height of edge bleed masking.
242 saturatedMaskName : `str`, optional
243 Mask name for saturation.
244 log : `logging.Logger`, optional
245 Logger to handle messages.
248 log = log
if log
else logging.getLogger(__name__)
250 maskedImage = exposure.maskedImage
251 saturatedBit = maskedImage.mask.getPlaneBitMask(saturatedMaskName)
253 thresh = afwDetection.Threshold(saturatedBit, afwDetection.Threshold.BITMASK)
255 fpList = afwDetection.FootprintSet(exposure.mask, thresh).getFootprints()
257 satAreas = numpy.asarray([fp.getArea()
for fp
in fpList])
258 largeAreas, = numpy.where((satAreas >= e2vEdgeBleedSatMinArea)
259 & (satAreas < e2vEdgeBleedSatMaxArea))
260 for largeAreasIndex
in largeAreas:
261 fpCore = fpList[largeAreasIndex]
262 xCore, yCore = fpCore.getCentroid()
266 for amp
in exposure.getDetector():
267 if amp.getBBox().contains(xCore, yCore):
268 ampName = amp.getName()
269 if ampName[:2] ==
'C0':
272 if fpCore.getBBox().getMinY() == 0:
280 log.info(
"Found E2V edge bleed in amp %s, column %d.", ampName, xCore)
281 maskedImage.mask[amp.getBBox()].array[:e2vEdgeBleedYMax, :] |= saturatedBit
285 nSigma=5.0, nRowsCheck=20, minLowPixelsPerRow=30, minLowPixelsExtent=10,
286 marginFraction=0.125, saturatedMaskName="SAT", log=None):
287 """Mask DECam-style edge bleeds with low rows next to the read register.
289 For each amplifier that contains a large saturated footprint reaching
290 within ``approachRows`` of its read edge, this function confirms the dip
291 from the per-row count of pixels more than ``nSigma`` below the clipped
292 sky, measures how far the dip extends inward, and marks those rows
293 (plus a margin) with the saturation mask plane.
297 exposure : `lsst.afw.image.Exposure`
298 Assembled exposure to mask.
299 satMinArea : `int`, optional
300 Minimum area (pixels) of a saturated footprint to be considered.
301 satMaxArea : `int`, optional
302 Maximum area (pixels) of a saturated footprint to be considered.
303 approachRows : `int`, optional
304 A footprint must come within this many rows of the read edge.
305 nSigma : `float`, optional
306 A pixel is "low" if it is more than this many sigma below sky.
307 nRowsCheck : `int`, optional
308 Number of rows from the read edge used to confirm the bleed.
309 minLowPixelsPerRow : `int`, optional
310 Mean number of low pixels per row over the check rows required
311 to confirm the bleed.
312 minLowPixelsExtent : `int`, optional
313 Number of low pixels a row must exceed to count toward the bleed
314 height; the scan stops after five consecutive rows that do not.
315 Should be smaller than ``minLowPixelsPerRow``.
316 marginFraction : `float`, optional
317 Extra rows masked beyond the measured height, as a fraction of
318 that height (plus one row).
319 saturatedMaskName : `str`, optional
320 Mask plane name for saturation.
321 log : `logging.Logger`, optional
322 Logger to handle messages.
327 Raised if the exposure has no detector with amplifiers.
329 log = log
if log
else logging.getLogger(__name__)
331 detector = exposure.getDetector()
332 if detector
is None or len(detector) == 0:
333 raise RuntimeError(
"Cannot mask DECam edge bleeds: exposure has no detector with amplifiers.")
335 maskedImage = exposure.maskedImage
336 saturatedBit = maskedImage.mask.getPlaneBitMask(saturatedMaskName)
337 unusableBits = maskedImage.mask.getPlaneBitMask([saturatedMaskName,
"BAD",
"NO_DATA"])
338 statsBits = unusableBits | maskedImage.mask.getPlaneBitMask(
"SUSPECT")
340 thresh = afwDetection.Threshold(saturatedBit, afwDetection.Threshold.BITMASK)
341 fpList = afwDetection.FootprintSet(maskedImage.mask, thresh).getFootprints()
344 if satMinArea <= fp.getArea() < satMaxArea:
345 xCore, yCore = fp.getCentroid()
346 candidates.append((int(xCore), int(yCore), fp.getBBox()))
348 log.debug(
"No saturated footprints in the DECam edge bleed area range.")
351 statsCtrl = afwMath.StatisticsControl()
352 statsCtrl.setAndMask(statsBits)
357 ampBBox = amp.getBBox()
358 if not exposure.getBBox().contains(ampBBox):
360 readTop = amp.getReadoutCorner()
in (camGeom.ReadoutCorner.UL, camGeom.ReadoutCorner.UR)
363 reachesReadEdge = any(ampBBox.contains(x, y)
364 and fpBBox.getMaxY() >= ampBBox.getMaxY() - approachRows
365 for x, y, fpBBox
in candidates)
367 reachesReadEdge = any(ampBBox.contains(x, y)
368 and fpBBox.getMinY() <= ampBBox.getMinY() + approachRows
369 for x, y, fpBBox
in candidates)
370 if not reachesReadEdge:
373 ampImage = maskedImage[ampBBox]
374 stats = afwMath.makeStatistics(ampImage, afwMath.MEANCLIP | afwMath.STDEVCLIP | afwMath.NPOINT,
376 if stats.getValue(afwMath.NPOINT) < 1000:
377 log.debug(
"Skipping DECam edge bleed check in amp %s: too few unmasked pixels.",
380 sky = stats.getValue(afwMath.MEANCLIP)
381 sigma = stats.getValue(afwMath.STDEVCLIP)
383 usable = (ampImage.mask.array & unusableBits) == 0
384 low = (ampImage.image.array < sky - nSigma*sigma) & usable
386 profile = low.sum(axis=1)
387 nUsable = usable.sum(axis=1)
389 profile = profile[::-1]
390 nUsable = nUsable[::-1]
393 enoughUsable = numpy.nonzero(nUsable >= minLowPixelsPerRow)[0]
394 if len(enoughUsable) == 0:
395 log.debug(
"Skipping DECam edge bleed check in amp %s: no rows with usable pixels.",
398 start = enoughUsable[0]
400 if profile[start:start + nRowsCheck].mean() <= minLowPixelsPerRow:
401 log.debug(
"Saturated footprint reaches read edge of amp %s but no dip found.",
407 for i
in range(start, len(profile)):
408 if profile[i] > minLowPixelsExtent:
413 if nBelow >= nRowsStop:
415 height += int(height*marginFraction) + 1
416 height = min(height, ampBBox.getHeight())
419 maskedImage.mask[ampBBox].array[-height:, :] |= saturatedBit
421 maskedImage.mask[ampBBox].array[:height, :] |= saturatedBit
422 log.info(
"Found DECam edge bleed in amp %s at the %s read edge; masked %d rows.",
423 amp.getName(),
"top" if readTop
else "bottom", height)
427 fpCore, itlEdgeBleedSatMinArea=10000,
428 itlEdgeBleedSatMaxArea=100000,
429 itlEdgeBleedThreshold=5000.,
430 itlEdgeBleedModelConstant=0.02,
431 saturatedMaskName="SAT", log=None):
432 """Mask edge bleeds in ITL detectors.
436 ccdExposure : `lsst.afw.image.Exposure`
437 Exposure to apply masking to.
438 badAmpDict : `dict` [`str`, `bool`]
439 Dictionary of amplifiers, keyed by name, value is True if
440 amplifier is fully masked.
441 fpCore : `lsst.afw.detection._detection.Footprint`
442 Footprint of saturated core.
443 itlEdgeBleedThreshold : `float`, optional
444 Threshold above median sky background for edge bleed detection
446 itlEdgeBleedModelConstant : `float`, optional
447 Constant in the decaying exponential in the edge bleed masking.
448 saturatedMaskName : `str`, optional
449 Mask name for saturation.
450 log : `logging.Logger`, optional
451 Logger to handle messages.
454 log = log
if log
else logging.getLogger(__name__)
457 satLevel = numpy.nanmedian([ccdExposure.metadata[f
"LSST ISR SATURATION LEVEL {amp.getName()}"]
458 for amp
in ccdExposure.getDetector()
if not badAmpDict[amp.getName()]])
462 xCore, yCore = fpCore.getCentroid()
464 yCoreFP = int(yCore) - fpCore.getBBox().getMinY()
468 checkCoreNbRow = fpCore.getSpans().asArray()[yCoreFP, :]
471 indexSwitchFalse = []
472 if checkCoreNbRow[0]:
476 indexSwitchTrue.append(0)
481 for i, value
in enumerate(checkCoreNbRow):
484 indexSwitchTrue.append(i)
489 indexSwitchFalse.append(i)
497 xEdgesCores.append(int((indexSwitchTrue[1] + indexSwitchFalse[0])/2))
498 xEdgesCores.append(fpCore.getSpans().asArray().shape[1])
500 for i
in range(nbCore):
501 subfp = fpCore.getSpans().asArray()[:, xEdgesCores[i]:xEdgesCores[i+1]]
502 xCoreFP = int(xEdgesCores[i] + numpy.argmax(numpy.sum(subfp, axis=0)))
504 xCore = xCoreFP + fpCore.getBBox().getMinX()
507 if subfp.shape[0] <= 200:
508 yCoreFP = int(numpy.argmax(numpy.sum(subfp, axis=1)))
510 yCoreFP = int(numpy.argmax(numpy.sum(subfp[100:-100, :],
512 yCoreFP = 100+yCoreFP
515 widthSat = numpy.sum(subfp[int(yCoreFP), :])
517 subfpArea = numpy.sum(subfp)
518 if subfpArea > itlEdgeBleedSatMinArea
and subfpArea < itlEdgeBleedSatMaxArea:
521 itlEdgeBleedThreshold,
522 itlEdgeBleedModelConstant,
523 saturatedMaskName, log)
527 "Too many (%d) cores in saturated footprint to mask edge bleeds.",
532 xCore, yCore = fpCore.getCentroid()
534 yCoreFP = yCore - fpCore.getBBox().getMinY()
536 widthSat = numpy.sum(fpCore.getSpans().asArray()[int(yCoreFP), :])
538 satLevel, widthSat, itlEdgeBleedThreshold,
539 itlEdgeBleedModelConstant, saturatedMaskName, log)
544 itlEdgeBleedThreshold=5000.,
545 itlEdgeBleedModelConstant=0.03,
546 saturatedMaskName="SAT", log=None):
547 """Apply ITL edge bleed masking model.
551 ccdExposure : `lsst.afw.image.Exposure`
552 Exposure to apply masking to.
554 X coordinate of the saturated core.
556 Minimum saturation level of the detector.
558 Width of the saturated core.
559 itlEdgeBleedThreshold : `float`, optional
560 Threshold above median sky background for edge bleed detection
562 itlEdgeBleedModelConstant : `float`, optional
563 Constant in the decaying exponential in the edge bleed masking.
564 saturatedMaskName : `str`, optional
565 Mask name for saturation.
566 log : `logging.Logger`, optional
567 Logger to handle messages.
569 log = log
if log
else logging.getLogger(__name__)
571 maskedImage = ccdExposure.maskedImage
572 xmax = maskedImage.image.array.shape[1]
573 saturatedBit = maskedImage.mask.getPlaneBitMask(saturatedMaskName)
575 for amp
in ccdExposure.getDetector():
581 yBox = amp.getBBox().getCenter()[1]
582 if amp.getBBox().contains(xCore, yBox):
585 ampName = amp.getName()
595 if ampName[:2] ==
'C1':
596 sliceImage = maskedImage.image.array[:200, :]
597 sliceMask = maskedImage.mask.array[:200, :]
598 elif ampName[:2] ==
'C0':
599 sliceImage = numpy.flipud(maskedImage.image.array[-200:, :])
600 sliceMask = numpy.flipud(maskedImage.mask.array[-200:, :])
612 lowerRangeSmall = int(xCore)-5
613 upperRangeSmall = int(xCore)+5
614 if lowerRangeSmall < 0:
616 if upperRangeSmall > xmax:
617 upperRangeSmall = xmax
618 ampImageBG = numpy.median(maskedImage[amp.getBBox()].image.array)
619 edgeMedian = numpy.median(sliceImage[:50, lowerRangeSmall:upperRangeSmall])
620 if edgeMedian > (ampImageBG + itlEdgeBleedThreshold):
622 log.info(
"Found ITL edge bleed in amp %s, column %d.", ampName, xCore)
631 subImageXMin = int(xCore)-250
632 subImageXMax = int(xCore)+250
635 elif subImageXMax > xmax:
638 subImage = sliceImage[:100, subImageXMin:subImageXMax]
639 maxWidthEdgeBleed = numpy.max(numpy.sum(subImage > 0.45*satLevel,
644 edgeBleedHalfWidth = \
645 int(((maxWidthEdgeBleed)*numpy.exp(-itlEdgeBleedModelConstant*y)
647 lowerRange = int(xCore)-edgeBleedHalfWidth
648 upperRange = int(xCore)+edgeBleedHalfWidth
654 if upperRange > xmax:
656 sliceMask[y, lowerRange:upperRange] |= saturatedBit
660 """Mask columns presenting saturation sag in saturated footprints in
665 ccdExposure : `lsst.afw.image.Exposure`
666 Exposure to apply masking to.
667 fpCore : `lsst.afw.detection._detection.Footprint`
668 Footprint of saturated core.
669 saturatedMaskName : `str`, optional
670 Mask name for saturation.
675 maskedImage = ccdExposure.maskedImage
676 saturatedBit = maskedImage.mask.getPlaneBitMask(saturatedMaskName)
678 cc = numpy.sum(fpCore.getSpans().asArray(), axis=0)
681 columnsToMaskFP = numpy.where(cc > fpCore.getSpans().asArray().shape[0]/5.)
683 columnsToMask = [x + int(fpCore.getBBox().getMinX())
for x
in columnsToMaskFP]
684 maskedImage.mask.array[:, columnsToMask] |= saturatedBit
687def maskITLDip(exposure, detectorConfig, maskPlaneNames=["SUSPECT", "ITL_DIP"], log=None):
688 """Add mask bits according to the ITL dip model.
692 exposure : `lsst.afw.image.Exposure`
693 Exposure to do ITL dip masking.
694 detectorConfig : `lsst.ip.isr.overscanAmpConfig.OverscanDetectorConfig`
695 Configuration for this detector.
696 maskPlaneNames : `list [`str`], optional
697 Name of the ITL Dip mask planes.
698 log : `logging.Logger`, optional
699 If not set, a default logger will be used.
701 if detectorConfig.itlDipBackgroundFraction == 0.0:
706 log = logging.getLogger(__name__)
708 thresh = afwDetection.Threshold(
709 exposure.mask.getPlaneBitMask(
"SAT"),
710 afwDetection.Threshold.BITMASK,
712 fpList = afwDetection.FootprintSet(exposure.mask, thresh).getFootprints()
714 heights = numpy.asarray([fp.getBBox().getHeight()
for fp
in fpList])
716 largeHeights, = numpy.where(heights >= detectorConfig.itlDipMinHeight)
718 if len(largeHeights) == 0:
722 approxBackground = numpy.median(exposure.image.array)
723 maskValue = exposure.mask.getPlaneBitMask(maskPlaneNames)
725 maskBak = exposure.mask.array.copy()
728 for index
in largeHeights:
730 center = fp.getCentroid()
732 nSat = numpy.sum(fp.getSpans().asArray(), axis=0)
733 width = numpy.sum(nSat > detectorConfig.itlDipMinHeight)
735 if width < detectorConfig.itlDipMinWidth:
738 width = numpy.clip(width,
None, detectorConfig.itlDipMaxWidth)
740 dipMax = detectorConfig.itlDipBackgroundFraction * approxBackground * width
743 if dipMax < detectorConfig.itlDipMinBackgroundNoiseFraction * numpy.sqrt(approxBackground):
746 minCol = int(center.getX() - (detectorConfig.itlDipWidthScale * width) / 2.)
747 maxCol = int(center.getX() + (detectorConfig.itlDipWidthScale * width) / 2.)
748 minCol = numpy.clip(minCol, 0,
None)
749 maxCol = numpy.clip(maxCol,
None, exposure.mask.array.shape[1] - 1)
752 "Found ITL dip (width %d; bkg %.2f); masking column %d to %d.",
759 exposure.mask.array[:, minCol: maxCol + 1] |= maskValue
761 nMaskedCols += (maxCol - minCol + 1)
763 if nMaskedCols > detectorConfig.itlDipMaxColsPerImage:
765 "Too many (%d) columns would be masked on this image from dip masking; restoring original mask.",
768 exposure.mask.array[:, :] = maskBak
772 maskNameList=['SAT'], fallbackValue=None, useLegacyInterp=True):
773 """Interpolate over defects identified by a particular set of mask planes.
777 maskedImage : `lsst.afw.image.MaskedImage`
780 FWHM of double Gaussian smoothing kernel.
781 growSaturatedFootprints : scalar, optional
782 Number of pixels to grow footprints for saturated pixels.
783 maskNameList : `List` of `str`, optional
785 fallbackValue : scalar, optional
786 Value of last resort for interpolation.
790 The ``fwhm`` parameter is used to create a PSF, but the underlying
791 interpolation code (`lsst.meas.algorithms.interpolateOverDefects`) does
792 not currently make use of this information.
794 mask = maskedImage.getMask()
796 if growSaturatedFootprints > 0
and "SAT" in maskNameList:
800 growMasks(mask, radius=growSaturatedFootprints, maskNameList=[
'SAT'], maskValue=
"SAT")
802 thresh = afwDetection.Threshold(mask.getPlaneBitMask(maskNameList), afwDetection.Threshold.BITMASK)
803 fpSet = afwDetection.FootprintSet(mask, thresh)
804 defectList = Defects.fromFootprintList(fpSet.getFootprints())
807 maskNameList=maskNameList, useLegacyInterp=useLegacyInterp)
813 fallbackValue=None, useLegacyInterp=True):
814 """Mark saturated pixels and optionally interpolate over them
818 maskedImage : `lsst.afw.image.MaskedImage`
821 Saturation level used as the detection threshold.
823 FWHM of double Gaussian smoothing kernel.
824 growFootprints : scalar, optional
825 Number of pixels to grow footprints of detected regions.
826 interpolate : Bool, optional
827 If True, saturated pixels are interpolated over.
828 maskName : str, optional
830 fallbackValue : scalar, optional
831 Value of last resort for interpolation.
835 The ``fwhm`` parameter is used to create a PSF, but the underlying
836 interpolation code (`lsst.meas.algorithms.interpolateOverDefects`) does
837 not currently make use of this information.
840 maskedImage=maskedImage,
841 threshold=saturation,
842 growFootprints=growFootprints,
847 maskNameList=[maskName], useLegacyInterp=useLegacyInterp)
853 """Compute number of edge trim pixels to match the calibration data.
855 Use the dimension difference between the raw exposure and the
856 calibration exposure to compute the edge trim pixels. This trim
857 is applied symmetrically, with the same number of pixels masked on
862 rawMaskedImage : `lsst.afw.image.MaskedImage`
864 calibMaskedImage : `lsst.afw.image.MaskedImage`
865 Calibration image to draw new bounding box from.
869 replacementMaskedImage : `lsst.afw.image.MaskedImage`
870 ``rawMaskedImage`` trimmed to the appropriate size.
875 Raised if ``rawMaskedImage`` cannot be symmetrically trimmed to
876 match ``calibMaskedImage``.
878 nx, ny = rawMaskedImage.getBBox().getDimensions() - calibMaskedImage.getBBox().getDimensions()
880 raise RuntimeError(
"Raw and calib maskedImages are trimmed differently in X and Y.")
882 raise RuntimeError(
"Calibration maskedImage is trimmed unevenly in X.")
884 raise RuntimeError(
"Calibration maskedImage is larger than raw data.")
888 replacementMaskedImage = rawMaskedImage[nEdge:-nEdge, nEdge:-nEdge, afwImage.LOCAL]
889 SourceDetectionTask.setEdgeBits(
891 replacementMaskedImage.getBBox(),
892 rawMaskedImage.getMask().getPlaneBitMask(
"EDGE")
895 replacementMaskedImage = rawMaskedImage
897 return replacementMaskedImage
901 """Apply bias correction in place.
905 maskedImage : `lsst.afw.image.MaskedImage`
906 Image to process. The image is modified by this method.
907 biasMaskedImage : `lsst.afw.image.MaskedImage`
908 Bias image of the same size as ``maskedImage``
909 trimToFit : `Bool`, optional
910 If True, raw data is symmetrically trimmed to match
916 Raised if ``maskedImage`` and ``biasMaskedImage`` do not have
923 if maskedImage.getBBox(afwImage.LOCAL) != biasMaskedImage.getBBox(afwImage.LOCAL):
924 raise RuntimeError(
"maskedImage bbox %s != biasMaskedImage bbox %s" %
925 (maskedImage.getBBox(afwImage.LOCAL), biasMaskedImage.getBBox(afwImage.LOCAL)))
926 maskedImage -= biasMaskedImage
929def darkCorrection(maskedImage, darkMaskedImage, expScale, darkScale, invert=False, trimToFit=False):
930 """Apply dark correction in place.
934 maskedImage : `lsst.afw.image.MaskedImage`
935 Image to process. The image is modified by this method.
936 darkMaskedImage : `lsst.afw.image.MaskedImage`
937 Dark image of the same size as ``maskedImage``.
939 Dark exposure time for ``maskedImage``.
941 Dark exposure time for ``darkMaskedImage``.
942 invert : `Bool`, optional
943 If True, re-add the dark to an already corrected image.
944 trimToFit : `Bool`, optional
945 If True, raw data is symmetrically trimmed to match
951 Raised if ``maskedImage`` and ``darkMaskedImage`` do not have
956 The dark correction is applied by calculating:
957 maskedImage -= dark * expScaling / darkScaling
962 if maskedImage.getBBox(afwImage.LOCAL) != darkMaskedImage.getBBox(afwImage.LOCAL):
963 raise RuntimeError(
"maskedImage bbox %s != darkMaskedImage bbox %s" %
964 (maskedImage.getBBox(afwImage.LOCAL), darkMaskedImage.getBBox(afwImage.LOCAL)))
966 scale = expScale / darkScale
968 maskedImage.scaledMinus(scale, darkMaskedImage)
970 maskedImage.scaledPlus(scale, darkMaskedImage)
974 """Set the variance plane based on the image plane.
976 The maskedImage must have units of `adu` (if gain != 1.0) or
977 electron (if gain == 1.0). This routine will always produce a
978 variance plane in the same units as the image.
982 maskedImage : `lsst.afw.image.MaskedImage`
983 Image to process. The variance plane is modified.
985 The amplifier gain in electron/adu.
987 The amplifier read noise in electron/pixel.
988 replace : `bool`, optional
989 Replace the current variance? If False, the image
990 variance will be added to the current variance plane.
992 var = maskedImage.variance
994 var[:, :] = maskedImage.image
996 var[:, :] += maskedImage.image
998 var += (readNoise/gain)**2
1001def flatCorrection(maskedImage, flatMaskedImage, scalingType, userScale=1.0, invert=False, trimToFit=False):
1002 """Apply flat correction in place.
1006 maskedImage : `lsst.afw.image.MaskedImage`
1007 Image to process. The image is modified.
1008 flatMaskedImage : `lsst.afw.image.MaskedImage`
1009 Flat image of the same size as ``maskedImage``
1011 Flat scale computation method. Allowed values are 'MEAN',
1012 'MEDIAN', or 'USER'.
1013 userScale : scalar, optional
1014 Scale to use if ``scalingType='USER'``.
1015 invert : `Bool`, optional
1016 If True, unflatten an already flattened image.
1017 trimToFit : `Bool`, optional
1018 If True, raw data is symmetrically trimmed to match
1024 Raised if ``maskedImage`` and ``flatMaskedImage`` do not have
1025 the same size or if ``scalingType`` is not an allowed value.
1030 if maskedImage.getBBox(afwImage.LOCAL) != flatMaskedImage.getBBox(afwImage.LOCAL):
1031 raise RuntimeError(
"maskedImage bbox %s != flatMaskedImage bbox %s" %
1032 (maskedImage.getBBox(afwImage.LOCAL), flatMaskedImage.getBBox(afwImage.LOCAL)))
1038 if scalingType
in (
'MEAN',
'MEDIAN'):
1039 scalingType = afwMath.stringToStatisticsProperty(scalingType)
1040 flatScale = afwMath.makeStatistics(flatMaskedImage.image, scalingType).getValue()
1041 elif scalingType ==
'USER':
1042 flatScale = userScale
1044 raise RuntimeError(
'%s : %s not implemented' % (
"flatCorrection", scalingType))
1047 maskedImage.scaledDivides(1.0/flatScale, flatMaskedImage)
1049 maskedImage.scaledMultiplies(1.0/flatScale, flatMaskedImage)
1053 """Apply illumination correction in place.
1057 maskedImage : `lsst.afw.image.MaskedImage`
1058 Image to process. The image is modified.
1059 illumMaskedImage : `lsst.afw.image.MaskedImage`
1060 Illumination correction image of the same size as ``maskedImage``.
1062 Scale factor for the illumination correction.
1063 trimToFit : `Bool`, optional
1064 If True, raw data is symmetrically trimmed to match
1070 Raised if ``maskedImage`` and ``illumMaskedImage`` do not have
1076 if maskedImage.getBBox(afwImage.LOCAL) != illumMaskedImage.getBBox(afwImage.LOCAL):
1077 raise RuntimeError(
"maskedImage bbox %s != illumMaskedImage bbox %s" %
1078 (maskedImage.getBBox(afwImage.LOCAL), illumMaskedImage.getBBox(afwImage.LOCAL)))
1080 maskedImage.scaledDivides(1.0/illumScale, illumMaskedImage)
1084def gainContext(exp, image, apply, gains=None, invert=False, isTrimmed=True):
1085 """Context manager that applies and removes gain.
1089 exp : `lsst.afw.image.Exposure`
1090 Exposure to apply/remove gain.
1091 image : `lsst.afw.image.Image`
1092 Image to apply/remove gain.
1094 If True, apply and remove the amplifier gain.
1095 gains : `dict` [`str`, `float`], optional
1096 A dictionary, keyed by amplifier name, of the gains to use.
1097 If gains is None, the nominal gains in the amplifier object are used.
1098 invert : `bool`, optional
1099 Invert the gains (e.g. convert electrons to adu temporarily)?
1100 isTrimmed : `bool`, optional
1101 Is this a trimmed exposure?
1105 exp : `lsst.afw.image.Exposure`
1106 Exposure with the gain applied.
1110 if gains
and apply
is True:
1111 ampNames = [amp.getName()
for amp
in exp.getDetector()]
1112 for ampName
in ampNames:
1113 if ampName
not in gains.keys():
1114 raise RuntimeError(f
"Gains provided to gain context, but no entry found for amp {ampName}")
1117 ccd = exp.getDetector()
1119 sim = image.Factory(image, amp.getBBox()
if isTrimmed
else amp.getRawBBox())
1121 gain = gains[amp.getName()]
1123 gain = amp.getGain()
1133 ccd = exp.getDetector()
1135 sim = image.Factory(image, amp.getBBox()
if isTrimmed
else amp.getRawBBox())
1137 gain = gains[amp.getName()]
1139 gain = amp.getGain()
1147 sensorTransmission=None, atmosphereTransmission=None):
1148 """Attach a TransmissionCurve to an Exposure, given separate curves for
1149 different components.
1153 exposure : `lsst.afw.image.Exposure`
1154 Exposure object to modify by attaching the product of all given
1155 ``TransmissionCurves`` in post-assembly trimmed detector coordinates.
1156 Must have a valid ``Detector`` attached that matches the detector
1157 associated with sensorTransmission.
1158 opticsTransmission : `lsst.afw.image.TransmissionCurve`
1159 A ``TransmissionCurve`` that represents the throughput of the optics,
1160 to be evaluated in focal-plane coordinates.
1161 filterTransmission : `lsst.afw.image.TransmissionCurve`
1162 A ``TransmissionCurve`` that represents the throughput of the filter
1163 itself, to be evaluated in focal-plane coordinates.
1164 sensorTransmission : `lsst.afw.image.TransmissionCurve`
1165 A ``TransmissionCurve`` that represents the throughput of the sensor
1166 itself, to be evaluated in post-assembly trimmed detector coordinates.
1167 atmosphereTransmission : `lsst.afw.image.TransmissionCurve`
1168 A ``TransmissionCurve`` that represents the throughput of the
1169 atmosphere, assumed to be spatially constant.
1173 combined : `lsst.afw.image.TransmissionCurve`
1174 The TransmissionCurve attached to the exposure.
1178 All ``TransmissionCurve`` arguments are optional; if none are provided, the
1179 attached ``TransmissionCurve`` will have unit transmission everywhere.
1181 combined = afwImage.TransmissionCurve.makeIdentity()
1182 if atmosphereTransmission
is not None:
1183 combined *= atmosphereTransmission
1184 if opticsTransmission
is not None:
1185 combined *= opticsTransmission
1186 if filterTransmission
is not None:
1187 combined *= filterTransmission
1188 detector = exposure.getDetector()
1189 fpToPix = detector.getTransform(fromSys=camGeom.FOCAL_PLANE,
1190 toSys=camGeom.PIXELS)
1191 combined = combined.transformedBy(fpToPix)
1192 if sensorTransmission
is not None:
1193 combined *= sensorTransmission
1194 exposure.getInfo().setTransmissionCurve(combined)
1198def applyGains(exposure, normalizeGains=False, ptcGains=None, isTrimmed=True):
1199 """Scale an exposure by the amplifier gains.
1203 exposure : `lsst.afw.image.Exposure`
1204 Exposure to process. The image is modified.
1205 normalizeGains : `Bool`, optional
1206 If True, then amplifiers are scaled to force the median of
1207 each amplifier to equal the median of those medians.
1208 ptcGains : `dict`[`str`], optional
1209 Dictionary keyed by amp name containing the PTC gains.
1210 isTrimmed : `bool`, optional
1211 Is the input image trimmed?
1213 ccd = exposure.getDetector()
1214 ccdImage = exposure.getMaskedImage()
1219 sim = ccdImage.Factory(ccdImage, amp.getBBox())
1221 sim = ccdImage.Factory(ccdImage, amp.getRawBBox())
1223 sim *= ptcGains[amp.getName()]
1225 sim *= amp.getGain()
1228 medians.append(numpy.median(sim.getImage().getArray()))
1231 median = numpy.median(numpy.array(medians))
1232 for index, amp
in enumerate(ccd):
1234 sim = ccdImage.Factory(ccdImage, amp.getBBox())
1236 sim = ccdImage.Factory(ccdImage, amp.getRawBBox())
1237 if medians[index] != 0.0:
1238 sim *= median/medians[index]
1242 """Grow the saturation trails by an amount dependent on the width of the
1247 mask : `lsst.afw.image.Mask`
1248 Mask which will have the saturated areas grown.
1252 for i
in range(1, 6):
1253 extraGrowDict[i] = 0
1254 for i
in range(6, 8):
1255 extraGrowDict[i] = 1
1256 for i
in range(8, 10):
1257 extraGrowDict[i] = 3
1260 if extraGrowMax <= 0:
1263 saturatedBit = mask.getPlaneBitMask(
"SAT")
1265 xmin, ymin = mask.getBBox().getMin()
1266 width = mask.getWidth()
1268 thresh = afwDetection.Threshold(saturatedBit, afwDetection.Threshold.BITMASK)
1269 fpList = afwDetection.FootprintSet(mask, thresh).getFootprints()
1272 for s
in fp.getSpans():
1273 x0, x1 = s.getX0(), s.getX1()
1275 extraGrow = extraGrowDict.get(x1 - x0 + 1, extraGrowMax)
1278 x0 -= xmin + extraGrow
1279 x1 -= xmin - extraGrow
1286 mask.array[y, x0:x1+1] |= saturatedBit
1290 """Set all BAD areas of the chip to the average of the rest of the exposure
1294 exposure : `lsst.afw.image.Exposure`
1295 Exposure to mask. The exposure mask is modified.
1296 badStatistic : `str`, optional
1297 Statistic to use to generate the replacement value from the
1298 image data. Allowed values are 'MEDIAN' or 'MEANCLIP'.
1302 badPixelCount : scalar
1303 Number of bad pixels masked.
1304 badPixelValue : scalar
1305 Value substituted for bad pixels.
1310 Raised if `badStatistic` is not an allowed value.
1312 if badStatistic ==
"MEDIAN":
1313 statistic = afwMath.MEDIAN
1314 elif badStatistic ==
"MEANCLIP":
1315 statistic = afwMath.MEANCLIP
1317 raise RuntimeError(
"Impossible method %s of bad region correction" % badStatistic)
1319 mi = exposure.getMaskedImage()
1321 BAD = mask.getPlaneBitMask(
"BAD")
1322 INTRP = mask.getPlaneBitMask(
"INTRP")
1324 sctrl = afwMath.StatisticsControl()
1325 sctrl.setAndMask(BAD)
1326 value = afwMath.makeStatistics(mi, statistic, sctrl).getValue()
1328 maskArray = mask.getArray()
1329 imageArray = mi.getImage().getArray()
1330 badPixels = numpy.logical_and((maskArray & BAD) > 0, (maskArray & INTRP) == 0)
1331 imageArray[:] = numpy.where(badPixels, value, imageArray)
1333 return badPixels.sum(), value
1337 """Check to see if an exposure is in a filter specified by a list.
1339 The goal of this is to provide a unified filter checking interface
1340 for all filter dependent stages.
1344 exposure : `lsst.afw.image.Exposure`
1345 Exposure to examine.
1346 filterList : `list` [`str`]
1347 List of physical_filter names to check.
1348 log : `logging.Logger`
1349 Logger to handle messages.
1354 True if the exposure's filter is contained in the list.
1356 if len(filterList) == 0:
1358 thisFilter = exposure.getFilter()
1359 if thisFilter
is None:
1360 log.warning(
"No FilterLabel attached to this exposure!")
1364 if thisPhysicalFilter
in filterList:
1366 elif thisFilter.bandLabel
in filterList:
1368 log.warning(
"Physical filter (%s) should be used instead of band %s for filter configurations"
1369 " (%s)", thisPhysicalFilter, thisFilter.bandLabel, filterList)
1376 """Get the physical filter label associated with the given filterLabel.
1378 If ``filterLabel`` is `None` or there is no physicalLabel attribute
1379 associated with the given ``filterLabel``, the returned label will be
1384 filterLabel : `lsst.afw.image.FilterLabel`
1385 The `lsst.afw.image.FilterLabel` object from which to derive the
1386 physical filter label.
1387 log : `logging.Logger`
1388 Logger to handle messages.
1392 physicalFilter : `str`
1393 The value returned by the physicalLabel attribute of ``filterLabel`` if
1394 it exists, otherwise set to \"Unknown\".
1396 if filterLabel
is None:
1397 physicalFilter =
"Unknown"
1398 log.warning(
"filterLabel is None. Setting physicalFilter to \"Unknown\".")
1401 physicalFilter = filterLabel.physicalLabel
1402 except RuntimeError:
1403 log.warning(
"filterLabel has no physicalLabel attribute. Setting physicalFilter to \"Unknown\".")
1404 physicalFilter =
"Unknown"
1405 return physicalFilter
1409 """Count the number of pixels in a given mask plane.
1413 maskedIm : `~lsst.afw.image.MaskedImage`
1414 Masked image to examine.
1416 Name of the mask plane to examine.
1421 Number of pixels in the requested mask plane.
1423 maskBit = maskedIm.mask.getPlaneBitMask(maskPlane)
1424 nPix = numpy.where(numpy.bitwise_and(maskedIm.mask.array, maskBit))[0].flatten().size
1429 """Get the per-amplifier gains used for this exposure.
1433 exposure : `lsst.afw.image.Exposure`
1434 The exposure to find gains for.
1438 gains : `dict` [`str` `float`]
1439 Dictionary of gain values, keyed by amplifier name.
1440 Returns empty dict when detector is None.
1442 det = exposure.getDetector()
1446 metadata = exposure.getMetadata()
1449 ampName = amp.getName()
1451 if (key1 := f
"LSST ISR GAIN {ampName}")
in metadata:
1452 gains[ampName] = metadata[key1]
1453 elif (key2 := f
"LSST GAIN {ampName}")
in metadata:
1454 gains[ampName] = metadata[key2]
1456 gains[ampName] = amp.getGain()
1461 """Get the per-amplifier read noise used for this exposure.
1465 exposure : `lsst.afw.image.Exposure`
1466 The exposure to find read noise for.
1470 readnoises : `dict` [`str` `float`]
1471 Dictionary of read noise values, keyed by amplifier name.
1472 Returns empty dict when detector is None.
1474 det = exposure.getDetector()
1478 metadata = exposure.getMetadata()
1481 ampName = amp.getName()
1483 if (key1 := f
"LSST ISR READNOISE {ampName}")
in metadata:
1484 readnoises[ampName] = metadata[key1]
1485 elif (key2 := f
"LSST READNOISE {ampName}")
in metadata:
1486 readnoises[ampName] = metadata[key2]
1488 readnoises[ampName] = amp.getReadNoise()
1493 """Check if the unused pixels (pre-/over-scan pixels) have
1494 been trimmed from an exposure.
1498 exposure : `lsst.afw.image.Exposure`
1499 The exposure to check.
1504 True if the image is trimmed, else False.
1506 return exposure.getDetector().getBBox() == exposure.getBBox()
1510 """Check if the unused pixels (pre-/over-scan pixels) have
1511 been trimmed from an image
1515 image : `lsst.afw.image.Image`
1517 detector : `lsst.afw.cameraGeom.Detector`
1518 The detector associated with the image.
1523 True if the image is trimmed, else False.
1525 return detector.getBBox() == image.getBBox()
1529 doRaiseOnCalibMismatch,
1530 cameraKeywordsToCompare,
1536 """Compare header keywords to confirm camera states match.
1540 doRaiseOnCalibMismatch : `bool`
1541 Raise on calibration mismatch? Otherwise, log a warning.
1542 cameraKeywordsToCompare : `list` [`str`]
1543 List of camera keywords to compare.
1544 exposureMetadata : `lsst.daf.base.PropertyList`
1545 Header for the exposure being processed.
1546 calib : `lsst.afw.image.Exposure` or `lsst.ip.isr.IsrCalib`
1547 Calibration to be applied.
1549 Calib type for log message.
1550 log : `logging.Logger`, optional
1551 Logger to handle messages.
1554 calibMetadata = calib.metadata
1555 except AttributeError:
1558 log = log
if log
else logging.getLogger(__name__)
1560 missingKeywords = []
1561 for keyword
in cameraKeywordsToCompare:
1562 exposureValue = exposureMetadata.get(keyword,
None)
1563 if exposureValue
is None:
1564 log.debug(
"Sequencer keyword %s not found in exposure metadata.", keyword)
1567 calibValue = calibMetadata.get(keyword,
None)
1570 if calibValue
is None:
1571 missingKeywords.append(keyword)
1574 if exposureValue != calibValue:
1575 if doRaiseOnCalibMismatch:
1577 "Sequencer mismatch for %s [%s]: exposure: %s calib: %s",
1585 "Sequencer mismatch for %s [%s]: exposure: %s calib: %s",
1591 exposureMetadata[f
"ISR {calibName.upper()} SEQUENCER MISMATCH"] =
True
1595 "Calibration %s missing keywords %s, which were not checked.",
1597 ",".join(missingKeywords),
1602 """ Copy array over 4 quadrants prior to convolution.
1606 inputarray : `numpy.array`
1607 Input array to symmetrize.
1611 aSym : `numpy.array`
1614 targetShape = list(inputArray.shape)
1615 r1, r2 = inputArray.shape[-1], inputArray.shape[-2]
1616 targetShape[-1] = 2*r1-1
1617 targetShape[-2] = 2*r2-1
1618 aSym = numpy.ndarray(tuple(targetShape))
1619 aSym[..., r2-1:, r1-1:] = inputArray
1620 aSym[..., r2-1:, r1-1::-1] = inputArray
1621 aSym[..., r2-1::-1, r1-1::-1] = inputArray
1622 aSym[..., r2-1::-1, r1-1:] = inputArray
gainContext(exp, image, apply, gains=None, invert=False, isTrimmed=True)
makeThresholdMask(maskedImage, threshold, growFootprints=1, maskName='SAT')
maskITLDip(exposure, detectorConfig, maskPlaneNames=["SUSPECT", "ITL_DIP"], log=None)
_applyMaskITLEdgeBleed(ccdExposure, xCore, satLevel, widthSat, itlEdgeBleedThreshold=5000., itlEdgeBleedModelConstant=0.03, saturatedMaskName="SAT", log=None)
maskDECamEdgeBleed(exposure, satMinArea=10000, satMaxArea=100000, approachRows=20, nSigma=5.0, nRowsCheck=20, minLowPixelsPerRow=30, minLowPixelsExtent=10, marginFraction=0.125, saturatedMaskName="SAT", log=None)
illuminationCorrection(maskedImage, illumMaskedImage, illumScale, trimToFit=True)
setBadRegions(exposure, badStatistic="MEDIAN")
isTrimmedImage(image, detector)
getExposureReadNoises(exposure)
maskE2VEdgeBleed(exposure, e2vEdgeBleedSatMinArea=10000, e2vEdgeBleedSatMaxArea=100000, e2vEdgeBleedYMax=350, saturatedMaskName="SAT", log=None)
flatCorrection(maskedImage, flatMaskedImage, scalingType, userScale=1.0, invert=False, trimToFit=False)
applyGains(exposure, normalizeGains=False, ptcGains=None, isTrimmed=True)
trimToMatchCalibBBox(rawMaskedImage, calibMaskedImage)
updateVariance(maskedImage, gain, readNoise, replace=True)
interpolateFromMask(maskedImage, fwhm, growSaturatedFootprints=1, maskNameList=['SAT'], fallbackValue=None, useLegacyInterp=True)
biasCorrection(maskedImage, biasMaskedImage, trimToFit=False)
darkCorrection(maskedImage, darkMaskedImage, expScale, darkScale, invert=False, trimToFit=False)
maskITLSatSag(ccdExposure, fpCore, saturatedMaskName="SAT")
isTrimmedExposure(exposure)
growMasks(mask, radius=0, maskNameList=['BAD'], maskValue="BAD")
checkFilter(exposure, filterList, log)
transposeMaskedImage(maskedImage)
compareCameraKeywords(doRaiseOnCalibMismatch, cameraKeywordsToCompare, exposureMetadata, calib, calibName, log=None)
interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=None, maskNameList=None, useLegacyInterp=True)
countMaskedPixels(maskedIm, maskPlane)
getExposureGains(exposure)
getPhysicalFilter(filterLabel, log)
saturationCorrection(maskedImage, saturation, fwhm, growFootprints=1, interpolate=True, maskName='SAT', fallbackValue=None, useLegacyInterp=True)
widenSaturationTrails(mask)
attachTransmissionCurve(exposure, opticsTransmission=None, filterTransmission=None, sensorTransmission=None, atmosphereTransmission=None)
maskITLEdgeBleed(ccdExposure, badAmpDict, fpCore, itlEdgeBleedSatMinArea=10000, itlEdgeBleedSatMaxArea=100000, itlEdgeBleedThreshold=5000., itlEdgeBleedModelConstant=0.02, saturatedMaskName="SAT", log=None)