Coverage for python / lsst / afw / multiband.py: 29%

107 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-23 01:26 -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__ = ["MultibandBase"] 

23 

24from abc import ABC, abstractmethod 

25 

26from deprecated.sphinx import deprecated 

27 

28from lsst.geom import Box2I 

29 

30 

31class MultibandBase(ABC): 

32 """Base class for multiband objects 

33 

34 The LSST stack has a number of image-like classes that have 

35 data in multiple bands that are stored as separate objects. 

36 Analyzing the data can be easier using a Multiband object that 

37 wraps the underlying data as a single data cube that can be sliced and 

38 updated as a single object. 

39 

40 `MultibandBase` is designed to contain the most important universal 

41 methods for initializing, slicing, and extracting common parameters 

42 (such as the bounding box or XY0 position) to all of the single band classes, 

43 as long as derived classes either call the base class `__init__` 

44 or set the `_bands`, `_singles`, and `_bbox`. 

45 

46 Parameters 

47 ---------- 

48 bands: `list` 

49 List of band names. 

50 singles: `list` 

51 List of single band objects 

52 bbox: `Box2I` 

53 By default `MultibandBase` uses `singles[0].getBBox()` to set 

54 the bounding box of the multiband 

55 """ 

56 def __init__(self, bands, singles, bbox=None): 

57 self._bands = tuple([f for f in bands]) 

58 self._singles = tuple(singles) 

59 

60 if bbox is None: 

61 self._bbox = self._singles[0].getBBox() 

62 if not all([s.getBBox() == self.getBBox() for s in self.singles]): 

63 bboxes = [s.getBBox() == self.getBBox() for s in self.singles] 

64 err = "`singles` are required to have the same bounding box, received {0}" 

65 raise ValueError(err.format(bboxes)) 

66 else: 

67 self._bbox = bbox 

68 

69 @abstractmethod 

70 def clone(self, deep=True): 

71 """Copy the current object 

72 

73 This must be overloaded in a subclass of `MultibandBase` 

74 

75 Parameters 

76 ---------- 

77 deep: `bool` 

78 Whether or not to make a deep copy 

79 

80 Returns 

81 ------- 

82 result: `MultibandBase` 

83 copy of the instance that inherits from `MultibandBase` 

84 """ 

85 pass 

86 

87 @property 

88 @deprecated(reason="This has been replaced with `bands`. Will be removed after v29.", 

89 version="v29.0", category=FutureWarning) 

90 def filters(self): 

91 """List of filter names for the single band objects (deprecated) 

92 

93 Use `bands` instead. 

94 """ 

95 return self._bands 

96 

97 @property 

98 def bands(self): 

99 """List of band names for the single band objects 

100 """ 

101 return self._bands 

102 

103 @property 

104 def singles(self): 

105 """List of single band objects 

106 """ 

107 return self._singles 

108 

109 def getBBox(self): 

110 """Bounding box 

111 """ 

112 return self._bbox 

113 

114 def getXY0(self): 

115 """Minimum coordinate in the bounding box 

116 """ 

117 return self.getBBox().getMin() 

118 

119 @property 

120 def x0(self): 

121 """X0 

122 

123 X component of XY0 `Point2I.getX()` 

124 """ 

125 return self.getBBox().getMinX() 

126 

127 @property 

128 def y0(self): 

129 """Y0 

130 

131 Y component of XY0 `Point2I.getY()` 

132 """ 

133 return self.getBBox().getMinY() 

134 

135 @property 

136 def origin(self): 

137 """Minimum (y,x) position 

138 

139 This is the position of `self.getBBox().getMin()`, 

140 but available as a tuple for numpy array indexing. 

141 """ 

142 return (self.y0, self.x0) 

143 

144 @property 

145 def width(self): 

146 """Width of the images 

147 """ 

148 return self.getBBox().getWidth() 

149 

150 @property 

151 def height(self): 

152 """Height of the images 

153 """ 

154 return self.getBBox().getHeight() 

155 

156 def __len__(self): 

157 return len(self.bands) 

158 

159 def __getitem__(self, args): 

160 """Get a slice of the underlying array 

161 

162 If only a single band is specified, 

163 return the single band object sliced 

164 appropriately. 

165 """ 

166 if not isinstance(args, tuple): 

167 indices = (args,) 

168 else: 

169 indices = args 

170 

171 # Return the single band object if the first 

172 # index is not a list or slice. 

173 bands, bandIndex = self._bandNamesToIndex(indices[0]) 

174 if not isinstance(bandIndex, slice) and len(bandIndex) == 1: 

175 if len(indices) > 2: 

176 return self.singles[bandIndex[0]][indices[1:]] 

177 elif len(indices) == 2: 

178 return self.singles[bandIndex[0]][indices[1]] 

179 else: 

180 return self.singles[bandIndex[0]] 

181 

182 return self._slice(bands=bands, bandIndex=bandIndex, indices=indices[1:]) 

183 

184 def __iter__(self): 

185 self._bandIndex = 0 

186 return self 

187 

188 def __next__(self): 

189 if self._bandIndex < len(self.bands): 

190 result = self.singles[self._bandIndex] 

191 self._bandIndex += 1 

192 else: 

193 raise StopIteration 

194 return result 

195 

196 def _bandNamesToIndex(self, bandIndex): 

197 """Convert a list of band names to an index or a slice 

198 

199 Parameters 

200 ---------- 

201 bandIndex: iterable or `object` 

202 Index to specify a band or list of bands, 

203 usually a string or enum. 

204 For example `bandIndex` can be 

205 `"R"` or `["R", "G", "B"]` or `[Band.R, Band.G, Band.B]`, 

206 if `Band` is an enum. 

207 

208 Returns 

209 ------- 

210 bandNames: `list` 

211 Names of the bands in the slice 

212 bandIndex: `slice` or `list` of `int` 

213 Index of each band in `bandNames` in 

214 `self.bands`. 

215 """ 

216 if isinstance(bandIndex, slice): 

217 if bandIndex.start is not None: 

218 start = self.bands.index(bandIndex.start) 

219 else: 

220 start = None 

221 if bandIndex.stop is not None: 

222 stop = self.bands.index(bandIndex.stop) 

223 else: 

224 stop = None 

225 bandIndices = slice(start, stop, bandIndex.step) 

226 bandNames = self.bands[bandIndices] 

227 else: 

228 if isinstance(bandIndex, str): 

229 bandNames = [bandIndex] 

230 bandIndices = [self.bands.index(bandIndex)] 

231 else: 

232 try: 

233 # Check to see if the bandIndex is an iterable 

234 bandNames = [f for f in bandIndex] 

235 except TypeError: 

236 bandNames = [bandIndex] 

237 bandIndices = [self.bands.index(f) for f in bandNames] 

238 return tuple(bandNames), bandIndices 

239 

240 def setXY0(self, xy0): 

241 """Shift the bounding box but keep the same Extent 

242 

243 Parameters 

244 ---------- 

245 xy0: `Point2I` 

246 New minimum bounds of the bounding box 

247 """ 

248 self._bbox = Box2I(xy0, self._bbox.getDimensions()) 

249 for singleObj in self.singles: 

250 singleObj.setXY0(xy0) 

251 

252 def shiftedTo(self, xy0): 

253 """Shift the bounding box but keep the same Extent 

254 

255 This method is broken until DM-10781 is completed. 

256 

257 Parameters 

258 ---------- 

259 xy0: `Point2I` 

260 New minimum bounds of the bounding box 

261 

262 Returns 

263 ------- 

264 result: `MultibandBase` 

265 A copy of the object, shifted to `xy0`. 

266 """ 

267 raise NotImplementedError("shiftedBy not implemented until DM-10781") 

268 result = self.clone(False) 

269 result._bbox = Box2I(xy0, result._bbox.getDimensions()) 

270 for singleObj in result.singles: 

271 singleObj.setXY0(xy0) 

272 return result 

273 

274 def shiftedBy(self, offset): 

275 """Shift a bounding box by an offset, but keep the same Extent 

276 

277 This method is broken until DM-10781 is completed. 

278 

279 Parameters 

280 ---------- 

281 offset: `Extent2I` 

282 Amount to shift the bounding box in x and y. 

283 

284 Returns 

285 ------- 

286 result: `MultibandBase` 

287 A copy of the object, shifted by `offset` 

288 """ 

289 raise NotImplementedError("shiftedBy not implemented until DM-10781") 

290 xy0 = self._bbox.getMin() + offset 

291 return self.shiftedTo(xy0) 

292 

293 @abstractmethod 

294 def _slice(self, bands, bandIndex, indices): 

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

296 

297 Different inherited classes will handling slicing differently, 

298 so this method must be overloaded in inherited classes. 

299 

300 Parameters 

301 ---------- 

302 bands: `list` of `str` 

303 List of band names for the slice. This is a subset of the 

304 bands in the parent multiband object 

305 bandIndex: `list` of `int` or `slice` 

306 Index along the band dimension 

307 indices: `tuple` of remaining indices 

308 `MultibandBase.__getitem__` separates the first (band) 

309 index from the remaining indices, so `indices` is a tuple 

310 of all of the indices that come after `band` in the 

311 `args` passed to `MultibandBase.__getitem__`. 

312 

313 Returns 

314 ------- 

315 result: `object` 

316 Sliced version of the current object, which could be the 

317 same class or a different class depending on the 

318 slice being made. 

319 """ 

320 pass 

321 

322 def __repr__(self): 

323 result = "<{0}, bands={1}, bbox={2}>".format( 

324 self.__class__.__name__, self.bands, self.getBBox().__repr__()) 

325 return result 

326 

327 def __str__(self): 

328 if hasattr(self, "array"): 

329 return str(self.array) 

330 return self.__repr__()