Coverage for python/lsst/meas/algorithms/stamps.py : 27%

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"""
25__all__ = ["Stamp", "Stamps", "StampsBase", "writeFits", "readFitsWithOptions"]
27from collections.abc import Sequence
28import abc
29from dataclasses import dataclass
30import numpy
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
38def writeFits(filename, stamp_ims, metadata, write_mask, write_variance):
39 """Write a single FITS file containing all stamps.
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()
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
81def readFitsWithOptions(filename, stamp_factory, options):
82 """Read stamps from FITS file, allowing for only a
83 subregion of the stamps to be read.
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.
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))
139 return stamps, metadata
142@dataclass
143class AbstractStamp(abc.ABC):
144 """Single abstract stamp
146 Parameters
147 ----------
148 Inherit from this class to add metadata to the stamp
149 """
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.
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``
170 Returns
171 -------
172 stamp : `AbstractStamp`
173 An instance of this class
174 """
175 raise NotImplementedError
178@dataclass
179class Stamp(AbstractStamp):
180 """Single stamp
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
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.
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``
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)))
227class StampsBase(abc.ABC, Sequence):
228 """Collection of stamps and associated metadata.
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``.
242 Notes
243 -----
244 A butler can be used to read only a part of the stamps,
245 specified by a bbox:
247 >>> starSubregions = butler.get("brightStarStamps_sub", dataId, bbox=bbox)
248 """
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
262 @classmethod
263 @abc.abstractmethod
264 def readFits(cls, filename):
265 """Build an instance of this class from a file.
267 Parameters
268 ----------
269 filename : `str`
270 Name of the file to read
271 """
272 raise NotImplementedError
274 @classmethod
275 @abc.abstractmethod
276 def readFitsWithOptions(cls, filename, options):
277 """Build an instance of this class with options.
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
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
295 def writeFits(self, filename):
296 """Write this object to a file.
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)
307 def __len__(self):
308 return len(self._stamps)
310 def __getitem__(self, index):
311 return self._stamps[index]
313 def __iter__(self):
314 return iter(self._stamps)
316 def getMaskedImages(self):
317 """Retrieve star images.
319 Returns
320 -------
321 maskedImages :
322 `list` [`lsst.afw.image.maskedImage.maskedImage.MaskedImageF`]
323 """
324 return [stamp.stamp_im for stamp in self._stamps]
326 @property
327 def metadata(self):
328 return self._metadata
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]
337 def getPositions(self):
338 return [s.position for s in self._stamps]
340 def append(self, item):
341 """Add an additional stamp.
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
353 def extend(self, stamp_list):
354 """Extend Stamps instance by appending elements from another instance.
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
366 @classmethod
367 def readFits(cls, filename):
368 """Build an instance of this class from a file.
370 Parameters
371 ----------
372 filename : `str`
373 Name of the file to read
375 Returns
376 -------
377 object : `Stamps`
378 An instance of this class
379 """
380 return cls.readFitsWithOptions(filename, None)
382 @classmethod
383 def readFitsWithOptions(cls, filename, options):
384 """Build an instance of this class with options.
386 Parameters
387 ----------
388 filename : `str`
389 Name of the file to read
390 options : `PropertyList`
391 Collection of metadata parameters
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'])