Skip to content

Commit 7c1554f

Browse files
committed
Use sky-subtracted warps for ref-image selection
_defineWarps() now rejects any image with all NaNs along any image edge, and creates the cost function using a sky-subtracted image. This sky-subtraction fits a 1st order Chebyshev polynomial to the masked image background. Also fixed a bug from LSK refactor by inserting a blank sky model into the background model list at the chosen reference image index.
1 parent b0463a5 commit 7c1554f

File tree

1 file changed

+114
-71
lines changed

1 file changed

+114
-71
lines changed

python/lsst/pipe/tasks/matchBackgrounds.py

+114-71
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
import lsstDebug
2525
import numpy as np
26-
from lsst.afw.image import LOCAL, PARENT, Mask, MaskedImageF
26+
from lsst.afw.image import LOCAL, PARENT, ImageF, Mask, MaskedImageF
2727
from lsst.afw.math import (
2828
MEAN,
2929
MEANCLIP,
@@ -250,12 +250,11 @@ def run(self, warps):
250250
raise TaskError("No exposures to match")
251251

252252
# Define a reference warp; 'warps' is modified in-place to exclude it
253-
refWarp = self._defineWarps(warps=warps, refWarpVisit=self.config.refWarpVisit)
254-
breakpoint()
253+
refWarp, refInd = self._defineWarps(warps=warps, refWarpVisit=self.config.refWarpVisit)
255254

256255
# Images must be scaled to a common ZP
257256
# Converting everything to nJy to accomplish this
258-
refExposure = refMatchedWarp.get()
257+
refExposure = refWarp.get()
259258
refMI = self._fluxScale(refExposure) # Also modifies refExposure
260259

261260
# TODO: figure out what this was creating and why
@@ -270,7 +269,7 @@ def run(self, warps):
270269

271270
# TODO: refactor below to construct blank bg model
272271
im = refExposure.getMaskedImage()
273-
blankIm = im.Factory(im, True) # Don't do this
272+
blankIm = im.Factory(im, True)
274273
blankIm.image.array *= 0
275274

276275
width = blankIm.getWidth()
@@ -286,53 +285,43 @@ def run(self, warps):
286285
bctrl.setUndersampleStyle(self.config.undersampleStyle)
287286

288287
bkgd = makeBackground(blankIm, bctrl)
288+
blank = BackgroundList(
289+
(
290+
bkgd,
291+
stringToInterpStyle(self.config.interpStyle),
292+
stringToUndersampleStyle(self.config.undersampleStyle),
293+
ApproximateControl.UNKNOWN,
294+
0,
295+
0,
296+
False,
297+
)
298+
)
289299

290300
backgroundInfoList = []
291301
for ind, exp in enumerate(warps):
292-
if ind in refIndSet:
293-
backgroundInfoStruct = BackgroundList(
294-
(
295-
bkgd,
296-
stringToInterpStyle(self.config.interpStyle),
297-
stringToUndersampleStyle(self.config.undersampleStyle),
298-
ApproximateControl.UNKNOWN,
299-
0,
300-
0,
301-
False,
302-
)
302+
# TODO: simplify this maybe, using only visit IDs?
303+
self.log.info("Matching background of %s to %s", exp.dataId, refWarp.dataId)
304+
toMatchExposure = exp.get()
305+
try:
306+
# TODO: adjust logic to avoid creating spurious MIs like this
307+
toMatchMI = self._fluxScale(toMatchExposure)
308+
309+
# store a string specifying the visit to label debug plot
310+
# self.debugDataIdString = ''.join([str(toMatchRef.dataId[vk]) for vk in debugIdKeyList])
311+
312+
backgroundInfoStruct = self.matchBackgrounds(
313+
refExposure=refExposure,
314+
sciExposure=toMatchExposure,
303315
)
304-
else:
305-
# TODO: simplify this maybe, using only visit IDs?
306-
self.log.info("Matching background of %s to %s", exp.dataId, refMatchedWarp.dataId)
307-
toMatchExposure = exp.get()
308-
try:
309-
# Seems to require exposure, not masked exposure.
310-
toMatchMI = self._fluxScale(toMatchExposure)
311-
312-
# store a string specifying the visit to label debug plot
313-
# self.debugDataIdString = ''.join([str(toMatchRef.dataId[vk]) for vk in debugIdKeyList])
314-
315-
backgroundInfoStruct = self.matchBackgrounds(
316-
refExposure=refExposure,
317-
sciExposure=toMatchExposure,
318-
)
319-
backgroundInfoStruct.isReference = False
320-
except Exception as e:
321-
self.log.warning("Failed to fit background %s: %s", exp.dataId, e)
322-
backgroundInfoStruct = BackgroundList(
323-
(
324-
bkgd,
325-
stringToInterpStyle(self.config.interpStyle),
326-
stringToUndersampleStyle(self.config.undersampleStyle),
327-
ApproximateControl.UNKNOWN,
328-
0,
329-
0,
330-
False,
331-
)
332-
)
316+
backgroundInfoStruct.isReference = False
317+
except Exception as e:
318+
self.log.warning("Failed to fit background %s: %s", exp.dataId, e)
319+
backgroundInfoStruct = blank
333320

334321
backgroundInfoList.append(backgroundInfoStruct)
335322

323+
# TODO: more elegant solution than inserting blank model at ref ind?
324+
backgroundInfoList.insert(refInd, blank)
336325
return Struct(backgroundInfoList=backgroundInfoList)
337326

338327
@timeMethod
@@ -377,15 +366,38 @@ def _defineWarps(self, warps, refWarpVisit=None):
377366
warpNPoints = []
378367
for warpDDH in warps:
379368
warp = warpDDH.get()
369+
370+
# First check if any image edge is all NaN
371+
# If so, don't use
372+
leftBool = np.all(np.isnan(warp.image.array[:, 0]))
373+
rightBool = np.all(np.isnan(warp.image.array[:, warp.image.getHeight() - 1]))
374+
bottomBool = np.all(np.isnan(warp.image.array[0, :]))
375+
topBool = np.all(np.isnan(warp.image.array[warp.image.getWidth() - 1, :]))
376+
if np.any([leftBool, rightBool, bottomBool, topBool]):
377+
continue
378+
380379
warp.image.array *= warp.getPhotoCalib().instFluxToNanojansky(1) # Convert image plane to nJy
381-
warpStats = makeStatistics(warp.image, warp.mask, MEAN | VARIANCE | NPOINT, self.statsCtrl)
380+
381+
# Select reference based on BG of plane sky-subtracted images
382+
bkgd, __, __, __ = self._setupBackground(warp)
383+
384+
weightByInverseVariance = self.config.approxWeighting
385+
actrl = ApproximateControl(ApproximateControl.CHEBYSHEV, 1, 1, weightByInverseVariance)
386+
undersampleStyle = stringToUndersampleStyle(self.config.undersampleStyle)
387+
approx = bkgd.getApproximate(actrl, undersampleStyle)
388+
bgSubImage = ImageF(warp.image.array - approx.getImage().array)
389+
390+
warpStats = makeStatistics(bgSubImage, warp.mask, MEAN | VARIANCE | NPOINT, self.statsCtrl)
382391
warpMean, _ = warpStats.getResult(MEAN)
383392
warpVar, _ = warpStats.getResult(VARIANCE)
384393
warpNPoint, _ = warpStats.getResult(NPOINT)
385394
warpMeans.append(warpMean)
386395
warpVars.append(warpVar)
387396
warpNPoints.append(int(warpNPoint))
388397

398+
if len(warpNPoints) == 0:
399+
raise TaskError("No suitable reference visit found in list of warps.")
400+
389401
# Normalize mean/var/npoints to range from 0 to 1
390402
warpMeansFrac = np.array(warpMeans) / np.nanmax(warpMeans)
391403
warpVarsFrac = np.array(warpVars) / np.nanmax(warpVars)
@@ -396,21 +408,22 @@ def _defineWarps(self, warps, refWarpVisit=None):
396408
costFunctionVals += self.config.bestRefWeightVariance * warpVarsFrac
397409
costFunctionVals += self.config.bestRefWeightCoverage * warpNPointsFrac
398410

399-
refWarp = warps.pop(np.nanargmin(costFunctionVals))
411+
ind = np.nanargmin(costFunctionVals)
412+
refWarp = warps.pop(ind)
400413
self.log.info("Using best reference visit %d", refWarp.dataId["visit"])
401-
return refWarp
414+
return refWarp, ind
402415

403416
def _fluxScale(self, exposure):
404417
"""Scales image to nJy flux using photometric calibration.
405418
406419
Parameters
407420
----------
408-
exposure: ``
421+
exposure: `lsst.afw.image._exposure.ExposureF`
409422
Exposure to scale.
410423
411424
Returns
412425
-------
413-
maskedImage: ``
426+
maskedImage: `lsst.afw.image._maskedImage.MaskedImageF`
414427
Flux-scaled masked exposure.
415428
"""
416429
maskedImage = exposure.getMaskedImage()
@@ -419,6 +432,56 @@ def _fluxScale(self, exposure):
419432

420433
return maskedImage
421434

435+
def _setupBackground(self, warp):
436+
"""Set up and return a background model container and associated images
437+
and controllers.
438+
439+
Uses the following config parameters:
440+
- ``gridStatistic``
441+
- ``numSigmaClip``
442+
- ``numIter``
443+
- ``binSize``
444+
- ``undersampleStyle``
445+
446+
Parameters
447+
----------
448+
warp: `lsst.afw.image._exposure.ExposureF`
449+
Warped exposure or difference image for which to estimate
450+
background.
451+
452+
Returns
453+
-------
454+
bkgd: `lsst.afw.math.BackgroundMI`
455+
Background model container.
456+
bctrl: `lsst.afw.math.BackgroundControl`
457+
Background model control
458+
warpMI: `lsst.afw.image._maskedImage.MaskedImageF`
459+
Masked image associated with warp
460+
statsFlag: `lsst.afw.math.Property`
461+
Flag for grid statistic property (self.config.gridStatistic)
462+
"""
463+
statsFlag = stringToStatisticsProperty(self.config.gridStatistic)
464+
self.statsCtrl.setNumSigmaClip(self.config.numSigmaClip)
465+
self.statsCtrl.setNumIter(self.config.numIter)
466+
467+
warpMI = warp.getMaskedImage()
468+
469+
width = warpMI.getWidth()
470+
height = warpMI.getHeight()
471+
nx = width // self.config.binSize
472+
if width % self.config.binSize != 0:
473+
nx += 1
474+
ny = height // self.config.binSize
475+
if height % self.config.binSize != 0:
476+
ny += 1
477+
478+
bctrl = BackgroundControl(nx, ny, self.statsCtrl, statsFlag)
479+
bctrl.setUndersampleStyle(self.config.undersampleStyle)
480+
481+
bkgd = makeBackground(warpMI, bctrl)
482+
483+
return bkgd, bctrl, warpMI, statsFlag
484+
422485
@timeMethod
423486
def matchBackgrounds(self, refExposure, sciExposure):
424487
"""Match science exposure's background level to that of reference
@@ -489,27 +552,7 @@ def matchBackgrounds(self, refExposure, sciExposure):
489552
"Exposures are different dimensions. sci:(%i, %i) vs. ref:(%i, %i)" % (wSci, hSci, wRef, hRef)
490553
)
491554

492-
statsFlag = stringToStatisticsProperty(self.config.gridStatistic)
493-
self.statsCtrl.setNumSigmaClip(self.config.numSigmaClip)
494-
self.statsCtrl.setNumIter(self.config.numIter)
495-
496-
im = refExposure.getMaskedImage()
497-
diffMI = im.Factory(im, True)
498-
diffMI -= sciExposure.getMaskedImage()
499-
500-
width = diffMI.getWidth()
501-
height = diffMI.getHeight()
502-
nx = width // self.config.binSize
503-
if width % self.config.binSize != 0:
504-
nx += 1
505-
ny = height // self.config.binSize
506-
if height % self.config.binSize != 0:
507-
ny += 1
508-
509-
bctrl = BackgroundControl(nx, ny, self.statsCtrl, statsFlag)
510-
bctrl.setUndersampleStyle(self.config.undersampleStyle)
511-
512-
bkgd = makeBackground(diffMI, bctrl)
555+
bkgd, bctrl, diffMI, statsFlag = self._setupBackground(refExposure)
513556

514557
# Some config and input checks if config.usePolynomial:
515558
# 1) Check that order/bin size make sense:

0 commit comments

Comments
 (0)