Skip to content

Commit 7a03e69

Browse files
committed
Make AdaptiveThresholdDetection compatible with SourceDetectionTask.
...at least as far as the usage in CalibrateImageTask. This includes: - Nesting the baseline detection configuration. - Modifying the run arguments to accept only those expected by SourceDetectionTask.run (possibly since the initial threshold and multiplier are now nested in that baseline configuration). - Modifying the result struct to add new attributes to the struct returned by SourceDetectionTask instead of nesting that within a new struct.
1 parent 7002cb2 commit 7a03e69

File tree

2 files changed

+49
-64
lines changed

2 files changed

+49
-64
lines changed

python/lsst/meas/algorithms/adaptive_thresholds.py

Lines changed: 44 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626

2727
import 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

3232
from .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

95113
class 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+

tests/test_adaptiveThresholdDetection.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,14 @@ def tearDown(self):
4444
del self.config
4545

4646
def check(self, expectFactor, initialThreshold=None):
47+
if initialThreshold is None:
48+
maxSn = float(np.nanmax(self.exposure.image.array/np.sqrt(self.exposure.variance.array)))
49+
initialThreshold = min(maxSn, 5.0)
4750
schema = SourceTable.makeMinimalSchema()
4851
table = SourceTable.make(schema)
52+
self.config.baseline.thresholdValue = initialThreshold
4953
task = AdaptiveThresholdDetectionTask(config=self.config)
50-
results = task.run(table, self.exposure, initialThreshold=initialThreshold)
54+
results = task.run(table, self.exposure)
5155
self.assertFloatsAlmostEqual(results.thresholdValue, expectFactor, rtol=self.rtol)
5256

5357
def testUncrowded(self):

0 commit comments

Comments
 (0)