lsst.meas.algorithms  21.0.0-15-g490e301a+3bb30e68ef
stamps.py
Go to the documentation of this file.
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 
27 from collections.abc import Sequence
28 import abc
29 from dataclasses import dataclass
30 import numpy
31 
32 import lsst.afw.image as afwImage
33 import lsst.afw.fits as afwFits
34 from lsst.geom import Box2I, Point2I, Extent2I, Angle, degrees, SpherePoint
35 from lsst.daf.base import PropertyList
36 
37 
38 def 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 
81 def 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
145 class 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
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 
229 class 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 = stamps
260  self._metadata_metadata = PropertyList() if metadata is None else metadata.deepCopy()
261  self.use_maskuse_mask = use_mask
262  self.use_varianceuse_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_refresh_metadata()
306  stamps_ims = self.getMaskedImagesgetMaskedImages()
307  writeFits(filename, stamps_ims, self._metadata_metadata, self.use_maskuse_mask, self.use_varianceuse_variance)
308 
309  def __len__(self):
310  return len(self._stamps_stamps)
311 
312  def __getitem__(self, index):
313  return self._stamps_stamps[index]
314 
315  def __iter__(self):
316  return iter(self._stamps_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_stamps]
327 
328  @property
329  def metadata(self):
330  return self._metadata_metadata
331 
332 
334  def _refresh_metadata(self):
335  positions = self.getPositionsgetPositions()
336  self._metadata_metadata['RA_DEG'] = [p.getRa().asDegrees() for p in positions]
337  self._metadata_metadata['DEC_DEG'] = [p.getDec().asDegrees() for p in positions]
338 
339  def getPositions(self):
340  return [s.position for s in self._stamps_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_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_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.readFitsWithOptionsreadFitsWithOptionsreadFitsWithOptions(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'])
def factory(cls, stamp_im, metadata, index)
Definition: stamps.py:155
def factory(cls, stamp_im, metadata, index)
Definition: stamps.py:196
def __init__(self, stamps, metadata=None, use_mask=True, use_variance=True)
Definition: stamps.py:252
def readFitsWithOptions(cls, filename, options)
Definition: stamps.py:278
def extend(self, stamp_list)
Definition: stamps.py:355
def readFits(cls, filename)
Definition: stamps.py:369
def readFitsWithOptions(cls, filename, options)
Definition: stamps.py:385
def writeFits(filename, stamp_ims, metadata, write_mask, write_variance)
Definition: stamps.py:38
def readFitsWithOptions(filename, stamp_factory, options)
Definition: stamps.py:81