Coverage for python/lsst/afw/image/_exposure/_multiband.py: 20%

90 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-24 02:29 -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/>. 

21 

22__all__ = ["MultibandExposure", "computePsfImage", "IncompleteDataError"] 

23 

24import numpy as np 

25 

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 

33 

34 

35class IncompleteDataError(Exception): 

36 """The PSF could not be computed due to incomplete data 

37 """ 

38 pass 

39 

40 

41def computePsfImage(psfModels, position, bands, useKernelImage=True): 

42 """Get a multiband PSF image 

43 

44 The PSF Kernel Image is computed for each band 

45 and combined into a (filter, y, x) array. 

46 

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]) 

64 

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 

80 

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 

88 

89 

90class MultibandExposure(MultibandTripleBase): 

91 """MultibandExposure class 

92 

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. 

96 

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) 

104 

105 @staticmethod 

106 def fromExposures(filters, singles): 

107 """Construct a MultibandImage from a collection of single band images 

108 

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) 

113 

114 @staticmethod 

115 def fromArrays(filters, image, mask, variance, bbox=None): 

116 """Construct a MultibandExposure from a collection of arrays 

117 

118 see `tripleFromArrays` for a description of parameters 

119 """ 

120 return tripleFromArrays(MultibandExposure, filters, image, mask, variance, bbox) 

121 

122 @staticmethod 

123 def fromKwargs(filters, filterKwargs, singleType=ExposureF, **kwargs): 

124 """Build a MultibandImage from a set of keyword arguments 

125 

126 see `makeTripleFromKwargs` for a description of parameters 

127 """ 

128 return makeTripleFromKwargs(MultibandExposure, filters, filterKwargs, singleType, **kwargs) 

129 

130 def _buildSingles(self, image=None, mask=None, variance=None): 

131 """Make a new list of single band objects 

132 

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. 

141 

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 

156 

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) 

163 

164 @staticmethod 

165 def fromButler(butler, bands, *args, **kwargs): 

166 """Load a multiband exposure from a butler 

167 

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 

171 

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. 

184 

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) 

196 

197 def computePsfKernelImage(self, position): 

198 """Get a multiband PSF image 

199 

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. 

206 

207 Parameters 

208 ---------- 

209 position: `Point2D` or `tuple` 

210 Coordinates to evaluate the PSF. 

211 

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 ) 

223 

224 def computePsfImage(self, position=None): 

225 """Get a multiband PSF image 

226 

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. 

233 

234 Parameters 

235 ---------- 

236 position: `Point2D` or `tuple` 

237 Coordinates to evaluate the PSF. If `position` is `None` 

238 then `Psf.getAveragePosition()` is used. 

239 

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 ) 

251 

252 def getPsfs(self): 

253 """Extract the PSF model in each band 

254 

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] 

261 

262 def _slice(self, filters, filterIndex, indices): 

263 """Slice the current object and return the result 

264 

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 

278 

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) 

286 

287 result = MultibandExposure( 

288 filters=filters, 

289 image=image, 

290 mask=mask, 

291 variance=variance, 

292 psfs=self.getPsfs(), 

293 ) 

294 

295 assert all([r.getBBox() == result._bbox for r in [result._mask, result._variance]]) 

296 return result