2__all__ = [
"DynamicDetectionConfig",
"DynamicDetectionTask"]
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)")
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 = {}
67 self.
skySchemaskySchema = SourceTable.makeMinimalSchema()
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:
97 - ``multiplicative``: multiplicative factor to be applied to the
98 configured detection threshold (`float`).
99 - ``additive``: additive factor to be applied to the background
103 fp = self.skyObjects.
run(exposure.maskedImage.mask, seed)
105 skyFootprints.setFootprints(fp)
106 table = SourceTable.make(self.
skyMeasurementskyMeasurement.schema)
108 catalog.reserve(len(skyFootprints.getFootprints()))
109 skyFootprints.makeSources(catalog)
110 key = catalog.getCentroidSlot().getMeasKey()
111 for source
in catalog:
112 peaks = source.getFootprint().getPeaks()
113 assert len(peaks) == 1
114 source.set(key, peaks[0].getF())
115 source.updateCoord(exposure.getWcs())
118 self.
skyMeasurementskyMeasurement.
run(catalog, exposure, catalog, exposure.getWcs())
121 fluxes = catalog[
"base_PsfFlux_instFlux"]
122 area = catalog[
"base_PsfFlux_area"]
123 bg = catalog[
"base_LocalBackground_instFlux"]
125 good = (~catalog[
"base_PsfFlux_flag"] & ~catalog[
"base_LocalBackground_flag"]
126 & np.isfinite(fluxes) & np.isfinite(area) & np.isfinite(bg))
128 if good.sum() < self.config.minNumSources:
129 self.log.
warning(
"Insufficient good flux measurements (%d < %d) for dynamic threshold"
130 " calculation", good.sum(), self.config.minNumSources)
131 return Struct(multiplicative=1.0, additive=0.0)
133 bgMedian = np.median((fluxes/area)[good])
135 lq, uq = np.percentile((fluxes - bg*area)[good], [25.0, 75.0])
136 stdevMeas = 0.741*(uq - lq)
137 medianError = np.median(catalog[
"base_PsfFlux_instFluxErr"][good])
138 return Struct(multiplicative=medianError/stdevMeas, additive=bgMedian)
140 def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None):
141 """Detect footprints with a dynamic threshold
143 This varies from the vanilla ``detectFootprints`` method because we
144 do detection twice: one
with a low threshold so that we can find
145 sky uncontaminated by objects, then one more
with the new calculated
151 Exposure to process; DETECTED{,_NEGATIVE} mask plane will be
153 doSmooth : `bool`, optional
154 If
True, smooth the image before detection using a Gaussian
156 sigma : `float`, optional
157 Gaussian Sigma of PSF (pixels); used
for smoothing
and to grow
158 detections;
if `
None` then measure the sigma of the PSF of the
160 clearMask : `bool`, optional
161 Clear both DETECTED
and DETECTED_NEGATIVE planes before running
163 expId : `int`, optional
164 Exposure identifier, used
as a seed
for the random number
165 generator. If absent, the seed will be the sum of the image.
167 Return Struct contents
168 ----------------------
170 Positive polarity footprints (may be `
None`)
172 Negative polarity footprints (may be `
None`)
174 Number of footprints
in positive
or 0
if detection polarity was
177 Number of footprints
in negative
or 0
if detection polarity was
180 Re-estimated background. `
None`
if
181 ``reEstimateBackground==
False``.
183 Multiplication factor applied to the configured detection
185 prelim : `lsst.pipe.base.Struct`
186 Results
from preliminary detection
pass.
188 maskedImage = exposure.maskedImage
191 self.
clearMaskclearMask(maskedImage.mask)
193 oldDetected = maskedImage.mask.array & maskedImage.mask.getPlaneBitMask([
"DETECTED",
194 "DETECTED_NEGATIVE"])
199 psf = self.
getPsfgetPsf(exposure, sigma=sigma)
200 convolveResults = self.
convolveImageconvolveImage(maskedImage, psf, doSmooth=doSmooth)
201 middle = convolveResults.middle
202 sigma = convolveResults.sigma
203 prelim = self.
applyThresholdapplyThreshold(middle, maskedImage.getBBox(), self.config.prelimThresholdFactor)
204 self.
finalizeFootprintsfinalizeFootprints(maskedImage.mask, prelim, sigma, self.config.prelimThresholdFactor)
208 seed = (expId
if expId
is not None else int(maskedImage.image.array.sum())) % (2**31 - 1)
209 threshResults = self.
calculateThresholdcalculateThreshold(exposure, seed, sigma=sigma)
210 factor = threshResults.multiplicative
211 self.log.
info(
"Modifying configured detection threshold by factor %f to %f",
212 factor, factor*self.config.thresholdValue)
215 self.
clearMaskclearMask(maskedImage.mask)
217 maskedImage.mask.array |= oldDetected
220 results = self.
applyThresholdapplyThreshold(middle, maskedImage.getBBox(), factor)
221 results.prelim = prelim
223 if self.config.doTempLocalBackground:
229 if self.config.reEstimateBackground:
232 self.
displaydisplay(exposure, results, middle)
234 if self.config.doBackgroundTweak:
241 originalMask = maskedImage.mask.array.copy()
244 convolveResults = self.
convolveImageconvolveImage(maskedImage, psf, doSmooth=doSmooth)
245 tweakDetResults = self.
applyThresholdapplyThreshold(convolveResults.middle, maskedImage.getBBox(), factor)
246 self.
finalizeFootprintsfinalizeFootprints(maskedImage.mask, tweakDetResults, sigma, factor)
247 bgLevel = self.
calculateThresholdcalculateThreshold(exposure, seed, sigma=sigma).additive
249 maskedImage.mask.array[:] = originalMask
250 self.
tweakBackgroundtweakBackground(exposure, bgLevel, results.background)
255 """Modify the background by a constant value
260 Exposure for which to tweak background.
262 Background level to remove
264 List of backgrounds to append to.
269 Constant background model.
271 self.log.info("Tweaking background by %f to match sky photometry", bgLevel)
272 exposure.image -= bgLevel
273 bgStats = lsst.afw.image.MaskedImageF(1, 1)
274 bgStats.set(bgLevel, 0, bgLevel)
276 bgData = (bg, lsst.afw.math.Interpolate.LINEAR, lsst.afw.math.REDUCE_INTERP_ORDER,
277 lsst.afw.math.ApproximateControl.UNKNOWN, 0, 0,
False)
278 if bgList
is not None:
279 bgList.append(bgData)
A class to contain the data, WCS, and other information needed to describe an image of the sky.
A class to evaluate image background levels.
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)
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.