LSST Applications 26.0.0,g0265f82a02+6660c170cc,g07994bdeae+30b05a742e,g0a0026dc87+17526d298f,g0a60f58ba1+17526d298f,g0e4bf8285c+96dd2c2ea9,g0ecae5effc+c266a536c8,g1e7d6db67d+6f7cb1f4bb,g26482f50c6+6346c0633c,g2bbee38e9b+6660c170cc,g2cc88a2952+0a4e78cd49,g3273194fdb+f6908454ef,g337abbeb29+6660c170cc,g337c41fc51+9a8f8f0815,g37c6e7c3d5+7bbafe9d37,g44018dc512+6660c170cc,g4a941329ef+4f7594a38e,g4c90b7bd52+5145c320d2,g58be5f913a+bea990ba40,g635b316a6c+8d6b3a3e56,g67924a670a+bfead8c487,g6ae5381d9b+81bc2a20b4,g93c4d6e787+26b17396bd,g98cecbdb62+ed2cb6d659,g98ffbb4407+81bc2a20b4,g9ddcbc5298+7f7571301f,ga1e77700b3+99e9273977,gae46bcf261+6660c170cc,gb2715bf1a1+17526d298f,gc86a011abf+17526d298f,gcf0d15dbbd+96dd2c2ea9,gdaeeff99f8+0d8dbea60f,gdb4ec4c597+6660c170cc,ge23793e450+96dd2c2ea9,gf041782ebf+171108ac67
LSST Data Management Base Package
Loading...
Searching...
No Matches
detection.py
Go to the documentation of this file.
2# LSST Data Management System
3#
4# Copyright 2008-2017 AURA/LSST.
5#
6# This product includes software developed by the
7# LSST Project (http://www.lsst.org/).
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the LSST License Statement and
20# the GNU General Public License along with this program. If not,
21# see <https://www.lsstcorp.org/LegalNotices/>.
22#
23
24__all__ = ("SourceDetectionConfig", "SourceDetectionTask", "addExposures")
25
26from contextlib import contextmanager
27
28import numpy as np
29
30import lsst.geom
31import lsst.afw.display as afwDisplay
32import lsst.afw.detection as afwDet
33import lsst.afw.geom as afwGeom
34import lsst.afw.image as afwImage
35import lsst.afw.math as afwMath
36import lsst.afw.table as afwTable
37import lsst.pex.config as pexConfig
38import lsst.pipe.base as pipeBase
39from lsst.utils.timer import timeMethod
40from .subtractBackground import SubtractBackgroundTask
41
42
43class SourceDetectionConfig(pexConfig.Config):
44 """Configuration parameters for the SourceDetectionTask
45 """
46 minPixels = pexConfig.RangeField(
47 doc="detected sources with fewer than the specified number of pixels will be ignored",
48 dtype=int, optional=False, default=1, min=0,
49 )
50 isotropicGrow = pexConfig.Field(
51 doc="Pixels should be grown as isotropically as possible (slower)",
52 dtype=bool, optional=False, default=False,
53 )
54 combinedGrow = pexConfig.Field(
55 doc="Grow all footprints at the same time? This allows disconnected footprints to merge.",
56 dtype=bool, default=True,
57 )
58 nSigmaToGrow = pexConfig.Field(
59 doc="Grow detections by nSigmaToGrow * [PSF RMS width]; if 0 then do not grow",
60 dtype=float, default=2.4, # 2.4 pixels/sigma is roughly one pixel/FWHM
61 )
62 returnOriginalFootprints = pexConfig.Field(
63 doc="Grow detections to set the image mask bits, but return the original (not-grown) footprints",
64 dtype=bool, optional=False, default=False,
65 )
66 thresholdValue = pexConfig.RangeField(
67 doc="Threshold for footprints; exact meaning and units depend on thresholdType.",
68 dtype=float, optional=False, default=5.0, min=0.0,
69 )
70 includeThresholdMultiplier = pexConfig.RangeField(
71 doc="Include threshold relative to thresholdValue",
72 dtype=float, default=1.0, min=0.0,
73 )
74 thresholdType = pexConfig.ChoiceField(
75 doc="specifies the desired flavor of Threshold",
76 dtype=str, optional=False, default="stdev",
77 allowed={
78 "variance": "threshold applied to image variance",
79 "stdev": "threshold applied to image std deviation",
80 "value": "threshold applied to image value",
81 "pixel_stdev": "threshold applied to per-pixel std deviation",
82 },
83 )
84 thresholdPolarity = pexConfig.ChoiceField(
85 doc="specifies whether to detect positive, or negative sources, or both",
86 dtype=str, optional=False, default="positive",
87 allowed={
88 "positive": "detect only positive sources",
89 "negative": "detect only negative sources",
90 "both": "detect both positive and negative sources",
91 },
92 )
93 adjustBackground = pexConfig.Field(
94 dtype=float,
95 doc="Fiddle factor to add to the background; debugging only",
96 default=0.0,
97 )
98 reEstimateBackground = pexConfig.Field(
99 dtype=bool,
100 doc="Estimate the background again after final source detection?",
101 default=True, optional=False,
102 )
103 background = pexConfig.ConfigurableField(
104 doc="Background re-estimation; ignored if reEstimateBackground false",
105 target=SubtractBackgroundTask,
106 )
107 tempLocalBackground = pexConfig.ConfigurableField(
108 doc=("A local (small-scale), temporary background estimation step run between "
109 "detecting above-threshold regions and detecting the peaks within "
110 "them; used to avoid detecting spuerious peaks in the wings."),
111 target=SubtractBackgroundTask,
112 )
113 doTempLocalBackground = pexConfig.Field(
114 dtype=bool,
115 doc="Enable temporary local background subtraction? (see tempLocalBackground)",
116 default=True,
117 )
118 tempWideBackground = pexConfig.ConfigurableField(
119 doc=("A wide (large-scale) background estimation and removal before footprint and peak detection. "
120 "It is added back into the image after detection. The purpose is to suppress very large "
121 "footprints (e.g., from large artifacts) that the deblender may choke on."),
122 target=SubtractBackgroundTask,
123 )
124 doTempWideBackground = pexConfig.Field(
125 dtype=bool,
126 doc="Do temporary wide (large-scale) background subtraction before footprint detection?",
127 default=False,
128 )
129 nPeaksMaxSimple = pexConfig.Field(
130 dtype=int,
131 doc=("The maximum number of peaks in a Footprint before trying to "
132 "replace its peaks using the temporary local background"),
133 default=1,
134 )
135 nSigmaForKernel = pexConfig.Field(
136 dtype=float,
137 doc=("Multiple of PSF RMS size to use for convolution kernel bounding box size; "
138 "note that this is not a half-size. The size will be rounded up to the nearest odd integer"),
139 default=7.0,
140 )
141 statsMask = pexConfig.ListField(
142 dtype=str,
143 doc="Mask planes to ignore when calculating statistics of image (for thresholdType=stdev)",
144 default=['BAD', 'SAT', 'EDGE', 'NO_DATA'],
145 )
146 excludeMaskPlanes = lsst.pex.config.ListField(
147 dtype=str,
148 default=[],
149 doc="Mask planes to exclude when detecting sources."
150 )
151
152 def setDefaults(self):
153 self.tempLocalBackground.binSize = 64
154 self.tempLocalBackground.algorithm = "AKIMA_SPLINE"
155 self.tempLocalBackground.useApprox = False
156 # Background subtraction to remove a large-scale background (e.g., scattered light); restored later.
157 # Want to keep it from exceeding the deblender size limit of 1 Mpix, so half that is reasonable.
158 self.tempWideBackground.binSize = 512
159 self.tempWideBackground.algorithm = "AKIMA_SPLINE"
160 self.tempWideBackground.useApprox = False
161 # Ensure we can remove even bright scattered light that is DETECTED
162 for maskPlane in ("DETECTED", "DETECTED_NEGATIVE"):
163 if maskPlane in self.tempWideBackground.ignoredPixelMask:
164 self.tempWideBackground.ignoredPixelMask.remove(maskPlane)
165
166
167class SourceDetectionTask(pipeBase.Task):
168 """Detect peaks and footprints of sources in an image.
169
170 This task convolves the image with a Gaussian approximation to the PSF,
171 matched to the sigma of the input exposure, because this is separable and
172 fast. The PSF would have to be very non-Gaussian or non-circular for this
173 approximation to have a significant impact on the signal-to-noise of the
174 detected sources.
175
176 Parameters
177 ----------
178 schema : `lsst.afw.table.Schema`
179 Schema object used to create the output `lsst.afw.table.SourceCatalog`
180 **kwds
181 Keyword arguments passed to `lsst.pipe.base.Task.__init__`
182
183 If schema is not None and configured for 'both' detections,
184 a 'flags.negative' field will be added to label detections made with a
185 negative threshold.
186
187 Notes
188 -----
189 This task can add fields to the schema, so any code calling this task must
190 ensure that these columns are indeed present in the input match list.
191 """
192 ConfigClass = SourceDetectionConfig
193 _DefaultName = "sourceDetection"
194
195 def __init__(self, schema=None, **kwds):
196 pipeBase.Task.__init__(self, **kwds)
197 if schema is not None and self.config.thresholdPolarity == "both":
198 self.negativeFlagKey = schema.addField(
199 "flags_negative", type="Flag",
200 doc="set if source was detected as significantly negative"
201 )
202 else:
203 if self.config.thresholdPolarity == "both":
204 self.log.warning("Detection polarity set to 'both', but no flag will be "
205 "set to distinguish between positive and negative detections")
206 self.negativeFlagKey = None
207 if self.config.reEstimateBackground:
208 self.makeSubtask("background")
209 if self.config.doTempLocalBackground:
210 self.makeSubtask("tempLocalBackground")
211 if self.config.doTempWideBackground:
212 self.makeSubtask("tempWideBackground")
213
214 @timeMethod
215 def run(self, table, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None,
216 background=None):
217 r"""Detect sources and return catalog(s) of detections.
218
219 Parameters
220 ----------
222 Table object that will be used to create the SourceCatalog.
223 exposure : `lsst.afw.image.Exposure`
224 Exposure to process; DETECTED mask plane will be set in-place.
225 doSmooth : `bool`, optional
226 If True, smooth the image before detection using a Gaussian of width
227 ``sigma``, or the measured PSF width. Set to False when running on
228 e.g. a pre-convolved image, or a mask plane.
229 sigma : `float`, optional
230 Sigma of PSF (pixels); used for smoothing and to grow detections;
231 if None then measure the sigma of the PSF of the exposure
232 clearMask : `bool`, optional
233 Clear DETECTED{,_NEGATIVE} planes before running detection.
234 expId : `int`, optional
235 Exposure identifier; unused by this implementation, but used for
236 RNG seed by subclasses.
237 background : `lsst.afw.math.BackgroundList`, optional
238 Background that was already subtracted from the exposure; will be
239 modified in-place if ``reEstimateBackground=True``.
240
241 Returns
242 -------
243 result : `lsst.pipe.base.Struct`
244 The `~lsst.pipe.base.Struct` contains:
245
246 ``sources``
247 Detected sources on the exposure.
249 ``positive``
250 Positive polarity footprints.
252 ``negative``
253 Negative polarity footprints.
255 ``numPos``
256 Number of footprints in positive or 0 if detection polarity was
257 negative. (`int`)
258 ``numNeg``
259 Number of footprints in negative or 0 if detection polarity was
260 positive. (`int`)
261 ``background``
262 Re-estimated background. `None` if
263 ``reEstimateBackground==False``.
265 ``factor``
266 Multiplication factor applied to the configured detection
267 threshold. (`float`)
268
269 Raises
270 ------
271 ValueError
272 Raised if flags.negative is needed, but isn't in table's schema.
273 lsst.pipe.base.TaskError
274 Raised if sigma=None, doSmooth=True and the exposure has no PSF.
275
276 Notes
277 -----
278 If you want to avoid dealing with Sources and Tables, you can use
279 `detectFootprints()` to just get the
281 """
282 if self.negativeFlagKey is not None and self.negativeFlagKey not in table.getSchema():
283 raise ValueError("Table has incorrect Schema")
284 results = self.detectFootprints(exposure=exposure, doSmooth=doSmooth, sigma=sigma,
285 clearMask=clearMask, expId=expId, background=background)
286 sources = afwTable.SourceCatalog(table)
287 sources.reserve(results.numPos + results.numNeg)
288 if results.negative:
289 results.negative.makeSources(sources)
290 if self.negativeFlagKey:
291 for record in sources:
292 record.set(self.negativeFlagKey, True)
293 if results.positive:
294 results.positive.makeSources(sources)
295 results.sources = sources
296 return results
297
298 def display(self, exposure, results, convolvedImage=None):
299 """Display detections if so configured
300
301 Displays the ``exposure`` in frame 0, overlays the detection peaks.
302
303 Requires that ``lsstDebug`` has been set up correctly, so that
304 ``lsstDebug.Info("lsst.meas.algorithms.detection")`` evaluates `True`.
305
306 If the ``convolvedImage`` is non-`None` and
307 ``lsstDebug.Info("lsst.meas.algorithms.detection") > 1``, the
308 ``convolvedImage`` will be displayed in frame 1.
309
310 Parameters
311 ----------
312 exposure : `lsst.afw.image.Exposure`
313 Exposure to display, on which will be plotted the detections.
314 results : `lsst.pipe.base.Struct`
315 Results of the 'detectFootprints' method, containing positive and
316 negative footprints (which contain the peak positions that we will
317 plot). This is a `Struct` with ``positive`` and ``negative``
318 elements that are of type `lsst.afw.detection.FootprintSet`.
319 convolvedImage : `lsst.afw.image.Image`, optional
320 Convolved image used for thresholding.
321 """
322 try:
323 import lsstDebug
324 display = lsstDebug.Info(__name__).display
325 except ImportError:
326 try:
327 display
328 except NameError:
329 display = False
330 if not display:
331 return
332
333 afwDisplay.setDefaultMaskTransparency(75)
334
335 disp0 = afwDisplay.Display(frame=0)
336 disp0.mtv(exposure, title="detection")
337
338 def plotPeaks(fps, ctype):
339 if fps is None:
340 return
341 with disp0.Buffering():
342 for fp in fps.getFootprints():
343 for pp in fp.getPeaks():
344 disp0.dot("+", pp.getFx(), pp.getFy(), ctype=ctype)
345 plotPeaks(results.positive, "yellow")
346 plotPeaks(results.negative, "red")
347
348 if convolvedImage and display > 1:
349 disp1 = afwDisplay.Display(frame=1)
350 disp1.mtv(convolvedImage, title="PSF smoothed")
351
352 disp2 = afwDisplay.Display(frame=2)
353 disp2.mtv(afwImage.ImageF(np.sqrt(exposure.variance.array)), title="stddev")
354
355 def applyTempLocalBackground(self, exposure, middle, results):
356 """Apply a temporary local background subtraction
357
358 This temporary local background serves to suppress noise fluctuations
359 in the wings of bright objects.
360
361 Peaks in the footprints will be updated.
362
363 Parameters
364 ----------
365 exposure : `lsst.afw.image.Exposure`
366 Exposure for which to fit local background.
368 Convolved image on which detection will be performed
369 (typically smaller than ``exposure`` because the
370 half-kernel has been removed around the edges).
371 results : `lsst.pipe.base.Struct`
372 Results of the 'detectFootprints' method, containing positive and
373 negative footprints (which contain the peak positions that we will
374 plot). This is a `Struct` with ``positive`` and ``negative``
375 elements that are of type `lsst.afw.detection.FootprintSet`.
376 """
377 # Subtract the local background from the smoothed image. Since we
378 # never use the smoothed again we don't need to worry about adding
379 # it back in.
380 bg = self.tempLocalBackground.fitBackground(exposure.getMaskedImage())
381 bgImage = bg.getImageF(self.tempLocalBackground.config.algorithm,
382 self.tempLocalBackground.config.undersampleStyle)
383 middle -= bgImage.Factory(bgImage, middle.getBBox())
384 if self.config.thresholdPolarity != "negative":
385 results.positiveThreshold = self.makeThreshold(middle, "positive")
386 self.updatePeaks(results.positive, middle, results.positiveThreshold)
387 if self.config.thresholdPolarity != "positive":
388 results.negativeThreshold = self.makeThreshold(middle, "negative")
389 self.updatePeaks(results.negative, middle, results.negativeThreshold)
390
391 def clearMask(self, mask):
392 """Clear the DETECTED and DETECTED_NEGATIVE mask planes.
393
394 Removes any previous detection mask in preparation for a new
395 detection pass.
396
397 Parameters
398 ----------
399 mask : `lsst.afw.image.Mask`
400 Mask to be cleared.
401 """
402 mask &= ~(mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE"))
403
404 def calculateKernelSize(self, sigma):
405 """Calculate the size of the smoothing kernel.
406
407 Uses the ``nSigmaForKernel`` configuration parameter. Note
408 that that is the full width of the kernel bounding box
409 (so a value of 7 means 3.5 sigma on either side of center).
410 The value will be rounded up to the nearest odd integer.
411
412 Parameters
413 ----------
414 sigma : `float`
415 Gaussian sigma of smoothing kernel.
416
417 Returns
418 -------
419 size : `int`
420 Size of the smoothing kernel.
421 """
422 return (int(sigma * self.config.nSigmaForKernel + 0.5)//2)*2 + 1 # make sure it is odd
423
424 def getPsf(self, exposure, sigma=None):
425 """Create a single Gaussian PSF for an exposure.
426
427 If ``sigma`` is provided, we make a `~lsst.afw.detection.GaussianPsf`
428 with that, otherwise use the sigma from the psf of the ``exposure`` to
430
431 Parameters
432 ----------
433 exposure : `lsst.afw.image.Exposure`
434 Exposure from which to retrieve the PSF.
435 sigma : `float`, optional
436 Gaussian sigma to use if provided.
437
438 Returns
439 -------
441 PSF to use for detection.
442
443 Raises
444 ------
445 RuntimeError
446 Raised if ``sigma`` is not provided and ``exposure`` does not
447 contain a ``Psf`` object.
448 """
449 if sigma is None:
450 psf = exposure.getPsf()
451 if psf is None:
452 raise RuntimeError("Unable to determine PSF to use for detection: no sigma provided")
453 sigma = psf.computeShape(psf.getAveragePosition()).getDeterminantRadius()
454 size = self.calculateKernelSize(sigma)
455 psf = afwDet.GaussianPsf(size, size, sigma)
456 return psf
457
458 def convolveImage(self, maskedImage, psf, doSmooth=True):
459 """Convolve the image with the PSF.
460
461 We convolve the image with a Gaussian approximation to the PSF,
462 because this is separable and therefore fast. It's technically a
463 correlation rather than a convolution, but since we use a symmetric
464 Gaussian there's no difference.
465
466 The convolution can be disabled with ``doSmooth=False``. If we do
467 convolve, we mask the edges as ``EDGE`` and return the convolved image
468 with the edges removed. This is because we can't convolve the edges
469 because the kernel would extend off the image.
470
471 Parameters
472 ----------
473 maskedImage : `lsst.afw.image.MaskedImage`
474 Image to convolve.
476 PSF to convolve with (actually with a Gaussian approximation
477 to it).
478 doSmooth : `bool`
479 Actually do the convolution? Set to False when running on
480 e.g. a pre-convolved image, or a mask plane.
481
482 Returns
483 -------
484 results : `lsst.pipe.base.Struct`
485 The `~lsst.pipe.base.Struct` contains:
486
487 ``middle``
488 Convolved image, without the edges. (`lsst.afw.image.MaskedImage`)
489 ``sigma``
490 Gaussian sigma used for the convolution. (`float`)
491 """
492 self.metadata["doSmooth"] = doSmooth
493 sigma = psf.computeShape(psf.getAveragePosition()).getDeterminantRadius()
494 self.metadata["sigma"] = sigma
495
496 if not doSmooth:
497 middle = maskedImage.Factory(maskedImage, deep=True)
498 return pipeBase.Struct(middle=middle, sigma=sigma)
499
500 # Smooth using a Gaussian (which is separable, hence fast) of width sigma
501 # Make a SingleGaussian (separable) kernel with the 'sigma'
502 kWidth = self.calculateKernelSize(sigma)
503 self.metadata["smoothingKernelWidth"] = kWidth
504 gaussFunc = afwMath.GaussianFunction1D(sigma)
505 gaussKernel = afwMath.SeparableKernel(kWidth, kWidth, gaussFunc, gaussFunc)
506
507 convolvedImage = maskedImage.Factory(maskedImage.getBBox())
508
509 afwMath.convolve(convolvedImage, maskedImage, gaussKernel, afwMath.ConvolutionControl())
510
511 # Only search psf-smoothed part of frame
512 goodBBox = gaussKernel.shrinkBBox(convolvedImage.getBBox())
513 middle = convolvedImage.Factory(convolvedImage, goodBBox, afwImage.PARENT, False)
514
515 # Mark the parts of the image outside goodBBox as EDGE
516 self.setEdgeBits(maskedImage, goodBBox, maskedImage.getMask().getPlaneBitMask("EDGE"))
517
518 return pipeBase.Struct(middle=middle, sigma=sigma)
519
520 def applyThreshold(self, middle, bbox, factor=1.0, factorNeg=None):
521 r"""Apply thresholds to the convolved image
522
523 Identifies `~lsst.afw.detection.Footprint`\s, both positive and negative.
524 The threshold can be modified by the provided multiplication
525 ``factor``.
526
527 Parameters
528 ----------
530 Convolved image to threshold.
531 bbox : `lsst.geom.Box2I`
532 Bounding box of unconvolved image.
533 factor : `float`
534 Multiplier for the configured threshold.
535 factorNeg : `float` or `None`
536 Multiplier for the configured threshold for negative detection polarity.
537 If `None`, will be set equal to ``factor`` (i.e. equal to the factor used
538 for positive detection polarity).
539
540 Returns
541 -------
542 results : `lsst.pipe.base.Struct`
543 The `~lsst.pipe.base.Struct` contains:
544
545 ``positive``
546 Positive detection footprints, if configured.
548 ``negative``
549 Negative detection footprints, if configured.
551 ``factor``
552 Multiplier for the configured threshold.
553 (`float`)
554 ``factorNeg``
555 Multiplier for the configured threshold for negative detection polarity.
556 (`float`)
557 """
558 if factorNeg is None:
559 factorNeg = factor
560 self.log.info("Setting factor for negative detections equal to that for positive "
561 "detections: %f", factor)
562 results = pipeBase.Struct(positive=None, negative=None, factor=factor, factorNeg=factorNeg,
563 positiveThreshold=None, negativeThreshold=None)
564 # Detect the Footprints (peaks may be replaced if doTempLocalBackground)
565 if self.config.reEstimateBackground or self.config.thresholdPolarity != "negative":
566 results.positiveThreshold = self.makeThreshold(middle, "positive", factor=factor)
567 results.positive = afwDet.FootprintSet(
568 middle,
569 results.positiveThreshold,
570 "DETECTED",
571 self.config.minPixels
572 )
573 results.positive.setRegion(bbox)
574 if self.config.reEstimateBackground or self.config.thresholdPolarity != "positive":
575 results.negativeThreshold = self.makeThreshold(middle, "negative", factor=factorNeg)
576 results.negative = afwDet.FootprintSet(
577 middle,
578 results.negativeThreshold,
579 "DETECTED_NEGATIVE",
580 self.config.minPixels
581 )
582 results.negative.setRegion(bbox)
583
584 return results
585
586 def finalizeFootprints(self, mask, results, sigma, factor=1.0, factorNeg=None):
587 """Finalize the detected footprints.
588
589 Grow the footprints, set the ``DETECTED`` and ``DETECTED_NEGATIVE``
590 mask planes, and log the results.
591
592 ``numPos`` (number of positive footprints), ``numPosPeaks`` (number
593 of positive peaks), ``numNeg`` (number of negative footprints),
594 ``numNegPeaks`` (number of negative peaks) entries are added to the
595 ``results`` struct.
596
597 Parameters
598 ----------
599 mask : `lsst.afw.image.Mask`
600 Mask image on which to flag detected pixels.
601 results : `lsst.pipe.base.Struct`
602 Struct of detection results, including ``positive`` and
603 ``negative`` entries; modified.
604 sigma : `float`
605 Gaussian sigma of PSF.
606 factor : `float`
607 Multiplier for the configured threshold. Note that this is only
608 used here for logging purposes.
609 factorNeg : `float` or `None`
610 Multiplier used for the negative detection polarity threshold.
611 If `None`, a factor equal to ``factor`` (i.e. equal to the one used
612 for positive detection polarity) is assumed. Note that this is only
613 used here for logging purposes.
614 """
615 factorNeg = factor if factorNeg is None else factorNeg
616 for polarity, maskName in (("positive", "DETECTED"), ("negative", "DETECTED_NEGATIVE")):
617 fpSet = getattr(results, polarity)
618 if fpSet is None:
619 continue
620 if self.config.nSigmaToGrow > 0:
621 nGrow = int((self.config.nSigmaToGrow * sigma) + 0.5)
622 self.metadata["nGrow"] = nGrow
623 if self.config.combinedGrow:
624 fpSet = afwDet.FootprintSet(fpSet, nGrow, self.config.isotropicGrow)
625 else:
626 stencil = (afwGeom.Stencil.CIRCLE if self.config.isotropicGrow else
627 afwGeom.Stencil.MANHATTAN)
628 for fp in fpSet:
629 fp.dilate(nGrow, stencil)
630 fpSet.setMask(mask, maskName)
631 if not self.config.returnOriginalFootprints:
632 setattr(results, polarity, fpSet)
633
634 results.numPos = 0
635 results.numPosPeaks = 0
636 results.numNeg = 0
637 results.numNegPeaks = 0
638 positive = ""
639 negative = ""
640
641 if results.positive is not None:
642 results.numPos = len(results.positive.getFootprints())
643 results.numPosPeaks = sum(len(fp.getPeaks()) for fp in results.positive.getFootprints())
644 positive = " %d positive peaks in %d footprints" % (results.numPosPeaks, results.numPos)
645 if results.negative is not None:
646 results.numNeg = len(results.negative.getFootprints())
647 results.numNegPeaks = sum(len(fp.getPeaks()) for fp in results.negative.getFootprints())
648 negative = " %d negative peaks in %d footprints" % (results.numNegPeaks, results.numNeg)
649
650 self.log.info("Detected%s%s%s to %g +ve and %g -ve %s",
651 positive, " and" if positive and negative else "", negative,
652 self.config.thresholdValue*self.config.includeThresholdMultiplier*factor,
653 self.config.thresholdValue*self.config.includeThresholdMultiplier*factorNeg,
654 "DN" if self.config.thresholdType == "value" else "sigma")
655
656 def reEstimateBackground(self, maskedImage, backgrounds):
657 """Estimate the background after detection
658
659 Parameters
660 ----------
661 maskedImage : `lsst.afw.image.MaskedImage`
662 Image on which to estimate the background.
663 backgrounds : `lsst.afw.math.BackgroundList`
664 List of backgrounds; modified.
665
666 Returns
667 -------
668 bg : `lsst.afw.math.backgroundMI`
669 Empirical background model.
670 """
671 bg = self.background.fitBackground(maskedImage)
672 if self.config.adjustBackground:
673 self.log.warning("Fiddling the background by %g", self.config.adjustBackground)
674 bg += self.config.adjustBackground
675 self.log.info("Resubtracting the background after object detection")
676 maskedImage -= bg.getImageF(self.background.config.algorithm,
677 self.background.config.undersampleStyle)
678
679 actrl = bg.getBackgroundControl().getApproximateControl()
680 backgrounds.append((bg, getattr(afwMath.Interpolate, self.background.config.algorithm),
681 bg.getAsUsedUndersampleStyle(), actrl.getStyle(), actrl.getOrderX(),
682 actrl.getOrderY(), actrl.getWeighting()))
683 return bg
684
685 def clearUnwantedResults(self, mask, results):
686 """Clear unwanted results from the Struct of results
687
688 If we specifically want only positive or only negative detections,
689 drop the ones we don't want, and its associated mask plane.
690
691 Parameters
692 ----------
693 mask : `lsst.afw.image.Mask`
694 Mask image.
695 results : `lsst.pipe.base.Struct`
696 Detection results, with ``positive`` and ``negative`` elements;
697 modified.
698 """
699 if self.config.thresholdPolarity == "positive":
700 if self.config.reEstimateBackground:
701 mask &= ~mask.getPlaneBitMask("DETECTED_NEGATIVE")
702 results.negative = None
703 elif self.config.thresholdPolarity == "negative":
704 if self.config.reEstimateBackground:
705 mask &= ~mask.getPlaneBitMask("DETECTED")
706 results.positive = None
707
708 @timeMethod
709 def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None,
710 background=None):
711 """Detect footprints on an exposure.
712
713 Parameters
714 ----------
715 exposure : `lsst.afw.image.Exposure`
716 Exposure to process; DETECTED{,_NEGATIVE} mask plane will be
717 set in-place.
718 doSmooth : `bool`, optional
719 If True, smooth the image before detection using a Gaussian
720 of width ``sigma``, or the measured PSF width of ``exposure``.
721 Set to False when running on e.g. a pre-convolved image, or a mask
722 plane.
723 sigma : `float`, optional
724 Gaussian Sigma of PSF (pixels); used for smoothing and to grow
725 detections; if `None` then measure the sigma of the PSF of the
726 ``exposure``.
727 clearMask : `bool`, optional
728 Clear both DETECTED and DETECTED_NEGATIVE planes before running
729 detection.
730 expId : `dict`, optional
731 Exposure identifier; unused by this implementation, but used for
732 RNG seed by subclasses.
733 background : `lsst.afw.math.BackgroundList`, optional
734 Background that was already subtracted from the exposure; will be
735 modified in-place if ``reEstimateBackground=True``.
736
737 Returns
738 -------
739 results : `lsst.pipe.base.Struct`
740 A `~lsst.pipe.base.Struct` containing:
741
742 ``positive``
743 Positive polarity footprints.
745 ``negative``
746 Negative polarity footprints.
748 ``numPos``
749 Number of footprints in positive or 0 if detection polarity was
750 negative. (`int`)
751 ``numNeg``
752 Number of footprints in negative or 0 if detection polarity was
753 positive. (`int`)
754 ``background``
755 Re-estimated background. `None` or the input ``background``
756 if ``reEstimateBackground==False``.
758 ``factor``
759 Multiplication factor applied to the configured detection
760 threshold. (`float`)
761 """
762 maskedImage = exposure.maskedImage
763
764 if clearMask:
765 self.clearMask(maskedImage.getMask())
766
767 psf = self.getPsf(exposure, sigma=sigma)
768 with self.tempWideBackgroundContext(exposure):
769 convolveResults = self.convolveImage(maskedImage, psf, doSmooth=doSmooth)
770 middle = convolveResults.middle
771 sigma = convolveResults.sigma
772 self.removeBadPixels(middle)
773
774 results = self.applyThreshold(middle, maskedImage.getBBox())
775 results.background = background if background is not None else afwMath.BackgroundList()
776
777 if self.config.doTempLocalBackground:
778 self.applyTempLocalBackground(exposure, middle, results)
779 self.finalizeFootprints(maskedImage.mask, results, sigma)
780
781 # Compute the significance of peaks after the peaks have been
782 # finalized and after local background correction/updatePeaks, so
783 # that the significance represents the "final" detection S/N.
784 results.positive = self.setPeakSignificance(middle, results.positive, results.positiveThreshold)
785 results.negative = self.setPeakSignificance(middle, results.negative, results.negativeThreshold,
786 negative=True)
787
788 if self.config.reEstimateBackground:
789 self.reEstimateBackground(maskedImage, results.background)
790
791 self.clearUnwantedResults(maskedImage.getMask(), results)
792
793 self.display(exposure, results, middle)
794
795 return results
796
797 def removeBadPixels(self, middle):
798 """Set the significance of flagged pixels to zero.
799
800 Parameters
801 ----------
802 middle : `lsst.afw.image.ExposureF`
803 Score or maximum likelihood difference image.
804 The image plane will be modified in place.
805 """
806 badPixelMask = lsst.afw.image.Mask.getPlaneBitMask(self.config.excludeMaskPlanes)
807 badPixels = middle.mask.array & badPixelMask > 0
808 middle.image.array[badPixels] = 0
809
810 def setPeakSignificance(self, exposure, footprints, threshold, negative=False):
811 """Set the significance of each detected peak to the pixel value divided
812 by the appropriate standard-deviation for ``config.thresholdType``.
813
814 Only sets significance for "stdev" and "pixel_stdev" thresholdTypes;
815 we leave it undefined for "value" and "variance" as it does not have a
816 well-defined meaning in those cases.
817
818 Parameters
819 ----------
820 exposure : `lsst.afw.image.Exposure`
821 Exposure that footprints were detected on, likely the convolved,
822 local background-subtracted image.
824 Footprints detected on the image.
825 threshold : `lsst.afw.detection.Threshold`
826 Threshold used to find footprints.
827 negative : `bool`, optional
828 Are we calculating for negative sources?
829 """
830 if footprints is None or footprints.getFootprints() == []:
831 return footprints
832 polarity = -1 if negative else 1
833
834 # All incoming footprints have the same schema.
835 mapper = afwTable.SchemaMapper(footprints.getFootprints()[0].peaks.schema)
836 mapper.addMinimalSchema(footprints.getFootprints()[0].peaks.schema)
837 mapper.addOutputField("significance", type=float,
838 doc="Ratio of peak value to configured standard deviation.")
839
840 # Copy the old peaks to the new ones with a significance field.
841 # Do this independent of the threshold type, so we always have a
842 # significance field.
843 newFootprints = afwDet.FootprintSet(footprints)
844 for old, new in zip(footprints.getFootprints(), newFootprints.getFootprints()):
845 newPeaks = afwDet.PeakCatalog(mapper.getOutputSchema())
846 newPeaks.extend(old.peaks, mapper=mapper)
847 new.getPeaks().clear()
848 new.setPeakCatalog(newPeaks)
849
850 # Compute the significance values.
851 if self.config.thresholdType == "pixel_stdev":
852 for footprint in newFootprints.getFootprints():
853 footprint.updatePeakSignificance(exposure.variance, polarity)
854 elif self.config.thresholdType == "stdev":
855 sigma = threshold.getValue() / self.config.thresholdValue
856 for footprint in newFootprints.getFootprints():
857 footprint.updatePeakSignificance(polarity*sigma)
858 else:
859 for footprint in newFootprints.getFootprints():
860 for peak in footprint.peaks:
861 peak["significance"] = 0
862
863 return newFootprints
864
865 def makeThreshold(self, image, thresholdParity, factor=1.0):
866 """Make an afw.detection.Threshold object corresponding to the task's
867 configuration and the statistics of the given image.
868
869 Parameters
870 ----------
871 image : `afw.image.MaskedImage`
872 Image to measure noise statistics from if needed.
873 thresholdParity: `str`
874 One of "positive" or "negative", to set the kind of fluctuations
875 the Threshold will detect.
876 factor : `float`
877 Factor by which to multiply the configured detection threshold.
878 This is useful for tweaking the detection threshold slightly.
879
880 Returns
881 -------
882 threshold : `lsst.afw.detection.Threshold`
883 Detection threshold.
884 """
885 parity = False if thresholdParity == "negative" else True
886 thresholdValue = self.config.thresholdValue
887 thresholdType = self.config.thresholdType
888 if self.config.thresholdType == 'stdev':
889 bad = image.getMask().getPlaneBitMask(self.config.statsMask)
891 sctrl.setAndMask(bad)
892 stats = afwMath.makeStatistics(image, afwMath.STDEVCLIP, sctrl)
893 thresholdValue *= stats.getValue(afwMath.STDEVCLIP)
894 thresholdType = 'value'
895
896 threshold = afwDet.createThreshold(thresholdValue*factor, thresholdType, parity)
897 threshold.setIncludeMultiplier(self.config.includeThresholdMultiplier)
898 self.log.debug("Detection threshold: %s", threshold)
899 return threshold
900
901 def updatePeaks(self, fpSet, image, threshold):
902 """Update the Peaks in a FootprintSet by detecting new Footprints and
903 Peaks in an image and using the new Peaks instead of the old ones.
904
905 Parameters
906 ----------
908 Set of Footprints whose Peaks should be updated.
909 image : `afw.image.MaskedImage`
910 Image to detect new Footprints and Peak in.
911 threshold : `afw.detection.Threshold`
912 Threshold object for detection.
913
914 Input Footprints with fewer Peaks than self.config.nPeaksMaxSimple
915 are not modified, and if no new Peaks are detected in an input
916 Footprint, the brightest original Peak in that Footprint is kept.
917 """
918 for footprint in fpSet.getFootprints():
919 oldPeaks = footprint.getPeaks()
920 if len(oldPeaks) <= self.config.nPeaksMaxSimple:
921 continue
922 # We detect a new FootprintSet within each non-simple Footprint's
923 # bbox to avoid a big O(N^2) comparison between the two sets of
924 # Footprints.
925 sub = image.Factory(image, footprint.getBBox())
926 fpSetForPeaks = afwDet.FootprintSet(
927 sub,
928 threshold,
929 "", # don't set a mask plane
930 self.config.minPixels
931 )
932 newPeaks = afwDet.PeakCatalog(oldPeaks.getTable())
933 for fpForPeaks in fpSetForPeaks.getFootprints():
934 for peak in fpForPeaks.getPeaks():
935 if footprint.contains(peak.getI()):
936 newPeaks.append(peak)
937 if len(newPeaks) > 0:
938 del oldPeaks[:]
939 oldPeaks.extend(newPeaks)
940 else:
941 del oldPeaks[1:]
942
943 @staticmethod
944 def setEdgeBits(maskedImage, goodBBox, edgeBitmask):
945 """Set the edgeBitmask bits for all of maskedImage outside goodBBox
946
947 Parameters
948 ----------
949 maskedImage : `lsst.afw.image.MaskedImage`
950 Image on which to set edge bits in the mask.
951 goodBBox : `lsst.geom.Box2I`
952 Bounding box of good pixels, in ``LOCAL`` coordinates.
953 edgeBitmask : `lsst.afw.image.MaskPixel`
954 Bit mask to OR with the existing mask bits in the region
955 outside ``goodBBox``.
956 """
957 msk = maskedImage.getMask()
958
959 mx0, my0 = maskedImage.getXY0()
960 for x0, y0, w, h in ([0, 0,
961 msk.getWidth(), goodBBox.getBeginY() - my0],
962 [0, goodBBox.getEndY() - my0, msk.getWidth(),
963 maskedImage.getHeight() - (goodBBox.getEndY() - my0)],
964 [0, 0,
965 goodBBox.getBeginX() - mx0, msk.getHeight()],
966 [goodBBox.getEndX() - mx0, 0,
967 maskedImage.getWidth() - (goodBBox.getEndX() - mx0), msk.getHeight()],
968 ):
969 edgeMask = msk.Factory(msk, lsst.geom.BoxI(lsst.geom.PointI(x0, y0),
970 lsst.geom.ExtentI(w, h)), afwImage.LOCAL)
971 edgeMask |= edgeBitmask
972
973 @contextmanager
974 def tempWideBackgroundContext(self, exposure):
975 """Context manager for removing wide (large-scale) background
976
977 Removing a wide (large-scale) background helps to suppress the
978 detection of large footprints that may overwhelm the deblender.
979 It does, however, set a limit on the maximum scale of objects.
980
981 The background that we remove will be restored upon exit from
982 the context manager.
983
984 Parameters
985 ----------
986 exposure : `lsst.afw.image.Exposure`
987 Exposure on which to remove large-scale background.
988
989 Returns
990 -------
991 context : context manager
992 Context manager that will ensure the temporary wide background
993 is restored.
994 """
995 doTempWideBackground = self.config.doTempWideBackground
996 if doTempWideBackground:
997 self.log.info("Applying temporary wide background subtraction")
998 original = exposure.maskedImage.image.array[:].copy()
999 self.tempWideBackground.run(exposure).background
1000 # Remove NO_DATA regions (e.g., edge of the field-of-view); these can cause detections after
1001 # subtraction because of extrapolation of the background model into areas with no constraints.
1002 image = exposure.maskedImage.image
1003 mask = exposure.maskedImage.mask
1004 noData = mask.array & mask.getPlaneBitMask("NO_DATA") > 0
1005 isGood = mask.array & mask.getPlaneBitMask(self.config.statsMask) == 0
1006 image.array[noData] = np.median(image.array[~noData & isGood])
1007 try:
1008 yield
1009 finally:
1010 if doTempWideBackground:
1011 exposure.maskedImage.image.array[:] = original
1012
1013
1014def addExposures(exposureList):
1015 """Add a set of exposures together.
1016
1017 Parameters
1018 ----------
1019 exposureList : `list` of `lsst.afw.image.Exposure`
1020 Sequence of exposures to add.
1021
1022 Returns
1023 -------
1024 addedExposure : `lsst.afw.image.Exposure`
1025 An exposure of the same size as each exposure in ``exposureList``,
1026 with the metadata from ``exposureList[0]`` and a masked image equal
1027 to the sum of all the exposure's masked images.
1028 """
1029 exposure0 = exposureList[0]
1030 image0 = exposure0.getMaskedImage()
1031
1032 addedImage = image0.Factory(image0, True)
1033 addedImage.setXY0(image0.getXY0())
1034
1035 for exposure in exposureList[1:]:
1036 image = exposure.getMaskedImage()
1037 addedImage += image
1038
1039 addedExposure = exposure0.Factory(addedImage, exposure0.getWcs())
1040 return addedExposure
Class to describe the properties of a detected object from an image.
Definition Footprint.h:63
A set of Footprints, associated with a MaskedImage.
A circularly symmetric Gaussian Psf class with no spatial variation, intended mostly for testing purp...
Definition GaussianPsf.h:42
A polymorphic base class for representing an image's Point Spread Function.
Definition Psf.h:76
A Threshold is used to pass a threshold value to detection algorithms.
Definition Threshold.h:43
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 represent a 2-dimensional array of pixels.
Definition Image.h:51
Represent a 2-dimensional array of bitmask pixels.
Definition Mask.h:77
static MaskPixelT getPlaneBitMask(const std::vector< std::string > &names)
Return the bitmask corresponding to a vector of plane names OR'd together.
Definition Mask.cc:376
A class to manipulate images, masks, and variance as a single object.
Definition MaskedImage.h:74
Parameters to control convolution.
A kernel described by a pair of functions: func(x, y) = colFunc(x) * rowFunc(y)
Definition Kernel.h:860
Pass parameters to a Statistics object.
Definition Statistics.h:83
Defines the fields and offsets for a table.
Definition Schema.h:51
A mapping between the keys of two Schemas, used to copy data between them.
Table class that contains measurements made on a single exposure.
Definition Source.h:217
An integer coordinate rectangle.
Definition Box.h:55
makeThreshold(self, image, thresholdParity, factor=1.0)
Definition detection.py:865
applyTempLocalBackground(self, exposure, middle, results)
Definition detection.py:355
setEdgeBits(maskedImage, goodBBox, edgeBitmask)
Definition detection.py:944
setPeakSignificance(self, exposure, footprints, threshold, negative=False)
Definition detection.py:810
detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None, background=None)
Definition detection.py:710
applyThreshold(self, middle, bbox, factor=1.0, factorNeg=None)
Definition detection.py:520
convolveImage(self, maskedImage, psf, doSmooth=True)
Definition detection.py:458
display(self, exposure, results, convolvedImage=None)
Definition detection.py:298
finalizeFootprints(self, mask, results, sigma, factor=1.0, factorNeg=None)
Definition detection.py:586
reEstimateBackground(self, maskedImage, backgrounds)
Definition detection.py:656
Threshold createThreshold(const double value, const std::string type="value", const bool polarity=true)
Factory method for creating Threshold objects.
Definition Threshold.cc:109
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)
Definition Statistics.h:361
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, ConvolutionControl const &convolutionControl=ConvolutionControl())
Convolve an Image or MaskedImage with a Kernel, setting pixels of an existing output image.