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

102 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-04-13 02:49 -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 lsst.geom import Box2I 

27 

28 

29class MultibandBase(ABC): 

30 """Base class for multiband objects 

31 

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

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

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

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

36 updated as a single object. 

37 

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

39 methods for initializing, slicing, and extracting common parameters 

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

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

42 or set the `_filters`, `_singles`, and `_bbox`. 

43 

44 Parameters 

45 ---------- 

46 filters: `list` 

47 List of filter names. 

48 singles: `list` 

49 List of single band objects 

50 bbox: `Box2I` 

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

52 the bounding box of the multiband 

53 """ 

54 def __init__(self, filters, singles, bbox=None): 

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

56 self._singles = tuple(singles) 

57 

58 if bbox is None: 

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

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

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

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

63 raise ValueError(err.format(bboxes)) 

64 else: 

65 self._bbox = bbox 

66 

67 @abstractmethod 

68 def clone(self, deep=True): 

69 """Copy the current object 

70 

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

72 

73 Parameters 

74 ---------- 

75 deep: `bool` 

76 Whether or not to make a deep copy 

77 

78 Returns 

79 ------- 

80 result: `MultibandBase` 

81 copy of the instance that inherits from `MultibandBase` 

82 """ 

83 pass 

84 

85 @property 

86 def filters(self): 

87 """List of filter names for the single band objects 

88 """ 

89 return self._filters 

90 

91 @property 

92 def singles(self): 

93 """List of single band objects 

94 """ 

95 return self._singles 

96 

97 def getBBox(self): 

98 """Bounding box 

99 """ 

100 return self._bbox 

101 

102 def getXY0(self): 

103 """Minimum coordinate in the bounding box 

104 """ 

105 return self.getBBox().getMin() 

106 

107 @property 

108 def x0(self): 

109 """X0 

110 

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

112 """ 

113 return self.getBBox().getMinX() 

114 

115 @property 

116 def y0(self): 

117 """Y0 

118 

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

120 """ 

121 return self.getBBox().getMinY() 

122 

123 @property 

124 def origin(self): 

125 """Minimum (y,x) position 

126 

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

128 but available as a tuple for numpy array indexing. 

129 """ 

130 return (self.y0, self.x0) 

131 

132 @property 

133 def width(self): 

134 """Width of the images 

135 """ 

136 return self.getBBox().getWidth() 

137 

138 @property 

139 def height(self): 

140 """Height of the images 

141 """ 

142 return self.getBBox().getHeight() 

143 

144 def __len__(self): 

145 return len(self.filters) 

146 

147 def __getitem__(self, args): 

148 """Get a slice of the underlying array 

149 

150 If only a single filter is specified, 

151 return the single band object sliced 

152 appropriately. 

153 """ 

154 if not isinstance(args, tuple): 

155 indices = (args,) 

156 else: 

157 indices = args 

158 

159 # Return the single band object if the first 

160 # index is not a list or slice. 

161 filters, filterIndex = self._filterNamesToIndex(indices[0]) 

162 if not isinstance(filterIndex, slice) and len(filterIndex) == 1: 

163 if len(indices) > 2: 

164 return self.singles[filterIndex[0]][indices[1:]] 

165 elif len(indices) == 2: 

166 return self.singles[filterIndex[0]][indices[1]] 

167 else: 

168 return self.singles[filterIndex[0]] 

169 

170 return self._slice(filters=filters, filterIndex=filterIndex, indices=indices[1:]) 

171 

172 def __iter__(self): 

173 self._filterIndex = 0 

174 return self 

175 

176 def __next__(self): 

177 if self._filterIndex < len(self.filters): 

178 result = self.singles[self._filterIndex] 

179 self._filterIndex += 1 

180 else: 

181 raise StopIteration 

182 return result 

183 

184 def _filterNamesToIndex(self, filterIndex): 

185 """Convert a list of filter names to an index or a slice 

186 

187 Parameters 

188 ---------- 

189 filterIndex: iterable or `object` 

190 Index to specify a filter or list of filters, 

191 usually a string or enum. 

192 For example `filterIndex` can be 

193 `"R"` or `["R", "G", "B"]` or `[Filter.R, Filter.G, Filter.B]`, 

194 if `Filter` is an enum. 

195 

196 Returns 

197 ------- 

198 filterNames: `list` 

199 Names of the filters in the slice 

200 filterIndex: `slice` or `list` of `int` 

201 Index of each filter in `filterNames` in 

202 `self.filters`. 

203 """ 

204 if isinstance(filterIndex, slice): 

205 if filterIndex.start is not None: 

206 start = self.filters.index(filterIndex.start) 

207 else: 

208 start = None 

209 if filterIndex.stop is not None: 

210 stop = self.filters.index(filterIndex.stop) 

211 else: 

212 stop = None 

213 filterIndices = slice(start, stop, filterIndex.step) 

214 filterNames = self.filters[filterIndices] 

215 else: 

216 if isinstance(filterIndex, str): 

217 filterNames = [filterIndex] 

218 filterIndices = [self.filters.index(filterIndex)] 

219 else: 

220 try: 

221 # Check to see if the filterIndex is an iterable 

222 filterNames = [f for f in filterIndex] 

223 except TypeError: 

224 filterNames = [filterIndex] 

225 filterIndices = [self.filters.index(f) for f in filterNames] 

226 return tuple(filterNames), filterIndices 

227 

228 def setXY0(self, xy0): 

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

230 

231 Parameters 

232 ---------- 

233 xy0: `Point2I` 

234 New minimum bounds of the bounding box 

235 """ 

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

237 for singleObj in self.singles: 

238 singleObj.setXY0(xy0) 

239 

240 def shiftedTo(self, xy0): 

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

242 

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

244 

245 Parameters 

246 ---------- 

247 xy0: `Point2I` 

248 New minimum bounds of the bounding box 

249 

250 Returns 

251 ------- 

252 result: `MultibandBase` 

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

254 """ 

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

256 result = self.clone(False) 

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

258 for singleObj in result.singles: 

259 singleObj.setXY0(xy0) 

260 return result 

261 

262 def shiftedBy(self, offset): 

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

264 

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

266 

267 Parameters 

268 ---------- 

269 offset: `Extent2I` 

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

271 

272 Returns 

273 ------- 

274 result: `MultibandBase` 

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

276 """ 

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

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

279 return self.shiftedTo(xy0) 

280 

281 @abstractmethod 

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

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

284 

285 Different inherited classes will handling slicing differently, 

286 so this method must be overloaded in inherited classes. 

287 

288 Parameters 

289 ---------- 

290 filters: `list` of `str` 

291 List of filter names for the slice. This is a subset of the 

292 filters in the parent multiband object 

293 filterIndex: `list` of `int` or `slice` 

294 Index along the filter dimension 

295 indices: `tuple` of remaining indices 

296 `MultibandBase.__getitem__` separates the first (filter) 

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

298 of all of the indices that come after `filter` in the 

299 `args` passed to `MultibandBase.__getitem__`. 

300 

301 Returns 

302 ------- 

303 result: `object` 

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

305 same class or a different class depending on the 

306 slice being made. 

307 """ 

308 pass 

309 

310 def __repr__(self): 

311 result = "<{0}, filters={1}, bbox={2}>".format( 

312 self.__class__.__name__, self.filters, self.getBBox().__repr__()) 

313 return result 

314 

315 def __str__(self): 

316 if hasattr(self, "array"): 

317 return str(self.array) 

318 return self.__repr__()