lsst.ip.isr  14.0-10-g2094653+11
isrTask.py
Go to the documentation of this file.
1 from __future__ import absolute_import, division, print_function
2 from builtins import range
3 from builtins import object
4 #
5 # LSST Data Management System
6 # Copyright 2008-2016 AURA/LSST.
7 #
8 # This product includes software developed by the
9 # LSST Project (http://www.lsst.org/).
10 #
11 # This program is free software: you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation, either version 3 of the License, or
14 # (at your option) any later version.
15 #
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
20 #
21 # You should have received a copy of the LSST License Statement and
22 # the GNU General Public License along with this program. If not,
23 # see <http://www.lsstcorp.org/LegalNotices/>.
24 #
25 import math
26 import numpy
27 
28 import lsst.afw.geom as afwGeom
29 import lsst.afw.image as afwImage
30 import lsst.meas.algorithms as measAlg
31 import lsst.pex.config as pexConfig
32 import lsst.pipe.base as pipeBase
33 import lsst.afw.math as afwMath
34 from lsst.daf.persistence import ButlerDataRef
35 from lsstDebug import getDebugFrame
36 from lsst.afw.display import getDisplay
37 from . import isrFunctions
38 from .assembleCcdTask import AssembleCcdTask
39 from .fringe import FringeTask
40 from lsst.afw.geom import Polygon
41 from lsst.afw.geom.wcsUtils import makeDistortedTanWcs
42 from lsst.afw.cameraGeom import PIXELS, FOCAL_PLANE, FIELD_ANGLE, NullLinearityType
43 from contextlib import contextmanager
44 from .isr import maskNans
45 from .crosstalk import CrosstalkTask
46 
47 
48 class IsrTaskConfig(pexConfig.Config):
49  doBias = pexConfig.Field(
50  dtype=bool,
51  doc="Apply bias frame correction?",
52  default=True,
53  )
54  doDark = pexConfig.Field(
55  dtype=bool,
56  doc="Apply dark frame correction?",
57  default=True,
58  )
59  doFlat = pexConfig.Field(
60  dtype=bool,
61  doc="Apply flat field correction?",
62  default=True,
63  )
64  doFringe = pexConfig.Field(
65  dtype=bool,
66  doc="Apply fringe correction?",
67  default=True,
68  )
69  doDefect = pexConfig.Field(
70  dtype=bool,
71  doc="Apply correction for CCD defects, e.g. hot pixels?",
72  default=True,
73  )
74  doAddDistortionModel = pexConfig.Field(
75  dtype=bool,
76  doc="Apply a distortion model based on camera geometry to the WCS?",
77  default=True,
78  )
79  doWrite = pexConfig.Field(
80  dtype=bool,
81  doc="Persist postISRCCD?",
82  default=True,
83  )
84  assembleCcd = pexConfig.ConfigurableField(
85  target=AssembleCcdTask,
86  doc="CCD assembly task",
87  )
88  gain = pexConfig.Field(
89  dtype=float,
90  doc="The gain to use if no Detector is present in the Exposure (ignored if NaN)",
91  default=float("NaN"),
92  )
93  readNoise = pexConfig.Field(
94  dtype=float,
95  doc="The read noise to use if no Detector is present in the Exposure",
96  default=0.0,
97  )
98  saturation = pexConfig.Field(
99  dtype=float,
100  doc="The saturation level to use if no Detector is present in the Exposure (ignored if NaN)",
101  default=float("NaN"),
102  )
103  fringeAfterFlat = pexConfig.Field(
104  dtype=bool,
105  doc="Do fringe subtraction after flat-fielding?",
106  default=True,
107  )
108  fringe = pexConfig.ConfigurableField(
109  target=FringeTask,
110  doc="Fringe subtraction task",
111  )
112  fwhm = pexConfig.Field(
113  dtype=float,
114  doc="FWHM of PSF (arcsec)",
115  default=1.0,
116  )
117  saturatedMaskName = pexConfig.Field(
118  dtype=str,
119  doc="Name of mask plane to use in saturation detection and interpolation",
120  default="SAT",
121  )
122  suspectMaskName = pexConfig.Field(
123  dtype=str,
124  doc="Name of mask plane to use for suspect pixels",
125  default="SUSPECT",
126  )
127  flatScalingType = pexConfig.ChoiceField(
128  dtype=str,
129  doc="The method for scaling the flat on the fly.",
130  default='USER',
131  allowed={
132  "USER": "Scale by flatUserScale",
133  "MEAN": "Scale by the inverse of the mean",
134  "MEDIAN": "Scale by the inverse of the median",
135  },
136  )
137  flatUserScale = pexConfig.Field(
138  dtype=float,
139  doc="If flatScalingType is 'USER' then scale flat by this amount; ignored otherwise",
140  default=1.0,
141  )
142  overscanFitType = pexConfig.ChoiceField(
143  dtype=str,
144  doc="The method for fitting the overscan bias level.",
145  default='MEDIAN',
146  allowed={
147  "POLY": "Fit ordinary polynomial to the longest axis of the overscan region",
148  "CHEB": "Fit Chebyshev polynomial to the longest axis of the overscan region",
149  "LEG": "Fit Legendre polynomial to the longest axis of the overscan region",
150  "NATURAL_SPLINE": "Fit natural spline to the longest axis of the overscan region",
151  "CUBIC_SPLINE": "Fit cubic spline to the longest axis of the overscan region",
152  "AKIMA_SPLINE": "Fit Akima spline to the longest axis of the overscan region",
153  "MEAN": "Correct using the mean of the overscan region",
154  "MEDIAN": "Correct using the median of the overscan region",
155  },
156  )
157  overscanOrder = pexConfig.Field(
158  dtype=int,
159  doc=("Order of polynomial or to fit if overscan fit type is a polynomial, " +
160  "or number of spline knots if overscan fit type is a spline."),
161  default=1,
162  )
163  overscanRej = pexConfig.Field(
164  dtype=float,
165  doc="Rejection threshold (sigma) for collapsing overscan before fit",
166  default=3.0,
167  )
168  growSaturationFootprintSize = pexConfig.Field(
169  dtype=int,
170  doc="Number of pixels by which to grow the saturation footprints",
171  default=1,
172  )
173  doSaturationInterpolation = pexConfig.Field(
174  dtype=bool,
175  doc="Perform interpolation over pixels masked as saturated?",
176  default=True,
177  )
178  doNanInterpAfterFlat = pexConfig.Field(
179  dtype=bool,
180  doc=("If True, ensure we interpolate NaNs after flat-fielding, even if we "
181  "also have to interpolate them before flat-fielding."),
182  default=False,
183  )
184  fluxMag0T1 = pexConfig.Field(
185  dtype=float,
186  doc="The approximate flux of a zero-magnitude object in a one-second exposure",
187  default=1e10,
188  )
189  keysToRemoveFromAssembledCcd = pexConfig.ListField(
190  dtype=str,
191  doc="fields to remove from the metadata of the assembled ccd.",
192  default=[],
193  )
194  doAssembleIsrExposures = pexConfig.Field(
195  dtype=bool,
196  default=False,
197  doc="Assemble amp-level calibration exposures into ccd-level exposure?"
198  )
199  doAssembleCcd = pexConfig.Field(
200  dtype=bool,
201  default=True,
202  doc="Assemble amp-level exposures into a ccd-level exposure?"
203  )
204  expectWcs = pexConfig.Field(
205  dtype=bool,
206  default=True,
207  doc="Expect input science images to have a WCS (set False for e.g. spectrographs)"
208  )
209  doLinearize = pexConfig.Field(
210  dtype=bool,
211  doc="Correct for nonlinearity of the detector's response?",
212  default=True,
213  )
214  doCrosstalk = pexConfig.Field(
215  dtype=bool,
216  doc="Apply intra-CCD crosstalk correction?",
217  default=False,
218  )
219  crosstalk = pexConfig.ConfigurableField(
220  target=CrosstalkTask,
221  doc="Intra-CCD crosstalk correction",
222  )
223  doBrighterFatter = pexConfig.Field(
224  dtype=bool,
225  default=False,
226  doc="Apply the brighter fatter correction"
227  )
228  brighterFatterKernelFile = pexConfig.Field(
229  dtype=str,
230  default='',
231  doc="Kernel file used for the brighter fatter correction"
232  )
233  brighterFatterMaxIter = pexConfig.Field(
234  dtype=int,
235  default=10,
236  doc="Maximum number of iterations for the brighter fatter correction"
237  )
238  brighterFatterThreshold = pexConfig.Field(
239  dtype=float,
240  default=1000,
241  doc="Threshold used to stop iterating the brighter fatter correction. It is the "
242  " absolute value of the difference between the current corrected image and the one"
243  " from the previous iteration summed over all the pixels."
244  )
245  brighterFatterApplyGain = pexConfig.Field(
246  dtype=bool,
247  default=True,
248  doc="Should the gain be applied when applying the brighter fatter correction?"
249  )
250  datasetType = pexConfig.Field(
251  dtype=str,
252  doc="Dataset type for input data; users will typically leave this alone, "
253  "but camera-specific ISR tasks will override it",
254  default="raw",
255  )
256  fallbackFilterName = pexConfig.Field(dtype=str,
257  doc="Fallback default filter name for calibrations", optional=True)
258  doAttachTransmissionCurve = pexConfig.Field(
259  dtype=bool,
260  default=False,
261  doc="Construct and attach a wavelength-dependent throughput curve for this CCD image?"
262  )
263  doUseOpticsTransmission = pexConfig.Field(
264  dtype=bool,
265  default=True,
266  doc="Load and use transmission_optics (if doAttachTransmissionCurve is True)?"
267  )
268  doUseFilterTransmission = pexConfig.Field(
269  dtype=bool,
270  default=True,
271  doc="Load and use transmission_filter (if doAttachTransmissionCurve is True)?"
272  )
273  doUseSensorTransmission = pexConfig.Field(
274  dtype=bool,
275  default=True,
276  doc="Load and use transmission_sensor (if doAttachTransmissionCurve is True)?"
277  )
278  doUseAtmosphereTransmission = pexConfig.Field(
279  dtype=bool,
280  default=True,
281  doc="Load and use transmission_atmosphere (if doAttachTransmissionCurve is True)?"
282  )
283 
284 
290 
291 
292 class IsrTask(pipeBase.CmdLineTask):
293  """!
294  @anchor IsrTask_
295 
296  @brief Apply common instrument signature correction algorithms to a raw frame.
297 
298  @section ip_isr_isr_Contents Contents
299 
300  - @ref ip_isr_isr_Purpose
301  - @ref ip_isr_isr_Initialize
302  - @ref ip_isr_isr_IO
303  - @ref ip_isr_isr_Config
304  - @ref ip_isr_isr_Debug
305 
306 
307  @section ip_isr_isr_Purpose Description
308 
309  The process for correcting imaging data is very similar from camera to camera.
310  This task provides a vanilla implementation of doing these corrections, including
311  the ability to turn certain corrections off if they are not needed.
312  The inputs to the primary method, run, are a raw exposure to be corrected and the
313  calibration data products. The raw input is a single chip sized mosaic of all amps
314  including overscans and other non-science pixels.
315  The method runDataRef() is intended for use by a lsst.pipe.base.cmdLineTask.CmdLineTask
316  and takes as input only a daf.persistence.butlerSubset.ButlerDataRef.
317  This task may not meet all needs and it is expected that it will be subclassed for
318  specific applications.
319 
320  @section ip_isr_isr_Initialize Task initialization
321 
322  @copydoc \_\_init\_\_
323 
324  @section ip_isr_isr_IO Inputs/Outputs to the run method
325 
326  @copydoc run
327 
328  @section ip_isr_isr_Config Configuration parameters
329 
330  See @ref IsrTaskConfig
331 
332  @section ip_isr_isr_Debug Debug variables
333 
334  The @link lsst.pipe.base.cmdLineTask.CmdLineTask command line task@endlink interface supports a
335  flag @c --debug, @c -d to import @b debug.py from your @c PYTHONPATH; see <a
336  href="http://lsst-web.ncsa.illinois.edu/~buildbot/doxygen/x_masterDoxyDoc/base_debug.html">
337  Using lsstDebug to control debugging output</a> for more about @b debug.py files.
338 
339  The available variables in IsrTask are:
340  <DL>
341  <DT> @c display
342  <DD> A dictionary containing debug point names as keys with frame number as value. Valid keys are:
343  <DL>
344  <DT> postISRCCD
345  <DD> display exposure after ISR has been applied
346  </DL>
347  </DL>
348 
349  For example, put something like
350  @code{.py}
351  import lsstDebug
352  def DebugInfo(name):
353  di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
354  if name == "lsst.ip.isrFunctions.isrTask":
355  di.display = {'postISRCCD':2}
356  return di
357  lsstDebug.Info = DebugInfo
358  @endcode
359  into your debug.py file and run the commandline task with the @c --debug flag.
360 
361  <HR>
362  """
363  ConfigClass = IsrTaskConfig
364  _DefaultName = "isr"
365 
366  def __init__(self, *args, **kwargs):
367  '''!Constructor for IsrTask
368  @param[in] *args a list of positional arguments passed on to the Task constructor
369  @param[in] **kwargs a dictionary of keyword arguments passed on to the Task constructor
370  Call the lsst.pipe.base.task.Task.__init__ method
371  Then setup the assembly and fringe correction subtasks
372  '''
373  pipeBase.Task.__init__(self, *args, **kwargs)
374  self.makeSubtask("assembleCcd")
375  self.makeSubtask("fringe")
376  self.makeSubtask("crosstalk")
377 
378  def readIsrData(self, dataRef, rawExposure):
379  """!Retrieve necessary frames for instrument signature removal
380  @param[in] dataRef a daf.persistence.butlerSubset.ButlerDataRef
381  of the detector data to be processed
382  @param[in] rawExposure a reference raw exposure that will later be
383  corrected with the retrieved calibration data;
384  should not be modified in this method.
385  @return a pipeBase.Struct with fields containing kwargs expected by run()
386  - bias: exposure of bias frame
387  - dark: exposure of dark frame
388  - flat: exposure of flat field
389  - defects: list of detects
390  - fringeStruct: a pipeBase.Struct with field fringes containing
391  exposure of fringe frame or list of fringe exposure
392  """
393  ccd = rawExposure.getDetector()
394 
395  biasExposure = self.getIsrExposure(dataRef, "bias") if self.config.doBias else None
396  # immediate=True required for functors and linearizers are functors; see ticket DM-6515
397  linearizer = dataRef.get("linearizer", immediate=True) if self.doLinearize(ccd) else None
398  darkExposure = self.getIsrExposure(dataRef, "dark") if self.config.doDark else None
399  flatExposure = self.getIsrExposure(dataRef, "flat") if self.config.doFlat else None
400  brighterFatterKernel = dataRef.get("brighterFatterKernel") if self.config.doBrighterFatter else None
401  defectList = dataRef.get("defects") if self.config.doDefect else None
402 
403  if self.config.doFringe and self.fringe.checkFilter(rawExposure):
404  fringeStruct = self.fringe.readFringes(dataRef, assembler=self.assembleCcd
405  if self.config.doAssembleIsrExposures else None)
406  else:
407  fringeStruct = pipeBase.Struct(fringes=None)
408 
409  if self.config.doAttachTransmissionCurve:
410  opticsTransmission = (dataRef.get("transmission_optics")
411  if self.config.doUseOpticsTransmission else None)
412  filterTransmission = (dataRef.get("transmission_filter")
413  if self.config.doUseFilterTransmission else None)
414  sensorTransmission = (dataRef.get("transmission_sensor")
415  if self.config.doUseSensorTransmission else None)
416  atmosphereTransmission = (dataRef.get("transmission_atmosphere")
417  if self.config.doUseAtmosphereTransmission else None)
418  else:
419  opticsTransmission = None
420  filterTransmission = None
421  sensorTransmission = None
422  atmosphereTransmission = None
423 
424  # Struct should include only kwargs to run()
425  return pipeBase.Struct(bias=biasExposure,
426  linearizer=linearizer,
427  dark=darkExposure,
428  flat=flatExposure,
429  defects=defectList,
430  fringes=fringeStruct,
431  bfKernel=brighterFatterKernel,
432  opticsTransmission=opticsTransmission,
433  filterTransmission=filterTransmission,
434  sensorTransmission=sensorTransmission,
435  atmosphereTransmission=atmosphereTransmission,
436  )
437 
438  @pipeBase.timeMethod
439  def run(self, ccdExposure, bias=None, linearizer=None, dark=None, flat=None, defects=None,
440  fringes=None, bfKernel=None, camera=None,
441  opticsTransmission=None, filterTransmission=None,
442  sensorTransmission=None, atmosphereTransmission=None):
443  """!Perform instrument signature removal on an exposure
444 
445  Steps include:
446  - Detect saturation, apply overscan correction, bias, dark and flat
447  - Perform CCD assembly
448  - Interpolate over defects, saturated pixels and all NaNs
449 
450  @param[in] ccdExposure lsst.afw.image.exposure of detector data
451  @param[in] bias exposure of bias frame
452  @param[in] linearizer linearizing functor; a subclass of lsst.ip.isrFunctions.LinearizeBase
453  @param[in] dark exposure of dark frame
454  @param[in] flat exposure of flatfield
455  @param[in] defects list of detects
456  @param[in] fringes a pipeBase.Struct with field fringes containing
457  exposure of fringe frame or list of fringe exposure
458  @param[in] bfKernel kernel for brighter-fatter correction
459  @param[in] camera camera geometry, an lsst.afw.cameraGeom.Camera;
460  used by addDistortionModel
461  @param[in] opticsTransmission a TransmissionCurve for the optics
462  @param[in] filterTransmission a TransmissionCurve for the filter
463  @param[in] sensorTransmission a TransmissionCurve for the sensor
464  @param[in] atmosphereTransmission a TransmissionCurve for the atmosphere
465 
466  @return a pipeBase.Struct with field:
467  - exposure
468  """
469  # parseAndRun expects to be able to call run() with a dataRef; see DM-6640
470  if isinstance(ccdExposure, ButlerDataRef):
471  return self.runDataRef(ccdExposure)
472 
473  ccd = ccdExposure.getDetector()
474 
475  # Validate Input
476  if self.config.doBias and bias is None:
477  raise RuntimeError("Must supply a bias exposure if config.doBias True")
478  if self.doLinearize(ccd) and linearizer is None:
479  raise RuntimeError("Must supply a linearizer if config.doBias True")
480  if self.config.doDark and dark is None:
481  raise RuntimeError("Must supply a dark exposure if config.doDark True")
482  if self.config.doFlat and flat is None:
483  raise RuntimeError("Must supply a flat exposure if config.doFlat True")
484  if self.config.doBrighterFatter and bfKernel is None:
485  raise RuntimeError("Must supply a kernel if config.doBrighterFatter True")
486  if fringes is None:
487  fringes = pipeBase.Struct(fringes=None)
488  if self.config.doFringe and not isinstance(fringes, pipeBase.Struct):
489  raise RuntimeError("Must supply fringe exposure as a pipeBase.Struct")
490  if self.config.doDefect and defects is None:
491  raise RuntimeError("Must supply defects if config.doDefect True")
492  if self.config.doAddDistortionModel and camera is None:
493  raise RuntimeError("Must supply camera if config.doAddDistortionModel True")
494 
495  ccdExposure = self.convertIntToFloat(ccdExposure)
496 
497  if not ccd:
498  assert not self.config.doAssembleCcd, "You need a Detector to run assembleCcd"
499  ccd = [FakeAmp(ccdExposure, self.config)]
500 
501  for amp in ccd:
502  # if ccdExposure is one amp, check for coverage to prevent performing ops multiple times
503  if ccdExposure.getBBox().contains(amp.getBBox()):
504  self.saturationDetection(ccdExposure, amp)
505  self.suspectDetection(ccdExposure, amp)
506  self.overscanCorrection(ccdExposure, amp)
507 
508  if self.config.doAssembleCcd:
509  ccdExposure = self.assembleCcd.assembleCcd(ccdExposure)
510  if self.config.expectWcs and not ccdExposure.getWcs():
511  self.log.warn("No WCS found in input exposure")
512 
513  if self.config.doBias:
514  self.biasCorrection(ccdExposure, bias)
515 
516  if self.doLinearize(ccd):
517  linearizer(image=ccdExposure.getMaskedImage().getImage(), detector=ccd, log=self.log)
518 
519  if self.config.doCrosstalk:
520  self.crosstalk.run(ccdExposure)
521 
522  for amp in ccd:
523  # if ccdExposure is one amp, check for coverage to prevent performing ops multiple times
524  if ccdExposure.getBBox().contains(amp.getBBox()):
525  ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
526  self.updateVariance(ampExposure, amp)
527 
528  interpolationDone = False
529 
530  if self.config.doBrighterFatter:
531 
532  # We need to apply flats and darks before we can interpolate, and we
533  # need to interpolate before we do B-F, but we do B-F without the
534  # flats and darks applied so we can work in units of electrons or holes.
535  # This context manager applies and then removes the darks and flats.
536  with self.flatContext(ccdExposure, flat, dark):
537  if self.config.doDefect:
538  self.maskAndInterpDefect(ccdExposure, defects)
539  if self.config.doSaturationInterpolation:
540  self.saturationInterpolation(ccdExposure)
541  self.maskAndInterpNan(ccdExposure)
542  interpolationDone = True
543 
544  self.brighterFatterCorrection(ccdExposure, bfKernel,
545  self.config.brighterFatterMaxIter,
546  self.config.brighterFatterThreshold,
547  self.config.brighterFatterApplyGain,
548  )
549 
550  if self.config.doDark:
551  self.darkCorrection(ccdExposure, dark)
552 
553  if self.config.doFringe and not self.config.fringeAfterFlat:
554  self.fringe.run(ccdExposure, **fringes.getDict())
555 
556  if self.config.doFlat:
557  self.flatCorrection(ccdExposure, flat)
558 
559  if not interpolationDone:
560  if self.config.doDefect:
561  self.maskAndInterpDefect(ccdExposure, defects)
562  if self.config.doSaturationInterpolation:
563  self.saturationInterpolation(ccdExposure)
564  if not interpolationDone or self.config.doNanInterpAfterFlat:
565  self.maskAndInterpNan(ccdExposure)
566 
567  if self.config.doFringe and self.config.fringeAfterFlat:
568  self.fringe.run(ccdExposure, **fringes.getDict())
569 
570  exposureTime = ccdExposure.getInfo().getVisitInfo().getExposureTime()
571  ccdExposure.getCalib().setFluxMag0(self.config.fluxMag0T1*exposureTime)
572 
573  if self.config.doAddDistortionModel:
574  self.addDistortionModel(exposure=ccdExposure, camera=camera)
575 
576  if self.config.doAttachTransmissionCurve:
577  self.attachTransmissionCurve(ccdExposure, opticsTransmission=opticsTransmission,
578  filterTransmission=filterTransmission,
579  sensorTransmission=sensorTransmission,
580  atmosphereTransmission=atmosphereTransmission)
581 
582  frame = getDebugFrame(self._display, "postISRCCD")
583  if frame:
584  getDisplay(frame).mtv(ccdExposure)
585 
586  return pipeBase.Struct(
587  exposure=ccdExposure,
588  )
589 
590  @pipeBase.timeMethod
591  def runDataRef(self, sensorRef):
592  """!Perform instrument signature removal on a ButlerDataRef of a Sensor
593 
594  - Read in necessary detrending/isr/calibration data
595  - Process raw exposure in run()
596  - Persist the ISR-corrected exposure as "postISRCCD" if config.doWrite is True
597 
598  @param[in] sensorRef daf.persistence.butlerSubset.ButlerDataRef of the
599  detector data to be processed
600  @return a pipeBase.Struct with fields:
601  - exposure: the exposure after application of ISR
602  """
603  self.log.info("Performing ISR on sensor %s" % (sensorRef.dataId))
604  ccdExposure = sensorRef.get('raw')
605  camera = sensorRef.get("camera")
606  if camera is None and self.config.doAddDistortionModel:
607  raise RuntimeError("config.doAddDistortionModel is True "
608  "but could not get a camera from the butler")
609  isrData = self.readIsrData(sensorRef, ccdExposure)
610 
611  result = self.run(ccdExposure, camera=camera, **isrData.getDict())
612 
613  if self.config.doWrite:
614  sensorRef.put(result.exposure, "postISRCCD")
615 
616  return result
617 
618  def convertIntToFloat(self, exposure):
619  """Convert an exposure from uint16 to float, set variance plane to 1 and mask plane to 0
620  """
621  if isinstance(exposure, afwImage.ExposureF):
622  # Nothing to be done
623  return exposure
624  if not hasattr(exposure, "convertF"):
625  raise RuntimeError("Unable to convert exposure (%s) to float" % type(exposure))
626 
627  newexposure = exposure.convertF()
628  maskedImage = newexposure.getMaskedImage()
629  varArray = maskedImage.getVariance().getArray()
630  varArray[:, :] = 1
631  maskArray = maskedImage.getMask().getArray()
632  maskArray[:, :] = 0
633  return newexposure
634 
635  def biasCorrection(self, exposure, biasExposure):
636  """!Apply bias correction in place
637 
638  @param[in,out] exposure exposure to process
639  @param[in] biasExposure bias exposure of same size as exposure
640  """
641  isrFunctions.biasCorrection(exposure.getMaskedImage(), biasExposure.getMaskedImage())
642 
643  def darkCorrection(self, exposure, darkExposure, invert=False):
644  """!Apply dark correction in place
645 
646  @param[in,out] exposure exposure to process
647  @param[in] darkExposure dark exposure of same size as exposure
648  @param[in] invert if True, remove the dark from an already-corrected image
649  """
650  expScale = exposure.getInfo().getVisitInfo().getDarkTime()
651  if math.isnan(expScale):
652  raise RuntimeError("Exposure darktime is NAN")
653  darkScale = darkExposure.getInfo().getVisitInfo().getDarkTime()
654  if math.isnan(darkScale):
655  raise RuntimeError("Dark calib darktime is NAN")
656  isrFunctions.darkCorrection(
657  maskedImage=exposure.getMaskedImage(),
658  darkMaskedImage=darkExposure.getMaskedImage(),
659  expScale=expScale,
660  darkScale=darkScale,
661  invert=invert
662  )
663 
664  def doLinearize(self, detector):
665  """!Is linearization wanted for this detector?
666 
667  Checks config.doLinearize and the linearity type of the first amplifier.
668 
669  @param[in] detector detector information (an lsst.afw.cameraGeom.Detector)
670  """
671  return self.config.doLinearize and \
672  detector.getAmpInfoCatalog()[0].getLinearityType() != NullLinearityType
673 
674  def updateVariance(self, ampExposure, amp):
675  """!Set the variance plane based on the image plane, plus amplifier gain and read noise
676 
677  @param[in,out] ampExposure exposure to process
678  @param[in] amp amplifier detector information
679  """
680  gain = amp.getGain()
681  if not math.isnan(gain):
682  if gain <= 0:
683  patchedGain = 1.0
684  self.log.warn("Gain for amp %s == %g <= 0; setting to %f" %
685  (amp.getName(), gain, patchedGain))
686  gain = patchedGain
687 
688  isrFunctions.updateVariance(
689  maskedImage=ampExposure.getMaskedImage(),
690  gain=gain,
691  readNoise=amp.getReadNoise(),
692  )
693 
694  def flatCorrection(self, exposure, flatExposure, invert=False):
695  """!Apply flat correction in place
696 
697  @param[in,out] exposure exposure to process
698  @param[in] flatExposure flatfield exposure same size as exposure
699  @param[in] invert if True, unflatten an already-flattened image instead.
700  """
701  isrFunctions.flatCorrection(
702  maskedImage=exposure.getMaskedImage(),
703  flatMaskedImage=flatExposure.getMaskedImage(),
704  scalingType=self.config.flatScalingType,
705  userScale=self.config.flatUserScale,
706  invert=invert
707  )
708 
709  def getIsrExposure(self, dataRef, datasetType, immediate=True):
710  """!Retrieve a calibration dataset for removing instrument signature
711 
712  @param[in] dataRef data reference for exposure
713  @param[in] datasetType type of dataset to retrieve (e.g. 'bias', 'flat')
714  @param[in] immediate if True, disable butler proxies to enable error
715  handling within this routine
716  @return exposure
717  """
718  try:
719  exp = dataRef.get(datasetType, immediate=immediate)
720  except Exception as exc1:
721  if not self.config.fallbackFilterName:
722  raise RuntimeError("Unable to retrieve %s for %s: %s" % (datasetType, dataRef.dataId, exc1))
723  try:
724  exp = dataRef.get(datasetType, filter=self.config.fallbackFilterName, immediate=immediate)
725  except Exception as exc2:
726  raise RuntimeError("Unable to retrieve %s for %s, even with fallback filter %s: %s AND %s" %
727  (datasetType, dataRef.dataId, self.config.fallbackFilterName, exc1, exc2))
728  self.log.warn("Using fallback calibration from filter %s" % self.config.fallbackFilterName)
729 
730  if self.config.doAssembleIsrExposures:
731  exp = self.assembleCcd.assembleCcd(exp)
732  return exp
733 
734  def saturationDetection(self, exposure, amp):
735  """!Detect saturated pixels and mask them using mask plane config.saturatedMaskName, in place
736 
737  @param[in,out] exposure exposure to process; only the amp DataSec is processed
738  @param[in] amp amplifier device data
739  """
740  if not math.isnan(amp.getSaturation()):
741  maskedImage = exposure.getMaskedImage()
742  dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
743  isrFunctions.makeThresholdMask(
744  maskedImage=dataView,
745  threshold=amp.getSaturation(),
746  growFootprints=0,
747  maskName=self.config.saturatedMaskName,
748  )
749 
750  def saturationInterpolation(self, ccdExposure):
751  """!Interpolate over saturated pixels, in place
752 
753  @param[in,out] ccdExposure exposure to process
754 
755  @warning:
756  - Call saturationDetection first, so that saturated pixels have been identified in the "SAT" mask.
757  - Call this after CCD assembly, since saturated regions may cross amplifier boundaries
758  """
759  isrFunctions.interpolateFromMask(
760  maskedImage=ccdExposure.getMaskedImage(),
761  fwhm=self.config.fwhm,
762  growFootprints=self.config.growSaturationFootprintSize,
763  maskName=self.config.saturatedMaskName,
764  )
765 
766  def suspectDetection(self, exposure, amp):
767  """!Detect suspect pixels and mask them using mask plane config.suspectMaskName, in place
768 
769  Suspect pixels are pixels whose value is greater than amp.getSuspectLevel().
770  This is intended to indicate pixels that may be affected by unknown systematics;
771  for example if non-linearity corrections above a certain level are unstable
772  then that would be a useful value for suspectLevel. A value of `nan` indicates
773  that no such level exists and no pixels are to be masked as suspicious.
774 
775  @param[in,out] exposure exposure to process; only the amp DataSec is processed
776  @param[in] amp amplifier device data
777  """
778  suspectLevel = amp.getSuspectLevel()
779  if math.isnan(suspectLevel):
780  return
781 
782  maskedImage = exposure.getMaskedImage()
783  dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
784  isrFunctions.makeThresholdMask(
785  maskedImage=dataView,
786  threshold=suspectLevel,
787  growFootprints=0,
788  maskName=self.config.suspectMaskName,
789  )
790 
791  def maskAndInterpDefect(self, ccdExposure, defectBaseList):
792  """!Mask defects using mask plane "BAD" and interpolate over them, in place
793 
794  @param[in,out] ccdExposure exposure to process
795  @param[in] defectBaseList a list of defects to mask and interpolate
796 
797  @warning: call this after CCD assembly, since defects may cross amplifier boundaries
798  """
799  maskedImage = ccdExposure.getMaskedImage()
800  defectList = []
801  for d in defectBaseList:
802  bbox = d.getBBox()
803  nd = measAlg.Defect(bbox)
804  defectList.append(nd)
805  isrFunctions.maskPixelsFromDefectList(maskedImage, defectList, maskName='BAD')
806  isrFunctions.interpolateDefectList(
807  maskedImage=maskedImage,
808  defectList=defectList,
809  fwhm=self.config.fwhm,
810  )
811 
812  def maskAndInterpNan(self, exposure):
813  """!Mask NaNs using mask plane "UNMASKEDNAN" and interpolate over them, in place
814 
815  We mask and interpolate over all NaNs, including those
816  that are masked with other bits (because those may or may
817  not be interpolated over later, and we want to remove all
818  NaNs). Despite this behaviour, the "UNMASKEDNAN" mask plane
819  is used to preserve the historical name.
820 
821  @param[in,out] exposure exposure to process
822  """
823  maskedImage = exposure.getMaskedImage()
824 
825  # Find and mask NaNs
826  maskedImage.getMask().addMaskPlane("UNMASKEDNAN")
827  maskVal = maskedImage.getMask().getPlaneBitMask("UNMASKEDNAN")
828  numNans = maskNans(maskedImage, maskVal)
829  self.metadata.set("NUMNANS", numNans)
830 
831  # Interpolate over these previously-unmasked NaNs
832  if numNans > 0:
833  self.log.warn("There were %i unmasked NaNs", numNans)
834  nanDefectList = isrFunctions.getDefectListFromMask(
835  maskedImage=maskedImage,
836  maskName='UNMASKEDNAN',
837  )
838  isrFunctions.interpolateDefectList(
839  maskedImage=exposure.getMaskedImage(),
840  defectList=nanDefectList,
841  fwhm=self.config.fwhm,
842  )
843 
844  def overscanCorrection(self, exposure, amp):
845  """!Apply overscan correction, in place
846 
847  @param[in,out] exposure exposure to process; must include both DataSec and BiasSec pixels
848  @param[in] amp amplifier device data
849  """
850  if not amp.getHasRawInfo():
851  raise RuntimeError("This method must be executed on an amp with raw information.")
852 
853  if amp.getRawHorizontalOverscanBBox().isEmpty():
854  self.log.info("No Overscan region. Not performing Overscan Correction.")
855  return None
856 
857  maskedImage = exposure.getMaskedImage()
858  dataView = maskedImage.Factory(maskedImage, amp.getRawDataBBox())
859 
860  expImage = exposure.getMaskedImage().getImage()
861  overscanImage = expImage.Factory(expImage, amp.getRawHorizontalOverscanBBox())
862 
863  isrFunctions.overscanCorrection(
864  ampMaskedImage=dataView,
865  overscanImage=overscanImage,
866  fitType=self.config.overscanFitType,
867  order=self.config.overscanOrder,
868  collapseRej=self.config.overscanRej,
869  )
870 
871  def addDistortionModel(self, exposure, camera):
872  """!Update the WCS in exposure with a distortion model based on camera geometry
873 
874  Add a model for optical distortion based on geometry found in `camera`
875  and the `exposure`'s detector. The raw input exposure is assumed
876  have a TAN WCS that has no compensation for optical distortion.
877  Two other possibilities are:
878  - The raw input exposure already has a model for optical distortion,
879  as is the case for raw DECam data.
880  In that case you should set config.doAddDistortionModel False.
881  - The raw input exposure has a model for distortion, but it has known
882  deficiencies severe enough to be worth fixing (e.g. because they
883  cause problems for fitting a better WCS). In that case you should
884  override this method with a version suitable for your raw data.
885 
886  @param[in,out] exposure exposure to process; must include a Detector and a WCS;
887  the WCS of the exposure is modified in place
888  @param[in] camera camera geometry; an lsst.afw.cameraGeom.Camera
889  """
890  self.log.info("Adding a distortion model to the WCS")
891  wcs = exposure.getWcs()
892  if wcs is None:
893  raise RuntimeError("exposure has no WCS")
894  if camera is None:
895  raise RuntimeError("camera is None")
896  detector = exposure.getDetector()
897  if detector is None:
898  raise RuntimeError("exposure has no Detector")
899  pixelToFocalPlane = detector.getTransform(PIXELS, FOCAL_PLANE)
900  focalPlaneToFieldAngle = camera.getTransformMap().getTransform(FOCAL_PLANE, FIELD_ANGLE)
901  distortedWcs = makeDistortedTanWcs(wcs, pixelToFocalPlane, focalPlaneToFieldAngle)
902  exposure.setWcs(distortedWcs)
903 
904  def setValidPolygonIntersect(self, ccdExposure, fpPolygon):
905  """!Set the valid polygon as the intersection of fpPolygon and the ccd corners
906 
907  @param[in,out] ccdExposure exposure to process
908  @param[in] fpPolygon Polygon in focal plane coordinates
909  """
910  # Get ccd corners in focal plane coordinates
911  ccd = ccdExposure.getDetector()
912  fpCorners = ccd.getCorners(FOCAL_PLANE)
913  ccdPolygon = Polygon(fpCorners)
914 
915  # Get intersection of ccd corners with fpPolygon
916  intersect = ccdPolygon.intersectionSingle(fpPolygon)
917 
918  # Transform back to pixel positions and build new polygon
919  ccdPoints = ccd.transform(intersect, FOCAL_PLANE, PIXELS)
920  validPolygon = Polygon(ccdPoints)
921  ccdExposure.getInfo().setValidPolygon(validPolygon)
922 
923  def brighterFatterCorrection(self, exposure, kernel, maxIter, threshold, applyGain):
924  """Apply brighter fatter correction in place for the image
925 
926  This correction takes a kernel that has been derived from flat field images to
927  redistribute the charge. The gradient of the kernel is the deflection
928  field due to the accumulated charge.
929 
930  Given the original image I(x) and the kernel K(x) we can compute the corrected image Ic(x)
931  using the following equation:
932 
933  Ic(x) = I(x) + 0.5*d/dx(I(x)*d/dx(int( dy*K(x-y)*I(y))))
934 
935  To evaluate the derivative term we expand it as follows:
936 
937  0.5 * ( d/dx(I(x))*d/dx(int(dy*K(x-y)*I(y))) + I(x)*d^2/dx^2(int(dy* K(x-y)*I(y))) )
938 
939  Because we use the measured counts instead of the incident counts we apply the correction
940  iteratively to reconstruct the original counts and the correction. We stop iterating when the
941  summed difference between the current corrected image and the one from the previous iteration
942  is below the threshold. We do not require convergence because the number of iterations is
943  too large a computational cost. How we define the threshold still needs to be evaluated, the
944  current default was shown to work reasonably well on a small set of images. For more information
945  on the method see DocuShare Document-19407.
946 
947  The edges as defined by the kernel are not corrected because they have spurious values
948  due to the convolution.
949  """
950  self.log.info("Applying brighter fatter correction")
951 
952  image = exposure.getMaskedImage().getImage()
953 
954  # The image needs to be units of electrons/holes
955  with self.gainContext(exposure, image, applyGain):
956 
957  kLx = numpy.shape(kernel)[0]
958  kLy = numpy.shape(kernel)[1]
959  kernelImage = afwImage.ImageD(kLx, kLy)
960  kernelImage.getArray()[:, :] = kernel
961  tempImage = image.clone()
962 
963  nanIndex = numpy.isnan(tempImage.getArray())
964  tempImage.getArray()[nanIndex] = 0.
965 
966  outImage = afwImage.ImageF(image.getDimensions())
967  corr = numpy.zeros_like(image.getArray())
968  prev_image = numpy.zeros_like(image.getArray())
969  convCntrl = afwMath.ConvolutionControl(False, True, 1)
970  fixedKernel = afwMath.FixedKernel(kernelImage)
971 
972  # Define boundary by convolution region. The region that the correction will be
973  # calculated for is one fewer in each dimension because of the second derivative terms.
974  # NOTE: these need to use integer math, as we're using start:end as numpy index ranges.
975  startX = kLx//2
976  endX = -kLx//2
977  startY = kLy//2
978  endY = -kLy//2
979 
980  for iteration in range(maxIter):
981 
982  afwMath.convolve(outImage, tempImage, fixedKernel, convCntrl)
983  tmpArray = tempImage.getArray()
984  outArray = outImage.getArray()
985 
986  with numpy.errstate(invalid="ignore", over="ignore"):
987  # First derivative term
988  gradTmp = numpy.gradient(tmpArray[startY:endY, startX:endX])
989  gradOut = numpy.gradient(outArray[startY:endY, startX:endX])
990  first = (gradTmp[0]*gradOut[0] + gradTmp[1]*gradOut[1])[1:-1, 1:-1]
991 
992  # Second derivative term
993  diffOut20 = numpy.diff(outArray, 2, 0)[startY:endY, startX + 1:endX - 1]
994  diffOut21 = numpy.diff(outArray, 2, 1)[startY + 1:endY - 1, startX:endX]
995  second = tmpArray[startY + 1:endY - 1, startX + 1:endX - 1]*(diffOut20 + diffOut21)
996 
997  corr[startY + 1:endY - 1, startX + 1:endX - 1] = 0.5*(first + second)
998 
999  tmpArray[:, :] = image.getArray()[:, :]
1000  tmpArray[nanIndex] = 0.
1001  tmpArray[startY:endY, startX:endX] += corr[startY:endY, startX:endX]
1002 
1003  if iteration > 0:
1004  diff = numpy.sum(numpy.abs(prev_image - tmpArray))
1005 
1006  if diff < threshold:
1007  break
1008  prev_image[:, :] = tmpArray[:, :]
1009 
1010  if iteration == maxIter - 1:
1011  self.log.warn("Brighter fatter correction did not converge, final difference %f" % diff)
1012 
1013  self.log.info("Finished brighter fatter in %d iterations" % (iteration + 1))
1014  image.getArray()[startY + 1:endY - 1, startX + 1:endX - 1] += \
1015  corr[startY + 1:endY - 1, startX + 1:endX - 1]
1016 
1017  def attachTransmissionCurve(self, exposure, opticsTransmission=None, filterTransmission=None,
1018  sensorTransmission=None, atmosphereTransmission=None):
1019  """Attach a TransmissionCurve to an Exposure, given separate curves for
1020  different components.
1021 
1022  Parameters
1023  ----------
1024  exposure : `lsst.afw.image.Exposure`
1025  Exposure object to modify by attaching the product of all given
1026  ``TransmissionCurves`` in post-assembly trimmed detector
1027  coordinates. Must have a valid ``Detector`` attached that matches
1028  the detector associated with sensorTransmission.
1029  opticsTransmission : `lsst.afw.image.TransmissionCurve`
1030  A ``TransmissionCurve`` that represents the throughput of the
1031  optics, to be evaluated in focal-plane coordinates.
1032  filterTransmission : `lsst.afw.image.TransmissionCurve`
1033  A ``TransmissionCurve`` that represents the throughput of the
1034  filter itself, to be evaluated in focal-plane coordinates.
1035  sensorTransmission : `lsst.afw.image.TransmissionCurve`
1036  A ``TransmissionCurve`` that represents the throughput of the
1037  sensor itself, to be evaluated in post-assembly trimmed detector
1038  coordinates.
1039  atmosphereTransmission : `lsst.afw.image.TransmissionCurve`
1040  A ``TransmissionCurve`` that represents the throughput of the
1041  atmosphere, assumed to be spatially constant.
1042 
1043  All ``TransmissionCurve`` arguments are optional; if none are provided,
1044  the attached ``TransmissionCurve`` will have unit transmission
1045  everywhere.
1046 
1047  Returns
1048  -------
1049  combined : ``lsst.afw.image.TransmissionCurve``
1050  The TransmissionCurve attached to the exposure.
1051  """
1052  return isrFunctions.attachTransmissionCurve(exposure, opticsTransmission=opticsTransmission,
1053  filterTransmission=filterTransmission,
1054  sensorTransmission=sensorTransmission,
1055  atmosphereTransmission=atmosphereTransmission)
1056 
1057  @contextmanager
1058  def gainContext(self, exp, image, apply):
1059  """Context manager that applies and removes gain
1060  """
1061  if apply:
1062  ccd = exp.getDetector()
1063  for amp in ccd:
1064  sim = image.Factory(image, amp.getBBox())
1065  sim *= amp.getGain()
1066 
1067  try:
1068  yield exp
1069  finally:
1070  if apply:
1071  ccd = exp.getDetector()
1072  for amp in ccd:
1073  sim = image.Factory(image, amp.getBBox())
1074  sim /= amp.getGain()
1075 
1076  @contextmanager
1077  def flatContext(self, exp, flat, dark=None):
1078  """Context manager that applies and removes flats and darks,
1079  if the task is configured to apply them.
1080  """
1081  if self.config.doDark and dark is not None:
1082  self.darkCorrection(exp, dark)
1083  if self.config.doFlat:
1084  self.flatCorrection(exp, flat)
1085  try:
1086  yield exp
1087  finally:
1088  if self.config.doFlat:
1089  self.flatCorrection(exp, flat, invert=True)
1090  if self.config.doDark and dark is not None:
1091  self.darkCorrection(exp, dark, invert=True)
1092 
1093 
1094 class FakeAmp(object):
1095  """A Detector-like object that supports returning gain and saturation level"""
1096 
1097  def __init__(self, exposure, config):
1098  self._bbox = exposure.getBBox(afwImage.LOCAL)
1099  self._RawHorizontalOverscanBBox = afwGeom.Box2I()
1100  self._gain = config.gain
1101  self._readNoise = config.readNoise
1102  self._saturation = config.saturation
1103 
1104  def getBBox(self):
1105  return self._bbox
1106 
1107  def getRawBBox(self):
1108  return self._bbox
1109 
1110  def getHasRawInfo(self):
1111  return True # but see getRawHorizontalOverscanBBox()
1112 
1114  return self._RawHorizontalOverscanBBox
1115 
1116  def getGain(self):
1117  return self._gain
1118 
1119  def getReadNoise(self):
1120  return self._readNoise
1121 
1122  def getSaturation(self):
1123  return self._saturation
1124 
1125  def getSuspectLevel(self):
1126  return float("NaN")
def brighterFatterCorrection(self, exposure, kernel, maxIter, threshold, applyGain)
Definition: isrTask.py:923
def runDataRef(self, sensorRef)
Perform instrument signature removal on a ButlerDataRef of a Sensor.
Definition: isrTask.py:591
def gainContext(self, exp, image, apply)
Definition: isrTask.py:1058
def __init__(self, args, kwargs)
Constructor for IsrTask.
Definition: isrTask.py:366
def readIsrData(self, dataRef, rawExposure)
Retrieve necessary frames for instrument signature removal.
Definition: isrTask.py:378
def attachTransmissionCurve(self, exposure, opticsTransmission=None, filterTransmission=None, sensorTransmission=None, atmosphereTransmission=None)
Definition: isrTask.py:1018
def maskAndInterpNan(self, exposure)
Mask NaNs using mask plane "UNMASKEDNAN" and interpolate over them, in place.
Definition: isrTask.py:812
def saturationInterpolation(self, ccdExposure)
Interpolate over saturated pixels, in place.
Definition: isrTask.py:750
Apply common instrument signature correction algorithms to a raw frame.
Definition: isrTask.py:292
def getRawHorizontalOverscanBBox(self)
Definition: isrTask.py:1113
def convertIntToFloat(self, exposure)
Definition: isrTask.py:618
def flatCorrection(self, exposure, flatExposure, invert=False)
Apply flat correction in place.
Definition: isrTask.py:694
def getIsrExposure(self, dataRef, datasetType, immediate=True)
Retrieve a calibration dataset for removing instrument signature.
Definition: isrTask.py:709
def run(self, ccdExposure, bias=None, linearizer=None, dark=None, flat=None, defects=None, fringes=None, bfKernel=None, camera=None, opticsTransmission=None, filterTransmission=None, sensorTransmission=None, atmosphereTransmission=None)
Perform instrument signature removal on an exposure.
Definition: isrTask.py:442
def darkCorrection(self, exposure, darkExposure, invert=False)
Apply dark correction in place.
Definition: isrTask.py:643
def doLinearize(self, detector)
Is linearization wanted for this detector?
Definition: isrTask.py:664
def addDistortionModel(self, exposure, camera)
Update the WCS in exposure with a distortion model based on camera geometry.
Definition: isrTask.py:871
def setValidPolygonIntersect(self, ccdExposure, fpPolygon)
Set the valid polygon as the intersection of fpPolygon and the ccd corners.
Definition: isrTask.py:904
def biasCorrection(self, exposure, biasExposure)
Apply bias correction in place.
Definition: isrTask.py:635
def flatContext(self, exp, flat, dark=None)
Definition: isrTask.py:1077
def updateVariance(self, ampExposure, amp)
Set the variance plane based on the image plane, plus amplifier gain and read noise.
Definition: isrTask.py:674
def overscanCorrection(self, exposure, amp)
Apply overscan correction, in place.
Definition: isrTask.py:844
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 suspectDetection(self, exposure, amp)
Detect suspect pixels and mask them using mask plane config.suspectMaskName, in place.
Definition: isrTask.py:766
def maskAndInterpDefect(self, ccdExposure, defectBaseList)
Mask defects using mask plane "BAD" and interpolate over them, in place.
Definition: isrTask.py:791
def saturationDetection(self, exposure, amp)
Detect saturated pixels and mask them using mask plane config.saturatedMaskName, in place...
Definition: isrTask.py:734
def __init__(self, exposure, config)
Definition: isrTask.py:1097