700 Mask SATURATED and SUSPECT pixels and check if any amplifiers
705 badAmpDict : `str` [`bool`]
706 Dictionary of amplifiers, keyed by name, value is True if
707 amplifier is fully masked.
708 ccdExposure : `lsst.afw.image.Exposure`
709 Input exposure to be masked.
710 detector : `lsst.afw.cameraGeom.Detector`
712 defects : `lsst.ip.isr.Defects`
713 List of defects. Used to determine if an entire
715 detectorConfig : `lsst.ip.isr.OverscanDetectorConfig`
716 Per-amplifier configurations.
717 ptc : `lsst.ip.isr.PhotonTransferCurveDataset`, optional
718 PTC dataset (used if configured to use PTCTURNOFF).
722 badAmpDict : `str`[`bool`]
723 Dictionary of amplifiers, keyed by name.
725 maskedImage = ccdExposure.getMaskedImage()
727 metadata = ccdExposure.metadata
729 if self.config.doSaturation
and self.config.defaultSaturationSource ==
"PTCTURNOFF" and ptc
is None:
730 raise RuntimeError(
"Must provide ptc if using PTCTURNOFF as saturation source.")
731 if self.config.doSuspect
and self.config.defaultSuspectSource ==
"PTCTURNOFF" and ptc
is None:
732 raise RuntimeError(
"Must provide ptc if using PTCTURNOFF as suspect source.")
735 ampName = amp.getName()
737 ampConfig = detectorConfig.getOverscanAmpConfig(amp)
739 if badAmpDict[ampName]:
745 if self.config.doSaturation:
746 if self.config.defaultSaturationSource ==
"PTCTURNOFF":
747 limits.update({self.config.saturatedMaskName: ptc.ptcTurnoff[amp.getName()]})
748 elif self.config.defaultSaturationSource ==
"CAMERAMODEL":
750 limits.update({self.config.saturatedMaskName: amp.getSaturation()})
751 elif self.config.defaultSaturationSource ==
"NONE":
752 limits.update({self.config.saturatedMaskName: 1e100})
755 if math.isfinite(ampConfig.saturation):
756 limits.update({self.config.saturatedMaskName: ampConfig.saturation})
757 metadata[f
"LSST ISR SATURATION LEVEL {ampName}"] = limits[self.config.saturatedMaskName]
759 if self.config.doSuspect:
760 if self.config.defaultSuspectSource ==
"PTCTURNOFF":
761 limits.update({self.config.suspectMaskName: ptc.ptcTurnoff[amp.getName()]})
762 elif self.config.defaultSuspectSource ==
"CAMERAMODEL":
764 limits.update({self.config.suspectMaskName: amp.getSuspectLevel()})
765 elif self.config.defaultSuspectSource ==
"NONE":
766 limits.update({self.config.suspectMaskName: 1e100})
769 if math.isfinite(ampConfig.suspectLevel):
770 limits.update({self.config.suspectMaskName: ampConfig.suspectLevel})
771 metadata[f
"LSST ISR SUSPECT LEVEL {ampName}"] = limits[self.config.suspectMaskName]
773 for maskName, maskThreshold
in limits.items():
774 if not math.isnan(maskThreshold):
775 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
776 isrFunctions.makeThresholdMask(
777 maskedImage=dataView,
778 threshold=maskThreshold,
785 maskView =
afwImage.Mask(maskedImage.getMask(), amp.getRawDataBBox(),
787 maskVal = maskView.getPlaneBitMask([self.config.saturatedMaskName,
788 self.config.suspectMaskName])
789 if numpy.all(maskView.getArray() & maskVal > 0):
790 self.log.warning(
"Amplifier %s is bad (completely SATURATED or SUSPECT)", ampName)
791 badAmpDict[ampName] =
True
792 maskView |= maskView.getPlaneBitMask(
"BAD")
797 """Apply serial overscan correction in place to all amps.
799 The actual overscan subtraction is performed by the
800 `lsst.ip.isr.overscan.OverscanTask`, which is called here.
805 Must be `SERIAL` or `PARALLEL`.
806 detectorConfig : `lsst.ip.isr.OverscanDetectorConfig`
807 Per-amplifier configurations.
808 detector : `lsst.afw.cameraGeom.Detector`
811 Dictionary of amp name to whether it is a bad amp.
812 ccdExposure : `lsst.afw.image.Exposure`
813 Exposure to have overscan correction performed.
817 overscans : `list` [`lsst.pipe.base.Struct` or None]
818 Overscan measurements (always in adu).
819 Each result struct has components:
822 Value or fit subtracted from the amplifier image data.
823 (scalar or `lsst.afw.image.Image`)
825 Value or fit subtracted from the overscan image data.
826 (scalar or `lsst.afw.image.Image`)
828 Image of the overscan region with the overscan
829 correction applied. This quantity is used to estimate
830 the amplifier read noise empirically.
831 (`lsst.afw.image.Image`)
833 Mean overscan fit value. (`float`)
835 Median overscan fit value. (`float`)
837 Clipped standard deviation of the overscan fit. (`float`)
839 Mean of the overscan after fit subtraction. (`float`)
841 Median of the overscan after fit subtraction. (`float`)
843 Clipped standard deviation of the overscan after fit
844 subtraction. (`float`)
848 lsst.ip.isr.overscan.OverscanTask
850 if mode
not in [
"SERIAL",
"PARALLEL"]:
851 raise ValueError(
"Mode must be SERIAL or PARALLEL")
856 for i, amp
in enumerate(detector):
857 ampName = amp.getName()
859 ampConfig = detectorConfig.getOverscanAmpConfig(amp)
861 if mode ==
"SERIAL" and not ampConfig.doSerialOverscan:
863 "ISR_OSCAN: Amplifier %s/%s configured to skip serial overscan.",
868 elif mode ==
"PARALLEL" and not ampConfig.doParallelOverscan:
870 "ISR_OSCAN: Amplifier %s configured to skip parallel overscan.",
875 elif badAmpDict[ampName]
or not ccdExposure.getBBox().contains(amp.getBBox()):
880 if isTrimmedExposure(ccdExposure):
882 "ISR_OSCAN: No overscan region for amp %s. Not performing overscan correction.",
891 results = serialOverscan.run(ccdExposure, amp)
893 config = ampConfig.parallelOverscanConfig
898 metadata = ccdExposure.metadata
907 if self.config.doSaturation:
908 saturationLevel = metadata[f
"LSST ISR SATURATION LEVEL {amp.getName()}"]
909 saturationLevel *= config.parallelOverscanSaturationLevelAdjustmentFactor
911 saturationLevel = config.parallelOverscanSaturationLevel
912 if ccdExposure.metadata[
"LSST ISR UNITS"] ==
"electron":
914 saturationLevel *= metadata[f
"LSST ISR GAIN {amp.getName()}"]
917 "Using saturation level of %.2f for parallel overscan amp %s",
922 parallelOverscan.maskParallelOverscanAmp(
925 saturationLevel=saturationLevel,
928 results = parallelOverscan.run(ccdExposure, amp)
930 metadata = ccdExposure.metadata
931 keyBase =
"LSST ISR OVERSCAN"
939 metadata[f
"{keyBase} {mode} UNITS"] = ccdExposure.metadata[
"LSST ISR UNITS"]
940 metadata[f
"{keyBase} {mode} MEAN {ampName}"] = results.overscanMean
941 metadata[f
"{keyBase} {mode} MEDIAN {ampName}"] = results.overscanMedian
942 metadata[f
"{keyBase} {mode} STDEV {ampName}"] = results.overscanSigma
944 metadata[f
"{keyBase} RESIDUAL {mode} MEAN {ampName}"] = results.residualMean
945 metadata[f
"{keyBase} RESIDUAL {mode} MEDIAN {ampName}"] = results.residualMedian
946 metadata[f
"{keyBase} RESIDUAL {mode} STDEV {ampName}"] = results.residualSigma
948 overscans.append(results)
951 ccdExposure.metadata.set(
"OVERSCAN",
"Overscan corrected")
1189 """Apply a brighter fatter correction to the image using the
1190 method defined in Coulton et al. 2019.
1192 Note that this correction requires that the image is in units
1197 ccdExposure : `lsst.afw.image.Exposure`
1198 Exposure to process.
1199 flat : `lsst.afw.image.Exposure`
1200 Flat exposure the same size as ``exp``.
1201 dark : `lsst.afw.image.Exposure`, optional
1202 Dark exposure the same size as ``exp``.
1203 bfKernel : `lsst.ip.isr.BrighterFatterKernel`
1204 The brighter-fatter kernel.
1205 brighterFatterApplyGain : `bool`
1206 Apply the gain to convert the image to electrons?
1208 The gains to use if brighterFatterApplyGain = True.
1212 exp : `lsst.afw.image.Exposure`
1213 The flat and dark corrected exposure.
1215 interpExp = ccdExposure.clone()
1219 isrFunctions.interpolateFromMask(
1220 maskedImage=interpExp.getMaskedImage(),
1221 fwhm=self.config.brighterFatterFwhmForInterpolation,
1222 growSaturatedFootprints=self.config.growSaturationFootprintSize,
1223 maskNameList=list(self.config.brighterFatterMaskListToInterpolate),
1224 useLegacyInterp=self.config.useLegacyInterp,
1226 bfExp = interpExp.clone()
1227 bfResults = isrFunctions.brighterFatterCorrection(bfExp, bfKernel,
1228 self.config.brighterFatterMaxIter,
1229 self.config.brighterFatterThreshold,
1230 brighterFatterApplyGain,
1232 if bfResults[1] == self.config.brighterFatterMaxIter:
1233 self.log.warning(
"Brighter-fatter correction did not converge, final difference %f.",
1236 self.log.info(
"Finished brighter-fatter correction in %d iterations.",
1239 image = ccdExposure.getMaskedImage().getImage()
1240 bfCorr = bfExp.getMaskedImage().getImage()
1241 bfCorr -= interpExp.getMaskedImage().getImage()
1250 self.log.info(
"Ensuring image edges are masked as EDGE to the brighter-fatter kernel size.")
1251 self.
maskEdges(ccdExposure, numEdgePixels=numpy.max(bfKernel.shape) // 2,
1254 if self.config.brighterFatterMaskGrowSize > 0:
1255 self.log.info(
"Growing masks to account for brighter-fatter kernel convolution.")
1256 for maskPlane
in self.config.brighterFatterMaskListToInterpolate:
1257 isrFunctions.growMasks(ccdExposure.getMask(),
1258 radius=self.config.brighterFatterMaskGrowSize,
1259 maskNameList=maskPlane,
1260 maskValue=maskPlane)
1530 def run(self, ccdExposure, *, dnlLUT=None, bias=None, deferredChargeCalib=None, linearizer=None,
1531 ptc=None, crosstalk=None, defects=None, bfKernel=None, bfGains=None, dark=None,
1532 flat=None, camera=None, **kwargs
1535 detector = ccdExposure.getDetector()
1537 overscanDetectorConfig = self.config.overscanCamera.getOverscanDetectorConfig(detector)
1539 if self.config.doBootstrap
and ptc
is not None:
1540 self.log.warning(
"Task configured with doBootstrap=True. Ignoring provided PTC.")
1544 exposureMetadata = ccdExposure.metadata
1545 if not self.config.doBootstrap:
1548 if self.config.doCorrectGains:
1549 raise RuntimeError(
"doCorrectGains is True but no ptc provided.")
1550 if self.config.doDiffNonLinearCorrection:
1552 raise RuntimeError(
"doDiffNonLinearCorrection is True but no dnlLUT provided.")
1554 if self.config.doLinearize:
1555 if linearizer
is None:
1556 raise RuntimeError(
"doLinearize is True but no linearizer provided.")
1558 if self.config.doBias:
1560 raise RuntimeError(
"doBias is True but no bias provided.")
1563 if self.config.doCrosstalk:
1564 if crosstalk
is None:
1565 raise RuntimeError(
"doCrosstalk is True but no crosstalk provided.")
1567 if self.config.doDeferredCharge:
1568 if deferredChargeCalib
is None:
1569 raise RuntimeError(
"doDeferredCharge is True but no deferredChargeCalib provided.")
1571 if self.config.doDefect:
1573 raise RuntimeError(
"doDefect is True but no defects provided.")
1575 if self.config.doDark:
1577 raise RuntimeError(
"doDark is True but no dark frame provided.")
1580 if self.config.doBrighterFatter:
1581 if bfKernel
is None:
1582 raise RuntimeError(
"doBrighterFatter is True not no bfKernel provided.")
1584 if self.config.doFlat:
1586 raise RuntimeError(
"doFlat is True but no flat provided.")
1589 if self.config.doSaturation:
1590 if self.config.defaultSaturationSource
in [
"PTCTURNOFF",]:
1593 "doSaturation is True and defaultSaturationSource is "
1594 f
"{self.config.defaultSaturationSource}, but no ptc provided."
1596 if self.config.doSuspect:
1597 if self.config.defaultSuspectSource
in [
"PTCTURNOFF",]:
1600 "doSuspect is True and defaultSuspectSource is "
1601 f
"{self.config.defaultSuspectSource}, but no ptc provided."
1608 exposureMetadata[
"LSST ISR UNITS"] =
"adu"
1610 if self.config.doBootstrap:
1611 self.log.info(
"Configured using doBootstrap=True; using gain of 1.0 (adu units)")
1613 for amp
in detector:
1614 ptc.gain[amp.getName()] = 1.0
1615 ptc.noise[amp.getName()] = 0.0
1617 exposureMetadata[
"LSST ISR BOOTSTRAP"] = self.config.doBootstrap
1624 for amp
in detector:
1625 if not math.isnan(gain := overscanDetectorConfig.getOverscanAmpConfig(amp).gain):
1626 gains[amp.getName()] = gain
1628 "Overriding gain for amp %s with configured value of %.3f.",
1635 self.log.debug(
"Converting exposure to floating point values.")
1645 if self.config.doDiffNonLinearCorrection:
1656 if overscanDetectorConfig.doAnySerialOverscan:
1659 overscanDetectorConfig,
1665 if self.config.doBootstrap:
1667 for amp, serialOverscan
in zip(detector, serialOverscans):
1668 if serialOverscan
is None:
1669 ptc.noise[amp.getName()] = 0.0
1677 ptc.noise[amp.getName()] = serialOverscan.residualSigma * gains[amp.getName()]
1679 serialOverscans = [
None]*len(detector)
1691 overscanDetectorConfig,
1695 if self.config.doCorrectGains:
1698 self.log.info(
"Apply temperature dependence to the gains.")
1699 gains, readNoise = self.
correctGains(ccdExposure, ptc, gains)
1704 if self.config.doApplyGains:
1705 self.log.info(
"Using gain values to convert from adu to electron units.")
1706 isrFunctions.applyGains(ccdExposure, normalizeGains=
False, ptcGains=gains, isTrimmed=
False)
1708 exposureMetadata[
"LSST ISR UNITS"] =
"electron"
1712 for amp
in detector:
1713 ampName = amp.getName()
1714 if (key := f
"LSST ISR SATURATION LEVEL {ampName}")
in exposureMetadata:
1715 exposureMetadata[key] *= gains[ampName]
1716 if (key := f
"LSST ISR SUSPECT LEVEL {ampName}")
in exposureMetadata:
1717 exposureMetadata[key] *= gains[ampName]
1720 metadata = ccdExposure.metadata
1721 metadata[
"LSST ISR READNOISE UNITS"] =
"electron"
1722 for amp
in detector:
1724 metadata[f
"LSST ISR GAIN {amp.getName()}"] = gains[amp.getName()]
1727 noise = ptc.noise[amp.getName()]
1728 metadata[f
"LSST ISR READNOISE {amp.getName()}"] = noise
1732 if self.config.doCrosstalk:
1733 self.log.info(
"Applying crosstalk corrections to full amplifier region.")
1734 if self.config.doBootstrap
and numpy.any(crosstalk.fitGains != 0):
1735 crosstalkGains =
None
1737 crosstalkGains = gains
1740 crosstalk=crosstalk,
1742 gains=crosstalkGains,
1744 badAmpDict=badAmpDict,
1749 if overscanDetectorConfig.doAnyParallelOverscan:
1753 overscanDetectorConfig,
1761 if self.config.doLinearize:
1762 self.log.info(
"Applying linearizer.")
1766 if exposureMetadata[
"LSST ISR UNITS"] ==
"electron":
1767 linearityGains = gains
1769 linearityGains =
None
1770 linearizer.applyLinearity(
1771 image=ccdExposure.image,
1774 gains=linearityGains,
1780 if self.config.doDeferredCharge:
1781 self.deferredChargeCorrection.run(
1783 deferredChargeCalib,
1789 if self.config.doAssembleCcd:
1790 self.log.info(
"Assembling CCD from amplifiers.")
1791 ccdExposure = self.assembleCcd.assembleCcd(ccdExposure)
1793 if self.config.expectWcs
and not ccdExposure.getWcs():
1794 self.log.warning(
"No WCS found in input exposure.")
1798 if self.config.doBias:
1799 self.log.info(
"Applying bias correction.")
1802 isrFunctions.biasCorrection(ccdExposure.maskedImage, bias.maskedImage)
1806 if self.config.doDark:
1807 self.log.info(
"Applying dark subtraction.")
1816 if self.config.doDefect:
1817 self.log.info(
"Applying defect masking.")
1820 if self.config.doNanMasking:
1821 self.log.info(
"Masking non-finite (NAN, inf) value pixels.")
1824 if self.config.doWidenSaturationTrails:
1825 self.log.info(
"Widening saturation trails.")
1826 isrFunctions.widenSaturationTrails(ccdExposure.getMaskedImage().getMask())
1830 if self.config.doBrighterFatter:
1831 self.log.info(
"Applying brighter-fatter correction.")
1837 if exposureMetadata[
"LSST ISR UNITS"] ==
"electron":
1838 brighterFatterApplyGain =
False
1840 brighterFatterApplyGain =
True
1842 if brighterFatterApplyGain
and (ptc
is not None)
and (bfGains != gains):
1849 self.log.warning(
"Need to apply gain for brighter-fatter, but the stored"
1850 "gains in the kernel are not the same as the gains stored"
1851 "in the PTC. Using the kernel gains.")
1854 brighterFatterApplyGain, bfGains)
1858 if self.config.doVariance:
1865 if self.config.doFlat:
1866 self.log.info(
"Applying flat correction.")
1871 if self.config.doSaveInterpPixels:
1872 preInterpExp = ccdExposure.clone()
1874 if self.config.doSetBadRegions:
1875 self.log.info(
'Setting values in large contiguous bad regions.')
1878 if self.config.doInterpolate:
1879 self.log.info(
"Interpolating masked pixels.")
1880 isrFunctions.interpolateFromMask(
1881 maskedImage=ccdExposure.getMaskedImage(),
1882 fwhm=self.config.brighterFatterFwhmForInterpolation,
1883 growSaturatedFootprints=self.config.growSaturationFootprintSize,
1884 maskNameList=list(self.config.maskListToInterpolate),
1885 useLegacyInterp=self.config.useLegacyInterp,
1889 if self.config.doAmpOffset:
1890 if self.config.ampOffset.doApplyAmpOffset:
1891 self.log.info(
"Measuring and applying amp offset corrections.")
1893 self.log.info(
"Measuring amp offset corrections only, without applying them.")
1894 self.ampOffset.run(ccdExposure)
1897 if self.config.doStandardStatistics:
1898 metadata = ccdExposure.metadata
1899 for amp
in detector:
1900 ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
1901 ampName = amp.getName()
1902 metadata[f
"LSST ISR MASK SAT {ampName}"] = isrFunctions.countMaskedPixels(
1903 ampExposure.getMaskedImage(),
1904 [self.config.saturatedMaskName]
1906 metadata[f
"LSST ISR MASK BAD {ampName}"] = isrFunctions.countMaskedPixels(
1907 ampExposure.getMaskedImage(),
1911 afwMath.MEAN | afwMath.MEDIAN | afwMath.STDEVCLIP)
1913 metadata[f
"LSST ISR FINAL MEAN {ampName}"] = qaStats.getValue(afwMath.MEAN)
1914 metadata[f
"LSST ISR FINAL MEDIAN {ampName}"] = qaStats.getValue(afwMath.MEDIAN)
1915 metadata[f
"LSST ISR FINAL STDEV {ampName}"] = qaStats.getValue(afwMath.STDEVCLIP)
1917 k1 = f
"LSST ISR FINAL MEDIAN {ampName}"
1918 k2 = f
"LSST ISR OVERSCAN SERIAL MEDIAN {ampName}"
1919 if overscanDetectorConfig.doAnySerialOverscan
and k1
in metadata
and k2
in metadata:
1920 metadata[f
"LSST ISR LEVEL {ampName}"] = metadata[k1] - metadata[k2]
1922 metadata[f
"LSST ISR LEVEL {ampName}"] = numpy.nan
1925 outputStatistics =
None
1926 if self.config.doCalculateStatistics:
1927 outputStatistics = self.isrStats.run(ccdExposure, overscanResults=serialOverscans,
1928 bias=bias, dark=dark, flat=flat, ptc=ptc,
1929 defects=defects).results
1932 outputBin1Exposure =
None
1933 outputBin2Exposure =
None
1934 if self.config.doBinnedExposures:
1935 self.log.info(
"Creating binned exposures.")
1936 outputBin1Exposure = self.binning.run(
1938 binFactor=self.config.binFactor1,
1940 outputBin2Exposure = self.binning.run(
1942 binFactor=self.config.binFactor2,
1945 return pipeBase.Struct(
1946 exposure=ccdExposure,
1948 outputBin1Exposure=outputBin1Exposure,
1949 outputBin2Exposure=outputBin2Exposure,
1951 preInterpExposure=preInterpExp,
1952 outputExposure=ccdExposure,
1953 outputStatistics=outputStatistics,