LSSTApplications  18.1.0
LSSTDataManagementBasePackage
sfm.py
Go to the documentation of this file.
1 # This file is part of meas_base.
2 #
3 # Developed for the LSST Data Management System.
4 # This product includes software developed by the LSST Project
5 # (https://www.lsst.org).
6 # See the COPYRIGHT file at the top-level directory of this distribution
7 # for details of code ownership.
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 GNU General Public License
20 # along with this program. If not, see <https://www.gnu.org/licenses/>.
21 
22 r"""Base classes for single-frame measurement plugins and the associated task.
23 
24 In single-frame measurement, we assume that detection and probably deblending
25 have already been run on the same frame, so a `~lsst.afw.table.SourceCatalog`
26 has already been created with `lsst.afw.detection.Footprint`\ s (which may be
27 "heavy" — that is, include pixel data). Measurements are generally recorded in
28 the coordinate system of the image being measured (and all slot-eligible
29 fields must be), but non-slot fields may be recorded in other coordinate
30 systems if necessary to avoid information loss (this should, of course, be
31 indicated in the field documentation).
32 """
33 
34 import lsst.pipe.base as pipeBase
35 
36 from .pluginRegistry import PluginRegistry
37 from .baseMeasurement import (BaseMeasurementPluginConfig, BaseMeasurementPlugin,
38  BaseMeasurementConfig, BaseMeasurementTask)
39 from .noiseReplacer import NoiseReplacer, DummyNoiseReplacer
40 
41 __all__ = ("SingleFramePluginConfig", "SingleFramePlugin",
42  "SingleFrameMeasurementConfig", "SingleFrameMeasurementTask")
43 
44 
46  """Base class for single-frame plugin configuration classes.
47  """
48  pass
49 
50 
52  """Base class for single-frame measurement plugin.
53 
54  Parameters
55  ----------
56  config : `SingleFramePlugin.ConfigClass`
57  Configuration for this plugin.
58  name : `str`
59  The string with which the plugin was registered.
60  schema : `lsst.afw.table.Schema`
61  The schema for the source table . New fields are added here to
62  hold measurements produced by this plugin.
63  metadata : `lsst.daf.base.PropertySet`
64  Plugin metadata that will be attached to the output catalog
65  logName : `str`, optional
66  Name to use when logging errors.
67 
68  Notes
69  -----
70  New plugins can be created in Python by inheriting directly from this
71  class and implementing the `measure`, `fail` (from `BasePlugin`), and
72  optionally `__init__` and `measureN` methods. Plugins can also be defined
73  in C++ via the `WrappedSingleFramePlugin` class.
74  """
75 
76  registry = PluginRegistry(SingleFramePluginConfig)
77  """Registry of subclasses of `SingleFramePlugin` (`PluginRegistry`).
78  """
79 
80  ConfigClass = SingleFramePluginConfig
81 
82  def __init__(self, config, name, schema, metadata, logName=None, **kwds):
83  BaseMeasurementPlugin.__init__(self, config, name, logName=logName)
84 
85  def measure(self, measRecord, exposure):
86  """Measure the properties of a source on a single image.
87 
88  The image may be from a single epoch, or it may be a coadd.
89 
90  Parameters
91  ----------
92  measRecord : `lsst.afw.table.SourceRecord`
93  Record describing the object being measured. Previously-measured
94  quantities may be retrieved from here, and it will be updated
95  in-place tih the outputs of this plugin.
96  exposure : `lsst.afw.image.ExposureF`
97  The pixel data to be measured, together with the associated PSF,
98  WCS, etc. All other sources in the image should have been replaced
99  by noise according to deblender outputs.
100  """
101  raise NotImplementedError()
102 
103  def measureN(self, measCat, exposure):
104  """Measure the properties of blended sources on a single image.
105 
106  This operates on all members of a blend family at once. The image may
107  be from a single epoch, or it may be a coadd.
108 
109  Parameters
110  ----------
111  measCat : `lsst.afw.table.SourceCatalog`
112  Catalog describing the objects (and only those objects) being
113  measured. Previously-measured quantities will be retrieved from
114  here, and it will be updated in-place with the outputs of this
115  plugin.
116  exposure : `lsst.afw.image.ExposureF`
117  The pixel data to be measured, together with the associated PSF,
118  WCS, etc. All other sources in the image should have been replaced
119  by noise according to deblender outputs.
120 
121  Notes
122  -----
123  Derived classes that do not implement ``measureN`` should just inherit
124  this disabled version. Derived classes that do implement ``measureN``
125  should additionally add a bool doMeasureN config field to their config
126  class to signal that measureN-mode is available.
127  """
128  raise NotImplementedError()
129 
130 
132  """Config class for single frame measurement driver task.
133  """
134 
135  plugins = SingleFramePlugin.registry.makeField(
136  multi=True,
137  default=["base_PixelFlags",
138  "base_SdssCentroid",
139  "base_NaiveCentroid",
140  "base_SdssShape",
141  "base_GaussianFlux",
142  "base_PsfFlux",
143  "base_CircularApertureFlux",
144  "base_SkyCoord",
145  "base_Variance",
146  "base_Blendedness",
147  "base_LocalBackground",
148  ],
149  doc="Plugins to be run and their configuration"
150  )
151  algorithms = property(lambda self: self.plugins, doc="backwards-compatibility alias for plugins")
152  undeblended = SingleFramePlugin.registry.makeField(
153  multi=True,
154  default=[],
155  doc="Plugins to run on undeblended image"
156  )
157 
158 
160  """A subtask for measuring the properties of sources on a single exposure.
161 
162  Parameters
163  ----------
164  schema : `lsst.afw.table.Schema`
165  Schema of the output resultant catalog. Will be updated to provide
166  fields to accept the outputs of plugins which will be executed by this
167  task.
168  algMetadata : `lsst.daf.base.PropertyList`, optional
169  Used to record metadaa about algorithm execution. An empty
170  `lsst.daf.base.PropertyList` will be created if `None`.
171  **kwds
172  Keyword arguments forwarded to `BaseMeasurementTask`.
173  """
174 
175  ConfigClass = SingleFrameMeasurementConfig
176 
177  NOISE_SEED_MULTIPLIER = "NOISE_SEED_MULTIPLIER"
178  """Name by which the noise seed multiplier is recorded in metadata ('str').
179  """
180 
181  NOISE_SOURCE = "NOISE_SOURCE"
182  """Name by which the noise source is recorded in metadata ('str').
183  """
184 
185  NOISE_OFFSET = "NOISE_OFFSET"
186  """Name by which the noise offset is recorded in metadata ('str').
187  """
188 
189  NOISE_EXPOSURE_ID = "NOISE_EXPOSURE_ID"
190  """Name by which the noise exposire ID is recorded in metadata ('str').
191  """
192 
193  def __init__(self, schema, algMetadata=None, **kwds):
194  super(SingleFrameMeasurementTask, self).__init__(algMetadata=algMetadata, **kwds)
195  self.schema = schema
196  self.config.slots.setupSchema(self.schema)
197  self.initializePlugins(schema=self.schema)
198 
199  # Check to see if blendedness is one of the plugins
200  if 'base_Blendedness' in self.plugins:
201  self.doBlendedness = True
202  self.blendPlugin = self.plugins['base_Blendedness']
203  else:
204  self.doBlendedness = False
205 
206  @pipeBase.timeMethod
207  def run(self, measCat, exposure, noiseImage=None, exposureId=None, beginOrder=None, endOrder=None):
208  r"""Run single frame measurement over an exposure and source catalog.
209 
210  Parameters
211  ----------
212  measCat : `lsst.afw.table.SourceCatalog`
213  Catalog to be filled with the results of measurement. Must contain
214  all the `lsst.afw.table.SourceRecord`\ s to be measured (with
215  `lsst.afw.detection.Footprint`\ s attached), and have a schema
216  that is a superset of ``self.schema``.
217  exposure : `lsst.afw.image.ExposureF`
218  Image containing the pixel data to be measured together with
219  associated PSF, WCS, etc.
220  noiseImage : `lsst.afw.image.ImageF`, optional
221  Can be used to specify the a predictable noise replacement field
222  for testing purposes.
223  exposureId : `int`, optional
224  Unique exposure identifier used to calculate the random number
225  generator seed during noise replacement.
226  beginOrder : `float`, optional
227  Start execution order (inclusive): measurements with
228  ``executionOrder < beginOrder`` are not executed. `None` for no
229  limit.
230  endOrder : `float`, optional
231  Final execution order (exclusive): measurements with
232  ``executionOrder >= endOrder`` are not executed. `None` for no
233  limit.
234  """
235  assert measCat.getSchema().contains(self.schema)
236  footprints = {measRecord.getId(): (measRecord.getParent(), measRecord.getFootprint())
237  for measRecord in measCat}
238 
239  # noiseReplacer is used to fill the footprints with noise and save
240  # heavy footprints of the source pixels so that they can be restored
241  # one at a time for measurement. After the NoiseReplacer is
242  # constructed, all pixels in the exposure.getMaskedImage() which
243  # belong to objects in measCat will be replaced with noise
244 
245  if self.config.doReplaceWithNoise:
246  noiseReplacer = NoiseReplacer(self.config.noiseReplacer, exposure, footprints,
247  noiseImage=noiseImage, log=self.log, exposureId=exposureId)
248  algMetadata = measCat.getMetadata()
249  if algMetadata is not None:
250  algMetadata.addInt(self.NOISE_SEED_MULTIPLIER, self.config.noiseReplacer.noiseSeedMultiplier)
251  algMetadata.addString(self.NOISE_SOURCE, self.config.noiseReplacer.noiseSource)
252  algMetadata.addDouble(self.NOISE_OFFSET, self.config.noiseReplacer.noiseOffset)
253  if exposureId is not None:
254  algMetadata.addLong(self.NOISE_EXPOSURE_ID, exposureId)
255  else:
256  noiseReplacer = DummyNoiseReplacer()
257 
258  self.runPlugins(noiseReplacer, measCat, exposure, beginOrder, endOrder)
259 
260  def runPlugins(self, noiseReplacer, measCat, exposure, beginOrder=None, endOrder=None):
261  r"""Call the configured measument plugins on an image.
262 
263  Parameters
264  ----------
265  noiseReplacer : `NoiseReplacer`
266  Used to fill sources not being measured with noise.
267  measCat : `lsst.afw.table.SourceCatalog`
268  Catalog to be filled with the results of measurement. Must contain
269  all the `lsst.afw.table.SourceRecord`\ s to be measured (with
270  `lsst.afw.detection.Footprint`\ s attached), and have a schema
271  that is a superset of ``self.schema``.
272  exposure : `lsst.afw.image.ExposureF`
273  Image containing the pixel data to be measured together with
274  associated PSF, WCS, etc.
275  beginOrder : `float`, optional
276  Start execution order (inclusive): measurements with
277  ``executionOrder < beginOrder`` are not executed. `None` for no
278  limit.
279  endOrder : `float`, optional
280  Final execution order (exclusive): measurements with
281  ``executionOrder >= endOrder`` are not executed. `None` for no
282  limit.
283  """
284  # First, create a catalog of all parentless sources. Loop through all
285  # the parent sources, first processing the children, then the parent.
286  measParentCat = measCat.getChildren(0)
287 
288  nMeasCat = len(measCat)
289  nMeasParentCat = len(measParentCat)
290  self.log.info("Measuring %d source%s (%d parent%s, %d child%s) ",
291  nMeasCat, ("" if nMeasCat == 1 else "s"),
292  nMeasParentCat, ("" if nMeasParentCat == 1 else "s"),
293  nMeasCat - nMeasParentCat, ("" if nMeasCat - nMeasParentCat == 1 else "ren"))
294 
295  for parentIdx, measParentRecord in enumerate(measParentCat):
296  # first get all the children of this parent, insert footprint in
297  # turn, and measure
298  measChildCat = measCat.getChildren(measParentRecord.getId())
299  # TODO: skip this loop if there are no plugins configured for
300  # single-object mode
301  for measChildRecord in measChildCat:
302  noiseReplacer.insertSource(measChildRecord.getId())
303  self.callMeasure(measChildRecord, exposure, beginOrder=beginOrder, endOrder=endOrder)
304 
305  if self.doBlendedness:
306  self.blendPlugin.cpp.measureChildPixels(exposure.getMaskedImage(), measChildRecord)
307 
308  noiseReplacer.removeSource(measChildRecord.getId())
309 
310  # Then insert the parent footprint, and measure that
311  noiseReplacer.insertSource(measParentRecord.getId())
312  self.callMeasure(measParentRecord, exposure, beginOrder=beginOrder, endOrder=endOrder)
313 
314  if self.doBlendedness:
315  self.blendPlugin.cpp.measureChildPixels(exposure.getMaskedImage(), measParentRecord)
316 
317  # Finally, process both parent and child set through measureN
318  self.callMeasureN(measParentCat[parentIdx:parentIdx+1], exposure,
319  beginOrder=beginOrder, endOrder=endOrder)
320  self.callMeasureN(measChildCat, exposure, beginOrder=beginOrder, endOrder=endOrder)
321  noiseReplacer.removeSource(measParentRecord.getId())
322 
323  # When done, restore the exposure to its original state
324  noiseReplacer.end()
325 
326  # Undeblended plugins only fire if we're running everything
327  if endOrder is None:
328  for source in measCat:
329  for plugin in self.undeblendedPlugins.iter():
330  self.doMeasurement(plugin, source, exposure)
331  # Now we loop over all of the sources one more time to compute the
332  # blendedness metrics
333  if self.doBlendedness:
334  for source in measCat:
335  self.blendPlugin.cpp.measureParentPixels(exposure.getMaskedImage(), source)
336 
337  def measure(self, measCat, exposure):
338  """Backwards-compatibility alias for `run`.
339  """
340  self.run(measCat, exposure)
bool contains(VertexIterator const begin, VertexIterator const end, UnitVector3d const &v)
def measure(self, measRecord, exposure)
Definition: sfm.py:85
def measureN(self, measCat, exposure)
Definition: sfm.py:103
def measure(self, measCat, exposure)
Definition: sfm.py:337
def runPlugins(self, noiseReplacer, measCat, exposure, beginOrder=None, endOrder=None)
Definition: sfm.py:260
def __init__(self, config, name, schema, metadata, logName=None, kwds)
Definition: sfm.py:82
def run(self, measCat, exposure, noiseImage=None, exposureId=None, beginOrder=None, endOrder=None)
Definition: sfm.py:207
def __init__(self, schema, algMetadata=None, kwds)
Definition: sfm.py:193
def doMeasurement(self, plugin, measRecord, args, kwds)