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