Coverage for python/lsst/afw/image/_image/_slicing.py: 11%

107 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-05-01 15:48 -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# (https://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 <https://www.gnu.org/licenses/>. 

21 

22from lsst.geom import Point2I, Box2I, Extent2I 

23from ._imageLib import LOCAL, PARENT, ImageOrigin 

24 

25__all__ = ["supportSlicing"] 

26 

27 

28def splitSliceArgs(sliceArgs): 

29 """Separate the actual slice from origin arguments to __getitem__ or 

30 __setitem__, using PARENT for the origin if it is not provided. 

31 

32 Parameter 

33 --------- 

34 sliceArgs : `tuple`, `Box2I`, or `Point2I` 

35 The first argument passed to an image-like object's 

36 ``__getitem__`` or ``__setitem__``. 

37 

38 Returns 

39 ------- 

40 sliceArgs : `tuple`, `Box2I`, or `Point2I` 

41 The original sliceArgs passed in, with any ImageOrigin argument 

42 stripped off. 

43 origin : `ImageOrigin` 

44 Enum value that sets whether or not to consider xy0 in positions. 

45 

46 See interpretSliceArgs for more information. 

47 

48 Intended primarily for internal use by `supportSlicing()`. 

49 """ 

50 defaultOrigin = PARENT 

51 try: 

52 if isinstance(sliceArgs[-1], ImageOrigin): 

53 # Args are already a tuple that includes the origin. 

54 if len(sliceArgs) == 2: 

55 return sliceArgs[0], sliceArgs[-1] 

56 else: 

57 return sliceArgs[:-1], sliceArgs[-1] 

58 else: 

59 # Args are a tuple that does not include the origin; add it to make origin explicit. 

60 return sliceArgs, defaultOrigin 

61 except TypeError: # Arg is a scalar; return it along with the default origin. 

62 return sliceArgs, defaultOrigin 

63 

64 

65def handleNegativeIndex(index, size, origin, default): 

66 """Handle negative indices passed to image accessors. 

67 

68 When negative indices are used in LOCAL coordinates, we interpret them as 

69 relative to the upper bounds of the array, as in regular negative indexing 

70 in Python. 

71 

72 Using negative indices in PARENT coordinates is not allowed unless passed 

73 via a `Point2I` or `Box2I`; the potential for confusion between actual 

74 negative indices (when ``xy0 < 0``) and offsets relative to the upper 

75 bounds of the array is too great. 

76 

77 Parameters 

78 ---------- 

79 index : `int` or `None` 

80 1-d pixel index to interpret, as given by a caller to an image-like 

81 object's ``__getitem__`` or ``__setitem__``. 

82 size : `int` 

83 Size of the image in the dimension corresponding to ``index``. 

84 origin : `ImageOrigin` 

85 Enum value that sets whether or not to consider xy0 in positions. 

86 default : `int` 

87 Index to return if `index` is None. 

88 

89 Returns 

90 ------- 

91 index : `int` 

92 If ``origin==PARENT``, either the given ``index`` or ``default``. 

93 If ``origin==LOCAL``, an equivalent index guaranteed to be nonnegative. 

94 

95 Intended primarily for internal use by `supportSlicing()`. 

96 """ 

97 if index is None: 

98 assert default is not None 

99 return default 

100 if index < 0: 

101 if origin == LOCAL: 

102 index = size + index 

103 else: 

104 raise IndexError("Negative indices are not permitted with the PARENT origin. " 

105 "Use LOCAL to use negative to index relative to the end, " 

106 "and Point2I or Box2I indexing to access negative pixels " 

107 "in PARENT coordinates.") 

108 return index 

109 

110 

111def translateSliceArgs(sliceArgs, bboxGetter): 

112 """Transform a tuple of slice objects into a Box2I, correctly handling negative indices. 

113 

114 see `interpretSliceArgs` for a description of parameters 

115 

116 Returns 

117 ------- 

118 box : `Box2I` or `None` 

119 A box to use to create a subimage, or None if the slice refers to a 

120 scalar. 

121 index: `tuple` or `None` 

122 An ``(x, y)`` tuple of integers, or None if the slice refers to a 

123 box. 

124 origin : `ImageOrigin` 

125 Enum indicating whether to account for xy0. 

126 """ 

127 slices, origin = splitSliceArgs(sliceArgs) 

128 if isinstance(slices, Point2I): 

129 return None, slices, origin 

130 elif isinstance(slices, Box2I): 

131 return slices, None, origin 

132 

133 x, y, origin = interpretSliceArgs(sliceArgs, bboxGetter) 

134 

135 if isinstance(x, slice): 

136 assert isinstance(y, slice) 

137 if x.step is not None or y.step is not None: 

138 raise ValueError("Slices with steps are not supported in image indexing.") 

139 begin = Point2I(x.start, y.start) 

140 end = Point2I(x.stop, y.stop) 

141 return Box2I(begin, end - begin), None, origin 

142 

143 assert not isinstance(y, slice) 

144 return None, Point2I(x, y), origin 

145 

146 

147def interpretSliceArgs(sliceArgs, bboxGetter): 

148 """Transform arguments to __getitem__ or __setitem__ to a standard form. 

149 

150 Parameters 

151 ---------- 

152 sliceArgs : `tuple`, `Box2I`, or `Point2I` 

153 Slice arguments passed directly to `__getitem__` or `__setitem__`. 

154 bboxGetter : callable 

155 Callable that accepts an ImageOrigin enum value and returns the 

156 appropriate image bounding box. Usually the bound getBBox method 

157 of an Image, Mask, or MaskedImage object. 

158 

159 Returns 

160 ------- 

161 x : int or slice 

162 Index or slice in the x dimension 

163 y : int or slice 

164 Index or slice in the y dimension 

165 origin : `ImageOrigin` 

166 Either `PARENT` (coordinates respect XY0) or LOCAL 

167 (coordinates do not respect XY0) 

168 """ 

169 slices, origin = splitSliceArgs(sliceArgs) 

170 if isinstance(slices, Point2I): 

171 return slices.getX(), slices.getY(), origin 

172 elif isinstance(slices, Box2I): 

173 x0 = slices.getMinX() 

174 y0 = slices.getMinY() 

175 return slice(x0, x0 + slices.getWidth()), slice(y0, y0 + slices.getHeight()), origin 

176 elif isinstance(slices, slice): 

177 if slices.start is not None or slices.stop is not None or slices.step is not None: 

178 raise TypeError("Single-dimension slices must not have bounds.") 

179 x = slices 

180 y = slices 

181 origin = LOCAL # doesn't matter, as the slices cover the full image 

182 else: 

183 x, y = slices 

184 

185 bbox = bboxGetter(origin) 

186 if isinstance(x, slice): 

187 if isinstance(y, slice): 

188 xSlice = slice(handleNegativeIndex(x.start, bbox.getWidth(), origin, default=bbox.getBeginX()), 

189 handleNegativeIndex(x.stop, bbox.getWidth(), origin, default=bbox.getEndX())) 

190 ySlice = slice(handleNegativeIndex(y.start, bbox.getHeight(), origin, default=bbox.getBeginY()), 

191 handleNegativeIndex(y.stop, bbox.getHeight(), origin, default=bbox.getEndY())) 

192 return xSlice, ySlice, origin 

193 raise TypeError("Mixed indices of the form (slice, int) are not supported for images.") 

194 

195 if isinstance(y, slice): 

196 raise TypeError("Mixed indices of the form (int, slice) are not supported for images.") 

197 x = handleNegativeIndex(x, bbox.getWidth(), origin, default=None) 

198 y = handleNegativeIndex(y, bbox.getHeight(), origin, default=None) 

199 return x, y, origin 

200 

201 

202def imageIndicesToNumpy(sliceArgs, bboxGetter): 

203 """Convert slicing format to numpy 

204 

205 LSST `afw` image-like objects use an `[x,y]` coordinate 

206 convention, accept `Point2I` and `Box2I` 

207 objects for slicing, and slice relative to the 

208 bounding box `XY0` location; 

209 while python and numpy use the convention `[y,x]` 

210 with no `XY0`, so this method converts the `afw` 

211 indices or slices into numpy indices or slices 

212 

213 Parameters 

214 ---------- 

215 sliceArgs: `sequence`, `Point2I` or `Box2I` 

216 An `(xIndex, yIndex)` pair, or a single `(xIndex,)` tuple, 

217 where `xIndex` and `yIndex` can be a `slice` or `int`, 

218 or list of `int` objects, and if only a single `xIndex` is 

219 given, a `Point2I` or `Box2I`. 

220 bboxGetter : callable 

221 Callable that accepts an ImageOrigin enum value and returns the 

222 appropriate image bounding box. Usually the bound getBBox method 

223 of an Image, Mask, or MaskedImage object. 

224 

225 Returns 

226 ------- 

227 y: `int` or `slice` 

228 Index or `slice` in the y dimension 

229 x: `int` or `slice` 

230 Index or `slice` in the x dimension 

231 bbox: `Box2I` 

232 Bounding box of the image. 

233 If `bbox` is `None` then the result is a point and 

234 not a subset of an image. 

235 """ 

236 # Use a common slicing algorithm as single band images 

237 x, y, origin = interpretSliceArgs(sliceArgs, bboxGetter) 

238 x0 = bboxGetter().getMinX() 

239 y0 = bboxGetter().getMinY() 

240 

241 if origin == PARENT: 

242 if isinstance(x, slice): 

243 assert isinstance(y, slice) 

244 bbox = Box2I(Point2I(x.start, y.start), Extent2I(x.stop-x.start, y.stop-y.start)) 

245 x = slice(x.start - x0, x.stop - x0) 

246 y = slice(y.start - y0, y.stop - y0) 

247 else: 

248 x = x - x0 

249 y = y - y0 

250 bbox = None 

251 return y, x, bbox 

252 elif origin != LOCAL: 

253 raise ValueError("Unrecognized value for origin") 

254 

255 # Use a local bounding box 

256 if isinstance(x, slice): 

257 assert isinstance(y, slice) 

258 bbox = Box2I(Point2I(x.start + x0, y.start + y0), 

259 Extent2I(x.stop-x.start, y.stop-y.start)) 

260 else: 

261 bbox = None 

262 return y, x, bbox 

263 

264 

265def supportSlicing(cls): 

266 """Support image slicing 

267 """ 

268 

269 def Factory(self, *args, **kwargs): 

270 """Return an object of this type 

271 """ 

272 return cls(*args, **kwargs) 

273 cls.Factory = Factory 

274 

275 def clone(self): 

276 """Return a deep copy of self""" 

277 return cls(self, True) 

278 cls.clone = clone 

279 

280 def __getitem__(self, imageSlice): # noqa: N807 

281 box, index, origin = translateSliceArgs(imageSlice, self.getBBox) 

282 if box is not None: 

283 return self.subset(box, origin=origin) 

284 return self._get(index, origin=origin) 

285 cls.__getitem__ = __getitem__ 

286 

287 def __setitem__(self, imageSlice, rhs): # noqa: N807 

288 box, index, origin = translateSliceArgs(imageSlice, self.getBBox) 

289 if box is not None: 

290 if self.assign(rhs, box, origin) is NotImplemented: 

291 lhs = self.subset(box, origin=origin) 

292 lhs.set(rhs) 

293 else: 

294 self._set(index, origin=origin, value=rhs) 

295 cls.__setitem__ = __setitem__