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

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"""Collection of small images (stamps), each centered on a bright star. 

23""" 

24 

25__all__ = ["BrightStarStamp", "BrightStarStamps"] 

26 

27from dataclasses import dataclass 

28from operator import ior 

29from functools import reduce 

30from typing import Optional 

31 

32from lsst.afw.image import MaskedImageF 

33from lsst.afw import geom as afwGeom 

34from lsst.afw import math as afwMath 

35from .stamps import StampsBase, AbstractStamp, readFitsWithOptions 

36 

37 

38@dataclass 

39class BrightStarStamp(AbstractStamp): 

40 """Single stamp centered on a bright star, normalized by its 

41 annularFlux. 

42 

43 Parameters 

44 ---------- 

45 stamp_im : `lsst.afw.image.MaskedImage` 

46 Pixel data for this postage stamp 

47 gaiaGMag : `float` 

48 Gaia G magnitude for the object in this stamp 

49 gaiaId : `int` 

50 Gaia object identifier 

51 annularFlux : `Optional[float]` 

52 Flux in an annulus around the object 

53 """ 

54 stamp_im: MaskedImageF 

55 gaiaGMag: float 

56 gaiaId: int 

57 annularFlux: Optional[float] = None 

58 

59 @classmethod 

60 def factory(cls, stamp_im, metadata, idx): 

61 """This method is needed to service the FITS reader. 

62 We need a standard interface to construct objects like this. 

63 Parameters needed to construct this object are passed in via 

64 a metadata dictionary and then passed to the constructor of 

65 this class. This particular factory method requires keys: 

66 G_MAGS, GAIA_IDS, and ANNULAR_FLUXES. They should each 

67 point to lists of values. 

68 

69 Parameters 

70 ---------- 

71 stamp_im : `lsst.afw.image.MaskedImage` 

72 Pixel data to pass to the constructor 

73 metadata : `dict` 

74 Dictionary containing the information 

75 needed by the constructor. 

76 idx : `int` 

77 Index into the lists in ``metadata`` 

78 

79 Returns 

80 ------- 

81 brightstarstamp : `BrightStarStamp` 

82 An instance of this class 

83 """ 

84 return cls(stamp_im=stamp_im, 

85 gaiaGMag=metadata.getArray('G_MAGS')[idx], 

86 gaiaId=metadata.getArray('GAIA_IDS')[idx], 

87 annularFlux=metadata.getArray('ANNULAR_FLUXES')[idx]) 

88 

89 def measureAndNormalize(self, annulus, statsControl=afwMath.StatisticsControl(), 

90 statsFlag=afwMath.stringToStatisticsProperty("MEAN"), 

91 badMaskPlanes=('BAD', 'SAT', 'NO_DATA')): 

92 """Compute "annularFlux", the integrated flux within an annulus 

93 around an object's center, and normalize it. 

94 

95 Since the center of bright stars are saturated and/or heavily affected 

96 by ghosts, we measure their flux in an annulus with a large enough 

97 inner radius to avoid the most severe ghosts and contain enough 

98 non-saturated pixels. 

99 

100 Parameters 

101 ---------- 

102 annulus : `lsst.afw.geom.spanSet.SpanSet` 

103 SpanSet containing the annulus to use for normalization. 

104 statsControl : `lsst.afw.math.statistics.StatisticsControl`, optional 

105 StatisticsControl to be used when computing flux over all pixels 

106 within the annulus. 

107 statsFlag : `lsst.afw.math.statistics.Property`, optional 

108 statsFlag to be passed on to ``afwMath.makeStatistics`` to compute 

109 annularFlux. Defaults to a simple MEAN. 

110 badMaskPlanes : `collections.abc.Collection` [`str`] 

111 Collection of mask planes to ignore when computing annularFlux. 

112 """ 

113 stampSize = self.stamp_im.getDimensions() 

114 # create image with the same pixel values within annulus, NO_DATA 

115 # elsewhere 

116 maskPlaneDict = self.stamp_im.mask.getMaskPlaneDict() 

117 annulusImage = MaskedImageF(stampSize, planeDict=maskPlaneDict) 

118 annulusMask = annulusImage.mask 

119 annulusMask.array[:] = 2**maskPlaneDict['NO_DATA'] 

120 annulus.copyMaskedImage(self.stamp_im, annulusImage) 

121 # set mask planes to be ignored 

122 andMask = reduce(ior, (annulusMask.getPlaneBitMask(bm) for bm in badMaskPlanes)) 

123 statsControl.setAndMask(andMask) 

124 # compute annularFlux 

125 annulusStat = afwMath.makeStatistics(annulusImage, statsFlag, statsControl) 

126 self.annularFlux = annulusStat.getValue() 

127 # normalize stamps 

128 self.stamp_im.image.array /= self.annularFlux 

129 return None 

130 

131 

132class BrightStarStamps(StampsBase): 

133 """Collection of bright star stamps and associated metadata. 

134 

135 Parameters 

136 ---------- 

137 starStamps : `collections.abc.Sequence` [`BrightStarStamp`] 

138 Sequence of star stamps. Cannot contain both normalized and 

139 unnormalized stamps. 

140 innerRadius : `int`, optional 

141 Inner radius value, in pixels. This and ``outerRadius`` define the 

142 annulus used to compute the ``"annularFlux"`` values within each 

143 ``starStamp``. Must be provided if ``normalize`` is True. 

144 outerRadius : `int`, optional 

145 Outer radius value, in pixels. This and ``innerRadius`` define the 

146 annulus used to compute the ``"annularFlux"`` values within each 

147 ``starStamp``. Must be provided if ``normalize`` is True. 

148 metadata : `lsst.daf.base.PropertyList`, optional 

149 Metadata associated with the bright stars. 

150 use_mask : `bool` 

151 If `True` read and write mask data. Default `True`. 

152 use_variance : `bool` 

153 If ``True`` read and write variance data. Default ``False``. 

154 

155 Raises 

156 ------ 

157 ValueError 

158 Raised if one of the star stamps provided does not contain the 

159 required keys. 

160 AttributeError 

161 Raised if there is a mix-and-match of normalized and unnormalized 

162 stamps, stamps normalized with different annulus definitions, or if 

163 stamps are to be normalized but annular radii were not provided. 

164 

165 

166 Notes 

167 ----- 

168 A butler can be used to read only a part of the stamps, specified by a 

169 bbox: 

170 

171 >>> starSubregions = butler.get("brightStarStamps_sub", dataId, bbox=bbox) 

172 """ 

173 

174 def __init__(self, starStamps, innerRadius=None, outerRadius=None, 

175 metadata=None, use_mask=True, use_variance=False): 

176 super().__init__(starStamps, metadata, use_mask, use_variance) 

177 # Ensure stamps contain a flux measurement if and only if they are 

178 # already expected to be normalized 

179 self._checkNormalization(False, innerRadius, outerRadius) 

180 self._innerRadius, self._outerRadius = innerRadius, outerRadius 

181 if innerRadius is not None and outerRadius is not None: 

182 self.normalized = True 

183 else: 

184 self.normalized = False 

185 

186 @classmethod 

187 def initAndNormalize(cls, starStamps, innerRadius, outerRadius, 

188 metadata=None, use_mask=True, use_variance=False, 

189 imCenter=None, 

190 statsControl=afwMath.StatisticsControl(), 

191 statsFlag=afwMath.stringToStatisticsProperty("MEAN"), 

192 badMaskPlanes=('BAD', 'SAT', 'NO_DATA')): 

193 """Normalize a set of bright star stamps and initialize a 

194 BrightStarStamps instance. 

195 

196 Since the center of bright stars are saturated and/or heavily affected 

197 by ghosts, we measure their flux in an annulus with a large enough 

198 inner radius to avoid the most severe ghosts and contain enough 

199 non-saturated pixels. 

200 

201 Parameters 

202 ---------- 

203 starStamps : `collections.abc.Sequence` [`BrightStarStamp`] 

204 Sequence of star stamps. Cannot contain both normalized and 

205 unnormalized stamps. 

206 innerRadius : `int` 

207 Inner radius value, in pixels. This and ``outerRadius`` define the 

208 annulus used to compute the ``"annularFlux"`` values within each 

209 ``starStamp``. 

210 outerRadius : `int` 

211 Outer radius value, in pixels. This and ``innerRadius`` define the 

212 annulus used to compute the ``"annularFlux"`` values within each 

213 ``starStamp``. 

214 metadata : `lsst.daf.base.PropertyList`, optional 

215 Metadata associated with the bright stars. 

216 use_mask : `bool` 

217 If `True` read and write mask data. Default `True`. 

218 use_variance : `bool` 

219 If ``True`` read and write variance data. Default ``False``. 

220 imCenter : `collections.abc.Sequence`, optional 

221 Center of the object, in pixels. If not provided, the center of the 

222 first stamp's pixel grid will be used. 

223 statsControl : `lsst.afw.math.statistics.StatisticsControl`, optional 

224 StatisticsControl to be used when computing flux over all pixels 

225 within the annulus. 

226 statsFlag : `lsst.afw.math.statistics.Property`, optional 

227 statsFlag to be passed on to ``afwMath.makeStatistics`` to compute 

228 annularFlux. Defaults to a simple MEAN. 

229 badMaskPlanes : `collections.abc.Collection` [`str`] 

230 Collection of mask planes to ignore when computing annularFlux. 

231 

232 Raises 

233 ------ 

234 ValueError 

235 Raised if one of the star stamps provided does not contain the 

236 required keys. 

237 AttributeError 

238 Raised if there is a mix-and-match of normalized and unnormalized 

239 stamps, stamps normalized with different annulus definitions, or if 

240 stamps are to be normalized but annular radii were not provided. 

241 """ 

242 if imCenter is None: 

243 stampSize = starStamps[0].stamp_im.getDimensions() 

244 imCenter = stampSize[0]//2, stampSize[1]//2 

245 # Create SpanSet of annulus 

246 outerCircle = afwGeom.SpanSet.fromShape(outerRadius, afwGeom.Stencil.CIRCLE, offset=imCenter) 

247 innerCircle = afwGeom.SpanSet.fromShape(innerRadius, afwGeom.Stencil.CIRCLE, offset=imCenter) 

248 annulus = outerCircle.intersectNot(innerCircle) 

249 # Initialize (unnormalized) brightStarStamps instance 

250 bss = cls(starStamps, innerRadius=None, outerRadius=None, 

251 metadata=metadata, use_mask=use_mask, 

252 use_variance=use_variance) 

253 # Ensure no stamps had already been normalized 

254 bss._checkNormalization(True, innerRadius, outerRadius) 

255 bss._innerRadius, bss._outerRadius = innerRadius, outerRadius 

256 # Apply normalization 

257 for stamp in bss._stamps: 

258 stamp.measureAndNormalize(annulus, statsControl=statsControl, statsFlag=statsFlag, 

259 badMaskPlanes=badMaskPlanes) 

260 bss.normalized = True 

261 return bss 

262 

263 def _refresh_metadata(self): 

264 """Refresh the metadata. Should be called before writing this object 

265 out. 

266 """ 

267 # add full list of Gaia magnitudes, IDs and annularFlxes to shared 

268 # metadata 

269 self._metadata["G_MAGS"] = self.getMagnitudes() 

270 self._metadata["GAIA_IDS"] = self.getGaiaIds() 

271 self._metadata["ANNULAR_FLUXES"] = self.getAnnularFluxes() 

272 self._metadata["NORMALIZED"] = self.normalized 

273 self._metadata["INNER_RADIUS"] = self._innerRadius 

274 self._metadata["OUTER_RADIUS"] = self._outerRadius 

275 return None 

276 

277 @classmethod 

278 def readFits(cls, filename): 

279 """Build an instance of this class from a file. 

280 

281 Parameters 

282 ---------- 

283 filename : `str` 

284 Name of the file to read 

285 """ 

286 return cls.readFitsWithOptions(filename, None) 

287 

288 @classmethod 

289 def readFitsWithOptions(cls, filename, options): 

290 """Build an instance of this class with options. 

291 

292 Parameters 

293 ---------- 

294 filename : `str` 

295 Name of the file to read 

296 options : `PropertyList` 

297 Collection of metadata parameters 

298 """ 

299 stamps, metadata = readFitsWithOptions(filename, BrightStarStamp.factory, options) 

300 if metadata["NORMALIZED"]: 

301 return cls(stamps, 

302 innerRadius=metadata["INNER_RADIUS"], outerRadius=metadata["OUTER_RADIUS"], 

303 metadata=metadata, use_mask=metadata['HAS_MASK'], 

304 use_variance=metadata['HAS_VARIANCE']) 

305 else: 

306 return cls(stamps, metadata=metadata, use_mask=metadata['HAS_MASK'], 

307 use_variance=metadata['HAS_VARIANCE']) 

308 

309 def append(self, item, innerRadius=None, outerRadius=None): 

310 """Add an additional bright star stamp. 

311 

312 Parameters 

313 ---------- 

314 item : `BrightStarStamp` 

315 Bright star stamp to append. 

316 innerRadius : `int`, optional 

317 Inner radius value, in pixels. This and ``outerRadius`` define the 

318 annulus used to compute the ``"annularFlux"`` values within each 

319 ``BrightStarStamp``. 

320 outerRadius : `int`, optional 

321 Outer radius value, in pixels. This and ``innerRadius`` define the 

322 annulus used to compute the ``"annularFlux"`` values within each 

323 ``BrightStarStamp``. 

324 """ 

325 if not isinstance(item, BrightStarStamp): 

326 raise ValueError(f"Can only add instances of BrightStarStamp, got {type(item)}.") 

327 if (item.annularFlux is None) == self.normalized: 

328 raise AttributeError("Trying to append an unnormalized stamp to a normalized BrightStarStamps " 

329 "instance, or vice-versa.") 

330 else: 

331 self._checkRadius(innerRadius, outerRadius) 

332 self._stamps.append(item) 

333 return None 

334 

335 def extend(self, bss): 

336 """Extend BrightStarStamps instance by appending elements from another 

337 instance. 

338 

339 Parameters 

340 ---------- 

341 bss : `BrightStarStamps` 

342 Other instance to concatenate. 

343 """ 

344 if not isinstance(bss, BrightStarStamps): 

345 raise ValueError('Can only extend with a BrightStarStamps object. ' 

346 f'Got {type(bss)}.') 

347 self._checkRadius(bss._innerRadius, bss._outerRadius) 

348 self._stamps += bss._stamps 

349 

350 def getMagnitudes(self): 

351 """Retrieve Gaia G magnitudes for each star. 

352 

353 Returns 

354 ------- 

355 gaiaGMags : `list` [`float`] 

356 """ 

357 return [stamp.gaiaGMag for stamp in self._stamps] 

358 

359 def getGaiaIds(self): 

360 """Retrieve Gaia IDs for each star. 

361 

362 Returns 

363 ------- 

364 gaiaIds : `list` [`int`] 

365 """ 

366 return [stamp.gaiaId for stamp in self._stamps] 

367 

368 def getAnnularFluxes(self): 

369 """Retrieve normalization factors for each star. 

370 

371 These are computed by integrating the flux in annulus centered on the 

372 bright star, far enough from center to be beyond most severe ghosts and 

373 saturation. The inner and outer radii that define the annulus can be 

374 recovered from the metadata. 

375 

376 Returns 

377 ------- 

378 annularFluxes : `list` [`float`] 

379 """ 

380 return [stamp.annularFlux for stamp in self._stamps] 

381 

382 def selectByMag(self, magMin=None, magMax=None): 

383 """Return the subset of bright star stamps for objects with specified 

384 magnitude cuts (in Gaia G). 

385 

386 Parameters 

387 ---------- 

388 magMin : `float`, optional 

389 Keep only stars fainter than this value. 

390 magMax : `float`, optional 

391 Keep only stars brighter than this value. 

392 """ 

393 subset = [stamp for stamp in self._stamps 

394 if (magMin is None or stamp.gaiaGMag > magMin) 

395 and (magMax is None or stamp.gaiaGMag < magMax)] 

396 # This is an optimization to save looping over the init argument when 

397 # it is already guaranteed to be the correct type 

398 instance = BrightStarStamps((), 

399 innerRadius=self._innerRadius, outerRadius=self._outerRadius, 

400 metadata=self._metadata) 

401 instance._stamps = subset 

402 return instance 

403 

404 def _checkRadius(self, innerRadius, outerRadius): 

405 """Ensure provided annulus radius is consistent with that already 

406 present in the instance, or with arguments passed on at initialization. 

407 """ 

408 if innerRadius != self._innerRadius or outerRadius != self._outerRadius: 

409 raise AttributeError("Trying to mix stamps normalized with annulus radii " 

410 f"{innerRadius, outerRadius} with those of BrightStarStamp instance\n" 

411 f"(computed with annular radii {self._innerRadius, self._outerRadius}).") 

412 

413 def _checkNormalization(self, normalize, innerRadius, outerRadius): 

414 """Ensure there is no mixing of normalized and unnormalized stars, and 

415 that, if requested, normalization can be performed. 

416 """ 

417 noneFluxCount = self.getAnnularFluxes().count(None) 

418 nStamps = len(self) 

419 nFluxVals = nStamps - noneFluxCount 

420 if noneFluxCount and noneFluxCount < nStamps: 

421 # at least one stamp contains an annularFlux value (i.e. has been 

422 # normalized), but not all of them do 

423 raise AttributeError(f"Only {nFluxVals} stamps contain an annularFlux value.\nAll stamps in a " 

424 "BrightStarStamps instance must either be normalized with the same annulus " 

425 "definition, or none of them can contain an annularFlux value.") 

426 elif normalize: 

427 # stamps are to be normalized; ensure annular radii are specified 

428 # and they have no annularFlux 

429 if innerRadius is None or outerRadius is None: 

430 raise AttributeError("For stamps to be normalized (normalize=True), please provide a valid " 

431 "value (in pixels) for both innerRadius and outerRadius.") 

432 elif noneFluxCount < nStamps: 

433 raise AttributeError(f"{nFluxVals} stamps already contain an annularFlux value. For stamps to" 

434 " be normalized, all their annularFlux must be None.") 

435 elif innerRadius is not None and outerRadius is not None: 

436 # Radii provided, but normalize=False; check that stamps 

437 # already contain annularFluxes 

438 if noneFluxCount: 

439 raise AttributeError(f"{noneFluxCount} stamps contain no annularFlux, but annular radius " 

440 "values were provided and normalize=False.\nTo normalize stamps, set " 

441 "normalize to True.") 

442 else: 

443 # At least one radius value is missing; ensure no stamps have 

444 # already been normalized 

445 if nFluxVals: 

446 raise AttributeError(f"{nFluxVals} stamps contain an annularFlux value. If stamps have " 

447 "been normalized, the innerRadius and outerRadius values used must " 

448 "be provided.") 

449 return None