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

23""" 

24 

25__all__ = ["Stamp", "Stamps", "StampsBase", "writeFits", "readFitsWithOptions"] 

26 

27from collections.abc import Sequence 

28import abc 

29from dataclasses import dataclass 

30import numpy 

31 

32import lsst.afw.image as afwImage 

33import lsst.afw.fits as afwFits 

34from lsst.geom import Box2I, Point2I, Extent2I, Angle, degrees, SpherePoint 

35from lsst.daf.base import PropertyList 

36 

37 

38def writeFits(filename, stamp_ims, metadata, write_mask, write_variance): 

39 """Write a single FITS file containing all stamps. 

40 

41 Parameters 

42 ---------- 

43 filename : `str` 

44 A string indicating the output filename 

45 stamps_ims : iterable of `lsst.afw.image.MaskedImageF` 

46 An iterable of masked images 

47 metadata : `PropertyList` 

48 A collection of key, value metadata pairs to be 

49 written to the primary header 

50 write_mask : `bool` 

51 Write the mask data to the output file? 

52 write_variance : `bool` 

53 Write the variance data to the output file? 

54 """ 

55 metadata['HAS_MASK'] = write_mask 

56 metadata['HAS_VARIANCE'] = write_variance 

57 metadata['N_STAMPS'] = len(stamp_ims) 

58 # create primary HDU with global metadata 

59 fitsPrimary = afwFits.Fits(filename, "w") 

60 fitsPrimary.createEmpty() 

61 fitsPrimary.writeMetadata(metadata) 

62 fitsPrimary.closeFile() 

63 

64 # add all pixel data optionally writing mask and variance information 

65 for i, stamp in enumerate(stamp_ims): 

66 metadata = PropertyList() 

67 # EXTVER should be 1-based, the index from enumerate is 0-based 

68 metadata.update({'EXTVER': i+1, 'EXTNAME': 'IMAGE'}) 

69 stamp.getImage().writeFits(filename, metadata=metadata, mode='a') 

70 if write_mask: 

71 metadata = PropertyList() 

72 metadata.update({'EXTVER': i+1, 'EXTNAME': 'MASK'}) 

73 stamp.getMask().writeFits(filename, metadata=metadata, mode='a') 

74 if write_variance: 

75 metadata = PropertyList() 

76 metadata.update({'EXTVER': i+1, 'EXTNAME': 'VARIANCE'}) 

77 stamp.getVariance().writeFits(filename, metadata=metadata, mode='a') 

78 return None 

79 

80 

81def readFitsWithOptions(filename, stamp_factory, options): 

82 """Read stamps from FITS file, allowing for only a 

83 subregion of the stamps to be read. 

84 

85 Parameters 

86 ---------- 

87 filename : `str` 

88 A string indicating the file to read 

89 stamp_factory : classmethod 

90 A factory function defined on a dataclass for constructing 

91 stamp objects a la `lsst.meas.alrogithm.Stamp` 

92 options : `PropertyList` 

93 A collection of parameters. If certain keys are available 

94 (``llcX``, ``llcY``, ``width``, ``height``), a bounding box 

95 is constructed and passed to the ``FitsReader`` in order 

96 to return a sub-image. 

97 

98 Returns 

99 ------- 

100 stamps : `list` of dataclass objects like `Stamp`, PropertyList 

101 A tuple of a list of `Stamp`-like objects 

102 metadata : `PropertyList` 

103 The metadata 

104 """ 

105 # extract necessary info from metadata 

106 metadata = afwFits.readMetadata(filename, hdu=0) 

107 nStamps = metadata["N_STAMPS"] 

108 # check if a bbox was provided 

109 kwargs = {} 

110 if options and options.exists("llcX"): 

111 llcX = options["llcX"] 

112 llcY = options["llcY"] 

113 width = options["width"] 

114 height = options["height"] 

115 bbox = Box2I(Point2I(llcX, llcY), Extent2I(width, height)) 

116 kwargs["bbox"] = bbox 

117 stamp_parts = {} 

118 idx = 1 

119 while len(stamp_parts) < nStamps: 

120 md = afwFits.readMetadata(filename, hdu=idx) 

121 if md['EXTNAME'] in ('IMAGE', 'VARIANCE'): 

122 reader = afwImage.ImageFitsReader(filename, hdu=idx) 

123 elif md['EXTNAME'] == 'MASK': 

124 reader = afwImage.MaskFitsReader(filename, hdu=idx) 

125 else: 

126 raise ValueError(f"Unknown extension type: {md['EXTNAME']}") 

127 stamp_parts.setdefault(md['EXTVER'], {})[md['EXTNAME'].lower()] = reader.read(**kwargs) 

128 idx += 1 

129 # construct stamps themselves 

130 stamps = [] 

131 # Indexing into vectors in a PropertyList has less convenient semantics than 

132 # does dict. 

133 meta_dict = metadata.toDict() 

134 for k in range(nStamps): 

135 # Need to increment by one since EXTVER starts at 1 

136 maskedImage = afwImage.MaskedImageF(**stamp_parts[k+1]) 

137 stamps.append(stamp_factory(maskedImage, meta_dict, k)) 

138 

139 return stamps, metadata 

140 

141 

142@dataclass 

143class AbstractStamp(abc.ABC): 

144 """Single abstract stamp 

145 

146 Parameters 

147 ---------- 

148 Inherit from this class to add metadata to the stamp 

149 """ 

150 

151 @classmethod 

152 @abc.abstractmethod 

153 def factory(cls, stamp_im, metadata, index): 

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

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

156 Parameters needed to construct this object are passed in via 

157 a metadata dictionary and then passed to the constructor of 

158 this class. 

159 

160 Parameters 

161 ---------- 

162 stamp : `lsst.afw.image.MaskedImage` 

163 Pixel data to pass to the constructor 

164 metadata : `dict` 

165 Dictionary containing the information 

166 needed by the constructor. 

167 idx : `int` 

168 Index into the lists in ``metadata`` 

169 

170 Returns 

171 ------- 

172 stamp : `AbstractStamp` 

173 An instance of this class 

174 """ 

175 raise NotImplementedError 

176 

177 

178@dataclass 

179class Stamp(AbstractStamp): 

180 """Single stamp 

181 

182 Parameters 

183 ---------- 

184 stamp_im : `lsst.afw.image.MaskedImageF` 

185 The actual pixel values for the postage stamp 

186 position : `lsst.geom.SpherePoint` 

187 Position of the center of the stamp. Note the user 

188 must keep track of the coordinate system 

189 """ 

190 stamp_im: afwImage.maskedImage.MaskedImageF 

191 position: SpherePoint 

192 

193 @classmethod 

194 def factory(cls, stamp_im, metadata, index): 

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

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

197 Parameters needed to construct this object are passed in via 

198 a metadata dictionary and then passed to the constructor of 

199 this class. If lists of values are passed with the following 

200 keys, they will be passed to the constructor, otherwise dummy 

201 values will be passed: RA_DEG, DEC_DEG. They should 

202 each point to lists of values. 

203 

204 Parameters 

205 ---------- 

206 stamp : `lsst.afw.image.MaskedImage` 

207 Pixel data to pass to the constructor 

208 metadata : `dict` 

209 Dictionary containing the information 

210 needed by the constructor. 

211 idx : `int` 

212 Index into the lists in ``metadata`` 

213 

214 Returns 

215 ------- 

216 stamp : `Stamp` 

217 An instance of this class 

218 """ 

219 if 'RA_DEG' in metadata and 'DEC_DEG' in metadata: 

220 return cls(stamp_im=stamp_im, 

221 position=SpherePoint(Angle(metadata['RA_DEG'][index], degrees), 

222 Angle(metadata['DEC_DEG'][index], degrees))) 

223 else: 

224 return cls(stamp_im=stamp_im, position=SpherePoint(Angle(numpy.nan), Angle(numpy.nan))) 

225 

226 

227class StampsBase(abc.ABC, Sequence): 

228 """Collection of stamps and associated metadata. 

229 

230 Parameters 

231 ---------- 

232 stamps : iterable 

233 This should be an iterable of dataclass objects 

234 a la ``lsst.meas.algorithms.Stamp``. 

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

236 Metadata associated with the bright stars. 

237 use_mask : `bool`, optional 

238 If ``True`` read and write the mask data. Default ``True``. 

239 use_variance : `bool`, optional 

240 If ``True`` read and write the variance data. Default ``True``. 

241 

242 Notes 

243 ----- 

244 A butler can be used to read only a part of the stamps, 

245 specified by a bbox: 

246 

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

248 """ 

249 

250 def __init__(self, stamps, metadata=None, use_mask=True, use_variance=True): 

251 if not hasattr(stamps, '__iter__'): 

252 raise ValueError('The stamps parameter must be iterable.') 

253 for stamp in stamps: 

254 if not isinstance(stamp, AbstractStamp): 

255 raise ValueError('The entries in stamps must inherit from AbstractStamp. ' 

256 f'Got {type(stamp)}.') 

257 self._stamps = stamps 

258 self._metadata = PropertyList() if metadata is None else metadata.deepCopy() 

259 self.use_mask = use_mask 

260 self.use_variance = use_variance 

261 

262 @classmethod 

263 @abc.abstractmethod 

264 def readFits(cls, filename): 

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

266 

267 Parameters 

268 ---------- 

269 filename : `str` 

270 Name of the file to read 

271 """ 

272 raise NotImplementedError 

273 

274 @classmethod 

275 @abc.abstractmethod 

276 def readFitsWithOptions(cls, filename, options): 

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

278 

279 Parameters 

280 ---------- 

281 filename : `str` 

282 Name of the file to read 

283 options : `PropertyList` 

284 Collection of metadata parameters 

285 """ 

286 raise NotImplementedError 

287 

288 @abc.abstractmethod 

289 def _refresh_metadata(self): 

290 """Make sure metadata is up to date since this object 

291 can be extende 

292 """ 

293 raise NotImplementedError 

294 

295 def writeFits(self, filename): 

296 """Write this object to a file. 

297 

298 Parameters 

299 ---------- 

300 filename : `str` 

301 Name of file to write 

302 """ 

303 self._refresh_metadata() 

304 stamps_ims = self.getMaskedImages() 

305 writeFits(filename, stamps_ims, self._metadata, self.use_mask, self.use_variance) 

306 

307 def __len__(self): 

308 return len(self._stamps) 

309 

310 def __getitem__(self, index): 

311 return self._stamps[index] 

312 

313 def __iter__(self): 

314 return iter(self._stamps) 

315 

316 def getMaskedImages(self): 

317 """Retrieve star images. 

318 

319 Returns 

320 ------- 

321 maskedImages : 

322 `list` [`lsst.afw.image.maskedImage.maskedImage.MaskedImageF`] 

323 """ 

324 return [stamp.stamp_im for stamp in self._stamps] 

325 

326 @property 

327 def metadata(self): 

328 return self._metadata 

329 

330 

331class Stamps(StampsBase): 

332 def _refresh_metadata(self): 

333 positions = self.getPositions() 

334 self._metadata['RA_DEG'] = [p.getRa().asDegrees() for p in positions] 

335 self._metadata['DEC_DEG'] = [p.getDec().asDegrees() for p in positions] 

336 

337 def getPositions(self): 

338 return [s.position for s in self._stamps] 

339 

340 def append(self, item): 

341 """Add an additional stamp. 

342 

343 Parameters 

344 ---------- 

345 item : `Stamp` 

346 Stamp object to append. 

347 """ 

348 if not isinstance(item, Stamp): 

349 raise ValueError("Ojbects added must be a Stamp object.") 

350 self._stamps.append(item) 

351 return None 

352 

353 def extend(self, stamp_list): 

354 """Extend Stamps instance by appending elements from another instance. 

355 

356 Parameters 

357 ---------- 

358 stamps_list : `list` [`Stamp`] 

359 List of Stamp object to append. 

360 """ 

361 for s in stamp_list: 

362 if not isinstance(s, Stamp): 

363 raise ValueError('Can only extend with Stamp objects') 

364 self._stamps += stamp_list 

365 

366 @classmethod 

367 def readFits(cls, filename): 

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

369 

370 Parameters 

371 ---------- 

372 filename : `str` 

373 Name of the file to read 

374 

375 Returns 

376 ------- 

377 object : `Stamps` 

378 An instance of this class 

379 """ 

380 return cls.readFitsWithOptions(filename, None) 

381 

382 @classmethod 

383 def readFitsWithOptions(cls, filename, options): 

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

385 

386 Parameters 

387 ---------- 

388 filename : `str` 

389 Name of the file to read 

390 options : `PropertyList` 

391 Collection of metadata parameters 

392 

393 Returns 

394 ------- 

395 object : `Stamps` 

396 An instance of this class 

397 """ 

398 stamps, metadata = readFitsWithOptions(filename, Stamp.factory, options) 

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

400 use_variance=metadata['HAS_VARIANCE'])