LSSTApplications  11.0-13-gbb96280,12.1+18,12.1+7,12.1-1-g14f38d3+72,12.1-1-g16c0db7+5,12.1-1-g5961e7a+84,12.1-1-ge22e12b+23,12.1-11-g06625e2+4,12.1-11-g0d7f63b+4,12.1-19-gd507bfc,12.1-2-g7dda0ab+38,12.1-2-gc0bc6ab+81,12.1-21-g6ffe579+2,12.1-21-gbdb6c2a+4,12.1-24-g941c398+5,12.1-3-g57f6835+7,12.1-3-gf0736f3,12.1-37-g3ddd237,12.1-4-gf46015e+5,12.1-5-g06c326c+20,12.1-5-g648ee80+3,12.1-5-gc2189d7+4,12.1-6-ga608fc0+1,12.1-7-g3349e2a+5,12.1-7-gfd75620+9,12.1-9-g577b946+5,12.1-9-gc4df26a+10
LSSTDataManagementBasePackage
detection.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008-2016 AURA/LSST.
4 #
5 # This product includes software developed by the
6 # LSST Project (http://www.lsst.org/).
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <https://www.lsstcorp.org/LegalNotices/>.
21 #
22 import lsst.afw.detection as afwDet
23 import lsst.afw.display.ds9 as ds9
24 import lsst.afw.geom as afwGeom
25 import lsst.afw.image as afwImage
26 import lsst.afw.math as afwMath
27 import lsst.afw.table as afwTable
28 import lsst.pex.config as pexConfig
29 import lsst.pipe.base as pipeBase
30 from .subtractBackground import SubtractBackgroundTask
31 
32 __all__ = ("SourceDetectionConfig", "SourceDetectionTask", "addExposures")
33 
34 
35 class SourceDetectionConfig(pexConfig.Config):
36  """!Configuration parameters for the SourceDetectionTask
37  """
38  minPixels = pexConfig.RangeField(
39  doc="detected sources with fewer than the specified number of pixels will be ignored",
40  dtype=int, optional=False, default=1, min=0,
41  )
42  isotropicGrow = pexConfig.Field(
43  doc="Pixels should be grown as isotropically as possible (slower)",
44  dtype=bool, optional=False, default=False,
45  )
46  nSigmaToGrow = pexConfig.Field(
47  doc="Grow detections by nSigmaToGrow * sigma; if 0 then do not grow",
48  dtype=float, default=2.4, # 2.4 pixels/sigma is roughly one pixel/FWHM
49  )
50  returnOriginalFootprints = pexConfig.Field(
51  doc="Grow detections to set the image mask bits, but return the original (not-grown) footprints",
52  dtype=bool, optional=False, default=False,
53  )
54  thresholdValue = pexConfig.RangeField(
55  doc="Threshold for footprints",
56  dtype=float, optional=False, default=5.0, min=0.0,
57  )
58  includeThresholdMultiplier = pexConfig.RangeField(
59  doc="Include threshold relative to thresholdValue",
60  dtype=float, default=1.0, min=0.0,
61  )
62  thresholdType = pexConfig.ChoiceField(
63  doc="specifies the desired flavor of Threshold",
64  dtype=str, optional=False, default="stdev",
65  allowed={
66  "variance": "threshold applied to image variance",
67  "stdev": "threshold applied to image std deviation",
68  "value": "threshold applied to image value",
69  "pixel_stdev": "threshold applied to per-pixel std deviation",
70  },
71  )
72  thresholdPolarity = pexConfig.ChoiceField(
73  doc="specifies whether to detect positive, or negative sources, or both",
74  dtype=str, optional=False, default="positive",
75  allowed={
76  "positive": "detect only positive sources",
77  "negative": "detect only negative sources",
78  "both": "detect both positive and negative sources",
79  },
80  )
81  adjustBackground = pexConfig.Field(
82  dtype=float,
83  doc="Fiddle factor to add to the background; debugging only",
84  default=0.0,
85  )
86  reEstimateBackground = pexConfig.Field(
87  dtype=bool,
88  doc="Estimate the background again after final source detection?",
89  default=True, optional=False,
90  )
91  background = pexConfig.ConfigurableField(
92  doc="Background re-estimation; ignored if reEstimateBackground false",
93  target=SubtractBackgroundTask,
94  )
95  tempLocalBackground = pexConfig.ConfigurableField(
96  doc=("A seperate background estimation and removal before footprint and peak detection. "
97  "It is added back into the image after detection."),
98  target=SubtractBackgroundTask,
99  )
100  doTempLocalBackground = pexConfig.Field(
101  dtype=bool,
102  doc="Do temporary interpolated background subtraction before footprint detection?",
103  default=True,
104  )
105 
106  def setDefaults(self):
107  self.tempLocalBackground.binSize = 64
108  self.tempLocalBackground.algorithm = "AKIMA_SPLINE"
109  self.tempLocalBackground.useApprox = False
110 
111 ## \addtogroup LSST_task_documentation
112 ## \{
113 ## \page sourceDetectionTask
114 ## \ref SourceDetectionTask_ "SourceDetectionTask"
115 ## \copybrief SourceDetectionTask
116 ## \}
117 
118 
119 class SourceDetectionTask(pipeBase.Task):
120  """!
121 \anchor SourceDetectionTask_
122 
123 \brief Detect positive and negative sources on an exposure and return a new \link table.SourceCatalog\endlink.
124 
125 \section meas_algorithms_detection_Contents Contents
126 
127  - \ref meas_algorithms_detection_Purpose
128  - \ref meas_algorithms_detection_Initialize
129  - \ref meas_algorithms_detection_Invoke
130  - \ref meas_algorithms_detection_Config
131  - \ref meas_algorithms_detection_Debug
132  - \ref meas_algorithms_detection_Example
133 
134 \section meas_algorithms_detection_Purpose Description
135 
136 \copybrief SourceDetectionTask
137 
138 \section meas_algorithms_detection_Initialize Task initialisation
139 
140 \copydoc \_\_init\_\_
141 
142 \section meas_algorithms_detection_Invoke Invoking the Task
143 
144 \copydoc run
145 
146 \section meas_algorithms_detection_Config Configuration parameters
147 
148 See \ref SourceDetectionConfig
149 
150 \section meas_algorithms_detection_Debug Debug variables
151 
152 The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
153 flag \c -d to import \b debug.py from your \c PYTHONPATH; see \ref baseDebug for more about \b debug.py files.
154 
155 The available variables in SourceDetectionTask are:
156 <DL>
157  <DT> \c display
158  <DD>
159  - If True, display the exposure on ds9's frame 0. +ve detections in blue, -ve detections in cyan
160  - If display > 1, display the convolved exposure on frame 1
161 </DL>
162 
163 \section meas_algorithms_detection_Example A complete example of using SourceDetectionTask
164 
165 This code is in \link measAlgTasks.py\endlink in the examples directory, and can be run as \em e.g.
166 \code
167 examples/measAlgTasks.py --ds9
168 \endcode
169 \dontinclude measAlgTasks.py
170 The example also runs the SourceMeasurementTask; see \ref meas_algorithms_measurement_Example for more
171 explanation.
172 
173 Import the task (there are some other standard imports; read the file if you're confused)
174 \skipline SourceDetectionTask
175 
176 We need to create our task before processing any data as the task constructor
177 can add an extra column to the schema, but first we need an almost-empty Schema
178 \skipline makeMinimalSchema
179 after which we can call the constructor:
180 \skip SourceDetectionTask.ConfigClass
181 @until detectionTask
182 
183 We're now ready to process the data (we could loop over multiple exposures/catalogues using the same
184 task objects). First create the output table:
185 \skipline afwTable
186 
187 And process the image
188 \skipline result
189 (You may not be happy that the threshold was set in the config before creating the Task rather than being set
190 separately for each exposure. You \em can reset it just before calling the run method if you must, but we
191 should really implement a better solution).
192 
193 We can then unpack and use the results:
194 \skip sources
195 @until print
196 
197 <HR>
198 To investigate the \ref meas_algorithms_detection_Debug, put something like
199 \code{.py}
200  import lsstDebug
201  def DebugInfo(name):
202  di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
203  if name == "lsst.meas.algorithms.detection":
204  di.display = 1
205 
206  return di
207 
208  lsstDebug.Info = DebugInfo
209 \endcode
210 into your debug.py file and run measAlgTasks.py with the \c --debug flag.
211  """
212  ConfigClass = SourceDetectionConfig
213  _DefaultName = "sourceDetection"
214 
215  def __init__(self, schema=None, **kwds):
216  """!Create the detection task. Most arguments are simply passed onto pipe.base.Task.
217 
218  \param schema An lsst::afw::table::Schema used to create the output lsst.afw.table.SourceCatalog
219  \param **kwds Keyword arguments passed to lsst.pipe.base.task.Task.__init__.
220 
221  If schema is not None and configured for 'both' detections,
222  a 'flags.negative' field will be added to label detections made with a
223  negative threshold.
224 
225  \note This task can add fields to the schema, so any code calling this task must ensure that
226  these columns are indeed present in the input match list; see \ref Example
227  """
228  pipeBase.Task.__init__(self, **kwds)
229  if schema is not None and self.config.thresholdPolarity == "both":
230  self.negativeFlagKey = schema.addField(
231  "flags_negative", type="Flag",
232  doc="set if source was detected as significantly negative"
233  )
234  else:
235  if self.config.thresholdPolarity == "both":
236  self.log.warn("Detection polarity set to 'both', but no flag will be "
237  "set to distinguish between positive and negative detections")
238  self.negativeFlagKey = None
239  if self.config.reEstimateBackground:
240  self.makeSubtask("background")
241  if self.config.doTempLocalBackground:
242  self.makeSubtask("tempLocalBackground")
243 
244  @pipeBase.timeMethod
245  def run(self, table, exposure, doSmooth=True, sigma=None, clearMask=True):
246  """!Run source detection and create a SourceCatalog.
247 
248  \param table lsst.afw.table.SourceTable object that will be used to create the SourceCatalog.
249  \param exposure Exposure to process; DETECTED mask plane will be set in-place.
250  \param doSmooth if True, smooth the image before detection using a Gaussian of width sigma
251  (default: True)
252  \param sigma sigma of PSF (pixels); used for smoothing and to grow detections;
253  if None then measure the sigma of the PSF of the exposure (default: None)
254  \param clearMask Clear DETECTED{,_NEGATIVE} planes before running detection (default: True)
255 
256  \return a lsst.pipe.base.Struct with:
257  - sources -- an lsst.afw.table.SourceCatalog object
258  - fpSets --- lsst.pipe.base.Struct returned by \link detectFootprints \endlink
259 
260  \throws ValueError if flags.negative is needed, but isn't in table's schema
261  \throws lsst.pipe.base.TaskError if sigma=None, doSmooth=True and the exposure has no PSF
262 
263  \note
264  If you want to avoid dealing with Sources and Tables, you can use detectFootprints()
265  to just get the afw::detection::FootprintSet%s.
266  """
267  if self.negativeFlagKey is not None and self.negativeFlagKey not in table.getSchema():
268  raise ValueError("Table has incorrect Schema")
269  fpSets = self.detectFootprints(exposure=exposure, doSmooth=doSmooth, sigma=sigma,
270  clearMask=clearMask)
271  sources = afwTable.SourceCatalog(table)
272  table.preallocate(fpSets.numPos + fpSets.numNeg) # not required, but nice
273  if fpSets.negative:
274  fpSets.negative.makeSources(sources)
275  if self.negativeFlagKey:
276  for record in sources:
277  record.set(self.negativeFlagKey, True)
278  if fpSets.positive:
279  fpSets.positive.makeSources(sources)
280  return pipeBase.Struct(
281  sources=sources,
282  fpSets=fpSets
283  )
284 
285  ## An alias for run \deprecated Remove this alias after checking for where it's used
286  makeSourceCatalog = run
287 
288  @pipeBase.timeMethod
289  def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True):
290  """!Detect footprints.
291 
292  \param exposure Exposure to process; DETECTED{,_NEGATIVE} mask plane will be set in-place.
293  \param doSmooth if True, smooth the image before detection using a Gaussian of width sigma
294  \param sigma sigma of PSF (pixels); used for smoothing and to grow detections;
295  if None then measure the sigma of the PSF of the exposure
296  \param clearMask Clear both DETECTED and DETECTED_NEGATIVE planes before running detection
297 
298  \return a lsst.pipe.base.Struct with fields:
299  - positive: lsst.afw.detection.FootprintSet with positive polarity footprints (may be None)
300  - negative: lsst.afw.detection.FootprintSet with negative polarity footprints (may be None)
301  - numPos: number of footprints in positive or 0 if detection polarity was negative
302  - numNeg: number of footprints in negative or 0 if detection polarity was positive
303  - background: re-estimated background. None if reEstimateBackground==False
304 
305  \throws lsst.pipe.base.TaskError if sigma=None and the exposure has no PSF
306  """
307  try:
308  import lsstDebug
309  display = lsstDebug.Info(__name__).display
310  except ImportError:
311  try:
312  display
313  except NameError:
314  display = False
315 
316  if exposure is None:
317  raise RuntimeError("No exposure for detection")
318 
319  maskedImage = exposure.getMaskedImage()
320  region = maskedImage.getBBox()
321 
322  if clearMask:
323  mask = maskedImage.getMask()
324  mask &= ~(mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE"))
325  del mask
326 
327  if self.config.doTempLocalBackground:
328  tempBgRes = self.tempLocalBackground.run(exposure)
329  tempLocalBkgdImage = tempBgRes.background.getImage()
330 
331  if sigma is None:
332  psf = exposure.getPsf()
333  if psf is None:
334  raise pipeBase.TaskError("exposure has no PSF; must specify sigma")
335  shape = psf.computeShape()
336  sigma = shape.getDeterminantRadius()
337 
338  self.metadata.set("sigma", sigma)
339  self.metadata.set("doSmooth", doSmooth)
340 
341  if not doSmooth:
342  convolvedImage = maskedImage.Factory(maskedImage)
343  middle = convolvedImage
344  else:
345  # smooth using a Gaussian (which is separate, hence fast) of width sigma
346  # make a SingleGaussian (separable) kernel with the 'sigma'
347  psf = exposure.getPsf()
348  kWidth = (int(sigma * 7 + 0.5) // 2) * 2 + 1 # make sure it is odd
349  self.metadata.set("smoothingKernelWidth", kWidth)
350  gaussFunc = afwMath.GaussianFunction1D(sigma)
351  gaussKernel = afwMath.SeparableKernel(kWidth, kWidth, gaussFunc, gaussFunc)
352 
353  convolvedImage = maskedImage.Factory(maskedImage.getBBox())
354 
355  afwMath.convolve(convolvedImage, maskedImage, gaussKernel, afwMath.ConvolutionControl())
356  #
357  # Only search psf-smooth part of frame
358  #
359  goodBBox = gaussKernel.shrinkBBox(convolvedImage.getBBox())
360  middle = convolvedImage.Factory(convolvedImage, goodBBox, afwImage.PARENT, False)
361  #
362  # Mark the parts of the image outside goodBBox as EDGE
363  #
364  self.setEdgeBits(maskedImage, goodBBox, maskedImage.getMask().getPlaneBitMask("EDGE"))
365 
366  fpSets = pipeBase.Struct(positive=None, negative=None)
367 
368  if self.config.thresholdPolarity != "negative":
369  fpSets.positive = self.thresholdImage(middle, "positive")
370  if self.config.reEstimateBackground or self.config.thresholdPolarity != "positive":
371  fpSets.negative = self.thresholdImage(middle, "negative")
372 
373  for polarity, maskName in (("positive", "DETECTED"), ("negative", "DETECTED_NEGATIVE")):
374  fpSet = getattr(fpSets, polarity)
375  if fpSet is None:
376  continue
377  fpSet.setRegion(region)
378  if self.config.nSigmaToGrow > 0:
379  nGrow = int((self.config.nSigmaToGrow * sigma) + 0.5)
380  self.metadata.set("nGrow", nGrow)
381  fpSet = afwDet.FootprintSet(fpSet, nGrow, self.config.isotropicGrow)
382  fpSet.setMask(maskedImage.getMask(), maskName)
383  if not self.config.returnOriginalFootprints:
384  setattr(fpSets, polarity, fpSet)
385 
386  fpSets.numPos = len(fpSets.positive.getFootprints()) if fpSets.positive is not None else 0
387  fpSets.numNeg = len(fpSets.negative.getFootprints()) if fpSets.negative is not None else 0
388 
389  if self.config.thresholdPolarity != "negative":
390  self.log.info("Detected %d positive sources to %g sigma.",
391  fpSets.numPos, self.config.thresholdValue*self.config.includeThresholdMultiplier)
392 
393  if self.config.doTempLocalBackground:
394  maskedImage += tempLocalBkgdImage
395 
396  fpSets.background = None
397  if self.config.reEstimateBackground:
398  mi = exposure.getMaskedImage()
399  bkgd = self.background.fitBackground(mi)
400 
401  if self.config.adjustBackground:
402  self.log.warn("Fiddling the background by %g", self.config.adjustBackground)
403 
404  bkgd += self.config.adjustBackground
405  fpSets.background = bkgd
406  self.log.info("Resubtracting the background after object detection")
407 
408  mi -= bkgd.getImageF()
409  del mi
410 
411  if self.config.thresholdPolarity == "positive":
412  if self.config.reEstimateBackground:
413  mask = maskedImage.getMask()
414  mask &= ~mask.getPlaneBitMask("DETECTED_NEGATIVE")
415  del mask
416  fpSets.negative = None
417  else:
418  self.log.info("Detected %d negative sources to %g %s",
419  fpSets.numNeg, self.config.thresholdValue,
420  ("DN" if self.config.thresholdType == "value" else "sigma"))
421 
422  if display:
423  ds9.mtv(exposure, frame=0, title="detection")
424  x0, y0 = exposure.getXY0()
425 
426  def plotPeaks(fps, ctype):
427  if fps is None:
428  return
429  with ds9.Buffering():
430  for fp in fps.getFootprints():
431  for pp in fp.getPeaks():
432  ds9.dot("+", pp.getFx() - x0, pp.getFy() - y0, ctype=ctype)
433  plotPeaks(fpSets.positive, "yellow")
434  plotPeaks(fpSets.negative, "red")
435 
436  if convolvedImage and display and display > 1:
437  ds9.mtv(convolvedImage, frame=1, title="PSF smoothed")
438 
439  return fpSets
440 
441  def thresholdImage(self, image, thresholdParity, maskName="DETECTED"):
442  """!Threshold the convolved image, returning a FootprintSet.
443  Helper function for detect().
444 
445  \param image The (optionally convolved) MaskedImage to threshold
446  \param thresholdParity Parity of threshold
447  \param maskName Name of mask to set
448 
449  \return FootprintSet
450  """
451  parity = False if thresholdParity == "negative" else True
452  threshold = afwDet.createThreshold(self.config.thresholdValue, self.config.thresholdType, parity)
453  threshold.setIncludeMultiplier(self.config.includeThresholdMultiplier)
454 
455  if self.config.thresholdType == 'stdev':
456  bad = image.getMask().getPlaneBitMask(['BAD', 'SAT', 'EDGE', 'NO_DATA', ])
457  sctrl = afwMath.StatisticsControl()
458  sctrl.setAndMask(bad)
459  stats = afwMath.makeStatistics(image, afwMath.STDEVCLIP, sctrl)
460  thres = stats.getValue(afwMath.STDEVCLIP) * self.config.thresholdValue
461  threshold = afwDet.createThreshold(thres, 'value', parity)
462  threshold.setIncludeMultiplier(self.config.includeThresholdMultiplier)
463 
464  fpSet = afwDet.FootprintSet(image, threshold, maskName, self.config.minPixels)
465  return fpSet
466 
467  @staticmethod
468  def setEdgeBits(maskedImage, goodBBox, edgeBitmask):
469  """!Set the edgeBitmask bits for all of maskedImage outside goodBBox
470 
471  \param[in,out] maskedImage image on which to set edge bits in the mask
472  \param[in] goodBBox bounding box of good pixels, in LOCAL coordinates
473  \param[in] edgeBitmask bit mask to OR with the existing mask bits in the region outside goodBBox
474  """
475  msk = maskedImage.getMask()
476 
477  mx0, my0 = maskedImage.getXY0()
478  for x0, y0, w, h in ([0, 0,
479  msk.getWidth(), goodBBox.getBeginY() - my0],
480  [0, goodBBox.getEndY() - my0, msk.getWidth(),
481  maskedImage.getHeight() - (goodBBox.getEndY() - my0)],
482  [0, 0,
483  goodBBox.getBeginX() - mx0, msk.getHeight()],
484  [goodBBox.getEndX() - mx0, 0,
485  maskedImage.getWidth() - (goodBBox.getEndX() - mx0), msk.getHeight()],
486  ):
487  edgeMask = msk.Factory(msk, afwGeom.BoxI(afwGeom.PointI(x0, y0),
488  afwGeom.ExtentI(w, h)), afwImage.LOCAL)
489  edgeMask |= edgeBitmask
490 
491 
492 def addExposures(exposureList):
493  """!Add a set of exposures together.
494 
495  \param[in] exposureList sequence of exposures to add
496 
497  \return an exposure of the same size as each exposure in exposureList,
498  with the metadata from exposureList[0] and a masked image equal to the
499  sum of all the exposure's masked images.
500 
501  \throw LsstException if the exposures do not all have the same dimensions (but does not check xy0)
502  """
503  exposure0 = exposureList[0]
504  image0 = exposure0.getMaskedImage()
505 
506  addedImage = image0.Factory(image0, True)
507  addedImage.setXY0(image0.getXY0())
508 
509  for exposure in exposureList[1:]:
510  image = exposure.getMaskedImage()
511  addedImage += image
512 
513  addedExposure = exposure0.Factory(addedImage, exposure0.getWcs())
514  return addedExposure
def addExposures
Add a set of exposures together.
Definition: detection.py:492
Parameters to control convolution.
Definition: ConvolveImage.h:57
A kernel described by a pair of functions: func(x, y) = colFunc(x) * rowFunc(y)
Definition: Kernel.h:983
def setEdgeBits
Set the edgeBitmask bits for all of maskedImage outside goodBBox.
Definition: detection.py:468
Detect positive and negative sources on an exposure and return a new table.SourceCatalog.
Definition: detection.py:119
An integer coordinate rectangle.
Definition: Box.h:53
Pass parameters to a Statistics objectA class to pass parameters which control how the stats are calc...
Definition: Statistics.h:92
def run
Run source detection and create a SourceCatalog.
Definition: detection.py:245
Configuration parameters for the SourceDetectionTask.
Definition: detection.py:35
Custom catalog class for record/table subclasses that are guaranteed to have an ID, and should generally be sorted by that ID.
Definition: fwd.h:55
Threshold createThreshold(double const value, std::string const typeStr, bool const polarity)
Factory method for creating Threshold objects.
Definition: Threshold.cc:138
def thresholdImage
Threshold the convolved image, returning a FootprintSet.
Definition: detection.py:441
A set of Footprints, associated with a MaskedImage.
Definition: FootprintSet.h:55
Statistics makeStatistics(afwImage::Mask< afwImage::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl)
Specialization to handle Masks.
Definition: Statistics.cc:1107
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, bool doNormalize, bool doCopyEdge=false)
Old, deprecated version of convolve.