LSST Applications g0b6bd0c080+a72a5dd7e6,g1182afd7b4+2a019aa3bb,g17e5ecfddb+2b8207f7de,g1d67935e3f+06cf436103,g38293774b4+ac198e9f13,g396055baef+6a2097e274,g3b44f30a73+6611e0205b,g480783c3b1+98f8679e14,g48ccf36440+89c08d0516,g4b93dc025c+98f8679e14,g5c4744a4d9+a302e8c7f0,g613e996a0d+e1c447f2e0,g6c8d09e9e7+25247a063c,g7271f0639c+98f8679e14,g7a9cd813b8+124095ede6,g9d27549199+a302e8c7f0,ga1cf026fa3+ac198e9f13,ga32aa97882+7403ac30ac,ga786bb30fb+7a139211af,gaa63f70f4e+9994eb9896,gabf319e997+ade567573c,gba47b54d5d+94dc90c3ea,gbec6a3398f+06cf436103,gc6308e37c7+07dd123edb,gc655b1545f+ade567573c,gcc9029db3c+ab229f5caf,gd01420fc67+06cf436103,gd877ba84e5+06cf436103,gdb4cecd868+6f279b5b48,ge2d134c3d5+cc4dbb2e3f,ge448b5faa6+86d1ceac1d,gecc7e12556+98f8679e14,gf3ee170dca+25247a063c,gf4ac96e456+ade567573c,gf9f5ea5b4d+ac198e9f13,gff490e6085+8c2580be5c,w.2022.27
LSST Data Management Base Package
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
147 def setDefaults(self):
148 self.tempLocalBackgroundtempLocalBackground.binSize = 64
149 self.tempLocalBackgroundtempLocalBackground.algorithm = "AKIMA_SPLINE"
150 self.tempLocalBackgroundtempLocalBackground.useApprox = False
151 # Background subtraction to remove a large-scale background (e.g., scattered light); restored later.
152 # Want to keep it from exceeding the deblender size limit of 1 Mpix, so half that is reasonable.
153 self.tempWideBackgroundtempWideBackground.binSize = 512
154 self.tempWideBackgroundtempWideBackground.algorithm = "AKIMA_SPLINE"
155 self.tempWideBackgroundtempWideBackground.useApprox = False
156 # Ensure we can remove even bright scattered light that is DETECTED
157 for maskPlane in ("DETECTED", "DETECTED_NEGATIVE"):
158 if maskPlane in self.tempWideBackgroundtempWideBackground.ignoredPixelMask:
159 self.tempWideBackgroundtempWideBackground.ignoredPixelMask.remove(maskPlane)
160
161
162class SourceDetectionTask(pipeBase.Task):
163 """Create the detection task. Most arguments are simply passed onto pipe.base.Task.
164
165 Parameters
166 ----------
167 schema : `lsst.afw.table.Schema`
168 Schema object used to create the output `lsst.afw.table.SourceCatalog`
169 **kwds
170 Keyword arguments passed to `lsst.pipe.base.task.Task.__init__`
171
172 If schema is not None and configured for 'both' detections,
173 a 'flags.negative' field will be added to label detections made with a
174 negative threshold.
175
176 Notes
177 -----
178 This task can add fields to the schema, so any code calling this task must ensure that
179 these columns are indeed present in the input match list.
180 """
181
182 ConfigClass = SourceDetectionConfig
183 _DefaultName = "sourceDetection"
184
185 def __init__(self, schema=None, **kwds):
186 pipeBase.Task.__init__(self, **kwds)
187 if schema is not None and self.config.thresholdPolarity == "both":
188 self.negativeFlagKeynegativeFlagKey = schema.addField(
189 "flags_negative", type="Flag",
190 doc="set if source was detected as significantly negative"
191 )
192 else:
193 if self.config.thresholdPolarity == "both":
194 self.log.warning("Detection polarity set to 'both', but no flag will be "
195 "set to distinguish between positive and negative detections")
196 self.negativeFlagKeynegativeFlagKey = None
197 if self.config.reEstimateBackground:
198 self.makeSubtask("background")
199 if self.config.doTempLocalBackground:
200 self.makeSubtask("tempLocalBackground")
201 if self.config.doTempWideBackground:
202 self.makeSubtask("tempWideBackground")
203
204 @timeMethod
205 def run(self, table, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None):
206 """Run source detection and create a SourceCatalog of detections.
207
208 Parameters
209 ----------
211 Table object that will be used to create the SourceCatalog.
212 exposure : `lsst.afw.image.Exposure`
213 Exposure to process; DETECTED mask plane will be set in-place.
214 doSmooth : `bool`
215 If True, smooth the image before detection using a Gaussian of width
216 ``sigma``, or the measured PSF width. Set to False when running on
217 e.g. a pre-convolved image, or a mask plane.
218 sigma : `float`
219 Sigma of PSF (pixels); used for smoothing and to grow detections;
220 if None then measure the sigma of the PSF of the exposure
221 clearMask : `bool`
222 Clear DETECTED{,_NEGATIVE} planes before running detection.
223 expId : `int`
224 Exposure identifier; unused by this implementation, but used for
225 RNG seed by subclasses.
226
227 Returns
228 -------
229 result : `lsst.pipe.base.Struct`
230 ``sources``
231 The detected sources (`lsst.afw.table.SourceCatalog`)
232 ``fpSets``
233 The result resturned by `detectFootprints`
234 (`lsst.pipe.base.Struct`).
235
236 Raises
237 ------
238 ValueError
239 If flags.negative is needed, but isn't in table's schema.
240 lsst.pipe.base.TaskError
241 If sigma=None, doSmooth=True and the exposure has no PSF.
242
243 Notes
244 -----
245 If you want to avoid dealing with Sources and Tables, you can use
247 """
248 if self.negativeFlagKeynegativeFlagKey is not None and self.negativeFlagKeynegativeFlagKey not in table.getSchema():
249 raise ValueError("Table has incorrect Schema")
250 results = self.detectFootprintsdetectFootprints(exposure=exposure, doSmooth=doSmooth, sigma=sigma,
251 clearMask=clearMask, expId=expId)
252 sources = afwTable.SourceCatalog(table)
253 sources.reserve(results.numPos + results.numNeg)
254 if results.negative:
255 results.negative.makeSources(sources)
256 if self.negativeFlagKeynegativeFlagKey:
257 for record in sources:
258 record.set(self.negativeFlagKeynegativeFlagKey, True)
259 if results.positive:
260 results.positive.makeSources(sources)
261 results.fpSets = results.copy() # Backward compatibility
262 results.sources = sources
263 return results
264
265 def display(self, exposure, results, convolvedImage=None):
266 """Display detections if so configured
267
268 Displays the ``exposure`` in frame 0, overlays the detection peaks.
269
270 Requires that ``lsstDebug`` has been set up correctly, so that
271 ``lsstDebug.Info("lsst.meas.algorithms.detection")`` evaluates `True`.
272
273 If the ``convolvedImage`` is non-`None` and
274 ``lsstDebug.Info("lsst.meas.algorithms.detection") > 1``, the
275 ``convolvedImage`` will be displayed in frame 1.
276
277 Parameters
278 ----------
279 exposure : `lsst.afw.image.Exposure`
280 Exposure to display, on which will be plotted the detections.
281 results : `lsst.pipe.base.Struct`
282 Results of the 'detectFootprints' method, containing positive and
283 negative footprints (which contain the peak positions that we will
284 plot). This is a `Struct` with ``positive`` and ``negative``
285 elements that are of type `lsst.afw.detection.FootprintSet`.
286 convolvedImage : `lsst.afw.image.Image`, optional
287 Convolved image used for thresholding.
288 """
289 try:
290 import lsstDebug
291 display = lsstDebug.Info(__name__).display
292 except ImportError:
293 try:
294 display
295 except NameError:
296 display = False
297 if not display:
298 return
299
300 afwDisplay.setDefaultMaskTransparency(75)
301
302 disp0 = afwDisplay.Display(frame=0)
303 disp0.mtv(exposure, title="detection")
304
305 def plotPeaks(fps, ctype):
306 if fps is None:
307 return
308 with disp0.Buffering():
309 for fp in fps.getFootprints():
310 for pp in fp.getPeaks():
311 disp0.dot("+", pp.getFx(), pp.getFy(), ctype=ctype)
312 plotPeaks(results.positive, "yellow")
313 plotPeaks(results.negative, "red")
314
315 if convolvedImage and display > 1:
316 disp1 = afwDisplay.Display(frame=1)
317 disp1.mtv(convolvedImage, title="PSF smoothed")
318
319 disp2 = afwDisplay.Display(frame=2)
320 disp2.mtv(afwImage.ImageF(np.sqrt(exposure.variance.array)), title="stddev")
321
322 def applyTempLocalBackground(self, exposure, middle, results):
323 """Apply a temporary local background subtraction
324
325 This temporary local background serves to suppress noise fluctuations
326 in the wings of bright objects.
327
328 Peaks in the footprints will be updated.
329
330 Parameters
331 ----------
332 exposure : `lsst.afw.image.Exposure`
333 Exposure for which to fit local background.
335 Convolved image on which detection will be performed
336 (typically smaller than ``exposure`` because the
337 half-kernel has been removed around the edges).
338 results : `lsst.pipe.base.Struct`
339 Results of the 'detectFootprints' method, containing positive and
340 negative footprints (which contain the peak positions that we will
341 plot). This is a `Struct` with ``positive`` and ``negative``
342 elements that are of type `lsst.afw.detection.FootprintSet`.
343 """
344 # Subtract the local background from the smoothed image. Since we
345 # never use the smoothed again we don't need to worry about adding
346 # it back in.
347 bg = self.tempLocalBackground.fitBackground(exposure.getMaskedImage())
348 bgImage = bg.getImageF(self.tempLocalBackground.config.algorithm,
349 self.tempLocalBackground.config.undersampleStyle)
350 middle -= bgImage.Factory(bgImage, middle.getBBox())
351 if self.config.thresholdPolarity != "negative":
352 results.positiveThreshold = self.makeThresholdmakeThreshold(middle, "positive")
353 self.updatePeaksupdatePeaks(results.positive, middle, results.positiveThreshold)
354 if self.config.thresholdPolarity != "positive":
355 results.negativeThreshold = self.makeThresholdmakeThreshold(middle, "negative")
356 self.updatePeaksupdatePeaks(results.negative, middle, results.negativeThreshold)
357
358 def clearMask(self, mask):
359 """Clear the DETECTED and DETECTED_NEGATIVE mask planes
360
361 Removes any previous detection mask in preparation for a new
362 detection pass.
363
364 Parameters
365 ----------
366 mask : `lsst.afw.image.Mask`
367 Mask to be cleared.
368 """
369 mask &= ~(mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE"))
370
371 def calculateKernelSize(self, sigma):
372 """Calculate size of smoothing kernel
373
374 Uses the ``nSigmaForKernel`` configuration parameter. Note
375 that that is the full width of the kernel bounding box
376 (so a value of 7 means 3.5 sigma on either side of center).
377 The value will be rounded up to the nearest odd integer.
378
379 Parameters
380 ----------
381 sigma : `float`
382 Gaussian sigma of smoothing kernel.
383
384 Returns
385 -------
386 size : `int`
387 Size of the smoothing kernel.
388 """
389 return (int(sigma * self.config.nSigmaForKernel + 0.5)//2)*2 + 1 # make sure it is odd
390
391 def getPsf(self, exposure, sigma=None):
392 """Retrieve the PSF for an exposure
393
394 If ``sigma`` is provided, we make a ``GaussianPsf`` with that,
395 otherwise use the one from the ``exposure``.
396
397 Parameters
398 ----------
399 exposure : `lsst.afw.image.Exposure`
400 Exposure from which to retrieve the PSF.
401 sigma : `float`, optional
402 Gaussian sigma to use if provided.
403
404 Returns
405 -------
407 PSF to use for detection.
408 """
409 if sigma is None:
410 psf = exposure.getPsf()
411 if psf is None:
412 raise RuntimeError("Unable to determine PSF to use for detection: no sigma provided")
413 sigma = psf.computeShape(psf.getAveragePosition()).getDeterminantRadius()
414 size = self.calculateKernelSizecalculateKernelSize(sigma)
415 psf = afwDet.GaussianPsf(size, size, sigma)
416 return psf
417
418 def convolveImage(self, maskedImage, psf, doSmooth=True):
419 """Convolve the image with the PSF
420
421 We convolve the image with a Gaussian approximation to the PSF,
422 because this is separable and therefore fast. It's technically a
423 correlation rather than a convolution, but since we use a symmetric
424 Gaussian there's no difference.
425
426 The convolution can be disabled with ``doSmooth=False``. If we do
427 convolve, we mask the edges as ``EDGE`` and return the convolved image
428 with the edges removed. This is because we can't convolve the edges
429 because the kernel would extend off the image.
430
431 Parameters
432 ----------
433 maskedImage : `lsst.afw.image.MaskedImage`
434 Image to convolve.
436 PSF to convolve with (actually with a Gaussian approximation
437 to it).
438 doSmooth : `bool`
439 Actually do the convolution? Set to False when running on
440 e.g. a pre-convolved image, or a mask plane.
441
442 Return Struct contents
443 ----------------------
445 Convolved image, without the edges.
446 sigma : `float`
447 Gaussian sigma used for the convolution.
448 """
449 self.metadata["doSmooth"] = doSmooth
450 sigma = psf.computeShape(psf.getAveragePosition()).getDeterminantRadius()
451 self.metadata["sigma"] = sigma
452
453 if not doSmooth:
454 middle = maskedImage.Factory(maskedImage, deep=True)
455 return pipeBase.Struct(middle=middle, sigma=sigma)
456
457 # Smooth using a Gaussian (which is separable, hence fast) of width sigma
458 # Make a SingleGaussian (separable) kernel with the 'sigma'
459 kWidth = self.calculateKernelSizecalculateKernelSize(sigma)
460 self.metadata["smoothingKernelWidth"] = kWidth
461 gaussFunc = afwMath.GaussianFunction1D(sigma)
462 gaussKernel = afwMath.SeparableKernel(kWidth, kWidth, gaussFunc, gaussFunc)
463
464 convolvedImage = maskedImage.Factory(maskedImage.getBBox())
465
466 afwMath.convolve(convolvedImage, maskedImage, gaussKernel, afwMath.ConvolutionControl())
467 #
468 # Only search psf-smoothed part of frame
469 #
470 goodBBox = gaussKernel.shrinkBBox(convolvedImage.getBBox())
471 middle = convolvedImage.Factory(convolvedImage, goodBBox, afwImage.PARENT, False)
472 #
473 # Mark the parts of the image outside goodBBox as EDGE
474 #
475 self.setEdgeBitssetEdgeBits(maskedImage, goodBBox, maskedImage.getMask().getPlaneBitMask("EDGE"))
476
477 return pipeBase.Struct(middle=middle, sigma=sigma)
478
479 def applyThreshold(self, middle, bbox, factor=1.0):
480 """Apply thresholds to the convolved image
481
482 Identifies ``Footprint``s, both positive and negative.
483
484 The threshold can be modified by the provided multiplication
485 ``factor``.
486
487 Parameters
488 ----------
490 Convolved image to threshold.
491 bbox : `lsst.geom.Box2I`
492 Bounding box of unconvolved image.
493 factor : `float`
494 Multiplier for the configured threshold.
495
496 Return Struct contents
497 ----------------------
498 positive : `lsst.afw.detection.FootprintSet` or `None`
499 Positive detection footprints, if configured.
500 negative : `lsst.afw.detection.FootprintSet` or `None`
501 Negative detection footprints, if configured.
502 factor : `float`
503 Multiplier for the configured threshold.
504 """
505 results = pipeBase.Struct(positive=None, negative=None, factor=factor,
506 positiveThreshold=None, negativeThreshold=None)
507 # Detect the Footprints (peaks may be replaced if doTempLocalBackground)
508 if self.config.reEstimateBackground or self.config.thresholdPolarity != "negative":
509 results.positiveThreshold = self.makeThresholdmakeThreshold(middle, "positive", factor=factor)
510 results.positive = afwDet.FootprintSet(
511 middle,
512 results.positiveThreshold,
513 "DETECTED",
514 self.config.minPixels
515 )
516 results.positive.setRegion(bbox)
517 if self.config.reEstimateBackground or self.config.thresholdPolarity != "positive":
518 results.negativeThreshold = self.makeThresholdmakeThreshold(middle, "negative", factor=factor)
519 results.negative = afwDet.FootprintSet(
520 middle,
521 results.negativeThreshold,
522 "DETECTED_NEGATIVE",
523 self.config.minPixels
524 )
525 results.negative.setRegion(bbox)
526
527 return results
528
529 def finalizeFootprints(self, mask, results, sigma, factor=1.0):
530 """Finalize the detected footprints
531
532 Grows the footprints, sets the ``DETECTED`` and ``DETECTED_NEGATIVE``
533 mask planes, and logs the results.
534
535 ``numPos`` (number of positive footprints), ``numPosPeaks`` (number
536 of positive peaks), ``numNeg`` (number of negative footprints),
537 ``numNegPeaks`` (number of negative peaks) entries are added to the
538 detection results.
539
540 Parameters
541 ----------
542 mask : `lsst.afw.image.Mask`
543 Mask image on which to flag detected pixels.
544 results : `lsst.pipe.base.Struct`
545 Struct of detection results, including ``positive`` and
546 ``negative`` entries; modified.
547 sigma : `float`
548 Gaussian sigma of PSF.
549 factor : `float`
550 Multiplier for the configured threshold.
551 """
552 for polarity, maskName in (("positive", "DETECTED"), ("negative", "DETECTED_NEGATIVE")):
553 fpSet = getattr(results, polarity)
554 if fpSet is None:
555 continue
556 if self.config.nSigmaToGrow > 0:
557 nGrow = int((self.config.nSigmaToGrow * sigma) + 0.5)
558 self.metadata["nGrow"] = nGrow
559 if self.config.combinedGrow:
560 fpSet = afwDet.FootprintSet(fpSet, nGrow, self.config.isotropicGrow)
561 else:
562 stencil = (afwGeom.Stencil.CIRCLE if self.config.isotropicGrow else
563 afwGeom.Stencil.MANHATTAN)
564 for fp in fpSet:
565 fp.dilate(nGrow, stencil)
566 fpSet.setMask(mask, maskName)
567 if not self.config.returnOriginalFootprints:
568 setattr(results, polarity, fpSet)
569
570 results.numPos = 0
571 results.numPosPeaks = 0
572 results.numNeg = 0
573 results.numNegPeaks = 0
574 positive = ""
575 negative = ""
576
577 if results.positive is not None:
578 results.numPos = len(results.positive.getFootprints())
579 results.numPosPeaks = sum(len(fp.getPeaks()) for fp in results.positive.getFootprints())
580 positive = " %d positive peaks in %d footprints" % (results.numPosPeaks, results.numPos)
581 if results.negative is not None:
582 results.numNeg = len(results.negative.getFootprints())
583 results.numNegPeaks = sum(len(fp.getPeaks()) for fp in results.negative.getFootprints())
584 negative = " %d negative peaks in %d footprints" % (results.numNegPeaks, results.numNeg)
585
586 self.log.info("Detected%s%s%s to %g %s",
587 positive, " and" if positive and negative else "", negative,
588 self.config.thresholdValue*self.config.includeThresholdMultiplier*factor,
589 "DN" if self.config.thresholdType == "value" else "sigma")
590
591 def reEstimateBackground(self, maskedImage, backgrounds):
592 """Estimate the background after detection
593
594 Parameters
595 ----------
596 maskedImage : `lsst.afw.image.MaskedImage`
597 Image on which to estimate the background.
598 backgrounds : `lsst.afw.math.BackgroundList`
599 List of backgrounds; modified.
600
601 Returns
602 -------
603 bg : `lsst.afw.math.backgroundMI`
604 Empirical background model.
605 """
606 bg = self.background.fitBackground(maskedImage)
607 if self.config.adjustBackground:
608 self.log.warning("Fiddling the background by %g", self.config.adjustBackground)
609 bg += self.config.adjustBackground
610 self.log.info("Resubtracting the background after object detection")
611 maskedImage -= bg.getImageF(self.background.config.algorithm,
612 self.background.config.undersampleStyle)
613
614 actrl = bg.getBackgroundControl().getApproximateControl()
615 backgrounds.append((bg, getattr(afwMath.Interpolate, self.background.config.algorithm),
616 bg.getAsUsedUndersampleStyle(), actrl.getStyle(), actrl.getOrderX(),
617 actrl.getOrderY(), actrl.getWeighting()))
618 return bg
619
620 def clearUnwantedResults(self, mask, results):
621 """Clear unwanted results from the Struct of results
622
623 If we specifically want only positive or only negative detections,
624 drop the ones we don't want, and its associated mask plane.
625
626 Parameters
627 ----------
628 mask : `lsst.afw.image.Mask`
629 Mask image.
630 results : `lsst.pipe.base.Struct`
631 Detection results, with ``positive`` and ``negative`` elements;
632 modified.
633 """
634 if self.config.thresholdPolarity == "positive":
635 if self.config.reEstimateBackground:
636 mask &= ~mask.getPlaneBitMask("DETECTED_NEGATIVE")
637 results.negative = None
638 elif self.config.thresholdPolarity == "negative":
639 if self.config.reEstimateBackground:
640 mask &= ~mask.getPlaneBitMask("DETECTED")
641 results.positive = None
642
643 @timeMethod
644 def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None):
645 """Detect footprints on an exposure.
646
647 Parameters
648 ----------
649 exposure : `lsst.afw.image.Exposure`
650 Exposure to process; DETECTED{,_NEGATIVE} mask plane will be
651 set in-place.
652 doSmooth : `bool`, optional
653 If True, smooth the image before detection using a Gaussian
654 of width ``sigma``, or the measured PSF width of ``exposure``.
655 Set to False when running on e.g. a pre-convolved image, or a mask
656 plane.
657 sigma : `float`, optional
658 Gaussian Sigma of PSF (pixels); used for smoothing and to grow
659 detections; if `None` then measure the sigma of the PSF of the
660 ``exposure``.
661 clearMask : `bool`, optional
662 Clear both DETECTED and DETECTED_NEGATIVE planes before running
663 detection.
664 expId : `dict`, optional
665 Exposure identifier; unused by this implementation, but used for
666 RNG seed by subclasses.
667
668 Return Struct contents
669 ----------------------
671 Positive polarity footprints (may be `None`)
673 Negative polarity footprints (may be `None`)
674 numPos : `int`
675 Number of footprints in positive or 0 if detection polarity was
676 negative.
677 numNeg : `int`
678 Number of footprints in negative or 0 if detection polarity was
679 positive.
680 background : `lsst.afw.math.BackgroundList`
681 Re-estimated background. `None` if
682 ``reEstimateBackground==False``.
683 factor : `float`
684 Multiplication factor applied to the configured detection
685 threshold.
686 """
687 maskedImage = exposure.maskedImage
688
689 if clearMask:
690 self.clearMaskclearMask(maskedImage.getMask())
691
692 psf = self.getPsfgetPsf(exposure, sigma=sigma)
693 with self.tempWideBackgroundContexttempWideBackgroundContext(exposure):
694 convolveResults = self.convolveImageconvolveImage(maskedImage, psf, doSmooth=doSmooth)
695 middle = convolveResults.middle
696 sigma = convolveResults.sigma
697
698 results = self.applyThresholdapplyThreshold(middle, maskedImage.getBBox())
699 results.background = afwMath.BackgroundList()
700 if self.config.doTempLocalBackground:
701 self.applyTempLocalBackgroundapplyTempLocalBackground(exposure, middle, results)
702 self.finalizeFootprintsfinalizeFootprints(maskedImage.mask, results, sigma)
703
704 # Compute the significance of peaks after the peaks have been
705 # finalized and after local background correction/updatePeaks, so
706 # that the significance represents the "final" detection S/N.
707 results.positive = self.setPeakSignificancesetPeakSignificance(middle, results.positive, results.positiveThreshold)
708 results.negative = self.setPeakSignificancesetPeakSignificance(middle, results.negative, results.negativeThreshold,
709 negative=True)
710
711 if self.config.reEstimateBackground:
712 self.reEstimateBackgroundreEstimateBackground(maskedImage, results.background)
713
714 self.clearUnwantedResultsclearUnwantedResults(maskedImage.getMask(), results)
715
716 self.displaydisplay(exposure, results, middle)
717
718 return results
719
720 def setPeakSignificance(self, exposure, footprints, threshold, negative=False):
721 """Set the significance of each detected peak to the pixel value divided
722 by the appropriate standard-deviation for ``config.thresholdType``.
723
724 Only sets significance for "stdev" and "pixel_stdev" thresholdTypes;
725 we leave it undefined for "value" and "variance" as it does not have a
726 well-defined meaning in those cases.
727
728 Parameters
729 ----------
730 exposure : `lsst.afw.image.Exposure`
731 Exposure that footprints were detected on, likely the convolved,
732 local background-subtracted image.
734 Footprints detected on the image.
735 threshold : `lsst.afw.detection.Threshold`
736 Threshold used to find footprints.
737 negative : `bool`, optional
738 Are we calculating for negative sources?
739 """
740 if footprints is None or footprints.getFootprints() == []:
741 return footprints
742 polarity = -1 if negative else 1
743
744 # All incoming footprints have the same schema.
745 mapper = afwTable.SchemaMapper(footprints.getFootprints()[0].peaks.schema)
746 mapper.addMinimalSchema(footprints.getFootprints()[0].peaks.schema)
747 mapper.addOutputField("significance", type=float,
748 doc="Ratio of peak value to configured standard deviation.")
749
750 # Copy the old peaks to the new ones with a significance field.
751 # Do this independent of the threshold type, so we always have a
752 # significance field.
753 newFootprints = afwDet.FootprintSet(footprints)
754 for old, new in zip(footprints.getFootprints(), newFootprints.getFootprints()):
755 newPeaks = afwDet.PeakCatalog(mapper.getOutputSchema())
756 newPeaks.extend(old.peaks, mapper=mapper)
757 new.getPeaks().clear()
758 new.setPeakCatalog(newPeaks)
759
760 # Compute the significance values.
761 if self.config.thresholdType == "pixel_stdev":
762 for footprint in newFootprints.getFootprints():
763 footprint.updatePeakSignificance(exposure.variance, polarity)
764 elif self.config.thresholdType == "stdev":
765 sigma = threshold.getValue() / self.config.thresholdValue
766 for footprint in newFootprints.getFootprints():
767 footprint.updatePeakSignificance(polarity*sigma)
768 else:
769 for footprint in newFootprints.getFootprints():
770 for peak in footprint.peaks:
771 peak["significance"] = 0
772
773 return newFootprints
774
775 def makeThreshold(self, image, thresholdParity, factor=1.0):
776 """Make an afw.detection.Threshold object corresponding to the task's
777 configuration and the statistics of the given image.
778
779 Parameters
780 ----------
781 image : `afw.image.MaskedImage`
782 Image to measure noise statistics from if needed.
783 thresholdParity: `str`
784 One of "positive" or "negative", to set the kind of fluctuations
785 the Threshold will detect.
786 factor : `float`
787 Factor by which to multiply the configured detection threshold.
788 This is useful for tweaking the detection threshold slightly.
789
790 Returns
791 -------
792 threshold : `lsst.afw.detection.Threshold`
793 Detection threshold.
794 """
795 parity = False if thresholdParity == "negative" else True
796 thresholdValue = self.config.thresholdValue
797 thresholdType = self.config.thresholdType
798 if self.config.thresholdType == 'stdev':
799 bad = image.getMask().getPlaneBitMask(self.config.statsMask)
801 sctrl.setAndMask(bad)
802 stats = afwMath.makeStatistics(image, afwMath.STDEVCLIP, sctrl)
803 thresholdValue *= stats.getValue(afwMath.STDEVCLIP)
804 thresholdType = 'value'
805
806 threshold = afwDet.createThreshold(thresholdValue*factor, thresholdType, parity)
807 threshold.setIncludeMultiplier(self.config.includeThresholdMultiplier)
808 self.log.debug("Detection threshold: %s", threshold)
809 return threshold
810
811 def updatePeaks(self, fpSet, image, threshold):
812 """Update the Peaks in a FootprintSet by detecting new Footprints and
813 Peaks in an image and using the new Peaks instead of the old ones.
814
815 Parameters
816 ----------
818 Set of Footprints whose Peaks should be updated.
819 image : `afw.image.MaskedImage`
820 Image to detect new Footprints and Peak in.
821 threshold : `afw.detection.Threshold`
822 Threshold object for detection.
823
824 Input Footprints with fewer Peaks than self.config.nPeaksMaxSimple
825 are not modified, and if no new Peaks are detected in an input
826 Footprint, the brightest original Peak in that Footprint is kept.
827 """
828 for footprint in fpSet.getFootprints():
829 oldPeaks = footprint.getPeaks()
830 if len(oldPeaks) <= self.config.nPeaksMaxSimple:
831 continue
832 # We detect a new FootprintSet within each non-simple Footprint's
833 # bbox to avoid a big O(N^2) comparison between the two sets of
834 # Footprints.
835 sub = image.Factory(image, footprint.getBBox())
836 fpSetForPeaks = afwDet.FootprintSet(
837 sub,
838 threshold,
839 "", # don't set a mask plane
840 self.config.minPixels
841 )
842 newPeaks = afwDet.PeakCatalog(oldPeaks.getTable())
843 for fpForPeaks in fpSetForPeaks.getFootprints():
844 for peak in fpForPeaks.getPeaks():
845 if footprint.contains(peak.getI()):
846 newPeaks.append(peak)
847 if len(newPeaks) > 0:
848 del oldPeaks[:]
849 oldPeaks.extend(newPeaks)
850 else:
851 del oldPeaks[1:]
852
853 @staticmethod
854 def setEdgeBits(maskedImage, goodBBox, edgeBitmask):
855 """Set the edgeBitmask bits for all of maskedImage outside goodBBox
856
857 Parameters
858 ----------
859 maskedImage : `lsst.afw.image.MaskedImage`
860 Image on which to set edge bits in the mask.
861 goodBBox : `lsst.geom.Box2I`
862 Bounding box of good pixels, in ``LOCAL`` coordinates.
863 edgeBitmask : `lsst.afw.image.MaskPixel`
864 Bit mask to OR with the existing mask bits in the region
865 outside ``goodBBox``.
866 """
867 msk = maskedImage.getMask()
868
869 mx0, my0 = maskedImage.getXY0()
870 for x0, y0, w, h in ([0, 0,
871 msk.getWidth(), goodBBox.getBeginY() - my0],
872 [0, goodBBox.getEndY() - my0, msk.getWidth(),
873 maskedImage.getHeight() - (goodBBox.getEndY() - my0)],
874 [0, 0,
875 goodBBox.getBeginX() - mx0, msk.getHeight()],
876 [goodBBox.getEndX() - mx0, 0,
877 maskedImage.getWidth() - (goodBBox.getEndX() - mx0), msk.getHeight()],
878 ):
879 edgeMask = msk.Factory(msk, lsst.geom.BoxI(lsst.geom.PointI(x0, y0),
880 lsst.geom.ExtentI(w, h)), afwImage.LOCAL)
881 edgeMask |= edgeBitmask
882
883 @contextmanager
884 def tempWideBackgroundContext(self, exposure):
885 """Context manager for removing wide (large-scale) background
886
887 Removing a wide (large-scale) background helps to suppress the
888 detection of large footprints that may overwhelm the deblender.
889 It does, however, set a limit on the maximum scale of objects.
890
891 The background that we remove will be restored upon exit from
892 the context manager.
893
894 Parameters
895 ----------
896 exposure : `lsst.afw.image.Exposure`
897 Exposure on which to remove large-scale background.
898
899 Returns
900 -------
901 context : context manager
902 Context manager that will ensure the temporary wide background
903 is restored.
904 """
905 doTempWideBackground = self.config.doTempWideBackground
906 if doTempWideBackground:
907 self.log.info("Applying temporary wide background subtraction")
908 original = exposure.maskedImage.image.array[:].copy()
909 self.tempWideBackground.run(exposure).background
910 # Remove NO_DATA regions (e.g., edge of the field-of-view); these can cause detections after
911 # subtraction because of extrapolation of the background model into areas with no constraints.
912 image = exposure.maskedImage.image
913 mask = exposure.maskedImage.mask
914 noData = mask.array & mask.getPlaneBitMask("NO_DATA") > 0
915 isGood = mask.array & mask.getPlaneBitMask(self.config.statsMask) == 0
916 image.array[noData] = np.median(image.array[~noData & isGood])
917 try:
918 yield
919 finally:
920 if doTempWideBackground:
921 exposure.maskedImage.image.array[:] = original
922
923
924def addExposures(exposureList):
925 """Add a set of exposures together.
926
927 Parameters
928 ----------
929 exposureList : `list` of `lsst.afw.image.Exposure`
930 Sequence of exposures to add.
931
932 Returns
933 -------
934 addedExposure : `lsst.afw.image.Exposure`
935 An exposure of the same size as each exposure in ``exposureList``,
936 with the metadata from ``exposureList[0]`` and a masked image equal
937 to the sum of all the exposure's masked images.
938 """
939 exposure0 = exposureList[0]
940 image0 = exposure0.getMaskedImage()
941
942 addedImage = image0.Factory(image0, True)
943 addedImage.setXY0(image0.getXY0())
944
945 for exposure in exposureList[1:]:
946 image = exposure.getMaskedImage()
947 addedImage += image
948
949 addedExposure = exposure0.Factory(addedImage, exposure0.getWcs())
950 return addedExposure
A set of Footprints, associated with a MaskedImage.
Definition: FootprintSet.h:55
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
A class to manipulate images, masks, and variance as a single object.
Definition: MaskedImage.h:73
Parameters to control convolution.
Definition: ConvolveImage.h:50
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:92
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.
Definition: SchemaMapper.h:21
Table class that contains measurements made on a single exposure.
Definition: Source.h:217
An integer coordinate rectangle.
Definition: Box.h:55
def getPsf(self, exposure, sigma=None)
Definition: detection.py:391
def makeThreshold(self, image, thresholdParity, factor=1.0)
Definition: detection.py:775
def setPeakSignificance(self, exposure, footprints, threshold, negative=False)
Definition: detection.py:720
def run(self, table, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None)
Definition: detection.py:205
def convolveImage(self, maskedImage, psf, doSmooth=True)
Definition: detection.py:418
def applyTempLocalBackground(self, exposure, middle, results)
Definition: detection.py:322
def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None)
Definition: detection.py:644
def reEstimateBackground(self, maskedImage, backgrounds)
Definition: detection.py:591
def updatePeaks(self, fpSet, image, threshold)
Definition: detection.py:811
def finalizeFootprints(self, mask, results, sigma, factor=1.0)
Definition: detection.py:529
def setEdgeBits(maskedImage, goodBBox, edgeBitmask)
Definition: detection.py:854
def display(self, exposure, results, convolvedImage=None)
Definition: detection.py:265
def applyThreshold(self, middle, bbox, factor=1.0)
Definition: detection.py:479
Threshold createThreshold(const double value, const std::string type="value", const bool polarity=true)
Factory method for creating Threshold objects.
Definition: Threshold.cc:109
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.
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:359
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.
def addExposures(exposureList)
Definition: detection.py:924