Coverage for python/lsst/afw/multiband.py: 27%
102 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-05-29 01:21 -0700
« prev ^ index » next coverage.py v7.14.1, created at 2026-05-29 01:21 -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/>.
22__all__ = ["MultibandBase"]
24from abc import ABC, abstractmethod
26from lsst.geom import Box2I
29class MultibandBase(ABC):
30 """Base class for multiband objects
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.
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 `_bands`, `_singles`, and `_bbox`.
44 Parameters
45 ----------
46 bands: `list`
47 List of band 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, bands, singles, bbox=None):
55 self._bands = tuple([f for f in bands])
56 self._singles = tuple(singles)
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
67 @abstractmethod
68 def clone(self, deep=True):
69 """Copy the current object
71 This must be overloaded in a subclass of `MultibandBase`
73 Parameters
74 ----------
75 deep: `bool`
76 Whether or not to make a deep copy
78 Returns
79 -------
80 result: `MultibandBase`
81 copy of the instance that inherits from `MultibandBase`
82 """
83 pass
85 @property
86 def bands(self):
87 """List of band names for the single band objects
88 """
89 return self._bands
91 @property
92 def singles(self):
93 """List of single band objects
94 """
95 return self._singles
97 def getBBox(self):
98 """Bounding box
99 """
100 return self._bbox
102 def getXY0(self):
103 """Minimum coordinate in the bounding box
104 """
105 return self.getBBox().getMin()
107 @property
108 def x0(self):
109 """X0
111 X component of XY0 `Point2I.getX()`
112 """
113 return self.getBBox().getMinX()
115 @property
116 def y0(self):
117 """Y0
119 Y component of XY0 `Point2I.getY()`
120 """
121 return self.getBBox().getMinY()
123 @property
124 def origin(self):
125 """Minimum (y,x) position
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)
132 @property
133 def width(self):
134 """Width of the images
135 """
136 return self.getBBox().getWidth()
138 @property
139 def height(self):
140 """Height of the images
141 """
142 return self.getBBox().getHeight()
144 def __len__(self):
145 return len(self.bands)
147 def __getitem__(self, args):
148 """Get a slice of the underlying array
150 If only a single band 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
159 # Return the single band object if the first
160 # index is not a list or slice.
161 bands, bandIndex = self._bandNamesToIndex(indices[0])
162 if not isinstance(bandIndex, slice) and len(bandIndex) == 1:
163 if len(indices) > 2:
164 return self.singles[bandIndex[0]][indices[1:]]
165 elif len(indices) == 2:
166 return self.singles[bandIndex[0]][indices[1]]
167 else:
168 return self.singles[bandIndex[0]]
170 return self._slice(bands=bands, bandIndex=bandIndex, indices=indices[1:])
172 def __iter__(self):
173 self._bandIndex = 0
174 return self
176 def __next__(self):
177 if self._bandIndex < len(self.bands):
178 result = self.singles[self._bandIndex]
179 self._bandIndex += 1
180 else:
181 raise StopIteration
182 return result
184 def _bandNamesToIndex(self, bandIndex):
185 """Convert a list of band names to an index or a slice
187 Parameters
188 ----------
189 bandIndex: iterable or `object`
190 Index to specify a band or list of bands,
191 usually a string or enum.
192 For example `bandIndex` can be
193 `"R"` or `["R", "G", "B"]` or `[Band.R, Band.G, Band.B]`,
194 if `Band` is an enum.
196 Returns
197 -------
198 bandNames: `list`
199 Names of the bands in the slice
200 bandIndex: `slice` or `list` of `int`
201 Index of each band in `bandNames` in
202 `self.bands`.
203 """
204 if isinstance(bandIndex, slice):
205 if bandIndex.start is not None:
206 start = self.bands.index(bandIndex.start)
207 else:
208 start = None
209 if bandIndex.stop is not None:
210 stop = self.bands.index(bandIndex.stop)
211 else:
212 stop = None
213 bandIndices = slice(start, stop, bandIndex.step)
214 bandNames = self.bands[bandIndices]
215 else:
216 if isinstance(bandIndex, str):
217 bandNames = [bandIndex]
218 bandIndices = [self.bands.index(bandIndex)]
219 else:
220 try:
221 # Check to see if the bandIndex is an iterable
222 bandNames = [f for f in bandIndex]
223 except TypeError:
224 bandNames = [bandIndex]
225 bandIndices = [self.bands.index(f) for f in bandNames]
226 return tuple(bandNames), bandIndices
228 def setXY0(self, xy0):
229 """Shift the bounding box but keep the same Extent
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)
240 def shiftedTo(self, xy0):
241 """Shift the bounding box but keep the same Extent
243 This method is broken until DM-10781 is completed.
245 Parameters
246 ----------
247 xy0: `Point2I`
248 New minimum bounds of the bounding box
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
262 def shiftedBy(self, offset):
263 """Shift a bounding box by an offset, but keep the same Extent
265 This method is broken until DM-10781 is completed.
267 Parameters
268 ----------
269 offset: `Extent2I`
270 Amount to shift the bounding box in x and y.
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)
281 @abstractmethod
282 def _slice(self, bands, bandIndex, indices):
283 """Slice the current object and return the result
285 Different inherited classes will handling slicing differently,
286 so this method must be overloaded in inherited classes.
288 Parameters
289 ----------
290 bands: `list` of `str`
291 List of band names for the slice. This is a subset of the
292 bands in the parent multiband object
293 bandIndex: `list` of `int` or `slice`
294 Index along the band dimension
295 indices: `tuple` of remaining indices
296 `MultibandBase.__getitem__` separates the first (band)
297 index from the remaining indices, so `indices` is a tuple
298 of all of the indices that come after `band` in the
299 `args` passed to `MultibandBase.__getitem__`.
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
310 def __repr__(self):
311 result = "<{0}, bands={1}, bbox={2}>".format(
312 self.__class__.__name__, self.bands, self.getBBox().__repr__())
313 return result
315 def __str__(self):
316 if hasattr(self, "array"):
317 return str(self.array)
318 return self.__repr__()