1 from __future__
import division
2 from builtins
import range
3 from builtins
import object
35 from lsstDebug
import getDebugFrame
38 from .isrLib
import maskNans
39 from .assembleCcdTask
import AssembleCcdTask
40 from .fringe
import FringeTask
43 from contextlib
import contextmanager
47 doBias = pexConfig.Field(
49 doc=
"Apply bias frame correction?",
52 doDark = pexConfig.Field(
54 doc=
"Apply dark frame correction?",
57 doFlat = pexConfig.Field(
59 doc=
"Apply flat field correction?",
62 doFringe = pexConfig.Field(
64 doc=
"Apply fringe correction?",
67 doWrite = pexConfig.Field(
69 doc=
"Persist postISRCCD?",
72 assembleCcd = pexConfig.ConfigurableField(
73 target=AssembleCcdTask,
74 doc=
"CCD assembly task",
76 gain = pexConfig.Field(
78 doc=
"The gain to use if no Detector is present in the Exposure (ignored if NaN)",
81 readNoise = pexConfig.Field(
83 doc=
"The read noise to use if no Detector is present in the Exposure",
86 saturation = pexConfig.Field(
88 doc=
"The saturation level to use if no Detector is present in the Exposure (ignored if NaN)",
91 fringeAfterFlat = pexConfig.Field(
93 doc=
"Do fringe subtraction after flat-fielding?",
96 fringe = pexConfig.ConfigurableField(
98 doc=
"Fringe subtraction task",
100 fwhm = pexConfig.Field(
102 doc=
"FWHM of PSF (arcsec)",
105 saturatedMaskName = pexConfig.Field(
107 doc=
"Name of mask plane to use in saturation detection and interpolation",
110 suspectMaskName = pexConfig.Field(
112 doc=
"Name of mask plane to use for suspect pixels",
115 flatScalingType = pexConfig.ChoiceField(
117 doc=
"The method for scaling the flat on the fly.",
120 "USER":
"Scale by flatUserScale",
121 "MEAN":
"Scale by the inverse of the mean",
122 "MEDIAN":
"Scale by the inverse of the median",
125 flatUserScale = pexConfig.Field(
127 doc=
"If flatScalingType is 'USER' then scale flat by this amount; ignored otherwise",
130 overscanFitType = pexConfig.ChoiceField(
132 doc=
"The method for fitting the overscan bias level.",
135 "POLY":
"Fit ordinary polynomial to the longest axis of the overscan region",
136 "CHEB":
"Fit Chebyshev polynomial to the longest axis of the overscan region",
137 "LEG":
"Fit Legendre polynomial to the longest axis of the overscan region",
138 "NATURAL_SPLINE":
"Fit natural spline to the longest axis of the overscan region",
139 "CUBIC_SPLINE":
"Fit cubic spline to the longest axis of the overscan region",
140 "AKIMA_SPLINE":
"Fit Akima spline to the longest axis of the overscan region",
141 "MEAN":
"Correct using the mean of the overscan region",
142 "MEDIAN":
"Correct using the median of the overscan region",
145 overscanOrder = pexConfig.Field(
147 doc=(
"Order of polynomial or to fit if overscan fit type is a polynomial, " +
148 "or number of spline knots if overscan fit type is a spline."),
151 overscanRej = pexConfig.Field(
153 doc=
"Rejection threshold (sigma) for collapsing overscan before fit",
156 growSaturationFootprintSize = pexConfig.Field(
158 doc=
"Number of pixels by which to grow the saturation footprints",
161 fluxMag0T1 = pexConfig.Field(
163 doc=
"The approximate flux of a zero-magnitude object in a one-second exposure",
166 setGainAssembledCcd = pexConfig.Field(
168 doc=
"update exposure metadata in the assembled ccd to reflect the "
169 "effective gain of the assembled chip",
172 keysToRemoveFromAssembledCcd = pexConfig.ListField(
174 doc=
"fields to remove from the metadata of the assembled ccd.",
177 doAssembleIsrExposures = pexConfig.Field(
180 doc=
"Assemble amp-level calibration exposures into ccd-level exposure?"
182 doAssembleCcd = pexConfig.Field(
185 doc=
"Assemble amp-level exposures into a ccd-level exposure?"
187 doLinearize = pexConfig.Field(
189 doc=
"Correct for nonlinearity of the detector's response?",
192 doBrighterFatter = pexConfig.Field(
195 doc=
"Apply the brighter fatter correction"
197 brighterFatterKernelFile = pexConfig.Field(
200 doc=
"Kernel file used for the brighter fatter correction"
202 brighterFatterMaxIter = pexConfig.Field(
205 doc=
"Maximum number of iterations for the brighter fatter correction"
207 brighterFatterThreshold = pexConfig.Field(
210 doc=
"Threshold used to stop iterating the brighter fatter correction. It is the "
211 " absolute value of the difference between the current corrected image and the one"
212 " from the previous iteration summed over all the pixels."
214 brighterFatterApplyGain = pexConfig.Field(
217 doc=
"Should the gain be applied when applying the brighter fatter correction?"
219 datasetType = pexConfig.Field(
221 doc=
"Dataset type for input data; users will typically leave this alone, "
222 "but camera-specific ISR tasks will override it",
225 fallbackFilterName = pexConfig.Field(dtype=str,
226 doc=
"Fallback default filter name for calibrations", optional=
True)
240 \brief Apply common instrument signature correction algorithms to a raw frame.
242 \section ip_isr_isr_Contents Contents
244 - \ref ip_isr_isr_Purpose
245 - \ref ip_isr_isr_Initialize
247 - \ref ip_isr_isr_Config
248 - \ref ip_isr_isr_Debug
249 - \ref ip_isr_isr_Example
251 \section ip_isr_isr_Purpose Description
253 The process for correcting imaging data is very similar from camera to camera.
254 This task provides a vanilla implementation of doing these corrections, including
255 the ability to turn certain corrections off if they are not needed.
256 The inputs to the primary method, run, are a raw exposure to be corrected and the
257 calibration data products. The raw input is a single chip sized mosaic of all amps
258 including overscans and other non-science pixels.
259 The method runDataRef() is intended for use by a lsst.pipe.base.cmdLineTask.CmdLineTask
260 and takes as input only a daf.persistence.butlerSubset.ButlerDataRef.
261 This task may not meet all needs and it is expected that it will be subclassed for
262 specific applications.
264 \section ip_isr_isr_Initialize Task initialization
266 \copydoc \_\_init\_\_
268 \section ip_isr_isr_IO Inputs/Outputs to the run method
272 \section ip_isr_isr_Config Configuration parameters
274 See \ref IsrTaskConfig
276 \section ip_isr_isr_Debug Debug variables
278 The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
279 flag \c --debug, \c -d to import \b debug.py from your \c PYTHONPATH; see <a
280 href="http://lsst-web.ncsa.illinois.edu/~buildbot/doxygen/x_masterDoxyDoc/base_debug.html">
281 Using lsstDebug to control debugging output</a> for more about \b debug.py files.
283 The available variables in IsrTask are:
286 <DD> A dictionary containing debug point names as keys with frame number as value. Valid keys are:
289 <DD> display exposure after ISR has been applied
293 For example, put something like
297 di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
298 if name == "lsst.ip.isr.isrTask":
299 di.display = {'postISRCCD':2}
301 lsstDebug.Info = DebugInfo
303 into your debug.py file and run the commandline task with the \c --debug flag.
305 \section ip_isr_isr_Example A complete example of using IsrTask
307 This code is in runIsrTask.py in the examples directory and can be run as \em e.g.
309 python examples/runIsrTask.py
311 # optional arguments:
312 # --debug, -d Load debug.py?
313 # --ds9 Display the result?
314 # --write Write the result?
318 Stepping through the example:
320 \dontinclude runIsrTask.py
321 Import the task. There are other imports. Read the source file for more info.
323 \skipline exampleUtils
325 Create the raw input data with the help of some utilities in \link exampleUtils.py \endlink
326 also in the examples directory.
327 \dontinclude runIsrTask.py
328 We will only do overscan, dark and flat correction.
329 The data are constructed by hand so that all effects will be corrected for essentially perfectly.
332 The above numbers can be changed to modify the gradient in the flat, for example.
333 For the parameters in this particular example,
334 the image after ISR will be a constant 5000 counts
335 (with some variation in floating point represenation).
337 \note Alternatively, images can be read from disk, either using the Butler or manually:
339 import lsst.afw.image as afwImage
340 darkExposure = afwImage.ExposureF("/path/to/dark.fits")
341 flatExposure = afwImage.ExposureF("/path/to/flat.fits")
342 rawExposure = afwImage.ExposureF("/path/to/raw.fits")
344 In order to perform overscanCorrection IsrTask.run() requires Exposures which have
345 a \link lsst.afw.cameraGeom.Detector \endlink.
346 Detector objects describe details such as data dimensions, number of amps,
347 orientation and overscan dimensions.
348 If requesting images from the Butler, Exposures will automatically have detector information.
349 If running IsrTask on arbitrary images from a camera without an obs_ package,
350 a lsst.afw.cameraGeom.Detector can be generated using lsst.afw.cameraGeom.fitsUtils.DetectorBuilder
353 rawExposure.setDetector(myDetectorObject)
355 See \link lsst.afw.cameraGeom.fitsUtils.DetectorBuilder \endlink for more details.
357 \note The updateVariance and saturationDetection steps are not run for Exposures
358 without a \link lsst.afw.cameraGeom.Detector \endlink, unless \ref IsrTaskConfig.gain,
359 \ref IsrTaskConfig.readNoise,
360 and/or \ref IsrTaskConfig.saturation
361 are set in the config, in which case they are applied to the entire image rather than per amp.
363 \dontinclude runIsrTask.py
364 Construct the task and set some config parameters. Specifically, we don't want to
365 do zero or fringe correction. We also don't want the assembler messing with the gain.
367 @until config=isrConfig
369 Finally, run the exposures through ISR.
370 \skipline isrTask.run
374 ConfigClass = IsrTaskConfig
378 '''!Constructor for IsrTask
379 \param[in] *args -- a list of positional arguments passed on to the Task constructor
380 \param[in] **kwargs -- a dictionary of keyword arguments passed on to the Task constructor
381 Call the lsst.pipe.base.task.Task.__init__ method
382 Then setup the assembly and fringe correction subtasks
384 pipeBase.Task.__init__(self, *args, **kwargs)
385 self.makeSubtask(
"assembleCcd")
386 self.makeSubtask(
"fringe")
389 """!Retrieve necessary frames for instrument signature removal
390 \param[in] dataRef -- a daf.persistence.butlerSubset.ButlerDataRef
391 of the detector data to be processed
392 \param[in] rawExposure -- a reference raw exposure that will later be
393 corrected with the retrieved calibration data;
394 should not be modified in this method.
395 \return a pipeBase.Struct with fields containing kwargs expected by run()
396 - bias: exposure of bias frame
397 - dark: exposure of dark frame
398 - flat: exposure of flat field
399 - defects: list of detects
400 - fringeStruct: a pipeBase.Struct with field fringes containing
401 exposure of fringe frame or list of fringe exposure
403 ccd = rawExposure.getDetector()
405 biasExposure = self.
getIsrExposure(dataRef,
"bias")
if self.config.doBias
else None
407 linearizer = dataRef.get(
"linearizer", immediate=
True)
if self.
doLinearize(ccd)
else None
408 darkExposure = self.
getIsrExposure(dataRef,
"dark")
if self.config.doDark
else None
409 flatExposure = self.
getIsrExposure(dataRef,
"flat")
if self.config.doFlat
else None
410 brighterFatterKernel = dataRef.get(
"brighterFatterKernel")
if self.config.doBrighterFatter
else None
412 defectList = dataRef.get(
"defects")
414 if self.config.doFringe
and self.fringe.checkFilter(rawExposure):
415 fringeStruct = self.fringe.readFringes(dataRef, assembler=self.assembleCcd
416 if self.config.doAssembleIsrExposures
else None)
418 fringeStruct = pipeBase.Struct(fringes=
None)
421 return pipeBase.Struct(bias=biasExposure,
422 linearizer=linearizer,
426 fringes=fringeStruct,
427 bfKernel=brighterFatterKernel
431 def run(self, ccdExposure, bias=None, linearizer=None, dark=None, flat=None, defects=None,
432 fringes=
None, bfKernel=
None):
433 """!Perform instrument signature removal on an exposure
436 - Detect saturation, apply overscan correction, bias, dark and flat
437 - Perform CCD assembly
438 - Interpolate over defects, saturated pixels and all NaNs
440 \param[in] ccdExposure -- lsst.afw.image.exposure of detector data
441 \param[in] bias -- exposure of bias frame
442 \param[in] linearizer -- linearizing functor; a subclass of lsst.ip.isr.LinearizeBase
443 \param[in] dark -- exposure of dark frame
444 \param[in] flat -- exposure of flatfield
445 \param[in] defects -- list of detects
446 \param[in] fringes -- a pipeBase.Struct with field fringes containing
447 exposure of fringe frame or list of fringe exposure
448 \param[in] bfKernel -- kernel for brighter-fatter correction
450 \return a pipeBase.Struct with field:
455 if isinstance(ccdExposure, ButlerDataRef):
458 ccd = ccdExposure.getDetector()
461 if self.config.doBias
and bias
is None:
462 raise RuntimeError(
"Must supply a bias exposure if config.doBias True")
464 raise RuntimeError(
"Must supply a linearizer if config.doBias True")
465 if self.config.doDark
and dark
is None:
466 raise RuntimeError(
"Must supply a dark exposure if config.doDark True")
467 if self.config.doFlat
and flat
is None:
468 raise RuntimeError(
"Must supply a flat exposure if config.doFlat True")
469 if self.config.doBrighterFatter
and bfKernel
is None:
470 raise RuntimeError(
"Must supply a kernel if config.doBrighterFatter True")
472 fringes = pipeBase.Struct(fringes=
None)
473 if self.config.doFringe
and not isinstance(fringes, pipeBase.Struct):
474 raise RuntimeError(
"Must supply fringe exposure as a pipeBase.Struct")
476 defects = []
if defects
is None else defects
481 assert not self.config.doAssembleCcd,
"You need a Detector to run assembleCcd"
482 ccd = [
FakeAmp(ccdExposure, self.config)]
486 if ccdExposure.getBBox().contains(amp.getBBox()):
491 if self.config.doAssembleCcd:
492 ccdExposure = self.assembleCcd.assembleCcd(ccdExposure)
494 if self.config.doBias:
498 linearizer(image=ccdExposure.getMaskedImage().getImage(), detector=ccd, log=self.log)
500 if self.config.doBrighterFatter:
502 self.config.brighterFatterMaxIter,
503 self.config.brighterFatterThreshold,
504 self.config.brighterFatterApplyGain,
507 if self.config.doDark:
512 if ccdExposure.getBBox().contains(amp.getBBox()):
513 ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
516 if self.config.doFringe
and not self.config.fringeAfterFlat:
517 self.fringe.run(ccdExposure, **fringes.getDict())
519 if self.config.doFlat:
528 if self.config.doFringe
and self.config.fringeAfterFlat:
529 self.fringe.run(ccdExposure, **fringes.getDict())
531 ccdExposure.getCalib().setFluxMag0(self.config.fluxMag0T1 * ccdExposure.getCalib().getExptime())
537 return pipeBase.Struct(
538 exposure=ccdExposure,
543 """!Perform instrument signature removal on a ButlerDataRef of a Sensor
545 - Read in necessary detrending/isr/calibration data
546 - Process raw exposure in run()
547 - Persist the ISR-corrected exposure as "postISRCCD" if config.doWrite is True
549 \param[in] sensorRef -- daf.persistence.butlerSubset.ButlerDataRef of the
550 detector data to be processed
551 \return a pipeBase.Struct with fields:
552 - exposure: the exposure after application of ISR
554 self.log.info(
"Performing ISR on sensor %s" % (sensorRef.dataId))
555 ccdExposure = sensorRef.get(
'raw')
558 result = self.
run(ccdExposure, **isrData.getDict())
560 if self.config.doWrite:
561 sensorRef.put(result.exposure,
"postISRCCD")
566 """Convert an exposure from uint16 to float, set variance plane to 1 and mask plane to 0
568 if isinstance(exposure, afwImage.ExposureF):
571 if not hasattr(exposure,
"convertF"):
572 raise RuntimeError(
"Unable to convert exposure (%s) to float" % type(exposure))
574 newexposure = exposure.convertF()
575 maskedImage = newexposure.getMaskedImage()
576 varArray = maskedImage.getVariance().getArray()
578 maskArray = maskedImage.getMask().getArray()
583 """!Apply bias correction in place
585 \param[in,out] exposure exposure to process
586 \param[in] biasExposure bias exposure of same size as exposure
588 isr.biasCorrection(exposure.getMaskedImage(), biasExposure.getMaskedImage())
591 """!Apply dark correction in place
593 \param[in,out] exposure exposure to process
594 \param[in] darkExposure dark exposure of same size as exposure
596 darkCalib = darkExposure.getCalib()
598 maskedImage=exposure.getMaskedImage(),
599 darkMaskedImage=darkExposure.getMaskedImage(),
600 expScale=exposure.getCalib().getExptime(),
601 darkScale=darkCalib.getExptime(),
605 """!Is linearization wanted for this detector?
607 Checks config.doLinearize and the linearity type of the first amplifier.
609 \param[in] detector detector information (an lsst.afw.cameraGeom.Detector)
611 return self.config.doLinearize
and \
612 detector.getAmpInfoCatalog()[0].getLinearityType() != NullLinearityType
615 """!Set the variance plane based on the image plane, plus amplifier gain and read noise
617 \param[in,out] ampExposure exposure to process
618 \param[in] amp amplifier detector information
620 if not math.isnan(amp.getGain()):
622 maskedImage=ampExposure.getMaskedImage(),
624 readNoise=amp.getReadNoise(),
628 """!Apply flat correction in place
630 \param[in,out] exposure exposure to process
631 \param[in] flatExposure flatfield exposure same size as exposure
634 maskedImage=exposure.getMaskedImage(),
635 flatMaskedImage=flatExposure.getMaskedImage(),
636 scalingType=self.config.flatScalingType,
637 userScale=self.config.flatUserScale,
641 """!Retrieve a calibration dataset for removing instrument signature
643 \param[in] dataRef data reference for exposure
644 \param[in] datasetType type of dataset to retrieve (e.g. 'bias', 'flat')
645 \param[in] immediate if True, disable butler proxies to enable error
646 handling within this routine
650 exp = dataRef.get(datasetType, immediate=immediate)
651 except Exception
as exc1:
652 if not self.config.fallbackFilterName:
653 raise RuntimeError(
"Unable to retrieve %s for %s: %s" % (datasetType, dataRef.dataId, exc1))
655 exp = dataRef.get(datasetType, filter=self.config.fallbackFilterName, immediate=immediate)
656 except Exception
as exc2:
657 raise RuntimeError(
"Unable to retrieve %s for %s, even with fallback filter %s: %s AND %s" %
658 (datasetType, dataRef.dataId, self.config.fallbackFilterName, exc1, exc2))
659 self.log.warn(
"Using fallback calibration from filter %s" % self.config.fallbackFilterName)
661 if self.config.doAssembleIsrExposures:
662 exp = self.assembleCcd.assembleCcd(exp)
666 """!Detect saturated pixels and mask them using mask plane config.saturatedMaskName, in place
668 \param[in,out] exposure exposure to process; only the amp DataSec is processed
669 \param[in] amp amplifier device data
671 if not math.isnan(amp.getSaturation()):
672 maskedImage = exposure.getMaskedImage()
673 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
674 isr.makeThresholdMask(
675 maskedImage=dataView,
676 threshold=amp.getSaturation(),
678 maskName=self.config.saturatedMaskName,
682 """!Interpolate over saturated pixels, in place
684 \param[in,out] ccdExposure exposure to process
687 - Call saturationDetection first, so that saturated pixels have been identified in the "SAT" mask.
688 - Call this after CCD assembly, since saturated regions may cross amplifier boundaries
690 isr.interpolateFromMask(
691 maskedImage=ccdExposure.getMaskedImage(),
692 fwhm=self.config.fwhm,
693 growFootprints=self.config.growSaturationFootprintSize,
694 maskName=self.config.saturatedMaskName,
698 """!Detect suspect pixels and mask them using mask plane config.suspectMaskName, in place
700 Suspect pixels are pixels whose value is greater than amp.getSuspectLevel().
701 This is intended to indicate pixels that may be affected by unknown systematics;
702 for example if non-linearity corrections above a certain level are unstable
703 then that would be a useful value for suspectLevel. A value of `nan` indicates
704 that no such level exists and no pixels are to be masked as suspicious.
706 \param[in,out] exposure exposure to process; only the amp DataSec is processed
707 \param[in] amp amplifier device data
709 suspectLevel = amp.getSuspectLevel()
710 if math.isnan(suspectLevel):
713 maskedImage = exposure.getMaskedImage()
714 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
715 isr.makeThresholdMask(
716 maskedImage=dataView,
717 threshold=suspectLevel,
719 maskName=self.config.suspectMaskName,
723 """!Mask defects using mask plane "BAD" and interpolate over them, in place
725 \param[in,out] ccdExposure exposure to process
726 \param[in] defectBaseList a list of defects to mask and interpolate
728 \warning: call this after CCD assembly, since defects may cross amplifier boundaries
730 maskedImage = ccdExposure.getMaskedImage()
731 defectList = measAlg.DefectListT()
732 for d
in defectBaseList:
735 defectList.append(nd)
736 isr.maskPixelsFromDefectList(maskedImage, defectList, maskName=
'BAD')
737 isr.interpolateDefectList(
738 maskedImage=maskedImage,
739 defectList=defectList,
740 fwhm=self.config.fwhm,
744 """!Mask NaNs using mask plane "UNMASKEDNAN" and interpolate over them, in place
746 We mask and interpolate over all NaNs, including those
747 that are masked with other bits (because those may or may
748 not be interpolated over later, and we want to remove all
749 NaNs). Despite this behaviour, the "UNMASKEDNAN" mask plane
750 is used to preserve the historical name.
752 \param[in,out] exposure exposure to process
754 maskedImage = exposure.getMaskedImage()
757 maskedImage.getMask().addMaskPlane(
"UNMASKEDNAN")
758 maskVal = maskedImage.getMask().getPlaneBitMask(
"UNMASKEDNAN")
759 numNans =
maskNans(maskedImage, maskVal)
760 self.metadata.set(
"NUMNANS", numNans)
764 self.log.warn(
"There were %i unmasked NaNs", numNans)
765 nanDefectList = isr.getDefectListFromMask(
766 maskedImage=maskedImage,
767 maskName=
'UNMASKEDNAN',
770 isr.interpolateDefectList(
771 maskedImage=exposure.getMaskedImage(),
772 defectList=nanDefectList,
773 fwhm=self.config.fwhm,
777 """!Apply overscan correction, in place
779 \param[in,out] exposure exposure to process; must include both DataSec and BiasSec pixels
780 \param[in] amp amplifier device data
782 if not amp.getHasRawInfo():
783 raise RuntimeError(
"This method must be executed on an amp with raw information.")
785 if amp.getRawHorizontalOverscanBBox().isEmpty():
786 self.log.info(
"No Overscan region. Not performing Overscan Correction.")
789 maskedImage = exposure.getMaskedImage()
790 dataView = maskedImage.Factory(maskedImage, amp.getRawDataBBox())
792 expImage = exposure.getMaskedImage().getImage()
793 overscanImage = expImage.Factory(expImage, amp.getRawHorizontalOverscanBBox())
795 isr.overscanCorrection(
796 ampMaskedImage=dataView,
797 overscanImage=overscanImage,
798 fitType=self.config.overscanFitType,
799 order=self.config.overscanOrder,
800 collapseRej=self.config.overscanRej,
804 """!Set the valid polygon as the intersection of fpPolygon and the ccd corners
806 \param[in,out] exposure exposure to process
807 \param[in] fpPolygon Polygon in focal plane coordinates
810 ccd = ccdExposure.getDetector()
811 fpCorners = ccd.getCorners(FOCAL_PLANE)
812 ccdPolygon =
Polygon(fpCorners)
815 intersect = ccdPolygon.intersectionSingle(fpPolygon)
818 ccdPoints = [ccd.transform(ccd.makeCameraPoint(x, FOCAL_PLANE), PIXELS).getPoint()
for x
in intersect]
819 validPolygon =
Polygon(ccdPoints)
820 ccdExposure.getInfo().setValidPolygon(validPolygon)
823 """Apply brighter fatter correction in place for the image
825 This correction takes a kernel that has been derived from flat field images to
826 redistribute the charge. The gradient of the kernel is the deflection
827 field due to the accumulated charge.
829 Given the original image I(x) and the kernel K(x) we can compute the corrected image Ic(x)
830 using the following equation:
832 Ic(x) = I(x) + 0.5*d/dx(I(x)*d/dx(int( dy*K(x-y)*I(y))))
834 To evaluate the derivative term we expand it as follows:
836 0.5 * ( d/dx(I(x))*d/dx(int(dy*K(x-y)*I(y))) + I(x)*d^2/dx^2(int(dy* K(x-y)*I(y))) )
838 Because we use the measured counts instead of the incident counts we apply the correction
839 iteratively to reconstruct the original counts and the correction. We stop iterating when the
840 summed difference between the current corrected image and the one from the previous iteration
841 is below the threshold. We do not require convergence because the number of iterations is
842 too large a computational cost. How we define the threshold still needs to be evaluated, the
843 current default was shown to work reasonably well on a small set of images. For more information
844 on the method see DocuShare Document-19407.
846 The edges as defined by the kernel are not corrected because they have spurious values
847 due to the convolution.
849 self.log.info(
"Applying brighter fatter correction")
851 image = exposure.getMaskedImage().getImage()
856 kLx = numpy.shape(kernel)[0]
857 kLy = numpy.shape(kernel)[1]
858 kernelImage = afwImage.ImageD(kernel.astype(numpy.float64))
859 tempImage = image.clone()
861 nanIndex = numpy.isnan(tempImage.getArray())
862 tempImage.getArray()[nanIndex] = 0.
864 outImage = afwImage.ImageF(image.getDimensions())
865 corr = numpy.zeros_like(image.getArray())
866 prev_image = numpy.zeros_like(image.getArray())
878 for iteration
in range(maxIter):
881 tmpArray = tempImage.getArray()
882 outArray = outImage.getArray()
885 gradTmp = numpy.gradient(tmpArray[startY:endY, startX:endX])
886 gradOut = numpy.gradient(outArray[startY:endY, startX:endX])
887 first = (gradTmp[0]*gradOut[0] + gradTmp[1]*gradOut[1])
890 diffOut20 = numpy.gradient(gradOut[0])
891 diffOut21 = numpy.gradient(gradOut[1])
892 second = tmpArray[startY:endY, startX:endX]*(diffOut20[0] + diffOut21[1])
894 corr[startY:endY, startX:endX] = 0.5*(first + second)
897 tmpArray[:, :] = image.getArray()[:, :]
898 tmpArray[nanIndex] = 0.
899 tmpArray[startY:endY, startX:endX] += corr[startY:endY, startX:endX]
902 diff = numpy.sum(numpy.abs(prev_image - tmpArray))
906 prev_image[:, :] = tmpArray[:, :]
908 if iteration == maxIter - 1:
909 self.log.warn(
"Brighter fatter correction did not converge, final difference %f" % diff)
911 self.log.info(
"Finished brighter fatter in %d iterations" % (iteration))
912 image.getArray()[startY:endY, startX:endX] += corr[startY:endY, startX:endX]
916 """Context manager that applies and removes gain
919 ccd = exp.getDetector()
921 sim = image.Factory(image, amp.getBBox())
928 ccd = exp.getDetector()
930 sim = image.Factory(image, amp.getBBox())
935 """A Detector-like object that supports returning gain and saturation level"""
938 self.
_bbox = exposure.getBBox(afwImage.LOCAL)
def doLinearize
Is linearization wanted for this detector?
def getIsrExposure
Retrieve a calibration dataset for removing instrument signature.
def maskAndInterpNan
Mask NaNs using mask plane "UNMASKEDNAN" and interpolate over them, in place.
Parameters to control convolution.
def flatCorrection
Apply flat correction in place.
def run
Perform instrument signature removal on an exposure.
def __init__
Constructor for IsrTask.
Apply common instrument signature correction algorithms to a raw frame.
def runDataRef
Perform instrument signature removal on a ButlerDataRef of a Sensor.
An integer coordinate rectangle.
def suspectDetection
Detect suspect pixels and mask them using mask plane config.suspectMaskName, in place.
def updateVariance
Set the variance plane based on the image plane, plus amplifier gain and read noise.
def maskAndInterpDefect
Mask defects using mask plane "BAD" and interpolate over them, in place.
def setValidPolygonIntersect
Set the valid polygon as the intersection of fpPolygon and the ccd corners.
def overscanCorrection
Apply overscan correction, in place.
_RawHorizontalOverscanBBox
def saturationDetection
Detect saturated pixels and mask them using mask plane config.saturatedMaskName, in place...
def darkCorrection
Apply dark correction in place.
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, bool doNormalize, bool doCopyEdge=false)
Old, deprecated version of convolve.
size_t maskNans(afw::image::MaskedImage< PixelT > const &mi, afw::image::MaskPixel maskVal, afw::image::MaskPixel allow=0)
Encapsulate information about a bad portion of a detector.
def saturationInterpolation
Interpolate over saturated pixels, in place.
A kernel created from an Image.
def getRawHorizontalOverscanBBox
def brighterFatterCorrection
def biasCorrection
Apply bias correction in place.
def readIsrData
Retrieve necessary frames for instrument signature removal.