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.
sctrlsctrl.setAndMask(afwImage.Mask.getPlaneBitMask(self.config.badMaskPlanes))
147 self.
sctrlsctrl.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)
245 self.
debugDataIdStringdebugDataIdString =
''.join([str(toMatchRef.dataId[vk])
for vk
in debugIdKeyList])
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.
sctrlsctrl)
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.
sctrlsctrl.setNumSigmaClip(self.config.numSigmaClip)
383 self.
sctrlsctrl.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)
406 if self.config.usePolynomial:
407 order = self.config.order
408 bgX, bgY, bgZ, bgdZ = self.
_gridImage_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(self.config.interpStyle, self.config.undersampleStyle)
440 except Exception
as e:
441 raise RuntimeError(
"Background/Approximation failed to interp image %s: %s" % (
444 sciMI = sciExposure.getMaskedImage()
450 X, Y, Z, dZ = self.
_gridImage_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[int(X[i]-x0), int(Y[i]-y0), afwImage.LOCAL]
455 resids = Z - modelValueArr
456 rms = numpy.sqrt(numpy.mean(resids[~numpy.isnan(resids)]**2))
459 sciExposure.writeFits(
lsstDebug.Info(__name__).figpath +
'sciMatchedExposure.fits')
462 bbox =
geom.Box2D(refExposure.getMaskedImage().getBBox())
464 self.
_debugPlot_debugPlot(X, Y, Z, dZ, bkgdImage, bbox, modelValueArr, resids)
465 except Exception
as e:
466 self.log.
warn(
'Debug plot not generated: %s'%(e))
469 afwMath.MEANCLIP, self.
sctrlsctrl).getValue()
471 diffIm = diffMI.getImage()
476 outBkgd = approx
if self.config.usePolynomial
else bkgd
478 return pipeBase.Struct(
479 backgroundModel=outBkgd,
484 def _debugPlot(self, X, Y, Z, dZ, modelImage, bbox, model, resids):
485 """Generate a plot showing the background fit and residuals.
487 It is called when lsstDebug.Info(__name__).savefig = True
488 Saves the fig to lsstDebug.Info(__name__).figpath
489 Displays on screen if lsstDebug.Info(__name__).display = True
491 @param X: array of x positions
492 @param Y: array of y positions
493 @param Z: array of the grid values that were interpolated
494 @param dZ: array of the error on the grid values
495 @param modelImage: image ofthe model of the fit
496 @param model: array of len(Z) containing the grid values predicted by the model
497 @param resids: Z - model
499 import matplotlib.pyplot
as plt
500 import matplotlib.colors
501 from mpl_toolkits.axes_grid1
import ImageGrid
502 zeroIm = afwImage.MaskedImageF(
geom.Box2I(bbox))
504 x0, y0 = zeroIm.getXY0()
505 dx, dy = zeroIm.getDimensions()
507 self.log.
warn(
"No grid. Skipping plot generation.")
509 max, min = numpy.max(Z), numpy.min(Z)
510 norm = matplotlib.colors.normalize(vmax=max, vmin=min)
511 maxdiff = numpy.max(numpy.abs(resids))
512 diffnorm = matplotlib.colors.normalize(vmax=maxdiff, vmin=-maxdiff)
513 rms = numpy.sqrt(numpy.mean(resids**2))
514 fig = plt.figure(1, (8, 6))
515 meanDz = numpy.mean(dZ)
516 grid = ImageGrid(fig, 111, nrows_ncols=(1, 2), axes_pad=0.1,
517 share_all=
True, label_mode=
"L", cbar_mode=
"each",
518 cbar_size=
"7%", cbar_pad=
"2%", cbar_location=
"top")
519 im = grid[0].imshow(zeroIm.getImage().getArray(),
520 extent=(x0, x0+dx, y0+dy, y0), norm=norm,
522 im = grid[0].scatter(X, Y, c=Z, s=15.*meanDz/dZ, edgecolor=
'none', norm=norm,
523 marker=
'o', cmap=
'Spectral')
524 im2 = grid[1].scatter(X, Y, c=resids, edgecolor=
'none', norm=diffnorm,
525 marker=
's', cmap=
'seismic')
526 grid.cbar_axes[0].colorbar(im)
527 grid.cbar_axes[1].colorbar(im2)
528 grid[0].axis([x0, x0+dx, y0+dy, y0])
529 grid[1].axis([x0, x0+dx, y0+dy, y0])
530 grid[0].set_xlabel(
"model and grid")
531 grid[1].set_xlabel(
"residuals. rms = %0.3f"%(rms))
538 def _gridImage(self, maskedImage, binsize, statsFlag):
539 """Private method to grid an image for debugging"""
540 width, height = maskedImage.getDimensions()
541 x0, y0 = maskedImage.getXY0()
542 xedges = numpy.arange(0, width, binsize)
543 yedges = numpy.arange(0, height, binsize)
544 xedges = numpy.hstack((xedges, width))
545 yedges = numpy.hstack((yedges, height))
554 for ymin, ymax
in zip(yedges[0:-1], yedges[1:]):
555 for xmin, xmax
in zip(xedges[0:-1], xedges[1:]):
558 subIm = afwImage.MaskedImageF(maskedImage, subBBox, afwImage.PARENT,
False)
560 afwMath.MEAN | afwMath.MEANCLIP | afwMath.MEDIAN
561 | afwMath.NPOINT | afwMath.STDEV,
563 npoints, _ = stats.getResult(afwMath.NPOINT)
565 stdev, _ = stats.getResult(afwMath.STDEV)
566 if stdev < self.config.gridStdevEpsilon:
567 stdev = self.config.gridStdevEpsilon
568 bgX.append(0.5 * (x0 + xmin + x0 + xmax))
569 bgY.append(0.5 * (y0 + ymin + y0 + ymax))
570 bgdZ.append(stdev/numpy.sqrt(npoints))
571 est, _ = stats.getResult(statsFlag)
574 return numpy.array(bgX), numpy.array(bgY), numpy.array(bgZ), numpy.array(bgdZ)
578 """Match data references for a specified dataset type
580 Note that this is not exact, but should suffice for this task
581 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
591 self.
_keyNames_keyNames = butler.getKeys(datasetType)
593 def _makeKey(self, ref):
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_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_makeKey(ref) == key0)
Represent a 2-dimensional array of bitmask pixels.
Control how to make an approximation.
Pass parameters to a Background object.
Pass parameters to a Statistics object.
A floating-point coordinate rectangle geometry.
An integer coordinate rectangle.
def __init__(self, butler, datasetType)
def matchList(self, ref0, refList)
def isMatch(self, ref0, ref1)
def selectRefExposure(self, expRefList, imageScalerList, expDatasetType)
def run(self, expRefList, expDatasetType, imageScalerList=None, refExpDataRef=None, refImageScaler=None)
def _gridImage(self, maskedImage, binsize, statsFlag)
def _debugPlot(self, X, Y, Z, dZ, modelImage, bbox, model, resids)
def matchBackgrounds(self, refExposure, sciExposure)
def __init__(self, *args, **kwargs)
daf::base::PropertySet * set
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.
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)
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.