Hide keyboard shortcuts

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/>. 

21 

22r"""Base classes for single-frame measurement plugins and the associated task. 

23 

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""" 

33 

34import lsst.pipe.base as pipeBase 

35 

36from .pluginRegistry import PluginRegistry 

37from .baseMeasurement import (BaseMeasurementPluginConfig, BaseMeasurementPlugin, 

38 BaseMeasurementConfig, BaseMeasurementTask) 

39from .noiseReplacer import NoiseReplacer, DummyNoiseReplacer 

40 

41__all__ = ("SingleFramePluginConfig", "SingleFramePlugin", 

42 "SingleFrameMeasurementConfig", "SingleFrameMeasurementTask") 

43 

44 

45class SingleFramePluginConfig(BaseMeasurementPluginConfig): 

46 """Base class for single-frame plugin configuration classes. 

47 """ 

48 pass 

49 

50 

51class SingleFramePlugin(BaseMeasurementPlugin): 

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 

131class SingleFrameMeasurementConfig(BaseMeasurementConfig): 

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") 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 ) 

157 

158 

159class SingleFrameMeasurementTask(BaseMeasurementTask): 

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)