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

111 statements  

« prev     ^ index     » next       coverage.py v6.4.1, created at 2022-06-11 02:47 -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__ = ["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 

80def heavyFootprintToImage(heavy, fill=np.nan, bbox=None, imageType=MaskedImage): 

81 """Create an image of a HeavyFootprint 

82 

83 Parameters 

84 ---------- 

85 heavy : `HeavyFootprint` 

86 The HeavyFootprint to insert into the image 

87 fill: number 

88 Number to fill the pixels in the image that are not 

89 contained in `heavy`. 

90 bbox : `Box2I` 

91 Bounding box of the output image. 

92 imageType : `type` 

93 This should be either a `MaskedImage` or `Image` and describes 

94 the type of the output image. 

95 

96 Returns 

97 ------- 

98 image : `lsst.afw.image.MaskedImage` or `lsst.afw.image.Image` 

99 An image defined by `bbox` and padded with `fill` that 

100 contains the projected flux in `heavy`. 

101 """ 

102 if bbox is None: 

103 bbox = heavy.getBBox() 

104 image = imageType(bbox, dtype=heavy.getImageArray().dtype) 

105 image.set(fill) 

106 heavy.insert(image) 

107 return image 

108 

109 

110class MultibandFootprint(MultibandBase): 

111 """Multiband Footprint class 

112 

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

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

115 

116 Parameters 

117 ---------- 

118 filters : `list` 

119 List of filter names. 

120 singles : `list` 

121 A list of single band `HeavyFootprint` objects. 

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

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

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

125 """ 

126 def __init__(self, filters, singles): 

127 super().__init__(filters, singles) 

128 # Ensure that all HeavyFootprints have the same SpanSet 

129 spans = singles[0].getSpans() 

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

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

132 

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

134 footprint = Footprint(spans) 

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

136 self._footprint = footprint 

137 

138 @staticmethod 

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

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

141 

142 Parameters 

143 ---------- 

144 filters : `list` 

145 List of filter names. 

146 image: array 

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

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

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

150 mask : array 

151 Mask for the `image` array. 

152 variance : array 

153 Variance of the `image` array. 

154 footprint : `Footprint` 

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

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

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

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

159 xy0 : `Point2I` 

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

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

162 `images`. 

163 thresh : `float` or list of floats 

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

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

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

167 peaks : `PeakCatalog` 

168 Catalog containing information about the peaks located in the 

169 footprints. 

170 

171 Returns 

172 ------- 

173 result : `MultibandFootprint` 

174 MultibandFootprint created from the arrays 

175 """ 

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

177 if footprint is None: 

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

179 footprint = Footprint(spans) 

180 else: 

181 imageBBox = footprint.getBBox() 

182 

183 if peaks is not None: 

184 footprint.setPeakCatalog(peaks) 

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

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

187 return MultibandFootprint(filters, singles) 

188 

189 @staticmethod 

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

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

192 

193 Parameters 

194 ---------- 

195 filters : `list` 

196 List of filter names. 

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

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

199 to convert into `HeavyFootprint` objects. 

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

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

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

203 Mask for the `image`. 

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

205 Variance of the `image`. 

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

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

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

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

210 peaks : `PeakCatalog` 

211 Catalog containing information about the peaks located in the 

212 footprints. 

213 

214 Returns 

215 ------- 

216 result : `MultibandFootprint` 

217 MultibandFootprint created from the image, mask, and variance 

218 """ 

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

220 if footprint is None: 

221 spans, imageBBox = getSpanSetFromImages(image, thresh) 

222 footprint = Footprint(spans) 

223 

224 if peaks is not None: 

225 footprint.setPeakCatalog(peaks) 

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

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

228 return MultibandFootprint(filters, singles) 

229 

230 @staticmethod 

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

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

233 

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

235 

236 Parameters 

237 ---------- 

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

239 MaskedImages to extract the single band heavy footprints from. 

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

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

242 to the `PeakCatalog`. 

243 

244 Returns 

245 ------- 

246 result : `MultibandFootprint` 

247 MultibandFootprint created from the image, mask, and variance 

248 """ 

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

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

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

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

253 

254 def getSpans(self): 

255 """Get the full `SpanSet`""" 

256 return self._footprint.getSpans() 

257 

258 @property 

259 def footprint(self): 

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

261 return self._footprint 

262 

263 @property 

264 def mMaskedImage(self): 

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

266 return self._mMaskedImage 

267 

268 @property 

269 def spans(self): 

270 """`SpanSet` of the `MultibandFootprint`""" 

271 return self._footprint.getSpans() 

272 

273 def getPeaks(self): 

274 """Get the `PeakCatalog`""" 

275 return self._footprint.getPeaks() 

276 

277 @property 

278 def peaks(self): 

279 """`PeakCatalog` of the `MultibandFootprint`""" 

280 return self._footprint.getPeaks() 

281 

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

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

284 

285 `MultibandFootprint` objects cannot be sliced along the image 

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

287 

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

289 """ 

290 if len(indices) > 0: 

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

292 

293 if isinstance(filterIndex, slice): 

294 singles = self.singles[filterIndex] 

295 else: 

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

297 

298 return MultibandFootprint(filters, singles) 

299 

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

301 """Convert a `MultibandFootprint` to a `MultibandImage` 

302 

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

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

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

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

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

308 the images. 

309 

310 Parameters 

311 ---------- 

312 bbox : `Box2I` 

313 Bounding box of the resulting image. 

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

315 of the footprint is used. 

316 fill : `float` 

317 Value to use for any pixel in the resulting image 

318 outside of the `SpanSet`. 

319 imageType : `type` 

320 This should be either a `MultibandMaskedImage` 

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

322 

323 Returns 

324 ------- 

325 result : `MultibandBase` 

326 The resulting `MultibandImage` or `MultibandMaskedImage` created 

327 from the `MultibandHeavyFootprint`. 

328 """ 

329 if imageType == MultibandMaskedImage: 

330 singleType = MaskedImage 

331 elif imageType == MultibandImage: 

332 singleType = Image 

333 else: 

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

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

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

337 return mMaskedImage 

338 

339 def clone(self, deep=True): 

340 """Copy the current object 

341 

342 Parameters 

343 ---------- 

344 deep : `bool` 

345 Whether or not to make a deep copy 

346 

347 Returns 

348 ------- 

349 result : `MultibandFootprint` 

350 The cloned footprint. 

351 """ 

352 if deep: 

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

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

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

356 mMaskedImage = self.getImage() 

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

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

359 else: 

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

361 return result