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 f = afwFits.Fits(filename, 'r') 

108 nExtensions = f.countHdus() 

109 nStamps = metadata["N_STAMPS"] 

110 # check if a bbox was provided 

111 kwargs = {} 

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

113 llcX = options["llcX"] 

114 llcY = options["llcY"] 

115 width = options["width"] 

116 height = options["height"] 

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

118 kwargs["bbox"] = bbox 

119 stamp_parts = {} 

120 # We need to be careful because nExtensions includes the primary 

121 # header data unit 

122 for idx in range(nExtensions-1): 

123 md = afwFits.readMetadata(filename, hdu=idx+1) 

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

125 reader = afwImage.ImageFitsReader(filename, hdu=idx+1) 

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

127 reader = afwImage.MaskFitsReader(filename, hdu=idx+1) 

128 else: 

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

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

131 if len(stamp_parts) != nStamps: 

132 raise ValueError(f'Number of stamps read ({len(stamp_parts)}) does not agree with the ' 

133 f'number of stamps recorded in the metadata ({nStamps}).') 

134 # construct stamps themselves 

135 stamps = [] 

136 for k in range(nStamps): 

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

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

139 stamps.append(stamp_factory(maskedImage, metadata, k)) 

140 

141 return stamps, metadata 

142 

143 

144@dataclass 

145class AbstractStamp(abc.ABC): 

146 """Single abstract stamp 

147 

148 Parameters 

149 ---------- 

150 Inherit from this class to add metadata to the stamp 

151 """ 

152 

153 @classmethod 

154 @abc.abstractmethod 

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

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

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

158 Parameters needed to construct this object are passed in via 

159 a metadata dictionary and then passed to the constructor of 

160 this class. 

161 

162 Parameters 

163 ---------- 

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

165 Pixel data to pass to the constructor 

166 metadata : `dict` 

167 Dictionary containing the information 

168 needed by the constructor. 

169 idx : `int` 

170 Index into the lists in ``metadata`` 

171 

172 Returns 

173 ------- 

174 stamp : `AbstractStamp` 

175 An instance of this class 

176 """ 

177 raise NotImplementedError 

178 

179 

180@dataclass 

181class Stamp(AbstractStamp): 

182 """Single stamp 

183 

184 Parameters 

185 ---------- 

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

187 The actual pixel values for the postage stamp 

188 position : `lsst.geom.SpherePoint` 

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

190 must keep track of the coordinate system 

191 """ 

192 stamp_im: afwImage.maskedImage.MaskedImageF 

193 position: SpherePoint 

194 

195 @classmethod 

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

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

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

199 Parameters needed to construct this object are passed in via 

200 a metadata dictionary and then passed to the constructor of 

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

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

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

204 each point to lists of values. 

205 

206 Parameters 

207 ---------- 

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

209 Pixel data to pass to the constructor 

210 metadata : `dict` 

211 Dictionary containing the information 

212 needed by the constructor. 

213 idx : `int` 

214 Index into the lists in ``metadata`` 

215 

216 Returns 

217 ------- 

218 stamp : `Stamp` 

219 An instance of this class 

220 """ 

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

222 return cls(stamp_im=stamp_im, 

223 position=SpherePoint(Angle(metadata.getArray('RA_DEG')[index], degrees), 

224 Angle(metadata.getArray('DEC_DEG')[index], degrees))) 

225 else: 

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

227 

228 

229class StampsBase(abc.ABC, Sequence): 

230 """Collection of stamps and associated metadata. 

231 

232 Parameters 

233 ---------- 

234 stamps : iterable 

235 This should be an iterable of dataclass objects 

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

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

238 Metadata associated with the bright stars. 

239 use_mask : `bool`, optional 

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

241 use_variance : `bool`, optional 

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

243 

244 Notes 

245 ----- 

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

247 specified by a bbox: 

248 

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

250 """ 

251 

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

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

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

255 for stamp in stamps: 

256 if not isinstance(stamp, AbstractStamp): 

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

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

259 self._stamps = stamps 

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

261 self.use_mask = use_mask 

262 self.use_variance = use_variance 

263 

264 @classmethod 

265 @abc.abstractmethod 

266 def readFits(cls, filename): 

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

268 

269 Parameters 

270 ---------- 

271 filename : `str` 

272 Name of the file to read 

273 """ 

274 raise NotImplementedError 

275 

276 @classmethod 

277 @abc.abstractmethod 

278 def readFitsWithOptions(cls, filename, options): 

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

280 

281 Parameters 

282 ---------- 

283 filename : `str` 

284 Name of the file to read 

285 options : `PropertyList` 

286 Collection of metadata parameters 

287 """ 

288 raise NotImplementedError 

289 

290 @abc.abstractmethod 

291 def _refresh_metadata(self): 

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

293 can be extende 

294 """ 

295 raise NotImplementedError 

296 

297 def writeFits(self, filename): 

298 """Write this object to a file. 

299 

300 Parameters 

301 ---------- 

302 filename : `str` 

303 Name of file to write 

304 """ 

305 self._refresh_metadata() 

306 stamps_ims = self.getMaskedImages() 

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

308 

309 def __len__(self): 

310 return len(self._stamps) 

311 

312 def __getitem__(self, index): 

313 return self._stamps[index] 

314 

315 def __iter__(self): 

316 return iter(self._stamps) 

317 

318 def getMaskedImages(self): 

319 """Retrieve star images. 

320 

321 Returns 

322 ------- 

323 maskedImages : 

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

325 """ 

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

327 

328 @property 

329 def metadata(self): 

330 return self._metadata 

331 

332 

333class Stamps(StampsBase): 

334 def _refresh_metadata(self): 

335 positions = self.getPositions() 

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

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

338 

339 def getPositions(self): 

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

341 

342 def append(self, item): 

343 """Add an additional stamp. 

344 

345 Parameters 

346 ---------- 

347 item : `Stamp` 

348 Stamp object to append. 

349 """ 

350 if not isinstance(item, Stamp): 

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

352 self._stamps.append(item) 

353 return None 

354 

355 def extend(self, stamp_list): 

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

357 

358 Parameters 

359 ---------- 

360 stamps_list : `list` [`Stamp`] 

361 List of Stamp object to append. 

362 """ 

363 for s in stamp_list: 

364 if not isinstance(s, Stamp): 

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

366 self._stamps += stamp_list 

367 

368 @classmethod 

369 def readFits(cls, filename): 

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

371 

372 Parameters 

373 ---------- 

374 filename : `str` 

375 Name of the file to read 

376 

377 Returns 

378 ------- 

379 object : `Stamps` 

380 An instance of this class 

381 """ 

382 return cls.readFitsWithOptions(filename, None) 

383 

384 @classmethod 

385 def readFitsWithOptions(cls, filename, options): 

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

387 

388 Parameters 

389 ---------- 

390 filename : `str` 

391 Name of the file to read 

392 options : `PropertyList` 

393 Collection of metadata parameters 

394 

395 Returns 

396 ------- 

397 object : `Stamps` 

398 An instance of this class 

399 """ 

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

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

402 use_variance=metadata['HAS_VARIANCE'])