Coverage for python/lsst/meas/base/sfm.py : 27%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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.pipe.base as pipeBase
36from .pluginRegistry import PluginRegistry
37from .baseMeasurement import (BaseMeasurementPluginConfig, BaseMeasurementPlugin,
38 BaseMeasurementConfig, BaseMeasurementTask)
39from .noiseReplacer import NoiseReplacer, DummyNoiseReplacer
41__all__ = ("SingleFramePluginConfig", "SingleFramePlugin",
42 "SingleFrameMeasurementConfig", "SingleFrameMeasurementTask")
45class SingleFramePluginConfig(BaseMeasurementPluginConfig):
46 """Base class for single-frame plugin configuration classes.
47 """
48 pass
51class SingleFramePlugin(BaseMeasurementPlugin):
52 """Base class for single-frame measurement plugin.
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.
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 """
76 registry = PluginRegistry(SingleFramePluginConfig)
77 """Registry of subclasses of `SingleFramePlugin` (`PluginRegistry`).
78 """
80 ConfigClass = SingleFramePluginConfig
82 def __init__(self, config, name, schema, metadata, logName=None, **kwds):
83 BaseMeasurementPlugin.__init__(self, config, name, logName=logName)
85 def measure(self, measRecord, exposure):
86 """Measure the properties of a source on a single image.
88 The image may be from a single epoch, or it may be a coadd.
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()
103 def measureN(self, measCat, exposure):
104 """Measure the properties of blended sources on a single image.
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.
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.
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()
131class SingleFrameMeasurementConfig(BaseMeasurementConfig):
132 """Config class for single frame measurement driver task.
133 """
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") 151 ↛ exitline 151 didn't run the lambda on line 151
152 undeblended = SingleFramePlugin.registry.makeField(
153 multi=True,
154 default=[],
155 doc="Plugins to run on undeblended image"
156 )
159class SingleFrameMeasurementTask(BaseMeasurementTask):
160 """A subtask for measuring the properties of sources on a single exposure.
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 """
175 ConfigClass = SingleFrameMeasurementConfig
177 NOISE_SEED_MULTIPLIER = "NOISE_SEED_MULTIPLIER"
178 """Name by which the noise seed multiplier is recorded in metadata ('str').
179 """
181 NOISE_SOURCE = "NOISE_SOURCE"
182 """Name by which the noise source is recorded in metadata ('str').
183 """
185 NOISE_OFFSET = "NOISE_OFFSET"
186 """Name by which the noise offset is recorded in metadata ('str').
187 """
189 NOISE_EXPOSURE_ID = "NOISE_EXPOSURE_ID"
190 """Name by which the noise exposire ID is recorded in metadata ('str').
191 """
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)
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
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.
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}
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
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()
258 self.runPlugins(noiseReplacer, measCat, exposure, beginOrder, endOrder)
260 def runPlugins(self, noiseReplacer, measCat, exposure, beginOrder=None, endOrder=None):
261 r"""Call the configured measument plugins on an image.
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)
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"))
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)
305 if self.doBlendedness:
306 self.blendPlugin.cpp.measureChildPixels(exposure.getMaskedImage(), measChildRecord)
308 noiseReplacer.removeSource(measChildRecord.getId())
310 # Then insert the parent footprint, and measure that
311 noiseReplacer.insertSource(measParentRecord.getId())
312 self.callMeasure(measParentRecord, exposure, beginOrder=beginOrder, endOrder=endOrder)
314 if self.doBlendedness:
315 self.blendPlugin.cpp.measureChildPixels(exposure.getMaskedImage(), measParentRecord)
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())
323 # When done, restore the exposure to its original state
324 noiseReplacer.end()
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)
337 def measure(self, measCat, exposure):
338 """Backwards-compatibility alias for `run`.
339 """
340 self.run(measCat, exposure)