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"],
46 itemCheck =
lambda x: x
in afwImage.MaskU().getMaskPlaneDict().keys(),
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)",
138 ConfigClass = MatchBackgroundsConfig
139 _DefaultName =
"matchBackgrounds"
141 pipeBase.Task.__init__(self, *args, **kwargs)
144 self.sctrl.setAndMask(afwImage.MaskU.getPlaneBitMask(self.config.badMaskPlanes))
145 self.sctrl.setNanSafe(
True)
148 def run(self, expRefList, expDatasetType, imageScalerList=None, refExpDataRef=None, refImageScaler=None):
149 """Match the backgrounds of a list of coadd temp exposures to a reference coadd temp exposure.
151 Choose a refExpDataRef automatically if none supplied.
153 @param[in] expRefList: list of data references to science exposures to be background-matched;
154 all exposures must exist.
155 @param[in] expDatasetType: dataset type of exposures, e.g. 'goodSeeingCoadd_tempExp'
156 @param[in] imageScalerList: list of image scalers (coaddUtils.ImageScaler);
157 if None then the images are not scaled
158 @param[in] refExpDataRef: data reference for the reference exposure.
159 If None, then this task selects the best exposures from expRefList.
160 if not None then must be one of the exposures in expRefList.
161 @param[in] refImageScaler: image scaler for reference image;
162 ignored if refExpDataRef is None, else scaling is not performed if None
164 @return: a pipBase.Struct containing these fields:
165 - backgroundInfoList: a list of pipeBase.Struct, one per exposure in expRefList,
166 each of which contains these fields:
167 - isReference: this is the reference exposure (only one returned Struct will
168 contain True for this value, unless the ref exposure is listed multiple times)
169 - backgroundModel: differential background model (afw.Math.Background or afw.Math.Approximate).
170 Add this to the science exposure to match the reference exposure.
171 - fitRMS: rms of the fit. This is the sqrt(mean(residuals**2)).
172 - matchedMSE: the MSE of the reference and matched images: mean((refImage - matchedSciImage)**2);
173 should be comparable to difference image's mean variance.
174 - diffImVar: the mean variance of the difference image.
175 All fields except isReference will be None if isReference True or the fit failed.
177 @warning: all exposures must exist on disk
180 numExp = len(expRefList)
182 raise pipeBase.TaskError(
"No exposures to match")
184 if expDatasetType
is None:
185 raise pipeBase.TaskError(
"Must specify expDatasetType")
187 if imageScalerList
is None:
188 self.log.info(
"imageScalerList is None; no scaling will be performed")
189 imageScalerList = [
None] * numExp
191 if len(expRefList) != len(imageScalerList):
192 raise RuntimeError(
"len(expRefList) = %s != %s = len(imageScalerList)" % \
193 (len(expRefList), len(imageScalerList)))
196 if refExpDataRef
is None:
199 expRefList = expRefList,
200 imageScalerList = imageScalerList,
201 expDatasetType = expDatasetType,
203 refExpDataRef = expRefList[refInd]
204 refImageScaler = imageScalerList[refInd]
209 expKeyList = refExpDataRef.butlerSubset.butler.getKeys(expDatasetType)
210 refMatcher =
DataRefMatcher(refExpDataRef.butlerSubset.butler, expDatasetType)
211 refIndSet = set(refMatcher.matchList(ref0 = refExpDataRef, refList = expRefList))
213 if refInd
is not None and refInd
not in refIndSet:
214 raise RuntimeError(
"Internal error: selected reference %s not found in expRefList")
216 refExposure = refExpDataRef.get(expDatasetType, immediate=
True)
217 if refImageScaler
is not None:
218 refMI = refExposure.getMaskedImage()
219 refImageScaler.scaleMaskedImage(refMI)
221 debugIdKeyList = tuple(set(expKeyList) - set([
'tract',
'patch']))
223 self.log.info(
"Matching %d Exposures" % (numExp))
225 backgroundInfoList = []
226 for ind, (toMatchRef, imageScaler)
in enumerate(zip(expRefList, imageScalerList)):
228 backgroundInfoStruct = pipeBase.Struct(
230 backgroundModel =
None,
236 self.log.info(
"Matching background of %s to %s" % (toMatchRef.dataId, refExpDataRef.dataId))
238 toMatchExposure = toMatchRef.get(expDatasetType, immediate=
True)
239 if imageScaler
is not None:
240 toMatchMI = toMatchExposure.getMaskedImage()
241 imageScaler.scaleMaskedImage(toMatchMI)
245 refExposure = refExposure,
246 sciExposure = toMatchExposure,
248 backgroundInfoStruct.isReference =
False
250 self.log.warn(
"Failed to fit background %s: %s" % (toMatchRef.dataId, e))
251 backgroundInfoStruct = pipeBase.Struct(
253 backgroundModel =
None,
259 backgroundInfoList.append(backgroundInfoStruct)
261 return pipeBase.Struct(
262 backgroundInfoList = backgroundInfoList)
266 """Find best exposure to use as the reference exposure
268 Calculate an appropriate reference exposure by minimizing a cost function that penalizes
269 high variance, high background level, and low coverage. Use the following config parameters:
270 - bestRefWeightCoverage
271 - bestRefWeightVariance
274 @param[in] expRefList: list of data references to exposures.
275 Retrieves dataset type specified by expDatasetType.
276 If an exposure is not found, it is skipped with a warning.
277 @param[in] imageScalerList: list of image scalers (coaddUtils.ImageScaler);
278 must be the same length as expRefList
279 @param[in] expDatasetType: dataset type of exposure: e.g. 'goodSeeingCoadd_tempExp'
281 @return: index of best exposure
283 @raise pipeBase.TaskError if none of the exposures in expRefList are found.
285 self.log.info(
"Calculating best reference visit")
287 meanBkgdLevelList = []
290 if len(expRefList) != len(imageScalerList):
291 raise RuntimeError(
"len(expRefList) = %s != %s = len(imageScalerList)" % \
292 (len(expRefList), len(imageScalerList)))
294 for expRef, imageScaler
in zip(expRefList, imageScalerList):
295 exposure = expRef.get(expDatasetType, immediate=
True)
296 maskedImage = exposure.getMaskedImage()
297 if imageScaler
is not None:
299 imageScaler.scaleMaskedImage(maskedImage)
302 varList.append(numpy.nan)
303 meanBkgdLevelList.append(numpy.nan)
304 coverageList.append(numpy.nan)
307 afwMath.MEAN | afwMath.NPOINT | afwMath.VARIANCE, self.
sctrl)
308 meanVar, meanVarErr = statObjIm.getResult(afwMath.VARIANCE)
309 meanBkgdLevel, meanBkgdLevelErr = statObjIm.getResult(afwMath.MEAN)
310 npoints, npointsErr = statObjIm.getResult(afwMath.NPOINT)
311 varList.append(meanVar)
312 meanBkgdLevelList.append(meanBkgdLevel)
313 coverageList.append(npoints)
315 raise pipeBase.TaskError(
316 "None of the candidate %s exist; cannot select best reference exposure" % (expDatasetType,))
319 varArr = numpy.array(varList)/numpy.nanmax(varList)
320 meanBkgdLevelArr = numpy.array(meanBkgdLevelList)/numpy.nanmax(meanBkgdLevelList)
321 coverageArr = numpy.nanmin(coverageList)/numpy.array(coverageList)
323 costFunctionArr = self.config.bestRefWeightVariance * varArr
324 costFunctionArr += self.config.bestRefWeightLevel * meanBkgdLevelArr
325 costFunctionArr += self.config.bestRefWeightCoverage * coverageArr
326 return numpy.nanargmin(costFunctionArr)
332 Match science exposure's background level to that of reference exposure.
334 Process creates a difference image of the reference exposure minus the science exposure, and then
335 generates an afw.math.Background object. It assumes (but does not require/check) that the mask plane
336 already has detections set. If detections have not been set/masked, sources will bias the
337 background estimation.
338 The 'background' of the difference image is smoothed by spline interpolation (by the Background class)
339 or by polynomial interpolation by the Approximate class. This model of difference image is added to the
340 science exposure in memory.
341 Fit diagnostics are also calculated and returned.
343 @param[in] refExposure: reference exposure
344 @param[in,out] sciExposure: science exposure; modified by changing the background level
345 to match that of the reference exposure
346 @returns a pipBase.Struct with fields:
347 - backgroundModel: an afw.math.Approximate or an afw.math.Background.
348 - fitRMS: rms of the fit. This is the sqrt(mean(residuals**2)).
349 - matchedMSE: the MSE of the reference and matched images: mean((refImage - matchedSciImage)**2);
350 should be comparable to difference image's mean variance.
351 - diffImVar: the mean variance of the difference image.
355 refExposure.writeFits(
lsstDebug.Info(__name__).figpath +
'refExposure.fits')
356 sciExposure.writeFits(
lsstDebug.Info(__name__).figpath +
'sciExposure.fits')
359 if self.config.usePolynomial:
360 x, y = sciExposure.getDimensions()
361 shortSideLength = min(x, y)
362 if shortSideLength < self.config.binSize:
363 raise ValueError(
"%d = config.binSize > shorter dimension = %d" % (self.config.binSize,
365 npoints = shortSideLength // self.config.binSize
366 if shortSideLength % self.config.binSize != 0:
369 if self.config.order > npoints - 1:
370 raise ValueError(
"%d = config.order > npoints - 1 = %d" % (self.config.order, npoints - 1))
373 if (sciExposure.getDimensions() != refExposure.getDimensions()):
374 wSci, hSci = sciExposure.getDimensions()
375 wRef, hRef = refExposure.getDimensions()
377 "Exposures are different dimensions. sci:(%i, %i) vs. ref:(%i, %i)" %
378 (wSci,hSci,wRef,hRef))
380 statsFlag = getattr(afwMath, self.config.gridStatistic)
381 self.sctrl.setNumSigmaClip(self.config.numSigmaClip)
382 self.sctrl.setNumIter(self.config.numIter)
384 im = refExposure.getMaskedImage()
385 diffMI = im.Factory(im,
True)
386 diffMI -= sciExposure.getMaskedImage()
388 width = diffMI.getWidth()
389 height = diffMI.getHeight()
390 nx = width // self.config.binSize
391 if width % self.config.binSize != 0:
393 ny = height // self.config.binSize
394 if height % self.config.binSize != 0:
398 bctrl.setUndersampleStyle(self.config.undersampleStyle)
399 bctrl.setInterpStyle(self.config.interpStyle)
406 if self.config.usePolynomial:
407 order = self.config.order
408 bgX, bgY, bgZ, bgdZ = self.
_gridImage(diffMI, self.config.binSize, statsFlag)
409 minNumberGridPoints = min(len(set(bgX)),len(set(bgY)))
411 raise ValueError(
"No overlap with reference. Nothing to match")
412 elif minNumberGridPoints <= self.config.order:
414 if self.config.undersampleStyle ==
"THROW_EXCEPTION":
415 raise ValueError(
"Image does not cover enough of ref image for order and binsize")
416 elif self.config.undersampleStyle ==
"REDUCE_INTERP_ORDER":
417 self.log.warn(
"Reducing order to %d"%(minNumberGridPoints - 1))
418 order = minNumberGridPoints - 1
419 elif self.config.undersampleStyle ==
"INCREASE_NXNYSAMPLE":
420 newBinSize = (minNumberGridPoints*self.config.binSize)// (self.config.order +1)
421 bctrl.setNxSample(newBinSize)
422 bctrl.setNySample(newBinSize)
424 self.log.warn(
"Decreasing binsize to %d"%(newBinSize))
427 isUniformImageDiff =
not numpy.any(bgdZ > self.config.gridStdevEpsilon)
428 weightByInverseVariance =
False if isUniformImageDiff
else self.config.approxWeighting
432 if self.config.usePolynomial:
434 order, order, weightByInverseVariance)
435 undersampleStyle = getattr(afwMath, self.config.undersampleStyle)
436 approx = bkgd.getApproximate(actrl,undersampleStyle)
437 bkgdImage = approx.getImage()
439 bkgdImage = bkgd.getImageF()
441 raise RuntimeError(
"Background/Approximation failed to interp image %s: %s" % (
444 sciMI = sciExposure.getMaskedImage()
450 X, Y, Z, dZ = self.
_gridImage(diffMI, self.config.binSize, statsFlag)
451 x0, y0 = diffMI.getXY0()
452 modelValueArr = numpy.empty(len(Z))
453 for i
in range(len(X)):
454 modelValueArr[i] = bkgdImage.get(int(X[i]-x0),int(Y[i]-y0))
455 resids = Z - modelValueArr
456 rms = numpy.sqrt(numpy.mean(resids[~numpy.isnan(resids)]**2))
459 sciExposure.writeFits(
lsstDebug.Info(__name__).figpath +
'sciMatchedExposure.fits')
464 self.
_debugPlot(X, Y, Z, dZ, bkgdImage, bbox, modelValueArr, resids)
466 self.log.warn(
'Debug plot not generated: %s'%(e))
469 afwMath.MEANCLIP, self.
sctrl).getValue()
471 diffIm = diffMI.getImage()
476 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))
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.
585 """Construct a DataRefMatcher
588 @param[in] datasetType: dataset type to match
594 """Return a tuple of values for the specified keyNames
596 @param[in] ref: data reference
598 @raise KeyError if ref.dataId is missing a key in keyNames
600 return tuple(ref.dataId[key]
for key
in self.
_keyNames)
603 """Return True if ref0 == ref1
605 @param[in] ref0: data ref 0
606 @param[in] ref1: data ref 1
608 @raise KeyError if either ID is missing a key in keyNames
613 """Return a list of indices of matches
615 @return tuple of indices of matches
617 @raise KeyError if any ID is missing a key in keyNames
620 return tuple(ind
for ind, ref
in enumerate(refList)
if self.
_makeKey(ref) == key0)
boost::shared_ptr< Background > makeBackground(ImageT const &img, BackgroundControl const &bgCtrl)
A convenience function that uses function overloading to make the correct type of Background...
Pass parameters to a Background object.
An integer coordinate rectangle.
Control how to make an approximation.
Pass parameters to a Statistics objectA class to pass parameters which control how the stats are calc...
Statistics makeStatistics(afwImage::Mask< afwImage::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl)
Specialization to handle Masks.
A floating-point coordinate rectangle geometry.