28from lsst.utils.timer
import timeMethod
33 usePolynomial = pexConfig.Field(
35 doc=
"Fit background difference with Chebychev polynomial interpolation "
36 "(using afw.math.Approximate)? If False, fit with spline interpolation using afw.math.Background",
39 order = pexConfig.Field(
41 doc=
"Order of Chebyshev polynomial background model. Ignored if usePolynomial False",
44 badMaskPlanes = pexConfig.ListField(
45 doc=
"Names of mask planes to ignore while estimating the background",
46 dtype=str, default=[
"NO_DATA",
"DETECTED",
"DETECTED_NEGATIVE",
"SAT",
"BAD",
"INTRP",
"CR"],
49 gridStatistic = pexConfig.ChoiceField(
51 doc=
"Type of statistic to estimate pixel value for the grid points",
56 "MEANCLIP":
"clipped mean"
59 undersampleStyle = pexConfig.ChoiceField(
60 doc=
"Behaviour if there are too few points in grid for requested interpolation style. "
61 "Note: INCREASE_NXNYSAMPLE only allowed for usePolynomial=True.",
63 default=
"REDUCE_INTERP_ORDER",
65 "THROW_EXCEPTION":
"throw an exception if there are too few points",
66 "REDUCE_INTERP_ORDER":
"use an interpolation style with a lower order.",
67 "INCREASE_NXNYSAMPLE":
"Increase the number of samples used to make the interpolation grid.",
70 binSize = pexConfig.Field(
71 doc=
"Bin size for gridding the difference image and fitting a spatial model",
75 interpStyle = pexConfig.ChoiceField(
77 doc=
"Algorithm to interpolate the background values; ignored if usePolynomial is True"
78 "Maps to an enum; see afw.math.Background",
79 default=
"AKIMA_SPLINE",
81 "CONSTANT":
"Use a single constant value",
82 "LINEAR":
"Use linear interpolation",
83 "NATURAL_SPLINE":
"cubic spline with zero second derivative at endpoints",
84 "AKIMA_SPLINE":
"higher-level nonlinear spline that is more robust to outliers",
85 "NONE":
"No background estimation is to be attempted",
88 numSigmaClip = pexConfig.Field(
90 doc=
"Sigma for outlier rejection; ignored if gridStatistic != 'MEANCLIP'.",
93 numIter = pexConfig.Field(
95 doc=
"Number of iterations of outlier rejection; ignored if gridStatistic != 'MEANCLIP'.",
98 bestRefWeightCoverage = pexConfig.RangeField(
100 doc=
"Weight given to coverage (number of pixels that overlap with patch), "
101 "when calculating best reference exposure. Higher weight prefers exposures with high coverage."
102 "Ignored when reference visit is supplied",
106 bestRefWeightVariance = pexConfig.RangeField(
108 doc=
"Weight given to image variance when calculating best reference exposure. "
109 "Higher weight prefers exposures with low image variance. Ignored when reference visit is supplied",
113 bestRefWeightLevel = pexConfig.RangeField(
115 doc=
"Weight given to mean background level when calculating best reference exposure. "
116 "Higher weight prefers exposures with low mean background level. "
117 "Ignored when reference visit is supplied.",
121 approxWeighting = pexConfig.Field(
123 doc=(
"Use inverse-variance weighting when approximating background offset model? "
124 "This will fail when the background offset is constant "
125 "(this is usually only the case in testing with artificial images)."
126 "(usePolynomial=True)"),
129 gridStdevEpsilon = pexConfig.RangeField(
131 doc=
"Tolerance on almost zero standard deviation in a background-offset grid bin. "
132 "If all bins have a standard deviation below this value, the background offset model "
133 "is approximated without inverse-variance weighting. (usePolynomial=True)",
140 ConfigClass = MatchBackgroundsConfig
141 _DefaultName =
"matchBackgrounds"
144 pipeBase.Task.__init__(self, *args, **kwargs)
147 self.
sctrlsctrl.setAndMask(afwImage.Mask.getPlaneBitMask(self.config.badMaskPlanes))
148 self.
sctrlsctrl.setNanSafe(
True)
151 def run(self, expRefList, expDatasetType, imageScalerList=None, refExpDataRef=None, refImageScaler=None):
152 """Match the backgrounds of a list of coadd temp exposures to a reference coadd temp exposure.
154 Choose a refExpDataRef automatically if none supplied.
156 @param[
in] expRefList: list of data references to science exposures to be background-matched;
157 all exposures must exist.
158 @param[
in] expDatasetType: dataset type of exposures, e.g.
'goodSeeingCoadd_tempExp'
159 @param[
in] imageScalerList: list of image scalers (coaddUtils.ImageScaler);
160 if None then the images are
not scaled
161 @param[
in] refExpDataRef: data reference
for the reference exposure.
162 If
None, then this task selects the best exposures
from expRefList.
163 if not None then must be one of the exposures
in expRefList.
164 @param[
in] refImageScaler: image scaler
for reference image;
165 ignored
if refExpDataRef
is None,
else scaling
is not performed
if None
167 @return: a pipBase.Struct containing these fields:
168 - backgroundInfoList: a list of pipeBase.Struct, one per exposure
in expRefList,
169 each of which contains these fields:
170 - isReference: this
is the reference exposure (only one returned Struct will
171 contain
True for this value, unless the ref exposure
is listed multiple times)
172 - backgroundModel: differential background model (afw.Math.Background
or afw.Math.Approximate).
173 Add this to the science exposure to match the reference exposure.
174 - fitRMS: rms of the fit. This
is the sqrt(mean(residuals**2)).
175 - matchedMSE: the MSE of the reference
and matched images: mean((refImage - matchedSciImage)**2);
176 should be comparable to difference image
's mean variance.
177 - diffImVar: the mean variance of the difference image.
178 All fields except isReference will be
None if isReference
True or the fit failed.
180 @warning: all exposures must exist on disk
183 numExp = len(expRefList)
185 raise pipeBase.TaskError(
"No exposures to match")
187 if expDatasetType
is None:
188 raise pipeBase.TaskError(
"Must specify expDatasetType")
190 if imageScalerList
is None:
191 self.log.
info(
"imageScalerList is None; no scaling will be performed")
192 imageScalerList = [
None] * numExp
194 if len(expRefList) != len(imageScalerList):
195 raise RuntimeError(
"len(expRefList) = %s != %s = len(imageScalerList)" %
196 (len(expRefList), len(imageScalerList)))
199 if refExpDataRef
is None:
202 expRefList=expRefList,
203 imageScalerList=imageScalerList,
204 expDatasetType=expDatasetType,
206 refExpDataRef = expRefList[refInd]
207 refImageScaler = imageScalerList[refInd]
212 expKeyList = refExpDataRef.butlerSubset.butler.getKeys(expDatasetType)
213 refMatcher =
DataRefMatcher(refExpDataRef.butlerSubset.butler, expDatasetType)
214 refIndSet =
set(refMatcher.matchList(ref0=refExpDataRef, refList=expRefList))
216 if refInd
is not None and refInd
not in refIndSet:
217 raise RuntimeError(
"Internal error: selected reference %s not found in expRefList")
219 refExposure = refExpDataRef.get(expDatasetType, immediate=
True)
220 if refImageScaler
is not None:
221 refMI = refExposure.getMaskedImage()
222 refImageScaler.scaleMaskedImage(refMI)
224 debugIdKeyList = tuple(
set(expKeyList) -
set([
'tract',
'patch']))
226 self.log.
info(
"Matching %d Exposures", numExp)
228 backgroundInfoList = []
229 for ind, (toMatchRef, imageScaler)
in enumerate(zip(expRefList, imageScalerList)):
231 backgroundInfoStruct = pipeBase.Struct(
233 backgroundModel=
None,
239 self.log.
info(
"Matching background of %s to %s", toMatchRef.dataId, refExpDataRef.dataId)
241 toMatchExposure = toMatchRef.get(expDatasetType, immediate=
True)
242 if imageScaler
is not None:
243 toMatchMI = toMatchExposure.getMaskedImage()
244 imageScaler.scaleMaskedImage(toMatchMI)
248 refExposure=refExposure,
249 sciExposure=toMatchExposure,
251 backgroundInfoStruct.isReference =
False
252 except Exception
as e:
253 self.log.
warning(
"Failed to fit background %s: %s", toMatchRef.dataId, e)
254 backgroundInfoStruct = pipeBase.Struct(
256 backgroundModel=
None,
262 backgroundInfoList.append(backgroundInfoStruct)
264 return pipeBase.Struct(
265 backgroundInfoList=backgroundInfoList)
269 """Find best exposure to use as the reference exposure
271 Calculate an appropriate reference exposure by minimizing a cost function that penalizes
272 high variance, high background level, and low coverage. Use the following config parameters:
273 - bestRefWeightCoverage
274 - bestRefWeightVariance
277 @param[
in] expRefList: list of data references to exposures.
278 Retrieves dataset type specified by expDatasetType.
279 If an exposure
is not found, it
is skipped
with a warning.
280 @param[
in] imageScalerList: list of image scalers (coaddUtils.ImageScaler);
281 must be the same length
as expRefList
282 @param[
in] expDatasetType: dataset type of exposure: e.g.
'goodSeeingCoadd_tempExp'
284 @return: index of best exposure
286 @raise pipeBase.TaskError
if none of the exposures
in expRefList are found.
288 self.log.info("Calculating best reference visit")
290 meanBkgdLevelList = []
293 if len(expRefList) != len(imageScalerList):
294 raise RuntimeError(
"len(expRefList) = %s != %s = len(imageScalerList)" %
295 (len(expRefList), len(imageScalerList)))
297 for expRef, imageScaler
in zip(expRefList, imageScalerList):
298 exposure = expRef.get(expDatasetType, immediate=
True)
299 maskedImage = exposure.getMaskedImage()
300 if imageScaler
is not None:
302 imageScaler.scaleMaskedImage(maskedImage)
305 varList.append(numpy.nan)
306 meanBkgdLevelList.append(numpy.nan)
307 coverageList.append(numpy.nan)
310 afwMath.MEAN | afwMath.NPOINT | afwMath.VARIANCE, self.
sctrlsctrl)
311 meanVar, meanVarErr = statObjIm.getResult(afwMath.VARIANCE)
312 meanBkgdLevel, meanBkgdLevelErr = statObjIm.getResult(afwMath.MEAN)
313 npoints, npointsErr = statObjIm.getResult(afwMath.NPOINT)
314 varList.append(meanVar)
315 meanBkgdLevelList.append(meanBkgdLevel)
316 coverageList.append(npoints)
318 raise pipeBase.TaskError(
319 "None of the candidate %s exist; cannot select best reference exposure" % (expDatasetType,))
322 varArr = numpy.array(varList)/numpy.nanmax(varList)
323 meanBkgdLevelArr = numpy.array(meanBkgdLevelList)/numpy.nanmax(meanBkgdLevelList)
324 coverageArr = numpy.nanmin(coverageList)/numpy.array(coverageList)
326 costFunctionArr = self.config.bestRefWeightVariance * varArr
327 costFunctionArr += self.config.bestRefWeightLevel * meanBkgdLevelArr
328 costFunctionArr += self.config.bestRefWeightCoverage * coverageArr
329 return numpy.nanargmin(costFunctionArr)
334 Match science exposure's background level to that of reference exposure.
336 Process creates a difference image of the reference exposure minus the science exposure, and then
337 generates an
afw.math.Background object. It assumes (but does
not require/check) that the mask plane
338 already has detections set. If detections have
not been set/masked, sources will bias the
339 background estimation.
340 The
'background' of the difference image
is smoothed by spline interpolation (by the Background
class)
341 or by polynomial interpolation by the Approximate
class. This model of difference image
342 is added to the science exposure
in memory.
343 Fit diagnostics are also calculated
and returned.
345 @param[
in] refExposure: reference exposure
346 @param[
in,out] sciExposure: science exposure; modified by changing the background level
347 to match that of the reference exposure
348 @returns a pipBase.Struct
with fields:
350 - fitRMS: rms of the fit. This
is the sqrt(mean(residuals**2)).
351 - matchedMSE: the MSE of the reference
and matched images: mean((refImage - matchedSciImage)**2);
352 should be comparable to difference image
's mean variance.
353 - diffImVar: the mean variance of the difference image.
357 refExposure.writeFits(
lsstDebug.Info(__name__).figpath +
'refExposure.fits')
358 sciExposure.writeFits(
lsstDebug.Info(__name__).figpath +
'sciExposure.fits')
361 if self.config.usePolynomial:
362 x, y = sciExposure.getDimensions()
363 shortSideLength =
min(x, y)
364 if shortSideLength < self.config.binSize:
365 raise ValueError(
"%d = config.binSize > shorter dimension = %d" % (self.config.binSize,
367 npoints = shortSideLength // self.config.binSize
368 if shortSideLength % self.config.binSize != 0:
371 if self.config.order > npoints - 1:
372 raise ValueError(
"%d = config.order > npoints - 1 = %d" % (self.config.order, npoints - 1))
375 if (sciExposure.getDimensions() != refExposure.getDimensions()):
376 wSci, hSci = sciExposure.getDimensions()
377 wRef, hRef = refExposure.getDimensions()
379 "Exposures are different dimensions. sci:(%i, %i) vs. ref:(%i, %i)" %
380 (wSci, hSci, wRef, hRef))
382 statsFlag = getattr(afwMath, self.config.gridStatistic)
383 self.
sctrlsctrl.setNumSigmaClip(self.config.numSigmaClip)
384 self.
sctrlsctrl.setNumIter(self.config.numIter)
386 im = refExposure.getMaskedImage()
387 diffMI = im.Factory(im,
True)
388 diffMI -= sciExposure.getMaskedImage()
390 width = diffMI.getWidth()
391 height = diffMI.getHeight()
392 nx = width // self.config.binSize
393 if width % self.config.binSize != 0:
395 ny = height // self.config.binSize
396 if height % self.config.binSize != 0:
400 bctrl.setUndersampleStyle(self.config.undersampleStyle)
407 if self.config.usePolynomial:
408 order = self.config.order
409 bgX, bgY, bgZ, bgdZ = self.
_gridImage_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.
warning(
"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.
warning(
"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(self.config.interpStyle, self.config.undersampleStyle)
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_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')
463 bbox =
geom.Box2D(refExposure.getMaskedImage().getBBox())
465 self.
_debugPlot_debugPlot(X, Y, Z, dZ, bkgdImage, bbox, modelValueArr, resids)
466 except Exception
as e:
467 self.log.
warning(
'Debug plot not generated: %s', e)
470 afwMath.MEANCLIP, self.
sctrlsctrl).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.
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
503 zeroIm = afwImage.MaskedImageF(
geom.Box2I(bbox))
505 x0, y0 = zeroIm.getXY0()
506 dx, dy = zeroIm.getDimensions()
508 self.log.
warning(
"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_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_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_makeKey(ref) == key0)
Represent a 2-dimensional array of bitmask pixels.
Control how to make an approximation.
Approximate values for a MaskedImage.
Pass parameters to a Background object.
A virtual base class to evaluate image background levels.
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.
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.
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)