2 __all__ = [
"DynamicDetectionConfig",
"DynamicDetectionTask"]
9 from .detection
import SourceDetectionConfig, SourceDetectionTask
10 from .skyObjects
import SkyObjectsTask
21 """Configuration for DynamicDetectionTask""" 22 prelimThresholdFactor =
Field(dtype=float, default=0.5,
23 doc=
"Fraction of the threshold to use for first pass (to find sky objects)")
25 doBackgroundTweak =
Field(dtype=bool, default=
True,
26 doc=
"Tweak background level so median PSF flux of sky objects is zero?")
27 minNumSources =
Field(dtype=int, default=10,
28 doc=
"Minimum number of sky sources in statistical sample; " 29 "if below this number, we refuse to modify the threshold.")
32 SourceDetectionConfig.setDefaults(self)
37 """Detection of sources on an image with a dynamic threshold 39 We first detect sources using a lower threshold than normal (see config 40 parameter ``prelimThresholdFactor``) in order to identify good sky regions 41 (configurable ``skyObjects``). Then we perform forced PSF photometry on 42 those sky regions. Using those PSF flux measurements and estimated errors, 43 we set the threshold so that the stdev of the measurements matches the 44 median estimated error. 46 ConfigClass = DynamicDetectionConfig
47 _DefaultName =
"dynamicDetection" 52 Besides the usual initialisation of configurables, we also set up 53 the forced measurement which is deliberately not represented in 54 this Task's configuration parameters because we're using it as part 55 of the algorithm and we don't want to allow it to be modified. 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 = {}
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. 84 exposure : `lsst.afw.image.Exposure` 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)
108 catalog.reserve(len(skyFootprints.getFootprints()))
109 skyFootprints.makeSources(catalog)
110 key = catalog.getCentroidKey()
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())
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.
warn(
"Insufficient good flux measurements (%d < %d) for dynamic threshold calculation",
130 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 150 exposure : `lsst.afw.image.Exposure` 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 ---------------------- 169 positive : `lsst.afw.detection.FootprintSet` 170 Positive polarity footprints (may be `None`) 171 negative : `lsst.afw.detection.FootprintSet` 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 179 background : `lsst.afw.math.BackgroundList` 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
193 oldDetected = maskedImage.mask.array & maskedImage.mask.getPlaneBitMask([
"DETECTED",
194 "DETECTED_NEGATIVE"])
199 psf = self.
getPsf(exposure, sigma=sigma)
200 convolveResults = self.
convolveImage(maskedImage, psf, doSmooth=doSmooth)
201 middle = convolveResults.middle
202 sigma = convolveResults.sigma
203 prelim = self.
applyThreshold(middle, maskedImage.getBBox(), self.config.prelimThresholdFactor)
204 self.
finalizeFootprints(maskedImage.mask, prelim, sigma, self.config.prelimThresholdFactor)
208 seed = (expId
if expId
is not None else int(maskedImage.image.array.sum())) % (2**31 - 1)
210 factor = threshResults.multiplicative
211 self.log.
info(
"Modifying configured detection threshold by factor %f to %f",
212 factor, factor*self.config.thresholdValue)
217 maskedImage.mask.array |= oldDetected
220 results = self.
applyThreshold(middle, maskedImage.getBBox(), factor)
221 results.prelim = prelim
223 if self.config.doTempLocalBackground:
229 if self.config.reEstimateBackground:
232 self.
display(exposure, results, middle)
234 if self.config.doBackgroundTweak:
241 originalMask = maskedImage.mask.array.copy()
244 convolveResults = self.
convolveImage(maskedImage, psf, doSmooth=doSmooth)
245 tweakDetResults = self.
applyThreshold(convolveResults.middle, maskedImage.getBBox(), factor)
249 maskedImage.mask.array[:] = originalMask
255 """Modify the background by a constant value 259 exposure : `lsst.afw.image.Exposure` 260 Exposure for which to tweak background. 262 Background level to remove 263 bgList : `lsst.afw.math.BackgroundList`, optional 264 List of backgrounds to append to. 268 bg : `lsst.afw.math.BackgroundMI` 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)
def tweakBackground(self, exposure, bgLevel, bgList=None)
def applyThreshold(self, middle, bbox, factor=1.0)
def tempWideBackgroundContext(self, exposure)
def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None)
def calculateThreshold(self, exposure, seed, sigma=None)
def convolveImage(self, maskedImage, psf, doSmooth=True)
def applyTempLocalBackground(self, exposure, middle, results)
def display(self, exposure, results, convolvedImage=None)
Detect positive and negative sources on an exposure and return a new table.SourceCatalog.
Configuration parameters for the SourceDetectionTask.
def clearUnwantedResults(self, mask, results)
def getPsf(self, exposure, sigma=None)
def finalizeFootprints(self, mask, results, sigma, factor=1.0)
def reEstimateBackground(self, maskedImage, backgrounds)
def run(self, table, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None)
Run source detection and create a SourceCatalog.
def clearMask(self, mask)
A class to evaluate image background levels.
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects...
def __init__(self, args, kwargs)