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

101 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-19 04:04 -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, MultibandImage 

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 Attributes 

39 ---------- 

40 missingBands: `list[str]` 

41 The bands for which the PSF could not be calculated. 

42 position: `Point2D` 

43 The point at which the PSF could not be calcualted in the 

44 missing bands. 

45 partialPsf: `MultibandImage` 

46 The image of the PSF using only the bands that successfully 

47 computed a PSF image. 

48 

49 Parameters 

50 ---------- 

51 bands : `list` of `str` 

52 The full list of bands in the `MultibandExposure` generating 

53 the PSF. 

54 """ 

55 def __init__(self, bands, position, partialPsf): 

56 missingBands = [band for band in bands if band not in partialPsf.filters] 

57 

58 self.missingBands = missingBands 

59 self.position = position 

60 self.partialPsf = partialPsf 

61 message = f"Failed to compute PSF at {position} in {missingBands}" 

62 super().__init__(message) 

63 

64 

65def computePsfImage(psfModels, position, useKernelImage=True): 

66 """Get a multiband PSF image 

67 

68 The PSF Image or PSF Kernel Image is computed for each band 

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

70 

71 Parameters 

72 ---------- 

73 psfModels : `dict[str, lsst.afw.detection.Psf]` 

74 The list of PSFs in each band. 

75 position : `Point2D` or `tuple` 

76 Coordinates to evaluate the PSF. 

77 useKernelImage: `bool` 

78 Execute ``Psf.computeKernelImage`` when ``True`, 

79 ``PSF/computeImage`` when ``False``. 

80 

81 Returns 

82 ------- 

83 psfs: `lsst.afw.image.MultibandImage` 

84 The multiband PSF image. 

85 """ 

86 psfs = {} 

87 # Make the coordinates into a Point2D (if necessary) 

88 if not isinstance(position, Point2D): 

89 position = Point2D(position[0], position[1]) 

90 

91 incomplete = False 

92 

93 for band, psfModel in psfModels.items(): 

94 try: 

95 if useKernelImage: 

96 psf = psfModel.computeKernelImage(position) 

97 else: 

98 psf = psfModel.computeImage(position) 

99 psfs[band] = psf 

100 except InvalidParameterError: 

101 incomplete = True 

102 

103 left = np.min([psf.getBBox().getMinX() for psf in psfs.values()]) 

104 bottom = np.min([psf.getBBox().getMinY() for psf in psfs.values()]) 

105 right = np.max([psf.getBBox().getMaxX() for psf in psfs.values()]) 

106 top = np.max([psf.getBBox().getMaxY() for psf in psfs.values()]) 

107 bbox = Box2I(Point2I(left, bottom), Point2I(right, top)) 

108 

109 psf_images = [projectImage(psf, bbox) for psf in psfs.values()] 

110 

111 mPsf = MultibandImage.fromImages(list(psfs.keys()), psf_images) 

112 

113 if incomplete: 

114 raise IncompleteDataError(list(psfModels.keys()), position, mPsf) 

115 

116 return mPsf 

117 

118 

119class MultibandExposure(MultibandTripleBase): 

120 """MultibandExposure class 

121 

122 This class acts as a container for multiple `afw.Exposure` objects. 

123 All exposures must have the same bounding box, and the associated 

124 images must all have the same data type. 

125 

126 See `MultibandTripleBase` for parameter definitions. 

127 """ 

128 def __init__(self, filters, image, mask, variance, psfs=None): 

129 super().__init__(filters, image, mask, variance) 

130 if psfs is not None: 

131 for psf, exposure in zip(psfs, self.singles): 

132 exposure.setPsf(psf) 

133 

134 @staticmethod 

135 def fromExposures(filters, singles): 

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

137 

138 see `tripleFromExposures` for a description of parameters 

139 """ 

140 psfs = [s.getPsf() for s in singles] 

141 return tripleFromSingles(MultibandExposure, filters, singles, psfs=psfs) 

142 

143 @staticmethod 

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

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

146 

147 see `tripleFromArrays` for a description of parameters 

148 """ 

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

150 

151 @staticmethod 

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

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

154 

155 see `makeTripleFromKwargs` for a description of parameters 

156 """ 

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

158 

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

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

161 

162 Parameters 

163 ---------- 

164 image: `list` 

165 List of `Image` objects that represent the image in each band. 

166 mask: `list` 

167 List of `Mask` objects that represent the mask in each band. 

168 variance: `list` 

169 List of `Image` objects that represent the variance in each band. 

170 

171 Returns 

172 ------- 

173 singles: tuple 

174 Tuple of `MaskedImage` objects for each band, 

175 where the `image`, `mask`, and `variance` of each `single` 

176 point to the multiband objects. 

177 """ 

178 singles = [] 

179 if image is None: 

180 image = self.image 

181 if mask is None: 

182 mask = self.mask 

183 if variance is None: 

184 variance = self.variance 

185 

186 dtype = image.array.dtype 

187 for f in self.filters: 

188 maskedImage = MaskedImage(image=image[f], mask=mask[f], variance=variance[f], dtype=dtype) 

189 single = Exposure(maskedImage, dtype=dtype) 

190 singles.append(single) 

191 return tuple(singles) 

192 

193 @staticmethod 

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

195 """Load a multiband exposure from a butler 

196 

197 Because each band is stored in a separate exposure file, 

198 this method can be used to load all of the exposures for 

199 a given set of bands 

200 

201 Parameters 

202 ---------- 

203 butler: `lsst.daf.butler.Butler` 

204 Butler connection to use to load the single band 

205 calibrated images 

206 bands: `list` or `str` 

207 List of names for each band 

208 args: `list` 

209 Arguments to the Butler. 

210 kwargs: `dict` 

211 Keyword arguments to pass to the Butler 

212 that are the same in all bands. 

213 

214 Returns 

215 ------- 

216 result: `MultibandExposure` 

217 The new `MultibandExposure` created by combining all of the 

218 single band exposures. 

219 """ 

220 # Load the Exposure in each band 

221 exposures = [] 

222 for band in bands: 

223 exposures.append(butler.get(*args, band=band, **kwargs)) 

224 return MultibandExposure.fromExposures(bands, exposures) 

225 

226 def computePsfKernelImage(self, position): 

227 """Get a multiband PSF image 

228 

229 The PSF Kernel Image is computed for each band 

230 and combined into a (filter, y, x) array and stored 

231 as `self._psfImage`. 

232 The result is not cached, so if the same PSF is expected 

233 to be used multiple times it is a good idea to store the 

234 result in another variable. 

235 

236 Parameters 

237 ---------- 

238 position: `Point2D` or `tuple` 

239 Coordinates to evaluate the PSF. 

240 

241 Returns 

242 ------- 

243 self._psfImage: array 

244 The multiband PSF image. 

245 """ 

246 return computePsfImage( 

247 psfModels=self.getPsfs(), 

248 position=position, 

249 useKernelImage=True, 

250 ) 

251 

252 def computePsfImage(self, position=None): 

253 """Get a multiband PSF image 

254 

255 The PSF Kernel Image is computed for each band 

256 and combined into a (filter, y, x) array and stored 

257 as `self._psfImage`. 

258 The result is not cached, so if the same PSF is expected 

259 to be used multiple times it is a good idea to store the 

260 result in another variable. 

261 

262 Parameters 

263 ---------- 

264 position: `Point2D` or `tuple` 

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

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

267 

268 Returns 

269 ------- 

270 self._psfImage: array 

271 The multiband PSF image. 

272 """ 

273 return computePsfImage( 

274 psfModels=self.getPsfs(), 

275 position=position, 

276 useKernelImage=True, 

277 ) 

278 

279 def getPsfs(self): 

280 """Extract the PSF model in each band 

281 

282 Returns 

283 ------- 

284 psfs : `dict` of `lsst.afw.detection.Psf` 

285 The PSF in each band 

286 """ 

287 return {band: self[band].getPsf() for band in self.filters} 

288 

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

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

291 

292 See `Multiband._slice` for a list of the parameters. 

293 This overwrites the base method to attach the PSF to 

294 each individual exposure. 

295 """ 

296 image = self.image._slice(filters, filterIndex, indices) 

297 if self.mask is not None: 

298 mask = self._mask._slice(filters, filterIndex, indices) 

299 else: 

300 mask = None 

301 if self.variance is not None: 

302 variance = self._variance._slice(filters, filterIndex, indices) 

303 else: 

304 variance = None 

305 

306 # If only a single pixel is selected, return the tuple of MultibandPixels 

307 if isinstance(image, MultibandPixel): 

308 if mask is not None: 

309 assert isinstance(mask, MultibandPixel) 

310 if variance is not None: 

311 assert isinstance(variance, MultibandPixel) 

312 return (image, mask, variance) 

313 

314 _psfs = self.getPsfs() 

315 psfs = [_psfs[band] for band in filters] 

316 

317 result = MultibandExposure( 

318 filters=filters, 

319 image=image, 

320 mask=mask, 

321 variance=variance, 

322 psfs=psfs, 

323 ) 

324 

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

326 return result