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