LSST Applications  21.0.0+04719a4bac,21.0.0-1-ga51b5d4+f5e6047307,21.0.0-11-g2b59f77+a9c1acf22d,21.0.0-11-ga42c5b2+86977b0b17,21.0.0-12-gf4ce030+76814010d2,21.0.0-13-g1721dae+760e7a6536,21.0.0-13-g3a573fe+768d78a30a,21.0.0-15-g5a7caf0+f21cbc5713,21.0.0-16-g0fb55c1+b60e2d390c,21.0.0-19-g4cded4ca+71a93a33c0,21.0.0-2-g103fe59+bb20972958,21.0.0-2-g45278ab+04719a4bac,21.0.0-2-g5242d73+3ad5d60fb1,21.0.0-2-g7f82c8f+8babb168e8,21.0.0-2-g8f08a60+06509c8b61,21.0.0-2-g8faa9b5+616205b9df,21.0.0-2-ga326454+8babb168e8,21.0.0-2-gde069b7+5e4aea9c2f,21.0.0-2-gecfae73+1d3a86e577,21.0.0-2-gfc62afb+3ad5d60fb1,21.0.0-25-g1d57be3cd+e73869a214,21.0.0-3-g357aad2+ed88757d29,21.0.0-3-g4a4ce7f+3ad5d60fb1,21.0.0-3-g4be5c26+3ad5d60fb1,21.0.0-3-g65f322c+e0b24896a3,21.0.0-3-g7d9da8d+616205b9df,21.0.0-3-ge02ed75+a9c1acf22d,21.0.0-4-g591bb35+a9c1acf22d,21.0.0-4-g65b4814+b60e2d390c,21.0.0-4-gccdca77+0de219a2bc,21.0.0-4-ge8a399c+6c55c39e83,21.0.0-5-gd00fb1e+05fce91b99,21.0.0-6-gc675373+3ad5d60fb1,21.0.0-64-g1122c245+4fb2b8f86e,21.0.0-7-g04766d7+cd19d05db2,21.0.0-7-gdf92d54+04719a4bac,21.0.0-8-g5674e7b+d1bd76f71f,master-gac4afde19b+a9c1acf22d,w.2021.13
LSST Data Management Base Package
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.tempLocalBackgroundtempLocalBackground.binSize = 64
148  self.tempLocalBackgroundtempLocalBackground.algorithm = "AKIMA_SPLINE"
149  self.tempLocalBackgroundtempLocalBackground.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.tempWideBackgroundtempWideBackground.binSize = 512
153  self.tempWideBackgroundtempWideBackground.algorithm = "AKIMA_SPLINE"
154  self.tempWideBackgroundtempWideBackground.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.tempWideBackgroundtempWideBackground.ignoredPixelMask:
158  self.tempWideBackgroundtempWideBackground.ignoredPixelMask.remove(maskPlane)
159 
160 
161 class SourceDetectionTask(pipeBase.Task):
162  """Create the detection task. Most arguments are simply passed onto pipe.base.Task.
163 
164  Parameters
165  ----------
166  schema : `lsst.afw.table.Schema`
167  Schema object used to create the output `lsst.afw.table.SourceCatalog`
168  **kwds
169  Keyword arguments passed to `lsst.pipe.base.task.Task.__init__`
170 
171  If schema is not None and configured for 'both' detections,
172  a 'flags.negative' field will be added to label detections made with a
173  negative threshold.
174 
175  Notes
176  -----
177  This task can add fields to the schema, so any code calling this task must ensure that
178  these columns are indeed present in the input match list.
179  """
180 
181  ConfigClass = SourceDetectionConfig
182  _DefaultName = "sourceDetection"
183 
184  def __init__(self, schema=None, **kwds):
185  pipeBase.Task.__init__(self, **kwds)
186  if schema is not None and self.config.thresholdPolarity == "both":
187  self.negativeFlagKeynegativeFlagKey = schema.addField(
188  "flags_negative", type="Flag",
189  doc="set if source was detected as significantly negative"
190  )
191  else:
192  if self.config.thresholdPolarity == "both":
193  self.log.warn("Detection polarity set to 'both', but no flag will be "
194  "set to distinguish between positive and negative detections")
195  self.negativeFlagKeynegativeFlagKey = None
196  if self.config.reEstimateBackground:
197  self.makeSubtask("background")
198  if self.config.doTempLocalBackground:
199  self.makeSubtask("tempLocalBackground")
200  if self.config.doTempWideBackground:
201  self.makeSubtask("tempWideBackground")
202 
203  @pipeBase.timeMethod
204  def run(self, table, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None):
205  """Run source detection and create a SourceCatalog of detections.
206 
207  Parameters
208  ----------
209  table : `lsst.afw.table.SourceTable`
210  Table object that will be used to create the SourceCatalog.
211  exposure : `lsst.afw.image.Exposure`
212  Exposure to process; DETECTED mask plane will be set in-place.
213  doSmooth : `bool`
214  If True, smooth the image before detection using a Gaussian of width
215  ``sigma``, or the measured PSF width. Set to False when running on
216  e.g. a pre-convolved image, or a mask plane.
217  sigma : `float`
218  Sigma of PSF (pixels); used for smoothing and to grow detections;
219  if None then measure the sigma of the PSF of the exposure
220  clearMask : `bool`
221  Clear DETECTED{,_NEGATIVE} planes before running detection.
222  expId : `int`
223  Exposure identifier; unused by this implementation, but used for
224  RNG seed by subclasses.
225 
226  Returns
227  -------
228  result : `lsst.pipe.base.Struct`
229  ``sources``
230  The detected sources (`lsst.afw.table.SourceCatalog`)
231  ``fpSets``
232  The result resturned by `detectFootprints`
233  (`lsst.pipe.base.Struct`).
234 
235  Raises
236  ------
237  ValueError
238  If flags.negative is needed, but isn't in table's schema.
239  lsst.pipe.base.TaskError
240  If sigma=None, doSmooth=True and the exposure has no PSF.
241 
242  Notes
243  -----
244  If you want to avoid dealing with Sources and Tables, you can use
245  detectFootprints() to just get the `lsst.afw.detection.FootprintSet`s.
246  """
247  if self.negativeFlagKeynegativeFlagKey is not None and self.negativeFlagKeynegativeFlagKey not in table.getSchema():
248  raise ValueError("Table has incorrect Schema")
249  results = self.detectFootprintsdetectFootprints(exposure=exposure, doSmooth=doSmooth, sigma=sigma,
250  clearMask=clearMask, expId=expId)
251  sources = afwTable.SourceCatalog(table)
252  sources.reserve(results.numPos + results.numNeg)
253  if results.negative:
254  results.negative.makeSources(sources)
255  if self.negativeFlagKeynegativeFlagKey:
256  for record in sources:
257  record.set(self.negativeFlagKeynegativeFlagKey, True)
258  if results.positive:
259  results.positive.makeSources(sources)
260  results.fpSets = results.copy() # Backward compatibility
261  results.sources = sources
262  return results
263 
264  def display(self, exposure, results, convolvedImage=None):
265  """Display detections if so configured
266 
267  Displays the ``exposure`` in frame 0, overlays the detection peaks.
268 
269  Requires that ``lsstDebug`` has been set up correctly, so that
270  ``lsstDebug.Info("lsst.meas.algorithms.detection")`` evaluates `True`.
271 
272  If the ``convolvedImage`` is non-`None` and
273  ``lsstDebug.Info("lsst.meas.algorithms.detection") > 1``, the
274  ``convolvedImage`` will be displayed in frame 1.
275 
276  Parameters
277  ----------
278  exposure : `lsst.afw.image.Exposure`
279  Exposure to display, on which will be plotted the detections.
280  results : `lsst.pipe.base.Struct`
281  Results of the 'detectFootprints' method, containing positive and
282  negative footprints (which contain the peak positions that we will
283  plot). This is a `Struct` with ``positive`` and ``negative``
284  elements that are of type `lsst.afw.detection.FootprintSet`.
285  convolvedImage : `lsst.afw.image.Image`, optional
286  Convolved image used for thresholding.
287  """
288  try:
289  import lsstDebug
290  display = lsstDebug.Info(__name__).display
291  except ImportError:
292  try:
293  display
294  except NameError:
295  display = False
296  if not display:
297  return
298 
299  afwDisplay.setDefaultMaskTransparency(75)
300 
301  disp0 = afwDisplay.Display(frame=0)
302  disp0.mtv(exposure, title="detection")
303 
304  def plotPeaks(fps, ctype):
305  if fps is None:
306  return
307  with disp0.Buffering():
308  for fp in fps.getFootprints():
309  for pp in fp.getPeaks():
310  disp0.dot("+", pp.getFx(), pp.getFy(), ctype=ctype)
311  plotPeaks(results.positive, "yellow")
312  plotPeaks(results.negative, "red")
313 
314  if convolvedImage and display > 1:
315  disp1 = afwDisplay.Display(frame=1)
316  disp1.mtv(convolvedImage, title="PSF smoothed")
317 
318  def applyTempLocalBackground(self, exposure, middle, results):
319  """Apply a temporary local background subtraction
320 
321  This temporary local background serves to suppress noise fluctuations
322  in the wings of bright objects.
323 
324  Peaks in the footprints will be updated.
325 
326  Parameters
327  ----------
328  exposure : `lsst.afw.image.Exposure`
329  Exposure for which to fit local background.
330  middle : `lsst.afw.image.MaskedImage`
331  Convolved image on which detection will be performed
332  (typically smaller than ``exposure`` because the
333  half-kernel has been removed around the edges).
334  results : `lsst.pipe.base.Struct`
335  Results of the 'detectFootprints' method, containing positive and
336  negative footprints (which contain the peak positions that we will
337  plot). This is a `Struct` with ``positive`` and ``negative``
338  elements that are of type `lsst.afw.detection.FootprintSet`.
339  """
340  # Subtract the local background from the smoothed image. Since we
341  # never use the smoothed again we don't need to worry about adding
342  # it back in.
343  bg = self.tempLocalBackground.fitBackground(exposure.getMaskedImage())
344  bgImage = bg.getImageF(self.tempLocalBackground.config.algorithm,
345  self.tempLocalBackground.config.undersampleStyle)
346  middle -= bgImage.Factory(bgImage, middle.getBBox())
347  thresholdPos = self.makeThresholdmakeThreshold(middle, "positive")
348  thresholdNeg = self.makeThresholdmakeThreshold(middle, "negative")
349  if self.config.thresholdPolarity != "negative":
350  self.updatePeaksupdatePeaks(results.positive, middle, thresholdPos)
351  if self.config.thresholdPolarity != "positive":
352  self.updatePeaksupdatePeaks(results.negative, middle, thresholdNeg)
353 
354  def clearMask(self, mask):
355  """Clear the DETECTED and DETECTED_NEGATIVE mask planes
356 
357  Removes any previous detection mask in preparation for a new
358  detection pass.
359 
360  Parameters
361  ----------
362  mask : `lsst.afw.image.Mask`
363  Mask to be cleared.
364  """
365  mask &= ~(mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE"))
366 
367  def calculateKernelSize(self, sigma):
368  """Calculate size of smoothing kernel
369 
370  Uses the ``nSigmaForKernel`` configuration parameter. Note
371  that that is the full width of the kernel bounding box
372  (so a value of 7 means 3.5 sigma on either side of center).
373  The value will be rounded up to the nearest odd integer.
374 
375  Parameters
376  ----------
377  sigma : `float`
378  Gaussian sigma of smoothing kernel.
379 
380  Returns
381  -------
382  size : `int`
383  Size of the smoothing kernel.
384  """
385  return (int(sigma * self.config.nSigmaForKernel + 0.5)//2)*2 + 1 # make sure it is odd
386 
387  def getPsf(self, exposure, sigma=None):
388  """Retrieve the PSF for an exposure
389 
390  If ``sigma`` is provided, we make a ``GaussianPsf`` with that,
391  otherwise use the one from the ``exposure``.
392 
393  Parameters
394  ----------
395  exposure : `lsst.afw.image.Exposure`
396  Exposure from which to retrieve the PSF.
397  sigma : `float`, optional
398  Gaussian sigma to use if provided.
399 
400  Returns
401  -------
402  psf : `lsst.afw.detection.Psf`
403  PSF to use for detection.
404  """
405  if sigma is None:
406  psf = exposure.getPsf()
407  if psf is None:
408  raise RuntimeError("Unable to determine PSF to use for detection: no sigma provided")
409  sigma = psf.computeShape().getDeterminantRadius()
410  size = self.calculateKernelSizecalculateKernelSize(sigma)
411  psf = afwDet.GaussianPsf(size, size, sigma)
412  return psf
413 
414  def convolveImage(self, maskedImage, psf, doSmooth=True):
415  """Convolve the image with the PSF
416 
417  We convolve the image with a Gaussian approximation to the PSF,
418  because this is separable and therefore fast. It's technically a
419  correlation rather than a convolution, but since we use a symmetric
420  Gaussian there's no difference.
421 
422  The convolution can be disabled with ``doSmooth=False``. If we do
423  convolve, we mask the edges as ``EDGE`` and return the convolved image
424  with the edges removed. This is because we can't convolve the edges
425  because the kernel would extend off the image.
426 
427  Parameters
428  ----------
429  maskedImage : `lsst.afw.image.MaskedImage`
430  Image to convolve.
431  psf : `lsst.afw.detection.Psf`
432  PSF to convolve with (actually with a Gaussian approximation
433  to it).
434  doSmooth : `bool`
435  Actually do the convolution? Set to False when running on
436  e.g. a pre-convolved image, or a mask plane.
437 
438  Return Struct contents
439  ----------------------
440  middle : `lsst.afw.image.MaskedImage`
441  Convolved image, without the edges.
442  sigma : `float`
443  Gaussian sigma used for the convolution.
444  """
445  self.metadata.set("doSmooth", doSmooth)
446  sigma = psf.computeShape().getDeterminantRadius()
447  self.metadata.set("sigma", sigma)
448 
449  if not doSmooth:
450  middle = maskedImage.Factory(maskedImage, deep=True)
451  return pipeBase.Struct(middle=middle, sigma=sigma)
452 
453  # Smooth using a Gaussian (which is separable, hence fast) of width sigma
454  # Make a SingleGaussian (separable) kernel with the 'sigma'
455  kWidth = self.calculateKernelSizecalculateKernelSize(sigma)
456  self.metadata.set("smoothingKernelWidth", kWidth)
457  gaussFunc = afwMath.GaussianFunction1D(sigma)
458  gaussKernel = afwMath.SeparableKernel(kWidth, kWidth, gaussFunc, gaussFunc)
459 
460  convolvedImage = maskedImage.Factory(maskedImage.getBBox())
461 
462  afwMath.convolve(convolvedImage, maskedImage, gaussKernel, afwMath.ConvolutionControl())
463  #
464  # Only search psf-smoothed part of frame
465  #
466  goodBBox = gaussKernel.shrinkBBox(convolvedImage.getBBox())
467  middle = convolvedImage.Factory(convolvedImage, goodBBox, afwImage.PARENT, False)
468  #
469  # Mark the parts of the image outside goodBBox as EDGE
470  #
471  self.setEdgeBitssetEdgeBits(maskedImage, goodBBox, maskedImage.getMask().getPlaneBitMask("EDGE"))
472 
473  return pipeBase.Struct(middle=middle, sigma=sigma)
474 
475  def applyThreshold(self, middle, bbox, factor=1.0):
476  """Apply thresholds to the convolved image
477 
478  Identifies ``Footprint``s, both positive and negative.
479 
480  The threshold can be modified by the provided multiplication
481  ``factor``.
482 
483  Parameters
484  ----------
485  middle : `lsst.afw.image.MaskedImage`
486  Convolved image to threshold.
487  bbox : `lsst.geom.Box2I`
488  Bounding box of unconvolved image.
489  factor : `float`
490  Multiplier for the configured threshold.
491 
492  Return Struct contents
493  ----------------------
494  positive : `lsst.afw.detection.FootprintSet` or `None`
495  Positive detection footprints, if configured.
496  negative : `lsst.afw.detection.FootprintSet` or `None`
497  Negative detection footprints, if configured.
498  factor : `float`
499  Multiplier for the configured threshold.
500  """
501  results = pipeBase.Struct(positive=None, negative=None, factor=factor)
502  # Detect the Footprints (peaks may be replaced if doTempLocalBackground)
503  if self.config.reEstimateBackground or self.config.thresholdPolarity != "negative":
504  threshold = self.makeThresholdmakeThreshold(middle, "positive", factor=factor)
505  results.positive = afwDet.FootprintSet(
506  middle,
507  threshold,
508  "DETECTED",
509  self.config.minPixels
510  )
511  results.positive.setRegion(bbox)
512  if self.config.reEstimateBackground or self.config.thresholdPolarity != "positive":
513  threshold = self.makeThresholdmakeThreshold(middle, "negative", factor=factor)
514  results.negative = afwDet.FootprintSet(
515  middle,
516  threshold,
517  "DETECTED_NEGATIVE",
518  self.config.minPixels
519  )
520  results.negative.setRegion(bbox)
521 
522  return results
523 
524  def finalizeFootprints(self, mask, results, sigma, factor=1.0):
525  """Finalize the detected footprints
526 
527  Grows the footprints, sets the ``DETECTED`` and ``DETECTED_NEGATIVE``
528  mask planes, and logs the results.
529 
530  ``numPos`` (number of positive footprints), ``numPosPeaks`` (number
531  of positive peaks), ``numNeg`` (number of negative footprints),
532  ``numNegPeaks`` (number of negative peaks) entries are added to the
533  detection results.
534 
535  Parameters
536  ----------
537  mask : `lsst.afw.image.Mask`
538  Mask image on which to flag detected pixels.
539  results : `lsst.pipe.base.Struct`
540  Struct of detection results, including ``positive`` and
541  ``negative`` entries; modified.
542  sigma : `float`
543  Gaussian sigma of PSF.
544  factor : `float`
545  Multiplier for the configured threshold.
546  """
547  for polarity, maskName in (("positive", "DETECTED"), ("negative", "DETECTED_NEGATIVE")):
548  fpSet = getattr(results, polarity)
549  if fpSet is None:
550  continue
551  if self.config.nSigmaToGrow > 0:
552  nGrow = int((self.config.nSigmaToGrow * sigma) + 0.5)
553  self.metadata.set("nGrow", nGrow)
554  if self.config.combinedGrow:
555  fpSet = afwDet.FootprintSet(fpSet, nGrow, self.config.isotropicGrow)
556  else:
557  stencil = (afwGeom.Stencil.CIRCLE if self.config.isotropicGrow else
558  afwGeom.Stencil.MANHATTAN)
559  for fp in fpSet:
560  fp.dilate(nGrow, stencil)
561  fpSet.setMask(mask, maskName)
562  if not self.config.returnOriginalFootprints:
563  setattr(results, polarity, fpSet)
564 
565  results.numPos = 0
566  results.numPosPeaks = 0
567  results.numNeg = 0
568  results.numNegPeaks = 0
569  positive = ""
570  negative = ""
571 
572  if results.positive is not None:
573  results.numPos = len(results.positive.getFootprints())
574  results.numPosPeaks = sum(len(fp.getPeaks()) for fp in results.positive.getFootprints())
575  positive = " %d positive peaks in %d footprints" % (results.numPosPeaks, results.numPos)
576  if results.negative is not None:
577  results.numNeg = len(results.negative.getFootprints())
578  results.numNegPeaks = sum(len(fp.getPeaks()) for fp in results.negative.getFootprints())
579  negative = " %d negative peaks in %d footprints" % (results.numNegPeaks, results.numNeg)
580 
581  self.log.info("Detected%s%s%s to %g %s" %
582  (positive, " and" if positive and negative else "", negative,
583  self.config.thresholdValue*self.config.includeThresholdMultiplier*factor,
584  "DN" if self.config.thresholdType == "value" else "sigma"))
585 
586  def reEstimateBackground(self, maskedImage, backgrounds):
587  """Estimate the background after detection
588 
589  Parameters
590  ----------
591  maskedImage : `lsst.afw.image.MaskedImage`
592  Image on which to estimate the background.
593  backgrounds : `lsst.afw.math.BackgroundList`
594  List of backgrounds; modified.
595 
596  Returns
597  -------
598  bg : `lsst.afw.math.backgroundMI`
599  Empirical background model.
600  """
601  bg = self.background.fitBackground(maskedImage)
602  if self.config.adjustBackground:
603  self.log.warn("Fiddling the background by %g", self.config.adjustBackground)
604  bg += self.config.adjustBackground
605  self.log.info("Resubtracting the background after object detection")
606  maskedImage -= bg.getImageF(self.background.config.algorithm,
607  self.background.config.undersampleStyle)
608 
609  actrl = bg.getBackgroundControl().getApproximateControl()
610  backgrounds.append((bg, getattr(afwMath.Interpolate, self.background.config.algorithm),
611  bg.getAsUsedUndersampleStyle(), actrl.getStyle(), actrl.getOrderX(),
612  actrl.getOrderY(), actrl.getWeighting()))
613  return bg
614 
615  def clearUnwantedResults(self, mask, results):
616  """Clear unwanted results from the Struct of results
617 
618  If we specifically want only positive or only negative detections,
619  drop the ones we don't want, and its associated mask plane.
620 
621  Parameters
622  ----------
623  mask : `lsst.afw.image.Mask`
624  Mask image.
625  results : `lsst.pipe.base.Struct`
626  Detection results, with ``positive`` and ``negative`` elements;
627  modified.
628  """
629  if self.config.thresholdPolarity == "positive":
630  if self.config.reEstimateBackground:
631  mask &= ~mask.getPlaneBitMask("DETECTED_NEGATIVE")
632  results.negative = None
633  elif self.config.thresholdPolarity == "negative":
634  if self.config.reEstimateBackground:
635  mask &= ~mask.getPlaneBitMask("DETECTED")
636  results.positive = None
637 
638  @pipeBase.timeMethod
639  def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None):
640  """Detect footprints on an exposure.
641 
642  Parameters
643  ----------
644  exposure : `lsst.afw.image.Exposure`
645  Exposure to process; DETECTED{,_NEGATIVE} mask plane will be
646  set in-place.
647  doSmooth : `bool`, optional
648  If True, smooth the image before detection using a Gaussian
649  of width ``sigma``, or the measured PSF width of ``exposure``.
650  Set to False when running on e.g. a pre-convolved image, or a mask
651  plane.
652  sigma : `float`, optional
653  Gaussian Sigma of PSF (pixels); used for smoothing and to grow
654  detections; if `None` then measure the sigma of the PSF of the
655  ``exposure``.
656  clearMask : `bool`, optional
657  Clear both DETECTED and DETECTED_NEGATIVE planes before running
658  detection.
659  expId : `dict`, optional
660  Exposure identifier; unused by this implementation, but used for
661  RNG seed by subclasses.
662 
663  Return Struct contents
664  ----------------------
665  positive : `lsst.afw.detection.FootprintSet`
666  Positive polarity footprints (may be `None`)
667  negative : `lsst.afw.detection.FootprintSet`
668  Negative polarity footprints (may be `None`)
669  numPos : `int`
670  Number of footprints in positive or 0 if detection polarity was
671  negative.
672  numNeg : `int`
673  Number of footprints in negative or 0 if detection polarity was
674  positive.
675  background : `lsst.afw.math.BackgroundList`
676  Re-estimated background. `None` if
677  ``reEstimateBackground==False``.
678  factor : `float`
679  Multiplication factor applied to the configured detection
680  threshold.
681  """
682  maskedImage = exposure.maskedImage
683 
684  if clearMask:
685  self.clearMaskclearMask(maskedImage.getMask())
686 
687  psf = self.getPsfgetPsf(exposure, sigma=sigma)
688  with self.tempWideBackgroundContexttempWideBackgroundContext(exposure):
689  convolveResults = self.convolveImageconvolveImage(maskedImage, psf, doSmooth=doSmooth)
690  middle = convolveResults.middle
691  sigma = convolveResults.sigma
692 
693  results = self.applyThresholdapplyThreshold(middle, maskedImage.getBBox())
694  results.background = afwMath.BackgroundList()
695  if self.config.doTempLocalBackground:
696  self.applyTempLocalBackgroundapplyTempLocalBackground(exposure, middle, results)
697  self.finalizeFootprintsfinalizeFootprints(maskedImage.mask, results, sigma)
698 
699  if self.config.reEstimateBackground:
700  self.reEstimateBackgroundreEstimateBackground(maskedImage, results.background)
701 
702  self.clearUnwantedResultsclearUnwantedResults(maskedImage.getMask(), results)
703  self.displaydisplay(exposure, results, middle)
704 
705  return results
706 
707  def makeThreshold(self, image, thresholdParity, factor=1.0):
708  """Make an afw.detection.Threshold object corresponding to the task's
709  configuration and the statistics of the given image.
710 
711  Parameters
712  ----------
713  image : `afw.image.MaskedImage`
714  Image to measure noise statistics from if needed.
715  thresholdParity: `str`
716  One of "positive" or "negative", to set the kind of fluctuations
717  the Threshold will detect.
718  factor : `float`
719  Factor by which to multiply the configured detection threshold.
720  This is useful for tweaking the detection threshold slightly.
721 
722  Returns
723  -------
724  threshold : `lsst.afw.detection.Threshold`
725  Detection threshold.
726  """
727  parity = False if thresholdParity == "negative" else True
728  thresholdValue = self.config.thresholdValue
729  thresholdType = self.config.thresholdType
730  if self.config.thresholdType == 'stdev':
731  bad = image.getMask().getPlaneBitMask(self.config.statsMask)
732  sctrl = afwMath.StatisticsControl()
733  sctrl.setAndMask(bad)
734  stats = afwMath.makeStatistics(image, afwMath.STDEVCLIP, sctrl)
735  thresholdValue *= stats.getValue(afwMath.STDEVCLIP)
736  thresholdType = 'value'
737 
738  threshold = afwDet.createThreshold(thresholdValue*factor, thresholdType, parity)
739  threshold.setIncludeMultiplier(self.config.includeThresholdMultiplier)
740  return threshold
741 
742  def updatePeaks(self, fpSet, image, threshold):
743  """Update the Peaks in a FootprintSet by detecting new Footprints and
744  Peaks in an image and using the new Peaks instead of the old ones.
745 
746  Parameters
747  ----------
748  fpSet : `afw.detection.FootprintSet`
749  Set of Footprints whose Peaks should be updated.
750  image : `afw.image.MaskedImage`
751  Image to detect new Footprints and Peak in.
752  threshold : `afw.detection.Threshold`
753  Threshold object for detection.
754 
755  Input Footprints with fewer Peaks than self.config.nPeaksMaxSimple
756  are not modified, and if no new Peaks are detected in an input
757  Footprint, the brightest original Peak in that Footprint is kept.
758  """
759  for footprint in fpSet.getFootprints():
760  oldPeaks = footprint.getPeaks()
761  if len(oldPeaks) <= self.config.nPeaksMaxSimple:
762  continue
763  # We detect a new FootprintSet within each non-simple Footprint's
764  # bbox to avoid a big O(N^2) comparison between the two sets of
765  # Footprints.
766  sub = image.Factory(image, footprint.getBBox())
767  fpSetForPeaks = afwDet.FootprintSet(
768  sub,
769  threshold,
770  "", # don't set a mask plane
771  self.config.minPixels
772  )
773  newPeaks = afwDet.PeakCatalog(oldPeaks.getTable())
774  for fpForPeaks in fpSetForPeaks.getFootprints():
775  for peak in fpForPeaks.getPeaks():
776  if footprint.contains(peak.getI()):
777  newPeaks.append(peak)
778  if len(newPeaks) > 0:
779  del oldPeaks[:]
780  oldPeaks.extend(newPeaks)
781  else:
782  del oldPeaks[1:]
783 
784  @staticmethod
785  def setEdgeBits(maskedImage, goodBBox, edgeBitmask):
786  """Set the edgeBitmask bits for all of maskedImage outside goodBBox
787 
788  Parameters
789  ----------
790  maskedImage : `lsst.afw.image.MaskedImage`
791  Image on which to set edge bits in the mask.
792  goodBBox : `lsst.geom.Box2I`
793  Bounding box of good pixels, in ``LOCAL`` coordinates.
794  edgeBitmask : `lsst.afw.image.MaskPixel`
795  Bit mask to OR with the existing mask bits in the region
796  outside ``goodBBox``.
797  """
798  msk = maskedImage.getMask()
799 
800  mx0, my0 = maskedImage.getXY0()
801  for x0, y0, w, h in ([0, 0,
802  msk.getWidth(), goodBBox.getBeginY() - my0],
803  [0, goodBBox.getEndY() - my0, msk.getWidth(),
804  maskedImage.getHeight() - (goodBBox.getEndY() - my0)],
805  [0, 0,
806  goodBBox.getBeginX() - mx0, msk.getHeight()],
807  [goodBBox.getEndX() - mx0, 0,
808  maskedImage.getWidth() - (goodBBox.getEndX() - mx0), msk.getHeight()],
809  ):
810  edgeMask = msk.Factory(msk, lsst.geom.BoxI(lsst.geom.PointI(x0, y0),
811  lsst.geom.ExtentI(w, h)), afwImage.LOCAL)
812  edgeMask |= edgeBitmask
813 
814  @contextmanager
815  def tempWideBackgroundContext(self, exposure):
816  """Context manager for removing wide (large-scale) background
817 
818  Removing a wide (large-scale) background helps to suppress the
819  detection of large footprints that may overwhelm the deblender.
820  It does, however, set a limit on the maximum scale of objects.
821 
822  The background that we remove will be restored upon exit from
823  the context manager.
824 
825  Parameters
826  ----------
827  exposure : `lsst.afw.image.Exposure`
828  Exposure on which to remove large-scale background.
829 
830  Returns
831  -------
832  context : context manager
833  Context manager that will ensure the temporary wide background
834  is restored.
835  """
836  doTempWideBackground = self.config.doTempWideBackground
837  if doTempWideBackground:
838  self.log.info("Applying temporary wide background subtraction")
839  original = exposure.maskedImage.image.array[:].copy()
840  self.tempWideBackground.run(exposure).background
841  # Remove NO_DATA regions (e.g., edge of the field-of-view); these can cause detections after
842  # subtraction because of extrapolation of the background model into areas with no constraints.
843  image = exposure.maskedImage.image
844  mask = exposure.maskedImage.mask
845  noData = mask.array & mask.getPlaneBitMask("NO_DATA") > 0
846  isGood = mask.array & mask.getPlaneBitMask(self.config.statsMask) == 0
847  image.array[noData] = np.median(image.array[~noData & isGood])
848  try:
849  yield
850  finally:
851  if doTempWideBackground:
852  exposure.maskedImage.image.array[:] = original
853 
854 
855 def addExposures(exposureList):
856  """Add a set of exposures together.
857 
858  Parameters
859  ----------
860  exposureList : `list` of `lsst.afw.image.Exposure`
861  Sequence of exposures to add.
862 
863  Returns
864  -------
865  addedExposure : `lsst.afw.image.Exposure`
866  An exposure of the same size as each exposure in ``exposureList``,
867  with the metadata from ``exposureList[0]`` and a masked image equal
868  to the sum of all the exposure's masked images.
869  """
870  exposure0 = exposureList[0]
871  image0 = exposure0.getMaskedImage()
872 
873  addedImage = image0.Factory(image0, True)
874  addedImage.setXY0(image0.getXY0())
875 
876  for exposure in exposureList[1:]:
877  image = exposure.getMaskedImage()
878  addedImage += image
879 
880  addedExposure = exposure0.Factory(addedImage, exposure0.getWcs())
881  return addedExposure
A set of Footprints, associated with a MaskedImage.
Definition: FootprintSet.h:53
A circularly symmetric Gaussian Psf class with no spatial variation, intended mostly for testing purp...
Definition: GaussianPsf.h:42
Parameters to control convolution.
Definition: ConvolveImage.h:50
A kernel described by a pair of functions: func(x, y) = colFunc(x) * rowFunc(y)
Definition: Kernel.h:861
Pass parameters to a Statistics object.
Definition: Statistics.h:93
An integer coordinate rectangle.
Definition: Box.h:55
def getPsf(self, exposure, sigma=None)
Definition: detection.py:387
def makeThreshold(self, image, thresholdParity, factor=1.0)
Definition: detection.py:707
def run(self, table, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None)
Definition: detection.py:204
def convolveImage(self, maskedImage, psf, doSmooth=True)
Definition: detection.py:414
def applyTempLocalBackground(self, exposure, middle, results)
Definition: detection.py:318
def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None)
Definition: detection.py:639
def reEstimateBackground(self, maskedImage, backgrounds)
Definition: detection.py:586
def updatePeaks(self, fpSet, image, threshold)
Definition: detection.py:742
def finalizeFootprints(self, mask, results, sigma, factor=1.0)
Definition: detection.py:524
def setEdgeBits(maskedImage, goodBBox, edgeBitmask)
Definition: detection.py:785
def display(self, exposure, results, convolvedImage=None)
Definition: detection.py:264
def applyThreshold(self, middle, bbox, factor=1.0)
Definition: detection.py:475
daf::base::PropertySet * set
Definition: fits.cc:912
Threshold createThreshold(const double value, const std::string type="value", const bool polarity=true)
Factory method for creating Threshold objects.
Definition: Threshold.cc:109
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)
Definition: Statistics.h:354
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, ConvolutionControl const &convolutionControl=ConvolutionControl())
Convolve an Image or MaskedImage with a Kernel, setting pixels of an existing output image.
def addExposures(exposureList)
Definition: detection.py:855