LSST Applications g070148d5b3+33e5256705,g0d53e28543+25c8b88941,g0da5cf3356+2dd1178308,g1081da9e2a+62d12e78cb,g17e5ecfddb+7e422d6136,g1c76d35bf8+ede3a706f7,g295839609d+225697d880,g2e2c1a68ba+cc1f6f037e,g2ffcdf413f+853cd4dcde,g38293774b4+62d12e78cb,g3b44f30a73+d953f1ac34,g48ccf36440+885b902d19,g4b2f1765b6+7dedbde6d2,g5320a0a9f6+0c5d6105b6,g56b687f8c9+ede3a706f7,g5c4744a4d9+ef6ac23297,g5ffd174ac0+0c5d6105b6,g6075d09f38+66af417445,g667d525e37+2ced63db88,g670421136f+2ced63db88,g71f27ac40c+2ced63db88,g774830318a+463cbe8d1f,g7876bc68e5+1d137996f1,g7985c39107+62d12e78cb,g7fdac2220c+0fd8241c05,g96f01af41f+368e6903a7,g9ca82378b8+2ced63db88,g9d27549199+ef6ac23297,gabe93b2c52+e3573e3735,gb065e2a02a+3dfbe639da,gbc3249ced9+0c5d6105b6,gbec6a3398f+0c5d6105b6,gc9534b9d65+35b9f25267,gd01420fc67+0c5d6105b6,geee7ff78d7+a14128c129,gf63283c776+ede3a706f7,gfed783d017+0c5d6105b6,w.2022.47
LSST Data Management Base Package
Loading...
Searching...
No Matches
dynamicDetection.py
Go to the documentation of this file.
2__all__ = ["DynamicDetectionConfig", "DynamicDetectionTask"]
3
4import numpy as np
5
6from lsst.pex.config import Field, ConfigurableField
7from lsst.pipe.base import Struct
8
9from .detection import SourceDetectionConfig, SourceDetectionTask
10from .skyObjects import SkyObjectsTask
11
12from lsst.afw.detection import FootprintSet
13from lsst.afw.geom import makeCdMatrix, makeSkyWcs
14from lsst.afw.table import SourceCatalog, SourceTable
15from lsst.meas.base import ForcedMeasurementTask
16
17import lsst.afw.image
18import lsst.afw.math
19import lsst.geom as geom
20
21
23 """Configuration for DynamicDetectionTask
24 """
25 prelimThresholdFactor = Field(dtype=float, default=0.5,
26 doc="Fraction of the threshold to use for first pass (to find sky objects)")
27 skyObjects = ConfigurableField(target=SkyObjectsTask, doc="Generate 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.")
33
34 def setDefaults(self):
35 SourceDetectionConfig.setDefaults(self)
36 self.skyObjects.nSources = 1000 # For good statistics
37
38
40 """Detection of sources on an image with a dynamic threshold
41
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.
48
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.
53 """
54 ConfigClass = DynamicDetectionConfig
55 _DefaultName = "dynamicDetection"
56
57 def __init__(self, *args, **kwargs):
58
59 SourceDetectionTask.__init__(self, *args, **kwargs)
60 self.makeSubtask("skyObjects")
61
62 # Set up forced measurement.
63 config = ForcedMeasurementTask.ConfigClass()
64 config.plugins.names = ['base_TransformedCentroid', 'base_PsfFlux', 'base_LocalBackground']
65 # We'll need the "centroid" and "psfFlux" slots
66 for slot in ("shape", "psfShape", "apFlux", "modelFlux", "gaussianFlux", "calibFlux"):
67 setattr(config.slots, slot, None)
68 config.copyColumns = {}
69 self.skySchema = SourceTable.makeMinimalSchema()
70 self.skyMeasurement = ForcedMeasurementTask(config=config, name="skyMeasurement", parentTask=self,
71 refSchema=self.skySchema)
72
73 def calculateThreshold(self, exposure, seed, sigma=None):
74 """Calculate new threshold
75
76 This is the main functional addition to the vanilla
77 `SourceDetectionTask`.
78
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.
83
84 Parameters
85 ----------
86 exposureOrig : `lsst.afw.image.Exposure`
87 Exposure on which we're detecting sources.
88 seed : `int`
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.
93
94 Returns
95 -------
96 result : `lsst.pipe.base.Struct`
97 Result struct with components:
98
99 ``multiplicative``
100 Multiplicative factor to be applied to the
101 configured detection threshold (`float`).
102 ``additive``
103 Additive factor to be applied to the background
104 level (`float`).
105 """
106 wcsIsNone = exposure.getWcs() is None
107 if wcsIsNone: # create a dummy WCS as needed by ForcedMeasurementTask
108 self.log.info("WCS for exposure is None. Setting a dummy WCS for dynamic detection.")
109 exposure.setWcs(makeSkyWcs(crpix=geom.Point2D(0, 0),
110 crval=geom.SpherePoint(0, 0, geom.degrees),
111 cdMatrix=makeCdMatrix(scale=1e-5*geom.degrees)))
112 fp = self.skyObjects.run(exposure.maskedImage.mask, seed)
113 skyFootprints = FootprintSet(exposure.getBBox())
114 skyFootprints.setFootprints(fp)
115 table = SourceTable.make(self.skyMeasurement.schema)
116 catalog = SourceCatalog(table)
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())
125
126 # Forced photometry on sky objects
127 self.skyMeasurement.run(catalog, exposure, catalog, exposure.getWcs())
128
129 # Calculate new threshold
130 fluxes = catalog["base_PsfFlux_instFlux"]
131 area = catalog["base_PsfFlux_area"]
132 bg = catalog["base_LocalBackground_instFlux"]
133
134 good = (~catalog["base_PsfFlux_flag"] & ~catalog["base_LocalBackground_flag"]
135 & np.isfinite(fluxes) & np.isfinite(area) & np.isfinite(bg))
136
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)
141
142 bgMedian = np.median((fluxes/area)[good])
143
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])
147 if wcsIsNone:
148 exposure.setWcs(None)
149 return Struct(multiplicative=medianError/stdevMeas, additive=bgMedian)
150
151 def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None):
152 """Detect footprints with a dynamic threshold
153
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
157 threshold.
158
159 Parameters
160 ----------
161 exposure : `lsst.afw.image.Exposure`
162 Exposure to process; DETECTED{,_NEGATIVE} mask plane will be
163 set in-place.
164 doSmooth : `bool`, optional
165 If True, smooth the image before detection using a Gaussian
166 of width ``sigma``.
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
170 ``exposure``.
171 clearMask : `bool`, optional
172 Clear both DETECTED and DETECTED_NEGATIVE planes before running
173 detection.
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.
177
178 Returns
179 -------
180 resutls : `lsst.pipe.base.Struct`
181 The results `~lsst.pipe.base.Struct` contains:
182
183 ``positive``
184 Positive polarity footprints.
186 ``negative``
187 Negative polarity footprints.
189 ``numPos``
190 Number of footprints in positive or 0 if detection polarity was
191 negative. (`int`)
192 ``numNeg``
193 Number of footprints in negative or 0 if detection polarity was
194 positive. (`int`)
195 ``background``
196 Re-estimated background. `None` if
197 ``reEstimateBackground==False``.
199 ``factor``
200 Multiplication factor applied to the configured detection
201 threshold. (`float`)
202 ``prelim``
203 Results from preliminary detection pass.
204 (`lsst.pipe.base.Struct`)
205 """
206 maskedImage = exposure.maskedImage
207
208 if clearMask:
209 self.clearMask(maskedImage.mask)
210 else:
211 oldDetected = maskedImage.mask.array & maskedImage.mask.getPlaneBitMask(["DETECTED",
212 "DETECTED_NEGATIVE"])
213
214 with self.tempWideBackgroundContext(exposure):
215 # Could potentially smooth with a wider kernel than the PSF in
216 # order to better pick up the wings of stars and galaxies, but for
217 # now sticking with the PSF as that's more simple.
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)
224
225 # Calculate the proper threshold
226 # seed needs to fit in a C++ 'int' so pybind doesn't choke on it
227 seed = (expId if expId is not None else int(maskedImage.image.array.sum())) % (2**31 - 1)
228 threshResults = self.calculateThreshold(exposure, seed, sigma=sigma)
229 factor = threshResults.multiplicative
230 self.log.info("Modifying configured detection threshold by factor %f to %f",
231 factor, factor*self.config.thresholdValue)
232
233 # Blow away preliminary (low threshold) detection mask
234 self.clearMask(maskedImage.mask)
235 if not clearMask:
236 maskedImage.mask.array |= oldDetected
237
238 # Rinse and repeat thresholding with new calculated threshold
239 results = self.applyThreshold(middle, maskedImage.getBBox(), factor)
240 results.prelim = prelim
241 results.background = lsst.afw.math.BackgroundList()
242 if self.config.doTempLocalBackground:
243 self.applyTempLocalBackground(exposure, middle, results)
244 self.finalizeFootprints(maskedImage.mask, results, sigma, factor)
245
246 self.clearUnwantedResults(maskedImage.mask, results)
247
248 if self.config.reEstimateBackground:
249 self.reEstimateBackground(maskedImage, results.background)
250
251 self.display(exposure, results, middle)
252
253 if self.config.doBackgroundTweak:
254 # Re-do the background tweak after any temporary backgrounds have
255 # been restored.
256 #
257 # But we want to keep any large-scale background (e.g., scattered
258 # light from bright stars) from being selected for sky objects in
259 # the calculation, so do another detection pass without either the
260 # local or wide temporary background subtraction; the DETECTED
261 # pixels will mark the area to ignore.
262 originalMask = maskedImage.mask.array.copy()
263 try:
264 self.clearMask(exposure.mask)
265 convolveResults = self.convolveImage(maskedImage, psf, doSmooth=doSmooth)
266 tweakDetResults = self.applyThreshold(convolveResults.middle, maskedImage.getBBox(), factor)
267 self.finalizeFootprints(maskedImage.mask, tweakDetResults, sigma, factor)
268 bgLevel = self.calculateThreshold(exposure, seed, sigma=sigma).additive
269 finally:
270 maskedImage.mask.array[:] = originalMask
271 self.tweakBackground(exposure, bgLevel, results.background)
272
273 return results
274
275 def tweakBackground(self, exposure, bgLevel, bgList=None):
276 """Modify the background by a constant value
277
278 Parameters
279 ----------
280 exposure : `lsst.afw.image.Exposure`
281 Exposure for which to tweak background.
282 bgLevel : `float`
283 Background level to remove
284 bgList : `lsst.afw.math.BackgroundList`, optional
285 List of backgrounds to append to.
286
287 Returns
288 -------
290 Constant background model.
291 """
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)
296 bg = lsst.afw.math.BackgroundMI(exposure.getBBox(), bgStats)
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)
301 return bg
A set of Footprints, associated with a MaskedImage.
Definition: FootprintSet.h:55
A class to contain the data, WCS, and other information needed to describe an image of the sky.
Definition: Exposure.h:72
A class to evaluate image background levels.
Definition: Background.h:434
Point in an unspecified spherical coordinate system.
Definition: SpherePoint.h:57
def getPsf(self, exposure, sigma=None)
Definition: detection.py:394
def convolveImage(self, maskedImage, psf, doSmooth=True)
Definition: detection.py:421
def applyTempLocalBackground(self, exposure, middle, results)
Definition: detection.py:325
def reEstimateBackground(self, maskedImage, backgrounds)
Definition: detection.py:603
def finalizeFootprints(self, mask, results, sigma, factor=1.0)
Definition: detection.py:541
def display(self, exposure, results, convolvedImage=None)
Definition: detection.py:268
def applyThreshold(self, middle, bbox, factor=1.0)
Definition: detection.py:485
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)