lsst.ip.isr  19.0.0-19-ga9bf60b
isrFunctions.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008, 2009, 2010 LSST Corporation.
4 #
5 # This product includes software developed by the
6 # LSST Project (http://www.lsst.org/).
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <http://www.lsstcorp.org/LegalNotices/>.
21 #
22 import math
23 import numpy
24 from deprecated.sphinx import deprecated
25 
26 import lsst.geom
27 import lsst.afw.image as afwImage
28 import lsst.afw.detection as afwDetection
29 import lsst.afw.math as afwMath
30 import lsst.meas.algorithms as measAlg
31 import lsst.afw.cameraGeom as camGeom
32 
33 from lsst.afw.geom.wcsUtils import makeDistortedTanWcs
34 from lsst.meas.algorithms.detection import SourceDetectionTask
35 
36 from contextlib import contextmanager
37 
38 from .overscan import OverscanCorrectionTask, OverscanCorrectionTaskConfig
39 
40 
41 def createPsf(fwhm):
42  """Make a double Gaussian PSF.
43 
44  Parameters
45  ----------
46  fwhm : scalar
47  FWHM of double Gaussian smoothing kernel.
48 
49  Returns
50  -------
51  psf : `lsst.meas.algorithms.DoubleGaussianPsf`
52  The created smoothing kernel.
53  """
54  ksize = 4*int(fwhm) + 1
55  return measAlg.DoubleGaussianPsf(ksize, ksize, fwhm/(2*math.sqrt(2*math.log(2))))
56 
57 
58 def transposeMaskedImage(maskedImage):
59  """Make a transposed copy of a masked image.
60 
61  Parameters
62  ----------
63  maskedImage : `lsst.afw.image.MaskedImage`
64  Image to process.
65 
66  Returns
67  -------
68  transposed : `lsst.afw.image.MaskedImage`
69  The transposed copy of the input image.
70  """
71  transposed = maskedImage.Factory(lsst.geom.Extent2I(maskedImage.getHeight(), maskedImage.getWidth()))
72  transposed.getImage().getArray()[:] = maskedImage.getImage().getArray().T
73  transposed.getMask().getArray()[:] = maskedImage.getMask().getArray().T
74  transposed.getVariance().getArray()[:] = maskedImage.getVariance().getArray().T
75  return transposed
76 
77 
78 def interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=None):
79  """Interpolate over defects specified in a defect list.
80 
81  Parameters
82  ----------
83  maskedImage : `lsst.afw.image.MaskedImage`
84  Image to process.
85  defectList : `lsst.meas.algorithms.Defects`
86  List of defects to interpolate over.
87  fwhm : scalar
88  FWHM of double Gaussian smoothing kernel.
89  fallbackValue : scalar, optional
90  Fallback value if an interpolated value cannot be determined.
91  If None, then the clipped mean of the image is used.
92  """
93  psf = createPsf(fwhm)
94  if fallbackValue is None:
95  fallbackValue = afwMath.makeStatistics(maskedImage.getImage(), afwMath.MEANCLIP).getValue()
96  if 'INTRP' not in maskedImage.getMask().getMaskPlaneDict():
97  maskedImage.getMask().addMaskPlane('INTRP')
98  measAlg.interpolateOverDefects(maskedImage, psf, defectList, fallbackValue, True)
99  return maskedImage
100 
101 
102 def makeThresholdMask(maskedImage, threshold, growFootprints=1, maskName='SAT'):
103  """Mask pixels based on threshold detection.
104 
105  Parameters
106  ----------
107  maskedImage : `lsst.afw.image.MaskedImage`
108  Image to process. Only the mask plane is updated.
109  threshold : scalar
110  Detection threshold.
111  growFootprints : scalar, optional
112  Number of pixels to grow footprints of detected regions.
113  maskName : str, optional
114  Mask plane name, or list of names to convert
115 
116  Returns
117  -------
118  defectList : `lsst.meas.algorithms.Defects`
119  Defect list constructed from pixels above the threshold.
120  """
121  # find saturated regions
122  thresh = afwDetection.Threshold(threshold)
123  fs = afwDetection.FootprintSet(maskedImage, thresh)
124 
125  if growFootprints > 0:
126  fs = afwDetection.FootprintSet(fs, rGrow=growFootprints, isotropic=False)
127  fpList = fs.getFootprints()
128 
129  # set mask
130  mask = maskedImage.getMask()
131  bitmask = mask.getPlaneBitMask(maskName)
132  afwDetection.setMaskFromFootprintList(mask, fpList, bitmask)
133 
134  return measAlg.Defects.fromFootprintList(fpList)
135 
136 
137 def growMasks(mask, radius=0, maskNameList=['BAD'], maskValue="BAD"):
138  """Grow a mask by an amount and add to the requested plane.
139 
140  Parameters
141  ----------
142  mask : `lsst.afw.image.Mask`
143  Mask image to process.
144  radius : scalar
145  Amount to grow the mask.
146  maskNameList : `str` or `list` [`str`]
147  Mask names that should be grown.
148  maskValue : `str`
149  Mask plane to assign the newly masked pixels to.
150  """
151  if radius > 0:
152  thresh = afwDetection.Threshold(mask.getPlaneBitMask(maskNameList), afwDetection.Threshold.BITMASK)
153  fpSet = afwDetection.FootprintSet(mask, thresh)
154  fpSet = afwDetection.FootprintSet(fpSet, rGrow=radius, isotropic=False)
155  fpSet.setMask(mask, maskValue)
156 
157 
158 def interpolateFromMask(maskedImage, fwhm, growSaturatedFootprints=1,
159  maskNameList=['SAT'], fallbackValue=None):
160  """Interpolate over defects identified by a particular set of mask planes.
161 
162  Parameters
163  ----------
164  maskedImage : `lsst.afw.image.MaskedImage`
165  Image to process.
166  fwhm : scalar
167  FWHM of double Gaussian smoothing kernel.
168  growSaturatedFootprints : scalar, optional
169  Number of pixels to grow footprints for saturated pixels.
170  maskNameList : `List` of `str`, optional
171  Mask plane name.
172  fallbackValue : scalar, optional
173  Value of last resort for interpolation.
174  """
175  mask = maskedImage.getMask()
176 
177  if growSaturatedFootprints > 0 and "SAT" in maskNameList:
178  # If we are interpolating over an area larger than the original masked region, we need
179  # to expand the original mask bit to the full area to explain why we interpolated there.
180  growMasks(mask, radius=growSaturatedFootprints, maskNameList=['SAT'], maskValue="SAT")
181 
182  thresh = afwDetection.Threshold(mask.getPlaneBitMask(maskNameList), afwDetection.Threshold.BITMASK)
183  fpSet = afwDetection.FootprintSet(mask, thresh)
184  defectList = measAlg.Defects.fromFootprintList(fpSet.getFootprints())
185 
186  interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=fallbackValue)
187 
188  return maskedImage
189 
190 
191 def saturationCorrection(maskedImage, saturation, fwhm, growFootprints=1, interpolate=True, maskName='SAT',
192  fallbackValue=None):
193  """Mark saturated pixels and optionally interpolate over them
194 
195  Parameters
196  ----------
197  maskedImage : `lsst.afw.image.MaskedImage`
198  Image to process.
199  saturation : scalar
200  Saturation level used as the detection threshold.
201  fwhm : scalar
202  FWHM of double Gaussian smoothing kernel.
203  growFootprints : scalar, optional
204  Number of pixels to grow footprints of detected regions.
205  interpolate : Bool, optional
206  If True, saturated pixels are interpolated over.
207  maskName : str, optional
208  Mask plane name.
209  fallbackValue : scalar, optional
210  Value of last resort for interpolation.
211  """
212  defectList = makeThresholdMask(
213  maskedImage=maskedImage,
214  threshold=saturation,
215  growFootprints=growFootprints,
216  maskName=maskName,
217  )
218  if interpolate:
219  interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=fallbackValue)
220 
221  return maskedImage
222 
223 
224 def trimToMatchCalibBBox(rawMaskedImage, calibMaskedImage):
225  """Compute number of edge trim pixels to match the calibration data.
226 
227  Use the dimension difference between the raw exposure and the
228  calibration exposure to compute the edge trim pixels. This trim
229  is applied symmetrically, with the same number of pixels masked on
230  each side.
231 
232  Parameters
233  ----------
234  rawMaskedImage : `lsst.afw.image.MaskedImage`
235  Image to trim.
236  calibMaskedImage : `lsst.afw.image.MaskedImage`
237  Calibration image to draw new bounding box from.
238 
239  Returns
240  -------
241  replacementMaskedImage : `lsst.afw.image.MaskedImage`
242  ``rawMaskedImage`` trimmed to the appropriate size
243  Raises
244  ------
245  RuntimeError
246  Rasied if ``rawMaskedImage`` cannot be symmetrically trimmed to
247  match ``calibMaskedImage``.
248  """
249  nx, ny = rawMaskedImage.getBBox().getDimensions() - calibMaskedImage.getBBox().getDimensions()
250  if nx != ny:
251  raise RuntimeError("Raw and calib maskedImages are trimmed differently in X and Y.")
252  if nx % 2 != 0:
253  raise RuntimeError("Calibration maskedImage is trimmed unevenly in X.")
254  if nx < 0:
255  raise RuntimeError("Calibration maskedImage is larger than raw data.")
256 
257  nEdge = nx//2
258  if nEdge > 0:
259  replacementMaskedImage = rawMaskedImage[nEdge:-nEdge, nEdge:-nEdge, afwImage.LOCAL]
260  SourceDetectionTask.setEdgeBits(
261  rawMaskedImage,
262  replacementMaskedImage.getBBox(),
263  rawMaskedImage.getMask().getPlaneBitMask("EDGE")
264  )
265  else:
266  replacementMaskedImage = rawMaskedImage
267 
268  return replacementMaskedImage
269 
270 
271 def biasCorrection(maskedImage, biasMaskedImage, trimToFit=False):
272  """Apply bias correction in place.
273 
274  Parameters
275  ----------
276  maskedImage : `lsst.afw.image.MaskedImage`
277  Image to process. The image is modified by this method.
278  biasMaskedImage : `lsst.afw.image.MaskedImage`
279  Bias image of the same size as ``maskedImage``
280  trimToFit : `Bool`, optional
281  If True, raw data is symmetrically trimmed to match
282  calibration size.
283 
284  Raises
285  ------
286  RuntimeError
287  Raised if ``maskedImage`` and ``biasMaskedImage`` do not have
288  the same size.
289 
290  """
291  if trimToFit:
292  maskedImage = trimToMatchCalibBBox(maskedImage, biasMaskedImage)
293 
294  if maskedImage.getBBox(afwImage.LOCAL) != biasMaskedImage.getBBox(afwImage.LOCAL):
295  raise RuntimeError("maskedImage bbox %s != biasMaskedImage bbox %s" %
296  (maskedImage.getBBox(afwImage.LOCAL), biasMaskedImage.getBBox(afwImage.LOCAL)))
297  maskedImage -= biasMaskedImage
298 
299 
300 def darkCorrection(maskedImage, darkMaskedImage, expScale, darkScale, invert=False, trimToFit=False):
301  """Apply dark correction in place.
302 
303  Parameters
304  ----------
305  maskedImage : `lsst.afw.image.MaskedImage`
306  Image to process. The image is modified by this method.
307  darkMaskedImage : `lsst.afw.image.MaskedImage`
308  Dark image of the same size as ``maskedImage``.
309  expScale : scalar
310  Dark exposure time for ``maskedImage``.
311  darkScale : scalar
312  Dark exposure time for ``darkMaskedImage``.
313  invert : `Bool`, optional
314  If True, re-add the dark to an already corrected image.
315  trimToFit : `Bool`, optional
316  If True, raw data is symmetrically trimmed to match
317  calibration size.
318 
319  Raises
320  ------
321  RuntimeError
322  Raised if ``maskedImage`` and ``darkMaskedImage`` do not have
323  the same size.
324 
325  Notes
326  -----
327  The dark correction is applied by calculating:
328  maskedImage -= dark * expScaling / darkScaling
329  """
330  if trimToFit:
331  maskedImage = trimToMatchCalibBBox(maskedImage, darkMaskedImage)
332 
333  if maskedImage.getBBox(afwImage.LOCAL) != darkMaskedImage.getBBox(afwImage.LOCAL):
334  raise RuntimeError("maskedImage bbox %s != darkMaskedImage bbox %s" %
335  (maskedImage.getBBox(afwImage.LOCAL), darkMaskedImage.getBBox(afwImage.LOCAL)))
336 
337  scale = expScale / darkScale
338  if not invert:
339  maskedImage.scaledMinus(scale, darkMaskedImage)
340  else:
341  maskedImage.scaledPlus(scale, darkMaskedImage)
342 
343 
344 def updateVariance(maskedImage, gain, readNoise):
345  """Set the variance plane based on the image plane.
346 
347  Parameters
348  ----------
349  maskedImage : `lsst.afw.image.MaskedImage`
350  Image to process. The variance plane is modified.
351  gain : scalar
352  The amplifier gain in electrons/ADU.
353  readNoise : scalar
354  The amplifier read nmoise in ADU/pixel.
355  """
356  var = maskedImage.getVariance()
357  var[:] = maskedImage.getImage()
358  var /= gain
359  var += readNoise**2
360 
361 
362 def flatCorrection(maskedImage, flatMaskedImage, scalingType, userScale=1.0, invert=False, trimToFit=False):
363  """Apply flat correction in place.
364 
365  Parameters
366  ----------
367  maskedImage : `lsst.afw.image.MaskedImage`
368  Image to process. The image is modified.
369  flatMaskedImage : `lsst.afw.image.MaskedImage`
370  Flat image of the same size as ``maskedImage``
371  scalingType : str
372  Flat scale computation method. Allowed values are 'MEAN',
373  'MEDIAN', or 'USER'.
374  userScale : scalar, optional
375  Scale to use if ``scalingType``='USER'.
376  invert : `Bool`, optional
377  If True, unflatten an already flattened image.
378  trimToFit : `Bool`, optional
379  If True, raw data is symmetrically trimmed to match
380  calibration size.
381 
382  Raises
383  ------
384  RuntimeError
385  Raised if ``maskedImage`` and ``flatMaskedImage`` do not have
386  the same size or if ``scalingType`` is not an allowed value.
387  """
388  if trimToFit:
389  maskedImage = trimToMatchCalibBBox(maskedImage, flatMaskedImage)
390 
391  if maskedImage.getBBox(afwImage.LOCAL) != flatMaskedImage.getBBox(afwImage.LOCAL):
392  raise RuntimeError("maskedImage bbox %s != flatMaskedImage bbox %s" %
393  (maskedImage.getBBox(afwImage.LOCAL), flatMaskedImage.getBBox(afwImage.LOCAL)))
394 
395  # Figure out scale from the data
396  # Ideally the flats are normalized by the calibration product pipeline, but this allows some flexibility
397  # in the case that the flat is created by some other mechanism.
398  if scalingType in ('MEAN', 'MEDIAN'):
399  scalingType = afwMath.stringToStatisticsProperty(scalingType)
400  flatScale = afwMath.makeStatistics(flatMaskedImage.image, scalingType).getValue()
401  elif scalingType == 'USER':
402  flatScale = userScale
403  else:
404  raise RuntimeError('%s : %s not implemented' % ("flatCorrection", scalingType))
405 
406  if not invert:
407  maskedImage.scaledDivides(1.0/flatScale, flatMaskedImage)
408  else:
409  maskedImage.scaledMultiplies(1.0/flatScale, flatMaskedImage)
410 
411 
412 def illuminationCorrection(maskedImage, illumMaskedImage, illumScale, trimToFit=True):
413  """Apply illumination correction in place.
414 
415  Parameters
416  ----------
417  maskedImage : `lsst.afw.image.MaskedImage`
418  Image to process. The image is modified.
419  illumMaskedImage : `lsst.afw.image.MaskedImage`
420  Illumination correction image of the same size as ``maskedImage``.
421  illumScale : scalar
422  Scale factor for the illumination correction.
423  trimToFit : `Bool`, optional
424  If True, raw data is symmetrically trimmed to match
425  calibration size.
426 
427  Raises
428  ------
429  RuntimeError
430  Raised if ``maskedImage`` and ``illumMaskedImage`` do not have
431  the same size.
432  """
433  if trimToFit:
434  maskedImage = trimToMatchCalibBBox(maskedImage, illumMaskedImage)
435 
436  if maskedImage.getBBox(afwImage.LOCAL) != illumMaskedImage.getBBox(afwImage.LOCAL):
437  raise RuntimeError("maskedImage bbox %s != illumMaskedImage bbox %s" %
438  (maskedImage.getBBox(afwImage.LOCAL), illumMaskedImage.getBBox(afwImage.LOCAL)))
439 
440  maskedImage.scaledDivides(1.0/illumScale, illumMaskedImage)
441 
442 
443 def overscanCorrection(ampMaskedImage, overscanImage, fitType='MEDIAN', order=1, collapseRej=3.0,
444  statControl=None, overscanIsInt=True):
445  """Apply overscan correction in place.
446 
447  Parameters
448  ----------
449  ampMaskedImage : `lsst.afw.image.MaskedImage`
450  Image of amplifier to correct; modified.
451  overscanImage : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
452  Image of overscan; modified.
453  fitType : `str`
454  Type of fit for overscan correction. May be one of:
455 
456  - ``MEAN``: use mean of overscan.
457  - ``MEANCLIP``: use clipped mean of overscan.
458  - ``MEDIAN``: use median of overscan.
459  - ``MEDIAN_PER_ROW``: use median per row of overscan.
460  - ``POLY``: fit with ordinary polynomial.
461  - ``CHEB``: fit with Chebyshev polynomial.
462  - ``LEG``: fit with Legendre polynomial.
463  - ``NATURAL_SPLINE``: fit with natural spline.
464  - ``CUBIC_SPLINE``: fit with cubic spline.
465  - ``AKIMA_SPLINE``: fit with Akima spline.
466 
467  order : `int`
468  Polynomial order or number of spline knots; ignored unless
469  ``fitType`` indicates a polynomial or spline.
470  statControl : `lsst.afw.math.StatisticsControl`
471  Statistics control object. In particular, we pay attention to numSigmaClip
472  overscanIsInt : `bool`
473  Treat the overscan region as consisting of integers, even if it's been
474  converted to float. E.g. handle ties properly.
475 
476  Returns
477  -------
478  result : `lsst.pipe.base.Struct`
479  Result struct with components:
480 
481  - ``imageFit``: Value(s) removed from image (scalar or
482  `lsst.afw.image.Image`)
483  - ``overscanFit``: Value(s) removed from overscan (scalar or
484  `lsst.afw.image.Image`)
485  - ``overscanImage``: Overscan corrected overscan region
486  (`lsst.afw.image.Image`)
487  Raises
488  ------
489  pexExcept.Exception
490  Raised if ``fitType`` is not an allowed value.
491 
492  Notes
493  -----
494  The ``ampMaskedImage`` and ``overscanImage`` are modified, with the fit
495  subtracted. Note that the ``overscanImage`` should not be a subimage of
496  the ``ampMaskedImage``, to avoid being subtracted twice.
497 
498  Debug plots are available for the SPLINE fitTypes by setting the
499  `debug.display` for `name` == "lsst.ip.isr.isrFunctions". These
500  plots show the scatter plot of the overscan data (collapsed along
501  the perpendicular dimension) as a function of position on the CCD
502  (normalized between +/-1).
503  """
504  ampImage = ampMaskedImage.getImage()
505 
507  if fitType:
508  config.fitType = fitType
509  if order:
510  config.order = order
511  if collapseRej:
512  config.numSigmaClip = collapseRej
513  if overscanIsInt:
514  config.overscanIsInt = True
515 
516  overscanTask = OverscanCorrectionTask(config=config)
517  return overscanTask.run(ampImage, overscanImage)
518 
519 
520 def brighterFatterCorrection(exposure, kernel, maxIter, threshold, applyGain, gains=None):
521  """Apply brighter fatter correction in place for the image.
522 
523  Parameters
524  ----------
525  exposure : `lsst.afw.image.Exposure`
526  Exposure to have brighter-fatter correction applied. Modified
527  by this method.
528  kernel : `numpy.ndarray`
529  Brighter-fatter kernel to apply.
530  maxIter : scalar
531  Number of correction iterations to run.
532  threshold : scalar
533  Convergence threshold in terms of the sum of absolute
534  deviations between an iteration and the previous one.
535  applyGain : `Bool`
536  If True, then the exposure values are scaled by the gain prior
537  to correction.
538  gains : `dict` [`str`, `float`]
539  A dictionary, keyed by amplifier name, of the gains to use.
540  If gains is None, the nominal gains in the amplifier object are used.
541 
542  Returns
543  -------
544  diff : `float`
545  Final difference between iterations achieved in correction.
546  iteration : `int`
547  Number of iterations used to calculate correction.
548 
549  Notes
550  -----
551  This correction takes a kernel that has been derived from flat
552  field images to redistribute the charge. The gradient of the
553  kernel is the deflection field due to the accumulated charge.
554 
555  Given the original image I(x) and the kernel K(x) we can compute
556  the corrected image Ic(x) using the following equation:
557 
558  Ic(x) = I(x) + 0.5*d/dx(I(x)*d/dx(int( dy*K(x-y)*I(y))))
559 
560  To evaluate the derivative term we expand it as follows:
561 
562  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))) )
563 
564  Because we use the measured counts instead of the incident counts
565  we apply the correction iteratively to reconstruct the original
566  counts and the correction. We stop iterating when the summed
567  difference between the current corrected image and the one from
568  the previous iteration is below the threshold. We do not require
569  convergence because the number of iterations is too large a
570  computational cost. How we define the threshold still needs to be
571  evaluated, the current default was shown to work reasonably well
572  on a small set of images. For more information on the method see
573  DocuShare Document-19407.
574 
575  The edges as defined by the kernel are not corrected because they
576  have spurious values due to the convolution.
577  """
578  image = exposure.getMaskedImage().getImage()
579 
580  # The image needs to be units of electrons/holes
581  with gainContext(exposure, image, applyGain, gains):
582 
583  kLx = numpy.shape(kernel)[0]
584  kLy = numpy.shape(kernel)[1]
585  kernelImage = afwImage.ImageD(kLx, kLy)
586  kernelImage.getArray()[:, :] = kernel
587  tempImage = image.clone()
588 
589  nanIndex = numpy.isnan(tempImage.getArray())
590  tempImage.getArray()[nanIndex] = 0.
591 
592  outImage = afwImage.ImageF(image.getDimensions())
593  corr = numpy.zeros_like(image.getArray())
594  prev_image = numpy.zeros_like(image.getArray())
595  convCntrl = afwMath.ConvolutionControl(False, True, 1)
596  fixedKernel = afwMath.FixedKernel(kernelImage)
597 
598  # Define boundary by convolution region. The region that the correction will be
599  # calculated for is one fewer in each dimension because of the second derivative terms.
600  # NOTE: these need to use integer math, as we're using start:end as numpy index ranges.
601  startX = kLx//2
602  endX = -kLx//2
603  startY = kLy//2
604  endY = -kLy//2
605 
606  for iteration in range(maxIter):
607 
608  afwMath.convolve(outImage, tempImage, fixedKernel, convCntrl)
609  tmpArray = tempImage.getArray()
610  outArray = outImage.getArray()
611 
612  with numpy.errstate(invalid="ignore", over="ignore"):
613  # First derivative term
614  gradTmp = numpy.gradient(tmpArray[startY:endY, startX:endX])
615  gradOut = numpy.gradient(outArray[startY:endY, startX:endX])
616  first = (gradTmp[0]*gradOut[0] + gradTmp[1]*gradOut[1])[1:-1, 1:-1]
617 
618  # Second derivative term
619  diffOut20 = numpy.diff(outArray, 2, 0)[startY:endY, startX + 1:endX - 1]
620  diffOut21 = numpy.diff(outArray, 2, 1)[startY + 1:endY - 1, startX:endX]
621  second = tmpArray[startY + 1:endY - 1, startX + 1:endX - 1]*(diffOut20 + diffOut21)
622 
623  corr[startY + 1:endY - 1, startX + 1:endX - 1] = 0.5*(first + second)
624 
625  tmpArray[:, :] = image.getArray()[:, :]
626  tmpArray[nanIndex] = 0.
627  tmpArray[startY:endY, startX:endX] += corr[startY:endY, startX:endX]
628 
629  if iteration > 0:
630  diff = numpy.sum(numpy.abs(prev_image - tmpArray))
631 
632  if diff < threshold:
633  break
634  prev_image[:, :] = tmpArray[:, :]
635 
636  image.getArray()[startY + 1:endY - 1, startX + 1:endX - 1] += \
637  corr[startY + 1:endY - 1, startX + 1:endX - 1]
638 
639  return diff, iteration
640 
641 
642 @contextmanager
643 def gainContext(exp, image, apply, gains=None):
644  """Context manager that applies and removes gain.
645 
646  Parameters
647  ----------
648  exp : `lsst.afw.image.Exposure`
649  Exposure to apply/remove gain.
650  image : `lsst.afw.image.Image`
651  Image to apply/remove gain.
652  apply : `Bool`
653  If True, apply and remove the amplifier gain.
654  gains : `dict` [`str`, `float`]
655  A dictionary, keyed by amplifier name, of the gains to use.
656  If gains is None, the nominal gains in the amplifier object are used.
657 
658  Yields
659  ------
660  exp : `lsst.afw.image.Exposure`
661  Exposure with the gain applied.
662  """
663  # check we have all of them if provided because mixing and matching would
664  # be a real mess
665  if gains and apply is True:
666  ampNames = [amp.getName() for amp in exp.getDetector()]
667  for ampName in ampNames:
668  if ampName not in gains.keys():
669  raise RuntimeError(f"Gains provided to gain context, but no entry found for amp {ampName}")
670 
671  if apply:
672  ccd = exp.getDetector()
673  for amp in ccd:
674  sim = image.Factory(image, amp.getBBox())
675  if gains:
676  gain = gains[amp.getName()]
677  else:
678  gain = amp.getGain()
679  sim *= gain
680 
681  try:
682  yield exp
683  finally:
684  if apply:
685  ccd = exp.getDetector()
686  for amp in ccd:
687  sim = image.Factory(image, amp.getBBox())
688  if gains:
689  gain = gains[amp.getName()]
690  else:
691  gain = amp.getGain()
692  sim /= gain
693 
694 
695 def attachTransmissionCurve(exposure, opticsTransmission=None, filterTransmission=None,
696  sensorTransmission=None, atmosphereTransmission=None):
697  """Attach a TransmissionCurve to an Exposure, given separate curves for
698  different components.
699 
700  Parameters
701  ----------
702  exposure : `lsst.afw.image.Exposure`
703  Exposure object to modify by attaching the product of all given
704  ``TransmissionCurves`` in post-assembly trimmed detector coordinates.
705  Must have a valid ``Detector`` attached that matches the detector
706  associated with sensorTransmission.
707  opticsTransmission : `lsst.afw.image.TransmissionCurve`
708  A ``TransmissionCurve`` that represents the throughput of the optics,
709  to be evaluated in focal-plane coordinates.
710  filterTransmission : `lsst.afw.image.TransmissionCurve`
711  A ``TransmissionCurve`` that represents the throughput of the filter
712  itself, to be evaluated in focal-plane coordinates.
713  sensorTransmission : `lsst.afw.image.TransmissionCurve`
714  A ``TransmissionCurve`` that represents the throughput of the sensor
715  itself, to be evaluated in post-assembly trimmed detector coordinates.
716  atmosphereTransmission : `lsst.afw.image.TransmissionCurve`
717  A ``TransmissionCurve`` that represents the throughput of the
718  atmosphere, assumed to be spatially constant.
719 
720  Returns
721  -------
722  combined : `lsst.afw.image.TransmissionCurve`
723  The TransmissionCurve attached to the exposure.
724 
725  Notes
726  -----
727  All ``TransmissionCurve`` arguments are optional; if none are provided, the
728  attached ``TransmissionCurve`` will have unit transmission everywhere.
729  """
730  combined = afwImage.TransmissionCurve.makeIdentity()
731  if atmosphereTransmission is not None:
732  combined *= atmosphereTransmission
733  if opticsTransmission is not None:
734  combined *= opticsTransmission
735  if filterTransmission is not None:
736  combined *= filterTransmission
737  detector = exposure.getDetector()
738  fpToPix = detector.getTransform(fromSys=camGeom.FOCAL_PLANE,
739  toSys=camGeom.PIXELS)
740  combined = combined.transformedBy(fpToPix)
741  if sensorTransmission is not None:
742  combined *= sensorTransmission
743  exposure.getInfo().setTransmissionCurve(combined)
744  return combined
745 
746 
747 @deprecated(reason="Camera geometry-based SkyWcs are now set when reading raws. To be removed after v19.",
748  category=FutureWarning)
749 def addDistortionModel(exposure, camera):
750  """!Update the WCS in exposure with a distortion model based on camera
751  geometry.
752 
753  Parameters
754  ----------
755  exposure : `lsst.afw.image.Exposure`
756  Exposure to process. Must contain a Detector and WCS. The
757  exposure is modified.
758  camera : `lsst.afw.cameraGeom.Camera`
759  Camera geometry.
760 
761  Raises
762  ------
763  RuntimeError
764  Raised if ``exposure`` is lacking a Detector or WCS, or if
765  ``camera`` is None.
766  Notes
767  -----
768  Add a model for optical distortion based on geometry found in ``camera``
769  and the ``exposure``'s detector. The raw input exposure is assumed
770  have a TAN WCS that has no compensation for optical distortion.
771  Two other possibilities are:
772  - The raw input exposure already has a model for optical distortion,
773  as is the case for raw DECam data.
774  In that case you should set config.doAddDistortionModel False.
775  - The raw input exposure has a model for distortion, but it has known
776  deficiencies severe enough to be worth fixing (e.g. because they
777  cause problems for fitting a better WCS). In that case you should
778  override this method with a version suitable for your raw data.
779 
780  """
781  wcs = exposure.getWcs()
782  if wcs is None:
783  raise RuntimeError("exposure has no WCS")
784  if camera is None:
785  raise RuntimeError("camera is None")
786  detector = exposure.getDetector()
787  if detector is None:
788  raise RuntimeError("exposure has no Detector")
789  pixelToFocalPlane = detector.getTransform(camGeom.PIXELS, camGeom.FOCAL_PLANE)
790  focalPlaneToFieldAngle = camera.getTransformMap().getTransform(camGeom.FOCAL_PLANE,
791  camGeom.FIELD_ANGLE)
792  distortedWcs = makeDistortedTanWcs(wcs, pixelToFocalPlane, focalPlaneToFieldAngle)
793  exposure.setWcs(distortedWcs)
794 
795 
796 def applyGains(exposure, normalizeGains=False):
797  """Scale an exposure by the amplifier gains.
798 
799  Parameters
800  ----------
801  exposure : `lsst.afw.image.Exposure`
802  Exposure to process. The image is modified.
803  normalizeGains : `Bool`, optional
804  If True, then amplifiers are scaled to force the median of
805  each amplifier to equal the median of those medians.
806  """
807  ccd = exposure.getDetector()
808  ccdImage = exposure.getMaskedImage()
809 
810  medians = []
811  for amp in ccd:
812  sim = ccdImage.Factory(ccdImage, amp.getBBox())
813  sim *= amp.getGain()
814 
815  if normalizeGains:
816  medians.append(numpy.median(sim.getImage().getArray()))
817 
818  if normalizeGains:
819  median = numpy.median(numpy.array(medians))
820  for index, amp in enumerate(ccd):
821  sim = ccdImage.Factory(ccdImage, amp.getBBox())
822  if medians[index] != 0.0:
823  sim *= median/medians[index]
824 
825 
827  """Grow the saturation trails by an amount dependent on the width of the trail.
828 
829  Parameters
830  ----------
831  mask : `lsst.afw.image.Mask`
832  Mask which will have the saturated areas grown.
833  """
834 
835  extraGrowDict = {}
836  for i in range(1, 6):
837  extraGrowDict[i] = 0
838  for i in range(6, 8):
839  extraGrowDict[i] = 1
840  for i in range(8, 10):
841  extraGrowDict[i] = 3
842  extraGrowMax = 4
843 
844  if extraGrowMax <= 0:
845  return
846 
847  saturatedBit = mask.getPlaneBitMask("SAT")
848 
849  xmin, ymin = mask.getBBox().getMin()
850  width = mask.getWidth()
851 
852  thresh = afwDetection.Threshold(saturatedBit, afwDetection.Threshold.BITMASK)
853  fpList = afwDetection.FootprintSet(mask, thresh).getFootprints()
854 
855  for fp in fpList:
856  for s in fp.getSpans():
857  x0, x1 = s.getX0(), s.getX1()
858 
859  extraGrow = extraGrowDict.get(x1 - x0 + 1, extraGrowMax)
860  if extraGrow > 0:
861  y = s.getY() - ymin
862  x0 -= xmin + extraGrow
863  x1 -= xmin - extraGrow
864 
865  if x0 < 0:
866  x0 = 0
867  if x1 >= width - 1:
868  x1 = width - 1
869 
870  mask.array[y, x0:x1+1] |= saturatedBit
871 
872 
873 def setBadRegions(exposure, badStatistic="MEDIAN"):
874  """Set all BAD areas of the chip to the average of the rest of the exposure
875 
876  Parameters
877  ----------
878  exposure : `lsst.afw.image.Exposure`
879  Exposure to mask. The exposure mask is modified.
880  badStatistic : `str`, optional
881  Statistic to use to generate the replacement value from the
882  image data. Allowed values are 'MEDIAN' or 'MEANCLIP'.
883 
884  Returns
885  -------
886  badPixelCount : scalar
887  Number of bad pixels masked.
888  badPixelValue : scalar
889  Value substituted for bad pixels.
890 
891  Raises
892  ------
893  RuntimeError
894  Raised if `badStatistic` is not an allowed value.
895  """
896  if badStatistic == "MEDIAN":
897  statistic = afwMath.MEDIAN
898  elif badStatistic == "MEANCLIP":
899  statistic = afwMath.MEANCLIP
900  else:
901  raise RuntimeError("Impossible method %s of bad region correction" % badStatistic)
902 
903  mi = exposure.getMaskedImage()
904  mask = mi.getMask()
905  BAD = mask.getPlaneBitMask("BAD")
906  INTRP = mask.getPlaneBitMask("INTRP")
907 
908  sctrl = afwMath.StatisticsControl()
909  sctrl.setAndMask(BAD)
910  value = afwMath.makeStatistics(mi, statistic, sctrl).getValue()
911 
912  maskArray = mask.getArray()
913  imageArray = mi.getImage().getArray()
914  badPixels = numpy.logical_and((maskArray & BAD) > 0, (maskArray & INTRP) == 0)
915  imageArray[:] = numpy.where(badPixels, value, imageArray)
916 
917  return badPixels.sum(), value
def addDistortionModel(exposure, camera)
Update the WCS in exposure with a distortion model based on camera geometry.
def saturationCorrection(maskedImage, saturation, fwhm, growFootprints=1, interpolate=True, maskName='SAT', fallbackValue=None)
def setBadRegions(exposure, badStatistic="MEDIAN")
def interpolateFromMask(maskedImage, fwhm, growSaturatedFootprints=1, maskNameList=['SAT'], fallbackValue=None)
def interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=None)
Definition: isrFunctions.py:78
def brighterFatterCorrection(exposure, kernel, maxIter, threshold, applyGain, gains=None)
def transposeMaskedImage(maskedImage)
Definition: isrFunctions.py:58
def trimToMatchCalibBBox(rawMaskedImage, calibMaskedImage)
def attachTransmissionCurve(exposure, opticsTransmission=None, filterTransmission=None, sensorTransmission=None, atmosphereTransmission=None)
def flatCorrection(maskedImage, flatMaskedImage, scalingType, userScale=1.0, invert=False, trimToFit=False)
def makeThresholdMask(maskedImage, threshold, growFootprints=1, maskName='SAT')
def growMasks(mask, radius=0, maskNameList=['BAD'], maskValue="BAD")
def applyGains(exposure, normalizeGains=False)
def darkCorrection(maskedImage, darkMaskedImage, expScale, darkScale, invert=False, trimToFit=False)
def biasCorrection(maskedImage, biasMaskedImage, trimToFit=False)
def gainContext(exp, image, apply, gains=None)
def illuminationCorrection(maskedImage, illumMaskedImage, illumScale, trimToFit=True)
def updateVariance(maskedImage, gain, readNoise)
def overscanCorrection(ampMaskedImage, overscanImage, fitType='MEDIAN', order=1, collapseRej=3.0, statControl=None, overscanIsInt=True)