Coverage for python/lsst/afw/image/_exposure/_multiband.py: 20%
90 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-22 02:38 -0700
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-22 02:38 -0700
1# This file is part of afw.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://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 <http://www.gnu.org/licenses/>.
22__all__ = ["MultibandExposure", "computePsfImage", "IncompleteDataError"]
24import numpy as np
26from lsst.geom import Point2D, Point2I, Box2I
27from lsst.pex.exceptions import InvalidParameterError
28from . import Exposure, ExposureF
29from ..utils import projectImage
30from .._image._multiband import MultibandTripleBase, MultibandPixel
31from .._image._multiband import tripleFromSingles, tripleFromArrays, makeTripleFromKwargs
32from .._maskedImage import MaskedImage
35class IncompleteDataError(Exception):
36 """The PSF could not be computed due to incomplete data
37 """
38 pass
41def computePsfImage(psfModels, position, bands, useKernelImage=True):
42 """Get a multiband PSF image
44 The PSF Kernel Image is computed for each band
45 and combined into a (filter, y, x) array.
47 Parameters
48 ----------
49 psfList : `list` of `lsst.afw.detection.Psf`
50 The list of PSFs in each band.
51 position : `Point2D` or `tuple`
52 Coordinates to evaluate the PSF.
53 bands: `list` or `str`
54 List of names for each band
55 Returns
56 -------
57 psfs: `np.ndarray`
58 The multiband PSF image.
59 """
60 psfs = []
61 # Make the coordinates into a Point2D (if necessary)
62 if not isinstance(position, Point2D):
63 position = Point2D(position[0], position[1])
65 for bidx, psfModel in enumerate(psfModels):
66 try:
67 if useKernelImage:
68 psf = psfModel.computeKernelImage(position)
69 else:
70 psf = psfModel.computeImage(position)
71 psfs.append(psf)
72 except InvalidParameterError:
73 # This band failed to compute the PSF due to incomplete data
74 # at that location. This is unlikely to be a problem for Rubin,
75 # however the edges of some HSC COSMOS fields contain incomplete
76 # data in some bands, so we track this error to distinguish it
77 # from unknown errors.
78 msg = "Failed to compute PSF at {} in band {}"
79 raise IncompleteDataError(msg.format(position, bands[bidx])) from None
81 left = np.min([psf.getBBox().getMinX() for psf in psfs])
82 bottom = np.min([psf.getBBox().getMinY() for psf in psfs])
83 right = np.max([psf.getBBox().getMaxX() for psf in psfs])
84 top = np.max([psf.getBBox().getMaxY() for psf in psfs])
85 bbox = Box2I(Point2I(left, bottom), Point2I(right, top))
86 psfs = np.array([projectImage(psf, bbox).array for psf in psfs])
87 return psfs
90class MultibandExposure(MultibandTripleBase):
91 """MultibandExposure class
93 This class acts as a container for multiple `afw.Exposure` objects.
94 All exposures must have the same bounding box, and the associated
95 images must all have the same data type.
97 See `MultibandTripleBase` for parameter definitions.
98 """
99 def __init__(self, filters, image, mask, variance, psfs=None):
100 super().__init__(filters, image, mask, variance)
101 if psfs is not None:
102 for psf, exposure in zip(psfs, self.singles):
103 exposure.setPsf(psf)
105 @staticmethod
106 def fromExposures(filters, singles):
107 """Construct a MultibandImage from a collection of single band images
109 see `tripleFromExposures` for a description of parameters
110 """
111 psfs = [s.getPsf() for s in singles]
112 return tripleFromSingles(MultibandExposure, filters, singles, psfs=psfs)
114 @staticmethod
115 def fromArrays(filters, image, mask, variance, bbox=None):
116 """Construct a MultibandExposure from a collection of arrays
118 see `tripleFromArrays` for a description of parameters
119 """
120 return tripleFromArrays(MultibandExposure, filters, image, mask, variance, bbox)
122 @staticmethod
123 def fromKwargs(filters, filterKwargs, singleType=ExposureF, **kwargs):
124 """Build a MultibandImage from a set of keyword arguments
126 see `makeTripleFromKwargs` for a description of parameters
127 """
128 return makeTripleFromKwargs(MultibandExposure, filters, filterKwargs, singleType, **kwargs)
130 def _buildSingles(self, image=None, mask=None, variance=None):
131 """Make a new list of single band objects
133 Parameters
134 ----------
135 image: `list`
136 List of `Image` objects that represent the image in each band.
137 mask: `list`
138 List of `Mask` objects that represent the mask in each band.
139 variance: `list`
140 List of `Image` objects that represent the variance in each band.
142 Returns
143 -------
144 singles: tuple
145 Tuple of `MaskedImage` objects for each band,
146 where the `image`, `mask`, and `variance` of each `single`
147 point to the multiband objects.
148 """
149 singles = []
150 if image is None:
151 image = self.image
152 if mask is None:
153 mask = self.mask
154 if variance is None:
155 variance = self.variance
157 dtype = image.array.dtype
158 for f in self.filters:
159 maskedImage = MaskedImage(image=image[f], mask=mask[f], variance=variance[f], dtype=dtype)
160 single = Exposure(maskedImage, dtype=dtype)
161 singles.append(single)
162 return tuple(singles)
164 @staticmethod
165 def fromButler(butler, bands, *args, **kwargs):
166 """Load a multiband exposure from a butler
168 Because each band is stored in a separate exposure file,
169 this method can be used to load all of the exposures for
170 a given set of bands
172 Parameters
173 ----------
174 butler: `lsst.daf.butler.Butler`
175 Butler connection to use to load the single band
176 calibrated images
177 bands: `list` or `str`
178 List of names for each band
179 args: `list`
180 Arguments to the Butler.
181 kwargs: `dict`
182 Keyword arguments to pass to the Butler
183 that are the same in all bands.
185 Returns
186 -------
187 result: `MultibandExposure`
188 The new `MultibandExposure` created by combining all of the
189 single band exposures.
190 """
191 # Load the Exposure in each band
192 exposures = []
193 for band in bands:
194 exposures.append(butler.get(*args, band=band, **kwargs))
195 return MultibandExposure.fromExposures(bands, exposures)
197 def computePsfKernelImage(self, position):
198 """Get a multiband PSF image
200 The PSF Kernel Image is computed for each band
201 and combined into a (filter, y, x) array and stored
202 as `self._psfImage`.
203 The result is not cached, so if the same PSF is expected
204 to be used multiple times it is a good idea to store the
205 result in another variable.
207 Parameters
208 ----------
209 position: `Point2D` or `tuple`
210 Coordinates to evaluate the PSF.
212 Returns
213 -------
214 self._psfImage: array
215 The multiband PSF image.
216 """
217 return computePsfImage(
218 psfModels=self.getPsfs(),
219 position=position,
220 bands=self.filters,
221 useKernelImage=True,
222 )
224 def computePsfImage(self, position=None):
225 """Get a multiband PSF image
227 The PSF Kernel Image is computed for each band
228 and combined into a (filter, y, x) array and stored
229 as `self._psfImage`.
230 The result is not cached, so if the same PSF is expected
231 to be used multiple times it is a good idea to store the
232 result in another variable.
234 Parameters
235 ----------
236 position: `Point2D` or `tuple`
237 Coordinates to evaluate the PSF. If `position` is `None`
238 then `Psf.getAveragePosition()` is used.
240 Returns
241 -------
242 self._psfImage: array
243 The multiband PSF image.
244 """
245 return computePsfImage(
246 psfModels=self.getPsfs(),
247 position=position,
248 bands=self.filters,
249 useKernelImage=True,
250 )
252 def getPsfs(self):
253 """Extract the PSF model in each band
255 Returns
256 -------
257 psfs : `list` of `lsst.afw.detection.Psf`
258 The PSF in each band
259 """
260 return [s.getPsf() for s in self]
262 def _slice(self, filters, filterIndex, indices):
263 """Slice the current object and return the result
265 See `Multiband._slice` for a list of the parameters.
266 This overwrites the base method to attach the PSF to
267 each individual exposure.
268 """
269 image = self.image._slice(filters, filterIndex, indices)
270 if self.mask is not None:
271 mask = self._mask._slice(filters, filterIndex, indices)
272 else:
273 mask = None
274 if self.variance is not None:
275 variance = self._variance._slice(filters, filterIndex, indices)
276 else:
277 variance = None
279 # If only a single pixel is selected, return the tuple of MultibandPixels
280 if isinstance(image, MultibandPixel):
281 if mask is not None:
282 assert isinstance(mask, MultibandPixel)
283 if variance is not None:
284 assert isinstance(variance, MultibandPixel)
285 return (image, mask, variance)
287 result = MultibandExposure(
288 filters=filters,
289 image=image,
290 mask=mask,
291 variance=variance,
292 psfs=self.getPsfs(),
293 )
295 assert all([r.getBBox() == result._bbox for r in [result._mask, result._variance]])
296 return result