1 from __future__
import absolute_import, division, print_function
2 from builtins
import range
3 from builtins
import object
35 from lsstDebug
import getDebugFrame
37 from .
import isrFunctions
38 from .assembleCcdTask
import AssembleCcdTask
39 from .fringe
import FringeTask
42 from contextlib
import contextmanager
43 from .isr
import maskNans
44 from .crosstalk
import CrosstalkTask
48 doBias = pexConfig.Field(
50 doc=
"Apply bias frame correction?",
53 doDark = pexConfig.Field(
55 doc=
"Apply dark frame correction?",
58 doFlat = pexConfig.Field(
60 doc=
"Apply flat field correction?",
63 doFringe = pexConfig.Field(
65 doc=
"Apply fringe correction?",
68 doDefect = pexConfig.Field(
70 doc=
"Apply correction for CCD defects, e.g. hot pixels?",
73 doWrite = pexConfig.Field(
75 doc=
"Persist postISRCCD?",
78 assembleCcd = pexConfig.ConfigurableField(
79 target=AssembleCcdTask,
80 doc=
"CCD assembly task",
82 gain = pexConfig.Field(
84 doc=
"The gain to use if no Detector is present in the Exposure (ignored if NaN)",
87 readNoise = pexConfig.Field(
89 doc=
"The read noise to use if no Detector is present in the Exposure",
92 saturation = pexConfig.Field(
94 doc=
"The saturation level to use if no Detector is present in the Exposure (ignored if NaN)",
97 fringeAfterFlat = pexConfig.Field(
99 doc=
"Do fringe subtraction after flat-fielding?",
102 fringe = pexConfig.ConfigurableField(
104 doc=
"Fringe subtraction task",
106 fwhm = pexConfig.Field(
108 doc=
"FWHM of PSF (arcsec)",
111 saturatedMaskName = pexConfig.Field(
113 doc=
"Name of mask plane to use in saturation detection and interpolation",
116 suspectMaskName = pexConfig.Field(
118 doc=
"Name of mask plane to use for suspect pixels",
121 flatScalingType = pexConfig.ChoiceField(
123 doc=
"The method for scaling the flat on the fly.",
126 "USER":
"Scale by flatUserScale",
127 "MEAN":
"Scale by the inverse of the mean",
128 "MEDIAN":
"Scale by the inverse of the median",
131 flatUserScale = pexConfig.Field(
133 doc=
"If flatScalingType is 'USER' then scale flat by this amount; ignored otherwise",
136 overscanFitType = pexConfig.ChoiceField(
138 doc=
"The method for fitting the overscan bias level.",
141 "POLY":
"Fit ordinary polynomial to the longest axis of the overscan region",
142 "CHEB":
"Fit Chebyshev polynomial to the longest axis of the overscan region",
143 "LEG":
"Fit Legendre polynomial to the longest axis of the overscan region",
144 "NATURAL_SPLINE":
"Fit natural spline to the longest axis of the overscan region",
145 "CUBIC_SPLINE":
"Fit cubic spline to the longest axis of the overscan region",
146 "AKIMA_SPLINE":
"Fit Akima spline to the longest axis of the overscan region",
147 "MEAN":
"Correct using the mean of the overscan region",
148 "MEDIAN":
"Correct using the median of the overscan region",
151 overscanOrder = pexConfig.Field(
153 doc=(
"Order of polynomial or to fit if overscan fit type is a polynomial, " +
154 "or number of spline knots if overscan fit type is a spline."),
157 overscanRej = pexConfig.Field(
159 doc=
"Rejection threshold (sigma) for collapsing overscan before fit",
162 growSaturationFootprintSize = pexConfig.Field(
164 doc=
"Number of pixels by which to grow the saturation footprints",
167 doSaturationInterpolation = pexConfig.Field(
169 doc=
"Perform interpolation over pixels masked as saturated?",
172 doNanInterpAfterFlat = pexConfig.Field(
174 doc=(
"If True, ensure we interpolate NaNs after flat-fielding, even if we " 175 "also have to interpolate them before flat-fielding."),
178 fluxMag0T1 = pexConfig.Field(
180 doc=
"The approximate flux of a zero-magnitude object in a one-second exposure",
183 keysToRemoveFromAssembledCcd = pexConfig.ListField(
185 doc=
"fields to remove from the metadata of the assembled ccd.",
188 doAssembleIsrExposures = pexConfig.Field(
191 doc=
"Assemble amp-level calibration exposures into ccd-level exposure?" 193 doAssembleCcd = pexConfig.Field(
196 doc=
"Assemble amp-level exposures into a ccd-level exposure?" 198 expectWcs = pexConfig.Field(
201 doc=
"Expect input science images to have a WCS (set False for e.g. spectrographs)" 203 doLinearize = pexConfig.Field(
205 doc=
"Correct for nonlinearity of the detector's response?",
208 doCrosstalk = pexConfig.Field(
210 doc=
"Apply intra-CCD crosstalk correction?",
213 crosstalk = pexConfig.ConfigurableField(
214 target=CrosstalkTask,
215 doc=
"Intra-CCD crosstalk correction",
217 doBrighterFatter = pexConfig.Field(
220 doc=
"Apply the brighter fatter correction" 222 brighterFatterKernelFile = pexConfig.Field(
225 doc=
"Kernel file used for the brighter fatter correction" 227 brighterFatterMaxIter = pexConfig.Field(
230 doc=
"Maximum number of iterations for the brighter fatter correction" 232 brighterFatterThreshold = pexConfig.Field(
235 doc=
"Threshold used to stop iterating the brighter fatter correction. It is the " 236 " absolute value of the difference between the current corrected image and the one" 237 " from the previous iteration summed over all the pixels." 239 brighterFatterApplyGain = pexConfig.Field(
242 doc=
"Should the gain be applied when applying the brighter fatter correction?" 244 datasetType = pexConfig.Field(
246 doc=
"Dataset type for input data; users will typically leave this alone, " 247 "but camera-specific ISR tasks will override it",
250 fallbackFilterName = pexConfig.Field(dtype=str,
251 doc=
"Fallback default filter name for calibrations", optional=
True)
265 \brief Apply common instrument signature correction algorithms to a raw frame. 267 \section ip_isr_isr_Contents Contents 269 - \ref ip_isr_isr_Purpose 270 - \ref ip_isr_isr_Initialize 272 - \ref ip_isr_isr_Config 273 - \ref ip_isr_isr_Debug 276 \section ip_isr_isr_Purpose Description 278 The process for correcting imaging data is very similar from camera to camera. 279 This task provides a vanilla implementation of doing these corrections, including 280 the ability to turn certain corrections off if they are not needed. 281 The inputs to the primary method, run, are a raw exposure to be corrected and the 282 calibration data products. The raw input is a single chip sized mosaic of all amps 283 including overscans and other non-science pixels. 284 The method runDataRef() is intended for use by a lsst.pipe.base.cmdLineTask.CmdLineTask 285 and takes as input only a daf.persistence.butlerSubset.ButlerDataRef. 286 This task may not meet all needs and it is expected that it will be subclassed for 287 specific applications. 289 \section ip_isr_isr_Initialize Task initialization 291 \copydoc \_\_init\_\_ 293 \section ip_isr_isr_IO Inputs/Outputs to the run method 297 \section ip_isr_isr_Config Configuration parameters 299 See \ref IsrTaskConfig 301 \section ip_isr_isr_Debug Debug variables 303 The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a 304 flag \c --debug, \c -d to import \b debug.py from your \c PYTHONPATH; see <a 305 href="http://lsst-web.ncsa.illinois.edu/~buildbot/doxygen/x_masterDoxyDoc/base_debug.html"> 306 Using lsstDebug to control debugging output</a> for more about \b debug.py files. 308 The available variables in IsrTask are: 311 <DD> A dictionary containing debug point names as keys with frame number as value. Valid keys are: 314 <DD> display exposure after ISR has been applied 318 For example, put something like 322 di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively 323 if name == "lsst.ip.isrFunctions.isrTask": 324 di.display = {'postISRCCD':2} 326 lsstDebug.Info = DebugInfo 328 into your debug.py file and run the commandline task with the \c --debug flag. 332 ConfigClass = IsrTaskConfig
336 '''!Constructor for IsrTask 337 \param[in] *args -- a list of positional arguments passed on to the Task constructor 338 \param[in] **kwargs -- a dictionary of keyword arguments passed on to the Task constructor 339 Call the lsst.pipe.base.task.Task.__init__ method 340 Then setup the assembly and fringe correction subtasks 342 pipeBase.Task.__init__(self, *args, **kwargs)
343 self.makeSubtask(
"assembleCcd")
344 self.makeSubtask(
"fringe")
345 self.makeSubtask(
"crosstalk")
348 """!Retrieve necessary frames for instrument signature removal 349 \param[in] dataRef -- a daf.persistence.butlerSubset.ButlerDataRef 350 of the detector data to be processed 351 \param[in] rawExposure -- a reference raw exposure that will later be 352 corrected with the retrieved calibration data; 353 should not be modified in this method. 354 \return a pipeBase.Struct with fields containing kwargs expected by run() 355 - bias: exposure of bias frame 356 - dark: exposure of dark frame 357 - flat: exposure of flat field 358 - defects: list of detects 359 - fringeStruct: a pipeBase.Struct with field fringes containing 360 exposure of fringe frame or list of fringe exposure 362 ccd = rawExposure.getDetector()
364 biasExposure = self.
getIsrExposure(dataRef,
"bias")
if self.config.doBias
else None 366 linearizer = dataRef.get(
"linearizer", immediate=
True)
if self.
doLinearize(ccd)
else None 367 darkExposure = self.
getIsrExposure(dataRef,
"dark")
if self.config.doDark
else None 368 flatExposure = self.
getIsrExposure(dataRef,
"flat")
if self.config.doFlat
else None 369 brighterFatterKernel = dataRef.get(
"brighterFatterKernel")
if self.config.doBrighterFatter
else None 370 defectList = dataRef.get(
"defects")
if self.config.doDefect
else None 372 if self.config.doFringe
and self.fringe.checkFilter(rawExposure):
373 fringeStruct = self.fringe.readFringes(dataRef, assembler=self.assembleCcd
374 if self.config.doAssembleIsrExposures
else None)
376 fringeStruct = pipeBase.Struct(fringes=
None)
379 return pipeBase.Struct(bias=biasExposure,
380 linearizer=linearizer,
384 fringes=fringeStruct,
385 bfKernel=brighterFatterKernel
389 def run(self, ccdExposure, bias=None, linearizer=None, dark=None, flat=None, defects=None,
390 fringes=None, bfKernel=None):
391 """!Perform instrument signature removal on an exposure 394 - Detect saturation, apply overscan correction, bias, dark and flat 395 - Perform CCD assembly 396 - Interpolate over defects, saturated pixels and all NaNs 398 \param[in] ccdExposure -- lsst.afw.image.exposure of detector data 399 \param[in] bias -- exposure of bias frame 400 \param[in] linearizer -- linearizing functor; a subclass of lsst.ip.isrFunctions.LinearizeBase 401 \param[in] dark -- exposure of dark frame 402 \param[in] flat -- exposure of flatfield 403 \param[in] defects -- list of detects 404 \param[in] fringes -- a pipeBase.Struct with field fringes containing 405 exposure of fringe frame or list of fringe exposure 406 \param[in] bfKernel -- kernel for brighter-fatter correction 408 \return a pipeBase.Struct with field: 413 if isinstance(ccdExposure, ButlerDataRef):
416 ccd = ccdExposure.getDetector()
419 if self.config.doBias
and bias
is None:
420 raise RuntimeError(
"Must supply a bias exposure if config.doBias True")
422 raise RuntimeError(
"Must supply a linearizer if config.doBias True")
423 if self.config.doDark
and dark
is None:
424 raise RuntimeError(
"Must supply a dark exposure if config.doDark True")
425 if self.config.doFlat
and flat
is None:
426 raise RuntimeError(
"Must supply a flat exposure if config.doFlat True")
427 if self.config.doBrighterFatter
and bfKernel
is None:
428 raise RuntimeError(
"Must supply a kernel if config.doBrighterFatter True")
430 fringes = pipeBase.Struct(fringes=
None)
431 if self.config.doFringe
and not isinstance(fringes, pipeBase.Struct):
432 raise RuntimeError(
"Must supply fringe exposure as a pipeBase.Struct")
433 if self.config.doDefect
and defects
is None:
434 raise RuntimeError(
"Must supply defects if config.doDefect True")
439 assert not self.config.doAssembleCcd,
"You need a Detector to run assembleCcd" 440 ccd = [
FakeAmp(ccdExposure, self.config)]
444 if ccdExposure.getBBox().contains(amp.getBBox()):
449 if self.config.doAssembleCcd:
450 ccdExposure = self.assembleCcd.assembleCcd(ccdExposure)
451 if self.config.expectWcs
and not ccdExposure.getWcs():
452 self.log.warn(
"No WCS found in input exposure")
454 if self.config.doBias:
458 linearizer(image=ccdExposure.getMaskedImage().getImage(), detector=ccd, log=self.log)
460 if self.config.doCrosstalk:
461 self.crosstalk.
run(ccdExposure)
465 if ccdExposure.getBBox().contains(amp.getBBox()):
466 ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
469 interpolationDone =
False 471 if self.config.doBrighterFatter:
478 if self.config.doDefect:
480 if self.config.doSaturationInterpolation:
483 interpolationDone =
True 486 self.config.brighterFatterMaxIter,
487 self.config.brighterFatterThreshold,
488 self.config.brighterFatterApplyGain,
491 if self.config.doDark:
494 if self.config.doFringe
and not self.config.fringeAfterFlat:
495 self.fringe.
run(ccdExposure, **fringes.getDict())
497 if self.config.doFlat:
500 if not interpolationDone:
501 if self.config.doDefect:
503 if self.config.doSaturationInterpolation:
505 if not interpolationDone
or self.config.doNanInterpAfterFlat:
508 if self.config.doFringe
and self.config.fringeAfterFlat:
509 self.fringe.
run(ccdExposure, **fringes.getDict())
511 exposureTime = ccdExposure.getInfo().getVisitInfo().getExposureTime()
512 ccdExposure.getCalib().setFluxMag0(self.config.fluxMag0T1*exposureTime)
514 frame = getDebugFrame(self._display,
"postISRCCD")
516 getDisplay(frame).mtv(ccdExposure)
518 return pipeBase.Struct(
519 exposure=ccdExposure,
524 """!Perform instrument signature removal on a ButlerDataRef of a Sensor 526 - Read in necessary detrending/isr/calibration data 527 - Process raw exposure in run() 528 - Persist the ISR-corrected exposure as "postISRCCD" if config.doWrite is True 530 \param[in] sensorRef -- daf.persistence.butlerSubset.ButlerDataRef of the 531 detector data to be processed 532 \return a pipeBase.Struct with fields: 533 - exposure: the exposure after application of ISR 535 self.log.info(
"Performing ISR on sensor %s" % (sensorRef.dataId))
536 ccdExposure = sensorRef.get(
'raw')
539 result = self.
run(ccdExposure, **isrData.getDict())
541 if self.config.doWrite:
542 sensorRef.put(result.exposure,
"postISRCCD")
547 """Convert an exposure from uint16 to float, set variance plane to 1 and mask plane to 0 549 if isinstance(exposure, afwImage.ExposureF):
552 if not hasattr(exposure,
"convertF"):
553 raise RuntimeError(
"Unable to convert exposure (%s) to float" % type(exposure))
555 newexposure = exposure.convertF()
556 maskedImage = newexposure.getMaskedImage()
557 varArray = maskedImage.getVariance().getArray()
559 maskArray = maskedImage.getMask().getArray()
564 """!Apply bias correction in place 566 \param[in,out] exposure exposure to process 567 \param[in] biasExposure bias exposure of same size as exposure 569 isrFunctions.biasCorrection(exposure.getMaskedImage(), biasExposure.getMaskedImage())
572 """!Apply dark correction in place 574 \param[in,out] exposure exposure to process 575 \param[in] darkExposure dark exposure of same size as exposure 576 \param[in] invert if True, remove the dark from an already-corrected image 578 expScale = exposure.getInfo().getVisitInfo().getDarkTime()
579 if math.isnan(expScale):
580 raise RuntimeError(
"Exposure darktime is NAN")
581 darkScale = darkExposure.getInfo().getVisitInfo().getDarkTime()
582 if math.isnan(darkScale):
583 raise RuntimeError(
"Dark calib darktime is NAN")
584 isrFunctions.darkCorrection(
585 maskedImage=exposure.getMaskedImage(),
586 darkMaskedImage=darkExposure.getMaskedImage(),
593 """!Is linearization wanted for this detector? 595 Checks config.doLinearize and the linearity type of the first amplifier. 597 \param[in] detector detector information (an lsst.afw.cameraGeom.Detector) 599 return self.config.doLinearize
and \
600 detector.getAmpInfoCatalog()[0].getLinearityType() != NullLinearityType
603 """!Set the variance plane based on the image plane, plus amplifier gain and read noise 605 \param[in,out] ampExposure exposure to process 606 \param[in] amp amplifier detector information 608 if not math.isnan(amp.getGain()):
609 isrFunctions.updateVariance(
610 maskedImage=ampExposure.getMaskedImage(),
612 readNoise=amp.getReadNoise(),
616 """!Apply flat correction in place 618 \param[in,out] exposure exposure to process 619 \param[in] flatExposure flatfield exposure same size as exposure 620 \param[in] invert if True, unflatten an already-flattened image instead. 622 isrFunctions.flatCorrection(
623 maskedImage=exposure.getMaskedImage(),
624 flatMaskedImage=flatExposure.getMaskedImage(),
625 scalingType=self.config.flatScalingType,
626 userScale=self.config.flatUserScale,
631 """!Retrieve a calibration dataset for removing instrument signature 633 \param[in] dataRef data reference for exposure 634 \param[in] datasetType type of dataset to retrieve (e.g. 'bias', 'flat') 635 \param[in] immediate if True, disable butler proxies to enable error 636 handling within this routine 640 exp = dataRef.get(datasetType, immediate=immediate)
641 except Exception
as exc1:
642 if not self.config.fallbackFilterName:
643 raise RuntimeError(
"Unable to retrieve %s for %s: %s" % (datasetType, dataRef.dataId, exc1))
645 exp = dataRef.get(datasetType, filter=self.config.fallbackFilterName, immediate=immediate)
646 except Exception
as exc2:
647 raise RuntimeError(
"Unable to retrieve %s for %s, even with fallback filter %s: %s AND %s" %
648 (datasetType, dataRef.dataId, self.config.fallbackFilterName, exc1, exc2))
649 self.log.warn(
"Using fallback calibration from filter %s" % self.config.fallbackFilterName)
651 if self.config.doAssembleIsrExposures:
652 exp = self.assembleCcd.assembleCcd(exp)
656 """!Detect saturated pixels and mask them using mask plane config.saturatedMaskName, in place 658 \param[in,out] exposure exposure to process; only the amp DataSec is processed 659 \param[in] amp amplifier device data 661 if not math.isnan(amp.getSaturation()):
662 maskedImage = exposure.getMaskedImage()
663 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
664 isrFunctions.makeThresholdMask(
665 maskedImage=dataView,
666 threshold=amp.getSaturation(),
668 maskName=self.config.saturatedMaskName,
672 """!Interpolate over saturated pixels, in place 674 \param[in,out] ccdExposure exposure to process 677 - Call saturationDetection first, so that saturated pixels have been identified in the "SAT" mask. 678 - Call this after CCD assembly, since saturated regions may cross amplifier boundaries 680 isrFunctions.interpolateFromMask(
681 maskedImage=ccdExposure.getMaskedImage(),
682 fwhm=self.config.fwhm,
683 growFootprints=self.config.growSaturationFootprintSize,
684 maskName=self.config.saturatedMaskName,
688 """!Detect suspect pixels and mask them using mask plane config.suspectMaskName, in place 690 Suspect pixels are pixels whose value is greater than amp.getSuspectLevel(). 691 This is intended to indicate pixels that may be affected by unknown systematics; 692 for example if non-linearity corrections above a certain level are unstable 693 then that would be a useful value for suspectLevel. A value of `nan` indicates 694 that no such level exists and no pixels are to be masked as suspicious. 696 \param[in,out] exposure exposure to process; only the amp DataSec is processed 697 \param[in] amp amplifier device data 699 suspectLevel = amp.getSuspectLevel()
700 if math.isnan(suspectLevel):
703 maskedImage = exposure.getMaskedImage()
704 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
705 isrFunctions.makeThresholdMask(
706 maskedImage=dataView,
707 threshold=suspectLevel,
709 maskName=self.config.suspectMaskName,
713 """!Mask defects using mask plane "BAD" and interpolate over them, in place 715 \param[in,out] ccdExposure exposure to process 716 \param[in] defectBaseList a list of defects to mask and interpolate 718 \warning: call this after CCD assembly, since defects may cross amplifier boundaries 720 maskedImage = ccdExposure.getMaskedImage()
722 for d
in defectBaseList:
724 nd = measAlg.Defect(bbox)
725 defectList.append(nd)
726 isrFunctions.maskPixelsFromDefectList(maskedImage, defectList, maskName=
'BAD')
727 isrFunctions.interpolateDefectList(
728 maskedImage=maskedImage,
729 defectList=defectList,
730 fwhm=self.config.fwhm,
734 """!Mask NaNs using mask plane "UNMASKEDNAN" and interpolate over them, in place 736 We mask and interpolate over all NaNs, including those 737 that are masked with other bits (because those may or may 738 not be interpolated over later, and we want to remove all 739 NaNs). Despite this behaviour, the "UNMASKEDNAN" mask plane 740 is used to preserve the historical name. 742 \param[in,out] exposure exposure to process 744 maskedImage = exposure.getMaskedImage()
747 maskedImage.getMask().addMaskPlane(
"UNMASKEDNAN")
748 maskVal = maskedImage.getMask().getPlaneBitMask(
"UNMASKEDNAN")
749 numNans =
maskNans(maskedImage, maskVal)
750 self.metadata.set(
"NUMNANS", numNans)
754 self.log.warn(
"There were %i unmasked NaNs", numNans)
755 nanDefectList = isrFunctions.getDefectListFromMask(
756 maskedImage=maskedImage,
757 maskName=
'UNMASKEDNAN',
759 isrFunctions.interpolateDefectList(
760 maskedImage=exposure.getMaskedImage(),
761 defectList=nanDefectList,
762 fwhm=self.config.fwhm,
766 """!Apply overscan correction, in place 768 \param[in,out] exposure exposure to process; must include both DataSec and BiasSec pixels 769 \param[in] amp amplifier device data 771 if not amp.getHasRawInfo():
772 raise RuntimeError(
"This method must be executed on an amp with raw information.")
774 if amp.getRawHorizontalOverscanBBox().isEmpty():
775 self.log.info(
"No Overscan region. Not performing Overscan Correction.")
778 maskedImage = exposure.getMaskedImage()
779 dataView = maskedImage.Factory(maskedImage, amp.getRawDataBBox())
781 expImage = exposure.getMaskedImage().getImage()
782 overscanImage = expImage.Factory(expImage, amp.getRawHorizontalOverscanBBox())
784 isrFunctions.overscanCorrection(
785 ampMaskedImage=dataView,
786 overscanImage=overscanImage,
787 fitType=self.config.overscanFitType,
788 order=self.config.overscanOrder,
789 collapseRej=self.config.overscanRej,
793 """!Set the valid polygon as the intersection of fpPolygon and the ccd corners 795 \param[in,out] ccdExposure exposure to process 796 \param[in] fpPolygon Polygon in focal plane coordinates 799 ccd = ccdExposure.getDetector()
800 fpCorners = ccd.getCorners(FOCAL_PLANE)
801 ccdPolygon =
Polygon(fpCorners)
804 intersect = ccdPolygon.intersectionSingle(fpPolygon)
807 ccdPoints = [ccd.transform(ccd.makeCameraPoint(x, FOCAL_PLANE), PIXELS).getPoint()
for x
in intersect]
808 validPolygon =
Polygon(ccdPoints)
809 ccdExposure.getInfo().setValidPolygon(validPolygon)
812 """Apply brighter fatter correction in place for the image 814 This correction takes a kernel that has been derived from flat field images to 815 redistribute the charge. The gradient of the kernel is the deflection 816 field due to the accumulated charge. 818 Given the original image I(x) and the kernel K(x) we can compute the corrected image Ic(x) 819 using the following equation: 821 Ic(x) = I(x) + 0.5*d/dx(I(x)*d/dx(int( dy*K(x-y)*I(y)))) 823 To evaluate the derivative term we expand it as follows: 825 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))) ) 827 Because we use the measured counts instead of the incident counts we apply the correction 828 iteratively to reconstruct the original counts and the correction. We stop iterating when the 829 summed difference between the current corrected image and the one from the previous iteration 830 is below the threshold. We do not require convergence because the number of iterations is 831 too large a computational cost. How we define the threshold still needs to be evaluated, the 832 current default was shown to work reasonably well on a small set of images. For more information 833 on the method see DocuShare Document-19407. 835 The edges as defined by the kernel are not corrected because they have spurious values 836 due to the convolution. 838 self.log.info(
"Applying brighter fatter correction")
840 image = exposure.getMaskedImage().getImage()
845 kLx = numpy.shape(kernel)[0]
846 kLy = numpy.shape(kernel)[1]
847 kernelImage = afwImage.ImageD(kLx, kLy)
848 kernelImage.getArray()[:, :] = kernel
849 tempImage = image.clone()
851 nanIndex = numpy.isnan(tempImage.getArray())
852 tempImage.getArray()[nanIndex] = 0.
854 outImage = afwImage.ImageF(image.getDimensions())
855 corr = numpy.zeros_like(image.getArray())
856 prev_image = numpy.zeros_like(image.getArray())
857 convCntrl = afwMath.ConvolutionControl(
False,
True, 1)
858 fixedKernel = afwMath.FixedKernel(kernelImage)
868 for iteration
in range(maxIter):
870 afwMath.convolve(outImage, tempImage, fixedKernel, convCntrl)
871 tmpArray = tempImage.getArray()
872 outArray = outImage.getArray()
875 gradTmp = numpy.gradient(tmpArray[startY:endY, startX:endX])
876 gradOut = numpy.gradient(outArray[startY:endY, startX:endX])
877 first = (gradTmp[0]*gradOut[0] + gradTmp[1]*gradOut[1])[1:-1, 1:-1]
880 diffOut20 = numpy.diff(outArray, 2, 0)[startY:endY, startX + 1:endX - 1]
881 diffOut21 = numpy.diff(outArray, 2, 1)[startY + 1:endY - 1, startX:endX]
882 second = tmpArray[startY + 1:endY - 1, startX + 1:endX - 1]*(diffOut20 + diffOut21)
884 corr[startY + 1:endY - 1, startX + 1:endX - 1] = 0.5*(first + second)
886 tmpArray[:, :] = image.getArray()[:, :]
887 tmpArray[nanIndex] = 0.
888 tmpArray[startY:endY, startX:endX] += corr[startY:endY, startX:endX]
891 diff = numpy.sum(numpy.abs(prev_image - tmpArray))
895 prev_image[:, :] = tmpArray[:, :]
897 if iteration == maxIter - 1:
898 self.log.warn(
"Brighter fatter correction did not converge, final difference %f" % diff)
900 self.log.info(
"Finished brighter fatter in %d iterations" % (iteration + 1))
901 image.getArray()[startY + 1:endY - 1, startX + 1:endX - 1] += \
902 corr[startY + 1:endY - 1, startX + 1:endX - 1]
907 """Context manager that applies and removes gain 910 ccd = exp.getDetector()
912 sim = image.Factory(image, amp.getBBox())
919 ccd = exp.getDetector()
921 sim = image.Factory(image, amp.getBBox())
927 """Context manager that applies and removes flats and darks, 928 if the task is configured to apply them. 930 if self.config.doDark
and dark
is not None:
932 if self.config.doFlat:
937 if self.config.doFlat:
939 if self.config.doDark
and dark
is not None:
944 """A Detector-like object that supports returning gain and saturation level""" 947 self.
_bbox = exposure.getBBox(afwImage.LOCAL)
949 self.
_gain = config.gain
def brighterFatterCorrection(self, exposure, kernel, maxIter, threshold, applyGain)
def runDataRef(self, sensorRef)
Perform instrument signature removal on a ButlerDataRef of a Sensor.
def gainContext(self, exp, image, apply)
def __init__(self, args, kwargs)
Constructor for IsrTask.
def readIsrData(self, dataRef, rawExposure)
Retrieve necessary frames for instrument signature removal.
def maskAndInterpNan(self, exposure)
Mask NaNs using mask plane "UNMASKEDNAN" and interpolate over them, in place.
def saturationInterpolation(self, ccdExposure)
Interpolate over saturated pixels, in place.
def run(self, ccdExposure, bias=None, linearizer=None, dark=None, flat=None, defects=None, fringes=None, bfKernel=None)
Perform instrument signature removal on an exposure.
Apply common instrument signature correction algorithms to a raw frame.
def getRawHorizontalOverscanBBox(self)
def getSuspectLevel(self)
def convertIntToFloat(self, exposure)
def flatCorrection(self, exposure, flatExposure, invert=False)
Apply flat correction in place.
def getIsrExposure(self, dataRef, datasetType, immediate=True)
Retrieve a calibration dataset for removing instrument signature.
_RawHorizontalOverscanBBox
def darkCorrection(self, exposure, darkExposure, invert=False)
Apply dark correction in place.
def doLinearize(self, detector)
Is linearization wanted for this detector?
def setValidPolygonIntersect(self, ccdExposure, fpPolygon)
Set the valid polygon as the intersection of fpPolygon and the ccd corners.
def biasCorrection(self, exposure, biasExposure)
Apply bias correction in place.
def flatContext(self, exp, flat, dark=None)
def updateVariance(self, ampExposure, amp)
Set the variance plane based on the image plane, plus amplifier gain and read noise.
def overscanCorrection(self, exposure, amp)
Apply overscan correction, in place.
size_t maskNans(afw::image::MaskedImage< PixelT > const &mi, afw::image::MaskPixel maskVal, afw::image::MaskPixel allow=0)
Mask NANs in an image.
def suspectDetection(self, exposure, amp)
Detect suspect pixels and mask them using mask plane config.suspectMaskName, in place.
def maskAndInterpDefect(self, ccdExposure, defectBaseList)
Mask defects using mask plane "BAD" and interpolate over them, in place.
def saturationDetection(self, exposure, amp)
Detect saturated pixels and mask them using mask plane config.saturatedMaskName, in place...
def __init__(self, exposure, config)