24__all__ = (
"PsfGenerationError",
"SourceDetectionConfig",
"SourceDetectionTask",
"addExposures")
26from contextlib
import contextmanager
39from lsst.utils.timer
import timeMethod
40from .subtractBackground
import SubtractBackgroundTask, backgroundFlatContext
44 """Raised when we cannot generate a PSF for detection
50 **kwargs : `dict`, optional
51 Additional keyword arguments to initialize the Exception base class.
60 return f
"{self.msg}: {self.metadata}"
65 if not isinstance(value, (int, float, str)):
66 raise TypeError(f
"{key} is of type {type(value)}, but only (int, float, str) are allowed.")
71 """Configuration parameters for the SourceDetectionTask
73 minPixels = pexConfig.RangeField(
74 doc=
"detected sources with fewer than the specified number of pixels will be ignored",
75 dtype=int, optional=
False, default=1, min=0,
77 isotropicGrow = pexConfig.Field(
78 doc=
"Grow pixels as isotropically as possible? If False, use a Manhattan metric instead.",
79 dtype=bool, default=
True,
81 combinedGrow = pexConfig.Field(
82 doc=
"Grow all footprints at the same time? This allows disconnected footprints to merge.",
83 dtype=bool, default=
True,
85 nSigmaToGrow = pexConfig.Field(
86 doc=
"Grow detections by nSigmaToGrow * [PSF RMS width]; if 0 then do not grow",
87 dtype=float, default=2.4,
89 returnOriginalFootprints = pexConfig.Field(
90 doc=
"Grow detections to set the image mask bits, but return the original (not-grown) footprints",
91 dtype=bool, optional=
False, default=
False,
93 thresholdValue = pexConfig.RangeField(
94 doc=
"Threshold for detecting footprints; exact meaning and units depend on thresholdType.",
95 dtype=float, optional=
False, default=5.0, min=0.0,
97 includeThresholdMultiplier = pexConfig.RangeField(
98 doc=
"Multiplier on thresholdValue for whether a source is included in the output catalog."
99 " For example, thresholdValue=5, includeThresholdMultiplier=10, thresholdType='pixel_stdev' "
100 "results in a catalog of sources at >50 sigma with the detection mask and footprints "
101 "including pixels >5 sigma.",
102 dtype=float, default=1.0, min=0.0,
104 thresholdType = pexConfig.ChoiceField(
105 doc=
"Specifies the meaning of thresholdValue.",
106 dtype=str, optional=
False, default=
"pixel_stdev",
108 "variance":
"threshold applied to image variance",
109 "stdev":
"threshold applied to image std deviation",
110 "value":
"threshold applied to image value",
111 "pixel_stdev":
"threshold applied to per-pixel std deviation",
114 thresholdPolarity = pexConfig.ChoiceField(
115 doc=
"Specifies whether to detect positive, or negative sources, or both.",
116 dtype=str, optional=
False, default=
"positive",
118 "positive":
"detect only positive sources",
119 "negative":
"detect only negative sources",
120 "both":
"detect both positive and negative sources",
123 adjustBackground = pexConfig.Field(
125 doc=
"Fiddle factor to add to the background; debugging only",
128 reEstimateBackground = pexConfig.Field(
130 doc=
"Estimate the background again after final source detection?",
131 default=
True, optional=
False,
133 doApplyFlatBackgroundRatio = pexConfig.Field(
134 doc=
"Convert from a photometrically flat image to one suitable for background subtraction? "
135 "Only used if reEstimateBackground is True."
136 "If True, then a backgroundToPhotometricRatio must be supplied to the task run method.",
140 background = pexConfig.ConfigurableField(
141 doc=
"Background re-estimation; ignored if reEstimateBackground false",
142 target=SubtractBackgroundTask,
144 tempLocalBackground = pexConfig.ConfigurableField(
145 doc=(
"A local (small-scale), temporary background estimation step run between "
146 "detecting above-threshold regions and detecting the peaks within "
147 "them; used to avoid detecting spuerious peaks in the wings."),
148 target=SubtractBackgroundTask,
150 doTempLocalBackground = pexConfig.Field(
152 doc=
"Enable temporary local background subtraction? (see tempLocalBackground)",
155 tempWideBackground = pexConfig.ConfigurableField(
156 doc=(
"A wide (large-scale) background estimation and removal before footprint and peak detection. "
157 "It is added back into the image after detection. The purpose is to suppress very large "
158 "footprints (e.g., from large artifacts) that the deblender may choke on."),
159 target=SubtractBackgroundTask,
161 doTempWideBackground = pexConfig.Field(
163 doc=
"Do temporary wide (large-scale) background subtraction before footprint detection?",
166 nPeaksMaxSimple = pexConfig.Field(
168 doc=(
"The maximum number of peaks in a Footprint before trying to "
169 "replace its peaks using the temporary local background"),
172 nSigmaForKernel = pexConfig.Field(
174 doc=(
"Multiple of PSF RMS size to use for convolution kernel bounding box size; "
175 "note that this is not a half-size. The size will be rounded up to the nearest odd integer"),
178 statsMask = pexConfig.ListField(
180 doc=
"Mask planes to ignore when calculating statistics of image (for thresholdType=stdev)",
181 default=[
'BAD',
'SAT',
'EDGE',
'NO_DATA'],
183 excludeMaskPlanes = lsst.pex.config.ListField(
186 doc=
"Mask planes to exclude when detecting sources."
199 for maskPlane
in (
"DETECTED",
"DETECTED_NEGATIVE"):
205 """Detect peaks and footprints of sources in an image.
207 This task expects the image to have been background subtracted first.
208 Running detection on images with a non-zero-centered background may result
209 in a single source detected on the entire image containing thousands of
210 peaks, or other pathological outputs.
214 schema : `lsst.afw.table.Schema`
215 Schema object used to create the output `lsst.afw.table.SourceCatalog`
217 Keyword arguments passed to `lsst.pipe.base.Task.__init__`
219 If schema is not None and configured for 'both' detections,
220 a 'flags.negative' field will be added to label detections made with a
225 This task convolves the image with a Gaussian approximation to the PSF,
226 matched to the sigma of the input exposure, because this is separable and
227 fast. The PSF would have to be very non-Gaussian or non-circular for this
228 approximation to have a significant impact on the signal-to-noise of the
231 ConfigClass = SourceDetectionConfig
232 _DefaultName =
"sourceDetection"
235 pipeBase.Task.__init__(self, **kwds)
236 if schema
is not None and self.config.thresholdPolarity ==
"both":
238 "is_negative", type=
"Flag",
239 doc=
"Set if source peak was detected as negative."
242 if self.config.thresholdPolarity ==
"both":
243 self.log.warning(
"Detection polarity set to 'both', but no flag will be "
244 "set to distinguish between positive and negative detections")
246 if self.config.reEstimateBackground:
247 self.makeSubtask(
"background")
248 if self.config.doTempLocalBackground:
249 self.makeSubtask(
"tempLocalBackground")
250 if self.config.doTempWideBackground:
251 self.makeSubtask(
"tempWideBackground")
254 def run(self, table, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None,
255 background=None, backgroundToPhotometricRatio=None):
256 r"""Detect sources and return catalog(s) of detections.
260 table : `lsst.afw.table.SourceTable`
261 Table object that will be used to create the SourceCatalog.
262 exposure : `lsst.afw.image.Exposure`
263 Exposure to process; DETECTED mask plane will be set in-place.
264 doSmooth : `bool`, optional
265 If True, smooth the image before detection using a Gaussian of width
266 ``sigma``, or the measured PSF width. Set to False when running on
267 e.g. a pre-convolved image, or a mask plane.
268 sigma : `float`, optional
269 Sigma of PSF (pixels); used for smoothing and to grow detections;
270 if None then measure the sigma of the PSF of the exposure
271 clearMask : `bool`, optional
272 Clear DETECTED{,_NEGATIVE} planes before running detection.
273 expId : `int`, optional
274 Exposure identifier; unused by this implementation, but used for
275 RNG seed by subclasses.
276 background : `lsst.afw.math.BackgroundList`, optional
277 Background that was already subtracted from the exposure; will be
278 modified in-place if ``reEstimateBackground=True``.
279 backgroundToPhotometricRatio : `lsst.afw.image.Image`, optional
280 Image to convert photometric-flattened image to
281 background-flattened image if ``reEstimateBackground=True`` and
282 exposure has been photometric-flattened.
286 result : `lsst.pipe.base.Struct`
287 The `~lsst.pipe.base.Struct` contains:
290 Detected sources on the exposure.
291 (`lsst.afw.table.SourceCatalog`)
293 Positive polarity footprints.
294 (`lsst.afw.detection.FootprintSet` or `None`)
296 Negative polarity footprints.
297 (`lsst.afw.detection.FootprintSet` or `None`)
299 Number of footprints in positive or 0 if detection polarity was
302 Number of footprints in negative or 0 if detection polarity was
305 Re-estimated background. `None` if
306 ``reEstimateBackground==False``.
307 (`lsst.afw.math.BackgroundList`)
309 Multiplication factor applied to the configured detection
315 Raised if flags.negative is needed, but isn't in table's schema.
316 lsst.pipe.base.TaskError
317 Raised if sigma=None, doSmooth=True and the exposure has no PSF.
321 If you want to avoid dealing with Sources and Tables, you can use
322 `detectFootprints()` to just get the
323 `~lsst.afw.detection.FootprintSet`\s.
326 raise ValueError(
"Table has incorrect Schema")
327 results = self.
detectFootprints(exposure=exposure, doSmooth=doSmooth, sigma=sigma,
328 clearMask=clearMask, expId=expId, background=background,
329 backgroundToPhotometricRatio=backgroundToPhotometricRatio)
330 sources = afwTable.SourceCatalog(table)
331 sources.reserve(results.numPos + results.numNeg)
333 results.negative.makeSources(sources)
335 for record
in sources:
338 results.positive.makeSources(sources)
339 results.sources = sources
342 def display(self, exposure, results, convolvedImage=None):
343 """Display detections if so configured
345 Displays the ``exposure`` in frame 0, overlays the detection peaks.
347 Requires that ``lsstDebug`` has been set up correctly, so that
348 ``lsstDebug.Info("lsst.meas.algorithms.detection")`` evaluates `True`.
350 If the ``convolvedImage`` is non-`None` and
351 ``lsstDebug.Info("lsst.meas.algorithms.detection") > 1``, the
352 ``convolvedImage`` will be displayed in frame 1.
356 exposure : `lsst.afw.image.Exposure`
357 Exposure to display, on which will be plotted the detections.
358 results : `lsst.pipe.base.Struct`
359 Results of the 'detectFootprints' method, containing positive and
360 negative footprints (which contain the peak positions that we will
361 plot). This is a `Struct` with ``positive`` and ``negative``
362 elements that are of type `lsst.afw.detection.FootprintSet`.
363 convolvedImage : `lsst.afw.image.Image`, optional
364 Convolved image used for thresholding.
377 afwDisplay.setDefaultMaskTransparency(75)
379 disp0 = afwDisplay.Display(frame=0)
380 disp0.mtv(exposure, title=
"detection")
382 def plotPeaks(fps, ctype):
385 with disp0.Buffering():
386 for fp
in fps.getFootprints():
387 for pp
in fp.getPeaks():
388 disp0.dot(
"+", pp.getFx(), pp.getFy(), ctype=ctype)
389 plotPeaks(results.positive,
"yellow")
390 plotPeaks(results.negative,
"red")
392 if convolvedImage
and display > 1:
393 disp1 = afwDisplay.Display(frame=1)
394 disp1.mtv(convolvedImage, title=
"PSF smoothed")
396 disp2 = afwDisplay.Display(frame=2)
397 disp2.mtv(afwImage.ImageF(np.sqrt(exposure.variance.array)), title=
"stddev")
400 """Apply a temporary local background subtraction
402 This temporary local background serves to suppress noise fluctuations
403 in the wings of bright objects.
405 Peaks in the footprints will be updated.
409 exposure : `lsst.afw.image.Exposure`
410 Exposure for which to fit local background.
411 middle : `lsst.afw.image.MaskedImage`
412 Convolved image on which detection will be performed
413 (typically smaller than ``exposure`` because the
414 half-kernel has been removed around the edges).
415 results : `lsst.pipe.base.Struct`
416 Results of the 'detectFootprints' method, containing positive and
417 negative footprints (which contain the peak positions that we will
418 plot). This is a `Struct` with ``positive`` and ``negative``
419 elements that are of type `lsst.afw.detection.FootprintSet`.
424 bg = self.tempLocalBackground.fitBackground(exposure.getMaskedImage())
425 bgImage = bg.getImageF(self.tempLocalBackground.config.algorithm,
426 self.tempLocalBackground.config.undersampleStyle)
427 middle -= bgImage.Factory(bgImage, middle.getBBox())
428 if self.config.thresholdPolarity !=
"negative":
429 results.positiveThreshold = self.
makeThreshold(middle,
"positive")
430 self.
updatePeaks(results.positive, middle, results.positiveThreshold)
431 if self.config.thresholdPolarity !=
"positive":
432 results.negativeThreshold = self.
makeThreshold(middle,
"negative")
433 self.
updatePeaks(results.negative, middle, results.negativeThreshold)
436 """Clear the DETECTED and DETECTED_NEGATIVE mask planes.
438 Removes any previous detection mask in preparation for a new
443 mask : `lsst.afw.image.Mask`
446 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
449 """Calculate the size of the smoothing kernel.
451 Uses the ``nSigmaForKernel`` configuration parameter. Note
452 that that is the full width of the kernel bounding box
453 (so a value of 7 means 3.5 sigma on either side of center).
454 The value will be rounded up to the nearest odd integer.
459 Gaussian sigma of smoothing kernel.
464 Size of the smoothing kernel.
466 return (int(sigma * self.config.nSigmaForKernel + 0.5)//2)*2 + 1
469 """Create a single Gaussian PSF for an exposure.
471 If ``sigma`` is provided, we make a `~lsst.afw.detection.GaussianPsf`
472 with that, otherwise use the sigma from the psf of the ``exposure`` to
473 make the `~lsst.afw.detection.GaussianPsf`.
477 exposure : `lsst.afw.image.Exposure`
478 Exposure from which to retrieve the PSF.
479 sigma : `float`, optional
480 Gaussian sigma to use if provided.
484 psf : `lsst.afw.detection.GaussianPsf`
485 PSF to use for detection.
490 Raised if ``sigma`` is not provided and ``exposure`` does not
491 contain a ``Psf`` object.
494 psf = exposure.getPsf()
496 raise PsfGenerationError(
"Unable to determine PSF to use for detection: no sigma provided")
497 sigma = psf.computeShape(psf.getAveragePosition()).getDeterminantRadius()
498 if not np.isfinite(sigma)
or sigma <= 0.0:
501 psf = afwDet.GaussianPsf(size, size, sigma)
505 """Convolve the image with the PSF.
507 We convolve the image with a Gaussian approximation to the PSF,
508 because this is separable and therefore fast. It's technically a
509 correlation rather than a convolution, but since we use a symmetric
510 Gaussian there's no difference.
512 The convolution can be disabled with ``doSmooth=False``. If we do
513 convolve, we mask the edges as ``EDGE`` and return the convolved image
514 with the edges removed. This is because we can't convolve the edges
515 because the kernel would extend off the image.
519 maskedImage : `lsst.afw.image.MaskedImage`
521 psf : `lsst.afw.detection.Psf`
522 PSF to convolve with (actually with a Gaussian approximation
525 Actually do the convolution? Set to False when running on
526 e.g. a pre-convolved image, or a mask plane.
530 results : `lsst.pipe.base.Struct`
531 The `~lsst.pipe.base.Struct` contains:
534 Convolved image, without the edges. (`lsst.afw.image.MaskedImage`)
536 Gaussian sigma used for the convolution. (`float`)
538 self.metadata[
"doSmooth"] = doSmooth
539 sigma = psf.computeShape(psf.getAveragePosition()).getDeterminantRadius()
540 self.metadata[
"sigma"] = sigma
543 middle = maskedImage.Factory(maskedImage, deep=
True)
544 return pipeBase.Struct(middle=middle, sigma=sigma)
549 self.metadata[
"smoothingKernelWidth"] = kWidth
550 gaussFunc = afwMath.GaussianFunction1D(sigma)
551 gaussKernel = afwMath.SeparableKernel(kWidth, kWidth, gaussFunc, gaussFunc)
553 convolvedImage = maskedImage.Factory(maskedImage.getBBox())
555 afwMath.convolve(convolvedImage, maskedImage, gaussKernel, afwMath.ConvolutionControl())
558 goodBBox = gaussKernel.shrinkBBox(convolvedImage.getBBox())
559 middle = convolvedImage.Factory(convolvedImage, goodBBox, afwImage.PARENT,
False)
562 self.
setEdgeBits(maskedImage, goodBBox, maskedImage.getMask().getPlaneBitMask(
"EDGE"))
564 return pipeBase.Struct(middle=middle, sigma=sigma)
567 r"""Apply thresholds to the convolved image
569 Identifies `~lsst.afw.detection.Footprint`\s, both positive and negative.
570 The threshold can be modified by the provided multiplication
575 middle : `lsst.afw.image.MaskedImage`
576 Convolved image to threshold.
577 bbox : `lsst.geom.Box2I`
578 Bounding box of unconvolved image.
579 factor : `float`, optional
580 Multiplier for the configured threshold.
581 factorNeg : `float` or `None`, optional
582 Multiplier for the configured threshold for negative detection
583 polarity. If `None`, will be set equal to ``factor`` (i.e. equal
584 to the factor used for positive detection polarity).
588 results : `lsst.pipe.base.Struct`
589 The `~lsst.pipe.base.Struct` contains:
592 Positive detection footprints, if configured.
593 (`lsst.afw.detection.FootprintSet` or `None`)
595 Negative detection footprints, if configured.
596 (`lsst.afw.detection.FootprintSet` or `None`)
598 Multiplier for the configured threshold for positive detection
601 Multiplier for the configured threshold for negative detection
604 if factorNeg
is None:
606 self.log.info(
"Threshold scaling factor for positive detections is: %.3f. For negative "
607 "detections it is: %.3f", factor, factorNeg)
608 results = pipeBase.Struct(positive=
None, negative=
None, factor=factor, factorNeg=factorNeg,
609 positiveThreshold=
None, negativeThreshold=
None)
611 if self.config.reEstimateBackground
or self.config.thresholdPolarity !=
"negative":
612 results.positiveThreshold = self.
makeThreshold(middle,
"positive", factor=factor)
613 results.positive = afwDet.FootprintSet(
615 results.positiveThreshold,
617 self.config.minPixels
619 results.positive.setRegion(bbox)
620 if self.config.reEstimateBackground
or self.config.thresholdPolarity !=
"positive":
621 results.negativeThreshold = self.
makeThreshold(middle,
"negative", factor=factorNeg)
622 results.negative = afwDet.FootprintSet(
624 results.negativeThreshold,
626 self.config.minPixels
628 results.negative.setRegion(bbox)
633 """Finalize the detected footprints.
635 Grow the footprints, set the ``DETECTED`` and ``DETECTED_NEGATIVE``
636 mask planes, and log the results.
638 ``numPos`` (number of positive footprints), ``numPosPeaks`` (number
639 of positive peaks), ``numNeg`` (number of negative footprints),
640 ``numNegPeaks`` (number of negative peaks) entries are added to the
645 mask : `lsst.afw.image.Mask`
646 Mask image on which to flag detected pixels.
647 results : `lsst.pipe.base.Struct`
648 Struct of detection results, including ``positive`` and
649 ``negative`` entries; modified.
651 Gaussian sigma of PSF.
652 factor : `float`, optional
653 Multiplier for the configured threshold. Note that this is only
654 used here for logging purposes.
655 factorNeg : `float` or `None`, optional
656 Multiplier used for the negative detection polarity threshold.
657 If `None`, a factor equal to ``factor`` (i.e. equal to the one used
658 for positive detection polarity) is assumed. Note that this is only
659 used here for logging purposes.
660 growOverride : `float` or `None`, optional
661 Override to use for ``nSigmaToGrow``, regardless of the value set
662 in ``config.nSigmaToGrow``.
664 if growOverride
is not None:
665 self.log.warning(
"config.nSigmaToGrow is set to %.2f, but the caller has set "
666 "growOverride to %.2f, so the footprints will be grown by "
667 "%.2f sigma.", self.config.nSigmaToGrow, growOverride, growOverride)
668 nSigmaToGrow = growOverride
670 nSigmaToGrow = self.config.nSigmaToGrow
671 factorNeg = factor
if factorNeg
is None else factorNeg
672 for polarity, maskName
in ((
"positive",
"DETECTED"), (
"negative",
"DETECTED_NEGATIVE")):
673 fpSet = getattr(results, polarity)
677 nGrow = int((self.config.nSigmaToGrow * sigma) + 0.5)
678 self.metadata[
"nGrow"] = nGrow
679 if self.config.combinedGrow:
680 fpSet = afwDet.FootprintSet(fpSet, nGrow, self.config.isotropicGrow)
682 stencil = (afwGeom.Stencil.CIRCLE
if self.config.isotropicGrow
else
683 afwGeom.Stencil.MANHATTAN)
685 fp.dilate(nGrow, stencil)
686 fpSet.setMask(mask, maskName)
687 if not self.config.returnOriginalFootprints:
688 setattr(results, polarity, fpSet)
691 results.numPosPeaks = 0
693 results.numNegPeaks = 0
697 if results.positive
is not None:
698 results.numPos = len(results.positive.getFootprints())
699 results.numPosPeaks = sum(len(fp.getPeaks())
for fp
in results.positive.getFootprints())
700 positive =
" %d positive peaks in %d footprints" % (results.numPosPeaks, results.numPos)
701 if results.negative
is not None:
702 results.numNeg = len(results.negative.getFootprints())
703 results.numNegPeaks = sum(len(fp.getPeaks())
for fp
in results.negative.getFootprints())
704 negative =
" %d negative peaks in %d footprints" % (results.numNegPeaks, results.numNeg)
706 self.log.info(
"Detected%s%s%s to %g +ve and %g -ve %s",
707 positive,
" and" if positive
and negative
else "", negative,
708 self.config.thresholdValue*self.config.includeThresholdMultiplier*factor,
709 self.config.thresholdValue*self.config.includeThresholdMultiplier*factorNeg,
710 "DN" if self.config.thresholdType ==
"value" else "sigma")
713 """Estimate the background after detection
717 maskedImage : `lsst.afw.image.MaskedImage`
718 Image on which to estimate the background.
719 backgrounds : `lsst.afw.math.BackgroundList`
720 List of backgrounds; modified.
721 backgroundToPhotometricRatio : `lsst.afw.image.Image`, optional
722 Image to multiply a photometrically-flattened image by to obtain a
723 background-flattened image.
724 Only used if ``config.doApplyFlatBackgroundRatio`` is ``True``.
728 bg : `lsst.afw.math.backgroundMI`
729 Empirical background model.
733 self.config.doApplyFlatBackgroundRatio,
734 backgroundToPhotometricRatio=backgroundToPhotometricRatio,
736 bg = self.background.fitBackground(maskedImage)
737 if self.config.adjustBackground:
738 self.log.warning(
"Fiddling the background by %g", self.config.adjustBackground)
739 bg += self.config.adjustBackground
740 self.log.info(
"Resubtracting the background after object detection (median background "
741 "value = %.2f)", np.median(bg.getImageF().array))
742 maskedImage -= bg.getImageF(self.background.config.algorithm,
743 self.background.config.undersampleStyle)
745 actrl = bg.getBackgroundControl().getApproximateControl()
746 backgrounds.append((bg, getattr(afwMath.Interpolate, self.background.config.algorithm),
747 bg.getAsUsedUndersampleStyle(), actrl.getStyle(), actrl.getOrderX(),
748 actrl.getOrderY(), actrl.getWeighting()))
752 """Clear unwanted results from the Struct of results
754 If we specifically want only positive or only negative detections,
755 drop the ones we don't want, and its associated mask plane.
759 mask : `lsst.afw.image.Mask`
761 results : `lsst.pipe.base.Struct`
762 Detection results, with ``positive`` and ``negative`` elements;
765 if self.config.thresholdPolarity ==
"positive":
766 if self.config.reEstimateBackground:
767 mask &= ~mask.getPlaneBitMask(
"DETECTED_NEGATIVE")
768 results.negative =
None
769 elif self.config.thresholdPolarity ==
"negative":
770 if self.config.reEstimateBackground:
771 mask &= ~mask.getPlaneBitMask(
"DETECTED")
772 results.positive =
None
775 def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None,
776 background=None, backgroundToPhotometricRatio=None, factor=1.0, factorNeg=None):
777 """Detect footprints on an exposure.
781 exposure : `lsst.afw.image.Exposure`
782 Exposure to process; DETECTED{,_NEGATIVE} mask plane will be
784 doSmooth : `bool`, optional
785 If True, smooth the image before detection using a Gaussian
786 of width ``sigma``, or the measured PSF width of ``exposure``.
787 Set to False when running on e.g. a pre-convolved image, or a mask
789 sigma : `float`, optional
790 Gaussian Sigma of PSF (pixels); used for smoothing and to grow
791 detections; if `None` then measure the sigma of the PSF of the
793 clearMask : `bool`, optional
794 Clear both DETECTED and DETECTED_NEGATIVE planes before running
796 expId : `dict`, optional
797 Exposure identifier; unused by this implementation, but used for
798 RNG seed by subclasses.
799 background : `lsst.afw.math.BackgroundList`, optional
800 Background that was already subtracted from the exposure; will be
801 modified in-place if ``reEstimateBackground=True``.
802 backgroundToPhotometricRatio : `lsst.afw.image.Image`, optional
803 Image to convert photometric-flattened image to
804 background-flattened image if ``reEstimateBackground=True`` and
805 exposure has been photometric-flattened.
806 factor : `float`, optional
807 Multiplier for the configured threshold for positive detection
809 factorNeg : `float` or `None`, optional
810 Multiplier for the configured threshold for negative detection
811 polarity. If `None`, will be set equal to ``factor`` (i.e. equal
812 to the factor used for positive detection polarity).
816 results : `lsst.pipe.base.Struct`
817 A `~lsst.pipe.base.Struct` containing:
820 Positive polarity footprints.
821 (`lsst.afw.detection.FootprintSet` or `None`)
823 Negative polarity footprints.
824 (`lsst.afw.detection.FootprintSet` or `None`)
826 Number of footprints in positive or 0 if detection polarity was
829 Number of footprints in negative or 0 if detection polarity was
832 Re-estimated background. `None` or the input ``background``
833 if ``reEstimateBackground==False``.
834 (`lsst.afw.math.BackgroundList`)
836 Multiplication factor applied to the configured threshold
837 for positive detection polarity. (`float`)
839 Multiplication factor applied to the configured threshold
840 for negative detection polarity. (`float`)
842 maskedImage = exposure.maskedImage
847 psf = self.
getPsf(exposure, sigma=sigma)
849 convolveResults = self.
convolveImage(maskedImage, psf, doSmooth=doSmooth)
850 middle = convolveResults.middle
851 sigma = convolveResults.sigma
854 results = self.
applyThreshold(middle, maskedImage.getBBox(), factor=factor, factorNeg=factorNeg)
855 results.background = background
if background
is not None else afwMath.BackgroundList()
857 if self.config.doTempLocalBackground:
859 self.
finalizeFootprints(maskedImage.mask, results, sigma, factor=factor, factorNeg=factorNeg)
864 results.positive = self.
setPeakSignificance(middle, results.positive, results.positiveThreshold)
865 results.negative = self.
setPeakSignificance(middle, results.negative, results.negativeThreshold,
868 if self.config.reEstimateBackground:
872 backgroundToPhotometricRatio=backgroundToPhotometricRatio,
877 self.
display(exposure, results, middle)
882 """Set the significance of flagged pixels to zero.
886 middle : `lsst.afw.image.Exposure`
887 Score or maximum likelihood difference image.
888 The image plane will be modified in place.
890 badPixelMask = middle.mask.getPlaneBitMask(self.config.excludeMaskPlanes)
891 badPixels = middle.mask.array & badPixelMask > 0
892 middle.image.array[badPixels] = 0
895 """Set the significance of each detected peak to the pixel value divided
896 by the appropriate standard-deviation for ``config.thresholdType``.
898 Only sets significance for "stdev" and "pixel_stdev" thresholdTypes;
899 we leave it undefined for "value" and "variance" as it does not have a
900 well-defined meaning in those cases.
904 exposure : `lsst.afw.image.Exposure`
905 Exposure that footprints were detected on, likely the convolved,
906 local background-subtracted image.
907 footprints : `lsst.afw.detection.FootprintSet`
908 Footprints detected on the image.
909 threshold : `lsst.afw.detection.Threshold`
910 Threshold used to find footprints.
911 negative : `bool`, optional
912 Are we calculating for negative sources?
914 if footprints
is None or footprints.getFootprints() == []:
916 polarity = -1
if negative
else 1
919 mapper = afwTable.SchemaMapper(footprints.getFootprints()[0].peaks.schema)
920 mapper.addMinimalSchema(footprints.getFootprints()[0].peaks.schema)
921 mapper.addOutputField(
"significance", type=float,
922 doc=
"Ratio of peak value to configured standard deviation.")
927 newFootprints = afwDet.FootprintSet(footprints)
928 for old, new
in zip(footprints.getFootprints(), newFootprints.getFootprints()):
929 newPeaks = afwDet.PeakCatalog(mapper.getOutputSchema())
930 newPeaks.extend(old.peaks, mapper=mapper)
931 new.getPeaks().clear()
932 new.setPeakCatalog(newPeaks)
935 if self.config.thresholdType ==
"pixel_stdev":
936 for footprint
in newFootprints.getFootprints():
937 footprint.updatePeakSignificance(exposure.variance, polarity)
938 elif self.config.thresholdType ==
"stdev":
939 sigma = threshold.getValue() / self.config.thresholdValue
940 for footprint
in newFootprints.getFootprints():
941 footprint.updatePeakSignificance(polarity*sigma)
943 for footprint
in newFootprints.getFootprints():
944 for peak
in footprint.peaks:
945 peak[
"significance"] = 0
950 """Make an afw.detection.Threshold object corresponding to the task's
951 configuration and the statistics of the given image.
955 image : `afw.image.MaskedImage`
956 Image to measure noise statistics from if needed.
957 thresholdParity: `str`
958 One of "positive" or "negative", to set the kind of fluctuations
959 the Threshold will detect.
961 Factor by which to multiply the configured detection threshold.
962 This is useful for tweaking the detection threshold slightly.
966 threshold : `lsst.afw.detection.Threshold`
969 parity =
False if thresholdParity ==
"negative" else True
970 thresholdValue = self.config.thresholdValue
971 thresholdType = self.config.thresholdType
972 if self.config.thresholdType ==
'stdev':
973 bad = image.getMask().getPlaneBitMask(self.config.statsMask)
974 sctrl = afwMath.StatisticsControl()
975 sctrl.setAndMask(bad)
976 stats = afwMath.makeStatistics(image, afwMath.STDEVCLIP, sctrl)
977 thresholdValue *= stats.getValue(afwMath.STDEVCLIP)
978 thresholdType =
'value'
980 threshold = afwDet.createThreshold(thresholdValue*factor, thresholdType, parity)
981 threshold.setIncludeMultiplier(self.config.includeThresholdMultiplier)
982 self.log.debug(
"Detection threshold: %s", threshold)
986 """Update the Peaks in a FootprintSet by detecting new Footprints and
987 Peaks in an image and using the new Peaks instead of the old ones.
991 fpSet : `afw.detection.FootprintSet`
992 Set of Footprints whose Peaks should be updated.
993 image : `afw.image.MaskedImage`
994 Image to detect new Footprints and Peak in.
995 threshold : `afw.detection.Threshold`
996 Threshold object for detection.
998 Input Footprints with fewer Peaks than self.config.nPeaksMaxSimple
999 are not modified, and if no new Peaks are detected in an input
1000 Footprint, the brightest original Peak in that Footprint is kept.
1002 for footprint
in fpSet.getFootprints():
1003 oldPeaks = footprint.getPeaks()
1004 if len(oldPeaks) <= self.config.nPeaksMaxSimple:
1009 sub = image.Factory(image, footprint.getBBox())
1010 fpSetForPeaks = afwDet.FootprintSet(
1014 self.config.minPixels
1016 newPeaks = afwDet.PeakCatalog(oldPeaks.getTable())
1017 for fpForPeaks
in fpSetForPeaks.getFootprints():
1018 for peak
in fpForPeaks.getPeaks():
1019 if footprint.contains(peak.getI()):
1020 newPeaks.append(peak)
1021 if len(newPeaks) > 0:
1023 oldPeaks.extend(newPeaks)
1029 """Set the edgeBitmask bits for all of maskedImage outside goodBBox
1033 maskedImage : `lsst.afw.image.MaskedImage`
1034 Image on which to set edge bits in the mask.
1035 goodBBox : `lsst.geom.Box2I`
1036 Bounding box of good pixels, in ``LOCAL`` coordinates.
1037 edgeBitmask : `lsst.afw.image.MaskPixel`
1038 Bit mask to OR with the existing mask bits in the region
1039 outside ``goodBBox``.
1041 msk = maskedImage.getMask()
1043 mx0, my0 = maskedImage.getXY0()
1044 for x0, y0, w, h
in ([0, 0,
1045 msk.getWidth(), goodBBox.getBeginY() - my0],
1046 [0, goodBBox.getEndY() - my0, msk.getWidth(),
1047 maskedImage.getHeight() - (goodBBox.getEndY() - my0)],
1049 goodBBox.getBeginX() - mx0, msk.getHeight()],
1050 [goodBBox.getEndX() - mx0, 0,
1051 maskedImage.getWidth() - (goodBBox.getEndX() - mx0), msk.getHeight()],
1055 edgeMask |= edgeBitmask
1059 """Context manager for removing wide (large-scale) background
1061 Removing a wide (large-scale) background helps to suppress the
1062 detection of large footprints that may overwhelm the deblender.
1063 It does, however, set a limit on the maximum scale of objects.
1065 The background that we remove will be restored upon exit from
1066 the context manager.
1070 exposure : `lsst.afw.image.Exposure`
1071 Exposure on which to remove large-scale background.
1075 context : context manager
1076 Context manager that will ensure the temporary wide background
1079 doTempWideBackground = self.config.doTempWideBackground
1080 if doTempWideBackground:
1081 self.log.info(
"Applying temporary wide background subtraction")
1082 original = exposure.maskedImage.image.array[:].copy()
1083 self.tempWideBackground.
run(exposure).background
1086 image = exposure.maskedImage.image
1087 mask = exposure.maskedImage.mask
1088 noData = mask.array & mask.getPlaneBitMask(
"NO_DATA") > 0
1089 isGood = mask.array & mask.getPlaneBitMask(self.config.statsMask) == 0
1090 image.array[noData] = np.median(image.array[~noData & isGood])
1094 if doTempWideBackground:
1095 exposure.maskedImage.image.array[:] = original
1098def addExposures(exposureList):
1099 """Add a set of exposures together.
1103 exposureList : `list` of `lsst.afw.image.Exposure`
1104 Sequence of exposures to add.
1108 addedExposure : `lsst.afw.image.Exposure`
1109 An exposure of the same size as each exposure in ``exposureList``,
1110 with the metadata from ``exposureList[0]`` and a masked image equal
1111 to the sum of all the exposure's masked images.
1113 exposure0 = exposureList[0]
1114 image0 = exposure0.getMaskedImage()
1116 addedImage = image0.Factory(image0,
True)
1117 addedImage.setXY0(image0.getXY0())
1119 for exposure
in exposureList[1:]:
1120 image = exposure.getMaskedImage()
1123 addedExposure = exposure0.Factory(addedImage, exposure0.getWcs())
1124 return addedExposure
__init__(self, msg, **kwargs)
makeThreshold(self, image, thresholdParity, factor=1.0)
applyTempLocalBackground(self, exposure, middle, results)
calculateKernelSize(self, sigma)
detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None, background=None, backgroundToPhotometricRatio=None, factor=1.0, factorNeg=None)
setEdgeBits(maskedImage, goodBBox, edgeBitmask)
tempWideBackgroundContext(self, exposure)
setPeakSignificance(self, exposure, footprints, threshold, negative=False)
clearUnwantedResults(self, mask, results)
updatePeaks(self, fpSet, image, threshold)
getPsf(self, exposure, sigma=None)
applyThreshold(self, middle, bbox, factor=1.0, factorNeg=None)
convolveImage(self, maskedImage, psf, doSmooth=True)
display(self, exposure, results, convolvedImage=None)
removeBadPixels(self, middle)
__init__(self, schema=None, **kwds)
finalizeFootprints(self, mask, results, sigma, factor=1.0, factorNeg=None, growOverride=None)
run(self, table, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None, background=None, backgroundToPhotometricRatio=None)
reEstimateBackground(self, maskedImage, backgrounds, backgroundToPhotometricRatio=None)
backgroundFlatContext(maskedImage, doApply, backgroundToPhotometricRatio=None)