lsst.meas.algorithms  21.0.0-6-g2b8c1f54+c0fdfc48d7
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  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
143 class 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
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 
227 class 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 
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'])
lsst::meas::algorithms.stamps.Stamps.append
def append(self, item)
Definition: stamps.py:340
lsst::afw::image
lsst::meas::algorithms.stamps.writeFits
def writeFits(filename, stamp_ims, metadata, write_mask, write_variance)
Definition: stamps.py:38
lsst::meas::algorithms.stamps.Stamps.readFits
def readFits(cls, filename)
Definition: stamps.py:367
lsst::meas::algorithms.stamps.Stamp.factory
def factory(cls, stamp_im, metadata, index)
Definition: stamps.py:194
lsst::meas::algorithms.stamps.Stamps
Definition: stamps.py:331
lsst::daf::base::PropertyList
lsst::meas::algorithms.stamps.StampsBase.__getitem__
def __getitem__(self, index)
Definition: stamps.py:310
lsst::meas::algorithms.stamps.AbstractStamp
Definition: stamps.py:143
lsst::meas::algorithms.stamps.StampsBase.__len__
def __len__(self)
Definition: stamps.py:307
lsst::meas::algorithms.stamps.Stamps.readFitsWithOptions
def readFitsWithOptions(cls, filename, options)
Definition: stamps.py:383
lsst::meas::algorithms.stamps.Stamps.getPositions
def getPositions(self)
Definition: stamps.py:337
lsst::meas::algorithms.stamps.StampsBase.getMaskedImages
def getMaskedImages(self)
Definition: stamps.py:316
lsst::meas::algorithms.stamps.StampsBase.__iter__
def __iter__(self)
Definition: stamps.py:313
lsst::meas::algorithms.stamps.StampsBase._stamps
_stamps
Definition: stamps.py:257
lsst::meas::algorithms.stamps.AbstractStamp.factory
def factory(cls, stamp_im, metadata, index)
Definition: stamps.py:153
lsst::meas::algorithms.stamps.StampsBase
Definition: stamps.py:227
lsst::geom
lsst::meas::algorithms.stamps.StampsBase.use_variance
use_variance
Definition: stamps.py:260
lsst::meas::algorithms.stamps.readFitsWithOptions
def readFitsWithOptions(filename, stamp_factory, options)
Definition: stamps.py:81
lsst::meas::algorithms.stamps.StampsBase.readFitsWithOptions
def readFitsWithOptions(cls, filename, options)
Definition: stamps.py:276
lsst::daf::base
lsst::meas::algorithms.stamps.Stamp
Definition: stamps.py:179
lsst::afw::fits
Point< int, 2 >
lsst::meas::algorithms.stamps.StampsBase.use_mask
use_mask
Definition: stamps.py:259
lsst::geom::Angle
lsst::geom::Box2I
lsst::geom::SpherePoint
lsst::meas::algorithms.stamps.StampsBase.writeFits
def writeFits(self, filename)
Definition: stamps.py:295
lsst::meas::algorithms.stamps.Stamps.extend
def extend(self, stamp_list)
Definition: stamps.py:353
lsst::meas::algorithms.stamps.StampsBase.__init__
def __init__(self, stamps, metadata=None, use_mask=True, use_variance=True)
Definition: stamps.py:250
lsst::meas::algorithms.stamps.StampsBase.readFits
def readFits(cls, filename)
Definition: stamps.py:264
lsst::meas::algorithms.stamps.StampsBase.metadata
def metadata(self)
Definition: stamps.py:327
Extent< int, 2 >
lsst::meas::algorithms.stamps.StampsBase._metadata
_metadata
Definition: stamps.py:258
lsst::meas::algorithms.stamps.StampsBase._refresh_metadata
def _refresh_metadata(self)
Definition: stamps.py:289