lsst.ip.isr g535a204a91+603d5f9333
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 limits.update({self.config.saturatedMaskName: amp.getSaturation()})
589 if self.config.doSuspect:
590 limits.update({self.config.suspectMaskName: amp.getSuspectLevel()})
591 if math.isfinite(self.config.saturation):
592 limits.update({self.config.saturatedMaskName: self.config.saturation})
593
594 for maskName, maskThreshold in limits.items():
595 if not math.isnan(maskThreshold):
596 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
597 isrFunctions.makeThresholdMask(
598 maskedImage=dataView,
599 threshold=maskThreshold,
600 growFootprints=0,
601 maskName=maskName
602 )
603
604 # Determine if we've fully masked this amplifier with SUSPECT and
605 # SAT pixels.
606 maskView = afwImage.Mask(maskedImage.getMask(), amp.getRawDataBBox(),
607 afwImage.PARENT)
608 maskVal = maskView.getPlaneBitMask([self.config.saturatedMaskName,
609 self.config.suspectMaskName])
610 if numpy.all(maskView.getArray() & maskVal > 0):
611 self.log.warning("Amplifier %s is bad (completely SATURATED or SUSPECT)", ampName)
612 badAmpDict[ampName] = True
613 maskView |= maskView.getPlaneBitMask("BAD")
614
615 return badAmpDict
616
617 def overscanCorrection(self, mode, detectorConfig, detector, badAmpDict, ccdExposure):
618 """Apply serial overscan correction in place to all amps.
619
620 The actual overscan subtraction is performed by the
621 `lsst.ip.isr.overscan.OverscanTask`, which is called here.
622
623 Parameters
624 ----------
625 mode : `str`
626 Must be `SERIAL` or `PARALLEL`.
627 detectorConfig : `lsst.ip.isr.OverscanDetectorConfig`
628 Per-amplifier configurations.
629 detector : `lsst.afw.cameraGeom.Detector`
630 Detector object.
631 badAmpDict : `dict`
632 Dictionary of amp name to whether it is a bad amp.
633 ccdExposure : `lsst.afw.image.Exposure`
634 Exposure to have overscan correction performed.
635
636 Returns
637 -------
638 overscans : `list` [`lsst.pipe.base.Struct` or None]
639 Each result struct has components:
640
641 ``imageFit``
642 Value or fit subtracted from the amplifier image data.
643 (scalar or `lsst.afw.image.Image`)
644 ``overscanFit``
645 Value or fit subtracted from the overscan image data.
646 (scalar or `lsst.afw.image.Image`)
647 ``overscanImage``
648 Image of the overscan region with the overscan
649 correction applied. This quantity is used to estimate
650 the amplifier read noise empirically.
651 (`lsst.afw.image.Image`)
652 ``overscanMean``
653 Mean overscan fit value. (`float`)
654 ``overscanMedian``
655 Median overscan fit value. (`float`)
656 ``overscanSigma``
657 Clipped standard deviation of the overscan fit. (`float`)
658 ``residualMean``
659 Mean of the overscan after fit subtraction. (`float`)
660 ``residualMedian``
661 Median of the overscan after fit subtraction. (`float`)
662 ``residualSigma``
663 Clipped standard deviation of the overscan after fit
664 subtraction. (`float`)
665
666 See Also
667 --------
668 lsst.ip.isr.overscan.OverscanTask
669 """
670 if mode not in ["SERIAL", "PARALLEL"]:
671 raise ValueError("Mode must be SERIAL or PARALLEL")
672
673 # This returns a list in amp order, with None for uncorrected amps.
674 overscans = []
675
676 for i, amp in enumerate(detector):
677 ampName = amp.getName()
678
679 ampConfig = detectorConfig.getOverscanAmpConfig(ampName)
680
681 if mode == "SERIAL" and not ampConfig.doSerialOverscan:
682 self.log.debug(
683 "ISR_OSCAN: Amplifier %s/%s configured to skip serial overscan.",
684 detector.getName(),
685 ampName,
686 )
687 results = None
688 elif mode == "PARALLEL" and not ampConfig.doParallelOverscan:
689 self.log.debug(
690 "ISR_OSCAN: Amplifier %s configured to skip parallel overscan.",
691 detector.getName(),
692 ampName,
693 )
694 results = None
695 elif badAmpDict[ampName] or not ccdExposure.getBBox().contains(amp.getBBox()):
696 results = None
697 else:
698 # This check is to confirm that we are not trying to run
699 # overscan on an already trimmed image. Therefore, always
700 # checking just the horizontal overscan bounding box is
701 # sufficient.
702 if amp.getRawHorizontalOverscanBBox().isEmpty():
703 self.log.warning(
704 "ISR_OSCAN: No overscan region for amp %s. Not performing overscan correction.",
705 ampName,
706 )
707 results = None
708 else:
709 if mode == "SERIAL":
710 # We need to set up the subtask here with a custom
711 # configuration.
712 serialOverscan = SerialOverscanCorrectionTask(config=ampConfig.serialOverscanConfig)
713 results = serialOverscan.run(ccdExposure, amp)
714 else:
715 parallelOverscan = ParallelOverscanCorrectionTask(
716 config=ampConfig.parallelOverscanConfig,
717 )
718 results = parallelOverscan.run(ccdExposure, amp)
719
720 metadata = ccdExposure.getMetadata()
721 keyBase = "LSST ISR OVERSCAN"
722 metadata[f"{keyBase} {mode} MEAN {ampName}"] = results.overscanMean
723 metadata[f"{keyBase} {mode} MEDIAN {ampName}"] = results.overscanMedian
724 metadata[f"{keyBase} {mode} STDEV {ampName}"] = results.overscanSigma
725
726 metadata[f"{keyBase} RESIDUAL {mode} MEAN {ampName}"] = results.residualMean
727 metadata[f"{keyBase} RESIDUAL {mode} MEDIAN {ampName}"] = results.residualMedian
728 metadata[f"{keyBase} RESIDUAL {mode} STDEV {ampName}"] = results.residualSigma
729
730 overscans[i] = results
731
732 # Question: should this be finer grained?
733 ccdExposure.getMetadata().set("OVERSCAN", "Overscan corrected")
734
735 return overscans
736
737 def getLinearizer(self, detector):
738 # Here we assume linearizer as dict or LUT are not supported
739 # TODO DM 28741
740
741 # TODO construct isrcalib input
742 linearizer = linearize.Linearizer(detector=detector, log=self.log)
743 self.log.warning("Constructing linearizer from cameraGeom information.")
744
745 return linearizer
746
747 def gainNormalize(self, **kwargs):
748 # TODO DM 36639
749 gains = []
750 readNoise = []
751
752 return gains, readNoise
753
754 def updateVariance(self, ampExposure, amp, ptcDataset=None):
755 """Set the variance plane using the gain and read noise.
756
757 Parameters
758 ----------
759 ampExposure : `lsst.afw.image.Exposure`
760 Exposure to process.
761 amp : `lsst.afw.cameraGeom.Amplifier` or `FakeAmp`
762 Amplifier detector data.
763 ptcDataset : `lsst.ip.isr.PhotonTransferCurveDataset`, optional
764 PTC dataset containing the gains and read noise.
765
766 Raises
767 ------
768 RuntimeError
769 Raised if ptcDataset is not provided.
770
771 See also
772 --------
773 lsst.ip.isr.isrFunctions.updateVariance
774 """
775 # Get gains from PTC
776 if ptcDataset is None:
777 raise RuntimeError("No ptcDataset provided to use PTC gains.")
778 else:
779 gain = ptcDataset.gain[amp.getName()]
780 self.log.debug("Getting gain from Photon Transfer Curve.")
781
782 if math.isnan(gain):
783 gain = 1.0
784 self.log.warning("Gain set to NAN! Updating to 1.0 to generate Poisson variance.")
785 elif gain <= 0:
786 patchedGain = 1.0
787 self.log.warning("Gain for amp %s == %g <= 0; setting to %f.",
788 amp.getName(), gain, patchedGain)
789 gain = patchedGain
790
791 # Get read noise from PTC
792 if ptcDataset is None:
793 raise RuntimeError("No ptcDataset provided to use PTC readnoise.")
794 else:
795 readNoise = ptcDataset.noise[amp.getName()]
796 self.log.debug("Getting read noise from Photon Transfer Curve.")
797
798 metadata = ampExposure.getMetadata()
799 metadata[f'LSST GAIN {amp.getName()}'] = gain
800 metadata[f'LSST READNOISE {amp.getName()}'] = readNoise
801
802 isrFunctions.updateVariance(
803 maskedImage=ampExposure.getMaskedImage(),
804 gain=gain,
805 readNoise=readNoise,
806 )
807
808 def maskNegativeVariance(self, exposure):
809 """Identify and mask pixels with negative variance values.
810
811 Parameters
812 ----------
813 exposure : `lsst.afw.image.Exposure`
814 Exposure to process.
815
816 See Also
817 --------
818 lsst.ip.isr.isrFunctions.updateVariance
819 """
820 maskPlane = exposure.getMask().getPlaneBitMask(self.config.negativeVarianceMaskName)
821 bad = numpy.where(exposure.getVariance().getArray() <= 0.0)
822 exposure.mask.array[bad] |= maskPlane
823
824 def variancePlane(self, ccdExposure, ccd, ptc):
825 for amp in ccd:
826 if ccdExposure.getBBox().contains(amp.getBBox()):
827 self.log.debug("Constructing variance map for amplifer %s.", amp.getName())
828 ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
829
830 self.updateVariance(ampExposure, amp, ptcDataset=ptc)
831
832 if self.config.qa is not None and self.config.qa.saveStats is True:
833 qaStats = afwMath.makeStatistics(ampExposure.getVariance(),
834 afwMath.MEDIAN | afwMath.STDEVCLIP)
835 self.log.debug(" Variance stats for amplifer %s: %f +/- %f.",
836 amp.getName(), qaStats.getValue(afwMath.MEDIAN),
837 qaStats.getValue(afwMath.STDEVCLIP))
838 if self.config.maskNegativeVariance:
839 self.maskNegativeVariance(ccdExposure)
840
841 def maskDefect(self, exposure, defectBaseList):
842 """Mask defects using mask plane "BAD", in place.
843
844 Parameters
845 ----------
846 exposure : `lsst.afw.image.Exposure`
847 Exposure to process.
848
849 defectBaseList : defect-type
850 List of defects to mask. Can be of type `lsst.ip.isr.Defects`
851 or `list` of `lsst.afw.image.DefectBase`.
852 """
853 maskedImage = exposure.getMaskedImage()
854 if not isinstance(defectBaseList, Defects):
855 # Promotes DefectBase to Defect
856 defectList = Defects(defectBaseList)
857 else:
858 defectList = defectBaseList
859 defectList.maskPixels(maskedImage, maskName="BAD")
860
861 def maskEdges(self, exposure, numEdgePixels=0, maskPlane="SUSPECT", level='DETECTOR'):
862 """Mask edge pixels with applicable mask plane.
863
864 Parameters
865 ----------
866 exposure : `lsst.afw.image.Exposure`
867 Exposure to process.
868 numEdgePixels : `int`, optional
869 Number of edge pixels to mask.
870 maskPlane : `str`, optional
871 Mask plane name to use.
872 level : `str`, optional
873 Level at which to mask edges.
874 """
875 maskedImage = exposure.getMaskedImage()
876 maskBitMask = maskedImage.getMask().getPlaneBitMask(maskPlane)
877
878 if numEdgePixels > 0:
879 if level == 'DETECTOR':
880 boxes = [maskedImage.getBBox()]
881 elif level == 'AMP':
882 boxes = [amp.getBBox() for amp in exposure.getDetector()]
883
884 for box in boxes:
885 # This makes a bbox numEdgeSuspect pixels smaller than the
886 # image on each side
887 subImage = maskedImage[box]
888 box.grow(-numEdgePixels)
889 # Mask pixels outside box
890 SourceDetectionTask.setEdgeBits(
891 subImage,
892 box,
893 maskBitMask)
894
895 def maskNan(self, exposure):
896 """Mask NaNs using mask plane "UNMASKEDNAN", in place.
897
898 Parameters
899 ----------
900 exposure : `lsst.afw.image.Exposure`
901 Exposure to process.
902
903 Notes
904 -----
905 We mask over all non-finite values (NaN, inf), including those
906 that are masked with other bits (because those may or may not be
907 interpolated over later, and we want to remove all NaN/infs).
908 Despite this behaviour, the "UNMASKEDNAN" mask plane is used to
909 preserve the historical name.
910 """
911 maskedImage = exposure.getMaskedImage()
912
913 # Find and mask NaNs
914 maskedImage.getMask().addMaskPlane("UNMASKEDNAN")
915 maskVal = maskedImage.getMask().getPlaneBitMask("UNMASKEDNAN")
916 numNans = maskNans(maskedImage, maskVal)
917 self.metadata["NUMNANS"] = numNans
918 if numNans > 0:
919 self.log.warning("There were %d unmasked NaNs.", numNans)
920
921 def countBadPixels(self, exposure):
922 """
923 Notes
924 -----
925 Reset and interpolate bad pixels.
926
927 Large contiguous bad regions (which should have the BAD mask
928 bit set) should have their values set to the image median.
929 This group should include defects and bad amplifiers. As the
930 area covered by these defects are large, there's little
931 reason to expect that interpolation would provide a more
932 useful value.
933
934 Smaller defects can be safely interpolated after the larger
935 regions have had their pixel values reset. This ensures
936 that the remaining defects adjacent to bad amplifiers (as an
937 example) do not attempt to interpolate extreme values.
938 """
939 badPixelCount, badPixelValue = isrFunctions.setBadRegions(exposure)
940 if badPixelCount > 0:
941 self.log.info("Set %d BAD pixels to %f.", badPixelCount, badPixelValue)
942
943 @contextmanager
944 def flatContext(self, exp, flat, dark=None):
945 """Context manager that applies and removes flats and darks,
946 if the task is configured to apply them.
947
948 Parameters
949 ----------
950 exp : `lsst.afw.image.Exposure`
951 Exposure to process.
952 flat : `lsst.afw.image.Exposure`
953 Flat exposure the same size as ``exp``.
954 dark : `lsst.afw.image.Exposure`, optional
955 Dark exposure the same size as ``exp``.
956
957 Yields
958 ------
959 exp : `lsst.afw.image.Exposure`
960 The flat and dark corrected exposure.
961 """
962 if self.config.doDark and dark is not None:
963 self.darkCorrection(exp, dark)
964 if self.config.doFlat:
965 self.flatCorrection(exp, flat)
966 try:
967 yield exp
968 finally:
969 if self.config.doFlat:
970 self.flatCorrection(exp, flat, invert=True)
971 if self.config.doDark and dark is not None:
972 self.darkCorrection(exp, dark, invert=True)
973
974 def getBrighterFatterKernel(self, detector, bfKernel):
975 detName = detector.getName()
976
977 # This is expected to be a dictionary of amp-wise gains.
978 bfGains = bfKernel.gain
979 if bfKernel.level == 'DETECTOR':
980 if detName in bfKernel.detKernels:
981 bfKernelOut = bfKernel.detKernels[detName]
982 return bfKernelOut, bfGains
983 else:
984 raise RuntimeError("Failed to extract kernel from new-style BF kernel.")
985 elif bfKernel.level == 'AMP':
986 self.log.warning("Making DETECTOR level kernel from AMP based brighter "
987 "fatter kernels.")
988 bfKernel.makeDetectorKernelFromAmpwiseKernels(detName)
989 bfKernelOut = bfKernel.detKernels[detName]
990 return bfKernelOut, bfGains
991
992 def applyBrighterFatterCorrection(self, ccdExposure, flat, dark, bfKernel, bfGains):
993 # We need to apply flats and darks before we can interpolate, and
994 # we need to interpolate before we do B-F, but we do B-F without
995 # the flats and darks applied so we can work in units of electrons
996 # or holes. This context manager applies and then removes the darks
997 # and flats.
998 #
999 # We also do not want to interpolate values here, so operate on
1000 # temporary images so we can apply only the BF-correction and roll
1001 # back the interpolation.
1002 # This won't be necessary once the gain normalization
1003 # is done appropriately.
1004 interpExp = ccdExposure.clone()
1005 with self.flatContext(interpExp, flat, dark):
1006 isrFunctions.interpolateFromMask(
1007 maskedImage=interpExp.getMaskedImage(),
1008 fwhm=self.config.brighterFatterFwhmForInterpolation,
1009 growSaturatedFootprints=self.config.growSaturationFootprintSize,
1010 maskNameList=list(self.config.brighterFatterMaskListToInterpolate)
1011 )
1012 bfExp = interpExp.clone()
1013 self.log.info("Applying brighter-fatter correction using kernel type %s / gains %s.",
1014 type(bfKernel), type(bfGains))
1015 bfResults = isrFunctions.brighterFatterCorrection(bfExp, bfKernel,
1016 self.config.brighterFatterMaxIter,
1017 self.config.brighterFatterThreshold,
1018 self.config.brighterFatterApplyGain,
1019 bfGains)
1020 if bfResults[1] == self.config.brighterFatterMaxIter:
1021 self.log.warning("Brighter-fatter correction did not converge, final difference %f.",
1022 bfResults[0])
1023 else:
1024 self.log.info("Finished brighter-fatter correction in %d iterations.",
1025 bfResults[1])
1026
1027 image = ccdExposure.getMaskedImage().getImage()
1028 bfCorr = bfExp.getMaskedImage().getImage()
1029 bfCorr -= interpExp.getMaskedImage().getImage()
1030 image += bfCorr
1031
1032 # Applying the brighter-fatter correction applies a
1033 # convolution to the science image. At the edges this
1034 # convolution may not have sufficient valid pixels to
1035 # produce a valid correction. Mark pixels within the size
1036 # of the brighter-fatter kernel as EDGE to warn of this
1037 # fact.
1038 self.log.info("Ensuring image edges are masked as EDGE to the brighter-fatter kernel size.")
1039 self.maskEdges(ccdExposure, numEdgePixels=numpy.max(bfKernel.shape) // 2,
1040 maskPlane="EDGE")
1041
1042 if self.config.brighterFatterMaskGrowSize > 0:
1043 self.log.info("Growing masks to account for brighter-fatter kernel convolution.")
1044 for maskPlane in self.config.brighterFatterMaskListToInterpolate:
1045 isrFunctions.growMasks(ccdExposure.getMask(),
1046 radius=self.config.brighterFatterMaskGrowSize,
1047 maskNameList=maskPlane,
1048 maskValue=maskPlane)
1049
1050 return ccdExposure
1051
1052 def darkCorrection(self, exposure, darkExposure, invert=False):
1053 """Apply dark correction in place.
1054
1055 Parameters
1056 ----------
1057 exposure : `lsst.afw.image.Exposure`
1058 Exposure to process.
1059 darkExposure : `lsst.afw.image.Exposure`
1060 Dark exposure of the same size as ``exposure``.
1061 invert : `Bool`, optional
1062 If True, re-add the dark to an already corrected image.
1063
1064 Raises
1065 ------
1066 RuntimeError
1067 Raised if either ``exposure`` or ``darkExposure`` do not
1068 have their dark time defined.
1069
1070 See Also
1071 --------
1072 lsst.ip.isr.isrFunctions.darkCorrection
1073 """
1074 expScale = exposure.getInfo().getVisitInfo().getDarkTime()
1075 if math.isnan(expScale):
1076 raise RuntimeError("Exposure darktime is NAN.")
1077 if darkExposure.getInfo().getVisitInfo() is not None \
1078 and not math.isnan(darkExposure.getInfo().getVisitInfo().getDarkTime()):
1079 darkScale = darkExposure.getInfo().getVisitInfo().getDarkTime()
1080 else:
1081 # DM-17444: darkExposure.getInfo.getVisitInfo() is None
1082 # so getDarkTime() does not exist.
1083 self.log.warning("darkExposure.getInfo().getVisitInfo() does not exist. Using darkScale = 1.0.")
1084 darkScale = 1.0
1085
1086 isrFunctions.darkCorrection(
1087 maskedImage=exposure.getMaskedImage(),
1088 darkMaskedImage=darkExposure.getMaskedImage(),
1089 expScale=expScale,
1090 darkScale=darkScale,
1091 invert=invert,
1092 )
1093
1094 @staticmethod
1096 """Extract common calibration metadata values that will be written to
1097 output header.
1098
1099 Parameters
1100 ----------
1101 calib : `lsst.afw.image.Exposure` or `lsst.ip.isr.IsrCalib`
1102 Calibration to pull date information from.
1103
1104 Returns
1105 -------
1106 dateString : `str`
1107 Calibration creation date string to add to header.
1108 """
1109 if hasattr(calib, "getMetadata"):
1110 if 'CALIB_CREATION_DATE' in calib.getMetadata():
1111 return " ".join((calib.getMetadata().get("CALIB_CREATION_DATE", "Unknown"),
1112 calib.getMetadata().get("CALIB_CREATION_TIME", "Unknown")))
1113 else:
1114 return " ".join((calib.getMetadata().get("CALIB_CREATE_DATE", "Unknown"),
1115 calib.getMetadata().get("CALIB_CREATE_TIME", "Unknown")))
1116 else:
1117 return "Unknown Unknown"
1118
1119 def doLinearize(self, detector):
1120 """Check if linearization is needed for the detector cameraGeom.
1121
1122 Checks config.doLinearize and the linearity type of the first
1123 amplifier.
1124
1125 Parameters
1126 ----------
1127 detector : `lsst.afw.cameraGeom.Detector`
1128 Detector to get linearity type from.
1129
1130 Returns
1131 -------
1132 doLinearize : `Bool`
1133 If True, linearization should be performed.
1134 """
1135 return self.config.doLinearize and \
1136 detector.getAmplifiers()[0].getLinearityType() != NullLinearityType
1137
1138 def makeBinnedImages(self, exposure):
1139 """Make visualizeVisit style binned exposures.
1140
1141 Parameters
1142 ----------
1143 exposure : `lsst.afw.image.Exposure`
1144 Exposure to bin.
1145
1146 Returns
1147 -------
1148 bin1 : `lsst.afw.image.Exposure`
1149 Binned exposure using binFactor1.
1150 bin2 : `lsst.afw.image.Exposure`
1151 Binned exposure using binFactor2.
1152 """
1153 mi = exposure.getMaskedImage()
1154
1155 bin1 = afwMath.binImage(mi, self.config.binFactor1)
1156 bin2 = afwMath.binImage(mi, self.config.binFactor2)
1157
1158 return bin1, bin2
1159
1160 def run(self, *, ccdExposure, dnlLUT=None, bias=None, deferredChargeCalib=None, linearizer=None,
1161 ptc=None, crosstalk=None, defects=None, bfKernel=None, bfGains=None, dark=None,
1162 flat=None, camera=None, **kwargs
1163 ):
1164
1165 detector = ccdExposure.getDetector()
1166
1167 overscanDetectorConfig = self.config.overscanCamera.getOverscanDetectorConfig(detector)
1168
1169 if self.config.doHeaderProvenance:
1170 # Inputs have been validated, so we can add their date
1171 # information to the output header.
1172 exposureMetadata = ccdExposure.getMetadata()
1173 exposureMetadata["LSST CALIB OVERSCAN HASH"] = overscanDetectorConfig.md5
1174 exposureMetadata["LSST CALIB DATE PTC"] = self.extractCalibDate(ptc)
1175 if self.config.doDiffNonLinearCorrection:
1176 exposureMetadata["LSST CALIB DATE DNL"] = self.extractCalibDate(dnlLUT)
1177 if self.config.doBias:
1178 exposureMetadata["LSST CALIB DATE BIAS"] = self.extractCalibDate(bias)
1179 if self.config.doDeferredCharge:
1180 exposureMetadata["LSST CALIB DATE CTI"] = self.extractCalibDate(deferredChargeCalib)
1181 if self.doLinearize(detector):
1182 exposureMetadata["LSST CALIB DATE LINEARIZER"] = self.extractCalibDate(linearizer)
1183 if self.config.doCrosstalk or overscanDetectorConfig.doAnyParallelOverscanCrosstalk:
1184 exposureMetadata["LSST CALIB DATE CROSSTALK"] = self.extractCalibDate(crosstalk)
1185 if self.config.doDefect:
1186 exposureMetadata["LSST CALIB DATE DEFECTS"] = self.extractCalibDate(defects)
1187 if self.config.doBrighterFatter:
1188 exposureMetadata["LSST CALIB DATE BFK"] = self.extractCalibDate(bfKernel)
1189 if self.config.doDark:
1190 exposureMetadata["LSST CALIB DATE DARK"] = self.extractCalibDate(dark)
1191
1192 # First we mark which amplifiers are completely bad from defects.
1193 badAmpDict = self.maskFullDefectAmplifiers(ccdExposure, detector, defects)
1194
1195 if self.config.doDiffNonLinearCorrection:
1196 self.diffNonLinearCorrection(ccdExposure, dnlLUT)
1197
1198 if overscanDetectorConfig.doAnySerialOverscan:
1199 # Input units: ADU
1200 serialOverscans = self.overscanCorrection(
1201 "SERIAL",
1202 overscanDetectorConfig,
1203 detector,
1204 badAmpDict,
1205 ccdExposure,
1206 )
1207 else:
1208 serialOverscans = [None]*len(detector)
1209
1210 if overscanDetectorConfig.doAnyParallelOverscanCrosstalk:
1211 # Input units: ADU
1212 # Make sure that the units here are consistent with later
1213 # application.
1214 self.crosstalk.run(
1215 ccdExposure,
1216 crosstalk=crosstalk,
1217 camera=camera,
1218 parallelOverscanRegion=True,
1219 detectorConfig=overscanDetectorConfig,
1220 )
1221
1222 # After serial overscan correction, we can mask SATURATED and
1223 # SUSPECT pixels. This updates badAmpDict if any amplifier
1224 # is fully saturated after serial overscan correction.
1225 badAmpDict = self.maskSaturatedPixels(badAmpDict, ccdExposure, detector)
1226
1227 if overscanDetectorConfig.doAnyParallelOverscan:
1228 # Input units: ADU
1229 # At the moment we do not use the parallelOverscans return value.
1230 _ = self.overscanCorrection(
1231 "PARALLEL",
1232 overscanDetectorConfig,
1233 detector,
1234 badAmpDict,
1235 ccdExposure,
1236 )
1237
1238 if self.config.doAssembleCcd:
1239 # Input units: ADU
1240 self.log.info("Assembling CCD from amplifiers.")
1241 ccdExposure = self.assembleCcd.assembleCcd(ccdExposure)
1242
1243 if self.config.expectWcs and not ccdExposure.getWcs():
1244 self.log.warning("No WCS found in input exposure.")
1245
1246 if self.config.doLinearize:
1247 # Input units: ADU
1248 self.log.info("Applying linearizer.")
1249 linearizer = self.getLinearizer(detector=detector)
1250 linearizer.applyLinearity(image=ccdExposure.getMaskedImage().getImage(),
1251 detector=detector, log=self.log)
1252
1253 if self.config.doCrosstalk:
1254 # Input units: ADU
1255 self.log.info("Applying crosstalk correction.")
1256 self.crosstalk.run(ccdExposure, crosstalk=crosstalk)
1257
1258 if self.config.doBias:
1259 # Input units: ADU
1260 self.log.info("Applying bias correction.")
1261 isrFunctions.biasCorrection(ccdExposure.getMaskedImage(), bias.getMaskedImage())
1262
1263 if self.config.doGainNormalize:
1264 # Input units: ADU
1265 # Output units: electrons
1266 # TODO DM 36639
1267 gains, readNoise = self.gainNormalize(**kwargs)
1268
1269 if self.config.doDeferredCharge:
1270 # Input units: electrons
1271 self.log.info("Applying deferred charge/CTI correction.")
1272 self.deferredChargeCorrection.run(ccdExposure, deferredChargeCalib)
1273
1274 if self.config.doVariance:
1275 # Input units: electrons
1276 self.variancePlane(ccdExposure, detector, ptc)
1277
1278 # Masking block (defects, NAN pixels and trails).
1279 # Saturated and suspect pixels have already been masked.
1280 if self.config.doDefect:
1281 # Input units: electrons
1282 self.log.info("Applying defects masking.")
1283 self.maskDefect(ccdExposure, defects)
1284
1285 if self.config.doNanMasking:
1286 self.log.info("Masking non-finite (NAN, inf) value pixels.")
1287 self.maskNan(ccdExposure)
1288
1289 if self.config.doWidenSaturationTrails:
1290 self.log.info("Widening saturation trails.")
1291 isrFunctions.widenSaturationTrails(ccdExposure.getMaskedImage().getMask())
1292
1293 preInterpExp = None
1294 if self.config.doSaveInterpPixels:
1295 preInterpExp = ccdExposure.clone()
1296
1297 if self.config.doSetBadRegions:
1298 self.log.info('Counting pixels in BAD regions.')
1299 self.countBadPixels(ccdExposure)
1300
1301 if self.config.doInterpolate:
1302 self.log.info("Interpolating masked pixels.")
1303 isrFunctions.interpolateFromMask(
1304 maskedImage=ccdExposure.getMaskedImage(),
1305 fwhm=self.config.brighterFatterFwhmForInterpolation,
1306 growSaturatedFootprints=self.config.growSaturationFootprintSize,
1307 maskNameList=list(self.config.maskListToInterpolate)
1308 )
1309
1310 if self.config.doDark:
1311 # Input units: electrons
1312 self.log.info("Applying dark subtraction.")
1313 self.darkCorrection(ccdExposure, dark)
1314
1315 if self.config.doBrighterFatter:
1316 # Input units: electrons
1317 self.log.info("Applying Bright-Fatter kernels.")
1318 bfKernelOut, bfGains = self.getBrighterFatterKernel(detector, bfKernel)
1319 ccdExposure = self.applyBrighterFatterCorrection(ccdExposure, flat, dark, bfKernelOut, bfGains)
1320
1321 # Calculate standard image quality statistics
1322 if self.config.doStandardStatistics:
1323 metadata = ccdExposure.getMetadata()
1324 for amp in detector:
1325 ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
1326 ampName = amp.getName()
1327 metadata[f"LSST ISR MASK SAT {ampName}"] = isrFunctions.countMaskedPixels(
1328 ampExposure.getMaskedImage(),
1329 [self.config.saturatedMaskName]
1330 )
1331 metadata[f"LSST ISR MASK BAD {ampName}"] = isrFunctions.countMaskedPixels(
1332 ampExposure.getMaskedImage(),
1333 ["BAD"]
1334 )
1335 qaStats = afwMath.makeStatistics(ampExposure.getImage(),
1336 afwMath.MEAN | afwMath.MEDIAN | afwMath.STDEVCLIP)
1337
1338 metadata[f"LSST ISR FINAL MEAN {ampName}"] = qaStats.getValue(afwMath.MEAN)
1339 metadata[f"LSST ISR FINAL MEDIAN {ampName}"] = qaStats.getValue(afwMath.MEDIAN)
1340 metadata[f"LSST ISR FINAL STDEV {ampName}"] = qaStats.getValue(afwMath.STDEVCLIP)
1341
1342 k1 = f"LSST ISR FINAL MEDIAN {ampName}"
1343 k2 = f"LSST ISR OVERSCAN SERIAL MEDIAN {ampName}"
1344 if overscanDetectorConfig.doAnySerialOverscan and k1 in metadata and k2 in metadata:
1345 metadata[f"LSST ISR LEVEL {ampName}"] = metadata[k1] - metadata[k2]
1346 else:
1347 metadata[f"LSST ISR LEVEL {ampName}"] = numpy.nan
1348
1349 # calculate additional statistics.
1350 outputStatistics = None
1351 if self.config.doCalculateStatistics:
1352 outputStatistics = self.isrStats.run(ccdExposure, overscanResults=serialOverscans,
1353 ptc=ptc).results
1354
1355 # do image binning.
1356 outputBin1Exposure = None
1357 outputBin2Exposure = None
1358 if self.config.doBinnedExposures:
1359 outputBin1Exposure, outputBin2Exposure = self.makeBinnedImages(ccdExposure)
1360
1361 return pipeBase.Struct(
1362 exposure=ccdExposure,
1363
1364 outputBin1Exposure=outputBin1Exposure,
1365 outputBin2Exposure=outputBin2Exposure,
1366
1367 preInterpExposure=preInterpExp,
1368 outputExposure=ccdExposure,
1369 outputStatistics=outputStatistics,
1370 )
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)