2323
2424import lsstDebug
2525import numpy as np
26- from lsst .afw .image import LOCAL , PARENT , ExposureF , ImageF , Mask , MaskedImageF
26+ from lsst .afw .image import LOCAL , PARENT , ImageF , Mask , MaskedImageF
2727from lsst .afw .math import (
2828 MEAN ,
2929 MEANCLIP ,
4343 stringToStatisticsProperty ,
4444 stringToUndersampleStyle ,
4545)
46- from lsst .geom import Box2D , Box2I , PointI
46+ from lsst .geom import Box2I , PointI
4747from lsst .pex .config import ChoiceField , Field , ListField , RangeField
4848from lsst .pipe .base import PipelineTask , PipelineTaskConfig , PipelineTaskConnections , Struct , TaskError
4949from lsst .pipe .base .connectionTypes import Input , Output
@@ -137,7 +137,16 @@ class MatchBackgroundsConfig(PipelineTaskConfig, pipelineConnections=MatchBackgr
137137 )
138138 badMaskPlanes = ListField [str ](
139139 doc = "Names of mask planes to ignore while estimating the background." ,
140- default = ["NO_DATA" , "DETECTED" , "DETECTED_NEGATIVE" , "SAT" , "BAD" , "INTRP" , "CR" ],
140+ default = [
141+ "NO_DATA" ,
142+ "DETECTED" ,
143+ "DETECTED_NEGATIVE" ,
144+ "SAT" ,
145+ "BAD" ,
146+ "INTRP" ,
147+ "CR" ,
148+ "NOT_DEBLENDED" ,
149+ ],
141150 itemCheck = lambda x : x in Mask ().getMaskPlaneDict (),
142151 )
143152 gridStatistic = ChoiceField (
@@ -237,9 +246,6 @@ def __init__(self, *args, **kwargs):
237246 super ().__init__ (** kwargs )
238247 self .statsFlag = stringToStatisticsProperty (self .config .gridStatistic )
239248 self .statsCtrl = StatisticsControl ()
240- # TODO: Check that setting the mask planes here work - these planes
241- # can vary from exposure to exposure, I think?
242- # Aaron: I think only the bit values vary, not the names, which this is referencing.
243249 self .statsCtrl .setAndMask (Mask .getPlaneBitMask (self .config .badMaskPlanes ))
244250 self .statsCtrl .setNanSafe (True )
245251 self .statsCtrl .setNumSigmaClip (self .config .numSigmaClip )
@@ -278,7 +284,7 @@ def run(self, warps):
278284 raise TaskError ("No exposures to match" )
279285
280286 # Define a reference warp; 'warps' is modified in-place to exclude it
281- refWarp , refInd = self ._defineWarps (warps = warps , refWarpVisit = self .config .refWarpVisit )
287+ refWarp , refInd , bkgd = self ._defineWarps (warps = warps , refWarpVisit = self .config .refWarpVisit )
282288
283289 # Images must be scaled to a common ZP
284290 # Converting everything to nJy to accomplish this
@@ -287,29 +293,11 @@ def run(self, warps):
287293
288294 self .log .info ("Matching %d Exposures" , numExp )
289295
290- # Creating a null BackgroundList object by fitting a blank image
291- statsFlag = stringToStatisticsProperty (self .config .gridStatistic )
292- self .statsCtrl .setNumSigmaClip (self .config .numSigmaClip )
293- self .statsCtrl .setNumIter (self .config .numIter )
294-
295- # TODO: refactor below to construct blank bg model
296- im = refExposure .getMaskedImage ()
297- blankIm = im .clone ()
298- blankIm .image .array *= 0
299-
300- width = blankIm .getWidth ()
301- height = blankIm .getHeight ()
302- nx = width // self .config .binSize
303- if width % self .config .binSize != 0 :
304- nx += 1
305- ny = height // self .config .binSize
306- if height % self .config .binSize != 0 :
307- ny += 1
308-
309- bctrl = BackgroundControl (nx , ny , self .statsCtrl , statsFlag )
310- bctrl .setUndersampleStyle (self .config .undersampleStyle )
311-
312- bkgd = makeBackground (blankIm , bctrl )
296+ # Blank ref warp background as reference background
297+ bkgdIm = bkgd .getImageF ()
298+ bkgdStatsIm = bkgd .getStatsImage ()
299+ bkgdIm *= 0
300+ bkgdStatsIm *= 0
313301 blank = BackgroundList (
314302 (
315303 bkgd ,
@@ -325,7 +313,6 @@ def run(self, warps):
325313 backgroundInfoList = []
326314 matchedImageList = []
327315 for exp in warps :
328- # TODO: simplify what this prints?
329316 self .log .info (
330317 "Matching background of %s to %s" ,
331318 exp .dataId ,
@@ -347,7 +334,6 @@ def run(self, warps):
347334 toMatchExposure .image /= instFluxToNanojansky # Back to cts
348335 matchedImageList .append (toMatchExposure )
349336
350- # TODO: more elegant solution than inserting blank model at ref ind?
351337 backgroundInfoList .insert (refInd , blank )
352338 refExposure .image /= instFluxToNanojanskyRef # Back to cts
353339 matchedImageList .insert (refInd , refExposure )
@@ -377,6 +363,8 @@ def _defineWarps(self, warps, refWarpVisit=None):
377363 Reference warped exposure.
378364 refWarpIndex : `int`
379365 Index of the reference removed from the list of warps.
366+ warpBg : `~lsst.afw.math.BackgroundMI`
367+ Temporary background model, used to make a blank BG for the ref
380368
381369 Notes
382370 -----
@@ -454,7 +442,7 @@ def _defineWarps(self, warps, refWarpVisit=None):
454442 ind = np .nanargmin (costFunctionVals )
455443 refWarp = warps .pop (ind )
456444 self .log .info ("Using best reference visit %d" , refWarp .dataId ["visit" ])
457- return refWarp , ind
445+ return refWarp , ind , warpBg
458446
459447 def _makeBackground (self , warp : MaskedImageF , binSize ) -> tuple [BackgroundMI , BackgroundControl ]:
460448 """Generate a simple binned background masked image for warped data.
@@ -528,11 +516,6 @@ def matchBackgrounds(self, refExposure, sciExposure):
528516 model : `~lsst.afw.math.BackgroundMI`
529517 Background model of difference image, reference - science
530518 """
531- # TODO: this is deprecated
532- if lsstDebug .Info (__name__ ).savefits :
533- refExposure .writeFits (lsstDebug .Info (__name__ ).figpath + "refExposure.fits" )
534- sciExposure .writeFits (lsstDebug .Info (__name__ ).figpath + "sciExposure.fits" )
535-
536519 # Check Configs for polynomials:
537520 if self .config .usePolynomial :
538521 x , y = sciExposure .getDimensions ()
@@ -622,17 +605,6 @@ def matchBackgrounds(self, refExposure, sciExposure):
622605 resids = bgZ - modelValueArr
623606 rms = np .sqrt (np .mean (resids [~ np .isnan (resids )] ** 2 ))
624607
625- # TODO: also deprecated; _gridImage() maybe can go?
626- if lsstDebug .Info (__name__ ).savefits :
627- sciExposure .writeFits (lsstDebug .Info (__name__ ).figpath + "sciMatchedExposure.fits" )
628-
629- if lsstDebug .Info (__name__ ).savefig :
630- bbox = Box2D (refExposure .getMaskedImage ().getBBox ())
631- try :
632- self ._debugPlot (bgX , bgY , bgZ , bgdZ , bkgdImage , bbox , modelValueArr , resids )
633- except Exception as e :
634- self .log .warning ("Debug plot not generated: %s" , e )
635-
636608 meanVar = makeStatistics (diffMI .getVariance (), diffMI .getMask (), MEANCLIP , self .statsCtrl ).getValue ()
637609
638610 diffIm = diffMI .getImage ()
@@ -642,7 +614,6 @@ def matchBackgrounds(self, refExposure, sciExposure):
642614
643615 outBkgd = approx if self .config .usePolynomial else bkgd
644616 # Convert this back into counts
645- # TODO: is there a one-line way to do this?
646617 statsIm = outBkgd .getStatsImage ()
647618 statsIm /= instFluxToNanojansky
648619 bkgdIm = outBkgd .getImageF ()
@@ -667,119 +638,3 @@ def matchBackgrounds(self, refExposure, sciExposure):
667638 False ,
668639 )
669640 )
670-
671- def _debugPlot (self , X , Y , Z , dZ , modelImage , bbox , model , resids ):
672- """
673- Consider deleting this entirely
674- Generate a plot showing the background fit and residuals.
675-
676- It is called when lsstDebug.Info(__name__).savefig = True.
677- Saves the fig to lsstDebug.Info(__name__).figpath.
678- Displays on screen if lsstDebug.Info(__name__).display = True.
679-
680- Parameters
681- ----------
682- X : `np.ndarray`, (N,)
683- Array of x positions.
684- Y : `np.ndarray`, (N,)
685- Array of y positions.
686- Z : `np.ndarray`
687- Array of the grid values that were interpolated.
688- dZ : `np.ndarray`, (len(Z),)
689- Array of the error on the grid values.
690- modelImage : `Unknown`
691- Image of the model of the fit.
692- model : `np.ndarray`, (len(Z),)
693- Array of len(Z) containing the grid values predicted by the model.
694- resids : `Unknown`
695- Z - model.
696- """
697- import matplotlib .colors
698- import matplotlib .pyplot as plt
699- from mpl_toolkits .axes_grid1 import ImageGrid
700-
701- zeroIm = MaskedImageF (Box2I (bbox ))
702- zeroIm += modelImage
703- x0 , y0 = zeroIm .getXY0 ()
704- dx , dy = zeroIm .getDimensions ()
705- if len (Z ) == 0 :
706- self .log .warning ("No grid. Skipping plot generation." )
707- else :
708- max , min = np .max (Z ), np .min (Z )
709- norm = matplotlib .colors .normalize (vmax = max , vmin = min )
710- maxdiff = np .max (np .abs (resids ))
711- diffnorm = matplotlib .colors .normalize (vmax = maxdiff , vmin = - maxdiff )
712- rms = np .sqrt (np .mean (resids ** 2 ))
713- fig = plt .figure (1 , (8 , 6 ))
714- meanDz = np .mean (dZ )
715- grid = ImageGrid (
716- fig ,
717- 111 ,
718- nrows_ncols = (1 , 2 ),
719- axes_pad = 0.1 ,
720- share_all = True ,
721- label_mode = "L" ,
722- cbar_mode = "each" ,
723- cbar_size = "7%" ,
724- cbar_pad = "2%" ,
725- cbar_location = "top" ,
726- )
727- im = grid [0 ].imshow (
728- zeroIm .getImage ().getArray (), extent = (x0 , x0 + dx , y0 + dy , y0 ), norm = norm , cmap = "Spectral"
729- )
730- im = grid [0 ].scatter (
731- X , Y , c = Z , s = 15.0 * meanDz / dZ , edgecolor = "none" , norm = norm , marker = "o" , cmap = "Spectral"
732- )
733- im2 = grid [1 ].scatter (X , Y , c = resids , edgecolor = "none" , norm = diffnorm , marker = "s" , cmap = "seismic" )
734- grid .cbar_axes [0 ].colorbar (im )
735- grid .cbar_axes [1 ].colorbar (im2 )
736- grid [0 ].axis ([x0 , x0 + dx , y0 + dy , y0 ])
737- grid [1 ].axis ([x0 , x0 + dx , y0 + dy , y0 ])
738- grid [0 ].set_xlabel ("model and grid" )
739- grid [1 ].set_xlabel ("residuals. rms = %0.3f" % (rms ))
740- if lsstDebug .Info (__name__ ).savefig :
741- fig .savefig (lsstDebug .Info (__name__ ).figpath + self .debugDataIdString + ".png" )
742- if lsstDebug .Info (__name__ ).display :
743- plt .show ()
744- plt .clf ()
745-
746- def _gridImage (self , maskedImage , binsize , statsFlag ):
747- """Private method to grid an image for debugging."""
748- width , height = maskedImage .getDimensions ()
749- x0 , y0 = maskedImage .getXY0 ()
750- xedges = np .arange (0 , width , binsize )
751- yedges = np .arange (0 , height , binsize )
752- xedges = np .hstack ((xedges , width )) # add final edge
753- yedges = np .hstack ((yedges , height )) # add final edge
754-
755- # Use lists/append to protect against the case where
756- # a bin has no valid pixels and should not be included in the fit
757- bgX = []
758- bgY = []
759- bgZ = []
760- bgdZ = []
761-
762- for ymin , ymax in zip (yedges [0 :- 1 ], yedges [1 :]):
763- for xmin , xmax in zip (xedges [0 :- 1 ], xedges [1 :]):
764- subBBox = Box2I (
765- PointI (int (x0 + xmin ), int (y0 + ymin )),
766- PointI (int (x0 + xmax - 1 ), int (y0 + ymax - 1 )),
767- )
768- subIm = MaskedImageF (maskedImage , subBBox , PARENT , False )
769- stats = makeStatistics (
770- subIm ,
771- MEAN | MEANCLIP | MEDIAN | NPOINT | STDEV ,
772- self .statsCtrl ,
773- )
774- npoints , _ = stats .getResult (NPOINT )
775- if npoints >= 2 :
776- stdev , _ = stats .getResult (STDEV )
777- if stdev < self .config .gridStdevEpsilon :
778- stdev = self .config .gridStdevEpsilon
779- bgX .append (0.5 * (x0 + xmin + x0 + xmax ))
780- bgY .append (0.5 * (y0 + ymin + y0 + ymax ))
781- bgdZ .append (stdev / np .sqrt (npoints ))
782- est , _ = stats .getResult (statsFlag )
783- bgZ .append (est )
784-
785- return np .array (bgX ), np .array (bgY ), np .array (bgZ ), np .array (bgdZ )
0 commit comments