LSSTApplications  18.0.0+106,18.0.0+50,19.0.0,19.0.0+1,19.0.0+10,19.0.0+11,19.0.0+13,19.0.0+17,19.0.0+2,19.0.0-1-g20d9b18+6,19.0.0-1-g425ff20,19.0.0-1-g5549ca4,19.0.0-1-g580fafe+6,19.0.0-1-g6fe20d0+1,19.0.0-1-g7011481+9,19.0.0-1-g8c57eb9+6,19.0.0-1-gb5175dc+11,19.0.0-1-gdc0e4a7+9,19.0.0-1-ge272bc4+6,19.0.0-1-ge3aa853,19.0.0-10-g448f008b,19.0.0-12-g6990b2c,19.0.0-2-g0d9f9cd+11,19.0.0-2-g3d9e4fb2+11,19.0.0-2-g5037de4,19.0.0-2-gb96a1c4+3,19.0.0-2-gd955cfd+15,19.0.0-3-g2d13df8,19.0.0-3-g6f3c7dc,19.0.0-4-g725f80e+11,19.0.0-4-ga671dab3b+1,19.0.0-4-gad373c5+3,19.0.0-5-ga2acb9c+2,19.0.0-5-gfe96e6c+2,w.2020.01
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 
28 import numpy as np
29 
30 import lsst.geom
31 import lsst.afw.display as afwDisplay
32 import lsst.afw.detection as afwDet
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; exact meaning and units depend on thresholdType.",
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  r"""!
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 afwDisplay.Display's frame 0.
209  +ve detections in blue, -ve detections in cyan
210  - If display > 1, display the convolved exposure on frame 1
211 </DL>
212 
213 @section meas_algorithms_detection_Example A complete example of using SourceDetectionTask
214 
215 This code is in @link measAlgTasks.py@endlink in the examples directory, and can be run as @em e.g.
216 @code
217 examples/measAlgTasks.py --doDisplay
218 @endcode
219 @dontinclude measAlgTasks.py
220 The example also runs the SingleFrameMeasurementTask; see @ref meas_algorithms_measurement_Example for more
221 explanation.
222 
223 Import the task (there are some other standard imports; read the file if you're confused)
224 @skipline SourceDetectionTask
225 
226 We need to create our task before processing any data as the task constructor
227 can add an extra column to the schema, but first we need an almost-empty Schema
228 @skipline makeMinimalSchema
229 after which we can call the constructor:
230 @skip SourceDetectionTask.ConfigClass
231 @until detectionTask
232 
233 We're now ready to process the data (we could loop over multiple exposures/catalogues using the same
234 task objects). First create the output table:
235 @skipline afwTable
236 
237 And process the image
238 @skipline result
239 (You may not be happy that the threshold was set in the config before creating the Task rather than being set
240 separately for each exposure. You @em can reset it just before calling the run method if you must, but we
241 should really implement a better solution).
242 
243 We can then unpack and use the results:
244 @skip sources
245 @until print
246 
247 <HR>
248 To investigate the @ref meas_algorithms_detection_Debug, put something like
249 @code{.py}
250  import lsstDebug
251  def DebugInfo(name):
252  di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
253  if name == "lsst.meas.algorithms.detection":
254  di.display = 1
255 
256  return di
257 
258  lsstDebug.Info = DebugInfo
259 @endcode
260 into your debug.py file and run measAlgTasks.py with the @c --debug flag.
261  """
262  ConfigClass = SourceDetectionConfig
263  _DefaultName = "sourceDetection"
264 
265  def __init__(self, schema=None, **kwds):
266  """!Create the detection task. Most arguments are simply passed onto pipe.base.Task.
267 
268  @param schema An lsst::afw::table::Schema used to create the output lsst.afw.table.SourceCatalog
269  @param **kwds Keyword arguments passed to lsst.pipe.base.task.Task.__init__.
270 
271  If schema is not None and configured for 'both' detections,
272  a 'flags.negative' field will be added to label detections made with a
273  negative threshold.
274 
275  @note This task can add fields to the schema, so any code calling this task must ensure that
276  these columns are indeed present in the input match list; see @ref Example
277  """
278  pipeBase.Task.__init__(self, **kwds)
279  if schema is not None and self.config.thresholdPolarity == "both":
280  self.negativeFlagKey = schema.addField(
281  "flags_negative", type="Flag",
282  doc="set if source was detected as significantly negative"
283  )
284  else:
285  if self.config.thresholdPolarity == "both":
286  self.log.warn("Detection polarity set to 'both', but no flag will be "
287  "set to distinguish between positive and negative detections")
288  self.negativeFlagKey = None
289  if self.config.reEstimateBackground:
290  self.makeSubtask("background")
291  if self.config.doTempLocalBackground:
292  self.makeSubtask("tempLocalBackground")
293  if self.config.doTempWideBackground:
294  self.makeSubtask("tempWideBackground")
295 
296  @pipeBase.timeMethod
297  def run(self, table, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None):
298  """Run source detection and create a SourceCatalog of detections.
299 
300  Parameters
301  ----------
302  table : `lsst.afw.table.SourceTable`
303  Table object that will be used to create the SourceCatalog.
304  exposure : `lsst.afw.image.Exposure`
305  Exposure to process; DETECTED mask plane will be set in-place.
306  doSmooth : `bool`
307  If True, smooth the image before detection using a Gaussian of width
308  ``sigma``, or the measured PSF width. Set to False when running on
309  e.g. a pre-convolved image, or a mask plane.
310  sigma : `float`
311  Sigma of PSF (pixels); used for smoothing and to grow detections;
312  if None then measure the sigma of the PSF of the exposure
313  clearMask : `bool`
314  Clear DETECTED{,_NEGATIVE} planes before running detection.
315  expId : `int`
316  Exposure identifier; unused by this implementation, but used for
317  RNG seed by subclasses.
318 
319  Returns
320  -------
321  result : `lsst.pipe.base.Struct`
322  ``sources``
323  The detected sources (`lsst.afw.table.SourceCatalog`)
324  ``fpSets``
325  The result resturned by `detectFootprints`
326  (`lsst.pipe.base.Struct`).
327 
328  Raises
329  ------
330  ValueError
331  If flags.negative is needed, but isn't in table's schema.
332  lsst.pipe.base.TaskError
333  If sigma=None, doSmooth=True and the exposure has no PSF.
334 
335  Notes
336  -----
337  If you want to avoid dealing with Sources and Tables, you can use
338  detectFootprints() to just get the `lsst.afw.detection.FootprintSet`s.
339  """
340  if self.negativeFlagKey is not None and self.negativeFlagKey not in table.getSchema():
341  raise ValueError("Table has incorrect Schema")
342  results = self.detectFootprints(exposure=exposure, doSmooth=doSmooth, sigma=sigma,
343  clearMask=clearMask, expId=expId)
344  sources = afwTable.SourceCatalog(table)
345  sources.reserve(results.numPos + results.numNeg)
346  if results.negative:
347  results.negative.makeSources(sources)
348  if self.negativeFlagKey:
349  for record in sources:
350  record.set(self.negativeFlagKey, True)
351  if results.positive:
352  results.positive.makeSources(sources)
353  results.fpSets = results.copy() # Backward compatibility
354  results.sources = sources
355  return results
356 
357 
358  makeSourceCatalog = run
359 
360  def display(self, exposure, results, convolvedImage=None):
361  """Display detections if so configured
362 
363  Displays the ``exposure`` in frame 0, overlays the detection peaks.
364 
365  Requires that ``lsstDebug`` has been set up correctly, so that
366  ``lsstDebug.Info("lsst.meas.algorithms.detection")`` evaluates `True`.
367 
368  If the ``convolvedImage`` is non-`None` and
369  ``lsstDebug.Info("lsst.meas.algorithms.detection") > 1``, the
370  ``convolvedImage`` will be displayed in frame 1.
371 
372  Parameters
373  ----------
374  exposure : `lsst.afw.image.Exposure`
375  Exposure to display, on which will be plotted the detections.
376  results : `lsst.pipe.base.Struct`
377  Results of the 'detectFootprints' method, containing positive and
378  negative footprints (which contain the peak positions that we will
379  plot). This is a `Struct` with ``positive`` and ``negative``
380  elements that are of type `lsst.afw.detection.FootprintSet`.
381  convolvedImage : `lsst.afw.image.Image`, optional
382  Convolved image used for thresholding.
383  """
384  try:
385  import lsstDebug
386  display = lsstDebug.Info(__name__).display
387  except ImportError:
388  try:
389  display
390  except NameError:
391  display = False
392  if not display:
393  return
394 
395  afwDisplay.setDefaultMaskTransparency(75)
396 
397  disp0 = afwDisplay.Display(frame=0)
398  disp0.mtv(exposure, title="detection")
399 
400  def plotPeaks(fps, ctype):
401  if fps is None:
402  return
403  with disp0.Buffering():
404  for fp in fps.getFootprints():
405  for pp in fp.getPeaks():
406  disp0.dot("+", pp.getFx(), pp.getFy(), ctype=ctype)
407  plotPeaks(results.positive, "yellow")
408  plotPeaks(results.negative, "red")
409 
410  if convolvedImage and display > 1:
411  disp1 = afwDisplay.Display(frame=1)
412  disp1.mtv(convolvedImage, title="PSF smoothed")
413 
414  def applyTempLocalBackground(self, exposure, middle, results):
415  """Apply a temporary local background subtraction
416 
417  This temporary local background serves to suppress noise fluctuations
418  in the wings of bright objects.
419 
420  Peaks in the footprints will be updated.
421 
422  Parameters
423  ----------
424  exposure : `lsst.afw.image.Exposure`
425  Exposure for which to fit local background.
426  middle : `lsst.afw.image.MaskedImage`
427  Convolved image on which detection will be performed
428  (typically smaller than ``exposure`` because the
429  half-kernel has been removed around the edges).
430  results : `lsst.pipe.base.Struct`
431  Results of the 'detectFootprints' method, containing positive and
432  negative footprints (which contain the peak positions that we will
433  plot). This is a `Struct` with ``positive`` and ``negative``
434  elements that are of type `lsst.afw.detection.FootprintSet`.
435  """
436  # Subtract the local background from the smoothed image. Since we
437  # never use the smoothed again we don't need to worry about adding
438  # it back in.
439  bg = self.tempLocalBackground.fitBackground(exposure.getMaskedImage())
440  bgImage = bg.getImageF()
441  middle -= bgImage.Factory(bgImage, middle.getBBox())
442  thresholdPos = self.makeThreshold(middle, "positive")
443  thresholdNeg = self.makeThreshold(middle, "negative")
444  if self.config.thresholdPolarity != "negative":
445  self.updatePeaks(results.positive, middle, thresholdPos)
446  if self.config.thresholdPolarity != "positive":
447  self.updatePeaks(results.negative, middle, thresholdNeg)
448 
449  def clearMask(self, mask):
450  """Clear the DETECTED and DETECTED_NEGATIVE mask planes
451 
452  Removes any previous detection mask in preparation for a new
453  detection pass.
454 
455  Parameters
456  ----------
457  mask : `lsst.afw.image.Mask`
458  Mask to be cleared.
459  """
460  mask &= ~(mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE"))
461 
462  def calculateKernelSize(self, sigma):
463  """Calculate size of smoothing kernel
464 
465  Uses the ``nSigmaForKernel`` configuration parameter. Note
466  that that is the full width of the kernel bounding box
467  (so a value of 7 means 3.5 sigma on either side of center).
468  The value will be rounded up to the nearest odd integer.
469 
470  Parameters
471  ----------
472  sigma : `float`
473  Gaussian sigma of smoothing kernel.
474 
475  Returns
476  -------
477  size : `int`
478  Size of the smoothing kernel.
479  """
480  return (int(sigma * self.config.nSigmaForKernel + 0.5)//2)*2 + 1 # make sure it is odd
481 
482  def getPsf(self, exposure, sigma=None):
483  """Retrieve the PSF for an exposure
484 
485  If ``sigma`` is provided, we make a ``GaussianPsf`` with that,
486  otherwise use the one from the ``exposure``.
487 
488  Parameters
489  ----------
490  exposure : `lsst.afw.image.Exposure`
491  Exposure from which to retrieve the PSF.
492  sigma : `float`, optional
493  Gaussian sigma to use if provided.
494 
495  Returns
496  -------
497  psf : `lsst.afw.detection.Psf`
498  PSF to use for detection.
499  """
500  if sigma is None:
501  psf = exposure.getPsf()
502  if psf is None:
503  raise RuntimeError("Unable to determine PSF to use for detection: no sigma provided")
504  sigma = psf.computeShape().getDeterminantRadius()
505  size = self.calculateKernelSize(sigma)
506  psf = afwDet.GaussianPsf(size, size, sigma)
507  return psf
508 
509  def convolveImage(self, maskedImage, psf, doSmooth=True):
510  """Convolve the image with the PSF
511 
512  We convolve the image with a Gaussian approximation to the PSF,
513  because this is separable and therefore fast. It's technically a
514  correlation rather than a convolution, but since we use a symmetric
515  Gaussian there's no difference.
516 
517  The convolution can be disabled with ``doSmooth=False``. If we do
518  convolve, we mask the edges as ``EDGE`` and return the convolved image
519  with the edges removed. This is because we can't convolve the edges
520  because the kernel would extend off the image.
521 
522  Parameters
523  ----------
524  maskedImage : `lsst.afw.image.MaskedImage`
525  Image to convolve.
526  psf : `lsst.afw.detection.Psf`
527  PSF to convolve with (actually with a Gaussian approximation
528  to it).
529  doSmooth : `bool`
530  Actually do the convolution? Set to False when running on
531  e.g. a pre-convolved image, or a mask plane.
532 
533  Return Struct contents
534  ----------------------
535  middle : `lsst.afw.image.MaskedImage`
536  Convolved image, without the edges.
537  sigma : `float`
538  Gaussian sigma used for the convolution.
539  """
540  self.metadata.set("doSmooth", doSmooth)
541  sigma = psf.computeShape().getDeterminantRadius()
542  self.metadata.set("sigma", sigma)
543 
544  if not doSmooth:
545  middle = maskedImage.Factory(maskedImage)
546  return pipeBase.Struct(middle=middle, sigma=sigma)
547 
548  # Smooth using a Gaussian (which is separable, hence fast) of width sigma
549  # Make a SingleGaussian (separable) kernel with the 'sigma'
550  kWidth = self.calculateKernelSize(sigma)
551  self.metadata.set("smoothingKernelWidth", kWidth)
552  gaussFunc = afwMath.GaussianFunction1D(sigma)
553  gaussKernel = afwMath.SeparableKernel(kWidth, kWidth, gaussFunc, gaussFunc)
554 
555  convolvedImage = maskedImage.Factory(maskedImage.getBBox())
556 
557  afwMath.convolve(convolvedImage, maskedImage, gaussKernel, afwMath.ConvolutionControl())
558  #
559  # Only search psf-smoothed part of frame
560  #
561  goodBBox = gaussKernel.shrinkBBox(convolvedImage.getBBox())
562  middle = convolvedImage.Factory(convolvedImage, goodBBox, afwImage.PARENT, False)
563  #
564  # Mark the parts of the image outside goodBBox as EDGE
565  #
566  self.setEdgeBits(maskedImage, goodBBox, maskedImage.getMask().getPlaneBitMask("EDGE"))
567 
568  return pipeBase.Struct(middle=middle, sigma=sigma)
569 
570  def applyThreshold(self, middle, bbox, factor=1.0):
571  """Apply thresholds to the convolved image
572 
573  Identifies ``Footprint``s, both positive and negative.
574 
575  The threshold can be modified by the provided multiplication
576  ``factor``.
577 
578  Parameters
579  ----------
580  middle : `lsst.afw.image.MaskedImage`
581  Convolved image to threshold.
582  bbox : `lsst.geom.Box2I`
583  Bounding box of unconvolved image.
584  factor : `float`
585  Multiplier for the configured threshold.
586 
587  Return Struct contents
588  ----------------------
589  positive : `lsst.afw.detection.FootprintSet` or `None`
590  Positive detection footprints, if configured.
591  negative : `lsst.afw.detection.FootprintSet` or `None`
592  Negative detection footprints, if configured.
593  factor : `float`
594  Multiplier for the configured threshold.
595  """
596  results = pipeBase.Struct(positive=None, negative=None, factor=factor)
597  # Detect the Footprints (peaks may be replaced if doTempLocalBackground)
598  if self.config.reEstimateBackground or self.config.thresholdPolarity != "negative":
599  threshold = self.makeThreshold(middle, "positive", factor=factor)
600  results.positive = afwDet.FootprintSet(
601  middle,
602  threshold,
603  "DETECTED",
604  self.config.minPixels
605  )
606  results.positive.setRegion(bbox)
607  if self.config.reEstimateBackground or self.config.thresholdPolarity != "positive":
608  threshold = self.makeThreshold(middle, "negative", factor=factor)
609  results.negative = afwDet.FootprintSet(
610  middle,
611  threshold,
612  "DETECTED_NEGATIVE",
613  self.config.minPixels
614  )
615  results.negative.setRegion(bbox)
616 
617  return results
618 
619  def finalizeFootprints(self, mask, results, sigma, factor=1.0):
620  """Finalize the detected footprints
621 
622  Grows the footprints, sets the ``DETECTED`` and ``DETECTED_NEGATIVE``
623  mask planes, and logs the results.
624 
625  ``numPos`` (number of positive footprints), ``numPosPeaks`` (number
626  of positive peaks), ``numNeg`` (number of negative footprints),
627  ``numNegPeaks`` (number of negative peaks) entries are added to the
628  detection results.
629 
630  Parameters
631  ----------
632  mask : `lsst.afw.image.Mask`
633  Mask image on which to flag detected pixels.
634  results : `lsst.pipe.base.Struct`
635  Struct of detection results, including ``positive`` and
636  ``negative`` entries; modified.
637  sigma : `float`
638  Gaussian sigma of PSF.
639  factor : `float`
640  Multiplier for the configured threshold.
641  """
642  for polarity, maskName in (("positive", "DETECTED"), ("negative", "DETECTED_NEGATIVE")):
643  fpSet = getattr(results, polarity)
644  if fpSet is None:
645  continue
646  if self.config.nSigmaToGrow > 0:
647  nGrow = int((self.config.nSigmaToGrow * sigma) + 0.5)
648  self.metadata.set("nGrow", nGrow)
649  if self.config.combinedGrow:
650  fpSet = afwDet.FootprintSet(fpSet, nGrow, self.config.isotropicGrow)
651  else:
652  stencil = (afwGeom.Stencil.CIRCLE if self.config.isotropicGrow else
653  afwGeom.Stencil.MANHATTAN)
654  for fp in fpSet:
655  fp.dilate(nGrow, stencil)
656  fpSet.setMask(mask, maskName)
657  if not self.config.returnOriginalFootprints:
658  setattr(results, polarity, fpSet)
659 
660  results.numPos = 0
661  results.numPosPeaks = 0
662  results.numNeg = 0
663  results.numNegPeaks = 0
664  positive = ""
665  negative = ""
666 
667  if results.positive is not None:
668  results.numPos = len(results.positive.getFootprints())
669  results.numPosPeaks = sum(len(fp.getPeaks()) for fp in results.positive.getFootprints())
670  positive = " %d positive peaks in %d footprints" % (results.numPosPeaks, results.numPos)
671  if results.negative is not None:
672  results.numNeg = len(results.negative.getFootprints())
673  results.numNegPeaks = sum(len(fp.getPeaks()) for fp in results.negative.getFootprints())
674  negative = " %d negative peaks in %d footprints" % (results.numNegPeaks, results.numNeg)
675 
676  self.log.info("Detected%s%s%s to %g %s" %
677  (positive, " and" if positive and negative else "", negative,
678  self.config.thresholdValue*self.config.includeThresholdMultiplier*factor,
679  "DN" if self.config.thresholdType == "value" else "sigma"))
680 
681  def reEstimateBackground(self, maskedImage, backgrounds):
682  """Estimate the background after detection
683 
684  Parameters
685  ----------
686  maskedImage : `lsst.afw.image.MaskedImage`
687  Image on which to estimate the background.
688  backgrounds : `lsst.afw.math.BackgroundList`
689  List of backgrounds; modified.
690 
691  Returns
692  -------
693  bg : `lsst.afw.math.backgroundMI`
694  Empirical background model.
695  """
696  bg = self.background.fitBackground(maskedImage)
697  if self.config.adjustBackground:
698  self.log.warn("Fiddling the background by %g", self.config.adjustBackground)
699  bg += self.config.adjustBackground
700  self.log.info("Resubtracting the background after object detection")
701  maskedImage -= bg.getImageF()
702  backgrounds.append(bg)
703  return bg
704 
705  def clearUnwantedResults(self, mask, results):
706  """Clear unwanted results from the Struct of results
707 
708  If we specifically want only positive or only negative detections,
709  drop the ones we don't want, and its associated mask plane.
710 
711  Parameters
712  ----------
713  mask : `lsst.afw.image.Mask`
714  Mask image.
715  results : `lsst.pipe.base.Struct`
716  Detection results, with ``positive`` and ``negative`` elements;
717  modified.
718  """
719  if self.config.thresholdPolarity == "positive":
720  if self.config.reEstimateBackground:
721  mask &= ~mask.getPlaneBitMask("DETECTED_NEGATIVE")
722  results.negative = None
723  elif self.config.thresholdPolarity == "negative":
724  if self.config.reEstimateBackground:
725  mask &= ~mask.getPlaneBitMask("DETECTED")
726  results.positive = None
727 
728  @pipeBase.timeMethod
729  def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None):
730  """Detect footprints on an exposure.
731 
732  Parameters
733  ----------
734  exposure : `lsst.afw.image.Exposure`
735  Exposure to process; DETECTED{,_NEGATIVE} mask plane will be
736  set in-place.
737  doSmooth : `bool`, optional
738  If True, smooth the image before detection using a Gaussian
739  of width ``sigma``, or the measured PSF width of ``exposure``.
740  Set to False when running on e.g. a pre-convolved image, or a mask
741  plane.
742  sigma : `float`, optional
743  Gaussian Sigma of PSF (pixels); used for smoothing and to grow
744  detections; if `None` then measure the sigma of the PSF of the
745  ``exposure``.
746  clearMask : `bool`, optional
747  Clear both DETECTED and DETECTED_NEGATIVE planes before running
748  detection.
749  expId : `dict`, optional
750  Exposure identifier; unused by this implementation, but used for
751  RNG seed by subclasses.
752 
753  Return Struct contents
754  ----------------------
755  positive : `lsst.afw.detection.FootprintSet`
756  Positive polarity footprints (may be `None`)
757  negative : `lsst.afw.detection.FootprintSet`
758  Negative polarity footprints (may be `None`)
759  numPos : `int`
760  Number of footprints in positive or 0 if detection polarity was
761  negative.
762  numNeg : `int`
763  Number of footprints in negative or 0 if detection polarity was
764  positive.
765  background : `lsst.afw.math.BackgroundList`
766  Re-estimated background. `None` if
767  ``reEstimateBackground==False``.
768  factor : `float`
769  Multiplication factor applied to the configured detection
770  threshold.
771  """
772  maskedImage = exposure.maskedImage
773 
774  if clearMask:
775  self.clearMask(maskedImage.getMask())
776 
777  psf = self.getPsf(exposure, sigma=sigma)
778  with self.tempWideBackgroundContext(exposure):
779  convolveResults = self.convolveImage(maskedImage, psf, doSmooth=doSmooth)
780  middle = convolveResults.middle
781  sigma = convolveResults.sigma
782 
783  results = self.applyThreshold(middle, maskedImage.getBBox())
784  results.background = afwMath.BackgroundList()
785  if self.config.doTempLocalBackground:
786  self.applyTempLocalBackground(exposure, middle, results)
787  self.finalizeFootprints(maskedImage.mask, results, sigma)
788 
789  if self.config.reEstimateBackground:
790  self.reEstimateBackground(maskedImage, results.background)
791 
792  self.clearUnwantedResults(maskedImage.getMask(), results)
793  self.display(exposure, results, middle)
794 
795  return results
796 
797  def makeThreshold(self, image, thresholdParity, factor=1.0):
798  """Make an afw.detection.Threshold object corresponding to the task's
799  configuration and the statistics of the given image.
800 
801  Parameters
802  ----------
803  image : `afw.image.MaskedImage`
804  Image to measure noise statistics from if needed.
805  thresholdParity: `str`
806  One of "positive" or "negative", to set the kind of fluctuations
807  the Threshold will detect.
808  factor : `float`
809  Factor by which to multiply the configured detection threshold.
810  This is useful for tweaking the detection threshold slightly.
811 
812  Returns
813  -------
814  threshold : `lsst.afw.detection.Threshold`
815  Detection threshold.
816  """
817  parity = False if thresholdParity == "negative" else True
818  thresholdValue = self.config.thresholdValue
819  thresholdType = self.config.thresholdType
820  if self.config.thresholdType == 'stdev':
821  bad = image.getMask().getPlaneBitMask(self.config.statsMask)
822  sctrl = afwMath.StatisticsControl()
823  sctrl.setAndMask(bad)
824  stats = afwMath.makeStatistics(image, afwMath.STDEVCLIP, sctrl)
825  thresholdValue *= stats.getValue(afwMath.STDEVCLIP)
826  thresholdType = 'value'
827 
828  threshold = afwDet.createThreshold(thresholdValue*factor, thresholdType, parity)
829  threshold.setIncludeMultiplier(self.config.includeThresholdMultiplier)
830  return threshold
831 
832  def updatePeaks(self, fpSet, image, threshold):
833  """Update the Peaks in a FootprintSet by detecting new Footprints and
834  Peaks in an image and using the new Peaks instead of the old ones.
835 
836  Parameters
837  ----------
838  fpSet : `afw.detection.FootprintSet`
839  Set of Footprints whose Peaks should be updated.
840  image : `afw.image.MaskedImage`
841  Image to detect new Footprints and Peak in.
842  threshold : `afw.detection.Threshold`
843  Threshold object for detection.
844 
845  Input Footprints with fewer Peaks than self.config.nPeaksMaxSimple
846  are not modified, and if no new Peaks are detected in an input
847  Footprint, the brightest original Peak in that Footprint is kept.
848  """
849  for footprint in fpSet.getFootprints():
850  oldPeaks = footprint.getPeaks()
851  if len(oldPeaks) <= self.config.nPeaksMaxSimple:
852  continue
853  # We detect a new FootprintSet within each non-simple Footprint's
854  # bbox to avoid a big O(N^2) comparison between the two sets of
855  # Footprints.
856  sub = image.Factory(image, footprint.getBBox())
857  fpSetForPeaks = afwDet.FootprintSet(
858  sub,
859  threshold,
860  "", # don't set a mask plane
861  self.config.minPixels
862  )
863  newPeaks = afwDet.PeakCatalog(oldPeaks.getTable())
864  for fpForPeaks in fpSetForPeaks.getFootprints():
865  for peak in fpForPeaks.getPeaks():
866  if footprint.contains(peak.getI()):
867  newPeaks.append(peak)
868  if len(newPeaks) > 0:
869  del oldPeaks[:]
870  oldPeaks.extend(newPeaks)
871  else:
872  del oldPeaks[1:]
873 
874  @staticmethod
875  def setEdgeBits(maskedImage, goodBBox, edgeBitmask):
876  """Set the edgeBitmask bits for all of maskedImage outside goodBBox
877 
878  Parameters
879  ----------
880  maskedImage : `lsst.afw.image.MaskedImage`
881  Image on which to set edge bits in the mask.
882  goodBBox : `lsst.geom.Box2I`
883  Bounding box of good pixels, in ``LOCAL`` coordinates.
884  edgeBitmask : `lsst.afw.image.MaskPixel`
885  Bit mask to OR with the existing mask bits in the region
886  outside ``goodBBox``.
887  """
888  msk = maskedImage.getMask()
889 
890  mx0, my0 = maskedImage.getXY0()
891  for x0, y0, w, h in ([0, 0,
892  msk.getWidth(), goodBBox.getBeginY() - my0],
893  [0, goodBBox.getEndY() - my0, msk.getWidth(),
894  maskedImage.getHeight() - (goodBBox.getEndY() - my0)],
895  [0, 0,
896  goodBBox.getBeginX() - mx0, msk.getHeight()],
897  [goodBBox.getEndX() - mx0, 0,
898  maskedImage.getWidth() - (goodBBox.getEndX() - mx0), msk.getHeight()],
899  ):
900  edgeMask = msk.Factory(msk, lsst.geom.BoxI(lsst.geom.PointI(x0, y0),
901  lsst.geom.ExtentI(w, h)), afwImage.LOCAL)
902  edgeMask |= edgeBitmask
903 
904  @contextmanager
905  def tempWideBackgroundContext(self, exposure):
906  """Context manager for removing wide (large-scale) background
907 
908  Removing a wide (large-scale) background helps to suppress the
909  detection of large footprints that may overwhelm the deblender.
910  It does, however, set a limit on the maximum scale of objects.
911 
912  The background that we remove will be restored upon exit from
913  the context manager.
914 
915  Parameters
916  ----------
917  exposure : `lsst.afw.image.Exposure`
918  Exposure on which to remove large-scale background.
919 
920  Returns
921  -------
922  context : context manager
923  Context manager that will ensure the temporary wide background
924  is restored.
925  """
926  doTempWideBackground = self.config.doTempWideBackground
927  if doTempWideBackground:
928  self.log.info("Applying temporary wide background subtraction")
929  original = exposure.maskedImage.image.array[:].copy()
930  self.tempWideBackground.run(exposure).background
931  # Remove NO_DATA regions (e.g., edge of the field-of-view); these can cause detections after
932  # subtraction because of extrapolation of the background model into areas with no constraints.
933  image = exposure.maskedImage.image
934  mask = exposure.maskedImage.mask
935  noData = mask.array & mask.getPlaneBitMask("NO_DATA") > 0
936  isGood = mask.array & mask.getPlaneBitMask(self.config.statsMask) == 0
937  image.array[noData] = np.median(image.array[~noData & isGood])
938  try:
939  yield
940  finally:
941  if doTempWideBackground:
942  exposure.maskedImage.image.array[:] = original
943 
944 
945 def addExposures(exposureList):
946  """Add a set of exposures together.
947 
948  Parameters
949  ----------
950  exposureList : `list` of `lsst.afw.image.Exposure`
951  Sequence of exposures to add.
952 
953  Returns
954  -------
955  addedExposure : `lsst.afw.image.Exposure`
956  An exposure of the same size as each exposure in ``exposureList``,
957  with the metadata from ``exposureList[0]`` and a masked image equal
958  to the sum of all the exposure's masked images.
959  """
960  exposure0 = exposureList[0]
961  image0 = exposure0.getMaskedImage()
962 
963  addedImage = image0.Factory(image0, True)
964  addedImage.setXY0(image0.getXY0())
965 
966  for exposure in exposureList[1:]:
967  image = exposure.getMaskedImage()
968  addedImage += image
969 
970  addedExposure = exposure0.Factory(addedImage, exposure0.getWcs())
971  return addedExposure
def updatePeaks(self, fpSet, image, threshold)
Definition: detection.py:832
def applyThreshold(self, middle, bbox, factor=1.0)
Definition: detection.py:570
def addExposures(exposureList)
Definition: detection.py:945
Parameters to control convolution.
Definition: ConvolveImage.h:50
def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None)
Definition: detection.py:729
def convolveImage(self, maskedImage, psf, doSmooth=True)
Definition: detection.py:509
A kernel described by a pair of functions: func(x, y) = colFunc(x) * rowFunc(y)
Definition: Kernel.h:907
def applyTempLocalBackground(self, exposure, middle, results)
Definition: detection.py:414
daf::base::PropertySet * set
Definition: fits.cc:902
def display(self, exposure, results, convolvedImage=None)
Definition: detection.py:360
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())
The makeStatistics() overload to handle lsst::afw::math::MaskedVector<>
Definition: Statistics.h:520
def makeThreshold(self, image, thresholdParity, factor=1.0)
Definition: detection.py:797
Pass parameters to a Statistics object.
Definition: Statistics.h:93
Configuration parameters for the SourceDetectionTask.
Definition: detection.py:42
def getPsf(self, exposure, sigma=None)
Definition: detection.py:482
Threshold createThreshold(const double value, const std::string type="value", const bool polarity=true)
Factory method for creating Threshold objects.
Definition: Threshold.cc:109
def finalizeFootprints(self, mask, results, sigma, factor=1.0)
Definition: detection.py:619
def __init__(self, schema=None, kwds)
Create the detection task.
Definition: detection.py:265
A set of Footprints, associated with a MaskedImage.
Definition: FootprintSet.h:53
def reEstimateBackground(self, maskedImage, backgrounds)
Definition: detection.py:681
def run(self, table, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None)
Definition: detection.py:297
def setEdgeBits(maskedImage, goodBBox, edgeBitmask)
Definition: detection.py:875
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, bool doNormalize, bool doCopyEdge=false)
Old, deprecated version of convolve.
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects...
An integer coordinate rectangle.
Definition: Box.h:55
A circularly symmetric Gaussian Psf class with no spatial variation, intended mostly for testing purp...
Definition: GaussianPsf.h:42