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

104 statements  

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

28 

29# Need to use the private name of the package for imports here because 

30# lsst.afw.image itself imports lsst.afw.detection 

31from lsst.afw.image._image import Mask, Image, MultibandImage, MultibandMaskedImage 

32from lsst.afw.image._maskedImage import MaskedImage 

33 

34from lsst.afw.multiband import MultibandBase 

35from . import Footprint, makeHeavyFootprint 

36 

37 

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

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

40 

41 Parameters 

42 ---------- 

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

44 Images to extract the footprint from 

45 thresh : `float` 

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

47 xy0 : `lsst.geom.Point2I` 

48 Location of the minimum value of the images bounding box 

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

50 

51 Returns 

52 ------- 

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

54 Union of all spans in the images above the threshold 

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

56 Bounding box for the input images. 

57 """ 

58 # Set the threshold for each band 

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

60 thresh = [thresh] * len(images) 

61 

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

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

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

65 spans = SpanSet() 

66 for n, image in enumerate(images): 

67 mask = image.array > thresh[n] 

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

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

70 imageBBox = images[0].getBBox() 

71 else: 

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

73 thresh = np.array(thresh) 

74 if xy0 is None: 

75 xy0 = Point2I(0, 0) 

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

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

78 spans = SpanSet.fromMask(mask) 

79 imageBBox = mask.getBBox() 

80 return spans, imageBBox 

81 

82 

83class MultibandFootprint(MultibandBase): 

84 """Multiband Footprint class 

85 

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

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

88 

89 Parameters 

90 ---------- 

91 filters : `list` 

92 List of filter names. 

93 singles : `list` 

94 A list of single band `HeavyFootprint` objects. 

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

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

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

98 """ 

99 def __init__(self, filters, singles): 

100 super().__init__(filters, singles) 

101 # Ensure that all HeavyFootprints have the same SpanSet 

102 spans = singles[0].getSpans() 

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

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

105 

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

107 footprint = Footprint(spans) 

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

109 self._footprint = footprint 

110 

111 @staticmethod 

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

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

114 

115 Parameters 

116 ---------- 

117 filters : `list` 

118 List of filter names. 

119 image: array 

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

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

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

123 mask : array 

124 Mask for the `image` array. 

125 variance : array 

126 Variance of the `image` array. 

127 footprint : `Footprint` 

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

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

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

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

132 xy0 : `Point2I` 

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

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

135 `images`. 

136 thresh : `float` or list of floats 

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

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

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

140 peaks : `PeakCatalog` 

141 Catalog containing information about the peaks located in the 

142 footprints. 

143 

144 Returns 

145 ------- 

146 result : `MultibandFootprint` 

147 MultibandFootprint created from the arrays 

148 """ 

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

150 if footprint is None: 

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

152 footprint = Footprint(spans) 

153 else: 

154 imageBBox = footprint.getBBox() 

155 

156 if peaks is not None: 

157 footprint.setPeakCatalog(peaks) 

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

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

160 return MultibandFootprint(filters, singles) 

161 

162 @staticmethod 

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

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

165 

166 Parameters 

167 ---------- 

168 filters : `list` 

169 List of filter names. 

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

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

172 to convert into `HeavyFootprint` objects. 

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

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

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

176 Mask for the `image`. 

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

178 Variance of the `image`. 

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

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

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

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

183 peaks : `PeakCatalog` 

184 Catalog containing information about the peaks located in the 

185 footprints. 

186 

187 Returns 

188 ------- 

189 result : `MultibandFootprint` 

190 MultibandFootprint created from the image, mask, and variance 

191 """ 

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

193 if footprint is None: 

194 spans, imageBBox = getSpanSetFromImages(image, thresh) 

195 footprint = Footprint(spans) 

196 

197 if peaks is not None: 

198 footprint.setPeakCatalog(peaks) 

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

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

201 return MultibandFootprint(filters, singles) 

202 

203 @staticmethod 

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

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

206 

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

208 

209 Parameters 

210 ---------- 

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

212 MaskedImages to extract the single band heavy footprints from. 

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

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

215 to the `PeakCatalog`. 

216 

217 Returns 

218 ------- 

219 result : `MultibandFootprint` 

220 MultibandFootprint created from the image, mask, and variance 

221 """ 

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

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

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

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

226 

227 def getSpans(self): 

228 """Get the full `SpanSet`""" 

229 return self._footprint.getSpans() 

230 

231 @property 

232 def footprint(self): 

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

234 return self._footprint 

235 

236 @property 

237 def mMaskedImage(self): 

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

239 return self._mMaskedImage 

240 

241 @property 

242 def spans(self): 

243 """`SpanSet` of the `MultibandFootprint`""" 

244 return self._footprint.getSpans() 

245 

246 def getPeaks(self): 

247 """Get the `PeakCatalog`""" 

248 return self._footprint.getPeaks() 

249 

250 @property 

251 def peaks(self): 

252 """`PeakCatalog` of the `MultibandFootprint`""" 

253 return self._footprint.getPeaks() 

254 

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

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

257 

258 `MultibandFootprint` objects cannot be sliced along the image 

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

260 

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

262 """ 

263 if len(indices) > 0: 

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

265 

266 if isinstance(filterIndex, slice): 

267 singles = self.singles[filterIndex] 

268 else: 

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

270 

271 return MultibandFootprint(filters, singles) 

272 

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

274 """Convert a `MultibandFootprint` to a `MultibandImage` 

275 

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

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

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

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

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

281 the images. 

282 

283 Parameters 

284 ---------- 

285 bbox : `Box2I` 

286 Bounding box of the resulting image. 

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

288 of the footprint is used. 

289 fill : `float` 

290 Value to use for any pixel in the resulting image 

291 outside of the `SpanSet`. 

292 imageType : `type` 

293 This should be either a `MultibandMaskedImage` 

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

295 

296 Returns 

297 ------- 

298 result : `MultibandBase` 

299 The resulting `MultibandImage` or `MultibandMaskedImage` created 

300 from the `MultibandHeavyFootprint`. 

301 """ 

302 if imageType == MultibandMaskedImage: 

303 singleType = MaskedImage 

304 elif imageType == MultibandImage: 

305 singleType = Image 

306 else: 

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

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

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

310 return mMaskedImage 

311 

312 def clone(self, deep=True): 

313 """Copy the current object 

314 

315 Parameters 

316 ---------- 

317 deep : `bool` 

318 Whether or not to make a deep copy 

319 

320 Returns 

321 ------- 

322 result : `MultibandFootprint` 

323 The cloned footprint. 

324 """ 

325 if deep: 

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

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

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

329 mMaskedImage = self.getImage() 

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

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

332 else: 

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

334 return result