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