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