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