33 __all__ = (
"SourceDetectionConfig",
"SourceDetectionTask",
"getBackground",
34 "estimateBackground",
"BackgroundConfig",
"addExposures")
39 """!Config for background estimation
41 statisticsProperty = pexConfig.ChoiceField(
42 doc=
"type of statistic to use for grid points",
43 dtype=str, default=
"MEANCLIP",
45 "MEANCLIP":
"clipped mean",
46 "MEAN":
"unclipped mean",
50 undersampleStyle = pexConfig.ChoiceField(
51 doc=
"behaviour if there are too few points in grid for requested interpolation style",
52 dtype=str, default=
"REDUCE_INTERP_ORDER",
54 "THROW_EXCEPTION":
"throw an exception if there are too few points",
55 "REDUCE_INTERP_ORDER":
"use an interpolation style with a lower order.",
56 "INCREASE_NXNYSAMPLE":
"Increase the number of samples used to make the interpolation grid.",
59 binSize = pexConfig.RangeField(
60 doc=
"how large a region of the sky should be used for each background point",
61 dtype=int, default=256, min=10
63 algorithm = pexConfig.ChoiceField(
64 doc=
"how to interpolate the background values. This maps to an enum; see afw::math::Background",
65 dtype=str, default=
"NATURAL_SPLINE", optional=
True,
67 "CONSTANT" :
"Use a single constant value",
68 "LINEAR" :
"Use linear interpolation",
69 "NATURAL_SPLINE" :
"cubic spline with zero second derivative at endpoints",
70 "AKIMA_SPLINE":
"higher-level nonlinear spline that is more robust to outliers",
71 "NONE":
"No background estimation is to be attempted",
74 ignoredPixelMask = pexConfig.ListField(
75 doc=
"Names of mask planes to ignore while estimating the background",
76 dtype=str, default = [
"BAD",
"EDGE",
"DETECTED",
"DETECTED_NEGATIVE",
"NO_DATA",],
77 itemCheck =
lambda x: x
in afwImage.MaskU().getMaskPlaneDict().keys(),
79 isNanSafe = pexConfig.Field(
80 doc=
"Ignore NaNs when estimating the background",
81 dtype=bool, default=
False,
84 useApprox = pexConfig.Field(
85 doc=
"Use Approximate (Chebyshev) to model background.",
86 dtype=bool, default=
False,
88 approxOrderX = pexConfig.Field(
89 doc=
"Approximation order in X for background Chebyshev (valid only with useApprox=True)",
95 approxOrderY = pexConfig.Field(
96 doc=
"Approximation order in Y for background Chebyshev (valid only with useApprox=True)",
97 dtype=int, default=-1,
99 weighting = pexConfig.Field(
100 doc=
"Use inverse variance weighting in calculation (valid only with useApprox=True)",
101 dtype=bool, default=
True,
105 pexConfig.Config.validate(self)
111 """!Configuration parameters for the SourceDetectionTask
113 minPixels = pexConfig.RangeField(
114 doc=
"detected sources with fewer than the specified number of pixels will be ignored",
115 dtype=int, optional=
False, default=1, min=0,
117 isotropicGrow = pexConfig.Field(
118 doc=
"Pixels should be grown as isotropically as possible (slower)",
119 dtype=bool, optional=
False, default=
False,
121 nSigmaToGrow = pexConfig.Field(
122 doc=
"Grow detections by nSigmaToGrow * sigma; if 0 then do not grow",
123 dtype=float, default=2.4,
125 returnOriginalFootprints = pexConfig.Field(
126 doc=
"Grow detections to set the image mask bits, but return the original (not-grown) footprints",
127 dtype=bool, optional=
False, default=
True
129 thresholdValue = pexConfig.RangeField(
130 doc=
"Threshold for footprints",
131 dtype=float, optional=
False, default=5.0, min=0.0,
133 includeThresholdMultiplier = pexConfig.RangeField(
134 doc=
"Include threshold relative to thresholdValue",
135 dtype=float, default=1.0, min=0.0,
137 thresholdType = pexConfig.ChoiceField(
138 doc=
"specifies the desired flavor of Threshold",
139 dtype=str, optional=
False, default=
"stdev",
141 "variance":
"threshold applied to image variance",
142 "stdev":
"threshold applied to image std deviation",
143 "value":
"threshold applied to image value",
144 "pixel_stdev":
"threshold applied to per-pixel std deviation",
147 thresholdPolarity = pexConfig.ChoiceField(
148 doc=
"specifies whether to detect positive, or negative sources, or both",
149 dtype=str, optional=
False, default=
"positive",
151 "positive":
"detect only positive sources",
152 "negative":
"detect only negative sources",
153 "both":
"detect both positive and negative sources",
156 adjustBackground = pexConfig.Field(
158 doc =
"Fiddle factor to add to the background; debugging only",
161 reEstimateBackground = pexConfig.Field(
163 doc =
"Estimate the background again after final source detection?",
164 default=
True, optional=
False,
166 background = pexConfig.ConfigField(
167 dtype=BackgroundConfig,
168 doc=
"Background re-estimation configuration"
180 \anchor SourceDetectionTask_
182 \brief Detect positive and negative sources on an exposure and return a new \link table.SourceCatalog\endlink.
184 \section meas_algorithms_detection_Contents Contents
186 - \ref meas_algorithms_detection_Purpose
187 - \ref meas_algorithms_detection_Initialize
188 - \ref meas_algorithms_detection_Invoke
189 - \ref meas_algorithms_detection_Config
190 - \ref meas_algorithms_detection_Debug
191 - \ref meas_algorithms_detection_Example
193 \section meas_algorithms_detection_Purpose Description
195 \copybrief SourceDetectionTask
197 \section meas_algorithms_detection_Initialize Task initialisation
201 \section meas_algorithms_detection_Invoke Invoking the Task
205 \section meas_algorithms_detection_Config Configuration parameters
207 See \ref SourceDetectionConfig
209 \section meas_algorithms_detection_Debug Debug variables
211 The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
212 flag \c -d to import \b debug.py from your \c PYTHONPATH; see \ref baseDebug for more about \b debug.py files.
214 The available variables in SourceDetectionTask are:
218 - If True, display the exposure on ds9's frame 0. +ve detections in blue, -ve detections in cyan
219 - If display > 1, display the convolved exposure on frame 1
222 \section meas_algorithms_detection_Example A complete example of using SourceDetectionTask
224 This code is in \link measAlgTasks.py\endlink in the examples directory, and can be run as \em e.g.
226 examples/measAlgTasks.py --ds9
228 \dontinclude measAlgTasks.py
229 The example also runs the SourceMeasurementTask; see \ref meas_algorithms_measurement_Example for more explanation.
231 Import the task (there are some other standard imports; read the file if you're confused)
232 \skipline SourceDetectionTask
234 We need to create our task before processing any data as the task constructor
235 can add an extra column to the schema, but first we need an almost-empty Schema
236 \skipline makeMinimalSchema
237 after which we can call the constructor:
238 \skip SourceDetectionTask.ConfigClass
241 We're now ready to process the data (we could loop over multiple exposures/catalogues using the same
242 task objects). First create the output table:
245 And process the image
247 (You may not be happy that the threshold was set in the config before creating the Task rather than being set
248 separately for each exposure. You \em can reset it just before calling the run method if you must, but we
249 should really implement a better solution).
251 We can then unpack and use the results:
256 To investigate the \ref meas_algorithms_detection_Debug, put something like
260 di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
261 if name == "lsst.meas.algorithms.detection":
266 lsstDebug.Info = DebugInfo
268 into your debug.py file and run measAlgTasks.py with the \c --debug flag.
270 ConfigClass = SourceDetectionConfig
271 _DefaultName =
"sourceDetection"
274 def init(self, schema=None, **kwds):
275 """!Create the detection task. Most arguments are simply passed onto pipe.base.Task.
277 \param schema An lsst::afw::table::Schema used to create the output lsst.afw.table.SourceCatalog
278 \param **kwds Keyword arguments passed to lsst.pipe.base.task.Task.__init__.
280 If schema is not None, a 'flags.negative' field will be added to label detections
281 made with a negative threshold.
283 \note This task can add fields to the schema, so any code calling this task must ensure that
284 these columns are indeed present in the input match list; see \ref Example
289 """!Create the detection task. See SourceDetectionTask.init for documentation
291 pipeBase.Task.__init__(self, **kwds)
292 if schema
is not None:
294 "flags_negative", type=
"Flag",
295 doc=
"set if source was detected as significantly negative"
298 if self.config.thresholdPolarity ==
"both":
299 self.log.log(self.log.WARN,
"Detection polarity set to 'both', but no flag will be "\
300 "set to distinguish between positive and negative detections")
304 def run(self, table, exposure, doSmooth=True, sigma=None, clearMask=True):
305 """!Run source detection and create a SourceCatalog.
307 \param table lsst.afw.table.SourceTable object that will be used to create the SourceCatalog.
308 \param exposure Exposure to process; DETECTED mask plane will be set in-place.
309 \param doSmooth if True, smooth the image before detection using a Gaussian of width sigma (default: True)
310 \param sigma sigma of PSF (pixels); used for smoothing and to grow detections;
311 if None then measure the sigma of the PSF of the exposure (default: None)
312 \param clearMask Clear DETECTED{,_NEGATIVE} planes before running detection (default: True)
314 \return a lsst.pipe.base.Struct with:
315 - sources -- an lsst.afw.table.SourceCatalog object
316 - fpSets --- lsst.pipe.base.Struct returned by \link detectFootprints \endlink
318 \throws ValueError if flags.negative is needed, but isn't in table's schema
319 \throws lsst.pipe.base.TaskError if sigma=None, doSmooth=True and the exposure has no PSF
322 If you want to avoid dealing with Sources and Tables, you can use detectFootprints()
323 to just get the afw::detection::FootprintSet%s.
326 raise ValueError(
"Table has incorrect Schema")
327 fpSets = self.
detectFootprints(exposure=exposure, doSmooth=doSmooth, sigma=sigma,
330 table.preallocate(fpSets.numPos + fpSets.numNeg)
332 fpSets.negative.makeSources(sources)
334 for record
in sources:
337 fpSets.positive.makeSources(sources)
338 return pipeBase.Struct(
344 makeSourceCatalog = run
348 """!Detect footprints.
350 \param exposure Exposure to process; DETECTED{,_NEGATIVE} mask plane will be set in-place.
351 \param doSmooth if True, smooth the image before detection using a Gaussian of width sigma
352 \param sigma sigma of PSF (pixels); used for smoothing and to grow detections;
353 if None then measure the sigma of the PSF of the exposure
354 \param clearMask Clear both DETECTED and DETECTED_NEGATIVE planes before running detection
356 \return a lsst.pipe.base.Struct with fields:
357 - positive: lsst.afw.detection.FootprintSet with positive polarity footprints (may be None)
358 - negative: lsst.afw.detection.FootprintSet with negative polarity footprints (may be None)
359 - numPos: number of footprints in positive or 0 if detection polarity was negative
360 - numNeg: number of footprints in negative or 0 if detection polarity was positive
361 - background: re-estimated background. None if reEstimateBackground==False
363 \throws lsst.pipe.base.TaskError if sigma=None and the exposure has no PSF
375 raise RuntimeError(
"No exposure for detection")
377 maskedImage = exposure.getMaskedImage()
378 region = maskedImage.getBBox()
381 mask = maskedImage.getMask()
382 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
386 psf = exposure.getPsf()
388 raise pipeBase.TaskError(
"exposure has no PSF; must specify sigma")
389 shape = psf.computeShape()
390 sigma = shape.getDeterminantRadius()
392 self.metadata.set(
"sigma", sigma)
393 self.metadata.set(
"doSmooth", doSmooth)
396 convolvedImage = maskedImage.Factory(maskedImage)
397 middle = convolvedImage
401 psf = exposure.getPsf()
402 kWidth = (int(sigma * 7 + 0.5) // 2) * 2 + 1
403 self.metadata.set(
"smoothingKernelWidth", kWidth)
404 gaussFunc = afwMath.GaussianFunction1D(sigma)
407 convolvedImage = maskedImage.Factory(maskedImage.getBBox())
413 goodBBox = gaussKernel.shrinkBBox(convolvedImage.getBBox())
414 middle = convolvedImage.Factory(convolvedImage, goodBBox, afwImage.PARENT,
False)
418 self.
setEdgeBits(maskedImage, goodBBox, maskedImage.getMask().getPlaneBitMask(
"EDGE"))
420 fpSets = pipeBase.Struct(positive=
None, negative=
None)
422 if self.config.thresholdPolarity !=
"negative":
424 if self.config.reEstimateBackground
or self.config.thresholdPolarity !=
"positive":
427 for polarity, maskName
in ((
"positive",
"DETECTED"), (
"negative",
"DETECTED_NEGATIVE")):
428 fpSet = getattr(fpSets, polarity)
431 fpSet.setRegion(region)
432 if self.config.nSigmaToGrow > 0:
433 nGrow = int((self.config.nSigmaToGrow * sigma) + 0.5)
434 self.metadata.set(
"nGrow", nGrow)
436 fpSet.setMask(maskedImage.getMask(), maskName)
437 if not self.config.returnOriginalFootprints:
438 setattr(fpSets, polarity, fpSet)
440 fpSets.numPos = len(fpSets.positive.getFootprints())
if fpSets.positive
is not None else 0
441 fpSets.numNeg = len(fpSets.negative.getFootprints())
if fpSets.negative
is not None else 0
443 if self.config.thresholdPolarity !=
"negative":
444 self.log.log(self.log.INFO,
"Detected %d positive sources to %g sigma." %
445 (fpSets.numPos, self.config.thresholdValue))
447 fpSets.background =
None
448 if self.config.reEstimateBackground:
449 mi = exposure.getMaskedImage()
452 if self.config.adjustBackground:
453 self.log.log(self.log.WARN,
"Fiddling the background by %g" % self.config.adjustBackground)
455 bkgd += self.config.adjustBackground
456 fpSets.background = bkgd
457 self.log.log(self.log.INFO,
"Resubtracting the background after object detection")
459 mi -= bkgd.getImageF()
462 if self.config.thresholdPolarity ==
"positive":
463 if self.config.reEstimateBackground:
464 mask = maskedImage.getMask()
465 mask &= ~mask.getPlaneBitMask(
"DETECTED_NEGATIVE")
467 fpSets.negative =
None
469 self.log.log(self.log.INFO,
"Detected %d negative sources to %g %s" %
470 (fpSets.numNeg, self.config.thresholdValue,
471 (
"DN" if self.config.thresholdType ==
"value" else "sigma")))
474 ds9.mtv(exposure, frame=0, title=
"detection")
476 if convolvedImage
and display
and display > 1:
477 ds9.mtv(convolvedImage, frame=1, title=
"PSF smoothed")
482 """!Threshold the convolved image, returning a FootprintSet.
483 Helper function for detect().
485 \param image The (optionally convolved) MaskedImage to threshold
486 \param thresholdParity Parity of threshold
487 \param maskName Name of mask to set
491 parity =
False if thresholdParity ==
"negative" else True
493 threshold.setIncludeMultiplier(self.config.includeThresholdMultiplier)
495 if self.config.thresholdType ==
'stdev':
496 bad = image.getMask().getPlaneBitMask([
'BAD',
'SAT',
'EDGE',
'NO_DATA',])
498 sctrl.setAndMask(bad)
500 thres = stats.getValue(afwMath.STDEVCLIP) * self.config.thresholdValue
502 threshold.setIncludeMultiplier(self.config.includeThresholdMultiplier)
509 """!Set the edgeBitmask bits for all of maskedImage outside goodBBox
511 \param[in,out] maskedImage image on which to set edge bits in the mask
512 \param[in] goodBBox bounding box of good pixels, in LOCAL coordinates
513 \param[in] edgeBitmask bit mask to OR with the existing mask bits in the region outside goodBBox
515 msk = maskedImage.getMask()
517 mx0, my0 = maskedImage.getXY0()
518 for x0, y0, w, h
in ([0, 0,
519 msk.getWidth(), goodBBox.getBeginY() - my0],
520 [0, goodBBox.getEndY() - my0, msk.getWidth(),
521 maskedImage.getHeight() - (goodBBox.getEndY() - my0)],
523 goodBBox.getBeginX() - mx0, msk.getHeight()],
524 [goodBBox.getEndX() - mx0, 0,
525 maskedImage.getWidth() - (goodBBox.getEndX() - mx0), msk.getHeight()],
529 edgeMask |= edgeBitmask
532 """!Add a set of exposures together.
534 \param[in] exposureList sequence of exposures to add
536 \return an exposure of the same size as each exposure in exposureList,
537 with the metadata from exposureList[0] and a masked image equal to the
538 sum of all the exposure's masked images.
540 \throw LsstException if the exposures do not all have the same dimensions (but does not check xy0)
542 exposure0 = exposureList[0]
543 image0 = exposure0.getMaskedImage()
545 addedImage = image0.Factory(image0,
True)
546 addedImage.setXY0(image0.getXY0())
548 for exposure
in exposureList[1:]:
549 image = exposure.getMaskedImage()
552 addedExposure = exposure0.Factory(addedImage, exposure0.getWcs())
556 """!Estimate the background of an image (a thin layer on lsst.afw.math.makeBackground)
558 \param[in] image image whose background is to be computed
559 \param[in] backgroundConfig configuration (a BackgroundConfig)
560 \param[in] nx number of x bands; 0 for default
561 \param[in] ny number of y bands; 0 for default
562 \param[in] algorithm name of interpolation algorithm; see lsst.afw.math.BackgroundControl for details
564 backgroundConfig.validate();
566 logger = pexLogging.getDefaultLog()
567 logger =
pexLogging.Log(logger,
"lsst.meas.algorithms.detection.getBackground")
570 nx = image.getWidth()//backgroundConfig.binSize + 1
572 ny = image.getHeight()//backgroundConfig.binSize + 1
575 sctrl.setAndMask(reduce(
lambda x, y: x | image.getMask().getPlaneBitMask(y),
576 backgroundConfig.ignoredPixelMask, 0x0))
577 sctrl.setNanSafe(backgroundConfig.isNanSafe)
580 pl.debug(3,
"Ignoring mask planes: %s" %
", ".join(backgroundConfig.ignoredPixelMask))
583 algorithm = backgroundConfig.algorithm
586 backgroundConfig.undersampleStyle, sctrl,
587 backgroundConfig.statisticsProperty)
602 if backgroundConfig.useApprox:
603 if not backgroundConfig.approxOrderY
in (backgroundConfig.approxOrderX,-1):
604 raise ValueError(
"Error: approxOrderY not in (approxOrderX, -1)")
605 order = backgroundConfig.approxOrderX
606 minNumberGridPoints = backgroundConfig.approxOrderX + 1
607 if min(nx,ny) <= backgroundConfig.approxOrderX:
608 logger.warn(
"Too few points in grid to constrain fit: min(nx, ny) < approxOrder) "+
609 "[min(%d, %d) < %d]" % (nx, ny, backgroundConfig.approxOrderX))
610 if backgroundConfig.undersampleStyle ==
"THROW_EXCEPTION":
611 raise ValueError(
"Too few points in grid (%d, %d) for order (%d) and binsize (%d)" % (
612 nx, ny, backgroundConfig.approxOrderX, backgroundConfig.binSize))
613 elif backgroundConfig.undersampleStyle ==
"REDUCE_INTERP_ORDER":
615 raise ValueError(
"Cannot reduce approxOrder below 0. " +
616 "Try using undersampleStyle = \"INCREASE_NXNYSAMPLE\" instead?")
617 order = min(nx, ny) - 1
618 logger.warn(
"Reducing approxOrder to %d" % order)
619 elif backgroundConfig.undersampleStyle ==
"INCREASE_NXNYSAMPLE":
620 newBinSize = min(image.getWidth(),image.getHeight())//(minNumberGridPoints-1)
622 raise ValueError(
"Binsize must be greater than 0")
623 newNx = image.getWidth()//newBinSize + 1
624 newNy = image.getHeight()//newBinSize + 1
625 bctrl.setNxSample(newNx)
626 bctrl.setNySample(newNy)
627 logger.warn(
"Decreasing binSize from %d to %d for a grid of (%d, %d)" %
628 (backgroundConfig.binSize, newBinSize, newNx, newNy))
631 backgroundConfig.weighting)
632 bctrl.setApproximateControl(actrl)
636 getBackground.ConfigClass = BackgroundConfig
640 """!Estimate exposure's background using parameters in backgroundConfig.
642 If subtract is true, make a copy of the exposure and subtract the background.
643 If `stats` is True, measure the mean and variance of the background and
644 add them to the background-subtracted exposure's metadata with keys
645 "BGMEAN" and "BGVAR", or the keys given in `statsKeys` (2-tuple of strings).
647 Return background, backgroundSubtractedExposure
652 maskedImage = exposure.getMaskedImage()
657 raise RuntimeError,
"Unable to estimate background for exposure"
661 if displayBackground > 1:
662 bgimg = background.getImageF()
663 ds9.mtv(bgimg, title=
"background", frame=1)
666 return background,
None
668 bbox = maskedImage.getBBox()
669 backgroundSubtractedExposure = exposure.Factory(exposure, bbox, afwImage.PARENT,
True)
670 copyImage = backgroundSubtractedExposure.getMaskedImage().getImage()
672 bgimg = background.getImageF()
678 if statsKeys
is None:
682 mnkey, varkey = statsKeys
683 meta = backgroundSubtractedExposure.getMetadata()
685 bgmean = s.getValue(afwMath.MEAN)
686 bgvar = s.getValue(afwMath.VARIANCE)
687 meta.addDouble(mnkey, bgmean)
688 meta.addDouble(varkey, bgvar)
690 if displayBackground:
691 ds9.mtv(backgroundSubtractedExposure, title=
"subtracted")
693 return background, backgroundSubtractedExposure
694 estimateBackground.ConfigClass = BackgroundConfig
boost::shared_ptr< Background > makeBackground(ImageT const &img, BackgroundControl const &bgCtrl)
A convenience function that uses function overloading to make the correct type of Background...
def detectFootprints
Detect footprints.
def addExposures
Add a set of exposures together.
Parameters to control convolution.
Config for background estimation.
A kernel described by a pair of functions: func(x, y) = colFunc(x) * rowFunc(y)
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.
Pass parameters to a Background object.
a place to record messages and descriptions of the state of processing.
An integer coordinate rectangle.
Control how to make an approximation.
Pass parameters to a Statistics objectA class to pass parameters which control how the stats are calc...
def run
Run source detection and create a SourceCatalog.
Configuration parameters for the SourceDetectionTask.
Custom catalog class for record/table subclasses that are guaranteed to have an ID, and should generally be sorted by that ID.
def init
Create the detection task.
def __init__
Create the detection task.
def estimateBackground
Estimate exposure's background using parameters in backgroundConfig.
Threshold createThreshold(const double value, const std::string type="value", const bool polarity=true)
Factory method for creating Threshold objects.
def thresholdImage
Threshold the convolved image, returning a FootprintSet.
Statistics makeStatistics(afwImage::Mask< afwImage::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl)
Specialization to handle Masks.
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, bool doNormalize, bool doCopyEdge=false)
Old, deprecated version of convolve.
def getBackground
Estimate the background of an image (a thin layer on lsst.afw.math.makeBackground) ...