32     usePolynomial = pexConfig.Field(
    34         doc=
"Fit background difference with Chebychev polynomial interpolation "    35         "(using afw.math.Approximate)? If False, fit with spline interpolation using afw.math.Background",
    38     order = pexConfig.Field(
    40         doc=
"Order of Chebyshev polynomial background model. Ignored if usePolynomial False",
    43     badMaskPlanes = pexConfig.ListField(
    44         doc=
"Names of mask planes to ignore while estimating the background",
    45         dtype=str, default=[
"NO_DATA", 
"DETECTED", 
"DETECTED_NEGATIVE", 
"SAT", 
"BAD", 
"INTRP", 
"CR"],
    48     gridStatistic = pexConfig.ChoiceField(
    50         doc=
"Type of statistic to estimate pixel value for the grid points",
    55             "MEANCLIP": 
"clipped mean"    58     undersampleStyle = pexConfig.ChoiceField(
    59         doc=
"Behaviour if there are too few points in grid for requested interpolation style. "    60         "Note: INCREASE_NXNYSAMPLE only allowed for usePolynomial=True.",
    62         default=
"REDUCE_INTERP_ORDER",
    64             "THROW_EXCEPTION": 
"throw an exception if there are too few points",
    65             "REDUCE_INTERP_ORDER": 
"use an interpolation style with a lower order.",
    66             "INCREASE_NXNYSAMPLE": 
"Increase the number of samples used to make the interpolation grid.",
    69     binSize = pexConfig.Field(
    70         doc=
"Bin size for gridding the difference image and fitting a spatial model",
    74     interpStyle = pexConfig.ChoiceField(
    76         doc=
"Algorithm to interpolate the background values; ignored if usePolynomial is True"    77         "Maps to an enum; see afw.math.Background",
    78         default=
"AKIMA_SPLINE",
    80             "CONSTANT": 
"Use a single constant value",
    81             "LINEAR": 
"Use linear interpolation",
    82             "NATURAL_SPLINE": 
"cubic spline with zero second derivative at endpoints",
    83             "AKIMA_SPLINE": 
"higher-level nonlinear spline that is more robust to outliers",
    84             "NONE": 
"No background estimation is to be attempted",
    87     numSigmaClip = pexConfig.Field(
    89         doc=
"Sigma for outlier rejection; ignored if gridStatistic != 'MEANCLIP'.",
    92     numIter = pexConfig.Field(
    94         doc=
"Number of iterations of outlier rejection; ignored if gridStatistic != 'MEANCLIP'.",
    97     bestRefWeightCoverage = pexConfig.RangeField(
    99         doc=
"Weight given to coverage (number of pixels that overlap with patch), "   100         "when calculating best reference exposure. Higher weight prefers exposures with high coverage."   101         "Ignored when reference visit is supplied",
   105     bestRefWeightVariance = pexConfig.RangeField(
   107         doc=
"Weight given to image variance when calculating best reference exposure. "   108         "Higher weight prefers exposures with low image variance. Ignored when reference visit is supplied",
   112     bestRefWeightLevel = pexConfig.RangeField(
   114         doc=
"Weight given to mean background level when calculating best reference exposure. "   115         "Higher weight prefers exposures with low mean background level. "   116         "Ignored when reference visit is supplied.",
   120     approxWeighting = pexConfig.Field(
   122         doc=(
"Use inverse-variance weighting when approximating background offset model? "   123              "This will fail when the background offset is constant "   124              "(this is usually only the case in testing with artificial images)."   125              "(usePolynomial=True)"),
   128     gridStdevEpsilon = pexConfig.RangeField(
   130         doc=
"Tolerance on almost zero standard deviation in a background-offset grid bin. "   131         "If all bins have a standard deviation below this value, the background offset model "   132         "is approximated without inverse-variance weighting. (usePolynomial=True)",
   139     ConfigClass = MatchBackgroundsConfig
   140     _DefaultName = 
"matchBackgrounds"   143         pipeBase.Task.__init__(self, *args, **kwargs)
   146         self.
sctrl.setAndMask(afwImage.Mask.getPlaneBitMask(self.config.badMaskPlanes))
   147         self.
sctrl.setNanSafe(
True)
   150     def run(self, expRefList, expDatasetType, imageScalerList=None, refExpDataRef=None, refImageScaler=None):
   151         """Match the backgrounds of a list of coadd temp exposures to a reference coadd temp exposure.   153         Choose a refExpDataRef automatically if none supplied.   155         @param[in] expRefList: list of data references to science exposures to be background-matched;   156             all exposures must exist.   157         @param[in] expDatasetType: dataset type of exposures, e.g. 'goodSeeingCoadd_tempExp'   158         @param[in] imageScalerList: list of image scalers (coaddUtils.ImageScaler);   159             if None then the images are not scaled   160         @param[in] refExpDataRef: data reference for the reference exposure.   161             If None, then this task selects the best exposures from expRefList.   162             if not None then must be one of the exposures in expRefList.   163         @param[in] refImageScaler: image scaler for reference image;   164             ignored if refExpDataRef is None, else scaling is not performed if None   166         @return: a pipBase.Struct containing these fields:   167         - backgroundInfoList: a list of pipeBase.Struct, one per exposure in expRefList,   168             each of which contains these fields:   169             - isReference: this is the reference exposure (only one returned Struct will   170                 contain True for this value, unless the ref exposure is listed multiple times)   171             - backgroundModel: differential background model (afw.Math.Background or afw.Math.Approximate).   172                 Add this to the science exposure to match the reference exposure.   173             - fitRMS: rms of the fit. This is the sqrt(mean(residuals**2)).   174             - matchedMSE: the MSE of the reference and matched images: mean((refImage - matchedSciImage)**2);   175               should be comparable to difference image's mean variance.   176             - diffImVar: the mean variance of the difference image.   177             All fields except isReference will be None if isReference True or the fit failed.   179         @warning: all exposures must exist on disk   182         numExp = len(expRefList)
   184             raise pipeBase.TaskError(
"No exposures to match")
   186         if expDatasetType 
is None:
   187             raise pipeBase.TaskError(
"Must specify expDatasetType")
   189         if imageScalerList 
is None:
   190             self.log.
info(
"imageScalerList is None; no scaling will be performed")
   191             imageScalerList = [
None] * numExp
   193         if len(expRefList) != len(imageScalerList):
   194             raise RuntimeError(
"len(expRefList) = %s != %s = len(imageScalerList)" %
   195                                (len(expRefList), len(imageScalerList)))
   198         if refExpDataRef 
is None:
   201                 expRefList=expRefList,
   202                 imageScalerList=imageScalerList,
   203                 expDatasetType=expDatasetType,
   205             refExpDataRef = expRefList[refInd]
   206             refImageScaler = imageScalerList[refInd]
   211         expKeyList = refExpDataRef.butlerSubset.butler.getKeys(expDatasetType)
   212         refMatcher = 
DataRefMatcher(refExpDataRef.butlerSubset.butler, expDatasetType)
   213         refIndSet = 
set(refMatcher.matchList(ref0=refExpDataRef, refList=expRefList))
   215         if refInd 
is not None and refInd 
not in refIndSet:
   216             raise RuntimeError(
"Internal error: selected reference %s not found in expRefList")
   218         refExposure = refExpDataRef.get(expDatasetType, immediate=
True)
   219         if refImageScaler 
is not None:
   220             refMI = refExposure.getMaskedImage()
   221             refImageScaler.scaleMaskedImage(refMI)
   223         debugIdKeyList = tuple(
set(expKeyList) - 
set([
'tract', 
'patch']))
   225         self.log.
info(
"Matching %d Exposures" % (numExp))
   227         backgroundInfoList = []
   228         for ind, (toMatchRef, imageScaler) 
in enumerate(zip(expRefList, imageScalerList)):
   230                 backgroundInfoStruct = pipeBase.Struct(
   232                     backgroundModel=
None,
   238                 self.log.
info(
"Matching background of %s to %s" % (toMatchRef.dataId, refExpDataRef.dataId))
   240                     toMatchExposure = toMatchRef.get(expDatasetType, immediate=
True)
   241                     if imageScaler 
is not None:
   242                         toMatchMI = toMatchExposure.getMaskedImage()
   243                         imageScaler.scaleMaskedImage(toMatchMI)
   247                         refExposure=refExposure,
   248                         sciExposure=toMatchExposure,
   250                     backgroundInfoStruct.isReference = 
False   251                 except Exception 
as e:
   252                     self.log.
warn(
"Failed to fit background %s: %s" % (toMatchRef.dataId, e))
   253                     backgroundInfoStruct = pipeBase.Struct(
   255                         backgroundModel=
None,
   261             backgroundInfoList.append(backgroundInfoStruct)
   263         return pipeBase.Struct(
   264             backgroundInfoList=backgroundInfoList)
   268         """Find best exposure to use as the reference exposure   270         Calculate an appropriate reference exposure by minimizing a cost function that penalizes   271         high variance,  high background level, and low coverage. Use the following config parameters:   272         - bestRefWeightCoverage   273         - bestRefWeightVariance   276         @param[in] expRefList: list of data references to exposures.   277             Retrieves dataset type specified by expDatasetType.   278             If an exposure is not found, it is skipped with a warning.   279         @param[in] imageScalerList: list of image scalers (coaddUtils.ImageScaler);   280             must be the same length as expRefList   281         @param[in] expDatasetType: dataset type of exposure: e.g. 'goodSeeingCoadd_tempExp'   283         @return: index of best exposure   285         @raise pipeBase.TaskError if none of the exposures in expRefList are found.   287         self.log.
info(
"Calculating best reference visit")
   289         meanBkgdLevelList = []
   292         if len(expRefList) != len(imageScalerList):
   293             raise RuntimeError(
"len(expRefList) = %s != %s = len(imageScalerList)" %
   294                                (len(expRefList), len(imageScalerList)))
   296         for expRef, imageScaler 
in zip(expRefList, imageScalerList):
   297             exposure = expRef.get(expDatasetType, immediate=
True)
   298             maskedImage = exposure.getMaskedImage()
   299             if imageScaler 
is not None:
   301                     imageScaler.scaleMaskedImage(maskedImage)
   304                     varList.append(numpy.nan)
   305                     meanBkgdLevelList.append(numpy.nan)
   306                     coverageList.append(numpy.nan)
   309                                                afwMath.MEAN | afwMath.NPOINT | afwMath.VARIANCE, self.
sctrl)
   310             meanVar, meanVarErr = statObjIm.getResult(afwMath.VARIANCE)
   311             meanBkgdLevel, meanBkgdLevelErr = statObjIm.getResult(afwMath.MEAN)
   312             npoints, npointsErr = statObjIm.getResult(afwMath.NPOINT)
   313             varList.append(meanVar)
   314             meanBkgdLevelList.append(meanBkgdLevel)
   315             coverageList.append(npoints)
   317             raise pipeBase.TaskError(
   318                 "None of the candidate %s exist; cannot select best reference exposure" % (expDatasetType,))
   321         varArr = numpy.array(varList)/numpy.nanmax(varList)
   322         meanBkgdLevelArr = numpy.array(meanBkgdLevelList)/numpy.nanmax(meanBkgdLevelList)
   323         coverageArr = numpy.nanmin(coverageList)/numpy.array(coverageList)
   325         costFunctionArr = self.config.bestRefWeightVariance * varArr
   326         costFunctionArr += self.config.bestRefWeightLevel * meanBkgdLevelArr
   327         costFunctionArr += self.config.bestRefWeightCoverage * coverageArr
   328         return numpy.nanargmin(costFunctionArr)
   333         Match science exposure's background level to that of reference exposure.   335         Process creates a difference image of the reference exposure minus the science exposure, and then   336         generates an afw.math.Background object. It assumes (but does not require/check) that the mask plane   337         already has detections set. If detections have not been set/masked, sources will bias the   338         background estimation.   339         The 'background' of the difference image is smoothed by spline interpolation (by the Background class)   340         or by polynomial interpolation by the Approximate class. This model of difference image   341         is added to the science exposure in memory.   342         Fit diagnostics are also calculated and returned.   344         @param[in] refExposure: reference exposure   345         @param[in,out] sciExposure: science exposure; modified by changing the background level   346             to match that of the reference exposure   347         @returns a pipBase.Struct with fields:   348             - backgroundModel: an afw.math.Approximate or an afw.math.Background.   349             - fitRMS: rms of the fit. This is the sqrt(mean(residuals**2)).   350             - matchedMSE: the MSE of the reference and matched images: mean((refImage - matchedSciImage)**2);   351               should be comparable to difference image's mean variance.   352             - diffImVar: the mean variance of the difference image.   356             refExposure.writeFits(
lsstDebug.Info(__name__).figpath + 
'refExposure.fits')
   357             sciExposure.writeFits(
lsstDebug.Info(__name__).figpath + 
'sciExposure.fits')
   360         if self.config.usePolynomial:
   361             x, y = sciExposure.getDimensions()
   362             shortSideLength = 
min(x, y)
   363             if shortSideLength < self.config.binSize:
   364                 raise ValueError(
"%d = config.binSize > shorter dimension = %d" % (self.config.binSize,
   366             npoints = shortSideLength // self.config.binSize
   367             if shortSideLength % self.config.binSize != 0:
   370             if self.config.order > npoints - 1:
   371                 raise ValueError(
"%d = config.order > npoints - 1 = %d" % (self.config.order, npoints - 1))
   374         if (sciExposure.getDimensions() != refExposure.getDimensions()):
   375             wSci, hSci = sciExposure.getDimensions()
   376             wRef, hRef = refExposure.getDimensions()
   378                 "Exposures are different dimensions. sci:(%i, %i) vs. ref:(%i, %i)" %
   379                 (wSci, hSci, wRef, hRef))
   381         statsFlag = getattr(afwMath, self.config.gridStatistic)
   382         self.
sctrl.setNumSigmaClip(self.config.numSigmaClip)
   383         self.
sctrl.setNumIter(self.config.numIter)
   385         im = refExposure.getMaskedImage()
   386         diffMI = im.Factory(im, 
True)
   387         diffMI -= sciExposure.getMaskedImage()
   389         width = diffMI.getWidth()
   390         height = diffMI.getHeight()
   391         nx = width // self.config.binSize
   392         if width % self.config.binSize != 0:
   394         ny = height // self.config.binSize
   395         if height % self.config.binSize != 0:
   399         bctrl.setUndersampleStyle(self.config.undersampleStyle)
   400         bctrl.setInterpStyle(self.config.interpStyle)
   407         if self.config.usePolynomial:
   408             order = self.config.order
   409             bgX, bgY, bgZ, bgdZ = self.
_gridImage(diffMI, self.config.binSize, statsFlag)
   410             minNumberGridPoints = 
min(len(
set(bgX)), len(
set(bgY)))
   412                 raise ValueError(
"No overlap with reference. Nothing to match")
   413             elif minNumberGridPoints <= self.config.order:
   415                 if self.config.undersampleStyle == 
"THROW_EXCEPTION":
   416                     raise ValueError(
"Image does not cover enough of ref image for order and binsize")
   417                 elif self.config.undersampleStyle == 
"REDUCE_INTERP_ORDER":
   418                     self.log.
warn(
"Reducing order to %d"%(minNumberGridPoints - 1))
   419                     order = minNumberGridPoints - 1
   420                 elif self.config.undersampleStyle == 
"INCREASE_NXNYSAMPLE":
   421                     newBinSize = (minNumberGridPoints*self.config.binSize) // (self.config.order + 1)
   422                     bctrl.setNxSample(newBinSize)
   423                     bctrl.setNySample(newBinSize)
   425                     self.log.
warn(
"Decreasing binsize to %d"%(newBinSize))
   428             isUniformImageDiff = 
not numpy.any(bgdZ > self.config.gridStdevEpsilon)
   429             weightByInverseVariance = 
False if isUniformImageDiff 
else self.config.approxWeighting
   433             if self.config.usePolynomial:
   435                                                    order, order, weightByInverseVariance)
   436                 undersampleStyle = getattr(afwMath, self.config.undersampleStyle)
   437                 approx = bkgd.getApproximate(actrl, undersampleStyle)
   438                 bkgdImage = approx.getImage()
   440                 bkgdImage = bkgd.getImageF()
   441         except Exception 
as e:
   442             raise RuntimeError(
"Background/Approximation failed to interp image %s: %s" % (
   445         sciMI = sciExposure.getMaskedImage()
   451         X, Y, Z, dZ = self.
_gridImage(diffMI, self.config.binSize, statsFlag)
   452         x0, y0 = diffMI.getXY0()
   453         modelValueArr = numpy.empty(len(Z))
   454         for i 
in range(len(X)):
   455             modelValueArr[i] = bkgdImage[
int(X[i]-x0), 
int(Y[i]-y0), afwImage.LOCAL]
   456         resids = Z - modelValueArr
   457         rms = numpy.sqrt(numpy.mean(resids[~numpy.isnan(resids)]**2))
   460             sciExposure.writeFits(
lsstDebug.Info(__name__).figpath + 
'sciMatchedExposure.fits')
   465                 self.
_debugPlot(X, Y, Z, dZ, bkgdImage, bbox, modelValueArr, resids)
   466             except Exception 
as e:
   467                 self.log.
warn(
'Debug plot not generated: %s'%(e))
   470                                          afwMath.MEANCLIP, self.
sctrl).getValue()
   472         diffIm = diffMI.getImage()
   477         outBkgd = approx 
if self.config.usePolynomial 
else bkgd
   479         return pipeBase.Struct(
   480             backgroundModel=outBkgd,
   485     def _debugPlot(self, X, Y, Z, dZ, modelImage, bbox, model, resids):
   486         """Generate a plot showing the background fit and residuals.   488         It is called when lsstDebug.Info(__name__).savefig = True   489         Saves the fig to lsstDebug.Info(__name__).figpath   490         Displays on screen if lsstDebug.Info(__name__).display = True   492         @param X: array of x positions   493         @param Y: array of y positions   494         @param Z: array of the grid values that were interpolated   495         @param dZ: array of the error on the grid values   496         @param modelImage: image ofthe model of the fit   497         @param model: array of len(Z) containing the grid values predicted by the model   498         @param resids: Z - model   500         import matplotlib.pyplot 
as plt
   501         import matplotlib.colors
   502         from mpl_toolkits.axes_grid1 
import ImageGrid
   505         x0, y0 = zeroIm.getXY0()
   506         dx, dy = zeroIm.getDimensions()
   508             self.log.
warn(
"No grid. Skipping plot generation.")
   510             max, min = numpy.max(Z), numpy.min(Z)
   511             norm = matplotlib.colors.normalize(vmax=max, vmin=min)
   512             maxdiff = numpy.max(numpy.abs(resids))
   513             diffnorm = matplotlib.colors.normalize(vmax=maxdiff, vmin=-maxdiff)
   514             rms = numpy.sqrt(numpy.mean(resids**2))
   515             fig = plt.figure(1, (8, 6))
   516             meanDz = numpy.mean(dZ)
   517             grid = ImageGrid(fig, 111, nrows_ncols=(1, 2), axes_pad=0.1,
   518                              share_all=
True, label_mode=
"L", cbar_mode=
"each",
   519                              cbar_size=
"7%", cbar_pad=
"2%", cbar_location=
"top")
   520             im = grid[0].imshow(zeroIm.getImage().getArray(),
   521                                 extent=(x0, x0+dx, y0+dy, y0), norm=norm,
   523             im = grid[0].scatter(X, Y, c=Z, s=15.*meanDz/dZ, edgecolor=
'none', norm=norm,
   524                                  marker=
'o', cmap=
'Spectral')
   525             im2 = grid[1].scatter(X, Y, c=resids, edgecolor=
'none', norm=diffnorm,
   526                                   marker=
's', cmap=
'seismic')
   527             grid.cbar_axes[0].colorbar(im)
   528             grid.cbar_axes[1].colorbar(im2)
   529             grid[0].axis([x0, x0+dx, y0+dy, y0])
   530             grid[1].axis([x0, x0+dx, y0+dy, y0])
   531             grid[0].set_xlabel(
"model and grid")
   532             grid[1].set_xlabel(
"residuals. rms = %0.3f"%(rms))
   539     def _gridImage(self, maskedImage, binsize, statsFlag):
   540         """Private method to grid an image for debugging"""   541         width, height = maskedImage.getDimensions()
   542         x0, y0 = maskedImage.getXY0()
   543         xedges = numpy.arange(0, width, binsize)
   544         yedges = numpy.arange(0, height, binsize)
   545         xedges = numpy.hstack((xedges, width))  
   546         yedges = numpy.hstack((yedges, height))  
   555         for ymin, ymax 
in zip(yedges[0:-1], yedges[1:]):
   556             for xmin, xmax 
in zip(xedges[0:-1], xedges[1:]):
   559                 subIm = afwImage.MaskedImageF(maskedImage, subBBox, afwImage.PARENT, 
False)
   561                                                afwMath.MEAN | afwMath.MEANCLIP | afwMath.MEDIAN |
   562                                                afwMath.NPOINT | afwMath.STDEV,
   564                 npoints, _ = stats.getResult(afwMath.NPOINT)
   566                     stdev, _ = stats.getResult(afwMath.STDEV)
   567                     if stdev < self.config.gridStdevEpsilon:
   568                         stdev = self.config.gridStdevEpsilon
   569                     bgX.append(0.5 * (x0 + xmin + x0 + xmax))
   570                     bgY.append(0.5 * (y0 + ymin + y0 + ymax))
   571                     bgdZ.append(stdev/numpy.sqrt(npoints))
   572                     est, _ = stats.getResult(statsFlag)
   575         return numpy.array(bgX), numpy.array(bgY), numpy.array(bgZ), numpy.array(bgdZ)
   579     """Match data references for a specified dataset type   581     Note that this is not exact, but should suffice for this task   582     until there is better support for this kind of thing in the butler.   586         """Construct a DataRefMatcher   589         @param[in] datasetType: dataset type to match   592         self.
_keyNames = butler.getKeys(datasetType)
   594     def _makeKey(self, ref):
   595         """Return a tuple of values for the specified keyNames   597         @param[in] ref: data reference   599         @raise KeyError if ref.dataId is missing a key in keyNames   601         return tuple(ref.dataId[key] 
for key 
in self.
_keyNames)
   604         """Return True if ref0 == ref1   606         @param[in] ref0: data ref 0   607         @param[in] ref1: data ref 1   609         @raise KeyError if either ID is missing a key in keyNames   614         """Return a list of indices of matches   616         @return tuple of indices of matches   618         @raise KeyError if any ID is missing a key in keyNames   621         return tuple(ind 
for ind, ref 
in enumerate(refList) 
if self.
_makeKey(ref) == key0)
 
A floating-point coordinate rectangle geometry. 
def isMatch(self, ref0, ref1)
def __init__(self, butler, datasetType)
def selectRefExposure(self, expRefList, imageScalerList, expDatasetType)
def matchBackgrounds(self, refExposure, sciExposure)
def _debugPlot(self, X, Y, Z, dZ, modelImage, bbox, model, resids)
std::shared_ptr< Background > makeBackground(ImageT const &img, BackgroundControl const &bgCtrl)
A convenience function that uses function overloading to make the correct type of Background...
daf::base::PropertySet * set
Pass parameters to a Background object. 
Statistics makeStatistics(lsst::afw::math::MaskedVector< EntryT > const &mv, std::vector< WeightPixel > const &vweights, int const flags, StatisticsControl const &sctrl=StatisticsControl())
The makeStatistics() overload to handle lsst::afw::math::MaskedVector<> 
Control how to make an approximation. 
Pass parameters to a Statistics object. 
def run(self, expRefList, expDatasetType, imageScalerList=None, refExpDataRef=None, refImageScaler=None)
Represent a 2-dimensional array of bitmask pixels. 
def __init__(self, args, kwargs)
def _gridImage(self, maskedImage, binsize, statsFlag)
def matchList(self, ref0, refList)
An integer coordinate rectangle.