22__all__ = [
"OverscanCorrectionTaskConfig",
"OverscanCorrectionTask"]
31from .isr
import fitOverscanImage
32from .isrFunctions
import makeThresholdMask, countMaskedPixels
36 """Overscan correction options.
38 fitType = pexConfig.ChoiceField(
40 doc="The method for fitting the overscan bias level.",
43 "POLY":
"Fit ordinary polynomial to the longest axis of the overscan region",
44 "CHEB":
"Fit Chebyshev polynomial to the longest axis of the overscan region",
45 "LEG":
"Fit Legendre polynomial to the longest axis of the overscan region",
46 "NATURAL_SPLINE":
"Fit natural spline to the longest axis of the overscan region",
47 "CUBIC_SPLINE":
"Fit cubic spline to the longest axis of the overscan region",
48 "AKIMA_SPLINE":
"Fit Akima spline to the longest axis of the overscan region",
49 "MEAN":
"Correct using the mean of the overscan region",
50 "MEANCLIP":
"Correct using a clipped mean of the overscan region",
51 "MEDIAN":
"Correct using the median of the overscan region",
52 "MEDIAN_PER_ROW":
"Correct using the median per row of the overscan region",
55 order = pexConfig.Field(
57 doc=(
"Order of polynomial to fit if overscan fit type is a polynomial, "
58 "or number of spline knots if overscan fit type is a spline."),
61 numSigmaClip = pexConfig.Field(
63 doc=
"Rejection threshold (sigma) for collapsing overscan before fit",
66 maskPlanes = pexConfig.ListField(
68 doc=
"Mask planes to reject when measuring overscan",
69 default=[
'BAD',
'SAT'],
71 overscanIsInt = pexConfig.Field(
73 doc=
"Treat overscan as an integer image for purposes of fitType=MEDIAN"
74 " and fitType=MEDIAN_PER_ROW.",
78 doParallelOverscan = pexConfig.Field(
80 doc=
"Correct using parallel overscan after serial overscan correction?",
83 parallelOverscanMaskThreshold = pexConfig.RangeField(
85 doc=
"Minimum fraction of pixels in parallel overscan region necessary "
86 "for parallel overcan correction.",
94 leadingColumnsToSkip = pexConfig.Field(
96 doc=
"Number of leading columns to skip in serial overscan correction.",
99 trailingColumnsToSkip = pexConfig.Field(
101 doc=
"Number of trailing columns to skip in serial overscan correction.",
104 leadingRowsToSkip = pexConfig.Field(
106 doc=
"Number of leading rows to skip in parallel overscan correction.",
109 trailingRowsToSkip = pexConfig.Field(
111 doc=
"Number of trailing rows to skip in parallel overscan correction.",
115 maxDeviation = pexConfig.Field(
117 doc=
"Maximum deviation from median (in ADU) to mask in overscan correction.",
118 default=1000.0, check=
lambda x: x > 0,
123 """Correction task for overscan.
125 This class contains
a number of utilities that are easier
to
126 understand
and use when they are
not embedded
in nested
if/
else
132 Statistics control object.
134 ConfigClass = OverscanCorrectionTaskConfig
135 _DefaultName = "overscan"
145 self.
statControl.setNumSigmaClip(self.config.numSigmaClip)
146 self.
statControl.setAndMask(afwImage.Mask.getPlaneBitMask(self.config.maskPlanes))
148 def run(self, exposure, amp, isTransposed=False):
149 """Measure and remove an overscan from an amplifier image.
154 Image data that will have the overscan corrections applied.
156 Amplifier to use for debugging purposes.
157 isTransposed : `bool`, optional
158 Is the image transposed, such that serial
and parallel
159 overscan regions are reversed? Default
is False.
163 overscanResults : `lsst.pipe.base.Struct`
164 Result struct
with components:
167 Value
or fit subtracted
from the amplifier image data
170 Value
or fit subtracted
from the serial overscan image
173 Image of the serial overscan region
with the serial
174 overscan correction applied
176 estimate the amplifier read noise empirically.
177 ``parallelOverscanFit``
178 Value
or fit subtracted
from the parallel overscan
180 ``parallelOverscanImage``
181 Image of the parallel overscan region
with the
182 parallel overscan correction applied
188 Raised
if an invalid overscan type
is set.
191 serialOverscanBBox = amp.getRawSerialOverscanBBox()
192 imageBBox = amp.getRawDataBBox()
194 if self.config.doParallelOverscan:
197 parallelOverscanBBox = amp.getRawParallelOverscanBBox()
198 imageBBox = imageBBox.expandedTo(parallelOverscanBBox)
201 imageBBox.getMinY()),
203 imageBBox.getHeight()))
205 imageBBox, serialOverscanBBox, isTransposed=isTransposed)
206 overscanMean = serialResults.overscanMean
207 overscanSigma = serialResults.overscanSigma
208 residualMean = serialResults.overscanMeanResidual
209 residualSigma = serialResults.overscanSigmaResidual
212 parallelResults =
None
213 if self.config.doParallelOverscan:
216 parallelOverscanBBox = amp.getRawParallelOverscanBBox()
217 imageBBox = amp.getRawDataBBox()
219 maskIm = exposure.getMaskedImage()
220 maskIm = maskIm.Factory(maskIm, parallelOverscanBBox)
228 thresholdLevel = self.config.numSigmaClip * serialResults.overscanSigmaResidual
229 makeThresholdMask(maskIm, threshold=thresholdLevel, growFootprints=0)
230 maskPix = countMaskedPixels(maskIm, self.config.maskPlanes)
231 xSize, ySize = parallelOverscanBBox.getDimensions()
232 if maskPix > xSize*ySize*self.config.parallelOverscanMaskThreshold:
233 self.log.warning(
'Fraction of masked pixels for parallel overscan calculation larger'
234 ' than %f of total pixels (i.e. %f masked pixels) on amp %s.',
235 self.config.parallelOverscanMaskThreshold, maskPix, amp.getName())
236 self.log.warning(
'Not doing parallel overscan correction.')
239 imageBBox, parallelOverscanBBox,
240 isTransposed=
not isTransposed)
242 overscanMean = (overscanMean, parallelResults.overscanMean)
243 overscanSigma = (overscanSigma, parallelResults.overscanSigma)
244 residualMean = (residualMean, parallelResults.overscanMeanResidual)
245 residualSigma = (residualSigma, parallelResults.overscanSigmaResidual)
246 parallelOverscanFit = parallelResults.overscanOverscanModel
if parallelResults
else None
247 parallelOverscanImage = parallelResults.overscanImage
if parallelResults
else None
249 return pipeBase.Struct(imageFit=serialResults.ampOverscanModel,
250 overscanFit=serialResults.overscanOverscanModel,
251 overscanImage=serialResults.overscanImage,
253 parallelOverscanFit=parallelOverscanFit,
254 parallelOverscanImage=parallelOverscanImage,
255 overscanMean=overscanMean,
256 overscanSigma=overscanSigma,
257 residualMean=residualMean,
258 residualSigma=residualSigma)
263 overscanBox = self.trimOverscan(exposure, amp, overscanBBox,
264 self.config.leadingColumnsToSkip,
265 self.config.trailingColumnsToSkip,
266 transpose=isTransposed)
267 overscanImage = exposure[overscanBox].getMaskedImage()
268 overscanArray = overscanImage.image.array
271 maskVal = overscanImage.mask.getPlaneBitMask(self.config.maskPlanes)
272 overscanMask = ~((overscanImage.mask.array & maskVal) == 0)
274 median = np.ma.median(np.ma.masked_where(overscanMask, overscanArray))
275 bad = np.where(np.abs(overscanArray - median) > self.config.maxDeviation)
276 overscanMask[bad] = overscanImage.mask.getPlaneBitMask(
"SAT")
280 overscanResults = self.
fitOverscan(overscanImage, isTransposed=isTransposed)
283 ampImage = exposure[imageBBox]
285 ampImage.image.array,
286 transpose=isTransposed)
287 ampImage.image.array -= ampOverscanModel
291 overscanImage = exposure[overscanBBox]
294 overscanImage.image.array)
295 overscanImage.image.array -= overscanOverscanModel
297 self.
debugView(overscanImage, overscanResults.overscanValue, amp)
301 afwMath.MEDIAN | afwMath.STDEVCLIP, self.
statControl)
302 residualMean = stats.getValue(afwMath.MEDIAN)
303 residualSigma = stats.getValue(afwMath.STDEVCLIP)
305 return pipeBase.Struct(ampOverscanModel=ampOverscanModel,
306 overscanOverscanModel=overscanOverscanModel,
307 overscanImage=overscanImage,
308 overscanValue=overscanResults.overscanValue,
310 overscanMean=overscanResults.overscanMean,
311 overscanSigma=overscanResults.overscanSigma,
312 overscanMeanResidual=residualMean,
313 overscanSigmaResidual=residualSigma
317 """Broadcast 0 or 1 dimension fit to appropriate shape.
321 overscanValue : `numpy.ndarray`, (Nrows, ) or scalar
322 Overscan fit to broadcast.
323 imageArray : `numpy.ndarray`, (Nrows, Ncols)
324 Image array that we want to match.
325 transpose : `bool`, optional
326 Switch order to broadcast along the other axis.
330 overscanModel : `numpy.ndarray`, (Nrows, Ncols)
or scalar
331 Expanded overscan fit.
336 Raised
if no axis has the appropriate dimension.
338 if isinstance(overscanValue, np.ndarray):
339 overscanModel = np.zeros_like(imageArray)
341 if transpose
is False:
342 if imageArray.shape[0] == overscanValue.shape[0]:
343 overscanModel[:, :] = overscanValue[:, np.newaxis]
344 elif imageArray.shape[1] == overscanValue.shape[0]:
345 overscanModel[:, :] = overscanValue[np.newaxis, :]
346 elif imageArray.shape[0] == overscanValue.shape[1]:
347 overscanModel[:, :] = overscanValue[np.newaxis, :]
349 raise RuntimeError(f
"Could not broadcast {overscanValue.shape} to "
350 f
"match {imageArray.shape}")
352 if imageArray.shape[1] == overscanValue.shape[0]:
353 overscanModel[:, :] = overscanValue[np.newaxis, :]
354 elif imageArray.shape[0] == overscanValue.shape[0]:
355 overscanModel[:, :] = overscanValue[:, np.newaxis]
356 elif imageArray.shape[1] == overscanValue.shape[1]:
357 overscanModel[:, :] = overscanValue[:, np.newaxis]
359 raise RuntimeError(f
"Could not broadcast {overscanValue.shape} to "
360 f
"match {imageArray.shape}")
362 overscanModel = overscanValue
366 def trimOverscan(self, exposure, amp, bbox, skipLeading, skipTrailing, transpose=False):
367 """Trim overscan region to remove edges.
372 Exposure containing data.
374 Amplifier containing geometry information.
376 Bounding box of the overscan region.
378 Number of leading (towards data region) rows/columns to skip.
380 Number of trailing (away from data region) rows/columns to skip.
381 transpose : `bool`, optional
382 Operate on the transposed array.
386 overscanArray : `numpy.array`, (N, M)
388 overscanMask : `numpy.array`, (N, M)
391 dx0, dy0, dx1, dy1 = (0, 0, 0, 0)
392 dataBBox = amp.getRawDataBBox()
394 if dataBBox.getBeginY() < bbox.getBeginY():
401 if dataBBox.getBeginX() < bbox.getBeginX():
410 bbox.getHeight() - dy0 + dy1))
414 if self.config.fitType
in (
'MEAN',
'MEANCLIP',
'MEDIAN'):
417 overscanValue = overscanResult.overscanValue
418 overscanMean = overscanValue
420 elif self.config.fitType
in (
'MEDIAN_PER_ROW',
'POLY',
'CHEB',
'LEG',
421 'NATURAL_SPLINE',
'CUBIC_SPLINE',
'AKIMA_SPLINE'):
424 overscanValue = overscanResult.overscanValue
427 afwMath.MEDIAN | afwMath.STDEVCLIP, self.
statControl)
428 overscanMean = stats.getValue(afwMath.MEDIAN)
429 overscanSigma = stats.getValue(afwMath.STDEVCLIP)
431 raise ValueError(
'%s : %s an invalid overscan type' %
432 (
"overscanCorrection", self.config.fitType))
434 return pipeBase.Struct(overscanValue=overscanValue,
435 overscanMean=overscanMean,
436 overscanSigma=overscanSigma,
441 """Return an integer version of the input image.
446 Image to convert to integers.
451 The integer converted image.
456 Raised
if the input image could
not be converted.
458 if hasattr(image,
"image"):
460 imageI = image.image.convertI()
461 outI = afwImage.MaskedImageI(imageI, image.mask, image.variance)
462 elif hasattr(image,
"convertI"):
464 outI = image.convertI()
465 elif hasattr(image,
"astype"):
467 outI = image.astype(int)
469 raise RuntimeError(
"Could not convert this to integers: %s %s %s",
470 image,
type(image), dir(image))
475 """Measure a constant overscan value.
480 Image data to measure the overscan
from.
484 results : `lsst.pipe.base.Struct`
485 Overscan result
with entries:
486 - ``overscanValue``: Overscan value to subtract (`float`)
487 - ``isTransposed``: Orientation of the overscan (`bool`)
489 if self.config.fitType ==
'MEDIAN':
496 return pipeBase.Struct(overscanValue=overscanValue,
501 """Extract the numpy array from the input image.
506 Image data to pull array
from.
508 calcImage : `numpy.ndarray`
509 Image data array
for numpy operating.
511 if hasattr(image,
"getImage"):
512 calcImage = image.getImage().getArray()
513 calcImage = np.ma.masked_where(image.getMask().getArray() & self.
statControl.getAndMask(),
516 calcImage = image.getArray()
520 """Mask outliers in a row of overscan data
from a robust sigma
525 imageArray : `numpy.ndarray`
526 Image to filter along numpy axis=1.
530 maskedArray : `numpy.ma.masked_array`
531 Masked image marking outliers.
533 lq, median, uq = np.percentile(imageArray, [25.0, 50.0, 75.0], axis=1)
535 axisStdev = 0.74*(uq - lq)
537 diff = np.abs(imageArray - axisMedians[:, np.newaxis])
538 return np.ma.masked_where(diff > self.
statControl.getNumSigmaClip()
539 * axisStdev[:, np.newaxis], imageArray)
543 """Collapse overscan array (and mask) to a 1-D vector of values.
547 maskedArray : `numpy.ma.masked_array`
548 Masked array of input overscan data.
552 collapsed : `numpy.ma.masked_array`
553 Single dimensional overscan data, combined with the mean.
555 collapsed = np.mean(maskedArray, axis=1)
556 if collapsed.mask.sum() > 0:
557 collapsed.data[collapsed.mask] = np.mean(maskedArray.data[collapsed.mask], axis=1)
561 """Collapse overscan array (and mask) to a 1-D vector of using the
562 correct integer median of row-values.
566 maskedArray : `numpy.ma.masked_array`
567 Masked array of input overscan data.
571 collapsed : `numpy.ma.masked_array`
572 Single dimensional overscan data, combined with the afwMath median.
578 for row
in integerMI:
579 newRow = row.compressed()
584 collapsed.append(rowMedian)
586 return np.array(collapsed)
589 """Wrapper function to match spline fit API to polynomial fit API.
593 indices : `numpy.ndarray`
594 Locations to evaluate the spline.
595 collapsed : `numpy.ndarray`
596 Collapsed overscan values corresponding to the spline
599 Number of bins to use in constructing the spline.
604 Interpolation object
for later evaluation.
606 if not np.ma.is_masked(collapsed):
607 collapsed.mask = np.array(len(collapsed)*[np.ma.nomask])
609 numPerBin, binEdges = np.histogram(indices, bins=numBins,
610 weights=1 - collapsed.mask.astype(int))
611 with np.errstate(invalid=
"ignore"):
612 values = np.histogram(indices, bins=numBins,
613 weights=collapsed.data*~collapsed.mask)[0]/numPerBin
614 binCenters = np.histogram(indices, bins=numBins,
615 weights=indices*~collapsed.mask)[0]/numPerBin
617 if len(binCenters[numPerBin > 0]) < 5:
618 self.log.warn(
"Cannot do spline fitting for overscan: %s valid points.",
619 len(binCenters[numPerBin > 0]))
623 if len(values[numPerBin > 0]) != 0:
624 return float(values[numPerBin > 0][0])
629 values.astype(float)[numPerBin > 0],
635 """Wrapper function to match spline evaluation API to polynomial fit
640 indices : `numpy.ndarray`
641 Locations to evaluate the spline.
642 interp : `lsst.afw.math.interpolate`
643 Interpolation object to use.
647 values : `numpy.ndarray`
648 Evaluated spline values at each index.
651 return interp.interpolate(indices.astype(float))
655 """Create mask if edges are extrapolated.
659 collapsed : `numpy.ma.masked_array`
660 Masked array to check the edges of.
664 maskArray : `numpy.ndarray`
665 Boolean numpy array of pixels to mask.
667 maskArray = np.full_like(collapsed, False, dtype=bool)
668 if np.ma.is_masked(collapsed):
670 for low
in range(num):
671 if not collapsed.mask[low]:
674 maskArray[:low] =
True
675 for high
in range(1, num):
676 if not collapsed.mask[-high]:
679 maskArray[-high:] =
True
683 """Calculate the 1-d vector overscan from the input overscan image.
688 Image containing the overscan data.
689 isTransposed : `bool`
690 If true, the image has been transposed.
694 results : `lsst.pipe.base.Struct`
695 Overscan result with entries:
698 Overscan value to subtract (`float`)
700 List of rows that should be masked
as ``SUSPECT`` when the
701 overscan solution
is applied. (`list` [ `bool` ])
703 Indicates
if the overscan data was transposed during
704 calcuation, noting along which axis the overscan should be
711 calcImage = np.transpose(calcImage)
714 if self.config.fitType ==
'MEDIAN_PER_ROW':
715 mi = afwImage.MaskedImageI(image.getBBox())
716 masked = masked.astype(int)
718 masked = masked.transpose()
720 mi.image.array[:, :] = masked.data[:, :]
721 if bool(masked.mask.shape):
722 mi.mask.array[:, :] = masked.mask[:, :]
724 overscanVector = fitOverscanImage(mi, self.config.maskPlanes, isTransposed)
730 indices = 2.0*np.arange(num)/float(num) - 1.0
734 'POLY': (poly.polynomial.polyfit, poly.polynomial.polyval),
735 'CHEB': (poly.chebyshev.chebfit, poly.chebyshev.chebval),
736 'LEG': (poly.legendre.legfit, poly.legendre.legval),
740 }[self.config.fitType]
744 coeffs =
fitter(indices, collapsed, self.config.order)
746 if isinstance(coeffs, float):
747 self.log.warn(
"Using fallback value %f due to fitter failure. Amplifier will be masked.",
749 overscanVector = np.full_like(indices, coeffs)
750 maskArray = np.full_like(collapsed,
True, dtype=bool)
753 overscanVector = evaler(indices, coeffs)
756 return pipeBase.Struct(overscanValue=np.array(overscanVector),
758 isTransposed=isTransposed)
761 """Debug display for the final overscan solution.
766 Input image the overscan solution was determined from.
767 model : `numpy.ndarray`
or `float`
768 Overscan model determined
for the image.
770 Amplifier to extract diagnostic information.
780 calcImage = np.transpose(calcImage)
785 indices = 2.0 * np.arange(num)/float(num) - 1.0
787 if np.ma.is_masked(collapsed):
788 collapsedMask = collapsed.mask
790 collapsedMask = np.array(num*[np.ma.nomask])
792 import matplotlib.pyplot
as plot
793 figure = plot.figure(1)
795 axes = figure.add_axes((0.1, 0.1, 0.8, 0.8))
796 axes.plot(indices[~collapsedMask], collapsed[~collapsedMask],
'k+')
797 if collapsedMask.sum() > 0:
798 axes.plot(indices[collapsedMask], collapsed.data[collapsedMask],
'b+')
799 if isinstance(model, np.ndarray):
802 plotModel = np.zeros_like(indices)
804 axes.plot(indices, plotModel,
'r-')
805 plot.xlabel(
"centered/scaled position along overscan region")
806 plot.ylabel(
"pixel value/fit value")
808 plot.title(f
"{amp.getName()} DataX: "
809 f
"[{amp.getRawDataBBox().getBeginX()}:{amp.getRawBBox().getEndX()}]"
810 f
"OscanX: [{amp.getRawHorizontalOverscanBBox().getBeginX()}:"
811 f
"{amp.getRawHorizontalOverscanBBox().getEndX()}] {self.config.fitType}")
813 plot.title(
"No amp supplied.")
815 prompt =
"Press Enter or c to continue [chp]..."
817 ans = input(prompt).lower()
818 if ans
in (
"",
" ",
"c",):
827 print(
"[h]elp [c]ontinue [p]db e[x]itDebug")
Geometry and electronic information about raw amplifier images.
A class to contain the data, WCS, and other information needed to describe an image of the sky.
A class to represent a 2-dimensional array of pixels.
A class to manipulate images, masks, and variance as a single object.
Pass parameters to a Statistics object.
An integer coordinate rectangle.
def collapseArrayMedian(self, maskedArray)
def maskExtrapolated(collapsed)
def fitOverscan(self, overscanImage, isTransposed=False)
def measureVectorOverscan(self, image, isTransposed=False)
def __init__(self, statControl=None, **kwargs)
def collapseArray(maskedArray)
def maskOutliers(self, imageArray)
def debugView(self, image, model, amp=None)
def getImageArray(self, image)
def integerConvert(image)
def splineFit(self, indices, collapsed, numBins)
def broadcastFitToImage(self, overscanValue, imageArray, transpose=False)
def correctOverscan(self, exposure, amp, imageBBox, overscanBBox, isTransposed=True)
def trimOverscan(self, exposure, amp, bbox, skipLeading, skipTrailing, transpose=False)
def splineEval(indices, interp)
def measureConstantOverscan(self, image)
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)
Property stringToStatisticsProperty(std::string const property)
Conversion function to switch a string to a Property (see Statistics.h)
Interpolate::Style stringToInterpStyle(std::string const &style)
Conversion function to switch a string to an Interpolate::Style.
std::shared_ptr< Interpolate > makeInterpolate(std::vector< double > const &x, std::vector< double > const &y, Interpolate::Style const style=Interpolate::AKIMA_SPLINE)
A factory function to make Interpolate objects.