LSST Applications 29.0.1,g0fba68d861+132dd21e0a,g107a963962+1bb9f809a9,g1fd858c14a+005be21cae,g21d47ad084+8a07b29876,g325378336f+5d73323c8f,g330003fc43+40b4eaffc6,g35bb328faa+fcb1d3bbc8,g36ff55ed5b+9c28a42a87,g4e0f332c67+5fbd1e3e73,g53246c7159+fcb1d3bbc8,g60b5630c4e+9c28a42a87,g67b6fd64d1+a38b34ea13,g78460c75b0+2f9a1b4bcd,g786e29fd12+cf7ec2a62a,g7b71ed6315+fcb1d3bbc8,g86c591e316+6b2b2d0295,g8852436030+bf14db0e33,g89139ef638+a38b34ea13,g8b8da53e10+e3777245af,g9125e01d80+fcb1d3bbc8,g989de1cb63+a38b34ea13,g9f1445be69+9c28a42a87,g9f33ca652e+52c8f07962,ga9baa6287d+9c28a42a87,ga9e4eb89a6+9f84bd6575,gabe3b4be73+1e0a283bba,gb037a4e798+f3cbcd26c0,gb1101e3267+e7be8da0f8,gb58c049af0+f03b321e39,gb89ab40317+a38b34ea13,gcf25f946ba+bf14db0e33,gd6cbbdb0b4+bce7f7457e,gd9a9a58781+fcb1d3bbc8,gde0f65d7ad+53d424b1ae,ge278dab8ac+222406d50a,ge410e46f29+a38b34ea13,ge80e9994a3+664d6357dc,gf67bdafdda+a38b34ea13
LSST Data Management Base Package
Loading...
Searching...
No Matches
isrFunctions.py
Go to the documentation of this file.
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
23__all__ = [
24 "applyGains",
25 "attachTransmissionCurve",
26 "biasCorrection",
27 "brighterFatterCorrection",
28 "checkFilter",
29 "countMaskedPixels",
30 "createPsf",
31 "darkCorrection",
32 "flatCorrection",
33 "fluxConservingBrighterFatterCorrection",
34 "gainContext",
35 "getPhysicalFilter",
36 "growMasks",
37 "maskITLEdgeBleed",
38 "maskITLSatSag",
39 "maskITLDip",
40 "illuminationCorrection",
41 "interpolateDefectList",
42 "interpolateFromMask",
43 "makeThresholdMask",
44 "saturationCorrection",
45 "setBadRegions",
46 "transferFlux",
47 "transposeMaskedImage",
48 "trimToMatchCalibBBox",
49 "updateVariance",
50 "widenSaturationTrails",
51 "getExposureGains",
52 "getExposureReadNoises",
53]
54
55import logging
56import math
57import numpy
58
59import lsst.geom
60import lsst.afw.image as afwImage
61import lsst.afw.detection as afwDetection
62import lsst.afw.math as afwMath
63import lsst.meas.algorithms as measAlg
64import lsst.afw.cameraGeom as camGeom
65
66from lsst.meas.algorithms.detection import SourceDetectionTask
67
68from contextlib import contextmanager
69
70from .defects import Defects
71
72
73def createPsf(fwhm):
74 """Make a double Gaussian PSF.
75
76 Parameters
77 ----------
78 fwhm : scalar
79 FWHM of double Gaussian smoothing kernel.
80
81 Returns
82 -------
83 psf : `lsst.meas.algorithms.DoubleGaussianPsf`
84 The created smoothing kernel.
85 """
86 ksize = 4*int(fwhm) + 1
87 return measAlg.DoubleGaussianPsf(ksize, ksize, fwhm/(2*math.sqrt(2*math.log(2))))
88
89
90def transposeMaskedImage(maskedImage):
91 """Make a transposed copy of a masked image.
92
93 Parameters
94 ----------
95 maskedImage : `lsst.afw.image.MaskedImage`
96 Image to process.
97
98 Returns
99 -------
100 transposed : `lsst.afw.image.MaskedImage`
101 The transposed copy of the input image.
102 """
103 transposed = maskedImage.Factory(lsst.geom.Extent2I(maskedImage.getHeight(), maskedImage.getWidth()))
104 transposed.getImage().getArray()[:] = maskedImage.getImage().getArray().T
105 transposed.getMask().getArray()[:] = maskedImage.getMask().getArray().T
106 transposed.getVariance().getArray()[:] = maskedImage.getVariance().getArray().T
107 return transposed
108
109
110def interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=None,
111 maskNameList=None, useLegacyInterp=True):
112 """Interpolate over defects specified in a defect list.
113
114 Parameters
115 ----------
116 maskedImage : `lsst.afw.image.MaskedImage`
117 Image to process.
118 defectList : `lsst.meas.algorithms.Defects`
119 List of defects to interpolate over.
120 fwhm : `float`
121 FWHM of double Gaussian smoothing kernel.
122 fallbackValue : scalar, optional
123 Fallback value if an interpolated value cannot be determined.
124 If None, then the clipped mean of the image is used.
125 maskNameList : `list [string]`
126 List of the defects to interpolate over (used for GP interpolator).
127 useLegacyInterp : `bool`
128 Use the legacy interpolation (polynomial interpolation) if True. Use
129 Gaussian Process interpolation if False.
130
131 Notes
132 -----
133 The ``fwhm`` parameter is used to create a PSF, but the underlying
134 interpolation code (`lsst.meas.algorithms.interpolateOverDefects`) does
135 not currently make use of this information in legacy Interpolation, but use
136 if for the Gaussian Process as an estimation of the correlation lenght.
137 """
138 psf = createPsf(fwhm)
139 if fallbackValue is None:
140 fallbackValue = afwMath.makeStatistics(maskedImage.getImage(), afwMath.MEANCLIP).getValue()
141 if 'INTRP' not in maskedImage.getMask().getMaskPlaneDict():
142 maskedImage.getMask().addMaskPlane('INTRP')
143
144 # Hardcoded fwhm value. PSF estimated latter in step1,
145 # not in ISR.
146 if useLegacyInterp:
147 kwargs = {}
148 fwhm = fwhm
149 else:
150 # tested on a dozens of images and looks a good set of
151 # hyperparameters, but cannot guarrenty this is optimal,
152 # need further testing.
153 kwargs = {"bin_spacing": 20,
154 "threshold_dynamic_binning": 2000,
155 "threshold_subdivide": 20000}
156 fwhm = 15
157
158 measAlg.interpolateOverDefects(maskedImage, psf, defectList,
159 fallbackValue=fallbackValue,
160 useFallbackValueAtEdge=True,
161 fwhm=fwhm,
162 useLegacyInterp=useLegacyInterp,
163 maskNameList=maskNameList, **kwargs)
164 return maskedImage
165
166
167def makeThresholdMask(maskedImage, threshold, growFootprints=1, maskName='SAT'):
168 """Mask pixels based on threshold detection.
169
170 Parameters
171 ----------
172 maskedImage : `lsst.afw.image.MaskedImage`
173 Image to process. Only the mask plane is updated.
174 threshold : scalar
175 Detection threshold.
176 growFootprints : scalar, optional
177 Number of pixels to grow footprints of detected regions.
178 maskName : str, optional
179 Mask plane name, or list of names to convert
180
181 Returns
182 -------
183 defectList : `lsst.meas.algorithms.Defects`
184 Defect list constructed from pixels above the threshold.
185 """
186 # find saturated regions
187 thresh = afwDetection.Threshold(threshold)
188 fs = afwDetection.FootprintSet(maskedImage, thresh)
189
190 if growFootprints > 0:
191 fs = afwDetection.FootprintSet(fs, rGrow=growFootprints, isotropic=False)
192 fpList = fs.getFootprints()
193
194 # set mask
195 mask = maskedImage.getMask()
196 bitmask = mask.getPlaneBitMask(maskName)
197 afwDetection.setMaskFromFootprintList(mask, fpList, bitmask)
198
199 return Defects.fromFootprintList(fpList)
200
201
202def growMasks(mask, radius=0, maskNameList=['BAD'], maskValue="BAD"):
203 """Grow a mask by an amount and add to the requested plane.
204
205 Parameters
206 ----------
207 mask : `lsst.afw.image.Mask`
208 Mask image to process.
209 radius : scalar
210 Amount to grow the mask.
211 maskNameList : `str` or `list` [`str`]
212 Mask names that should be grown.
213 maskValue : `str`
214 Mask plane to assign the newly masked pixels to.
215 """
216 if radius > 0:
217 thresh = afwDetection.Threshold(mask.getPlaneBitMask(maskNameList), afwDetection.Threshold.BITMASK)
218 fpSet = afwDetection.FootprintSet(mask, thresh)
219 fpSet = afwDetection.FootprintSet(fpSet, rGrow=radius, isotropic=False)
220 fpSet.setMask(mask, maskValue)
221
222
223def maskITLEdgeBleed(ccdExposure, badAmpDict,
224 fpCore, itlEdgeBleedSatMinArea=10000,
225 itlEdgeBleedSatMaxArea=100000,
226 itlEdgeBleedThreshold=5000.,
227 itlEdgeBleedModelConstant=0.02,
228 saturatedMaskName="SAT", log=None):
229 """Mask edge bleeds in ITL detectors.
230
231 Parameters
232 ----------
233 ccdExposure : `lsst.afw.image.Exposure`
234 Exposure to apply masking to.
235 badAmpDict : `dict` [`str`, `bool`]
236 Dictionary of amplifiers, keyed by name, value is True if
237 amplifier is fully masked.
238 fpCore : `lsst.afw.detection._detection.Footprint`
239 Footprint of saturated core.
240 itlEdgeBleedThreshold : `float`, optional
241 Threshold above median sky background for edge bleed detection
242 (electron units).
243 itlEdgeBleedModelConstant : `float`, optional
244 Constant in the decaying exponential in the edge bleed masking.
245 saturatedMaskName : `str`, optional
246 Mask name for saturation.
247 log : `logging.Logger`, optional
248 Logger to handle messages.
249 """
250
251 log = log if log else logging.getLogger(__name__)
252
253 # Get median of amplifier saturation level
254 satLevel = numpy.nanmedian([ccdExposure.metadata[f"LSST ISR SATURATION LEVEL {amp.getName()}"]
255 for amp in ccdExposure.getDetector() if not badAmpDict[amp.getName()]])
256
257 # 1. we check if there are several cores in the footprint:
258 # Get centroid of saturated core
259 xCore, yCore = fpCore.getCentroid()
260 # Turn the Y detector coordinate into Y footprint coordinate
261 yCoreFP = int(yCore) - fpCore.getBBox().getMinY()
262 # Now test if there is one or more cores by checking if the slice at the
263 # center is full of saturated pixels or has several segments of saturated
264 # columns (i.e. several cores with trails)
265 checkCoreNbRow = fpCore.getSpans().asArray()[yCoreFP, :]
266 nbCore = 0
267 indexSwitchTrue = []
268 indexSwitchFalse = []
269 if checkCoreNbRow[0]:
270 # If the slice starts with saturated pixels
271 inSatSegment = True
272 nbCore = 1
273 indexSwitchTrue.append(0)
274 else:
275 # If the slice starts with non saturated pixels
276 inSatSegment = False
277
278 for i, value in enumerate(checkCoreNbRow):
279 if value:
280 if not inSatSegment:
281 indexSwitchTrue.append(i)
282 # nbCore is the number of detected cores.
283 nbCore += 1
284 inSatSegment = True
285 elif inSatSegment:
286 indexSwitchFalse.append(i)
287 inSatSegment = False
288
289 # 1. we look for edge bleed in saturated cores in the footprint
290 if nbCore == 2:
291 # we now estimate the x coordinates of the edges of the subfootprint
292 # for each core
293 xEdgesCores = [0]
294 xEdgesCores.append(int((indexSwitchTrue[1] + indexSwitchFalse[0])/2))
295 xEdgesCores.append(fpCore.getSpans().asArray().shape[1])
296 # Get the X and Y footprint coordinates of the cores
297 for i in range(nbCore):
298 subfp = fpCore.getSpans().asArray()[:, xEdgesCores[i]:xEdgesCores[i+1]]
299 xCoreFP = int(xEdgesCores[i] + numpy.argmax(numpy.sum(subfp, axis=0)))
300 # turn into X coordinate in detector space
301 xCore = xCoreFP + fpCore.getBBox().getMinX()
302 # get Y footprint coordinate of the core
303 # by trimming the edges where edge bleeds are potentially dominant
304 if subfp.shape[0] < 200:
305 yCoreFP = int(numpy.argmax(numpy.sum(subfp, axis=1)))
306 else:
307 yCoreFP = int(numpy.argmax(numpy.sum(subfp[100:-100, :],
308 axis=1)))
309 yCoreFP = 100+yCoreFP
310
311 # Estimate the width of the saturated core
312 widthSat = numpy.sum(subfp[int(yCoreFP), :])
313
314 subfpArea = numpy.sum(subfp)
315 if subfpArea > itlEdgeBleedSatMinArea and subfpArea < itlEdgeBleedSatMaxArea:
316 _applyMaskITLEdgeBleed(ccdExposure, xCore,
317 satLevel, widthSat,
318 itlEdgeBleedThreshold,
319 itlEdgeBleedModelConstant,
320 saturatedMaskName, log)
321 elif nbCore > 2:
322 # TODO DM-49736: support N cores in saturated footprint
323 log.warning(
324 "Too many (%d) cores in saturated footprint to mask edge bleeds.",
325 nbCore,
326 )
327 else:
328 # Get centroid of saturated core
329 xCore, yCore = fpCore.getCentroid()
330 # Turn the Y detector coordinate into Y footprint coordinate
331 yCoreFP = yCore - fpCore.getBBox().getMinY()
332 # Get the number of saturated columns around the centroid
333 widthSat = numpy.sum(fpCore.getSpans().asArray()[int(yCoreFP), :])
334 _applyMaskITLEdgeBleed(ccdExposure, xCore,
335 satLevel, widthSat, itlEdgeBleedThreshold,
336 itlEdgeBleedModelConstant, saturatedMaskName, log)
337
338
339def _applyMaskITLEdgeBleed(ccdExposure, xCore,
340 satLevel, widthSat,
341 itlEdgeBleedThreshold=5000.,
342 itlEdgeBleedModelConstant=0.03,
343 saturatedMaskName="SAT", log=None):
344 """Apply ITL edge bleed masking model.
345
346 Parameters
347 ----------
348 ccdExposure : `lsst.afw.image.Exposure`
349 Exposure to apply masking to.
350 xCore: `int`
351 X coordinate of the saturated core.
352 satLevel: `float`
353 Minimum saturation level of the detector.
354 widthSat: `float`
355 Width of the saturated core.
356 itlEdgeBleedThreshold : `float`, optional
357 Threshold above median sky background for edge bleed detection
358 (electron units).
359 itlEdgeBleedModelConstant : `float`, optional
360 Constant in the decaying exponential in the edge bleed masking.
361 saturatedMaskName : `str`, optional
362 Mask name for saturation.
363 log : `logging.Logger`, optional
364 Logger to handle messages.
365 """
366 log = log if log else logging.getLogger(__name__)
367
368 maskedImage = ccdExposure.maskedImage
369 xmax = maskedImage.image.array.shape[1]
370 saturatedBit = maskedImage.mask.getPlaneBitMask(saturatedMaskName)
371
372 for amp in ccdExposure.getDetector():
373 # Select the 2 top and bottom amplifiers around the saturated
374 # core with a potential edge bleed by selecting the amplifiers
375 # that have the same X coordinate as the saturated core.
376 # As we don't care about the Y coordinate, we set it to the
377 # center of the BBox.
378 yBox = amp.getBBox().getCenter()[1]
379 if amp.getBBox().contains(xCore, yBox):
380
381 # Get the amp name
382 ampName = amp.getName()
383
384 # Because in ITLs the edge bleed happens on both edges
385 # of the detector, we make a cutout around
386 # both the top and bottom
387 # edge bleed candidates around the saturated core.
388 # We flip the cutout of the top amplifier
389 # to then work with the same coordinates for both.
390 # The way of selecting top vs bottom amp
391 # is very specific to ITL.
392 if ampName[:2] == 'C1':
393 sliceImage = maskedImage.image.array[:200, :]
394 sliceMask = maskedImage.mask.array[:200, :]
395 elif ampName[:2] == 'C0':
396 sliceImage = numpy.flipud(maskedImage.image.array[-200:, :])
397 sliceMask = numpy.flipud(maskedImage.mask.array[-200:, :])
398
399 # The middle columns of edge bleeds often have
400 # high counts, so we check there is an edge bleed
401 # by looking at a small image up to 50 pixels from the edge
402 # and around the saturated columns
403 # of the saturated core, and checking its median is
404 # above the sky background by itlEdgeBleedThreshold
405
406 # If the centroid is too close to the edge of the detector
407 # (within 5 pixels), we set the limit to the mean check
408 # to the edge of the detector
409 lowerRangeSmall = int(xCore)-5
410 upperRangeSmall = int(xCore)+5
411 if lowerRangeSmall < 0:
412 lowerRangeSmall = 0
413 if upperRangeSmall > xmax:
414 upperRangeSmall = xmax
415 ampImageBG = numpy.median(maskedImage[amp.getBBox()].image.array)
416 edgeMedian = numpy.median(sliceImage[:50, lowerRangeSmall:upperRangeSmall])
417 if edgeMedian > (ampImageBG + itlEdgeBleedThreshold):
418
419 log.info("Found edge bleed around column %d", xCore)
420
421 # We need an estimate of the maximum width
422 # of the edge bleed for our masking model
423 # so we now estimate it by measuring the width of
424 # areas above 60 percent of the saturation level
425 # close to the edge,
426 # in a cutout up to 100 pixels from the edge,
427 # with a width of around the width of an amplifier.
428 subImageXMin = int(xCore)-250
429 subImageXMax = int(xCore)+250
430 if subImageXMin < 0:
431 subImageXMin = 0
432 elif subImageXMax > xmax:
433 subImageXMax = xmax
434
435 subImage = sliceImage[:100, subImageXMin:subImageXMax]
436 maxWidthEdgeBleed = numpy.max(numpy.sum(subImage > 0.45*satLevel,
437 axis=1))
438
439 # Mask edge bleed with a decaying exponential model
440 for y in range(200):
441 edgeBleedHalfWidth = \
442 int(((maxWidthEdgeBleed)*numpy.exp(-itlEdgeBleedModelConstant*y)
443 + widthSat)/2.)
444 lowerRange = int(xCore)-edgeBleedHalfWidth
445 upperRange = int(xCore)+edgeBleedHalfWidth
446 # If the edge bleed model goes outside the detector
447 # we set the limit for the masking
448 # to the edge of the detector
449 if lowerRange < 0:
450 lowerRange = 0
451 if upperRange > xmax:
452 upperRange = xmax
453 sliceMask[y, lowerRange:upperRange] |= saturatedBit
454
455
456def maskITLSatSag(ccdExposure, fpCore, saturatedMaskName="SAT"):
457 """Mask columns presenting saturation sag in saturated footprints in
458 ITL detectors.
459
460 Parameters
461 ----------
462 ccdExposure : `lsst.afw.image.Exposure`
463 Exposure to apply masking to.
464 fpCore : `lsst.afw.detection._detection.Footprint`
465 Footprint of saturated core.
466 saturatedMaskName : `str`, optional
467 Mask name for saturation.
468 """
469
470 # TODO DM-49736: add a flux level check to apply masking
471
472 maskedImage = ccdExposure.maskedImage
473 saturatedBit = maskedImage.mask.getPlaneBitMask(saturatedMaskName)
474
475 cc = numpy.sum(fpCore.getSpans().asArray(), axis=0)
476 # Mask full columns that have 20 percent of the height of the footprint
477 # saturated
478 columnsToMaskFP = numpy.where(cc > fpCore.getSpans().asArray().shape[0]/5.)
479
480 columnsToMask = [x + int(fpCore.getBBox().getMinX()) for x in columnsToMaskFP]
481 maskedImage.mask.array[:, columnsToMask] |= saturatedBit
482
483
484def maskITLDip(exposure, detectorConfig, maskPlaneNames=["SUSPECT", "ITL_DIP"], log=None):
485 """Add mask bits according to the ITL dip model.
486
487 Parameters
488 ----------
489 exposure : `lsst.afw.image.Exposure`
490 Exposure to do ITL dip masking.
491 detectorConfig : `lsst.ip.isr.overscanAmpConfig.OverscanDetectorConfig`
492 Configuration for this detector.
493 maskPlaneNames : `list [`str`], optional
494 Name of the ITL Dip mask planes.
495 log : `logging.Logger`, optional
496 If not set, a default logger will be used.
497 """
498 if detectorConfig.itlDipBackgroundFraction == 0.0:
499 # Nothing to do.
500 return
501
502 if log is None:
503 log = logging.getLogger(__name__)
504
505 thresh = afwDetection.Threshold(
506 exposure.mask.getPlaneBitMask("SAT"),
507 afwDetection.Threshold.BITMASK,
508 )
509 fpList = afwDetection.FootprintSet(exposure.mask, thresh).getFootprints()
510
511 heights = numpy.asarray([fp.getBBox().getHeight() for fp in fpList])
512
513 largeHeights, = numpy.where(heights >= detectorConfig.itlDipMinHeight)
514
515 if len(largeHeights) == 0:
516 return
517
518 # Get the approximate image background.
519 approxBackground = numpy.median(exposure.image.array)
520 maskValue = exposure.mask.getPlaneBitMask(maskPlaneNames)
521
522 maskBak = exposure.mask.array.copy()
523 nMaskedCols = 0
524
525 for index in largeHeights:
526 fp = fpList[index]
527 center = fp.getCentroid()
528
529 nSat = numpy.sum(fp.getSpans().asArray(), axis=0)
530 width = numpy.sum(nSat > detectorConfig.itlDipMinHeight)
531
532 if width < detectorConfig.itlDipMinWidth:
533 continue
534
535 width = numpy.clip(width, None, detectorConfig.itlDipMaxWidth)
536
537 dipMax = detectorConfig.itlDipBackgroundFraction * approxBackground * width
538
539 # Assume sky-noise dominated; we could add in read noise here.
540 if dipMax < detectorConfig.itlDipMinBackgroundNoiseFraction * numpy.sqrt(approxBackground):
541 continue
542
543 minCol = int(center.getX() - (detectorConfig.itlDipWidthScale * width) / 2.)
544 maxCol = int(center.getX() + (detectorConfig.itlDipWidthScale * width) / 2.)
545 minCol = numpy.clip(minCol, 0, None)
546 maxCol = numpy.clip(maxCol, None, exposure.mask.array.shape[1] - 1)
547
548 log.info(
549 "Found ITL dip (width %d; bkg %.2f); masking column %d to %d",
550 width,
551 approxBackground,
552 minCol,
553 maxCol,
554 )
555
556 exposure.mask.array[:, minCol: maxCol + 1] |= maskValue
557
558 nMaskedCols += (maxCol - minCol + 1)
559
560 if nMaskedCols > detectorConfig.itlDipMaxColsPerImage:
561 log.warning(
562 "Too many (%d) columns would be masked on this image from dip masking; restoring original mask.",
563 nMaskedCols,
564 )
565 exposure.mask.array[:, :] = maskBak
566
567
568def interpolateFromMask(maskedImage, fwhm, growSaturatedFootprints=1,
569 maskNameList=['SAT'], fallbackValue=None, useLegacyInterp=True):
570 """Interpolate over defects identified by a particular set of mask planes.
571
572 Parameters
573 ----------
574 maskedImage : `lsst.afw.image.MaskedImage`
575 Image to process.
576 fwhm : `float`
577 FWHM of double Gaussian smoothing kernel.
578 growSaturatedFootprints : scalar, optional
579 Number of pixels to grow footprints for saturated pixels.
580 maskNameList : `List` of `str`, optional
581 Mask plane name.
582 fallbackValue : scalar, optional
583 Value of last resort for interpolation.
584
585 Notes
586 -----
587 The ``fwhm`` parameter is used to create a PSF, but the underlying
588 interpolation code (`lsst.meas.algorithms.interpolateOverDefects`) does
589 not currently make use of this information.
590 """
591 mask = maskedImage.getMask()
592
593 if growSaturatedFootprints > 0 and "SAT" in maskNameList:
594 # If we are interpolating over an area larger than the original masked
595 # region, we need to expand the original mask bit to the full area to
596 # explain why we interpolated there.
597 growMasks(mask, radius=growSaturatedFootprints, maskNameList=['SAT'], maskValue="SAT")
598
599 thresh = afwDetection.Threshold(mask.getPlaneBitMask(maskNameList), afwDetection.Threshold.BITMASK)
600 fpSet = afwDetection.FootprintSet(mask, thresh)
601 defectList = Defects.fromFootprintList(fpSet.getFootprints())
602
603 interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=fallbackValue,
604 maskNameList=maskNameList, useLegacyInterp=useLegacyInterp)
605
606 return maskedImage
607
608
609def saturationCorrection(maskedImage, saturation, fwhm, growFootprints=1, interpolate=True, maskName='SAT',
610 fallbackValue=None, useLegacyInterp=True):
611 """Mark saturated pixels and optionally interpolate over them
612
613 Parameters
614 ----------
615 maskedImage : `lsst.afw.image.MaskedImage`
616 Image to process.
617 saturation : scalar
618 Saturation level used as the detection threshold.
619 fwhm : `float`
620 FWHM of double Gaussian smoothing kernel.
621 growFootprints : scalar, optional
622 Number of pixels to grow footprints of detected regions.
623 interpolate : Bool, optional
624 If True, saturated pixels are interpolated over.
625 maskName : str, optional
626 Mask plane name.
627 fallbackValue : scalar, optional
628 Value of last resort for interpolation.
629
630 Notes
631 -----
632 The ``fwhm`` parameter is used to create a PSF, but the underlying
633 interpolation code (`lsst.meas.algorithms.interpolateOverDefects`) does
634 not currently make use of this information.
635 """
636 defectList = makeThresholdMask(
637 maskedImage=maskedImage,
638 threshold=saturation,
639 growFootprints=growFootprints,
640 maskName=maskName,
641 )
642 if interpolate:
643 interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=fallbackValue,
644 maskNameList=[maskName], useLegacyInterp=useLegacyInterp)
645
646 return maskedImage
647
648
649def trimToMatchCalibBBox(rawMaskedImage, calibMaskedImage):
650 """Compute number of edge trim pixels to match the calibration data.
651
652 Use the dimension difference between the raw exposure and the
653 calibration exposure to compute the edge trim pixels. This trim
654 is applied symmetrically, with the same number of pixels masked on
655 each side.
656
657 Parameters
658 ----------
659 rawMaskedImage : `lsst.afw.image.MaskedImage`
660 Image to trim.
661 calibMaskedImage : `lsst.afw.image.MaskedImage`
662 Calibration image to draw new bounding box from.
663
664 Returns
665 -------
666 replacementMaskedImage : `lsst.afw.image.MaskedImage`
667 ``rawMaskedImage`` trimmed to the appropriate size.
668
669 Raises
670 ------
671 RuntimeError
672 Raised if ``rawMaskedImage`` cannot be symmetrically trimmed to
673 match ``calibMaskedImage``.
674 """
675 nx, ny = rawMaskedImage.getBBox().getDimensions() - calibMaskedImage.getBBox().getDimensions()
676 if nx != ny:
677 raise RuntimeError("Raw and calib maskedImages are trimmed differently in X and Y.")
678 if nx % 2 != 0:
679 raise RuntimeError("Calibration maskedImage is trimmed unevenly in X.")
680 if nx < 0:
681 raise RuntimeError("Calibration maskedImage is larger than raw data.")
682
683 nEdge = nx//2
684 if nEdge > 0:
685 replacementMaskedImage = rawMaskedImage[nEdge:-nEdge, nEdge:-nEdge, afwImage.LOCAL]
686 SourceDetectionTask.setEdgeBits(
687 rawMaskedImage,
688 replacementMaskedImage.getBBox(),
689 rawMaskedImage.getMask().getPlaneBitMask("EDGE")
690 )
691 else:
692 replacementMaskedImage = rawMaskedImage
693
694 return replacementMaskedImage
695
696
697def biasCorrection(maskedImage, biasMaskedImage, trimToFit=False):
698 """Apply bias correction in place.
699
700 Parameters
701 ----------
702 maskedImage : `lsst.afw.image.MaskedImage`
703 Image to process. The image is modified by this method.
704 biasMaskedImage : `lsst.afw.image.MaskedImage`
705 Bias image of the same size as ``maskedImage``
706 trimToFit : `Bool`, optional
707 If True, raw data is symmetrically trimmed to match
708 calibration size.
709
710 Raises
711 ------
712 RuntimeError
713 Raised if ``maskedImage`` and ``biasMaskedImage`` do not have
714 the same size.
715
716 """
717 if trimToFit:
718 maskedImage = trimToMatchCalibBBox(maskedImage, biasMaskedImage)
719
720 if maskedImage.getBBox(afwImage.LOCAL) != biasMaskedImage.getBBox(afwImage.LOCAL):
721 raise RuntimeError("maskedImage bbox %s != biasMaskedImage bbox %s" %
722 (maskedImage.getBBox(afwImage.LOCAL), biasMaskedImage.getBBox(afwImage.LOCAL)))
723 maskedImage -= biasMaskedImage
724
725
726def darkCorrection(maskedImage, darkMaskedImage, expScale, darkScale, invert=False, trimToFit=False):
727 """Apply dark correction in place.
728
729 Parameters
730 ----------
731 maskedImage : `lsst.afw.image.MaskedImage`
732 Image to process. The image is modified by this method.
733 darkMaskedImage : `lsst.afw.image.MaskedImage`
734 Dark image of the same size as ``maskedImage``.
735 expScale : scalar
736 Dark exposure time for ``maskedImage``.
737 darkScale : scalar
738 Dark exposure time for ``darkMaskedImage``.
739 invert : `Bool`, optional
740 If True, re-add the dark to an already corrected image.
741 trimToFit : `Bool`, optional
742 If True, raw data is symmetrically trimmed to match
743 calibration size.
744
745 Raises
746 ------
747 RuntimeError
748 Raised if ``maskedImage`` and ``darkMaskedImage`` do not have
749 the same size.
750
751 Notes
752 -----
753 The dark correction is applied by calculating:
754 maskedImage -= dark * expScaling / darkScaling
755 """
756 if trimToFit:
757 maskedImage = trimToMatchCalibBBox(maskedImage, darkMaskedImage)
758
759 if maskedImage.getBBox(afwImage.LOCAL) != darkMaskedImage.getBBox(afwImage.LOCAL):
760 raise RuntimeError("maskedImage bbox %s != darkMaskedImage bbox %s" %
761 (maskedImage.getBBox(afwImage.LOCAL), darkMaskedImage.getBBox(afwImage.LOCAL)))
762
763 scale = expScale / darkScale
764 if not invert:
765 maskedImage.scaledMinus(scale, darkMaskedImage)
766 else:
767 maskedImage.scaledPlus(scale, darkMaskedImage)
768
769
770def updateVariance(maskedImage, gain, readNoise, replace=True):
771 """Set the variance plane based on the image plane.
772
773 The maskedImage must have units of `adu` (if gain != 1.0) or
774 electron (if gain == 1.0). This routine will always produce a
775 variance plane in the same units as the image.
776
777 Parameters
778 ----------
779 maskedImage : `lsst.afw.image.MaskedImage`
780 Image to process. The variance plane is modified.
781 gain : scalar
782 The amplifier gain in electron/adu.
783 readNoise : scalar
784 The amplifier read noise in electron/pixel.
785 replace : `bool`, optional
786 Replace the current variance? If False, the image
787 variance will be added to the current variance plane.
788 """
789 var = maskedImage.variance
790 if replace:
791 var[:, :] = maskedImage.image
792 else:
793 var[:, :] += maskedImage.image
794 var /= gain
795 var += (readNoise/gain)**2
796
797
798def flatCorrection(maskedImage, flatMaskedImage, scalingType, userScale=1.0, invert=False, trimToFit=False):
799 """Apply flat correction in place.
800
801 Parameters
802 ----------
803 maskedImage : `lsst.afw.image.MaskedImage`
804 Image to process. The image is modified.
805 flatMaskedImage : `lsst.afw.image.MaskedImage`
806 Flat image of the same size as ``maskedImage``
807 scalingType : str
808 Flat scale computation method. Allowed values are 'MEAN',
809 'MEDIAN', or 'USER'.
810 userScale : scalar, optional
811 Scale to use if ``scalingType='USER'``.
812 invert : `Bool`, optional
813 If True, unflatten an already flattened image.
814 trimToFit : `Bool`, optional
815 If True, raw data is symmetrically trimmed to match
816 calibration size.
817
818 Raises
819 ------
820 RuntimeError
821 Raised if ``maskedImage`` and ``flatMaskedImage`` do not have
822 the same size or if ``scalingType`` is not an allowed value.
823 """
824 if trimToFit:
825 maskedImage = trimToMatchCalibBBox(maskedImage, flatMaskedImage)
826
827 if maskedImage.getBBox(afwImage.LOCAL) != flatMaskedImage.getBBox(afwImage.LOCAL):
828 raise RuntimeError("maskedImage bbox %s != flatMaskedImage bbox %s" %
829 (maskedImage.getBBox(afwImage.LOCAL), flatMaskedImage.getBBox(afwImage.LOCAL)))
830
831 # Figure out scale from the data
832 # Ideally the flats are normalized by the calibration product pipeline,
833 # but this allows some flexibility in the case that the flat is created by
834 # some other mechanism.
835 if scalingType in ('MEAN', 'MEDIAN'):
836 scalingType = afwMath.stringToStatisticsProperty(scalingType)
837 flatScale = afwMath.makeStatistics(flatMaskedImage.image, scalingType).getValue()
838 elif scalingType == 'USER':
839 flatScale = userScale
840 else:
841 raise RuntimeError('%s : %s not implemented' % ("flatCorrection", scalingType))
842
843 if not invert:
844 maskedImage.scaledDivides(1.0/flatScale, flatMaskedImage)
845 else:
846 maskedImage.scaledMultiplies(1.0/flatScale, flatMaskedImage)
847
848
849def illuminationCorrection(maskedImage, illumMaskedImage, illumScale, trimToFit=True):
850 """Apply illumination correction in place.
851
852 Parameters
853 ----------
854 maskedImage : `lsst.afw.image.MaskedImage`
855 Image to process. The image is modified.
856 illumMaskedImage : `lsst.afw.image.MaskedImage`
857 Illumination correction image of the same size as ``maskedImage``.
858 illumScale : scalar
859 Scale factor for the illumination correction.
860 trimToFit : `Bool`, optional
861 If True, raw data is symmetrically trimmed to match
862 calibration size.
863
864 Raises
865 ------
866 RuntimeError
867 Raised if ``maskedImage`` and ``illumMaskedImage`` do not have
868 the same size.
869 """
870 if trimToFit:
871 maskedImage = trimToMatchCalibBBox(maskedImage, illumMaskedImage)
872
873 if maskedImage.getBBox(afwImage.LOCAL) != illumMaskedImage.getBBox(afwImage.LOCAL):
874 raise RuntimeError("maskedImage bbox %s != illumMaskedImage bbox %s" %
875 (maskedImage.getBBox(afwImage.LOCAL), illumMaskedImage.getBBox(afwImage.LOCAL)))
876
877 maskedImage.scaledDivides(1.0/illumScale, illumMaskedImage)
878
879
880def brighterFatterCorrection(exposure, kernel, maxIter, threshold, applyGain, gains=None):
881 """Apply brighter fatter correction in place for the image.
882
883 Parameters
884 ----------
885 exposure : `lsst.afw.image.Exposure`
886 Exposure to have brighter-fatter correction applied. Modified
887 by this method.
888 kernel : `numpy.ndarray`
889 Brighter-fatter kernel to apply.
890 maxIter : scalar
891 Number of correction iterations to run.
892 threshold : scalar
893 Convergence threshold in terms of the sum of absolute
894 deviations between an iteration and the previous one.
895 applyGain : `Bool`
896 If True, then the exposure values are scaled by the gain prior
897 to correction.
898 gains : `dict` [`str`, `float`]
899 A dictionary, keyed by amplifier name, of the gains to use.
900 If gains is None, the nominal gains in the amplifier object are used.
901
902 Returns
903 -------
904 diff : `float`
905 Final difference between iterations achieved in correction.
906 iteration : `int`
907 Number of iterations used to calculate correction.
908
909 Notes
910 -----
911 This correction takes a kernel that has been derived from flat
912 field images to redistribute the charge. The gradient of the
913 kernel is the deflection field due to the accumulated charge.
914
915 Given the original image I(x) and the kernel K(x) we can compute
916 the corrected image Ic(x) using the following equation:
917
918 Ic(x) = I(x) + 0.5*d/dx(I(x)*d/dx(int( dy*K(x-y)*I(y))))
919
920 To evaluate the derivative term we expand it as follows:
921
922 0.5 * ( d/dx(I(x))*d/dx(int(dy*K(x-y)*I(y)))
923 + I(x)*d^2/dx^2(int(dy* K(x-y)*I(y))) )
924
925 Because we use the measured counts instead of the incident counts
926 we apply the correction iteratively to reconstruct the original
927 counts and the correction. We stop iterating when the summed
928 difference between the current corrected image and the one from
929 the previous iteration is below the threshold. We do not require
930 convergence because the number of iterations is too large a
931 computational cost. How we define the threshold still needs to be
932 evaluated, the current default was shown to work reasonably well
933 on a small set of images. For more information on the method see
934 DocuShare Document-19407.
935
936 The edges as defined by the kernel are not corrected because they
937 have spurious values due to the convolution.
938 """
939 image = exposure.getMaskedImage().getImage()
940
941 # The image needs to be units of electrons/holes
942 with gainContext(exposure, image, applyGain, gains):
943
944 kLx = numpy.shape(kernel)[0]
945 kLy = numpy.shape(kernel)[1]
946 kernelImage = afwImage.ImageD(kLx, kLy)
947 kernelImage.getArray()[:, :] = kernel
948 tempImage = afwImage.ImageD(image, deep=True)
949
950 nanIndex = numpy.isnan(tempImage.getArray())
951 tempImage.getArray()[nanIndex] = 0.
952
953 outImage = afwImage.ImageD(image.getDimensions())
954 corr = numpy.zeros(image.array.shape, dtype=numpy.float64)
955 prev_image = numpy.zeros(image.array.shape, dtype=numpy.float64)
956 convCntrl = afwMath.ConvolutionControl(False, True, 1)
957 fixedKernel = afwMath.FixedKernel(kernelImage)
958
959 # Define boundary by convolution region. The region that the
960 # correction will be calculated for is one fewer in each dimension
961 # because of the second derivative terms.
962 # NOTE: these need to use integer math, as we're using start:end as
963 # numpy index ranges.
964 startX = kLx//2
965 endX = -kLx//2
966 startY = kLy//2
967 endY = -kLy//2
968
969 for iteration in range(maxIter):
970
971 afwMath.convolve(outImage, tempImage, fixedKernel, convCntrl)
972 tmpArray = tempImage.getArray()
973 outArray = outImage.getArray()
974
975 with numpy.errstate(invalid="ignore", over="ignore"):
976 # First derivative term
977 gradTmp = numpy.gradient(tmpArray[startY:endY, startX:endX])
978 gradOut = numpy.gradient(outArray[startY:endY, startX:endX])
979 first = (gradTmp[0]*gradOut[0] + gradTmp[1]*gradOut[1])[1:-1, 1:-1]
980
981 # Second derivative term
982 diffOut20 = numpy.diff(outArray, 2, 0)[startY:endY, startX + 1:endX - 1]
983 diffOut21 = numpy.diff(outArray, 2, 1)[startY + 1:endY - 1, startX:endX]
984 second = tmpArray[startY + 1:endY - 1, startX + 1:endX - 1]*(diffOut20 + diffOut21)
985
986 corr[startY + 1:endY - 1, startX + 1:endX - 1] = 0.5*(first + second)
987
988 tmpArray[:, :] = image.getArray()[:, :]
989 tmpArray[nanIndex] = 0.
990 tmpArray[startY:endY, startX:endX] += corr[startY:endY, startX:endX]
991
992 if iteration > 0:
993 diff = numpy.sum(numpy.abs(prev_image - tmpArray), dtype=numpy.float64)
994
995 if diff < threshold:
996 break
997 prev_image[:, :] = tmpArray[:, :]
998
999 image.getArray()[startY + 1:endY - 1, startX + 1:endX - 1] += \
1000 corr[startY + 1:endY - 1, startX + 1:endX - 1]
1001
1002 return diff, iteration
1003
1004
1005def transferFlux(cFunc, fStep, correctionMode=True):
1006 """Take the input convolved deflection potential and the flux array
1007 to compute and apply the flux transfer into the correction array.
1008
1009 Parameters
1010 ----------
1011 cFunc: `numpy.array`
1012 Deflection potential, being the convolution of the flux F with the
1013 kernel K.
1014 fStep: `numpy.array`
1015 The array of flux values which act as the source of the flux transfer.
1016 correctionMode: `bool`
1017 Defines if applying correction (True) or generating sims (False).
1018
1019 Returns
1020 -------
1021 corr:
1022 BFE correction array
1023 """
1024
1025 if cFunc.shape != fStep.shape:
1026 raise RuntimeError(f'transferFlux: array shapes do not match: {cFunc.shape}, {fStep.shape}')
1027
1028 # set the sign of the correction and set its value for the
1029 # time averaged solution
1030 if correctionMode:
1031 # negative sign if applying BFE correction
1032 factor = -0.5
1033 else:
1034 # positive sign if generating BFE simulations
1035 factor = 0.5
1036
1037 # initialise the BFE correction image to zero
1038 corr = numpy.zeros(cFunc.shape, dtype=numpy.float64)
1039
1040 # Generate a 2D mesh of x,y coordinates
1041 yDim, xDim = cFunc.shape
1042 y = numpy.arange(yDim, dtype=int)
1043 x = numpy.arange(xDim, dtype=int)
1044 xc, yc = numpy.meshgrid(x, y)
1045
1046 # process each axis in turn
1047 for ax in [0, 1]:
1048
1049 # gradient of phi on right/upper edge of pixel
1050 diff = numpy.diff(cFunc, axis=ax)
1051
1052 # expand array back to full size with zero gradient at the end
1053 gx = numpy.zeros(cFunc.shape, dtype=numpy.float64)
1054 yDiff, xDiff = diff.shape
1055 gx[:yDiff, :xDiff] += diff
1056
1057 # select pixels with either positive gradients on the right edge,
1058 # flux flowing to the right/up
1059 # or negative gradients, flux flowing to the left/down
1060 for i, sel in enumerate([gx > 0, gx < 0]):
1061 xSelPixels = xc[sel]
1062 ySelPixels = yc[sel]
1063 # and add the flux into the pixel to the right or top
1064 # depending on which axis we are handling
1065 if ax == 0:
1066 xPix = xSelPixels
1067 yPix = ySelPixels+1
1068 else:
1069 xPix = xSelPixels+1
1070 yPix = ySelPixels
1071 # define flux as the either current pixel value or pixel
1072 # above/right
1073 # depending on whether positive or negative gradient
1074 if i == 0:
1075 # positive gradients, flux flowing to higher coordinate values
1076 flux = factor * fStep[sel]*gx[sel]
1077 else:
1078 # negative gradients, flux flowing to lower coordinate values
1079 flux = factor * fStep[yPix, xPix]*gx[sel]
1080 # change the fluxes of the donor and receiving pixels
1081 # such that flux is conserved
1082 corr[sel] -= flux
1083 corr[yPix, xPix] += flux
1084
1085 # return correction array
1086 return corr
1087
1088
1089def fluxConservingBrighterFatterCorrection(exposure, kernel, maxIter, threshold, applyGain,
1090 gains=None, correctionMode=True):
1091 """Apply brighter fatter correction in place for the image.
1092
1093 This version presents a modified version of the algorithm
1094 found in ``lsst.ip.isr.isrFunctions.brighterFatterCorrection``
1095 which conserves the image flux, resulting in improved
1096 correction of the cores of stars. The convolution has also been
1097 modified to mitigate edge effects.
1098
1099 Parameters
1100 ----------
1101 exposure : `lsst.afw.image.Exposure`
1102 Exposure to have brighter-fatter correction applied. Modified
1103 by this method.
1104 kernel : `numpy.ndarray`
1105 Brighter-fatter kernel to apply.
1106 maxIter : scalar
1107 Number of correction iterations to run.
1108 threshold : scalar
1109 Convergence threshold in terms of the sum of absolute
1110 deviations between an iteration and the previous one.
1111 applyGain : `Bool`
1112 If True, then the exposure values are scaled by the gain prior
1113 to correction.
1114 gains : `dict` [`str`, `float`]
1115 A dictionary, keyed by amplifier name, of the gains to use.
1116 If gains is None, the nominal gains in the amplifier object are used.
1117 correctionMode : `Bool`
1118 If True (default) the function applies correction for BFE. If False,
1119 the code can instead be used to generate a simulation of BFE (sign
1120 change in the direction of the effect)
1121
1122 Returns
1123 -------
1124 diff : `float`
1125 Final difference between iterations achieved in correction.
1126 iteration : `int`
1127 Number of iterations used to calculate correction.
1128
1129 Notes
1130 -----
1131 Modified version of ``lsst.ip.isr.isrFunctions.brighterFatterCorrection``.
1132
1133 This correction takes a kernel that has been derived from flat
1134 field images to redistribute the charge. The gradient of the
1135 kernel is the deflection field due to the accumulated charge.
1136
1137 Given the original image I(x) and the kernel K(x) we can compute
1138 the corrected image Ic(x) using the following equation:
1139
1140 Ic(x) = I(x) + 0.5*d/dx(I(x)*d/dx(int( dy*K(x-y)*I(y))))
1141
1142 Improved algorithm at this step applies the divergence theorem to
1143 obtain a pixelised correction.
1144
1145 Because we use the measured counts instead of the incident counts
1146 we apply the correction iteratively to reconstruct the original
1147 counts and the correction. We stop iterating when the summed
1148 difference between the current corrected image and the one from
1149 the previous iteration is below the threshold. We do not require
1150 convergence because the number of iterations is too large a
1151 computational cost. How we define the threshold still needs to be
1152 evaluated, the current default was shown to work reasonably well
1153 on a small set of images.
1154
1155 Edges are handled in the convolution by padding. This is still not
1156 a physical model for the edge, but avoids discontinuity in the correction.
1157
1158 Author of modified version: Lance.Miller@physics.ox.ac.uk
1159 (see DM-38555).
1160 """
1161 image = exposure.getMaskedImage().getImage()
1162
1163 # The image needs to be units of electrons/holes
1164 with gainContext(exposure, image, applyGain, gains):
1165
1166 # get kernel and its shape
1167 kLy, kLx = kernel.shape
1168 kernelImage = afwImage.ImageD(kLx, kLy)
1169 kernelImage.getArray()[:, :] = kernel
1170 tempImage = afwImage.ImageD(image, deep=True)
1171
1172 nanIndex = numpy.isnan(tempImage.getArray())
1173 tempImage.getArray()[nanIndex] = 0.
1174
1175 outImage = afwImage.ImageD(image.getDimensions())
1176 corr = numpy.zeros(image.array.shape, dtype=numpy.float64)
1177 prevImage = numpy.zeros(image.array.shape, dtype=numpy.float64)
1178 convCntrl = afwMath.ConvolutionControl(False, False, 1)
1179 fixedKernel = afwMath.FixedKernel(kernelImage)
1180
1181 # set the padding amount
1182 # ensure we pad by an even amount larger than the kernel
1183 kLy = 2 * ((1+kLy)//2)
1184 kLx = 2 * ((1+kLx)//2)
1185
1186 # The deflection potential only depends on the gradient of
1187 # the convolution, so we can subtract the mean, which then
1188 # allows us to pad the image with zeros and avoid wrap-around effects
1189 # (although still not handling the image edges with a physical model)
1190 # This wouldn't be great if there were a strong image gradient.
1191 imYdimension, imXdimension = tempImage.array.shape
1192 imean = numpy.mean(tempImage.getArray()[~nanIndex], dtype=numpy.float64)
1193 # subtract mean from image
1194 tempImage -= imean
1195 tempImage.array[nanIndex] = 0.0
1196 padArray = numpy.pad(tempImage.getArray(), ((0, kLy), (0, kLx)))
1197 outImage = afwImage.ImageD(numpy.pad(outImage.getArray(), ((0, kLy), (0, kLx))))
1198 # Convert array to afw image so afwMath.convolve works
1199 padImage = afwImage.ImageD(padArray.shape[1], padArray.shape[0])
1200 padImage.array[:] = padArray
1201
1202 for iteration in range(maxIter):
1203
1204 # create deflection potential, convolution of flux with kernel
1205 # using padded counts array
1206 afwMath.convolve(outImage, padImage, fixedKernel, convCntrl)
1207 tmpArray = tempImage.getArray()
1208 outArray = outImage.getArray()
1209
1210 # trim convolution output back to original shape
1211 outArray = outArray[:imYdimension, :imXdimension]
1212
1213 # generate the correction array, with correctionMode set as input
1214 corr[...] = transferFlux(outArray, tmpArray, correctionMode=correctionMode)
1215
1216 # update the arrays for the next iteration
1217 tmpArray[:, :] = image.getArray()[:, :]
1218 tmpArray += corr
1219 tmpArray[nanIndex] = 0.
1220 # update padded array
1221 # subtract mean
1222 tmpArray -= imean
1223 tempImage.array[nanIndex] = 0.
1224 padArray = numpy.pad(tempImage.getArray(), ((0, kLy), (0, kLx)))
1225
1226 if iteration > 0:
1227 diff = numpy.sum(numpy.abs(prevImage - tmpArray), dtype=numpy.float64)
1228
1229 if diff < threshold:
1230 break
1231 prevImage[:, :] = tmpArray[:, :]
1232
1233 image.getArray()[:] += corr[:]
1234
1235 return diff, iteration
1236
1237
1238@contextmanager
1239def gainContext(exp, image, apply, gains=None, invert=False, isTrimmed=True):
1240 """Context manager that applies and removes gain.
1241
1242 Parameters
1243 ----------
1244 exp : `lsst.afw.image.Exposure`
1245 Exposure to apply/remove gain.
1246 image : `lsst.afw.image.Image`
1247 Image to apply/remove gain.
1248 apply : `bool`
1249 If True, apply and remove the amplifier gain.
1250 gains : `dict` [`str`, `float`], optional
1251 A dictionary, keyed by amplifier name, of the gains to use.
1252 If gains is None, the nominal gains in the amplifier object are used.
1253 invert : `bool`, optional
1254 Invert the gains (e.g. convert electrons to adu temporarily)?
1255 isTrimmed : `bool`, optional
1256 Is this a trimmed exposure?
1257
1258 Yields
1259 ------
1260 exp : `lsst.afw.image.Exposure`
1261 Exposure with the gain applied.
1262 """
1263 # check we have all of them if provided because mixing and matching would
1264 # be a real mess
1265 if gains and apply is True:
1266 ampNames = [amp.getName() for amp in exp.getDetector()]
1267 for ampName in ampNames:
1268 if ampName not in gains.keys():
1269 raise RuntimeError(f"Gains provided to gain context, but no entry found for amp {ampName}")
1270
1271 if apply:
1272 ccd = exp.getDetector()
1273 for amp in ccd:
1274 sim = image.Factory(image, amp.getBBox() if isTrimmed else amp.getRawBBox())
1275 if gains:
1276 gain = gains[amp.getName()]
1277 else:
1278 gain = amp.getGain()
1279 if invert:
1280 sim /= gain
1281 else:
1282 sim *= gain
1283
1284 try:
1285 yield exp
1286 finally:
1287 if apply:
1288 ccd = exp.getDetector()
1289 for amp in ccd:
1290 sim = image.Factory(image, amp.getBBox() if isTrimmed else amp.getRawBBox())
1291 if gains:
1292 gain = gains[amp.getName()]
1293 else:
1294 gain = amp.getGain()
1295 if invert:
1296 sim *= gain
1297 else:
1298 sim /= gain
1299
1300
1301def attachTransmissionCurve(exposure, opticsTransmission=None, filterTransmission=None,
1302 sensorTransmission=None, atmosphereTransmission=None):
1303 """Attach a TransmissionCurve to an Exposure, given separate curves for
1304 different components.
1305
1306 Parameters
1307 ----------
1308 exposure : `lsst.afw.image.Exposure`
1309 Exposure object to modify by attaching the product of all given
1310 ``TransmissionCurves`` in post-assembly trimmed detector coordinates.
1311 Must have a valid ``Detector`` attached that matches the detector
1312 associated with sensorTransmission.
1313 opticsTransmission : `lsst.afw.image.TransmissionCurve`
1314 A ``TransmissionCurve`` that represents the throughput of the optics,
1315 to be evaluated in focal-plane coordinates.
1316 filterTransmission : `lsst.afw.image.TransmissionCurve`
1317 A ``TransmissionCurve`` that represents the throughput of the filter
1318 itself, to be evaluated in focal-plane coordinates.
1319 sensorTransmission : `lsst.afw.image.TransmissionCurve`
1320 A ``TransmissionCurve`` that represents the throughput of the sensor
1321 itself, to be evaluated in post-assembly trimmed detector coordinates.
1322 atmosphereTransmission : `lsst.afw.image.TransmissionCurve`
1323 A ``TransmissionCurve`` that represents the throughput of the
1324 atmosphere, assumed to be spatially constant.
1325
1326 Returns
1327 -------
1328 combined : `lsst.afw.image.TransmissionCurve`
1329 The TransmissionCurve attached to the exposure.
1330
1331 Notes
1332 -----
1333 All ``TransmissionCurve`` arguments are optional; if none are provided, the
1334 attached ``TransmissionCurve`` will have unit transmission everywhere.
1335 """
1336 combined = afwImage.TransmissionCurve.makeIdentity()
1337 if atmosphereTransmission is not None:
1338 combined *= atmosphereTransmission
1339 if opticsTransmission is not None:
1340 combined *= opticsTransmission
1341 if filterTransmission is not None:
1342 combined *= filterTransmission
1343 detector = exposure.getDetector()
1344 fpToPix = detector.getTransform(fromSys=camGeom.FOCAL_PLANE,
1345 toSys=camGeom.PIXELS)
1346 combined = combined.transformedBy(fpToPix)
1347 if sensorTransmission is not None:
1348 combined *= sensorTransmission
1349 exposure.getInfo().setTransmissionCurve(combined)
1350 return combined
1351
1352
1353def applyGains(exposure, normalizeGains=False, ptcGains=None, isTrimmed=True):
1354 """Scale an exposure by the amplifier gains.
1355
1356 Parameters
1357 ----------
1358 exposure : `lsst.afw.image.Exposure`
1359 Exposure to process. The image is modified.
1360 normalizeGains : `Bool`, optional
1361 If True, then amplifiers are scaled to force the median of
1362 each amplifier to equal the median of those medians.
1363 ptcGains : `dict`[`str`], optional
1364 Dictionary keyed by amp name containing the PTC gains.
1365 isTrimmed : `bool`, optional
1366 Is the input image trimmed?
1367 """
1368 ccd = exposure.getDetector()
1369 ccdImage = exposure.getMaskedImage()
1370
1371 medians = []
1372 for amp in ccd:
1373 if isTrimmed:
1374 sim = ccdImage.Factory(ccdImage, amp.getBBox())
1375 else:
1376 sim = ccdImage.Factory(ccdImage, amp.getRawBBox())
1377 if ptcGains:
1378 sim *= ptcGains[amp.getName()]
1379 else:
1380 sim *= amp.getGain()
1381
1382 if normalizeGains:
1383 medians.append(numpy.median(sim.getImage().getArray()))
1384
1385 if normalizeGains:
1386 median = numpy.median(numpy.array(medians))
1387 for index, amp in enumerate(ccd):
1388 if isTrimmed:
1389 sim = ccdImage.Factory(ccdImage, amp.getBBox())
1390 else:
1391 sim = ccdImage.Factory(ccdImage, amp.getRawBBox())
1392 if medians[index] != 0.0:
1393 sim *= median/medians[index]
1394
1395
1397 """Grow the saturation trails by an amount dependent on the width of the
1398 trail.
1399
1400 Parameters
1401 ----------
1402 mask : `lsst.afw.image.Mask`
1403 Mask which will have the saturated areas grown.
1404 """
1405
1406 extraGrowDict = {}
1407 for i in range(1, 6):
1408 extraGrowDict[i] = 0
1409 for i in range(6, 8):
1410 extraGrowDict[i] = 1
1411 for i in range(8, 10):
1412 extraGrowDict[i] = 3
1413 extraGrowMax = 4
1414
1415 if extraGrowMax <= 0:
1416 return
1417
1418 saturatedBit = mask.getPlaneBitMask("SAT")
1419
1420 xmin, ymin = mask.getBBox().getMin()
1421 width = mask.getWidth()
1422
1423 thresh = afwDetection.Threshold(saturatedBit, afwDetection.Threshold.BITMASK)
1424 fpList = afwDetection.FootprintSet(mask, thresh).getFootprints()
1425
1426 for fp in fpList:
1427 for s in fp.getSpans():
1428 x0, x1 = s.getX0(), s.getX1()
1429
1430 extraGrow = extraGrowDict.get(x1 - x0 + 1, extraGrowMax)
1431 if extraGrow > 0:
1432 y = s.getY() - ymin
1433 x0 -= xmin + extraGrow
1434 x1 -= xmin - extraGrow
1435
1436 if x0 < 0:
1437 x0 = 0
1438 if x1 >= width - 1:
1439 x1 = width - 1
1440
1441 mask.array[y, x0:x1+1] |= saturatedBit
1442
1443
1444def setBadRegions(exposure, badStatistic="MEDIAN"):
1445 """Set all BAD areas of the chip to the average of the rest of the exposure
1446
1447 Parameters
1448 ----------
1449 exposure : `lsst.afw.image.Exposure`
1450 Exposure to mask. The exposure mask is modified.
1451 badStatistic : `str`, optional
1452 Statistic to use to generate the replacement value from the
1453 image data. Allowed values are 'MEDIAN' or 'MEANCLIP'.
1454
1455 Returns
1456 -------
1457 badPixelCount : scalar
1458 Number of bad pixels masked.
1459 badPixelValue : scalar
1460 Value substituted for bad pixels.
1461
1462 Raises
1463 ------
1464 RuntimeError
1465 Raised if `badStatistic` is not an allowed value.
1466 """
1467 if badStatistic == "MEDIAN":
1468 statistic = afwMath.MEDIAN
1469 elif badStatistic == "MEANCLIP":
1470 statistic = afwMath.MEANCLIP
1471 else:
1472 raise RuntimeError("Impossible method %s of bad region correction" % badStatistic)
1473
1474 mi = exposure.getMaskedImage()
1475 mask = mi.getMask()
1476 BAD = mask.getPlaneBitMask("BAD")
1477 INTRP = mask.getPlaneBitMask("INTRP")
1478
1480 sctrl.setAndMask(BAD)
1481 value = afwMath.makeStatistics(mi, statistic, sctrl).getValue()
1482
1483 maskArray = mask.getArray()
1484 imageArray = mi.getImage().getArray()
1485 badPixels = numpy.logical_and((maskArray & BAD) > 0, (maskArray & INTRP) == 0)
1486 imageArray[:] = numpy.where(badPixels, value, imageArray)
1487
1488 return badPixels.sum(), value
1489
1490
1491def checkFilter(exposure, filterList, log):
1492 """Check to see if an exposure is in a filter specified by a list.
1493
1494 The goal of this is to provide a unified filter checking interface
1495 for all filter dependent stages.
1496
1497 Parameters
1498 ----------
1499 exposure : `lsst.afw.image.Exposure`
1500 Exposure to examine.
1501 filterList : `list` [`str`]
1502 List of physical_filter names to check.
1503 log : `logging.Logger`
1504 Logger to handle messages.
1505
1506 Returns
1507 -------
1508 result : `bool`
1509 True if the exposure's filter is contained in the list.
1510 """
1511 if len(filterList) == 0:
1512 return False
1513 thisFilter = exposure.getFilter()
1514 if thisFilter is None:
1515 log.warning("No FilterLabel attached to this exposure!")
1516 return False
1517
1518 thisPhysicalFilter = getPhysicalFilter(thisFilter, log)
1519 if thisPhysicalFilter in filterList:
1520 return True
1521 elif thisFilter.bandLabel in filterList:
1522 if log:
1523 log.warning("Physical filter (%s) should be used instead of band %s for filter configurations"
1524 " (%s)", thisPhysicalFilter, thisFilter.bandLabel, filterList)
1525 return True
1526 else:
1527 return False
1528
1529
1530def getPhysicalFilter(filterLabel, log):
1531 """Get the physical filter label associated with the given filterLabel.
1532
1533 If ``filterLabel`` is `None` or there is no physicalLabel attribute
1534 associated with the given ``filterLabel``, the returned label will be
1535 "Unknown".
1536
1537 Parameters
1538 ----------
1539 filterLabel : `lsst.afw.image.FilterLabel`
1540 The `lsst.afw.image.FilterLabel` object from which to derive the
1541 physical filter label.
1542 log : `logging.Logger`
1543 Logger to handle messages.
1544
1545 Returns
1546 -------
1547 physicalFilter : `str`
1548 The value returned by the physicalLabel attribute of ``filterLabel`` if
1549 it exists, otherwise set to \"Unknown\".
1550 """
1551 if filterLabel is None:
1552 physicalFilter = "Unknown"
1553 log.warning("filterLabel is None. Setting physicalFilter to \"Unknown\".")
1554 else:
1555 try:
1556 physicalFilter = filterLabel.physicalLabel
1557 except RuntimeError:
1558 log.warning("filterLabel has no physicalLabel attribute. Setting physicalFilter to \"Unknown\".")
1559 physicalFilter = "Unknown"
1560 return physicalFilter
1561
1562
1563def countMaskedPixels(maskedIm, maskPlane):
1564 """Count the number of pixels in a given mask plane.
1565
1566 Parameters
1567 ----------
1568 maskedIm : `~lsst.afw.image.MaskedImage`
1569 Masked image to examine.
1570 maskPlane : `str`
1571 Name of the mask plane to examine.
1572
1573 Returns
1574 -------
1575 nPix : `int`
1576 Number of pixels in the requested mask plane.
1577 """
1578 maskBit = maskedIm.mask.getPlaneBitMask(maskPlane)
1579 nPix = numpy.where(numpy.bitwise_and(maskedIm.mask.array, maskBit))[0].flatten().size
1580 return nPix
1581
1582
1583def getExposureGains(exposure):
1584 """Get the per-amplifier gains used for this exposure.
1585
1586 Parameters
1587 ----------
1588 exposure : `lsst.afw.image.Exposure`
1589 The exposure to find gains for.
1590
1591 Returns
1592 -------
1593 gains : `dict` [`str` `float`]
1594 Dictionary of gain values, keyed by amplifier name.
1595 Returns empty dict when detector is None.
1596 """
1597 det = exposure.getDetector()
1598 if det is None:
1599 return dict()
1600
1601 metadata = exposure.getMetadata()
1602 gains = {}
1603 for amp in det:
1604 ampName = amp.getName()
1605 # The key may use the new LSST ISR or the old LSST prefix
1606 if (key1 := f"LSST ISR GAIN {ampName}") in metadata:
1607 gains[ampName] = metadata[key1]
1608 elif (key2 := f"LSST GAIN {ampName}") in metadata:
1609 gains[ampName] = metadata[key2]
1610 else:
1611 gains[ampName] = amp.getGain()
1612 return gains
1613
1614
1616 """Get the per-amplifier read noise used for this exposure.
1617
1618 Parameters
1619 ----------
1620 exposure : `lsst.afw.image.Exposure`
1621 The exposure to find read noise for.
1622
1623 Returns
1624 -------
1625 readnoises : `dict` [`str` `float`]
1626 Dictionary of read noise values, keyed by amplifier name.
1627 Returns empty dict when detector is None.
1628 """
1629 det = exposure.getDetector()
1630 if det is None:
1631 return dict()
1632
1633 metadata = exposure.getMetadata()
1634 readnoises = {}
1635 for amp in det:
1636 ampName = amp.getName()
1637 # The key may use the new LSST ISR or the old LSST prefix
1638 if (key1 := f"LSST ISR READNOISE {ampName}") in metadata:
1639 readnoises[ampName] = metadata[key1]
1640 elif (key2 := f"LSST READNOISE {ampName}") in metadata:
1641 readnoises[ampName] = metadata[key2]
1642 else:
1643 readnoises[ampName] = amp.getReadNoise()
1644 return readnoises
1645
1646
1647def isTrimmedExposure(exposure):
1648 """Check if the unused pixels (pre-/over-scan pixels) have
1649 been trimmed from an exposure.
1650
1651 Parameters
1652 ----------
1653 exposure : `lsst.afw.image.Exposure`
1654 The exposure to check.
1655
1656 Returns
1657 -------
1658 result : `bool`
1659 True if the image is trimmed, else False.
1660 """
1661 return exposure.getDetector().getBBox() == exposure.getBBox()
1662
1663
1664def isTrimmedImage(image, detector):
1665 """Check if the unused pixels (pre-/over-scan pixels) have
1666 been trimmed from an image
1667
1668 Parameters
1669 ----------
1670 image : `lsst.afw.image.Image`
1671 The image to check.
1672 detector : `lsst.afw.cameraGeom.Detector`
1673 The detector associated with the image.
1674
1675 Returns
1676 -------
1677 result : `bool`
1678 True if the image is trimmed, else False.
1679 """
1680 return detector.getBBox() == image.getBBox()
Parameters to control convolution.
A kernel created from an Image.
Definition Kernel.h:471
Pass parameters to a Statistics object.
Definition Statistics.h:83
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)
Definition Statistics.h:361
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, ConvolutionControl const &convolutionControl=ConvolutionControl())
Convolve an Image or MaskedImage with a Kernel, setting pixels of an existing output image.
Property stringToStatisticsProperty(std::string const property)
Conversion function to switch a string to a Property (see Statistics.h)
gainContext(exp, image, apply, gains=None, invert=False, isTrimmed=True)
interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=None, maskNameList=None, useLegacyInterp=True)
setBadRegions(exposure, badStatistic="MEDIAN")
countMaskedPixels(maskedIm, maskPlane)
maskITLEdgeBleed(ccdExposure, badAmpDict, fpCore, itlEdgeBleedSatMinArea=10000, itlEdgeBleedSatMaxArea=100000, itlEdgeBleedThreshold=5000., itlEdgeBleedModelConstant=0.02, saturatedMaskName="SAT", log=None)
attachTransmissionCurve(exposure, opticsTransmission=None, filterTransmission=None, sensorTransmission=None, atmosphereTransmission=None)
flatCorrection(maskedImage, flatMaskedImage, scalingType, userScale=1.0, invert=False, trimToFit=False)
illuminationCorrection(maskedImage, illumMaskedImage, illumScale, trimToFit=True)
growMasks(mask, radius=0, maskNameList=['BAD'], maskValue="BAD")
maskITLSatSag(ccdExposure, fpCore, saturatedMaskName="SAT")
transferFlux(cFunc, fStep, correctionMode=True)
biasCorrection(maskedImage, biasMaskedImage, trimToFit=False)
checkFilter(exposure, filterList, log)
darkCorrection(maskedImage, darkMaskedImage, expScale, darkScale, invert=False, trimToFit=False)
fluxConservingBrighterFatterCorrection(exposure, kernel, maxIter, threshold, applyGain, gains=None, correctionMode=True)
brighterFatterCorrection(exposure, kernel, maxIter, threshold, applyGain, gains=None)
makeThresholdMask(maskedImage, threshold, growFootprints=1, maskName='SAT')
transposeMaskedImage(maskedImage)
interpolateFromMask(maskedImage, fwhm, growSaturatedFootprints=1, maskNameList=['SAT'], fallbackValue=None, useLegacyInterp=True)
isTrimmedImage(image, detector)
maskITLDip(exposure, detectorConfig, maskPlaneNames=["SUSPECT", "ITL_DIP"], log=None)
updateVariance(maskedImage, gain, readNoise, replace=True)
applyGains(exposure, normalizeGains=False, ptcGains=None, isTrimmed=True)
getPhysicalFilter(filterLabel, log)
_applyMaskITLEdgeBleed(ccdExposure, xCore, satLevel, widthSat, itlEdgeBleedThreshold=5000., itlEdgeBleedModelConstant=0.03, saturatedMaskName="SAT", log=None)
saturationCorrection(maskedImage, saturation, fwhm, growFootprints=1, interpolate=True, maskName='SAT', fallbackValue=None, useLegacyInterp=True)
trimToMatchCalibBBox(rawMaskedImage, calibMaskedImage)