LSSTApplications  11.0-13-gbb96280,12.1+18,12.1+7,12.1-1-g14f38d3+72,12.1-1-g16c0db7+5,12.1-1-g5961e7a+84,12.1-1-ge22e12b+23,12.1-11-g06625e2+4,12.1-11-g0d7f63b+4,12.1-19-gd507bfc,12.1-2-g7dda0ab+38,12.1-2-gc0bc6ab+81,12.1-21-g6ffe579+2,12.1-21-gbdb6c2a+4,12.1-24-g941c398+5,12.1-3-g57f6835+7,12.1-3-gf0736f3,12.1-37-g3ddd237,12.1-4-gf46015e+5,12.1-5-g06c326c+20,12.1-5-g648ee80+3,12.1-5-gc2189d7+4,12.1-6-ga608fc0+1,12.1-7-g3349e2a+5,12.1-7-gfd75620+9,12.1-9-g577b946+5,12.1-9-gc4df26a+10
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 
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  <HR>
306  """
307  ConfigClass = IsrTaskConfig
308  _DefaultName = "isr"
309 
310  def __init__(self, *args, **kwargs):
311  '''!Constructor for IsrTask
312  \param[in] *args -- a list of positional arguments passed on to the Task constructor
313  \param[in] **kwargs -- a dictionary of keyword arguments passed on to the Task constructor
314  Call the lsst.pipe.base.task.Task.__init__ method
315  Then setup the assembly and fringe correction subtasks
316  '''
317  pipeBase.Task.__init__(self, *args, **kwargs)
318  self.makeSubtask("assembleCcd")
319  self.makeSubtask("fringe")
320 
321  def readIsrData(self, dataRef, rawExposure):
322  """!Retrieve necessary frames for instrument signature removal
323  \param[in] dataRef -- a daf.persistence.butlerSubset.ButlerDataRef
324  of the detector data to be processed
325  \param[in] rawExposure -- a reference raw exposure that will later be
326  corrected with the retrieved calibration data;
327  should not be modified in this method.
328  \return a pipeBase.Struct with fields containing kwargs expected by run()
329  - bias: exposure of bias frame
330  - dark: exposure of dark frame
331  - flat: exposure of flat field
332  - defects: list of detects
333  - fringeStruct: a pipeBase.Struct with field fringes containing
334  exposure of fringe frame or list of fringe exposure
335  """
336  ccd = rawExposure.getDetector()
337 
338  biasExposure = self.getIsrExposure(dataRef, "bias") if self.config.doBias else None
339  # immediate=True required for functors and linearizers are functors; see ticket DM-6515
340  linearizer = dataRef.get("linearizer", immediate=True) if self.doLinearize(ccd) else None
341  darkExposure = self.getIsrExposure(dataRef, "dark") if self.config.doDark else None
342  flatExposure = self.getIsrExposure(dataRef, "flat") if self.config.doFlat else None
343  brighterFatterKernel = dataRef.get("brighterFatterKernel") if self.config.doBrighterFatter else None
344 
345  defectList = dataRef.get("defects")
346 
347  if self.config.doFringe and self.fringe.checkFilter(rawExposure):
348  fringeStruct = self.fringe.readFringes(dataRef, assembler=self.assembleCcd
349  if self.config.doAssembleIsrExposures else None)
350  else:
351  fringeStruct = pipeBase.Struct(fringes=None)
352 
353  # Struct should include only kwargs to run()
354  return pipeBase.Struct(bias=biasExposure,
355  linearizer=linearizer,
356  dark=darkExposure,
357  flat=flatExposure,
358  defects=defectList,
359  fringes=fringeStruct,
360  bfKernel=brighterFatterKernel
361  )
362 
363  @pipeBase.timeMethod
364  def run(self, ccdExposure, bias=None, linearizer=None, dark=None, flat=None, defects=None,
365  fringes=None, bfKernel=None):
366  """!Perform instrument signature removal on an exposure
367 
368  Steps include:
369  - Detect saturation, apply overscan correction, bias, dark and flat
370  - Perform CCD assembly
371  - Interpolate over defects, saturated pixels and all NaNs
372 
373  \param[in] ccdExposure -- lsst.afw.image.exposure of detector data
374  \param[in] bias -- exposure of bias frame
375  \param[in] linearizer -- linearizing functor; a subclass of lsst.ip.isr.LinearizeBase
376  \param[in] dark -- exposure of dark frame
377  \param[in] flat -- exposure of flatfield
378  \param[in] defects -- list of detects
379  \param[in] fringes -- a pipeBase.Struct with field fringes containing
380  exposure of fringe frame or list of fringe exposure
381  \param[in] bfKernel -- kernel for brighter-fatter correction
382 
383  \return a pipeBase.Struct with field:
384  - exposure
385  """
386 
387  # parseAndRun expects to be able to call run() with a dataRef; see DM-6640
388  if isinstance(ccdExposure, ButlerDataRef):
389  return self.runDataRef(ccdExposure)
390 
391  ccd = ccdExposure.getDetector()
392 
393  # Validate Input
394  if self.config.doBias and bias is None:
395  raise RuntimeError("Must supply a bias exposure if config.doBias True")
396  if self.doLinearize(ccd) and linearizer is None:
397  raise RuntimeError("Must supply a linearizer if config.doBias True")
398  if self.config.doDark and dark is None:
399  raise RuntimeError("Must supply a dark exposure if config.doDark True")
400  if self.config.doFlat and flat is None:
401  raise RuntimeError("Must supply a flat exposure if config.doFlat True")
402  if self.config.doBrighterFatter and bfKernel is None:
403  raise RuntimeError("Must supply a kernel if config.doBrighterFatter True")
404  if fringes is None:
405  fringes = pipeBase.Struct(fringes=None)
406  if self.config.doFringe and not isinstance(fringes, pipeBase.Struct):
407  raise RuntimeError("Must supply fringe exposure as a pipeBase.Struct")
408 
409  defects = [] if defects is None else defects
410 
411  ccdExposure = self.convertIntToFloat(ccdExposure)
412 
413  if not ccd:
414  assert not self.config.doAssembleCcd, "You need a Detector to run assembleCcd"
415  ccd = [FakeAmp(ccdExposure, self.config)]
416 
417  for amp in ccd:
418  # if ccdExposure is one amp, check for coverage to prevent performing ops multiple times
419  if ccdExposure.getBBox().contains(amp.getBBox()):
420  self.saturationDetection(ccdExposure, amp)
421  self.suspectDetection(ccdExposure, amp)
422  self.overscanCorrection(ccdExposure, amp)
423 
424  if self.config.doAssembleCcd:
425  ccdExposure = self.assembleCcd.assembleCcd(ccdExposure)
426 
427  if self.config.doBias:
428  self.biasCorrection(ccdExposure, bias)
429 
430  if self.doLinearize(ccd):
431  linearizer(image=ccdExposure.getMaskedImage().getImage(), detector=ccd, log=self.log)
432 
433  if self.config.doBrighterFatter:
434  self.brighterFatterCorrection(ccdExposure, bfKernel,
435  self.config.brighterFatterMaxIter,
436  self.config.brighterFatterThreshold,
437  self.config.brighterFatterApplyGain,
438  )
439 
440  if self.config.doDark:
441  self.darkCorrection(ccdExposure, dark)
442 
443  for amp in ccd:
444  # if ccdExposure is one amp, check for coverage to prevent performing ops multiple times
445  if ccdExposure.getBBox().contains(amp.getBBox()):
446  ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
447  self.updateVariance(ampExposure, amp)
448 
449  if self.config.doFringe and not self.config.fringeAfterFlat:
450  self.fringe.run(ccdExposure, **fringes.getDict())
451 
452  if self.config.doFlat:
453  self.flatCorrection(ccdExposure, flat)
454 
455  self.maskAndInterpDefect(ccdExposure, defects)
456 
457  self.saturationInterpolation(ccdExposure)
458 
459  self.maskAndInterpNan(ccdExposure)
460 
461  if self.config.doFringe and self.config.fringeAfterFlat:
462  self.fringe.run(ccdExposure, **fringes.getDict())
463 
464  exposureTime = ccdExposure.getInfo().getVisitInfo().getExposureTime()
465  ccdExposure.getCalib().setFluxMag0(self.config.fluxMag0T1*exposureTime)
466 
467  frame = getDebugFrame(self._display, "postISRCCD")
468  if frame:
469  getDisplay(frame).mtv(ccdExposure)
470 
471  return pipeBase.Struct(
472  exposure=ccdExposure,
473  )
474 
475  @pipeBase.timeMethod
476  def runDataRef(self, sensorRef):
477  """!Perform instrument signature removal on a ButlerDataRef of a Sensor
478 
479  - Read in necessary detrending/isr/calibration data
480  - Process raw exposure in run()
481  - Persist the ISR-corrected exposure as "postISRCCD" if config.doWrite is True
482 
483  \param[in] sensorRef -- daf.persistence.butlerSubset.ButlerDataRef of the
484  detector data to be processed
485  \return a pipeBase.Struct with fields:
486  - exposure: the exposure after application of ISR
487  """
488  self.log.info("Performing ISR on sensor %s" % (sensorRef.dataId))
489  ccdExposure = sensorRef.get('raw')
490  isrData = self.readIsrData(sensorRef, ccdExposure)
491 
492  result = self.run(ccdExposure, **isrData.getDict())
493 
494  if self.config.doWrite:
495  sensorRef.put(result.exposure, "postISRCCD")
496 
497  return result
498 
499  def convertIntToFloat(self, exposure):
500  """Convert an exposure from uint16 to float, set variance plane to 1 and mask plane to 0
501  """
502  if isinstance(exposure, afwImage.ExposureF):
503  # Nothing to be done
504  return exposure
505  if not hasattr(exposure, "convertF"):
506  raise RuntimeError("Unable to convert exposure (%s) to float" % type(exposure))
507 
508  newexposure = exposure.convertF()
509  maskedImage = newexposure.getMaskedImage()
510  varArray = maskedImage.getVariance().getArray()
511  varArray[:, :] = 1
512  maskArray = maskedImage.getMask().getArray()
513  maskArray[:, :] = 0
514  return newexposure
515 
516  def biasCorrection(self, exposure, biasExposure):
517  """!Apply bias correction in place
518 
519  \param[in,out] exposure exposure to process
520  \param[in] biasExposure bias exposure of same size as exposure
521  """
522  isr.biasCorrection(exposure.getMaskedImage(), biasExposure.getMaskedImage())
523 
524  def darkCorrection(self, exposure, darkExposure):
525  """!Apply dark correction in place
526 
527  \param[in,out] exposure exposure to process
528  \param[in] darkExposure dark exposure of same size as exposure
529  """
530  expScale = exposure.getInfo().getVisitInfo().getDarkTime()
531  if math.isnan(expScale):
532  raise RuntimeError("Exposure darktime is NAN")
533  darkScale = darkExposure.getInfo().getVisitInfo().getDarkTime()
534  if math.isnan(darkScale):
535  raise RuntimeError("Dark calib darktime is NAN")
536  isr.darkCorrection(
537  maskedImage=exposure.getMaskedImage(),
538  darkMaskedImage=darkExposure.getMaskedImage(),
539  expScale=expScale,
540  darkScale=darkScale,
541  )
542 
543  def doLinearize(self, detector):
544  """!Is linearization wanted for this detector?
545 
546  Checks config.doLinearize and the linearity type of the first amplifier.
547 
548  \param[in] detector detector information (an lsst.afw.cameraGeom.Detector)
549  """
550  return self.config.doLinearize and \
551  detector.getAmpInfoCatalog()[0].getLinearityType() != NullLinearityType
552 
553  def updateVariance(self, ampExposure, amp):
554  """!Set the variance plane based on the image plane, plus amplifier gain and read noise
555 
556  \param[in,out] ampExposure exposure to process
557  \param[in] amp amplifier detector information
558  """
559  if not math.isnan(amp.getGain()):
560  isr.updateVariance(
561  maskedImage=ampExposure.getMaskedImage(),
562  gain=amp.getGain(),
563  readNoise=amp.getReadNoise(),
564  )
565 
566  def flatCorrection(self, exposure, flatExposure):
567  """!Apply flat correction in place
568 
569  \param[in,out] exposure exposure to process
570  \param[in] flatExposure flatfield exposure same size as exposure
571  """
572  isr.flatCorrection(
573  maskedImage=exposure.getMaskedImage(),
574  flatMaskedImage=flatExposure.getMaskedImage(),
575  scalingType=self.config.flatScalingType,
576  userScale=self.config.flatUserScale,
577  )
578 
579  def getIsrExposure(self, dataRef, datasetType, immediate=True):
580  """!Retrieve a calibration dataset for removing instrument signature
581 
582  \param[in] dataRef data reference for exposure
583  \param[in] datasetType type of dataset to retrieve (e.g. 'bias', 'flat')
584  \param[in] immediate if True, disable butler proxies to enable error
585  handling within this routine
586  \return exposure
587  """
588  try:
589  exp = dataRef.get(datasetType, immediate=immediate)
590  except Exception as exc1:
591  if not self.config.fallbackFilterName:
592  raise RuntimeError("Unable to retrieve %s for %s: %s" % (datasetType, dataRef.dataId, exc1))
593  try:
594  exp = dataRef.get(datasetType, filter=self.config.fallbackFilterName, immediate=immediate)
595  except Exception as exc2:
596  raise RuntimeError("Unable to retrieve %s for %s, even with fallback filter %s: %s AND %s" %
597  (datasetType, dataRef.dataId, self.config.fallbackFilterName, exc1, exc2))
598  self.log.warn("Using fallback calibration from filter %s" % self.config.fallbackFilterName)
599 
600  if self.config.doAssembleIsrExposures:
601  exp = self.assembleCcd.assembleCcd(exp)
602  return exp
603 
604  def saturationDetection(self, exposure, amp):
605  """!Detect saturated pixels and mask them using mask plane config.saturatedMaskName, in place
606 
607  \param[in,out] exposure exposure to process; only the amp DataSec is processed
608  \param[in] amp amplifier device data
609  """
610  if not math.isnan(amp.getSaturation()):
611  maskedImage = exposure.getMaskedImage()
612  dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
613  isr.makeThresholdMask(
614  maskedImage=dataView,
615  threshold=amp.getSaturation(),
616  growFootprints=0,
617  maskName=self.config.saturatedMaskName,
618  )
619 
620  def saturationInterpolation(self, ccdExposure):
621  """!Interpolate over saturated pixels, in place
622 
623  \param[in,out] ccdExposure exposure to process
624 
625  \warning:
626  - Call saturationDetection first, so that saturated pixels have been identified in the "SAT" mask.
627  - Call this after CCD assembly, since saturated regions may cross amplifier boundaries
628  """
629  isr.interpolateFromMask(
630  maskedImage=ccdExposure.getMaskedImage(),
631  fwhm=self.config.fwhm,
632  growFootprints=self.config.growSaturationFootprintSize,
633  maskName=self.config.saturatedMaskName,
634  )
635 
636  def suspectDetection(self, exposure, amp):
637  """!Detect suspect pixels and mask them using mask plane config.suspectMaskName, in place
638 
639  Suspect pixels are pixels whose value is greater than amp.getSuspectLevel().
640  This is intended to indicate pixels that may be affected by unknown systematics;
641  for example if non-linearity corrections above a certain level are unstable
642  then that would be a useful value for suspectLevel. A value of `nan` indicates
643  that no such level exists and no pixels are to be masked as suspicious.
644 
645  \param[in,out] exposure exposure to process; only the amp DataSec is processed
646  \param[in] amp amplifier device data
647  """
648  suspectLevel = amp.getSuspectLevel()
649  if math.isnan(suspectLevel):
650  return
651 
652  maskedImage = exposure.getMaskedImage()
653  dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
654  isr.makeThresholdMask(
655  maskedImage=dataView,
656  threshold=suspectLevel,
657  growFootprints=0,
658  maskName=self.config.suspectMaskName,
659  )
660 
661  def maskAndInterpDefect(self, ccdExposure, defectBaseList):
662  """!Mask defects using mask plane "BAD" and interpolate over them, in place
663 
664  \param[in,out] ccdExposure exposure to process
665  \param[in] defectBaseList a list of defects to mask and interpolate
666 
667  \warning: call this after CCD assembly, since defects may cross amplifier boundaries
668  """
669  maskedImage = ccdExposure.getMaskedImage()
670  defectList = measAlg.DefectListT()
671  for d in defectBaseList:
672  bbox = d.getBBox()
673  nd = measAlg.Defect(bbox)
674  defectList.append(nd)
675  isr.maskPixelsFromDefectList(maskedImage, defectList, maskName='BAD')
676  isr.interpolateDefectList(
677  maskedImage=maskedImage,
678  defectList=defectList,
679  fwhm=self.config.fwhm,
680  )
681 
682  def maskAndInterpNan(self, exposure):
683  """!Mask NaNs using mask plane "UNMASKEDNAN" and interpolate over them, in place
684 
685  We mask and interpolate over all NaNs, including those
686  that are masked with other bits (because those may or may
687  not be interpolated over later, and we want to remove all
688  NaNs). Despite this behaviour, the "UNMASKEDNAN" mask plane
689  is used to preserve the historical name.
690 
691  \param[in,out] exposure exposure to process
692  """
693  maskedImage = exposure.getMaskedImage()
694 
695  # Find and mask NaNs
696  maskedImage.getMask().addMaskPlane("UNMASKEDNAN")
697  maskVal = maskedImage.getMask().getPlaneBitMask("UNMASKEDNAN")
698  numNans = maskNans(maskedImage, maskVal)
699  self.metadata.set("NUMNANS", numNans)
700 
701  # Interpolate over these previously-unmasked NaNs
702  if numNans > 0:
703  self.log.warn("There were %i unmasked NaNs", numNans)
704  nanDefectList = isr.getDefectListFromMask(
705  maskedImage=maskedImage,
706  maskName='UNMASKEDNAN',
707  growFootprints=0,
708  )
709  isr.interpolateDefectList(
710  maskedImage=exposure.getMaskedImage(),
711  defectList=nanDefectList,
712  fwhm=self.config.fwhm,
713  )
714 
715  def overscanCorrection(self, exposure, amp):
716  """!Apply overscan correction, in place
717 
718  \param[in,out] exposure exposure to process; must include both DataSec and BiasSec pixels
719  \param[in] amp amplifier device data
720  """
721  if not amp.getHasRawInfo():
722  raise RuntimeError("This method must be executed on an amp with raw information.")
723 
724  if amp.getRawHorizontalOverscanBBox().isEmpty():
725  self.log.info("No Overscan region. Not performing Overscan Correction.")
726  return None
727 
728  maskedImage = exposure.getMaskedImage()
729  dataView = maskedImage.Factory(maskedImage, amp.getRawDataBBox())
730 
731  expImage = exposure.getMaskedImage().getImage()
732  overscanImage = expImage.Factory(expImage, amp.getRawHorizontalOverscanBBox())
733 
734  isr.overscanCorrection(
735  ampMaskedImage=dataView,
736  overscanImage=overscanImage,
737  fitType=self.config.overscanFitType,
738  order=self.config.overscanOrder,
739  collapseRej=self.config.overscanRej,
740  )
741 
742  def setValidPolygonIntersect(self, ccdExposure, fpPolygon):
743  """!Set the valid polygon as the intersection of fpPolygon and the ccd corners
744 
745  \param[in,out] ccdExposure exposure to process
746  \param[in] fpPolygon Polygon in focal plane coordinates
747  """
748  # Get ccd corners in focal plane coordinates
749  ccd = ccdExposure.getDetector()
750  fpCorners = ccd.getCorners(FOCAL_PLANE)
751  ccdPolygon = Polygon(fpCorners)
752 
753  # Get intersection of ccd corners with fpPolygon
754  intersect = ccdPolygon.intersectionSingle(fpPolygon)
755 
756  # Transform back to pixel positions and build new polygon
757  ccdPoints = [ccd.transform(ccd.makeCameraPoint(x, FOCAL_PLANE), PIXELS).getPoint() for x in intersect]
758  validPolygon = Polygon(ccdPoints)
759  ccdExposure.getInfo().setValidPolygon(validPolygon)
760 
761  def brighterFatterCorrection(self, exposure, kernel, maxIter, threshold, applyGain):
762  """Apply brighter fatter correction in place for the image
763 
764  This correction takes a kernel that has been derived from flat field images to
765  redistribute the charge. The gradient of the kernel is the deflection
766  field due to the accumulated charge.
767 
768  Given the original image I(x) and the kernel K(x) we can compute the corrected image Ic(x)
769  using the following equation:
770 
771  Ic(x) = I(x) + 0.5*d/dx(I(x)*d/dx(int( dy*K(x-y)*I(y))))
772 
773  To evaluate the derivative term we expand it as follows:
774 
775  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))) )
776 
777  Because we use the measured counts instead of the incident counts we apply the correction
778  iteratively to reconstruct the original counts and the correction. We stop iterating when the
779  summed difference between the current corrected image and the one from the previous iteration
780  is below the threshold. We do not require convergence because the number of iterations is
781  too large a computational cost. How we define the threshold still needs to be evaluated, the
782  current default was shown to work reasonably well on a small set of images. For more information
783  on the method see DocuShare Document-19407.
784 
785  The edges as defined by the kernel are not corrected because they have spurious values
786  due to the convolution.
787  """
788  self.log.info("Applying brighter fatter correction")
789 
790  image = exposure.getMaskedImage().getImage()
791 
792  # The image needs to be units of electrons/holes
793  with self.gainContext(exposure, image, applyGain):
794 
795  kLx = numpy.shape(kernel)[0]
796  kLy = numpy.shape(kernel)[1]
797  kernelImage = afwImage.ImageD(kernel.astype(numpy.float64))
798  tempImage = image.clone()
799 
800  nanIndex = numpy.isnan(tempImage.getArray())
801  tempImage.getArray()[nanIndex] = 0.
802 
803  outImage = afwImage.ImageF(image.getDimensions())
804  corr = numpy.zeros_like(image.getArray())
805  prev_image = numpy.zeros_like(image.getArray())
806  convCntrl = afwMath.ConvolutionControl(False, True, 1)
807  fixedKernel = afwMath.FixedKernel(kernelImage)
808 
809  # Define boundary by convolution region. The region that the correction will be
810  # calculated for is one fewer in each dimension because of the second derivative terms.
811  # NOTE: these need to use integer math, as we're using start:end as numpy index ranges.
812  startX = kLx//2
813  endX = -kLx//2
814  startY = kLy//2
815  endY = -kLy//2
816 
817  for iteration in range(maxIter):
818 
819  afwMath.convolve(outImage, tempImage, fixedKernel, convCntrl)
820  tmpArray = tempImage.getArray()
821  outArray = outImage.getArray()
822 
823  # First derivative term
824  gradTmp = numpy.gradient(tmpArray[startY:endY, startX:endX])
825  gradOut = numpy.gradient(outArray[startY:endY, startX:endX])
826  first = (gradTmp[0]*gradOut[0] + gradTmp[1]*gradOut[1])
827 
828  # Second derivative term
829  diffOut20 = numpy.gradient(gradOut[0])
830  diffOut21 = numpy.gradient(gradOut[1])
831  second = tmpArray[startY:endY, startX:endX]*(diffOut20[0] + diffOut21[1])
832 
833  corr[startY:endY, startX:endX] = 0.5*(first + second)
834 
835  # reset tmp image and apply correction
836  tmpArray[:, :] = image.getArray()[:, :]
837  tmpArray[nanIndex] = 0.
838  tmpArray[startY:endY, startX:endX] += corr[startY:endY, startX:endX]
839 
840  if iteration > 0:
841  diff = numpy.sum(numpy.abs(prev_image - tmpArray))
842 
843  if diff < threshold:
844  break
845  prev_image[:, :] = tmpArray[:, :]
846 
847  if iteration == maxIter - 1:
848  self.log.warn("Brighter fatter correction did not converge, final difference %f" % diff)
849 
850  self.log.info("Finished brighter fatter in %d iterations" % (iteration))
851  image.getArray()[startY:endY, startX:endX] += corr[startY:endY, startX:endX]
852 
853  @contextmanager
854  def gainContext(self, exp, image, apply):
855  """Context manager that applies and removes gain
856  """
857  if apply:
858  ccd = exp.getDetector()
859  for amp in ccd:
860  sim = image.Factory(image, amp.getBBox())
861  sim *= amp.getGain()
862 
863  try:
864  yield exp
865  finally:
866  if apply:
867  ccd = exp.getDetector()
868  for amp in ccd:
869  sim = image.Factory(image, amp.getBBox())
870  sim /= amp.getGain()
871 
872 
873 class FakeAmp(object):
874  """A Detector-like object that supports returning gain and saturation level"""
875 
876  def __init__(self, exposure, config):
877  self._bbox = exposure.getBBox(afwImage.LOCAL)
879  self._gain = config.gain
880  self._readNoise = config.readNoise
881  self._saturation = config.saturation
882 
883  def getBBox(self):
884  return self._bbox
885 
886  def getRawBBox(self):
887  return self._bbox
888 
889  def getHasRawInfo(self):
890  return True # but see getRawHorizontalOverscanBBox()
891 
893  return self._RawHorizontalOverscanBBox
894 
895  def getGain(self):
896  return self._gain
897 
898  def getReadNoise(self):
899  return self._readNoise
900 
901  def getSaturation(self):
902  return self._saturation
903 
904  def getSuspectLevel(self):
905  return float("NaN")
def doLinearize
Is linearization wanted for this detector?
Definition: isrTask.py:543
def getIsrExposure
Retrieve a calibration dataset for removing instrument signature.
Definition: isrTask.py:579
def maskAndInterpNan
Mask NaNs using mask plane &quot;UNMASKEDNAN&quot; and interpolate over them, in place.
Definition: isrTask.py:682
Parameters to control convolution.
Definition: ConvolveImage.h:57
def flatCorrection
Apply flat correction in place.
Definition: isrTask.py:566
def run
Perform instrument signature removal on an exposure.
Definition: isrTask.py:365
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations...
def __init__
Constructor for IsrTask.
Definition: isrTask.py:310
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:476
An integer coordinate rectangle.
Definition: Box.h:53
size_t maskNans(afw::image::MaskedImage< PixelT > const &mi, afw::image::MaskPixel maskVal, afw::image::MaskPixel allow)
Mask NANs in an image.
Definition: Isr.cc:34
def suspectDetection
Detect suspect pixels and mask them using mask plane config.suspectMaskName, in place.
Definition: isrTask.py:636
def updateVariance
Set the variance plane based on the image plane, plus amplifier gain and read noise.
Definition: isrTask.py:553
def maskAndInterpDefect
Mask defects using mask plane &quot;BAD&quot; and interpolate over them, in place.
Definition: isrTask.py:661
def setValidPolygonIntersect
Set the valid polygon as the intersection of fpPolygon and the ccd corners.
Definition: isrTask.py:742
def overscanCorrection
Apply overscan correction, in place.
Definition: isrTask.py:715
def saturationDetection
Detect saturated pixels and mask them using mask plane config.saturatedMaskName, in place...
Definition: isrTask.py:604
def getDebugFrame
Definition: lsstDebug.py:66
def darkCorrection
Apply dark correction in place.
Definition: isrTask.py:524
Cartesian polygons.
Definition: Polygon.h:55
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, bool doNormalize, bool doCopyEdge=false)
Old, deprecated version of convolve.
Encapsulate information about a bad portion of a detector.
Definition: Interp.h:70
def saturationInterpolation
Interpolate over saturated pixels, in place.
Definition: isrTask.py:620
A kernel created from an Image.
Definition: Kernel.h:548
def biasCorrection
Apply bias correction in place.
Definition: isrTask.py:516
def readIsrData
Retrieve necessary frames for instrument signature removal.
Definition: isrTask.py:321