lsst.meas.algorithms g8d527e0710+abd7d88e05
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 .subtractBackground import SubtractBackgroundTask
40
41
42class SourceDetectionConfig(pexConfig.Config):
43 """Configuration parameters for the SourceDetectionTask
44 """
45 minPixels = pexConfig.RangeField(
46 doc="detected sources with fewer than the specified number of pixels will be ignored",
47 dtype=int, optional=False, default=1, min=0,
48 )
49 isotropicGrow = pexConfig.Field(
50 doc="Pixels should be grown as isotropically as possible (slower)",
51 dtype=bool, optional=False, default=False,
52 )
53 combinedGrow = pexConfig.Field(
54 doc="Grow all footprints at the same time? This allows disconnected footprints to merge.",
55 dtype=bool, default=True,
56 )
57 nSigmaToGrow = pexConfig.Field(
58 doc="Grow detections by nSigmaToGrow * [PSF RMS width]; if 0 then do not grow",
59 dtype=float, default=2.4, # 2.4 pixels/sigma is roughly one pixel/FWHM
60 )
61 returnOriginalFootprints = pexConfig.Field(
62 doc="Grow detections to set the image mask bits, but return the original (not-grown) footprints",
63 dtype=bool, optional=False, default=False,
64 )
65 thresholdValue = pexConfig.RangeField(
66 doc="Threshold for footprints; exact meaning and units depend on thresholdType.",
67 dtype=float, optional=False, default=5.0, min=0.0,
68 )
69 includeThresholdMultiplier = pexConfig.RangeField(
70 doc="Include threshold relative to thresholdValue",
71 dtype=float, default=1.0, min=0.0,
72 )
73 thresholdType = pexConfig.ChoiceField(
74 doc="specifies the desired flavor of Threshold",
75 dtype=str, optional=False, default="stdev",
76 allowed={
77 "variance": "threshold applied to image variance",
78 "stdev": "threshold applied to image std deviation",
79 "value": "threshold applied to image value",
80 "pixel_stdev": "threshold applied to per-pixel std deviation",
81 },
82 )
83 thresholdPolarity = pexConfig.ChoiceField(
84 doc="specifies whether to detect positive, or negative sources, or both",
85 dtype=str, optional=False, default="positive",
86 allowed={
87 "positive": "detect only positive sources",
88 "negative": "detect only negative sources",
89 "both": "detect both positive and negative sources",
90 },
91 )
92 adjustBackground = pexConfig.Field(
93 dtype=float,
94 doc="Fiddle factor to add to the background; debugging only",
95 default=0.0,
96 )
97 reEstimateBackground = pexConfig.Field(
98 dtype=bool,
99 doc="Estimate the background again after final source detection?",
100 default=True, optional=False,
101 )
102 background = pexConfig.ConfigurableField(
103 doc="Background re-estimation; ignored if reEstimateBackground false",
104 target=SubtractBackgroundTask,
105 )
106 tempLocalBackground = pexConfig.ConfigurableField(
107 doc=("A local (small-scale), temporary background estimation step run between "
108 "detecting above-threshold regions and detecting the peaks within "
109 "them; used to avoid detecting spuerious peaks in the wings."),
110 target=SubtractBackgroundTask,
111 )
112 doTempLocalBackground = pexConfig.Field(
113 dtype=bool,
114 doc="Enable temporary local background subtraction? (see tempLocalBackground)",
115 default=True,
116 )
117 tempWideBackground = pexConfig.ConfigurableField(
118 doc=("A wide (large-scale) background estimation and removal before footprint and peak detection. "
119 "It is added back into the image after detection. The purpose is to suppress very large "
120 "footprints (e.g., from large artifacts) that the deblender may choke on."),
121 target=SubtractBackgroundTask,
122 )
123 doTempWideBackground = pexConfig.Field(
124 dtype=bool,
125 doc="Do temporary wide (large-scale) background subtraction before footprint detection?",
126 default=False,
127 )
128 nPeaksMaxSimple = pexConfig.Field(
129 dtype=int,
130 doc=("The maximum number of peaks in a Footprint before trying to "
131 "replace its peaks using the temporary local background"),
132 default=1,
133 )
134 nSigmaForKernel = pexConfig.Field(
135 dtype=float,
136 doc=("Multiple of PSF RMS size to use for convolution kernel bounding box size; "
137 "note that this is not a half-size. The size will be rounded up to the nearest odd integer"),
138 default=7.0,
139 )
140 statsMask = pexConfig.ListField(
141 dtype=str,
142 doc="Mask planes to ignore when calculating statistics of image (for thresholdType=stdev)",
143 default=['BAD', 'SAT', 'EDGE', 'NO_DATA'],
144 )
145
146 def setDefaults(self):
147 self.tempLocalBackgroundtempLocalBackground.binSize = 64
148 self.tempLocalBackgroundtempLocalBackground.algorithm = "AKIMA_SPLINE"
149 self.tempLocalBackgroundtempLocalBackground.useApprox = False
150 # Background subtraction to remove a large-scale background (e.g., scattered light); restored later.
151 # Want to keep it from exceeding the deblender size limit of 1 Mpix, so half that is reasonable.
152 self.tempWideBackgroundtempWideBackground.binSize = 512
153 self.tempWideBackgroundtempWideBackground.algorithm = "AKIMA_SPLINE"
154 self.tempWideBackgroundtempWideBackground.useApprox = False
155 # Ensure we can remove even bright scattered light that is DETECTED
156 for maskPlane in ("DETECTED", "DETECTED_NEGATIVE"):
157 if maskPlane in self.tempWideBackgroundtempWideBackground.ignoredPixelMask:
158 self.tempWideBackgroundtempWideBackground.ignoredPixelMask.remove(maskPlane)
159
160
161class SourceDetectionTask(pipeBase.Task):
162 """Create the detection task. Most arguments are simply passed onto pipe.base.Task.
163
164 Parameters
165 ----------
166 schema : `lsst.afw.table.Schema`
167 Schema object used to create the output `lsst.afw.table.SourceCatalog`
168 **kwds
169 Keyword arguments passed to `lsst.pipe.base.task.Task.__init__`
170
171 If schema is not None and configured for 'both' detections,
172 a 'flags.negative' field will be added to label detections made with a
173 negative threshold.
174
175 Notes
176 -----
177 This task can add fields to the schema, so any code calling this task must ensure that
178 these columns are indeed present in the input match list.
179 """
180
181 ConfigClass = SourceDetectionConfig
182 _DefaultName = "sourceDetection"
183
184 def __init__(self, schema=None, **kwds):
185 pipeBase.Task.__init__(self, **kwds)
186 if schema is not None and self.config.thresholdPolarity == "both":
187 self.negativeFlagKeynegativeFlagKey = schema.addField(
188 "flags_negative", type="Flag",
189 doc="set if source was detected as significantly negative"
190 )
191 else:
192 if self.config.thresholdPolarity == "both":
193 self.log.warning("Detection polarity set to 'both', but no flag will be "
194 "set to distinguish between positive and negative detections")
195 self.negativeFlagKeynegativeFlagKey = None
196 if self.config.reEstimateBackground:
197 self.makeSubtask("background")
198 if self.config.doTempLocalBackground:
199 self.makeSubtask("tempLocalBackground")
200 if self.config.doTempWideBackground:
201 self.makeSubtask("tempWideBackground")
202
203 @pipeBase.timeMethod
204 def run(self, table, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None):
205 """Run source detection and create a SourceCatalog of detections.
206
207 Parameters
208 ----------
210 Table object that will be used to create the SourceCatalog.
211 exposure : `lsst.afw.image.Exposure`
212 Exposure to process; DETECTED mask plane will be set in-place.
213 doSmooth : `bool`
214 If True, smooth the image before detection using a Gaussian of width
215 ``sigma``, or the measured PSF width. Set to False when running on
216 e.g. a pre-convolved image, or a mask plane.
217 sigma : `float`
218 Sigma of PSF (pixels); used for smoothing and to grow detections;
219 if None then measure the sigma of the PSF of the exposure
220 clearMask : `bool`
221 Clear DETECTED{,_NEGATIVE} planes before running detection.
222 expId : `int`
223 Exposure identifier; unused by this implementation, but used for
224 RNG seed by subclasses.
225
226 Returns
227 -------
228 result : `lsst.pipe.base.Struct`
229 ``sources``
230 The detected sources (`lsst.afw.table.SourceCatalog`)
231 ``fpSets``
232 The result resturned by `detectFootprints`
233 (`lsst.pipe.base.Struct`).
234
235 Raises
236 ------
237 ValueError
238 If flags.negative is needed, but isn't in table's schema.
239 lsst.pipe.base.TaskError
240 If sigma=None, doSmooth=True and the exposure has no PSF.
241
242 Notes
243 -----
244 If you want to avoid dealing with Sources and Tables, you can use
246 """
247 if self.negativeFlagKeynegativeFlagKey is not None and self.negativeFlagKeynegativeFlagKey not in table.getSchema():
248 raise ValueError("Table has incorrect Schema")
249 results = self.detectFootprintsdetectFootprints(exposure=exposure, doSmooth=doSmooth, sigma=sigma,
250 clearMask=clearMask, expId=expId)
251 sources = afwTable.SourceCatalog(table)
252 sources.reserve(results.numPos + results.numNeg)
253 if results.negative:
254 results.negative.makeSources(sources)
255 if self.negativeFlagKeynegativeFlagKey:
256 for record in sources:
257 record.set(self.negativeFlagKeynegativeFlagKey, True)
258 if results.positive:
259 results.positive.makeSources(sources)
260 results.fpSets = results.copy() # Backward compatibility
261 results.sources = sources
262 return results
263
264 def display(self, exposure, results, convolvedImage=None):
265 """Display detections if so configured
266
267 Displays the ``exposure`` in frame 0, overlays the detection peaks.
268
269 Requires that ``lsstDebug`` has been set up correctly, so that
270 ``lsstDebug.Info("lsst.meas.algorithms.detection")`` evaluates `True`.
271
272 If the ``convolvedImage`` is non-`None` and
273 ``lsstDebug.Info("lsst.meas.algorithms.detection") > 1``, the
274 ``convolvedImage`` will be displayed in frame 1.
275
276 Parameters
277 ----------
278 exposure : `lsst.afw.image.Exposure`
279 Exposure to display, on which will be plotted the detections.
280 results : `lsst.pipe.base.Struct`
281 Results of the 'detectFootprints' method, containing positive and
282 negative footprints (which contain the peak positions that we will
283 plot). This is a `Struct` with ``positive`` and ``negative``
284 elements that are of type `lsst.afw.detection.FootprintSet`.
285 convolvedImage : `lsst.afw.image.Image`, optional
286 Convolved image used for thresholding.
287 """
288 try:
289 import lsstDebug
290 display = lsstDebug.Info(__name__).display
291 except ImportError:
292 try:
293 display
294 except NameError:
295 display = False
296 if not display:
297 return
298
299 afwDisplay.setDefaultMaskTransparency(75)
300
301 disp0 = afwDisplay.Display(frame=0)
302 disp0.mtv(exposure, title="detection")
303
304 def plotPeaks(fps, ctype):
305 if fps is None:
306 return
307 with disp0.Buffering():
308 for fp in fps.getFootprints():
309 for pp in fp.getPeaks():
310 disp0.dot("+", pp.getFx(), pp.getFy(), ctype=ctype)
311 plotPeaks(results.positive, "yellow")
312 plotPeaks(results.negative, "red")
313
314 if convolvedImage and display > 1:
315 disp1 = afwDisplay.Display(frame=1)
316 disp1.mtv(convolvedImage, title="PSF smoothed")
317
318 def applyTempLocalBackground(self, exposure, middle, results):
319 """Apply a temporary local background subtraction
320
321 This temporary local background serves to suppress noise fluctuations
322 in the wings of bright objects.
323
324 Peaks in the footprints will be updated.
325
326 Parameters
327 ----------
328 exposure : `lsst.afw.image.Exposure`
329 Exposure for which to fit local background.
331 Convolved image on which detection will be performed
332 (typically smaller than ``exposure`` because the
333 half-kernel has been removed around the edges).
334 results : `lsst.pipe.base.Struct`
335 Results of the 'detectFootprints' method, containing positive and
336 negative footprints (which contain the peak positions that we will
337 plot). This is a `Struct` with ``positive`` and ``negative``
338 elements that are of type `lsst.afw.detection.FootprintSet`.
339 """
340 # Subtract the local background from the smoothed image. Since we
341 # never use the smoothed again we don't need to worry about adding
342 # it back in.
343 bg = self.tempLocalBackground.fitBackground(exposure.getMaskedImage())
344 bgImage = bg.getImageF(self.tempLocalBackground.config.algorithm,
345 self.tempLocalBackground.config.undersampleStyle)
346 middle -= bgImage.Factory(bgImage, middle.getBBox())
347 thresholdPos = self.makeThresholdmakeThreshold(middle, "positive")
348 thresholdNeg = self.makeThresholdmakeThreshold(middle, "negative")
349 if self.config.thresholdPolarity != "negative":
350 self.updatePeaksupdatePeaks(results.positive, middle, thresholdPos)
351 if self.config.thresholdPolarity != "positive":
352 self.updatePeaksupdatePeaks(results.negative, middle, thresholdNeg)
353
354 def clearMask(self, mask):
355 """Clear the DETECTED and DETECTED_NEGATIVE mask planes
356
357 Removes any previous detection mask in preparation for a new
358 detection pass.
359
360 Parameters
361 ----------
362 mask : `lsst.afw.image.Mask`
363 Mask to be cleared.
364 """
365 mask &= ~(mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE"))
366
367 def calculateKernelSize(self, sigma):
368 """Calculate size of smoothing kernel
369
370 Uses the ``nSigmaForKernel`` configuration parameter. Note
371 that that is the full width of the kernel bounding box
372 (so a value of 7 means 3.5 sigma on either side of center).
373 The value will be rounded up to the nearest odd integer.
374
375 Parameters
376 ----------
377 sigma : `float`
378 Gaussian sigma of smoothing kernel.
379
380 Returns
381 -------
382 size : `int`
383 Size of the smoothing kernel.
384 """
385 return (int(sigma * self.config.nSigmaForKernel + 0.5)//2)*2 + 1 # make sure it is odd
386
387 def getPsf(self, exposure, sigma=None):
388 """Retrieve the PSF for an exposure
389
390 If ``sigma`` is provided, we make a ``GaussianPsf`` with that,
391 otherwise use the one from the ``exposure``.
392
393 Parameters
394 ----------
395 exposure : `lsst.afw.image.Exposure`
396 Exposure from which to retrieve the PSF.
397 sigma : `float`, optional
398 Gaussian sigma to use if provided.
399
400 Returns
401 -------
403 PSF to use for detection.
404 """
405 if sigma is None:
406 psf = exposure.getPsf()
407 if psf is None:
408 raise RuntimeError("Unable to determine PSF to use for detection: no sigma provided")
409 sigma = psf.computeShape().getDeterminantRadius()
410 size = self.calculateKernelSizecalculateKernelSize(sigma)
411 psf = afwDet.GaussianPsf(size, size, sigma)
412 return psf
413
414 def convolveImage(self, maskedImage, psf, doSmooth=True):
415 """Convolve the image with the PSF
416
417 We convolve the image with a Gaussian approximation to the PSF,
418 because this is separable and therefore fast. It's technically a
419 correlation rather than a convolution, but since we use a symmetric
420 Gaussian there's no difference.
421
422 The convolution can be disabled with ``doSmooth=False``. If we do
423 convolve, we mask the edges as ``EDGE`` and return the convolved image
424 with the edges removed. This is because we can't convolve the edges
425 because the kernel would extend off the image.
426
427 Parameters
428 ----------
429 maskedImage : `lsst.afw.image.MaskedImage`
430 Image to convolve.
432 PSF to convolve with (actually with a Gaussian approximation
433 to it).
434 doSmooth : `bool`
435 Actually do the convolution? Set to False when running on
436 e.g. a pre-convolved image, or a mask plane.
437
438 Return Struct contents
439 ----------------------
441 Convolved image, without the edges.
442 sigma : `float`
443 Gaussian sigma used for the convolution.
444 """
445 self.metadata.set("doSmooth", doSmooth)
446 sigma = psf.computeShape().getDeterminantRadius()
447 self.metadata.set("sigma", sigma)
448
449 if not doSmooth:
450 middle = maskedImage.Factory(maskedImage, deep=True)
451 return pipeBase.Struct(middle=middle, sigma=sigma)
452
453 # Smooth using a Gaussian (which is separable, hence fast) of width sigma
454 # Make a SingleGaussian (separable) kernel with the 'sigma'
455 kWidth = self.calculateKernelSizecalculateKernelSize(sigma)
456 self.metadata.set("smoothingKernelWidth", kWidth)
457 gaussFunc = afwMath.GaussianFunction1D(sigma)
458 gaussKernel = afwMath.SeparableKernel(kWidth, kWidth, gaussFunc, gaussFunc)
459
460 convolvedImage = maskedImage.Factory(maskedImage.getBBox())
461
462 afwMath.convolve(convolvedImage, maskedImage, gaussKernel, afwMath.ConvolutionControl())
463 #
464 # Only search psf-smoothed part of frame
465 #
466 goodBBox = gaussKernel.shrinkBBox(convolvedImage.getBBox())
467 middle = convolvedImage.Factory(convolvedImage, goodBBox, afwImage.PARENT, False)
468 #
469 # Mark the parts of the image outside goodBBox as EDGE
470 #
471 self.setEdgeBitssetEdgeBits(maskedImage, goodBBox, maskedImage.getMask().getPlaneBitMask("EDGE"))
472
473 return pipeBase.Struct(middle=middle, sigma=sigma)
474
475 def applyThreshold(self, middle, bbox, factor=1.0):
476 """Apply thresholds to the convolved image
477
478 Identifies ``Footprint``s, both positive and negative.
479
480 The threshold can be modified by the provided multiplication
481 ``factor``.
482
483 Parameters
484 ----------
486 Convolved image to threshold.
487 bbox : `lsst.geom.Box2I`
488 Bounding box of unconvolved image.
489 factor : `float`
490 Multiplier for the configured threshold.
491
492 Return Struct contents
493 ----------------------
494 positive : `lsst.afw.detection.FootprintSet` or `None`
495 Positive detection footprints, if configured.
496 negative : `lsst.afw.detection.FootprintSet` or `None`
497 Negative detection footprints, if configured.
498 factor : `float`
499 Multiplier for the configured threshold.
500 """
501 results = pipeBase.Struct(positive=None, negative=None, factor=factor)
502 # Detect the Footprints (peaks may be replaced if doTempLocalBackground)
503 if self.config.reEstimateBackground or self.config.thresholdPolarity != "negative":
504 threshold = self.makeThresholdmakeThreshold(middle, "positive", factor=factor)
505 results.positive = afwDet.FootprintSet(
506 middle,
507 threshold,
508 "DETECTED",
509 self.config.minPixels
510 )
511 results.positive.setRegion(bbox)
512 if self.config.reEstimateBackground or self.config.thresholdPolarity != "positive":
513 threshold = self.makeThresholdmakeThreshold(middle, "negative", factor=factor)
514 results.negative = afwDet.FootprintSet(
515 middle,
516 threshold,
517 "DETECTED_NEGATIVE",
518 self.config.minPixels
519 )
520 results.negative.setRegion(bbox)
521
522 return results
523
524 def finalizeFootprints(self, mask, results, sigma, factor=1.0):
525 """Finalize the detected footprints
526
527 Grows the footprints, sets the ``DETECTED`` and ``DETECTED_NEGATIVE``
528 mask planes, and logs the results.
529
530 ``numPos`` (number of positive footprints), ``numPosPeaks`` (number
531 of positive peaks), ``numNeg`` (number of negative footprints),
532 ``numNegPeaks`` (number of negative peaks) entries are added to the
533 detection results.
534
535 Parameters
536 ----------
537 mask : `lsst.afw.image.Mask`
538 Mask image on which to flag detected pixels.
539 results : `lsst.pipe.base.Struct`
540 Struct of detection results, including ``positive`` and
541 ``negative`` entries; modified.
542 sigma : `float`
543 Gaussian sigma of PSF.
544 factor : `float`
545 Multiplier for the configured threshold.
546 """
547 for polarity, maskName in (("positive", "DETECTED"), ("negative", "DETECTED_NEGATIVE")):
548 fpSet = getattr(results, polarity)
549 if fpSet is None:
550 continue
551 if self.config.nSigmaToGrow > 0:
552 nGrow = int((self.config.nSigmaToGrow * sigma) + 0.5)
553 self.metadata.set("nGrow", nGrow)
554 if self.config.combinedGrow:
555 fpSet = afwDet.FootprintSet(fpSet, nGrow, self.config.isotropicGrow)
556 else:
557 stencil = (afwGeom.Stencil.CIRCLE if self.config.isotropicGrow else
558 afwGeom.Stencil.MANHATTAN)
559 for fp in fpSet:
560 fp.dilate(nGrow, stencil)
561 fpSet.setMask(mask, maskName)
562 if not self.config.returnOriginalFootprints:
563 setattr(results, polarity, fpSet)
564
565 results.numPos = 0
566 results.numPosPeaks = 0
567 results.numNeg = 0
568 results.numNegPeaks = 0
569 positive = ""
570 negative = ""
571
572 if results.positive is not None:
573 results.numPos = len(results.positive.getFootprints())
574 results.numPosPeaks = sum(len(fp.getPeaks()) for fp in results.positive.getFootprints())
575 positive = " %d positive peaks in %d footprints" % (results.numPosPeaks, results.numPos)
576 if results.negative is not None:
577 results.numNeg = len(results.negative.getFootprints())
578 results.numNegPeaks = sum(len(fp.getPeaks()) for fp in results.negative.getFootprints())
579 negative = " %d negative peaks in %d footprints" % (results.numNegPeaks, results.numNeg)
580
581 self.log.info("Detected%s%s%s to %g %s",
582 positive, " and" if positive and negative else "", negative,
583 self.config.thresholdValue*self.config.includeThresholdMultiplier*factor,
584 "DN" if self.config.thresholdType == "value" else "sigma")
585
586 def reEstimateBackground(self, maskedImage, backgrounds):
587 """Estimate the background after detection
588
589 Parameters
590 ----------
591 maskedImage : `lsst.afw.image.MaskedImage`
592 Image on which to estimate the background.
593 backgrounds : `lsst.afw.math.BackgroundList`
594 List of backgrounds; modified.
595
596 Returns
597 -------
598 bg : `lsst.afw.math.backgroundMI`
599 Empirical background model.
600 """
601 bg = self.background.fitBackground(maskedImage)
602 if self.config.adjustBackground:
603 self.log.warning("Fiddling the background by %g", self.config.adjustBackground)
604 bg += self.config.adjustBackground
605 self.log.info("Resubtracting the background after object detection")
606 maskedImage -= bg.getImageF(self.background.config.algorithm,
607 self.background.config.undersampleStyle)
608
609 actrl = bg.getBackgroundControl().getApproximateControl()
610 backgrounds.append((bg, getattr(afwMath.Interpolate, self.background.config.algorithm),
611 bg.getAsUsedUndersampleStyle(), actrl.getStyle(), actrl.getOrderX(),
612 actrl.getOrderY(), actrl.getWeighting()))
613 return bg
614
615 def clearUnwantedResults(self, mask, results):
616 """Clear unwanted results from the Struct of results
617
618 If we specifically want only positive or only negative detections,
619 drop the ones we don't want, and its associated mask plane.
620
621 Parameters
622 ----------
623 mask : `lsst.afw.image.Mask`
624 Mask image.
625 results : `lsst.pipe.base.Struct`
626 Detection results, with ``positive`` and ``negative`` elements;
627 modified.
628 """
629 if self.config.thresholdPolarity == "positive":
630 if self.config.reEstimateBackground:
631 mask &= ~mask.getPlaneBitMask("DETECTED_NEGATIVE")
632 results.negative = None
633 elif self.config.thresholdPolarity == "negative":
634 if self.config.reEstimateBackground:
635 mask &= ~mask.getPlaneBitMask("DETECTED")
636 results.positive = None
637
638 @pipeBase.timeMethod
639 def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None):
640 """Detect footprints on an exposure.
641
642 Parameters
643 ----------
644 exposure : `lsst.afw.image.Exposure`
645 Exposure to process; DETECTED{,_NEGATIVE} mask plane will be
646 set in-place.
647 doSmooth : `bool`, optional
648 If True, smooth the image before detection using a Gaussian
649 of width ``sigma``, or the measured PSF width of ``exposure``.
650 Set to False when running on e.g. a pre-convolved image, or a mask
651 plane.
652 sigma : `float`, optional
653 Gaussian Sigma of PSF (pixels); used for smoothing and to grow
654 detections; if `None` then measure the sigma of the PSF of the
655 ``exposure``.
656 clearMask : `bool`, optional
657 Clear both DETECTED and DETECTED_NEGATIVE planes before running
658 detection.
659 expId : `dict`, optional
660 Exposure identifier; unused by this implementation, but used for
661 RNG seed by subclasses.
662
663 Return Struct contents
664 ----------------------
666 Positive polarity footprints (may be `None`)
668 Negative polarity footprints (may be `None`)
669 numPos : `int`
670 Number of footprints in positive or 0 if detection polarity was
671 negative.
672 numNeg : `int`
673 Number of footprints in negative or 0 if detection polarity was
674 positive.
675 background : `lsst.afw.math.BackgroundList`
676 Re-estimated background. `None` if
677 ``reEstimateBackground==False``.
678 factor : `float`
679 Multiplication factor applied to the configured detection
680 threshold.
681 """
682 maskedImage = exposure.maskedImage
683
684 if clearMask:
685 self.clearMaskclearMask(maskedImage.getMask())
686
687 psf = self.getPsfgetPsf(exposure, sigma=sigma)
688 with self.tempWideBackgroundContexttempWideBackgroundContext(exposure):
689 convolveResults = self.convolveImageconvolveImage(maskedImage, psf, doSmooth=doSmooth)
690 middle = convolveResults.middle
691 sigma = convolveResults.sigma
692
693 results = self.applyThresholdapplyThreshold(middle, maskedImage.getBBox())
694 results.background = afwMath.BackgroundList()
695 if self.config.doTempLocalBackground:
696 self.applyTempLocalBackgroundapplyTempLocalBackground(exposure, middle, results)
697 self.finalizeFootprintsfinalizeFootprints(maskedImage.mask, results, sigma)
698
699 if self.config.reEstimateBackground:
700 self.reEstimateBackgroundreEstimateBackground(maskedImage, results.background)
701
702 self.clearUnwantedResultsclearUnwantedResults(maskedImage.getMask(), results)
703 self.displaydisplay(exposure, results, middle)
704
705 return results
706
707 def makeThreshold(self, image, thresholdParity, factor=1.0):
708 """Make an afw.detection.Threshold object corresponding to the task's
709 configuration and the statistics of the given image.
710
711 Parameters
712 ----------
713 image : `afw.image.MaskedImage`
714 Image to measure noise statistics from if needed.
715 thresholdParity: `str`
716 One of "positive" or "negative", to set the kind of fluctuations
717 the Threshold will detect.
718 factor : `float`
719 Factor by which to multiply the configured detection threshold.
720 This is useful for tweaking the detection threshold slightly.
721
722 Returns
723 -------
724 threshold : `lsst.afw.detection.Threshold`
725 Detection threshold.
726 """
727 parity = False if thresholdParity == "negative" else True
728 thresholdValue = self.config.thresholdValue
729 thresholdType = self.config.thresholdType
730 if self.config.thresholdType == 'stdev':
731 bad = image.getMask().getPlaneBitMask(self.config.statsMask)
732 sctrl = afwMath.StatisticsControl()
733 sctrl.setAndMask(bad)
734 stats = afwMath.makeStatistics(image, afwMath.STDEVCLIP, sctrl)
735 thresholdValue *= stats.getValue(afwMath.STDEVCLIP)
736 thresholdType = 'value'
737
738 threshold = afwDet.createThreshold(thresholdValue*factor, thresholdType, parity)
739 threshold.setIncludeMultiplier(self.config.includeThresholdMultiplier)
740 return threshold
741
742 def updatePeaks(self, fpSet, image, threshold):
743 """Update the Peaks in a FootprintSet by detecting new Footprints and
744 Peaks in an image and using the new Peaks instead of the old ones.
745
746 Parameters
747 ----------
749 Set of Footprints whose Peaks should be updated.
750 image : `afw.image.MaskedImage`
751 Image to detect new Footprints and Peak in.
752 threshold : `afw.detection.Threshold`
753 Threshold object for detection.
754
755 Input Footprints with fewer Peaks than self.config.nPeaksMaxSimple
756 are not modified, and if no new Peaks are detected in an input
757 Footprint, the brightest original Peak in that Footprint is kept.
758 """
759 for footprint in fpSet.getFootprints():
760 oldPeaks = footprint.getPeaks()
761 if len(oldPeaks) <= self.config.nPeaksMaxSimple:
762 continue
763 # We detect a new FootprintSet within each non-simple Footprint's
764 # bbox to avoid a big O(N^2) comparison between the two sets of
765 # Footprints.
766 sub = image.Factory(image, footprint.getBBox())
767 fpSetForPeaks = afwDet.FootprintSet(
768 sub,
769 threshold,
770 "", # don't set a mask plane
771 self.config.minPixels
772 )
773 newPeaks = afwDet.PeakCatalog(oldPeaks.getTable())
774 for fpForPeaks in fpSetForPeaks.getFootprints():
775 for peak in fpForPeaks.getPeaks():
776 if footprint.contains(peak.getI()):
777 newPeaks.append(peak)
778 if len(newPeaks) > 0:
779 del oldPeaks[:]
780 oldPeaks.extend(newPeaks)
781 else:
782 del oldPeaks[1:]
783
784 @staticmethod
785 def setEdgeBits(maskedImage, goodBBox, edgeBitmask):
786 """Set the edgeBitmask bits for all of maskedImage outside goodBBox
787
788 Parameters
789 ----------
790 maskedImage : `lsst.afw.image.MaskedImage`
791 Image on which to set edge bits in the mask.
792 goodBBox : `lsst.geom.Box2I`
793 Bounding box of good pixels, in ``LOCAL`` coordinates.
794 edgeBitmask : `lsst.afw.image.MaskPixel`
795 Bit mask to OR with the existing mask bits in the region
796 outside ``goodBBox``.
797 """
798 msk = maskedImage.getMask()
799
800 mx0, my0 = maskedImage.getXY0()
801 for x0, y0, w, h in ([0, 0,
802 msk.getWidth(), goodBBox.getBeginY() - my0],
803 [0, goodBBox.getEndY() - my0, msk.getWidth(),
804 maskedImage.getHeight() - (goodBBox.getEndY() - my0)],
805 [0, 0,
806 goodBBox.getBeginX() - mx0, msk.getHeight()],
807 [goodBBox.getEndX() - mx0, 0,
808 maskedImage.getWidth() - (goodBBox.getEndX() - mx0), msk.getHeight()],
809 ):
810 edgeMask = msk.Factory(msk, lsst.geom.BoxI(lsst.geom.PointI(x0, y0),
811 lsst.geom.ExtentI(w, h)), afwImage.LOCAL)
812 edgeMask |= edgeBitmask
813
814 @contextmanager
815 def tempWideBackgroundContext(self, exposure):
816 """Context manager for removing wide (large-scale) background
817
818 Removing a wide (large-scale) background helps to suppress the
819 detection of large footprints that may overwhelm the deblender.
820 It does, however, set a limit on the maximum scale of objects.
821
822 The background that we remove will be restored upon exit from
823 the context manager.
824
825 Parameters
826 ----------
827 exposure : `lsst.afw.image.Exposure`
828 Exposure on which to remove large-scale background.
829
830 Returns
831 -------
832 context : context manager
833 Context manager that will ensure the temporary wide background
834 is restored.
835 """
836 doTempWideBackground = self.config.doTempWideBackground
837 if doTempWideBackground:
838 self.log.info("Applying temporary wide background subtraction")
839 original = exposure.maskedImage.image.array[:].copy()
840 self.tempWideBackground.run(exposure).background
841 # Remove NO_DATA regions (e.g., edge of the field-of-view); these can cause detections after
842 # subtraction because of extrapolation of the background model into areas with no constraints.
843 image = exposure.maskedImage.image
844 mask = exposure.maskedImage.mask
845 noData = mask.array & mask.getPlaneBitMask("NO_DATA") > 0
846 isGood = mask.array & mask.getPlaneBitMask(self.config.statsMask) == 0
847 image.array[noData] = np.median(image.array[~noData & isGood])
848 try:
849 yield
850 finally:
851 if doTempWideBackground:
852 exposure.maskedImage.image.array[:] = original
853
854
855def addExposures(exposureList):
856 """Add a set of exposures together.
857
858 Parameters
859 ----------
860 exposureList : `list` of `lsst.afw.image.Exposure`
861 Sequence of exposures to add.
862
863 Returns
864 -------
865 addedExposure : `lsst.afw.image.Exposure`
866 An exposure of the same size as each exposure in ``exposureList``,
867 with the metadata from ``exposureList[0]`` and a masked image equal
868 to the sum of all the exposure's masked images.
869 """
870 exposure0 = exposureList[0]
871 image0 = exposure0.getMaskedImage()
872
873 addedImage = image0.Factory(image0, True)
874 addedImage.setXY0(image0.getXY0())
875
876 for exposure in exposureList[1:]:
877 image = exposure.getMaskedImage()
878 addedImage += image
879
880 addedExposure = exposure0.Factory(addedImage, exposure0.getWcs())
881 return addedExposure
def getPsf(self, exposure, sigma=None)
Definition: detection.py:387
def makeThreshold(self, image, thresholdParity, factor=1.0)
Definition: detection.py:707
def run(self, table, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None)
Definition: detection.py:204
def convolveImage(self, maskedImage, psf, doSmooth=True)
Definition: detection.py:414
def applyTempLocalBackground(self, exposure, middle, results)
Definition: detection.py:318
def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None)
Definition: detection.py:639
def reEstimateBackground(self, maskedImage, backgrounds)
Definition: detection.py:586
def updatePeaks(self, fpSet, image, threshold)
Definition: detection.py:742
def finalizeFootprints(self, mask, results, sigma, factor=1.0)
Definition: detection.py:524
def setEdgeBits(maskedImage, goodBBox, edgeBitmask)
Definition: detection.py:785
def display(self, exposure, results, convolvedImage=None)
Definition: detection.py:264
def applyThreshold(self, middle, bbox, factor=1.0)
Definition: detection.py:475
def addExposures(exposureList)
Definition: detection.py:855