lsst.ip.isr  14.0-9-gdb3c3f2+1
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" % (amp.getName(), gain, patchedGain))
685  gain = patchedGain
686 
687  isrFunctions.updateVariance(
688  maskedImage=ampExposure.getMaskedImage(),
689  gain=gain,
690  readNoise=amp.getReadNoise(),
691  )
692 
693  def flatCorrection(self, exposure, flatExposure, invert=False):
694  """!Apply flat correction in place
695 
696  @param[in,out] exposure exposure to process
697  @param[in] flatExposure flatfield exposure same size as exposure
698  @param[in] invert if True, unflatten an already-flattened image instead.
699  """
700  isrFunctions.flatCorrection(
701  maskedImage=exposure.getMaskedImage(),
702  flatMaskedImage=flatExposure.getMaskedImage(),
703  scalingType=self.config.flatScalingType,
704  userScale=self.config.flatUserScale,
705  invert=invert
706  )
707 
708  def getIsrExposure(self, dataRef, datasetType, immediate=True):
709  """!Retrieve a calibration dataset for removing instrument signature
710 
711  @param[in] dataRef data reference for exposure
712  @param[in] datasetType type of dataset to retrieve (e.g. 'bias', 'flat')
713  @param[in] immediate if True, disable butler proxies to enable error
714  handling within this routine
715  @return exposure
716  """
717  try:
718  exp = dataRef.get(datasetType, immediate=immediate)
719  except Exception as exc1:
720  if not self.config.fallbackFilterName:
721  raise RuntimeError("Unable to retrieve %s for %s: %s" % (datasetType, dataRef.dataId, exc1))
722  try:
723  exp = dataRef.get(datasetType, filter=self.config.fallbackFilterName, immediate=immediate)
724  except Exception as exc2:
725  raise RuntimeError("Unable to retrieve %s for %s, even with fallback filter %s: %s AND %s" %
726  (datasetType, dataRef.dataId, self.config.fallbackFilterName, exc1, exc2))
727  self.log.warn("Using fallback calibration from filter %s" % self.config.fallbackFilterName)
728 
729  if self.config.doAssembleIsrExposures:
730  exp = self.assembleCcd.assembleCcd(exp)
731  return exp
732 
733  def saturationDetection(self, exposure, amp):
734  """!Detect saturated pixels and mask them using mask plane config.saturatedMaskName, in place
735 
736  @param[in,out] exposure exposure to process; only the amp DataSec is processed
737  @param[in] amp amplifier device data
738  """
739  if not math.isnan(amp.getSaturation()):
740  maskedImage = exposure.getMaskedImage()
741  dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
742  isrFunctions.makeThresholdMask(
743  maskedImage=dataView,
744  threshold=amp.getSaturation(),
745  growFootprints=0,
746  maskName=self.config.saturatedMaskName,
747  )
748 
749  def saturationInterpolation(self, ccdExposure):
750  """!Interpolate over saturated pixels, in place
751 
752  @param[in,out] ccdExposure exposure to process
753 
754  @warning:
755  - Call saturationDetection first, so that saturated pixels have been identified in the "SAT" mask.
756  - Call this after CCD assembly, since saturated regions may cross amplifier boundaries
757  """
758  isrFunctions.interpolateFromMask(
759  maskedImage=ccdExposure.getMaskedImage(),
760  fwhm=self.config.fwhm,
761  growFootprints=self.config.growSaturationFootprintSize,
762  maskName=self.config.saturatedMaskName,
763  )
764 
765  def suspectDetection(self, exposure, amp):
766  """!Detect suspect pixels and mask them using mask plane config.suspectMaskName, in place
767 
768  Suspect pixels are pixels whose value is greater than amp.getSuspectLevel().
769  This is intended to indicate pixels that may be affected by unknown systematics;
770  for example if non-linearity corrections above a certain level are unstable
771  then that would be a useful value for suspectLevel. A value of `nan` indicates
772  that no such level exists and no pixels are to be masked as suspicious.
773 
774  @param[in,out] exposure exposure to process; only the amp DataSec is processed
775  @param[in] amp amplifier device data
776  """
777  suspectLevel = amp.getSuspectLevel()
778  if math.isnan(suspectLevel):
779  return
780 
781  maskedImage = exposure.getMaskedImage()
782  dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
783  isrFunctions.makeThresholdMask(
784  maskedImage=dataView,
785  threshold=suspectLevel,
786  growFootprints=0,
787  maskName=self.config.suspectMaskName,
788  )
789 
790  def maskAndInterpDefect(self, ccdExposure, defectBaseList):
791  """!Mask defects using mask plane "BAD" and interpolate over them, in place
792 
793  @param[in,out] ccdExposure exposure to process
794  @param[in] defectBaseList a list of defects to mask and interpolate
795 
796  @warning: call this after CCD assembly, since defects may cross amplifier boundaries
797  """
798  maskedImage = ccdExposure.getMaskedImage()
799  defectList = []
800  for d in defectBaseList:
801  bbox = d.getBBox()
802  nd = measAlg.Defect(bbox)
803  defectList.append(nd)
804  isrFunctions.maskPixelsFromDefectList(maskedImage, defectList, maskName='BAD')
805  isrFunctions.interpolateDefectList(
806  maskedImage=maskedImage,
807  defectList=defectList,
808  fwhm=self.config.fwhm,
809  )
810 
811  def maskAndInterpNan(self, exposure):
812  """!Mask NaNs using mask plane "UNMASKEDNAN" and interpolate over them, in place
813 
814  We mask and interpolate over all NaNs, including those
815  that are masked with other bits (because those may or may
816  not be interpolated over later, and we want to remove all
817  NaNs). Despite this behaviour, the "UNMASKEDNAN" mask plane
818  is used to preserve the historical name.
819 
820  @param[in,out] exposure exposure to process
821  """
822  maskedImage = exposure.getMaskedImage()
823 
824  # Find and mask NaNs
825  maskedImage.getMask().addMaskPlane("UNMASKEDNAN")
826  maskVal = maskedImage.getMask().getPlaneBitMask("UNMASKEDNAN")
827  numNans = maskNans(maskedImage, maskVal)
828  self.metadata.set("NUMNANS", numNans)
829 
830  # Interpolate over these previously-unmasked NaNs
831  if numNans > 0:
832  self.log.warn("There were %i unmasked NaNs", numNans)
833  nanDefectList = isrFunctions.getDefectListFromMask(
834  maskedImage=maskedImage,
835  maskName='UNMASKEDNAN',
836  )
837  isrFunctions.interpolateDefectList(
838  maskedImage=exposure.getMaskedImage(),
839  defectList=nanDefectList,
840  fwhm=self.config.fwhm,
841  )
842 
843  def overscanCorrection(self, exposure, amp):
844  """!Apply overscan correction, in place
845 
846  @param[in,out] exposure exposure to process; must include both DataSec and BiasSec pixels
847  @param[in] amp amplifier device data
848  """
849  if not amp.getHasRawInfo():
850  raise RuntimeError("This method must be executed on an amp with raw information.")
851 
852  if amp.getRawHorizontalOverscanBBox().isEmpty():
853  self.log.info("No Overscan region. Not performing Overscan Correction.")
854  return None
855 
856  maskedImage = exposure.getMaskedImage()
857  dataView = maskedImage.Factory(maskedImage, amp.getRawDataBBox())
858 
859  expImage = exposure.getMaskedImage().getImage()
860  overscanImage = expImage.Factory(expImage, amp.getRawHorizontalOverscanBBox())
861 
862  isrFunctions.overscanCorrection(
863  ampMaskedImage=dataView,
864  overscanImage=overscanImage,
865  fitType=self.config.overscanFitType,
866  order=self.config.overscanOrder,
867  collapseRej=self.config.overscanRej,
868  )
869 
870  def addDistortionModel(self, exposure, camera):
871  """!Update the WCS in exposure with a distortion model based on camera geometry
872 
873  Add a model for optical distortion based on geometry found in `camera`
874  and the `exposure`'s detector. The raw input exposure is assumed
875  have a TAN WCS that has no compensation for optical distortion.
876  Two other possibilities are:
877  - The raw input exposure already has a model for optical distortion,
878  as is the case for raw DECam data.
879  In that case you should set config.doAddDistortionModel False.
880  - The raw input exposure has a model for distortion, but it has known
881  deficiencies severe enough to be worth fixing (e.g. because they
882  cause problems for fitting a better WCS). In that case you should
883  override this method with a version suitable for your raw data.
884 
885  @param[in,out] exposure exposure to process; must include a Detector and a WCS;
886  the WCS of the exposure is modified in place
887  @param[in] camera camera geometry; an lsst.afw.cameraGeom.Camera
888  """
889  self.log.info("Adding a distortion model to the WCS")
890  wcs = exposure.getWcs()
891  if wcs is None:
892  raise RuntimeError("exposure has no WCS")
893  if camera is None:
894  raise RuntimeError("camera is None")
895  detector = exposure.getDetector()
896  if detector is None:
897  raise RuntimeError("exposure has no Detector")
898  pixelToFocalPlane = detector.getTransform(PIXELS, FOCAL_PLANE)
899  focalPlaneToFieldAngle = camera.getTransformMap().getTransform(FOCAL_PLANE, FIELD_ANGLE)
900  distortedWcs = makeDistortedTanWcs(wcs, pixelToFocalPlane, focalPlaneToFieldAngle)
901  exposure.setWcs(distortedWcs)
902 
903  def setValidPolygonIntersect(self, ccdExposure, fpPolygon):
904  """!Set the valid polygon as the intersection of fpPolygon and the ccd corners
905 
906  @param[in,out] ccdExposure exposure to process
907  @param[in] fpPolygon Polygon in focal plane coordinates
908  """
909  # Get ccd corners in focal plane coordinates
910  ccd = ccdExposure.getDetector()
911  fpCorners = ccd.getCorners(FOCAL_PLANE)
912  ccdPolygon = Polygon(fpCorners)
913 
914  # Get intersection of ccd corners with fpPolygon
915  intersect = ccdPolygon.intersectionSingle(fpPolygon)
916 
917  # Transform back to pixel positions and build new polygon
918  ccdPoints = [ccd.transform(ccd.makeCameraPoint(x, FOCAL_PLANE), PIXELS).getPoint() for x in intersect]
919  validPolygon = Polygon(ccdPoints)
920  ccdExposure.getInfo().setValidPolygon(validPolygon)
921 
922  def brighterFatterCorrection(self, exposure, kernel, maxIter, threshold, applyGain):
923  """Apply brighter fatter correction in place for the image
924 
925  This correction takes a kernel that has been derived from flat field images to
926  redistribute the charge. The gradient of the kernel is the deflection
927  field due to the accumulated charge.
928 
929  Given the original image I(x) and the kernel K(x) we can compute the corrected image Ic(x)
930  using the following equation:
931 
932  Ic(x) = I(x) + 0.5*d/dx(I(x)*d/dx(int( dy*K(x-y)*I(y))))
933 
934  To evaluate the derivative term we expand it as follows:
935 
936  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))) )
937 
938  Because we use the measured counts instead of the incident counts we apply the correction
939  iteratively to reconstruct the original counts and the correction. We stop iterating when the
940  summed difference between the current corrected image and the one from the previous iteration
941  is below the threshold. We do not require convergence because the number of iterations is
942  too large a computational cost. How we define the threshold still needs to be evaluated, the
943  current default was shown to work reasonably well on a small set of images. For more information
944  on the method see DocuShare Document-19407.
945 
946  The edges as defined by the kernel are not corrected because they have spurious values
947  due to the convolution.
948  """
949  self.log.info("Applying brighter fatter correction")
950 
951  image = exposure.getMaskedImage().getImage()
952 
953  # The image needs to be units of electrons/holes
954  with self.gainContext(exposure, image, applyGain):
955 
956  kLx = numpy.shape(kernel)[0]
957  kLy = numpy.shape(kernel)[1]
958  kernelImage = afwImage.ImageD(kLx, kLy)
959  kernelImage.getArray()[:, :] = kernel
960  tempImage = image.clone()
961 
962  nanIndex = numpy.isnan(tempImage.getArray())
963  tempImage.getArray()[nanIndex] = 0.
964 
965  outImage = afwImage.ImageF(image.getDimensions())
966  corr = numpy.zeros_like(image.getArray())
967  prev_image = numpy.zeros_like(image.getArray())
968  convCntrl = afwMath.ConvolutionControl(False, True, 1)
969  fixedKernel = afwMath.FixedKernel(kernelImage)
970 
971  # Define boundary by convolution region. The region that the correction will be
972  # calculated for is one fewer in each dimension because of the second derivative terms.
973  # NOTE: these need to use integer math, as we're using start:end as numpy index ranges.
974  startX = kLx//2
975  endX = -kLx//2
976  startY = kLy//2
977  endY = -kLy//2
978 
979  for iteration in range(maxIter):
980 
981  afwMath.convolve(outImage, tempImage, fixedKernel, convCntrl)
982  tmpArray = tempImage.getArray()
983  outArray = outImage.getArray()
984 
985  with numpy.errstate(invalid="ignore", over="ignore"):
986  # First derivative term
987  gradTmp = numpy.gradient(tmpArray[startY:endY, startX:endX])
988  gradOut = numpy.gradient(outArray[startY:endY, startX:endX])
989  first = (gradTmp[0]*gradOut[0] + gradTmp[1]*gradOut[1])[1:-1, 1:-1]
990 
991  # Second derivative term
992  diffOut20 = numpy.diff(outArray, 2, 0)[startY:endY, startX + 1:endX - 1]
993  diffOut21 = numpy.diff(outArray, 2, 1)[startY + 1:endY - 1, startX:endX]
994  second = tmpArray[startY + 1:endY - 1, startX + 1:endX - 1]*(diffOut20 + diffOut21)
995 
996  corr[startY + 1:endY - 1, startX + 1:endX - 1] = 0.5*(first + second)
997 
998  tmpArray[:, :] = image.getArray()[:, :]
999  tmpArray[nanIndex] = 0.
1000  tmpArray[startY:endY, startX:endX] += corr[startY:endY, startX:endX]
1001 
1002  if iteration > 0:
1003  diff = numpy.sum(numpy.abs(prev_image - tmpArray))
1004 
1005  if diff < threshold:
1006  break
1007  prev_image[:, :] = tmpArray[:, :]
1008 
1009  if iteration == maxIter - 1:
1010  self.log.warn("Brighter fatter correction did not converge, final difference %f" % diff)
1011 
1012  self.log.info("Finished brighter fatter in %d iterations" % (iteration + 1))
1013  image.getArray()[startY + 1:endY - 1, startX + 1:endX - 1] += \
1014  corr[startY + 1:endY - 1, startX + 1:endX - 1]
1015 
1016  def attachTransmissionCurve(self, exposure, opticsTransmission=None, filterTransmission=None,
1017  sensorTransmission=None, atmosphereTransmission=None):
1018  """Attach a TransmissionCurve to an Exposure, given separate curves for
1019  different components.
1020 
1021  Parameters
1022  ----------
1023  exposure : `lsst.afw.image.Exposure`
1024  Exposure object to modify by attaching the product of all given
1025  ``TransmissionCurves`` in post-assembly trimmed detector
1026  coordinates. Must have a valid ``Detector`` attached that matches
1027  the detector associated with sensorTransmission.
1028  opticsTransmission : `lsst.afw.image.TransmissionCurve`
1029  A ``TransmissionCurve`` that represents the throughput of the
1030  optics, to be evaluated in focal-plane coordinates.
1031  filterTransmission : `lsst.afw.image.TransmissionCurve`
1032  A ``TransmissionCurve`` that represents the throughput of the
1033  filter itself, to be evaluated in focal-plane coordinates.
1034  sensorTransmission : `lsst.afw.image.TransmissionCurve`
1035  A ``TransmissionCurve`` that represents the throughput of the
1036  sensor itself, to be evaluated in post-assembly trimmed detector
1037  coordinates.
1038  atmosphereTransmission : `lsst.afw.image.TransmissionCurve`
1039  A ``TransmissionCurve`` that represents the throughput of the
1040  atmosphere, assumed to be spatially constant.
1041 
1042  All ``TransmissionCurve`` arguments are optional; if none are provided,
1043  the attached ``TransmissionCurve`` will have unit transmission
1044  everywhere.
1045 
1046  Returns
1047  -------
1048  combined : ``lsst.afw.image.TransmissionCurve``
1049  The TransmissionCurve attached to the exposure.
1050  """
1051  return isrFunctions.attachTransmissionCurve(exposure, opticsTransmission=opticsTransmission,
1052  filterTransmission=filterTransmission,
1053  sensorTransmission=sensorTransmission,
1054  atmosphereTransmission=atmosphereTransmission)
1055 
1056  @contextmanager
1057  def gainContext(self, exp, image, apply):
1058  """Context manager that applies and removes gain
1059  """
1060  if apply:
1061  ccd = exp.getDetector()
1062  for amp in ccd:
1063  sim = image.Factory(image, amp.getBBox())
1064  sim *= amp.getGain()
1065 
1066  try:
1067  yield exp
1068  finally:
1069  if apply:
1070  ccd = exp.getDetector()
1071  for amp in ccd:
1072  sim = image.Factory(image, amp.getBBox())
1073  sim /= amp.getGain()
1074 
1075  @contextmanager
1076  def flatContext(self, exp, flat, dark=None):
1077  """Context manager that applies and removes flats and darks,
1078  if the task is configured to apply them.
1079  """
1080  if self.config.doDark and dark is not None:
1081  self.darkCorrection(exp, dark)
1082  if self.config.doFlat:
1083  self.flatCorrection(exp, flat)
1084  try:
1085  yield exp
1086  finally:
1087  if self.config.doFlat:
1088  self.flatCorrection(exp, flat, invert=True)
1089  if self.config.doDark and dark is not None:
1090  self.darkCorrection(exp, dark, invert=True)
1091 
1092 
1093 class FakeAmp(object):
1094  """A Detector-like object that supports returning gain and saturation level"""
1095 
1096  def __init__(self, exposure, config):
1097  self._bbox = exposure.getBBox(afwImage.LOCAL)
1098  self._RawHorizontalOverscanBBox = afwGeom.Box2I()
1099  self._gain = config.gain
1100  self._readNoise = config.readNoise
1101  self._saturation = config.saturation
1102 
1103  def getBBox(self):
1104  return self._bbox
1105 
1106  def getRawBBox(self):
1107  return self._bbox
1108 
1109  def getHasRawInfo(self):
1110  return True # but see getRawHorizontalOverscanBBox()
1111 
1113  return self._RawHorizontalOverscanBBox
1114 
1115  def getGain(self):
1116  return self._gain
1117 
1118  def getReadNoise(self):
1119  return self._readNoise
1120 
1121  def getSaturation(self):
1122  return self._saturation
1123 
1124  def getSuspectLevel(self):
1125  return float("NaN")
def brighterFatterCorrection(self, exposure, kernel, maxIter, threshold, applyGain)
Definition: isrTask.py:922
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:1057
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:1017
def maskAndInterpNan(self, exposure)
Mask NaNs using mask plane "UNMASKEDNAN" and interpolate over them, in place.
Definition: isrTask.py:811
def saturationInterpolation(self, ccdExposure)
Interpolate over saturated pixels, in place.
Definition: isrTask.py:749
Apply common instrument signature correction algorithms to a raw frame.
Definition: isrTask.py:292
def getRawHorizontalOverscanBBox(self)
Definition: isrTask.py:1112
def convertIntToFloat(self, exposure)
Definition: isrTask.py:618
def flatCorrection(self, exposure, flatExposure, invert=False)
Apply flat correction in place.
Definition: isrTask.py:693
def getIsrExposure(self, dataRef, datasetType, immediate=True)
Retrieve a calibration dataset for removing instrument signature.
Definition: isrTask.py:708
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:870
def setValidPolygonIntersect(self, ccdExposure, fpPolygon)
Set the valid polygon as the intersection of fpPolygon and the ccd corners.
Definition: isrTask.py:903
def biasCorrection(self, exposure, biasExposure)
Apply bias correction in place.
Definition: isrTask.py:635
def flatContext(self, exp, flat, dark=None)
Definition: isrTask.py:1076
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:843
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:765
def maskAndInterpDefect(self, ccdExposure, defectBaseList)
Mask defects using mask plane "BAD" and interpolate over them, in place.
Definition: isrTask.py:790
def saturationDetection(self, exposure, amp)
Detect saturated pixels and mask them using mask plane config.saturatedMaskName, in place...
Definition: isrTask.py:733
def __init__(self, exposure, config)
Definition: isrTask.py:1096