LSSTApplications  11.0-13-gbb96280,12.1.rc1,12.1.rc1+1,12.1.rc1+2,12.1.rc1+5,12.1.rc1+8,12.1.rc1-1-g06d7636+1,12.1.rc1-1-g253890b+5,12.1.rc1-1-g3d31b68+7,12.1.rc1-1-g3db6b75+1,12.1.rc1-1-g5c1385a+3,12.1.rc1-1-g83b2247,12.1.rc1-1-g90cb4cf+6,12.1.rc1-1-g91da24b+3,12.1.rc1-2-g3521f8a,12.1.rc1-2-g39433dd+4,12.1.rc1-2-g486411b+2,12.1.rc1-2-g4c2be76,12.1.rc1-2-gc9c0491,12.1.rc1-2-gda2cd4f+6,12.1.rc1-3-g3391c73+2,12.1.rc1-3-g8c1bd6c+1,12.1.rc1-3-gcf4b6cb+2,12.1.rc1-4-g057223e+1,12.1.rc1-4-g19ed13b+2,12.1.rc1-4-g30492a7
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, a 'flags.negative' field will be added to label detections
222  made with a negative threshold.
223 
224  \note This task can add fields to the schema, so any code calling this task must ensure that
225  these columns are indeed present in the input match list; see \ref Example
226  """
227  pipeBase.Task.__init__(self, **kwds)
228  if schema is not None:
229  self.negativeFlagKey = schema.addField(
230  "flags_negative", type="Flag",
231  doc="set if source was detected as significantly negative"
232  )
233  else:
234  if self.config.thresholdPolarity == "both":
235  self.log.log(self.log.WARN, "Detection polarity set to 'both', but no flag will be "
236  "set to distinguish between positive and negative detections")
237  self.negativeFlagKey = None
238  if self.config.reEstimateBackground:
239  self.makeSubtask("background")
240  if self.config.doTempLocalBackground:
241  self.makeSubtask("tempLocalBackground")
242 
243  @pipeBase.timeMethod
244  def run(self, table, exposure, doSmooth=True, sigma=None, clearMask=True):
245  """!Run source detection and create a SourceCatalog.
246 
247  \param table lsst.afw.table.SourceTable object that will be used to create the SourceCatalog.
248  \param exposure Exposure to process; DETECTED mask plane will be set in-place.
249  \param doSmooth if True, smooth the image before detection using a Gaussian of width sigma
250  (default: True)
251  \param sigma sigma of PSF (pixels); used for smoothing and to grow detections;
252  if None then measure the sigma of the PSF of the exposure (default: None)
253  \param clearMask Clear DETECTED{,_NEGATIVE} planes before running detection (default: True)
254 
255  \return a lsst.pipe.base.Struct with:
256  - sources -- an lsst.afw.table.SourceCatalog object
257  - fpSets --- lsst.pipe.base.Struct returned by \link detectFootprints \endlink
258 
259  \throws ValueError if flags.negative is needed, but isn't in table's schema
260  \throws lsst.pipe.base.TaskError if sigma=None, doSmooth=True and the exposure has no PSF
261 
262  \note
263  If you want to avoid dealing with Sources and Tables, you can use detectFootprints()
264  to just get the afw::detection::FootprintSet%s.
265  """
266  if self.negativeFlagKey is not None and self.negativeFlagKey not in table.getSchema():
267  raise ValueError("Table has incorrect Schema")
268  fpSets = self.detectFootprints(exposure=exposure, doSmooth=doSmooth, sigma=sigma,
269  clearMask=clearMask)
270  sources = afwTable.SourceCatalog(table)
271  table.preallocate(fpSets.numPos + fpSets.numNeg) # not required, but nice
272  if fpSets.negative:
273  fpSets.negative.makeSources(sources)
274  if self.negativeFlagKey:
275  for record in sources:
276  record.set(self.negativeFlagKey, True)
277  if fpSets.positive:
278  fpSets.positive.makeSources(sources)
279  return pipeBase.Struct(
280  sources=sources,
281  fpSets=fpSets
282  )
283 
284  ## An alias for run \deprecated Remove this alias after checking for where it's used
285  makeSourceCatalog = run
286 
287  @pipeBase.timeMethod
288  def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True):
289  """!Detect footprints.
290 
291  \param exposure Exposure to process; DETECTED{,_NEGATIVE} mask plane will be set in-place.
292  \param doSmooth if True, smooth the image before detection using a Gaussian of width sigma
293  \param sigma sigma of PSF (pixels); used for smoothing and to grow detections;
294  if None then measure the sigma of the PSF of the exposure
295  \param clearMask Clear both DETECTED and DETECTED_NEGATIVE planes before running detection
296 
297  \return a lsst.pipe.base.Struct with fields:
298  - positive: lsst.afw.detection.FootprintSet with positive polarity footprints (may be None)
299  - negative: lsst.afw.detection.FootprintSet with negative polarity footprints (may be None)
300  - numPos: number of footprints in positive or 0 if detection polarity was negative
301  - numNeg: number of footprints in negative or 0 if detection polarity was positive
302  - background: re-estimated background. None if reEstimateBackground==False
303 
304  \throws lsst.pipe.base.TaskError if sigma=None and the exposure has no PSF
305  """
306  try:
307  import lsstDebug
308  display = lsstDebug.Info(__name__).display
309  except ImportError:
310  try:
311  display
312  except NameError:
313  display = False
314 
315  if exposure is None:
316  raise RuntimeError("No exposure for detection")
317 
318  maskedImage = exposure.getMaskedImage()
319  region = maskedImage.getBBox()
320 
321  if clearMask:
322  mask = maskedImage.getMask()
323  mask &= ~(mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE"))
324  del mask
325 
326  if self.config.doTempLocalBackground:
327  tempBgRes = self.tempLocalBackground.run(exposure)
328  tempLocalBkgdImage = tempBgRes.background.getImage()
329 
330  if sigma is None:
331  psf = exposure.getPsf()
332  if psf is None:
333  raise pipeBase.TaskError("exposure has no PSF; must specify sigma")
334  shape = psf.computeShape()
335  sigma = shape.getDeterminantRadius()
336 
337  self.metadata.set("sigma", sigma)
338  self.metadata.set("doSmooth", doSmooth)
339 
340  if not doSmooth:
341  convolvedImage = maskedImage.Factory(maskedImage)
342  middle = convolvedImage
343  else:
344  # smooth using a Gaussian (which is separate, hence fast) of width sigma
345  # make a SingleGaussian (separable) kernel with the 'sigma'
346  psf = exposure.getPsf()
347  kWidth = (int(sigma * 7 + 0.5) // 2) * 2 + 1 # make sure it is odd
348  self.metadata.set("smoothingKernelWidth", kWidth)
349  gaussFunc = afwMath.GaussianFunction1D(sigma)
350  gaussKernel = afwMath.SeparableKernel(kWidth, kWidth, gaussFunc, gaussFunc)
351 
352  convolvedImage = maskedImage.Factory(maskedImage.getBBox())
353 
354  afwMath.convolve(convolvedImage, maskedImage, gaussKernel, afwMath.ConvolutionControl())
355  #
356  # Only search psf-smooth part of frame
357  #
358  goodBBox = gaussKernel.shrinkBBox(convolvedImage.getBBox())
359  middle = convolvedImage.Factory(convolvedImage, goodBBox, afwImage.PARENT, False)
360  #
361  # Mark the parts of the image outside goodBBox as EDGE
362  #
363  self.setEdgeBits(maskedImage, goodBBox, maskedImage.getMask().getPlaneBitMask("EDGE"))
364 
365  fpSets = pipeBase.Struct(positive=None, negative=None)
366 
367  if self.config.thresholdPolarity != "negative":
368  fpSets.positive = self.thresholdImage(middle, "positive")
369  if self.config.reEstimateBackground or self.config.thresholdPolarity != "positive":
370  fpSets.negative = self.thresholdImage(middle, "negative")
371 
372  for polarity, maskName in (("positive", "DETECTED"), ("negative", "DETECTED_NEGATIVE")):
373  fpSet = getattr(fpSets, polarity)
374  if fpSet is None:
375  continue
376  fpSet.setRegion(region)
377  if self.config.nSigmaToGrow > 0:
378  nGrow = int((self.config.nSigmaToGrow * sigma) + 0.5)
379  self.metadata.set("nGrow", nGrow)
380  fpSet = afwDet.FootprintSet(fpSet, nGrow, self.config.isotropicGrow)
381  fpSet.setMask(maskedImage.getMask(), maskName)
382  if not self.config.returnOriginalFootprints:
383  setattr(fpSets, polarity, fpSet)
384 
385  fpSets.numPos = len(fpSets.positive.getFootprints()) if fpSets.positive is not None else 0
386  fpSets.numNeg = len(fpSets.negative.getFootprints()) if fpSets.negative is not None else 0
387 
388  if self.config.thresholdPolarity != "negative":
389  self.log.info("Detected %d positive sources to %g sigma.",
390  fpSets.numPos, self.config.thresholdValue*self.config.includeThresholdMultiplier)
391 
392  if self.config.doTempLocalBackground:
393  maskedImage += tempLocalBkgdImage
394 
395  fpSets.background = None
396  if self.config.reEstimateBackground:
397  mi = exposure.getMaskedImage()
398  bkgd = self.background.fitBackground(mi)
399 
400  if self.config.adjustBackground:
401  self.log.warn("Fiddling the background by %g", self.config.adjustBackground)
402 
403  bkgd += self.config.adjustBackground
404  fpSets.background = bkgd
405  self.log.info("Resubtracting the background after object detection")
406 
407  mi -= bkgd.getImageF()
408  del mi
409 
410  if self.config.thresholdPolarity == "positive":
411  if self.config.reEstimateBackground:
412  mask = maskedImage.getMask()
413  mask &= ~mask.getPlaneBitMask("DETECTED_NEGATIVE")
414  del mask
415  fpSets.negative = None
416  else:
417  self.log.info("Detected %d negative sources to %g %s",
418  fpSets.numNeg, self.config.thresholdValue,
419  ("DN" if self.config.thresholdType == "value" else "sigma"))
420 
421  if display:
422  ds9.mtv(exposure, frame=0, title="detection")
423  x0, y0 = exposure.getXY0()
424 
425  def plotPeaks(fps, ctype):
426  if fps is None:
427  return
428  with ds9.Buffering():
429  for fp in fps.getFootprints():
430  for pp in fp.getPeaks():
431  ds9.dot("+", pp.getFx() - x0, pp.getFy() - y0, ctype=ctype)
432  plotPeaks(fpSets.positive, "yellow")
433  plotPeaks(fpSets.negative, "red")
434 
435  if convolvedImage and display and display > 1:
436  ds9.mtv(convolvedImage, frame=1, title="PSF smoothed")
437 
438  return fpSets
439 
440  def thresholdImage(self, image, thresholdParity, maskName="DETECTED"):
441  """!Threshold the convolved image, returning a FootprintSet.
442  Helper function for detect().
443 
444  \param image The (optionally convolved) MaskedImage to threshold
445  \param thresholdParity Parity of threshold
446  \param maskName Name of mask to set
447 
448  \return FootprintSet
449  """
450  parity = False if thresholdParity == "negative" else True
451  threshold = afwDet.createThreshold(self.config.thresholdValue, self.config.thresholdType, parity)
452  threshold.setIncludeMultiplier(self.config.includeThresholdMultiplier)
453 
454  if self.config.thresholdType == 'stdev':
455  bad = image.getMask().getPlaneBitMask(['BAD', 'SAT', 'EDGE', 'NO_DATA', ])
456  sctrl = afwMath.StatisticsControl()
457  sctrl.setAndMask(bad)
458  stats = afwMath.makeStatistics(image, afwMath.STDEVCLIP, sctrl)
459  thres = stats.getValue(afwMath.STDEVCLIP) * self.config.thresholdValue
460  threshold = afwDet.createThreshold(thres, 'value', parity)
461  threshold.setIncludeMultiplier(self.config.includeThresholdMultiplier)
462 
463  fpSet = afwDet.FootprintSet(image, threshold, maskName, self.config.minPixels)
464  return fpSet
465 
466  @staticmethod
467  def setEdgeBits(maskedImage, goodBBox, edgeBitmask):
468  """!Set the edgeBitmask bits for all of maskedImage outside goodBBox
469 
470  \param[in,out] maskedImage image on which to set edge bits in the mask
471  \param[in] goodBBox bounding box of good pixels, in LOCAL coordinates
472  \param[in] edgeBitmask bit mask to OR with the existing mask bits in the region outside goodBBox
473  """
474  msk = maskedImage.getMask()
475 
476  mx0, my0 = maskedImage.getXY0()
477  for x0, y0, w, h in ([0, 0,
478  msk.getWidth(), goodBBox.getBeginY() - my0],
479  [0, goodBBox.getEndY() - my0, msk.getWidth(),
480  maskedImage.getHeight() - (goodBBox.getEndY() - my0)],
481  [0, 0,
482  goodBBox.getBeginX() - mx0, msk.getHeight()],
483  [goodBBox.getEndX() - mx0, 0,
484  maskedImage.getWidth() - (goodBBox.getEndX() - mx0), msk.getHeight()],
485  ):
486  edgeMask = msk.Factory(msk, afwGeom.BoxI(afwGeom.PointI(x0, y0),
487  afwGeom.ExtentI(w, h)), afwImage.LOCAL)
488  edgeMask |= edgeBitmask
489 
490 
491 def addExposures(exposureList):
492  """!Add a set of exposures together.
493 
494  \param[in] exposureList sequence of exposures to add
495 
496  \return an exposure of the same size as each exposure in exposureList,
497  with the metadata from exposureList[0] and a masked image equal to the
498  sum of all the exposure's masked images.
499 
500  \throw LsstException if the exposures do not all have the same dimensions (but does not check xy0)
501  """
502  exposure0 = exposureList[0]
503  image0 = exposure0.getMaskedImage()
504 
505  addedImage = image0.Factory(image0, True)
506  addedImage.setXY0(image0.getXY0())
507 
508  for exposure in exposureList[1:]:
509  image = exposure.getMaskedImage()
510  addedImage += image
511 
512  addedExposure = exposure0.Factory(addedImage, exposure0.getWcs())
513  return addedExposure
def addExposures
Add a set of exposures together.
Definition: detection.py:491
Parameters to control convolution.
Definition: ConvolveImage.h:58
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:467
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:244
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:440
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.