22from __future__
import annotations
25from copy
import deepcopy
26from typing
import Any, Callable, Sequence, cast
29from numpy.typing
import DTypeLike
32from .utils
import ScalarLike, ScalarTypes, convert_indices
34__all__ = [
"Image",
"MismatchedBoxError",
"MismatchedBandsError"]
38 """Attempt to compare images with different bands"""
42 """Attempt to compare images in different bounding boxes"""
45def get_dtypes(*data: np.ndarray | Image | ScalarLike) -> list[DTypeLike]:
46 """Get a list of dtypes from a list of arrays, images, or scalars
51 The arrays to use for calculating the dtype
58 dtypes: list[DTypeLike] = [float] * len(data)
59 for d, element
in enumerate(data):
60 if hasattr(element,
"dtype"):
61 dtypes[d] = cast(np.ndarray, element).dtype
63 dtypes[d] = np.dtype(type(element))
68 """Get the combined dtype for a collection of arrays to prevent loss
74 The arrays to use for calculating the dtype
86 """A numpy array with an origin and (optional) bands
88 This class contains a 2D numpy array with the addition of an
89 origin (``yx0``) and an optional first index (``bands``) that
90 allows an immutable named index to be used.
94 One of the main limitations of using numpy arrays to store image data
95 is the lack of an ``origin`` attribute that allows an array to retain
96 knowledge of it's location in a larger scene.
97 For example, if a numpy array ``x`` is sliced, eg. ``x[10:20, 30:40]``
98 the result will be a new ``10x10`` numpy array that has no meta
99 data to inform the user that it was sliced from a larger image.
100 In addition, astrophysical images are also multi-band data cubes,
101 with a 2D image in each band (in fact this is the simplifying
102 assumption that distinguishes scarlet lite from scarlet main).
103 However, the ordering of the bands during processing might differ from
104 the ordering of the bands to display multiband data.
105 So a mechanism was also desired to simplify the sorting and index of
106 an image by band name.
108 Thus, scarlet lite creates a numpy-array like class with the additional
109 ``bands`` and ``yx0`` attributes to keep track of the bands contained
110 in an array and the origin of that array (we specify ``yx0`` as opposed
111 to ``xy0`` to be consistent with the default numpy/C++ ``(y, x)``
112 ordering of arrays as opposed to the traditional cartesian ``(x, y)``
113 ordering used in astronomy and other modules in the science pipelines.
114 While this may be a small source of confusion for the user,
115 it is consistent with the ordering in the original scarlet package and
116 ensures the consistency of scarlet lite images and python index slicing.
121 The easiest way to create a new image is to use ``Image(numpy_array)``,
124 >>> import numpy as np
125 >>> from lsst.scarlet.lite import Image
127 >>> x = np.arange(12).reshape(3, 4)
135 bbox=Box(shape=(3, 4), origin=(0, 0))
137 This will create a single band :py:class:`~lsst.scarlet.lite.Image` with
139 To create a multi-band image the input array must have 3 dimensions and
140 the ``bands`` property must be specified:
142 >>> x = np.arange(24).reshape(2, 3, 4)
143 >>> image = Image(x, bands=("i", "z"))
154 bbox=Box(shape=(3, 4), origin=(0, 0))
156 It is also possible to create an empty single-band image using the
157 ``from_box`` static method:
159 >>> from lsst.scarlet.lite import Box
160 >>> image = Image.from_box(Box((3, 4), (100, 120)))
167 bbox=Box(shape=(3, 4), origin=(100, 120))
169 Similarly, an empty multi-band image can be created by passing a tuple
172 >>> image = Image.from_box(Box((3, 4)), bands=("r", "i"))
183 bbox=Box(shape=(3, 4), origin=(0, 0))
185 To select a sub-image use a ``Box`` to select a spatial region in either a
186 single-band or multi-band image:
188 >>> x = np.arange(60).reshape(3, 4, 5)
189 >>> image = Image(x, bands=("g", "r", "i"), yx0=(20, 30))
190 >>> bbox = Box((2, 2), (21, 32))
191 >>> print(image[bbox])
201 bands=('g', 'r', 'i')
202 bbox=Box(shape=(2, 2), origin=(21, 32))
205 To select a single-band image from a multi-band image,
206 pass the name of the band as an index:
208 >>> print(image["r"])
215 bbox=Box(shape=(4, 5), origin=(20, 30))
217 Multi-band images can also be sliced in the spatial dimension, for example
219 >>> print(image["g":"r"])
231 bbox=Box(shape=(4, 5), origin=(20, 30))
235 >>> print(image["r":"r"])
242 bbox=Box(shape=(4, 5), origin=(20, 30))
244 both extract a slice of a multi-band image.
247 Unlike numerical indices, where ``slice(x, y)`` will select the
248 subset of an array from ``x`` to ``y-1`` (excluding ``y``),
249 a spectral slice of an ``Image`` will return the image slice
250 including band ``y``.
252 It is also possible to change the order or index a subset of bands
253 in an image. For example:
255 >>> print(image[("r", "g", "i")])
271 bands=('r', 'g', 'i')
272 bbox=Box(shape=(4, 5), origin=(20, 30))
275 will return a new image with the bands re-ordered.
277 Images can be combined using the standard arithmetic operations similar to
278 numpy arrays, including ``+, -, *, /, **`` etc, however, if two images are
279 combined with different bounding boxes, the _union_ of the two
280 boxes is used for the result. For example:
282 >>> image1 = Image(np.ones((2, 3, 4)), bands=tuple("gr"))
283 >>> image2 = Image(np.ones((2, 3, 4)), bands=tuple("gr"), yx0=(2, 3))
284 >>> result = image1 + image2
287 [[[1. 1. 1. 1. 0. 0. 0.]
288 [1. 1. 1. 1. 0. 0. 0.]
289 [1. 1. 1. 2. 1. 1. 1.]
290 [0. 0. 0. 1. 1. 1. 1.]
291 [0. 0. 0. 1. 1. 1. 1.]]
293 [[1. 1. 1. 1. 0. 0. 0.]
294 [1. 1. 1. 1. 0. 0. 0.]
295 [1. 1. 1. 2. 1. 1. 1.]
296 [0. 0. 0. 1. 1. 1. 1.]
297 [0. 0. 0. 1. 1. 1. 1.]]]
299 bbox=Box(shape=(5, 7), origin=(0, 0))
301 If instead you want to additively ``insert`` image 1 into image 2,
302 so that they have the same bounding box as image 2, use
304 >>> _ = image2.insert(image1)
315 bbox=Box(shape=(3, 4), origin=(2, 3))
317 To insert an image using a different operation use
319 >>> from operator import truediv
320 >>> _ = image2.insert(image1, truediv)
331 bbox=Box(shape=(3, 4), origin=(2, 3))
334 However, depending on the operation you may get unexpected results
335 since now there could be ``NaN`` and ``inf`` values due to the zeros
336 in the non-overlapping regions.
337 Instead, to select only the overlap region one can use
339 >>> result = image1 / image2
340 >>> print(result[image1.bbox & image2.bbox])
346 bbox=Box(shape=(1, 1), origin=(2, 3))
352 The array data for the image.
354 The bands coving the image.
356 The (y, x) offset for the lower left of the image.
362 bands: Sequence |
None =
None,
363 yx0: tuple[int, int] |
None =
None,
365 if bands
is None or len(bands) == 0:
368 assert data.ndim == 2
371 assert data.ndim == 3
372 if data.shape[0] != len(bands):
373 raise ValueError(f
"Array has spectral size {data.shape[0]}, but {bands} bands")
381 def from_box(bbox: Box, bands: tuple |
None =
None, dtype: DTypeLike = float) -> Image:
382 """Initialize an empty image from a bounding Box and optional bands
387 The bounding box that contains the image.
389 The bands for the image.
390 If bands is `None` then a 2D image is created.
392 The numpy dtype of the image.
397 An empty image contained in ``bbox`` with ``bands`` bands.
399 if bands
is not None and len(bands) > 0:
400 shape = (len(bands),) + bbox.shape
403 data = np.zeros(shape, dtype=dtype)
404 return Image(data, bands=bands, yx0=cast(tuple[int, int], bbox.origin))
408 """The shape of the image.
410 This includes the spectral dimension, if there is one.
412 return self.
_data.shape
416 """The numpy dtype of the image."""
417 return self.
_data.dtype
421 """The bands used in the image."""
426 """Number of bands in the image.
428 If `n_bands == 0` then the image is 2D and does not have a spectral
435 """Whether or not the image has a spectral dimension."""
440 """Height of the image."""
441 return self.
shape[-2]
445 """Width of the image."""
446 return self.
shape[-1]
449 def yx0(self) -> tuple[int, int]:
450 """Origin of the image, in numpy/C++ y,x ordering."""
455 """location of the y-offset."""
460 """Location of the x-offset."""
465 """Bounding box for the special dimensions in the image."""
470 """The image viewed as a numpy array."""
475 """Number of dimensions in the image."""
476 return self.
_data.ndim
479 """The indices to extract each band in `bands` in order from the image
481 This converts a band name, or list of band names,
482 into numerical indices that can be used to slice the internal numpy
488 If `bands` is a list of band names, then the result will be an
489 index corresponding to each band, in order.
490 If `bands` is a slice, then the ``start`` and ``stop`` properties
491 should be band names, and the result will be a slice with the
492 appropriate indices to start at `bands.start` and end at
498 Tuple of indices for each band in this image.
500 return convert_indices(self.
bands, bands)
505 ) -> tuple[tuple[int, ...] | slice, tuple[int, ...] | slice]:
506 """Match bands between two images
511 The other image to match spectral indices to.
516 A tuple with a tuple of indices/slices for each dimension,
517 including the spectral dimension.
521 return slice(
None), slice(
None)
525 if self.
n_bands == 0
and other.n_bands > 1:
526 err =
"Attempted to insert a monochromatic image into a mutli-band image"
527 raise ValueError(err)
528 if other.n_bands == 0:
529 err =
"Attempted to insert a multi-band image into a monochromatic image"
530 raise ValueError(err)
532 common_bands = tuple(set(self.
bands).intersection(set(other.bands)))
534 matched_bands = tuple(self.
bands[bidx]
for bidx
in self_indices)
535 other_indices = cast(tuple[int, ...], other.spectral_indices(matched_bands))
536 return other_indices, self_indices
538 def matched_slices(self, bbox: Box) -> tuple[tuple[slice, ...], tuple[slice, ...]]:
539 """Get the slices to match this image to a given bounding box
544 The bounding box to match this image to.
549 Tuple of indices/slices to match this image to the given bbox.
553 _slice = (slice(
None),) * bbox.ndim
554 return _slice, _slice
561 bands: object | tuple[object] |
None =
None,
562 bbox: Box |
None =
None,
564 """Project this image into a different set of bands
569 Spectral bands to project this image into.
570 Not all bands have to be contained in the image, and not all
571 bands contained in the image have to be used in the projection.
573 A bounding box to project the image into.
578 A new image creating by projecting this image into
583 if not isinstance(bands, tuple):
587 data = self.
data[indices, :]
592 return Image(data, bands=bands, yx0=self.
yx0)
595 image = np.zeros((len(bands),) + bbox.shape, dtype=data.dtype)
596 slices = bbox.overlapped_slices(self.
bbox)
598 image[(slice(
None),) + slices[0]] = data[(slice(
None),) + slices[1]]
599 return Image(image, bands=bands, yx0=cast(tuple[int, int], bbox.origin))
601 image = np.zeros(bbox.shape, dtype=data.dtype)
602 slices = bbox.overlapped_slices(self.
bbox)
603 image[slices[0]] = data[slices[1]]
604 return Image(image, bands=bands, yx0=cast(tuple[int, int], bbox.origin))
608 """Return the slices required to slice a multiband image"""
614 op: Callable = operator.add,
616 """Insert this image into another image in place.
621 The image to insert this image into.
623 The operator to use when combining the images.
628 `image` updated by inserting this instance.
632 def insert(self, image: Image, op: Callable = operator.add) -> Image:
633 """Insert another image into this image in place.
638 The image to insert this image into.
640 The operator to use when combining the images.
645 This instance with `image` inserted.
650 """Project a 2D image into the spectral dimension
655 The bands in the projected image.
660 The 2D image repeated in each band in the spectral dimension.
663 raise ValueError(
"Image.repeat only works with 2D images")
665 np.repeat(self.
data[
None, :, :], len(bands), axis=0),
671 """Make a copy of this image.
676 The copy of this image.
681 """Make a deep copy of this image.
686 A dictionary of already copied objects to avoid infinite recursion.
690 The deep copy of this image.
694 return memo[id(self)]
697 result = Image.__new__(Image)
698 memo[id(self)] = result
702 data=deepcopy(self.
data, memo),
703 bands=deepcopy(self.
bands, memo),
704 yx0=deepcopy(self.
yx0, memo),
709 def copy(self, order=None) -> Image:
710 """Make a copy of this image.
715 The ordering to use for storing the bytes.
716 This is unlikely to be needed, and just defaults to
717 the numpy behavior (C) ordering.
722 The copy of this image.
728 data: np.ndarray |
None =
None,
729 order: str |
None =
None,
730 bands: tuple[str, ...] |
None =
None,
731 yx0: tuple[int, int] |
None =
None,
733 """Copy of this image with some parameters updated.
735 Any parameters not specified by the user will be copied from the
741 An update for the data in the image.
743 The ordering for stored bytes, from numpy.copy.
745 The bands that the resulting image will have.
746 The number of bands must be the same as the first dimension
749 The lower-left of the image bounding box.
764 return Image(data, bands, yx0)
766 def trimmed(self, threshold: float = 0) -> Image:
767 """Return a copy of the image trimmed to a threshold.
769 This is essentially the smallest image that contains all of the
770 pixels above the threshold.
775 The threshold to use for trimming the image.
780 A copy of the image trimmed to the threshold.
783 bbox = Box.from_data(data, threshold=threshold)
784 data = data[bbox.slices]
787 return Image(data, yx0=(y0 + self.
y0, x0 + self.
x0))
789 def at(self, y: int, x: int) -> ScalarLike | np.ndarray:
790 """The value of the image at a given location.
792 Image does not implment single index access because the
793 result is a scalar, while indexing an image returns another image.
798 The y-coordinate of the location.
800 The x-coordinate of the location.
805 The value of the image at the given location.
810 return self.
data[_y, _x]
811 return self.
data[:, _y, _x]
813 def _i_update(self, op: Callable, other: Image | ScalarLike) -> Image:
814 """Update the data array in place.
816 This is typically implemented by `__i<op>__` methods,
817 like `__iadd__`, to apply an operator and update this image
818 with the data in place.
823 Operator used to combine this image with the `other` image.
825 The other image that is combined with this one using the operator
831 This image, after being updated by the operator
834 if self.
dtype != dtype:
835 if hasattr(other,
"dtype"):
836 _dtype = cast(np.ndarray, other).dtype
839 msg = f
"Cannot update an array with type {self.dtype} with {_dtype}"
840 raise ValueError(msg)
842 self.
_data[:] = result.data
843 self.
_bands = result.bands
844 self.
_yx0 = result.yx0
848 """Compare this array to another.
850 This performs an element by element equality check.
855 The image to compare this image to.
857 The operator used for the comparision (==, !=, >=, <=).
862 An image made by checking all of the elements in this array with
868 If `other` is not an `Image`.
869 MismatchedBandsError:
870 If `other` has different bands.
872 if `other` exists in a different bounding box.
874 if isinstance(other, Image)
and other.bands == self.
bands and other.bbox == self.
bbox:
877 if not isinstance(other, Image):
878 if type(other)
in ScalarTypes:
880 raise TypeError(f
"Cannot compare images to {type(other)}")
882 if other.bands != self.
bands:
883 msg = f
"Cannot compare images with mismatched bands: {self.bands} vs {other.bands}"
887 f
"Cannot compare images with different bounds boxes: {self.bbox} vs. {other.bbox}"
890 def __eq__(self, other: object) -> Image:
891 """Check if this image is equal to another."""
892 if not isinstance(other, Image)
and not isinstance(other, ScalarTypes):
893 raise TypeError(f
"Cannot compare an Image to {type(other)}.")
896 def __ne__(self, other: object) -> Image:
897 """Check if this image is not equal to another."""
898 return ~self.
__eq__(other)
900 def __ge__(self, other: Image | ScalarLike) -> Image:
901 """Check if this image is greater than or equal to another."""
902 if type(other)
in ScalarTypes:
906 def __le__(self, other: Image | ScalarLike) -> Image:
907 """Check if this image is less than or equal to another."""
908 if type(other)
in ScalarTypes:
912 def __gt__(self, other: Image | ScalarLike) -> Image:
913 """Check if this image is greater than or equal to another."""
914 if type(other)
in ScalarTypes:
918 def __lt__(self, other: Image | ScalarLike) -> Image:
919 """Check if this image is less than or equal to another."""
920 if type(other)
in ScalarTypes:
925 """Take the negative of the image."""
929 """Make a copy using of the image."""
933 """Take the inverse (~) of the image."""
936 def __add__(self, other: Image | ScalarLike) -> Image:
937 """Combine this image and another image using addition."""
940 def __iadd__(self, other: Image | ScalarLike) -> Image:
941 """Combine this image and another image using addition and update
946 def __radd__(self, other: Image | ScalarLike) -> Image:
947 """Combine this image and another image using addition,
948 with this image on the right.
950 if type(other)
in ScalarTypes:
952 return cast(Image, other).__add__(self)
954 def __sub__(self, other: Image | ScalarLike) -> Image:
955 """Combine this image and another image using subtraction."""
958 def __isub__(self, other: Image | ScalarLike) -> Image:
959 """Combine this image and another image using subtraction,
960 with this image on the right.
964 def __rsub__(self, other: Image | ScalarLike) -> Image:
965 """Combine this image and another image using subtraction,
966 with this image on the right.
968 if type(other)
in ScalarTypes:
970 return cast(Image, other).__sub__(self)
972 def __mul__(self, other: Image | ScalarLike) -> Image:
973 """Combine this image and another image using multiplication."""
976 def __imul__(self, other: Image | ScalarLike) -> Image:
977 """Combine this image and another image using multiplication,
978 with this image on the right.
982 def __rmul__(self, other: Image | ScalarLike) -> Image:
983 """Combine this image and another image using multiplication,
984 with this image on the right.
986 if type(other)
in ScalarTypes:
988 return cast(Image, other).__mul__(self)
990 def __truediv__(self, other: Image | ScalarLike) -> Image:
991 """Divide this image by `other`."""
995 """Divide this image by `other` in place."""
999 """Divide this image by `other` with this on the right."""
1000 if type(other)
in ScalarTypes:
1002 return cast(Image, other).__truediv__(self)
1004 def __floordiv__(self, other: Image | ScalarLike) -> Image:
1005 """Floor divide this image by `other` in place."""
1009 """Floor divide this image by `other` in place."""
1013 """Floor divide this image by `other` with this on the right."""
1014 if type(other)
in ScalarTypes:
1016 return cast(Image, other).__floordiv__(self)
1018 def __pow__(self, other: Image | ScalarLike) -> Image:
1019 """Raise this image to the `other` power."""
1022 def __ipow__(self, other: Image | ScalarLike) -> Image:
1023 """Raise this image to the `other` power in place."""
1026 def __rpow__(self, other: Image | ScalarLike) -> Image:
1027 """Raise this other to the power of this image."""
1028 if type(other)
in ScalarTypes:
1030 return cast(Image, other).__pow__(self)
1032 def __mod__(self, other: Image | ScalarLike) -> Image:
1033 """Take the modulus of this % other."""
1036 def __imod__(self, other: Image | ScalarLike) -> Image:
1037 """Take the modulus of this % other in place."""
1040 def __rmod__(self, other: Image | ScalarLike) -> Image:
1041 """Take the modulus of other % this."""
1042 if type(other)
in ScalarTypes:
1044 return cast(Image, other).__mod__(self)
1046 def __and__(self, other: Image | ScalarLike) -> Image:
1047 """Take the bitwise and of this and other."""
1050 def __iand__(self, other: Image | ScalarLike) -> Image:
1051 """Take the bitwise and of this and other in place."""
1054 def __rand__(self, other: Image | ScalarLike) -> Image:
1055 """Take the bitwise and of other and this."""
1056 if type(other)
in ScalarTypes:
1058 return cast(Image, other).__and__(self)
1060 def __or__(self, other: Image | ScalarLike) -> Image:
1061 """Take the binary or of this or other."""
1064 def __ior__(self, other: Image | ScalarLike) -> Image:
1065 """Take the binary or of this or other in place."""
1068 def __ror__(self, other: Image | ScalarLike) -> Image:
1069 """Take the binary or of other or this."""
1070 if type(other)
in ScalarTypes:
1072 return cast(Image, other).__or__(self)
1074 def __xor__(self, other: Image | ScalarLike) -> Image:
1075 """Take the binary xor of this xor other."""
1078 def __ixor__(self, other: Image | ScalarLike) -> Image:
1079 """Take the binary xor of this xor other in place."""
1082 def __rxor__(self, other: Image | ScalarLike) -> Image:
1083 """Take the binary xor of other xor this."""
1084 if type(other)
in ScalarTypes:
1086 return cast(Image, other).__xor__(self)
1089 """Shift this image to the left by other bits."""
1090 if not issubclass(np.dtype(type(other)).type, np.integer):
1091 raise TypeError(
"Bit shifting an image can only be done with integers")
1095 """Shift this image to the left by other bits in place."""
1100 """Shift other to the left by this image bits."""
1104 """Shift this image to the right by other bits."""
1105 if not issubclass(np.dtype(type(other)).type, np.integer):
1106 raise TypeError(
"Bit shifting an image can only be done with integers")
1110 """Shift this image to the right by other bits in place."""
1115 """Shift other to the right by this image bits."""
1119 """Display the image array, bands, and bounding box."""
1120 return f
"Image:\n {str(self.data)}\n bands={self.bands}\n bbox={self.bbox}"
1123 """Check to see if an index is a spectral index.
1128 Either a slice, a tuple, or an element in `Image.bands`.
1133 ``True`` if `index` is band or tuple of bands.
1136 if isinstance(index, slice):
1137 if index.start
in bands
or index.stop
in bands
or (index.start
is None and index.stop
is None):
1140 if index
in self.
bands:
1142 if isinstance(index, tuple)
and index[0]
in self.
bands:
1147 """Get the slices of the image to insert it into the overlapping
1148 region with `bbox`."""
1149 overlap = self.
bbox & bbox
1151 raise IndexError(
"Bounding box is outside of the image")
1152 origin = bbox.origin
1154 y_start = origin[0] - self.
yx0[0]
1155 y_stop = origin[0] + shape[0] - self.
yx0[0]
1156 x_start = origin[1] - self.
yx0[1]
1157 x_stop = origin[1] + shape[1] - self.
yx0[1]
1158 y_index = slice(y_start, y_stop)
1159 x_index = slice(x_start, x_stop)
1160 return y_index, x_index
1162 def _get_sliced(self, indices: Any, value: Image |
None =
None) -> Image:
1163 """Select a subset of an image
1168 The indices to select a subsection of the image.
1169 The spectral index can either be a tuple of indices,
1170 a slice of indices, or a single index used to select a
1171 single-band 2D image.
1172 The spatial index (if present) is a `Box`.
1175 The value used to set this slice of the image.
1176 This allows the single `_get_sliced` method to be used for
1177 both getting a slice of an image and setting it.
1181 result: Image | np.ndarray
1182 The resulting image obtained by selecting subsets of the iamge
1183 based on the `indices`.
1185 if not isinstance(indices, tuple):
1186 indices = (indices,)
1190 if len(indices) > 1
and indices[1]
in self.
bands:
1195 y_index = x_index = slice(
None)
1199 if isinstance(spectral_index, slice):
1200 bands = self.
bands[spectral_index]
1201 elif len(spectral_index) == 1:
1203 spectral_index = spectral_index[0]
1205 bands = tuple(self.
bands[idx]
for idx
in spectral_index)
1206 indices = indices[1:]
1207 if len(indices) == 1:
1209 if not isinstance(indices[0], Box):
1210 raise IndexError(f
"Expected a Box for the spatial index but got {indices[1]}")
1212 elif len(indices) == 0:
1213 y_index = x_index = slice(
None)
1215 raise IndexError(f
"Too many spatial indices, expeected a Box bot got {indices}")
1216 full_index = (spectral_index, y_index, x_index)
1217 elif isinstance(indices[0], Box):
1220 full_index = (slice(
None), y_index, x_index)
1222 error = f
"3D images can only be indexed by spectral indices or bounding boxes, got {indices}"
1223 raise IndexError(error)
1225 if len(indices) != 1
or not isinstance(indices[0], Box):
1226 raise IndexError(f
"2D images can only be sliced by bounding box, got {indices}")
1229 full_index = (y_index, x_index)
1242 yx0 = (y0 + self.
yx0[0], x0 + self.
yx0[1])
1244 data = self.
data[full_index]
1248 return Image(data, yx0=yx0)
1249 return Image(data, bands=bands, yx0=yx0)
1252 self.
_data[full_index] = value.data
1256 """Get the slices needed to insert this image into a bounding box.
1261 The region to insert this image into.
1266 The slice of this image and the slice of the `bbox` required to
1267 insert the overlapping portion of this image.
1272 overlap = (slice(
None),) + overlap[0], (slice(
None),) + overlap[1]
1276 """Get the subset of an image
1281 The indices to select a subsection of the image.
1286 The resulting image obtained by selecting subsets of the iamge
1287 based on the `indices`.
1292 """Set a subset of an image to a given value
1297 The indices to select a subsection of the image.
1299 The value to use for the subset of the image.
1304 The resulting image obtained by selecting subsets of the image
1305 based on the `indices`.
1311 """Perform an operation on two images, that may or may not be spectrally
1312 and spatially aligned.
1317 The image on the LHS of the operation
1319 The image on the RHS of the operation
1321 The operation used to combine the images.
1326 The resulting combined image.
1328 if type(image2)
in ScalarTypes:
1329 return image1.copy_with(data=op(image1.data, image2))
1330 image2 = cast(Image, image2)
1331 if image1.bands == image2.bands
and image1.bbox == image2.bbox:
1333 with np.errstate(divide=
"ignore", invalid=
"ignore"):
1334 result = op(image1.data, image2.data)
1335 return Image(result, bands=image1.bands, yx0=image1.yx0)
1337 if op != operator.add
and op != operator.sub
and image1.bands != image2.bands:
1338 msg =
"Images with different bands can only be combined using addition and subtraction, "
1339 msg += f
"got {op}, with bands {image1.bands}, {image2.bands}"
1340 raise ValueError(msg)
1343 bands = image1.bands
1345 bands = bands + tuple(band
for band
in image2.bands
if band
not in bands)
1347 bbox = image1.bbox | image2.bbox
1350 shape = (len(bands),) + bbox.shape
1354 if op == operator.add
or op == operator.sub:
1356 result =
Image(np.zeros(shape, dtype=dtype), bands=bands, yx0=cast(tuple[int, int], bbox.origin))
1358 image1.insert_into(result, operator.add)
1360 image2.insert_into(result, op)
1363 image1 = image1.project(bbox=bbox)
1364 image2 = image2.project(bbox=bbox)
1365 result = op(image1, image2)
1372 op: Callable = operator.add,
1374 """Insert one image into another image
1379 The image that will have `sub_image` insertd.
1381 The image that is inserted into `main_image`.
1383 The operator to use for insertion
1384 (addition, subtraction, multiplication, etc.).
1389 The `main_image`, with the `sub_image` inserted in place.
1391 if len(main_image.bands) == 0
and len(sub_image.bands) == 0:
1392 slices = sub_image.matched_slices(main_image.bbox)
1393 image_slices = slices[1]
1394 self_slices = slices[0]
1396 band_indices = sub_image.matched_spectral_indices(main_image)
1397 slices = sub_image.matched_slices(main_image.bbox)
1398 image_slices = (band_indices[0],) + slices[1]
1399 self_slices = (band_indices[1],) + slices[0]
1401 main_image._data[image_slices] = op(main_image.data[image_slices], sub_image.data[self_slices])
copy_with(self, np.ndarray|None data=None, str|None order=None, tuple[str,...]|None bands=None, tuple[int, int]|None yx0=None)
Image __deepcopy__(self, dict[int, Any] memo)
Image _i_update(self, Callable op, Image|ScalarLike other)
tuple[tuple[int,...]|slice, slice, slice] multiband_slices(self)
Image __rlshift__(self, ScalarLike other)
Image __ne__(self, object other)
Image __rand__(self, Image|ScalarLike other)
Image _check_equality(self, Image|ScalarLike other, Callable op)
Image __rtruediv__(self, Image|ScalarLike other)
ScalarLike|np.ndarray at(self, int y, int x)
tuple[int, int] yx0(self)
__init__(self, np.ndarray data, Sequence|None bands=None, tuple[int, int]|None yx0=None)
tuple[tuple[int,...]|slice, tuple[int,...]|slice] matched_spectral_indices(self, Image other)
Image trimmed(self, float threshold=0)
Image __ilshift__(self, ScalarLike other)
Image __iadd__(self, Image|ScalarLike other)
Image __eq__(self, object other)
Image __rmul__(self, Image|ScalarLike other)
bool _is_spectral_index(self, Any index)
Image __itruediv__(self, Image|ScalarLike other)
Image copy(self, order=None)
Image __ipow__(self, Image|ScalarLike other)
Image __iand__(self, Image|ScalarLike other)
Image __lt__(self, Image|ScalarLike other)
Image __setitem__(self, indices, Image value)
Image __imod__(self, Image|ScalarLike other)
tuple[slice, slice] _get_box_slices(self, Box bbox)
Image __ge__(self, Image|ScalarLike other)
tuple[tuple[slice,...], tuple[slice,...]] overlapped_slices(self, Box bbox)
Image __gt__(self, Image|ScalarLike other)
tuple[int,...] shape(self)
Image __rpow__(self, Image|ScalarLike other)
tuple[tuple[slice,...], tuple[slice,...]] matched_slices(self, Box bbox)
Image __rfloordiv__(self, Image|ScalarLike other)
Image insert(self, Image image, Callable op=operator.add)
Image insert_into(self, Image image, Callable op=operator.add)
Image __imul__(self, Image|ScalarLike other)
Image __rsub__(self, Image|ScalarLike other)
Image __getitem__(self, Any indices)
Image __ror__(self, Image|ScalarLike other)
Image __lshift__(self, ScalarLike other)
Image from_box(Box bbox, tuple|None bands=None, DTypeLike dtype=float)
Image _get_sliced(self, Any indices, Image|None value=None)
Image __ixor__(self, Image|ScalarLike other)
tuple[int,...]|slice spectral_indices(self, Sequence|slice bands)
Image __rrshift__(self, ScalarLike other)
Image project(self, object|tuple[object]|None bands=None, Box|None bbox=None)
Image __rxor__(self, Image|ScalarLike other)
Image __le__(self, Image|ScalarLike other)
Image __isub__(self, Image|ScalarLike other)
Image __irshift__(self, ScalarLike other)
Image repeat(self, tuple bands)
Image __rmod__(self, Image|ScalarLike other)
Image __rshift__(self, ScalarLike other)
Image __radd__(self, Image|ScalarLike other)
Image __ifloordiv__(self, Image|ScalarLike other)
Image __ior__(self, Image|ScalarLike other)
Image _operate_on_images(Image image1, Image|ScalarLike image2, Callable op)
list[DTypeLike] get_dtypes(*np.ndarray|Image|ScalarLike data)
DTypeLike get_combined_dtype(*np.ndarray|Image|ScalarLike data)
Image insert_image(Image main_image, Image sub_image, Callable op=operator.add)