2__all__ = [
"DynamicDetectionConfig",
"DynamicDetectionTask"]
7from lsst.pipe.base
import Struct
9from .detection
import SourceDetectionConfig, SourceDetectionTask
10from .skyObjects
import SkyObjectsTask
21 """Configuration for DynamicDetectionTask
23 prelimThresholdFactor = Field(dtype=float, default=0.5,
24 doc="Fraction of the threshold to use for first pass (to find sky objects)")
25 skyObjects = ConfigurableField(target=SkyObjectsTask, doc=
"Generate sky objects")
26 doBackgroundTweak = Field(dtype=bool, default=
True,
27 doc=
"Tweak background level so median PSF flux of sky objects is zero?")
28 minNumSources = Field(dtype=int, default=10,
29 doc=
"Minimum number of sky sources in statistical sample; "
30 "if below this number, we refuse to modify the threshold.")
33 SourceDetectionConfig.setDefaults(self)
38 """Detection of sources on an image with a dynamic threshold
40 We first detect sources using a lower threshold than normal (see config
41 parameter ``prelimThresholdFactor``) in order to identify good sky regions
42 (configurable ``skyObjects``). Then we perform forced PSF photometry on
43 those sky regions. Using those PSF flux measurements
and estimated errors,
44 we set the threshold so that the stdev of the measurements matches the
45 median estimated error.
47 Besides the usual initialisation of configurables, we also set up
48 the forced measurement which
is deliberately
not represented
in
49 this Task
's configuration parameters because we're using it
as
50 part of the algorithm
and we don
't want to allow it to be modified.
52 ConfigClass = DynamicDetectionConfig
53 _DefaultName = "dynamicDetection"
57 SourceDetectionTask.__init__(self, *args, **kwargs)
58 self.makeSubtask(
"skyObjects")
61 config = ForcedMeasurementTask.ConfigClass()
62 config.plugins.names = [
'base_TransformedCentroid',
'base_PsfFlux',
'base_LocalBackground']
64 for slot
in (
"shape",
"psfShape",
"apFlux",
"modelFlux",
"gaussianFlux",
"calibFlux"):
65 setattr(config.slots, slot,
None)
66 config.copyColumns = {}
68 self.
skyMeasurement = ForcedMeasurementTask(config=config, name=
"skyMeasurement", parentTask=self,
72 """Calculate new threshold
74 This is the main functional addition to the vanilla
75 `SourceDetectionTask`.
77 We identify sky objects
and perform forced PSF photometry on
78 them. Using those PSF flux measurements
and estimated errors,
79 we set the threshold so that the stdev of the measurements
80 matches the median estimated error.
85 Exposure on which we
're detecting sources.
87 RNG seed to use for finding sky objects.
88 sigma : `float`, optional
89 Gaussian sigma of smoothing kernel;
if not provided,
90 will be deduced
from the exposure
's PSF.
94 result : `lsst.pipe.base.Struct`
95 Result struct with components:
98 Multiplicative factor to be applied to the
99 configured detection threshold (`float`).
101 Additive factor to be applied to the background
105 fp = self.skyObjects.
run(exposure.maskedImage.mask, seed)
107 skyFootprints.setFootprints(fp)
109 catalog = SourceCatalog(table)
110 catalog.reserve(len(skyFootprints.getFootprints()))
111 skyFootprints.makeSources(catalog)
112 key = catalog.getCentroidSlot().getMeasKey()
113 for source
in catalog:
114 peaks = source.getFootprint().getPeaks()
115 assert len(peaks) == 1
116 source.set(key, peaks[0].getF())
117 source.updateCoord(exposure.getWcs())
123 fluxes = catalog[
"base_PsfFlux_instFlux"]
124 area = catalog[
"base_PsfFlux_area"]
125 bg = catalog[
"base_LocalBackground_instFlux"]
127 good = (~catalog[
"base_PsfFlux_flag"] & ~catalog[
"base_LocalBackground_flag"]
128 & np.isfinite(fluxes) & np.isfinite(area) & np.isfinite(bg))
130 if good.sum() < self.config.minNumSources:
131 self.log.warning(
"Insufficient good flux measurements (%d < %d) for dynamic threshold"
132 " calculation", good.sum(), self.config.minNumSources)
133 return Struct(multiplicative=1.0, additive=0.0)
135 bgMedian = np.median((fluxes/area)[good])
137 lq, uq = np.percentile((fluxes - bg*area)[good], [25.0, 75.0])
138 stdevMeas = 0.741*(uq - lq)
139 medianError = np.median(catalog[
"base_PsfFlux_instFluxErr"][good])
140 return Struct(multiplicative=medianError/stdevMeas, additive=bgMedian)
142 def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None):
143 """Detect footprints with a dynamic threshold
145 This varies from the vanilla ``detectFootprints`` method because we
146 do detection twice: one
with a low threshold so that we can find
147 sky uncontaminated by objects, then one more
with the new calculated
153 Exposure to process; DETECTED{,_NEGATIVE} mask plane will be
155 doSmooth : `bool`, optional
156 If
True, smooth the image before detection using a Gaussian
158 sigma : `float`, optional
159 Gaussian Sigma of PSF (pixels); used
for smoothing
and to grow
160 detections;
if `
None` then measure the sigma of the PSF of the
162 clearMask : `bool`, optional
163 Clear both DETECTED
and DETECTED_NEGATIVE planes before running
165 expId : `int`, optional
166 Exposure identifier, used
as a seed
for the random number
167 generator. If absent, the seed will be the sum of the image.
171 resutls : `lsst.pipe.base.Struct`
172 The results `~lsst.pipe.base.Struct` contains:
175 Positive polarity footprints.
178 Negative polarity footprints.
181 Number of footprints
in positive
or 0
if detection polarity was
184 Number of footprints
in negative
or 0
if detection polarity was
187 Re-estimated background. `
None`
if
188 ``reEstimateBackground==
False``.
189 (`lsst.afw.math.BackgroundList`)
191 Multiplication factor applied to the configured detection
194 Results
from preliminary detection
pass.
195 (`lsst.pipe.base.Struct`)
197 maskedImage = exposure.maskedImage
202 oldDetected = maskedImage.mask.array & maskedImage.mask.getPlaneBitMask([
"DETECTED",
203 "DETECTED_NEGATIVE"])
208 psf = self.
getPsf(exposure, sigma=sigma)
209 convolveResults = self.
convolveImage(maskedImage, psf, doSmooth=doSmooth)
210 middle = convolveResults.middle
211 sigma = convolveResults.sigma
212 prelim = self.
applyThreshold(middle, maskedImage.getBBox(), self.config.prelimThresholdFactor)
213 self.
finalizeFootprints(maskedImage.mask, prelim, sigma, self.config.prelimThresholdFactor)
217 seed = (expId
if expId
is not None else int(maskedImage.image.array.sum())) % (2**31 - 1)
219 factor = threshResults.multiplicative
220 self.log.info(
"Modifying configured detection threshold by factor %f to %f",
221 factor, factor*self.config.thresholdValue)
226 maskedImage.mask.array |= oldDetected
229 results = self.
applyThreshold(middle, maskedImage.getBBox(), factor)
230 results.prelim = prelim
231 results.background = lsst.afw.math.BackgroundList()
232 if self.config.doTempLocalBackground:
238 if self.config.reEstimateBackground:
241 self.
display(exposure, results, middle)
243 if self.config.doBackgroundTweak:
250 originalMask = maskedImage.mask.array.copy()
253 convolveResults = self.
convolveImage(maskedImage, psf, doSmooth=doSmooth)
254 tweakDetResults = self.
applyThreshold(convolveResults.middle, maskedImage.getBBox(), factor)
258 maskedImage.mask.array[:] = originalMask
264 """Modify the background by a constant value
269 Exposure for which to tweak background.
271 Background level to remove
272 bgList : `lsst.afw.math.BackgroundList`, optional
273 List of backgrounds to append to.
278 Constant background model.
280 self.log.info("Tweaking background by %f to match sky photometry", bgLevel)
281 exposure.image -= bgLevel
282 bgStats = lsst.afw.image.MaskedImageF(1, 1)
283 bgStats.set(bgLevel, 0, bgLevel)
285 bgData = (bg, lsst.afw.math.Interpolate.LINEAR, lsst.afw.math.REDUCE_INTERP_ORDER,
286 lsst.afw.math.ApproximateControl.UNKNOWN, 0, 0,
False)
287 if bgList
is not None:
288 bgList.append(bgData)
def tempWideBackgroundContext(self, exposure)
def getPsf(self, exposure, sigma=None)
def run(self, table, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None)
def convolveImage(self, maskedImage, psf, doSmooth=True)
def applyTempLocalBackground(self, exposure, middle, results)
def reEstimateBackground(self, maskedImage, backgrounds)
def finalizeFootprints(self, mask, results, sigma, factor=1.0)
def clearUnwantedResults(self, mask, results)
def clearMask(self, mask)
def display(self, exposure, results, convolvedImage=None)
def applyThreshold(self, middle, bbox, factor=1.0)
def __init__(self, *args, **kwargs)
def tweakBackground(self, exposure, bgLevel, bgList=None)
def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None)
def calculateThreshold(self, exposure, seed, sigma=None)