Skip to content

Commit 228ff66

Browse files
committed
Merge branch 'tickets/DM-48966'
2 parents db1e2cd + 5c954e3 commit 228ff66

File tree

8 files changed

+687
-74
lines changed

8 files changed

+687
-74
lines changed

python/lsst/meas/algorithms/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,7 @@
7272
from .version import *
7373

7474
import lsst.utils
75+
76+
# adaptive_thresholds.py is intentionally not imported and lifted, to
77+
# (belatedly) try to limit how much code is run when just importing
78+
# lsst.meas.algorithm.

python/lsst/meas/algorithms/adaptive_thresholds.py

Lines changed: 330 additions & 0 deletions
Large diffs are not rendered by default.

python/lsst/meas/algorithms/detection.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -600,7 +600,7 @@ def applyThreshold(self, middle, bbox, factor=1.0, factorNeg=None):
600600

601601
return results
602602

603-
def finalizeFootprints(self, mask, results, sigma, factor=1.0, factorNeg=None):
603+
def finalizeFootprints(self, mask, results, sigma, factor=1.0, factorNeg=None, growOverride=None):
604604
"""Finalize the detected footprints.
605605
606606
Grow the footprints, set the ``DETECTED`` and ``DETECTED_NEGATIVE``
@@ -629,6 +629,10 @@ def finalizeFootprints(self, mask, results, sigma, factor=1.0, factorNeg=None):
629629
for positive detection polarity) is assumed. Note that this is only
630630
used here for logging purposes.
631631
"""
632+
if growOverride is not None:
633+
self.log.warning("config.nSigmaToGrow is set to %.2f, but the caller has set "
634+
"growOverride to %.2f, so the footprints will be grown by "
635+
"%.2f sigma.", self.config.nSigmaToGrow, growOverride, growOverride)
632636
factorNeg = factor if factorNeg is None else factorNeg
633637
for polarity, maskName in (("positive", "DETECTED"), ("negative", "DETECTED_NEGATIVE")):
634638
fpSet = getattr(results, polarity)

python/lsst/meas/algorithms/dynamicDetection.py

Lines changed: 91 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11

2-
__all__ = ["DynamicDetectionConfig", "DynamicDetectionTask", "InsufficientSourcesError"]
2+
__all__ = [
3+
"DynamicDetectionConfig",
4+
"DynamicDetectionTask",
5+
"InsufficientSourcesError",
6+
]
37

48
import numpy as np
59

610
from lsst.pex.config import Field, ConfigurableField
7-
from lsst.pipe.base import Struct
811

912
from .detection import SourceDetectionConfig, SourceDetectionTask
1013
from .skyObjects import SkyObjectsTask
@@ -13,6 +16,7 @@
1316
from lsst.afw.geom import makeCdMatrix, makeSkyWcs, SpanSet
1417
from lsst.afw.table import SourceCatalog, SourceTable
1518
from lsst.meas.base import ForcedMeasurementTask
19+
from lsst.pipe.base import Struct
1620

1721
import lsst.afw.image
1822
import 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

Comments
 (0)