lsst.meas.algorithms  14.0-14-gbf7a6f8a
dynamicDetection.py
Go to the documentation of this file.
1 from __future__ import absolute_import, division, print_function
2 
3 __all__ = ["DynamicDetectionConfig", "DynamicDetectionTask"]
4 
5 import numpy as np
6 
7 from lsst.pex.config import Field, ConfigurableField
8 from lsst.pipe.base import Task
9 
10 from .detection import SourceDetectionConfig, SourceDetectionTask
11 from .skyObjects import SkyObjectsTask
12 
13 from lsst.afw.detection import FootprintSet
14 from lsst.afw.table import SourceCatalog, SourceTable, IdFactory
15 from lsst.meas.base import ForcedMeasurementTask
16 
17 
18 class DynamicDetectionConfig(SourceDetectionConfig):
19  """Configuration for DynamicDetectionTask"""
20  prelimThresholdFactor = Field(dtype=float, default=0.5,
21  doc="Fraction of the threshold to use for first pass (to find sky objects)")
22  skyObjects = ConfigurableField(target=SkyObjectsTask, doc="Generate sky objects")
23 
24 
25 class DynamicDetectionTask(SourceDetectionTask):
26  """Detection of sources on an image with a dynamic threshold
27 
28  We first detect sources using a lower threshold than normal (see config
29  parameter ``prelimThresholdFactor``) in order to identify good sky regions
30  (configurable ``skyObjects``). Then we perform forced PSF photometry on
31  those sky regions. Using those PSF flux measurements and estimated errors,
32  we set the threshold so that the stdev of the measurements matches the
33  median estimated error.
34  """
35  ConfigClass = DynamicDetectionConfig
36  _DefaultName = "dynamicDetection"
37 
38  def __init__(self, *args, **kwargs):
39  """Constructor
40 
41  Besides the usual initialisation of configurables, we also set up
42  the forced measurement which is deliberately not represented in
43  this Task's configuration parameters because we're using it as part
44  of the algorithm and we don't want to allow it to be modified.
45  """
46  SourceDetectionTask.__init__(self, *args, **kwargs)
47  self.makeSubtask("skyObjects")
48 
49  # Set up forced measurement.
50  config = ForcedMeasurementTask.ConfigClass()
51  config.plugins.names = ['base_TransformedCentroid', 'base_PsfFlux']
52  # We'll need the "centroid" and "psfFlux" slots
53  for slot in ("shape", "psfShape", "apFlux", "modelFlux", "instFlux", "calibFlux"):
54  setattr(config.slots, slot, None)
55  config.copyColumns = {}
56  self.skySchema = SourceTable.makeMinimalSchema()
57  self.skyMeasurement = ForcedMeasurementTask(config=config, name="skyMeasurement", parentTask=self,
58  refSchema=self.skySchema)
59 
60  def calculateThreshold(self, exposure, seed, sigma=None):
61  """Calculate new threshold
62 
63  This is the main functional addition to the vanilla
64  `SourceDetectionTask`.
65 
66  We identify sky objects and perform forced PSF photometry on
67  them. Using those PSF flux measurements and estimated errors,
68  we set the threshold so that the stdev of the measurements
69  matches the median estimated error.
70 
71  Parameters
72  ----------
73  exposure : `lsst.afw.image.Exposure`
74  Exposure on which we're detecting sources.
75  seed : `int`
76  RNG seed to use for finding sky objects.
77  sigma : `float`, optional
78  Gaussian sigma of smoothing kernel; if not provided,
79  will be deduced from the exposure's PSF.
80 
81  Returns
82  -------
83  factor : `float`
84  Multiplication factor to be applied to the configured detection
85  threshold.
86  """
87  # Make a catalog of sky objects
88  fp = self.skyObjects.run(exposure.maskedImage.mask, seed)
89  skyFootprints = FootprintSet(exposure.getBBox())
90  skyFootprints.setFootprints(fp)
91  table = SourceTable.make(self.skyMeasurement.schema)
92  catalog = SourceCatalog(table)
93  table.preallocate(len(skyFootprints.getFootprints()))
94  skyFootprints.makeSources(catalog)
95  key = catalog.getCentroidKey()
96  for source in catalog:
97  peaks = source.getFootprint().getPeaks()
98  assert len(peaks) == 1
99  source.set(key, peaks[0].getF())
100  source.updateCoord(exposure.getWcs())
101 
102  # Forced photometry on sky objects
103  self.skyMeasurement.run(catalog, exposure, catalog, exposure.getWcs())
104 
105  # Calculate new threshold
106  fluxes = catalog["base_PsfFlux_flux"]
107  lq, uq = np.percentile(fluxes, [25.0, 75.0])
108  stdev = 0.741*(uq - lq)
109  errors = catalog["base_PsfFlux_fluxSigma"]
110  median = np.median(errors)
111  return median/stdev
112 
113  def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None):
114  """Detect footprints with a dynamic threshold
115 
116  This varies from the vanilla ``detectFootprints`` method because we
117  do detection twice: one with a low threshold so that we can find
118  sky uncontaminated by objects, then one more with the new calculated
119  threshold.
120 
121  Parameters
122  ----------
123  exposure : `lsst.afw.image.Exposure`
124  Exposure to process; DETECTED{,_NEGATIVE} mask plane will be
125  set in-place.
126  doSmooth : `bool`, optional
127  If True, smooth the image before detection using a Gaussian
128  of width ``sigma``.
129  sigma : `float`, optional
130  Gaussian Sigma of PSF (pixels); used for smoothing and to grow
131  detections; if `None` then measure the sigma of the PSF of the
132  ``exposure``.
133  clearMask : `bool`, optional
134  Clear both DETECTED and DETECTED_NEGATIVE planes before running
135  detection.
136  expId : `int`, optional
137  Exposure identifier, used as a seed for the random number
138  generator. If absent, the seed will be the sum of the image.
139 
140  Return Struct contents
141  ----------------------
142  positive : `lsst.afw.detection.FootprintSet`
143  Positive polarity footprints (may be `None`)
144  negative : `lsst.afw.detection.FootprintSet`
145  Negative polarity footprints (may be `None`)
146  numPos : `int`
147  Number of footprints in positive or 0 if detection polarity was
148  negative.
149  numNeg : `int`
150  Number of footprints in negative or 0 if detection polarity was
151  positive.
152  background : `lsst.afw.math.BackgroundMI`
153  Re-estimated background. `None` if
154  ``reEstimateBackground==False``.
155  factor : `float`
156  Multiplication factor applied to the configured detection
157  threshold.
158  """
159  maskedImage = exposure.maskedImage
160 
161  if clearMask:
162  self.clearMask(maskedImage.mask)
163  else:
164  oldDetected = maskedImage.mask.array & maskedImage.mask.getPlaneBitMask(["DETECTED",
165  "DETECTED_NEGATIVE"])
166 
167  # Could potentially smooth with a wider kernel than the PSF in order to better pick up the
168  # wings of stars and galaxies, but for now sticking with the PSF as that's more simple.
169  psf = self.getPsf(exposure, sigma=sigma)
170  convolveResults = self.convolveImage(maskedImage, psf, doSmooth=doSmooth)
171  middle = convolveResults.middle
172  sigma = convolveResults.sigma
173 
174  prelim = self.applyThreshold(middle, maskedImage.getBBox(), self.config.prelimThresholdFactor)
175  self.finalizeFootprints(maskedImage.mask, prelim, sigma, self.config.prelimThresholdFactor)
176 
177  # Calculate the proper threshold
178  # seed needs to fit in a C++ 'int' so pybind doesn't choke on it
179  seed = (expId if expId is not None else int(maskedImage.image.array.sum())) % (2**31 - 1)
180  factor = self.calculateThreshold(exposure, seed, sigma=sigma)
181  self.log.info("Modifying configured detection threshold by factor %f to %f",
182  factor, factor*self.config.thresholdValue)
183 
184  # Blow away preliminary (low threshold) detection mask
185  self.clearMask(maskedImage.mask)
186  if not clearMask:
187  maskedImage.mask.array |= oldDetected
188 
189  # Rinse and repeat thresholding with new calculated threshold
190  results = self.applyThreshold(middle, maskedImage.getBBox(), factor)
191  results.prelim = prelim
192  if self.config.doTempLocalBackground:
193  self.applyTempLocalBackground(exposure, middle, results)
194  self.finalizeFootprints(maskedImage.mask, results, sigma, factor)
195 
196  if self.config.reEstimateBackground:
197  self.reEstimateBackground(maskedImage, prelim)
198 
199  self.clearUnwantedResults(maskedImage.mask, results)
200  self.display(exposure, results, middle)
201 
202  return results
def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None)
def calculateThreshold(self, exposure, seed, sigma=None)