2__all__ = [
"DynamicDetectionConfig",
"DynamicDetectionTask"]
9from .detection
import SourceDetectionConfig, SourceDetectionTask
10from .skyObjects
import SkyObjectsTask
23 """Configuration for DynamicDetectionTask
25 prelimThresholdFactor = Field(dtype=float, default=0.5,
26 doc="Fraction of the threshold to use for first pass (to find sky objects)")
28 doBackgroundTweak =
Field(dtype=bool, default=
True,
29 doc=
"Tweak background level so median PSF flux of sky objects is zero?")
30 minNumSources =
Field(dtype=int, default=10,
31 doc=
"Minimum number of sky sources in statistical sample; "
32 "if below this number, we refuse to modify the threshold.")
35 SourceDetectionConfig.setDefaults(self)
40 """Detection of sources on an image with a dynamic threshold
42 We first detect sources using a lower threshold than normal (see config
43 parameter ``prelimThresholdFactor``) in order to identify good sky regions
44 (configurable ``skyObjects``). Then we perform forced PSF photometry on
45 those sky regions. Using those PSF flux measurements
and estimated errors,
46 we set the threshold so that the stdev of the measurements matches the
47 median estimated error.
49 Besides the usual initialisation of configurables, we also set up
50 the forced measurement which
is deliberately
not represented
in
51 this Task
's configuration parameters because we're using it
as
52 part of the algorithm
and we don
't want to allow it to be modified.
54 ConfigClass = DynamicDetectionConfig
55 _DefaultName = "dynamicDetection"
59 SourceDetectionTask.__init__(self, *args, **kwargs)
60 self.makeSubtask(
"skyObjects")
63 config = ForcedMeasurementTask.ConfigClass()
64 config.plugins.names = [
'base_TransformedCentroid',
'base_PsfFlux',
'base_LocalBackground']
66 for slot
in (
"shape",
"psfShape",
"apFlux",
"modelFlux",
"gaussianFlux",
"calibFlux"):
67 setattr(config.slots, slot,
None)
68 config.copyColumns = {}
74 """Calculate new threshold
76 This is the main functional addition to the vanilla
77 `SourceDetectionTask`.
79 We identify sky objects
and perform forced PSF photometry on
80 them. Using those PSF flux measurements
and estimated errors,
81 we set the threshold so that the stdev of the measurements
82 matches the median estimated error.
87 Exposure on which we
're detecting sources.
89 RNG seed to use for finding sky objects.
90 sigma : `float`, optional
91 Gaussian sigma of smoothing kernel;
if not provided,
92 will be deduced
from the exposure
's PSF.
96 result : `lsst.pipe.base.Struct`
97 Result struct with components:
100 Multiplicative factor to be applied to the
101 configured detection threshold (`float`).
103 Additive factor to be applied to the background
106 wcsIsNone = exposure.getWcs() is None
108 self.log.info(
"WCS for exposure is None. Setting a dummy WCS for dynamic detection.")
111 cdMatrix=makeCdMatrix(scale=1e-5*geom.degrees)))
112 fp = self.skyObjects.run(exposure.maskedImage.mask, seed)
114 skyFootprints.setFootprints(fp)
117 catalog.reserve(len(skyFootprints.getFootprints()))
118 skyFootprints.makeSources(catalog)
119 key = catalog.getCentroidSlot().getMeasKey()
120 for source
in catalog:
121 peaks = source.getFootprint().getPeaks()
122 assert len(peaks) == 1
123 source.set(key, peaks[0].getF())
124 source.updateCoord(exposure.getWcs())
127 self.
skyMeasurement.run(catalog, exposure, catalog, exposure.getWcs())
130 fluxes = catalog[
"base_PsfFlux_instFlux"]
131 area = catalog[
"base_PsfFlux_area"]
132 bg = catalog[
"base_LocalBackground_instFlux"]
134 good = (~catalog[
"base_PsfFlux_flag"] & ~catalog[
"base_LocalBackground_flag"]
135 & np.isfinite(fluxes) & np.isfinite(area) & np.isfinite(bg))
137 if good.sum() < self.config.minNumSources:
138 self.log.warning(
"Insufficient good flux measurements (%d < %d) for dynamic threshold"
139 " calculation", good.sum(), self.config.minNumSources)
140 return Struct(multiplicative=1.0, additive=0.0)
142 bgMedian = np.median((fluxes/area)[good])
144 lq, uq = np.percentile((fluxes - bg*area)[good], [25.0, 75.0])
145 stdevMeas = 0.741*(uq - lq)
146 medianError = np.median(catalog[
"base_PsfFlux_instFluxErr"][good])
148 exposure.setWcs(
None)
149 return Struct(multiplicative=medianError/stdevMeas, additive=bgMedian)
151 def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None):
152 """Detect footprints with a dynamic threshold
154 This varies from the vanilla ``detectFootprints`` method because we
155 do detection twice: one
with a low threshold so that we can find
156 sky uncontaminated by objects, then one more
with the new calculated
162 Exposure to process; DETECTED{,_NEGATIVE} mask plane will be
164 doSmooth : `bool`, optional
165 If
True, smooth the image before detection using a Gaussian
167 sigma : `float`, optional
168 Gaussian Sigma of PSF (pixels); used
for smoothing
and to grow
169 detections;
if `
None` then measure the sigma of the PSF of the
171 clearMask : `bool`, optional
172 Clear both DETECTED
and DETECTED_NEGATIVE planes before running
174 expId : `int`, optional
175 Exposure identifier, used
as a seed
for the random number
176 generator. If absent, the seed will be the sum of the image.
180 resutls : `lsst.pipe.base.Struct`
181 The results `~lsst.pipe.base.Struct` contains:
184 Positive polarity footprints.
187 Negative polarity footprints.
190 Number of footprints
in positive
or 0
if detection polarity was
193 Number of footprints
in negative
or 0
if detection polarity was
196 Re-estimated background. `
None`
if
197 ``reEstimateBackground==
False``.
200 Multiplication factor applied to the configured detection
203 Results
from preliminary detection
pass.
204 (`lsst.pipe.base.Struct`)
206 maskedImage = exposure.maskedImage
211 oldDetected = maskedImage.mask.array & maskedImage.mask.getPlaneBitMask([
"DETECTED",
212 "DETECTED_NEGATIVE"])
218 psf = self.
getPsf(exposure, sigma=sigma)
219 convolveResults = self.
convolveImage(maskedImage, psf, doSmooth=doSmooth)
220 middle = convolveResults.middle
221 sigma = convolveResults.sigma
222 prelim = self.
applyThreshold(middle, maskedImage.getBBox(), self.config.prelimThresholdFactor)
223 self.
finalizeFootprints(maskedImage.mask, prelim, sigma, self.config.prelimThresholdFactor)
227 seed = (expId
if expId
is not None else int(maskedImage.image.array.sum())) % (2**31 - 1)
229 factor = threshResults.multiplicative
230 self.log.info(
"Modifying configured detection threshold by factor %f to %f",
231 factor, factor*self.config.thresholdValue)
236 maskedImage.mask.array |= oldDetected
239 results = self.
applyThreshold(middle, maskedImage.getBBox(), factor)
240 results.prelim = prelim
242 if self.config.doTempLocalBackground:
248 if self.config.reEstimateBackground:
251 self.
display(exposure, results, middle)
253 if self.config.doBackgroundTweak:
262 originalMask = maskedImage.mask.array.copy()
265 convolveResults = self.
convolveImage(maskedImage, psf, doSmooth=doSmooth)
266 tweakDetResults = self.
applyThreshold(convolveResults.middle, maskedImage.getBBox(), factor)
270 maskedImage.mask.array[:] = originalMask
276 """Modify the background by a constant value
281 Exposure for which to tweak background.
283 Background level to remove
285 List of backgrounds to append to.
290 Constant background model.
292 self.log.info("Tweaking background by %f to match sky photometry", bgLevel)
293 exposure.image -= bgLevel
294 bgStats = lsst.afw.image.MaskedImageF(1, 1)
295 bgStats.set(bgLevel, 0, bgLevel)
297 bgData = (bg, lsst.afw.math.Interpolate.LINEAR, lsst.afw.math.REDUCE_INTERP_ORDER,
298 lsst.afw.math.ApproximateControl.UNKNOWN, 0, 0,
False)
299 if bgList
is not None:
300 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.
Point in an unspecified spherical coordinate system.
def tempWideBackgroundContext(self, exposure)
def getPsf(self, exposure, sigma=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)