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)
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects...
An integer coordinate rectangle.