Skip to content

Commit bc7a85a

Browse files
committed
Rework adaptive detection into a replacement for regular detection.
1 parent 07d5d00 commit bc7a85a

File tree

2 files changed

+74
-77
lines changed

2 files changed

+74
-77
lines changed

python/lsst/pipe/tasks/calibrateImage.py

Lines changed: 36 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -286,18 +286,14 @@ class CalibrateImageConfig(pipeBase.PipelineTaskConfig, pipelineConnections=Cali
286286
doc="Task to perform intial background subtraction, before first detection pass.",
287287
)
288288
psf_detection = pexConfig.ConfigurableField(
289-
target=lsst.meas.algorithms.SourceDetectionTask,
289+
target=AdaptiveThresholdDetectionTask,
290290
doc="Task to detect sources for PSF determination."
291291
)
292292
do_adaptive_threshold_detection = pexConfig.Field(
293293
dtype=bool,
294294
default=True,
295295
doc="Implement the adaptive detection thresholding approach?",
296296
)
297-
psf_adaptive_threshold_detection = pexConfig.ConfigurableField(
298-
target=AdaptiveThresholdDetectionTask,
299-
doc="Task to adaptively detect sources for PSF determination.",
300-
)
301297
psf_source_measurement = pexConfig.ConfigurableField(
302298
target=lsst.meas.base.SingleFrameMeasurementTask,
303299
doc="Task to measure sources to be used for psf estimation."
@@ -472,13 +468,8 @@ def setDefaults(self):
472468
# thresholdValue * includeThresholdMultiplier. The low thresholdValue
473469
# ensures that the footprints are large enough for the noise replacer
474470
# to mask out faint undetected neighbors that are not to be measured.
475-
self.psf_detection.thresholdValue = 10.0
476-
self.psf_detection.includeThresholdMultiplier = 5.0
477-
# TODO investigation: Probably want False here, but that may require
478-
# tweaking the background spatial scale, to make it small enough to
479-
# prevent extra peaks in the wings of bright objects.
480-
self.psf_detection.doTempLocalBackground = False
481-
self.psf_detection.reEstimateBackground = False
471+
self.psf_detection.baseline.thresholdValue = 10.0
472+
self.psf_detection.baseline.includeThresholdMultiplier = 5.0
482473

483474
# Minimal measurement plugins for PSF determination.
484475
# TODO DM-39203: We can drop GaussianFlux and PsfFlux, if we use
@@ -622,15 +613,15 @@ def validate(self):
622613
"CalibrateImageTask.psf_subtract_background must be configured with "
623614
"doApplyFlatBackgroundRatio=True if do_illumination_correction=True."
624615
)
625-
if self.psf_detection.reEstimateBackground:
616+
if getattr(self.psf_detection, "reEstimateBackground", False):
626617
if not self.psf_detection.doApplyFlatBackgroundRatio:
627618
raise pexConfig.FieldValidationError(
628619
CalibrateImageConfig.psf_detection,
629620
self,
630621
"CalibrateImageTask.psf_detection background must be configured with "
631622
"doApplyFlatBackgroundRatio=True if do_illumination_correction=True."
632623
)
633-
if self.star_detection.reEstimateBackground:
624+
if getattr(self.star_detection, "reEstimateBackground", False):
634625
if not self.star_detection.doApplyFlatBackgroundRatio:
635626
raise pexConfig.FieldValidationError(
636627
CalibrateImageConfig.star_detection,
@@ -643,17 +634,17 @@ def validate(self):
643634
if not os.getenv("SATTLE_URI_BASE"):
644635
raise pexConfig.FieldValidationError(CalibrateImageConfig.run_sattle, self,
645636
"Sattle requested but URI environment variable not set.")
637+
if (
638+
issubclass(self.psf_detection.target, AdaptiveThresholdDetectionTask)
639+
!= self.do_adaptive_threshold_detection
640+
):
641+
raise pexConfig.FieldValidationError(
642+
CalibrateImageConfig.psf_detection, self,
643+
"AdaptiveThresholdDetectionTask must be used for the psf_detection subtask "
644+
"if and only if do_adaptive_threshold_detection is True."
645+
)
646646

647647
if not self.do_adaptive_threshold_detection:
648-
if not self.psf_detection.reEstimateBackground:
649-
raise pexConfig.FieldValidationError(
650-
CalibrateImageConfig.psf_detection,
651-
self,
652-
"If not using the adaptive threshold detection implementation (i.e. "
653-
"config.do_adaptive_threshold_detection = False), CalibrateImageTask.psf_detection "
654-
"background must be configured with "
655-
"reEstimateBackground = True to maintain original behavior."
656-
)
657648
if not self.star_detection.reEstimateBackground:
658649
raise pexConfig.FieldValidationError(
659650
CalibrateImageConfig.psf_detection,
@@ -695,7 +686,6 @@ def __init__(self, initial_stars_schema=None, **kwargs):
695686
afwTable.CoordKey.addErrorFields(self.psf_schema)
696687
self.makeSubtask("psf_detection", schema=self.psf_schema)
697688
self.makeSubtask("psf_source_measurement", schema=self.psf_schema)
698-
self.makeSubtask("psf_adaptive_threshold_detection")
699689
self.makeSubtask("psf_measure_psf", schema=self.psf_schema)
700690
self.makeSubtask("psf_normalized_calibration_flux", schema=self.psf_schema)
701691

@@ -948,11 +938,12 @@ def run(
948938
illumination_correction,
949939
)
950940

951-
result.psf_stars_footprints, result.background, _, adaptiveDetResStruct = self._compute_psf(
941+
psf_detections, result.background, _ = self._compute_psf(
952942
result.exposure,
953943
id_generator,
954944
background_to_photometric_ratio=result.background_to_photometric_ratio,
955945
)
946+
result.psf_stars_footprints = psf_detections.sources
956947
have_fit_psf = True
957948

958949
# Check if all centroids have been flagged. This should happen
@@ -999,7 +990,7 @@ def run(
999990
result.background,
1000991
id_generator,
1001992
background_to_photometric_ratio=result.background_to_photometric_ratio,
1002-
adaptiveDetResStruct=adaptiveDetResStruct,
993+
psf_detections=psf_detections,
1003994
num_astrometry_matches=num_astrometry_matches,
1004995
)
1005996
psf = result.exposure.getPsf()
@@ -1147,8 +1138,10 @@ def _compute_psf(self, exposure, id_generator, background_to_photometric_ratio=N
11471138
11481139
Returns
11491140
-------
1150-
sources : `lsst.afw.table.SourceCatalog`
1151-
Catalog of detected bright sources.
1141+
detections : `lsst.pipe.base.Struct`
1142+
Struct returned by the ``psf_detection`` subtask, with its
1143+
``sources`` attribute further updated by measurement and PSF
1144+
modeling.
11521145
background : `lsst.afw.math.BackgroundList`
11531146
Background that was fit to the exposure during detection.
11541147
cell_set : `lsst.afw.math.SpatialCellSet`
@@ -1191,25 +1184,12 @@ def log_psf(msg, addToMetadata=False):
11911184
self.psf_repair.run(exposure=exposure, keepCRs=True)
11921185

11931186
table = afwTable.SourceTable.make(self.psf_schema, id_generator.make_table_id_factory())
1194-
if not self.config.do_adaptive_threshold_detection:
1195-
adaptiveDetResStruct = None
1196-
# Re-estimate the background during this detection step, so that
1197-
# measurement uses the most accurate background-subtraction.
1198-
detections = self.psf_detection.run(
1199-
table=table,
1200-
exposure=exposure,
1201-
background=background,
1202-
backgroundToPhotometricRatio=background_to_photometric_ratio,
1203-
)
1204-
else:
1205-
initialThreshold = self.config.psf_detection.thresholdValue
1206-
initialThresholdMultiplier = self.config.psf_detection.includeThresholdMultiplier
1207-
adaptiveDetResStruct = self.psf_adaptive_threshold_detection.run(
1208-
table, exposure,
1209-
initialThreshold=initialThreshold,
1210-
initialThresholdMultiplier=initialThresholdMultiplier,
1211-
)
1212-
detections = adaptiveDetResStruct.detections
1187+
detections = self.psf_detection.run(
1188+
table=table,
1189+
exposure=exposure,
1190+
background=background,
1191+
backgroundToPhotometricRatio=background_to_photometric_ratio,
1192+
)
12131193

12141194
self.metadata["initial_psf_positive_footprint_count"] = detections.numPos
12151195
self.metadata["initial_psf_negative_footprint_count"] = detections.numNeg
@@ -1258,7 +1238,7 @@ def log_psf(msg, addToMetadata=False):
12581238

12591239
# PSF is set on exposure; candidates are returned to use for
12601240
# calibration flux normalization and aperture corrections.
1261-
return detections.sources, background, psf_result.cellSet, adaptiveDetResStruct
1241+
return detections, background, psf_result.cellSet
12621242

12631243
def _measure_aperture_correction(self, exposure, bright_sources):
12641244
"""Measure and set the ApCorrMap on the Exposure, using
@@ -1290,8 +1270,8 @@ def _measure_aperture_correction(self, exposure, bright_sources):
12901270

12911271
exposure.info.setApCorrMap(ap_corr_map)
12921272

1293-
def _find_stars(self, exposure, background, id_generator, background_to_photometric_ratio=None,
1294-
adaptiveDetResStruct=None, num_astrometry_matches=None):
1273+
def _find_stars(self, exposure, background, id_generator, background_to_photometric_ratio=None, *,
1274+
psf_detections, num_astrometry_matches):
12951275
"""Detect stars on an exposure that has a PSF model, and measure their
12961276
PSF, circular aperture, compensated gaussian fluxes.
12971277
@@ -1307,6 +1287,10 @@ def _find_stars(self, exposure, background, id_generator, background_to_photomet
13071287
background_to_photometric_ratio : `lsst.afw.image.Image`, optional
13081288
Image to convert photometric-flattened image to
13091289
background-flattened image.
1290+
psf_detections : `lsst.pipe.base.Struct`
1291+
Struct returned from the ``psf_detection`` subtask.
1292+
num_astrometry_matches : `int`
1293+
Number of astrometry matches.
13101294
13111295
Returns
13121296
-------
@@ -1321,14 +1305,14 @@ def _find_stars(self, exposure, background, id_generator, background_to_photomet
13211305
maxAdaptiveDetIter = 8
13221306
adaptiveDetIter = 0
13231307
threshFactor = 0.2
1324-
if adaptiveDetResStruct is not None:
1308+
if self.config.do_adaptive_threshold_detection:
13251309
while inAdaptiveDet and adaptiveDetIter < maxAdaptiveDetIter:
13261310
inAdaptiveDet = False
13271311
adaptiveDetectionConfig = lsst.meas.algorithms.SourceDetectionConfig()
13281312
adaptiveDetectionConfig.reEstimateBackground = False
13291313
adaptiveDetectionConfig.includeThresholdMultiplier = 2.0
13301314
psfThreshold = (
1331-
adaptiveDetResStruct.thresholdValue*adaptiveDetResStruct.includeThresholdMultiplier
1315+
psf_detections.thresholdValue*psf_detections.includeThresholdMultiplier
13321316
)
13331317
adaptiveDetectionConfig.thresholdValue = max(
13341318
self.config.star_detection.thresholdValue,

tests/test_calibrateImage.py

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import lsst.daf.butler.tests as butlerTests
4040
import lsst.geom
4141
import lsst.meas.algorithms
42+
from lsst.meas.algorithms.adaptive_thresholds import AdaptiveThresholdDetectionTask
4243
from lsst.meas.algorithms import testUtils
4344
import lsst.meas.extensions.psfex
4445
import lsst.meas.base
@@ -115,6 +116,14 @@ def setUp(self):
115116

116117
# Test-specific configuration:
117118
self.config = CalibrateImageTask.ConfigClass()
119+
# Maintain original, no adaptive threshold detection, configs values.
120+
self.config.do_adaptive_threshold_detection = False
121+
self.config.psf_detection.retarget(lsst.meas.algorithms.SourceDetectionTask)
122+
self.config.psf_detection.thresholdValue = 10
123+
self.config.psf_detection.includeThresholdMultiplier = 5
124+
self.config.psf_detection.doTempLocalBackground = False
125+
self.config.psf_detection.reEstimateBackground = True
126+
self.config.star_detection.reEstimateBackground = True
118127
# We don't have many sources, so have to fit simpler models.
119128
self.config.psf_detection.background.approxOrderX = 1
120129
self.config.star_detection.background.approxOrderX = 1
@@ -127,10 +136,6 @@ def setUp(self):
127136
self.config.astrometry.sourceSelector["science"].flags.good = []
128137
self.config.astrometry.matcher.numPointsForShape = 3
129138
self.config.run_sattle = False
130-
# Maintain original, no adaptive threshold detection, configs values.
131-
self.config.do_adaptive_threshold_detection = False
132-
self.config.psf_detection.reEstimateBackground = True
133-
self.config.star_detection.reEstimateBackground = True
134139
# ApFlux has more noise than PsfFlux (the latter unrealistically small
135140
# in this test data), so we need to do magnitude rejection at higher
136141
# sigma, otherwise we can lose otherwise good sources.
@@ -241,19 +246,20 @@ def test_run(self):
241246

242247
self._check_run(calibrate, result)
243248

244-
def test_run_adaptive_threshold_deteection(self):
249+
def test_run_adaptive_threshold_detection(self):
245250
"""Test that run() runs with adaptive threshold detection turned on.
246251
"""
247-
config = copy.copy(self.config)
248252
# Set the adaptive threshold detection, config values...
249-
config.do_adaptive_threshold_detection = True
250-
config.psf_adaptive_threshold_detection.minFootprint = 4
251-
config.psf_adaptive_threshold_detection.minIsolated = 4
252-
config.psf_adaptive_threshold_detection.sufficientIsolated = 4
253-
config.psf_detection.reEstimateBackground = False
254-
config.star_detection.reEstimateBackground = False
253+
self.config.do_adaptive_threshold_detection = True
254+
self.config.psf_detection.retarget(AdaptiveThresholdDetectionTask)
255+
self.config.psf_detection.baseline.thresholdValue = 10
256+
self.config.psf_detection.baseline.includeThresholdMultiplier = 5
257+
self.config.psf_detection.minFootprint = 4
258+
self.config.psf_detection.minIsolated = 4
259+
self.config.psf_detection.sufficientIsolated = 4
260+
self.config.star_detection.reEstimateBackground = False
255261

256-
calibrate = CalibrateImageTask(config=config)
262+
calibrate = CalibrateImageTask(config=self.config)
257263
calibrate.astrometry.setRefObjLoader(self.ref_loader)
258264
calibrate.photometry.match.setRefObjLoader(self.ref_loader)
259265
with self.assertLogs("lsst.calibrateImage", level="INFO") as cm:
@@ -341,7 +347,8 @@ def test_compute_psf(self):
341347
that a PSF is assigned to the expopsure.
342348
"""
343349
calibrate = CalibrateImageTask(config=self.config)
344-
psf_stars, background, candidates, _ = calibrate._compute_psf(self.exposure, self.id_generator)
350+
psf_detections, background, _ = calibrate._compute_psf(self.exposure, self.id_generator)
351+
psf_stars = psf_detections.sources
345352

346353
# Catalog ids should be very large from this id generator.
347354
self.assertTrue(all(psf_stars['id'] > 1000000000))
@@ -413,7 +420,8 @@ def test_measure_aperture_correction(self):
413420
exposure.
414421
"""
415422
calibrate = CalibrateImageTask(config=self.config)
416-
psf_stars, background, candidates, _ = calibrate._compute_psf(self.exposure, self.id_generator)
423+
psf_detections, _, _ = calibrate._compute_psf(self.exposure, self.id_generator)
424+
psf_stars = psf_detections.sources
417425

418426
# First check that the exposure doesn't have an ApCorrMap.
419427
self.assertIsNone(self.exposure.apCorrMap)
@@ -428,10 +436,12 @@ def test_find_stars(self):
428436
in the image and returns them in the output catalog.
429437
"""
430438
calibrate = CalibrateImageTask(config=self.config)
431-
psf_stars, background, candidates, _ = calibrate._compute_psf(self.exposure, self.id_generator)
439+
psf_detections, background, _ = calibrate._compute_psf(self.exposure, self.id_generator)
440+
psf_stars = psf_detections.sources
432441
calibrate._measure_aperture_correction(self.exposure, psf_stars)
433442

434-
stars = calibrate._find_stars(self.exposure, background, self.id_generator)
443+
stars = calibrate._find_stars(self.exposure, background, self.id_generator,
444+
psf_detections=psf_detections, num_astrometry_matches=0)
435445

436446
# Catalog ids should be very large from this id generator.
437447
self.assertTrue(all(stars['id'] > 1000000000))
@@ -456,13 +466,13 @@ def test_astrometry(self):
456466
"""
457467
calibrate = CalibrateImageTask(config=self.config)
458468
calibrate.astrometry.setRefObjLoader(self.ref_loader)
459-
psf_stars, background, candidates, _ = calibrate._compute_psf(self.exposure, self.id_generator)
469+
psf_detections, background, _ = calibrate._compute_psf(self.exposure, self.id_generator)
470+
psf_stars = psf_detections.sources
460471
calibrate._measure_aperture_correction(self.exposure, psf_stars)
461472
calibrate._fit_astrometry(self.exposure, psf_stars)
462473

463474
# Check that we got reliable matches with the truth coordinates.
464-
sky = psf_stars["sky_source"]
465-
fitted = SkyCoord(psf_stars[~sky]['coord_ra'], psf_stars[~sky]['coord_dec'], unit="radian")
475+
fitted = SkyCoord(psf_stars['coord_ra'], psf_stars['coord_dec'], unit="radian")
466476
truth = SkyCoord(self.truth_cat['coord_ra'], self.truth_cat['coord_dec'], unit="radian")
467477
idx, d2d, _ = fitted.match_to_catalog_sky(truth)
468478
np.testing.assert_array_less(d2d.to_value(u.milliarcsecond), 35.0)
@@ -474,9 +484,11 @@ def test_photometry(self):
474484
calibrate = CalibrateImageTask(config=self.config)
475485
calibrate.astrometry.setRefObjLoader(self.ref_loader)
476486
calibrate.photometry.match.setRefObjLoader(self.ref_loader)
477-
psf_stars, background, candidates, _ = calibrate._compute_psf(self.exposure, self.id_generator)
487+
psf_detections, background, _ = calibrate._compute_psf(self.exposure, self.id_generator)
488+
psf_stars = psf_detections.sources
478489
calibrate._measure_aperture_correction(self.exposure, psf_stars)
479-
stars = calibrate._find_stars(self.exposure, background, self.id_generator)
490+
stars = calibrate._find_stars(self.exposure, background, self.id_generator,
491+
psf_detections=psf_detections, num_astrometry_matches=0)
480492
calibrate._fit_astrometry(self.exposure, stars)
481493

482494
stars, matches, meta, photoCalib = calibrate._fit_photometry(self.exposure, stars)
@@ -515,9 +527,11 @@ def test_match_psf_stars(self):
515527
and candidates.
516528
"""
517529
calibrate = CalibrateImageTask(config=self.config)
518-
psf_stars, background, candidates, _ = calibrate._compute_psf(self.exposure, self.id_generator)
530+
psf_detections, background, _ = calibrate._compute_psf(self.exposure, self.id_generator)
531+
psf_stars = psf_detections.sources
519532
calibrate._measure_aperture_correction(self.exposure, psf_stars)
520-
stars = calibrate._find_stars(self.exposure, background, self.id_generator)
533+
stars = calibrate._find_stars(self.exposure, background, self.id_generator,
534+
psf_detections=psf_detections, num_astrometry_matches=0)
521535

522536
# There should be no psf-related flags set at first.
523537
self.assertEqual(stars["calib_psf_candidate"].sum(), 0)
@@ -829,7 +843,6 @@ def test_runQuantum_illumination_correction(self):
829843
config = CalibrateImageTask.ConfigClass()
830844
config.do_illumination_correction = True
831845
config.psf_subtract_background.doApplyFlatBackgroundRatio = True
832-
config.psf_detection.doApplyFlatBackgroundRatio = True
833846
config.star_detection.doApplyFlatBackgroundRatio = True
834847
task = CalibrateImageTask(config=config)
835848
lsst.pipe.base.testUtils.assertValidInitOutput(task)

0 commit comments

Comments
 (0)