LSSTApplications  11.0-13-gbb96280,12.1.rc1,12.1.rc1+1,12.1.rc1+2,12.1.rc1+5,12.1.rc1+8,12.1.rc1-1-g06d7636+1,12.1.rc1-1-g253890b+5,12.1.rc1-1-g3d31b68+7,12.1.rc1-1-g3db6b75+1,12.1.rc1-1-g5c1385a+3,12.1.rc1-1-g83b2247,12.1.rc1-1-g90cb4cf+6,12.1.rc1-1-g91da24b+3,12.1.rc1-2-g3521f8a,12.1.rc1-2-g39433dd+4,12.1.rc1-2-g486411b+2,12.1.rc1-2-g4c2be76,12.1.rc1-2-gc9c0491,12.1.rc1-2-gda2cd4f+6,12.1.rc1-3-g3391c73+2,12.1.rc1-3-g8c1bd6c+1,12.1.rc1-3-gcf4b6cb+2,12.1.rc1-4-g057223e+1,12.1.rc1-4-g19ed13b+2,12.1.rc1-4-g30492a7
LSSTDataManagementBasePackage
isrTask.py
Go to the documentation of this file.
1 from __future__ import division
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 isr
38 from .isrLib import maskNans
39 from .assembleCcdTask import AssembleCcdTask
40 from .fringe import FringeTask
41 from lsst.afw.geom.polygon import Polygon
42 from lsst.afw.cameraGeom import PIXELS, FOCAL_PLANE, NullLinearityType
43 from contextlib import contextmanager
44 
45 
46 class IsrTaskConfig(pexConfig.Config):
47  doBias = pexConfig.Field(
48  dtype=bool,
49  doc="Apply bias frame correction?",
50  default=True,
51  )
52  doDark = pexConfig.Field(
53  dtype=bool,
54  doc="Apply dark frame correction?",
55  default=True,
56  )
57  doFlat = pexConfig.Field(
58  dtype=bool,
59  doc="Apply flat field correction?",
60  default=True,
61  )
62  doFringe = pexConfig.Field(
63  dtype=bool,
64  doc="Apply fringe correction?",
65  default=True,
66  )
67  doWrite = pexConfig.Field(
68  dtype=bool,
69  doc="Persist postISRCCD?",
70  default=True,
71  )
72  assembleCcd = pexConfig.ConfigurableField(
73  target=AssembleCcdTask,
74  doc="CCD assembly task",
75  )
76  gain = pexConfig.Field(
77  dtype=float,
78  doc="The gain to use if no Detector is present in the Exposure (ignored if NaN)",
79  default=float("NaN"),
80  )
81  readNoise = pexConfig.Field(
82  dtype=float,
83  doc="The read noise to use if no Detector is present in the Exposure",
84  default=0.0,
85  )
86  saturation = pexConfig.Field(
87  dtype=float,
88  doc="The saturation level to use if no Detector is present in the Exposure (ignored if NaN)",
89  default=float("NaN"),
90  )
91  fringeAfterFlat = pexConfig.Field(
92  dtype=bool,
93  doc="Do fringe subtraction after flat-fielding?",
94  default=True,
95  )
96  fringe = pexConfig.ConfigurableField(
97  target=FringeTask,
98  doc="Fringe subtraction task",
99  )
100  fwhm = pexConfig.Field(
101  dtype=float,
102  doc="FWHM of PSF (arcsec)",
103  default=1.0,
104  )
105  saturatedMaskName = pexConfig.Field(
106  dtype=str,
107  doc="Name of mask plane to use in saturation detection and interpolation",
108  default="SAT",
109  )
110  suspectMaskName = pexConfig.Field(
111  dtype=str,
112  doc="Name of mask plane to use for suspect pixels",
113  default="SUSPECT",
114  )
115  flatScalingType = pexConfig.ChoiceField(
116  dtype=str,
117  doc="The method for scaling the flat on the fly.",
118  default='USER',
119  allowed={
120  "USER": "Scale by flatUserScale",
121  "MEAN": "Scale by the inverse of the mean",
122  "MEDIAN": "Scale by the inverse of the median",
123  },
124  )
125  flatUserScale = pexConfig.Field(
126  dtype=float,
127  doc="If flatScalingType is 'USER' then scale flat by this amount; ignored otherwise",
128  default=1.0,
129  )
130  overscanFitType = pexConfig.ChoiceField(
131  dtype=str,
132  doc="The method for fitting the overscan bias level.",
133  default='MEDIAN',
134  allowed={
135  "POLY": "Fit ordinary polynomial to the longest axis of the overscan region",
136  "CHEB": "Fit Chebyshev polynomial to the longest axis of the overscan region",
137  "LEG": "Fit Legendre polynomial to the longest axis of the overscan region",
138  "NATURAL_SPLINE": "Fit natural spline to the longest axis of the overscan region",
139  "CUBIC_SPLINE": "Fit cubic spline to the longest axis of the overscan region",
140  "AKIMA_SPLINE": "Fit Akima spline to the longest axis of the overscan region",
141  "MEAN": "Correct using the mean of the overscan region",
142  "MEDIAN": "Correct using the median of the overscan region",
143  },
144  )
145  overscanOrder = pexConfig.Field(
146  dtype=int,
147  doc=("Order of polynomial or to fit if overscan fit type is a polynomial, " +
148  "or number of spline knots if overscan fit type is a spline."),
149  default=1,
150  )
151  overscanRej = pexConfig.Field(
152  dtype=float,
153  doc="Rejection threshold (sigma) for collapsing overscan before fit",
154  default=3.0,
155  )
156  growSaturationFootprintSize = pexConfig.Field(
157  dtype=int,
158  doc="Number of pixels by which to grow the saturation footprints",
159  default=1,
160  )
161  fluxMag0T1 = pexConfig.Field(
162  dtype=float,
163  doc="The approximate flux of a zero-magnitude object in a one-second exposure",
164  default=1e10,
165  )
166  setGainAssembledCcd = pexConfig.Field(
167  dtype=bool,
168  doc="update exposure metadata in the assembled ccd to reflect the "
169  "effective gain of the assembled chip",
170  default=True,
171  )
172  keysToRemoveFromAssembledCcd = pexConfig.ListField(
173  dtype=str,
174  doc="fields to remove from the metadata of the assembled ccd.",
175  default=[],
176  )
177  doAssembleIsrExposures = pexConfig.Field(
178  dtype=bool,
179  default=False,
180  doc="Assemble amp-level calibration exposures into ccd-level exposure?"
181  )
182  doAssembleCcd = pexConfig.Field(
183  dtype=bool,
184  default=True,
185  doc="Assemble amp-level exposures into a ccd-level exposure?"
186  )
187  doLinearize = pexConfig.Field(
188  dtype=bool,
189  doc="Correct for nonlinearity of the detector's response?",
190  default=True,
191  )
192  doBrighterFatter = pexConfig.Field(
193  dtype=bool,
194  default=False,
195  doc="Apply the brighter fatter correction"
196  )
197  brighterFatterKernelFile = pexConfig.Field(
198  dtype=str,
199  default='',
200  doc="Kernel file used for the brighter fatter correction"
201  )
202  brighterFatterMaxIter = pexConfig.Field(
203  dtype=int,
204  default=10,
205  doc="Maximum number of iterations for the brighter fatter correction"
206  )
207  brighterFatterThreshold = pexConfig.Field(
208  dtype=float,
209  default=1000,
210  doc="Threshold used to stop iterating the brighter fatter correction. It is the "
211  " absolute value of the difference between the current corrected image and the one"
212  " from the previous iteration summed over all the pixels."
213  )
214  brighterFatterApplyGain = pexConfig.Field(
215  dtype=bool,
216  default=True,
217  doc="Should the gain be applied when applying the brighter fatter correction?"
218  )
219  datasetType = pexConfig.Field(
220  dtype=str,
221  doc="Dataset type for input data; users will typically leave this alone, "
222  "but camera-specific ISR tasks will override it",
223  default="raw",
224  )
225  fallbackFilterName = pexConfig.Field(dtype=str,
226  doc="Fallback default filter name for calibrations", optional=True)
227 
228 ## \addtogroup LSST_task_documentation
229 ## \{
230 ## \page IsrTask
231 ## \ref IsrTask_ "IsrTask"
232 ## \copybrief IsrTask
233 ## \}
234 
235 
236 class IsrTask(pipeBase.CmdLineTask):
237  """!
238  \anchor IsrTask_
239 
240  \brief Apply common instrument signature correction algorithms to a raw frame.
241 
242  \section ip_isr_isr_Contents Contents
243 
244  - \ref ip_isr_isr_Purpose
245  - \ref ip_isr_isr_Initialize
246  - \ref ip_isr_isr_IO
247  - \ref ip_isr_isr_Config
248  - \ref ip_isr_isr_Debug
249  - \ref ip_isr_isr_Example
250 
251  \section ip_isr_isr_Purpose Description
252 
253  The process for correcting imaging data is very similar from camera to camera.
254  This task provides a vanilla implementation of doing these corrections, including
255  the ability to turn certain corrections off if they are not needed.
256  The inputs to the primary method, run, are a raw exposure to be corrected and the
257  calibration data products. The raw input is a single chip sized mosaic of all amps
258  including overscans and other non-science pixels.
259  The method runDataRef() is intended for use by a lsst.pipe.base.cmdLineTask.CmdLineTask
260  and takes as input only a daf.persistence.butlerSubset.ButlerDataRef.
261  This task may not meet all needs and it is expected that it will be subclassed for
262  specific applications.
263 
264  \section ip_isr_isr_Initialize Task initialization
265 
266  \copydoc \_\_init\_\_
267 
268  \section ip_isr_isr_IO Inputs/Outputs to the run method
269 
270  \copydoc run
271 
272  \section ip_isr_isr_Config Configuration parameters
273 
274  See \ref IsrTaskConfig
275 
276  \section ip_isr_isr_Debug Debug variables
277 
278  The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
279  flag \c --debug, \c -d to import \b debug.py from your \c PYTHONPATH; see <a
280  href="http://lsst-web.ncsa.illinois.edu/~buildbot/doxygen/x_masterDoxyDoc/base_debug.html">
281  Using lsstDebug to control debugging output</a> for more about \b debug.py files.
282 
283  The available variables in IsrTask are:
284  <DL>
285  <DT> \c display
286  <DD> A dictionary containing debug point names as keys with frame number as value. Valid keys are:
287  <DL>
288  <DT> postISRCCD
289  <DD> display exposure after ISR has been applied
290  </DL>
291  </DL>
292 
293  For example, put something like
294  \code{.py}
295  import lsstDebug
296  def DebugInfo(name):
297  di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
298  if name == "lsst.ip.isr.isrTask":
299  di.display = {'postISRCCD':2}
300  return di
301  lsstDebug.Info = DebugInfo
302  \endcode
303  into your debug.py file and run the commandline task with the \c --debug flag.
304 
305  \section ip_isr_isr_Example A complete example of using IsrTask
306 
307  This code is in runIsrTask.py in the examples directory and can be run as \em e.g.
308  \code
309  python examples/runIsrTask.py
310 
311  # optional arguments:
312  # --debug, -d Load debug.py?
313  # --ds9 Display the result?
314  # --write Write the result?
315  \endcode
316 
317  <HR>
318  Stepping through the example:
319 
320  \dontinclude runIsrTask.py
321  Import the task. There are other imports. Read the source file for more info.
322  \skipline IsrTask
323  \skipline exampleUtils
324 
325  Create the raw input data with the help of some utilities in \link exampleUtils.py \endlink
326  also in the examples directory.
327  \dontinclude runIsrTask.py
328  We will only do overscan, dark and flat correction.
329  The data are constructed by hand so that all effects will be corrected for essentially perfectly.
330  \skip DARKVAL
331  @until rawExposure
332  The above numbers can be changed to modify the gradient in the flat, for example.
333  For the parameters in this particular example,
334  the image after ISR will be a constant 5000 counts
335  (with some variation in floating point represenation).
336 
337  \note Alternatively, images can be read from disk, either using the Butler or manually:
338  \code
339  import lsst.afw.image as afwImage
340  darkExposure = afwImage.ExposureF("/path/to/dark.fits")
341  flatExposure = afwImage.ExposureF("/path/to/flat.fits")
342  rawExposure = afwImage.ExposureF("/path/to/raw.fits")
343  \endcode
344  In order to perform overscanCorrection IsrTask.run() requires Exposures which have
345  a \link lsst.afw.cameraGeom.Detector \endlink.
346  Detector objects describe details such as data dimensions, number of amps,
347  orientation and overscan dimensions.
348  If requesting images from the Butler, Exposures will automatically have detector information.
349  If running IsrTask on arbitrary images from a camera without an obs_ package,
350  a lsst.afw.cameraGeom.Detector can be generated using lsst.afw.cameraGeom.fitsUtils.DetectorBuilder
351  and set by calling
352  \code
353  rawExposure.setDetector(myDetectorObject)
354  \endcode
355  See \link lsst.afw.cameraGeom.fitsUtils.DetectorBuilder \endlink for more details.
356 
357  \note The updateVariance and saturationDetection steps are not run for Exposures
358  without a \link lsst.afw.cameraGeom.Detector \endlink, unless \ref IsrTaskConfig.gain,
359  \ref IsrTaskConfig.readNoise,
360  and/or \ref IsrTaskConfig.saturation
361  are set in the config, in which case they are applied to the entire image rather than per amp.
362 
363  \dontinclude runIsrTask.py
364  Construct the task and set some config parameters. Specifically, we don't want to
365  do zero or fringe correction. We also don't want the assembler messing with the gain.
366  \skip Create
367  @until config=isrConfig
368 
369  Finally, run the exposures through ISR.
370  \skipline isrTask.run
371 
372  <HR>
373  """
374  ConfigClass = IsrTaskConfig
375  _DefaultName = "isr"
376 
377  def __init__(self, *args, **kwargs):
378  '''!Constructor for IsrTask
379  \param[in] *args -- a list of positional arguments passed on to the Task constructor
380  \param[in] **kwargs -- a dictionary of keyword arguments passed on to the Task constructor
381  Call the lsst.pipe.base.task.Task.__init__ method
382  Then setup the assembly and fringe correction subtasks
383  '''
384  pipeBase.Task.__init__(self, *args, **kwargs)
385  self.makeSubtask("assembleCcd")
386  self.makeSubtask("fringe")
387 
388  def readIsrData(self, dataRef, rawExposure):
389  """!Retrieve necessary frames for instrument signature removal
390  \param[in] dataRef -- a daf.persistence.butlerSubset.ButlerDataRef
391  of the detector data to be processed
392  \param[in] rawExposure -- a reference raw exposure that will later be
393  corrected with the retrieved calibration data;
394  should not be modified in this method.
395  \return a pipeBase.Struct with fields containing kwargs expected by run()
396  - bias: exposure of bias frame
397  - dark: exposure of dark frame
398  - flat: exposure of flat field
399  - defects: list of detects
400  - fringeStruct: a pipeBase.Struct with field fringes containing
401  exposure of fringe frame or list of fringe exposure
402  """
403  ccd = rawExposure.getDetector()
404 
405  biasExposure = self.getIsrExposure(dataRef, "bias") if self.config.doBias else None
406  # immediate=True required for functors and linearizers are functors; see ticket DM-6515
407  linearizer = dataRef.get("linearizer", immediate=True) if self.doLinearize(ccd) else None
408  darkExposure = self.getIsrExposure(dataRef, "dark") if self.config.doDark else None
409  flatExposure = self.getIsrExposure(dataRef, "flat") if self.config.doFlat else None
410  brighterFatterKernel = dataRef.get("brighterFatterKernel") if self.config.doBrighterFatter else None
411 
412  defectList = dataRef.get("defects")
413 
414  if self.config.doFringe and self.fringe.checkFilter(rawExposure):
415  fringeStruct = self.fringe.readFringes(dataRef, assembler=self.assembleCcd
416  if self.config.doAssembleIsrExposures else None)
417  else:
418  fringeStruct = pipeBase.Struct(fringes=None)
419 
420  # Struct should include only kwargs to run()
421  return pipeBase.Struct(bias=biasExposure,
422  linearizer=linearizer,
423  dark=darkExposure,
424  flat=flatExposure,
425  defects=defectList,
426  fringes=fringeStruct,
427  bfKernel=brighterFatterKernel
428  )
429 
430  @pipeBase.timeMethod
431  def run(self, ccdExposure, bias=None, linearizer=None, dark=None, flat=None, defects=None,
432  fringes=None, bfKernel=None):
433  """!Perform instrument signature removal on an exposure
434 
435  Steps include:
436  - Detect saturation, apply overscan correction, bias, dark and flat
437  - Perform CCD assembly
438  - Interpolate over defects, saturated pixels and all NaNs
439 
440  \param[in] ccdExposure -- lsst.afw.image.exposure of detector data
441  \param[in] bias -- exposure of bias frame
442  \param[in] linearizer -- linearizing functor; a subclass of lsst.ip.isr.LinearizeBase
443  \param[in] dark -- exposure of dark frame
444  \param[in] flat -- exposure of flatfield
445  \param[in] defects -- list of detects
446  \param[in] fringes -- a pipeBase.Struct with field fringes containing
447  exposure of fringe frame or list of fringe exposure
448  \param[in] bfKernel -- kernel for brighter-fatter correction
449 
450  \return a pipeBase.Struct with field:
451  - exposure
452  """
453 
454  # parseAndRun expects to be able to call run() with a dataRef; see DM-6640
455  if isinstance(ccdExposure, ButlerDataRef):
456  return self.runDataRef(ccdExposure)
457 
458  ccd = ccdExposure.getDetector()
459 
460  # Validate Input
461  if self.config.doBias and bias is None:
462  raise RuntimeError("Must supply a bias exposure if config.doBias True")
463  if self.doLinearize(ccd) and linearizer is None:
464  raise RuntimeError("Must supply a linearizer if config.doBias True")
465  if self.config.doDark and dark is None:
466  raise RuntimeError("Must supply a dark exposure if config.doDark True")
467  if self.config.doFlat and flat is None:
468  raise RuntimeError("Must supply a flat exposure if config.doFlat True")
469  if self.config.doBrighterFatter and bfKernel is None:
470  raise RuntimeError("Must supply a kernel if config.doBrighterFatter True")
471  if fringes is None:
472  fringes = pipeBase.Struct(fringes=None)
473  if self.config.doFringe and not isinstance(fringes, pipeBase.Struct):
474  raise RuntimeError("Must supply fringe exposure as a pipeBase.Struct")
475 
476  defects = [] if defects is None else defects
477 
478  ccdExposure = self.convertIntToFloat(ccdExposure)
479 
480  if not ccd:
481  assert not self.config.doAssembleCcd, "You need a Detector to run assembleCcd"
482  ccd = [FakeAmp(ccdExposure, self.config)]
483 
484  for amp in ccd:
485  # if ccdExposure is one amp, check for coverage to prevent performing ops multiple times
486  if ccdExposure.getBBox().contains(amp.getBBox()):
487  self.saturationDetection(ccdExposure, amp)
488  self.suspectDetection(ccdExposure, amp)
489  self.overscanCorrection(ccdExposure, amp)
490 
491  if self.config.doAssembleCcd:
492  ccdExposure = self.assembleCcd.assembleCcd(ccdExposure)
493 
494  if self.config.doBias:
495  self.biasCorrection(ccdExposure, bias)
496 
497  if self.doLinearize(ccd):
498  linearizer(image=ccdExposure.getMaskedImage().getImage(), detector=ccd, log=self.log)
499 
500  if self.config.doBrighterFatter:
501  self.brighterFatterCorrection(ccdExposure, bfKernel,
502  self.config.brighterFatterMaxIter,
503  self.config.brighterFatterThreshold,
504  self.config.brighterFatterApplyGain,
505  )
506 
507  if self.config.doDark:
508  self.darkCorrection(ccdExposure, dark)
509 
510  for amp in ccd:
511  # if ccdExposure is one amp, check for coverage to prevent performing ops multiple times
512  if ccdExposure.getBBox().contains(amp.getBBox()):
513  ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
514  self.updateVariance(ampExposure, amp)
515 
516  if self.config.doFringe and not self.config.fringeAfterFlat:
517  self.fringe.run(ccdExposure, **fringes.getDict())
518 
519  if self.config.doFlat:
520  self.flatCorrection(ccdExposure, flat)
521 
522  self.maskAndInterpDefect(ccdExposure, defects)
523 
524  self.saturationInterpolation(ccdExposure)
525 
526  self.maskAndInterpNan(ccdExposure)
527 
528  if self.config.doFringe and self.config.fringeAfterFlat:
529  self.fringe.run(ccdExposure, **fringes.getDict())
530 
531  ccdExposure.getCalib().setFluxMag0(self.config.fluxMag0T1 * ccdExposure.getCalib().getExptime())
532 
533  frame = getDebugFrame(self._display, "postISRCCD")
534  if frame:
535  getDisplay(frame).mtv(ccdExposure)
536 
537  return pipeBase.Struct(
538  exposure=ccdExposure,
539  )
540 
541  @pipeBase.timeMethod
542  def runDataRef(self, sensorRef):
543  """!Perform instrument signature removal on a ButlerDataRef of a Sensor
544 
545  - Read in necessary detrending/isr/calibration data
546  - Process raw exposure in run()
547  - Persist the ISR-corrected exposure as "postISRCCD" if config.doWrite is True
548 
549  \param[in] sensorRef -- daf.persistence.butlerSubset.ButlerDataRef of the
550  detector data to be processed
551  \return a pipeBase.Struct with fields:
552  - exposure: the exposure after application of ISR
553  """
554  self.log.info("Performing ISR on sensor %s" % (sensorRef.dataId))
555  ccdExposure = sensorRef.get('raw')
556  isrData = self.readIsrData(sensorRef, ccdExposure)
557 
558  result = self.run(ccdExposure, **isrData.getDict())
559 
560  if self.config.doWrite:
561  sensorRef.put(result.exposure, "postISRCCD")
562 
563  return result
564 
565  def convertIntToFloat(self, exposure):
566  """Convert an exposure from uint16 to float, set variance plane to 1 and mask plane to 0
567  """
568  if isinstance(exposure, afwImage.ExposureF):
569  # Nothing to be done
570  return exposure
571  if not hasattr(exposure, "convertF"):
572  raise RuntimeError("Unable to convert exposure (%s) to float" % type(exposure))
573 
574  newexposure = exposure.convertF()
575  maskedImage = newexposure.getMaskedImage()
576  varArray = maskedImage.getVariance().getArray()
577  varArray[:, :] = 1
578  maskArray = maskedImage.getMask().getArray()
579  maskArray[:, :] = 0
580  return newexposure
581 
582  def biasCorrection(self, exposure, biasExposure):
583  """!Apply bias correction in place
584 
585  \param[in,out] exposure exposure to process
586  \param[in] biasExposure bias exposure of same size as exposure
587  """
588  isr.biasCorrection(exposure.getMaskedImage(), biasExposure.getMaskedImage())
589 
590  def darkCorrection(self, exposure, darkExposure):
591  """!Apply dark correction in place
592 
593  \param[in,out] exposure exposure to process
594  \param[in] darkExposure dark exposure of same size as exposure
595  """
596  darkCalib = darkExposure.getCalib()
597  isr.darkCorrection(
598  maskedImage=exposure.getMaskedImage(),
599  darkMaskedImage=darkExposure.getMaskedImage(),
600  expScale=exposure.getCalib().getExptime(),
601  darkScale=darkCalib.getExptime(),
602  )
603 
604  def doLinearize(self, detector):
605  """!Is linearization wanted for this detector?
606 
607  Checks config.doLinearize and the linearity type of the first amplifier.
608 
609  \param[in] detector detector information (an lsst.afw.cameraGeom.Detector)
610  """
611  return self.config.doLinearize and \
612  detector.getAmpInfoCatalog()[0].getLinearityType() != NullLinearityType
613 
614  def updateVariance(self, ampExposure, amp):
615  """!Set the variance plane based on the image plane, plus amplifier gain and read noise
616 
617  \param[in,out] ampExposure exposure to process
618  \param[in] amp amplifier detector information
619  """
620  if not math.isnan(amp.getGain()):
621  isr.updateVariance(
622  maskedImage=ampExposure.getMaskedImage(),
623  gain=amp.getGain(),
624  readNoise=amp.getReadNoise(),
625  )
626 
627  def flatCorrection(self, exposure, flatExposure):
628  """!Apply flat correction in place
629 
630  \param[in,out] exposure exposure to process
631  \param[in] flatExposure flatfield exposure same size as exposure
632  """
633  isr.flatCorrection(
634  maskedImage=exposure.getMaskedImage(),
635  flatMaskedImage=flatExposure.getMaskedImage(),
636  scalingType=self.config.flatScalingType,
637  userScale=self.config.flatUserScale,
638  )
639 
640  def getIsrExposure(self, dataRef, datasetType, immediate=True):
641  """!Retrieve a calibration dataset for removing instrument signature
642 
643  \param[in] dataRef data reference for exposure
644  \param[in] datasetType type of dataset to retrieve (e.g. 'bias', 'flat')
645  \param[in] immediate if True, disable butler proxies to enable error
646  handling within this routine
647  \return exposure
648  """
649  try:
650  exp = dataRef.get(datasetType, immediate=immediate)
651  except Exception as exc1:
652  if not self.config.fallbackFilterName:
653  raise RuntimeError("Unable to retrieve %s for %s: %s" % (datasetType, dataRef.dataId, exc1))
654  try:
655  exp = dataRef.get(datasetType, filter=self.config.fallbackFilterName, immediate=immediate)
656  except Exception as exc2:
657  raise RuntimeError("Unable to retrieve %s for %s, even with fallback filter %s: %s AND %s" %
658  (datasetType, dataRef.dataId, self.config.fallbackFilterName, exc1, exc2))
659  self.log.warn("Using fallback calibration from filter %s" % self.config.fallbackFilterName)
660 
661  if self.config.doAssembleIsrExposures:
662  exp = self.assembleCcd.assembleCcd(exp)
663  return exp
664 
665  def saturationDetection(self, exposure, amp):
666  """!Detect saturated pixels and mask them using mask plane config.saturatedMaskName, in place
667 
668  \param[in,out] exposure exposure to process; only the amp DataSec is processed
669  \param[in] amp amplifier device data
670  """
671  if not math.isnan(amp.getSaturation()):
672  maskedImage = exposure.getMaskedImage()
673  dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
674  isr.makeThresholdMask(
675  maskedImage=dataView,
676  threshold=amp.getSaturation(),
677  growFootprints=0,
678  maskName=self.config.saturatedMaskName,
679  )
680 
681  def saturationInterpolation(self, ccdExposure):
682  """!Interpolate over saturated pixels, in place
683 
684  \param[in,out] ccdExposure exposure to process
685 
686  \warning:
687  - Call saturationDetection first, so that saturated pixels have been identified in the "SAT" mask.
688  - Call this after CCD assembly, since saturated regions may cross amplifier boundaries
689  """
690  isr.interpolateFromMask(
691  maskedImage=ccdExposure.getMaskedImage(),
692  fwhm=self.config.fwhm,
693  growFootprints=self.config.growSaturationFootprintSize,
694  maskName=self.config.saturatedMaskName,
695  )
696 
697  def suspectDetection(self, exposure, amp):
698  """!Detect suspect pixels and mask them using mask plane config.suspectMaskName, in place
699 
700  Suspect pixels are pixels whose value is greater than amp.getSuspectLevel().
701  This is intended to indicate pixels that may be affected by unknown systematics;
702  for example if non-linearity corrections above a certain level are unstable
703  then that would be a useful value for suspectLevel. A value of `nan` indicates
704  that no such level exists and no pixels are to be masked as suspicious.
705 
706  \param[in,out] exposure exposure to process; only the amp DataSec is processed
707  \param[in] amp amplifier device data
708  """
709  suspectLevel = amp.getSuspectLevel()
710  if math.isnan(suspectLevel):
711  return
712 
713  maskedImage = exposure.getMaskedImage()
714  dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
715  isr.makeThresholdMask(
716  maskedImage=dataView,
717  threshold=suspectLevel,
718  growFootprints=0,
719  maskName=self.config.suspectMaskName,
720  )
721 
722  def maskAndInterpDefect(self, ccdExposure, defectBaseList):
723  """!Mask defects using mask plane "BAD" and interpolate over them, in place
724 
725  \param[in,out] ccdExposure exposure to process
726  \param[in] defectBaseList a list of defects to mask and interpolate
727 
728  \warning: call this after CCD assembly, since defects may cross amplifier boundaries
729  """
730  maskedImage = ccdExposure.getMaskedImage()
731  defectList = measAlg.DefectListT()
732  for d in defectBaseList:
733  bbox = d.getBBox()
734  nd = measAlg.Defect(bbox)
735  defectList.append(nd)
736  isr.maskPixelsFromDefectList(maskedImage, defectList, maskName='BAD')
737  isr.interpolateDefectList(
738  maskedImage=maskedImage,
739  defectList=defectList,
740  fwhm=self.config.fwhm,
741  )
742 
743  def maskAndInterpNan(self, exposure):
744  """!Mask NaNs using mask plane "UNMASKEDNAN" and interpolate over them, in place
745 
746  We mask and interpolate over all NaNs, including those
747  that are masked with other bits (because those may or may
748  not be interpolated over later, and we want to remove all
749  NaNs). Despite this behaviour, the "UNMASKEDNAN" mask plane
750  is used to preserve the historical name.
751 
752  \param[in,out] exposure exposure to process
753  """
754  maskedImage = exposure.getMaskedImage()
755 
756  # Find and mask NaNs
757  maskedImage.getMask().addMaskPlane("UNMASKEDNAN")
758  maskVal = maskedImage.getMask().getPlaneBitMask("UNMASKEDNAN")
759  numNans = maskNans(maskedImage, maskVal)
760  self.metadata.set("NUMNANS", numNans)
761 
762  # Interpolate over these previously-unmasked NaNs
763  if numNans > 0:
764  self.log.warn("There were %i unmasked NaNs", numNans)
765  nanDefectList = isr.getDefectListFromMask(
766  maskedImage=maskedImage,
767  maskName='UNMASKEDNAN',
768  growFootprints=0,
769  )
770  isr.interpolateDefectList(
771  maskedImage=exposure.getMaskedImage(),
772  defectList=nanDefectList,
773  fwhm=self.config.fwhm,
774  )
775 
776  def overscanCorrection(self, exposure, amp):
777  """!Apply overscan correction, in place
778 
779  \param[in,out] exposure exposure to process; must include both DataSec and BiasSec pixels
780  \param[in] amp amplifier device data
781  """
782  if not amp.getHasRawInfo():
783  raise RuntimeError("This method must be executed on an amp with raw information.")
784 
785  if amp.getRawHorizontalOverscanBBox().isEmpty():
786  self.log.info("No Overscan region. Not performing Overscan Correction.")
787  return None
788 
789  maskedImage = exposure.getMaskedImage()
790  dataView = maskedImage.Factory(maskedImage, amp.getRawDataBBox())
791 
792  expImage = exposure.getMaskedImage().getImage()
793  overscanImage = expImage.Factory(expImage, amp.getRawHorizontalOverscanBBox())
794 
795  isr.overscanCorrection(
796  ampMaskedImage=dataView,
797  overscanImage=overscanImage,
798  fitType=self.config.overscanFitType,
799  order=self.config.overscanOrder,
800  collapseRej=self.config.overscanRej,
801  )
802 
803  def setValidPolygonIntersect(self, ccdExposure, fpPolygon):
804  """!Set the valid polygon as the intersection of fpPolygon and the ccd corners
805 
806  \param[in,out] exposure exposure to process
807  \param[in] fpPolygon Polygon in focal plane coordinates
808  """
809  # Get ccd corners in focal plane coordinates
810  ccd = ccdExposure.getDetector()
811  fpCorners = ccd.getCorners(FOCAL_PLANE)
812  ccdPolygon = Polygon(fpCorners)
813 
814  # Get intersection of ccd corners with fpPolygon
815  intersect = ccdPolygon.intersectionSingle(fpPolygon)
816 
817  # Transform back to pixel positions and build new polygon
818  ccdPoints = [ccd.transform(ccd.makeCameraPoint(x, FOCAL_PLANE), PIXELS).getPoint() for x in intersect]
819  validPolygon = Polygon(ccdPoints)
820  ccdExposure.getInfo().setValidPolygon(validPolygon)
821 
822  def brighterFatterCorrection(self, exposure, kernel, maxIter, threshold, applyGain):
823  """Apply brighter fatter correction in place for the image
824 
825  This correction takes a kernel that has been derived from flat field images to
826  redistribute the charge. The gradient of the kernel is the deflection
827  field due to the accumulated charge.
828 
829  Given the original image I(x) and the kernel K(x) we can compute the corrected image Ic(x)
830  using the following equation:
831 
832  Ic(x) = I(x) + 0.5*d/dx(I(x)*d/dx(int( dy*K(x-y)*I(y))))
833 
834  To evaluate the derivative term we expand it as follows:
835 
836  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))) )
837 
838  Because we use the measured counts instead of the incident counts we apply the correction
839  iteratively to reconstruct the original counts and the correction. We stop iterating when the
840  summed difference between the current corrected image and the one from the previous iteration
841  is below the threshold. We do not require convergence because the number of iterations is
842  too large a computational cost. How we define the threshold still needs to be evaluated, the
843  current default was shown to work reasonably well on a small set of images. For more information
844  on the method see DocuShare Document-19407.
845 
846  The edges as defined by the kernel are not corrected because they have spurious values
847  due to the convolution.
848  """
849  self.log.info("Applying brighter fatter correction")
850 
851  image = exposure.getMaskedImage().getImage()
852 
853  # The image needs to be units of electrons/holes
854  with self.gainContext(exposure, image, applyGain):
855 
856  kLx = numpy.shape(kernel)[0]
857  kLy = numpy.shape(kernel)[1]
858  kernelImage = afwImage.ImageD(kernel.astype(numpy.float64))
859  tempImage = image.clone()
860 
861  nanIndex = numpy.isnan(tempImage.getArray())
862  tempImage.getArray()[nanIndex] = 0.
863 
864  outImage = afwImage.ImageF(image.getDimensions())
865  corr = numpy.zeros_like(image.getArray())
866  prev_image = numpy.zeros_like(image.getArray())
867  convCntrl = afwMath.ConvolutionControl(False, True, 1)
868  fixedKernel = afwMath.FixedKernel(kernelImage)
869 
870  # Define boundary by convolution region. The region that the correction will be
871  # calculated for is one fewer in each dimension because of the second derivative terms.
872  # NOTE: these need to use integer math, as we're using start:end as numpy index ranges.
873  startX = kLx//2
874  endX = -kLx//2
875  startY = kLy//2
876  endY = -kLy//2
877 
878  for iteration in range(maxIter):
879 
880  afwMath.convolve(outImage, tempImage, fixedKernel, convCntrl)
881  tmpArray = tempImage.getArray()
882  outArray = outImage.getArray()
883 
884  # First derivative term
885  gradTmp = numpy.gradient(tmpArray[startY:endY, startX:endX])
886  gradOut = numpy.gradient(outArray[startY:endY, startX:endX])
887  first = (gradTmp[0]*gradOut[0] + gradTmp[1]*gradOut[1])
888 
889  # Second derivative term
890  diffOut20 = numpy.gradient(gradOut[0])
891  diffOut21 = numpy.gradient(gradOut[1])
892  second = tmpArray[startY:endY, startX:endX]*(diffOut20[0] + diffOut21[1])
893 
894  corr[startY:endY, startX:endX] = 0.5*(first + second)
895 
896  # reset tmp image and apply correction
897  tmpArray[:, :] = image.getArray()[:, :]
898  tmpArray[nanIndex] = 0.
899  tmpArray[startY:endY, startX:endX] += corr[startY:endY, startX:endX]
900 
901  if iteration > 0:
902  diff = numpy.sum(numpy.abs(prev_image - tmpArray))
903 
904  if diff < threshold:
905  break
906  prev_image[:, :] = tmpArray[:, :]
907 
908  if iteration == maxIter - 1:
909  self.log.warn("Brighter fatter correction did not converge, final difference %f" % diff)
910 
911  self.log.info("Finished brighter fatter in %d iterations" % (iteration))
912  image.getArray()[startY:endY, startX:endX] += corr[startY:endY, startX:endX]
913 
914  @contextmanager
915  def gainContext(self, exp, image, apply):
916  """Context manager that applies and removes gain
917  """
918  if apply:
919  ccd = exp.getDetector()
920  for amp in ccd:
921  sim = image.Factory(image, amp.getBBox())
922  sim *= amp.getGain()
923 
924  try:
925  yield exp
926  finally:
927  if apply:
928  ccd = exp.getDetector()
929  for amp in ccd:
930  sim = image.Factory(image, amp.getBBox())
931  sim /= amp.getGain()
932 
933 
934 class FakeAmp(object):
935  """A Detector-like object that supports returning gain and saturation level"""
936 
937  def __init__(self, exposure, config):
938  self._bbox = exposure.getBBox(afwImage.LOCAL)
940  self._gain = config.gain
941  self._readNoise = config.readNoise
942  self._saturation = config.saturation
943 
944  def getBBox(self):
945  return self._bbox
946 
947  def getRawBBox(self):
948  return self._bbox
949 
950  def getHasRawInfo(self):
951  return True # but see getRawHorizontalOverscanBBox()
952 
954  return self._RawHorizontalOverscanBBox
955 
956  def getGain(self):
957  return self._gain
958 
959  def getReadNoise(self):
960  return self._readNoise
961 
962  def getSaturation(self):
963  return self._saturation
964 
965  def getSuspectLevel(self):
966  return float("NaN")
def doLinearize
Is linearization wanted for this detector?
Definition: isrTask.py:604
def getIsrExposure
Retrieve a calibration dataset for removing instrument signature.
Definition: isrTask.py:640
def maskAndInterpNan
Mask NaNs using mask plane &quot;UNMASKEDNAN&quot; and interpolate over them, in place.
Definition: isrTask.py:743
Parameters to control convolution.
Definition: ConvolveImage.h:58
def flatCorrection
Apply flat correction in place.
Definition: isrTask.py:627
def run
Perform instrument signature removal on an exposure.
Definition: isrTask.py:432
def __init__
Constructor for IsrTask.
Definition: isrTask.py:377
Apply common instrument signature correction algorithms to a raw frame.
Definition: isrTask.py:236
def runDataRef
Perform instrument signature removal on a ButlerDataRef of a Sensor.
Definition: isrTask.py:542
An integer coordinate rectangle.
Definition: Box.h:53
def suspectDetection
Detect suspect pixels and mask them using mask plane config.suspectMaskName, in place.
Definition: isrTask.py:697
def updateVariance
Set the variance plane based on the image plane, plus amplifier gain and read noise.
Definition: isrTask.py:614
def maskAndInterpDefect
Mask defects using mask plane &quot;BAD&quot; and interpolate over them, in place.
Definition: isrTask.py:722
def setValidPolygonIntersect
Set the valid polygon as the intersection of fpPolygon and the ccd corners.
Definition: isrTask.py:803
def overscanCorrection
Apply overscan correction, in place.
Definition: isrTask.py:776
def saturationDetection
Detect saturated pixels and mask them using mask plane config.saturatedMaskName, in place...
Definition: isrTask.py:665
def getDebugFrame
Definition: lsstDebug.py:66
def darkCorrection
Apply dark correction in place.
Definition: isrTask.py:590
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, bool doNormalize, bool doCopyEdge=false)
Old, deprecated version of convolve.
size_t maskNans(afw::image::MaskedImage< PixelT > const &mi, afw::image::MaskPixel maskVal, afw::image::MaskPixel allow=0)
Definition: Isr.cc:34
Encapsulate information about a bad portion of a detector.
Definition: Interp.h:70
def saturationInterpolation
Interpolate over saturated pixels, in place.
Definition: isrTask.py:681
A kernel created from an Image.
Definition: Kernel.h:548
def biasCorrection
Apply bias correction in place.
Definition: isrTask.py:582
def readIsrData
Retrieve necessary frames for instrument signature removal.
Definition: isrTask.py:388