22__all__ = [
"IsrTask",
"IsrTaskConfig"]
32import lsst.pipe.base.connectionTypes
as cT
34from contextlib
import contextmanager
35from lsstDebug
import getDebugFrame
40from lsst.utils.timer
import timeMethod
42from .
import isrFunctions
44from .
import linearize
45from .defects
import Defects
47from .assembleCcdTask
import AssembleCcdTask
48from .crosstalk
import CrosstalkTask, CrosstalkCalib
49from .fringe
import FringeTask
50from .isr
import maskNans
51from .masking
import MaskingTask
52from .overscan
import OverscanCorrectionTask
53from .straylight
import StrayLightTask
54from .vignette
import VignetteTask
55from .ampOffset
import AmpOffsetTask
56from .deferredCharge
import DeferredChargeTask
57from .isrStatistics
import IsrStatisticsTask
58from lsst.daf.butler
import DimensionGraph
62 """Lookup function to identify crosstalkSource entries.
64 This should return an empty list under most circumstances. Only
65 when inter-chip crosstalk has been identified should this be
72 registry : `lsst.daf.butler.Registry`
73 Butler registry to query.
74 quantumDataId : `lsst.daf.butler.ExpandedDataCoordinate`
75 Data id to transform to identify crosstalkSources. The
76 ``detector`` entry will be stripped.
77 collections : `lsst.daf.butler.CollectionSearch`
78 Collections to search through.
82 results : `list` [`lsst.daf.butler.DatasetRef`]
83 List of datasets that match the query that will be used
as
86 newDataId = quantumDataId.subset(DimensionGraph(registry.dimensions, names=["instrument",
"exposure"]))
87 results =
set(registry.queryDatasets(datasetType, collections=collections, dataId=newDataId,
94 return [ref.expanded(registry.expandDataId(ref.dataId, records=newDataId.records))
for ref
in results]
98 dimensions={
"instrument",
"exposure",
"detector"},
100 ccdExposure = cT.Input(
102 doc=
"Input exposure to process.",
103 storageClass=
"Exposure",
104 dimensions=[
"instrument",
"exposure",
"detector"],
106 camera = cT.PrerequisiteInput(
108 storageClass=
"Camera",
109 doc=
"Input camera to construct complete exposures.",
110 dimensions=[
"instrument"],
114 crosstalk = cT.PrerequisiteInput(
116 doc=
"Input crosstalk object",
117 storageClass=
"CrosstalkCalib",
118 dimensions=[
"instrument",
"detector"],
122 crosstalkSources = cT.PrerequisiteInput(
123 name=
"isrOverscanCorrected",
124 doc=
"Overscan corrected input images.",
125 storageClass=
"Exposure",
126 dimensions=[
"instrument",
"exposure",
"detector"],
129 lookupFunction=crosstalkSourceLookup,
132 bias = cT.PrerequisiteInput(
134 doc=
"Input bias calibration.",
135 storageClass=
"ExposureF",
136 dimensions=[
"instrument",
"detector"],
139 dark = cT.PrerequisiteInput(
141 doc=
"Input dark calibration.",
142 storageClass=
"ExposureF",
143 dimensions=[
"instrument",
"detector"],
146 flat = cT.PrerequisiteInput(
148 doc=
"Input flat calibration.",
149 storageClass=
"ExposureF",
150 dimensions=[
"instrument",
"physical_filter",
"detector"],
153 ptc = cT.PrerequisiteInput(
155 doc=
"Input Photon Transfer Curve dataset",
156 storageClass=
"PhotonTransferCurveDataset",
157 dimensions=[
"instrument",
"detector"],
160 fringes = cT.PrerequisiteInput(
162 doc=
"Input fringe calibration.",
163 storageClass=
"ExposureF",
164 dimensions=[
"instrument",
"physical_filter",
"detector"],
168 strayLightData = cT.PrerequisiteInput(
170 doc=
"Input stray light calibration.",
171 storageClass=
"StrayLightData",
172 dimensions=[
"instrument",
"physical_filter",
"detector"],
177 bfKernel = cT.PrerequisiteInput(
179 doc=
"Input brighter-fatter kernel.",
180 storageClass=
"NumpyArray",
181 dimensions=[
"instrument"],
185 newBFKernel = cT.PrerequisiteInput(
186 name=
'brighterFatterKernel',
187 doc=
"Newer complete kernel + gain solutions.",
188 storageClass=
"BrighterFatterKernel",
189 dimensions=[
"instrument",
"detector"],
193 defects = cT.PrerequisiteInput(
195 doc=
"Input defect tables.",
196 storageClass=
"Defects",
197 dimensions=[
"instrument",
"detector"],
200 linearizer = cT.PrerequisiteInput(
202 storageClass=
"Linearizer",
203 doc=
"Linearity correction calibration.",
204 dimensions=[
"instrument",
"detector"],
208 opticsTransmission = cT.PrerequisiteInput(
209 name=
"transmission_optics",
210 storageClass=
"TransmissionCurve",
211 doc=
"Transmission curve due to the optics.",
212 dimensions=[
"instrument"],
215 filterTransmission = cT.PrerequisiteInput(
216 name=
"transmission_filter",
217 storageClass=
"TransmissionCurve",
218 doc=
"Transmission curve due to the filter.",
219 dimensions=[
"instrument",
"physical_filter"],
222 sensorTransmission = cT.PrerequisiteInput(
223 name=
"transmission_sensor",
224 storageClass=
"TransmissionCurve",
225 doc=
"Transmission curve due to the sensor.",
226 dimensions=[
"instrument",
"detector"],
229 atmosphereTransmission = cT.PrerequisiteInput(
230 name=
"transmission_atmosphere",
231 storageClass=
"TransmissionCurve",
232 doc=
"Transmission curve due to the atmosphere.",
233 dimensions=[
"instrument"],
236 illumMaskedImage = cT.PrerequisiteInput(
238 doc=
"Input illumination correction.",
239 storageClass=
"MaskedImageF",
240 dimensions=[
"instrument",
"physical_filter",
"detector"],
243 deferredChargeCalib = cT.PrerequisiteInput(
245 doc=
"Deferred charge/CTI correction dataset.",
246 storageClass=
"IsrCalib",
247 dimensions=[
"instrument",
"detector"],
251 outputExposure = cT.Output(
253 doc=
"Output ISR processed exposure.",
254 storageClass=
"Exposure",
255 dimensions=[
"instrument",
"exposure",
"detector"],
257 preInterpExposure = cT.Output(
258 name=
'preInterpISRCCD',
259 doc=
"Output ISR processed exposure, with pixels left uninterpolated.",
260 storageClass=
"ExposureF",
261 dimensions=[
"instrument",
"exposure",
"detector"],
263 outputOssThumbnail = cT.Output(
265 doc=
"Output Overscan-subtracted thumbnail image.",
266 storageClass=
"Thumbnail",
267 dimensions=[
"instrument",
"exposure",
"detector"],
269 outputFlattenedThumbnail = cT.Output(
270 name=
"FlattenedThumb",
271 doc=
"Output flat-corrected thumbnail image.",
272 storageClass=
"Thumbnail",
273 dimensions=[
"instrument",
"exposure",
"detector"],
275 outputStatistics = cT.Output(
276 name=
"isrStatistics",
277 doc=
"Output of additional statistics table.",
278 storageClass=
"StructuredDataDict",
279 dimensions=[
"instrument",
"exposure",
"detector"],
285 if config.doBias
is not True:
286 self.prerequisiteInputs.remove(
"bias")
287 if config.doLinearize
is not True:
288 self.prerequisiteInputs.remove(
"linearizer")
289 if config.doCrosstalk
is not True:
290 self.prerequisiteInputs.remove(
"crosstalkSources")
291 self.prerequisiteInputs.remove(
"crosstalk")
292 if config.doBrighterFatter
is not True:
293 self.prerequisiteInputs.remove(
"bfKernel")
294 self.prerequisiteInputs.remove(
"newBFKernel")
295 if config.doDefect
is not True:
296 self.prerequisiteInputs.remove(
"defects")
297 if config.doDark
is not True:
298 self.prerequisiteInputs.remove(
"dark")
299 if config.doFlat
is not True:
300 self.prerequisiteInputs.remove(
"flat")
301 if config.doFringe
is not True:
302 self.prerequisiteInputs.remove(
"fringes")
303 if config.doStrayLight
is not True:
304 self.prerequisiteInputs.remove(
"strayLightData")
305 if config.usePtcGains
is not True and config.usePtcReadNoise
is not True:
306 self.prerequisiteInputs.remove(
"ptc")
307 if config.doAttachTransmissionCurve
is not True:
308 self.prerequisiteInputs.remove(
"opticsTransmission")
309 self.prerequisiteInputs.remove(
"filterTransmission")
310 self.prerequisiteInputs.remove(
"sensorTransmission")
311 self.prerequisiteInputs.remove(
"atmosphereTransmission")
313 if config.doUseOpticsTransmission
is not True:
314 self.prerequisiteInputs.remove(
"opticsTransmission")
315 if config.doUseFilterTransmission
is not True:
316 self.prerequisiteInputs.remove(
"filterTransmission")
317 if config.doUseSensorTransmission
is not True:
318 self.prerequisiteInputs.remove(
"sensorTransmission")
319 if config.doUseAtmosphereTransmission
is not True:
320 self.prerequisiteInputs.remove(
"atmosphereTransmission")
321 if config.doIlluminationCorrection
is not True:
322 self.prerequisiteInputs.remove(
"illumMaskedImage")
323 if config.doDeferredCharge
is not True:
324 self.prerequisiteInputs.remove(
"deferredChargeCalib")
326 if config.doWrite
is not True:
327 self.outputs.remove(
"outputExposure")
328 self.outputs.remove(
"preInterpExposure")
329 self.outputs.remove(
"outputFlattenedThumbnail")
330 self.outputs.remove(
"outputOssThumbnail")
331 self.outputs.remove(
"outputStatistics")
333 if config.doSaveInterpPixels
is not True:
334 self.outputs.remove(
"preInterpExposure")
335 if config.qa.doThumbnailOss
is not True:
336 self.outputs.remove(
"outputOssThumbnail")
337 if config.qa.doThumbnailFlattened
is not True:
338 self.outputs.remove(
"outputFlattenedThumbnail")
339 if config.doCalculateStatistics
is not True:
340 self.outputs.remove(
"outputStatistics")
344 pipelineConnections=IsrTaskConnections):
345 """Configuration parameters for IsrTask.
347 Items are grouped in the order
in which they are executed by the task.
349 datasetType = pexConfig.Field(
351 doc="Dataset type for input data; users will typically leave this alone, "
352 "but camera-specific ISR tasks will override it",
356 fallbackFilterName = pexConfig.Field(
358 doc=
"Fallback default filter name for calibrations.",
361 useFallbackDate = pexConfig.Field(
363 doc=
"Pass observation date when using fallback filter.",
366 expectWcs = pexConfig.Field(
369 doc=
"Expect input science images to have a WCS (set False for e.g. spectrographs)."
371 fwhm = pexConfig.Field(
373 doc=
"FWHM of PSF in arcseconds.",
376 qa = pexConfig.ConfigField(
378 doc=
"QA related configuration options.",
382 doConvertIntToFloat = pexConfig.Field(
384 doc=
"Convert integer raw images to floating point values?",
389 doSaturation = pexConfig.Field(
391 doc=
"Mask saturated pixels? NB: this is totally independent of the"
392 " interpolation option - this is ONLY setting the bits in the mask."
393 " To have them interpolated make sure doSaturationInterpolation=True",
396 saturatedMaskName = pexConfig.Field(
398 doc=
"Name of mask plane to use in saturation detection and interpolation",
401 saturation = pexConfig.Field(
403 doc=
"The saturation level to use if no Detector is present in the Exposure (ignored if NaN)",
404 default=float(
"NaN"),
406 growSaturationFootprintSize = pexConfig.Field(
408 doc=
"Number of pixels by which to grow the saturation footprints",
413 doSuspect = pexConfig.Field(
415 doc=
"Mask suspect pixels?",
418 suspectMaskName = pexConfig.Field(
420 doc=
"Name of mask plane to use for suspect pixels",
423 numEdgeSuspect = pexConfig.Field(
425 doc=
"Number of edge pixels to be flagged as untrustworthy.",
428 edgeMaskLevel = pexConfig.ChoiceField(
430 doc=
"Mask edge pixels in which coordinate frame: DETECTOR or AMP?",
433 'DETECTOR':
'Mask only the edges of the full detector.',
434 'AMP':
'Mask edges of each amplifier.',
439 doSetBadRegions = pexConfig.Field(
441 doc=
"Should we set the level of all BAD patches of the chip to the chip's average value?",
444 badStatistic = pexConfig.ChoiceField(
446 doc=
"How to estimate the average value for BAD regions.",
449 "MEANCLIP":
"Correct using the (clipped) mean of good data",
450 "MEDIAN":
"Correct using the median of the good data",
455 doOverscan = pexConfig.Field(
457 doc=
"Do overscan subtraction?",
460 overscan = pexConfig.ConfigurableField(
461 target=OverscanCorrectionTask,
462 doc=
"Overscan subtraction task for image segments.",
466 doAssembleCcd = pexConfig.Field(
469 doc=
"Assemble amp-level exposures into a ccd-level exposure?"
471 assembleCcd = pexConfig.ConfigurableField(
472 target=AssembleCcdTask,
473 doc=
"CCD assembly task",
477 doAssembleIsrExposures = pexConfig.Field(
480 doc=
"Assemble amp-level calibration exposures into ccd-level exposure?"
482 doTrimToMatchCalib = pexConfig.Field(
485 doc=
"Trim raw data to match calibration bounding boxes?"
489 doBias = pexConfig.Field(
491 doc=
"Apply bias frame correction?",
494 biasDataProductName = pexConfig.Field(
496 doc=
"Name of the bias data product",
499 doBiasBeforeOverscan = pexConfig.Field(
501 doc=
"Reverse order of overscan and bias correction.",
506 doDeferredCharge = pexConfig.Field(
508 doc=
"Apply deferred charge correction?",
511 deferredChargeCorrection = pexConfig.ConfigurableField(
512 target=DeferredChargeTask,
513 doc=
"Deferred charge correction task.",
517 doVariance = pexConfig.Field(
519 doc=
"Calculate variance?",
522 gain = pexConfig.Field(
524 doc=
"The gain to use if no Detector is present in the Exposure (ignored if NaN)",
525 default=float(
"NaN"),
527 readNoise = pexConfig.Field(
529 doc=
"The read noise to use if no Detector is present in the Exposure",
532 doEmpiricalReadNoise = pexConfig.Field(
535 doc=
"Calculate empirical read noise instead of value from AmpInfo data?"
537 usePtcReadNoise = pexConfig.Field(
540 doc=
"Use readnoise values from the Photon Transfer Curve?"
542 maskNegativeVariance = pexConfig.Field(
545 doc=
"Mask pixels that claim a negative variance? This likely indicates a failure "
546 "in the measurement of the overscan at an edge due to the data falling off faster "
547 "than the overscan model can account for it."
549 negativeVarianceMaskName = pexConfig.Field(
552 doc=
"Mask plane to use to mark pixels with negative variance, if `maskNegativeVariance` is True.",
555 doLinearize = pexConfig.Field(
557 doc=
"Correct for nonlinearity of the detector's response?",
562 doCrosstalk = pexConfig.Field(
564 doc=
"Apply intra-CCD crosstalk correction?",
567 doCrosstalkBeforeAssemble = pexConfig.Field(
569 doc=
"Apply crosstalk correction before CCD assembly, and before trimming?",
572 crosstalk = pexConfig.ConfigurableField(
573 target=CrosstalkTask,
574 doc=
"Intra-CCD crosstalk correction",
578 doDefect = pexConfig.Field(
580 doc=
"Apply correction for CCD defects, e.g. hot pixels?",
583 doNanMasking = pexConfig.Field(
585 doc=
"Mask non-finite (NAN, inf) pixels?",
588 doWidenSaturationTrails = pexConfig.Field(
590 doc=
"Widen bleed trails based on their width?",
595 doBrighterFatter = pexConfig.Field(
598 doc=
"Apply the brighter-fatter correction?"
600 brighterFatterLevel = pexConfig.ChoiceField(
603 doc=
"The level at which to correct for brighter-fatter.",
605 "AMP":
"Every amplifier treated separately.",
606 "DETECTOR":
"One kernel per detector",
609 brighterFatterMaxIter = pexConfig.Field(
612 doc=
"Maximum number of iterations for the brighter-fatter correction"
614 brighterFatterThreshold = pexConfig.Field(
617 doc=
"Threshold used to stop iterating the brighter-fatter correction. It is the "
618 "absolute value of the difference between the current corrected image and the one "
619 "from the previous iteration summed over all the pixels."
621 brighterFatterApplyGain = pexConfig.Field(
624 doc=
"Should the gain be applied when applying the brighter-fatter correction?"
626 brighterFatterMaskListToInterpolate = pexConfig.ListField(
628 doc=
"List of mask planes that should be interpolated over when applying the brighter-fatter "
630 default=[
"SAT",
"BAD",
"NO_DATA",
"UNMASKEDNAN"],
632 brighterFatterMaskGrowSize = pexConfig.Field(
635 doc=
"Number of pixels to grow the masks listed in config.brighterFatterMaskListToInterpolate "
636 "when brighter-fatter correction is applied."
640 doDark = pexConfig.Field(
642 doc=
"Apply dark frame correction?",
645 darkDataProductName = pexConfig.Field(
647 doc=
"Name of the dark data product",
652 doStrayLight = pexConfig.Field(
654 doc=
"Subtract stray light in the y-band (due to encoder LEDs)?",
657 strayLight = pexConfig.ConfigurableField(
658 target=StrayLightTask,
659 doc=
"y-band stray light correction"
663 doFlat = pexConfig.Field(
665 doc=
"Apply flat field correction?",
668 flatDataProductName = pexConfig.Field(
670 doc=
"Name of the flat data product",
673 flatScalingType = pexConfig.ChoiceField(
675 doc=
"The method for scaling the flat on the fly.",
678 "USER":
"Scale by flatUserScale",
679 "MEAN":
"Scale by the inverse of the mean",
680 "MEDIAN":
"Scale by the inverse of the median",
683 flatUserScale = pexConfig.Field(
685 doc=
"If flatScalingType is 'USER' then scale flat by this amount; ignored otherwise",
688 doTweakFlat = pexConfig.Field(
690 doc=
"Tweak flats to match observed amplifier ratios?",
696 doApplyGains = pexConfig.Field(
698 doc=
"Correct the amplifiers for their gains instead of applying flat correction",
701 usePtcGains = pexConfig.Field(
703 doc=
"Use the gain values from the Photon Transfer Curve?",
706 normalizeGains = pexConfig.Field(
708 doc=
"Normalize all the amplifiers in each CCD to have the same median value.",
713 doFringe = pexConfig.Field(
715 doc=
"Apply fringe correction?",
718 fringe = pexConfig.ConfigurableField(
720 doc=
"Fringe subtraction task",
722 fringeAfterFlat = pexConfig.Field(
724 doc=
"Do fringe subtraction after flat-fielding?",
729 doAmpOffset = pexConfig.Field(
730 doc=
"Calculate and apply amp offset corrections?",
734 ampOffset = pexConfig.ConfigurableField(
735 doc=
"Amp offset correction task.",
736 target=AmpOffsetTask,
740 doMeasureBackground = pexConfig.Field(
742 doc=
"Measure the background level on the reduced image?",
747 doCameraSpecificMasking = pexConfig.Field(
749 doc=
"Mask camera-specific bad regions?",
752 masking = pexConfig.ConfigurableField(
758 doInterpolate = pexConfig.Field(
760 doc=
"Interpolate masked pixels?",
763 doSaturationInterpolation = pexConfig.Field(
765 doc=
"Perform interpolation over pixels masked as saturated?"
766 " NB: This is independent of doSaturation; if that is False this plane"
767 " will likely be blank, resulting in a no-op here.",
770 doNanInterpolation = pexConfig.Field(
772 doc=
"Perform interpolation over pixels masked as NaN?"
773 " NB: This is independent of doNanMasking; if that is False this plane"
774 " will likely be blank, resulting in a no-op here.",
777 doNanInterpAfterFlat = pexConfig.Field(
779 doc=(
"If True, ensure we interpolate NaNs after flat-fielding, even if we "
780 "also have to interpolate them before flat-fielding."),
783 maskListToInterpolate = pexConfig.ListField(
785 doc=
"List of mask planes that should be interpolated.",
786 default=[
'SAT',
'BAD'],
788 doSaveInterpPixels = pexConfig.Field(
790 doc=
"Save a copy of the pre-interpolated pixel values?",
795 fluxMag0T1 = pexConfig.DictField(
798 doc=
"The approximate flux of a zero-magnitude object in a one-second exposure, per filter.",
799 default=dict((f, pow(10.0, 0.4*m))
for f, m
in ((
"Unknown", 28.0),
802 defaultFluxMag0T1 = pexConfig.Field(
804 doc=
"Default value for fluxMag0T1 (for an unrecognized filter).",
805 default=pow(10.0, 0.4*28.0)
809 doVignette = pexConfig.Field(
811 doc=(
"Compute and attach the validPolygon defining the unvignetted region to the exposure "
812 "according to vignetting parameters?"),
815 doMaskVignettePolygon = pexConfig.Field(
817 doc=(
"Add a mask bit for pixels within the vignetted region. Ignored if doVignette "
821 vignetteValue = pexConfig.Field(
823 doc=
"Value to replace image array pixels with in the vignetted region? Ignored if None.",
827 vignette = pexConfig.ConfigurableField(
829 doc=
"Vignetting task.",
833 doAttachTransmissionCurve = pexConfig.Field(
836 doc=
"Construct and attach a wavelength-dependent throughput curve for this CCD image?"
838 doUseOpticsTransmission = pexConfig.Field(
841 doc=
"Load and use transmission_optics (if doAttachTransmissionCurve is True)?"
843 doUseFilterTransmission = pexConfig.Field(
846 doc=
"Load and use transmission_filter (if doAttachTransmissionCurve is True)?"
848 doUseSensorTransmission = pexConfig.Field(
851 doc=
"Load and use transmission_sensor (if doAttachTransmissionCurve is True)?"
853 doUseAtmosphereTransmission = pexConfig.Field(
856 doc=
"Load and use transmission_atmosphere (if doAttachTransmissionCurve is True)?"
860 doIlluminationCorrection = pexConfig.Field(
863 doc=
"Perform illumination correction?"
865 illuminationCorrectionDataProductName = pexConfig.Field(
867 doc=
"Name of the illumination correction data product.",
870 illumScale = pexConfig.Field(
872 doc=
"Scale factor for the illumination correction.",
875 illumFilters = pexConfig.ListField(
878 doc=
"Only perform illumination correction for these filters."
882 doCalculateStatistics = pexConfig.Field(
884 doc=
"Should additional ISR statistics be calculated?",
887 isrStats = pexConfig.ConfigurableField(
888 target=IsrStatisticsTask,
889 doc=
"Task to calculate additional statistics.",
894 doWrite = pexConfig.Field(
896 doc=
"Persist postISRCCD?",
903 raise ValueError(
"You may not specify both doFlat and doApplyGains")
905 raise ValueError(
"You may not specify both doBiasBeforeOverscan and doTrimToMatchCalib")
915 """Apply common instrument signature correction algorithms to a raw frame.
917 The process for correcting imaging data
is very similar
from
918 camera to camera. This task provides a vanilla implementation of
919 doing these corrections, including the ability to turn certain
920 corrections off
if they are
not needed. The inputs to the primary
921 method, `run()`, are a raw exposure to be corrected
and the
922 calibration data products. The raw input
is a single chip sized
923 mosaic of all amps including overscans
and other non-science
926 The __init__ method sets up the subtasks
for ISR processing, using
932 Positional arguments passed to the Task constructor.
933 None used at this time.
934 kwargs : `dict`, optional
935 Keyword arguments passed on to the Task constructor.
936 None used at this time.
938 ConfigClass = IsrTaskConfig
941 def __init__(self, **kwargs):
942 super().__init__(**kwargs)
943 self.makeSubtask(
"assembleCcd")
944 self.makeSubtask(
"crosstalk")
945 self.makeSubtask(
"strayLight")
946 self.makeSubtask(
"fringe")
947 self.makeSubtask(
"masking")
948 self.makeSubtask(
"overscan")
949 self.makeSubtask(
"vignette")
950 self.makeSubtask(
"ampOffset")
951 self.makeSubtask(
"deferredChargeCorrection")
952 self.makeSubtask(
"isrStats")
955 inputs = butlerQC.get(inputRefs)
958 inputs[
'detectorNum'] = inputRefs.ccdExposure.dataId[
'detector']
959 except Exception
as e:
960 raise ValueError(
"Failure to find valid detectorNum value for Dataset %s: %s." %
963 detector = inputs[
'ccdExposure'].getDetector()
965 if self.config.doCrosstalk
is True:
968 if 'crosstalk' in inputs
and inputs[
'crosstalk']
is not None:
969 if not isinstance(inputs[
'crosstalk'], CrosstalkCalib):
970 inputs[
'crosstalk'] = CrosstalkCalib.fromTable(inputs[
'crosstalk'])
972 coeffVector = (self.config.crosstalk.crosstalkValues
973 if self.config.crosstalk.useConfigCoefficients
else None)
974 crosstalkCalib =
CrosstalkCalib().fromDetector(detector, coeffVector=coeffVector)
975 inputs[
'crosstalk'] = crosstalkCalib
976 if inputs[
'crosstalk'].interChip
and len(inputs[
'crosstalk'].interChip) > 0:
977 if 'crosstalkSources' not in inputs:
978 self.log.warning(
"No crosstalkSources found for chip with interChip terms!")
981 if 'linearizer' in inputs:
982 if isinstance(inputs[
'linearizer'], dict):
984 linearizer.fromYaml(inputs[
'linearizer'])
985 self.log.warning(
"Dictionary linearizers will be deprecated in DM-28741.")
986 elif isinstance(inputs[
'linearizer'], numpy.ndarray):
990 self.log.warning(
"Bare lookup table linearizers will be deprecated in DM-28741.")
992 linearizer = inputs[
'linearizer']
993 linearizer.log = self.log
994 inputs[
'linearizer'] = linearizer
997 self.log.warning(
"Constructing linearizer from cameraGeom information.")
999 if self.config.doDefect
is True:
1000 if "defects" in inputs
and inputs[
'defects']
is not None:
1004 if not isinstance(inputs[
"defects"], Defects):
1005 inputs[
"defects"] = Defects.fromTable(inputs[
"defects"])
1009 if self.config.doBrighterFatter:
1010 brighterFatterKernel = inputs.pop(
'newBFKernel',
None)
1011 if brighterFatterKernel
is None:
1012 brighterFatterKernel = inputs.get(
'bfKernel',
None)
1014 if brighterFatterKernel
is not None and not isinstance(brighterFatterKernel, numpy.ndarray):
1016 detName = detector.getName()
1017 level = brighterFatterKernel.level
1020 inputs[
'bfGains'] = brighterFatterKernel.gain
1021 if self.config.brighterFatterLevel ==
'DETECTOR':
1022 if level ==
'DETECTOR':
1023 if detName
in brighterFatterKernel.detKernels:
1024 inputs[
'bfKernel'] = brighterFatterKernel.detKernels[detName]
1026 raise RuntimeError(
"Failed to extract kernel from new-style BF kernel.")
1027 elif level ==
'AMP':
1028 self.log.warning(
"Making DETECTOR level kernel from AMP based brighter "
1030 brighterFatterKernel.makeDetectorKernelFromAmpwiseKernels(detName)
1031 inputs[
'bfKernel'] = brighterFatterKernel.detKernels[detName]
1032 elif self.config.brighterFatterLevel ==
'AMP':
1033 raise NotImplementedError(
"Per-amplifier brighter-fatter correction not implemented")
1035 if self.config.doFringe
is True and self.fringe.checkFilter(inputs[
'ccdExposure']):
1036 expId = inputs[
'ccdExposure'].info.id
1037 inputs[
'fringes'] = self.fringe.loadFringes(inputs[
'fringes'],
1039 assembler=self.assembleCcd
1040 if self.config.doAssembleIsrExposures
else None)
1042 inputs[
'fringes'] = pipeBase.Struct(fringes=
None)
1044 if self.config.doStrayLight
is True and self.strayLight.checkFilter(inputs[
'ccdExposure']):
1045 if 'strayLightData' not in inputs:
1046 inputs[
'strayLightData'] =
None
1048 outputs = self.
run(**inputs)
1049 butlerQC.put(outputs, outputRefs)
1052 def run(self, ccdExposure, *, camera=None, bias=None, linearizer=None,
1053 crosstalk=None, crosstalkSources=None,
1054 dark=None, flat=None, ptc=None, bfKernel=None, bfGains=None, defects=None,
1055 fringes=pipeBase.Struct(fringes=
None), opticsTransmission=
None, filterTransmission=
None,
1056 sensorTransmission=
None, atmosphereTransmission=
None,
1057 detectorNum=
None, strayLightData=
None, illumMaskedImage=
None,
1058 deferredChargeCalib=
None,
1060 """Perform instrument signature removal on an exposure.
1062 Steps included in the ISR processing,
in order performed, are:
1064 - saturation
and suspect pixel masking
1065 - overscan subtraction
1066 - CCD assembly of individual amplifiers
1068 - variance image construction
1069 - linearization of non-linear response
1071 - brighter-fatter correction
1074 - stray light subtraction
1076 - masking of known defects
and camera specific features
1077 - vignette calculation
1078 - appending transmission curve
and distortion model
1083 The raw exposure that
is to be run through ISR. The
1084 exposure
is modified by this method.
1086 The camera geometry
for this exposure. Required
if
1087 one
or more of ``ccdExposure``, ``bias``, ``dark``,
or
1088 ``flat`` does
not have an associated detector.
1090 Bias calibration frame.
1092 Functor
for linearization.
1094 Calibration
for crosstalk.
1095 crosstalkSources : `list`, optional
1096 List of possible crosstalk sources.
1098 Dark calibration frame.
1100 Flat calibration frame.
1102 Photon transfer curve dataset,
with, e.g., gains
1104 bfKernel : `numpy.ndarray`, optional
1105 Brighter-fatter kernel.
1106 bfGains : `dict` of `float`, optional
1107 Gains used to override the detector
's nominal gains for the
1108 brighter-fatter correction. A dict keyed by amplifier name for
1109 the detector
in question.
1112 fringes : `lsst.pipe.base.Struct`, optional
1113 Struct containing the fringe correction data,
with
1119 random seed derived
from the ``ccdExposureId``
for random
1120 number generator (`numpy.uint32`)
1122 A ``TransmissionCurve`` that represents the throughput of the,
1123 optics, to be evaluated
in focal-plane coordinates.
1125 A ``TransmissionCurve`` that represents the throughput of the
1126 filter itself, to be evaluated
in focal-plane coordinates.
1128 A ``TransmissionCurve`` that represents the throughput of the
1129 sensor itself, to be evaluated
in post-assembly trimmed detector
1132 A ``TransmissionCurve`` that represents the throughput of the
1133 atmosphere, assumed to be spatially constant.
1134 detectorNum : `int`, optional
1135 The integer number
for the detector to process.
1136 strayLightData : `object`, optional
1137 Opaque object containing calibration information
for stray-light
1138 correction. If `
None`, no correction will be performed.
1140 Illumination correction image.
1144 result : `lsst.pipe.base.Struct`
1145 Result struct
with component:
1148 The fully ISR corrected exposure.
1153 Thumbnail image of the exposure after overscan subtraction.
1156 Thumbnail image of the exposure after flat-field correction.
1158 ``outputStatistics``
1159 Values of the additional statistics calculated.
1164 Raised
if a configuration option
is set to `
True`, but the
1165 required calibration data has
not been specified.
1169 The current processed exposure can be viewed by setting the
1170 appropriate `lsstDebug` entries
in the ``debug.display``
1171 dictionary. The names of these entries correspond to some of
1172 the `IsrTaskConfig` Boolean options,
with the value denoting the
1173 frame to use. The exposure
is shown inside the matching
1174 option check
and after the processing of that step has
1175 finished. The steps
with debug points are:
1186 In addition, setting the ``postISRCCD`` entry displays the
1187 exposure after all ISR processing has finished.
1190 ccdExposure = self.ensureExposure(ccdExposure, camera, detectorNum)
1195 ccd = ccdExposure.getDetector()
1196 filterLabel = ccdExposure.getFilter()
1197 physicalFilter = isrFunctions.getPhysicalFilter(filterLabel, self.log)
1200 assert not self.config.doAssembleCcd,
"You need a Detector to run assembleCcd."
1201 ccd = [
FakeAmp(ccdExposure, self.config)]
1204 if self.config.doBias
and bias
is None:
1205 raise RuntimeError(
"Must supply a bias exposure if config.doBias=True.")
1207 raise RuntimeError(
"Must supply a linearizer if config.doLinearize=True for this detector.")
1208 if self.config.doBrighterFatter
and bfKernel
is None:
1209 raise RuntimeError(
"Must supply a kernel if config.doBrighterFatter=True.")
1210 if self.config.doDark
and dark
is None:
1211 raise RuntimeError(
"Must supply a dark exposure if config.doDark=True.")
1212 if self.config.doFlat
and flat
is None:
1213 raise RuntimeError(
"Must supply a flat exposure if config.doFlat=True.")
1214 if self.config.doDefect
and defects
is None:
1215 raise RuntimeError(
"Must supply defects if config.doDefect=True.")
1216 if (self.config.doFringe
and physicalFilter
in self.fringe.config.filters
1217 and fringes.fringes
is None):
1222 raise RuntimeError(
"Must supply fringe exposure as a pipeBase.Struct.")
1223 if (self.config.doIlluminationCorrection
and physicalFilter
in self.config.illumFilters
1224 and illumMaskedImage
is None):
1225 raise RuntimeError(
"Must supply an illumcor if config.doIlluminationCorrection=True.")
1226 if (self.config.doDeferredCharge
and deferredChargeCalib
is None):
1227 raise RuntimeError(
"Must supply a deferred charge calibration if config.doDeferredCharge=True.")
1230 if self.config.doConvertIntToFloat:
1231 self.log.info(
"Converting exposure to floating point values.")
1234 if self.config.doBias
and self.config.doBiasBeforeOverscan:
1235 self.log.info(
"Applying bias correction.")
1236 isrFunctions.biasCorrection(ccdExposure.getMaskedImage(), bias.getMaskedImage(),
1237 trimToFit=self.config.doTrimToMatchCalib)
1245 if ccdExposure.getBBox().contains(amp.getBBox()):
1250 if self.config.doOverscan
and not badAmp:
1253 self.log.debug(
"Corrected overscan for amplifier %s.", amp.getName())
1254 if overscanResults
is not None and \
1255 self.config.qa
is not None and self.config.qa.saveStats
is True:
1257 self.metadata[f
"FIT MEDIAN {amp.getName()}"] = overscanResults.overscanMean
1258 self.metadata[f
"FIT STDEV {amp.getName()}"] = overscanResults.overscanSigma
1259 self.log.debug(
" Overscan stats for amplifer %s: %f +/- %f",
1260 amp.getName(), overscanResults.overscanMean,
1261 overscanResults.overscanSigma)
1263 self.metadata[f
"RESIDUAL MEDIAN {amp.getName()}"] = overscanResults.residualMean
1264 self.metadata[f
"RESIDUAL STDEV {amp.getName()}"] = overscanResults.residualSigma
1265 self.log.debug(
" Overscan stats for amplifer %s after correction: %f +/- %f",
1266 amp.getName(), overscanResults.residualMean,
1267 overscanResults.residualSigma)
1269 ccdExposure.getMetadata().
set(
'OVERSCAN',
"Overscan corrected")
1272 self.log.warning(
"Amplifier %s is bad.", amp.getName())
1273 overscanResults =
None
1275 overscans.append(overscanResults
if overscanResults
is not None else None)
1277 self.log.info(
"Skipped OSCAN for %s.", amp.getName())
1279 if self.config.doDeferredCharge:
1280 self.log.info(
"Applying deferred charge/CTI correction.")
1281 self.deferredChargeCorrection.run(ccdExposure, deferredChargeCalib)
1282 self.
debugView(ccdExposure,
"doDeferredCharge")
1284 if self.config.doCrosstalk
and self.config.doCrosstalkBeforeAssemble:
1285 self.log.info(
"Applying crosstalk correction.")
1286 self.crosstalk.run(ccdExposure, crosstalk=crosstalk,
1287 crosstalkSources=crosstalkSources, camera=camera)
1288 self.
debugView(ccdExposure,
"doCrosstalk")
1290 if self.config.doAssembleCcd:
1291 self.log.info(
"Assembling CCD from amplifiers.")
1292 ccdExposure = self.assembleCcd.assembleCcd(ccdExposure)
1294 if self.config.expectWcs
and not ccdExposure.getWcs():
1295 self.log.warning(
"No WCS found in input exposure.")
1296 self.
debugView(ccdExposure,
"doAssembleCcd")
1299 if self.config.qa.doThumbnailOss:
1300 ossThumb = isrQa.makeThumbnail(ccdExposure, isrQaConfig=self.config.qa)
1302 if self.config.doBias
and not self.config.doBiasBeforeOverscan:
1303 self.log.info(
"Applying bias correction.")
1304 isrFunctions.biasCorrection(ccdExposure.getMaskedImage(), bias.getMaskedImage(),
1305 trimToFit=self.config.doTrimToMatchCalib)
1308 if self.config.doVariance:
1309 for amp, overscanResults
in zip(ccd, overscans):
1310 if ccdExposure.getBBox().contains(amp.getBBox()):
1311 self.log.debug(
"Constructing variance map for amplifer %s.", amp.getName())
1312 ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
1313 if overscanResults
is not None:
1315 overscanImage=overscanResults.overscanImage,
1321 if self.config.qa
is not None and self.config.qa.saveStats
is True:
1323 afwMath.MEDIAN | afwMath.STDEVCLIP)
1324 self.metadata[f
"ISR VARIANCE {amp.getName()} MEDIAN"] = \
1325 qaStats.getValue(afwMath.MEDIAN)
1326 self.metadata[f
"ISR VARIANCE {amp.getName()} STDEV"] = \
1327 qaStats.getValue(afwMath.STDEVCLIP)
1328 self.log.debug(
" Variance stats for amplifer %s: %f +/- %f.",
1329 amp.getName(), qaStats.getValue(afwMath.MEDIAN),
1330 qaStats.getValue(afwMath.STDEVCLIP))
1331 if self.config.maskNegativeVariance:
1335 self.log.info(
"Applying linearizer.")
1336 linearizer.applyLinearity(image=ccdExposure.getMaskedImage().getImage(),
1337 detector=ccd, log=self.log)
1339 if self.config.doCrosstalk
and not self.config.doCrosstalkBeforeAssemble:
1340 self.log.info(
"Applying crosstalk correction.")
1341 self.crosstalk.run(ccdExposure, crosstalk=crosstalk,
1342 crosstalkSources=crosstalkSources, isTrimmed=
True)
1343 self.
debugView(ccdExposure,
"doCrosstalk")
1348 if self.config.doDefect:
1349 self.log.info(
"Masking defects.")
1352 if self.config.numEdgeSuspect > 0:
1353 self.log.info(
"Masking edges as SUSPECT.")
1354 self.
maskEdges(ccdExposure, numEdgePixels=self.config.numEdgeSuspect,
1355 maskPlane=
"SUSPECT", level=self.config.edgeMaskLevel)
1357 if self.config.doNanMasking:
1358 self.log.info(
"Masking non-finite (NAN, inf) value pixels.")
1361 if self.config.doWidenSaturationTrails:
1362 self.log.info(
"Widening saturation trails.")
1363 isrFunctions.widenSaturationTrails(ccdExposure.getMaskedImage().getMask())
1365 if self.config.doCameraSpecificMasking:
1366 self.log.info(
"Masking regions for camera specific reasons.")
1367 self.masking.run(ccdExposure)
1369 if self.config.doBrighterFatter:
1379 interpExp = ccdExposure.clone()
1381 isrFunctions.interpolateFromMask(
1382 maskedImage=interpExp.getMaskedImage(),
1383 fwhm=self.config.fwhm,
1384 growSaturatedFootprints=self.config.growSaturationFootprintSize,
1385 maskNameList=
list(self.config.brighterFatterMaskListToInterpolate)
1387 bfExp = interpExp.clone()
1389 self.log.info(
"Applying brighter-fatter correction using kernel type %s / gains %s.",
1391 bfResults = isrFunctions.brighterFatterCorrection(bfExp, bfKernel,
1392 self.config.brighterFatterMaxIter,
1393 self.config.brighterFatterThreshold,
1394 self.config.brighterFatterApplyGain,
1396 if bfResults[1] == self.config.brighterFatterMaxIter:
1397 self.log.warning(
"Brighter-fatter correction did not converge, final difference %f.",
1400 self.log.info(
"Finished brighter-fatter correction in %d iterations.",
1402 image = ccdExposure.getMaskedImage().getImage()
1403 bfCorr = bfExp.getMaskedImage().getImage()
1404 bfCorr -= interpExp.getMaskedImage().getImage()
1413 self.log.info(
"Ensuring image edges are masked as EDGE to the brighter-fatter kernel size.")
1414 self.
maskEdges(ccdExposure, numEdgePixels=numpy.max(bfKernel.shape) // 2,
1417 if self.config.brighterFatterMaskGrowSize > 0:
1418 self.log.info(
"Growing masks to account for brighter-fatter kernel convolution.")
1419 for maskPlane
in self.config.brighterFatterMaskListToInterpolate:
1420 isrFunctions.growMasks(ccdExposure.getMask(),
1421 radius=self.config.brighterFatterMaskGrowSize,
1422 maskNameList=maskPlane,
1423 maskValue=maskPlane)
1425 self.
debugView(ccdExposure,
"doBrighterFatter")
1427 if self.config.doDark:
1428 self.log.info(
"Applying dark correction.")
1432 if self.config.doFringe
and not self.config.fringeAfterFlat:
1433 self.log.info(
"Applying fringe correction before flat.")
1434 self.fringe.run(ccdExposure, **fringes.getDict())
1437 if self.config.doStrayLight
and self.strayLight.check(ccdExposure):
1438 self.log.info(
"Checking strayLight correction.")
1439 self.strayLight.run(ccdExposure, strayLightData)
1440 self.
debugView(ccdExposure,
"doStrayLight")
1442 if self.config.doFlat:
1443 self.log.info(
"Applying flat correction.")
1447 if self.config.doApplyGains:
1448 self.log.info(
"Applying gain correction instead of flat.")
1449 if self.config.usePtcGains:
1450 self.log.info(
"Using gains from the Photon Transfer Curve.")
1451 isrFunctions.applyGains(ccdExposure, self.config.normalizeGains,
1454 isrFunctions.applyGains(ccdExposure, self.config.normalizeGains)
1456 if self.config.doFringe
and self.config.fringeAfterFlat:
1457 self.log.info(
"Applying fringe correction after flat.")
1458 self.fringe.run(ccdExposure, **fringes.getDict())
1460 if self.config.doVignette:
1461 if self.config.doMaskVignettePolygon:
1462 self.log.info(
"Constructing, attaching, and masking vignette polygon.")
1464 self.log.info(
"Constructing and attaching vignette polygon.")
1466 exposure=ccdExposure, doUpdateMask=self.config.doMaskVignettePolygon,
1467 vignetteValue=self.config.vignetteValue, log=self.log)
1469 if self.config.doAttachTransmissionCurve:
1470 self.log.info(
"Adding transmission curves.")
1471 isrFunctions.attachTransmissionCurve(ccdExposure, opticsTransmission=opticsTransmission,
1472 filterTransmission=filterTransmission,
1473 sensorTransmission=sensorTransmission,
1474 atmosphereTransmission=atmosphereTransmission)
1476 flattenedThumb =
None
1477 if self.config.qa.doThumbnailFlattened:
1478 flattenedThumb = isrQa.makeThumbnail(ccdExposure, isrQaConfig=self.config.qa)
1480 if self.config.doIlluminationCorrection
and physicalFilter
in self.config.illumFilters:
1481 self.log.info(
"Performing illumination correction.")
1482 isrFunctions.illuminationCorrection(ccdExposure.getMaskedImage(),
1483 illumMaskedImage, illumScale=self.config.illumScale,
1484 trimToFit=self.config.doTrimToMatchCalib)
1487 if self.config.doSaveInterpPixels:
1488 preInterpExp = ccdExposure.clone()
1503 if self.config.doSetBadRegions:
1504 badPixelCount, badPixelValue = isrFunctions.setBadRegions(ccdExposure)
1505 if badPixelCount > 0:
1506 self.log.info(
"Set %d BAD pixels to %f.", badPixelCount, badPixelValue)
1508 if self.config.doInterpolate:
1509 self.log.info(
"Interpolating masked pixels.")
1510 isrFunctions.interpolateFromMask(
1511 maskedImage=ccdExposure.getMaskedImage(),
1512 fwhm=self.config.fwhm,
1513 growSaturatedFootprints=self.config.growSaturationFootprintSize,
1514 maskNameList=
list(self.config.maskListToInterpolate)
1520 if self.config.doAmpOffset:
1521 self.log.info(
"Correcting amp offsets.")
1522 self.ampOffset.run(ccdExposure)
1524 if self.config.doMeasureBackground:
1525 self.log.info(
"Measuring background level.")
1528 if self.config.qa
is not None and self.config.qa.saveStats
is True:
1530 ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
1532 afwMath.MEDIAN | afwMath.STDEVCLIP)
1533 self.metadata[f
"ISR BACKGROUND {amp.getName()} MEDIAN"] = qaStats.getValue(afwMath.MEDIAN)
1534 self.metadata[f
"ISR BACKGROUND {amp.getName()} STDEV"] = \
1535 qaStats.getValue(afwMath.STDEVCLIP)
1536 self.log.debug(
" Background stats for amplifer %s: %f +/- %f",
1537 amp.getName(), qaStats.getValue(afwMath.MEDIAN),
1538 qaStats.getValue(afwMath.STDEVCLIP))
1541 outputStatistics =
None
1542 if self.config.doCalculateStatistics:
1543 outputStatistics = self.isrStats.run(ccdExposure, overscanResults=overscans,
1546 self.
debugView(ccdExposure,
"postISRCCD")
1548 return pipeBase.Struct(
1549 exposure=ccdExposure,
1551 flattenedThumb=flattenedThumb,
1553 preInterpExposure=preInterpExp,
1554 outputExposure=ccdExposure,
1555 outputOssThumbnail=ossThumb,
1556 outputFlattenedThumbnail=flattenedThumb,
1557 outputStatistics=outputStatistics,
1561 """Ensure that the data returned by Butler is a fully constructed exp.
1563 ISR requires exposure-level image data for historical reasons, so
if we
1564 did
not recieve that
from Butler, construct it
from what we have,
1565 modifying the input
in place.
1570 The input data structure obtained
from Butler.
1572 `lsst.afw.image.DecoratedImageU`,
1573 or `lsst.afw.image.ImageF`
1574 camera : `lsst.afw.cameraGeom.camera`, optional
1575 The camera associated
with the image. Used to find the appropriate
1576 detector
if detector
is not already set.
1577 detectorNum : `int`, optional
1578 The detector
in the camera to attach,
if the detector
is not
1584 The re-constructed exposure,
with appropriate detector parameters.
1589 Raised
if the input data cannot be used to construct an exposure.
1591 if isinstance(inputExp, afwImage.DecoratedImageU):
1593 elif isinstance(inputExp, afwImage.ImageF):
1595 elif isinstance(inputExp, afwImage.MaskedImageF):
1599 elif inputExp
is None:
1603 raise TypeError(
"Input Exposure is not known type in isrTask.ensureExposure: %s." %
1606 if inputExp.getDetector()
is None:
1607 if camera
is None or detectorNum
is None:
1608 raise RuntimeError(
'Must supply both a camera and detector number when using exposures '
1609 'without a detector set.')
1610 inputExp.setDetector(camera[detectorNum])
1615 """Convert exposure image from uint16 to float.
1617 If the exposure does not need to be converted, the input
is
1618 immediately returned. For exposures that are converted to use
1619 floating point pixels, the variance
is set to unity
and the
1625 The raw exposure to be converted.
1630 The input ``exposure``, converted to floating point pixels.
1635 Raised
if the exposure type cannot be converted to float.
1638 if isinstance(exposure, afwImage.ExposureF):
1640 self.log.debug(
"Exposure already of type float.")
1642 if not hasattr(exposure,
"convertF"):
1643 raise RuntimeError(
"Unable to convert exposure (%s) to float." %
type(exposure))
1645 newexposure = exposure.convertF()
1646 newexposure.variance[:] = 1
1647 newexposure.mask[:] = 0x0
1652 """Identify bad amplifiers, saturated and suspect pixels.
1657 Input exposure to be masked.
1659 Catalog of parameters defining the amplifier on this
1662 List of defects. Used to determine if the entire
1668 If this
is true, the entire amplifier area
is covered by
1669 defects
and unusable.
1672 maskedImage = ccdExposure.getMaskedImage()
1679 if defects
is not None:
1680 badAmp = bool(sum([v.getBBox().contains(amp.getBBox())
for v
in defects]))
1686 dataView = afwImage.MaskedImageF(maskedImage, amp.getRawBBox(),
1688 maskView = dataView.getMask()
1689 maskView |= maskView.getPlaneBitMask(
"BAD")
1697 if self.config.doSaturation
and not badAmp:
1698 limits.update({self.config.saturatedMaskName: amp.getSaturation()})
1699 if self.config.doSuspect
and not badAmp:
1700 limits.update({self.config.suspectMaskName: amp.getSuspectLevel()})
1701 if math.isfinite(self.config.saturation):
1702 limits.update({self.config.saturatedMaskName: self.config.saturation})
1704 for maskName, maskThreshold
in limits.items():
1705 if not math.isnan(maskThreshold):
1706 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
1707 isrFunctions.makeThresholdMask(
1708 maskedImage=dataView,
1709 threshold=maskThreshold,
1716 maskView =
afwImage.Mask(maskedImage.getMask(), amp.getRawDataBBox(),
1718 maskVal = maskView.getPlaneBitMask([self.config.saturatedMaskName,
1719 self.config.suspectMaskName])
1720 if numpy.all(maskView.getArray() & maskVal > 0):
1722 maskView |= maskView.getPlaneBitMask(
"BAD")
1727 """Apply overscan correction in place.
1729 This method does initial pixel rejection of the overscan
1730 region. The overscan can also be optionally segmented to
1731 allow for discontinuous overscan responses to be fit
1732 separately. The actual overscan subtraction
is performed by
1733 the `lsst.ip.isr.overscan.OverscanTask`, which
is called here
1734 after the amplifier
is preprocessed.
1739 Exposure to have overscan correction performed.
1740 amp : `lsst.afw.cameraGeom.Amplifer`
1741 The amplifier to consider
while correcting the overscan.
1745 overscanResults : `lsst.pipe.base.Struct`
1746 Result struct
with components:
1749 Value
or fit subtracted
from the amplifier image data.
1752 Value
or fit subtracted
from the overscan image data.
1755 Image of the overscan region
with the overscan
1756 correction applied. This quantity
is used to estimate
1757 the amplifier read noise empirically.
1762 Median overscan fit value. (`float`)
1764 Clipped standard deviation of the overscan after
1765 correction. (`float`)
1770 Raised
if the ``amp`` does
not contain raw pixel information.
1774 lsst.ip.isr.overscan.OverscanTask
1777 if amp.getRawHorizontalOverscanBBox().isEmpty():
1778 self.log.info(
"ISR_OSCAN: No overscan region. Not performing overscan correction.")
1782 overscanResults = self.overscan.run(ccdExposure, amp)
1784 metadata = ccdExposure.getMetadata()
1785 ampNum = amp.getName()
1786 metadata[f
"ISR_OSCAN_LEVEL{ampNum}"] = overscanResults.overscanMean
1787 metadata[f
"ISR_OSCAN_SIGMA{ampNum}"] = overscanResults.overscanSigma
1789 return overscanResults
1792 """Set the variance plane using the gain and read noise
1794 The read noise is calculated
from the ``overscanImage``
if the
1795 ``doEmpiricalReadNoise`` option
is set
in the configuration; otherwise
1796 the value
from the amplifier data
is used.
1801 Exposure to process.
1803 Amplifier detector data.
1805 Image of overscan, required only
for empirical read noise.
1807 PTC dataset containing the gains
and read noise.
1812 Raised
if either ``usePtcGains`` of ``usePtcReadNoise``
1813 are ``
True``, but ptcDataset
is not provided.
1815 Raised
if ```doEmpiricalReadNoise``
is ``
True`` but
1816 ``overscanImage``
is ``
None``.
1820 lsst.ip.isr.isrFunctions.updateVariance
1822 maskPlanes = [self.config.saturatedMaskName, self.config.suspectMaskName]
1823 if self.config.usePtcGains:
1824 if ptcDataset
is None:
1825 raise RuntimeError(
"No ptcDataset provided to use PTC gains.")
1827 gain = ptcDataset.gain[amp.getName()]
1828 self.log.info(
"Using gain from Photon Transfer Curve.")
1830 gain = amp.getGain()
1832 if math.isnan(gain):
1834 self.log.warning(
"Gain set to NAN! Updating to 1.0 to generate Poisson variance.")
1837 self.log.warning(
"Gain for amp %s == %g <= 0; setting to %f.",
1838 amp.getName(), gain, patchedGain)
1841 if self.config.doEmpiricalReadNoise
and overscanImage
is None:
1842 badPixels = isrFunctions.countMaskedPixels(ampExposure.getMaskedImage(),
1843 [self.config.saturatedMaskName,
1844 self.config.suspectMaskName,
1846 allPixels = ampExposure.getWidth() * ampExposure.getHeight()
1847 if allPixels == badPixels:
1849 self.log.info(
"Skipping empirical read noise for amp %s. No good pixels.",
1852 raise RuntimeError(
"Overscan is none for EmpiricalReadNoise.")
1854 if self.config.doEmpiricalReadNoise
and overscanImage
is not None:
1856 stats.setAndMask(overscanImage.mask.getPlaneBitMask(maskPlanes))
1858 afwMath.STDEVCLIP, stats).getValue()
1859 self.log.info(
"Calculated empirical read noise for amp %s: %f.",
1860 amp.getName(), readNoise)
1861 elif self.config.usePtcReadNoise:
1862 if ptcDataset
is None:
1863 raise RuntimeError(
"No ptcDataset provided to use PTC readnoise.")
1865 readNoise = ptcDataset.noise[amp.getName()]
1866 self.log.info(
"Using read noise from Photon Transfer Curve.")
1868 readNoise = amp.getReadNoise()
1870 isrFunctions.updateVariance(
1871 maskedImage=ampExposure.getMaskedImage(),
1873 readNoise=readNoise,
1877 """Identify and mask pixels with negative variance values.
1882 Exposure to process.
1886 lsst.ip.isr.isrFunctions.updateVariance
1888 maskPlane = exposure.getMask().getPlaneBitMask(self.config.negativeVarianceMaskName)
1889 bad = numpy.where(exposure.getVariance().getArray() <= 0.0)
1890 exposure.mask.array[bad] |= maskPlane
1893 """Apply dark correction in place.
1898 Exposure to process.
1900 Dark exposure of the same size as ``exposure``.
1901 invert : `Bool`, optional
1902 If
True, re-add the dark to an already corrected image.
1907 Raised
if either ``exposure``
or ``darkExposure`` do
not
1908 have their dark time defined.
1912 lsst.ip.isr.isrFunctions.darkCorrection
1914 expScale = exposure.getInfo().getVisitInfo().getDarkTime()
1915 if math.isnan(expScale):
1916 raise RuntimeError(
"Exposure darktime is NAN.")
1917 if darkExposure.getInfo().getVisitInfo()
is not None \
1918 and not math.isnan(darkExposure.getInfo().getVisitInfo().getDarkTime()):
1919 darkScale = darkExposure.getInfo().getVisitInfo().getDarkTime()
1923 self.log.warning(
"darkExposure.getInfo().getVisitInfo() does not exist. Using darkScale = 1.0.")
1926 isrFunctions.darkCorrection(
1927 maskedImage=exposure.getMaskedImage(),
1928 darkMaskedImage=darkExposure.getMaskedImage(),
1930 darkScale=darkScale,
1932 trimToFit=self.config.doTrimToMatchCalib
1936 """Check if linearization is needed for the detector cameraGeom.
1938 Checks config.doLinearize and the linearity type of the first
1944 Detector to get linearity type
from.
1948 doLinearize : `Bool`
1949 If
True, linearization should be performed.
1951 return self.config.doLinearize
and \
1952 detector.getAmplifiers()[0].getLinearityType() != NullLinearityType
1955 """Apply flat correction in place.
1960 Exposure to process.
1962 Flat exposure of the same size as ``exposure``.
1963 invert : `Bool`, optional
1964 If
True, unflatten an already flattened image.
1968 lsst.ip.isr.isrFunctions.flatCorrection
1970 isrFunctions.flatCorrection(
1971 maskedImage=exposure.getMaskedImage(),
1972 flatMaskedImage=flatExposure.getMaskedImage(),
1973 scalingType=self.config.flatScalingType,
1974 userScale=self.config.flatUserScale,
1976 trimToFit=self.config.doTrimToMatchCalib
1980 """Detect and mask saturated pixels in config.saturatedMaskName.
1985 Exposure to process. Only the amplifier DataSec is processed.
1987 Amplifier detector data.
1991 lsst.ip.isr.isrFunctions.makeThresholdMask
1993 if not math.isnan(amp.getSaturation()):
1994 maskedImage = exposure.getMaskedImage()
1995 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
1996 isrFunctions.makeThresholdMask(
1997 maskedImage=dataView,
1998 threshold=amp.getSaturation(),
2000 maskName=self.config.saturatedMaskName,
2004 """Interpolate over saturated pixels, in place.
2006 This method should be called after `saturationDetection`, to
2007 ensure that the saturated pixels have been identified in the
2008 SAT mask. It should also be called after `assembleCcd`, since
2009 saturated regions may cross amplifier boundaries.
2014 Exposure to process.
2018 lsst.ip.isr.isrTask.saturationDetection
2019 lsst.ip.isr.isrFunctions.interpolateFromMask
2021 isrFunctions.interpolateFromMask(
2022 maskedImage=exposure.getMaskedImage(),
2023 fwhm=self.config.fwhm,
2024 growSaturatedFootprints=self.config.growSaturationFootprintSize,
2025 maskNameList=list(self.config.saturatedMaskName),
2029 """Detect and mask suspect pixels in config.suspectMaskName.
2034 Exposure to process. Only the amplifier DataSec is processed.
2036 Amplifier detector data.
2040 lsst.ip.isr.isrFunctions.makeThresholdMask
2044 Suspect pixels are pixels whose value
is greater than
2045 amp.getSuspectLevel(). This
is intended to indicate pixels that may be
2046 affected by unknown systematics;
for example
if non-linearity
2047 corrections above a certain level are unstable then that would be a
2048 useful value
for suspectLevel. A value of `nan` indicates that no such
2049 level exists
and no pixels are to be masked
as suspicious.
2051 suspectLevel = amp.getSuspectLevel()
2052 if math.isnan(suspectLevel):
2055 maskedImage = exposure.getMaskedImage()
2056 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
2057 isrFunctions.makeThresholdMask(
2058 maskedImage=dataView,
2059 threshold=suspectLevel,
2061 maskName=self.config.suspectMaskName,
2065 """Mask defects using mask plane "BAD", in place.
2070 Exposure to process.
2071 defectBaseList : defect-type
2077 Call this after CCD assembly, since defects may cross amplifier
2080 maskedImage = exposure.getMaskedImage()
2081 if not isinstance(defectBaseList, Defects):
2083 defectList =
Defects(defectBaseList)
2085 defectList = defectBaseList
2086 defectList.maskPixels(maskedImage, maskName=
"BAD")
2088 def maskEdges(self, exposure, numEdgePixels=0, maskPlane="SUSPECT", level='DETECTOR'):
2089 """Mask edge pixels with applicable mask plane.
2094 Exposure to process.
2095 numEdgePixels : `int`, optional
2096 Number of edge pixels to mask.
2097 maskPlane : `str`, optional
2098 Mask plane name to use.
2099 level : `str`, optional
2100 Level at which to mask edges.
2102 maskedImage = exposure.getMaskedImage()
2103 maskBitMask = maskedImage.getMask().getPlaneBitMask(maskPlane)
2105 if numEdgePixels > 0:
2106 if level ==
'DETECTOR':
2107 boxes = [maskedImage.getBBox()]
2108 elif level ==
'AMP':
2109 boxes = [amp.getBBox()
for amp
in exposure.getDetector()]
2114 subImage = maskedImage[box]
2115 box.grow(-numEdgePixels)
2117 SourceDetectionTask.setEdgeBits(
2123 """Mask and interpolate defects using mask plane "BAD", in place.
2128 Exposure to process.
2129 defectBaseList : defects-like
2130 List of defects to mask and interpolate. Can be
2135 lsst.ip.isr.isrTask.maskDefect
2138 self.maskEdges(exposure, numEdgePixels=self.config.numEdgeSuspect,
2139 maskPlane="SUSPECT", level=self.config.edgeMaskLevel)
2140 isrFunctions.interpolateFromMask(
2141 maskedImage=exposure.getMaskedImage(),
2142 fwhm=self.config.fwhm,
2143 growSaturatedFootprints=0,
2144 maskNameList=[
"BAD"],
2148 """Mask NaNs using mask plane "UNMASKEDNAN", in place.
2153 Exposure to process.
2157 We mask over all non-finite values (NaN, inf), including those
2158 that are masked with other bits (because those may
or may
not be
2159 interpolated over later,
and we want to remove all NaN/infs).
2160 Despite this behaviour, the
"UNMASKEDNAN" mask plane
is used to
2161 preserve the historical name.
2163 maskedImage = exposure.getMaskedImage()
2166 maskedImage.getMask().addMaskPlane(
"UNMASKEDNAN")
2167 maskVal = maskedImage.getMask().getPlaneBitMask(
"UNMASKEDNAN")
2168 numNans = maskNans(maskedImage, maskVal)
2169 self.metadata[
"NUMNANS"] = numNans
2171 self.log.warning(
"There were %d unmasked NaNs.", numNans)
2174 """"Mask and interpolate NaN/infs using mask plane "UNMASKEDNAN",
2180 Exposure to process.
2184 lsst.ip.isr.isrTask.maskNan
2187 isrFunctions.interpolateFromMask(
2188 maskedImage=exposure.getMaskedImage(),
2189 fwhm=self.config.fwhm,
2190 growSaturatedFootprints=0,
2191 maskNameList=["UNMASKEDNAN"],
2195 """Measure the image background in subgrids, for quality control.
2200 Exposure to process.
2202 Configuration object containing parameters on which background
2203 statistics and subgrids to use.
2205 if IsrQaConfig
is not None:
2207 IsrQaConfig.flatness.nIter)
2208 maskVal = exposure.getMaskedImage().getMask().getPlaneBitMask([
"BAD",
"SAT",
"DETECTED"])
2209 statsControl.setAndMask(maskVal)
2210 maskedImage = exposure.getMaskedImage()
2212 skyLevel = stats.getValue(afwMath.MEDIAN)
2213 skySigma = stats.getValue(afwMath.STDEVCLIP)
2214 self.log.info(
"Flattened sky level: %f +/- %f.", skyLevel, skySigma)
2215 metadata = exposure.getMetadata()
2216 metadata[
"SKYLEVEL"] = skyLevel
2217 metadata[
"SKYSIGMA"] = skySigma
2220 stat = afwMath.MEANCLIP
if IsrQaConfig.flatness.doClip
else afwMath.MEAN
2221 meshXHalf = int(IsrQaConfig.flatness.meshX/2.)
2222 meshYHalf = int(IsrQaConfig.flatness.meshY/2.)
2223 nX = int((exposure.getWidth() + meshXHalf) / IsrQaConfig.flatness.meshX)
2224 nY = int((exposure.getHeight() + meshYHalf) / IsrQaConfig.flatness.meshY)
2225 skyLevels = numpy.zeros((nX, nY))
2228 yc = meshYHalf + j * IsrQaConfig.flatness.meshY
2230 xc = meshXHalf + i * IsrQaConfig.flatness.meshX
2232 xLLC = xc - meshXHalf
2233 yLLC = yc - meshYHalf
2234 xURC = xc + meshXHalf - 1
2235 yURC = yc + meshYHalf - 1
2238 miMesh = maskedImage.Factory(exposure.getMaskedImage(), bbox, afwImage.LOCAL)
2242 good = numpy.where(numpy.isfinite(skyLevels))
2243 skyMedian = numpy.median(skyLevels[good])
2244 flatness = (skyLevels[good] - skyMedian) / skyMedian
2245 flatness_rms = numpy.std(flatness)
2246 flatness_pp = flatness.max() - flatness.min()
if len(flatness) > 0
else numpy.nan
2248 self.log.info(
"Measuring sky levels in %dx%d grids: %f.", nX, nY, skyMedian)
2249 self.log.info(
"Sky flatness in %dx%d grids - pp: %f rms: %f.",
2250 nX, nY, flatness_pp, flatness_rms)
2252 metadata[
"FLATNESS_PP"] = float(flatness_pp)
2253 metadata[
"FLATNESS_RMS"] = float(flatness_rms)
2254 metadata[
"FLATNESS_NGRIDS"] =
'%dx%d' % (nX, nY)
2255 metadata[
"FLATNESS_MESHX"] = IsrQaConfig.flatness.meshX
2256 metadata[
"FLATNESS_MESHY"] = IsrQaConfig.flatness.meshY
2259 """Set an approximate magnitude zero point for the exposure.
2264 Exposure to process.
2266 filterLabel = exposure.getFilter()
2267 physicalFilter = isrFunctions.getPhysicalFilter(filterLabel, self.log)
2269 if physicalFilter
in self.config.fluxMag0T1:
2270 fluxMag0 = self.config.fluxMag0T1[physicalFilter]
2272 self.log.warning(
"No rough magnitude zero point defined for filter %s.", physicalFilter)
2273 fluxMag0 = self.config.defaultFluxMag0T1
2275 expTime = exposure.getInfo().getVisitInfo().getExposureTime()
2277 self.log.warning(
"Non-positive exposure time; skipping rough zero point.")
2280 self.log.info(
"Setting rough magnitude zero point for filter %s: %f",
2281 physicalFilter, 2.5*math.log10(fluxMag0*expTime))
2286 """Context manager that applies and removes flats and darks,
2287 if the task
is configured to apply them.
2292 Exposure to process.
2294 Flat exposure the same size
as ``exp``.
2296 Dark exposure the same size
as ``exp``.
2301 The flat
and dark corrected exposure.
2303 if self.config.doDark
and dark
is not None:
2305 if self.config.doFlat:
2310 if self.config.doFlat:
2312 if self.config.doDark
and dark
is not None:
2316 """Utility function to examine ISR exposure at different stages.
2323 State of processing to view.
2325 frame = getDebugFrame(self._display, stepname)
2327 display = getDisplay(frame)
2328 display.scale(
'asinh',
'zscale')
2329 display.mtv(exposure)
2330 prompt =
"Press Enter to continue [c]... "
2332 ans = input(prompt).lower()
2333 if ans
in (
"",
"c",):
2338 """A Detector-like object that supports returning gain and saturation level
2340 This is used when the input exposure does
not have a detector.
2345 Exposure to generate a fake amplifier
for.
2346 config : `lsst.ip.isr.isrTaskConfig`
2347 Configuration to apply to the fake amplifier.
2351 self.
_bbox = exposure.getBBox(afwImage.LOCAL)
2353 self.
_gain = config.gain
Geometry and electronic information about raw amplifier images.
An immutable representation of a camera.
A representation of a detector in a mosaic camera.
Encapsulate information about a bad portion of a detector.
A class to contain the data, WCS, and other information needed to describe an image of the sky.
A class to represent a 2-dimensional array of pixels.
Represent a 2-dimensional array of bitmask pixels.
A class to manipulate images, masks, and variance as a single object.
A spatially-varying transmission curve as a function of wavelength.
Pass parameters to a Statistics object.
An integer coordinate rectangle.
def getRawHorizontalOverscanBBox(self)
def getSuspectLevel(self)
_RawHorizontalOverscanBBox
def __init__(self, exposure, config)
doSaturationInterpolation
def __init__(self, *config=None)
def flatCorrection(self, exposure, flatExposure, invert=False)
def maskAndInterpolateNan(self, exposure)
def saturationInterpolation(self, exposure)
def maskNan(self, exposure)
def maskAmplifier(self, ccdExposure, amp, defects)
def debugView(self, exposure, stepname)
def ensureExposure(self, inputExp, camera=None, detectorNum=None)
def maskNegativeVariance(self, exposure)
def saturationDetection(self, exposure, amp)
def maskDefect(self, exposure, defectBaseList)
def runQuantum(self, butlerQC, inputRefs, outputRefs)
def maskEdges(self, exposure, numEdgePixels=0, maskPlane="SUSPECT", level='DETECTOR')
def overscanCorrection(self, ccdExposure, amp)
def run(self, ccdExposure, *camera=None, bias=None, linearizer=None, crosstalk=None, crosstalkSources=None, dark=None, flat=None, ptc=None, bfKernel=None, bfGains=None, defects=None, fringes=pipeBase.Struct(fringes=None), opticsTransmission=None, filterTransmission=None, sensorTransmission=None, atmosphereTransmission=None, detectorNum=None, strayLightData=None, illumMaskedImage=None, deferredChargeCalib=None)
def measureBackground(self, exposure, IsrQaConfig=None)
def roughZeroPoint(self, exposure)
def maskAndInterpolateDefects(self, exposure, defectBaseList)
def doLinearize(self, detector)
def flatContext(self, exp, flat, dark=None)
def convertIntToFloat(self, exposure)
def suspectDetection(self, exposure, amp)
def updateVariance(self, ampExposure, amp, overscanImage=None, ptcDataset=None)
def darkCorrection(self, exposure, darkExposure, invert=False)
daf::base::PropertyList * list
daf::base::PropertySet * set
std::shared_ptr< PhotoCalib > makePhotoCalibFromCalibZeroPoint(double instFluxMag0, double instFluxMag0Err)
Construct a PhotoCalib from the deprecated Calib-style instFluxMag0/instFluxMag0Err values.
MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > * makeMaskedImage(typename std::shared_ptr< Image< ImagePixelT > > image, typename std::shared_ptr< Mask< MaskPixelT > > mask=Mask< MaskPixelT >(), typename std::shared_ptr< Image< VariancePixelT > > variance=Image< VariancePixelT >())
A function to return a MaskedImage of the correct type (cf.
std::shared_ptr< Exposure< ImagePixelT, MaskPixelT, VariancePixelT > > makeExposure(MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > &mimage, std::shared_ptr< geom::SkyWcs const > wcs=std::shared_ptr< geom::SkyWcs const >())
A function to return an Exposure of the correct type (cf.
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)
def crosstalkSourceLookup(datasetType, registry, quantumDataId, collections)