Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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 import Mask, Image, MaskedImage, MultibandImage, MultibandMaskedImage 

29from lsst.afw.multiband import MultibandBase 

30from . import Footprint, makeHeavyFootprint 

31 

32 

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

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

35 

36 Parameters 

37 ---------- 

38 images: `MultibandImage` or list of `Image`, array 

39 Images to extract the footprint from 

40 thresh: `float` 

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

42 xy0: `Point2I` 

43 Location of the minimum value of the images bounding box 

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

45 

46 Returns 

47 ------- 

48 spans: `SpanSet` 

49 Union of all spans in the images above the threshold 

50 imageBBox: `Box2I` 

51 Bounding box for the input images. 

52 """ 

53 # Set the threshold for each band 

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

55 thresh = [thresh] * len(images) 

56 

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

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

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

60 spans = SpanSet() 

61 for n, image in enumerate(images): 

62 mask = image.array > thresh[n] 

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

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

65 imageBBox = images[0].getBBox() 

66 else: 

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

68 thresh = np.array(thresh) 

69 if xy0 is None: 

70 xy0 = Point2I(0, 0) 

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

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

73 spans = SpanSet.fromMask(mask) 

74 imageBBox = mask.getBBox() 

75 return spans, imageBBox 

76 

77 

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

79 """Create an image of a HeavyFootprint 

80 

81 Parameters 

82 ---------- 

83 heavy: `HeavyFootprint` 

84 The HeavyFootprint to insert into the image 

85 fill: number 

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

87 contained in `heavy`. 

88 bbox: `Box2I` 

89 Bounding box of the output image. 

90 imageType: `type` 

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

92 the type of the output image. 

93 

94 Returns 

95 ------- 

96 image: `MaskedImage` or `Image` 

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

98 contains the projected flux in `heavy`. 

99 """ 

100 if bbox is None: 

101 bbox = heavy.getBBox() 

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

103 image.set(fill) 

104 heavy.insert(image) 

105 return image 

106 

107 

108class MultibandFootprint(MultibandBase): 

109 """Multiband Footprint class 

110 

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

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

113 

114 Parameters 

115 ---------- 

116 filters: `list` 

117 List of filter names. 

118 singles: `list` 

119 A list of single band `HeavyFootprint` objects. 

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

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

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

123 """ 

124 def __init__(self, filters, singles): 

125 super().__init__(filters, singles) 

126 # Ensure that all HeavyFootprints have the same SpanSet 

127 spans = singles[0].getSpans() 

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

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

130 

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

132 footprint = Footprint(spans) 

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

134 self._footprint = footprint 

135 

136 @staticmethod 

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

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

139 

140 Parameters 

141 ---------- 

142 filters: `list` 

143 List of filter names. 

144 image: array 

145 An array to convert into `HeavyFootprint` objects. 

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

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

148 mask: array 

149 Mask for the `image` array. 

150 variance: array 

151 Variance of the `image` array. 

152 footprint: `Footprint` 

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

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

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

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

157 xy0: `Point2I` 

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

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

160 `images`. 

161 thresh: float or list of floats 

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

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

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

165 peaks: `PeakCatalog` 

166 Catalog containing information about the peaks located in the 

167 footprints. 

168 

169 Returns 

170 ------- 

171 result: `MultibandFootprint` 

172 MultibandFootprint created from the arrays 

173 """ 

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

175 if footprint is None: 

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

177 footprint = Footprint(spans) 

178 else: 

179 imageBBox = footprint.getBBox() 

180 

181 if peaks is not None: 

182 footprint.setPeakCatalog(peaks) 

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

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

185 return MultibandFootprint(filters, singles) 

186 

187 @staticmethod 

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

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

190 

191 Parameters 

192 ---------- 

193 filters: list 

194 List of filter names. 

195 image: `MultibandImage`, or list of `Image` 

196 A `MultibandImage` (or collection of images in each band) 

197 to convert into `HeavyFootprint` objects. 

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

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

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

201 Mask for the `image`. 

202 variance: `MultibandImage`, or list of `Image` 

203 Variance of the `image`. 

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

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

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

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

208 peaks: `PeakCatalog` 

209 Catalog containing information about the peaks located in the 

210 footprints. 

211 

212 Returns 

213 ------- 

214 result: `MultibandFootprint` 

215 MultibandFootprint created from the image, mask, and variance 

216 """ 

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

218 if footprint is None: 

219 spans, imageBBox = getSpanSetFromImages(image, thresh) 

220 footprint = Footprint(spans) 

221 

222 if peaks is not None: 

223 footprint.setPeakCatalog(peaks) 

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

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

226 return MultibandFootprint(filters, singles) 

227 

228 @staticmethod 

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

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

231 

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

233 

234 Parameters 

235 ---------- 

236 maskedImages: `list` of `MaskedImage` 

237 MaskedImages to extract the single band heavy footprints from. 

238 Like `fromImage`, if a `footprint` is not specified then all 

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

240 to the `PeakCatalog`. 

241 

242 Returns 

243 ------- 

244 result: `MultibandFootprint` 

245 MultibandFootprint created from the image, mask, and variance 

246 """ 

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

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

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

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

251 

252 def getSpans(self): 

253 """Get the full `SpanSet`""" 

254 return self._footprint.getSpans() 

255 

256 @property 

257 def footprint(self): 

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

259 return self._footprint 

260 

261 @property 

262 def mMaskedImage(self): 

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

264 return self._mMaskedImage 

265 

266 @property 

267 def spans(self): 

268 """`SpanSet` of the `MultibandFootprint`""" 

269 return self._footprint.getSpans() 

270 

271 def getPeaks(self): 

272 """Get the `PeakCatalog`""" 

273 return self._footprint.getPeaks() 

274 

275 @property 

276 def peaks(self): 

277 """`PeakCatalog` of the `MultibandFootprint`""" 

278 return self._footprint.getPeaks() 

279 

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

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

282 

283 `MultibandFootprint` objects cannot be sliced along the image 

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

285 

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

287 """ 

288 if len(indices) > 0: 

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

290 

291 if isinstance(filterIndex, slice): 

292 singles = self.singles[filterIndex] 

293 else: 

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

295 

296 return MultibandFootprint(filters, singles) 

297 

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

299 """Convert a `MultibandFootprint` to a `MultibandImage` 

300 

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

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

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

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

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

306 the images. 

307 

308 Parameters 

309 ---------- 

310 bbox: `Box2I` 

311 Bounding box of the resulting image. 

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

313 of the footprint is used. 

314 fill: `float` 

315 Value to use for any pixel in the resulting image 

316 outside of the `SpanSet`. 

317 imageType: `type` 

318 This should be either a `MultibandMaskedImage` 

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

320 

321 Returns 

322 ------- 

323 result: `MultibandBase` 

324 The resulting `MultibandImage` or `MultibandMaskedImage` created 

325 from the `MultibandHeavyFootprint`. 

326 """ 

327 if imageType == MultibandMaskedImage: 

328 singleType = MaskedImage 

329 elif imageType == MultibandImage: 

330 singleType = Image 

331 else: 

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

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

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

335 return mMaskedImage 

336 

337 def clone(self, deep=True): 

338 """Copy the current object 

339 

340 Parameters 

341 ---------- 

342 deep: `bool` 

343 Whether or not to make a deep copy 

344 

345 Returns 

346 ------- 

347 result: `MultibandFootprint` 

348 The cloned footprint. 

349 """ 

350 if deep: 

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

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

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

354 mMaskedImage = self.getImage() 

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

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

357 else: 

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

359 return result