26 import lsst.pex.config 
as pexConfig
 
   28 __all__ = [
"OverscanCorrectionTaskConfig", 
"OverscanCorrectionTask"]
 
   32     """Overscan correction options. 
   34     fitType = pexConfig.ChoiceField(
 
   36         doc=
"The method for fitting the overscan bias level.",
 
   39             "POLY": 
"Fit ordinary polynomial to the longest axis of the overscan region",
 
   40             "CHEB": 
"Fit Chebyshev polynomial to the longest axis of the overscan region",
 
   41             "LEG": 
"Fit Legendre polynomial to the longest axis of the overscan region",
 
   42             "NATURAL_SPLINE": 
"Fit natural spline to the longest axis of the overscan region",
 
   43             "CUBIC_SPLINE": 
"Fit cubic spline to the longest axis of the overscan region",
 
   44             "AKIMA_SPLINE": 
"Fit Akima spline to the longest axis of the overscan region",
 
   45             "MEAN": 
"Correct using the mean of the overscan region",
 
   46             "MEANCLIP": 
"Correct using a clipped mean of the overscan region",
 
   47             "MEDIAN": 
"Correct using the median of the overscan region",
 
   48             "MEDIAN_PER_ROW": 
"Correct using the median per row of the overscan region",
 
   51     order = pexConfig.Field(
 
   53         doc=(
"Order of polynomial to fit if overscan fit type is a polynomial, " +
 
   54              "or number of spline knots if overscan fit type is a spline."),
 
   57     numSigmaClip = pexConfig.Field(
 
   59         doc=
"Rejection threshold (sigma) for collapsing overscan before fit",
 
   62     maskPlanes = pexConfig.ListField(
 
   64         doc=
"Mask planes to reject when measuring overscan",
 
   67     overscanIsInt = pexConfig.Field(
 
   69         doc=
"Treat overscan as an integer image for purposes of fitType=MEDIAN" +
 
   70             " and fitType=MEDIAN_PER_ROW.",
 
   76     """Correction task for overscan. 
   78     This class contains a number of utilities that are easier to 
   79     understand and use when they are not embedded in nested if/else 
   84     statControl : `lsst.afw.math.StatisticsControl`, optional 
   85         Statistics control object. 
   87     ConfigClass = OverscanCorrectionTaskConfig
 
   88     _DefaultName = 
"overscan" 
   90     def __init__(self, statControl=None, **kwargs):
 
   96             self.
statControl.setNumSigmaClip(self.config.numSigmaClip)
 
   97             self.
statControl.setAndMask(afwImage.Mask.getPlaneBitMask(self.config.maskPlanes))
 
   99     def run(self, ampImage, overscanImage):
 
  100         """Measure and remove an overscan from an amplifier image. 
  104         ampImage : `lsst.afw.image.Image` 
  105             Image data that will have the overscan removed. 
  106         overscanImage : `lsst.afw.image.Image` 
  107             Overscan data that the overscan is measured from. 
  111         overscanResults : `lsst.pipe.base.Struct` 
  112             Result struct with components: 
  115                 Value or fit subtracted from the amplifier image data 
  116                 (scalar or `lsst.afw.image.Image`). 
  118                 Value or fit subtracted from the overscan image data 
  119                 (scalar or `lsst.afw.image.Image`). 
  121                 Image of the overscan region with the overscan 
  122                 correction applied (`lsst.afw.image.Image`). This 
  123                 quantity is used to estimate the amplifier read noise 
  129             Raised if an invalid overscan type is set. 
  132         if self.config.fitType 
in (
'MEAN', 
'MEANCLIP', 
'MEDIAN'):
 
  134             overscanValue = overscanResult.overscanValue
 
  135             offImage = overscanValue
 
  136             overscanModel = overscanValue
 
  138         elif self.config.fitType 
in (
'MEDIAN_PER_ROW', 
'POLY', 
'CHEB', 
'LEG',
 
  139                                      'NATURAL_SPLINE', 
'CUBIC_SPLINE', 
'AKIMA_SPLINE'):
 
  141             overscanValue = overscanResult.overscanValue
 
  142             maskArray = overscanResult.maskArray
 
  143             isTransposed = overscanResult.isTransposed
 
  145             offImage = afwImage.ImageF(ampImage.getDimensions())
 
  146             offArray = offImage.getArray()
 
  147             overscanModel = afwImage.ImageF(overscanImage.getDimensions())
 
  148             overscanArray = overscanModel.getArray()
 
  150             if hasattr(ampImage, 
'getMask'):
 
  156                 offArray[:, :] = overscanValue[np.newaxis, :]
 
  157                 overscanArray[:, :] = overscanValue[np.newaxis, :]
 
  159                     maskSuspect.getArray()[:, maskArray] |= ampImage.getMask().getPlaneBitMask(
"SUSPECT")
 
  161                 offArray[:, :] = overscanValue[:, np.newaxis]
 
  162                 overscanArray[:, :] = overscanValue[:, np.newaxis]
 
  164                     maskSuspect.getArray()[maskArray, :] |= ampImage.getMask().getPlaneBitMask(
"SUSPECT")
 
  166             raise RuntimeError(
'%s : %s an invalid overscan type' %
 
  167                                (
"overscanCorrection", self.config.fitType))
 
  169         self.
debugView(overscanImage, overscanValue)
 
  173             ampImage.getMask().getArray()[:, :] |= maskSuspect.getArray()[:, :]
 
  174         overscanImage -= overscanModel
 
  175         return pipeBase.Struct(imageFit=offImage,
 
  176                                overscanFit=overscanModel,
 
  177                                overscanImage=overscanImage,
 
  178                                edgeMask=maskSuspect)
 
  182         """Return an integer version of the input image. 
  186         image : `numpy.ndarray`, `lsst.afw.image.Image` or `MaskedImage` 
  187             Image to convert to integers. 
  191         outI : `numpy.ndarray`, `lsst.afw.image.Image` or `MaskedImage` 
  192             The integer converted image. 
  197             Raised if the input image could not be converted. 
  199         if hasattr(image, 
"image"):
 
  201             imageI = image.image.convertI()
 
  202             outI = afwImage.MaskedImageI(imageI, image.mask, image.variance)
 
  203         elif hasattr(image, 
"convertI"):
 
  205             outI = image.convertI()
 
  206         elif hasattr(image, 
"astype"):
 
  208             outI = image.astype(int)
 
  210             raise RuntimeError(
"Could not convert this to integers: %s %s %s",
 
  211                                image, 
type(image), dir(image))
 
  216         """Measure a constant overscan value. 
  220         image : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage` 
  221             Image data to measure the overscan from. 
  225         results : `lsst.pipe.base.Struct` 
  226             Overscan result with entries: 
  227             - ``overscanValue``: Overscan value to subtract (`float`) 
  228             - ``maskArray``: Placeholder for a mask array (`list`) 
  229             - ``isTransposed``: Orientation of the overscan (`bool`) 
  231         if self.config.fitType == 
'MEDIAN':
 
  239         return pipeBase.Struct(overscanValue=overscanValue,
 
  245         """Extract the numpy array from the input image. 
  249         image : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage` 
  250             Image data to pull array from. 
  252         calcImage : `numpy.ndarray` 
  253             Image data array for numpy operating. 
  255         if hasattr(image, 
"getImage"):
 
  256             calcImage = image.getImage().getArray()
 
  257             calcImage = np.ma.masked_where(image.getMask().getArray() & self.
statControl.getAndMask(),
 
  260             calcImage = image.getArray()
 
  265         """Transpose input numpy array if necessary. 
  269         imageArray : `numpy.ndarray` 
  270             Image data to transpose. 
  274         imageArray : `numpy.ndarray` 
  275             Transposed image data. 
  276         isTransposed : `bool` 
  277             Indicates whether the input data was transposed. 
  279         if np.argmin(imageArray.shape) == 0:
 
  280             return np.transpose(imageArray), 
True 
  282             return imageArray, 
False 
  285         """Mask  outliers in  a  row  of overscan  data  from  a robust  sigma 
  290         imageArray : `numpy.ndarray` 
  291             Image to filter along numpy axis=1. 
  295         maskedArray : `numpy.ma.masked_array` 
  296             Masked image marking outliers. 
  298         lq, median, uq = np.percentile(imageArray, [25.0, 50.0, 75.0], axis=1)
 
  300         axisStdev = 0.74*(uq - lq)  
 
  302         diff = np.abs(imageArray - axisMedians[:, np.newaxis])
 
  303         return np.ma.masked_where(diff > self.
statControl.getNumSigmaClip() *
 
  304                                   axisStdev[:, np.newaxis], imageArray)
 
  308         """Collapse overscan array (and mask) to a 1-D vector of values. 
  312         maskedArray : `numpy.ma.masked_array` 
  313             Masked array of input overscan data. 
  317         collapsed : `numpy.ma.masked_array` 
  318             Single dimensional overscan data, combined with the mean. 
  320         collapsed = np.mean(maskedArray, axis=1)
 
  321         if collapsed.mask.sum() > 0:
 
  322             collapsed.data[collapsed.mask] = np.mean(maskedArray.data[collapsed.mask], axis=1)
 
  326         """Collapse overscan array (and mask) to a 1-D vector of using the 
  327         correct integer median of row-values. 
  331         maskedArray : `numpy.ma.masked_array` 
  332             Masked array of input overscan data. 
  336         collapsed : `numpy.ma.masked_array` 
  337             Single dimensional overscan data, combined with the afwMath median. 
  343         for row 
in integerMI:
 
  345             collapsed.append(rowMedian)
 
  347         return np.array(collapsed)
 
  350         """Wrapper function to match spline fit API to polynomial fit API. 
  354         indices : `numpy.ndarray` 
  355             Locations to evaluate the spline. 
  356         collapsed : `numpy.ndarray` 
  357             Collapsed overscan values corresponding to the spline 
  360             Number of bins to use in constructing the spline. 
  364         interp : `lsst.afw.math.Interpolate` 
  365             Interpolation object for later evaluation. 
  367         if not np.ma.is_masked(collapsed):
 
  368             collapsed.mask = np.array(len(collapsed)*[np.ma.nomask])
 
  370         numPerBin, binEdges = np.histogram(indices, bins=numBins,
 
  371                                            weights=1 - collapsed.mask.astype(int))
 
  372         with np.errstate(invalid=
"ignore"):
 
  373             values = np.histogram(indices, bins=numBins,
 
  374                                   weights=collapsed.data*~collapsed.mask)[0]/numPerBin
 
  375             binCenters = np.histogram(indices, bins=numBins,
 
  376                                       weights=indices*~collapsed.mask)[0]/numPerBin
 
  378                                              values.astype(float)[numPerBin > 0],
 
  384         """Wrapper function to match spline evaluation API to polynomial fit API. 
  388         indices : `numpy.ndarray` 
  389             Locations to evaluate the spline. 
  390         interp : `lsst.afw.math.interpolate` 
  391             Interpolation object to use. 
  395         values : `numpy.ndarray` 
  396             Evaluated spline values at each index. 
  399         return interp.interpolate(indices.astype(float))
 
  403         """Create mask if edges are extrapolated. 
  407         collapsed : `numpy.ma.masked_array` 
  408             Masked array to check the edges of. 
  412         maskArray : `numpy.ndarray` 
  413             Boolean numpy array of pixels to mask. 
  415         maskArray = np.full_like(collapsed, 
False, dtype=bool)
 
  416         if np.ma.is_masked(collapsed):
 
  418             for low 
in range(num):
 
  419                 if not collapsed.mask[low]:
 
  422                 maskArray[:low] = 
True 
  423             for high 
in range(1, num):
 
  424                 if not collapsed.mask[-high]:
 
  427                 maskArray[-high:] = 
True 
  431         """Calculate the 1-d vector overscan from the input overscan image. 
  435         image : `lsst.afw.image.MaskedImage` 
  436             Image containing the overscan data. 
  440         results : `lsst.pipe.base.Struct` 
  441             Overscan result with entries: 
  442             - ``overscanValue``: Overscan value to subtract (`float`) 
  443             - ``maskArray`` : `list` [ `bool` ] 
  444                 List of rows that should be masked as ``SUSPECT`` when the 
  445                 overscan solution is applied. 
  446             - ``isTransposed`` : `bool` 
  447                Indicates if the overscan data was transposed during 
  448                calcuation, noting along which axis the overscan should be 
  454         calcImage, isTransposed = self.
transpose(calcImage)
 
  457         if self.config.fitType == 
'MEDIAN_PER_ROW':
 
  464             indices = 2.0*np.arange(num)/float(num) - 1.0
 
  468                 'POLY': (poly.polynomial.polyfit, poly.polynomial.polyval),
 
  469                 'CHEB': (poly.chebyshev.chebfit, poly.chebyshev.chebval),
 
  470                 'LEG': (poly.legendre.legfit, poly.legendre.legval),
 
  474             }[self.config.fitType]
 
  476             coeffs = 
fitter(indices, collapsed, self.config.order)
 
  477             overscanVector = evaler(indices, coeffs)
 
  479         return pipeBase.Struct(overscanValue=np.array(overscanVector),
 
  481                                isTransposed=isTransposed)
 
  484         """Debug display for the final overscan solution. 
  488         image : `lsst.afw.image.Image` 
  489             Input image the overscan solution was determined from. 
  490         model : `numpy.ndarray` or `float` 
  491             Overscan model determined for the image. 
  498         calcImage, isTransposed = self.
transpose(calcImage)
 
  503         indices = 2.0 * np.arange(num)/float(num) - 1.0
 
  505         if np.ma.is_masked(collapsed):
 
  506             collapsedMask = collapsed.mask
 
  508             collapsedMask = np.array(num*[np.ma.nomask])
 
  510         import matplotlib.pyplot 
as plot
 
  511         figure = plot.figure(1)
 
  513         axes = figure.add_axes((0.1, 0.1, 0.8, 0.8))
 
  514         axes.plot(indices[~collapsedMask], collapsed[~collapsedMask], 
'k+')
 
  515         if collapsedMask.sum() > 0:
 
  516             axes.plot(indices[collapsedMask], collapsed.data[collapsedMask], 
'b+')
 
  517         if isinstance(model, np.ndarray):
 
  520             plotModel = np.zeros_like(indices)
 
  522         axes.plot(indices, plotModel, 
'r-')
 
  523         plot.xlabel(
"centered/scaled position along overscan region")
 
  524         plot.ylabel(
"pixel value/fit value")
 
  526         prompt = 
"Press Enter or c to continue [chp]..." 
  528             ans = input(prompt).lower()
 
  529             if ans 
in (
"", 
" ", 
"c",):
 
  535                 print(
"[h]elp [c]ontinue [p]db")