lsst.meas.algorithms  20.0.0-9-g70cec07d+482e8042f7
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 from deprecated.sphinx import deprecated
28 
29 import numpy as np
30 
31 import lsst.geom
32 import lsst.afw.display as afwDisplay
33 import lsst.afw.detection as afwDet
34 import lsst.afw.geom as afwGeom
35 import lsst.afw.image as afwImage
36 import lsst.afw.math as afwMath
37 import lsst.afw.table as afwTable
38 import lsst.pex.config as pexConfig
39 import lsst.pipe.base as pipeBase
40 from .subtractBackground import SubtractBackgroundTask
41 
42 
43 class SourceDetectionConfig(pexConfig.Config):
44  """Configuration parameters for the SourceDetectionTask
45  """
46  minPixels = pexConfig.RangeField(
47  doc="detected sources with fewer than the specified number of pixels will be ignored",
48  dtype=int, optional=False, default=1, min=0,
49  )
50  isotropicGrow = pexConfig.Field(
51  doc="Pixels should be grown as isotropically as possible (slower)",
52  dtype=bool, optional=False, default=False,
53  )
54  combinedGrow = pexConfig.Field(
55  doc="Grow all footprints at the same time? This allows disconnected footprints to merge.",
56  dtype=bool, default=True,
57  )
58  nSigmaToGrow = pexConfig.Field(
59  doc="Grow detections by nSigmaToGrow * [PSF RMS width]; if 0 then do not grow",
60  dtype=float, default=2.4, # 2.4 pixels/sigma is roughly one pixel/FWHM
61  )
62  returnOriginalFootprints = pexConfig.Field(
63  doc="Grow detections to set the image mask bits, but return the original (not-grown) footprints",
64  dtype=bool, optional=False, default=False,
65  )
66  thresholdValue = pexConfig.RangeField(
67  doc="Threshold for footprints; exact meaning and units depend on thresholdType.",
68  dtype=float, optional=False, default=5.0, min=0.0,
69  )
70  includeThresholdMultiplier = pexConfig.RangeField(
71  doc="Include threshold relative to thresholdValue",
72  dtype=float, default=1.0, min=0.0,
73  )
74  thresholdType = pexConfig.ChoiceField(
75  doc="specifies the desired flavor of Threshold",
76  dtype=str, optional=False, default="stdev",
77  allowed={
78  "variance": "threshold applied to image variance",
79  "stdev": "threshold applied to image std deviation",
80  "value": "threshold applied to image value",
81  "pixel_stdev": "threshold applied to per-pixel std deviation",
82  },
83  )
84  thresholdPolarity = pexConfig.ChoiceField(
85  doc="specifies whether to detect positive, or negative sources, or both",
86  dtype=str, optional=False, default="positive",
87  allowed={
88  "positive": "detect only positive sources",
89  "negative": "detect only negative sources",
90  "both": "detect both positive and negative sources",
91  },
92  )
93  adjustBackground = pexConfig.Field(
94  dtype=float,
95  doc="Fiddle factor to add to the background; debugging only",
96  default=0.0,
97  )
98  reEstimateBackground = pexConfig.Field(
99  dtype=bool,
100  doc="Estimate the background again after final source detection?",
101  default=True, optional=False,
102  )
103  background = pexConfig.ConfigurableField(
104  doc="Background re-estimation; ignored if reEstimateBackground false",
105  target=SubtractBackgroundTask,
106  )
107  tempLocalBackground = pexConfig.ConfigurableField(
108  doc=("A local (small-scale), temporary background estimation step run between "
109  "detecting above-threshold regions and detecting the peaks within "
110  "them; used to avoid detecting spuerious peaks in the wings."),
111  target=SubtractBackgroundTask,
112  )
113  doTempLocalBackground = pexConfig.Field(
114  dtype=bool,
115  doc="Enable temporary local background subtraction? (see tempLocalBackground)",
116  default=True,
117  )
118  tempWideBackground = pexConfig.ConfigurableField(
119  doc=("A wide (large-scale) background estimation and removal before footprint and peak detection. "
120  "It is added back into the image after detection. The purpose is to suppress very large "
121  "footprints (e.g., from large artifacts) that the deblender may choke on."),
122  target=SubtractBackgroundTask,
123  )
124  doTempWideBackground = pexConfig.Field(
125  dtype=bool,
126  doc="Do temporary wide (large-scale) background subtraction before footprint detection?",
127  default=False,
128  )
129  nPeaksMaxSimple = pexConfig.Field(
130  dtype=int,
131  doc=("The maximum number of peaks in a Footprint before trying to "
132  "replace its peaks using the temporary local background"),
133  default=1,
134  )
135  nSigmaForKernel = pexConfig.Field(
136  dtype=float,
137  doc=("Multiple of PSF RMS size to use for convolution kernel bounding box size; "
138  "note that this is not a half-size. The size will be rounded up to the nearest odd integer"),
139  default=7.0,
140  )
141  statsMask = pexConfig.ListField(
142  dtype=str,
143  doc="Mask planes to ignore when calculating statistics of image (for thresholdType=stdev)",
144  default=['BAD', 'SAT', 'EDGE', 'NO_DATA'],
145  )
146 
147  def setDefaults(self):
148  self.tempLocalBackground.binSize = 64
149  self.tempLocalBackground.algorithm = "AKIMA_SPLINE"
150  self.tempLocalBackground.useApprox = False
151  # Background subtraction to remove a large-scale background (e.g., scattered light); restored later.
152  # Want to keep it from exceeding the deblender size limit of 1 Mpix, so half that is reasonable.
153  self.tempWideBackground.binSize = 512
154  self.tempWideBackground.algorithm = "AKIMA_SPLINE"
155  self.tempWideBackground.useApprox = False
156  # Ensure we can remove even bright scattered light that is DETECTED
157  for maskPlane in ("DETECTED", "DETECTED_NEGATIVE"):
158  if maskPlane in self.tempWideBackground.ignoredPixelMask:
159  self.tempWideBackground.ignoredPixelMask.remove(maskPlane)
160 
161 
162 class SourceDetectionTask(pipeBase.Task):
163  """Create the detection task. Most arguments are simply passed onto pipe.base.Task.
164 
165  Parameters
166  ----------
167  schema : `lsst.afw.table.Schema`
168  Schema object used to create the output `lsst.afw.table.SourceCatalog`
169  **kwds
170  Keyword arguments passed to `lsst.pipe.base.task.Task.__init__`
171 
172  If schema is not None and configured for 'both' detections,
173  a 'flags.negative' field will be added to label detections made with a
174  negative threshold.
175 
176  Notes
177  -----
178  This task can add fields to the schema, so any code calling this task must ensure that
179  these columns are indeed present in the input match list.
180  """
181 
182  ConfigClass = SourceDetectionConfig
183  _DefaultName = "sourceDetection"
184 
185  def __init__(self, schema=None, **kwds):
186  pipeBase.Task.__init__(self, **kwds)
187  if schema is not None and self.config.thresholdPolarity == "both":
188  self.negativeFlagKey = schema.addField(
189  "flags_negative", type="Flag",
190  doc="set if source was detected as significantly negative"
191  )
192  else:
193  if self.config.thresholdPolarity == "both":
194  self.log.warn("Detection polarity set to 'both', but no flag will be "
195  "set to distinguish between positive and negative detections")
196  self.negativeFlagKey = None
197  if self.config.reEstimateBackground:
198  self.makeSubtask("background")
199  if self.config.doTempLocalBackground:
200  self.makeSubtask("tempLocalBackground")
201  if self.config.doTempWideBackground:
202  self.makeSubtask("tempWideBackground")
203 
204  @pipeBase.timeMethod
205  def run(self, table, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None):
206  """Run source detection and create a SourceCatalog of detections.
207 
208  Parameters
209  ----------
210  table : `lsst.afw.table.SourceTable`
211  Table object that will be used to create the SourceCatalog.
212  exposure : `lsst.afw.image.Exposure`
213  Exposure to process; DETECTED mask plane will be set in-place.
214  doSmooth : `bool`
215  If True, smooth the image before detection using a Gaussian of width
216  ``sigma``, or the measured PSF width. Set to False when running on
217  e.g. a pre-convolved image, or a mask plane.
218  sigma : `float`
219  Sigma of PSF (pixels); used for smoothing and to grow detections;
220  if None then measure the sigma of the PSF of the exposure
221  clearMask : `bool`
222  Clear DETECTED{,_NEGATIVE} planes before running detection.
223  expId : `int`
224  Exposure identifier; unused by this implementation, but used for
225  RNG seed by subclasses.
226 
227  Returns
228  -------
229  result : `lsst.pipe.base.Struct`
230  ``sources``
231  The detected sources (`lsst.afw.table.SourceCatalog`)
232  ``fpSets``
233  The result resturned by `detectFootprints`
234  (`lsst.pipe.base.Struct`).
235 
236  Raises
237  ------
238  ValueError
239  If flags.negative is needed, but isn't in table's schema.
240  lsst.pipe.base.TaskError
241  If sigma=None, doSmooth=True and the exposure has no PSF.
242 
243  Notes
244  -----
245  If you want to avoid dealing with Sources and Tables, you can use
246  detectFootprints() to just get the `lsst.afw.detection.FootprintSet`s.
247  """
248  if self.negativeFlagKey is not None and self.negativeFlagKey not in table.getSchema():
249  raise ValueError("Table has incorrect Schema")
250  results = self.detectFootprints(exposure=exposure, doSmooth=doSmooth, sigma=sigma,
251  clearMask=clearMask, expId=expId)
252  sources = afwTable.SourceCatalog(table)
253  sources.reserve(results.numPos + results.numNeg)
254  if results.negative:
255  results.negative.makeSources(sources)
256  if self.negativeFlagKey:
257  for record in sources:
258  record.set(self.negativeFlagKey, True)
259  if results.positive:
260  results.positive.makeSources(sources)
261  results.fpSets = results.copy() # Backward compatibility
262  results.sources = sources
263  return results
264 
265  @deprecated(reason="Replaced by SourceDetectionTask.run(). Will be removed after v20.",
266  category=FutureWarning)
267  def makeSourceCatalog(self, *args, **kwargs):
268  return self.run(*args, **kwargs)
269 
270  def display(self, exposure, results, convolvedImage=None):
271  """Display detections if so configured
272 
273  Displays the ``exposure`` in frame 0, overlays the detection peaks.
274 
275  Requires that ``lsstDebug`` has been set up correctly, so that
276  ``lsstDebug.Info("lsst.meas.algorithms.detection")`` evaluates `True`.
277 
278  If the ``convolvedImage`` is non-`None` and
279  ``lsstDebug.Info("lsst.meas.algorithms.detection") > 1``, the
280  ``convolvedImage`` will be displayed in frame 1.
281 
282  Parameters
283  ----------
284  exposure : `lsst.afw.image.Exposure`
285  Exposure to display, on which will be plotted the detections.
286  results : `lsst.pipe.base.Struct`
287  Results of the 'detectFootprints' method, containing positive and
288  negative footprints (which contain the peak positions that we will
289  plot). This is a `Struct` with ``positive`` and ``negative``
290  elements that are of type `lsst.afw.detection.FootprintSet`.
291  convolvedImage : `lsst.afw.image.Image`, optional
292  Convolved image used for thresholding.
293  """
294  try:
295  import lsstDebug
296  display = lsstDebug.Info(__name__).display
297  except ImportError:
298  try:
299  display
300  except NameError:
301  display = False
302  if not display:
303  return
304 
305  afwDisplay.setDefaultMaskTransparency(75)
306 
307  disp0 = afwDisplay.Display(frame=0)
308  disp0.mtv(exposure, title="detection")
309 
310  def plotPeaks(fps, ctype):
311  if fps is None:
312  return
313  with disp0.Buffering():
314  for fp in fps.getFootprints():
315  for pp in fp.getPeaks():
316  disp0.dot("+", pp.getFx(), pp.getFy(), ctype=ctype)
317  plotPeaks(results.positive, "yellow")
318  plotPeaks(results.negative, "red")
319 
320  if convolvedImage and display > 1:
321  disp1 = afwDisplay.Display(frame=1)
322  disp1.mtv(convolvedImage, title="PSF smoothed")
323 
324  def applyTempLocalBackground(self, exposure, middle, results):
325  """Apply a temporary local background subtraction
326 
327  This temporary local background serves to suppress noise fluctuations
328  in the wings of bright objects.
329 
330  Peaks in the footprints will be updated.
331 
332  Parameters
333  ----------
334  exposure : `lsst.afw.image.Exposure`
335  Exposure for which to fit local background.
336  middle : `lsst.afw.image.MaskedImage`
337  Convolved image on which detection will be performed
338  (typically smaller than ``exposure`` because the
339  half-kernel has been removed around the edges).
340  results : `lsst.pipe.base.Struct`
341  Results of the 'detectFootprints' method, containing positive and
342  negative footprints (which contain the peak positions that we will
343  plot). This is a `Struct` with ``positive`` and ``negative``
344  elements that are of type `lsst.afw.detection.FootprintSet`.
345  """
346  # Subtract the local background from the smoothed image. Since we
347  # never use the smoothed again we don't need to worry about adding
348  # it back in.
349  bg = self.tempLocalBackground.fitBackground(exposure.getMaskedImage())
350  bgImage = bg.getImageF(self.tempLocalBackground.config.algorithm,
351  self.tempLocalBackground.config.undersampleStyle)
352  middle -= bgImage.Factory(bgImage, middle.getBBox())
353  thresholdPos = self.makeThreshold(middle, "positive")
354  thresholdNeg = self.makeThreshold(middle, "negative")
355  if self.config.thresholdPolarity != "negative":
356  self.updatePeaks(results.positive, middle, thresholdPos)
357  if self.config.thresholdPolarity != "positive":
358  self.updatePeaks(results.negative, middle, thresholdNeg)
359 
360  def clearMask(self, mask):
361  """Clear the DETECTED and DETECTED_NEGATIVE mask planes
362 
363  Removes any previous detection mask in preparation for a new
364  detection pass.
365 
366  Parameters
367  ----------
368  mask : `lsst.afw.image.Mask`
369  Mask to be cleared.
370  """
371  mask &= ~(mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE"))
372 
373  def calculateKernelSize(self, sigma):
374  """Calculate size of smoothing kernel
375 
376  Uses the ``nSigmaForKernel`` configuration parameter. Note
377  that that is the full width of the kernel bounding box
378  (so a value of 7 means 3.5 sigma on either side of center).
379  The value will be rounded up to the nearest odd integer.
380 
381  Parameters
382  ----------
383  sigma : `float`
384  Gaussian sigma of smoothing kernel.
385 
386  Returns
387  -------
388  size : `int`
389  Size of the smoothing kernel.
390  """
391  return (int(sigma * self.config.nSigmaForKernel + 0.5)//2)*2 + 1 # make sure it is odd
392 
393  def getPsf(self, exposure, sigma=None):
394  """Retrieve the PSF for an exposure
395 
396  If ``sigma`` is provided, we make a ``GaussianPsf`` with that,
397  otherwise use the one from the ``exposure``.
398 
399  Parameters
400  ----------
401  exposure : `lsst.afw.image.Exposure`
402  Exposure from which to retrieve the PSF.
403  sigma : `float`, optional
404  Gaussian sigma to use if provided.
405 
406  Returns
407  -------
408  psf : `lsst.afw.detection.Psf`
409  PSF to use for detection.
410  """
411  if sigma is None:
412  psf = exposure.getPsf()
413  if psf is None:
414  raise RuntimeError("Unable to determine PSF to use for detection: no sigma provided")
415  sigma = psf.computeShape().getDeterminantRadius()
416  size = self.calculateKernelSize(sigma)
417  psf = afwDet.GaussianPsf(size, size, sigma)
418  return psf
419 
420  def convolveImage(self, maskedImage, psf, doSmooth=True):
421  """Convolve the image with the PSF
422 
423  We convolve the image with a Gaussian approximation to the PSF,
424  because this is separable and therefore fast. It's technically a
425  correlation rather than a convolution, but since we use a symmetric
426  Gaussian there's no difference.
427 
428  The convolution can be disabled with ``doSmooth=False``. If we do
429  convolve, we mask the edges as ``EDGE`` and return the convolved image
430  with the edges removed. This is because we can't convolve the edges
431  because the kernel would extend off the image.
432 
433  Parameters
434  ----------
435  maskedImage : `lsst.afw.image.MaskedImage`
436  Image to convolve.
437  psf : `lsst.afw.detection.Psf`
438  PSF to convolve with (actually with a Gaussian approximation
439  to it).
440  doSmooth : `bool`
441  Actually do the convolution? Set to False when running on
442  e.g. a pre-convolved image, or a mask plane.
443 
444  Return Struct contents
445  ----------------------
446  middle : `lsst.afw.image.MaskedImage`
447  Convolved image, without the edges.
448  sigma : `float`
449  Gaussian sigma used for the convolution.
450  """
451  self.metadata.set("doSmooth", doSmooth)
452  sigma = psf.computeShape().getDeterminantRadius()
453  self.metadata.set("sigma", sigma)
454 
455  if not doSmooth:
456  middle = maskedImage.Factory(maskedImage, deep=True)
457  return pipeBase.Struct(middle=middle, sigma=sigma)
458 
459  # Smooth using a Gaussian (which is separable, hence fast) of width sigma
460  # Make a SingleGaussian (separable) kernel with the 'sigma'
461  kWidth = self.calculateKernelSize(sigma)
462  self.metadata.set("smoothingKernelWidth", kWidth)
463  gaussFunc = afwMath.GaussianFunction1D(sigma)
464  gaussKernel = afwMath.SeparableKernel(kWidth, kWidth, gaussFunc, gaussFunc)
465 
466  convolvedImage = maskedImage.Factory(maskedImage.getBBox())
467 
468  afwMath.convolve(convolvedImage, maskedImage, gaussKernel, afwMath.ConvolutionControl())
469  #
470  # Only search psf-smoothed part of frame
471  #
472  goodBBox = gaussKernel.shrinkBBox(convolvedImage.getBBox())
473  middle = convolvedImage.Factory(convolvedImage, goodBBox, afwImage.PARENT, False)
474  #
475  # Mark the parts of the image outside goodBBox as EDGE
476  #
477  self.setEdgeBits(maskedImage, goodBBox, maskedImage.getMask().getPlaneBitMask("EDGE"))
478 
479  return pipeBase.Struct(middle=middle, sigma=sigma)
480 
481  def applyThreshold(self, middle, bbox, factor=1.0):
482  """Apply thresholds to the convolved image
483 
484  Identifies ``Footprint``s, both positive and negative.
485 
486  The threshold can be modified by the provided multiplication
487  ``factor``.
488 
489  Parameters
490  ----------
491  middle : `lsst.afw.image.MaskedImage`
492  Convolved image to threshold.
493  bbox : `lsst.geom.Box2I`
494  Bounding box of unconvolved image.
495  factor : `float`
496  Multiplier for the configured threshold.
497 
498  Return Struct contents
499  ----------------------
500  positive : `lsst.afw.detection.FootprintSet` or `None`
501  Positive detection footprints, if configured.
502  negative : `lsst.afw.detection.FootprintSet` or `None`
503  Negative detection footprints, if configured.
504  factor : `float`
505  Multiplier for the configured threshold.
506  """
507  results = pipeBase.Struct(positive=None, negative=None, factor=factor)
508  # Detect the Footprints (peaks may be replaced if doTempLocalBackground)
509  if self.config.reEstimateBackground or self.config.thresholdPolarity != "negative":
510  threshold = self.makeThreshold(middle, "positive", factor=factor)
511  results.positive = afwDet.FootprintSet(
512  middle,
513  threshold,
514  "DETECTED",
515  self.config.minPixels
516  )
517  results.positive.setRegion(bbox)
518  if self.config.reEstimateBackground or self.config.thresholdPolarity != "positive":
519  threshold = self.makeThreshold(middle, "negative", factor=factor)
520  results.negative = afwDet.FootprintSet(
521  middle,
522  threshold,
523  "DETECTED_NEGATIVE",
524  self.config.minPixels
525  )
526  results.negative.setRegion(bbox)
527 
528  return results
529 
530  def finalizeFootprints(self, mask, results, sigma, factor=1.0):
531  """Finalize the detected footprints
532 
533  Grows the footprints, sets the ``DETECTED`` and ``DETECTED_NEGATIVE``
534  mask planes, and logs the results.
535 
536  ``numPos`` (number of positive footprints), ``numPosPeaks`` (number
537  of positive peaks), ``numNeg`` (number of negative footprints),
538  ``numNegPeaks`` (number of negative peaks) entries are added to the
539  detection results.
540 
541  Parameters
542  ----------
543  mask : `lsst.afw.image.Mask`
544  Mask image on which to flag detected pixels.
545  results : `lsst.pipe.base.Struct`
546  Struct of detection results, including ``positive`` and
547  ``negative`` entries; modified.
548  sigma : `float`
549  Gaussian sigma of PSF.
550  factor : `float`
551  Multiplier for the configured threshold.
552  """
553  for polarity, maskName in (("positive", "DETECTED"), ("negative", "DETECTED_NEGATIVE")):
554  fpSet = getattr(results, polarity)
555  if fpSet is None:
556  continue
557  if self.config.nSigmaToGrow > 0:
558  nGrow = int((self.config.nSigmaToGrow * sigma) + 0.5)
559  self.metadata.set("nGrow", nGrow)
560  if self.config.combinedGrow:
561  fpSet = afwDet.FootprintSet(fpSet, nGrow, self.config.isotropicGrow)
562  else:
563  stencil = (afwGeom.Stencil.CIRCLE if self.config.isotropicGrow else
564  afwGeom.Stencil.MANHATTAN)
565  for fp in fpSet:
566  fp.dilate(nGrow, stencil)
567  fpSet.setMask(mask, maskName)
568  if not self.config.returnOriginalFootprints:
569  setattr(results, polarity, fpSet)
570 
571  results.numPos = 0
572  results.numPosPeaks = 0
573  results.numNeg = 0
574  results.numNegPeaks = 0
575  positive = ""
576  negative = ""
577 
578  if results.positive is not None:
579  results.numPos = len(results.positive.getFootprints())
580  results.numPosPeaks = sum(len(fp.getPeaks()) for fp in results.positive.getFootprints())
581  positive = " %d positive peaks in %d footprints" % (results.numPosPeaks, results.numPos)
582  if results.negative is not None:
583  results.numNeg = len(results.negative.getFootprints())
584  results.numNegPeaks = sum(len(fp.getPeaks()) for fp in results.negative.getFootprints())
585  negative = " %d negative peaks in %d footprints" % (results.numNegPeaks, results.numNeg)
586 
587  self.log.info("Detected%s%s%s to %g %s" %
588  (positive, " and" if positive and negative else "", negative,
589  self.config.thresholdValue*self.config.includeThresholdMultiplier*factor,
590  "DN" if self.config.thresholdType == "value" else "sigma"))
591 
592  def reEstimateBackground(self, maskedImage, backgrounds):
593  """Estimate the background after detection
594 
595  Parameters
596  ----------
597  maskedImage : `lsst.afw.image.MaskedImage`
598  Image on which to estimate the background.
599  backgrounds : `lsst.afw.math.BackgroundList`
600  List of backgrounds; modified.
601 
602  Returns
603  -------
604  bg : `lsst.afw.math.backgroundMI`
605  Empirical background model.
606  """
607  bg = self.background.fitBackground(maskedImage)
608  if self.config.adjustBackground:
609  self.log.warn("Fiddling the background by %g", self.config.adjustBackground)
610  bg += self.config.adjustBackground
611  self.log.info("Resubtracting the background after object detection")
612  maskedImage -= bg.getImageF(self.background.config.algorithm,
613  self.background.config.undersampleStyle)
614 
615  actrl = bg.getBackgroundControl().getApproximateControl()
616  backgrounds.append((bg, getattr(afwMath.Interpolate, self.background.config.algorithm),
617  bg.getAsUsedUndersampleStyle(), actrl.getStyle(), actrl.getOrderX(),
618  actrl.getOrderY(), actrl.getWeighting()))
619  return bg
620 
621  def clearUnwantedResults(self, mask, results):
622  """Clear unwanted results from the Struct of results
623 
624  If we specifically want only positive or only negative detections,
625  drop the ones we don't want, and its associated mask plane.
626 
627  Parameters
628  ----------
629  mask : `lsst.afw.image.Mask`
630  Mask image.
631  results : `lsst.pipe.base.Struct`
632  Detection results, with ``positive`` and ``negative`` elements;
633  modified.
634  """
635  if self.config.thresholdPolarity == "positive":
636  if self.config.reEstimateBackground:
637  mask &= ~mask.getPlaneBitMask("DETECTED_NEGATIVE")
638  results.negative = None
639  elif self.config.thresholdPolarity == "negative":
640  if self.config.reEstimateBackground:
641  mask &= ~mask.getPlaneBitMask("DETECTED")
642  results.positive = None
643 
644  @pipeBase.timeMethod
645  def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None):
646  """Detect footprints on an exposure.
647 
648  Parameters
649  ----------
650  exposure : `lsst.afw.image.Exposure`
651  Exposure to process; DETECTED{,_NEGATIVE} mask plane will be
652  set in-place.
653  doSmooth : `bool`, optional
654  If True, smooth the image before detection using a Gaussian
655  of width ``sigma``, or the measured PSF width of ``exposure``.
656  Set to False when running on e.g. a pre-convolved image, or a mask
657  plane.
658  sigma : `float`, optional
659  Gaussian Sigma of PSF (pixels); used for smoothing and to grow
660  detections; if `None` then measure the sigma of the PSF of the
661  ``exposure``.
662  clearMask : `bool`, optional
663  Clear both DETECTED and DETECTED_NEGATIVE planes before running
664  detection.
665  expId : `dict`, optional
666  Exposure identifier; unused by this implementation, but used for
667  RNG seed by subclasses.
668 
669  Return Struct contents
670  ----------------------
671  positive : `lsst.afw.detection.FootprintSet`
672  Positive polarity footprints (may be `None`)
673  negative : `lsst.afw.detection.FootprintSet`
674  Negative polarity footprints (may be `None`)
675  numPos : `int`
676  Number of footprints in positive or 0 if detection polarity was
677  negative.
678  numNeg : `int`
679  Number of footprints in negative or 0 if detection polarity was
680  positive.
681  background : `lsst.afw.math.BackgroundList`
682  Re-estimated background. `None` if
683  ``reEstimateBackground==False``.
684  factor : `float`
685  Multiplication factor applied to the configured detection
686  threshold.
687  """
688  maskedImage = exposure.maskedImage
689 
690  if clearMask:
691  self.clearMask(maskedImage.getMask())
692 
693  psf = self.getPsf(exposure, sigma=sigma)
694  with self.tempWideBackgroundContext(exposure):
695  convolveResults = self.convolveImage(maskedImage, psf, doSmooth=doSmooth)
696  middle = convolveResults.middle
697  sigma = convolveResults.sigma
698 
699  results = self.applyThreshold(middle, maskedImage.getBBox())
700  results.background = afwMath.BackgroundList()
701  if self.config.doTempLocalBackground:
702  self.applyTempLocalBackground(exposure, middle, results)
703  self.finalizeFootprints(maskedImage.mask, results, sigma)
704 
705  if self.config.reEstimateBackground:
706  self.reEstimateBackground(maskedImage, results.background)
707 
708  self.clearUnwantedResults(maskedImage.getMask(), results)
709  self.display(exposure, results, middle)
710 
711  return results
712 
713  def makeThreshold(self, image, thresholdParity, factor=1.0):
714  """Make an afw.detection.Threshold object corresponding to the task's
715  configuration and the statistics of the given image.
716 
717  Parameters
718  ----------
719  image : `afw.image.MaskedImage`
720  Image to measure noise statistics from if needed.
721  thresholdParity: `str`
722  One of "positive" or "negative", to set the kind of fluctuations
723  the Threshold will detect.
724  factor : `float`
725  Factor by which to multiply the configured detection threshold.
726  This is useful for tweaking the detection threshold slightly.
727 
728  Returns
729  -------
730  threshold : `lsst.afw.detection.Threshold`
731  Detection threshold.
732  """
733  parity = False if thresholdParity == "negative" else True
734  thresholdValue = self.config.thresholdValue
735  thresholdType = self.config.thresholdType
736  if self.config.thresholdType == 'stdev':
737  bad = image.getMask().getPlaneBitMask(self.config.statsMask)
738  sctrl = afwMath.StatisticsControl()
739  sctrl.setAndMask(bad)
740  stats = afwMath.makeStatistics(image, afwMath.STDEVCLIP, sctrl)
741  thresholdValue *= stats.getValue(afwMath.STDEVCLIP)
742  thresholdType = 'value'
743 
744  threshold = afwDet.createThreshold(thresholdValue*factor, thresholdType, parity)
745  threshold.setIncludeMultiplier(self.config.includeThresholdMultiplier)
746  return threshold
747 
748  def updatePeaks(self, fpSet, image, threshold):
749  """Update the Peaks in a FootprintSet by detecting new Footprints and
750  Peaks in an image and using the new Peaks instead of the old ones.
751 
752  Parameters
753  ----------
754  fpSet : `afw.detection.FootprintSet`
755  Set of Footprints whose Peaks should be updated.
756  image : `afw.image.MaskedImage`
757  Image to detect new Footprints and Peak in.
758  threshold : `afw.detection.Threshold`
759  Threshold object for detection.
760 
761  Input Footprints with fewer Peaks than self.config.nPeaksMaxSimple
762  are not modified, and if no new Peaks are detected in an input
763  Footprint, the brightest original Peak in that Footprint is kept.
764  """
765  for footprint in fpSet.getFootprints():
766  oldPeaks = footprint.getPeaks()
767  if len(oldPeaks) <= self.config.nPeaksMaxSimple:
768  continue
769  # We detect a new FootprintSet within each non-simple Footprint's
770  # bbox to avoid a big O(N^2) comparison between the two sets of
771  # Footprints.
772  sub = image.Factory(image, footprint.getBBox())
773  fpSetForPeaks = afwDet.FootprintSet(
774  sub,
775  threshold,
776  "", # don't set a mask plane
777  self.config.minPixels
778  )
779  newPeaks = afwDet.PeakCatalog(oldPeaks.getTable())
780  for fpForPeaks in fpSetForPeaks.getFootprints():
781  for peak in fpForPeaks.getPeaks():
782  if footprint.contains(peak.getI()):
783  newPeaks.append(peak)
784  if len(newPeaks) > 0:
785  del oldPeaks[:]
786  oldPeaks.extend(newPeaks)
787  else:
788  del oldPeaks[1:]
789 
790  @staticmethod
791  def setEdgeBits(maskedImage, goodBBox, edgeBitmask):
792  """Set the edgeBitmask bits for all of maskedImage outside goodBBox
793 
794  Parameters
795  ----------
796  maskedImage : `lsst.afw.image.MaskedImage`
797  Image on which to set edge bits in the mask.
798  goodBBox : `lsst.geom.Box2I`
799  Bounding box of good pixels, in ``LOCAL`` coordinates.
800  edgeBitmask : `lsst.afw.image.MaskPixel`
801  Bit mask to OR with the existing mask bits in the region
802  outside ``goodBBox``.
803  """
804  msk = maskedImage.getMask()
805 
806  mx0, my0 = maskedImage.getXY0()
807  for x0, y0, w, h in ([0, 0,
808  msk.getWidth(), goodBBox.getBeginY() - my0],
809  [0, goodBBox.getEndY() - my0, msk.getWidth(),
810  maskedImage.getHeight() - (goodBBox.getEndY() - my0)],
811  [0, 0,
812  goodBBox.getBeginX() - mx0, msk.getHeight()],
813  [goodBBox.getEndX() - mx0, 0,
814  maskedImage.getWidth() - (goodBBox.getEndX() - mx0), msk.getHeight()],
815  ):
816  edgeMask = msk.Factory(msk, lsst.geom.BoxI(lsst.geom.PointI(x0, y0),
817  lsst.geom.ExtentI(w, h)), afwImage.LOCAL)
818  edgeMask |= edgeBitmask
819 
820  @contextmanager
821  def tempWideBackgroundContext(self, exposure):
822  """Context manager for removing wide (large-scale) background
823 
824  Removing a wide (large-scale) background helps to suppress the
825  detection of large footprints that may overwhelm the deblender.
826  It does, however, set a limit on the maximum scale of objects.
827 
828  The background that we remove will be restored upon exit from
829  the context manager.
830 
831  Parameters
832  ----------
833  exposure : `lsst.afw.image.Exposure`
834  Exposure on which to remove large-scale background.
835 
836  Returns
837  -------
838  context : context manager
839  Context manager that will ensure the temporary wide background
840  is restored.
841  """
842  doTempWideBackground = self.config.doTempWideBackground
843  if doTempWideBackground:
844  self.log.info("Applying temporary wide background subtraction")
845  original = exposure.maskedImage.image.array[:].copy()
846  self.tempWideBackground.run(exposure).background
847  # Remove NO_DATA regions (e.g., edge of the field-of-view); these can cause detections after
848  # subtraction because of extrapolation of the background model into areas with no constraints.
849  image = exposure.maskedImage.image
850  mask = exposure.maskedImage.mask
851  noData = mask.array & mask.getPlaneBitMask("NO_DATA") > 0
852  isGood = mask.array & mask.getPlaneBitMask(self.config.statsMask) == 0
853  image.array[noData] = np.median(image.array[~noData & isGood])
854  try:
855  yield
856  finally:
857  if doTempWideBackground:
858  exposure.maskedImage.image.array[:] = original
859 
860 
861 def addExposures(exposureList):
862  """Add a set of exposures together.
863 
864  Parameters
865  ----------
866  exposureList : `list` of `lsst.afw.image.Exposure`
867  Sequence of exposures to add.
868 
869  Returns
870  -------
871  addedExposure : `lsst.afw.image.Exposure`
872  An exposure of the same size as each exposure in ``exposureList``,
873  with the metadata from ``exposureList[0]`` and a masked image equal
874  to the sum of all the exposure's masked images.
875  """
876  exposure0 = exposureList[0]
877  image0 = exposure0.getMaskedImage()
878 
879  addedImage = image0.Factory(image0, True)
880  addedImage.setXY0(image0.getXY0())
881 
882  for exposure in exposureList[1:]:
883  image = exposure.getMaskedImage()
884  addedImage += image
885 
886  addedExposure = exposure0.Factory(addedImage, exposure0.getWcs())
887  return addedExposure
lsst::meas::algorithms.detection.SourceDetectionTask.clearMask
def clearMask(self, mask)
Definition: detection.py:360
lsst::afw::image
lsst::meas::algorithms.detection.addExposures
def addExposures(exposureList)
Definition: detection.py:861
lsst::meas::algorithms.detection.SourceDetectionTask.makeThreshold
def makeThreshold(self, image, thresholdParity, factor=1.0)
Definition: detection.py:713
lsst::meas::algorithms.detection.SourceDetectionConfig.tempLocalBackground
tempLocalBackground
Definition: detection.py:107
lsst::afw::display
lsst::meas::algorithms.detection.SourceDetectionTask.clearUnwantedResults
def clearUnwantedResults(self, mask, results)
Definition: detection.py:621
lsst::meas::algorithms.detection.SourceDetectionConfig.setDefaults
def setDefaults(self)
Definition: detection.py:147
lsst::meas::algorithms.detection.SourceDetectionTask.tempWideBackgroundContext
def tempWideBackgroundContext(self, exposure)
Definition: detection.py:821
lsst::meas::algorithms.detection.SourceDetectionTask.detectFootprints
def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None)
Definition: detection.py:645
lsst::meas::algorithms.detection.SourceDetectionConfig
Definition: detection.py:43
lsst::meas::algorithms.detection.SourceDetectionTask.finalizeFootprints
def finalizeFootprints(self, mask, results, sigma, factor=1.0)
Definition: detection.py:530
lsst::meas::algorithms.detection.SourceDetectionTask.convolveImage
def convolveImage(self, maskedImage, psf, doSmooth=True)
Definition: detection.py:420
lsst::meas::algorithms.detection.SourceDetectionTask.applyTempLocalBackground
def applyTempLocalBackground(self, exposure, middle, results)
Definition: detection.py:324
lsstDebug::Info
lsst::meas::algorithms.detection.SourceDetectionTask.calculateKernelSize
def calculateKernelSize(self, sigma)
Definition: detection.py:373
lsst::meas::algorithms.detection.SourceDetectionTask.__init__
def __init__(self, schema=None, **kwds)
Definition: detection.py:185
lsst::meas::algorithms.detection.SourceDetectionConfig.tempWideBackground
tempWideBackground
Definition: detection.py:118
lsst::meas::algorithms.detection.SourceDetectionTask.display
def display(self, exposure, results, convolvedImage=None)
Definition: detection.py:270
lsst::meas::algorithms.detection.SourceDetectionTask.negativeFlagKey
negativeFlagKey
Definition: detection.py:188
lsst::afw::table
lsst::pex::config
lsst::meas::algorithms.detection.SourceDetectionTask.setEdgeBits
def setEdgeBits(maskedImage, goodBBox, edgeBitmask)
Definition: detection.py:791
lsst::afw::detection
lsst::geom
lsst::afw::math
Point< int, 2 >
lsst::meas::algorithms.detection.SourceDetectionTask.getPsf
def getPsf(self, exposure, sigma=None)
Definition: detection.py:393
lsst::geom::Box2I
lsst::meas::algorithms.detection.SourceDetectionTask
Definition: detection.py:162
lsst::meas::algorithms.detection.SourceDetectionTask.updatePeaks
def updatePeaks(self, fpSet, image, threshold)
Definition: detection.py:748
lsst::pipe::base
lsst::meas::algorithms.detection.SourceDetectionTask.reEstimateBackground
def reEstimateBackground(self, maskedImage, backgrounds)
Definition: detection.py:592
Extent< int, 2 >
lsst::meas::algorithms.detection.SourceDetectionTask.run
def run(self, table, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None)
Definition: detection.py:205
lsst::meas::algorithms.detection.SourceDetectionTask.applyThreshold
def applyThreshold(self, middle, bbox, factor=1.0)
Definition: detection.py:481
lsst::meas::algorithms.detection.SourceDetectionTask.makeSourceCatalog
def makeSourceCatalog(self, *args, **kwargs)
Definition: detection.py:267
lsst::afw::geom