1 from builtins
import zip
2 from builtins
import range
3 from builtins
import object
36 usePolynomial = pexConfig.Field(
38 doc=
"Fit background difference with Chebychev polynomial interpolation "
39 "(using afw.math.Approximate)? If False, fit with spline interpolation using afw.math.Background",
42 order = pexConfig.Field(
44 doc=
"Order of Chebyshev polynomial background model. Ignored if usePolynomial False",
47 badMaskPlanes = pexConfig.ListField(
48 doc=
"Names of mask planes to ignore while estimating the background",
49 dtype=str, default=[
"NO_DATA",
"DETECTED",
"DETECTED_NEGATIVE",
"SAT",
"BAD",
"INTRP",
"CR"],
50 itemCheck=
lambda x: x
in afwImage.MaskU().getMaskPlaneDict(),
52 gridStatistic = pexConfig.ChoiceField(
54 doc=
"Type of statistic to estimate pixel value for the grid points",
59 "MEANCLIP":
"clipped mean"
62 undersampleStyle = pexConfig.ChoiceField(
63 doc=
"Behaviour if there are too few points in grid for requested interpolation style. "
64 "Note: INCREASE_NXNYSAMPLE only allowed for usePolynomial=True.",
66 default=
"REDUCE_INTERP_ORDER",
68 "THROW_EXCEPTION":
"throw an exception if there are too few points",
69 "REDUCE_INTERP_ORDER":
"use an interpolation style with a lower order.",
70 "INCREASE_NXNYSAMPLE":
"Increase the number of samples used to make the interpolation grid.",
73 binSize = pexConfig.Field(
74 doc=
"Bin size for gridding the difference image and fitting a spatial model",
78 interpStyle = pexConfig.ChoiceField(
80 doc=
"Algorithm to interpolate the background values; ignored if usePolynomial is True"
81 "Maps to an enum; see afw.math.Background",
82 default=
"AKIMA_SPLINE",
84 "CONSTANT":
"Use a single constant value",
85 "LINEAR":
"Use linear interpolation",
86 "NATURAL_SPLINE":
"cubic spline with zero second derivative at endpoints",
87 "AKIMA_SPLINE":
"higher-level nonlinear spline that is more robust to outliers",
88 "NONE":
"No background estimation is to be attempted",
91 numSigmaClip = pexConfig.Field(
93 doc=
"Sigma for outlier rejection; ignored if gridStatistic != 'MEANCLIP'.",
96 numIter = pexConfig.Field(
98 doc=
"Number of iterations of outlier rejection; ignored if gridStatistic != 'MEANCLIP'.",
101 bestRefWeightCoverage = pexConfig.RangeField(
103 doc=
"Weight given to coverage (number of pixels that overlap with patch), "
104 "when calculating best reference exposure. Higher weight prefers exposures with high coverage."
105 "Ignored when reference visit is supplied",
109 bestRefWeightVariance = pexConfig.RangeField(
111 doc=
"Weight given to image variance when calculating best reference exposure. "
112 "Higher weight prefers exposures with low image variance. Ignored when reference visit is supplied",
116 bestRefWeightLevel = pexConfig.RangeField(
118 doc=
"Weight given to mean background level when calculating best reference exposure. "
119 "Higher weight prefers exposures with low mean background level. "
120 "Ignored when reference visit is supplied.",
124 approxWeighting = pexConfig.Field(
126 doc=(
"Use inverse-variance weighting when approximating background offset model? " +
127 "This will fail when the background offset is constant " +
128 "(this is usually only the case in testing with artificial images)." +
129 "(usePolynomial=True)"),
132 gridStdevEpsilon = pexConfig.RangeField(
134 doc=
"Tolerance on almost zero standard deviation in a background-offset grid bin. "
135 "If all bins have a standard deviation below this value, the background offset model "
136 "is approximated without inverse-variance weighting. (usePolynomial=True)",
143 ConfigClass = MatchBackgroundsConfig
144 _DefaultName =
"matchBackgrounds"
147 pipeBase.Task.__init__(self, *args, **kwargs)
150 self.sctrl.setAndMask(afwImage.MaskU.getPlaneBitMask(self.config.badMaskPlanes))
151 self.sctrl.setNanSafe(
True)
154 def run(self, expRefList, expDatasetType, imageScalerList=None, refExpDataRef=None, refImageScaler=None):
155 """Match the backgrounds of a list of coadd temp exposures to a reference coadd temp exposure.
157 Choose a refExpDataRef automatically if none supplied.
159 @param[in] expRefList: list of data references to science exposures to be background-matched;
160 all exposures must exist.
161 @param[in] expDatasetType: dataset type of exposures, e.g. 'goodSeeingCoadd_tempExp'
162 @param[in] imageScalerList: list of image scalers (coaddUtils.ImageScaler);
163 if None then the images are not scaled
164 @param[in] refExpDataRef: data reference for the reference exposure.
165 If None, then this task selects the best exposures from expRefList.
166 if not None then must be one of the exposures in expRefList.
167 @param[in] refImageScaler: image scaler for reference image;
168 ignored if refExpDataRef is None, else scaling is not performed if None
170 @return: a pipBase.Struct containing these fields:
171 - backgroundInfoList: a list of pipeBase.Struct, one per exposure in expRefList,
172 each of which contains these fields:
173 - isReference: this is the reference exposure (only one returned Struct will
174 contain True for this value, unless the ref exposure is listed multiple times)
175 - backgroundModel: differential background model (afw.Math.Background or afw.Math.Approximate).
176 Add this to the science exposure to match the reference exposure.
177 - fitRMS: rms of the fit. This is the sqrt(mean(residuals**2)).
178 - matchedMSE: the MSE of the reference and matched images: mean((refImage - matchedSciImage)**2);
179 should be comparable to difference image's mean variance.
180 - diffImVar: the mean variance of the difference image.
181 All fields except isReference will be None if isReference True or the fit failed.
183 @warning: all exposures must exist on disk
186 numExp = len(expRefList)
188 raise pipeBase.TaskError(
"No exposures to match")
190 if expDatasetType
is None:
191 raise pipeBase.TaskError(
"Must specify expDatasetType")
193 if imageScalerList
is None:
194 self.log.info(
"imageScalerList is None; no scaling will be performed")
195 imageScalerList = [
None] * numExp
197 if len(expRefList) != len(imageScalerList):
198 raise RuntimeError(
"len(expRefList) = %s != %s = len(imageScalerList)" %
199 (len(expRefList), len(imageScalerList)))
202 if refExpDataRef
is None:
205 expRefList=expRefList,
206 imageScalerList=imageScalerList,
207 expDatasetType=expDatasetType,
209 refExpDataRef = expRefList[refInd]
210 refImageScaler = imageScalerList[refInd]
215 expKeyList = refExpDataRef.butlerSubset.butler.getKeys(expDatasetType)
216 refMatcher =
DataRefMatcher(refExpDataRef.butlerSubset.butler, expDatasetType)
217 refIndSet = set(refMatcher.matchList(ref0=refExpDataRef, refList=expRefList))
219 if refInd
is not None and refInd
not in refIndSet:
220 raise RuntimeError(
"Internal error: selected reference %s not found in expRefList")
222 refExposure = refExpDataRef.get(expDatasetType, immediate=
True)
223 if refImageScaler
is not None:
224 refMI = refExposure.getMaskedImage()
225 refImageScaler.scaleMaskedImage(refMI)
227 debugIdKeyList = tuple(set(expKeyList) - set([
'tract',
'patch']))
229 self.log.info(
"Matching %d Exposures" % (numExp))
231 backgroundInfoList = []
232 for ind, (toMatchRef, imageScaler)
in enumerate(zip(expRefList, imageScalerList)):
234 backgroundInfoStruct = pipeBase.Struct(
236 backgroundModel=
None,
242 self.log.info(
"Matching background of %s to %s" % (toMatchRef.dataId, refExpDataRef.dataId))
244 toMatchExposure = toMatchRef.get(expDatasetType, immediate=
True)
245 if imageScaler
is not None:
246 toMatchMI = toMatchExposure.getMaskedImage()
247 imageScaler.scaleMaskedImage(toMatchMI)
251 refExposure=refExposure,
252 sciExposure=toMatchExposure,
254 backgroundInfoStruct.isReference =
False
255 except Exception
as e:
256 self.log.warn(
"Failed to fit background %s: %s" % (toMatchRef.dataId, e))
257 backgroundInfoStruct = pipeBase.Struct(
259 backgroundModel=
None,
265 backgroundInfoList.append(backgroundInfoStruct)
267 return pipeBase.Struct(
268 backgroundInfoList=backgroundInfoList)
272 """Find best exposure to use as the reference exposure
274 Calculate an appropriate reference exposure by minimizing a cost function that penalizes
275 high variance, high background level, and low coverage. Use the following config parameters:
276 - bestRefWeightCoverage
277 - bestRefWeightVariance
280 @param[in] expRefList: list of data references to exposures.
281 Retrieves dataset type specified by expDatasetType.
282 If an exposure is not found, it is skipped with a warning.
283 @param[in] imageScalerList: list of image scalers (coaddUtils.ImageScaler);
284 must be the same length as expRefList
285 @param[in] expDatasetType: dataset type of exposure: e.g. 'goodSeeingCoadd_tempExp'
287 @return: index of best exposure
289 @raise pipeBase.TaskError if none of the exposures in expRefList are found.
291 self.log.info(
"Calculating best reference visit")
293 meanBkgdLevelList = []
296 if len(expRefList) != len(imageScalerList):
297 raise RuntimeError(
"len(expRefList) = %s != %s = len(imageScalerList)" %
298 (len(expRefList), len(imageScalerList)))
300 for expRef, imageScaler
in zip(expRefList, imageScalerList):
301 exposure = expRef.get(expDatasetType, immediate=
True)
302 maskedImage = exposure.getMaskedImage()
303 if imageScaler
is not None:
305 imageScaler.scaleMaskedImage(maskedImage)
308 varList.append(numpy.nan)
309 meanBkgdLevelList.append(numpy.nan)
310 coverageList.append(numpy.nan)
313 afwMath.MEAN | afwMath.NPOINT | afwMath.VARIANCE, self.
sctrl)
314 meanVar, meanVarErr = statObjIm.getResult(afwMath.VARIANCE)
315 meanBkgdLevel, meanBkgdLevelErr = statObjIm.getResult(afwMath.MEAN)
316 npoints, npointsErr = statObjIm.getResult(afwMath.NPOINT)
317 varList.append(meanVar)
318 meanBkgdLevelList.append(meanBkgdLevel)
319 coverageList.append(npoints)
321 raise pipeBase.TaskError(
322 "None of the candidate %s exist; cannot select best reference exposure" % (expDatasetType,))
325 varArr = numpy.array(varList)/numpy.nanmax(varList)
326 meanBkgdLevelArr = numpy.array(meanBkgdLevelList)/numpy.nanmax(meanBkgdLevelList)
327 coverageArr = numpy.nanmin(coverageList)/numpy.array(coverageList)
329 costFunctionArr = self.config.bestRefWeightVariance * varArr
330 costFunctionArr += self.config.bestRefWeightLevel * meanBkgdLevelArr
331 costFunctionArr += self.config.bestRefWeightCoverage * coverageArr
332 return numpy.nanargmin(costFunctionArr)
337 Match science exposure's background level to that of reference exposure.
339 Process creates a difference image of the reference exposure minus the science exposure, and then
340 generates an afw.math.Background object. It assumes (but does not require/check) that the mask plane
341 already has detections set. If detections have not been set/masked, sources will bias the
342 background estimation.
343 The 'background' of the difference image is smoothed by spline interpolation (by the Background class)
344 or by polynomial interpolation by the Approximate class. This model of difference image is added to the
345 science exposure in memory.
346 Fit diagnostics are also calculated and returned.
348 @param[in] refExposure: reference exposure
349 @param[in,out] sciExposure: science exposure; modified by changing the background level
350 to match that of the reference exposure
351 @returns a pipBase.Struct with fields:
352 - backgroundModel: an afw.math.Approximate or an afw.math.Background.
353 - fitRMS: rms of the fit. This is the sqrt(mean(residuals**2)).
354 - matchedMSE: the MSE of the reference and matched images: mean((refImage - matchedSciImage)**2);
355 should be comparable to difference image's mean variance.
356 - diffImVar: the mean variance of the difference image.
360 refExposure.writeFits(
lsstDebug.Info(__name__).figpath +
'refExposure.fits')
361 sciExposure.writeFits(
lsstDebug.Info(__name__).figpath +
'sciExposure.fits')
364 if self.config.usePolynomial:
365 x, y = sciExposure.getDimensions()
366 shortSideLength = min(x, y)
367 if shortSideLength < self.config.binSize:
368 raise ValueError(
"%d = config.binSize > shorter dimension = %d" % (self.config.binSize,
370 npoints = shortSideLength // self.config.binSize
371 if shortSideLength % self.config.binSize != 0:
374 if self.config.order > npoints - 1:
375 raise ValueError(
"%d = config.order > npoints - 1 = %d" % (self.config.order, npoints - 1))
378 if (sciExposure.getDimensions() != refExposure.getDimensions()):
379 wSci, hSci = sciExposure.getDimensions()
380 wRef, hRef = refExposure.getDimensions()
382 "Exposures are different dimensions. sci:(%i, %i) vs. ref:(%i, %i)" %
383 (wSci, hSci, wRef, hRef))
385 statsFlag = getattr(afwMath, self.config.gridStatistic)
386 self.sctrl.setNumSigmaClip(self.config.numSigmaClip)
387 self.sctrl.setNumIter(self.config.numIter)
389 im = refExposure.getMaskedImage()
390 diffMI = im.Factory(im,
True)
391 diffMI -= sciExposure.getMaskedImage()
393 width = diffMI.getWidth()
394 height = diffMI.getHeight()
395 nx = width // self.config.binSize
396 if width % self.config.binSize != 0:
398 ny = height // self.config.binSize
399 if height % self.config.binSize != 0:
403 bctrl.setUndersampleStyle(self.config.undersampleStyle)
404 bctrl.setInterpStyle(self.config.interpStyle)
411 if self.config.usePolynomial:
412 order = self.config.order
413 bgX, bgY, bgZ, bgdZ = self.
_gridImage(diffMI, self.config.binSize, statsFlag)
414 minNumberGridPoints = min(len(set(bgX)), len(set(bgY)))
416 raise ValueError(
"No overlap with reference. Nothing to match")
417 elif minNumberGridPoints <= self.config.order:
419 if self.config.undersampleStyle ==
"THROW_EXCEPTION":
420 raise ValueError(
"Image does not cover enough of ref image for order and binsize")
421 elif self.config.undersampleStyle ==
"REDUCE_INTERP_ORDER":
422 self.log.warn(
"Reducing order to %d"%(minNumberGridPoints - 1))
423 order = minNumberGridPoints - 1
424 elif self.config.undersampleStyle ==
"INCREASE_NXNYSAMPLE":
425 newBinSize = (minNumberGridPoints*self.config.binSize) // (self.config.order + 1)
426 bctrl.setNxSample(newBinSize)
427 bctrl.setNySample(newBinSize)
429 self.log.warn(
"Decreasing binsize to %d"%(newBinSize))
432 isUniformImageDiff =
not numpy.any(bgdZ > self.config.gridStdevEpsilon)
433 weightByInverseVariance =
False if isUniformImageDiff
else self.config.approxWeighting
437 if self.config.usePolynomial:
439 order, order, weightByInverseVariance)
440 undersampleStyle = getattr(afwMath, self.config.undersampleStyle)
441 approx = bkgd.getApproximate(actrl, undersampleStyle)
442 bkgdImage = approx.getImage()
444 bkgdImage = bkgd.getImageF()
445 except Exception
as e:
446 raise RuntimeError(
"Background/Approximation failed to interp image %s: %s" % (
449 sciMI = sciExposure.getMaskedImage()
455 X, Y, Z, dZ = self.
_gridImage(diffMI, self.config.binSize, statsFlag)
456 x0, y0 = diffMI.getXY0()
457 modelValueArr = numpy.empty(len(Z))
458 for i
in range(len(X)):
459 modelValueArr[i] = bkgdImage.get(int(X[i]-x0), int(Y[i]-y0))
460 resids = Z - modelValueArr
461 rms = numpy.sqrt(numpy.mean(resids[~numpy.isnan(resids)]**2))
464 sciExposure.writeFits(
lsstDebug.Info(__name__).figpath +
'sciMatchedExposure.fits')
469 self.
_debugPlot(X, Y, Z, dZ, bkgdImage, bbox, modelValueArr, resids)
470 except Exception
as e:
471 self.log.warn(
'Debug plot not generated: %s'%(e))
474 afwMath.MEANCLIP, self.
sctrl).getValue()
476 diffIm = diffMI.getImage()
481 outBkgd = approx
if self.config.usePolynomial
else bkgd
483 return pipeBase.Struct(
484 backgroundModel=outBkgd,
489 def _debugPlot(self, X, Y, Z, dZ, modelImage, bbox, model, resids):
490 """Generate a plot showing the background fit and residuals.
492 It is called when lsstDebug.Info(__name__).savefig = True
493 Saves the fig to lsstDebug.Info(__name__).figpath
494 Displays on screen if lsstDebug.Info(__name__).display = True
496 @param X: array of x positions
497 @param Y: array of y positions
498 @param Z: array of the grid values that were interpolated
499 @param dZ: array of the error on the grid values
500 @param modelImage: image ofthe model of the fit
501 @param model: array of len(Z) containing the grid values predicted by the model
502 @param resids: Z - model
504 import matplotlib.pyplot
as plt
505 import matplotlib.colors
506 from mpl_toolkits.axes_grid1
import ImageGrid
509 x0, y0 = zeroIm.getXY0()
510 dx, dy = zeroIm.getDimensions()
512 self.log.warn(
"No grid. Skipping plot generation.")
514 max, min = numpy.max(Z), numpy.min(Z)
515 norm = matplotlib.colors.normalize(vmax=max, vmin=min)
516 maxdiff = numpy.max(numpy.abs(resids))
517 diffnorm = matplotlib.colors.normalize(vmax=maxdiff, vmin=-maxdiff)
518 rms = numpy.sqrt(numpy.mean(resids**2))
519 fig = plt.figure(1, (8, 6))
520 meanDz = numpy.mean(dZ)
521 grid = ImageGrid(fig, 111, nrows_ncols=(1, 2), axes_pad=0.1,
522 share_all=
True, label_mode=
"L", cbar_mode=
"each",
523 cbar_size=
"7%", cbar_pad=
"2%", cbar_location=
"top")
524 im = grid[0].imshow(zeroIm.getImage().getArray(),
525 extent=(x0, x0+dx, y0+dy, y0), norm=norm,
527 im = grid[0].scatter(X, Y, c=Z, s=15.*meanDz/dZ, edgecolor=
'none', norm=norm,
528 marker=
'o', cmap=
'Spectral')
529 im2 = grid[1].scatter(X, Y, c=resids, edgecolor=
'none', norm=diffnorm,
530 marker=
's', cmap=
'seismic')
531 grid.cbar_axes[0].colorbar(im)
532 grid.cbar_axes[1].colorbar(im2)
533 grid[0].axis([x0, x0+dx, y0+dy, y0])
534 grid[1].axis([x0, x0+dx, y0+dy, y0])
535 grid[0].set_xlabel(
"model and grid")
536 grid[1].set_xlabel(
"residuals. rms = %0.3f"%(rms))
544 """Private method to grid an image for debugging"""
545 width, height = maskedImage.getDimensions()
546 x0, y0 = maskedImage.getXY0()
547 xedges = numpy.arange(0, width, binsize)
548 yedges = numpy.arange(0, height, binsize)
549 xedges = numpy.hstack((xedges, width))
550 yedges = numpy.hstack((yedges, height))
559 for ymin, ymax
in zip(yedges[0:-1], yedges[1:]):
560 for xmin, xmax
in zip(xedges[0:-1], xedges[1:]):
563 subIm = afwImage.MaskedImageF(maskedImage, subBBox, afwImage.PARENT,
False)
565 afwMath.MEAN | afwMath.MEANCLIP | afwMath.MEDIAN |
566 afwMath.NPOINT | afwMath.STDEV,
568 npoints, _ = stats.getResult(afwMath.NPOINT)
570 stdev, _ = stats.getResult(afwMath.STDEV)
571 if stdev < self.config.gridStdevEpsilon:
572 stdev = self.config.gridStdevEpsilon
573 bgX.append(0.5 * (x0 + xmin + x0 + xmax))
574 bgY.append(0.5 * (y0 + ymin + y0 + ymax))
575 bgdZ.append(stdev/numpy.sqrt(npoints))
576 est, _ = stats.getResult(statsFlag)
579 return numpy.array(bgX), numpy.array(bgY), numpy.array(bgZ), numpy.array(bgdZ)
583 """Match data references for a specified dataset type
585 Note that this is not exact, but should suffice for this task
586 until there is better support for this kind of thing in the butler.
590 """Construct a DataRefMatcher
593 @param[in] datasetType: dataset type to match
599 """Return a tuple of values for the specified keyNames
601 @param[in] ref: data reference
603 @raise KeyError if ref.dataId is missing a key in keyNames
605 return tuple(ref.dataId[key]
for key
in self.
_keyNames)
608 """Return True if ref0 == ref1
610 @param[in] ref0: data ref 0
611 @param[in] ref1: data ref 1
613 @raise KeyError if either ID is missing a key in keyNames
618 """Return a list of indices of matches
620 @return tuple of indices of matches
622 @raise KeyError if any ID is missing a key in keyNames
625 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.