11
2- __all__ = ["DynamicDetectionConfig" , "DynamicDetectionTask" , "InsufficientSourcesError" ]
2+ __all__ = [
3+ "DynamicDetectionConfig" ,
4+ "DynamicDetectionTask" ,
5+ "InsufficientSourcesError" ,
6+ ]
37
48import numpy as np
59
610from lsst .pex .config import Field , ConfigurableField
7- from lsst .pipe .base import Struct
811
912from .detection import SourceDetectionConfig , SourceDetectionTask
1013from .skyObjects import SkyObjectsTask
1316from lsst .afw .geom import makeCdMatrix , makeSkyWcs , SpanSet
1417from lsst .afw .table import SourceCatalog , SourceTable
1518from lsst .meas .base import ForcedMeasurementTask
19+ from lsst .pipe .base import Struct
1620
1721import lsst .afw .image
1822import lsst .afw .math
@@ -99,6 +103,15 @@ class DynamicDetectionConfig(SourceDetectionConfig):
99103 "suitable locations to lay down sky objects. To allow for best effort "
100104 "sky source placement, if True, this allows for a slight erosion of "
101105 "the detection masks." )
106+ maxPeakToFootRatio = Field (dtype = float , default = 150.0 ,
107+ doc = "Maximum ratio of peak per footprint in the detection mask. "
108+ "This is to help prevent single contiguous footprints that nothing "
109+ "can be done with (i.e. deblending will be skipped). If the current "
110+ "detection plane does not satisfy this constraint, the detection "
111+ "threshold is increased iteratively until it is. This behaviour is "
112+ "intended to be an effective no-op for most \" typical\" scenes/standard "
113+ "quality observations, but can avoid total meltdown in, e.g. very "
114+ "crowded regions." )
102115
103116 def setDefaults (self ):
104117 SourceDetectionConfig .setDefaults (self )
@@ -139,7 +152,7 @@ def __init__(self, *args, **kwargs):
139152
140153 # Set up forced measurement.
141154 config = ForcedMeasurementTask .ConfigClass ()
142- config .plugins .names = [' base_TransformedCentroid' , ' base_PsfFlux' , 'base_LocalBackground' ]
155+ config .plugins .names = [" base_TransformedCentroid" , " base_PsfFlux" ]
143156 # We'll need the "centroid" and "psfFlux" slots
144157 for slot in ("shape" , "psfShape" , "apFlux" , "modelFlux" , "gaussianFlux" , "calibFlux" ):
145158 setattr (config .slots , slot , None )
@@ -270,10 +283,7 @@ def calculateThreshold(self, exposure, seed, sigma=None, minFractionSourcesFacto
270283 # Calculate new threshold
271284 fluxes = catalog ["base_PsfFlux_instFlux" ]
272285 area = catalog ["base_PsfFlux_area" ]
273- bg = catalog ["base_LocalBackground_instFlux" ]
274-
275- good = (~ catalog ["base_PsfFlux_flag" ] & ~ catalog ["base_LocalBackground_flag" ]
276- & np .isfinite (fluxes ) & np .isfinite (area ) & np .isfinite (bg ))
286+ good = (~ catalog ["base_PsfFlux_flag" ] & np .isfinite (fluxes ))
277287
278288 if good .sum () < minNumSources :
279289 if not isBgTweak :
@@ -302,9 +312,9 @@ def calculateThreshold(self, exposure, seed, sigma=None, minFractionSourcesFacto
302312 else :
303313 self .log .info ("Number of good sky sources used for dynamic detection background tweak:"
304314 " %d (of %d requested)." , good .sum (), self .skyObjects .config .nSources )
305- bgMedian = np .median ((fluxes / area )[good ])
306315
307- lq , uq = np .percentile ((fluxes - bg * area )[good ], [25.0 , 75.0 ])
316+ bgMedian = np .median ((fluxes / area )[good ])
317+ lq , uq = np .percentile (fluxes [good ], [25.0 , 75.0 ])
308318 stdevMeas = 0.741 * (uq - lq )
309319 medianError = np .median (catalog ["base_PsfFlux_instFluxErr" ][good ])
310320 if wcsIsNone :
@@ -421,22 +431,59 @@ def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True,
421431 # seed needs to fit in a C++ 'int' so pybind doesn't choke on it
422432 seed = (expId if expId is not None else int (maskedImage .image .array .sum ())) % (2 ** 31 - 1 )
423433 threshResults = self .calculateThreshold (exposure , seed , sigma = sigma )
424- factor = threshResults .multiplicative
425- self .log .info ("Modifying configured detection threshold by factor %f to %f" ,
434+ minMultiplicative = 0.5
435+ if threshResults .multiplicative < minMultiplicative :
436+ self .log .warning ("threshResults.multiplicative = %.2f is less than minimum value (%.2f). "
437+ "Setting to %.2f." , threshResults .multiplicative , minMultiplicative ,
438+ minMultiplicative )
439+ factor = max (minMultiplicative , threshResults .multiplicative )
440+ self .log .info ("Modifying configured detection threshold by factor %.2f to %.2f" ,
426441 factor , factor * self .config .thresholdValue )
427442
428- # Blow away preliminary (low threshold) detection mask
429- self .clearMask (maskedImage .mask )
430- if not clearMask :
431- maskedImage .mask .array |= oldDetected
432-
433- # Rinse and repeat thresholding with new calculated threshold
434- results = self .applyThreshold (middle , maskedImage .getBBox (), factor )
435- results .prelim = prelim
436- results .background = background if background is not None else lsst .afw .math .BackgroundList ()
437- if self .config .doTempLocalBackground :
438- self .applyTempLocalBackground (exposure , middle , results )
439- self .finalizeFootprints (maskedImage .mask , results , sigma , factor = factor )
443+ growOverride = None
444+ inFinalize = True
445+ while inFinalize :
446+ inFinalize = False
447+ # Blow away preliminary (low threshold) detection mask
448+ self .clearMask (maskedImage .mask )
449+ if not clearMask :
450+ maskedImage .mask .array |= oldDetected
451+
452+ # Rinse and repeat thresholding with new calculated threshold
453+ results = self .applyThreshold (middle , maskedImage .getBBox (), factor )
454+ results .prelim = prelim
455+ results .background = background if background is not None else lsst .afw .math .BackgroundList ()
456+ if self .config .doTempLocalBackground :
457+ self .applyTempLocalBackground (exposure , middle , results )
458+ self .finalizeFootprints (maskedImage .mask , results , sigma , factor = factor ,
459+ growOverride = growOverride )
460+ self .log .warning ("nPeaks/nFootprint = %.2f (max is %.1f)" ,
461+ results .numPosPeaks / results .numPos ,
462+ self .config .maxPeakToFootRatio )
463+ if results .numPosPeaks / results .numPos > self .config .maxPeakToFootRatio :
464+ if results .numPosPeaks / results .numPos > 3 * self .config .maxPeakToFootRatio :
465+ factor *= 1.4
466+ else :
467+ factor *= 1.2
468+ if factor > 2.0 :
469+ if growOverride is None :
470+ growOverride = 0.75 * self .config .nSigmaToGrow
471+ else :
472+ growOverride *= 0.75
473+ self .log .warning ("Decreasing nSigmaToGrow to %.2f" , growOverride )
474+ if factor >= 5 :
475+ self .log .warning ("New theshold value would be > 5 times the initially requested "
476+ "one (%.2f > %.2f). Leaving inFinalize iteration without "
477+ "getting the number of peaks per footprint below %.1f" ,
478+ factor * self .config .thresholdValue , self .config .thresholdValue ,
479+ self .config .maxPeakToFootRatio )
480+ inFinalize = False
481+ else :
482+ inFinalize = True
483+ self .log .warning ("numPosPeaks/numPos (%d) > maxPeakPerFootprint (%.1f). "
484+ "Increasing threshold factor to %.2f and re-running," ,
485+ results .numPosPeaks / results .numPos , self .config .maxPeakToFootRatio ,
486+ factor )
440487
441488 self .clearUnwantedResults (maskedImage .mask , results )
442489
@@ -445,15 +492,22 @@ def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True,
445492
446493 self .display (exposure , results , middle )
447494
495+ # Re-do the background tweak after any temporary backgrounds have
496+ # been restored.
497+ #
498+ # But we want to keep any large-scale background (e.g., scattered
499+ # light from bright stars) from being selected for sky objects in
500+ # the calculation, so do another detection pass without either the
501+ # local or wide temporary background subtraction; the DETECTED
502+ # pixels will mark the area to ignore.
503+
504+ # The following if/else is to workaround the fact that it is
505+ # currently not possible to persist an empty BackgroundList, so
506+ # we instead set the value of the backround tweak to 0.0 if
507+ # doBackgroundTweak is False and call the tweakBackground function
508+ # regardless to get at least one background into the list (do we
509+ # need a TODO here?).
448510 if self .config .doBackgroundTweak :
449- # Re-do the background tweak after any temporary backgrounds have
450- # been restored.
451- #
452- # But we want to keep any large-scale background (e.g., scattered
453- # light from bright stars) from being selected for sky objects in
454- # the calculation, so do another detection pass without either the
455- # local or wide temporary background subtraction; the DETECTED
456- # pixels will mark the area to ignore.
457511 originalMask = maskedImage .mask .array .copy ()
458512 try :
459513 self .clearMask (exposure .mask )
@@ -464,7 +518,9 @@ def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True,
464518 isBgTweak = True ).additive
465519 finally :
466520 maskedImage .mask .array [:] = originalMask
467- self .tweakBackground (exposure , bgLevel , results .background )
521+ else :
522+ bgLevel = 0.0
523+ self .tweakBackground (exposure , bgLevel , results .background )
468524
469525 return results
470526
@@ -485,7 +541,8 @@ def tweakBackground(self, exposure, bgLevel, bgList=None):
485541 bg : `lsst.afw.math.BackgroundMI`
486542 Constant background model.
487543 """
488- self .log .info ("Tweaking background by %f to match sky photometry" , bgLevel )
544+ if bgLevel != 0.0 :
545+ self .log .info ("Tweaking background by %f to match sky photometry" , bgLevel )
489546 exposure .image -= bgLevel
490547 bgStats = lsst .afw .image .MaskedImageF (1 , 1 )
491548 bgStats .set (bgLevel , 0 , bgLevel )
@@ -564,7 +621,7 @@ def _computeBrightDetectionMask(self, maskedImage, convolveResults):
564621 format (nPixDetNeg , 100 * nPixDetNeg / nPix ))
565622 if nPixDetNeg / nPix > brightMaskFractionMax or nPixDet / nPix > brightMaskFractionMax :
566623 self .log .warn ("Too high a fraction (%.1f > %.1f) of pixels were masked with current "
567- "\" bright\" detection round thresholds. Increasing by a factor of %f "
624+ "\" bright\" detection round thresholds. Increasing by a factor of %.2f "
568625 "and trying again." , max (nPixDetNeg , nPixDet )/ nPix ,
569626 brightMaskFractionMax , self .config .bisectFactor )
570627
0 commit comments