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?")
30 SourceDetectionConfig.setDefaults(self)
35 """Detection of sources on an image with a dynamic threshold 37 We first detect sources using a lower threshold than normal (see config 38 parameter ``prelimThresholdFactor``) in order to identify good sky regions 39 (configurable ``skyObjects``). Then we perform forced PSF photometry on 40 those sky regions. Using those PSF flux measurements and estimated errors, 41 we set the threshold so that the stdev of the measurements matches the 42 median estimated error. 44 ConfigClass = DynamicDetectionConfig
45 _DefaultName =
"dynamicDetection" 50 Besides the usual initialisation of configurables, we also set up 51 the forced measurement which is deliberately not represented in 52 this Task's configuration parameters because we're using it as part 53 of the algorithm and we don't want to allow it to be modified. 55 SourceDetectionTask.__init__(self, *args, **kwargs)
56 self.makeSubtask(
"skyObjects")
59 config = ForcedMeasurementTask.ConfigClass()
60 config.plugins.names = [
'base_TransformedCentroid',
'base_PsfFlux']
62 for slot
in (
"shape",
"psfShape",
"apFlux",
"modelFlux",
"instFlux",
"calibFlux"):
63 setattr(config.slots, slot,
None)
64 config.copyColumns = {}
66 self.
skyMeasurement = ForcedMeasurementTask(config=config, name=
"skyMeasurement", parentTask=self,
70 """Calculate new threshold 72 This is the main functional addition to the vanilla 73 `SourceDetectionTask`. 75 We identify sky objects and perform forced PSF photometry on 76 them. Using those PSF flux measurements and estimated errors, 77 we set the threshold so that the stdev of the measurements 78 matches the median estimated error. 82 exposure : `lsst.afw.image.Exposure` 83 Exposure on which we're detecting sources. 85 RNG seed to use for finding sky objects. 86 sigma : `float`, optional 87 Gaussian sigma of smoothing kernel; if not provided, 88 will be deduced from the exposure's PSF. 92 result : `lsst.pipe.base.Struct` 93 Result struct with components: 95 - ``multiplicative``: multiplicative factor to be applied to the 96 configured detection threshold (`float`). 97 - ``additive``: additive factor to be applied to the background 101 fp = self.skyObjects.run(exposure.maskedImage.mask, seed)
103 skyFootprints.setFootprints(fp)
105 catalog = SourceCatalog(table)
106 table.preallocate(len(skyFootprints.getFootprints()))
107 skyFootprints.makeSources(catalog)
108 key = catalog.getCentroidKey()
109 for source
in catalog:
110 peaks = source.getFootprint().getPeaks()
111 assert len(peaks) == 1
112 source.set(key, peaks[0].getF())
113 source.updateCoord(exposure.getWcs())
116 self.
skyMeasurement.run(catalog, exposure, catalog, exposure.getWcs())
119 fluxes = catalog[
"base_PsfFlux_flux"]
121 bgMedian = np.median(fluxes/catalog[
"base_PsfFlux_area"])
123 lq, uq = np.percentile(fluxes, [25.0, 75.0])
124 stdevMeas = 0.741*(uq - lq)
125 medianError = np.median(catalog[
"base_PsfFlux_fluxSigma"])
126 return Struct(multiplicative=medianError/stdevMeas, additive=bgMedian)
128 def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None):
129 """Detect footprints with a dynamic threshold 131 This varies from the vanilla ``detectFootprints`` method because we 132 do detection twice: one with a low threshold so that we can find 133 sky uncontaminated by objects, then one more with the new calculated 138 exposure : `lsst.afw.image.Exposure` 139 Exposure to process; DETECTED{,_NEGATIVE} mask plane will be 141 doSmooth : `bool`, optional 142 If True, smooth the image before detection using a Gaussian 144 sigma : `float`, optional 145 Gaussian Sigma of PSF (pixels); used for smoothing and to grow 146 detections; if `None` then measure the sigma of the PSF of the 148 clearMask : `bool`, optional 149 Clear both DETECTED and DETECTED_NEGATIVE planes before running 151 expId : `int`, optional 152 Exposure identifier, used as a seed for the random number 153 generator. If absent, the seed will be the sum of the image. 155 Return Struct contents 156 ---------------------- 157 positive : `lsst.afw.detection.FootprintSet` 158 Positive polarity footprints (may be `None`) 159 negative : `lsst.afw.detection.FootprintSet` 160 Negative polarity footprints (may be `None`) 162 Number of footprints in positive or 0 if detection polarity was 165 Number of footprints in negative or 0 if detection polarity was 167 background : `lsst.afw.math.BackgroundList` 168 Re-estimated background. `None` if 169 ``reEstimateBackground==False``. 171 Multiplication factor applied to the configured detection 173 prelim : `lsst.pipe.base.Struct` 174 Results from preliminary detection pass. 176 maskedImage = exposure.maskedImage
179 self.clearMask(maskedImage.mask)
181 oldDetected = maskedImage.mask.array & maskedImage.mask.getPlaneBitMask([
"DETECTED",
182 "DETECTED_NEGATIVE"])
184 with self.tempWideBackgroundContext(exposure):
187 psf = self.getPsf(exposure, sigma=sigma)
188 convolveResults = self.convolveImage(maskedImage, psf, doSmooth=doSmooth)
189 middle = convolveResults.middle
190 sigma = convolveResults.sigma
192 prelim = self.applyThreshold(middle, maskedImage.getBBox(), self.config.prelimThresholdFactor)
193 self.finalizeFootprints(maskedImage.mask, prelim, sigma, self.config.prelimThresholdFactor)
197 seed = (expId
if expId
is not None else int(maskedImage.image.array.sum())) % (2**31 - 1)
199 factor = threshResults.multiplicative
200 self.log.info(
"Modifying configured detection threshold by factor %f to %f",
201 factor, factor*self.config.thresholdValue)
205 self.clearMask(maskedImage.mask)
207 maskedImage.mask.array |= oldDetected
210 results = self.applyThreshold(middle, maskedImage.getBBox(), factor)
211 results.prelim = prelim
212 results.background = lsst.afw.math.BackgroundList()
213 if self.config.doTempLocalBackground:
214 self.applyTempLocalBackground(exposure, middle, results)
215 self.finalizeFootprints(maskedImage.mask, results, sigma, factor)
217 self.clearUnwantedResults(maskedImage.mask, results)
219 if self.config.reEstimateBackground:
220 self.reEstimateBackground(maskedImage, results.background)
222 self.display(exposure, results, middle)
224 if self.config.doBackgroundTweak:
232 """Modify the background by a constant value 236 exposure : `lsst.afw.image.Exposure` 237 Exposure for which to tweak background. 239 Background level to remove 240 bgList : `lsst.afw.math.BackgroundList`, optional 241 List of backgrounds to append to. 245 bg : `lsst.afw.math.BackgroundMI` 246 Constant background model. 248 self.log.info(
"Tweaking background by %f to match sky photometry", bgLevel)
249 exposure.image -= bgLevel
250 bgStats = lsst.afw.image.MaskedImageF(1, 1)
251 bgStats.set(0, 0, (bgLevel, 0, bgLevel))
253 bgData = (bg, lsst.afw.math.Interpolate.LINEAR, lsst.afw.math.REDUCE_INTERP_ORDER,
254 lsst.afw.math.ApproximateControl.UNKNOWN, 0, 0,
False)
255 if bgList
is not None:
256 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)