LSSTApplications  18.1.0
LSSTDataManagementBasePackage
detection.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 #
4 # Copyright 2008-2017 AURA/LSST.
5 #
6 # This product includes software developed by the
7 # LSST Project (http://www.lsst.org/).
8 #
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the LSST License Statement and
20 # the GNU General Public License along with this program. If not,
21 # see <https://www.lsstcorp.org/LegalNotices/>.
22 #
23 
24 __all__ = ("SourceDetectionConfig", "SourceDetectionTask", "addExposures")
25 
26 from contextlib import contextmanager
27 
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",
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.
299 
300  @param table lsst.afw.table.SourceTable object that will be used to create the SourceCatalog.
301  @param exposure Exposure to process; DETECTED mask plane will be set in-place.
302  @param doSmooth if True, smooth the image before detection using a Gaussian of width sigma
303  (default: True)
304  @param sigma sigma of PSF (pixels); used for smoothing and to grow detections;
305  if None then measure the sigma of the PSF of the exposure (default: None)
306  @param clearMask Clear DETECTED{,_NEGATIVE} planes before running detection (default: True)
307  @param expId Exposure identifier (integer); unused by this implementation, but used for
308  RNG seed by subclasses.
309 
310  @return a lsst.pipe.base.Struct with:
311  - sources -- an lsst.afw.table.SourceCatalog object
312  - fpSets --- lsst.pipe.base.Struct returned by @link detectFootprints @endlink
313 
314  @throws ValueError if flags.negative is needed, but isn't in table's schema
315  @throws lsst.pipe.base.TaskError if sigma=None, doSmooth=True and the exposure has no PSF
316 
317  @note
318  If you want to avoid dealing with Sources and Tables, you can use detectFootprints()
319  to just get the afw::detection::FootprintSet%s.
320  """
321  if self.negativeFlagKey is not None and self.negativeFlagKey not in table.getSchema():
322  raise ValueError("Table has incorrect Schema")
323  results = self.detectFootprints(exposure=exposure, doSmooth=doSmooth, sigma=sigma,
324  clearMask=clearMask, expId=expId)
325  sources = afwTable.SourceCatalog(table)
326  sources.reserve(results.numPos + results.numNeg)
327  if results.negative:
328  results.negative.makeSources(sources)
329  if self.negativeFlagKey:
330  for record in sources:
331  record.set(self.negativeFlagKey, True)
332  if results.positive:
333  results.positive.makeSources(sources)
334  results.fpSets = results.copy() # Backward compatibility
335  results.sources = sources
336  return results
337 
338 
339  makeSourceCatalog = run
340 
341  def display(self, exposure, results, convolvedImage=None):
342  """Display detections if so configured
343 
344  Displays the ``exposure`` in frame 0, overlays the detection peaks.
345 
346  Requires that ``lsstDebug`` has been set up correctly, so that
347  ``lsstDebug.Info("lsst.meas.algorithms.detection")`` evaluates `True`.
348 
349  If the ``convolvedImage`` is non-`None` and
350  ``lsstDebug.Info("lsst.meas.algorithms.detection") > 1``, the
351  ``convolvedImage`` will be displayed in frame 1.
352 
353  Parameters
354  ----------
355  exposure : `lsst.afw.image.Exposure`
356  Exposure to display, on which will be plotted the detections.
357  results : `lsst.pipe.base.Struct`
358  Results of the 'detectFootprints' method, containing positive and
359  negative footprints (which contain the peak positions that we will
360  plot). This is a `Struct` with ``positive`` and ``negative``
361  elements that are of type `lsst.afw.detection.FootprintSet`.
362  convolvedImage : `lsst.afw.image.Image`, optional
363  Convolved image used for thresholding.
364  """
365  try:
366  import lsstDebug
367  display = lsstDebug.Info(__name__).display
368  except ImportError:
369  try:
370  display
371  except NameError:
372  display = False
373  if not display:
374  return
375 
376  afwDisplay.setDefaultMaskTransparency(75)
377 
378  disp0 = afwDisplay.Display(frame=0)
379  disp0.mtv(exposure, title="detection")
380 
381  def plotPeaks(fps, ctype):
382  if fps is None:
383  return
384  with disp0.Buffering():
385  for fp in fps.getFootprints():
386  for pp in fp.getPeaks():
387  disp0.dot("+", pp.getFx(), pp.getFy(), ctype=ctype)
388  plotPeaks(results.positive, "yellow")
389  plotPeaks(results.negative, "red")
390 
391  if convolvedImage and display > 1:
392  disp1 = afwDisplay.Display(frame=1)
393  disp1.mtv(convolvedImage, title="PSF smoothed")
394 
395  def applyTempLocalBackground(self, exposure, middle, results):
396  """Apply a temporary local background subtraction
397 
398  This temporary local background serves to suppress noise fluctuations
399  in the wings of bright objects.
400 
401  Peaks in the footprints will be updated.
402 
403  Parameters
404  ----------
405  exposure : `lsst.afw.image.Exposure`
406  Exposure for which to fit local background.
407  middle : `lsst.afw.image.MaskedImage`
408  Convolved image on which detection will be performed
409  (typically smaller than ``exposure`` because the
410  half-kernel has been removed around the edges).
411  results : `lsst.pipe.base.Struct`
412  Results of the 'detectFootprints' method, containing positive and
413  negative footprints (which contain the peak positions that we will
414  plot). This is a `Struct` with ``positive`` and ``negative``
415  elements that are of type `lsst.afw.detection.FootprintSet`.
416  """
417  # Subtract the local background from the smoothed image. Since we
418  # never use the smoothed again we don't need to worry about adding
419  # it back in.
420  bg = self.tempLocalBackground.fitBackground(exposure.getMaskedImage())
421  bgImage = bg.getImageF()
422  middle -= bgImage.Factory(bgImage, middle.getBBox())
423  thresholdPos = self.makeThreshold(middle, "positive")
424  thresholdNeg = self.makeThreshold(middle, "negative")
425  if self.config.thresholdPolarity != "negative":
426  self.updatePeaks(results.positive, middle, thresholdPos)
427  if self.config.thresholdPolarity != "positive":
428  self.updatePeaks(results.negative, middle, thresholdNeg)
429 
430  def clearMask(self, mask):
431  """Clear the DETECTED and DETECTED_NEGATIVE mask planes
432 
433  Removes any previous detection mask in preparation for a new
434  detection pass.
435 
436  Parameters
437  ----------
438  mask : `lsst.afw.image.Mask`
439  Mask to be cleared.
440  """
441  mask &= ~(mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE"))
442 
443  def calculateKernelSize(self, sigma):
444  """Calculate size of smoothing kernel
445 
446  Uses the ``nSigmaForKernel`` configuration parameter. Note
447  that that is the full width of the kernel bounding box
448  (so a value of 7 means 3.5 sigma on either side of center).
449  The value will be rounded up to the nearest odd integer.
450 
451  Parameters
452  ----------
453  sigma : `float`
454  Gaussian sigma of smoothing kernel.
455 
456  Returns
457  -------
458  size : `int`
459  Size of the smoothing kernel.
460  """
461  return (int(sigma * self.config.nSigmaForKernel + 0.5)//2)*2 + 1 # make sure it is odd
462 
463  def getPsf(self, exposure, sigma=None):
464  """Retrieve the PSF for an exposure
465 
466  If ``sigma`` is provided, we make a ``GaussianPsf`` with that,
467  otherwise use the one from the ``exposure``.
468 
469  Parameters
470  ----------
471  exposure : `lsst.afw.image.Exposure`
472  Exposure from which to retrieve the PSF.
473  sigma : `float`, optional
474  Gaussian sigma to use if provided.
475 
476  Returns
477  -------
478  psf : `lsst.afw.detection.Psf`
479  PSF to use for detection.
480  """
481  if sigma is None:
482  psf = exposure.getPsf()
483  if psf is None:
484  raise RuntimeError("Unable to determine PSF to use for detection: no sigma provided")
485  sigma = psf.computeShape().getDeterminantRadius()
486  size = self.calculateKernelSize(sigma)
487  psf = afwDet.GaussianPsf(size, size, sigma)
488  return psf
489 
490  def convolveImage(self, maskedImage, psf, doSmooth=True):
491  """Convolve the image with the PSF
492 
493  We convolve the image with a Gaussian approximation to the PSF,
494  because this is separable and therefore fast. It's technically a
495  correlation rather than a convolution, but since we use a symmetric
496  Gaussian there's no difference.
497 
498  The convolution can be disabled with ``doSmooth=False``. If we do
499  convolve, we mask the edges as ``EDGE`` and return the convolved image
500  with the edges removed. This is because we can't convolve the edges
501  because the kernel would extend off the image.
502 
503  Parameters
504  ----------
505  maskedImage : `lsst.afw.image.MaskedImage`
506  Image to convolve.
507  psf : `lsst.afw.detection.Psf`
508  PSF to convolve with (actually with a Gaussian approximation
509  to it).
510  doSmooth : `bool`
511  Actually do the convolution?
512 
513  Return Struct contents
514  ----------------------
515  middle : `lsst.afw.image.MaskedImage`
516  Convolved image, without the edges.
517  sigma : `float`
518  Gaussian sigma used for the convolution.
519  """
520  self.metadata.set("doSmooth", doSmooth)
521  sigma = psf.computeShape().getDeterminantRadius()
522  self.metadata.set("sigma", sigma)
523 
524  if not doSmooth:
525  middle = maskedImage.Factory(maskedImage)
526  return pipeBase.Struct(middle=middle, sigma=sigma)
527 
528  # Smooth using a Gaussian (which is separable, hence fast) of width sigma
529  # Make a SingleGaussian (separable) kernel with the 'sigma'
530  kWidth = self.calculateKernelSize(sigma)
531  self.metadata.set("smoothingKernelWidth", kWidth)
532  gaussFunc = afwMath.GaussianFunction1D(sigma)
533  gaussKernel = afwMath.SeparableKernel(kWidth, kWidth, gaussFunc, gaussFunc)
534 
535  convolvedImage = maskedImage.Factory(maskedImage.getBBox())
536 
537  afwMath.convolve(convolvedImage, maskedImage, gaussKernel, afwMath.ConvolutionControl())
538  #
539  # Only search psf-smoothed part of frame
540  #
541  goodBBox = gaussKernel.shrinkBBox(convolvedImage.getBBox())
542  middle = convolvedImage.Factory(convolvedImage, goodBBox, afwImage.PARENT, False)
543  #
544  # Mark the parts of the image outside goodBBox as EDGE
545  #
546  self.setEdgeBits(maskedImage, goodBBox, maskedImage.getMask().getPlaneBitMask("EDGE"))
547 
548  return pipeBase.Struct(middle=middle, sigma=sigma)
549 
550  def applyThreshold(self, middle, bbox, factor=1.0):
551  """Apply thresholds to the convolved image
552 
553  Identifies ``Footprint``s, both positive and negative.
554 
555  The threshold can be modified by the provided multiplication
556  ``factor``.
557 
558  Parameters
559  ----------
560  middle : `lsst.afw.image.MaskedImage`
561  Convolved image to threshold.
562  bbox : `lsst.geom.Box2I`
563  Bounding box of unconvolved image.
564  factor : `float`
565  Multiplier for the configured threshold.
566 
567  Return Struct contents
568  ----------------------
569  positive : `lsst.afw.detection.FootprintSet` or `None`
570  Positive detection footprints, if configured.
571  negative : `lsst.afw.detection.FootprintSet` or `None`
572  Negative detection footprints, if configured.
573  factor : `float`
574  Multiplier for the configured threshold.
575  """
576  results = pipeBase.Struct(positive=None, negative=None, factor=factor)
577  # Detect the Footprints (peaks may be replaced if doTempLocalBackground)
578  if self.config.reEstimateBackground or self.config.thresholdPolarity != "negative":
579  threshold = self.makeThreshold(middle, "positive", factor=factor)
580  results.positive = afwDet.FootprintSet(
581  middle,
582  threshold,
583  "DETECTED",
584  self.config.minPixels
585  )
586  results.positive.setRegion(bbox)
587  if self.config.reEstimateBackground or self.config.thresholdPolarity != "positive":
588  threshold = self.makeThreshold(middle, "negative", factor=factor)
589  results.negative = afwDet.FootprintSet(
590  middle,
591  threshold,
592  "DETECTED_NEGATIVE",
593  self.config.minPixels
594  )
595  results.negative.setRegion(bbox)
596 
597  return results
598 
599  def finalizeFootprints(self, mask, results, sigma, factor=1.0):
600  """Finalize the detected footprints
601 
602  Grows the footprints, sets the ``DETECTED`` and ``DETECTED_NEGATIVE``
603  mask planes, and logs the results.
604 
605  ``numPos`` (number of positive footprints), ``numPosPeaks`` (number
606  of positive peaks), ``numNeg`` (number of negative footprints),
607  ``numNegPeaks`` (number of negative peaks) entries are added to the
608  detection results.
609 
610  Parameters
611  ----------
612  mask : `lsst.afw.image.Mask`
613  Mask image on which to flag detected pixels.
614  results : `lsst.pipe.base.Struct`
615  Struct of detection results, including ``positive`` and
616  ``negative`` entries; modified.
617  sigma : `float`
618  Gaussian sigma of PSF.
619  factor : `float`
620  Multiplier for the configured threshold.
621  """
622  for polarity, maskName in (("positive", "DETECTED"), ("negative", "DETECTED_NEGATIVE")):
623  fpSet = getattr(results, polarity)
624  if fpSet is None:
625  continue
626  if self.config.nSigmaToGrow > 0:
627  nGrow = int((self.config.nSigmaToGrow * sigma) + 0.5)
628  self.metadata.set("nGrow", nGrow)
629  if self.config.combinedGrow:
630  fpSet = afwDet.FootprintSet(fpSet, nGrow, self.config.isotropicGrow)
631  else:
632  stencil = (afwGeom.Stencil.CIRCLE if self.config.isotropicGrow else
633  afwGeom.Stencil.MANHATTAN)
634  for fp in fpSet:
635  fp.dilate(nGrow, stencil)
636  fpSet.setMask(mask, maskName)
637  if not self.config.returnOriginalFootprints:
638  setattr(results, polarity, fpSet)
639 
640  results.numPos = 0
641  results.numPosPeaks = 0
642  results.numNeg = 0
643  results.numNegPeaks = 0
644  positive = ""
645  negative = ""
646 
647  if results.positive is not None:
648  results.numPos = len(results.positive.getFootprints())
649  results.numPosPeaks = sum(len(fp.getPeaks()) for fp in results.positive.getFootprints())
650  positive = " %d positive peaks in %d footprints" % (results.numPosPeaks, results.numPos)
651  if results.negative is not None:
652  results.numNeg = len(results.negative.getFootprints())
653  results.numNegPeaks = sum(len(fp.getPeaks()) for fp in results.negative.getFootprints())
654  negative = " %d negative peaks in %d footprints" % (results.numNegPeaks, results.numNeg)
655 
656  self.log.info("Detected%s%s%s to %g %s" %
657  (positive, " and" if positive and negative else "", negative,
658  self.config.thresholdValue*self.config.includeThresholdMultiplier*factor,
659  "DN" if self.config.thresholdType == "value" else "sigma"))
660 
661  def reEstimateBackground(self, maskedImage, backgrounds):
662  """Estimate the background after detection
663 
664  Parameters
665  ----------
666  maskedImage : `lsst.afw.image.MaskedImage`
667  Image on which to estimate the background.
668  backgrounds : `lsst.afw.math.BackgroundList`
669  List of backgrounds; modified.
670 
671  Returns
672  -------
673  bg : `lsst.afw.math.backgroundMI`
674  Empirical background model.
675  """
676  bg = self.background.fitBackground(maskedImage)
677  if self.config.adjustBackground:
678  self.log.warn("Fiddling the background by %g", self.config.adjustBackground)
679  bg += self.config.adjustBackground
680  self.log.info("Resubtracting the background after object detection")
681  maskedImage -= bg.getImageF()
682  backgrounds.append(bg)
683  return bg
684 
685  def clearUnwantedResults(self, mask, results):
686  """Clear unwanted results from the Struct of results
687 
688  If we specifically want only positive or only negative detections,
689  drop the ones we don't want, and its associated mask plane.
690 
691  Parameters
692  ----------
693  mask : `lsst.afw.image.Mask`
694  Mask image.
695  results : `lsst.pipe.base.Struct`
696  Detection results, with ``positive`` and ``negative`` elements;
697  modified.
698  """
699  if self.config.thresholdPolarity == "positive":
700  if self.config.reEstimateBackground:
701  mask &= ~mask.getPlaneBitMask("DETECTED_NEGATIVE")
702  results.negative = None
703  elif self.config.thresholdPolarity == "negative":
704  if self.config.reEstimateBackground:
705  mask &= ~mask.getPlaneBitMask("DETECTED")
706  results.positive = None
707 
708  @pipeBase.timeMethod
709  def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None):
710  """Detect footprints.
711 
712  Parameters
713  ----------
714  exposure : `lsst.afw.image.Exposure`
715  Exposure to process; DETECTED{,_NEGATIVE} mask plane will be
716  set in-place.
717  doSmooth : `bool`, optional
718  If True, smooth the image before detection using a Gaussian
719  of width ``sigma``.
720  sigma : `float`, optional
721  Gaussian Sigma of PSF (pixels); used for smoothing and to grow
722  detections; if `None` then measure the sigma of the PSF of the
723  ``exposure``.
724  clearMask : `bool`, optional
725  Clear both DETECTED and DETECTED_NEGATIVE planes before running
726  detection.
727  expId : `dict`, optional
728  Exposure identifier; unused by this implementation, but used for
729  RNG seed by subclasses.
730 
731  Return Struct contents
732  ----------------------
733  positive : `lsst.afw.detection.FootprintSet`
734  Positive polarity footprints (may be `None`)
735  negative : `lsst.afw.detection.FootprintSet`
736  Negative polarity footprints (may be `None`)
737  numPos : `int`
738  Number of footprints in positive or 0 if detection polarity was
739  negative.
740  numNeg : `int`
741  Number of footprints in negative or 0 if detection polarity was
742  positive.
743  background : `lsst.afw.math.BackgroundList`
744  Re-estimated background. `None` if
745  ``reEstimateBackground==False``.
746  factor : `float`
747  Multiplication factor applied to the configured detection
748  threshold.
749  """
750  maskedImage = exposure.maskedImage
751 
752  if clearMask:
753  self.clearMask(maskedImage.getMask())
754 
755  psf = self.getPsf(exposure, sigma=sigma)
756  with self.tempWideBackgroundContext(exposure):
757  convolveResults = self.convolveImage(maskedImage, psf, doSmooth=doSmooth)
758  middle = convolveResults.middle
759  sigma = convolveResults.sigma
760 
761  results = self.applyThreshold(middle, maskedImage.getBBox())
762  results.background = afwMath.BackgroundList()
763  if self.config.doTempLocalBackground:
764  self.applyTempLocalBackground(exposure, middle, results)
765  self.finalizeFootprints(maskedImage.mask, results, sigma)
766 
767  if self.config.reEstimateBackground:
768  self.reEstimateBackground(maskedImage, results.background)
769 
770  self.clearUnwantedResults(maskedImage.getMask(), results)
771  self.display(exposure, results, middle)
772 
773  return results
774 
775  def makeThreshold(self, image, thresholdParity, factor=1.0):
776  """Make an afw.detection.Threshold object corresponding to the task's
777  configuration and the statistics of the given image.
778 
779  Parameters
780  ----------
781  image : `afw.image.MaskedImage`
782  Image to measure noise statistics from if needed.
783  thresholdParity: `str`
784  One of "positive" or "negative", to set the kind of fluctuations
785  the Threshold will detect.
786  factor : `float`
787  Factor by which to multiply the configured detection threshold.
788  This is useful for tweaking the detection threshold slightly.
789 
790  Returns
791  -------
792  threshold : `lsst.afw.detection.Threshold`
793  Detection threshold.
794  """
795  parity = False if thresholdParity == "negative" else True
796  thresholdValue = self.config.thresholdValue
797  thresholdType = self.config.thresholdType
798  if self.config.thresholdType == 'stdev':
799  bad = image.getMask().getPlaneBitMask(self.config.statsMask)
800  sctrl = afwMath.StatisticsControl()
801  sctrl.setAndMask(bad)
802  stats = afwMath.makeStatistics(image, afwMath.STDEVCLIP, sctrl)
803  thresholdValue *= stats.getValue(afwMath.STDEVCLIP)
804  thresholdType = 'value'
805 
806  threshold = afwDet.createThreshold(thresholdValue*factor, thresholdType, parity)
807  threshold.setIncludeMultiplier(self.config.includeThresholdMultiplier)
808  return threshold
809 
810  def updatePeaks(self, fpSet, image, threshold):
811  """Update the Peaks in a FootprintSet by detecting new Footprints and
812  Peaks in an image and using the new Peaks instead of the old ones.
813 
814  Parameters
815  ----------
816  fpSet : `afw.detection.FootprintSet`
817  Set of Footprints whose Peaks should be updated.
818  image : `afw.image.MaskedImage`
819  Image to detect new Footprints and Peak in.
820  threshold : `afw.detection.Threshold`
821  Threshold object for detection.
822 
823  Input Footprints with fewer Peaks than self.config.nPeaksMaxSimple
824  are not modified, and if no new Peaks are detected in an input
825  Footprint, the brightest original Peak in that Footprint is kept.
826  """
827  for footprint in fpSet.getFootprints():
828  oldPeaks = footprint.getPeaks()
829  if len(oldPeaks) <= self.config.nPeaksMaxSimple:
830  continue
831  # We detect a new FootprintSet within each non-simple Footprint's
832  # bbox to avoid a big O(N^2) comparison between the two sets of
833  # Footprints.
834  sub = image.Factory(image, footprint.getBBox())
835  fpSetForPeaks = afwDet.FootprintSet(
836  sub,
837  threshold,
838  "", # don't set a mask plane
839  self.config.minPixels
840  )
841  newPeaks = afwDet.PeakCatalog(oldPeaks.getTable())
842  for fpForPeaks in fpSetForPeaks.getFootprints():
843  for peak in fpForPeaks.getPeaks():
844  if footprint.contains(peak.getI()):
845  newPeaks.append(peak)
846  if len(newPeaks) > 0:
847  del oldPeaks[:]
848  oldPeaks.extend(newPeaks)
849  else:
850  del oldPeaks[1:]
851 
852  @staticmethod
853  def setEdgeBits(maskedImage, goodBBox, edgeBitmask):
854  """Set the edgeBitmask bits for all of maskedImage outside goodBBox
855 
856  Parameters
857  ----------
858  maskedImage : `lsst.afw.image.MaskedImage`
859  Image on which to set edge bits in the mask.
860  goodBBox : `lsst.geom.Box2I`
861  Bounding box of good pixels, in ``LOCAL`` coordinates.
862  edgeBitmask : `lsst.afw.image.MaskPixel`
863  Bit mask to OR with the existing mask bits in the region
864  outside ``goodBBox``.
865  """
866  msk = maskedImage.getMask()
867 
868  mx0, my0 = maskedImage.getXY0()
869  for x0, y0, w, h in ([0, 0,
870  msk.getWidth(), goodBBox.getBeginY() - my0],
871  [0, goodBBox.getEndY() - my0, msk.getWidth(),
872  maskedImage.getHeight() - (goodBBox.getEndY() - my0)],
873  [0, 0,
874  goodBBox.getBeginX() - mx0, msk.getHeight()],
875  [goodBBox.getEndX() - mx0, 0,
876  maskedImage.getWidth() - (goodBBox.getEndX() - mx0), msk.getHeight()],
877  ):
878  edgeMask = msk.Factory(msk, lsst.geom.BoxI(lsst.geom.PointI(x0, y0),
879  lsst.geom.ExtentI(w, h)), afwImage.LOCAL)
880  edgeMask |= edgeBitmask
881 
882  @contextmanager
883  def tempWideBackgroundContext(self, exposure):
884  """Context manager for removing wide (large-scale) background
885 
886  Removing a wide (large-scale) background helps to suppress the
887  detection of large footprints that may overwhelm the deblender.
888  It does, however, set a limit on the maximum scale of objects.
889 
890  The background that we remove will be restored upon exit from
891  the context manager.
892 
893  Parameters
894  ----------
895  exposure : `lsst.afw.image.Exposure`
896  Exposure on which to remove large-scale background.
897 
898  Returns
899  -------
900  context : context manager
901  Context manager that will ensure the temporary wide background
902  is restored.
903  """
904  doTempWideBackground = self.config.doTempWideBackground
905  if doTempWideBackground:
906  self.log.info("Applying temporary wide background subtraction")
907  original = exposure.maskedImage.image.array[:].copy()
908  self.tempWideBackground.run(exposure).background
909  # Remove NO_DATA regions (e.g., edge of the field-of-view); these can cause detections after
910  # subtraction because of extrapolation of the background model into areas with no constraints.
911  image = exposure.maskedImage.image
912  mask = exposure.maskedImage.mask
913  noData = mask.array & mask.getPlaneBitMask("NO_DATA") > 0
914  isGood = mask.array & mask.getPlaneBitMask(self.config.statsMask) == 0
915  image.array[noData] = np.median(image.array[~noData & isGood])
916  try:
917  yield
918  finally:
919  if doTempWideBackground:
920  exposure.maskedImage.image.array[:] = original
921 
922 
923 def addExposures(exposureList):
924  """Add a set of exposures together.
925 
926  Parameters
927  ----------
928  exposureList : `list` of `lsst.afw.image.Exposure`
929  Sequence of exposures to add.
930 
931  Returns
932  -------
933  addedExposure : `lsst.afw.image.Exposure`
934  An exposure of the same size as each exposure in ``exposureList``,
935  with the metadata from ``exposureList[0]`` and a masked image equal
936  to the sum of all the exposure's masked images.
937  """
938  exposure0 = exposureList[0]
939  image0 = exposure0.getMaskedImage()
940 
941  addedImage = image0.Factory(image0, True)
942  addedImage.setXY0(image0.getXY0())
943 
944  for exposure in exposureList[1:]:
945  image = exposure.getMaskedImage()
946  addedImage += image
947 
948  addedExposure = exposure0.Factory(addedImage, exposure0.getWcs())
949  return addedExposure
def updatePeaks(self, fpSet, image, threshold)
Definition: detection.py:810
def applyThreshold(self, middle, bbox, factor=1.0)
Definition: detection.py:550
def addExposures(exposureList)
Definition: detection.py:923
Parameters to control convolution.
Definition: ConvolveImage.h:50
def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None)
Definition: detection.py:709
def convolveImage(self, maskedImage, psf, doSmooth=True)
Definition: detection.py:490
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:395
daf::base::PropertySet * set
Definition: fits.cc:884
def display(self, exposure, results, convolvedImage=None)
Definition: detection.py:341
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:775
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:463
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:599
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:661
def run(self, table, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None)
Run source detection and create a SourceCatalog.
Definition: detection.py:297
def setEdgeBits(maskedImage, goodBBox, edgeBitmask)
Definition: detection.py:853
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:54
A circularly symmetric Gaussian Psf class with no spatial variation, intended mostly for testing purp...
Definition: GaussianPsf.h:42