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