22"""Collection of small images (stamps), each centered on a bright star.
25__all__ = [
"BrightStarStamp",
"BrightStarStamps"]
27from dataclasses
import dataclass
28from operator
import ior
29from functools
import reduce
30from typing
import Optional
36from lsst.afw import table
as afwTable
38from .stamps
import Stamps, AbstractStamp, readFitsWithOptions
43 """Single stamp centered on a bright star, normalized by its
49 Pixel data for this postage stamp
51 Origin of the stamps
in its origin exposure (pixels)
53 Gaia G magnitude
for the object
in this stamp
55 Gaia object identifier
56 annularFlux : `Optional[float]`
57 Flux
in an annulus around the object
59 stamp_im: MaskedImageF
63 archive_element: Optional[afwTable.io.Persistable] = None
64 annularFlux: Optional[float] =
None
67 def factory(cls, stamp_im, metadata, idx, archive_element=None):
68 """This method is needed to service the FITS reader.
69 We need a standard interface to construct objects like this.
70 Parameters needed to construct this object are passed in via
71 a metadata dictionary
and then passed to the constructor of
72 this
class. This particular factory method requires keys:
73 G_MAGS, GAIA_IDS,
and ANNULAR_FLUXES. They should each
74 point to lists of values.
79 Pixel data to
pass to the constructor
81 Dictionary containing the information
82 needed by the constructor.
84 Index into the lists
in ``metadata``
85 archive_element : `lsst.afwTable.io.Persistable`, optional
86 Archive element (e.g. Transform
or WCS) associated
with this stamp.
90 brightstarstamp : `BrightStarStamp`
91 An instance of this
class
93 if 'X0S' in metadata
and 'Y0S' in metadata:
94 x0 = metadata.getArray(
'X0S')[idx]
95 y0 = metadata.getArray(
'Y0S')[idx]
96 position = Point2I(x0, y0)
99 return cls(stamp_im=stamp_im,
100 gaiaGMag=metadata.getArray(
'G_MAGS')[idx],
101 gaiaId=metadata.getArray(
'GAIA_IDS')[idx],
103 archive_element=archive_element,
104 annularFlux=metadata.getArray(
'ANNULAR_FLUXES')[idx])
108 badMaskPlanes=(
'BAD',
'SAT',
'NO_DATA')):
109 """Compute "annularFlux", the integrated flux within an annulus
110 around an object's center, and normalize it.
112 Since the center of bright stars are saturated and/
or heavily affected
113 by ghosts, we measure their flux
in an annulus
with a large enough
114 inner radius to avoid the most severe ghosts
and contain enough
115 non-saturated pixels.
119 annulus : `lsst.afw.geom.spanSet.SpanSet`
120 SpanSet containing the annulus to use
for normalization.
121 statsControl : `lsst.afw.math.statistics.StatisticsControl`, optional
122 StatisticsControl to be used when computing flux over all pixels
124 statsFlag : `lsst.afw.math.statistics.Property`, optional
125 statsFlag to be passed on to ``afwMath.makeStatistics`` to compute
126 annularFlux. Defaults to a simple MEAN.
127 badMaskPlanes : `collections.abc.Collection` [`str`]
128 Collection of mask planes to ignore when computing annularFlux.
130 stampSize = self.stamp_im.getDimensions()
133 maskPlaneDict = self.stamp_im.mask.getMaskPlaneDict()
134 annulusImage = MaskedImageF(stampSize, planeDict=maskPlaneDict)
135 annulusMask = annulusImage.mask
136 annulusMask.array[:] = 2**maskPlaneDict[
'NO_DATA']
137 annulus.copyMaskedImage(self.stamp_im, annulusImage)
139 andMask = reduce(ior, (annulusMask.getPlaneBitMask(bm)
for bm
in badMaskPlanes))
140 statsControl.setAndMask(andMask)
145 raise RuntimeError(
"Annular flux computation failed, likely because no pixels were valid.")
152 """Collection of bright star stamps and associated metadata.
156 starStamps : `collections.abc.Sequence` [`BrightStarStamp`]
157 Sequence of star stamps. Cannot contain both normalized and
159 innerRadius : `int`, optional
160 Inner radius value,
in pixels. This
and ``outerRadius`` define the
161 annulus used to compute the ``
"annularFlux"`` values within each
162 ``starStamp``. Must be provided
if ``normalize``
is True.
163 outerRadius : `int`, optional
164 Outer radius value,
in pixels. This
and ``innerRadius`` define the
165 annulus used to compute the ``
"annularFlux"`` values within each
166 ``starStamp``. Must be provided
if ``normalize``
is True.
167 nb90Rots : `int`, optional
168 Number of 90 degree rotations required to compensate
for detector
171 Metadata associated
with the bright stars.
173 If `
True` read
and write mask data. Default `
True`.
174 use_variance : `bool`
175 If ``
True`` read
and write variance data. Default ``
False``.
177 If ``
True`` read
and write an Archive that contains a Persistable
178 associated
with each stamp. In the case of bright stars, this
is
179 usually a ``TransformPoint2ToPoint2``, used to warp each stamp
180 to the same pixel grid before stacking.
185 Raised
if one of the star stamps provided does
not contain the
188 Raised
if there
is a mix-
and-match of normalized
and unnormalized
189 stamps, stamps normalized
with different annulus definitions,
or if
190 stamps are to be normalized but annular radii were
not provided.
195 A butler can be used to read only a part of the stamps, specified by a
198 >>> starSubregions = butler.get(
"brightStarStamps", dataId, parameters={
'bbox': bbox})
201 def __init__(self, starStamps, innerRadius=None, outerRadius=None, nb90Rots=None,
202 metadata=None, use_mask=True, use_variance=False, use_archive=False):
203 super().
__init__(starStamps, metadata, use_mask, use_variance, use_archive)
207 self._innerRadius, self.
_outerRadius = innerRadius, outerRadius
208 if innerRadius
is not None and outerRadius
is not None:
216 metadata=None, use_mask=True, use_variance=False,
217 use_archive=False, imCenter=None,
218 discardNanFluxObjects=True,
219 statsControl=afwMath.StatisticsControl(),
221 badMaskPlanes=(
'BAD',
'SAT',
'NO_DATA')):
222 """Normalize a set of bright star stamps and initialize a
223 BrightStarStamps instance.
225 Since the center of bright stars are saturated and/
or heavily affected
226 by ghosts, we measure their flux
in an annulus
with a large enough
227 inner radius to avoid the most severe ghosts
and contain enough
228 non-saturated pixels.
232 starStamps : `collections.abc.Sequence` [`BrightStarStamp`]
233 Sequence of star stamps. Cannot contain both normalized
and
236 Inner radius value,
in pixels. This
and ``outerRadius`` define the
237 annulus used to compute the ``
"annularFlux"`` values within each
240 Outer radius value,
in pixels. This
and ``innerRadius`` define the
241 annulus used to compute the ``
"annularFlux"`` values within each
243 nb90Rots : `int`, optional
244 Number of 90 degree rotations required to compensate
for detector
247 Metadata associated
with the bright stars.
249 If `
True` read
and write mask data. Default `
True`.
250 use_variance : `bool`
251 If ``
True`` read
and write variance data. Default ``
False``.
253 If ``
True`` read
and write an Archive that contains a Persistable
254 associated
with each stamp. In the case of bright stars, this
is
255 usually a ``TransformPoint2ToPoint2``, used to warp each stamp
256 to the same pixel grid before stacking.
257 imCenter : `collections.abc.Sequence`, optional
258 Center of the object,
in pixels. If
not provided, the center of the
259 first stamp
's pixel grid will be used.
260 discardNanFluxObjects : `bool`
261 Whether objects with NaN annular flux should be discarded.
262 If
False, these objects will
not be normalized.
263 statsControl : `lsst.afw.math.statistics.StatisticsControl`, optional
264 StatisticsControl to be used when computing flux over all pixels
266 statsFlag : `lsst.afw.math.statistics.Property`, optional
267 statsFlag to be passed on to ``afwMath.makeStatistics`` to compute
268 annularFlux. Defaults to a simple MEAN.
269 badMaskPlanes : `collections.abc.Collection` [`str`]
270 Collection of mask planes to ignore when computing annularFlux.
275 Raised
if one of the star stamps provided does
not contain the
278 Raised
if there
is a mix-
and-match of normalized
and unnormalized
279 stamps, stamps normalized
with different annulus definitions,
or if
280 stamps are to be normalized but annular radii were
not provided.
283 stampSize = starStamps[0].stamp_im.getDimensions()
284 imCenter = stampSize[0]//2, stampSize[1]//2
286 outerCircle = afwGeom.SpanSet.fromShape(outerRadius, afwGeom.Stencil.CIRCLE, offset=imCenter)
287 innerCircle = afwGeom.SpanSet.fromShape(innerRadius, afwGeom.Stencil.CIRCLE, offset=imCenter)
288 annulus = outerCircle.intersectNot(innerCircle)
290 bss = cls(starStamps, innerRadius=
None, outerRadius=
None, nb90Rots=nb90Rots,
291 metadata=metadata, use_mask=use_mask,
292 use_variance=use_variance, use_archive=use_archive)
294 bss._checkNormalization(
True, innerRadius, outerRadius)
295 bss._innerRadius, bss._outerRadius = innerRadius, outerRadius
297 for j, stamp
in enumerate(bss._stamps):
299 stamp.measureAndNormalize(annulus, statsControl=statsControl, statsFlag=statsFlag,
300 badMaskPlanes=badMaskPlanes)
305 if discardNanFluxObjects:
308 stamp.annularFlux = np.nan
309 bss.normalized =
True
312 def _refresh_metadata(self):
313 """Refresh the metadata. Should be called before writing this object
321 self.
_metadata[
"X0S"] = [xy0[0]
for xy0
in positions]
322 self.
_metadata[
"Y0S"] = [xy0[1]
for xy0
in positions]
325 self.
_metadata[
"INNER_RADIUS"] = self._innerRadius
333 """Build an instance of this class from a file.
338 Name of the file to read
343 def readFitsWithOptions(cls, filename, options):
344 """Build an instance of this class with options.
349 Name of the file to read
350 options : `PropertyList`
351 Collection of metadata parameters
353 stamps, metadata = readFitsWithOptions(filename, BrightStarStamp.factory, options)
354 nb90Rots = metadata["NB_90_ROTS"]
if "NB_90_ROTS" in metadata
else None
355 if metadata[
"NORMALIZED"]:
357 innerRadius=metadata[
"INNER_RADIUS"], outerRadius=metadata[
"OUTER_RADIUS"],
358 nb90Rots=nb90Rots, metadata=metadata, use_mask=metadata[
'HAS_MASK'],
359 use_variance=metadata[
'HAS_VARIANCE'], use_archive=metadata[
'HAS_ARCHIVE'])
361 return cls(stamps, nb90Rots=nb90Rots, metadata=metadata, use_mask=metadata[
'HAS_MASK'],
362 use_variance=metadata[
'HAS_VARIANCE'], use_archive=metadata[
'HAS_ARCHIVE'])
364 def append(self, item, innerRadius=None, outerRadius=None):
365 """Add an additional bright star stamp.
369 item : `BrightStarStamp`
370 Bright star stamp to append.
371 innerRadius : `int`, optional
372 Inner radius value, in pixels. This
and ``outerRadius`` define the
373 annulus used to compute the ``
"annularFlux"`` values within each
375 outerRadius : `int`, optional
376 Outer radius value,
in pixels. This
and ``innerRadius`` define the
377 annulus used to compute the ``
"annularFlux"`` values within each
380 if not isinstance(item, BrightStarStamp):
381 raise ValueError(f
"Can only add instances of BrightStarStamp, got {type(item)}.")
382 if (item.annularFlux
is None) == self.
normalized:
383 raise AttributeError(
"Trying to append an unnormalized stamp to a normalized BrightStarStamps "
384 "instance, or vice-versa.")
391 """Extend BrightStarStamps instance by appending elements from another
396 bss : `BrightStarStamps`
397 Other instance to concatenate.
399 if not isinstance(bss, BrightStarStamps):
400 raise ValueError(
'Can only extend with a BrightStarStamps object. '
406 """Retrieve Gaia G magnitudes for each star.
410 gaiaGMags : `list` [`float`]
412 return [stamp.gaiaGMag
for stamp
in self.
_stamps]
415 """Retrieve Gaia IDs for each star.
419 gaiaIds : `list` [`int`]
421 return [stamp.gaiaId
for stamp
in self.
_stamps]
424 """Retrieve normalization factors for each star.
426 These are computed by integrating the flux in annulus centered on the
427 bright star, far enough
from center to be beyond most severe ghosts
and
428 saturation. The inner
and outer radii that define the annulus can be
429 recovered
from the metadata.
433 annularFluxes : `list` [`float`]
435 return [stamp.annularFlux
for stamp
in self.
_stamps]
438 """Return the subset of bright star stamps for objects with specified
439 magnitude cuts (in Gaia G).
443 magMin : `float`, optional
444 Keep only stars fainter than this value.
445 magMax : `float`, optional
446 Keep only stars brighter than this value.
448 subset = [stamp for stamp
in self.
_stamps
449 if (magMin
is None or stamp.gaiaGMag > magMin)
450 and (magMax
is None or stamp.gaiaGMag < magMax)]
454 innerRadius=self._innerRadius, outerRadius=self.
_outerRadius,
456 instance._stamps = subset
459 def _checkRadius(self, innerRadius, outerRadius):
460 """Ensure provided annulus radius is consistent with that already
461 present in the instance,
or with arguments passed on at initialization.
463 if innerRadius != self._innerRadius
or outerRadius != self.
_outerRadius:
464 raise AttributeError(
"Trying to mix stamps normalized with annulus radii "
465 f
"{innerRadius, outerRadius} with those of BrightStarStamp instance\n"
466 f
"(computed with annular radii {self._innerRadius, self._outerRadius}).")
468 def _checkNormalization(self, normalize, innerRadius, outerRadius):
469 """Ensure there is no mixing of normalized and unnormalized stars, and
470 that, if requested, normalization can be performed.
474 nFluxVals = nStamps - noneFluxCount
475 if noneFluxCount
and noneFluxCount < nStamps:
478 raise AttributeError(f
"Only {nFluxVals} stamps contain an annularFlux value.\nAll stamps in a "
479 "BrightStarStamps instance must either be normalized with the same annulus "
480 "definition, or none of them can contain an annularFlux value.")
484 if innerRadius
is None or outerRadius
is None:
485 raise AttributeError(
"For stamps to be normalized (normalize=True), please provide a valid "
486 "value (in pixels) for both innerRadius and outerRadius.")
487 elif noneFluxCount < nStamps:
488 raise AttributeError(f
"{nFluxVals} stamps already contain an annularFlux value. For stamps to"
489 " be normalized, all their annularFlux must be None.")
490 elif innerRadius
is not None and outerRadius
is not None:
494 raise AttributeError(f
"{noneFluxCount} stamps contain no annularFlux, but annular radius "
495 "values were provided and normalize=False.\nTo normalize stamps, set "
496 "normalize to True.")
501 raise AttributeError(f
"{nFluxVals} stamps contain an annularFlux value. If stamps have "
502 "been normalized, the innerRadius and outerRadius values used must "
A class to manipulate images, masks, and variance as a single object.
Class for storing ordered metadata with comments.
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, nb90Rots=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, nb90Rots=None, metadata=None, use_mask=True, use_variance=False, use_archive=False, imCenter=None, discardNanFluxObjects=True, statsControl=afwMath.StatisticsControl(), statsFlag=afwMath.stringToStatisticsProperty("MEAN"), badMaskPlanes=('BAD', 'SAT', 'NO_DATA'))
def readFitsWithOptions(cls, filename, options)
def readFitsWithOptions(cls, filename, options)
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)
Property stringToStatisticsProperty(std::string const property)
Conversion function to switch a string to a Property (see Statistics.h)