LSST Applications  21.0.0-172-gfb10e10a+18fedfabac,22.0.0+297cba6710,22.0.0+80564b0ff1,22.0.0+8d77f4f51a,22.0.0+a28f4c53b1,22.0.0+dcf3732eb2,22.0.1-1-g7d6de66+2a20fdde0d,22.0.1-1-g8e32f31+297cba6710,22.0.1-1-geca5380+7fa3b7d9b6,22.0.1-12-g44dc1dc+2a20fdde0d,22.0.1-15-g6a90155+515f58c32b,22.0.1-16-g9282f48+790f5f2caa,22.0.1-2-g92698f7+dcf3732eb2,22.0.1-2-ga9b0f51+7fa3b7d9b6,22.0.1-2-gd1925c9+bf4f0e694f,22.0.1-24-g1ad7a390+a9625a72a8,22.0.1-25-g5bf6245+3ad8ecd50b,22.0.1-25-gb120d7b+8b5510f75f,22.0.1-27-g97737f7+2a20fdde0d,22.0.1-32-gf62ce7b1+aa4237961e,22.0.1-4-g0b3f228+2a20fdde0d,22.0.1-4-g243d05b+871c1b8305,22.0.1-4-g3a563be+32dcf1063f,22.0.1-4-g44f2e3d+9e4ab0f4fa,22.0.1-42-gca6935d93+ba5e5ca3eb,22.0.1-5-g15c806e+85460ae5f3,22.0.1-5-g58711c4+611d128589,22.0.1-5-g75bb458+99c117b92f,22.0.1-6-g1c63a23+7fa3b7d9b6,22.0.1-6-g50866e6+84ff5a128b,22.0.1-6-g8d3140d+720564cf76,22.0.1-6-gd805d02+cc5644f571,22.0.1-8-ge5750ce+85460ae5f3,master-g6e05de7fdc+babf819c66,master-g99da0e417a+8d77f4f51a,w.2021.48
LSST Data Management Base Package
_slicing.py
Go to the documentation of this file.
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 
22 from lsst.geom import Point2I, Box2I, Extent2I
23 from ._imageLib import LOCAL, PARENT, ImageOrigin
24 
25 __all__ = ["supportSlicing"]
26 
27 
28 def 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 
65 def 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 
111 def 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 
147 def 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 
202 def 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 
265 def 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__
An integer coordinate rectangle.
Definition: Box.h:55
def translateSliceArgs(sliceArgs, bboxGetter)
Definition: _slicing.py:111
def handleNegativeIndex(index, size, origin, default)
Definition: _slicing.py:65
def splitSliceArgs(sliceArgs)
Definition: _slicing.py:28
def imageIndicesToNumpy(sliceArgs, bboxGetter)
Definition: _slicing.py:202
def interpretSliceArgs(sliceArgs, bboxGetter)
Definition: _slicing.py:147
Extent< int, 2 > Extent2I
Definition: Extent.h:397
Point< int, 2 > Point2I
Definition: Point.h:321