2323
2424import lsstDebug
2525import 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
2727from lsst .afw .math import (
2828 MEAN ,
2929 MEANCLIP ,
3535 ApproximateControl ,
3636 BackgroundControl ,
3737 BackgroundList ,
38+ BackgroundMI ,
3839 StatisticsControl ,
3940 makeBackground ,
4041 makeStatistics ,
@@ -84,11 +85,11 @@ class MatchBackgroundsConfig(PipelineTaskConfig, pipelineConnections=MatchBackgr
8485 doc = "Visit ID of the reference warp. If None, the best warp is chosen from the list of warps." ,
8586 optional = True ,
8687 )
87- bestRefWeightCoverage = RangeField (
88+ bestRefWeightChi2 = RangeField (
8889 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 ,
9293 min = 0.0 ,
9394 max = 1.0 ,
9495 )
@@ -100,10 +101,18 @@ class MatchBackgroundsConfig(PipelineTaskConfig, pipelineConnections=MatchBackgr
100101 min = 0.0 ,
101102 max = 1.0 ,
102103 )
103- bestRefWeightLevel = RangeField (
104+ bestRefWeightGlobalCoverage = RangeField (
104105 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." ,
107116 default = 0.2 ,
108117 min = 0.0 ,
109118 max = 1.0 ,
@@ -143,7 +152,7 @@ class MatchBackgroundsConfig(PipelineTaskConfig, pipelineConnections=MatchBackgr
143152 )
144153 binSize = Field [int ](
145154 doc = "Bin size for gridding the difference image and fitting a spatial model." ,
146- default = 256 ,
155+ default = 1024 ,
147156 )
148157 interpStyle = ChoiceField (
149158 dtype = str ,
@@ -213,11 +222,16 @@ class MatchBackgroundsTask(PipelineTask):
213222
214223 def __init__ (self , * args , ** kwargs ):
215224 super ().__init__ (** kwargs )
225+ self .statsFlag = stringToStatisticsProperty (self .config .gridStatistic )
216226 self .statsCtrl = StatisticsControl ()
217227 # TODO: Check that setting the mask planes here work - these planes
218228 # can vary from exposure to exposure, I think?
219229 self .statsCtrl .setAndMask (Mask .getPlaneBitMask (self .config .badMaskPlanes ))
220230 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 )
221235
222236 def runQuantum (self , butlerQC , inputRefs , outputRefs ):
223237 inputs = butlerQC .get (inputRefs )
@@ -333,154 +347,129 @@ def _defineWarps(self, warps, refWarpVisit=None):
333347 minimizing a cost function that penalizes high variance, high
334348 background level, and low coverage.
335349
336- To find a reference warp, the following config parameters are used:
337- - ``bestRefWeightCoverage``
338- - ``bestRefWeightVariance``
339- - ``bestRefWeightLevel``
340-
341350 Parameters
342351 ----------
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.
345357
346358 Returns
347359 -------
348- refWarp : `~lsst.afw.image.Exposure `
360+ refWarp : `~lsst.afw.image.ExposureF `
349361 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.
352369 """
353- # User a reference visit, if one has been supplied
370+ # User-defined reference visit, if one has been supplied
354371 if refWarpVisit :
355- warpVisits = [warp .dataId ["visit" ] for warp in warps ]
372+ warpVisits = [warpDDH .dataId ["visit" ] for warpDDH in warps ]
356373 try :
357- refWarp = warps .pop (warpVisits .index (refWarpVisit ))
374+ refWarpIndex = warpVisits .index (refWarpVisit )
375+ refWarpDDH = warps .pop (refWarpIndex )
358376 self .log .info ("Using user-supplied reference visit %d" , refWarpVisit )
359- return refWarp
377+ return refWarpDDH . get (), refWarpIndex
360378 except ValueError :
361379 raise TaskError (f"Reference visit { refWarpVisit } is not found in the list of warps." )
362380
363381 # 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
367386 for warpDDH in warps :
368387 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 )
392401 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 )
395410 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 )
400413
401414 # 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 )
403416 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 )
405419
406420 # Calculate cost function values
407- costFunctionVals = self .config .bestRefWeightLevel * warpMeansFrac
421+ costFunctionVals = self .config .bestRefWeightChi2 * warpChi2sFrac
408422 costFunctionVals += self .config .bestRefWeightVariance * warpVarsFrac
409- costFunctionVals += self .config .bestRefWeightCoverage * warpNPointsFrac
423+ costFunctionVals += self .config .bestRefWeightGlobalCoverage * warpNPointsGlobalFrac
424+ costFunctionVals += self .config .bestRefWeightEdgeCoverage * warpNPointsEdgeFrac
410425
411426 ind = np .nanargmin (costFunctionVals )
412427 refWarp = warps .pop (ind )
413428 self .log .info ("Using best reference visit %d" , refWarp .dataId ["visit" ])
414429 return refWarp , ind
415430
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 .
418433
419434 Parameters
420435 ----------
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 .
423438
424439 Returns
425440 -------
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.
428445 """
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
432448
433- return maskedImage
449+ bgCtrl = BackgroundControl (nx , ny , self .statsCtrl , self .statsFlag )
450+ bgCtrl .setUndersampleStyle (self .config .undersampleStyle )
451+ warpBgMI = makeBackground (warp .getMaskedImage (), bgCtrl )
434452
435- def _setupBackground (self , warp ):
436- """Set up and return a background model container and associated images
437- and controllers.
453+ return warpBgMI , bgCtrl
438454
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.
445457
446458 Parameters
447459 ----------
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.
451462
452463 Returns
453464 -------
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.
462467 """
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
482471
483- return bkgd , bctrl , warpMI , statsFlag
472+ return maskedImage
484473
485474 @timeMethod
486475 def matchBackgrounds (self , refExposure , sciExposure ):
0 commit comments