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