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 .stamps
import StampsBase, AbstractStamp, readFitsWithOptions
41 """Single stamp centered on a bright star, normalized by its
46 stamp_im : `lsst.afw.image.MaskedImage`
47 Pixel data for this postage stamp
49 Gaia G magnitude for the object in this stamp
51 Gaia object identifier
52 annularFlux : `Optional[float]`
53 Flux in an annulus around the object
55 stamp_im: MaskedImageF
58 annularFlux: Optional[float] =
None
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.
72 stamp_im : `lsst.afw.image.MaskedImage`
73 Pixel data to pass to the constructor
75 Dictionary containing the information
76 needed by the constructor.
78 Index into the lists in ``metadata``
82 brightstarstamp : `BrightStarStamp`
83 An instance of this class
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])
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.
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
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
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.
114 stampSize = self.stamp_im.getDimensions()
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)
123 andMask = reduce(ior, (annulusMask.getPlaneBitMask(bm)
for bm
in badMaskPlanes))
124 statsControl.setAndMask(andMask)
126 annulusStat = afwMath.makeStatistics(annulusImage, statsFlag, statsControl)
129 raise RuntimeError(
"Annular flux computation failed, likely because no pixels were valid.")
131 self.stamp_im.image.array /= self.
annularFluxannularFlux
136 """Collection of bright star stamps and associated metadata.
140 starStamps : `collections.abc.Sequence` [`BrightStarStamp`]
141 Sequence of star stamps. Cannot contain both normalized and
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.
154 If `True` read and write mask data. Default `True`.
155 use_variance : `bool`
156 If ``True`` read and write variance data. Default ``False``.
161 Raised if one of the star stamps provided does not contain the
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.
171 A butler can be used to read only a part of the stamps, specified by a
174 >>> starSubregions = butler.get("brightStarStamps_sub", dataId, bbox=bbox)
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)
183 self._innerRadius, self.
_outerRadius_outerRadius = innerRadius, outerRadius
184 if innerRadius
is not None and outerRadius
is not None:
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.
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.
206 starStamps : `collections.abc.Sequence` [`BrightStarStamp`]
207 Sequence of star stamps. Cannot contain both normalized and
210 Inner radius value, in pixels. This and ``outerRadius`` define the
211 annulus used to compute the ``"annularFlux"`` values within each
214 Outer radius value, in pixels. This and ``innerRadius`` define the
215 annulus used to compute the ``"annularFlux"`` values within each
217 metadata : `lsst.daf.base.PropertyList`, optional
218 Metadata associated with the bright stars.
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
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.
241 Raised if one of the star stamps provided does not contain the
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.
249 stampSize = starStamps[0].stamp_im.getDimensions()
250 imCenter = stampSize[0]//2, stampSize[1]//2
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)
256 bss = cls(starStamps, innerRadius=
None, outerRadius=
None,
257 metadata=metadata, use_mask=use_mask,
258 use_variance=use_variance)
260 bss._checkNormalization(
True, innerRadius, outerRadius)
261 bss._innerRadius, bss._outerRadius = innerRadius, outerRadius
263 for j, stamp
in enumerate(bss._stamps):
265 stamp.measureAndNormalize(annulus, statsControl=statsControl, statsFlag=statsFlag,
266 badMaskPlanes=badMaskPlanes)
271 if discardNanFluxObjects:
274 stamp.annularFlux = np.nan
275 bss.normalized =
True
278 def _refresh_metadata(self):
279 """Refresh the metadata. Should be called before writing this object
288 self.
_metadata_metadata[
"INNER_RADIUS"] = self._innerRadius
294 """Build an instance of this class from a file.
299 Name of the file to read
305 """Build an instance of this class with options.
310 Name of the file to read
311 options : `PropertyList`
312 Collection of metadata parameters
315 if metadata[
"NORMALIZED"]:
317 innerRadius=metadata[
"INNER_RADIUS"], outerRadius=metadata[
"OUTER_RADIUS"],
318 metadata=metadata, use_mask=metadata[
'HAS_MASK'],
319 use_variance=metadata[
'HAS_VARIANCE'])
321 return cls(stamps, metadata=metadata, use_mask=metadata[
'HAS_MASK'],
322 use_variance=metadata[
'HAS_VARIANCE'])
324 def append(self, item, innerRadius=None, outerRadius=None):
325 """Add an additional bright star stamp.
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
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
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.")
351 """Extend BrightStarStamps instance by appending elements from another
356 bss : `BrightStarStamps`
357 Other instance to concatenate.
359 if not isinstance(bss, BrightStarStamps):
360 raise ValueError(
'Can only extend with a BrightStarStamps object. '
362 self.
_checkRadius_checkRadius(bss._innerRadius, bss._outerRadius)
363 self.
_stamps_stamps += bss._stamps
366 """Retrieve Gaia G magnitudes for each star.
370 gaiaGMags : `list` [`float`]
372 return [stamp.gaiaGMag
for stamp
in self.
_stamps_stamps]
375 """Retrieve Gaia IDs for each star.
379 gaiaIds : `list` [`int`]
381 return [stamp.gaiaId
for stamp
in self.
_stamps_stamps]
384 """Retrieve normalization factors for each star.
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.
393 annularFluxes : `list` [`float`]
395 return [stamp.annularFlux
for stamp
in self.
_stamps_stamps]
398 """Return the subset of bright star stamps for objects with specified
399 magnitude cuts (in Gaia G).
403 magMin : `float`, optional
404 Keep only stars fainter than this value.
405 magMax : `float`, optional
406 Keep only stars brighter than this value.
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)]
414 innerRadius=self._innerRadius, outerRadius=self.
_outerRadius_outerRadius,
416 instance._stamps = subset
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.
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}).")
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.
434 nFluxVals = nStamps - noneFluxCount
435 if noneFluxCount
and noneFluxCount < nStamps:
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.")
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:
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.")
461 raise AttributeError(f
"{nFluxVals} stamps contain an annularFlux value. If stamps have "
462 "been normalized, the innerRadius and outerRadius values used must "
def factory(cls, stamp_im, metadata, idx)
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 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)