LSSTApplications  17.0+105,17.0+11,17.0+61,18.0.0+13,18.0.0+25,18.0.0+5,18.0.0+54,18.0.0-4-g68ffd23,18.1.0-1-g0001055+8,18.1.0-1-g03d53ef+1,18.1.0-1-g1349e88+31,18.1.0-1-g2505f39+24,18.1.0-1-g5315e5e+1,18.1.0-1-g5e4b7ea+10,18.1.0-1-g7e8fceb+1,18.1.0-1-g85f8cd4+25,18.1.0-1-g9a6769a+13,18.1.0-1-ga1a4c1a+24,18.1.0-1-gd55f500+19,18.1.0-10-gfd5443f+1,18.1.0-12-g42eabe8e+13,18.1.0-14-gd04256d+18,18.1.0-17-gd2166b6e4,18.1.0-19-g6565cef+1,18.1.0-2-g5f9922c+1,18.1.0-2-gfbf3545+9,18.1.0-2-gfefb8b5+18,18.1.0-20-gf55fa0c7,18.1.0-3-g52aa583+13,18.1.0-3-g8f4a2b1+19,18.1.0-3-gb69f684+12,18.1.0-4-g1ee41a7+1,18.1.0-5-g5d04eb7+1,18.1.0-5-g6dbcb01+15,18.1.0-5-gc286bb7+3,18.1.0-7-g85d95c9+1,18.1.0-7-gae09a6d+1,18.1.0-7-gc4d902b+5,18.1.0-8-gc69d46e+1,w.2019.38
LSSTDataManagementBasePackage
multiband.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 # (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 
24 from abc import ABC, abstractmethod
25 
26 from lsst.geom import Box2I
27 
28 
29 class 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__()
def _filterNamesToIndex(self, filterIndex)
Definition: multiband.py:184
bool all(CoordinateExpr< N > const &expr) noexcept
Return true if all elements are true.
def _slice(self, filters, filterIndex, indices)
Definition: multiband.py:282
def clone(self, deep=True)
Definition: multiband.py:68
def __init__(self, filters, singles, bbox=None)
Definition: multiband.py:54