lsst.meas.algorithms  21.0.0-30-g727f315e+692e5d6c44
brightStarStamps.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), each centered on a bright star.
23 """
24 
25 __all__ = ["BrightStarStamp", "BrightStarStamps"]
26 
27 from dataclasses import dataclass
28 from operator import ior
29 from functools import reduce
30 from typing import Optional
31 import numpy as np
32 
33 from lsst.afw.image import MaskedImageF
34 from lsst.afw import geom as afwGeom
35 from lsst.afw import math as afwMath
36 from lsst.afw import table as afwTable
37 from .stamps import StampsBase, AbstractStamp, readFitsWithOptions
38 
39 
40 @dataclass
42  """Single stamp centered on a bright star, normalized by its
43  annularFlux.
44 
45  Parameters
46  ----------
47  stamp_im : `lsst.afw.image.MaskedImage`
48  Pixel data for this postage stamp
49  gaiaGMag : `float`
50  Gaia G magnitude for the object in this stamp
51  gaiaId : `int`
52  Gaia object identifier
53  annularFlux : `Optional[float]`
54  Flux in an annulus around the object
55  """
56  stamp_im: MaskedImageF
57  gaiaGMag: float
58  gaiaId: int
59  archive_element: Optional[afwTable.io.Persistable] = None
60  annularFlux: Optional[float] = None
61 
62  @classmethod
63  def factory(cls, stamp_im, metadata, idx, archive_element=None):
64  """This method is needed to service the FITS reader.
65  We need a standard interface to construct objects like this.
66  Parameters needed to construct this object are passed in via
67  a metadata dictionary and then passed to the constructor of
68  this class. This particular factory method requires keys:
69  G_MAGS, GAIA_IDS, and ANNULAR_FLUXES. They should each
70  point to lists of values.
71 
72  Parameters
73  ----------
74  stamp_im : `lsst.afw.image.MaskedImage`
75  Pixel data to pass to the constructor
76  metadata : `dict`
77  Dictionary containing the information
78  needed by the constructor.
79  idx : `int`
80  Index into the lists in ``metadata``
81  archive_element : `lsst.afwTable.io.Persistable`, optional
82  Archive element (e.g. Transform or WCS) associated with this stamp.
83 
84  Returns
85  -------
86  brightstarstamp : `BrightStarStamp`
87  An instance of this class
88  """
89  return cls(stamp_im=stamp_im,
90  gaiaGMag=metadata.getArray('G_MAGS')[idx],
91  gaiaId=metadata.getArray('GAIA_IDS')[idx],
92  archive_element=archive_element,
93  annularFlux=metadata.getArray('ANNULAR_FLUXES')[idx])
94 
95  def measureAndNormalize(self, annulus, statsControl=afwMath.StatisticsControl(),
96  statsFlag=afwMath.stringToStatisticsProperty("MEAN"),
97  badMaskPlanes=('BAD', 'SAT', 'NO_DATA')):
98  """Compute "annularFlux", the integrated flux within an annulus
99  around an object's center, and normalize it.
100 
101  Since the center of bright stars are saturated and/or heavily affected
102  by ghosts, we measure their flux in an annulus with a large enough
103  inner radius to avoid the most severe ghosts and contain enough
104  non-saturated pixels.
105 
106  Parameters
107  ----------
108  annulus : `lsst.afw.geom.spanSet.SpanSet`
109  SpanSet containing the annulus to use for normalization.
110  statsControl : `lsst.afw.math.statistics.StatisticsControl`, optional
111  StatisticsControl to be used when computing flux over all pixels
112  within the annulus.
113  statsFlag : `lsst.afw.math.statistics.Property`, optional
114  statsFlag to be passed on to ``afwMath.makeStatistics`` to compute
115  annularFlux. Defaults to a simple MEAN.
116  badMaskPlanes : `collections.abc.Collection` [`str`]
117  Collection of mask planes to ignore when computing annularFlux.
118  """
119  stampSize = self.stamp_im.getDimensions()
120  # create image with the same pixel values within annulus, NO_DATA
121  # elsewhere
122  maskPlaneDict = self.stamp_im.mask.getMaskPlaneDict()
123  annulusImage = MaskedImageF(stampSize, planeDict=maskPlaneDict)
124  annulusMask = annulusImage.mask
125  annulusMask.array[:] = 2**maskPlaneDict['NO_DATA']
126  annulus.copyMaskedImage(self.stamp_im, annulusImage)
127  # set mask planes to be ignored
128  andMask = reduce(ior, (annulusMask.getPlaneBitMask(bm) for bm in badMaskPlanes))
129  statsControl.setAndMask(andMask)
130  # compute annularFlux
131  annulusStat = afwMath.makeStatistics(annulusImage, statsFlag, statsControl)
132  self.annularFluxannularFlux = annulusStat.getValue()
133  if np.isnan(self.annularFluxannularFlux):
134  raise RuntimeError("Annular flux computation failed, likely because no pixels were valid.")
135  # normalize stamps
136  self.stamp_im.image.array /= self.annularFluxannularFlux
137  return None
138 
139 
141  """Collection of bright star stamps and associated metadata.
142 
143  Parameters
144  ----------
145  starStamps : `collections.abc.Sequence` [`BrightStarStamp`]
146  Sequence of star stamps. Cannot contain both normalized and
147  unnormalized stamps.
148  innerRadius : `int`, optional
149  Inner radius value, in pixels. This and ``outerRadius`` define the
150  annulus used to compute the ``"annularFlux"`` values within each
151  ``starStamp``. Must be provided if ``normalize`` is True.
152  outerRadius : `int`, optional
153  Outer radius value, in pixels. This and ``innerRadius`` define the
154  annulus used to compute the ``"annularFlux"`` values within each
155  ``starStamp``. Must be provided if ``normalize`` is True.
156  metadata : `lsst.daf.base.PropertyList`, optional
157  Metadata associated with the bright stars.
158  use_mask : `bool`
159  If `True` read and write mask data. Default `True`.
160  use_variance : `bool`
161  If ``True`` read and write variance data. Default ``False``.
162  use_archive : `bool`
163  If ``True`` read and write an Archive that contains a Persistable
164  associated with each stamp. In the case of bright stars, this is
165  usually a ``TransformPoint2ToPoint2``, used to warp each stamp
166  to the same pixel grid before stacking.
167 
168  Raises
169  ------
170  ValueError
171  Raised if one of the star stamps provided does not contain the
172  required keys.
173  AttributeError
174  Raised if there is a mix-and-match of normalized and unnormalized
175  stamps, stamps normalized with different annulus definitions, or if
176  stamps are to be normalized but annular radii were not provided.
177 
178 
179  Notes
180  -----
181  A butler can be used to read only a part of the stamps, specified by a
182  bbox:
183 
184  >>> starSubregions = butler.get("brightStarStamps", dataId, parameters={'bbox': bbox})
185  """
186 
187  def __init__(self, starStamps, innerRadius=None, outerRadius=None,
188  metadata=None, use_mask=True, use_variance=False, use_archive=False):
189  super().__init__(starStamps, metadata, use_mask, use_variance, use_archive)
190  # Ensure stamps contain a flux measurement if and only if they are
191  # already expected to be normalized
192  self._checkNormalization_checkNormalization(False, innerRadius, outerRadius)
193  self._innerRadius, self._outerRadius_outerRadius = innerRadius, outerRadius
194  if innerRadius is not None and outerRadius is not None:
195  self.normalizednormalized = True
196  else:
197  self.normalizednormalized = False
198 
199  @classmethod
200  def initAndNormalize(cls, starStamps, innerRadius, outerRadius,
201  metadata=None, use_mask=True, use_variance=False,
202  imCenter=None, discardNanFluxObjects=True,
203  statsControl=afwMath.StatisticsControl(),
204  statsFlag=afwMath.stringToStatisticsProperty("MEAN"),
205  badMaskPlanes=('BAD', 'SAT', 'NO_DATA')):
206  """Normalize a set of bright star stamps and initialize a
207  BrightStarStamps instance.
208 
209  Since the center of bright stars are saturated and/or heavily affected
210  by ghosts, we measure their flux in an annulus with a large enough
211  inner radius to avoid the most severe ghosts and contain enough
212  non-saturated pixels.
213 
214  Parameters
215  ----------
216  starStamps : `collections.abc.Sequence` [`BrightStarStamp`]
217  Sequence of star stamps. Cannot contain both normalized and
218  unnormalized stamps.
219  innerRadius : `int`
220  Inner radius value, in pixels. This and ``outerRadius`` define the
221  annulus used to compute the ``"annularFlux"`` values within each
222  ``starStamp``.
223  outerRadius : `int`
224  Outer radius value, in pixels. This and ``innerRadius`` define the
225  annulus used to compute the ``"annularFlux"`` values within each
226  ``starStamp``.
227  metadata : `lsst.daf.base.PropertyList`, optional
228  Metadata associated with the bright stars.
229  use_mask : `bool`
230  If `True` read and write mask data. Default `True`.
231  use_variance : `bool`
232  If ``True`` read and write variance data. Default ``False``.
233  imCenter : `collections.abc.Sequence`, optional
234  Center of the object, in pixels. If not provided, the center of the
235  first stamp's pixel grid will be used.
236  discardNanFluxObjects : `bool`
237  Whether objects with NaN annular flux should be discarded.
238  If False, these objects will not be normalized.
239  statsControl : `lsst.afw.math.statistics.StatisticsControl`, optional
240  StatisticsControl to be used when computing flux over all pixels
241  within the annulus.
242  statsFlag : `lsst.afw.math.statistics.Property`, optional
243  statsFlag to be passed on to ``afwMath.makeStatistics`` to compute
244  annularFlux. Defaults to a simple MEAN.
245  badMaskPlanes : `collections.abc.Collection` [`str`]
246  Collection of mask planes to ignore when computing annularFlux.
247 
248  Raises
249  ------
250  ValueError
251  Raised if one of the star stamps provided does not contain the
252  required keys.
253  AttributeError
254  Raised if there is a mix-and-match of normalized and unnormalized
255  stamps, stamps normalized with different annulus definitions, or if
256  stamps are to be normalized but annular radii were not provided.
257  """
258  if imCenter is None:
259  stampSize = starStamps[0].stamp_im.getDimensions()
260  imCenter = stampSize[0]//2, stampSize[1]//2
261  # Create SpanSet of annulus
262  outerCircle = afwGeom.SpanSet.fromShape(outerRadius, afwGeom.Stencil.CIRCLE, offset=imCenter)
263  innerCircle = afwGeom.SpanSet.fromShape(innerRadius, afwGeom.Stencil.CIRCLE, offset=imCenter)
264  annulus = outerCircle.intersectNot(innerCircle)
265  # Initialize (unnormalized) brightStarStamps instance
266  bss = cls(starStamps, innerRadius=None, outerRadius=None,
267  metadata=metadata, use_mask=use_mask,
268  use_variance=use_variance)
269  # Ensure no stamps had already been normalized
270  bss._checkNormalization(True, innerRadius, outerRadius)
271  bss._innerRadius, bss._outerRadius = innerRadius, outerRadius
272  # Apply normalization
273  for j, stamp in enumerate(bss._stamps):
274  try:
275  stamp.measureAndNormalize(annulus, statsControl=statsControl, statsFlag=statsFlag,
276  badMaskPlanes=badMaskPlanes)
277  except RuntimeError:
278  # Optionally keep NaN flux objects, for bookkeeping purposes,
279  # and to avoid having to re-find and redo the preprocessing
280  # steps needed before bright stars can be subtracted.
281  if discardNanFluxObjects:
282  bss._stamps.pop(j)
283  else:
284  stamp.annularFlux = np.nan
285  bss.normalized = True
286  return bss
287 
288  def _refresh_metadata(self):
289  """Refresh the metadata. Should be called before writing this object
290  out.
291  """
292  # add full list of Gaia magnitudes, IDs and annularFlxes to shared
293  # metadata
294  self._metadata_metadata["G_MAGS"] = self.getMagnitudesgetMagnitudes()
295  self._metadata_metadata["GAIA_IDS"] = self.getGaiaIdsgetGaiaIds()
296  self._metadata_metadata["ANNULAR_FLUXES"] = self.getAnnularFluxesgetAnnularFluxes()
297  self._metadata_metadata["NORMALIZED"] = self.normalizednormalized
298  self._metadata_metadata["INNER_RADIUS"] = self._innerRadius
299  self._metadata_metadata["OUTER_RADIUS"] = self._outerRadius_outerRadius
300  return None
301 
302  @classmethod
303  def readFits(cls, filename):
304  """Build an instance of this class from a file.
305 
306  Parameters
307  ----------
308  filename : `str`
309  Name of the file to read
310  """
311  return cls.readFitsWithOptionsreadFitsWithOptionsreadFitsWithOptions(filename, None)
312 
313  @classmethod
314  def readFitsWithOptions(cls, filename, options):
315  """Build an instance of this class with options.
316 
317  Parameters
318  ----------
319  filename : `str`
320  Name of the file to read
321  options : `PropertyList`
322  Collection of metadata parameters
323  """
324  stamps, metadata = readFitsWithOptions(filename, BrightStarStamp.factory, options)
325  if metadata["NORMALIZED"]:
326  return cls(stamps,
327  innerRadius=metadata["INNER_RADIUS"], outerRadius=metadata["OUTER_RADIUS"],
328  metadata=metadata, use_mask=metadata['HAS_MASK'],
329  use_variance=metadata['HAS_VARIANCE'], use_archive=metadata['HAS_ARCHIVE'])
330  else:
331  return cls(stamps, metadata=metadata, use_mask=metadata['HAS_MASK'],
332  use_variance=metadata['HAS_VARIANCE'], use_archive=metadata['HAS_ARCHIVE'])
333 
334  def append(self, item, innerRadius=None, outerRadius=None):
335  """Add an additional bright star stamp.
336 
337  Parameters
338  ----------
339  item : `BrightStarStamp`
340  Bright star stamp to append.
341  innerRadius : `int`, optional
342  Inner radius value, in pixels. This and ``outerRadius`` define the
343  annulus used to compute the ``"annularFlux"`` values within each
344  ``BrightStarStamp``.
345  outerRadius : `int`, optional
346  Outer radius value, in pixels. This and ``innerRadius`` define the
347  annulus used to compute the ``"annularFlux"`` values within each
348  ``BrightStarStamp``.
349  """
350  if not isinstance(item, BrightStarStamp):
351  raise ValueError(f"Can only add instances of BrightStarStamp, got {type(item)}.")
352  if (item.annularFlux is None) == self.normalizednormalized:
353  raise AttributeError("Trying to append an unnormalized stamp to a normalized BrightStarStamps "
354  "instance, or vice-versa.")
355  else:
356  self._checkRadius_checkRadius(innerRadius, outerRadius)
357  self._stamps_stamps.append(item)
358  return None
359 
360  def extend(self, bss):
361  """Extend BrightStarStamps instance by appending elements from another
362  instance.
363 
364  Parameters
365  ----------
366  bss : `BrightStarStamps`
367  Other instance to concatenate.
368  """
369  if not isinstance(bss, BrightStarStamps):
370  raise ValueError('Can only extend with a BrightStarStamps object. '
371  f'Got {type(bss)}.')
372  self._checkRadius_checkRadius(bss._innerRadius, bss._outerRadius)
373  self._stamps_stamps += bss._stamps
374 
375  def getMagnitudes(self):
376  """Retrieve Gaia G magnitudes for each star.
377 
378  Returns
379  -------
380  gaiaGMags : `list` [`float`]
381  """
382  return [stamp.gaiaGMag for stamp in self._stamps_stamps]
383 
384  def getGaiaIds(self):
385  """Retrieve Gaia IDs for each star.
386 
387  Returns
388  -------
389  gaiaIds : `list` [`int`]
390  """
391  return [stamp.gaiaId for stamp in self._stamps_stamps]
392 
393  def getAnnularFluxes(self):
394  """Retrieve normalization factors for each star.
395 
396  These are computed by integrating the flux in annulus centered on the
397  bright star, far enough from center to be beyond most severe ghosts and
398  saturation. The inner and outer radii that define the annulus can be
399  recovered from the metadata.
400 
401  Returns
402  -------
403  annularFluxes : `list` [`float`]
404  """
405  return [stamp.annularFlux for stamp in self._stamps_stamps]
406 
407  def selectByMag(self, magMin=None, magMax=None):
408  """Return the subset of bright star stamps for objects with specified
409  magnitude cuts (in Gaia G).
410 
411  Parameters
412  ----------
413  magMin : `float`, optional
414  Keep only stars fainter than this value.
415  magMax : `float`, optional
416  Keep only stars brighter than this value.
417  """
418  subset = [stamp for stamp in self._stamps_stamps
419  if (magMin is None or stamp.gaiaGMag > magMin)
420  and (magMax is None or stamp.gaiaGMag < magMax)]
421  # This is an optimization to save looping over the init argument when
422  # it is already guaranteed to be the correct type
423  instance = BrightStarStamps((),
424  innerRadius=self._innerRadius, outerRadius=self._outerRadius_outerRadius,
425  metadata=self._metadata_metadata)
426  instance._stamps = subset
427  return instance
428 
429  def _checkRadius(self, innerRadius, outerRadius):
430  """Ensure provided annulus radius is consistent with that already
431  present in the instance, or with arguments passed on at initialization.
432  """
433  if innerRadius != self._innerRadius or outerRadius != self._outerRadius_outerRadius:
434  raise AttributeError("Trying to mix stamps normalized with annulus radii "
435  f"{innerRadius, outerRadius} with those of BrightStarStamp instance\n"
436  f"(computed with annular radii {self._innerRadius, self._outerRadius}).")
437 
438  def _checkNormalization(self, normalize, innerRadius, outerRadius):
439  """Ensure there is no mixing of normalized and unnormalized stars, and
440  that, if requested, normalization can be performed.
441  """
442  noneFluxCount = self.getAnnularFluxesgetAnnularFluxes().count(None)
443  nStamps = len(self)
444  nFluxVals = nStamps - noneFluxCount
445  if noneFluxCount and noneFluxCount < nStamps:
446  # at least one stamp contains an annularFlux value (i.e. has been
447  # normalized), but not all of them do
448  raise AttributeError(f"Only {nFluxVals} stamps contain an annularFlux value.\nAll stamps in a "
449  "BrightStarStamps instance must either be normalized with the same annulus "
450  "definition, or none of them can contain an annularFlux value.")
451  elif normalize:
452  # stamps are to be normalized; ensure annular radii are specified
453  # and they have no annularFlux
454  if innerRadius is None or outerRadius is None:
455  raise AttributeError("For stamps to be normalized (normalize=True), please provide a valid "
456  "value (in pixels) for both innerRadius and outerRadius.")
457  elif noneFluxCount < nStamps:
458  raise AttributeError(f"{nFluxVals} stamps already contain an annularFlux value. For stamps to"
459  " be normalized, all their annularFlux must be None.")
460  elif innerRadius is not None and outerRadius is not None:
461  # Radii provided, but normalize=False; check that stamps
462  # already contain annularFluxes
463  if noneFluxCount:
464  raise AttributeError(f"{noneFluxCount} stamps contain no annularFlux, but annular radius "
465  "values were provided and normalize=False.\nTo normalize stamps, set "
466  "normalize to True.")
467  else:
468  # At least one radius value is missing; ensure no stamps have
469  # already been normalized
470  if nFluxVals:
471  raise AttributeError(f"{nFluxVals} stamps contain an annularFlux value. If stamps have "
472  "been normalized, the innerRadius and outerRadius values used must "
473  "be provided.")
474  return None
def factory(cls, stamp_im, metadata, idx, archive_element=None)
def measureAndNormalize(self, annulus, statsControl=afwMath.StatisticsControl(), statsFlag=afwMath.stringToStatisticsProperty("MEAN"), badMaskPlanes=('BAD', 'SAT', 'NO_DATA'))
def __init__(self, starStamps, innerRadius=None, outerRadius=None, metadata=None, use_mask=True, use_variance=False, use_archive=False)
def _checkNormalization(self, normalize, innerRadius, outerRadius)
def append(self, item, innerRadius=None, outerRadius=None)
def initAndNormalize(cls, starStamps, innerRadius, outerRadius, metadata=None, use_mask=True, use_variance=False, imCenter=None, discardNanFluxObjects=True, statsControl=afwMath.StatisticsControl(), statsFlag=afwMath.stringToStatisticsProperty("MEAN"), badMaskPlanes=('BAD', 'SAT', 'NO_DATA'))
def readFitsWithOptions(cls, filename, options)
Definition: stamps.py:325