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

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
1from lsst.geom import Point2I, Box2I, Extent2I
2from .image import LOCAL, PARENT, ImageOrigin
4__all__ = ["supportSlicing"]
7def splitSliceArgs(sliceArgs):
8 """Separate the actual slice from origin arguments to __getitem__ or
9 __setitem__, using PARENT for the origin if it is not provided.
11 Parameter
12 ---------
13 sliceArgs : `tuple`, `Box2I`, or `Point2I`
14 The first argument passed to an image-like object's
15 ``__getitem__`` or ``__setitem__``.
17 Returns
18 -------
19 sliceArgs : `tuple`, `Box2I`, or `Point2I`
20 The original sliceArgs passed in, with any ImageOrigin argument
21 stripped off.
22 origin : `ImageOrigin`
23 Enum value that sets whether or not to consider xy0 in positions.
25 See interpretSliceArgs for more information.
27 Intended primarily for internal use by `supportSlicing()`.
28 """
29 defaultOrigin = PARENT
30 try:
31 if isinstance(sliceArgs[-1], ImageOrigin):
32 # Args are already a tuple that includes the origin.
33 if len(sliceArgs) == 2:
34 return sliceArgs[0], sliceArgs[-1]
35 else:
36 return sliceArgs[:-1], sliceArgs[-1]
37 else:
38 # Args are a tuple that does not include the origin; add it to make origin explicit.
39 return sliceArgs, defaultOrigin
40 except TypeError: # Arg is a scalar; return it along with the default origin.
41 return sliceArgs, defaultOrigin
44def handleNegativeIndex(index, size, origin, default):
45 """Handle negative indices passed to image accessors.
47 When negative indices are used in LOCAL coordinates, we interpret them as
48 relative to the upper bounds of the array, as in regular negative indexing
49 in Python.
51 Using negative indices in PARENT coordinates is not allowed unless passed
52 via a `Point2I` or `Box2I`; the potential for confusion between actual
53 negative indices (when ``xy0 < 0``) and offsets relative to the upper
54 bounds of the array is too great.
56 Parameters
57 ----------
58 index : `int` or `None`
59 1-d pixel index to interpret, as given by a caller to an image-like
60 object's ``__getitem__`` or ``__setitem__``.
61 size : `int`
62 Size of the image in the dimension corresponding to ``index``.
63 origin : `ImageOrigin`
64 Enum value that sets whether or not to consider xy0 in positions.
65 default : `int`
66 Index to return if `index` is None.
68 Returns
69 -------
70 index : `int`
71 If ``origin==PARENT``, either the given ``index`` or ``default``.
72 If ``origin==LOCAL``, an equivalent index guaranteed to be nonnegative.
74 Intended primarily for internal use by `supportSlicing()`.
75 """
76 if index is None:
77 assert default is not None
78 return default
79 if index < 0:
80 if origin == LOCAL:
81 index = size + index
82 else:
83 raise IndexError("Negative indices are not permitted with the PARENT origin. "
84 "Use LOCAL to use negative to index relative to the end, "
85 "and Point2I or Box2I indexing to access negative pixels "
86 "in PARENT coordinates.")
87 return index
90def translateSliceArgs(sliceArgs, bboxGetter):
91 """Transform a tuple of slice objects into a Box2I, correctly handling negative indices.
93 see `interpretSliceArgs` for a description of parameters
95 Returns
96 -------
97 box : `Box2I` or `None`
98 A box to use to create a subimage, or None if the slice refers to a
99 scalar.
100 index: `tuple` or `None`
101 An ``(x, y)`` tuple of integers, or None if the slice refers to a
102 box.
103 origin : `ImageOrigin`
104 Enum indicating whether to account for xy0.
105 """
106 slices, origin = splitSliceArgs(sliceArgs)
107 if isinstance(slices, Point2I):
108 return None, slices, origin
109 elif isinstance(slices, Box2I):
110 return slices, None, origin
112 x, y, origin = interpretSliceArgs(sliceArgs, bboxGetter)
114 if isinstance(x, slice):
115 assert isinstance(y, slice)
116 if x.step is not None or y.step is not None:
117 raise ValueError("Slices with steps are not supported in image indexing.")
118 begin = Point2I(x.start, y.start)
119 end = Point2I(x.stop, y.stop)
120 return Box2I(begin, end - begin), None, origin
122 assert not isinstance(y, slice)
123 return None, Point2I(x, y), origin
126def interpretSliceArgs(sliceArgs, bboxGetter):
127 """Transform arguments to __getitem__ or __setitem__ to a standard form.
129 Parameters
130 ----------
131 sliceArgs : `tuple`, `Box2I`, or `Point2I`
132 Slice arguments passed directly to `__getitem__` or `__setitem__`.
133 bboxGetter : callable
134 Callable that accepts an ImageOrigin enum value and returns the
135 appropriate image bounding box. Usually the bound getBBox method
136 of an Image, Mask, or MaskedImage object.
138 Returns
139 -------
140 x : int or slice
141 Index or slice in the x dimension
142 y : int or slice
143 Index or slice in the y dimension
144 origin : `ImageOrigin`
145 Either `PARENT` (coordinates respect XY0) or LOCAL
146 (coordinates do not respect XY0)
147 """
148 slices, origin = splitSliceArgs(sliceArgs)
149 if isinstance(slices, Point2I):
150 return slices.getX(), slices.getY(), origin
151 elif isinstance(slices, Box2I):
152 x0 = slices.getMinX()
153 y0 = slices.getMinY()
154 return slice(x0, x0 + slices.getWidth()), slice(y0, y0 + slices.getHeight()), origin
155 elif isinstance(slices, slice):
156 if slices.start is not None or slices.stop is not None or slices.step is not None:
157 raise TypeError("Single-dimension slices must not have bounds.")
158 x = slices
159 y = slices
160 origin = LOCAL # doesn't matter, as the slices cover the full image
161 else:
162 x, y = slices
164 bbox = bboxGetter(origin)
165 if isinstance(x, slice):
166 if isinstance(y, slice):
167 xSlice = slice(handleNegativeIndex(x.start, bbox.getWidth(), origin, default=bbox.getBeginX()),
168 handleNegativeIndex(x.stop, bbox.getWidth(), origin, default=bbox.getEndX()))
169 ySlice = slice(handleNegativeIndex(y.start, bbox.getHeight(), origin, default=bbox.getBeginY()),
170 handleNegativeIndex(y.stop, bbox.getHeight(), origin, default=bbox.getEndY()))
171 return xSlice, ySlice, origin
172 raise TypeError("Mixed indices of the form (slice, int) are not supported for images.")
174 if isinstance(y, slice):
175 raise TypeError("Mixed indices of the form (int, slice) are not supported for images.")
176 x = handleNegativeIndex(x, bbox.getWidth(), origin, default=None)
177 y = handleNegativeIndex(y, bbox.getHeight(), origin, default=None)
178 return x, y, origin
181def imageIndicesToNumpy(sliceArgs, bboxGetter):
182 """Convert slicing format to numpy
184 LSST `afw` image-like objects use an `[x,y]` coordinate
185 convention, accept `Point2I` and `Box2I`
186 objects for slicing, and slice relative to the
187 bounding box `XY0` location;
188 while python and numpy use the convention `[y,x]`
189 with no `XY0`, so this method converts the `afw`
190 indices or slices into numpy indices or slices
192 Parameters
193 ----------
194 sliceArgs: `sequence`, `Point2I` or `Box2I`
195 An `(xIndex, yIndex)` pair, or a single `(xIndex,)` tuple,
196 where `xIndex` and `yIndex` can be a `slice` or `int`,
197 or list of `int` objects, and if only a single `xIndex` is
198 given, a `Point2I` or `Box2I`.
199 bboxGetter : callable
200 Callable that accepts an ImageOrigin enum value and returns the
201 appropriate image bounding box. Usually the bound getBBox method
202 of an Image, Mask, or MaskedImage object.
204 Returns
205 -------
206 y: `int` or `slice`
207 Index or `slice` in the y dimension
208 x: `int` or `slice`
209 Index or `slice` in the x dimension
210 bbox: `Box2I`
211 Bounding box of the image.
212 If `bbox` is `None` then the result is a point and
213 not a subset of an image.
214 """
215 # Use a common slicing algorithm as single band images
216 x, y, origin = interpretSliceArgs(sliceArgs, bboxGetter)
217 x0 = bboxGetter().getMinX()
218 y0 = bboxGetter().getMinY()
220 if origin == PARENT:
221 if isinstance(x, slice):
222 assert isinstance(y, slice)
223 bbox = Box2I(Point2I(x.start, y.start), Extent2I(x.stop-x.start, y.stop-y.start))
224 x = slice(x.start - x0, x.stop - x0)
225 y = slice(y.start - y0, y.stop - y0)
226 else:
227 x = x - x0
228 y = y - y0
229 bbox = None
230 return y, x, bbox
231 elif origin != LOCAL:
232 raise ValueError("Unrecognized value for origin")
234 # Use a local bounding box
235 if isinstance(x, slice):
236 assert isinstance(y, slice)
237 bbox = Box2I(Point2I(x.start + x0, y.start + y0),
238 Extent2I(x.stop-x.start, y.stop-y.start))
239 else:
240 bbox = None
241 return y, x, bbox
244def supportSlicing(cls):
245 """Support image slicing
246 """
248 def Factory(self, *args, **kwargs):
249 """Return an object of this type
250 """
251 return cls(*args, **kwargs)
252 cls.Factory = Factory
254 def clone(self):
255 """Return a deep copy of self"""
256 return cls(self, True)
257 cls.clone = clone
259 def __getitem__(self, imageSlice): # noqa: N807
260 box, index, origin = translateSliceArgs(imageSlice, self.getBBox)
261 if box is not None:
262 return self.subset(box, origin=origin)
263 return self._get(index, origin=origin)
264 cls.__getitem__ = __getitem__
266 def __setitem__(self, imageSlice, rhs): # noqa: N807
267 box, index, origin = translateSliceArgs(imageSlice, self.getBBox)
268 if box is not None:
269 if self.assign(rhs, box, origin) is NotImplemented:
270 lhs = self.subset(box, origin=origin)
271 lhs.set(rhs)
272 else:
273 self._set(index, origin=origin, value=rhs)
274 cls.__setitem__ = __setitem__