23
23
24
24
import lsstDebug
25
25
import numpy as np
26
- from lsst .afw .image import LOCAL , PARENT , ImageF , Mask , MaskedImageF
26
+ from lsst .afw .image import LOCAL , PARENT , ExposureF , ImageF , Mask , MaskedImageF
27
27
from lsst .afw .math import (
28
28
MEAN ,
29
29
MEANCLIP ,
35
35
ApproximateControl ,
36
36
BackgroundControl ,
37
37
BackgroundList ,
38
+ BackgroundMI ,
38
39
StatisticsControl ,
39
40
makeBackground ,
40
41
makeStatistics ,
@@ -84,11 +85,11 @@ class MatchBackgroundsConfig(PipelineTaskConfig, pipelineConnections=MatchBackgr
84
85
doc = "Visit ID of the reference warp. If None, the best warp is chosen from the list of warps." ,
85
86
optional = True ,
86
87
)
87
- bestRefWeightCoverage = RangeField (
88
+ bestRefWeightChi2 = RangeField (
88
89
dtype = float ,
89
- doc = "Coverage weight (Number of pixels overlapping the patch) when calculating the best reference "
90
- "exposure. Higher weights prefer exposures with high coverage . Ignored when a ref visit supplied." ,
91
- default = 0.4 ,
90
+ doc = "Mean background goodness of fit statistic weight when calculating the best reference exposure. "
91
+ "Higher weights prefer exposures with flatter backgrounds . Ignored when ref visit supplied." ,
92
+ default = 0.2 ,
92
93
min = 0.0 ,
93
94
max = 1.0 ,
94
95
)
@@ -100,10 +101,18 @@ class MatchBackgroundsConfig(PipelineTaskConfig, pipelineConnections=MatchBackgr
100
101
min = 0.0 ,
101
102
max = 1.0 ,
102
103
)
103
- bestRefWeightLevel = RangeField (
104
+ bestRefWeightGlobalCoverage = RangeField (
104
105
dtype = float ,
105
- doc = "Mean background level weight when calculating the best reference exposure. "
106
- "Higher weights prefer exposures with low mean background levels. Ignored when ref visit supplied." ,
106
+ doc = "Global coverage weight (total number of valid pixels) when calculating the best reference "
107
+ "exposure. Higher weights prefer exposures with high coverage. Ignored when a ref visit supplied." ,
108
+ default = 0.2 ,
109
+ min = 0.0 ,
110
+ max = 1.0 ,
111
+ )
112
+ bestRefWeightEdgeCoverage = RangeField (
113
+ dtype = float ,
114
+ doc = "Edge coverage weight (number of valid edge pixels) when calculating the best reference "
115
+ "exposure. Higher weights prefer exposures with high coverage. Ignored when a ref visit supplied." ,
107
116
default = 0.2 ,
108
117
min = 0.0 ,
109
118
max = 1.0 ,
@@ -143,7 +152,7 @@ class MatchBackgroundsConfig(PipelineTaskConfig, pipelineConnections=MatchBackgr
143
152
)
144
153
binSize = Field [int ](
145
154
doc = "Bin size for gridding the difference image and fitting a spatial model." ,
146
- default = 256 ,
155
+ default = 1024 ,
147
156
)
148
157
interpStyle = ChoiceField (
149
158
dtype = str ,
@@ -213,11 +222,16 @@ class MatchBackgroundsTask(PipelineTask):
213
222
214
223
def __init__ (self , * args , ** kwargs ):
215
224
super ().__init__ (** kwargs )
225
+ self .statsFlag = stringToStatisticsProperty (self .config .gridStatistic )
216
226
self .statsCtrl = StatisticsControl ()
217
227
# TODO: Check that setting the mask planes here work - these planes
218
228
# can vary from exposure to exposure, I think?
219
229
self .statsCtrl .setAndMask (Mask .getPlaneBitMask (self .config .badMaskPlanes ))
220
230
self .statsCtrl .setNanSafe (True )
231
+ self .statsCtrl .setNumSigmaClip (self .config .numSigmaClip )
232
+ self .statsCtrl .setNumIter (self .config .numIter )
233
+ self .stringToInterpStyle = stringToInterpStyle (self .config .interpStyle )
234
+ self .undersampleStyle = stringToUndersampleStyle (self .config .undersampleStyle )
221
235
222
236
def runQuantum (self , butlerQC , inputRefs , outputRefs ):
223
237
inputs = butlerQC .get (inputRefs )
@@ -333,154 +347,129 @@ def _defineWarps(self, warps, refWarpVisit=None):
333
347
minimizing a cost function that penalizes high variance, high
334
348
background level, and low coverage.
335
349
336
- To find a reference warp, the following config parameters are used:
337
- - ``bestRefWeightCoverage``
338
- - ``bestRefWeightVariance``
339
- - ``bestRefWeightLevel``
340
-
341
350
Parameters
342
351
----------
343
- warps : `list`[`~lsst.afw.image.Exposure`]
344
- List of warped science exposures.
352
+ warps : `list`[`~lsst.daf.butler.DeferredDatasetHandle`]
353
+ List of warped exposures (of type `~lsst.afw.image.ExposureF`).
354
+ refWarpVisit : `int`, optional
355
+ Visit ID of the reference warp.
356
+ If None, the best warp is chosen from the list of warps.
345
357
346
358
Returns
347
359
-------
348
- refWarp : `~lsst.afw.image.Exposure `
360
+ refWarp : `~lsst.afw.image.ExposureF `
349
361
Reference warped exposure.
350
- compWarps : `list`[`~lsst.afw.image.Exposure`]
351
- List of warped science exposures to compare to the reference.
362
+ refWarpIndex : `int`
363
+ Index of the reference removed from the list of warps.
364
+
365
+ Notes
366
+ -----
367
+ This method modifies the input list of warps in place by removing the
368
+ reference warp from it.
352
369
"""
353
- # User a reference visit, if one has been supplied
370
+ # User-defined reference visit, if one has been supplied
354
371
if refWarpVisit :
355
- warpVisits = [warp .dataId ["visit" ] for warp in warps ]
372
+ warpVisits = [warpDDH .dataId ["visit" ] for warpDDH in warps ]
356
373
try :
357
- refWarp = warps .pop (warpVisits .index (refWarpVisit ))
374
+ refWarpIndex = warpVisits .index (refWarpVisit )
375
+ refWarpDDH = warps .pop (refWarpIndex )
358
376
self .log .info ("Using user-supplied reference visit %d" , refWarpVisit )
359
- return refWarp
377
+ return refWarpDDH . get (), refWarpIndex
360
378
except ValueError :
361
379
raise TaskError (f"Reference visit { refWarpVisit } is not found in the list of warps." )
362
380
363
381
# Extract mean/var/npoints for each warp
364
- warpMeans = []
365
- warpVars = []
366
- warpNPoints = []
382
+ warpChi2s = [] # Background goodness of fit
383
+ warpVars = [] # Variance
384
+ warpNPointsGlobal = [] # Global coverage
385
+ warpNPointsEdge = [] # Edge coverage
367
386
for warpDDH in warps :
368
387
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
-
379
- warp .image .array *= warp .getPhotoCalib ().instFluxToNanojansky (1 ) # Convert image plane to nJy
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 )
391
- warpMean , _ = warpStats .getResult (MEAN )
388
+ instFluxToNanojansky = warp .getPhotoCalib ().instFluxToNanojansky (1 )
389
+ warp .image *= instFluxToNanojansky # Images in nJy to facilitate difference imaging
390
+ warp .variance *= instFluxToNanojansky
391
+ warpBg , _ = self ._makeBackground (warp )
392
+
393
+ # Return an approximation to the background
394
+ approxCtrl = ApproximateControl (ApproximateControl .CHEBYSHEV , 1 , 1 , self .config .approxWeighting )
395
+ warpApprox = warpBg .getApproximate (approxCtrl , self .undersampleStyle )
396
+ warpBgSub = ImageF (warp .image .array - warpApprox .getImage ().array )
397
+
398
+ warpStats = makeStatistics (warpBgSub , warp .mask , VARIANCE | NPOINT , self .statsCtrl )
399
+ # TODO: need to modify this to account for the background mask
400
+ warpChi2 = np .nansum (warpBgSub .array ** 2 / warp .variance .array )
392
401
warpVar , _ = warpStats .getResult (VARIANCE )
393
- warpNPoint , _ = warpStats .getResult (NPOINT )
394
- warpMeans .append (warpMean )
402
+ warpNPointGlobal , _ = warpStats .getResult (NPOINT )
403
+ warpNPointEdge = (
404
+ np .sum (~ np .isnan (warp .image .array [:, 0 ])) # Left edge
405
+ + np .sum (~ np .isnan (warp .image .array [:, - 1 ])) # Right edge
406
+ + np .sum (~ np .isnan (warp .image .array [0 , :])) # Bottom edge
407
+ + np .sum (~ np .isnan (warp .image .array [- 1 , :])) # Top edge
408
+ )
409
+ warpChi2s .append (warpChi2 )
395
410
warpVars .append (warpVar )
396
- warpNPoints .append (int (warpNPoint ))
397
-
398
- if len (warpNPoints ) == 0 :
399
- raise TaskError ("No suitable reference visit found in list of warps." )
411
+ warpNPointsGlobal .append (int (warpNPointGlobal ))
412
+ warpNPointsEdge .append (warpNPointEdge )
400
413
401
414
# Normalize mean/var/npoints to range from 0 to 1
402
- warpMeansFrac = np .array (warpMeans ) / np .nanmax (warpMeans )
415
+ warpChi2sFrac = np .array (warpChi2s ) / np .nanmax (warpChi2s )
403
416
warpVarsFrac = np .array (warpVars ) / np .nanmax (warpVars )
404
- warpNPointsFrac = np .nanmin (warpNPoints ) / np .array (warpNPoints )
417
+ warpNPointsGlobalFrac = np .nanmin (warpNPointsGlobal ) / np .array (warpNPointsGlobal )
418
+ warpNPointsEdgeFrac = np .nanmin (warpNPointsEdge ) / np .array (warpNPointsEdge )
405
419
406
420
# Calculate cost function values
407
- costFunctionVals = self .config .bestRefWeightLevel * warpMeansFrac
421
+ costFunctionVals = self .config .bestRefWeightChi2 * warpChi2sFrac
408
422
costFunctionVals += self .config .bestRefWeightVariance * warpVarsFrac
409
- costFunctionVals += self .config .bestRefWeightCoverage * warpNPointsFrac
423
+ costFunctionVals += self .config .bestRefWeightGlobalCoverage * warpNPointsGlobalFrac
424
+ costFunctionVals += self .config .bestRefWeightEdgeCoverage * warpNPointsEdgeFrac
410
425
411
426
ind = np .nanargmin (costFunctionVals )
412
427
refWarp = warps .pop (ind )
413
428
self .log .info ("Using best reference visit %d" , refWarp .dataId ["visit" ])
414
429
return refWarp , ind
415
430
416
- def _fluxScale (self , exposure ) :
417
- """Scales image to nJy flux using photometric calibration .
431
+ def _makeBackground (self , warp : ExposureF ) -> tuple [ BackgroundMI , BackgroundControl ] :
432
+ """Generate a simple binned background masked image for warped data .
418
433
419
434
Parameters
420
435
----------
421
- exposure : `lsst.afw.image._exposure .ExposureF`
422
- Exposure to scale .
436
+ warp : `~ lsst.afw.image.ExposureF`
437
+ Warped exposure for which to estimate background .
423
438
424
439
Returns
425
440
-------
426
- maskedImage: `lsst.afw.image._maskedImage.MaskedImageF`
427
- Flux-scaled masked exposure.
441
+ warpBgMI: `~lsst.afw.math.BackgroundMI`
442
+ Background-subtracted masked image.
443
+ bgCtrl: `~lsst.afw.math.BackgroundControl`
444
+ Background control object.
428
445
"""
429
- maskedImage = exposure .getMaskedImage ()
430
- fluxZp = exposure .getPhotoCalib ().instFluxToNanojansky (1 )
431
- exposure .image .array *= fluxZp
446
+ nx = warp .getWidth () // self .config .binSize
447
+ ny = warp .getHeight () // self .config .binSize
432
448
433
- return maskedImage
449
+ bgCtrl = BackgroundControl (nx , ny , self .statsCtrl , self .statsFlag )
450
+ bgCtrl .setUndersampleStyle (self .config .undersampleStyle )
451
+ warpBgMI = makeBackground (warp .getMaskedImage (), bgCtrl )
434
452
435
- def _setupBackground (self , warp ):
436
- """Set up and return a background model container and associated images
437
- and controllers.
453
+ return warpBgMI , bgCtrl
438
454
439
- Uses the following config parameters:
440
- - ``gridStatistic``
441
- - ``numSigmaClip``
442
- - ``numIter``
443
- - ``binSize``
444
- - ``undersampleStyle``
455
+ def _fluxScale (self , exposure ):
456
+ """Scales image to nJy flux using photometric calibration.
445
457
446
458
Parameters
447
459
----------
448
- warp: `lsst.afw.image._exposure.ExposureF`
449
- Warped exposure or difference image for which to estimate
450
- background.
460
+ exposure: `lsst.afw.image._exposure.ExposureF`
461
+ Exposure to scale.
451
462
452
463
Returns
453
464
-------
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)
465
+ maskedImage: `lsst.afw.image._maskedImage.MaskedImageF`
466
+ Flux-scaled masked exposure.
462
467
"""
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 )
468
+ maskedImage = exposure .getMaskedImage ()
469
+ fluxZp = exposure .getPhotoCalib ().instFluxToNanojansky (1 )
470
+ exposure .image .array *= fluxZp
482
471
483
- return bkgd , bctrl , warpMI , statsFlag
472
+ return maskedImage
484
473
485
474
@timeMethod
486
475
def matchBackgrounds (self , refExposure , sciExposure ):
0 commit comments