lsst.ip.isr gdc0c513512+764c50d4d2
Loading...
Searching...
No Matches
isrTaskLSST.py
Go to the documentation of this file.
1__all__ = ["IsrTaskLSST", "IsrTaskLSSTConfig"]
2
3import numpy
4import math
5
6from . import isrFunctions
7from . import isrQa
8from . import linearize
9from .defects import Defects
10
11from contextlib import contextmanager
12from lsst.afw.cameraGeom import NullLinearityType
13import lsst.pex.config as pexConfig
14import lsst.afw.math as afwMath
15import lsst.pipe.base as pipeBase
16import lsst.afw.image as afwImage
17import lsst.pipe.base.connectionTypes as cT
18from lsst.meas.algorithms.detection import SourceDetectionTask
19
20from .overscan import SerialOverscanCorrectionTask, ParallelOverscanCorrectionTask
21from .overscanAmpConfig import OverscanCameraConfig
22from .assembleCcdTask import AssembleCcdTask
23from .deferredCharge import DeferredChargeTask
24from .crosstalk import CrosstalkTask
25from .masking import MaskingTask
26from .isrStatistics import IsrStatisticsTask
27from .isr import maskNans
28
29
30class IsrTaskLSSTConnections(pipeBase.PipelineTaskConnections,
31 dimensions={"instrument", "exposure", "detector"},
32 defaultTemplates={}):
33 ccdExposure = cT.Input(
34 name="raw",
35 doc="Input exposure to process.",
36 storageClass="Exposure",
37 dimensions=["instrument", "exposure", "detector"],
38 )
39 camera = cT.PrerequisiteInput(
40 name="camera",
41 storageClass="Camera",
42 doc="Input camera to construct complete exposures.",
43 dimensions=["instrument"],
44 isCalibration=True,
45 )
46 dnlLUT = cT.PrerequisiteInput(
47 name="dnlLUT",
48 doc="Look-up table for differential non-linearity.",
49 storageClass="IsrCalib",
50 dimensions=["instrument", "exposure", "detector"],
51 isCalibration=True,
52 # TODO DM 36636
53 )
54 bias = cT.PrerequisiteInput(
55 name="bias",
56 doc="Input bias calibration.",
57 storageClass="ExposureF",
58 dimensions=["instrument", "detector"],
59 isCalibration=True,
60 )
61 deferredChargeCalib = cT.PrerequisiteInput(
62 name="cpCtiCalib",
63 doc="Deferred charge/CTI correction dataset.",
64 storageClass="IsrCalib",
65 dimensions=["instrument", "detector"],
66 isCalibration=True,
67 )
68 linearizer = cT.PrerequisiteInput(
69 name='linearizer',
70 storageClass="Linearizer",
71 doc="Linearity correction calibration.",
72 dimensions=["instrument", "detector"],
73 isCalibration=True,
74 )
75 ptc = cT.PrerequisiteInput(
76 name="ptc",
77 doc="Input Photon Transfer Curve dataset",
78 storageClass="PhotonTransferCurveDataset",
79 dimensions=["instrument", "detector"],
80 isCalibration=True,
81 )
82 crosstalk = cT.PrerequisiteInput(
83 name="crosstalk",
84 doc="Input crosstalk object",
85 storageClass="CrosstalkCalib",
86 dimensions=["instrument", "detector"],
87 isCalibration=True,
88 )
89 defects = cT.PrerequisiteInput(
90 name='defects',
91 doc="Input defect tables.",
92 storageClass="Defects",
93 dimensions=["instrument", "detector"],
94 isCalibration=True,
95 )
96 bfKernel = cT.PrerequisiteInput(
97 name='brighterFatterKernel',
98 doc="Complete kernel + gain solutions.",
99 storageClass="BrighterFatterKernel",
100 dimensions=["instrument", "detector"],
101 isCalibration=True,
102 )
103 dark = cT.PrerequisiteInput(
104 name='dark',
105 doc="Input dark calibration.",
106 storageClass="ExposureF",
107 dimensions=["instrument", "detector"],
108 isCalibration=True,
109 )
110 outputExposure = cT.Output(
111 name='postISRCCD',
112 doc="Output ISR processed exposure.",
113 storageClass="Exposure",
114 dimensions=["instrument", "exposure", "detector"],
115 )
116 preInterpExposure = cT.Output(
117 name='preInterpISRCCD',
118 doc="Output ISR processed exposure, with pixels left uninterpolated.",
119 storageClass="ExposureF",
120 dimensions=["instrument", "exposure", "detector"],
121 )
122 outputBin1Exposure = cT.Output(
123 name="postIsrBin1",
124 doc="First binned image.",
125 storageClass="ExposureF",
126 dimensions=["instrument", "exposure", "detector"],
127 )
128 outputBin2Exposure = cT.Output(
129 name="postIsrBin2",
130 doc="Second binned image.",
131 storageClass="ExposureF",
132 dimensions=["instrument", "exposure", "detector"],
133 )
134
135 outputStatistics = cT.Output(
136 name="isrStatistics",
137 doc="Output of additional statistics table.",
138 storageClass="StructuredDataDict",
139 dimensions=["instrument", "exposure", "detector"],
140 )
141
142 def __init__(self, *, config=None):
143 super().__init__(config=config)
144
145 if config.doDiffNonLinearCorrection is not True:
146 del self.dnlLUT
147 if config.doBias is not True:
148 del self.bias
149 if config.doDeferredCharge is not True:
150 del self.deferredChargeCalib
151 if config.doLinearize is not True:
152 del self.linearizer
153 if not config.doCrosstalk and not config.overscanCamera.doAnyParallelOverscanCrosstalk:
154 del self.crosstalk
155 if config.doDefect is not True:
156 del self.defects
157 if config.doBrighterFatter is not True:
158 del self.bfKernel
159 if config.doDark is not True:
160 del self.dark
161
162 if config.doBinnedExposures is not True:
163 del self.outputBin1Exposure
164 del self.outputBin2Exposure
165 if config.doSaveInterpPixels is not True:
166 del self.preInterpExposure
167
168 if config.doCalculateStatistics is not True:
169 del self.outputStatistics
170
171
172class IsrTaskLSSTConfig(pipeBase.PipelineTaskConfig,
173 pipelineConnections=IsrTaskLSSTConnections):
174 """Configuration parameters for IsrTaskLSST.
175
176 Items are grouped in the order in which they are executed by the task.
177 """
178 expectWcs = pexConfig.Field(
179 dtype=bool,
180 default=True,
181 doc="Expect input science images to have a WCS (set False for e.g. spectrographs)."
182 )
183 qa = pexConfig.ConfigField(
184 dtype=isrQa.IsrQaConfig,
185 doc="QA related configuration options.",
186 )
187 doHeaderProvenance = pexConfig.Field(
188 dtype=bool,
189 default=True,
190 doc="Write calibration identifiers into output exposure header.",
191 )
192
193 # Differential non-linearity correction.
194 doDiffNonLinearCorrection = pexConfig.Field(
195 dtype=bool,
196 doc="Do differential non-linearity correction?",
197 default=False,
198 )
199
200 overscanCamera = pexConfig.ConfigField(
201 dtype=OverscanCameraConfig,
202 doc="Per-detector and per-amplifier overscan configurations.",
203 )
204
205 # Amplifier to CCD assembly configuration.
206 doAssembleCcd = pexConfig.Field(
207 dtype=bool,
208 default=True,
209 doc="Assemble amp-level exposures into a ccd-level exposure?"
210 )
211 assembleCcd = pexConfig.ConfigurableField(
212 target=AssembleCcdTask,
213 doc="CCD assembly task.",
214 )
215
216 # Bias subtraction.
217 doBias = pexConfig.Field(
218 dtype=bool,
219 doc="Apply bias frame correction?",
220 default=True,
221 )
222
223 # Deferred charge correction.
224 doDeferredCharge = pexConfig.Field(
225 dtype=bool,
226 doc="Apply deferred charge correction?",
227 default=True,
228 )
229 deferredChargeCorrection = pexConfig.ConfigurableField(
230 target=DeferredChargeTask,
231 doc="Deferred charge correction task.",
232 )
233
234 # Linearization.
235 doLinearize = pexConfig.Field(
236 dtype=bool,
237 doc="Correct for nonlinearity of the detector's response?",
238 default=True,
239 )
240
241 # Normalize gain.
242 doGainNormalize = pexConfig.Field(
243 dtype=bool,
244 doc="Normalize by the gain.",
245 default=True,
246 )
247
248 # Variance construction.
249 doVariance = pexConfig.Field(
250 dtype=bool,
251 doc="Calculate variance?",
252 default=True
253 )
254 gain = pexConfig.Field(
255 dtype=float,
256 doc="The gain to use if no Detector is present in the Exposure (ignored if NaN).",
257 default=float("NaN"),
258 )
259 maskNegativeVariance = pexConfig.Field(
260 dtype=bool,
261 doc="Mask pixels that claim a negative variance. This likely indicates a failure "
262 "in the measurement of the overscan at an edge due to the data falling off faster "
263 "than the overscan model can account for it.",
264 default=True,
265 )
266 negativeVarianceMaskName = pexConfig.Field(
267 dtype=str,
268 doc="Mask plane to use to mark pixels with negative variance, if `maskNegativeVariance` is True.",
269 default="BAD",
270 )
271 saturatedMaskName = pexConfig.Field(
272 dtype=str,
273 doc="Name of mask plane to use in saturation detection and interpolation.",
274 default="SAT",
275 )
276 suspectMaskName = pexConfig.Field(
277 dtype=str,
278 doc="Name of mask plane to use for suspect pixels.",
279 default="SUSPECT",
280 )
281
282 # Crosstalk.
283 doCrosstalk = pexConfig.Field(
284 dtype=bool,
285 doc="Apply intra-CCD crosstalk correction?",
286 default=True,
287 )
288 crosstalk = pexConfig.ConfigurableField(
289 target=CrosstalkTask,
290 doc="Intra-CCD crosstalk correction.",
291 )
292
293 # Masking options.
294 doDefect = pexConfig.Field(
295 dtype=bool,
296 doc="Apply correction for CCD defects, e.g. hot pixels?",
297 default=True,
298 )
299 doNanMasking = pexConfig.Field(
300 dtype=bool,
301 doc="Mask non-finite (NAN, inf) pixels.",
302 default=True,
303 )
304 doWidenSaturationTrails = pexConfig.Field(
305 dtype=bool,
306 doc="Widen bleed trails based on their width.",
307 default=True,
308 )
309 masking = pexConfig.ConfigurableField(
310 target=MaskingTask,
311 doc="Masking task."
312 )
313
314 # Interpolation options.
315 doInterpolate = pexConfig.Field(
316 dtype=bool,
317 doc="Interpolate masked pixels?",
318 default=True,
319 )
320 maskListToInterpolate = pexConfig.ListField(
321 dtype=str,
322 doc="List of mask planes that should be interpolated.",
323 default=['SAT', 'BAD'],
324 )
325 doSaveInterpPixels = pexConfig.Field(
326 dtype=bool,
327 doc="Save a copy of the pre-interpolated pixel values?",
328 default=False,
329 )
330
331 # Initial masking options.
332 doSetBadRegions = pexConfig.Field(
333 dtype=bool,
334 doc="Should we set the level of all BAD patches of the chip to the chip's average value?",
335 default=True,
336 )
337
338 # Brighter-Fatter correction.
339 doBrighterFatter = pexConfig.Field(
340 dtype=bool,
341 doc="Apply the brighter-fatter correction?",
342 default=True,
343 )
344 brighterFatterLevel = pexConfig.ChoiceField(
345 dtype=str,
346 doc="The level at which to correct for brighter-fatter.",
347 allowed={
348 "AMP": "Every amplifier treated separately.",
349 "DETECTOR": "One kernel per detector.",
350 },
351 default="DETECTOR",
352 )
353 brighterFatterMaxIter = pexConfig.Field(
354 dtype=int,
355 doc="Maximum number of iterations for the brighter-fatter correction.",
356 default=10,
357 )
358 brighterFatterThreshold = pexConfig.Field(
359 dtype=float,
360 doc="Threshold used to stop iterating the brighter-fatter correction. It is the "
361 "absolute value of the difference between the current corrected image and the one "
362 "from the previous iteration summed over all the pixels.",
363 default=1000,
364 )
365 brighterFatterApplyGain = pexConfig.Field(
366 dtype=bool,
367 doc="Should the gain be applied when applying the brighter-fatter correction?",
368 default=True,
369 )
370 brighterFatterMaskListToInterpolate = pexConfig.ListField(
371 dtype=str,
372 doc="List of mask planes that should be interpolated over when applying the brighter-fatter "
373 "correction.",
374 default=["SAT", "BAD", "NO_DATA", "UNMASKEDNAN"],
375 )
376 brighterFatterMaskGrowSize = pexConfig.Field(
377 dtype=int,
378 doc="Number of pixels to grow the masks listed in config.brighterFatterMaskListToInterpolate "
379 "when brighter-fatter correction is applied.",
380 default=0,
381 )
382 brighterFatterFwhmForInterpolation = pexConfig.Field(
383 dtype=float,
384 doc="FWHM of PSF in arcseconds used for interpolation in brighter-fatter correction "
385 "(currently unused).",
386 default=1.0,
387 )
388 growSaturationFootprintSize = pexConfig.Field(
389 dtype=int,
390 doc="Number of pixels by which to grow the saturation footprints.",
391 default=1,
392 )
393 brighterFatterMaskListToInterpolate = pexConfig.ListField(
394 dtype=str,
395 doc="List of mask planes that should be interpolated over when applying the brighter-fatter."
396 "correction.",
397 default=["SAT", "BAD", "NO_DATA", "UNMASKEDNAN"],
398 )
399
400 # Dark subtraction.
401 doDark = pexConfig.Field(
402 dtype=bool,
403 doc="Apply dark frame correction.",
404 default=True,
405 )
406
407 # Calculate image quality statistics?
408 doStandardStatistics = pexConfig.Field(
409 dtype=bool,
410 doc="Should standard image quality statistics be calculated?",
411 default=True,
412 )
413 # Calculate additional statistics?
414 doCalculateStatistics = pexConfig.Field(
415 dtype=bool,
416 doc="Should additional ISR statistics be calculated?",
417 default=True,
418 )
419 isrStats = pexConfig.ConfigurableField(
420 target=IsrStatisticsTask,
421 doc="Task to calculate additional statistics.",
422 )
423
424 # Make binned images?
425 doBinnedExposures = pexConfig.Field(
426 dtype=bool,
427 doc="Should binned exposures be calculated?",
428 default=False,
429 )
430 binFactor1 = pexConfig.Field(
431 dtype=int,
432 doc="Binning factor for first binned exposure. This is intended for a finely binned output.",
433 default=8,
434 check=lambda x: x > 1,
435 )
436 binFactor2 = pexConfig.Field(
437 dtype=int,
438 doc="Binning factor for second binned exposure. This is intended for a coarsely binned output.",
439 default=64,
440 check=lambda x: x > 1,
441 )
442
443 def validate(self):
444 super().validate()
445
446 if self.doCalculateStatistics and self.isrStats.doCtiStatistics:
447 # DM-41912: Implement doApplyGains in LSST IsrTask
448 # if self.doApplyGains !=
449 # self.isrStats.doApplyGainsForCtiStatistics:
450 raise ValueError("doApplyGains must match isrStats.applyGainForCtiStatistics.")
451
452 def setDefaults(self):
453 super().setDefaults()
454
455
456class IsrTaskLSST(pipeBase.PipelineTask):
457 ConfigClass = IsrTaskLSSTConfig
458 _DefaultName = "isr"
459
460 def __init__(self, **kwargs):
461 super().__init__(**kwargs)
462 self.makeSubtask("assembleCcd")
463 self.makeSubtask("deferredChargeCorrection")
464 self.makeSubtask("crosstalk")
465 self.makeSubtask("masking")
466 self.makeSubtask("isrStats")
467
468 def runQuantum(self, butlerQC, inputRefs, outputRefs):
469
470 inputs = butlerQC.get(inputRefs)
471 self.validateInput(inputs)
472 super().runQuantum(butlerQC, inputRefs, outputRefs)
473
474 def validateInput(self, inputs):
475 """
476 This is a check that all the inputs required by the config
477 are available.
478 """
479
480 doCrosstalk = self.config.doCrosstalk or self.config.overscanCamera.doAnyParallelOverscanCrosstalk
481
482 inputMap = {'dnlLUT': self.config.doDiffNonLinearCorrection,
483 'bias': self.config.doBias,
484 'deferredChargeCalib': self.config.doDeferredCharge,
485 'linearizer': self.config.doLinearize,
486 'ptc': self.config.doGainNormalize,
487 'crosstalk': doCrosstalk,
488 'defects': self.config.doDefect,
489 'bfKernel': self.config.doBrighterFatter,
490 'dark': self.config.doDark,
491 }
492
493 for calibrationFile, configValue in inputMap.items():
494 if configValue and inputs[calibrationFile] is None:
495 raise RuntimeError("Must supply ", calibrationFile)
496
497 def diffNonLinearCorrection(self, ccdExposure, dnlLUT, **kwargs):
498 # TODO DM 36636
499 # isrFunctions.diffNonLinearCorrection
500 pass
501
502 def maskFullDefectAmplifiers(self, ccdExposure, detector, defects):
503 """
504 Check for fully masked bad amplifiers and mask them.
505
506 Full defect masking happens later to allow for defects which
507 cross amplifier boundaries.
508
509 Parameters
510 ----------
511 ccdExposure : `lsst.afw.image.Exposure`
512 Input exposure to be masked.
513 detector : `lsst.afw.cameraGeom.Detector`
514 Detector object.
515 defects : `lsst.ip.isr.Defects`
516 List of defects. Used to determine if an entire
517 amplifier is bad.
518
519 Returns
520 -------
521 badAmpDict : `str`[`bool`]
522 Dictionary of amplifiers, keyed by name, value is True if
523 amplifier is fully masked.
524 """
525 badAmpDict = {}
526
527 maskedImage = ccdExposure.getMaskedImage()
528
529 for amp in detector:
530 ampName = amp.getName()
531 badAmpDict[ampName] = False
532
533 # Check if entire amp region is defined as a defect
534 # NB: need to use amp.getBBox() for correct comparison with current
535 # defects definition.
536 if defects is not None:
537 badAmpDict[ampName] = bool(sum([v.getBBox().contains(amp.getBBox()) for v in defects]))
538
539 # In the case of a bad amp, we will set mask to "BAD"
540 # (here use amp.getRawBBox() for correct association with pixels in
541 # current ccdExposure).
542 if badAmpDict[ampName]:
543 dataView = afwImage.MaskedImageF(maskedImage, amp.getRawBBox(),
544 afwImage.PARENT)
545 maskView = dataView.getMask()
546 maskView |= maskView.getPlaneBitMask("BAD")
547 del maskView
548
549 self.log.warning("Amplifier %s is bad (completely covered with defects)", ampName)
550
551 return badAmpDict
552
553 def maskSaturatedPixels(self, badAmpDict, ccdExposure, detector):
554 """
555 Mask SATURATED and SUSPECT pixels and check if any amplifiers
556 are fully masked.
557
558 Parameters
559 ----------
560 badAmpDict : `str` [`bool`]
561 Dictionary of amplifiers, keyed by name, value is True if
562 amplifier is fully masked.
563 ccdExposure : `lsst.afw.image.Exposure`
564 Input exposure to be masked.
565 detector : `lsst.afw.cameraGeom.Detector`
566 Detector object.
567 defects : `lsst.ip.isr.Defects`
568 List of defects. Used to determine if an entire
569 amplifier is bad.
570
571 Returns
572 -------
573 badAmpDict : `str`[`bool`]
574 Dictionary of amplifiers, keyed by name.
575 """
576 maskedImage = ccdExposure.getMaskedImage()
577
578 for amp in detector:
579 ampName = amp.getName()
580
581 if badAmpDict[ampName]:
582 # No need to check fully bad amplifiers.
583 continue
584
585 # Mask saturated and suspect pixels.
586 limits = {}
587 if self.config.doSaturation:
588 # Set to the default from the camera model.
589 limits.update({self.config.saturatedMaskName: amp.getSaturation()})
590 # And update if it is set in the config.
591 if math.isfinite(self.config.saturation):
592 limits.update({self.config.saturatedMaskName: self.config.saturation})
593 if self.config.doSuspect:
594 limits.update({self.config.suspectMaskName: amp.getSuspectLevel()})
595
596 for maskName, maskThreshold in limits.items():
597 if not math.isnan(maskThreshold):
598 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
599 isrFunctions.makeThresholdMask(
600 maskedImage=dataView,
601 threshold=maskThreshold,
602 growFootprints=0,
603 maskName=maskName
604 )
605
606 # Determine if we've fully masked this amplifier with SUSPECT and
607 # SAT pixels.
608 maskView = afwImage.Mask(maskedImage.getMask(), amp.getRawDataBBox(),
609 afwImage.PARENT)
610 maskVal = maskView.getPlaneBitMask([self.config.saturatedMaskName,
611 self.config.suspectMaskName])
612 if numpy.all(maskView.getArray() & maskVal > 0):
613 self.log.warning("Amplifier %s is bad (completely SATURATED or SUSPECT)", ampName)
614 badAmpDict[ampName] = True
615 maskView |= maskView.getPlaneBitMask("BAD")
616
617 return badAmpDict
618
619 def overscanCorrection(self, mode, detectorConfig, detector, badAmpDict, ccdExposure):
620 """Apply serial overscan correction in place to all amps.
621
622 The actual overscan subtraction is performed by the
623 `lsst.ip.isr.overscan.OverscanTask`, which is called here.
624
625 Parameters
626 ----------
627 mode : `str`
628 Must be `SERIAL` or `PARALLEL`.
629 detectorConfig : `lsst.ip.isr.OverscanDetectorConfig`
630 Per-amplifier configurations.
631 detector : `lsst.afw.cameraGeom.Detector`
632 Detector object.
633 badAmpDict : `dict`
634 Dictionary of amp name to whether it is a bad amp.
635 ccdExposure : `lsst.afw.image.Exposure`
636 Exposure to have overscan correction performed.
637
638 Returns
639 -------
640 overscans : `list` [`lsst.pipe.base.Struct` or None]
641 Each result struct has components:
642
643 ``imageFit``
644 Value or fit subtracted from the amplifier image data.
645 (scalar or `lsst.afw.image.Image`)
646 ``overscanFit``
647 Value or fit subtracted from the overscan image data.
648 (scalar or `lsst.afw.image.Image`)
649 ``overscanImage``
650 Image of the overscan region with the overscan
651 correction applied. This quantity is used to estimate
652 the amplifier read noise empirically.
653 (`lsst.afw.image.Image`)
654 ``overscanMean``
655 Mean overscan fit value. (`float`)
656 ``overscanMedian``
657 Median overscan fit value. (`float`)
658 ``overscanSigma``
659 Clipped standard deviation of the overscan fit. (`float`)
660 ``residualMean``
661 Mean of the overscan after fit subtraction. (`float`)
662 ``residualMedian``
663 Median of the overscan after fit subtraction. (`float`)
664 ``residualSigma``
665 Clipped standard deviation of the overscan after fit
666 subtraction. (`float`)
667
668 See Also
669 --------
670 lsst.ip.isr.overscan.OverscanTask
671 """
672 if mode not in ["SERIAL", "PARALLEL"]:
673 raise ValueError("Mode must be SERIAL or PARALLEL")
674
675 # This returns a list in amp order, with None for uncorrected amps.
676 overscans = []
677
678 for i, amp in enumerate(detector):
679 ampName = amp.getName()
680
681 ampConfig = detectorConfig.getOverscanAmpConfig(ampName)
682
683 if mode == "SERIAL" and not ampConfig.doSerialOverscan:
684 self.log.debug(
685 "ISR_OSCAN: Amplifier %s/%s configured to skip serial overscan.",
686 detector.getName(),
687 ampName,
688 )
689 results = None
690 elif mode == "PARALLEL" and not ampConfig.doParallelOverscan:
691 self.log.debug(
692 "ISR_OSCAN: Amplifier %s configured to skip parallel overscan.",
693 detector.getName(),
694 ampName,
695 )
696 results = None
697 elif badAmpDict[ampName] or not ccdExposure.getBBox().contains(amp.getBBox()):
698 results = None
699 else:
700 # This check is to confirm that we are not trying to run
701 # overscan on an already trimmed image. Therefore, always
702 # checking just the horizontal overscan bounding box is
703 # sufficient.
704 if amp.getRawHorizontalOverscanBBox().isEmpty():
705 self.log.warning(
706 "ISR_OSCAN: No overscan region for amp %s. Not performing overscan correction.",
707 ampName,
708 )
709 results = None
710 else:
711 if mode == "SERIAL":
712 # We need to set up the subtask here with a custom
713 # configuration.
714 serialOverscan = SerialOverscanCorrectionTask(config=ampConfig.serialOverscanConfig)
715 results = serialOverscan.run(ccdExposure, amp)
716 else:
717 parallelOverscan = ParallelOverscanCorrectionTask(
718 config=ampConfig.parallelOverscanConfig,
719 )
720 results = parallelOverscan.run(ccdExposure, amp)
721
722 metadata = ccdExposure.getMetadata()
723 keyBase = "LSST ISR OVERSCAN"
724 metadata[f"{keyBase} {mode} MEAN {ampName}"] = results.overscanMean
725 metadata[f"{keyBase} {mode} MEDIAN {ampName}"] = results.overscanMedian
726 metadata[f"{keyBase} {mode} STDEV {ampName}"] = results.overscanSigma
727
728 metadata[f"{keyBase} RESIDUAL {mode} MEAN {ampName}"] = results.residualMean
729 metadata[f"{keyBase} RESIDUAL {mode} MEDIAN {ampName}"] = results.residualMedian
730 metadata[f"{keyBase} RESIDUAL {mode} STDEV {ampName}"] = results.residualSigma
731
732 overscans[i] = results
733
734 # Question: should this be finer grained?
735 ccdExposure.getMetadata().set("OVERSCAN", "Overscan corrected")
736
737 return overscans
738
739 def getLinearizer(self, detector):
740 # Here we assume linearizer as dict or LUT are not supported
741 # TODO DM 28741
742
743 # TODO construct isrcalib input
744 linearizer = linearize.Linearizer(detector=detector, log=self.log)
745 self.log.warning("Constructing linearizer from cameraGeom information.")
746
747 return linearizer
748
749 def gainNormalize(self, **kwargs):
750 # TODO DM 36639
751 gains = []
752 readNoise = []
753
754 return gains, readNoise
755
756 def updateVariance(self, ampExposure, amp, ptcDataset=None):
757 """Set the variance plane using the gain and read noise.
758
759 Parameters
760 ----------
761 ampExposure : `lsst.afw.image.Exposure`
762 Exposure to process.
763 amp : `lsst.afw.cameraGeom.Amplifier` or `FakeAmp`
764 Amplifier detector data.
765 ptcDataset : `lsst.ip.isr.PhotonTransferCurveDataset`, optional
766 PTC dataset containing the gains and read noise.
767
768 Raises
769 ------
770 RuntimeError
771 Raised if ptcDataset is not provided.
772
773 See also
774 --------
775 lsst.ip.isr.isrFunctions.updateVariance
776 """
777 # Get gains from PTC
778 if ptcDataset is None:
779 raise RuntimeError("No ptcDataset provided to use PTC gains.")
780 else:
781 gain = ptcDataset.gain[amp.getName()]
782 self.log.debug("Getting gain from Photon Transfer Curve.")
783
784 if math.isnan(gain):
785 gain = 1.0
786 self.log.warning("Gain set to NAN! Updating to 1.0 to generate Poisson variance.")
787 elif gain <= 0:
788 patchedGain = 1.0
789 self.log.warning("Gain for amp %s == %g <= 0; setting to %f.",
790 amp.getName(), gain, patchedGain)
791 gain = patchedGain
792
793 # Get read noise from PTC
794 if ptcDataset is None:
795 raise RuntimeError("No ptcDataset provided to use PTC readnoise.")
796 else:
797 readNoise = ptcDataset.noise[amp.getName()]
798 self.log.debug("Getting read noise from Photon Transfer Curve.")
799
800 metadata = ampExposure.getMetadata()
801 metadata[f'LSST GAIN {amp.getName()}'] = gain
802 metadata[f'LSST READNOISE {amp.getName()}'] = readNoise
803
804 isrFunctions.updateVariance(
805 maskedImage=ampExposure.getMaskedImage(),
806 gain=gain,
807 readNoise=readNoise,
808 )
809
810 def maskNegativeVariance(self, exposure):
811 """Identify and mask pixels with negative variance values.
812
813 Parameters
814 ----------
815 exposure : `lsst.afw.image.Exposure`
816 Exposure to process.
817
818 See Also
819 --------
820 lsst.ip.isr.isrFunctions.updateVariance
821 """
822 maskPlane = exposure.getMask().getPlaneBitMask(self.config.negativeVarianceMaskName)
823 bad = numpy.where(exposure.getVariance().getArray() <= 0.0)
824 exposure.mask.array[bad] |= maskPlane
825
826 def variancePlane(self, ccdExposure, ccd, ptc):
827 for amp in ccd:
828 if ccdExposure.getBBox().contains(amp.getBBox()):
829 self.log.debug("Constructing variance map for amplifer %s.", amp.getName())
830 ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
831
832 self.updateVariance(ampExposure, amp, ptcDataset=ptc)
833
834 if self.config.qa is not None and self.config.qa.saveStats is True:
835 qaStats = afwMath.makeStatistics(ampExposure.getVariance(),
836 afwMath.MEDIAN | afwMath.STDEVCLIP)
837 self.log.debug(" Variance stats for amplifer %s: %f +/- %f.",
838 amp.getName(), qaStats.getValue(afwMath.MEDIAN),
839 qaStats.getValue(afwMath.STDEVCLIP))
840 if self.config.maskNegativeVariance:
841 self.maskNegativeVariance(ccdExposure)
842
843 def maskDefect(self, exposure, defectBaseList):
844 """Mask defects using mask plane "BAD", in place.
845
846 Parameters
847 ----------
848 exposure : `lsst.afw.image.Exposure`
849 Exposure to process.
850
851 defectBaseList : defect-type
852 List of defects to mask. Can be of type `lsst.ip.isr.Defects`
853 or `list` of `lsst.afw.image.DefectBase`.
854 """
855 maskedImage = exposure.getMaskedImage()
856 if not isinstance(defectBaseList, Defects):
857 # Promotes DefectBase to Defect
858 defectList = Defects(defectBaseList)
859 else:
860 defectList = defectBaseList
861 defectList.maskPixels(maskedImage, maskName="BAD")
862
863 def maskEdges(self, exposure, numEdgePixels=0, maskPlane="SUSPECT", level='DETECTOR'):
864 """Mask edge pixels with applicable mask plane.
865
866 Parameters
867 ----------
868 exposure : `lsst.afw.image.Exposure`
869 Exposure to process.
870 numEdgePixels : `int`, optional
871 Number of edge pixels to mask.
872 maskPlane : `str`, optional
873 Mask plane name to use.
874 level : `str`, optional
875 Level at which to mask edges.
876 """
877 maskedImage = exposure.getMaskedImage()
878 maskBitMask = maskedImage.getMask().getPlaneBitMask(maskPlane)
879
880 if numEdgePixels > 0:
881 if level == 'DETECTOR':
882 boxes = [maskedImage.getBBox()]
883 elif level == 'AMP':
884 boxes = [amp.getBBox() for amp in exposure.getDetector()]
885
886 for box in boxes:
887 # This makes a bbox numEdgeSuspect pixels smaller than the
888 # image on each side
889 subImage = maskedImage[box]
890 box.grow(-numEdgePixels)
891 # Mask pixels outside box
892 SourceDetectionTask.setEdgeBits(
893 subImage,
894 box,
895 maskBitMask)
896
897 def maskNan(self, exposure):
898 """Mask NaNs using mask plane "UNMASKEDNAN", in place.
899
900 Parameters
901 ----------
902 exposure : `lsst.afw.image.Exposure`
903 Exposure to process.
904
905 Notes
906 -----
907 We mask over all non-finite values (NaN, inf), including those
908 that are masked with other bits (because those may or may not be
909 interpolated over later, and we want to remove all NaN/infs).
910 Despite this behaviour, the "UNMASKEDNAN" mask plane is used to
911 preserve the historical name.
912 """
913 maskedImage = exposure.getMaskedImage()
914
915 # Find and mask NaNs
916 maskedImage.getMask().addMaskPlane("UNMASKEDNAN")
917 maskVal = maskedImage.getMask().getPlaneBitMask("UNMASKEDNAN")
918 numNans = maskNans(maskedImage, maskVal)
919 self.metadata["NUMNANS"] = numNans
920 if numNans > 0:
921 self.log.warning("There were %d unmasked NaNs.", numNans)
922
923 def countBadPixels(self, exposure):
924 """
925 Notes
926 -----
927 Reset and interpolate bad pixels.
928
929 Large contiguous bad regions (which should have the BAD mask
930 bit set) should have their values set to the image median.
931 This group should include defects and bad amplifiers. As the
932 area covered by these defects are large, there's little
933 reason to expect that interpolation would provide a more
934 useful value.
935
936 Smaller defects can be safely interpolated after the larger
937 regions have had their pixel values reset. This ensures
938 that the remaining defects adjacent to bad amplifiers (as an
939 example) do not attempt to interpolate extreme values.
940 """
941 badPixelCount, badPixelValue = isrFunctions.setBadRegions(exposure)
942 if badPixelCount > 0:
943 self.log.info("Set %d BAD pixels to %f.", badPixelCount, badPixelValue)
944
945 @contextmanager
946 def flatContext(self, exp, flat, dark=None):
947 """Context manager that applies and removes flats and darks,
948 if the task is configured to apply them.
949
950 Parameters
951 ----------
952 exp : `lsst.afw.image.Exposure`
953 Exposure to process.
954 flat : `lsst.afw.image.Exposure`
955 Flat exposure the same size as ``exp``.
956 dark : `lsst.afw.image.Exposure`, optional
957 Dark exposure the same size as ``exp``.
958
959 Yields
960 ------
961 exp : `lsst.afw.image.Exposure`
962 The flat and dark corrected exposure.
963 """
964 if self.config.doDark and dark is not None:
965 self.darkCorrection(exp, dark)
966 if self.config.doFlat:
967 self.flatCorrection(exp, flat)
968 try:
969 yield exp
970 finally:
971 if self.config.doFlat:
972 self.flatCorrection(exp, flat, invert=True)
973 if self.config.doDark and dark is not None:
974 self.darkCorrection(exp, dark, invert=True)
975
976 def getBrighterFatterKernel(self, detector, bfKernel):
977 detName = detector.getName()
978
979 # This is expected to be a dictionary of amp-wise gains.
980 bfGains = bfKernel.gain
981 if bfKernel.level == 'DETECTOR':
982 if detName in bfKernel.detKernels:
983 bfKernelOut = bfKernel.detKernels[detName]
984 return bfKernelOut, bfGains
985 else:
986 raise RuntimeError("Failed to extract kernel from new-style BF kernel.")
987 elif bfKernel.level == 'AMP':
988 self.log.warning("Making DETECTOR level kernel from AMP based brighter "
989 "fatter kernels.")
990 bfKernel.makeDetectorKernelFromAmpwiseKernels(detName)
991 bfKernelOut = bfKernel.detKernels[detName]
992 return bfKernelOut, bfGains
993
994 def applyBrighterFatterCorrection(self, ccdExposure, flat, dark, bfKernel, bfGains):
995 # We need to apply flats and darks before we can interpolate, and
996 # we need to interpolate before we do B-F, but we do B-F without
997 # the flats and darks applied so we can work in units of electrons
998 # or holes. This context manager applies and then removes the darks
999 # and flats.
1000 #
1001 # We also do not want to interpolate values here, so operate on
1002 # temporary images so we can apply only the BF-correction and roll
1003 # back the interpolation.
1004 # This won't be necessary once the gain normalization
1005 # is done appropriately.
1006 interpExp = ccdExposure.clone()
1007 with self.flatContext(interpExp, flat, dark):
1008 isrFunctions.interpolateFromMask(
1009 maskedImage=interpExp.getMaskedImage(),
1010 fwhm=self.config.brighterFatterFwhmForInterpolation,
1011 growSaturatedFootprints=self.config.growSaturationFootprintSize,
1012 maskNameList=list(self.config.brighterFatterMaskListToInterpolate)
1013 )
1014 bfExp = interpExp.clone()
1015 self.log.info("Applying brighter-fatter correction using kernel type %s / gains %s.",
1016 type(bfKernel), type(bfGains))
1017 bfResults = isrFunctions.brighterFatterCorrection(bfExp, bfKernel,
1018 self.config.brighterFatterMaxIter,
1019 self.config.brighterFatterThreshold,
1020 self.config.brighterFatterApplyGain,
1021 bfGains)
1022 if bfResults[1] == self.config.brighterFatterMaxIter:
1023 self.log.warning("Brighter-fatter correction did not converge, final difference %f.",
1024 bfResults[0])
1025 else:
1026 self.log.info("Finished brighter-fatter correction in %d iterations.",
1027 bfResults[1])
1028
1029 image = ccdExposure.getMaskedImage().getImage()
1030 bfCorr = bfExp.getMaskedImage().getImage()
1031 bfCorr -= interpExp.getMaskedImage().getImage()
1032 image += bfCorr
1033
1034 # Applying the brighter-fatter correction applies a
1035 # convolution to the science image. At the edges this
1036 # convolution may not have sufficient valid pixels to
1037 # produce a valid correction. Mark pixels within the size
1038 # of the brighter-fatter kernel as EDGE to warn of this
1039 # fact.
1040 self.log.info("Ensuring image edges are masked as EDGE to the brighter-fatter kernel size.")
1041 self.maskEdges(ccdExposure, numEdgePixels=numpy.max(bfKernel.shape) // 2,
1042 maskPlane="EDGE")
1043
1044 if self.config.brighterFatterMaskGrowSize > 0:
1045 self.log.info("Growing masks to account for brighter-fatter kernel convolution.")
1046 for maskPlane in self.config.brighterFatterMaskListToInterpolate:
1047 isrFunctions.growMasks(ccdExposure.getMask(),
1048 radius=self.config.brighterFatterMaskGrowSize,
1049 maskNameList=maskPlane,
1050 maskValue=maskPlane)
1051
1052 return ccdExposure
1053
1054 def darkCorrection(self, exposure, darkExposure, invert=False):
1055 """Apply dark correction in place.
1056
1057 Parameters
1058 ----------
1059 exposure : `lsst.afw.image.Exposure`
1060 Exposure to process.
1061 darkExposure : `lsst.afw.image.Exposure`
1062 Dark exposure of the same size as ``exposure``.
1063 invert : `Bool`, optional
1064 If True, re-add the dark to an already corrected image.
1065
1066 Raises
1067 ------
1068 RuntimeError
1069 Raised if either ``exposure`` or ``darkExposure`` do not
1070 have their dark time defined.
1071
1072 See Also
1073 --------
1074 lsst.ip.isr.isrFunctions.darkCorrection
1075 """
1076 expScale = exposure.getInfo().getVisitInfo().getDarkTime()
1077 if math.isnan(expScale):
1078 raise RuntimeError("Exposure darktime is NAN.")
1079 if darkExposure.getInfo().getVisitInfo() is not None \
1080 and not math.isnan(darkExposure.getInfo().getVisitInfo().getDarkTime()):
1081 darkScale = darkExposure.getInfo().getVisitInfo().getDarkTime()
1082 else:
1083 # DM-17444: darkExposure.getInfo.getVisitInfo() is None
1084 # so getDarkTime() does not exist.
1085 self.log.warning("darkExposure.getInfo().getVisitInfo() does not exist. Using darkScale = 1.0.")
1086 darkScale = 1.0
1087
1088 isrFunctions.darkCorrection(
1089 maskedImage=exposure.getMaskedImage(),
1090 darkMaskedImage=darkExposure.getMaskedImage(),
1091 expScale=expScale,
1092 darkScale=darkScale,
1093 invert=invert,
1094 )
1095
1096 @staticmethod
1098 """Extract common calibration metadata values that will be written to
1099 output header.
1100
1101 Parameters
1102 ----------
1103 calib : `lsst.afw.image.Exposure` or `lsst.ip.isr.IsrCalib`
1104 Calibration to pull date information from.
1105
1106 Returns
1107 -------
1108 dateString : `str`
1109 Calibration creation date string to add to header.
1110 """
1111 if hasattr(calib, "getMetadata"):
1112 if 'CALIB_CREATION_DATE' in calib.getMetadata():
1113 return " ".join((calib.getMetadata().get("CALIB_CREATION_DATE", "Unknown"),
1114 calib.getMetadata().get("CALIB_CREATION_TIME", "Unknown")))
1115 else:
1116 return " ".join((calib.getMetadata().get("CALIB_CREATE_DATE", "Unknown"),
1117 calib.getMetadata().get("CALIB_CREATE_TIME", "Unknown")))
1118 else:
1119 return "Unknown Unknown"
1120
1121 def doLinearize(self, detector):
1122 """Check if linearization is needed for the detector cameraGeom.
1123
1124 Checks config.doLinearize and the linearity type of the first
1125 amplifier.
1126
1127 Parameters
1128 ----------
1129 detector : `lsst.afw.cameraGeom.Detector`
1130 Detector to get linearity type from.
1131
1132 Returns
1133 -------
1134 doLinearize : `Bool`
1135 If True, linearization should be performed.
1136 """
1137 return self.config.doLinearize and \
1138 detector.getAmplifiers()[0].getLinearityType() != NullLinearityType
1139
1140 def makeBinnedImages(self, exposure):
1141 """Make visualizeVisit style binned exposures.
1142
1143 Parameters
1144 ----------
1145 exposure : `lsst.afw.image.Exposure`
1146 Exposure to bin.
1147
1148 Returns
1149 -------
1150 bin1 : `lsst.afw.image.Exposure`
1151 Binned exposure using binFactor1.
1152 bin2 : `lsst.afw.image.Exposure`
1153 Binned exposure using binFactor2.
1154 """
1155 mi = exposure.getMaskedImage()
1156
1157 bin1 = afwMath.binImage(mi, self.config.binFactor1)
1158 bin2 = afwMath.binImage(mi, self.config.binFactor2)
1159
1160 return bin1, bin2
1161
1162 def run(self, *, ccdExposure, dnlLUT=None, bias=None, deferredChargeCalib=None, linearizer=None,
1163 ptc=None, crosstalk=None, defects=None, bfKernel=None, bfGains=None, dark=None,
1164 flat=None, camera=None, **kwargs
1165 ):
1166
1167 detector = ccdExposure.getDetector()
1168
1169 overscanDetectorConfig = self.config.overscanCamera.getOverscanDetectorConfig(detector)
1170
1171 if self.config.doHeaderProvenance:
1172 # Inputs have been validated, so we can add their date
1173 # information to the output header.
1174 exposureMetadata = ccdExposure.getMetadata()
1175 exposureMetadata["LSST CALIB OVERSCAN HASH"] = overscanDetectorConfig.md5
1176 exposureMetadata["LSST CALIB DATE PTC"] = self.extractCalibDate(ptc)
1177 if self.config.doDiffNonLinearCorrection:
1178 exposureMetadata["LSST CALIB DATE DNL"] = self.extractCalibDate(dnlLUT)
1179 if self.config.doBias:
1180 exposureMetadata["LSST CALIB DATE BIAS"] = self.extractCalibDate(bias)
1181 if self.config.doDeferredCharge:
1182 exposureMetadata["LSST CALIB DATE CTI"] = self.extractCalibDate(deferredChargeCalib)
1183 if self.doLinearize(detector):
1184 exposureMetadata["LSST CALIB DATE LINEARIZER"] = self.extractCalibDate(linearizer)
1185 if self.config.doCrosstalk or overscanDetectorConfig.doAnyParallelOverscanCrosstalk:
1186 exposureMetadata["LSST CALIB DATE CROSSTALK"] = self.extractCalibDate(crosstalk)
1187 if self.config.doDefect:
1188 exposureMetadata["LSST CALIB DATE DEFECTS"] = self.extractCalibDate(defects)
1189 if self.config.doBrighterFatter:
1190 exposureMetadata["LSST CALIB DATE BFK"] = self.extractCalibDate(bfKernel)
1191 if self.config.doDark:
1192 exposureMetadata["LSST CALIB DATE DARK"] = self.extractCalibDate(dark)
1193
1194 # First we mark which amplifiers are completely bad from defects.
1195 badAmpDict = self.maskFullDefectAmplifiers(ccdExposure, detector, defects)
1196
1197 if self.config.doDiffNonLinearCorrection:
1198 self.diffNonLinearCorrection(ccdExposure, dnlLUT)
1199
1200 if overscanDetectorConfig.doAnySerialOverscan:
1201 # Input units: ADU
1202 serialOverscans = self.overscanCorrection(
1203 "SERIAL",
1204 overscanDetectorConfig,
1205 detector,
1206 badAmpDict,
1207 ccdExposure,
1208 )
1209 else:
1210 serialOverscans = [None]*len(detector)
1211
1212 if overscanDetectorConfig.doAnyParallelOverscanCrosstalk:
1213 # Input units: ADU
1214 # Make sure that the units here are consistent with later
1215 # application.
1216 self.crosstalk.run(
1217 ccdExposure,
1218 crosstalk=crosstalk,
1219 camera=camera,
1220 parallelOverscanRegion=True,
1221 detectorConfig=overscanDetectorConfig,
1222 )
1223
1224 # After serial overscan correction, we can mask SATURATED and
1225 # SUSPECT pixels. This updates badAmpDict if any amplifier
1226 # is fully saturated after serial overscan correction.
1227 badAmpDict = self.maskSaturatedPixels(badAmpDict, ccdExposure, detector)
1228
1229 if overscanDetectorConfig.doAnyParallelOverscan:
1230 # Input units: ADU
1231 # At the moment we do not use the parallelOverscans return value.
1232 _ = self.overscanCorrection(
1233 "PARALLEL",
1234 overscanDetectorConfig,
1235 detector,
1236 badAmpDict,
1237 ccdExposure,
1238 )
1239
1240 if self.config.doAssembleCcd:
1241 # Input units: ADU
1242 self.log.info("Assembling CCD from amplifiers.")
1243 ccdExposure = self.assembleCcd.assembleCcd(ccdExposure)
1244
1245 if self.config.expectWcs and not ccdExposure.getWcs():
1246 self.log.warning("No WCS found in input exposure.")
1247
1248 if self.config.doLinearize:
1249 # Input units: ADU
1250 self.log.info("Applying linearizer.")
1251 linearizer = self.getLinearizer(detector=detector)
1252 linearizer.applyLinearity(image=ccdExposure.getMaskedImage().getImage(),
1253 detector=detector, log=self.log)
1254
1255 if self.config.doCrosstalk:
1256 # Input units: ADU
1257 self.log.info("Applying crosstalk correction.")
1258 self.crosstalk.run(ccdExposure, crosstalk=crosstalk)
1259
1260 if self.config.doBias:
1261 # Input units: ADU
1262 self.log.info("Applying bias correction.")
1263 isrFunctions.biasCorrection(ccdExposure.getMaskedImage(), bias.getMaskedImage())
1264
1265 if self.config.doGainNormalize:
1266 # Input units: ADU
1267 # Output units: electrons
1268 # TODO DM 36639
1269 gains, readNoise = self.gainNormalize(**kwargs)
1270
1271 if self.config.doDeferredCharge:
1272 # Input units: electrons
1273 self.log.info("Applying deferred charge/CTI correction.")
1274 self.deferredChargeCorrection.run(ccdExposure, deferredChargeCalib)
1275
1276 if self.config.doVariance:
1277 # Input units: electrons
1278 self.variancePlane(ccdExposure, detector, ptc)
1279
1280 # Masking block (defects, NAN pixels and trails).
1281 # Saturated and suspect pixels have already been masked.
1282 if self.config.doDefect:
1283 # Input units: electrons
1284 self.log.info("Applying defects masking.")
1285 self.maskDefect(ccdExposure, defects)
1286
1287 if self.config.doNanMasking:
1288 self.log.info("Masking non-finite (NAN, inf) value pixels.")
1289 self.maskNan(ccdExposure)
1290
1291 if self.config.doWidenSaturationTrails:
1292 self.log.info("Widening saturation trails.")
1293 isrFunctions.widenSaturationTrails(ccdExposure.getMaskedImage().getMask())
1294
1295 preInterpExp = None
1296 if self.config.doSaveInterpPixels:
1297 preInterpExp = ccdExposure.clone()
1298
1299 if self.config.doSetBadRegions:
1300 self.log.info('Counting pixels in BAD regions.')
1301 self.countBadPixels(ccdExposure)
1302
1303 if self.config.doInterpolate:
1304 self.log.info("Interpolating masked pixels.")
1305 isrFunctions.interpolateFromMask(
1306 maskedImage=ccdExposure.getMaskedImage(),
1307 fwhm=self.config.brighterFatterFwhmForInterpolation,
1308 growSaturatedFootprints=self.config.growSaturationFootprintSize,
1309 maskNameList=list(self.config.maskListToInterpolate)
1310 )
1311
1312 if self.config.doDark:
1313 # Input units: electrons
1314 self.log.info("Applying dark subtraction.")
1315 self.darkCorrection(ccdExposure, dark)
1316
1317 if self.config.doBrighterFatter:
1318 # Input units: electrons
1319 self.log.info("Applying Bright-Fatter kernels.")
1320 bfKernelOut, bfGains = self.getBrighterFatterKernel(detector, bfKernel)
1321 ccdExposure = self.applyBrighterFatterCorrection(ccdExposure, flat, dark, bfKernelOut, bfGains)
1322
1323 # Calculate standard image quality statistics
1324 if self.config.doStandardStatistics:
1325 metadata = ccdExposure.getMetadata()
1326 for amp in detector:
1327 ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
1328 ampName = amp.getName()
1329 metadata[f"LSST ISR MASK SAT {ampName}"] = isrFunctions.countMaskedPixels(
1330 ampExposure.getMaskedImage(),
1331 [self.config.saturatedMaskName]
1332 )
1333 metadata[f"LSST ISR MASK BAD {ampName}"] = isrFunctions.countMaskedPixels(
1334 ampExposure.getMaskedImage(),
1335 ["BAD"]
1336 )
1337 qaStats = afwMath.makeStatistics(ampExposure.getImage(),
1338 afwMath.MEAN | afwMath.MEDIAN | afwMath.STDEVCLIP)
1339
1340 metadata[f"LSST ISR FINAL MEAN {ampName}"] = qaStats.getValue(afwMath.MEAN)
1341 metadata[f"LSST ISR FINAL MEDIAN {ampName}"] = qaStats.getValue(afwMath.MEDIAN)
1342 metadata[f"LSST ISR FINAL STDEV {ampName}"] = qaStats.getValue(afwMath.STDEVCLIP)
1343
1344 k1 = f"LSST ISR FINAL MEDIAN {ampName}"
1345 k2 = f"LSST ISR OVERSCAN SERIAL MEDIAN {ampName}"
1346 if overscanDetectorConfig.doAnySerialOverscan and k1 in metadata and k2 in metadata:
1347 metadata[f"LSST ISR LEVEL {ampName}"] = metadata[k1] - metadata[k2]
1348 else:
1349 metadata[f"LSST ISR LEVEL {ampName}"] = numpy.nan
1350
1351 # calculate additional statistics.
1352 outputStatistics = None
1353 if self.config.doCalculateStatistics:
1354 outputStatistics = self.isrStats.run(ccdExposure, overscanResults=serialOverscans,
1355 ptc=ptc).results
1356
1357 # do image binning.
1358 outputBin1Exposure = None
1359 outputBin2Exposure = None
1360 if self.config.doBinnedExposures:
1361 outputBin1Exposure, outputBin2Exposure = self.makeBinnedImages(ccdExposure)
1362
1363 return pipeBase.Struct(
1364 exposure=ccdExposure,
1365
1366 outputBin1Exposure=outputBin1Exposure,
1367 outputBin2Exposure=outputBin2Exposure,
1368
1369 preInterpExposure=preInterpExp,
1370 outputExposure=ccdExposure,
1371 outputStatistics=outputStatistics,
1372 )
runQuantum(self, butlerQC, inputRefs, outputRefs)
variancePlane(self, ccdExposure, ccd, ptc)
flatContext(self, exp, flat, dark=None)
updateVariance(self, ampExposure, amp, ptcDataset=None)
maskSaturatedPixels(self, badAmpDict, ccdExposure, detector)
getBrighterFatterKernel(self, detector, bfKernel)
darkCorrection(self, exposure, darkExposure, invert=False)
overscanCorrection(self, mode, detectorConfig, detector, badAmpDict, ccdExposure)
diffNonLinearCorrection(self, ccdExposure, dnlLUT, **kwargs)
maskFullDefectAmplifiers(self, ccdExposure, detector, defects)
maskEdges(self, exposure, numEdgePixels=0, maskPlane="SUSPECT", level='DETECTOR')
maskDefect(self, exposure, defectBaseList)
applyBrighterFatterCorrection(self, ccdExposure, flat, dark, bfKernel, bfGains)
run(self, *ccdExposure, dnlLUT=None, bias=None, deferredChargeCalib=None, linearizer=None, ptc=None, crosstalk=None, defects=None, bfKernel=None, bfGains=None, dark=None, flat=None, camera=None, **kwargs)