22 """Collection of small images (stamps), each centered on a bright star.
25 __all__ = [
"BrightStarStamp",
"BrightStarStamps"]
27 from dataclasses
import dataclass
28 from operator
import ior
29 from functools
import reduce
30 from typing
import Optional
36 from lsst.afw import table
as afwTable
37 from .stamps
import StampsBase, AbstractStamp, readFitsWithOptions
42 """Single stamp centered on a bright star, normalized by its
47 stamp_im : `lsst.afw.image.MaskedImage`
48 Pixel data for this postage stamp
50 Gaia G magnitude for the object in this stamp
52 Gaia object identifier
53 annularFlux : `Optional[float]`
54 Flux in an annulus around the object
56 stamp_im: MaskedImageF
59 archive_element: Optional[afwTable.io.Persistable] =
None
60 annularFlux: Optional[float] =
None
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.
74 stamp_im : `lsst.afw.image.MaskedImage`
75 Pixel data to pass to the constructor
77 Dictionary containing the information
78 needed by the constructor.
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.
86 brightstarstamp : `BrightStarStamp`
87 An instance of this class
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])
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.
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.
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
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.
119 stampSize = self.stamp_im.getDimensions()
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)
128 andMask = reduce(ior, (annulusMask.getPlaneBitMask(bm)
for bm
in badMaskPlanes))
129 statsControl.setAndMask(andMask)
131 annulusStat = afwMath.makeStatistics(annulusImage, statsFlag, statsControl)
134 raise RuntimeError(
"Annular flux computation failed, likely because no pixels were valid.")
136 self.stamp_im.image.array /= self.
annularFluxannularFlux
141 """Collection of bright star stamps and associated metadata.
145 starStamps : `collections.abc.Sequence` [`BrightStarStamp`]
146 Sequence of star stamps. Cannot contain both normalized and
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.
159 If `True` read and write mask data. Default `True`.
160 use_variance : `bool`
161 If ``True`` read and write variance data. Default ``False``.
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.
171 Raised if one of the star stamps provided does not contain the
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.
181 A butler can be used to read only a part of the stamps, specified by a
184 >>> starSubregions = butler.get("brightStarStamps", dataId, parameters={'bbox': bbox})
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)
193 self._innerRadius, self.
_outerRadius_outerRadius = innerRadius, outerRadius
194 if innerRadius
is not None and outerRadius
is not None:
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.
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.
216 starStamps : `collections.abc.Sequence` [`BrightStarStamp`]
217 Sequence of star stamps. Cannot contain both normalized and
220 Inner radius value, in pixels. This and ``outerRadius`` define the
221 annulus used to compute the ``"annularFlux"`` values within each
224 Outer radius value, in pixels. This and ``innerRadius`` define the
225 annulus used to compute the ``"annularFlux"`` values within each
227 metadata : `lsst.daf.base.PropertyList`, optional
228 Metadata associated with the bright stars.
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
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.
251 Raised if one of the star stamps provided does not contain the
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.
259 stampSize = starStamps[0].stamp_im.getDimensions()
260 imCenter = stampSize[0]//2, stampSize[1]//2
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)
266 bss = cls(starStamps, innerRadius=
None, outerRadius=
None,
267 metadata=metadata, use_mask=use_mask,
268 use_variance=use_variance)
270 bss._checkNormalization(
True, innerRadius, outerRadius)
271 bss._innerRadius, bss._outerRadius = innerRadius, outerRadius
273 for j, stamp
in enumerate(bss._stamps):
275 stamp.measureAndNormalize(annulus, statsControl=statsControl, statsFlag=statsFlag,
276 badMaskPlanes=badMaskPlanes)
281 if discardNanFluxObjects:
284 stamp.annularFlux = np.nan
285 bss.normalized =
True
288 def _refresh_metadata(self):
289 """Refresh the metadata. Should be called before writing this object
298 self.
_metadata_metadata[
"INNER_RADIUS"] = self._innerRadius
304 """Build an instance of this class from a file.
309 Name of the file to read
315 """Build an instance of this class with options.
320 Name of the file to read
321 options : `PropertyList`
322 Collection of metadata parameters
325 if metadata[
"NORMALIZED"]:
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'])
331 return cls(stamps, metadata=metadata, use_mask=metadata[
'HAS_MASK'],
332 use_variance=metadata[
'HAS_VARIANCE'], use_archive=metadata[
'HAS_ARCHIVE'])
334 def append(self, item, innerRadius=None, outerRadius=None):
335 """Add an additional bright star stamp.
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
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
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.")
361 """Extend BrightStarStamps instance by appending elements from another
366 bss : `BrightStarStamps`
367 Other instance to concatenate.
369 if not isinstance(bss, BrightStarStamps):
370 raise ValueError(
'Can only extend with a BrightStarStamps object. '
372 self.
_checkRadius_checkRadius(bss._innerRadius, bss._outerRadius)
373 self.
_stamps_stamps += bss._stamps
376 """Retrieve Gaia G magnitudes for each star.
380 gaiaGMags : `list` [`float`]
382 return [stamp.gaiaGMag
for stamp
in self.
_stamps_stamps]
385 """Retrieve Gaia IDs for each star.
389 gaiaIds : `list` [`int`]
391 return [stamp.gaiaId
for stamp
in self.
_stamps_stamps]
394 """Retrieve normalization factors for each star.
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.
403 annularFluxes : `list` [`float`]
405 return [stamp.annularFlux
for stamp
in self.
_stamps_stamps]
408 """Return the subset of bright star stamps for objects with specified
409 magnitude cuts (in Gaia G).
413 magMin : `float`, optional
414 Keep only stars fainter than this value.
415 magMax : `float`, optional
416 Keep only stars brighter than this value.
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)]
424 innerRadius=self._innerRadius, outerRadius=self.
_outerRadius_outerRadius,
426 instance._stamps = subset
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.
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}).")
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.
444 nFluxVals = nStamps - noneFluxCount
445 if noneFluxCount
and noneFluxCount < nStamps:
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.")
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:
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.")
471 raise AttributeError(f
"{nFluxVals} stamps contain an annularFlux value. If stamps have "
472 "been normalized, the innerRadius and outerRadius values used must "
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 readFits(cls, filename)
def append(self, item, innerRadius=None, outerRadius=None)
def selectByMag(self, magMin=None, magMax=None)
def getAnnularFluxes(self)
def readFitsWithOptions(cls, filename, options)
def _checkRadius(self, innerRadius, outerRadius)
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)