lsst.ip.isr  7.9-hsc
isrTask.py
Go to the documentation of this file.
1 # This file is part of ip_isr.
2 #
3 # Developed for the LSST Data Management System.
4 # This product includes software developed by the LSST Project
5 # (https://www.lsst.org).
6 # See the COPYRIGHT file at the top-level directory of this distribution
7 # for details of code ownership.
8 #
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <https://www.gnu.org/licenses/>.
21 
22 import math
23 import numpy
24 
25 import lsst.afw.geom as afwGeom
26 import lsst.afw.image as afwImage
27 import lsst.afw.math as afwMath
28 import lsst.afw.table as afwTable
29 import lsst.meas.algorithms as measAlg
30 import lsst.pex.config as pexConfig
31 import lsst.pipe.base as pipeBase
32 
33 from contextlib import contextmanager
34 from lsstDebug import getDebugFrame
35 
36 from lsst.afw.cameraGeom import PIXELS, FOCAL_PLANE, NullLinearityType
37 from lsst.afw.display import getDisplay
38 from lsst.afw.geom import Polygon
39 from lsst.daf.persistence import ButlerDataRef
40 from lsst.daf.persistence.butler import NoResults
41 from lsst.meas.algorithms.detection import SourceDetectionTask
42 from lsst.meas.algorithms import Defect
43 
44 from . import isrFunctions
45 from . import isrQa
46 from . import linearize
47 
48 from .assembleCcdTask import AssembleCcdTask
49 from .crosstalk import CrosstalkTask
50 from .fringe import FringeTask
51 from .isr import maskNans
52 from .masking import MaskingTask
53 from .straylight import StrayLightTask
54 from .vignette import VignetteTask
55 
56 __all__ = ["IsrTask", "RunIsrTask"]
57 
58 
59 class IsrTaskConfig(pexConfig.Config):
60  """Configuration parameters for IsrTask.
61 
62  Items are grouped in the order in which they are executed by the task.
63  """
64  # General ISR configuration
65 
66  # gen3 options
67  isrName = pexConfig.Field(
68  dtype=str,
69  doc="Name of ISR",
70  default="ISR",
71  )
72 
73  # input datasets
74  ccdExposure = pipeBase.InputDatasetField(
75  doc="Input exposure to process",
76  name="raw",
77  scalar=True,
78  storageClass="ExposureU",
79  dimensions=["Instrument", "Exposure", "Detector"],
80  )
81  camera = pipeBase.InputDatasetField(
82  doc="Input camera to construct complete exposures.",
83  name="camera",
84  scalar=True,
85  storageClass="TablePersistableCamera",
86  dimensions=["Instrument", "CalibrationLabel"],
87  )
88  bias = pipeBase.InputDatasetField(
89  doc="Input bias calibration.",
90  name="bias",
91  scalar=True,
92  storageClass="ImageF",
93  dimensions=["Instrument", "CalibrationLabel", "Detector"],
94  )
95  dark = pipeBase.InputDatasetField(
96  doc="Input dark calibration.",
97  name="dark",
98  scalar=True,
99  storageClass="ImageF",
100  dimensions=["Instrument", "CalibrationLabel", "Detector"],
101  )
102  flat = pipeBase.InputDatasetField(
103  doc="Input flat calibration.",
104  name="flat",
105  scalar=True,
106  storageClass="MaskedImageF",
107  dimensions=["Instrument", "PhysicalFilter", "CalibrationLabel", "Detector"],
108  )
109  bfKernel = pipeBase.InputDatasetField(
110  doc="Input brighter-fatter kernel.",
111  name="bfKernel",
112  scalar=True,
113  storageClass="NumpyArray",
114  dimensions=["Instrument", "CalibrationLabel"],
115  )
116  defects = pipeBase.InputDatasetField(
117  doc="Input defect tables.",
118  name="defects",
119  scalar=True,
120  storageClass="Catalog",
121  dimensions=["Instrument", "CalibrationLabel", "Detector"],
122  )
123  opticsTransmission = pipeBase.InputDatasetField(
124  doc="Transmission curve due to the optics.",
125  name="transmission_optics",
126  scalar=True,
127  storageClass="TablePersistableTransmissionCurve",
128  dimensions=["Instrument", "CalibrationLabel"],
129  )
130  filterTransmission = pipeBase.InputDatasetField(
131  doc="Transmission curve due to the filter.",
132  name="transmission_filter",
133  scalar=True,
134  storageClass="TablePersistableTransmissionCurve",
135  dimensions=["Instrument", "PhysicalFilter", "CalibrationLabel"],
136  )
137  sensorTransmission = pipeBase.InputDatasetField(
138  doc="Transmission curve due to the sensor.",
139  name="transmission_sensor",
140  scalar=True,
141  storageClass="TablePersistableTransmissionCurve",
142  dimensions=["Instrument", "CalibrationLabel", "Detector"],
143  )
144  atmosphereTransmission = pipeBase.InputDatasetField(
145  doc="Transmission curve due to the atmosphere.",
146  name="transmission_atmosphere",
147  scalar=True,
148  storageClass="TablePersistableTransmissionCurve",
149  dimensions=["Instrument"],
150  )
151 
152  # output datasets
153  outputExposure = pipeBase.OutputDatasetField(
154  doc="Output ISR processed exposure.",
155  name="postISRCCD",
156  scalar=True,
157  storageClass="ExposureF",
158  dimensions=["Instrument", "Visit", "Detector"],
159  )
160  outputOssThumbnail = pipeBase.OutputDatasetField(
161  doc="Output Overscan-subtracted thumbnail image.",
162  name="OssThumb",
163  scalar=True,
164  storageClass="Thumbnail",
165  dimensions=["Instrument", "Visit", "Detector"],
166  )
167  outputFlattenedThumbnail = pipeBase.OutputDatasetField(
168  doc="Output flat-corrected thumbnail image.",
169  name="FlattenedThumb",
170  scalar=True,
171  storageClass="TextStorage",
172  dimensions=["Instrument", "Visit", "Detector"],
173  )
174 
175  quantum = pipeBase.QuantumConfig(
176  dimensions=["Visit", "Detector", "Instrument"],
177  )
178 
179 
180  datasetType = pexConfig.Field(
181  dtype=str,
182  doc="Dataset type for input data; users will typically leave this alone, "
183  "but camera-specific ISR tasks will override it",
184  default="raw",
185  )
186 
187  fallbackFilterName = pexConfig.Field(
188  dtype=str,
189  doc="Fallback default filter name for calibrations.",
190  optional=True
191  )
192  expectWcs = pexConfig.Field(
193  dtype=bool,
194  default=True,
195  doc="Expect input science images to have a WCS (set False for e.g. spectrographs)."
196  )
197  fwhm = pexConfig.Field(
198  dtype=float,
199  doc="FWHM of PSF in arcseconds.",
200  default=1.0,
201  )
202  qa = pexConfig.ConfigField(
203  dtype=isrQa.IsrQaConfig,
204  doc="QA related configuration options.",
205  )
206 
207  # Image conversion configuration
208  doConvertIntToFloat = pexConfig.Field(
209  dtype=bool,
210  doc="Convert integer raw images to floating point values?",
211  default=True,
212  )
213 
214  # Saturated pixel handling.
215  doSaturation = pexConfig.Field(
216  dtype=bool,
217  doc="Mask saturated pixels? NB: this is totally independent of the"
218  " interpolation option - this is ONLY setting the bits in the mask."
219  " To have them interpolated make sure doSaturationInterpolation=True",
220  default=True,
221  )
222  saturatedMaskName = pexConfig.Field(
223  dtype=str,
224  doc="Name of mask plane to use in saturation detection and interpolation",
225  default="SAT",
226  )
227  saturation = pexConfig.Field(
228  dtype=float,
229  doc="The saturation level to use if no Detector is present in the Exposure (ignored if NaN)",
230  default=float("NaN"),
231  )
232  growSaturationFootprintSize = pexConfig.Field(
233  dtype=int,
234  doc="Number of pixels by which to grow the saturation footprints",
235  default=1,
236  )
237 
238  # Suspect pixel handling.
239  doSuspect = pexConfig.Field(
240  dtype=bool,
241  doc="Mask suspect pixels?",
242  default=True,
243  )
244  suspectMaskName = pexConfig.Field(
245  dtype=str,
246  doc="Name of mask plane to use for suspect pixels",
247  default="SUSPECT",
248  )
249  numEdgeSuspect = pexConfig.Field(
250  dtype=int,
251  doc="Number of edge pixels to be flagged as untrustworthy.",
252  default=0,
253  )
254 
255  # Initial masking options.
256  doSetBadRegions = pexConfig.Field(
257  dtype=bool,
258  doc="Should we set the level of all BAD patches of the chip to the chip's average value?",
259  default=True,
260  )
261  badStatistic = pexConfig.ChoiceField(
262  dtype=str,
263  doc="How to estimate the average value for BAD regions.",
264  default='MEANCLIP',
265  allowed={
266  "MEANCLIP": "Correct using the (clipped) mean of good data",
267  "MEDIAN": "Correct using the median of the good data",
268  },
269  )
270 
271  # Overscan subtraction configuration.
272  doOverscan = pexConfig.Field(
273  dtype=bool,
274  doc="Do overscan subtraction?",
275  default=True,
276  )
277  overscanFitType = pexConfig.ChoiceField(
278  dtype=str,
279  doc="The method for fitting the overscan bias level.",
280  default='MEDIAN',
281  allowed={
282  "POLY": "Fit ordinary polynomial to the longest axis of the overscan region",
283  "CHEB": "Fit Chebyshev polynomial to the longest axis of the overscan region",
284  "LEG": "Fit Legendre polynomial to the longest axis of the overscan region",
285  "NATURAL_SPLINE": "Fit natural spline to the longest axis of the overscan region",
286  "CUBIC_SPLINE": "Fit cubic spline to the longest axis of the overscan region",
287  "AKIMA_SPLINE": "Fit Akima spline to the longest axis of the overscan region",
288  "MEAN": "Correct using the mean of the overscan region",
289  "MEANCLIP": "Correct using a clipped mean of the overscan region",
290  "MEDIAN": "Correct using the median of the overscan region",
291  },
292  )
293  overscanOrder = pexConfig.Field(
294  dtype=int,
295  doc=("Order of polynomial or to fit if overscan fit type is a polynomial, " +
296  "or number of spline knots if overscan fit type is a spline."),
297  default=1,
298  )
299  overscanNumSigmaClip = pexConfig.Field(
300  dtype=float,
301  doc="Rejection threshold (sigma) for collapsing overscan before fit",
302  default=3.0,
303  )
304  overscanIsInt = pexConfig.Field(
305  dtype=bool,
306  doc="Treat overscan as an integer image for purposes of overscan.FitType=MEDIAN",
307  default=True,
308  )
309  overscanNumLeadingColumnsToSkip = pexConfig.Field(
310  dtype=int,
311  doc="Number of columns to skip in overscan, i.e. those closest to amplifier",
312  default=0,
313  )
314  overscanNumTrailingColumnsToSkip = pexConfig.Field(
315  dtype=int,
316  doc="Number of columns to skip in overscan, i.e. those farthest from amplifier",
317  default=0,
318  )
319  overscanMaxDev = pexConfig.Field(
320  dtype=float,
321  doc="Maximum deviation from the median for overscan",
322  default=1000.0, check=lambda x: x > 0
323  )
324  overscanBiasJump = pexConfig.Field(
325  dtype=bool,
326  doc="Fit the overscan in a piecewise-fashion to correct for bias jumps?",
327  default=False,
328  )
329  overscanBiasJumpKeyword = pexConfig.Field(
330  dtype=str,
331  doc="Header keyword containing information about devices.",
332  default="NO_SUCH_KEY",
333  )
334  overscanBiasJumpDevices = pexConfig.ListField(
335  dtype=str,
336  doc="List of devices that need piecewise overscan correction.",
337  default=(),
338  )
339  overscanBiasJumpLocation = pexConfig.Field(
340  dtype=int,
341  doc="Location of bias jump along y-axis.",
342  default=0,
343  )
344 
345  # Amplifier to CCD assembly configuration
346  doAssembleCcd = pexConfig.Field(
347  dtype=bool,
348  default=True,
349  doc="Assemble amp-level exposures into a ccd-level exposure?"
350  )
351  assembleCcd = pexConfig.ConfigurableField(
352  target=AssembleCcdTask,
353  doc="CCD assembly task",
354  )
355 
356  # General calibration configuration.
357  doAssembleIsrExposures = pexConfig.Field(
358  dtype=bool,
359  default=False,
360  doc="Assemble amp-level calibration exposures into ccd-level exposure?"
361  )
362  doTrimToMatchCalib = pexConfig.Field(
363  dtype=bool,
364  default=False,
365  doc="Trim raw data to match calibration bounding boxes?"
366  )
367 
368  # Bias subtraction.
369  doBias = pexConfig.Field(
370  dtype=bool,
371  doc="Apply bias frame correction?",
372  default=True,
373  )
374  biasDataProductName = pexConfig.Field(
375  dtype=str,
376  doc="Name of the bias data product",
377  default="bias",
378  )
379 
380  # Variance construction
381  doVariance = pexConfig.Field(
382  dtype=bool,
383  doc="Calculate variance?",
384  default=True
385  )
386  gain = pexConfig.Field(
387  dtype=float,
388  doc="The gain to use if no Detector is present in the Exposure (ignored if NaN)",
389  default=float("NaN"),
390  )
391  readNoise = pexConfig.Field(
392  dtype=float,
393  doc="The read noise to use if no Detector is present in the Exposure",
394  default=0.0,
395  )
396  doEmpiricalReadNoise = pexConfig.Field(
397  dtype=bool,
398  default=False,
399  doc="Calculate empirical read noise instead of value from AmpInfo data?"
400  )
401 
402  # Linearization.
403  doLinearize = pexConfig.Field(
404  dtype=bool,
405  doc="Correct for nonlinearity of the detector's response?",
406  default=True,
407  )
408 
409  # Crosstalk.
410  doCrosstalk = pexConfig.Field(
411  dtype=bool,
412  doc="Apply intra-CCD crosstalk correction?",
413  default=False,
414  )
415  doCrosstalkBeforeAssemble = pexConfig.Field(
416  dtype=bool,
417  doc="Apply crosstalk correction before CCD assembly, and before trimming?",
418  default=True,
419  )
420  crosstalk = pexConfig.ConfigurableField(
421  target=CrosstalkTask,
422  doc="Intra-CCD crosstalk correction",
423  )
424 
425  # Masking options.
426  doDefect = pexConfig.Field(
427  dtype=bool,
428  doc="Apply correction for CCD defects, e.g. hot pixels?",
429  default=True,
430  )
431  numEdgeSuspect = pexConfig.Field(
432  dtype=int,
433  doc="Number of edge pixels to be flagged as untrustworthy.",
434  default=0,
435  )
436  doNanMasking = pexConfig.Field(
437  dtype=bool,
438  doc="Mask NAN pixels?",
439  default=True,
440  )
441  doWidenSaturationTrails = pexConfig.Field(
442  dtype=bool,
443  doc="Widen bleed trails based on their width?",
444  default=True
445  )
446 
447  # Brighter-Fatter correction.
448  doBrighterFatter = pexConfig.Field(
449  dtype=bool,
450  default=False,
451  doc="Apply the brighter fatter correction"
452  )
453  brighterFatterLevel = pexConfig.ChoiceField(
454  dtype=str,
455  default="DETECTOR",
456  doc="The level at which to correct for brighter-fatter.",
457  allowed={
458  "AMP": "Every amplifier treated separately.",
459  "DETECTOR": "One kernel per detector",
460  }
461  )
462  brighterFatterKernelFile = pexConfig.Field(
463  dtype=str,
464  default='',
465  doc="Kernel file used for the brighter fatter correction"
466  )
467  brighterFatterMaxIter = pexConfig.Field(
468  dtype=int,
469  default=10,
470  doc="Maximum number of iterations for the brighter fatter correction"
471  )
472  brighterFatterThreshold = pexConfig.Field(
473  dtype=float,
474  default=1000,
475  doc="Threshold used to stop iterating the brighter fatter correction. It is the "
476  " absolute value of the difference between the current corrected image and the one"
477  " from the previous iteration summed over all the pixels."
478  )
479  brighterFatterApplyGain = pexConfig.Field(
480  dtype=bool,
481  default=True,
482  doc="Should the gain be applied when applying the brighter fatter correction?"
483  )
484 
485  # Dark subtraction.
486  doDark = pexConfig.Field(
487  dtype=bool,
488  doc="Apply dark frame correction?",
489  default=True,
490  )
491  darkDataProductName = pexConfig.Field(
492  dtype=str,
493  doc="Name of the dark data product",
494  default="dark",
495  )
496 
497  # Camera-specific stray light removal.
498  doStrayLight = pexConfig.Field(
499  dtype=bool,
500  doc="Subtract stray light in the y-band (due to encoder LEDs)?",
501  default=False,
502  )
503  strayLight = pexConfig.ConfigurableField(
504  target=StrayLightTask,
505  doc="y-band stray light correction"
506  )
507 
508  # Flat correction.
509  doFlat = pexConfig.Field(
510  dtype=bool,
511  doc="Apply flat field correction?",
512  default=True,
513  )
514  flatDataProductName = pexConfig.Field(
515  dtype=str,
516  doc="Name of the flat data product",
517  default="flat",
518  )
519  flatScalingType = pexConfig.ChoiceField(
520  dtype=str,
521  doc="The method for scaling the flat on the fly.",
522  default='USER',
523  allowed={
524  "USER": "Scale by flatUserScale",
525  "MEAN": "Scale by the inverse of the mean",
526  "MEDIAN": "Scale by the inverse of the median",
527  },
528  )
529  flatUserScale = pexConfig.Field(
530  dtype=float,
531  doc="If flatScalingType is 'USER' then scale flat by this amount; ignored otherwise",
532  default=1.0,
533  )
534  doTweakFlat = pexConfig.Field(
535  dtype=bool,
536  doc="Tweak flats to match observed amplifier ratios?",
537  default=False
538  )
539 
540  # Amplifier normalization based on gains instead of using flats configuration.
541  doApplyGains = pexConfig.Field(
542  dtype=bool,
543  doc="Correct the amplifiers for their gains instead of applying flat correction",
544  default=False,
545  )
546  normalizeGains = pexConfig.Field(
547  dtype=bool,
548  doc="Normalize all the amplifiers in each CCD to have the same median value.",
549  default=False,
550  )
551 
552  # Fringe correction.
553  doFringe = pexConfig.Field(
554  dtype=bool,
555  doc="Apply fringe correction?",
556  default=True,
557  )
558  fringe = pexConfig.ConfigurableField(
559  target=FringeTask,
560  doc="Fringe subtraction task",
561  )
562  fringeAfterFlat = pexConfig.Field(
563  dtype=bool,
564  doc="Do fringe subtraction after flat-fielding?",
565  default=True,
566  )
567 
568  # Distortion model application.
569  doAddDistortionModel = pexConfig.Field(
570  dtype=bool,
571  doc="Apply a distortion model based on camera geometry to the WCS?",
572  default=True,
573  )
574 
575  # Initial CCD-level background statistics options.
576  doMeasureBackground = pexConfig.Field(
577  dtype=bool,
578  doc="Measure the background level on the reduced image?",
579  default=False,
580  )
581 
582  # Camera-specific masking configuration.
583  doCameraSpecificMasking = pexConfig.Field(
584  dtype=bool,
585  doc="Mask camera-specific bad regions?",
586  default=False,
587  )
588  masking = pexConfig.ConfigurableField(
589  target=MaskingTask,
590  doc="Masking task."
591  )
592 
593  # Interpolation options.
594 
595  doInterpolate = pexConfig.Field(
596  dtype=bool,
597  doc="Interpolate masked pixels?",
598  default=True,
599  )
600  doSaturationInterpolation = pexConfig.Field(
601  dtype=bool,
602  doc="Perform interpolation over pixels masked as saturated?"
603  " NB: This is independent of doSaturation; if that is False this plane"
604  " will likely be blank, resulting in a no-op here.",
605  default=True,
606  )
607  doNanInterpolation = pexConfig.Field(
608  dtype=bool,
609  doc="Perform interpolation over pixels masked as NaN?"
610  " NB: This is independent of doNanMasking; if that is False this plane"
611  " will likely be blank, resulting in a no-op here.",
612  default=True,
613  )
614  doNanInterpAfterFlat = pexConfig.Field(
615  dtype=bool,
616  doc=("If True, ensure we interpolate NaNs after flat-fielding, even if we "
617  "also have to interpolate them before flat-fielding."),
618  default=False,
619  )
620  maskListToInterpolate = pexConfig.ListField(
621  dtype=str,
622  doc="List of mask planes that should be interpolated.",
623  default=['SAT', 'BAD', 'UNMASKEDNAN'],
624  )
625  doSaveInterpPixels = pexConfig.Field(
626  dtype=bool,
627  doc="Save a copy of the pre-interpolated pixel values?",
628  default=False,
629  )
630 
631  # Default photometric calibration options.
632  fluxMag0T1 = pexConfig.DictField(
633  keytype=str,
634  itemtype=float,
635  doc="The approximate flux of a zero-magnitude object in a one-second exposure, per filter.",
636  default=dict((f, pow(10.0, 0.4*m)) for f, m in (("Unknown", 28.0),
637  ))
638  )
639  defaultFluxMag0T1 = pexConfig.Field(
640  dtype=float,
641  doc="Default value for fluxMag0T1 (for an unrecognized filter).",
642  default=pow(10.0, 0.4*28.0)
643  )
644 
645  # Vignette correction configuration.
646  doVignette = pexConfig.Field(
647  dtype=bool,
648  doc="Apply vignetting parameters?",
649  default=False,
650  )
651  vignette = pexConfig.ConfigurableField(
652  target=VignetteTask,
653  doc="Vignetting task.",
654  )
655 
656  # Transmission curve configuration.
657  doAttachTransmissionCurve = pexConfig.Field(
658  dtype=bool,
659  default=False,
660  doc="Construct and attach a wavelength-dependent throughput curve for this CCD image?"
661  )
662  doUseOpticsTransmission = pexConfig.Field(
663  dtype=bool,
664  default=True,
665  doc="Load and use transmission_optics (if doAttachTransmissionCurve is True)?"
666  )
667  doUseFilterTransmission = pexConfig.Field(
668  dtype=bool,
669  default=True,
670  doc="Load and use transmission_filter (if doAttachTransmissionCurve is True)?"
671  )
672  doUseSensorTransmission = pexConfig.Field(
673  dtype=bool,
674  default=True,
675  doc="Load and use transmission_sensor (if doAttachTransmissionCurve is True)?"
676  )
677  doUseAtmosphereTransmission = pexConfig.Field(
678  dtype=bool,
679  default=True,
680  doc="Load and use transmission_atmosphere (if doAttachTransmissionCurve is True)?"
681  )
682 
683  # Write the outputs to disk. If ISR is run as a subtask, this may not be needed.
684  doWrite = pexConfig.Field(
685  dtype=bool,
686  doc="Persist postISRCCD?",
687  default=True,
688  )
689 
690  def validate(self):
691  super().validate()
692  if self.doFlat and self.doApplyGains:
693  raise ValueError("You may not specify both doFlat and doApplyGains")
694  if self.doSaturationInterpolation and "SAT" not in self.maskListToInterpolate:
695  self.config.maskListToInterpolate.append("SAT")
696  if self.doNanInterpolation and "UNMASKEDNAN" not in self.maskListToInterpolate:
697  self.config.maskListToInterpolate.append("UNMASKEDNAN")
698 
699 
700 class IsrTask(pipeBase.PipelineTask, pipeBase.CmdLineTask):
701  r"""Apply common instrument signature correction algorithms to a raw frame.
702 
703  The process for correcting imaging data is very similar from
704  camera to camera. This task provides a vanilla implementation of
705  doing these corrections, including the ability to turn certain
706  corrections off if they are not needed. The inputs to the primary
707  method, `run()`, are a raw exposure to be corrected and the
708  calibration data products. The raw input is a single chip sized
709  mosaic of all amps including overscans and other non-science
710  pixels. The method `runDataRef()` identifies and defines the
711  calibration data products, and is intended for use by a
712  `lsst.pipe.base.cmdLineTask.CmdLineTask` and takes as input only a
713  `daf.persistence.butlerSubset.ButlerDataRef`. This task may be
714  subclassed for different camera, although the most camera specific
715  methods have been split into subtasks that can be redirected
716  appropriately.
717 
718  The __init__ method sets up the subtasks for ISR processing, using
719  the defaults from `lsst.ip.isr`.
720 
721  Parameters
722  ----------
723  args : `list`
724  Positional arguments passed to the Task constructor. None used at this time.
725  kwargs : `dict`, optional
726  Keyword arguments passed on to the Task constructor. None used at this time.
727  """
728  ConfigClass = IsrTaskConfig
729  _DefaultName = "isr"
730 
731  def __init__(self, **kwargs):
732  super().__init__(**kwargs)
733  self.makeSubtask("assembleCcd")
734  self.makeSubtask("crosstalk")
735  self.makeSubtask("strayLight")
736  self.makeSubtask("fringe")
737  self.makeSubtask("masking")
738  self.makeSubtask("vignette")
739 
740  @classmethod
741  def getInputDatasetTypes(cls, config):
742  inputTypeDict = super().getInputDatasetTypes(config)
743 
744  # Delete entries from the dictionary of InputDatasetTypes that we know we don't
745  # need because the configuration tells us we will not be bothering with the
746  # correction that uses that IDT.
747  if config.doBias is not True:
748  inputTypeDict.pop("bias", None)
749  if config.doLinearize is not True:
750  inputTypeDict.pop("linearizer", None)
751  if config.doCrosstalk is not True:
752  inputTypeDict.pop("crosstalkSources", None)
753  if config.doBrighterFatter is not True:
754  inputTypeDict.pop("bfKernel", None)
755  if config.doDefect is not True:
756  inputTypeDict.pop("defects", None)
757  if config.doDark is not True:
758  inputTypeDict.pop("dark", None)
759  if config.doFlat is not True:
760  inputTypeDict.pop("flat", None)
761  if config.doAttachTransmissionCurve is not True:
762  inputTypeDict.pop("opticsTransmission", None)
763  inputTypeDict.pop("filterTransmission", None)
764  inputTypeDict.pop("sensorTransmission", None)
765  inputTypeDict.pop("atmosphereTransmission", None)
766  if config.doUseOpticsTransmission is not True:
767  inputTypeDict.pop("opticsTransmission", None)
768  if config.doUseFilterTransmission is not True:
769  inputTypeDict.pop("filterTransmission", None)
770  if config.doUseSensorTransmission is not True:
771  inputTypeDict.pop("sensorTransmission", None)
772  if config.doUseAtmosphereTransmission is not True:
773  inputTypeDict.pop("atmosphereTransmission", None)
774 
775  return inputTypeDict
776 
777  @classmethod
778  def getOutputDatasetTypes(cls, config):
779  outputTypeDict = super().getOutputDatasetTypes(config)
780 
781  if config.qa.doThumbnailOss is not True:
782  outputTypeDict.pop("outputOssThumbnail", None)
783  if config.qa.doThumbnailFlattened is not True:
784  outputTypeDict.pop("outputFlattenedThumbnail", None)
785  if config.doWrite is not True:
786  outputTypeDict.pop("outputExposure", None)
787 
788  return outputTypeDict
789 
790  @classmethod
791  def getPrerequisiteDatasetTypes(cls, config):
792  # Input calibration datasets should not constrain the QuantumGraph
793  # (it'd be confusing if not having flats just silently resulted in no
794  # data being processed). Our nomenclature for that is that these are
795  # "prerequisite" datasets (only "ccdExposure" == "raw" isn't).
796  names = set(cls.getInputDatasetTypes(config))
797  names.remove("ccdExposure")
798  return names
799 
800  @classmethod
801  def getPerDatasetTypeDimensions(cls, config):
802  # Input calibration datasets of different types (i.e. flat, bias) need
803  # not have the same validity range. That makes CalibrationLabel
804  # (which maps directly to a validity range) a "per-DatasetType
805  # dimension".
806  return frozenset(["CalibrationLabel"])
807 
808  def adaptArgsAndRun(self, inputData, inputDataIds, outputDataIds, butler):
809  try:
810  inputData['detectorNum'] = int(inputDataIds['ccdExposure']['detector'])
811  except Exception as e:
812  raise ValueError(f"Failure to find valid detectorNum value for Dataset {inputDataIds}: {e}")
813 
814  inputData['isGen3'] = True
815 
816  if self.config.doLinearize is True:
817  if 'linearizer' not in inputData.keys():
818  detector = inputData['camera'][inputData['detectorNum']]
819  linearityName = detector.getAmpInfoCatalog()[0].getLinearityType()
820  inputData['linearizer'] = linearize.getLinearityTypeByName(linearityName)()
821 
822  if inputData['defects'] is not None:
823  # defects is loaded as a BaseCatalog with columns x0, y0, width, height.
824  # masking expects a list of defects defined by their bounding box
825  defectList = []
826 
827  for r in inputData['defects']:
828  bbox = afwGeom.BoxI(afwGeom.PointI(r.get("x0"), r.get("y0")),
829  afwGeom.ExtentI(r.get("width"), r.get("height")))
830  defectList.append(Defect(bbox))
831 
832  inputData['defects'] = defectList
833 
834  # Broken: DM-17169
835  # ci_hsc does not use crosstalkSources, as it's intra-CCD CT only. This needs to be
836  # fixed for non-HSC cameras in the future.
837  # inputData['crosstalkSources'] = (self.crosstalk.prepCrosstalk(inputDataIds['ccdExposure'])
838  # if self.config.doCrosstalk else None)
839 
840  # Broken: DM-17152
841  # Fringes are not tested to be handled correctly by Gen3 butler.
842  # inputData['fringes'] = (self.fringe.readFringes(inputDataIds['ccdExposure'],
843  # assembler=self.assembleCcd
844  # if self.config.doAssembleIsrExposures else None)
845  # if self.config.doFringe and
846  # self.fringe.checkFilter(inputData['ccdExposure'])
847  # else pipeBase.Struct(fringes=None))
848 
849  return super().adaptArgsAndRun(inputData, inputDataIds, outputDataIds, butler)
850 
851  def makeDatasetType(self, dsConfig):
852  return super().makeDatasetType(dsConfig)
853 
854  def readIsrData(self, dataRef, rawExposure):
855  """!Retrieve necessary frames for instrument signature removal.
856 
857  Pre-fetching all required ISR data products limits the IO
858  required by the ISR. Any conflict between the calibration data
859  available and that needed for ISR is also detected prior to
860  doing processing, allowing it to fail quickly.
861 
862  Parameters
863  ----------
864  dataRef : `daf.persistence.butlerSubset.ButlerDataRef`
865  Butler reference of the detector data to be processed
866  rawExposure : `afw.image.Exposure`
867  The raw exposure that will later be corrected with the
868  retrieved calibration data; should not be modified in this
869  method.
870 
871  Returns
872  -------
873  result : `lsst.pipe.base.Struct`
874  Result struct with components (which may be `None`):
875  - ``bias``: bias calibration frame (`afw.image.Exposure`)
876  - ``linearizer``: functor for linearization (`ip.isr.linearize.LinearizeBase`)
877  - ``crosstalkSources``: list of possible crosstalk sources (`list`)
878  - ``dark``: dark calibration frame (`afw.image.Exposure`)
879  - ``flat``: flat calibration frame (`afw.image.Exposure`)
880  - ``bfKernel``: Brighter-Fatter kernel (`numpy.ndarray`)
881  - ``defects``: list of defects (`list`)
882  - ``fringes``: `lsst.pipe.base.Struct` with components:
883  - ``fringes``: fringe calibration frame (`afw.image.Exposure`)
884  - ``seed``: random seed derived from the ccdExposureId for random
885  number generator (`uint32`)
886  - ``opticsTransmission``: `lsst.afw.image.TransmissionCurve`
887  A ``TransmissionCurve`` that represents the throughput of the optics,
888  to be evaluated in focal-plane coordinates.
889  - ``filterTransmission`` : `lsst.afw.image.TransmissionCurve`
890  A ``TransmissionCurve`` that represents the throughput of the filter
891  itself, to be evaluated in focal-plane coordinates.
892  - ``sensorTransmission`` : `lsst.afw.image.TransmissionCurve`
893  A ``TransmissionCurve`` that represents the throughput of the sensor
894  itself, to be evaluated in post-assembly trimmed detector coordinates.
895  - ``atmosphereTransmission`` : `lsst.afw.image.TransmissionCurve`
896  A ``TransmissionCurve`` that represents the throughput of the
897  atmosphere, assumed to be spatially constant.
898  - ``strayLightData`` : `object`
899  An opaque object containing calibration information for
900  stray-light correction. If `None`, no correction will be
901  performed.
902 
903  Raises
904  ------
905  NotImplementedError :
906  Raised if a per-amplifier brighter-fatter kernel is requested by the configuration.
907  """
908  ccd = rawExposure.getDetector()
909  rawExposure.mask.addMaskPlane("UNMASKEDNAN") # needed to match pre DM-15862 processing.
910  biasExposure = (self.getIsrExposure(dataRef, self.config.biasDataProductName)
911  if self.config.doBias else None)
912  # immediate=True required for functors and linearizers are functors; see ticket DM-6515
913  linearizer = (dataRef.get("linearizer", immediate=True)
914  if self.doLinearize(ccd) else None)
915  crosstalkSources = (self.crosstalk.prepCrosstalk(dataRef)
916  if self.config.doCrosstalk else None)
917  darkExposure = (self.getIsrExposure(dataRef, self.config.darkDataProductName)
918  if self.config.doDark else None)
919  flatExposure = (self.getIsrExposure(dataRef, self.config.flatDataProductName)
920  if self.config.doFlat else None)
921 
922  brighterFatterKernel = None
923  if self.config.doBrighterFatter is True:
924 
925  # Use the new-style cp_pipe version of the kernel is it exists.
926  try:
927  brighterFatterKernel = dataRef.get("brighterFatterKernel")
928  except NoResults:
929  # Fall back to the old-style numpy-ndarray style kernel if necessary.
930  try:
931  brighterFatterKernel = dataRef.get("bfKernel")
932  except NoResults:
933  brighterFatterKernel = None
934  if brighterFatterKernel is not None and not isinstance(brighterFatterKernel, numpy.ndarray):
935  # If the kernel is not an ndarray, it's the cp_pipe version, so extract the kernel for
936  # this detector, or raise an error.
937  if self.config.brighterFatterLevel == 'DETECTOR':
938  brighterFatterKernel = brighterFatterKernel.kernel[ccd.getId()]
939  else:
940  # TODO DM-15631 for implementing this
941  raise NotImplementedError("Per-amplifier brighter-fatter correction not implemented")
942 
943  defectList = (dataRef.get("defects")
944  if self.config.doDefect else None)
945  fringeStruct = (self.fringe.readFringes(dataRef, assembler=self.assembleCcd
946  if self.config.doAssembleIsrExposures else None)
947  if self.config.doFringe and self.fringe.checkFilter(rawExposure)
948  else pipeBase.Struct(fringes=None))
949 
950  if self.config.doAttachTransmissionCurve:
951  opticsTransmission = (dataRef.get("transmission_optics")
952  if self.config.doUseOpticsTransmission else None)
953  filterTransmission = (dataRef.get("transmission_filter")
954  if self.config.doUseFilterTransmission else None)
955  sensorTransmission = (dataRef.get("transmission_sensor")
956  if self.config.doUseSensorTransmission else None)
957  atmosphereTransmission = (dataRef.get("transmission_atmosphere")
958  if self.config.doUseAtmosphereTransmission else None)
959  else:
960  opticsTransmission = None
961  filterTransmission = None
962  sensorTransmission = None
963  atmosphereTransmission = None
964 
965  if self.config.doStrayLight:
966  strayLightData = self.strayLight.readIsrData(dataRef, rawExposure)
967  else:
968  strayLightData = None
969 
970  # Struct should include only kwargs to run()
971  return pipeBase.Struct(bias=biasExposure,
972  linearizer=linearizer,
973  crosstalkSources=crosstalkSources,
974  dark=darkExposure,
975  flat=flatExposure,
976  bfKernel=brighterFatterKernel,
977  defects=defectList,
978  fringes=fringeStruct,
979  opticsTransmission=opticsTransmission,
980  filterTransmission=filterTransmission,
981  sensorTransmission=sensorTransmission,
982  atmosphereTransmission=atmosphereTransmission,
983  strayLightData=strayLightData
984  )
985 
986  @pipeBase.timeMethod
987  def run(self, ccdExposure, camera=None, bias=None, linearizer=None, crosstalkSources=None,
988  dark=None, flat=None, bfKernel=None, defects=None, fringes=None,
989  opticsTransmission=None, filterTransmission=None,
990  sensorTransmission=None, atmosphereTransmission=None,
991  detectorNum=None, strayLightData=None, isGen3=False,
992  ):
993  """!Perform instrument signature removal on an exposure.
994 
995  Steps included in the ISR processing, in order performed, are:
996  - saturation and suspect pixel masking
997  - overscan subtraction
998  - CCD assembly of individual amplifiers
999  - bias subtraction
1000  - variance image construction
1001  - linearization of non-linear response
1002  - crosstalk masking
1003  - brighter-fatter correction
1004  - dark subtraction
1005  - fringe correction
1006  - stray light subtraction
1007  - flat correction
1008  - masking of known defects and camera specific features
1009  - vignette calculation
1010  - appending transmission curve and distortion model
1011 
1012  Parameters
1013  ----------
1014  ccdExposure : `lsst.afw.image.Exposure`
1015  The raw exposure that is to be run through ISR. The
1016  exposure is modified by this method.
1017  camera : `lsst.afw.cameraGeom.Camera`, optional
1018  The camera geometry for this exposure. Used to select the
1019  distortion model appropriate for this data.
1020  bias : `lsst.afw.image.Exposure`, optional
1021  Bias calibration frame.
1022  linearizer : `lsst.ip.isr.linearize.LinearizeBase`, optional
1023  Functor for linearization.
1024  crosstalkSources : `list`, optional
1025  List of possible crosstalk sources.
1026  dark : `lsst.afw.image.Exposure`, optional
1027  Dark calibration frame.
1028  flat : `lsst.afw.image.Exposure`, optional
1029  Flat calibration frame.
1030  bfKernel : `numpy.ndarray`, optional
1031  Brighter-fatter kernel.
1032  defects : `list`, optional
1033  List of defects.
1034  fringes : `lsst.pipe.base.Struct`, optional
1035  Struct containing the fringe correction data, with
1036  elements:
1037  - ``fringes``: fringe calibration frame (`afw.image.Exposure`)
1038  - ``seed``: random seed derived from the ccdExposureId for random
1039  number generator (`uint32`)
1040  opticsTransmission: `lsst.afw.image.TransmissionCurve`, optional
1041  A ``TransmissionCurve`` that represents the throughput of the optics,
1042  to be evaluated in focal-plane coordinates.
1043  filterTransmission : `lsst.afw.image.TransmissionCurve`
1044  A ``TransmissionCurve`` that represents the throughput of the filter
1045  itself, to be evaluated in focal-plane coordinates.
1046  sensorTransmission : `lsst.afw.image.TransmissionCurve`
1047  A ``TransmissionCurve`` that represents the throughput of the sensor
1048  itself, to be evaluated in post-assembly trimmed detector coordinates.
1049  atmosphereTransmission : `lsst.afw.image.TransmissionCurve`
1050  A ``TransmissionCurve`` that represents the throughput of the
1051  atmosphere, assumed to be spatially constant.
1052  detectorNum : `int`, optional
1053  The integer number for the detector to process.
1054  isGen3 : bool, optional
1055  Flag this call to run() as using the Gen3 butler environment.
1056  strayLightData : `object`, optional
1057  Opaque object containing calibration information for stray-light
1058  correction. If `None`, no correction will be performed.
1059 
1060  Returns
1061  -------
1062  result : `lsst.pipe.base.Struct`
1063  Result struct with component:
1064  - ``exposure`` : `afw.image.Exposure`
1065  The fully ISR corrected exposure.
1066  - ``outputExposure`` : `afw.image.Exposure`
1067  An alias for `exposure`
1068  - ``ossThumb`` : `numpy.ndarray`
1069  Thumbnail image of the exposure after overscan subtraction.
1070  - ``flattenedThumb`` : `numpy.ndarray`
1071  Thumbnail image of the exposure after flat-field correction.
1072 
1073  Raises
1074  ------
1075  RuntimeError
1076  Raised if a configuration option is set to True, but the
1077  required calibration data has not been specified.
1078 
1079  Notes
1080  -----
1081  The current processed exposure can be viewed by setting the
1082  appropriate lsstDebug entries in the `debug.display`
1083  dictionary. The names of these entries correspond to some of
1084  the IsrTaskConfig Boolean options, with the value denoting the
1085  frame to use. The exposure is shown inside the matching
1086  option check and after the processing of that step has
1087  finished. The steps with debug points are:
1088 
1089  doAssembleCcd
1090  doBias
1091  doCrosstalk
1092  doBrighterFatter
1093  doDark
1094  doFringe
1095  doStrayLight
1096  doFlat
1097 
1098  In addition, setting the "postISRCCD" entry displays the
1099  exposure after all ISR processing has finished.
1100 
1101  """
1102 
1103  if isGen3 is True:
1104  # Gen3 currently cannot automatically do configuration overrides.
1105  # DM-15257 looks to discuss this issue.
1106 
1107  self.config.doFringe = False
1108 
1109  # Configure input exposures;
1110  if detectorNum is None:
1111  raise RuntimeError("Must supply the detectorNum if running as Gen3")
1112 
1113  ccdExposure = self.ensureExposure(ccdExposure, camera, detectorNum)
1114  bias = self.ensureExposure(bias, camera, detectorNum)
1115  dark = self.ensureExposure(dark, camera, detectorNum)
1116  flat = self.ensureExposure(flat, camera, detectorNum)
1117  else:
1118  if isinstance(ccdExposure, ButlerDataRef):
1119  return self.runDataRef(ccdExposure)
1120 
1121  ccd = ccdExposure.getDetector()
1122 
1123  if not ccd:
1124  assert not self.config.doAssembleCcd, "You need a Detector to run assembleCcd"
1125  ccd = [FakeAmp(ccdExposure, self.config)]
1126 
1127  # Validate Input
1128  if self.config.doBias and bias is None:
1129  raise RuntimeError("Must supply a bias exposure if config.doBias=True.")
1130  if self.doLinearize(ccd) and linearizer is None:
1131  raise RuntimeError("Must supply a linearizer if config.doLinearize=True for this detector.")
1132  if self.config.doBrighterFatter and bfKernel is None:
1133  raise RuntimeError("Must supply a kernel if config.doBrighterFatter=True.")
1134  if self.config.doDark and dark is None:
1135  raise RuntimeError("Must supply a dark exposure if config.doDark=True.")
1136  if fringes is None:
1137  fringes = pipeBase.Struct(fringes=None)
1138  if self.config.doFringe and not isinstance(fringes, pipeBase.Struct):
1139  raise RuntimeError("Must supply fringe exposure as a pipeBase.Struct.")
1140  if self.config.doFlat and flat is None:
1141  raise RuntimeError("Must supply a flat exposure if config.doFlat=True.")
1142  if self.config.doDefect and defects is None:
1143  raise RuntimeError("Must supply defects if config.doDefect=True.")
1144  if self.config.doAddDistortionModel and camera is None:
1145  raise RuntimeError("Must supply camera if config.doAddDistortionModel=True.")
1146 
1147  # Begin ISR processing.
1148  if self.config.doConvertIntToFloat:
1149  self.log.info("Converting exposure to floating point values")
1150  ccdExposure = self.convertIntToFloat(ccdExposure)
1151 
1152  # Amplifier level processing.
1153  overscans = []
1154  for amp in ccd:
1155  # if ccdExposure is one amp, check for coverage to prevent performing ops multiple times
1156  if ccdExposure.getBBox().contains(amp.getBBox()):
1157  # Check for fully masked bad amplifiers, and generate masks for SUSPECT and SATURATED values.
1158  badAmp = self.maskAmplifier(ccdExposure, amp, defects)
1159 
1160  if self.config.doOverscan and not badAmp:
1161  # Overscan correction on amp-by-amp basis.
1162  overscanResults = self.overscanCorrection(ccdExposure, amp)
1163  self.log.debug("Corrected overscan for amplifier %s" % (amp.getName()))
1164  if self.config.qa is not None and self.config.qa.saveStats is True:
1165  if isinstance(overscanResults.overscanFit, float):
1166  qaMedian = overscanResults.overscanFit
1167  qaStdev = float("NaN")
1168  else:
1169  qaStats = afwMath.makeStatistics(overscanResults.overscanFit,
1170  afwMath.MEDIAN | afwMath.STDEVCLIP)
1171  qaMedian = qaStats.getValue(afwMath.MEDIAN)
1172  qaStdev = qaStats.getValue(afwMath.STDEVCLIP)
1173 
1174  self.metadata.set("ISR OSCAN {} MEDIAN".format(amp.getName()), qaMedian)
1175  self.metadata.set("ISR OSCAN {} STDEV".format(amp.getName()), qaStdev)
1176  self.log.debug(" Overscan stats for amplifer %s: %f +/- %f" %
1177  (amp.getName(), qaMedian, qaStdev))
1178  ccdExposure.getMetadata().set('OVERSCAN', "Overscan corrected")
1179  else:
1180  self.log.warn("Amplifier %s is bad." % (amp.getName()))
1181  overscanResults = None
1182 
1183  overscans.append(overscanResults if overscanResults is not None else None)
1184  else:
1185  self.log.info("Skipped OSCAN")
1186 
1187  if self.config.doCrosstalk and self.config.doCrosstalkBeforeAssemble:
1188  self.log.info("Applying crosstalk correction.")
1189  self.crosstalk.run(ccdExposure, crosstalkSources=crosstalkSources)
1190  self.debugView(ccdExposure, "doCrosstalk")
1191 
1192  if self.config.doAssembleCcd:
1193  self.log.info("Assembling CCD from amplifiers")
1194  ccdExposure = self.assembleCcd.assembleCcd(ccdExposure)
1195 
1196  if self.config.expectWcs and not ccdExposure.getWcs():
1197  self.log.warn("No WCS found in input exposure")
1198  self.debugView(ccdExposure, "doAssembleCcd")
1199 
1200  ossThumb = None
1201  if self.config.qa.doThumbnailOss:
1202  ossThumb = isrQa.makeThumbnail(ccdExposure, isrQaConfig=self.config.qa)
1203 
1204  if self.config.doBias:
1205  self.log.info("Applying bias correction.")
1206  isrFunctions.biasCorrection(ccdExposure.getMaskedImage(), bias.getMaskedImage(),
1207  trimToFit=self.config.doTrimToMatchCalib)
1208  self.debugView(ccdExposure, "doBias")
1209 
1210  if self.config.doVariance:
1211  for amp, overscanResults in zip(ccd, overscans):
1212  if ccdExposure.getBBox().contains(amp.getBBox()):
1213  self.log.debug("Constructing variance map for amplifer %s" % (amp.getName()))
1214  ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
1215  if overscanResults is not None:
1216  self.updateVariance(ampExposure, amp,
1217  overscanImage=overscanResults.overscanImage)
1218  else:
1219  self.updateVariance(ampExposure, amp,
1220  overscanImage=None)
1221  if self.config.qa is not None and self.config.qa.saveStats is True:
1222  qaStats = afwMath.makeStatistics(ampExposure.getVariance(),
1223  afwMath.MEDIAN | afwMath.STDEVCLIP)
1224  self.metadata.set("ISR VARIANCE {} MEDIAN".format(amp.getName()),
1225  qaStats.getValue(afwMath.MEDIAN))
1226  self.metadata.set("ISR VARIANCE {} STDEV".format(amp.getName()),
1227  qaStats.getValue(afwMath.STDEVCLIP))
1228  self.log.debug(" Variance stats for amplifer %s: %f +/- %f" %
1229  (amp.getName(), qaStats.getValue(afwMath.MEDIAN),
1230  qaStats.getValue(afwMath.STDEVCLIP)))
1231 
1232  if self.doLinearize(ccd):
1233  self.log.info("Applying linearizer.")
1234  linearizer(image=ccdExposure.getMaskedImage().getImage(), detector=ccd, log=self.log)
1235 
1236  if self.config.doCrosstalk and not self.config.doCrosstalkBeforeAssemble:
1237  self.log.info("Applying crosstalk correction.")
1238  self.crosstalk.run(ccdExposure, crosstalkSources=crosstalkSources, isTrimmed=True)
1239  self.debugView(ccdExposure, "doCrosstalk")
1240 
1241  # Masking block. Optionally mask known defects, NAN pixels, widen trails, and do
1242  # anything else the camera needs. Saturated and suspect pixels have already been masked.
1243  if self.config.doDefect:
1244  self.log.info("Masking defects.")
1245  self.maskDefect(ccdExposure, defects)
1246 
1247  if self.config.doNanMasking:
1248  self.log.info("Masking NAN value pixels.")
1249  self.maskNan(ccdExposure)
1250 
1251  if self.config.doWidenSaturationTrails:
1252  self.log.info("Widening saturation trails.")
1253  isrFunctions.widenSaturationTrails(ccdExposure.getMaskedImage().getMask())
1254 
1255  if self.config.doCameraSpecificMasking:
1256  self.log.info("Masking regions for camera specific reasons.")
1257  self.masking.run(ccdExposure)
1258 
1259  if self.config.doBrighterFatter:
1260  # We need to apply flats and darks before we can interpolate, and we
1261  # need to interpolate before we do B-F, but we do B-F without the
1262  # flats and darks applied so we can work in units of electrons or holes.
1263  # This context manager applies and then removes the darks and flats.
1264  #
1265  # We also do not want to interpolate values here, so operate on temporary
1266  # images so we can apply only the BF-correction and roll back the
1267  # interpolation.
1268  interpExp = ccdExposure.clone()
1269  with self.flatContext(interpExp, flat, dark):
1270  isrFunctions.interpolateFromMask(
1271  maskedImage=interpExp.getMaskedImage(),
1272  fwhm=self.config.fwhm,
1273  growSaturatedFootprints=self.config.growSaturationFootprintSize,
1274  maskNameList=self.config.maskListToInterpolate
1275  )
1276  bfExp = interpExp.clone()
1277 
1278  self.log.info("Applying brighter fatter correction.")
1279  isrFunctions.brighterFatterCorrection(bfExp, bfKernel,
1280  self.config.brighterFatterMaxIter,
1281  self.config.brighterFatterThreshold,
1282  self.config.brighterFatterApplyGain,
1283  )
1284  image = ccdExposure.getMaskedImage().getImage()
1285  bfCorr = bfExp.getMaskedImage().getImage()
1286  bfCorr -= interpExp.getMaskedImage().getImage()
1287  image += bfCorr
1288 
1289  self.debugView(ccdExposure, "doBrighterFatter")
1290 
1291  if self.config.doDark:
1292  self.log.info("Applying dark correction.")
1293  self.darkCorrection(ccdExposure, dark)
1294  self.debugView(ccdExposure, "doDark")
1295 
1296  if self.config.doFringe and not self.config.fringeAfterFlat:
1297  self.log.info("Applying fringe correction before flat.")
1298  self.fringe.run(ccdExposure, **fringes.getDict())
1299  self.debugView(ccdExposure, "doFringe")
1300 
1301  if self.config.doStrayLight:
1302  if strayLightData is not None:
1303  self.log.info("Applying stray light correction.")
1304  self.strayLight.run(ccdExposure, strayLightData)
1305  self.debugView(ccdExposure, "doStrayLight")
1306  else:
1307  self.log.debug("Skipping stray light correction: no data found for this image.")
1308 
1309  if self.config.doFlat:
1310  self.log.info("Applying flat correction.")
1311  self.flatCorrection(ccdExposure, flat)
1312  self.debugView(ccdExposure, "doFlat")
1313 
1314  if self.config.doApplyGains:
1315  self.log.info("Applying gain correction instead of flat.")
1316  isrFunctions.applyGains(ccdExposure, self.config.normalizeGains)
1317 
1318  if self.config.doFringe and self.config.fringeAfterFlat:
1319  self.log.info("Applying fringe correction after flat.")
1320  self.fringe.run(ccdExposure, **fringes.getDict())
1321 
1322  if self.config.doVignette:
1323  self.log.info("Constructing Vignette polygon.")
1324  self.vignettePolygon = self.vignette.run(ccdExposure)
1325 
1326  if self.config.vignette.doWriteVignettePolygon:
1327  self.setValidPolygonIntersect(ccdExposure, self.vignettePolygon)
1328 
1329  if self.config.doAttachTransmissionCurve:
1330  self.log.info("Adding transmission curves.")
1331  isrFunctions.attachTransmissionCurve(ccdExposure, opticsTransmission=opticsTransmission,
1332  filterTransmission=filterTransmission,
1333  sensorTransmission=sensorTransmission,
1334  atmosphereTransmission=atmosphereTransmission)
1335 
1336  if self.config.doAddDistortionModel:
1337  self.log.info("Adding a distortion model to the WCS.")
1338  isrFunctions.addDistortionModel(exposure=ccdExposure, camera=camera)
1339 
1340  flattenedThumb = None
1341  if self.config.qa.doThumbnailFlattened:
1342  flattenedThumb = isrQa.makeThumbnail(ccdExposure, isrQaConfig=self.config.qa)
1343 
1344  preInterpExp = None
1345  if self.config.doSaveInterpPixels:
1346  preInterpExp = ccdExposure.clone()
1347 
1348  # Reset and interpolate bad pixels.
1349  #
1350  # Large contiguous bad regions (which should have the BAD mask
1351  # bit set) should have their values set to the image median.
1352  # This group should include defects and bad amplifiers. As the
1353  # area covered by these defects are large, there's little
1354  # reason to expect that interpolation would provide a more
1355  # useful value.
1356  #
1357  # Smaller defects can be safely interpolated after the larger
1358  # regions have had their pixel values reset. This ensures
1359  # that the remaining defects adjacent to bad amplifiers (as an
1360  # example) do not attempt to interpolate extreme values.
1361  if self.config.doSetBadRegions:
1362  badPixelCount, badPixelValue = isrFunctions.setBadRegions(ccdExposure)
1363  if badPixelCount > 0:
1364  self.log.info("Set %d BAD pixels to %f." % (badPixelCount, badPixelValue))
1365 
1366  if self.config.doInterpolate:
1367  self.log.info("Interpolating masked pixels.")
1368  isrFunctions.interpolateFromMask(
1369  maskedImage=ccdExposure.getMaskedImage(),
1370  fwhm=self.config.fwhm,
1371  growSaturatedFootprints=self.config.growSaturationFootprintSize,
1372  maskNameList=list(self.config.maskListToInterpolate)
1373  )
1374 
1375  self.roughZeroPoint(ccdExposure)
1376 
1377  if self.config.doMeasureBackground:
1378  self.log.info("Measuring background level:")
1379  self.measureBackground(ccdExposure, self.config.qa)
1380 
1381  if self.config.qa is not None and self.config.qa.saveStats is True:
1382  for amp in ccd:
1383  ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
1384  qaStats = afwMath.makeStatistics(ampExposure.getImage(),
1385  afwMath.MEDIAN | afwMath.STDEVCLIP)
1386  self.metadata.set("ISR BACKGROUND {} MEDIAN".format(amp.getName()),
1387  qaStats.getValue(afwMath.MEDIAN))
1388  self.metadata.set("ISR BACKGROUND {} STDEV".format(amp.getName()),
1389  qaStats.getValue(afwMath.STDEVCLIP))
1390  self.log.debug(" Background stats for amplifer %s: %f +/- %f" %
1391  (amp.getName(), qaStats.getValue(afwMath.MEDIAN),
1392  qaStats.getValue(afwMath.STDEVCLIP)))
1393 
1394  self.debugView(ccdExposure, "postISRCCD")
1395 
1396  return pipeBase.Struct(
1397  exposure=ccdExposure,
1398  ossThumb=ossThumb,
1399  flattenedThumb=flattenedThumb,
1400 
1401  preInterpolatedExposure=preInterpExp,
1402  outputExposure=ccdExposure,
1403  outputOssThumbnail=ossThumb,
1404  outputFlattenedThumbnail=flattenedThumb,
1405  )
1406 
1407  @pipeBase.timeMethod
1408  def runDataRef(self, sensorRef):
1409  """Perform instrument signature removal on a ButlerDataRef of a Sensor.
1410 
1411  This method contains the `CmdLineTask` interface to the ISR
1412  processing. All IO is handled here, freeing the `run()` method
1413  to manage only pixel-level calculations. The steps performed
1414  are:
1415  - Read in necessary detrending/isr/calibration data.
1416  - Process raw exposure in `run()`.
1417  - Persist the ISR-corrected exposure as "postISRCCD" if
1418  config.doWrite=True.
1419 
1420  Parameters
1421  ----------
1422  sensorRef : `daf.persistence.butlerSubset.ButlerDataRef`
1423  DataRef of the detector data to be processed
1424 
1425  Returns
1426  -------
1427  result : `lsst.pipe.base.Struct`
1428  Result struct with component:
1429  - ``exposure`` : `afw.image.Exposure`
1430  The fully ISR corrected exposure.
1431 
1432  Raises
1433  ------
1434  RuntimeError
1435  Raised if a configuration option is set to True, but the
1436  required calibration data does not exist.
1437 
1438  """
1439  self.log.info("Performing ISR on sensor %s" % (sensorRef.dataId))
1440 
1441  ccdExposure = sensorRef.get(self.config.datasetType)
1442 
1443  camera = sensorRef.get("camera")
1444  if camera is None and self.config.doAddDistortionModel:
1445  raise RuntimeError("config.doAddDistortionModel is True "
1446  "but could not get a camera from the butler")
1447  isrData = self.readIsrData(sensorRef, ccdExposure)
1448 
1449  result = self.run(ccdExposure, camera=camera, **isrData.getDict())
1450 
1451  if self.config.doWrite:
1452  sensorRef.put(result.exposure, "postISRCCD")
1453  if result.preInterpolatedExposure is not None:
1454  sensorRef.put(result.preInterpolatedExposure, "postISRCCD_uninterpolated")
1455  if result.ossThumb is not None:
1456  isrQa.writeThumbnail(sensorRef, result.ossThumb, "ossThumb")
1457  if result.flattenedThumb is not None:
1458  isrQa.writeThumbnail(sensorRef, result.flattenedThumb, "flattenedThumb")
1459 
1460  return result
1461 
1462  def getIsrExposure(self, dataRef, datasetType, immediate=True):
1463  """!Retrieve a calibration dataset for removing instrument signature.
1464 
1465  Parameters
1466  ----------
1467 
1468  dataRef : `daf.persistence.butlerSubset.ButlerDataRef`
1469  DataRef of the detector data to find calibration datasets
1470  for.
1471  datasetType : `str`
1472  Type of dataset to retrieve (e.g. 'bias', 'flat', etc).
1473  immediate : `Bool`
1474  If True, disable butler proxies to enable error handling
1475  within this routine.
1476 
1477  Returns
1478  -------
1479  exposure : `lsst.afw.image.Exposure`
1480  Requested calibration frame.
1481 
1482  Raises
1483  ------
1484  RuntimeError
1485  Raised if no matching calibration frame can be found.
1486  """
1487  try:
1488  exp = dataRef.get(datasetType, immediate=immediate)
1489  except Exception as exc1:
1490  if not self.config.fallbackFilterName:
1491  raise RuntimeError("Unable to retrieve %s for %s: %s" % (datasetType, dataRef.dataId, exc1))
1492  try:
1493  exp = dataRef.get(datasetType, filter=self.config.fallbackFilterName, immediate=immediate)
1494  except Exception as exc2:
1495  raise RuntimeError("Unable to retrieve %s for %s, even with fallback filter %s: %s AND %s" %
1496  (datasetType, dataRef.dataId, self.config.fallbackFilterName, exc1, exc2))
1497  self.log.warn("Using fallback calibration from filter %s" % self.config.fallbackFilterName)
1498 
1499  if self.config.doAssembleIsrExposures:
1500  exp = self.assembleCcd.assembleCcd(exp)
1501  return exp
1502 
1503  def ensureExposure(self, inputExp, camera, detectorNum):
1504  """Ensure that the data returned by Butler is a fully constructed exposure.
1505 
1506  ISR requires exposure-level image data for historical reasons, so if we did
1507  not recieve that from Butler, construct it from what we have, modifying the
1508  input in place.
1509 
1510  Parameters
1511  ----------
1512  inputExp : `lsst.afw.image.Exposure`, `lsst.afw.image.DecoratedImageU`, or
1513  `lsst.afw.image.ImageF`
1514  The input data structure obtained from Butler.
1515  camera : `lsst.afw.cameraGeom.camera`
1516  The camera associated with the image. Used to find the appropriate
1517  detector.
1518  detectorNum : `int`
1519  The detector this exposure should match.
1520 
1521  Returns
1522  -------
1523  inputExp : `lsst.afw.image.Exposure`
1524  The re-constructed exposure, with appropriate detector parameters.
1525 
1526  Raises
1527  ------
1528  TypeError
1529  Raised if the input data cannot be used to construct an exposure.
1530  """
1531  if isinstance(inputExp, afwImage.DecoratedImageU):
1532  inputExp = afwImage.makeExposure(afwImage.makeMaskedImage(inputExp))
1533  elif isinstance(inputExp, afwImage.ImageF):
1534  inputExp = afwImage.makeExposure(afwImage.makeMaskedImage(inputExp))
1535  elif isinstance(inputExp, afwImage.MaskedImageF):
1536  inputExp = afwImage.makeExposure(inputExp)
1537  elif isinstance(inputExp, afwImage.Exposure):
1538  pass
1539  else:
1540  raise TypeError(f"Input Exposure is not known type in isrTask.ensureExposure: {type(inputExp)}")
1541 
1542  if inputExp.getDetector() is None:
1543  inputExp.setDetector(camera[detectorNum])
1544 
1545  return inputExp
1546 
1547  def convertIntToFloat(self, exposure):
1548  """Convert exposure image from uint16 to float.
1549 
1550  If the exposure does not need to be converted, the input is
1551  immediately returned. For exposures that are converted to use
1552  floating point pixels, the variance is set to unity and the
1553  mask to zero.
1554 
1555  Parameters
1556  ----------
1557  exposure : `lsst.afw.image.Exposure`
1558  The raw exposure to be converted.
1559 
1560  Returns
1561  -------
1562  newexposure : `lsst.afw.image.Exposure`
1563  The input ``exposure``, converted to floating point pixels.
1564 
1565  Raises
1566  ------
1567  RuntimeError
1568  Raised if the exposure type cannot be converted to float.
1569 
1570  """
1571  if isinstance(exposure, afwImage.ExposureF):
1572  # Nothing to be done
1573  return exposure
1574  if not hasattr(exposure, "convertF"):
1575  raise RuntimeError("Unable to convert exposure (%s) to float" % type(exposure))
1576 
1577  newexposure = exposure.convertF()
1578  newexposure.variance[:] = 1
1579  newexposure.mask[:] = 0x0
1580 
1581  return newexposure
1582 
1583  def maskAmplifier(self, ccdExposure, amp, defects):
1584  """Identify bad amplifiers, saturated and suspect pixels.
1585 
1586  Parameters
1587  ----------
1588  ccdExposure : `lsst.afw.image.Exposure`
1589  Input exposure to be masked.
1590  amp : `lsst.afw.table.AmpInfoCatalog`
1591  Catalog of parameters defining the amplifier on this
1592  exposure to mask.
1593  defects : `list`
1594  List of defects. Used to determine if the entire
1595  amplifier is bad.
1596 
1597  Returns
1598  -------
1599  badAmp : `Bool`
1600  If this is true, the entire amplifier area is covered by
1601  defects and unusable.
1602 
1603  """
1604  maskedImage = ccdExposure.getMaskedImage()
1605 
1606  badAmp = False
1607 
1608  # Check if entire amp region is defined as a defect (need to use amp.getBBox() for correct
1609  # comparison with current defects definition.
1610  if defects is not None:
1611  badAmp = bool(sum([v.getBBox().contains(amp.getBBox()) for v in defects]))
1612 
1613  # In the case of a bad amp, we will set mask to "BAD" (here use amp.getRawBBox() for correct
1614  # association with pixels in current ccdExposure).
1615  if badAmp:
1616  dataView = afwImage.MaskedImageF(maskedImage, amp.getRawBBox(),
1617  afwImage.PARENT)
1618  maskView = dataView.getMask()
1619  maskView |= maskView.getPlaneBitMask("BAD")
1620  del maskView
1621  return badAmp
1622 
1623  # Mask remaining defects after assembleCcd() to allow for defects that cross amplifier boundaries.
1624  # Saturation and suspect pixels can be masked now, though.
1625  limits = dict()
1626  if self.config.doSaturation and not badAmp:
1627  limits.update({self.config.saturatedMaskName: amp.getSaturation()})
1628  if self.config.doSuspect and not badAmp:
1629  limits.update({self.config.suspectMaskName: amp.getSuspectLevel()})
1630 
1631  for maskName, maskThreshold in limits.items():
1632  if not math.isnan(maskThreshold):
1633  dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
1634  isrFunctions.makeThresholdMask(
1635  maskedImage=dataView,
1636  threshold=maskThreshold,
1637  growFootprints=0,
1638  maskName=maskName
1639  )
1640 
1641  # Determine if we've fully masked this amplifier with SUSPECT and SAT pixels.
1642  maskView = afwImage.Mask(maskedImage.getMask(), amp.getRawDataBBox(),
1643  afwImage.PARENT)
1644  maskVal = maskView.getPlaneBitMask([self.config.saturatedMaskName,
1645  self.config.suspectMaskName])
1646  if numpy.all(maskView.getArray() & maskVal > 0):
1647  badAmp = True
1648  maskView |= maskView.getPlaneBitMask("BAD")
1649 
1650  return badAmp
1651 
1652  def overscanCorrection(self, ccdExposure, amp):
1653  """Apply overscan correction in place.
1654 
1655  This method does initial pixel rejection of the overscan
1656  region. The overscan can also be optionally segmented to
1657  allow for discontinuous overscan responses to be fit
1658  separately. The actual overscan subtraction is performed by
1659  the `lsst.ip.isr.isrFunctions.overscanCorrection` function,
1660  which is called here after the amplifier is preprocessed.
1661 
1662  Parameters
1663  ----------
1664  ccdExposure : `lsst.afw.image.Exposure`
1665  Exposure to have overscan correction performed.
1666  amp : `lsst.afw.table.AmpInfoCatalog`
1667  The amplifier to consider while correcting the overscan.
1668 
1669  Returns
1670  -------
1671  overscanResults : `lsst.pipe.base.Struct`
1672  Result struct with components:
1673  - ``imageFit`` : scalar or `lsst.afw.image.Image`
1674  Value or fit subtracted from the amplifier image data.
1675  - ``overscanFit`` : scalar or `lsst.afw.image.Image`
1676  Value or fit subtracted from the overscan image data.
1677  - ``overscanImage`` : `lsst.afw.image.Image`
1678  Image of the overscan region with the overscan
1679  correction applied. This quantity is used to estimate
1680  the amplifier read noise empirically.
1681 
1682  Raises
1683  ------
1684  RuntimeError
1685  Raised if the ``amp`` does not contain raw pixel information.
1686 
1687  See Also
1688  --------
1689  lsst.ip.isr.isrFunctions.overscanCorrection
1690  """
1691  if not amp.getHasRawInfo():
1692  raise RuntimeError("This method must be executed on an amp with raw information.")
1693 
1694  if amp.getRawHorizontalOverscanBBox().isEmpty():
1695  self.log.info("ISR_OSCAN: No overscan region. Not performing overscan correction.")
1696  return None
1697 
1698  statControl = afwMath.StatisticsControl()
1699  statControl.setAndMask(ccdExposure.mask.getPlaneBitMask("SAT"))
1700 
1701  # Determine the bounding boxes
1702  dataBBox = amp.getRawDataBBox()
1703  oscanBBox = amp.getRawHorizontalOverscanBBox()
1704  dx0 = 0
1705  dx1 = 0
1706 
1707  prescanBBox = amp.getRawPrescanBBox()
1708  if (oscanBBox.getBeginX() > prescanBBox.getBeginX()): # amp is at the right
1709  dx0 += self.config.overscanNumLeadingColumnsToSkip
1710  dx1 -= self.config.overscanNumTrailingColumnsToSkip
1711  else:
1712  dx0 += self.config.overscanNumTrailingColumnsToSkip
1713  dx1 -= self.config.overscanNumLeadingColumnsToSkip
1714 
1715  # Determine if we need to work on subregions of the amplifier and overscan.
1716  imageBBoxes = []
1717  overscanBBoxes = []
1718 
1719  if ((self.config.overscanBiasJump and
1720  self.config.overscanBiasJumpLocation) and
1721  (ccdExposure.getMetadata().exists(self.config.overscanBiasJumpKeyword) and
1722  ccdExposure.getMetadata().getScalar(self.config.overscanBiasJumpKeyword) in
1723  self.config.overscanBiasJumpDevices)):
1724  if amp.getReadoutCorner() in (afwTable.LL, afwTable.LR):
1725  yLower = self.config.overscanBiasJumpLocation
1726  yUpper = dataBBox.getHeight() - yLower
1727  else:
1728  yUpper = self.config.overscanBiasJumpLocation
1729  yLower = dataBBox.getHeight() - yUpper
1730 
1731  imageBBoxes.append(afwGeom.Box2I(dataBBox.getBegin(),
1732  afwGeom.Extent2I(dataBBox.getWidth(), yLower)))
1733  overscanBBoxes.append(afwGeom.Box2I(oscanBBox.getBegin() +
1734  afwGeom.Extent2I(dx0, 0),
1735  afwGeom.Extent2I(oscanBBox.getWidth() - dx0 + dx1,
1736  yLower)))
1737 
1738  imageBBoxes.append(afwGeom.Box2I(dataBBox.getBegin() + afwGeom.Extent2I(0, yLower),
1739  afwGeom.Extent2I(dataBBox.getWidth(), yUpper)))
1740  overscanBBoxes.append(afwGeom.Box2I(oscanBBox.getBegin() + afwGeom.Extent2I(dx0, yLower),
1741  afwGeom.Extent2I(oscanBBox.getWidth() - dx0 + dx1,
1742  yUpper)))
1743  else:
1744  imageBBoxes.append(afwGeom.Box2I(dataBBox.getBegin(),
1745  afwGeom.Extent2I(dataBBox.getWidth(), dataBBox.getHeight())))
1746  overscanBBoxes.append(afwGeom.Box2I(oscanBBox.getBegin() + afwGeom.Extent2I(dx0, 0),
1747  afwGeom.Extent2I(oscanBBox.getWidth() - dx0 + dx1,
1748  oscanBBox.getHeight())))
1749 
1750  # Perform overscan correction on subregions, ensuring saturated pixels are masked.
1751  for imageBBox, overscanBBox in zip(imageBBoxes, overscanBBoxes):
1752  ampImage = ccdExposure.maskedImage[imageBBox]
1753  overscanImage = ccdExposure.maskedImage[overscanBBox]
1754 
1755  overscanArray = overscanImage.image.array
1756  median = numpy.ma.median(numpy.ma.masked_where(overscanImage.mask.array, overscanArray))
1757  bad = numpy.where(numpy.abs(overscanArray - median) > self.config.overscanMaxDev)
1758  overscanImage.mask.array[bad] = overscanImage.mask.getPlaneBitMask("SAT")
1759 
1760  statControl = afwMath.StatisticsControl()
1761  statControl.setAndMask(ccdExposure.mask.getPlaneBitMask("SAT"))
1762 
1763  overscanResults = isrFunctions.overscanCorrection(ampMaskedImage=ampImage,
1764  overscanImage=overscanImage,
1765  fitType=self.config.overscanFitType,
1766  order=self.config.overscanOrder,
1767  collapseRej=self.config.overscanNumSigmaClip,
1768  statControl=statControl,
1769  overscanIsInt=self.config.overscanIsInt
1770  )
1771 
1772  # Measure average overscan levels and record them in the metadata
1773  levelStat = afwMath.MEDIAN
1774  sigmaStat = afwMath.STDEVCLIP
1775 
1776  sctrl = afwMath.StatisticsControl(self.config.qa.flatness.clipSigma,
1777  self.config.qa.flatness.nIter)
1778  metadata = ccdExposure.getMetadata()
1779  ampNum = amp.getName()
1780  if self.config.overscanFitType in ("MEDIAN", "MEAN", "MEANCLIP"):
1781  metadata.set("ISR_OSCAN_LEVEL%s" % ampNum, overscanResults.overscanFit)
1782  metadata.set("ISR_OSCAN_SIGMA%s" % ampNum, 0.0)
1783  else:
1784  stats = afwMath.makeStatistics(overscanResults.overscanFit, levelStat | sigmaStat, sctrl)
1785  metadata.set("ISR_OSCAN_LEVEL%s" % ampNum, stats.getValue(levelStat))
1786  metadata.set("ISR_OSCAN_SIGMA%s" % ampNum, stats.getValue(sigmaStat))
1787 
1788  return overscanResults
1789 
1790  def updateVariance(self, ampExposure, amp, overscanImage=None):
1791  """Set the variance plane using the amplifier gain and read noise
1792 
1793  The read noise is calculated from the ``overscanImage`` if the
1794  ``doEmpiricalReadNoise`` option is set in the configuration; otherwise
1795  the value from the amplifier data is used.
1796 
1797  Parameters
1798  ----------
1799  ampExposure : `lsst.afw.image.Exposure`
1800  Exposure to process.
1801  amp : `lsst.afw.table.AmpInfoRecord` or `FakeAmp`
1802  Amplifier detector data.
1803  overscanImage : `lsst.afw.image.MaskedImage`, optional.
1804  Image of overscan, required only for empirical read noise.
1805 
1806  See also
1807  --------
1808  lsst.ip.isr.isrFunctions.updateVariance
1809  """
1810  maskPlanes = [self.config.saturatedMaskName, self.config.suspectMaskName]
1811  gain = amp.getGain()
1812 
1813  if math.isnan(gain):
1814  gain = 1.0
1815  self.log.warn("Gain set to NAN! Updating to 1.0 to generate Poisson variance.")
1816  elif gain <= 0:
1817  patchedGain = 1.0
1818  self.log.warn("Gain for amp %s == %g <= 0; setting to %f" %
1819  (amp.getName(), gain, patchedGain))
1820  gain = patchedGain
1821 
1822  if self.config.doEmpiricalReadNoise and overscanImage is None:
1823  self.log.info("Overscan is none for EmpiricalReadNoise")
1824 
1825  if self.config.doEmpiricalReadNoise and overscanImage is not None:
1826  stats = afwMath.StatisticsControl()
1827  stats.setAndMask(overscanImage.mask.getPlaneBitMask(maskPlanes))
1828  readNoise = afwMath.makeStatistics(overscanImage, afwMath.STDEVCLIP, stats).getValue()
1829  self.log.info("Calculated empirical read noise for amp %s: %f", amp.getName(), readNoise)
1830  else:
1831  readNoise = amp.getReadNoise()
1832 
1833  isrFunctions.updateVariance(
1834  maskedImage=ampExposure.getMaskedImage(),
1835  gain=gain,
1836  readNoise=readNoise,
1837  )
1838 
1839  def darkCorrection(self, exposure, darkExposure, invert=False):
1840  """!Apply dark correction in place.
1841 
1842  Parameters
1843  ----------
1844  exposure : `lsst.afw.image.Exposure`
1845  Exposure to process.
1846  darkExposure : `lsst.afw.image.Exposure`
1847  Dark exposure of the same size as ``exposure``.
1848  invert : `Bool`, optional
1849  If True, re-add the dark to an already corrected image.
1850 
1851  Raises
1852  ------
1853  RuntimeError
1854  Raised if either ``exposure`` or ``darkExposure`` do not
1855  have their dark time defined.
1856 
1857  See Also
1858  --------
1859  lsst.ip.isr.isrFunctions.darkCorrection
1860  """
1861  expScale = exposure.getInfo().getVisitInfo().getDarkTime()
1862  if math.isnan(expScale):
1863  raise RuntimeError("Exposure darktime is NAN")
1864  if darkExposure.getInfo().getVisitInfo() is not None:
1865  darkScale = darkExposure.getInfo().getVisitInfo().getDarkTime()
1866  else:
1867  # DM-17444: darkExposure.getInfo.getVisitInfo() is None
1868  # so getDarkTime() does not exist.
1869  darkScale = 1.0
1870 
1871  if math.isnan(darkScale):
1872  raise RuntimeError("Dark calib darktime is NAN")
1873  isrFunctions.darkCorrection(
1874  maskedImage=exposure.getMaskedImage(),
1875  darkMaskedImage=darkExposure.getMaskedImage(),
1876  expScale=expScale,
1877  darkScale=darkScale,
1878  invert=invert,
1879  trimToFit=self.config.doTrimToMatchCalib
1880  )
1881 
1882  def doLinearize(self, detector):
1883  """!Check if linearization is needed for the detector cameraGeom.
1884 
1885  Checks config.doLinearize and the linearity type of the first
1886  amplifier.
1887 
1888  Parameters
1889  ----------
1890  detector : `lsst.afw.cameraGeom.Detector`
1891  Detector to get linearity type from.
1892 
1893  Returns
1894  -------
1895  doLinearize : `Bool`
1896  If True, linearization should be performed.
1897  """
1898  return self.config.doLinearize and \
1899  detector.getAmpInfoCatalog()[0].getLinearityType() != NullLinearityType
1900 
1901  def flatCorrection(self, exposure, flatExposure, invert=False):
1902  """!Apply flat correction in place.
1903 
1904  Parameters
1905  ----------
1906  exposure : `lsst.afw.image.Exposure`
1907  Exposure to process.
1908  flatExposure : `lsst.afw.image.Exposure`
1909  Flat exposure of the same size as ``exposure``.
1910  invert : `Bool`, optional
1911  If True, unflatten an already flattened image.
1912 
1913  See Also
1914  --------
1915  lsst.ip.isr.isrFunctions.flatCorrection
1916  """
1917  isrFunctions.flatCorrection(
1918  maskedImage=exposure.getMaskedImage(),
1919  flatMaskedImage=flatExposure.getMaskedImage(),
1920  scalingType=self.config.flatScalingType,
1921  userScale=self.config.flatUserScale,
1922  invert=invert,
1923  trimToFit=self.config.doTrimToMatchCalib
1924  )
1925 
1926  def saturationDetection(self, exposure, amp):
1927  """!Detect saturated pixels and mask them using mask plane config.saturatedMaskName, in place.
1928 
1929  Parameters
1930  ----------
1931  exposure : `lsst.afw.image.Exposure`
1932  Exposure to process. Only the amplifier DataSec is processed.
1933  amp : `lsst.afw.table.AmpInfoCatalog`
1934  Amplifier detector data.
1935 
1936  See Also
1937  --------
1938  lsst.ip.isr.isrFunctions.makeThresholdMask
1939  """
1940  if not math.isnan(amp.getSaturation()):
1941  maskedImage = exposure.getMaskedImage()
1942  dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
1943  isrFunctions.makeThresholdMask(
1944  maskedImage=dataView,
1945  threshold=amp.getSaturation(),
1946  growFootprints=0,
1947  maskName=self.config.saturatedMaskName,
1948  )
1949 
1950  def saturationInterpolation(self, exposure):
1951  """!Interpolate over saturated pixels, in place.
1952 
1953  This method should be called after `saturationDetection`, to
1954  ensure that the saturated pixels have been identified in the
1955  SAT mask. It should also be called after `assembleCcd`, since
1956  saturated regions may cross amplifier boundaries.
1957 
1958  Parameters
1959  ----------
1960  exposure : `lsst.afw.image.Exposure`
1961  Exposure to process.
1962 
1963  See Also
1964  --------
1965  lsst.ip.isr.isrTask.saturationDetection
1966  lsst.ip.isr.isrFunctions.interpolateFromMask
1967  """
1968  isrFunctions.interpolateFromMask(
1969  maskedImage=exposure.getMaskedImage(),
1970  fwhm=self.config.fwhm,
1971  growSaturatedFootprints=self.config.growSaturationFootprintSize,
1972  maskNameList=list(self.config.saturatedMaskName),
1973  )
1974 
1975  def suspectDetection(self, exposure, amp):
1976  """!Detect suspect pixels and mask them using mask plane config.suspectMaskName, in place.
1977 
1978  Parameters
1979  ----------
1980  exposure : `lsst.afw.image.Exposure`
1981  Exposure to process. Only the amplifier DataSec is processed.
1982  amp : `lsst.afw.table.AmpInfoCatalog`
1983  Amplifier detector data.
1984 
1985  See Also
1986  --------
1987  lsst.ip.isr.isrFunctions.makeThresholdMask
1988 
1989  Notes
1990  -----
1991  Suspect pixels are pixels whose value is greater than amp.getSuspectLevel().
1992  This is intended to indicate pixels that may be affected by unknown systematics;
1993  for example if non-linearity corrections above a certain level are unstable
1994  then that would be a useful value for suspectLevel. A value of `nan` indicates
1995  that no such level exists and no pixels are to be masked as suspicious.
1996  """
1997  suspectLevel = amp.getSuspectLevel()
1998  if math.isnan(suspectLevel):
1999  return
2000 
2001  maskedImage = exposure.getMaskedImage()
2002  dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
2003  isrFunctions.makeThresholdMask(
2004  maskedImage=dataView,
2005  threshold=suspectLevel,
2006  growFootprints=0,
2007  maskName=self.config.suspectMaskName,
2008  )
2009 
2010  def maskDefect(self, exposure, defectBaseList):
2011  """!Mask defects using mask plane "BAD", in place.
2012 
2013  Parameters
2014  ----------
2015  exposure : `lsst.afw.image.Exposure`
2016  Exposure to process.
2017  defectBaseList : `List`
2018  List of defects to mask and interpolate.
2019 
2020  Notes
2021  -----
2022  Call this after CCD assembly, since defects may cross amplifier boundaries.
2023  """
2024  maskedImage = exposure.getMaskedImage()
2025  defectList = []
2026  for d in defectBaseList:
2027  bbox = d.getBBox()
2028  nd = measAlg.Defect(bbox)
2029  defectList.append(nd)
2030  isrFunctions.maskPixelsFromDefectList(maskedImage, defectList, maskName='BAD')
2031 
2032  if self.config.numEdgeSuspect > 0:
2033  goodBBox = maskedImage.getBBox()
2034  # This makes a bbox numEdgeSuspect pixels smaller than the image on each side
2035  goodBBox.grow(-self.config.numEdgeSuspect)
2036  # Mask pixels outside goodBBox as SUSPECT
2037  SourceDetectionTask.setEdgeBits(
2038  maskedImage,
2039  goodBBox,
2040  maskedImage.getMask().getPlaneBitMask("SUSPECT")
2041  )
2042 
2043  def maskAndInterpolateDefects(self, exposure, defectBaseList):
2044  """Mask and interpolate defects using mask plane "BAD", in place.
2045 
2046  Parameters
2047  ----------
2048  exposure : `lsst.afw.image.Exposure`
2049  Exposure to process.
2050  defectBaseList : `List` of `Defects`
2051 
2052  """
2053  self.maskDefects(exposure, defectBaseList)
2054  isrFunctions.interpolateFromMask(
2055  maskedImage=exposure.getMaskedImage(),
2056  fwhm=self.config.fwhm,
2057  growSaturatedFootprints=0,
2058  maskNameList=["BAD"],
2059  )
2060 
2061  def maskNan(self, exposure):
2062  """Mask NaNs using mask plane "UNMASKEDNAN", in place.
2063 
2064  Parameters
2065  ----------
2066  exposure : `lsst.afw.image.Exposure`
2067  Exposure to process.
2068 
2069  Notes
2070  -----
2071  We mask over all NaNs, including those that are masked with
2072  other bits (because those may or may not be interpolated over
2073  later, and we want to remove all NaNs). Despite this
2074  behaviour, the "UNMASKEDNAN" mask plane is used to preserve
2075  the historical name.
2076  """
2077  maskedImage = exposure.getMaskedImage()
2078 
2079  # Find and mask NaNs
2080  maskedImage.getMask().addMaskPlane("UNMASKEDNAN")
2081  maskVal = maskedImage.getMask().getPlaneBitMask("UNMASKEDNAN")
2082  numNans = maskNans(maskedImage, maskVal)
2083  self.metadata.set("NUMNANS", numNans)
2084  if numNans > 0:
2085  self.log.warn("There were %i unmasked NaNs", numNans)
2086 
2087  def measureBackground(self, exposure, IsrQaConfig=None):
2088  """Measure the image background in subgrids, for quality control purposes.
2089 
2090  Parameters
2091  ----------
2092  exposure : `lsst.afw.image.Exposure`
2093  Exposure to process.
2094  IsrQaConfig : `lsst.ip.isr.isrQa.IsrQaConfig`
2095  Configuration object containing parameters on which background
2096  statistics and subgrids to use.
2097  """
2098  if IsrQaConfig is not None:
2099  statsControl = afwMath.StatisticsControl(IsrQaConfig.flatness.clipSigma,
2100  IsrQaConfig.flatness.nIter)
2101  maskVal = exposure.getMaskedImage().getMask().getPlaneBitMask(["BAD", "SAT", "DETECTED"])
2102  statsControl.setAndMask(maskVal)
2103  maskedImage = exposure.getMaskedImage()
2104  stats = afwMath.makeStatistics(maskedImage, afwMath.MEDIAN | afwMath.STDEVCLIP, statsControl)
2105  skyLevel = stats.getValue(afwMath.MEDIAN)
2106  skySigma = stats.getValue(afwMath.STDEVCLIP)
2107  self.log.info("Flattened sky level: %f +/- %f" % (skyLevel, skySigma))
2108  metadata = exposure.getMetadata()
2109  metadata.set('SKYLEVEL', skyLevel)
2110  metadata.set('SKYSIGMA', skySigma)
2111 
2112  # calcluating flatlevel over the subgrids
2113  stat = afwMath.MEANCLIP if IsrQaConfig.flatness.doClip else afwMath.MEAN
2114  meshXHalf = int(IsrQaConfig.flatness.meshX/2.)
2115  meshYHalf = int(IsrQaConfig.flatness.meshY/2.)
2116  nX = int((exposure.getWidth() + meshXHalf) / IsrQaConfig.flatness.meshX)
2117  nY = int((exposure.getHeight() + meshYHalf) / IsrQaConfig.flatness.meshY)
2118  skyLevels = numpy.zeros((nX, nY))
2119 
2120  for j in range(nY):
2121  yc = meshYHalf + j * IsrQaConfig.flatness.meshY
2122  for i in range(nX):
2123  xc = meshXHalf + i * IsrQaConfig.flatness.meshX
2124 
2125  xLLC = xc - meshXHalf
2126  yLLC = yc - meshYHalf
2127  xURC = xc + meshXHalf - 1
2128  yURC = yc + meshYHalf - 1
2129 
2130  bbox = afwGeom.Box2I(afwGeom.Point2I(xLLC, yLLC), afwGeom.Point2I(xURC, yURC))
2131  miMesh = maskedImage.Factory(exposure.getMaskedImage(), bbox, afwImage.LOCAL)
2132 
2133  skyLevels[i, j] = afwMath.makeStatistics(miMesh, stat, statsControl).getValue()
2134 
2135  good = numpy.where(numpy.isfinite(skyLevels))
2136  skyMedian = numpy.median(skyLevels[good])
2137  flatness = (skyLevels[good] - skyMedian) / skyMedian
2138  flatness_rms = numpy.std(flatness)
2139  flatness_pp = flatness.max() - flatness.min() if len(flatness) > 0 else numpy.nan
2140 
2141  self.log.info("Measuring sky levels in %dx%d grids: %f" % (nX, nY, skyMedian))
2142  self.log.info("Sky flatness in %dx%d grids - pp: %f rms: %f" %
2143  (nX, nY, flatness_pp, flatness_rms))
2144 
2145  metadata.set('FLATNESS_PP', float(flatness_pp))
2146  metadata.set('FLATNESS_RMS', float(flatness_rms))
2147  metadata.set('FLATNESS_NGRIDS', '%dx%d' % (nX, nY))
2148  metadata.set('FLATNESS_MESHX', IsrQaConfig.flatness.meshX)
2149  metadata.set('FLATNESS_MESHY', IsrQaConfig.flatness.meshY)
2150 
2151  def roughZeroPoint(self, exposure):
2152  """Set an approximate magnitude zero point for the exposure.
2153 
2154  Parameters
2155  ----------
2156  exposure : `lsst.afw.image.Exposure`
2157  Exposure to process.
2158  """
2159  filterName = afwImage.Filter(exposure.getFilter().getId()).getName() # Canonical name for filter
2160  if filterName in self.config.fluxMag0T1:
2161  fluxMag0 = self.config.fluxMag0T1[filterName]
2162  else:
2163  self.log.warn("No rough magnitude zero point set for filter %s" % filterName)
2164  fluxMag0 = self.config.defaultFluxMag0T1
2165 
2166  expTime = exposure.getInfo().getVisitInfo().getExposureTime()
2167  if not expTime > 0: # handle NaN as well as <= 0
2168  self.log.warn("Non-positive exposure time; skipping rough zero point")
2169  return
2170 
2171  self.log.info("Setting rough magnitude zero point: %f" % (2.5*math.log10(fluxMag0*expTime),))
2172  exposure.setPhotoCalib(afwImage.makePhotoCalibFromCalibZeroPoint(fluxMag0*expTime, 0.0))
2173 
2174  def setValidPolygonIntersect(self, ccdExposure, fpPolygon):
2175  """!Set the valid polygon as the intersection of fpPolygon and the ccd corners.
2176 
2177  Parameters
2178  ----------
2179  ccdExposure : `lsst.afw.image.Exposure`
2180  Exposure to process.
2181  fpPolygon : `lsst.afw.geom.Polygon`
2182  Polygon in focal plane coordinates.
2183  """
2184  # Get ccd corners in focal plane coordinates
2185  ccd = ccdExposure.getDetector()
2186  fpCorners = ccd.getCorners(FOCAL_PLANE)
2187  ccdPolygon = Polygon(fpCorners)
2188 
2189  # Get intersection of ccd corners with fpPolygon
2190  intersect = ccdPolygon.intersectionSingle(fpPolygon)
2191 
2192  # Transform back to pixel positions and build new polygon
2193  ccdPoints = ccd.transform(intersect, FOCAL_PLANE, PIXELS)
2194  validPolygon = Polygon(ccdPoints)
2195  ccdExposure.getInfo().setValidPolygon(validPolygon)
2196 
2197  @contextmanager
2198  def flatContext(self, exp, flat, dark=None):
2199  """Context manager that applies and removes flats and darks,
2200  if the task is configured to apply them.
2201 
2202  Parameters
2203  ----------
2204  exp : `lsst.afw.image.Exposure`
2205  Exposure to process.
2206  flat : `lsst.afw.image.Exposure`
2207  Flat exposure the same size as ``exp``.
2208  dark : `lsst.afw.image.Exposure`, optional
2209  Dark exposure the same size as ``exp``.
2210 
2211  Yields
2212  ------
2213  exp : `lsst.afw.image.Exposure`
2214  The flat and dark corrected exposure.
2215  """
2216  if self.config.doDark and dark is not None:
2217  self.darkCorrection(exp, dark)
2218  if self.config.doFlat:
2219  self.flatCorrection(exp, flat)
2220  try:
2221  yield exp
2222  finally:
2223  if self.config.doFlat:
2224  self.flatCorrection(exp, flat, invert=True)
2225  if self.config.doDark and dark is not None:
2226  self.darkCorrection(exp, dark, invert=True)
2227 
2228  def debugView(self, exposure, stepname):
2229  """Utility function to examine ISR exposure at different stages.
2230 
2231  Parameters
2232  ----------
2233  exposure : `lsst.afw.image.Exposure`
2234  Exposure to view.
2235  stepname : `str`
2236  State of processing to view.
2237  """
2238  frame = getDebugFrame(self._display, stepname)
2239  if frame:
2240  display = getDisplay(frame)
2241  display.scale('asinh', 'zscale')
2242  display.mtv(exposure)
2243  prompt = "Press Enter to continue [c]... "
2244  while True:
2245  ans = input(prompt).lower()
2246  if ans in ("", "c",):
2247  break
2248 
2249 
2250 class FakeAmp(object):
2251  """A Detector-like object that supports returning gain and saturation level
2252 
2253  This is used when the input exposure does not have a detector.
2254 
2255  Parameters
2256  ----------
2257  exposure : `lsst.afw.image.Exposure`
2258  Exposure to generate a fake amplifier for.
2259  config : `lsst.ip.isr.isrTaskConfig`
2260  Configuration to apply to the fake amplifier.
2261  """
2262 
2263  def __init__(self, exposure, config):
2264  self._bbox = exposure.getBBox(afwImage.LOCAL)
2265  self._RawHorizontalOverscanBBox = afwGeom.Box2I()
2266  self._gain = config.gain
2267  self._readNoise = config.readNoise
2268  self._saturation = config.saturation
2269 
2270  def getBBox(self):
2271  return self._bbox
2272 
2273  def getRawBBox(self):
2274  return self._bbox
2275 
2276  def getHasRawInfo(self):
2277  return True # but see getRawHorizontalOverscanBBox()
2278 
2280  return self._RawHorizontalOverscanBBox
2281 
2282  def getGain(self):
2283  return self._gain
2284 
2285  def getReadNoise(self):
2286  return self._readNoise
2287 
2288  def getSaturation(self):
2289  return self._saturation
2290 
2291  def getSuspectLevel(self):
2292  return float("NaN")
2293 
2294 
2295 class RunIsrConfig(pexConfig.Config):
2296  isr = pexConfig.ConfigurableField(target=IsrTask, doc="Instrument signature removal")
2297 
2298 
2299 class RunIsrTask(pipeBase.CmdLineTask):
2300  """Task to wrap the default IsrTask to allow it to be retargeted.
2301 
2302  The standard IsrTask can be called directly from a command line
2303  program, but doing so removes the ability of the task to be
2304  retargeted. As most cameras override some set of the IsrTask
2305  methods, this would remove those data-specific methods in the
2306  output post-ISR images. This wrapping class fixes the issue,
2307  allowing identical post-ISR images to be generated by both the
2308  processCcd and isrTask code.
2309  """
2310  ConfigClass = RunIsrConfig
2311  _DefaultName = "runIsr"
2312 
2313  def __init__(self, *args, **kwargs):
2314  super().__init__(*args, **kwargs)
2315  self.makeSubtask("isr")
2316 
2317  def runDataRef(self, dataRef):
2318  """
2319  Parameters
2320  ----------
2321  dataRef : `lsst.daf.persistence.ButlerDataRef`
2322  data reference of the detector data to be processed
2323 
2324  Returns
2325  -------
2326  result : `pipeBase.Struct`
2327  Result struct with component:
2328 
2329  - exposure : `lsst.afw.image.Exposure`
2330  Post-ISR processed exposure.
2331  """
2332  return self.isr.runDataRef(dataRef)
def getInputDatasetTypes(cls, config)
Definition: isrTask.py:741
def runDataRef(self, sensorRef)
Definition: isrTask.py:1408
def measureBackground(self, exposure, IsrQaConfig=None)
Definition: isrTask.py:2087
def debugView(self, exposure, stepname)
Definition: isrTask.py:2228
def __init__(self, kwargs)
Definition: isrTask.py:731
def ensureExposure(self, inputExp, camera, detectorNum)
Definition: isrTask.py:1503
def readIsrData(self, dataRef, rawExposure)
Retrieve necessary frames for instrument signature removal.
Definition: isrTask.py:854
def adaptArgsAndRun(self, inputData, inputDataIds, outputDataIds, butler)
Definition: isrTask.py:808
def runDataRef(self, dataRef)
Definition: isrTask.py:2317
def __init__(self, args, kwargs)
Definition: isrTask.py:2313
def getPrerequisiteDatasetTypes(cls, config)
Definition: isrTask.py:791
def roughZeroPoint(self, exposure)
Definition: isrTask.py:2151
def maskAndInterpolateDefects(self, exposure, defectBaseList)
Definition: isrTask.py:2043
def getRawHorizontalOverscanBBox(self)
Definition: isrTask.py:2279
def maskNan(self, exposure)
Definition: isrTask.py:2061
def getOutputDatasetTypes(cls, config)
Definition: isrTask.py:778
def maskDefect(self, exposure, defectBaseList)
Mask defects using mask plane "BAD", in place.
Definition: isrTask.py:2010
def overscanCorrection(self, ccdExposure, amp)
Definition: isrTask.py:1652
def convertIntToFloat(self, exposure)
Definition: isrTask.py:1547
def flatCorrection(self, exposure, flatExposure, invert=False)
Apply flat correction in place.
Definition: isrTask.py:1901
def makeDatasetType(self, dsConfig)
Definition: isrTask.py:851
def getIsrExposure(self, dataRef, datasetType, immediate=True)
Retrieve a calibration dataset for removing instrument signature.
Definition: isrTask.py:1462
def darkCorrection(self, exposure, darkExposure, invert=False)
Apply dark correction in place.
Definition: isrTask.py:1839
def doLinearize(self, detector)
Check if linearization is needed for the detector cameraGeom.
Definition: isrTask.py:1882
def setValidPolygonIntersect(self, ccdExposure, fpPolygon)
Set the valid polygon as the intersection of fpPolygon and the ccd corners.
Definition: isrTask.py:2174
def maskAmplifier(self, ccdExposure, amp, defects)
Definition: isrTask.py:1583
def getPerDatasetTypeDimensions(cls, config)
Definition: isrTask.py:801
def run(self, ccdExposure, camera=None, bias=None, linearizer=None, crosstalkSources=None, dark=None, flat=None, bfKernel=None, defects=None, fringes=None, opticsTransmission=None, filterTransmission=None, sensorTransmission=None, atmosphereTransmission=None, detectorNum=None, strayLightData=None, isGen3=False)
Perform instrument signature removal on an exposure.
Definition: isrTask.py:992
def flatContext(self, exp, flat, dark=None)
Definition: isrTask.py:2198
size_t maskNans(afw::image::MaskedImage< PixelT > const &mi, afw::image::MaskPixel maskVal, afw::image::MaskPixel allow=0)
Mask NANs in an image.
Definition: Isr.cc:34
def updateVariance(self, ampExposure, amp, overscanImage=None)
Definition: isrTask.py:1790
def suspectDetection(self, exposure, amp)
Detect suspect pixels and mask them using mask plane config.suspectMaskName, in place.
Definition: isrTask.py:1975
def saturationInterpolation(self, exposure)
Interpolate over saturated pixels, in place.
Definition: isrTask.py:1950
def saturationDetection(self, exposure, amp)
Detect saturated pixels and mask them using mask plane config.saturatedMaskName, in place...
Definition: isrTask.py:1926
def __init__(self, exposure, config)
Definition: isrTask.py:2263