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"),
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 updatePeaks(self, fpSet, image, threshold)
def addExposures(exposureList)
Add a set of exposures together.
def makeThreshold(self, image, thresholdParity)
Detect positive and negative sources on an exposure and return a new table.SourceCatalog.
def run(self, table, exposure, doSmooth=True, sigma=None, clearMask=True)
Run source detection and create a SourceCatalog.
Configuration parameters for the SourceDetectionTask.
def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True)
Detect footprints.
def __init__(self, schema=None, kwds)
Create the detection task.
def setEdgeBits(maskedImage, goodBBox, edgeBitmask)
Set the edgeBitmask bits for all of maskedImage outside goodBBox.