LSSTApplications  20.0.0
LSSTDataManagementBasePackage
detection.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 #
4 # Copyright 2008-2017 AURA/LSST.
5 #
6 # This product includes software developed by the
7 # LSST Project (http://www.lsst.org/).
8 #
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the LSST License Statement and
20 # the GNU General Public License along with this program. If not,
21 # see <https://www.lsstcorp.org/LegalNotices/>.
22 #
23 
24 __all__ = ("SourceDetectionConfig", "SourceDetectionTask", "addExposures")
25 
26 from contextlib import contextmanager
27 from deprecated.sphinx import deprecated
28 
29 import numpy as np
30 
31 import lsst.geom
32 import lsst.afw.display as afwDisplay
33 import lsst.afw.detection as afwDet
34 import lsst.afw.geom as afwGeom
35 import lsst.afw.image as afwImage
36 import lsst.afw.math as afwMath
37 import lsst.afw.table as afwTable
38 import lsst.pex.config as pexConfig
39 import lsst.pipe.base as pipeBase
40 from .subtractBackground import SubtractBackgroundTask
41 
42 
43 class SourceDetectionConfig(pexConfig.Config):
44  """!Configuration parameters for the SourceDetectionTask
45  """
46  minPixels = pexConfig.RangeField(
47  doc="detected sources with fewer than the specified number of pixels will be ignored",
48  dtype=int, optional=False, default=1, min=0,
49  )
50  isotropicGrow = pexConfig.Field(
51  doc="Pixels should be grown as isotropically as possible (slower)",
52  dtype=bool, optional=False, default=False,
53  )
54  combinedGrow = pexConfig.Field(
55  doc="Grow all footprints at the same time? This allows disconnected footprints to merge.",
56  dtype=bool, default=True,
57  )
58  nSigmaToGrow = pexConfig.Field(
59  doc="Grow detections by nSigmaToGrow * [PSF RMS width]; if 0 then do not grow",
60  dtype=float, default=2.4, # 2.4 pixels/sigma is roughly one pixel/FWHM
61  )
62  returnOriginalFootprints = pexConfig.Field(
63  doc="Grow detections to set the image mask bits, but return the original (not-grown) footprints",
64  dtype=bool, optional=False, default=False,
65  )
66  thresholdValue = pexConfig.RangeField(
67  doc="Threshold for footprints; exact meaning and units depend on thresholdType.",
68  dtype=float, optional=False, default=5.0, min=0.0,
69  )
70  includeThresholdMultiplier = pexConfig.RangeField(
71  doc="Include threshold relative to thresholdValue",
72  dtype=float, default=1.0, min=0.0,
73  )
74  thresholdType = pexConfig.ChoiceField(
75  doc="specifies the desired flavor of Threshold",
76  dtype=str, optional=False, default="stdev",
77  allowed={
78  "variance": "threshold applied to image variance",
79  "stdev": "threshold applied to image std deviation",
80  "value": "threshold applied to image value",
81  "pixel_stdev": "threshold applied to per-pixel std deviation",
82  },
83  )
84  thresholdPolarity = pexConfig.ChoiceField(
85  doc="specifies whether to detect positive, or negative sources, or both",
86  dtype=str, optional=False, default="positive",
87  allowed={
88  "positive": "detect only positive sources",
89  "negative": "detect only negative sources",
90  "both": "detect both positive and negative sources",
91  },
92  )
93  adjustBackground = pexConfig.Field(
94  dtype=float,
95  doc="Fiddle factor to add to the background; debugging only",
96  default=0.0,
97  )
98  reEstimateBackground = pexConfig.Field(
99  dtype=bool,
100  doc="Estimate the background again after final source detection?",
101  default=True, optional=False,
102  )
103  background = pexConfig.ConfigurableField(
104  doc="Background re-estimation; ignored if reEstimateBackground false",
105  target=SubtractBackgroundTask,
106  )
107  tempLocalBackground = pexConfig.ConfigurableField(
108  doc=("A local (small-scale), temporary background estimation step run between "
109  "detecting above-threshold regions and detecting the peaks within "
110  "them; used to avoid detecting spuerious peaks in the wings."),
111  target=SubtractBackgroundTask,
112  )
113  doTempLocalBackground = pexConfig.Field(
114  dtype=bool,
115  doc="Enable temporary local background subtraction? (see tempLocalBackground)",
116  default=True,
117  )
118  tempWideBackground = pexConfig.ConfigurableField(
119  doc=("A wide (large-scale) background estimation and removal before footprint and peak detection. "
120  "It is added back into the image after detection. The purpose is to suppress very large "
121  "footprints (e.g., from large artifacts) that the deblender may choke on."),
122  target=SubtractBackgroundTask,
123  )
124  doTempWideBackground = pexConfig.Field(
125  dtype=bool,
126  doc="Do temporary wide (large-scale) background subtraction before footprint detection?",
127  default=False,
128  )
129  nPeaksMaxSimple = pexConfig.Field(
130  dtype=int,
131  doc=("The maximum number of peaks in a Footprint before trying to "
132  "replace its peaks using the temporary local background"),
133  default=1,
134  )
135  nSigmaForKernel = pexConfig.Field(
136  dtype=float,
137  doc=("Multiple of PSF RMS size to use for convolution kernel bounding box size; "
138  "note that this is not a half-size. The size will be rounded up to the nearest odd integer"),
139  default=7.0,
140  )
141  statsMask = pexConfig.ListField(
142  dtype=str,
143  doc="Mask planes to ignore when calculating statistics of image (for thresholdType=stdev)",
144  default=['BAD', 'SAT', 'EDGE', 'NO_DATA'],
145  )
146 
147  def setDefaults(self):
148  self.tempLocalBackground.binSize = 64
149  self.tempLocalBackground.algorithm = "AKIMA_SPLINE"
150  self.tempLocalBackground.useApprox = False
151  # Background subtraction to remove a large-scale background (e.g., scattered light); restored later.
152  # Want to keep it from exceeding the deblender size limit of 1 Mpix, so half that is reasonable.
153  self.tempWideBackground.binSize = 512
154  self.tempWideBackground.algorithm = "AKIMA_SPLINE"
155  self.tempWideBackground.useApprox = False
156  # Ensure we can remove even bright scattered light that is DETECTED
157  for maskPlane in ("DETECTED", "DETECTED_NEGATIVE"):
158  if maskPlane in self.tempWideBackground.ignoredPixelMask:
159  self.tempWideBackground.ignoredPixelMask.remove(maskPlane)
160 
161 
167 
168 
169 class SourceDetectionTask(pipeBase.Task):
170  r"""!
171 @anchor SourceDetectionTask_
172 
173 @brief Detect positive and negative sources on an exposure and return a new @link table.SourceCatalog@endlink.
174 
175 @section meas_algorithms_detection_Contents Contents
176 
177  - @ref meas_algorithms_detection_Purpose
178  - @ref meas_algorithms_detection_Initialize
179  - @ref meas_algorithms_detection_Invoke
180  - @ref meas_algorithms_detection_Config
181  - @ref meas_algorithms_detection_Debug
182  - @ref meas_algorithms_detection_Example
183 
184 @section meas_algorithms_detection_Purpose Description
185 
186 @copybrief SourceDetectionTask
187 
188 @section meas_algorithms_detection_Initialize Task initialisation
189 
190 @copydoc \_\_init\_\_
191 
192 @section meas_algorithms_detection_Invoke Invoking the Task
193 
194 @copydoc run
195 
196 @section meas_algorithms_detection_Config Configuration parameters
197 
198 See @ref SourceDetectionConfig
199 
200 @section meas_algorithms_detection_Debug Debug variables
201 
202 The @link lsst.pipe.base.cmdLineTask.CmdLineTask command line task@endlink interface supports a
203 flag @c -d to import @b debug.py from your @c PYTHONPATH; see @ref baseDebug for more about @b debug.py files.
204 
205 The available variables in SourceDetectionTask are:
206 <DL>
207  <DT> @c display
208  <DD>
209  - If True, display the exposure on afwDisplay.Display's frame 0.
210  +ve detections in blue, -ve detections in cyan
211  - If display > 1, display the convolved exposure on frame 1
212 </DL>
213 
214 @section meas_algorithms_detection_Example A complete example of using SourceDetectionTask
215 
216 This code is in @link measAlgTasks.py@endlink in the examples directory, and can be run as @em e.g.
217 @code
218 examples/measAlgTasks.py --doDisplay
219 @endcode
220 @dontinclude measAlgTasks.py
221 The example also runs the SingleFrameMeasurementTask; see @ref meas_algorithms_measurement_Example for more
222 explanation.
223 
224 Import the task (there are some other standard imports; read the file if you're confused)
225 @skipline SourceDetectionTask
226 
227 We need to create our task before processing any data as the task constructor
228 can add an extra column to the schema, but first we need an almost-empty Schema
229 @skipline makeMinimalSchema
230 after which we can call the constructor:
231 @skip SourceDetectionTask.ConfigClass
232 @until detectionTask
233 
234 We're now ready to process the data (we could loop over multiple exposures/catalogues using the same
235 task objects). First create the output table:
236 @skipline afwTable
237 
238 And process the image
239 @skipline result
240 (You may not be happy that the threshold was set in the config before creating the Task rather than being set
241 separately for each exposure. You @em can reset it just before calling the run method if you must, but we
242 should really implement a better solution).
243 
244 We can then unpack and use the results:
245 @skip sources
246 @until print
247 
248 <HR>
249 To investigate the @ref meas_algorithms_detection_Debug, put something like
250 @code{.py}
251  import lsstDebug
252  def DebugInfo(name):
253  di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
254  if name == "lsst.meas.algorithms.detection":
255  di.display = 1
256 
257  return di
258 
259  lsstDebug.Info = DebugInfo
260 @endcode
261 into your debug.py file and run measAlgTasks.py with the @c --debug flag.
262  """
263  ConfigClass = SourceDetectionConfig
264  _DefaultName = "sourceDetection"
265 
266  def __init__(self, schema=None, **kwds):
267  """!Create the detection task. Most arguments are simply passed onto pipe.base.Task.
268 
269  @param schema An lsst::afw::table::Schema used to create the output lsst.afw.table.SourceCatalog
270  @param **kwds Keyword arguments passed to lsst.pipe.base.task.Task.__init__.
271 
272  If schema is not None and configured for 'both' detections,
273  a 'flags.negative' field will be added to label detections made with a
274  negative threshold.
275 
276  @note This task can add fields to the schema, so any code calling this task must ensure that
277  these columns are indeed present in the input match list; see @ref Example
278  """
279  pipeBase.Task.__init__(self, **kwds)
280  if schema is not None and self.config.thresholdPolarity == "both":
281  self.negativeFlagKey = schema.addField(
282  "flags_negative", type="Flag",
283  doc="set if source was detected as significantly negative"
284  )
285  else:
286  if self.config.thresholdPolarity == "both":
287  self.log.warn("Detection polarity set to 'both', but no flag will be "
288  "set to distinguish between positive and negative detections")
289  self.negativeFlagKey = None
290  if self.config.reEstimateBackground:
291  self.makeSubtask("background")
292  if self.config.doTempLocalBackground:
293  self.makeSubtask("tempLocalBackground")
294  if self.config.doTempWideBackground:
295  self.makeSubtask("tempWideBackground")
296 
297  @pipeBase.timeMethod
298  def run(self, table, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None):
299  """Run source detection and create a SourceCatalog of detections.
300 
301  Parameters
302  ----------
303  table : `lsst.afw.table.SourceTable`
304  Table object that will be used to create the SourceCatalog.
305  exposure : `lsst.afw.image.Exposure`
306  Exposure to process; DETECTED mask plane will be set in-place.
307  doSmooth : `bool`
308  If True, smooth the image before detection using a Gaussian of width
309  ``sigma``, or the measured PSF width. Set to False when running on
310  e.g. a pre-convolved image, or a mask plane.
311  sigma : `float`
312  Sigma of PSF (pixels); used for smoothing and to grow detections;
313  if None then measure the sigma of the PSF of the exposure
314  clearMask : `bool`
315  Clear DETECTED{,_NEGATIVE} planes before running detection.
316  expId : `int`
317  Exposure identifier; unused by this implementation, but used for
318  RNG seed by subclasses.
319 
320  Returns
321  -------
322  result : `lsst.pipe.base.Struct`
323  ``sources``
324  The detected sources (`lsst.afw.table.SourceCatalog`)
325  ``fpSets``
326  The result resturned by `detectFootprints`
327  (`lsst.pipe.base.Struct`).
328 
329  Raises
330  ------
331  ValueError
332  If flags.negative is needed, but isn't in table's schema.
333  lsst.pipe.base.TaskError
334  If sigma=None, doSmooth=True and the exposure has no PSF.
335 
336  Notes
337  -----
338  If you want to avoid dealing with Sources and Tables, you can use
339  detectFootprints() to just get the `lsst.afw.detection.FootprintSet`s.
340  """
341  if self.negativeFlagKey is not None and self.negativeFlagKey not in table.getSchema():
342  raise ValueError("Table has incorrect Schema")
343  results = self.detectFootprints(exposure=exposure, doSmooth=doSmooth, sigma=sigma,
344  clearMask=clearMask, expId=expId)
345  sources = afwTable.SourceCatalog(table)
346  sources.reserve(results.numPos + results.numNeg)
347  if results.negative:
348  results.negative.makeSources(sources)
349  if self.negativeFlagKey:
350  for record in sources:
351  record.set(self.negativeFlagKey, True)
352  if results.positive:
353  results.positive.makeSources(sources)
354  results.fpSets = results.copy() # Backward compatibility
355  results.sources = sources
356  return results
357 
358  @deprecated(reason="Replaced by SourceDetectionTask.run(). Will be removed after v20.",
359  category=FutureWarning)
360  def makeSourceCatalog(self, *args, **kwargs):
361  return self.run(*args, **kwargs)
362 
363  def display(self, exposure, results, convolvedImage=None):
364  """Display detections if so configured
365 
366  Displays the ``exposure`` in frame 0, overlays the detection peaks.
367 
368  Requires that ``lsstDebug`` has been set up correctly, so that
369  ``lsstDebug.Info("lsst.meas.algorithms.detection")`` evaluates `True`.
370 
371  If the ``convolvedImage`` is non-`None` and
372  ``lsstDebug.Info("lsst.meas.algorithms.detection") > 1``, the
373  ``convolvedImage`` will be displayed in frame 1.
374 
375  Parameters
376  ----------
377  exposure : `lsst.afw.image.Exposure`
378  Exposure to display, on which will be plotted the detections.
379  results : `lsst.pipe.base.Struct`
380  Results of the 'detectFootprints' method, containing positive and
381  negative footprints (which contain the peak positions that we will
382  plot). This is a `Struct` with ``positive`` and ``negative``
383  elements that are of type `lsst.afw.detection.FootprintSet`.
384  convolvedImage : `lsst.afw.image.Image`, optional
385  Convolved image used for thresholding.
386  """
387  try:
388  import lsstDebug
389  display = lsstDebug.Info(__name__).display
390  except ImportError:
391  try:
392  display
393  except NameError:
394  display = False
395  if not display:
396  return
397 
398  afwDisplay.setDefaultMaskTransparency(75)
399 
400  disp0 = afwDisplay.Display(frame=0)
401  disp0.mtv(exposure, title="detection")
402 
403  def plotPeaks(fps, ctype):
404  if fps is None:
405  return
406  with disp0.Buffering():
407  for fp in fps.getFootprints():
408  for pp in fp.getPeaks():
409  disp0.dot("+", pp.getFx(), pp.getFy(), ctype=ctype)
410  plotPeaks(results.positive, "yellow")
411  plotPeaks(results.negative, "red")
412 
413  if convolvedImage and display > 1:
414  disp1 = afwDisplay.Display(frame=1)
415  disp1.mtv(convolvedImage, title="PSF smoothed")
416 
417  def applyTempLocalBackground(self, exposure, middle, results):
418  """Apply a temporary local background subtraction
419 
420  This temporary local background serves to suppress noise fluctuations
421  in the wings of bright objects.
422 
423  Peaks in the footprints will be updated.
424 
425  Parameters
426  ----------
427  exposure : `lsst.afw.image.Exposure`
428  Exposure for which to fit local background.
429  middle : `lsst.afw.image.MaskedImage`
430  Convolved image on which detection will be performed
431  (typically smaller than ``exposure`` because the
432  half-kernel has been removed around the edges).
433  results : `lsst.pipe.base.Struct`
434  Results of the 'detectFootprints' method, containing positive and
435  negative footprints (which contain the peak positions that we will
436  plot). This is a `Struct` with ``positive`` and ``negative``
437  elements that are of type `lsst.afw.detection.FootprintSet`.
438  """
439  # Subtract the local background from the smoothed image. Since we
440  # never use the smoothed again we don't need to worry about adding
441  # it back in.
442  bg = self.tempLocalBackground.fitBackground(exposure.getMaskedImage())
443  bgImage = bg.getImageF(self.tempLocalBackground.config.algorithm,
444  self.tempLocalBackground.config.undersampleStyle)
445  middle -= bgImage.Factory(bgImage, middle.getBBox())
446  thresholdPos = self.makeThreshold(middle, "positive")
447  thresholdNeg = self.makeThreshold(middle, "negative")
448  if self.config.thresholdPolarity != "negative":
449  self.updatePeaks(results.positive, middle, thresholdPos)
450  if self.config.thresholdPolarity != "positive":
451  self.updatePeaks(results.negative, middle, thresholdNeg)
452 
453  def clearMask(self, mask):
454  """Clear the DETECTED and DETECTED_NEGATIVE mask planes
455 
456  Removes any previous detection mask in preparation for a new
457  detection pass.
458 
459  Parameters
460  ----------
461  mask : `lsst.afw.image.Mask`
462  Mask to be cleared.
463  """
464  mask &= ~(mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE"))
465 
466  def calculateKernelSize(self, sigma):
467  """Calculate size of smoothing kernel
468 
469  Uses the ``nSigmaForKernel`` configuration parameter. Note
470  that that is the full width of the kernel bounding box
471  (so a value of 7 means 3.5 sigma on either side of center).
472  The value will be rounded up to the nearest odd integer.
473 
474  Parameters
475  ----------
476  sigma : `float`
477  Gaussian sigma of smoothing kernel.
478 
479  Returns
480  -------
481  size : `int`
482  Size of the smoothing kernel.
483  """
484  return (int(sigma * self.config.nSigmaForKernel + 0.5)//2)*2 + 1 # make sure it is odd
485 
486  def getPsf(self, exposure, sigma=None):
487  """Retrieve the PSF for an exposure
488 
489  If ``sigma`` is provided, we make a ``GaussianPsf`` with that,
490  otherwise use the one from the ``exposure``.
491 
492  Parameters
493  ----------
494  exposure : `lsst.afw.image.Exposure`
495  Exposure from which to retrieve the PSF.
496  sigma : `float`, optional
497  Gaussian sigma to use if provided.
498 
499  Returns
500  -------
501  psf : `lsst.afw.detection.Psf`
502  PSF to use for detection.
503  """
504  if sigma is None:
505  psf = exposure.getPsf()
506  if psf is None:
507  raise RuntimeError("Unable to determine PSF to use for detection: no sigma provided")
508  sigma = psf.computeShape().getDeterminantRadius()
509  size = self.calculateKernelSize(sigma)
510  psf = afwDet.GaussianPsf(size, size, sigma)
511  return psf
512 
513  def convolveImage(self, maskedImage, psf, doSmooth=True):
514  """Convolve the image with the PSF
515 
516  We convolve the image with a Gaussian approximation to the PSF,
517  because this is separable and therefore fast. It's technically a
518  correlation rather than a convolution, but since we use a symmetric
519  Gaussian there's no difference.
520 
521  The convolution can be disabled with ``doSmooth=False``. If we do
522  convolve, we mask the edges as ``EDGE`` and return the convolved image
523  with the edges removed. This is because we can't convolve the edges
524  because the kernel would extend off the image.
525 
526  Parameters
527  ----------
528  maskedImage : `lsst.afw.image.MaskedImage`
529  Image to convolve.
530  psf : `lsst.afw.detection.Psf`
531  PSF to convolve with (actually with a Gaussian approximation
532  to it).
533  doSmooth : `bool`
534  Actually do the convolution? Set to False when running on
535  e.g. a pre-convolved image, or a mask plane.
536 
537  Return Struct contents
538  ----------------------
539  middle : `lsst.afw.image.MaskedImage`
540  Convolved image, without the edges.
541  sigma : `float`
542  Gaussian sigma used for the convolution.
543  """
544  self.metadata.set("doSmooth", doSmooth)
545  sigma = psf.computeShape().getDeterminantRadius()
546  self.metadata.set("sigma", sigma)
547 
548  if not doSmooth:
549  middle = maskedImage.Factory(maskedImage)
550  return pipeBase.Struct(middle=middle, sigma=sigma)
551 
552  # Smooth using a Gaussian (which is separable, hence fast) of width sigma
553  # Make a SingleGaussian (separable) kernel with the 'sigma'
554  kWidth = self.calculateKernelSize(sigma)
555  self.metadata.set("smoothingKernelWidth", kWidth)
556  gaussFunc = afwMath.GaussianFunction1D(sigma)
557  gaussKernel = afwMath.SeparableKernel(kWidth, kWidth, gaussFunc, gaussFunc)
558 
559  convolvedImage = maskedImage.Factory(maskedImage.getBBox())
560 
561  afwMath.convolve(convolvedImage, maskedImage, gaussKernel, afwMath.ConvolutionControl())
562  #
563  # Only search psf-smoothed part of frame
564  #
565  goodBBox = gaussKernel.shrinkBBox(convolvedImage.getBBox())
566  middle = convolvedImage.Factory(convolvedImage, goodBBox, afwImage.PARENT, False)
567  #
568  # Mark the parts of the image outside goodBBox as EDGE
569  #
570  self.setEdgeBits(maskedImage, goodBBox, maskedImage.getMask().getPlaneBitMask("EDGE"))
571 
572  return pipeBase.Struct(middle=middle, sigma=sigma)
573 
574  def applyThreshold(self, middle, bbox, factor=1.0):
575  """Apply thresholds to the convolved image
576 
577  Identifies ``Footprint``s, both positive and negative.
578 
579  The threshold can be modified by the provided multiplication
580  ``factor``.
581 
582  Parameters
583  ----------
584  middle : `lsst.afw.image.MaskedImage`
585  Convolved image to threshold.
586  bbox : `lsst.geom.Box2I`
587  Bounding box of unconvolved image.
588  factor : `float`
589  Multiplier for the configured threshold.
590 
591  Return Struct contents
592  ----------------------
593  positive : `lsst.afw.detection.FootprintSet` or `None`
594  Positive detection footprints, if configured.
595  negative : `lsst.afw.detection.FootprintSet` or `None`
596  Negative detection footprints, if configured.
597  factor : `float`
598  Multiplier for the configured threshold.
599  """
600  results = pipeBase.Struct(positive=None, negative=None, factor=factor)
601  # Detect the Footprints (peaks may be replaced if doTempLocalBackground)
602  if self.config.reEstimateBackground or self.config.thresholdPolarity != "negative":
603  threshold = self.makeThreshold(middle, "positive", factor=factor)
604  results.positive = afwDet.FootprintSet(
605  middle,
606  threshold,
607  "DETECTED",
608  self.config.minPixels
609  )
610  results.positive.setRegion(bbox)
611  if self.config.reEstimateBackground or self.config.thresholdPolarity != "positive":
612  threshold = self.makeThreshold(middle, "negative", factor=factor)
613  results.negative = afwDet.FootprintSet(
614  middle,
615  threshold,
616  "DETECTED_NEGATIVE",
617  self.config.minPixels
618  )
619  results.negative.setRegion(bbox)
620 
621  return results
622 
623  def finalizeFootprints(self, mask, results, sigma, factor=1.0):
624  """Finalize the detected footprints
625 
626  Grows the footprints, sets the ``DETECTED`` and ``DETECTED_NEGATIVE``
627  mask planes, and logs the results.
628 
629  ``numPos`` (number of positive footprints), ``numPosPeaks`` (number
630  of positive peaks), ``numNeg`` (number of negative footprints),
631  ``numNegPeaks`` (number of negative peaks) entries are added to the
632  detection results.
633 
634  Parameters
635  ----------
636  mask : `lsst.afw.image.Mask`
637  Mask image on which to flag detected pixels.
638  results : `lsst.pipe.base.Struct`
639  Struct of detection results, including ``positive`` and
640  ``negative`` entries; modified.
641  sigma : `float`
642  Gaussian sigma of PSF.
643  factor : `float`
644  Multiplier for the configured threshold.
645  """
646  for polarity, maskName in (("positive", "DETECTED"), ("negative", "DETECTED_NEGATIVE")):
647  fpSet = getattr(results, polarity)
648  if fpSet is None:
649  continue
650  if self.config.nSigmaToGrow > 0:
651  nGrow = int((self.config.nSigmaToGrow * sigma) + 0.5)
652  self.metadata.set("nGrow", nGrow)
653  if self.config.combinedGrow:
654  fpSet = afwDet.FootprintSet(fpSet, nGrow, self.config.isotropicGrow)
655  else:
656  stencil = (afwGeom.Stencil.CIRCLE if self.config.isotropicGrow else
657  afwGeom.Stencil.MANHATTAN)
658  for fp in fpSet:
659  fp.dilate(nGrow, stencil)
660  fpSet.setMask(mask, maskName)
661  if not self.config.returnOriginalFootprints:
662  setattr(results, polarity, fpSet)
663 
664  results.numPos = 0
665  results.numPosPeaks = 0
666  results.numNeg = 0
667  results.numNegPeaks = 0
668  positive = ""
669  negative = ""
670 
671  if results.positive is not None:
672  results.numPos = len(results.positive.getFootprints())
673  results.numPosPeaks = sum(len(fp.getPeaks()) for fp in results.positive.getFootprints())
674  positive = " %d positive peaks in %d footprints" % (results.numPosPeaks, results.numPos)
675  if results.negative is not None:
676  results.numNeg = len(results.negative.getFootprints())
677  results.numNegPeaks = sum(len(fp.getPeaks()) for fp in results.negative.getFootprints())
678  negative = " %d negative peaks in %d footprints" % (results.numNegPeaks, results.numNeg)
679 
680  self.log.info("Detected%s%s%s to %g %s" %
681  (positive, " and" if positive and negative else "", negative,
682  self.config.thresholdValue*self.config.includeThresholdMultiplier*factor,
683  "DN" if self.config.thresholdType == "value" else "sigma"))
684 
685  def reEstimateBackground(self, maskedImage, backgrounds):
686  """Estimate the background after detection
687 
688  Parameters
689  ----------
690  maskedImage : `lsst.afw.image.MaskedImage`
691  Image on which to estimate the background.
692  backgrounds : `lsst.afw.math.BackgroundList`
693  List of backgrounds; modified.
694 
695  Returns
696  -------
697  bg : `lsst.afw.math.backgroundMI`
698  Empirical background model.
699  """
700  bg = self.background.fitBackground(maskedImage)
701  if self.config.adjustBackground:
702  self.log.warn("Fiddling the background by %g", self.config.adjustBackground)
703  bg += self.config.adjustBackground
704  self.log.info("Resubtracting the background after object detection")
705  maskedImage -= bg.getImageF(self.background.config.algorithm,
706  self.background.config.undersampleStyle)
707 
708  actrl = bg.getBackgroundControl().getApproximateControl()
709  backgrounds.append((bg, getattr(afwMath.Interpolate, self.background.config.algorithm),
710  bg.getAsUsedUndersampleStyle(), actrl.getStyle(), actrl.getOrderX(),
711  actrl.getOrderY(), actrl.getWeighting()))
712  return bg
713 
714  def clearUnwantedResults(self, mask, results):
715  """Clear unwanted results from the Struct of results
716 
717  If we specifically want only positive or only negative detections,
718  drop the ones we don't want, and its associated mask plane.
719 
720  Parameters
721  ----------
722  mask : `lsst.afw.image.Mask`
723  Mask image.
724  results : `lsst.pipe.base.Struct`
725  Detection results, with ``positive`` and ``negative`` elements;
726  modified.
727  """
728  if self.config.thresholdPolarity == "positive":
729  if self.config.reEstimateBackground:
730  mask &= ~mask.getPlaneBitMask("DETECTED_NEGATIVE")
731  results.negative = None
732  elif self.config.thresholdPolarity == "negative":
733  if self.config.reEstimateBackground:
734  mask &= ~mask.getPlaneBitMask("DETECTED")
735  results.positive = None
736 
737  @pipeBase.timeMethod
738  def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None):
739  """Detect footprints on an exposure.
740 
741  Parameters
742  ----------
743  exposure : `lsst.afw.image.Exposure`
744  Exposure to process; DETECTED{,_NEGATIVE} mask plane will be
745  set in-place.
746  doSmooth : `bool`, optional
747  If True, smooth the image before detection using a Gaussian
748  of width ``sigma``, or the measured PSF width of ``exposure``.
749  Set to False when running on e.g. a pre-convolved image, or a mask
750  plane.
751  sigma : `float`, optional
752  Gaussian Sigma of PSF (pixels); used for smoothing and to grow
753  detections; if `None` then measure the sigma of the PSF of the
754  ``exposure``.
755  clearMask : `bool`, optional
756  Clear both DETECTED and DETECTED_NEGATIVE planes before running
757  detection.
758  expId : `dict`, optional
759  Exposure identifier; unused by this implementation, but used for
760  RNG seed by subclasses.
761 
762  Return Struct contents
763  ----------------------
764  positive : `lsst.afw.detection.FootprintSet`
765  Positive polarity footprints (may be `None`)
766  negative : `lsst.afw.detection.FootprintSet`
767  Negative polarity footprints (may be `None`)
768  numPos : `int`
769  Number of footprints in positive or 0 if detection polarity was
770  negative.
771  numNeg : `int`
772  Number of footprints in negative or 0 if detection polarity was
773  positive.
774  background : `lsst.afw.math.BackgroundList`
775  Re-estimated background. `None` if
776  ``reEstimateBackground==False``.
777  factor : `float`
778  Multiplication factor applied to the configured detection
779  threshold.
780  """
781  maskedImage = exposure.maskedImage
782 
783  if clearMask:
784  self.clearMask(maskedImage.getMask())
785 
786  psf = self.getPsf(exposure, sigma=sigma)
787  with self.tempWideBackgroundContext(exposure):
788  convolveResults = self.convolveImage(maskedImage, psf, doSmooth=doSmooth)
789  middle = convolveResults.middle
790  sigma = convolveResults.sigma
791 
792  results = self.applyThreshold(middle, maskedImage.getBBox())
793  results.background = afwMath.BackgroundList()
794  if self.config.doTempLocalBackground:
795  self.applyTempLocalBackground(exposure, middle, results)
796  self.finalizeFootprints(maskedImage.mask, results, sigma)
797 
798  if self.config.reEstimateBackground:
799  self.reEstimateBackground(maskedImage, results.background)
800 
801  self.clearUnwantedResults(maskedImage.getMask(), results)
802  self.display(exposure, results, middle)
803 
804  return results
805 
806  def makeThreshold(self, image, thresholdParity, factor=1.0):
807  """Make an afw.detection.Threshold object corresponding to the task's
808  configuration and the statistics of the given image.
809 
810  Parameters
811  ----------
812  image : `afw.image.MaskedImage`
813  Image to measure noise statistics from if needed.
814  thresholdParity: `str`
815  One of "positive" or "negative", to set the kind of fluctuations
816  the Threshold will detect.
817  factor : `float`
818  Factor by which to multiply the configured detection threshold.
819  This is useful for tweaking the detection threshold slightly.
820 
821  Returns
822  -------
823  threshold : `lsst.afw.detection.Threshold`
824  Detection threshold.
825  """
826  parity = False if thresholdParity == "negative" else True
827  thresholdValue = self.config.thresholdValue
828  thresholdType = self.config.thresholdType
829  if self.config.thresholdType == 'stdev':
830  bad = image.getMask().getPlaneBitMask(self.config.statsMask)
831  sctrl = afwMath.StatisticsControl()
832  sctrl.setAndMask(bad)
833  stats = afwMath.makeStatistics(image, afwMath.STDEVCLIP, sctrl)
834  thresholdValue *= stats.getValue(afwMath.STDEVCLIP)
835  thresholdType = 'value'
836 
837  threshold = afwDet.createThreshold(thresholdValue*factor, thresholdType, parity)
838  threshold.setIncludeMultiplier(self.config.includeThresholdMultiplier)
839  return threshold
840 
841  def updatePeaks(self, fpSet, image, threshold):
842  """Update the Peaks in a FootprintSet by detecting new Footprints and
843  Peaks in an image and using the new Peaks instead of the old ones.
844 
845  Parameters
846  ----------
847  fpSet : `afw.detection.FootprintSet`
848  Set of Footprints whose Peaks should be updated.
849  image : `afw.image.MaskedImage`
850  Image to detect new Footprints and Peak in.
851  threshold : `afw.detection.Threshold`
852  Threshold object for detection.
853 
854  Input Footprints with fewer Peaks than self.config.nPeaksMaxSimple
855  are not modified, and if no new Peaks are detected in an input
856  Footprint, the brightest original Peak in that Footprint is kept.
857  """
858  for footprint in fpSet.getFootprints():
859  oldPeaks = footprint.getPeaks()
860  if len(oldPeaks) <= self.config.nPeaksMaxSimple:
861  continue
862  # We detect a new FootprintSet within each non-simple Footprint's
863  # bbox to avoid a big O(N^2) comparison between the two sets of
864  # Footprints.
865  sub = image.Factory(image, footprint.getBBox())
866  fpSetForPeaks = afwDet.FootprintSet(
867  sub,
868  threshold,
869  "", # don't set a mask plane
870  self.config.minPixels
871  )
872  newPeaks = afwDet.PeakCatalog(oldPeaks.getTable())
873  for fpForPeaks in fpSetForPeaks.getFootprints():
874  for peak in fpForPeaks.getPeaks():
875  if footprint.contains(peak.getI()):
876  newPeaks.append(peak)
877  if len(newPeaks) > 0:
878  del oldPeaks[:]
879  oldPeaks.extend(newPeaks)
880  else:
881  del oldPeaks[1:]
882 
883  @staticmethod
884  def setEdgeBits(maskedImage, goodBBox, edgeBitmask):
885  """Set the edgeBitmask bits for all of maskedImage outside goodBBox
886 
887  Parameters
888  ----------
889  maskedImage : `lsst.afw.image.MaskedImage`
890  Image on which to set edge bits in the mask.
891  goodBBox : `lsst.geom.Box2I`
892  Bounding box of good pixels, in ``LOCAL`` coordinates.
893  edgeBitmask : `lsst.afw.image.MaskPixel`
894  Bit mask to OR with the existing mask bits in the region
895  outside ``goodBBox``.
896  """
897  msk = maskedImage.getMask()
898 
899  mx0, my0 = maskedImage.getXY0()
900  for x0, y0, w, h in ([0, 0,
901  msk.getWidth(), goodBBox.getBeginY() - my0],
902  [0, goodBBox.getEndY() - my0, msk.getWidth(),
903  maskedImage.getHeight() - (goodBBox.getEndY() - my0)],
904  [0, 0,
905  goodBBox.getBeginX() - mx0, msk.getHeight()],
906  [goodBBox.getEndX() - mx0, 0,
907  maskedImage.getWidth() - (goodBBox.getEndX() - mx0), msk.getHeight()],
908  ):
909  edgeMask = msk.Factory(msk, lsst.geom.BoxI(lsst.geom.PointI(x0, y0),
910  lsst.geom.ExtentI(w, h)), afwImage.LOCAL)
911  edgeMask |= edgeBitmask
912 
913  @contextmanager
914  def tempWideBackgroundContext(self, exposure):
915  """Context manager for removing wide (large-scale) background
916 
917  Removing a wide (large-scale) background helps to suppress the
918  detection of large footprints that may overwhelm the deblender.
919  It does, however, set a limit on the maximum scale of objects.
920 
921  The background that we remove will be restored upon exit from
922  the context manager.
923 
924  Parameters
925  ----------
926  exposure : `lsst.afw.image.Exposure`
927  Exposure on which to remove large-scale background.
928 
929  Returns
930  -------
931  context : context manager
932  Context manager that will ensure the temporary wide background
933  is restored.
934  """
935  doTempWideBackground = self.config.doTempWideBackground
936  if doTempWideBackground:
937  self.log.info("Applying temporary wide background subtraction")
938  original = exposure.maskedImage.image.array[:].copy()
939  self.tempWideBackground.run(exposure).background
940  # Remove NO_DATA regions (e.g., edge of the field-of-view); these can cause detections after
941  # subtraction because of extrapolation of the background model into areas with no constraints.
942  image = exposure.maskedImage.image
943  mask = exposure.maskedImage.mask
944  noData = mask.array & mask.getPlaneBitMask("NO_DATA") > 0
945  isGood = mask.array & mask.getPlaneBitMask(self.config.statsMask) == 0
946  image.array[noData] = np.median(image.array[~noData & isGood])
947  try:
948  yield
949  finally:
950  if doTempWideBackground:
951  exposure.maskedImage.image.array[:] = original
952 
953 
954 def addExposures(exposureList):
955  """Add a set of exposures together.
956 
957  Parameters
958  ----------
959  exposureList : `list` of `lsst.afw.image.Exposure`
960  Sequence of exposures to add.
961 
962  Returns
963  -------
964  addedExposure : `lsst.afw.image.Exposure`
965  An exposure of the same size as each exposure in ``exposureList``,
966  with the metadata from ``exposureList[0]`` and a masked image equal
967  to the sum of all the exposure's masked images.
968  """
969  exposure0 = exposureList[0]
970  image0 = exposure0.getMaskedImage()
971 
972  addedImage = image0.Factory(image0, True)
973  addedImage.setXY0(image0.getXY0())
974 
975  for exposure in exposureList[1:]:
976  image = exposure.getMaskedImage()
977  addedImage += image
978 
979  addedExposure = exposure0.Factory(addedImage, exposure0.getWcs())
980  return addedExposure
lsst::meas::algorithms.detection.SourceDetectionTask.clearMask
def clearMask(self, mask)
Definition: detection.py:453
lsst::afw::image
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.
Definition: imageAlgorithm.dox:1
lsst::log.log.logContinued.warn
def warn(fmt, *args)
Definition: logContinued.py:202
lsst::log.log.logContinued.info
def info(fmt, *args)
Definition: logContinued.py:198
lsst::meas::algorithms.detection.addExposures
def addExposures(exposureList)
Definition: detection.py:954
lsst::meas::algorithms.detection.SourceDetectionTask.makeThreshold
def makeThreshold(self, image, thresholdParity, factor=1.0)
Definition: detection.py:806
lsst::meas::algorithms.detection.SourceDetectionConfig.tempLocalBackground
tempLocalBackground
Definition: detection.py:107
lsst::afw.display
Definition: __init__.py:1
lsst::afw::math::makeStatistics
Statistics makeStatistics(lsst::afw::math::MaskedVector< EntryT > const &mv, std::vector< WeightPixel > const &vweights, int const flags, StatisticsControl const &sctrl=StatisticsControl())
The makeStatistics() overload to handle lsst::afw::math::MaskedVector<>
Definition: Statistics.h:520
lsst::meas::algorithms.detection.SourceDetectionTask.clearUnwantedResults
def clearUnwantedResults(self, mask, results)
Definition: detection.py:714
lsst::meas::algorithms.detection.SourceDetectionConfig.setDefaults
def setDefaults(self)
Definition: detection.py:147
lsst::meas::algorithms.detection.SourceDetectionTask.tempWideBackgroundContext
def tempWideBackgroundContext(self, exposure)
Definition: detection.py:914
lsst::meas::algorithms.detection.SourceDetectionTask.detectFootprints
def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None)
Definition: detection.py:738
lsst::meas::algorithms.detection.SourceDetectionConfig
Configuration parameters for the SourceDetectionTask.
Definition: detection.py:43
lsst::afw::detection::FootprintSet
A set of Footprints, associated with a MaskedImage.
Definition: FootprintSet.h:53
lsst::meas::algorithms.detection.SourceDetectionTask.finalizeFootprints
def finalizeFootprints(self, mask, results, sigma, factor=1.0)
Definition: detection.py:623
lsst::meas::algorithms.detection.SourceDetectionTask.convolveImage
def convolveImage(self, maskedImage, psf, doSmooth=True)
Definition: detection.py:513
lsst::meas::algorithms.detection.SourceDetectionTask.applyTempLocalBackground
def applyTempLocalBackground(self, exposure, middle, results)
Definition: detection.py:417
lsstDebug.Info
Definition: lsstDebug.py:28
lsst::meas::algorithms.detection.SourceDetectionTask.calculateKernelSize
def calculateKernelSize(self, sigma)
Definition: detection.py:466
lsst::afw::table._source.SourceCatalog
Definition: _source.py:33
lsst::meas::algorithms.detection.SourceDetectionTask.__init__
def __init__(self, schema=None, **kwds)
Create the detection task.
Definition: detection.py:266
lsst::afw::detection::GaussianPsf
A circularly symmetric Gaussian Psf class with no spatial variation, intended mostly for testing purp...
Definition: GaussianPsf.h:42
lsst::meas::algorithms.detection.SourceDetectionConfig.tempWideBackground
tempWideBackground
Definition: detection.py:118
lsst::meas::algorithms.detection.SourceDetectionTask.display
def display(self, exposure, results, convolvedImage=None)
Definition: detection.py:363
lsst::afw::detection::createThreshold
Threshold createThreshold(const double value, const std::string type="value", const bool polarity=true)
Factory method for creating Threshold objects.
Definition: Threshold.cc:109
lsst::afw::math.backgroundList.BackgroundList
Definition: backgroundList.py:32
lsst::meas::algorithms.detection.SourceDetectionTask.negativeFlagKey
negativeFlagKey
Definition: detection.py:281
lsst::afw::table
Definition: table.dox:3
lsst::meas::algorithms.detection.SourceDetectionTask.setEdgeBits
def setEdgeBits(maskedImage, goodBBox, edgeBitmask)
Definition: detection.py:884
lsst::afw::detection
Definition: Footprint.h:50
lsst::afw::math::StatisticsControl
Pass parameters to a Statistics object.
Definition: Statistics.h:93
lsst::geom
Definition: geomOperators.dox:4
lsst::afw::math::ConvolutionControl
Parameters to control convolution.
Definition: ConvolveImage.h:50
lsst::afw::math
Definition: statistics.dox:6
lsst::geom::Point< int, 2 >
lsst::meas::algorithms.detection.SourceDetectionTask.getPsf
def getPsf(self, exposure, sigma=None)
Definition: detection.py:486
lsst::geom::Box2I
An integer coordinate rectangle.
Definition: Box.h:55
lsst::afw::math::convolve
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, bool doNormalize, bool doCopyEdge=false)
Old, deprecated version of convolve.
Definition: ConvolveImage.cc:199
lsst::meas::algorithms.detection.SourceDetectionTask
Detect positive and negative sources on an exposure and return a new table.SourceCatalog.
Definition: detection.py:169
lsst::meas::algorithms.detection.SourceDetectionTask.updatePeaks
def updatePeaks(self, fpSet, image, threshold)
Definition: detection.py:841
lsst::afw::math::Interpolate
Definition: Interpolate.h:36
lsst.pipe.base
Definition: __init__.py:1
lsst::meas::algorithms.detection.SourceDetectionTask.reEstimateBackground
def reEstimateBackground(self, maskedImage, backgrounds)
Definition: detection.py:685
lsst::afw::table::CatalogT< PeakRecord >
lsst::geom::Extent< int, 2 >
set
daf::base::PropertySet * set
Definition: fits.cc:912
lsst::afw::math::SeparableKernel
A kernel described by a pair of functions: func(x, y) = colFunc(x) * rowFunc(y)
Definition: Kernel.h:907
lsst::meas::algorithms.detection.SourceDetectionTask.run
def run(self, table, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None)
Definition: detection.py:298
lsst::meas::algorithms.detection.SourceDetectionTask.applyThreshold
def applyThreshold(self, middle, bbox, factor=1.0)
Definition: detection.py:574
lsst::meas::algorithms.detection.SourceDetectionTask.makeSourceCatalog
def makeSourceCatalog(self, *args, **kwargs)
Definition: detection.py:360
lsst::afw::geom
Definition: frameSetUtils.h:40