Coverage for python/lsst/meas/algorithms/brightStarStamps.py : 24%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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"""
25__all__ = ["BrightStarStamp", "BrightStarStamps"]
27from dataclasses import dataclass
28from operator import ior
29from functools import reduce
30from typing import Optional
32from lsst.afw.image import MaskedImageF
33from lsst.afw import geom as afwGeom
34from lsst.afw import math as afwMath
35from .stamps import StampsBase, AbstractStamp, readFitsWithOptions
38@dataclass
39class BrightStarStamp(AbstractStamp):
40 """Single stamp centered on a bright star, normalized by its
41 annularFlux.
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
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.
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``
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])
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.
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.
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.annularFlux = annulusStat.getValue()
127 # normalize stamps
128 self.stamp_im.image.array /= self.annularFlux
129 return None
132class BrightStarStamps(StampsBase):
133 """Collection of bright star stamps and associated metadata.
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``.
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.
166 Notes
167 -----
168 A butler can be used to read only a part of the stamps, specified by a
169 bbox:
171 >>> starSubregions = butler.get("brightStarStamps_sub", dataId, bbox=bbox)
172 """
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(False, innerRadius, outerRadius)
180 self._innerRadius, self._outerRadius = innerRadius, outerRadius
181 if innerRadius is not None and outerRadius is not None:
182 self.normalized = True
183 else:
184 self.normalized = False
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.
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.
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.
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
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["G_MAGS"] = self.getMagnitudes()
270 self._metadata["GAIA_IDS"] = self.getGaiaIds()
271 self._metadata["ANNULAR_FLUXES"] = self.getAnnularFluxes()
272 self._metadata["NORMALIZED"] = self.normalized
273 self._metadata["INNER_RADIUS"] = self._innerRadius
274 self._metadata["OUTER_RADIUS"] = self._outerRadius
275 return None
277 @classmethod
278 def readFits(cls, filename):
279 """Build an instance of this class from a file.
281 Parameters
282 ----------
283 filename : `str`
284 Name of the file to read
285 """
286 return cls.readFitsWithOptions(filename, None)
288 @classmethod
289 def readFitsWithOptions(cls, filename, options):
290 """Build an instance of this class with options.
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'])
309 def append(self, item, innerRadius=None, outerRadius=None):
310 """Add an additional bright star stamp.
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.normalized:
328 raise AttributeError("Trying to append an unnormalized stamp to a normalized BrightStarStamps "
329 "instance, or vice-versa.")
330 else:
331 self._checkRadius(innerRadius, outerRadius)
332 self._stamps.append(item)
333 return None
335 def extend(self, bss):
336 """Extend BrightStarStamps instance by appending elements from another
337 instance.
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(bss._innerRadius, bss._outerRadius)
348 self._stamps += bss._stamps
350 def getMagnitudes(self):
351 """Retrieve Gaia G magnitudes for each star.
353 Returns
354 -------
355 gaiaGMags : `list` [`float`]
356 """
357 return [stamp.gaiaGMag for stamp in self._stamps]
359 def getGaiaIds(self):
360 """Retrieve Gaia IDs for each star.
362 Returns
363 -------
364 gaiaIds : `list` [`int`]
365 """
366 return [stamp.gaiaId for stamp in self._stamps]
368 def getAnnularFluxes(self):
369 """Retrieve normalization factors for each star.
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.
376 Returns
377 -------
378 annularFluxes : `list` [`float`]
379 """
380 return [stamp.annularFlux for stamp in self._stamps]
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).
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
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,
400 metadata=self._metadata)
401 instance._stamps = subset
402 return instance
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:
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}).")
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.getAnnularFluxes().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