LSSTApplications  16.0-10-g0ee56ad+5,16.0-11-ga33d1f2+5,16.0-12-g3ef5c14+3,16.0-12-g71e5ef5+18,16.0-12-gbdf3636+3,16.0-13-g118c103+3,16.0-13-g8f68b0a+3,16.0-15-gbf5c1cb+4,16.0-16-gfd17674+3,16.0-17-g7c01f5c+3,16.0-18-g0a50484+1,16.0-20-ga20f992+8,16.0-21-g0e05fd4+6,16.0-21-g15e2d33+4,16.0-22-g62d8060+4,16.0-22-g847a80f+4,16.0-25-gf00d9b8+1,16.0-28-g3990c221+4,16.0-3-gf928089+3,16.0-32-g88a4f23+5,16.0-34-gd7987ad+3,16.0-37-gc7333cb+2,16.0-4-g10fc685+2,16.0-4-g18f3627+26,16.0-4-g5f3a788+26,16.0-5-gaf5c3d7+4,16.0-5-gcc1f4bb+1,16.0-6-g3b92700+4,16.0-6-g4412fcd+3,16.0-6-g7235603+4,16.0-69-g2562ce1b+2,16.0-8-g14ebd58+4,16.0-8-g2df868b+1,16.0-8-g4cec79c+6,16.0-8-gadf6c7a+1,16.0-8-gfc7ad86,16.0-82-g59ec2a54a+1,16.0-9-g5400cdc+2,16.0-9-ge6233d7+5,master-g2880f2d8cf+3,v17.0.rc1
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
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",
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 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  sources.reserve(results.numPos + results.numNeg)
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 = lsst.afw.display.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.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, backgrounds):
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  backgrounds : `lsst.afw.math.BackgroundList`
666  List of backgrounds; 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  backgrounds.append(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.BackgroundList`
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  results.background = afwMath.BackgroundList()
760  if self.config.doTempLocalBackground:
761  self.applyTempLocalBackground(exposure, middle, results)
762  self.finalizeFootprints(maskedImage.mask, results, sigma)
763 
764  if self.config.reEstimateBackground:
765  self.reEstimateBackground(maskedImage, results.background)
766 
767  self.clearUnwantedResults(maskedImage.getMask(), results)
768  self.display(exposure, results, middle)
769 
770  return results
771 
772  def makeThreshold(self, image, thresholdParity, factor=1.0):
773  """Make an afw.detection.Threshold object corresponding to the task's
774  configuration and the statistics of the given image.
775 
776  Parameters
777  ----------
778  image : `afw.image.MaskedImage`
779  Image to measure noise statistics from if needed.
780  thresholdParity: `str`
781  One of "positive" or "negative", to set the kind of fluctuations
782  the Threshold will detect.
783  factor : `float`
784  Factor by which to multiply the configured detection threshold.
785  This is useful for tweaking the detection threshold slightly.
786 
787  Returns
788  -------
789  threshold : `lsst.afw.detection.Threshold`
790  Detection threshold.
791  """
792  parity = False if thresholdParity == "negative" else True
793  thresholdValue = self.config.thresholdValue
794  thresholdType = self.config.thresholdType
795  if self.config.thresholdType == 'stdev':
796  bad = image.getMask().getPlaneBitMask(self.config.statsMask)
797  sctrl = afwMath.StatisticsControl()
798  sctrl.setAndMask(bad)
799  stats = afwMath.makeStatistics(image, afwMath.STDEVCLIP, sctrl)
800  thresholdValue *= stats.getValue(afwMath.STDEVCLIP)
801  thresholdType = 'value'
802 
803  threshold = afwDet.createThreshold(thresholdValue*factor, thresholdType, parity)
804  threshold.setIncludeMultiplier(self.config.includeThresholdMultiplier)
805  return threshold
806 
807  def updatePeaks(self, fpSet, image, threshold):
808  """Update the Peaks in a FootprintSet by detecting new Footprints and
809  Peaks in an image and using the new Peaks instead of the old ones.
810 
811  Parameters
812  ----------
813  fpSet : `afw.detection.FootprintSet`
814  Set of Footprints whose Peaks should be updated.
815  image : `afw.image.MaskedImage`
816  Image to detect new Footprints and Peak in.
817  threshold : `afw.detection.Threshold`
818  Threshold object for detection.
819 
820  Input Footprints with fewer Peaks than self.config.nPeaksMaxSimple
821  are not modified, and if no new Peaks are detected in an input
822  Footprint, the brightest original Peak in that Footprint is kept.
823  """
824  for footprint in fpSet.getFootprints():
825  oldPeaks = footprint.getPeaks()
826  if len(oldPeaks) <= self.config.nPeaksMaxSimple:
827  continue
828  # We detect a new FootprintSet within each non-simple Footprint's
829  # bbox to avoid a big O(N^2) comparison between the two sets of
830  # Footprints.
831  sub = image.Factory(image, footprint.getBBox())
832  fpSetForPeaks = afwDet.FootprintSet(
833  sub,
834  threshold,
835  "", # don't set a mask plane
836  self.config.minPixels
837  )
838  newPeaks = afwDet.PeakCatalog(oldPeaks.getTable())
839  for fpForPeaks in fpSetForPeaks.getFootprints():
840  for peak in fpForPeaks.getPeaks():
841  if footprint.contains(peak.getI()):
842  newPeaks.append(peak)
843  if len(newPeaks) > 0:
844  del oldPeaks[:]
845  oldPeaks.extend(newPeaks)
846  else:
847  del oldPeaks[1:]
848 
849  @staticmethod
850  def setEdgeBits(maskedImage, goodBBox, edgeBitmask):
851  """Set the edgeBitmask bits for all of maskedImage outside goodBBox
852 
853  Parameters
854  ----------
855  maskedImage : `lsst.afw.image.MaskedImage`
856  Image on which to set edge bits in the mask.
857  goodBBox : `lsst.geom.Box2I`
858  Bounding box of good pixels, in ``LOCAL`` coordinates.
859  edgeBitmask : `lsst.afw.image.MaskPixel`
860  Bit mask to OR with the existing mask bits in the region
861  outside ``goodBBox``.
862  """
863  msk = maskedImage.getMask()
864 
865  mx0, my0 = maskedImage.getXY0()
866  for x0, y0, w, h in ([0, 0,
867  msk.getWidth(), goodBBox.getBeginY() - my0],
868  [0, goodBBox.getEndY() - my0, msk.getWidth(),
869  maskedImage.getHeight() - (goodBBox.getEndY() - my0)],
870  [0, 0,
871  goodBBox.getBeginX() - mx0, msk.getHeight()],
872  [goodBBox.getEndX() - mx0, 0,
873  maskedImage.getWidth() - (goodBBox.getEndX() - mx0), msk.getHeight()],
874  ):
875  edgeMask = msk.Factory(msk, lsst.geom.BoxI(lsst.geom.PointI(x0, y0),
876  lsst.geom.ExtentI(w, h)), afwImage.LOCAL)
877  edgeMask |= edgeBitmask
878 
879  @contextmanager
880  def tempWideBackgroundContext(self, exposure):
881  """Context manager for removing wide (large-scale) background
882 
883  Removing a wide (large-scale) background helps to suppress the
884  detection of large footprints that may overwhelm the deblender.
885  It does, however, set a limit on the maximum scale of objects.
886 
887  The background that we remove will be restored upon exit from
888  the context manager.
889 
890  Parameters
891  ----------
892  exposure : `lsst.afw.image.Exposure`
893  Exposure on which to remove large-scale background.
894 
895  Returns
896  -------
897  context : context manager
898  Context manager that will ensure the temporary wide background
899  is restored.
900  """
901  doTempWideBackground = self.config.doTempWideBackground
902  if doTempWideBackground:
903  self.log.info("Applying temporary wide background subtraction")
904  original = exposure.maskedImage.image.array[:].copy()
905  self.tempWideBackground.run(exposure).background
906  # Remove NO_DATA regions (e.g., edge of the field-of-view); these can cause detections after
907  # subtraction because of extrapolation of the background model into areas with no constraints.
908  image = exposure.maskedImage.image
909  mask = exposure.maskedImage.mask
910  noData = mask.array & mask.getPlaneBitMask("NO_DATA") > 0
911  isGood = mask.array & mask.getPlaneBitMask(self.config.statsMask) == 0
912  image.array[noData] = np.median(image.array[~noData & isGood])
913  try:
914  yield
915  finally:
916  if doTempWideBackground:
917  exposure.maskedImage.image.array[:] = original
918 
919 
920 def addExposures(exposureList):
921  """Add a set of exposures together.
922 
923  Parameters
924  ----------
925  exposureList : `list` of `lsst.afw.image.Exposure`
926  Sequence of exposures to add.
927 
928  Returns
929  -------
930  addedExposure : `lsst.afw.image.Exposure`
931  An exposure of the same size as each exposure in ``exposureList``,
932  with the metadata from ``exposureList[0]`` and a masked image equal
933  to the sum of all the exposure's masked images.
934  """
935  exposure0 = exposureList[0]
936  image0 = exposure0.getMaskedImage()
937 
938  addedImage = image0.Factory(image0, True)
939  addedImage.setXY0(image0.getXY0())
940 
941  for exposure in exposureList[1:]:
942  image = exposure.getMaskedImage()
943  addedImage += image
944 
945  addedExposure = exposure0.Factory(addedImage, exposure0.getWcs())
946  return addedExposure
def updatePeaks(self, fpSet, image, threshold)
Definition: detection.py:807
def applyThreshold(self, middle, bbox, factor=1.0)
Definition: detection.py:547
def addExposures(exposureList)
Definition: detection.py:920
Parameters to control convolution.
Definition: ConvolveImage.h:50
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
A kernel described by a pair of functions: func(x, y) = colFunc(x) * rowFunc(y)
Definition: Kernel.h:898
def applyTempLocalBackground(self, exposure, middle, results)
Definition: detection.py:392
daf::base::PropertySet * set
Definition: fits.cc:832
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())
The makeStatistics() overload to handle lsst::afw::math::MaskedVector<>
Definition: Statistics.h:520
def makeThreshold(self, image, thresholdParity, factor=1.0)
Definition: detection.py:772
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:460
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:596
def __init__(self, schema=None, kwds)
Create the detection task.
Definition: detection.py:264
A set of Footprints, associated with a MaskedImage.
Definition: FootprintSet.h:53
def reEstimateBackground(self, maskedImage, backgrounds)
Definition: detection.py:658
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:850
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, bool doNormalize, bool doCopyEdge=false)
Old, deprecated version of convolve.
An integer coordinate rectangle.
Definition: Box.h:54
A circularly symmetric Gaussian Psf class with no spatial variation, intended mostly for testing purp...
Definition: GaussianPsf.h:42