Coverage for python/lsst/afw/detection/multiband.py: 20%

104 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-12 02:30 -0800

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__ = ["MultibandFootprint"] 

23 

24import numpy as np 

25 

26from lsst.geom import Point2I 

27from lsst.afw.geom import SpanSet 

28from lsst.afw.image.image import Mask, Image, MultibandImage, MultibandMaskedImage 

29from lsst.afw.image.maskedImage import MaskedImage 

30 

31from lsst.afw.multiband import MultibandBase 

32from . import Footprint, makeHeavyFootprint 

33 

34 

35def getSpanSetFromImages(images, thresh=0, xy0=None): 

36 """Create a Footprint from a set of Images 

37 

38 Parameters 

39 ---------- 

40 images : `lsst.afw.image.MultibandImage` or list of `lsst.afw.image.Image`, array 

41 Images to extract the footprint from 

42 thresh : `float` 

43 All pixels above `thresh` will be included in the footprint 

44 xy0 : `lsst.geom.Point2I` 

45 Location of the minimum value of the images bounding box 

46 (if images is an array, otherwise the image bounding box is used). 

47 

48 Returns 

49 ------- 

50 spans : `lsst.afw.geom.SpanSet` 

51 Union of all spans in the images above the threshold 

52 imageBBox : `lsst.afw.detection.Box2I` 

53 Bounding box for the input images. 

54 """ 

55 # Set the threshold for each band 

56 if not hasattr(thresh, "__len__"): 

57 thresh = [thresh] * len(images) 

58 

59 # If images is a list of `afw Image` objects then 

60 # merge the SpanSet in each band into a single Footprint 

61 if isinstance(images, MultibandBase) or isinstance(images[0], Image): 

62 spans = SpanSet() 

63 for n, image in enumerate(images): 

64 mask = image.array > thresh[n] 

65 mask = Mask(mask.astype(np.int32), xy0=image.getBBox().getMin()) 

66 spans = spans.union(SpanSet.fromMask(mask)) 

67 imageBBox = images[0].getBBox() 

68 else: 

69 # Use thresh to detect the pixels above the threshold in each band 

70 thresh = np.array(thresh) 

71 if xy0 is None: 

72 xy0 = Point2I(0, 0) 

73 mask = np.any(images > thresh[:, None, None], axis=0) 

74 mask = Mask(mask.astype(np.int32), xy0=xy0) 

75 spans = SpanSet.fromMask(mask) 

76 imageBBox = mask.getBBox() 

77 return spans, imageBBox 

78 

79 

80class MultibandFootprint(MultibandBase): 

81 """Multiband Footprint class 

82 

83 A `MultibandFootprint` is a collection of HeavyFootprints that have 

84 the same `SpanSet` and `peakCatalog` but different flux in each band. 

85 

86 Parameters 

87 ---------- 

88 filters : `list` 

89 List of filter names. 

90 singles : `list` 

91 A list of single band `HeavyFootprint` objects. 

92 Each `HeavyFootprint` should have the same `PeakCatalog` 

93 and the same `SpanSet`, however to save CPU cycles there 

94 is no internal check for consistency of the peak catalog. 

95 """ 

96 def __init__(self, filters, singles): 

97 super().__init__(filters, singles) 

98 # Ensure that all HeavyFootprints have the same SpanSet 

99 spans = singles[0].getSpans() 

100 if not all([heavy.getSpans() == spans for heavy in singles]): 

101 raise ValueError("All HeavyFootprints in singles are expected to have the same SpanSet") 

102 

103 # Assume that all footprints have the same SpanSet and PeakCatalog 

104 footprint = Footprint(spans) 

105 footprint.setPeakCatalog(singles[0].getPeaks()) 

106 self._footprint = footprint 

107 

108 @staticmethod 

109 def fromArrays(filters, image, mask=None, variance=None, footprint=None, xy0=None, thresh=0, peaks=None): 

110 """Create a `MultibandFootprint` from an `image`, `mask`, `variance` 

111 

112 Parameters 

113 ---------- 

114 filters : `list` 

115 List of filter names. 

116 image: array 

117 An array to convert into `lsst.afw.detection.HeavyFootprint` objects. 

118 Only pixels above the `thresh` value for at least one band 

119 will be included in the `SpanSet` and resulting footprints. 

120 mask : array 

121 Mask for the `image` array. 

122 variance : array 

123 Variance of the `image` array. 

124 footprint : `Footprint` 

125 `Footprint` that contains the `SpanSet` and `PeakCatalog` 

126 to use for the `HeavyFootprint` in each band. 

127 If `footprint` is `None` then the `thresh` is used to create a 

128 `Footprint` based on the pixels above the `thresh` value. 

129 xy0 : `Point2I` 

130 If `image` is an array and `footprint` is `None` then specifying 

131 `xy0` gives the location of the minimum `x` and `y` value of the 

132 `images`. 

133 thresh : `float` or list of floats 

134 Threshold in each band (or the same threshold to be used in all bands) 

135 to include a pixel in the `SpanSet` of the `MultibandFootprint`. 

136 If `Footprint` is not `None` then `thresh` is ignored. 

137 peaks : `PeakCatalog` 

138 Catalog containing information about the peaks located in the 

139 footprints. 

140 

141 Returns 

142 ------- 

143 result : `MultibandFootprint` 

144 MultibandFootprint created from the arrays 

145 """ 

146 # Generate a new Footprint if one has not been specified 

147 if footprint is None: 

148 spans, imageBBox = getSpanSetFromImages(image, thresh, xy0) 

149 footprint = Footprint(spans) 

150 else: 

151 imageBBox = footprint.getBBox() 

152 

153 if peaks is not None: 

154 footprint.setPeakCatalog(peaks) 

155 mMaskedImage = MultibandMaskedImage.fromArrays(filters, image, mask, variance, imageBBox) 

156 singles = [makeHeavyFootprint(footprint, maskedImage) for maskedImage in mMaskedImage] 

157 return MultibandFootprint(filters, singles) 

158 

159 @staticmethod 

160 def fromImages(filters, image, mask=None, variance=None, footprint=None, thresh=0, peaks=None): 

161 """Create a `MultibandFootprint` from an `image`, `mask`, `variance` 

162 

163 Parameters 

164 ---------- 

165 filters : `list` 

166 List of filter names. 

167 image : `lsst.afw.image.MultibandImage`, or list of `lsst.afw.image.Image` 

168 A `lsst.afw.image.MultibandImage` (or collection of images in each band) 

169 to convert into `HeavyFootprint` objects. 

170 Only pixels above the `thresh` value for at least one band 

171 will be included in the `SpanSet` and resulting footprints. 

172 mask : `MultibandMask` or list of `Mask` 

173 Mask for the `image`. 

174 variance : `lsst.afw.image.MultibandImage`, or list of `lsst.afw.image.Image` 

175 Variance of the `image`. 

176 thresh : `float` or `list` of floats 

177 Threshold in each band (or the same threshold to be used in all bands) 

178 to include a pixel in the `SpanSet` of the `MultibandFootprint`. 

179 If `Footprint` is not `None` then `thresh` is ignored. 

180 peaks : `PeakCatalog` 

181 Catalog containing information about the peaks located in the 

182 footprints. 

183 

184 Returns 

185 ------- 

186 result : `MultibandFootprint` 

187 MultibandFootprint created from the image, mask, and variance 

188 """ 

189 # Generate a new Footprint if one has not been specified 

190 if footprint is None: 

191 spans, imageBBox = getSpanSetFromImages(image, thresh) 

192 footprint = Footprint(spans) 

193 

194 if peaks is not None: 

195 footprint.setPeakCatalog(peaks) 

196 mMaskedImage = MultibandMaskedImage(filters, image, mask, variance) 

197 singles = [makeHeavyFootprint(footprint, maskedImage) for maskedImage in mMaskedImage] 

198 return MultibandFootprint(filters, singles) 

199 

200 @staticmethod 

201 def fromMaskedImages(filters, maskedImages, footprint=None, thresh=0, peaks=None): 

202 """Create a `MultibandFootprint` from a list of `MaskedImage` 

203 

204 See `fromImages` for a description of the parameters not listed below 

205 

206 Parameters 

207 ---------- 

208 maskedImages : `list` of `lsst.afw.image.MaskedImage` 

209 MaskedImages to extract the single band heavy footprints from. 

210 Like `fromImages`, if a `footprint` is not specified then all 

211 pixels above `thresh` will be used, and `peaks` will be added 

212 to the `PeakCatalog`. 

213 

214 Returns 

215 ------- 

216 result : `MultibandFootprint` 

217 MultibandFootprint created from the image, mask, and variance 

218 """ 

219 image = [maskedImage.image for maskedImage in maskedImages] 

220 mask = [maskedImage.mask for maskedImage in maskedImages] 

221 variance = [maskedImage.variance for maskedImage in maskedImages] 

222 return MultibandFootprint.fromImages(filters, image, mask, variance, footprint, thresh, peaks) 

223 

224 def getSpans(self): 

225 """Get the full `SpanSet`""" 

226 return self._footprint.getSpans() 

227 

228 @property 

229 def footprint(self): 

230 """Common SpanSet and peak catalog for the single band footprints""" 

231 return self._footprint 

232 

233 @property 

234 def mMaskedImage(self): 

235 """MultibandMaskedImage that the footprints present a view into""" 

236 return self._mMaskedImage 

237 

238 @property 

239 def spans(self): 

240 """`SpanSet` of the `MultibandFootprint`""" 

241 return self._footprint.getSpans() 

242 

243 def getPeaks(self): 

244 """Get the `PeakCatalog`""" 

245 return self._footprint.getPeaks() 

246 

247 @property 

248 def peaks(self): 

249 """`PeakCatalog` of the `MultibandFootprint`""" 

250 return self._footprint.getPeaks() 

251 

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

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

254 

255 `MultibandFootprint` objects cannot be sliced along the image 

256 dimension, so an error is thrown if `indices` has any elements. 

257 

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

259 """ 

260 if len(indices) > 0: 

261 raise IndexError("MultibandFootprints can only be sliced in the filter dimension") 

262 

263 if isinstance(filterIndex, slice): 

264 singles = self.singles[filterIndex] 

265 else: 

266 singles = [self.singles[idx] for idx in filterIndex] 

267 

268 return MultibandFootprint(filters, singles) 

269 

270 def getImage(self, bbox=None, fill=np.nan, imageType=MultibandMaskedImage): 

271 """Convert a `MultibandFootprint` to a `MultibandImage` 

272 

273 This returns the heavy footprints converted into an `MultibandImage` or 

274 `MultibandMaskedImage` (depending on `imageType`). 

275 This might be different than the internal `mMaskedImage` property 

276 of the `MultibandFootprint`, as the `mMaskedImage` might contain 

277 some non-zero pixels not contained in the footprint but present in 

278 the images. 

279 

280 Parameters 

281 ---------- 

282 bbox : `Box2I` 

283 Bounding box of the resulting image. 

284 If no bounding box is specified, then the bounding box 

285 of the footprint is used. 

286 fill : `float` 

287 Value to use for any pixel in the resulting image 

288 outside of the `SpanSet`. 

289 imageType : `type` 

290 This should be either a `MultibandMaskedImage` 

291 or `MultibandImage` and describes the type of the output image. 

292 

293 Returns 

294 ------- 

295 result : `MultibandBase` 

296 The resulting `MultibandImage` or `MultibandMaskedImage` created 

297 from the `MultibandHeavyFootprint`. 

298 """ 

299 if imageType == MultibandMaskedImage: 

300 singleType = MaskedImage 

301 elif imageType == MultibandImage: 

302 singleType = Image 

303 else: 

304 raise TypeError("Expected imageType to be either MultibandImage or MultibandMaskedImage") 

305 maskedImages = [heavy.extractImage(fill, bbox, singleType) for heavy in self.singles] 

306 mMaskedImage = imageType.fromImages(self.filters, maskedImages) 

307 return mMaskedImage 

308 

309 def clone(self, deep=True): 

310 """Copy the current object 

311 

312 Parameters 

313 ---------- 

314 deep : `bool` 

315 Whether or not to make a deep copy 

316 

317 Returns 

318 ------- 

319 result : `MultibandFootprint` 

320 The cloned footprint. 

321 """ 

322 if deep: 

323 footprint = Footprint(self.footprint.getSpans()) 

324 for peak in self.footprint.getPeaks(): 

325 footprint.addPeak(peak.getX(), peak.getY(), peak.getValue()) 

326 mMaskedImage = self.getImage() 

327 filters = tuple([f for f in self.filters]) 

328 result = MultibandFootprint.fromMaskedImages(filters, mMaskedImage, footprint) 

329 else: 

330 result = MultibandFootprint(self.filters, self.singles) 

331 return result