LSSTApplications  16.0-10-g0ee56ad+5,16.0-11-ga33d1f2+5,16.0-12-g3ef5c14+3,16.0-12-g71e5ef5+18,16.0-12-gbdf3636+3,16.0-13-g118c103+3,16.0-13-g8f68b0a+3,16.0-15-gbf5c1cb+4,16.0-16-gfd17674+3,16.0-17-g7c01f5c+3,16.0-18-g0a50484+1,16.0-20-ga20f992+8,16.0-21-g0e05fd4+6,16.0-21-g15e2d33+4,16.0-22-g62d8060+4,16.0-22-g847a80f+4,16.0-25-gf00d9b8+1,16.0-28-g3990c221+4,16.0-3-gf928089+3,16.0-32-g88a4f23+5,16.0-34-gd7987ad+3,16.0-37-gc7333cb+2,16.0-4-g10fc685+2,16.0-4-g18f3627+26,16.0-4-g5f3a788+26,16.0-5-gaf5c3d7+4,16.0-5-gcc1f4bb+1,16.0-6-g3b92700+4,16.0-6-g4412fcd+3,16.0-6-g7235603+4,16.0-69-g2562ce1b+2,16.0-8-g14ebd58+4,16.0-8-g2df868b+1,16.0-8-g4cec79c+6,16.0-8-gadf6c7a+1,16.0-8-gfc7ad86,16.0-82-g59ec2a54a+1,16.0-9-g5400cdc+2,16.0-9-ge6233d7+5,master-g2880f2d8cf+3,v17.0.rc1
LSSTDataManagementBasePackage
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 
25 import lsst.afw.geom as afwGeom
26 import lsst.afw.image as afwImage
27 import lsst.afw.detection as afwDetection
28 import lsst.afw.math as afwMath
29 import lsst.meas.algorithms as measAlg
30 import lsst.pex.exceptions as pexExcept
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 from lsst.pipe.base import Struct
36 
37 from contextlib import contextmanager
38 
39 
40 def createPsf(fwhm):
41  """Make a double Gaussian PSF.
42 
43  Parameters
44  ----------
45  fwhm : scalar
46  FWHM of double Gaussian smoothing kernel.
47 
48  Returns
49  -------
50  psf : `lsst.meas.algorithms.DoubleGaussianPsf`
51  The created smoothing kernel.
52  """
53  ksize = 4*int(fwhm) + 1
54  return measAlg.DoubleGaussianPsf(ksize, ksize, fwhm/(2*math.sqrt(2*math.log(2))))
55 
56 
57 def transposeMaskedImage(maskedImage):
58  """Make a transposed copy of a masked image.
59 
60  Parameters
61  ----------
62  maskedImage : `lsst.afw.image.MaskedImage`
63  Image to process.
64 
65  Returns
66  -------
67  transposed : `lsst.afw.image.MaskedImage`
68  The transposed copy of the input image.
69  """
70  transposed = maskedImage.Factory(afwGeom.Extent2I(maskedImage.getHeight(), maskedImage.getWidth()))
71  transposed.getImage().getArray()[:] = maskedImage.getImage().getArray().T
72  transposed.getMask().getArray()[:] = maskedImage.getMask().getArray().T
73  transposed.getVariance().getArray()[:] = maskedImage.getVariance().getArray().T
74  return transposed
75 
76 
77 def interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=None):
78  """Interpolate over defects specified in a defect list.
79 
80  Parameters
81  ----------
82  maskedImage : `lsst.afw.image.MaskedImage`
83  Image to process.
84  defectList : `list`
85  List of defects to interpolate over.
86  fwhm : scalar
87  FWHM of double Gaussian smoothing kernel.
88  fallbackValue : scalar, optional
89  Fallback value if an interpolated value cannot be determined.
90  If None, then the clipped mean of the image is used.
91  """
92  psf = createPsf(fwhm)
93  if fallbackValue is None:
94  fallbackValue = afwMath.makeStatistics(maskedImage.getImage(), afwMath.MEANCLIP).getValue()
95  if 'INTRP' not in maskedImage.getMask().getMaskPlaneDict():
96  maskedImage.getMask.addMaskPlane('INTRP')
97  measAlg.interpolateOverDefects(maskedImage, psf, defectList, fallbackValue, True)
98 
99 
101  """Compute a defect list from a footprint list, optionally growing the footprints.
102 
103  Parameters
104  ----------
105  fpList : `list` of `lsst.afw.detection.Footprint`
106  Footprint list to process.
107 
108  Returns
109  -------
110  defectList : `list` of `lsst.afw.meas.algorithms.Defect`
111  List of defects.
112  """
113  defectList = []
114  for fp in fpList:
115  for bbox in afwDetection.footprintToBBoxList(fp):
116  defect = measAlg.Defect(bbox)
117  defectList.append(defect)
118  return defectList
119 
120 
121 def transposeDefectList(defectList):
122  """Make a transposed copy of a defect list.
123 
124  Parameters
125  ----------
126  defectList : `list` of `lsst.afw.meas.algorithms.Defect`
127  Input list of defects.
128 
129  Returns
130  -------
131  retDefectList : `list` of `lsst.afw.meas.algorithms.Defect`
132  Transposed list of defects.
133  """
134  retDefectList = []
135  for defect in defectList:
136  bbox = defect.getBBox()
137  nbbox = afwGeom.Box2I(afwGeom.Point2I(bbox.getMinY(), bbox.getMinX()),
138  afwGeom.Extent2I(bbox.getDimensions()[1], bbox.getDimensions()[0]))
139  retDefectList.append(measAlg.Defect(nbbox))
140  return retDefectList
141 
142 
143 def maskPixelsFromDefectList(maskedImage, defectList, maskName='BAD'):
144  """Set mask plane based on a defect list.
145 
146  Parameters
147  ----------
148  maskedImage : `lsst.afw.image.MaskedImage`
149  Image to process. Only the mask plane is updated.
150  defectList : `list` of `lsst.afw.meas.algorithms.Defect`
151  Defect list to mask.
152  maskName : str, optional
153  Mask plane name to use.
154  """
155  # mask bad pixels
156  mask = maskedImage.getMask()
157  bitmask = mask.getPlaneBitMask(maskName)
158  for defect in defectList:
159  bbox = defect.getBBox()
160  afwGeom.SpanSet(bbox).clippedTo(mask.getBBox()).setMask(mask, bitmask)
161 
162 
163 def getDefectListFromMask(maskedImage, maskName):
164  """Compute a defect list from a specified mask plane.
165 
166  Parameters
167  ----------
168  maskedImage : `lsst.afw.image.MaskedImage`
169  Image to process.
170  maskName : str or `list`
171  Mask plane name, or list of names to convert.
172 
173  Returns
174  -------
175  defectList : `list` of `lsst.afw.meas.algorithms.Defect`
176  Defect list constructed from masked pixels.
177  """
178  mask = maskedImage.getMask()
179  thresh = afwDetection.Threshold(mask.getPlaneBitMask(maskName), afwDetection.Threshold.BITMASK)
180  fpList = afwDetection.FootprintSet(mask, thresh).getFootprints()
181  return defectListFromFootprintList(fpList)
182 
183 
184 def makeThresholdMask(maskedImage, threshold, growFootprints=1, maskName='SAT'):
185  """Mask pixels based on threshold detection.
186 
187  Parameters
188  ----------
189  maskedImage : `lsst.afw.image.MaskedImage`
190  Image to process. Only the mask plane is updated.
191  threshold : scalar
192  Detection threshold.
193  growFootprints : scalar, optional
194  Number of pixels to grow footprints of detected regions.
195  maskName : str, optional
196  Mask plane name, or list of names to convert
197 
198  Returns
199  -------
200  defectList : `list` of `lsst.afw.meas.algorithms.Defect`
201  Defect list constructed from pixels above the threshold.
202  """
203  # find saturated regions
204  thresh = afwDetection.Threshold(threshold)
205  fs = afwDetection.FootprintSet(maskedImage, thresh)
206 
207  if growFootprints > 0:
208  fs = afwDetection.FootprintSet(fs, growFootprints)
209  fpList = fs.getFootprints()
210 
211  # set mask
212  mask = maskedImage.getMask()
213  bitmask = mask.getPlaneBitMask(maskName)
214  afwDetection.setMaskFromFootprintList(mask, fpList, bitmask)
215 
216  return defectListFromFootprintList(fpList)
217 
218 
219 def interpolateFromMask(maskedImage, fwhm, growFootprints=1, maskName='SAT', fallbackValue=None):
220  """Interpolate over defects identified by a particular mask plane.
221 
222  Parameters
223  ----------
224  maskedImage : `lsst.afw.image.MaskedImage`
225  Image to process.
226  fwhm : scalar
227  FWHM of double Gaussian smoothing kernel.
228  growFootprints : scalar, optional
229  Number of pixels to grow footprints of detected regions.
230  maskName : str, optional
231  Mask plane name.
232  fallbackValue : scalar, optional
233  Value of last resort for interpolation.
234  """
235  mask = maskedImage.getMask()
236  thresh = afwDetection.Threshold(mask.getPlaneBitMask(maskName), afwDetection.Threshold.BITMASK)
237  fpSet = afwDetection.FootprintSet(mask, thresh)
238  if growFootprints > 0:
239  fpSet = afwDetection.FootprintSet(fpSet, rGrow=growFootprints, isotropic=False)
240  # If we are interpolating over an area larger than the original masked region, we need
241  # to expand the original mask bit to the full area to explain why we interpolated there.
242  fpSet.setMask(mask, maskName)
243  defectList = defectListFromFootprintList(fpSet.getFootprints())
244  interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=fallbackValue)
245 
246 
247 def saturationCorrection(maskedImage, saturation, fwhm, growFootprints=1, interpolate=True, maskName='SAT',
248  fallbackValue=None):
249  """Mark saturated pixels and optionally interpolate over them
250 
251  Parameters
252  ----------
253  maskedImage : `lsst.afw.image.MaskedImage`
254  Image to process.
255  saturation : scalar
256  Saturation level used as the detection threshold.
257  fwhm : scalar
258  FWHM of double Gaussian smoothing kernel.
259  growFootprints : scalar, optional
260  Number of pixels to grow footprints of detected regions.
261  interpolate : Bool, optional
262  If True, saturated pixels are interpolated over.
263  maskName : str, optional
264  Mask plane name.
265  fallbackValue : scalar, optional
266  Value of last resort for interpolation.
267  """
268  defectList = makeThresholdMask(
269  maskedImage=maskedImage,
270  threshold=saturation,
271  growFootprints=growFootprints,
272  maskName=maskName,
273  )
274  if interpolate:
275  interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=fallbackValue)
276 
277 
278 def trimToMatchCalibBBox(rawMaskedImage, calibMaskedImage):
279  """Compute number of edge trim pixels to match the calibration data.
280 
281  Use the dimension difference between the raw exposure and the
282  calibration exposure to compute the edge trim pixels. This trim
283  is applied symmetrically, with the same number of pixels masked on
284  each side.
285 
286  Parameters
287  ----------
288  rawMaskedImage : `lsst.afw.image.MaskedImage`
289  Image to trim.
290  calibMaskedImage : `lsst.afw.image.MaskedImage`
291  Calibration image to draw new bounding box from.
292 
293  Returns
294  -------
295  replacementMaskedImage : `lsst.afw.image.MaskedImage`
296  ``rawMaskedImage`` trimmed to the appropriate size
297  Raises
298  ------
299  RuntimeError
300  Rasied if ``rawMaskedImage`` cannot be symmetrically trimmed to
301  match ``calibMaskedImage``.
302  """
303  nx, ny = rawMaskedImage.getBBox().getDimensions() - calibMaskedImage.getBBox().getDimensions()
304  if nx != ny:
305  raise RuntimeError("Raw and calib maskedImages are trimmed differently in X and Y.")
306  if nx % 2 != 0:
307  raise RuntimeError("Calibration maskedImage is trimmed unevenly in X.")
308  if nx <= 0:
309  raise RuntimeError("Calibration maskedImage is larger than raw data.")
310 
311  nEdge = nx//2
312  if nEdge > 0:
313  replacementMaskedImage = rawMaskedImage[nEdge:-nEdge, nEdge:-nEdge, afwImage.LOCAL]
314  SourceDetectionTask.setEdgeBits(
315  rawMaskedImage,
316  replacementMaskedImage.getBBox(),
317  rawMaskedImage.getMask().getPlaneBitMask("EDGE")
318  )
319  else:
320  replacementMaskedImage = rawMaskedImage
321 
322  return replacementMaskedImage
323 
324 
325 def biasCorrection(maskedImage, biasMaskedImage, trimToFit=False):
326  """Apply bias correction in place.
327 
328  Parameters
329  ----------
330  maskedImage : `lsst.afw.image.MaskedImage`
331  Image to process. The image is modified by this method.
332  biasMaskedImage : `lsst.afw.image.MaskedImage`
333  Bias image of the same size as ``maskedImage``
334  trimToFit : `Bool`, optional
335  If True, raw data is symmetrically trimmed to match
336  calibration size.
337 
338  Raises
339  ------
340  RuntimeError
341  Raised if ``maskedImage`` and ``biasMaskedImage`` do not have
342  the same size.
343 
344  """
345  if trimToFit:
346  maskedImage = trimToMatchCalibBBox(maskedImage, biasMaskedImage)
347 
348  if maskedImage.getBBox(afwImage.LOCAL) != biasMaskedImage.getBBox(afwImage.LOCAL):
349  raise RuntimeError("maskedImage bbox %s != biasMaskedImage bbox %s" %
350  (maskedImage.getBBox(afwImage.LOCAL), biasMaskedImage.getBBox(afwImage.LOCAL)))
351  maskedImage -= biasMaskedImage
352 
353 
354 def darkCorrection(maskedImage, darkMaskedImage, expScale, darkScale, invert=False, trimToFit=False):
355  """Apply dark correction in place.
356 
357  Parameters
358  ----------
359  maskedImage : `lsst.afw.image.MaskedImage`
360  Image to process. The image is modified by this method.
361  darkMaskedImage : `lsst.afw.image.MaskedImage`
362  Dark image of the same size as ``maskedImage``.
363  expScale : scalar
364  Dark exposure time for ``maskedImage``.
365  darkScale : scalar
366  Dark exposure time for ``darkMaskedImage``.
367  invert : `Bool`, optional
368  If True, re-add the dark to an already corrected image.
369  trimToFit : `Bool`, optional
370  If True, raw data is symmetrically trimmed to match
371  calibration size.
372 
373  Raises
374  ------
375  RuntimeError
376  Raised if ``maskedImage`` and ``darkMaskedImage`` do not have
377  the same size.
378 
379  Notes
380  -----
381  The dark correction is applied by calculating:
382  maskedImage -= dark * expScaling / darkScaling
383  """
384  if trimToFit:
385  maskedImage = trimToMatchCalibBBox(maskedImage, darkMaskedImage)
386 
387  if maskedImage.getBBox(afwImage.LOCAL) != darkMaskedImage.getBBox(afwImage.LOCAL):
388  raise RuntimeError("maskedImage bbox %s != darkMaskedImage bbox %s" %
389  (maskedImage.getBBox(afwImage.LOCAL), darkMaskedImage.getBBox(afwImage.LOCAL)))
390 
391  scale = expScale / darkScale
392  if not invert:
393  maskedImage.scaledMinus(scale, darkMaskedImage)
394  else:
395  maskedImage.scaledPlus(scale, darkMaskedImage)
396 
397 
398 def updateVariance(maskedImage, gain, readNoise):
399  """Set the variance plane based on the image plane.
400 
401  Parameters
402  ----------
403  maskedImage : `lsst.afw.image.MaskedImage`
404  Image to process. The variance plane is modified.
405  gain : scalar
406  The amplifier gain in electrons/ADU.
407  readNoise : scalar
408  The amplifier read nmoise in ADU/pixel.
409  """
410  var = maskedImage.getVariance()
411  var[:] = maskedImage.getImage()
412  var /= gain
413  var += readNoise**2
414 
415 
416 def flatCorrection(maskedImage, flatMaskedImage, scalingType, userScale=1.0, invert=False, trimToFit=False):
417  """Apply flat correction in place.
418 
419  Parameters
420  ----------
421  maskedImage : `lsst.afw.image.MaskedImage`
422  Image to process. The image is modified.
423  flatMaskedImage : `lsst.afw.image.MaskedImage`
424  Flat image of the same size as ``maskedImage``
425  scalingType : str
426  Flat scale computation method. Allowed values are 'MEAN',
427  'MEDIAN', or 'USER'.
428  userScale : scalar, optional
429  Scale to use if ``scalingType``='USER'.
430  invert : `Bool`, optional
431  If True, unflatten an already flattened image.
432  trimToFit : `Bool`, optional
433  If True, raw data is symmetrically trimmed to match
434  calibration size.
435 
436  Raises
437  ------
438  RuntimeError
439  Raised if ``maskedImage`` and ``flatMaskedImage`` do not have
440  the same size.
441  pexExcept.Exception
442  Raised if ``scalingType`` is not an allowed value.
443  """
444  if trimToFit:
445  maskedImage = trimToMatchCalibBBox(maskedImage, flatMaskedImage)
446 
447  if maskedImage.getBBox(afwImage.LOCAL) != flatMaskedImage.getBBox(afwImage.LOCAL):
448  raise RuntimeError("maskedImage bbox %s != flatMaskedImage bbox %s" %
449  (maskedImage.getBBox(afwImage.LOCAL), flatMaskedImage.getBBox(afwImage.LOCAL)))
450 
451  # Figure out scale from the data
452  # Ideally the flats are normalized by the calibration product pipeline, but this allows some flexibility
453  # in the case that the flat is created by some other mechanism.
454  if scalingType in ('MEAN', 'MEDIAN'):
455  scalingType = afwMath.stringToStatisticsProperty(scalingType)
456  flatScale = afwMath.makeStatistics(flatMaskedImage.image, scalingType).getValue()
457  elif scalingType == 'USER':
458  flatScale = userScale
459  else:
460  raise pexExcept.Exception('%s : %s not implemented' % ("flatCorrection", scalingType))
461 
462  if not invert:
463  maskedImage.scaledDivides(1.0/flatScale, flatMaskedImage)
464  else:
465  maskedImage.scaledMultiplies(1.0/flatScale, flatMaskedImage)
466 
467 
468 def illuminationCorrection(maskedImage, illumMaskedImage, illumScale):
469  """Apply illumination correction in place.
470 
471  Parameters
472  ----------
473  maskedImage : `lsst.afw.image.MaskedImage`
474  Image to process. The image is modified.
475  illumMaskedImage : `lsst.afw.image.MaskedImage`
476  Illumination correction image of the same size as ``maskedImage``.
477  illumScale : scalar
478  Scale factor for the illumination correction.
479 
480  Raises
481  ------
482  RuntimeError
483  Raised if ``maskedImage`` and ``illumMaskedImage`` do not have
484  the same size.
485  """
486  if maskedImage.getBBox(afwImage.LOCAL) != illumMaskedImage.getBBox(afwImage.LOCAL):
487  raise RuntimeError("maskedImage bbox %s != illumMaskedImage bbox %s" %
488  (maskedImage.getBBox(afwImage.LOCAL), illumMaskedImage.getBBox(afwImage.LOCAL)))
489 
490  maskedImage.scaledDivides(1./illumScale, illumMaskedImage)
491 
492 
493 def overscanCorrection(ampMaskedImage, overscanImage, fitType='MEDIAN', order=1, collapseRej=3.0,
494  statControl=None, overscanIsInt=True):
495  """Apply overscan correction in place.
496 
497  Parameters
498  ----------
499  ampMaskedImage : `lsst.afw.image.MaskedImage`
500  Image of amplifier to correct; modified.
501  overscanImage : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
502  Image of overscan; modified.
503  fitType : `str`
504  Type of fit for overscan correction. May be one of:
505 
506  - ``MEAN``: use mean of overscan.
507  - ``MEANCLIP``: use clipped mean of overscan.
508  - ``MEDIAN``: use median of overscan.
509  - ``POLY``: fit with ordinary polynomial.
510  - ``CHEB``: fit with Chebyshev polynomial.
511  - ``LEG``: fit with Legendre polynomial.
512  - ``NATURAL_SPLINE``: fit with natural spline.
513  - ``CUBIC_SPLINE``: fit with cubic spline.
514  - ``AKIMA_SPLINE``: fit with Akima spline.
515 
516  order : `int`
517  Polynomial order or number of spline knots; ignored unless
518  ``fitType`` indicates a polynomial or spline.
519  statControl : `lsst.afw.math.StatisticsControl`
520  Statistics control object. In particular, we pay attention to numSigmaClip
521  overscanIsInt : `bool`
522  Treat the overscan region as consisting of integers, even if it's been
523  converted to float. E.g. handle ties properly.
524 
525  Returns
526  -------
527  result : `lsst.pipe.base.Struct`
528  Result struct with components:
529 
530  - ``imageFit``: Value(s) removed from image (scalar or
531  `lsst.afw.image.Image`)
532  - ``overscanFit``: Value(s) removed from overscan (scalar or
533  `lsst.afw.image.Image`)
534  - ``overscanImage``: Overscan corrected overscan region
535  (`lsst.afw.image.Image`)
536  Raises
537  ------
538  pexExcept.Exception
539  Raised if ``fitType`` is not an allowed value.
540 
541  Notes
542  -----
543  The ``ampMaskedImage`` and ``overscanImage`` are modified, with the fit
544  subtracted. Note that the ``overscanImage`` should not be a subimage of
545  the ``ampMaskedImage``, to avoid being subtracted twice.
546 
547  Debug plots are available for the SPLINE fitTypes by setting the
548  `debug.display` for `name` == "lsst.ip.isr.isrFunctions". These
549  plots show the scatter plot of the overscan data (collapsed along
550  the perpendicular dimension) as a function of position on the CCD
551  (normalized between +/-1).
552  """
553  ampImage = ampMaskedImage.getImage()
554  if statControl is None:
555  statControl = afwMath.StatisticsControl()
556 
557  numSigmaClip = statControl.getNumSigmaClip()
558 
559  if fitType in ('MEAN', 'MEANCLIP'):
560  fitType = afwMath.stringToStatisticsProperty(fitType)
561  offImage = afwMath.makeStatistics(overscanImage, fitType, statControl).getValue()
562  overscanFit = offImage
563  elif fitType in ('MEDIAN',):
564  if overscanIsInt:
565  # we need an image with integer pixels to handle ties properly
566  if hasattr(overscanImage, "image"):
567  imageI = overscanImage.image.convertI()
568  overscanImageI = afwImage.MaskedImageI(imageI, overscanImage.mask, overscanImage.variance)
569  else:
570  overscanImageI = overscanImage.convertI()
571  else:
572  overscanImageI = overscanImage
573 
574  fitType = afwMath.stringToStatisticsProperty(fitType)
575  offImage = afwMath.makeStatistics(overscanImageI, fitType, statControl).getValue()
576  overscanFit = offImage
577 
578  if overscanIsInt:
579  del overscanImageI
580  elif fitType in ('POLY', 'CHEB', 'LEG', 'NATURAL_SPLINE', 'CUBIC_SPLINE', 'AKIMA_SPLINE'):
581  if hasattr(overscanImage, "getImage"):
582  biasArray = overscanImage.getImage().getArray()
583  biasArray = numpy.ma.masked_where(overscanImage.getMask().getArray() & statControl.getAndMask(),
584  biasArray)
585  else:
586  biasArray = overscanImage.getArray()
587  # Fit along the long axis, so collapse along each short row and fit the resulting array
588  shortInd = numpy.argmin(biasArray.shape)
589  if shortInd == 0:
590  # Convert to some 'standard' representation to make things easier
591  biasArray = numpy.transpose(biasArray)
592 
593  # Do a single round of clipping to weed out CR hits and signal leaking into the overscan
594  percentiles = numpy.percentile(biasArray, [25.0, 50.0, 75.0], axis=1)
595  medianBiasArr = percentiles[1]
596  stdevBiasArr = 0.74*(percentiles[2] - percentiles[0]) # robust stdev
597  diff = numpy.abs(biasArray - medianBiasArr[:, numpy.newaxis])
598  biasMaskedArr = numpy.ma.masked_where(diff > numSigmaClip*stdevBiasArr[:, numpy.newaxis], biasArray)
599  collapsed = numpy.mean(biasMaskedArr, axis=1)
600  if collapsed.mask.sum() > 0:
601  collapsed.data[collapsed.mask] = numpy.mean(biasArray.data[collapsed.mask], axis=1)
602  del biasArray, percentiles, stdevBiasArr, diff, biasMaskedArr
603 
604  if shortInd == 0:
605  collapsed = numpy.transpose(collapsed)
606 
607  num = len(collapsed)
608  indices = 2.0*numpy.arange(num)/float(num) - 1.0
609 
610  if fitType in ('POLY', 'CHEB', 'LEG'):
611  # A numpy polynomial
612  poly = numpy.polynomial
613  fitter, evaler = {"POLY": (poly.polynomial.polyfit, poly.polynomial.polyval),
614  "CHEB": (poly.chebyshev.chebfit, poly.chebyshev.chebval),
615  "LEG": (poly.legendre.legfit, poly.legendre.legval),
616  }[fitType]
617 
618  coeffs = fitter(indices, collapsed, order)
619  fitBiasArr = evaler(indices, coeffs)
620  elif 'SPLINE' in fitType:
621  # An afw interpolation
622  numBins = order
623  #
624  # numpy.histogram needs a real array for the mask, but numpy.ma "optimises" the case
625  # no-values-are-masked by replacing the mask array by a scalar, numpy.ma.nomask
626  #
627  # Issue DM-415
628  #
629  collapsedMask = collapsed.mask
630  try:
631  if collapsedMask == numpy.ma.nomask:
632  collapsedMask = numpy.array(len(collapsed)*[numpy.ma.nomask])
633  except ValueError: # If collapsedMask is an array the test fails [needs .all()]
634  pass
635 
636  numPerBin, binEdges = numpy.histogram(indices, bins=numBins,
637  weights=1-collapsedMask.astype(int))
638  # Binning is just a histogram, with weights equal to the values.
639  # Use a similar trick to get the bin centers (this deals with different numbers per bin).
640  with numpy.errstate(invalid="ignore"): # suppress NAN warnings
641  values = numpy.histogram(indices, bins=numBins,
642  weights=collapsed.data*~collapsedMask)[0]/numPerBin
643  binCenters = numpy.histogram(indices, bins=numBins,
644  weights=indices*~collapsedMask)[0]/numPerBin
645  interp = afwMath.makeInterpolate(binCenters.astype(float)[numPerBin > 0],
646  values.astype(float)[numPerBin > 0],
648  fitBiasArr = numpy.array([interp.interpolate(i) for i in indices])
649 
650  import lsstDebug
651  if lsstDebug.Info(__name__).display:
652  import matplotlib.pyplot as plot
653  figure = plot.figure(1)
654  figure.clear()
655  axes = figure.add_axes((0.1, 0.1, 0.8, 0.8))
656  axes.plot(indices[~collapsedMask], collapsed[~collapsedMask], 'k+')
657  if collapsedMask.sum() > 0:
658  axes.plot(indices[collapsedMask], collapsed.data[collapsedMask], 'b+')
659  axes.plot(indices, fitBiasArr, 'r-')
660  plot.xlabel("centered/scaled position along overscan region")
661  plot.ylabel("pixel value/fit value")
662  figure.show()
663  prompt = "Press Enter or c to continue [chp]... "
664  while True:
665  ans = input(prompt).lower()
666  if ans in ("", "c",):
667  break
668  if ans in ("p",):
669  import pdb
670  pdb.set_trace()
671  elif ans in ("h", ):
672  print("h[elp] c[ontinue] p[db]")
673  plot.close()
674 
675  offImage = ampImage.Factory(ampImage.getDimensions())
676  offArray = offImage.getArray()
677  overscanFit = afwImage.ImageF(overscanImage.getDimensions())
678  overscanArray = overscanFit.getArray()
679  if shortInd == 1:
680  offArray[:, :] = fitBiasArr[:, numpy.newaxis]
681  overscanArray[:, :] = fitBiasArr[:, numpy.newaxis]
682  else:
683  offArray[:, :] = fitBiasArr[numpy.newaxis, :]
684  overscanArray[:, :] = fitBiasArr[numpy.newaxis, :]
685 
686  # We don't trust any extrapolation: mask those pixels as SUSPECT
687  # This will occur when the top and or bottom edges of the overscan
688  # contain saturated values. The values will be extrapolated from
689  # the surrounding pixels, but we cannot entirely trust the value of
690  # the extrapolation, and will mark the image mask plane to flag the
691  # image as such.
692  mask = ampMaskedImage.getMask()
693  maskArray = mask.getArray() if shortInd == 1 else mask.getArray().transpose()
694  suspect = mask.getPlaneBitMask("SUSPECT")
695  try:
696  if collapsed.mask == numpy.ma.nomask:
697  # There is no mask, so the whole array is fine
698  pass
699  except ValueError: # If collapsed.mask is an array the test fails [needs .all()]
700  for low in range(num):
701  if not collapsed.mask[low]:
702  break
703  if low > 0:
704  maskArray[:low, :] |= suspect
705  for high in range(1, num):
706  if not collapsed.mask[-high]:
707  break
708  if high > 1:
709  maskArray[-high:, :] |= suspect
710 
711  else:
712  raise pexExcept.Exception('%s : %s an invalid overscan type' % ("overscanCorrection", fitType))
713  ampImage -= offImage
714  overscanImage -= overscanFit
715  return Struct(imageFit=offImage, overscanFit=overscanFit, overscanImage=overscanImage)
716 
717 
718 def brighterFatterCorrection(exposure, kernel, maxIter, threshold, applyGain):
719  """Apply brighter fatter correction in place for the image.
720 
721  Parameters
722  ----------
723  exposure : `lsst.afw.image.Exposure`
724  Exposure to have brighter-fatter correction applied. Modified
725  by this method.
726  kernel : `numpy.ndarray`
727  Brighter-fatter kernel to apply.
728  maxIter : scalar
729  Number of correction iterations to run.
730  threshold : scalar
731  Convergence threshold in terms of the sum of absolute
732  deviations between an iteration and the previous one.
733  applyGain : `Bool`
734  If True, then the exposure values are scaled by the gain prior
735  to correction.
736 
737  Notes
738  -----
739  This correction takes a kernel that has been derived from flat
740  field images to redistribute the charge. The gradient of the
741  kernel is the deflection field due to the accumulated charge.
742 
743  Given the original image I(x) and the kernel K(x) we can compute
744  the corrected image Ic(x) using the following equation:
745 
746  Ic(x) = I(x) + 0.5*d/dx(I(x)*d/dx(int( dy*K(x-y)*I(y))))
747 
748  To evaluate the derivative term we expand it as follows:
749 
750  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))) )
751 
752  Because we use the measured counts instead of the incident counts
753  we apply the correction iteratively to reconstruct the original
754  counts and the correction. We stop iterating when the summed
755  difference between the current corrected image and the one from
756  the previous iteration is below the threshold. We do not require
757  convergence because the number of iterations is too large a
758  computational cost. How we define the threshold still needs to be
759  evaluated, the current default was shown to work reasonably well
760  on a small set of images. For more information on the method see
761  DocuShare Document-19407.
762 
763  The edges as defined by the kernel are not corrected because they
764  have spurious values due to the convolution.
765  """
766  image = exposure.getMaskedImage().getImage()
767 
768  # The image needs to be units of electrons/holes
769  with gainContext(exposure, image, applyGain):
770 
771  kLx = numpy.shape(kernel)[0]
772  kLy = numpy.shape(kernel)[1]
773  kernelImage = afwImage.ImageD(kLx, kLy)
774  kernelImage.getArray()[:, :] = kernel
775  tempImage = image.clone()
776 
777  nanIndex = numpy.isnan(tempImage.getArray())
778  tempImage.getArray()[nanIndex] = 0.
779 
780  outImage = afwImage.ImageF(image.getDimensions())
781  corr = numpy.zeros_like(image.getArray())
782  prev_image = numpy.zeros_like(image.getArray())
783  convCntrl = afwMath.ConvolutionControl(False, True, 1)
784  fixedKernel = afwMath.FixedKernel(kernelImage)
785 
786  # Define boundary by convolution region. The region that the correction will be
787  # calculated for is one fewer in each dimension because of the second derivative terms.
788  # NOTE: these need to use integer math, as we're using start:end as numpy index ranges.
789  startX = kLx//2
790  endX = -kLx//2
791  startY = kLy//2
792  endY = -kLy//2
793 
794  for iteration in range(maxIter):
795 
796  afwMath.convolve(outImage, tempImage, fixedKernel, convCntrl)
797  tmpArray = tempImage.getArray()
798  outArray = outImage.getArray()
799 
800  with numpy.errstate(invalid="ignore", over="ignore"):
801  # First derivative term
802  gradTmp = numpy.gradient(tmpArray[startY:endY, startX:endX])
803  gradOut = numpy.gradient(outArray[startY:endY, startX:endX])
804  first = (gradTmp[0]*gradOut[0] + gradTmp[1]*gradOut[1])[1:-1, 1:-1]
805 
806  # Second derivative term
807  diffOut20 = numpy.diff(outArray, 2, 0)[startY:endY, startX + 1:endX - 1]
808  diffOut21 = numpy.diff(outArray, 2, 1)[startY + 1:endY - 1, startX:endX]
809  second = tmpArray[startY + 1:endY - 1, startX + 1:endX - 1]*(diffOut20 + diffOut21)
810 
811  corr[startY + 1:endY - 1, startX + 1:endX - 1] = 0.5*(first + second)
812 
813  tmpArray[:, :] = image.getArray()[:, :]
814  tmpArray[nanIndex] = 0.
815  tmpArray[startY:endY, startX:endX] += corr[startY:endY, startX:endX]
816 
817  if iteration > 0:
818  diff = numpy.sum(numpy.abs(prev_image - tmpArray))
819 
820  if diff < threshold:
821  break
822  prev_image[:, :] = tmpArray[:, :]
823 
824  # if iteration == maxIter - 1:
825  # self.log.warn("Brighter fatter correction did not converge,
826  # final difference %f" % diff)
827 
828  # self.log.info("Finished brighter fatter in %d iterations" % (iteration + 1))
829  image.getArray()[startY + 1:endY - 1, startX + 1:endX - 1] += \
830  corr[startY + 1:endY - 1, startX + 1:endX - 1]
831 
832 
833 @contextmanager
834 def gainContext(exp, image, apply):
835  """Context manager that applies and removes gain.
836 
837  Parameters
838  ----------
839  exp : `lsst.afw.image.Exposure`
840  Exposure to apply/remove gain.
841  image : `lsst.afw.image.Image`
842  Image to apply/remove gain.
843  apply : `Bool`
844  If True, apply and remove the amplifier gain.
845 
846  Yields
847  ------
848  exp : `lsst.afw.image.Exposure`
849  Exposure with the gain applied.
850  """
851  if apply:
852  ccd = exp.getDetector()
853  for amp in ccd:
854  sim = image.Factory(image, amp.getBBox())
855  sim *= amp.getGain()
856 
857  try:
858  yield exp
859  finally:
860  if apply:
861  ccd = exp.getDetector()
862  for amp in ccd:
863  sim = image.Factory(image, amp.getBBox())
864  sim /= amp.getGain()
865 
866 
867 def attachTransmissionCurve(exposure, opticsTransmission=None, filterTransmission=None,
868  sensorTransmission=None, atmosphereTransmission=None):
869  """Attach a TransmissionCurve to an Exposure, given separate curves for
870  different components.
871 
872  Parameters
873  ----------
874  exposure : `lsst.afw.image.Exposure`
875  Exposure object to modify by attaching the product of all given
876  ``TransmissionCurves`` in post-assembly trimmed detector coordinates.
877  Must have a valid ``Detector`` attached that matches the detector
878  associated with sensorTransmission.
879  opticsTransmission : `lsst.afw.image.TransmissionCurve`
880  A ``TransmissionCurve`` that represents the throughput of the optics,
881  to be evaluated in focal-plane coordinates.
882  filterTransmission : `lsst.afw.image.TransmissionCurve`
883  A ``TransmissionCurve`` that represents the throughput of the filter
884  itself, to be evaluated in focal-plane coordinates.
885  sensorTransmission : `lsst.afw.image.TransmissionCurve`
886  A ``TransmissionCurve`` that represents the throughput of the sensor
887  itself, to be evaluated in post-assembly trimmed detector coordinates.
888  atmosphereTransmission : `lsst.afw.image.TransmissionCurve`
889  A ``TransmissionCurve`` that represents the throughput of the
890  atmosphere, assumed to be spatially constant.
891 
892  Returns
893  -------
894  combined : `lsst.afw.image.TransmissionCurve`
895  The TransmissionCurve attached to the exposure.
896 
897  Notes
898  -----
899  All ``TransmissionCurve`` arguments are optional; if none are provided, the
900  attached ``TransmissionCurve`` will have unit transmission everywhere.
901  """
902  combined = afwImage.TransmissionCurve.makeIdentity()
903  if atmosphereTransmission is not None:
904  combined *= atmosphereTransmission
905  if opticsTransmission is not None:
906  combined *= opticsTransmission
907  if filterTransmission is not None:
908  combined *= filterTransmission
909  detector = exposure.getDetector()
910  fpToPix = detector.getTransform(fromSys=camGeom.FOCAL_PLANE,
911  toSys=camGeom.PIXELS)
912  combined = combined.transformedBy(fpToPix)
913  if sensorTransmission is not None:
914  combined *= sensorTransmission
915  exposure.getInfo().setTransmissionCurve(combined)
916  return combined
917 
918 
919 def addDistortionModel(exposure, camera):
920  """!Update the WCS in exposure with a distortion model based on camera
921  geometry.
922 
923  Parameters
924  ----------
925  exposure : `lsst.afw.image.Exposure`
926  Exposure to process. Must contain a Detector and WCS. The
927  exposure is modified.
928  camera : `lsst.afw.cameraGeom.Camera`
929  Camera geometry.
930 
931  Raises
932  ------
933  RuntimeError
934  Raised if ``exposure`` is lacking a Detector or WCS, or if
935  ``camera`` is None.
936  Notes
937  -----
938  Add a model for optical distortion based on geometry found in ``camera``
939  and the ``exposure``'s detector. The raw input exposure is assumed
940  have a TAN WCS that has no compensation for optical distortion.
941  Two other possibilities are:
942  - The raw input exposure already has a model for optical distortion,
943  as is the case for raw DECam data.
944  In that case you should set config.doAddDistortionModel False.
945  - The raw input exposure has a model for distortion, but it has known
946  deficiencies severe enough to be worth fixing (e.g. because they
947  cause problems for fitting a better WCS). In that case you should
948  override this method with a version suitable for your raw data.
949 
950  """
951  wcs = exposure.getWcs()
952  if wcs is None:
953  raise RuntimeError("exposure has no WCS")
954  if camera is None:
955  raise RuntimeError("camera is None")
956  detector = exposure.getDetector()
957  if detector is None:
958  raise RuntimeError("exposure has no Detector")
959  pixelToFocalPlane = detector.getTransform(camGeom.PIXELS, camGeom.FOCAL_PLANE)
960  focalPlaneToFieldAngle = camera.getTransformMap().getTransform(camGeom.FOCAL_PLANE,
961  camGeom.FIELD_ANGLE)
962  distortedWcs = makeDistortedTanWcs(wcs, pixelToFocalPlane, focalPlaneToFieldAngle)
963  exposure.setWcs(distortedWcs)
964 
965 
966 def applyGains(exposure, normalizeGains=False):
967  """Scale an exposure by the amplifier gains.
968 
969  Parameters
970  ----------
971  exposure : `lsst.afw.image.Exposure`
972  Exposure to process. The image is modified.
973  normalizeGains : `Bool`, optional
974  If True, then amplifiers are scaled to force the median of
975  each amplifier to equal the median of those medians.
976  """
977  ccd = exposure.getDetector()
978  ccdImage = exposure.getMaskedImage()
979 
980  medians = []
981  for amp in ccd:
982  sim = ccdImage.Factory(ccdImage, amp.getBBox())
983  sim *= amp.getGain()
984 
985  if normalizeGains:
986  medians.append(numpy.median(sim.getImage().getArray()))
987 
988  if normalizeGains:
989  median = numpy.median(numpy.array(medians))
990  for index, amp in enumerate(ccd):
991  sim = ccdImage.Factory(ccdImage, amp.getDataSec())
992  sim *= median/medians[index]
993 
994 
996  """Grow the saturation trails by an amount dependent on the width of the trail.
997 
998  Parameters
999  ----------
1000  mask : `lsst.afw.image.Mask`
1001  Mask which will have the saturated areas grown.
1002  """
1003 
1004  extraGrowDict = {}
1005  for i in range(1, 6):
1006  extraGrowDict[i] = 0
1007  for i in range(6, 8):
1008  extraGrowDict[i] = 1
1009  for i in range(8, 10):
1010  extraGrowDict[i] = 3
1011  extraGrowMax = 4
1012 
1013  if extraGrowMax <= 0:
1014  return
1015 
1016  saturatedBit = mask.getPlaneBitMask("SAT")
1017 
1018  xmin, ymin = mask.getBBox().getMin()
1019  width = mask.getWidth()
1020 
1021  thresh = afwDetection.Threshold(saturatedBit, afwDetection.Threshold.BITMASK)
1022  fpList = afwDetection.FootprintSet(mask, thresh).getFootprints()
1023 
1024  for fp in fpList:
1025  for s in fp.getSpans():
1026  x0, x1 = s.getX0(), s.getX1()
1027 
1028  extraGrow = extraGrowDict.get(x1 - x0 + 1, extraGrowMax)
1029  if extraGrow > 0:
1030  y = s.getY() - ymin
1031  x0 -= xmin + extraGrow
1032  x1 -= xmin - extraGrow
1033 
1034  if x0 < 0:
1035  x0 = 0
1036  if x1 >= width - 1:
1037  x1 = width - 1
1038 
1039  mask.array[y, x0:x1+1] |= saturatedBit
1040 
1041 
1042 def setBadRegions(exposure, badStatistic="MEDIAN"):
1043  """Set all BAD areas of the chip to the average of the rest of the exposure
1044 
1045  Parameters
1046  ----------
1047  exposure : `lsst.afw.image.Exposure`
1048  Exposure to mask. The exposure mask is modified.
1049  badStatistic : `str`, optional
1050  Statistic to use to generate the replacement value from the
1051  image data. Allowed values are 'MEDIAN' or 'MEANCLIP'.
1052 
1053  Returns
1054  -------
1055  badPixelCount : scalar
1056  Number of bad pixels masked.
1057  badPixelValue : scalar
1058  Value substituted for bad pixels.
1059 
1060  Raises
1061  ------
1062  RuntimeError
1063  Raised if `badStatistic` is not an allowed value.
1064  """
1065  if badStatistic == "MEDIAN":
1066  statistic = afwMath.MEDIAN
1067  elif badStatistic == "MEANCLIP":
1068  statistic = afwMath.MEANCLIP
1069  else:
1070  raise RuntimeError("Impossible method %s of bad region correction" % badStatistic)
1071 
1072  mi = exposure.getMaskedImage()
1073  mask = mi.getMask()
1074  BAD = mask.getPlaneBitMask("BAD")
1075  INTRP = mask.getPlaneBitMask("INTRP")
1076 
1077  sctrl = afwMath.StatisticsControl()
1078  sctrl.setAndMask(BAD)
1079  value = afwMath.makeStatistics(mi, statistic, sctrl).getValue()
1080 
1081  maskArray = mask.getArray()
1082  imageArray = mi.getImage().getArray()
1083  badPixels = numpy.logical_and((maskArray & BAD) > 0, (maskArray & INTRP) == 0)
1084  imageArray[:] = numpy.where(badPixels, value, imageArray)
1085 
1086  return badPixels.sum(), value
def illuminationCorrection(maskedImage, illumMaskedImage, illumScale)
Interpolate::Style stringToInterpStyle(std::string const &style)
Conversion function to switch a string to an Interpolate::Style.
Definition: Interpolate.cc:257
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 brighterFatterCorrection(exposure, kernel, maxIter, threshold, applyGain)
def transposeDefectList(defectList)
A compact representation of a collection of pixels.
Definition: SpanSet.h:77
Parameters to control convolution.
Definition: ConvolveImage.h:50
def getDefectListFromMask(maskedImage, maskName)
def interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=None)
Definition: isrFunctions.py:77
def defectListFromFootprintList(fpList)
Provides consistent interface for LSST exceptions.
Definition: Exception.h:107
def transposeMaskedImage(maskedImage)
Definition: isrFunctions.py:57
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations...
def interpolateFromMask(maskedImage, fwhm, growFootprints=1, maskName='SAT', fallbackValue=None)
def trimToMatchCalibBBox(rawMaskedImage, calibMaskedImage)
Statistics makeStatistics(lsst::afw::math::MaskedVector< EntryT > const &mv, std::vector< WeightPixel > const &vweights, int const flags, StatisticsControl const &sctrl=StatisticsControl())
The makeStatistics() overload to handle lsst::afw::math::MaskedVector<>
Definition: Statistics.h:520
def attachTransmissionCurve(exposure, opticsTransmission=None, filterTransmission=None, sensorTransmission=None, atmosphereTransmission=None)
Property stringToStatisticsProperty(std::string const property)
Conversion function to switch a string to a Property (see Statistics.h)
Definition: Statistics.cc:747
Pass parameters to a Statistics object.
Definition: Statistics.h:93
def flatCorrection(maskedImage, flatMaskedImage, scalingType, userScale=1.0, invert=False, trimToFit=False)
def makeThresholdMask(maskedImage, threshold, growFootprints=1, maskName='SAT')
std::shared_ptr< Interpolate > makeInterpolate(ndarray::Array< double const, 1 > const &x, ndarray::Array< double const, 1 > const &y, Interpolate::Style const style=Interpolate::AKIMA_SPLINE)
Definition: Interpolate.cc:353
def applyGains(exposure, normalizeGains=False)
def darkCorrection(maskedImage, darkMaskedImage, expScale, darkScale, invert=False, trimToFit=False)
def makeDistortedTanWcs(tanWcs, pixelToFocalPlane, focalPlaneToFieldAngle)
def biasCorrection(maskedImage, biasMaskedImage, trimToFit=False)
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, bool doNormalize, bool doCopyEdge=false)
Old, deprecated version of convolve.
def gainContext(exp, image, apply)
def updateVariance(maskedImage, gain, readNoise)
An integer coordinate rectangle.
Definition: Box.h:54
def overscanCorrection(ampMaskedImage, overscanImage, fitType='MEDIAN', order=1, collapseRej=3.0, statControl=None, overscanIsInt=True)
A kernel created from an Image.
Definition: Kernel.h:509
def maskPixelsFromDefectList(maskedImage, defectList, maskName='BAD')