LSST Applications g0f08755f38+82efc23009,g12f32b3c4e+e7bdf1200e,g1653933729+a8ce1bb630,g1a0ca8cf93+50eff2b06f,g28da252d5a+52db39f6a5,g2bbee38e9b+37c5a29d61,g2bc492864f+37c5a29d61,g2cdde0e794+c05ff076ad,g3156d2b45e+41e33cbcdc,g347aa1857d+37c5a29d61,g35bb328faa+a8ce1bb630,g3a166c0a6a+37c5a29d61,g3e281a1b8c+fb992f5633,g414038480c+7f03dfc1b0,g41af890bb2+11b950c980,g5fbc88fb19+17cd334064,g6b1c1869cb+12dd639c9a,g781aacb6e4+a8ce1bb630,g80478fca09+72e9651da0,g82479be7b0+04c31367b4,g858d7b2824+82efc23009,g9125e01d80+a8ce1bb630,g9726552aa6+8047e3811d,ga5288a1d22+e532dc0a0b,gae0086650b+a8ce1bb630,gb58c049af0+d64f4d3760,gc28159a63d+37c5a29d61,gcf0d15dbbd+2acd6d4d48,gd7358e8bfb+778a810b6e,gda3e153d99+82efc23009,gda6a2b7d83+2acd6d4d48,gdaeeff99f8+1711a396fd,ge2409df99d+6b12de1076,ge79ae78c31+37c5a29d61,gf0baf85859+d0a5978c5a,gf3967379c6+4954f8c433,gfb92a5be7c+82efc23009,gfec2e1e490+2aaed99252,w.2024.46
LSST Data Management Base Package
Loading...
Searching...
No Matches
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__all__ = ["Stamp", "Stamps", "StampsBase", "writeFits", "readFitsWithOptions"]
25
26import abc
27from collections.abc import Sequence
28from dataclasses import dataclass, field, fields
29
30import numpy as np
31from lsst.afw.fits import Fits, readMetadata
32from lsst.afw.image import ImageFitsReader, MaskedImage, MaskedImageF, MaskFitsReader
33from lsst.afw.table.io import InputArchive, OutputArchive, Persistable
34from lsst.daf.base import PropertyList
35from lsst.geom import Angle, Box2I, Extent2I, Point2I, SpherePoint, degrees
36from lsst.utils import doImport
37from lsst.utils.introspection import get_full_type_name
38
39
40def writeFits(filename, stamps, metadata, type_name, write_mask, write_variance, write_archive=False):
41 """Write a single FITS file containing all stamps.
42
43 Parameters
44 ----------
45 filename : `str`
46 A string indicating the output filename
47 stamps : iterable of `BaseStamp`
48 An iterable of Stamp objects
49 metadata : `PropertyList`
50 A collection of key, value metadata pairs to be
51 written to the primary header
52 type_name : `str`
53 Python type name of the StampsBase subclass to use
54 write_mask : `bool`
55 Write the mask data to the output file?
56 write_variance : `bool`
57 Write the variance data to the output file?
58 write_archive : `bool`, optional
59 Write an archive to store Persistables along with each stamp?
60 Default: ``False``.
61 """
62 metadata["HAS_MASK"] = write_mask
63 metadata["HAS_VARIANCE"] = write_variance
64 metadata["HAS_ARCHIVE"] = write_archive
65 metadata["N_STAMPS"] = len(stamps)
66 metadata["STAMPCLS"] = type_name
67 # Record version number in case of future code changes
68 metadata["VERSION"] = 1
69 # create primary HDU with global metadata
70 fitsFile = Fits(filename, "w")
71 fitsFile.createEmpty()
72 # Store Persistables in an OutputArchive and write it
73 if write_archive:
74 oa = OutputArchive()
75 archive_ids = [oa.put(stamp.archive_element) for stamp in stamps]
76 metadata["ARCHIVE_IDS"] = archive_ids
77 fitsFile.writeMetadata(metadata)
78 oa.writeFits(fitsFile)
79 else:
80 fitsFile.writeMetadata(metadata)
81 fitsFile.closeFile()
82 # add all pixel data optionally writing mask and variance information
83 for i, stamp in enumerate(stamps):
84 metadata = PropertyList()
85 # EXTVER should be 1-based, the index from enumerate is 0-based
86 metadata.update({"EXTVER": i + 1, "EXTNAME": "IMAGE"})
87 stamp.stamp_im.getImage().writeFits(filename, metadata=metadata, mode="a")
88 if write_mask:
89 metadata = PropertyList()
90 metadata.update({"EXTVER": i + 1, "EXTNAME": "MASK"})
91 stamp.stamp_im.getMask().writeFits(filename, metadata=metadata, mode="a")
92 if write_variance:
93 metadata = PropertyList()
94 metadata.update({"EXTVER": i + 1, "EXTNAME": "VARIANCE"})
95 stamp.stamp_im.getVariance().writeFits(filename, metadata=metadata, mode="a")
96 return None
97
98
99def readFitsWithOptions(filename, stamp_factory, options):
100 """Read stamps from FITS file, allowing for only a subregion of the stamps
101 to be read.
102
103 Parameters
104 ----------
105 filename : `str`
106 A string indicating the file to read
107 stamp_factory : classmethod
108 A factory function defined on a dataclass for constructing
109 stamp objects a la `~lsst.meas.algorithm.Stamp`
110 options : `PropertyList` or `dict`
111 A collection of parameters. If it contains a bounding box
112 (``bbox`` key), or if certain other keys (``llcX``, ``llcY``,
113 ``width``, ``height``) are available for one to be constructed,
114 the bounding box is passed to the ``FitsReader`` in order to
115 return a sub-image.
116
117 Returns
118 -------
119 stamps : `list` of dataclass objects like `Stamp`, PropertyList
120 A tuple of a list of `Stamp`-like objects
121 metadata : `PropertyList`
122 The metadata
123
124 Notes
125 -----
126 The data are read using the data type expected by the
127 `~lsst.afw.image.MaskedImage` class attached to the `AbstractStamp`
128 dataclass associated with the factory method.
129 """
130 # extract necessary info from metadata
131 metadata = readMetadata(filename, hdu=0)
132 nStamps = metadata["N_STAMPS"]
133 has_archive = metadata["HAS_ARCHIVE"]
134 if has_archive:
135 archive_ids = metadata.getArray("ARCHIVE_IDS")
136 with Fits(filename, "r") as f:
137 nExtensions = f.countHdus()
138 # check if a bbox was provided
139 kwargs = {}
140 if options:
141 # gen3 API
142 if "bbox" in options.keys():
143 kwargs["bbox"] = options["bbox"]
144 # gen2 API
145 elif "llcX" in options.keys():
146 llcX = options["llcX"]
147 llcY = options["llcY"]
148 width = options["width"]
149 height = options["height"]
150 bbox = Box2I(Point2I(llcX, llcY), Extent2I(width, height))
151 kwargs["bbox"] = bbox
152 stamp_parts = {}
153
154 # Determine the dtype from the factory. This allows a Stamp class
155 # to be defined in terms of MaskedImageD or MaskedImageI without
156 # forcing everything to floats.
157 masked_image_cls = None
158 for stamp_field in fields(stamp_factory.__self__):
159 if issubclass(stamp_field.type, MaskedImage):
160 masked_image_cls = stamp_field.type
161 break
162 else:
163 raise RuntimeError("Stamp factory does not use MaskedImage.")
164 default_dtype = np.dtype(masked_image_cls.dtype)
165 variance_dtype = np.dtype(np.float32) # Variance is always the same type.
166
167 # We need to be careful because nExtensions includes the primary HDU.
168 for idx in range(nExtensions - 1):
169 dtype = None
170 md = readMetadata(filename, hdu=idx + 1)
171 # Skip binary tables that aren't images or archives.
172 if md["XTENSION"] == "BINTABLE" and not ("ZIMAGE" in md and md["ZIMAGE"]):
173 if md["EXTNAME"] != "ARCHIVE_INDEX":
174 continue
175 if md["EXTNAME"] in ("IMAGE", "VARIANCE"):
176 reader = ImageFitsReader(filename, hdu=idx + 1)
177 if md["EXTNAME"] == "VARIANCE":
178 dtype = variance_dtype
179 else:
180 dtype = default_dtype
181 elif md["EXTNAME"] == "MASK":
182 reader = MaskFitsReader(filename, hdu=idx + 1)
183 elif md["EXTNAME"] == "ARCHIVE_INDEX":
184 f.setHdu(idx + 1)
185 archive = InputArchive.readFits(f)
186 continue
187 elif md["EXTTYPE"] == "ARCHIVE_DATA":
188 continue
189 else:
190 raise ValueError(f"Unknown extension type: {md['EXTNAME']}")
191 stamp_parts.setdefault(md["EXTVER"], {})[md["EXTNAME"].lower()] = reader.read(dtype=dtype,
192 **kwargs)
193 if len(stamp_parts) != nStamps:
194 raise ValueError(
195 f"Number of stamps read ({len(stamp_parts)}) does not agree with the "
196 f"number of stamps recorded in the metadata ({nStamps})."
197 )
198 # construct stamps themselves
199 stamps = []
200 for k in range(nStamps):
201 # Need to increment by one since EXTVER starts at 1
202 maskedImage = masked_image_cls(**stamp_parts[k + 1])
203 archive_element = archive.get(archive_ids[k]) if has_archive else None
204 stamps.append(stamp_factory(maskedImage, metadata, k, archive_element))
205
206 return stamps, metadata
207
208
209@dataclass
210class AbstractStamp(abc.ABC):
211 """Single abstract stamp.
212
213 Parameters
214 ----------
215 Inherit from this class to add metadata to the stamp.
216 """
217
218 @classmethod
219 @abc.abstractmethod
220 def factory(cls, stamp_im, metadata, index, archive_element=None):
221 """This method is needed to service the FITS reader. We need a standard
222 interface to construct objects like this. Parameters needed to
223 construct this object are passed in via a metadata dictionary and then
224 passed to the constructor of this class.
225
226 Parameters
227 ----------
228 stamp : `~lsst.afw.image.MaskedImage`
229 Pixel data to pass to the constructor
230 metadata : `dict`
231 Dictionary containing the information
232 needed by the constructor.
233 idx : `int`
234 Index into the lists in ``metadata``
235 archive_element : `~lsst.afw.table.io.Persistable`, optional
236 Archive element (e.g. Transform or WCS) associated with this stamp.
237
238 Returns
239 -------
240 stamp : `AbstractStamp`
241 An instance of this class
242 """
243 raise NotImplementedError
244
245
247 # SpherePoint is nominally mutable in C++ so we must use a factory
248 # and return an entirely new SpherePoint each time a Stamps is created.
249 return SpherePoint(Angle(np.nan), Angle(np.nan))
250
251
252@dataclass
254 """Single stamp.
255
256 Parameters
257 ----------
258 stamp_im : `~lsst.afw.image.MaskedImageF`
259 The actual pixel values for the postage stamp.
260 archive_element : `~lsst.afw.table.io.Persistable` or `None`, optional
261 Archive element (e.g. Transform or WCS) associated with this stamp.
262 position : `~lsst.geom.SpherePoint` or `None`, optional
263 Position of the center of the stamp. Note the user must keep track of
264 the coordinate system.
265 """
266
267 stamp_im: MaskedImageF
268 archive_element: Persistable | None = None
269 position: SpherePoint | None = field(default_factory=_default_position)
270
271 @classmethod
272 def factory(cls, stamp_im, metadata, index, archive_element=None):
273 """This method is needed to service the FITS reader. We need a standard
274 interface to construct objects like this. Parameters needed to
275 construct this object are passed in via a metadata dictionary and then
276 passed to the constructor of this class. If lists of values are passed
277 with the following keys, they will be passed to the constructor,
278 otherwise dummy values will be passed: RA_DEG, DEC_DEG. They should
279 each point to lists of values.
280
281 Parameters
282 ----------
283 stamp : `~lsst.afw.image.MaskedImage`
284 Pixel data to pass to the constructor
285 metadata : `dict`
286 Dictionary containing the information
287 needed by the constructor.
288 idx : `int`
289 Index into the lists in ``metadata``
290 archive_element : `~lsst.afw.table.io.Persistable`, optional
291 Archive element (e.g. Transform or WCS) associated with this stamp.
292
293 Returns
294 -------
295 stamp : `Stamp`
296 An instance of this class
297 """
298 if "RA_DEG" in metadata and "DEC_DEG" in metadata:
299 return cls(
300 stamp_im=stamp_im,
301 archive_element=archive_element,
302 position=SpherePoint(
303 Angle(metadata.getArray("RA_DEG")[index], degrees),
304 Angle(metadata.getArray("DEC_DEG")[index], degrees),
305 ),
306 )
307 else:
308 return cls(
309 stamp_im=stamp_im,
310 archive_element=archive_element,
311 position=SpherePoint(Angle(np.nan), Angle(np.nan)),
312 )
313
314
315class StampsBase(abc.ABC, Sequence):
316 """Collection of stamps and associated metadata.
317
318 Parameters
319 ----------
320 stamps : iterable
321 This should be an iterable of dataclass objects
322 a la ``~lsst.meas.algorithms.Stamp``.
323 metadata : `~lsst.daf.base.PropertyList`, optional
324 Metadata associated with the objects within the stamps.
325 use_mask : `bool`, optional
326 If ``True`` read and write the mask data. Default ``True``.
327 use_variance : `bool`, optional
328 If ``True`` read and write the variance data. Default ``True``.
329 use_archive : `bool`, optional
330 If ``True``, read and write an Archive that contains a Persistable
331 associated with each stamp, for example a Transform or a WCS.
332 Default ``False``.
333
334 Notes
335 -----
336 A butler can be used to read only a part of the stamps,
337 specified by a bbox:
338
339 >>> starSubregions = butler.get(
340 "brightStarStamps",
341 dataId,
342 parameters={"bbox": bbox}
343 )
344 """
345
346 def __init__(self, stamps, metadata=None, use_mask=True, use_variance=True, use_archive=False):
347 for stamp in stamps:
348 if not isinstance(stamp, AbstractStamp):
349 raise ValueError(f"The entries in stamps must inherit from AbstractStamp. Got {type(stamp)}.")
350 self._stamps = stamps
351 self._metadata = PropertyList() if metadata is None else metadata.deepCopy()
352 self.use_mask = use_mask
353 self.use_variance = use_variance
354 self.use_archive = use_archive
355
356 @classmethod
357 def readFits(cls, filename):
358 """Build an instance of this class from a file.
359
360 Parameters
361 ----------
362 filename : `str`
363 Name of the file to read
364 """
365
366 return cls.readFitsWithOptions(filename, None)
367
368 @classmethod
369 def readFitsWithOptions(cls, filename, options):
370 """Build an instance of this class with options.
371
372 Parameters
373 ----------
374 filename : `str`
375 Name of the file to read
376 options : `PropertyList`
377 Collection of metadata parameters
378 """
379 # To avoid problems since this is no longer an abstract method.
380 # TO-DO: Consider refactoring this method. This class check was added
381 # to allow the butler formatter to use a generic type but still end up
382 # giving the correct type back, ensuring that the abstract base class
383 # is not used by mistake. Perhaps this logic can be optimised.
384 if cls is not StampsBase:
385 raise NotImplementedError(f"Please implement specific FITS reader for class {cls}")
386
387 # Load metadata to get class
388 metadata = readMetadata(filename, hdu=0)
389 type_name = metadata.get("STAMPCLS")
390 if type_name is None:
391 raise RuntimeError(
392 f"No class name in file {filename}. Unable to instantiate correct stamps subclass. "
393 "Is this an old version format Stamps file?"
394 )
395
396 # Import class and override `cls`
397 stamp_type = doImport(type_name)
398 cls = stamp_type
399
400 return cls.readFitsWithOptions(filename, options)
401
402 @abc.abstractmethod
404 """Make sure metadata is up to date, as this object can be extended."""
405 raise NotImplementedError
406
407 def writeFits(self, filename):
408 """Write this object to a file.
409
410 Parameters
411 ----------
412 filename : `str`
413 Name of file to write.
414 """
415 self._refresh_metadata()
416 type_name = get_full_type_name(self)
417 writeFits(
418 filename,
419 self._stamps,
420 self._metadata,
421 type_name,
422 self.use_mask,
423 self.use_variance,
424 self.use_archive,
425 )
426
427 def __len__(self):
428 return len(self._stamps)
429
430 def __getitem__(self, index):
431 return self._stamps[index]
432
433 def __iter__(self):
434 return iter(self._stamps)
435
437 """Retrieve star images.
438
439 Returns
440 -------
441 maskedImages :
442 `list` [`~lsst.afw.image.MaskedImageF`]
443 """
444 return [stamp.stamp_im for stamp in self._stamps]
445
447 """Retrieve archive elements associated with each stamp.
448
449 Returns
450 -------
451 archiveElements :
452 `list` [`~lsst.afw.table.io.Persistable`]
453 """
454 return [stamp.archive_element for stamp in self._stamps]
455
456 @property
457 def metadata(self):
458 return self._metadata
459
460
463 positions = self.getPositions()
464 self._metadata["RA_DEG"] = [p.getRa().asDegrees() for p in positions]
465 self._metadata["DEC_DEG"] = [p.getDec().asDegrees() for p in positions]
466
467 def getPositions(self):
468 return [s.position for s in self._stamps]
469
470 def append(self, item):
471 """Add an additional stamp.
472
473 Parameters
474 ----------
475 item : `Stamp`
476 Stamp object to append.
477 """
478 if not isinstance(item, Stamp):
479 raise ValueError("Objects added must be a Stamp object.")
480 self._stamps.append(item)
481 return None
482
483 def extend(self, stamp_list):
484 """Extend Stamps instance by appending elements from another instance.
485
486 Parameters
487 ----------
488 stamps_list : `list` [`Stamp`]
489 List of Stamp object to append.
490 """
491 for s in stamp_list:
492 if not isinstance(s, Stamp):
493 raise ValueError("Can only extend with Stamp objects")
494 self._stamps += stamp_list
495
496 @classmethod
497 def readFits(cls, filename):
498 """Build an instance of this class from a file.
499
500 Parameters
501 ----------
502 filename : `str`
503 Name of the file to read.
504
505 Returns
506 -------
507 object : `Stamps`
508 An instance of this class.
509 """
510 return cls.readFitsWithOptionsreadFitsWithOptions(filename, None)
511
512 @classmethod
513 def readFitsWithOptions(cls, filename, options):
514 """Build an instance of this class with options.
515
516 Parameters
517 ----------
518 filename : `str`
519 Name of the file to read.
520 options : `PropertyList` or `dict`
521 Collection of metadata parameters.
522
523 Returns
524 -------
525 object : `Stamps`
526 An instance of this class.
527 """
528 stamps, metadata = readFitsWithOptions(filename, Stamp.factory, options)
529 return cls(
530 stamps,
531 metadata=metadata,
532 use_mask=metadata["HAS_MASK"],
533 use_variance=metadata["HAS_VARIANCE"],
534 use_archive=metadata["HAS_ARCHIVE"],
535 )
A simple struct that combines the two arguments that must be passed to most cfitsio routines and cont...
Definition fits.h:308
A FITS reader class for regular Images.
A FITS reader class for Masks.
A multi-catalog archive object used to save table::io::Persistable objects.
Class for storing ordered metadata with comments.
A class representing an angle.
Definition Angle.h:128
An integer coordinate rectangle.
Definition Box.h:55
Point in an unspecified spherical coordinate system.
Definition SpherePoint.h:57
factory(cls, stamp_im, metadata, index, archive_element=None)
Definition stamps.py:220
factory(cls, stamp_im, metadata, index, archive_element=None)
Definition stamps.py:272
__init__(self, stamps, metadata=None, use_mask=True, use_variance=True, use_archive=False)
Definition stamps.py:346
readFitsWithOptions(cls, filename, options)
Definition stamps.py:369
readFitsWithOptions(cls, filename, options)
Definition stamps.py:513
writeFits(filename, stamps, metadata, type_name, write_mask, write_variance, write_archive=False)
Definition stamps.py:40
readFitsWithOptions(filename, stamp_factory, options)
Definition stamps.py:99