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

104 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-22 01:28 -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 bands : `list` 

92 List of band 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, bands, singles): 

100 super().__init__(bands, 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]): 103 ↛ 104line 103 didn't jump to line 104 because the condition on line 103 was never true

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(bands, 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 bands : `list` 

118 List of band 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: 150 ↛ 154line 150 didn't jump to line 154 because the condition on line 150 was always true

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(bands, image, mask, variance, imageBBox) 

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

160 return MultibandFootprint(bands, singles) 

161 

162 @staticmethod 

163 def fromImages(bands, 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 bands : `list` 

169 List of band 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: 193 ↛ 197line 193 didn't jump to line 197 because the condition on line 193 was always true

194 spans, imageBBox = getSpanSetFromImages(image, thresh) 

195 footprint = Footprint(spans) 

196 

197 if peaks is not None: 197 ↛ 199line 197 didn't jump to line 199 because the condition on line 197 was always true

198 footprint.setPeakCatalog(peaks) 

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

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

201 return MultibandFootprint(bands, singles) 

202 

203 @staticmethod 

204 def fromMaskedImages(bands, 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(bands, 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, bands, bandIndex, 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 band dimension") 

265 

266 if isinstance(bandIndex, slice): 

267 singles = self.singles[bandIndex] 

268 else: 

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

270 

271 return MultibandFootprint(bands, 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: 302 ↛ 304line 302 didn't jump to line 304 because the condition on line 302 was always true

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.bands, 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 bands = tuple([f for f in self.bands]) 

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

332 else: 

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

334 return result