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

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 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))
141 return stamps, metadata
144@dataclass
145class AbstractStamp(abc.ABC):
146 """Single abstract stamp
148 Parameters
149 ----------
150 Inherit from this class to add metadata to the stamp
151 """
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.
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``
172 Returns
173 -------
174 stamp : `AbstractStamp`
175 An instance of this class
176 """
177 raise NotImplementedError
180@dataclass
181class Stamp(AbstractStamp):
182 """Single stamp
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
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.
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``
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)))
229class StampsBase(abc.ABC, Sequence):
230 """Collection of stamps and associated metadata.
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``.
244 Notes
245 -----
246 A butler can be used to read only a part of the stamps,
247 specified by a bbox:
249 >>> starSubregions = butler.get("brightStarStamps_sub", dataId, bbox=bbox)
250 """
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
264 @classmethod
265 @abc.abstractmethod
266 def readFits(cls, filename):
267 """Build an instance of this class from a file.
269 Parameters
270 ----------
271 filename : `str`
272 Name of the file to read
273 """
274 raise NotImplementedError
276 @classmethod
277 @abc.abstractmethod
278 def readFitsWithOptions(cls, filename, options):
279 """Build an instance of this class with options.
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
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
297 def writeFits(self, filename):
298 """Write this object to a file.
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)
309 def __len__(self):
310 return len(self._stamps)
312 def __getitem__(self, index):
313 return self._stamps[index]
315 def __iter__(self):
316 return iter(self._stamps)
318 def getMaskedImages(self):
319 """Retrieve star images.
321 Returns
322 -------
323 maskedImages :
324 `list` [`lsst.afw.image.maskedImage.maskedImage.MaskedImageF`]
325 """
326 return [stamp.stamp_im for stamp in self._stamps]
328 @property
329 def metadata(self):
330 return self._metadata
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]
339 def getPositions(self):
340 return [s.position for s in self._stamps]
342 def append(self, item):
343 """Add an additional stamp.
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
355 def extend(self, stamp_list):
356 """Extend Stamps instance by appending elements from another instance.
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
368 @classmethod
369 def readFits(cls, filename):
370 """Build an instance of this class from a file.
372 Parameters
373 ----------
374 filename : `str`
375 Name of the file to read
377 Returns
378 -------
379 object : `Stamps`
380 An instance of this class
381 """
382 return cls.readFitsWithOptions(filename, None)
384 @classmethod
385 def readFitsWithOptions(cls, filename, options):
386 """Build an instance of this class with options.
388 Parameters
389 ----------
390 filename : `str`
391 Name of the file to read
392 options : `PropertyList`
393 Collection of metadata parameters
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'])