2626
2727import numpy as np
2828
29- from lsst .pex .config import Field , Config , DictField , FieldValidationError
30- from lsst .pipe .base import Struct , Task
29+ from lsst .pex .config import Field , Config , ConfigField , DictField , FieldValidationError
30+ from lsst .pipe .base import Task
3131
3232from .detection import SourceDetectionConfig , SourceDetectionTask
3333
@@ -75,12 +75,25 @@ class AdaptiveThresholdDetectionConfig(Config):
7575 sufficientFractionIsolated = Field (dtype = float , default = 0.45 ,
7676 doc = "Fraction of single-peaked (isolated) footprints considered "
7777 "sufficient to exit the iteration loop." )
78+ baseline = ConfigField (
79+ "Baseline configuration for SourceDetectionTask in the absence of any iteration. "
80+ "All options other than thresholdPolarity, thresholdValue, and includeThresholdMultiplier "
81+ "are held fixed at these values." ,
82+ SourceDetectionConfig ,
83+ )
84+
85+ def setDefaults (self ):
86+ self .baseline .reEstimateBackground = False
87+ self .baseline .doTempWideBackground = True
88+ self .baseline .tempWideBackground .binSize = 512
89+ self .baseline .thresholdPolarity = "positive" # for schema and final run.
90+ self .baseline .includeThresholdMultiplier = 2.0
7891
7992 def validate (self ):
8093 super ().validate ()
8194 if "fallback" not in self .maxNumPeakPerBand :
8295 msg = ("Must include a \" fallback\" key in the config.maxNumPeakPerBand config dict. "
83- f"It is currenly : { self .maxNumPeakPerBand } ." )
96+ f"It is currently : { self .maxNumPeakPerBand } ." )
8497 raise FieldValidationError (self .__class__ .maxNumPeakPerBand , self , msg )
8598 if self .minFootprint < self .minIsolated :
8699 msg = (f"The config.minFootprint (= { self .minFootprint } ) must be >= that of "
@@ -90,19 +103,27 @@ def validate(self):
90103 msg = (f"The config.sufficientIsolated (= { self .sufficientIsolated } ) must be >= that of "
91104 f"config.minIsolated (= { self .minIsolated } )." )
92105 raise FieldValidationError (self .__class__ .sufficientIsolated , self , msg )
106+ if self .baseline .reEstimateBackground :
107+ raise FieldValidationError (
108+ self .__class__ .baseline , self ,
109+ "Baseline detection configuration must not include background re-estimation."
110+ )
93111
94112
95113class AdaptiveThresholdDetectionTask (Task ):
96114 """Detection of sources on an image using an adaptive scheme for
97115 the detection threshold.
98116 """
99117 ConfigClass = AdaptiveThresholdDetectionConfig
100- _DefaultName = "adaptiveThresholdDetection "
118+ _DefaultName = "detection "
101119
102- def __init__ (self , * args , ** kwargs ):
103- Task .__init__ (self , * args , ** kwargs )
120+ def __init__ (self , schema = None , ** kwargs ):
121+ super ().__init__ (** kwargs )
122+ # We make a baseline SourceDetectionTask only to set up the schema.
123+ if schema is not None :
124+ SourceDetectionTask (config = self .config .baseline , schema = schema )
104125
105- def run (self , table , exposure , initialThreshold = None , initialThresholdMultiplier = 2.0 ):
126+ def run (self , table , exposure , ** kwargs ):
106127 """Perform detection with an adaptive threshold detection scheme
107128 conditioned to maximize the likelihood of a successful PSF model fit
108129 for any given "scene".
@@ -131,42 +152,15 @@ def run(self, table, exposure, initialThreshold=None, initialThresholdMultiplier
131152 Table object that will be used to create the SourceCatalog.
132153 exposure : `lsst.afw.image.Exposure`
133154 Exposure to process; DETECTED mask plane will be set in-place.
134- initialThreshold : `float`, optional
135- Initial threshold for detection of PSF sources.
136- initialThresholdMultiplier : `float`, optional
137- Initial threshold for detection of PSF sources.
155+ **kwargs
156+ Forwarded to internal runs of `SourceDetectionTask`.
138157
139158 Returns
140159 -------
141160 results : `lsst.pipe.base.Struct`
142- The adaptive threshold detection results as a struct with
143- attributes :
161+ The adaptive threshold detection results. Most fields are directly
162+ produced by `SourceDetectionTask.run`. Additional fields include :
144163
145- ``detections``
146- Results of the final round of detection as a struch with
147- attributes:
148-
149- ``sources``
150- Detected sources on the exposure
151- (`lsst.afw.table.SourceCatalog`).
152- ``positive``
153- Positive polarity footprints
154- (`lsst.afw.detection.FootprintSet` or `None`).
155- ``negative``
156- Negative polarity footprints
157- (`lsst.afw.detection.FootprintSet` or `None`).
158- ``numPos``
159- Number of footprints in positive or 0 if detection polarity was
160- negative (`int`).
161- ``numNeg``
162- Number of footprints in negative or 0 if detection polarity was
163- positive (`int`).
164- ``background``
165- Always `None`; provided for compatibility with
166- `SourceDetectionTask`.
167- ``factor``
168- Multiplication factor applied to the configured detection
169- threshold. (`float`).
170164 ``thresholdValue``
171165 The final threshold value used to the configure the final round
172166 of detection (`float`).
@@ -187,29 +181,18 @@ def run(self, table, exposure, initialThreshold=None, initialThresholdMultiplier
187181 inAdaptiveDetection = True
188182 nAdaptiveDetIter = 0
189183 thresholdFactor = 1.0
190- if nAdaptiveDetIter == 0 :
191- if initialThreshold is None :
192- maxSn = float (np .nanmax (exposure .image .array / np .sqrt (exposure .variance .array )))
193- adaptiveDetThreshold = min (maxSn , 5.0 )
194- else :
195- adaptiveDetThreshold = initialThreshold
196- adaptiveDetectionConfig = SourceDetectionConfig ()
197- adaptiveDetectionConfig .thresholdValue = adaptiveDetThreshold
198- adaptiveDetectionConfig .includeThresholdMultiplier = initialThresholdMultiplier
199- adaptiveDetectionConfig .reEstimateBackground = False
200- adaptiveDetectionConfig .doTempWideBackground = True
201- adaptiveDetectionConfig .tempWideBackground .binSize = 512
202- adaptiveDetectionConfig .thresholdPolarity = "both"
203- self .log .info ("Using adaptive detection with thresholdValue = %.2f and multiplier = %.1f" ,
204- adaptiveDetectionConfig .thresholdValue ,
205- adaptiveDetectionConfig .includeThresholdMultiplier )
206- adaptiveDetectionTask = SourceDetectionTask (config = adaptiveDetectionConfig )
184+ adaptiveDetectionConfig = self .config .baseline .copy ()
185+ adaptiveDetectionConfig .thresholdPolarity = "both"
186+ self .log .info ("Using adaptive detection with thresholdValue = %.2f and multiplier = %.1f" ,
187+ adaptiveDetectionConfig .thresholdValue ,
188+ adaptiveDetectionConfig .includeThresholdMultiplier )
189+ adaptiveDetectionTask = SourceDetectionTask (config = adaptiveDetectionConfig )
207190
208191 maxNumNegFactor = 1.0
209192 while inAdaptiveDetection :
210193 inAdaptiveDetection = False
211194 nAdaptiveDetIter += 1
212- detRes = adaptiveDetectionTask .run (table = table , exposure = exposure , doSmooth = True )
195+ detRes = adaptiveDetectionTask .run (table = table , exposure = exposure , doSmooth = True , ** kwargs )
213196 sourceCat = detRes .sources
214197 nFootprint = len (sourceCat )
215198 nPeak = 0
@@ -338,10 +321,8 @@ def run(self, table, exposure, initialThreshold=None, initialThresholdMultiplier
338321 self .log .info ("Perfomring final round of detection with threshold %.2f and multiplier %.1f" ,
339322 adaptiveDetectionConfig .thresholdValue ,
340323 adaptiveDetectionConfig .includeThresholdMultiplier )
341- detRes = adaptiveDetectionTask .run (table = table , exposure = exposure , doSmooth = True ,
342- backgroundToPhotometricRatio = None )
343- return Struct (
344- detections = detRes ,
345- thresholdValue = adaptiveDetectionConfig .thresholdValue ,
346- includeThresholdMultiplier = adaptiveDetectionConfig .includeThresholdMultiplier ,
347- )
324+ detections = adaptiveDetectionTask .run (table = table , exposure = exposure , doSmooth = True , ** kwargs )
325+ detections .thresholdValue = adaptiveDetectionConfig .thresholdValue
326+ detections .includeThresholdMultiplier = adaptiveDetectionConfig .includeThresholdMultiplier
327+ return detections
328+
0 commit comments