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