1 from __future__
import absolute_import, division, print_function
3 __all__ = [
"DynamicDetectionConfig",
"DynamicDetectionTask"]
10 from .detection
import SourceDetectionConfig, SourceDetectionTask
11 from .skyObjects
import SkyObjectsTask
22 """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 ConfigClass = DynamicDetectionConfig
48 _DefaultName =
"dynamicDetection" 53 Besides the usual initialisation of configurables, we also set up 54 the forced measurement which is deliberately not represented in 55 this Task's configuration parameters because we're using it as part 56 of the algorithm and we don't want to allow it to be modified. 58 SourceDetectionTask.__init__(self, *args, **kwargs)
59 self.makeSubtask(
"skyObjects")
62 config = ForcedMeasurementTask.ConfigClass()
63 config.plugins.names = [
'base_TransformedCentroid',
'base_PsfFlux']
65 for slot
in (
"shape",
"psfShape",
"apFlux",
"modelFlux",
"instFlux",
"calibFlux"):
66 setattr(config.slots, slot,
None)
67 config.copyColumns = {}
69 self.
skyMeasurement = ForcedMeasurementTask(config=config, name=
"skyMeasurement", parentTask=self,
73 """Calculate new threshold 75 This is the main functional addition to the vanilla 76 `SourceDetectionTask`. 78 We identify sky objects and perform forced PSF photometry on 79 them. Using those PSF flux measurements and estimated errors, 80 we set the threshold so that the stdev of the measurements 81 matches the median estimated error. 85 exposure : `lsst.afw.image.Exposure` 86 Exposure on which we're detecting sources. 88 RNG seed to use for finding sky objects. 89 sigma : `float`, optional 90 Gaussian sigma of smoothing kernel; if not provided, 91 will be deduced from the exposure's PSF. 95 result : `lsst.pipe.base.Struct` 96 Result struct with components: 98 - ``multiplicative``: multiplicative factor to be applied to the 99 configured detection threshold (`float`). 100 - ``additive``: additive factor to be applied to the background 104 fp = self.skyObjects.run(exposure.maskedImage.mask, seed)
106 skyFootprints.setFootprints(fp)
108 catalog = SourceCatalog(table)
109 table.preallocate(len(skyFootprints.getFootprints()))
110 skyFootprints.makeSources(catalog)
111 key = catalog.getCentroidKey()
112 for source
in catalog:
113 peaks = source.getFootprint().getPeaks()
114 assert len(peaks) == 1
115 source.set(key, peaks[0].getF())
116 source.updateCoord(exposure.getWcs())
119 self.
skyMeasurement.run(catalog, exposure, catalog, exposure.getWcs())
122 fluxes = catalog[
"base_PsfFlux_flux"]
123 good = ~catalog[
"base_PsfFlux_flag"]
125 if good.sum() < self.config.minNumSources:
126 self.log.warn(
"Insufficient good flux measurements (%d < %d) for dynamic threshold calculation",
127 good.sum(), self.config.minNumSources)
128 return Struct(multiplicative=1.0, additive=0.0)
130 bgMedian = np.median(fluxes/catalog[
"base_PsfFlux_area"])
132 lq, uq = np.percentile(fluxes, [25.0, 75.0])
133 stdevMeas = 0.741*(uq - lq)
134 medianError = np.median(catalog[
"base_PsfFlux_fluxSigma"])
135 return Struct(multiplicative=medianError/stdevMeas, additive=bgMedian)
137 def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None):
138 """Detect footprints with a dynamic threshold 140 This varies from the vanilla ``detectFootprints`` method because we 141 do detection twice: one with a low threshold so that we can find 142 sky uncontaminated by objects, then one more with the new calculated 147 exposure : `lsst.afw.image.Exposure` 148 Exposure to process; DETECTED{,_NEGATIVE} mask plane will be 150 doSmooth : `bool`, optional 151 If True, smooth the image before detection using a Gaussian 153 sigma : `float`, optional 154 Gaussian Sigma of PSF (pixels); used for smoothing and to grow 155 detections; if `None` then measure the sigma of the PSF of the 157 clearMask : `bool`, optional 158 Clear both DETECTED and DETECTED_NEGATIVE planes before running 160 expId : `int`, optional 161 Exposure identifier, used as a seed for the random number 162 generator. If absent, the seed will be the sum of the image. 164 Return Struct contents 165 ---------------------- 166 positive : `lsst.afw.detection.FootprintSet` 167 Positive polarity footprints (may be `None`) 168 negative : `lsst.afw.detection.FootprintSet` 169 Negative polarity footprints (may be `None`) 171 Number of footprints in positive or 0 if detection polarity was 174 Number of footprints in negative or 0 if detection polarity was 176 background : `lsst.afw.math.BackgroundList` 177 Re-estimated background. `None` if 178 ``reEstimateBackground==False``. 180 Multiplication factor applied to the configured detection 182 prelim : `lsst.pipe.base.Struct` 183 Results from preliminary detection pass. 185 maskedImage = exposure.maskedImage
188 self.clearMask(maskedImage.mask)
190 oldDetected = maskedImage.mask.array & maskedImage.mask.getPlaneBitMask([
"DETECTED",
191 "DETECTED_NEGATIVE"])
193 with self.tempWideBackgroundContext(exposure):
196 psf = self.getPsf(exposure, sigma=sigma)
197 convolveResults = self.convolveImage(maskedImage, psf, doSmooth=doSmooth)
198 middle = convolveResults.middle
199 sigma = convolveResults.sigma
201 prelim = self.applyThreshold(middle, maskedImage.getBBox(), self.config.prelimThresholdFactor)
202 self.finalizeFootprints(maskedImage.mask, prelim, sigma, self.config.prelimThresholdFactor)
206 seed = (expId
if expId
is not None else int(maskedImage.image.array.sum())) % (2**31 - 1)
208 factor = threshResults.multiplicative
209 self.log.info(
"Modifying configured detection threshold by factor %f to %f",
210 factor, factor*self.config.thresholdValue)
214 self.clearMask(maskedImage.mask)
216 maskedImage.mask.array |= oldDetected
219 results = self.applyThreshold(middle, maskedImage.getBBox(), factor)
220 results.prelim = prelim
221 results.background = lsst.afw.math.BackgroundList()
222 if self.config.doTempLocalBackground:
223 self.applyTempLocalBackground(exposure, middle, results)
224 self.finalizeFootprints(maskedImage.mask, results, sigma, factor)
226 self.clearUnwantedResults(maskedImage.mask, results)
228 if self.config.reEstimateBackground:
229 self.reEstimateBackground(maskedImage, results.background)
231 self.display(exposure, results, middle)
233 if self.config.doBackgroundTweak:
241 """Modify the background by a constant value 245 exposure : `lsst.afw.image.Exposure` 246 Exposure for which to tweak background. 248 Background level to remove 249 bgList : `lsst.afw.math.BackgroundList`, optional 250 List of backgrounds to append to. 254 bg : `lsst.afw.math.BackgroundMI` 255 Constant background model. 257 self.log.info(
"Tweaking background by %f to match sky photometry", bgLevel)
258 exposure.image -= bgLevel
259 bgStats = lsst.afw.image.MaskedImageF(1, 1)
260 bgStats.set(0, 0, (bgLevel, 0, bgLevel))
262 bgData = (bg, lsst.afw.math.Interpolate.LINEAR, lsst.afw.math.REDUCE_INTERP_ORDER,
263 lsst.afw.math.ApproximateControl.UNKNOWN, 0, 0,
False)
264 if bgList
is not None:
265 bgList.append(bgData)
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)
def __init__(self, args, kwargs)