23 __all__ = (
"SourceDetectionConfig",
"SourceDetectionTask",
"addExposures")
26 import lsst.afw.display.ds9
as ds9
29 import lsst.afw.math
as afwMath
30 import lsst.afw.table
as afwTable
31 import lsst.pex.config
as pexConfig
32 import lsst.pipe.base
as pipeBase
33 from .subtractBackground
import SubtractBackgroundTask
37 """!Configuration parameters for the SourceDetectionTask
39 minPixels = pexConfig.RangeField(
40 doc=
"detected sources with fewer than the specified number of pixels will be ignored",
41 dtype=int, optional=
False, default=1, min=0,
43 isotropicGrow = pexConfig.Field(
44 doc=
"Pixels should be grown as isotropically as possible (slower)",
45 dtype=bool, optional=
False, default=
False,
47 nSigmaToGrow = pexConfig.Field(
48 doc=
"Grow detections by nSigmaToGrow * sigma; if 0 then do not grow",
49 dtype=float, default=2.4,
51 returnOriginalFootprints = pexConfig.Field(
52 doc=
"Grow detections to set the image mask bits, but return the original (not-grown) footprints",
53 dtype=bool, optional=
False, default=
False,
55 thresholdValue = pexConfig.RangeField(
56 doc=
"Threshold for footprints",
57 dtype=float, optional=
False, default=5.0, min=0.0,
59 includeThresholdMultiplier = pexConfig.RangeField(
60 doc=
"Include threshold relative to thresholdValue",
61 dtype=float, default=1.0, min=0.0,
63 thresholdType = pexConfig.ChoiceField(
64 doc=
"specifies the desired flavor of Threshold",
65 dtype=str, optional=
False, default=
"stdev",
67 "variance":
"threshold applied to image variance",
68 "stdev":
"threshold applied to image std deviation",
69 "value":
"threshold applied to image value",
70 "pixel_stdev":
"threshold applied to per-pixel std deviation",
73 thresholdPolarity = pexConfig.ChoiceField(
74 doc=
"specifies whether to detect positive, or negative sources, or both",
75 dtype=str, optional=
False, default=
"positive",
77 "positive":
"detect only positive sources",
78 "negative":
"detect only negative sources",
79 "both":
"detect both positive and negative sources",
82 adjustBackground = pexConfig.Field(
84 doc=
"Fiddle factor to add to the background; debugging only",
87 reEstimateBackground = pexConfig.Field(
89 doc=
"Estimate the background again after final source detection?",
90 default=
True, optional=
False,
92 background = pexConfig.ConfigurableField(
93 doc=
"Background re-estimation; ignored if reEstimateBackground false",
94 target=SubtractBackgroundTask,
96 tempLocalBackground = pexConfig.ConfigurableField(
97 doc=(
"A seperate background estimation and removal before footprint and peak detection. "
98 "It is added back into the image after detection."),
99 target=SubtractBackgroundTask,
101 doTempLocalBackground = pexConfig.Field(
103 doc=
"Do temporary interpolated background subtraction before footprint detection?",
106 nPeaksMaxSimple = pexConfig.Field(
108 doc=(
"The maximum number of peaks in a Footprint before trying to "
109 "replace its peaks using the temporary local background"),
114 self.tempLocalBackground.binSize = 64
115 self.tempLocalBackground.algorithm =
"AKIMA_SPLINE"
116 self.tempLocalBackground.useApprox =
False
128 \anchor SourceDetectionTask_
130 \brief Detect positive and negative sources on an exposure and return a new \link table.SourceCatalog\endlink.
132 \section meas_algorithms_detection_Contents Contents
134 - \ref meas_algorithms_detection_Purpose
135 - \ref meas_algorithms_detection_Initialize
136 - \ref meas_algorithms_detection_Invoke
137 - \ref meas_algorithms_detection_Config
138 - \ref meas_algorithms_detection_Debug
139 - \ref meas_algorithms_detection_Example
141 \section meas_algorithms_detection_Purpose Description
143 \copybrief SourceDetectionTask
145 \section meas_algorithms_detection_Initialize Task initialisation
147 \copydoc \_\_init\_\_
149 \section meas_algorithms_detection_Invoke Invoking the Task
153 \section meas_algorithms_detection_Config Configuration parameters
155 See \ref SourceDetectionConfig
157 \section meas_algorithms_detection_Debug Debug variables
159 The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
160 flag \c -d to import \b debug.py from your \c PYTHONPATH; see \ref baseDebug for more about \b debug.py files.
162 The available variables in SourceDetectionTask are:
166 - If True, display the exposure on ds9's frame 0. +ve detections in blue, -ve detections in cyan
167 - If display > 1, display the convolved exposure on frame 1
170 \section meas_algorithms_detection_Example A complete example of using SourceDetectionTask
172 This code is in \link measAlgTasks.py\endlink in the examples directory, and can be run as \em e.g.
174 examples/measAlgTasks.py --ds9
176 \dontinclude measAlgTasks.py
177 The example also runs the SourceMeasurementTask; see \ref meas_algorithms_measurement_Example for more
180 Import the task (there are some other standard imports; read the file if you're confused)
181 \skipline SourceDetectionTask
183 We need to create our task before processing any data as the task constructor
184 can add an extra column to the schema, but first we need an almost-empty Schema
185 \skipline makeMinimalSchema
186 after which we can call the constructor:
187 \skip SourceDetectionTask.ConfigClass
190 We're now ready to process the data (we could loop over multiple exposures/catalogues using the same
191 task objects). First create the output table:
194 And process the image
196 (You may not be happy that the threshold was set in the config before creating the Task rather than being set
197 separately for each exposure. You \em can reset it just before calling the run method if you must, but we
198 should really implement a better solution).
200 We can then unpack and use the results:
205 To investigate the \ref meas_algorithms_detection_Debug, put something like
209 di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
210 if name == "lsst.meas.algorithms.detection":
215 lsstDebug.Info = DebugInfo
217 into your debug.py file and run measAlgTasks.py with the \c --debug flag.
219 ConfigClass = SourceDetectionConfig
220 _DefaultName =
"sourceDetection"
223 """!Create the detection task. Most arguments are simply passed onto pipe.base.Task.
225 \param schema An lsst::afw::table::Schema used to create the output lsst.afw.table.SourceCatalog
226 \param **kwds Keyword arguments passed to lsst.pipe.base.task.Task.__init__.
228 If schema is not None and configured for 'both' detections,
229 a 'flags.negative' field will be added to label detections made with a
232 \note This task can add fields to the schema, so any code calling this task must ensure that
233 these columns are indeed present in the input match list; see \ref Example
235 pipeBase.Task.__init__(self, **kwds)
236 if schema
is not None and self.config.thresholdPolarity ==
"both":
238 "flags_negative", type=
"Flag",
239 doc=
"set if source was detected as significantly negative"
242 if self.config.thresholdPolarity ==
"both":
243 self.log.warn(
"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")
252 def run(self, table, exposure, doSmooth=True, sigma=None, clearMask=True):
253 """!Run source detection and create a SourceCatalog.
255 \param table lsst.afw.table.SourceTable object that will be used to create the SourceCatalog.
256 \param exposure Exposure to process; DETECTED mask plane will be set in-place.
257 \param doSmooth if True, smooth the image before detection using a Gaussian of width sigma
259 \param sigma sigma of PSF (pixels); used for smoothing and to grow detections;
260 if None then measure the sigma of the PSF of the exposure (default: None)
261 \param clearMask Clear DETECTED{,_NEGATIVE} planes before running detection (default: True)
263 \return a lsst.pipe.base.Struct with:
264 - sources -- an lsst.afw.table.SourceCatalog object
265 - fpSets --- lsst.pipe.base.Struct returned by \link detectFootprints \endlink
267 \throws ValueError if flags.negative is needed, but isn't in table's schema
268 \throws lsst.pipe.base.TaskError if sigma=None, doSmooth=True and the exposure has no PSF
271 If you want to avoid dealing with Sources and Tables, you can use detectFootprints()
272 to just get the afw::detection::FootprintSet%s.
275 raise ValueError(
"Table has incorrect Schema")
276 fpSets = self.
detectFootprints(exposure=exposure, doSmooth=doSmooth, sigma=sigma,
278 sources = afwTable.SourceCatalog(table)
279 table.preallocate(fpSets.numPos + fpSets.numNeg)
281 fpSets.negative.makeSources(sources)
283 for record
in sources:
286 fpSets.positive.makeSources(sources)
287 return pipeBase.Struct(
293 makeSourceCatalog = run
297 """!Detect footprints.
299 \param exposure Exposure to process; DETECTED{,_NEGATIVE} mask plane will be set in-place.
300 \param doSmooth if True, smooth the image before detection using a Gaussian of width sigma
301 \param sigma sigma of PSF (pixels); used for smoothing and to grow detections;
302 if None then measure the sigma of the PSF of the exposure
303 \param clearMask Clear both DETECTED and DETECTED_NEGATIVE planes before running detection
305 \return a lsst.pipe.base.Struct with fields:
306 - positive: lsst.afw.detection.FootprintSet with positive polarity footprints (may be None)
307 - negative: lsst.afw.detection.FootprintSet with negative polarity footprints (may be None)
308 - numPos: number of footprints in positive or 0 if detection polarity was negative
309 - numNeg: number of footprints in negative or 0 if detection polarity was positive
310 - background: re-estimated background. None if reEstimateBackground==False
312 \throws lsst.pipe.base.TaskError if sigma=None and the exposure has no PSF
316 display = lsstDebug.Info(__name__).display
324 raise RuntimeError(
"No exposure for detection")
326 maskedImage = exposure.getMaskedImage()
327 region = maskedImage.getBBox()
330 mask = maskedImage.getMask()
331 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
334 if self.config.doTempLocalBackground:
338 tempBg = self.tempLocalBackground.fitBackground(
339 exposure.getMaskedImage()
341 tempLocalBkgdImage = tempBg.getImageF()
344 psf = exposure.getPsf()
346 raise pipeBase.TaskError(
"exposure has no PSF; must specify sigma")
347 shape = psf.computeShape()
348 sigma = shape.getDeterminantRadius()
350 self.metadata.set(
"sigma", sigma)
351 self.metadata.set(
"doSmooth", doSmooth)
354 convolvedImage = maskedImage.Factory(maskedImage)
355 middle = convolvedImage
359 psf = exposure.getPsf()
360 kWidth = (int(sigma * 7 + 0.5) // 2) * 2 + 1
361 self.metadata.set(
"smoothingKernelWidth", kWidth)
362 gaussFunc = afwMath.GaussianFunction1D(sigma)
363 gaussKernel = afwMath.SeparableKernel(kWidth, kWidth, gaussFunc, gaussFunc)
365 convolvedImage = maskedImage.Factory(maskedImage.getBBox())
367 afwMath.convolve(convolvedImage, maskedImage, gaussKernel, afwMath.ConvolutionControl())
371 goodBBox = gaussKernel.shrinkBBox(convolvedImage.getBBox())
372 middle = convolvedImage.Factory(convolvedImage, goodBBox, afwImage.PARENT,
False)
376 self.
setEdgeBits(maskedImage, goodBBox, maskedImage.getMask().getPlaneBitMask(
"EDGE"))
378 fpSets = pipeBase.Struct(positive=
None, negative=
None)
381 if self.config.thresholdPolarity !=
"negative":
383 fpSets.positive = afwDet.FootprintSet(
387 self.config.minPixels
389 if self.config.reEstimateBackground
or self.config.thresholdPolarity !=
"positive":
391 fpSets.negative = afwDet.FootprintSet(
395 self.config.minPixels
398 if self.config.doTempLocalBackground:
402 tempLocalBkgdImage = tempLocalBkgdImage.Factory(tempLocalBkgdImage,
404 middle -= tempLocalBkgdImage
407 if self.config.thresholdPolarity !=
"negative":
408 self.
updatePeaks(fpSets.positive, middle, thresholdPos)
409 if self.config.thresholdPolarity !=
"positive":
410 self.
updatePeaks(fpSets.negative, middle, thresholdNeg)
412 for polarity, maskName
in ((
"positive",
"DETECTED"), (
"negative",
"DETECTED_NEGATIVE")):
413 fpSet = getattr(fpSets, polarity)
416 fpSet.setRegion(region)
417 if self.config.nSigmaToGrow > 0:
418 nGrow = int((self.config.nSigmaToGrow * sigma) + 0.5)
419 self.metadata.set(
"nGrow", nGrow)
420 fpSet = afwDet.FootprintSet(fpSet, nGrow, self.config.isotropicGrow)
421 fpSet.setMask(maskedImage.getMask(), maskName)
422 if not self.config.returnOriginalFootprints:
423 setattr(fpSets, polarity, fpSet)
425 fpSets.numPos = len(fpSets.positive.getFootprints())
if fpSets.positive
is not None else 0
426 fpSets.numNeg = len(fpSets.negative.getFootprints())
if fpSets.negative
is not None else 0
428 if self.config.thresholdPolarity !=
"negative":
429 self.log.info(
"Detected %d positive sources to %g sigma.",
430 fpSets.numPos, self.config.thresholdValue*self.config.includeThresholdMultiplier)
432 fpSets.background =
None
433 if self.config.reEstimateBackground:
434 mi = exposure.getMaskedImage()
435 bkgd = self.background.fitBackground(mi)
437 if self.config.adjustBackground:
438 self.log.warn(
"Fiddling the background by %g", self.config.adjustBackground)
440 bkgd += self.config.adjustBackground
441 fpSets.background = bkgd
442 self.log.info(
"Resubtracting the background after object detection")
444 mi -= bkgd.getImageF()
447 if self.config.thresholdPolarity ==
"positive":
448 if self.config.reEstimateBackground:
449 mask = maskedImage.getMask()
450 mask &= ~mask.getPlaneBitMask(
"DETECTED_NEGATIVE")
452 fpSets.negative =
None
454 self.log.info(
"Detected %d negative sources to %g %s",
455 fpSets.numNeg, self.config.thresholdValue,
456 (
"DN" if self.config.thresholdType ==
"value" else "sigma"))
459 ds9.mtv(exposure, frame=0, title=
"detection")
460 x0, y0 = exposure.getXY0()
462 def plotPeaks(fps, ctype):
465 with ds9.Buffering():
466 for fp
in fps.getFootprints():
467 for pp
in fp.getPeaks():
468 ds9.dot(
"+", pp.getFx() - x0, pp.getFy() - y0, ctype=ctype)
469 plotPeaks(fpSets.positive,
"yellow")
470 plotPeaks(fpSets.negative,
"red")
472 if convolvedImage
and display
and display > 1:
473 ds9.mtv(convolvedImage, frame=1, title=
"PSF smoothed")
478 """Make an afw.detection.Threshold object corresponding to the task's
479 configuration and the statistics of the given image.
483 image : `afw.image.MaskedImage`
484 Image to measure noise statistics from if needed.
485 thresholdParity: `str`
486 One of "positive" or "negative", to set the kind of fluctuations
487 the Threshold will detect.
489 parity =
False if thresholdParity ==
"negative" else True
490 threshold = afwDet.createThreshold(self.config.thresholdValue,
491 self.config.thresholdType, parity)
492 threshold.setIncludeMultiplier(self.config.includeThresholdMultiplier)
494 if self.config.thresholdType ==
'stdev':
495 bad = image.getMask().getPlaneBitMask([
'BAD',
'SAT',
'EDGE',
497 sctrl = afwMath.StatisticsControl()
498 sctrl.setAndMask(bad)
499 stats = afwMath.makeStatistics(image, afwMath.STDEVCLIP, sctrl)
500 thres = (stats.getValue(afwMath.STDEVCLIP) *
501 self.config.thresholdValue)
502 threshold = afwDet.createThreshold(thres,
'value', parity)
503 threshold.setIncludeMultiplier(
504 self.config.includeThresholdMultiplier
510 """Update the Peaks in a FootprintSet by detecting new Footprints and
511 Peaks in an image and using the new Peaks instead of the old ones.
515 fpSet : `afw.detection.FootprintSet`
516 Set of Footprints whose Peaks should be updated.
517 image : `afw.image.MaskedImage`
518 Image to detect new Footprints and Peak in.
519 threshold : `afw.detection.Threshold`
520 Threshold object for detection.
522 Input Footprints with fewer Peaks than self.config.nPeaksMaxSimple
523 are not modified, and if no new Peaks are detected in an input
524 Footprint, the brightest original Peak in that Footprint is kept.
526 for footprint
in fpSet.getFootprints():
527 oldPeaks = footprint.getPeaks()
528 if len(oldPeaks) <= self.config.nPeaksMaxSimple:
533 sub = image.Factory(image, footprint.getBBox())
534 fpSetForPeaks = afwDet.FootprintSet(
538 self.config.minPixels
540 newPeaks = afwDet.PeakCatalog(oldPeaks.getTable())
541 for fpForPeaks
in fpSetForPeaks.getFootprints():
542 for peak
in fpForPeaks.getPeaks():
543 if footprint.contains(peak.getI()):
544 newPeaks.append(peak)
545 if len(newPeaks) > 0:
547 oldPeaks.extend(newPeaks)
553 """!Set the edgeBitmask bits for all of maskedImage outside goodBBox
555 \param[in,out] maskedImage image on which to set edge bits in the mask
556 \param[in] goodBBox bounding box of good pixels, in LOCAL coordinates
557 \param[in] edgeBitmask bit mask to OR with the existing mask bits in the region outside goodBBox
559 msk = maskedImage.getMask()
561 mx0, my0 = maskedImage.getXY0()
562 for x0, y0, w, h
in ([0, 0,
563 msk.getWidth(), goodBBox.getBeginY() - my0],
564 [0, goodBBox.getEndY() - my0, msk.getWidth(),
565 maskedImage.getHeight() - (goodBBox.getEndY() - my0)],
567 goodBBox.getBeginX() - mx0, msk.getHeight()],
568 [goodBBox.getEndX() - mx0, 0,
569 maskedImage.getWidth() - (goodBBox.getEndX() - mx0), msk.getHeight()],
571 edgeMask = msk.Factory(msk, afwGeom.BoxI(afwGeom.PointI(x0, y0),
572 afwGeom.ExtentI(w, h)), afwImage.LOCAL)
573 edgeMask |= edgeBitmask
577 """!Add a set of exposures together.
579 \param[in] exposureList sequence of exposures to add
581 \return an exposure of the same size as each exposure in exposureList,
582 with the metadata from exposureList[0] and a masked image equal to the
583 sum of all the exposure's masked images.
585 \throw LsstException if the exposures do not all have the same dimensions (but does not check xy0)
587 exposure0 = exposureList[0]
588 image0 = exposure0.getMaskedImage()
590 addedImage = image0.Factory(image0,
True)
591 addedImage.setXY0(image0.getXY0())
593 for exposure
in exposureList[1:]:
594 image = exposure.getMaskedImage()
597 addedExposure = exposure0.Factory(addedImage, exposure0.getWcs())
def detectFootprints
Detect footprints.
def addExposures
Add a set of exposures together.
def setEdgeBits
Set the edgeBitmask bits for all of maskedImage outside goodBBox.
Detect positive and negative sources on an exposure and return a new table.SourceCatalog.
def run
Run source detection and create a SourceCatalog.
Configuration parameters for the SourceDetectionTask.
def __init__
Create the detection task.