22from __future__
import annotations
25from typing
import Any, Callable, Sequence, cast
28from numpy.typing
import DTypeLike
31from .utils
import ScalarLike, ScalarTypes
33__all__ = [
"Image",
"MismatchedBoxError",
"MismatchedBandsError"]
37 """Attempt to compare images with different bands"""
41 """Attempt to compare images in different bounding boxes"""
44def get_dtypes(*data: np.ndarray | Image | ScalarLike) -> list[DTypeLike]:
45 """Get a list of dtypes from a list of arrays, images, or scalars
50 The arrays to use for calculating the dtype
57 dtypes: list[DTypeLike] = [
None] * len(data)
58 for d, element
in enumerate(data):
59 if hasattr(element,
"dtype"):
60 dtypes[d] = cast(np.ndarray, element).dtype
62 dtypes[d] = np.dtype(type(element))
67 """Get the combined dtype for a collection of arrays to prevent loss
73 The arrays to use for calculating the dtype
85 """A numpy array with an origin and (optional) bands
87 This class contains a 2D numpy array with the addition of an
88 origin (``yx0``) and an optional first index (``bands``) that
89 allows an immutable named index to be used.
93 One of the main limitations of using numpy arrays to store image data
94 is the lack of an ``origin`` attribute that allows an array to retain
95 knowledge of it's location in a larger scene.
96 For example, if a numpy array ``x`` is sliced, eg. ``x[10:20, 30:40]``
97 the result will be a new ``10x10`` numpy array that has no meta
98 data to inform the user that it was sliced from a larger image.
99 In addition, astrophysical images are also multi-band data cubes,
100 with a 2D image in each band (in fact this is the simplifying
101 assumption that distinguishes scarlet lite from scarlet main).
102 However, the ordering of the bands during processing might differ from
103 the ordering of the bands to display multiband data.
104 So a mechanism was also desired to simplify the sorting and index of
105 an image by band name.
107 Thus, scarlet lite creates a numpy-array like class with the additional
108 ``bands`` and ``yx0`` attributes to keep track of the bands contained
109 in an array and the origin of that array (we specify ``yx0`` as opposed
110 to ``xy0`` to be consistent with the default numpy/C++ ``(y, x)``
111 ordering of arrays as opposed to the traditional cartesian ``(x, y)``
112 ordering used in astronomy and other modules in the science pipelines.
113 While this may be a small source of confusion for the user,
114 it is consistent with the ordering in the original scarlet package and
115 ensures the consistency of scarlet lite images and python index slicing.
120 The easiest way to create a new image is to use ``Image(numpy_array)``,
123 >>> import numpy as np
124 >>> from lsst.scarlet.lite import Image
126 >>> x = np.arange(12).reshape(3, 4)
134 bbox=Box(shape=(3, 4), origin=(0, 0))
136 This will create a single band :py:class:`~lsst.scarlet.lite.Image` with
138 To create a multi-band image the input array must have 3 dimensions and
139 the ``bands`` property must be specified:
141 >>> x = np.arange(24).reshape(2, 3, 4)
142 >>> image = Image(x, bands=("i", "z"))
153 bbox=Box(shape=(3, 4), origin=(0, 0))
155 It is also possible to create an empty single-band image using the
156 ``from_box`` static method:
158 >>> from lsst.scarlet.lite import Box
159 >>> image = Image.from_box(Box((3, 4), (100, 120)))
166 bbox=Box(shape=(3, 4), origin=(100, 120))
168 Similarly, an empty multi-band image can be created by passing a tuple
171 >>> image = Image.from_box(Box((3, 4)), bands=("r", "i"))
182 bbox=Box(shape=(3, 4), origin=(0, 0))
184 To select a sub-image use a ``Box`` to select a spatial region in either a
185 single-band or multi-band image:
187 >>> x = np.arange(60).reshape(3, 4, 5)
188 >>> image = Image(x, bands=("g", "r", "i"), yx0=(20, 30))
189 >>> bbox = Box((2, 2), (21, 32))
190 >>> print(image[bbox])
200 bands=('g', 'r', 'i')
201 bbox=Box(shape=(2, 2), origin=(21, 32))
204 To select a single-band image from a multi-band image,
205 pass the name of the band as an index:
207 >>> print(image["r"])
214 bbox=Box(shape=(4, 5), origin=(20, 30))
216 Multi-band images can also be sliced in the spatial dimension, for example
218 >>> print(image["g":"r"])
230 bbox=Box(shape=(4, 5), origin=(20, 30))
234 >>> print(image["r":"r"])
241 bbox=Box(shape=(4, 5), origin=(20, 30))
243 both extract a slice of a multi-band image.
246 Unlike numerical indices, where ``slice(x, y)`` will select the
247 subset of an array from ``x`` to ``y-1`` (excluding ``y``),
248 a spectral slice of an ``Image`` will return the image slice
249 including band ``y``.
251 It is also possible to change the order or index a subset of bands
252 in an image. For example:
254 >>> print(image[("r", "g", "i")])
270 bands=('r', 'g', 'i')
271 bbox=Box(shape=(4, 5), origin=(20, 30))
274 will return a new image with the bands re-ordered.
276 Images can be combined using the standard arithmetic operations similar to
277 numpy arrays, including ``+, -, *, /, **`` etc, however, if two images are
278 combined with different bounding boxes, the _union_ of the two
279 boxes is used for the result. For example:
281 >>> image1 = Image(np.ones((2, 3, 4)), bands=tuple("gr"))
282 >>> image2 = Image(np.ones((2, 3, 4)), bands=tuple("gr"), yx0=(2, 3))
283 >>> result = image1 + image2
286 [[[1. 1. 1. 1. 0. 0. 0.]
287 [1. 1. 1. 1. 0. 0. 0.]
288 [1. 1. 1. 2. 1. 1. 1.]
289 [0. 0. 0. 1. 1. 1. 1.]
290 [0. 0. 0. 1. 1. 1. 1.]]
292 [[1. 1. 1. 1. 0. 0. 0.]
293 [1. 1. 1. 1. 0. 0. 0.]
294 [1. 1. 1. 2. 1. 1. 1.]
295 [0. 0. 0. 1. 1. 1. 1.]
296 [0. 0. 0. 1. 1. 1. 1.]]]
298 bbox=Box(shape=(5, 7), origin=(0, 0))
300 If instead you want to additively ``insert`` image 1 into image 2,
301 so that they have the same bounding box as image 2, use
303 >>> _ = image2.insert(image1)
314 bbox=Box(shape=(3, 4), origin=(2, 3))
316 To insert an image using a different operation use
318 >>> from operator import truediv
319 >>> _ = image2.insert(image1, truediv)
330 bbox=Box(shape=(3, 4), origin=(2, 3))
333 However, depending on the operation you may get unexpected results
334 since now there could be ``NaN`` and ``inf`` values due to the zeros
335 in the non-overlapping regions.
336 Instead, to select only the overlap region one can use
338 >>> result = image1 / image2
339 >>> print(result[image1.bbox & image2.bbox])
345 bbox=Box(shape=(1, 1), origin=(2, 3))
351 The array data for the image.
353 The bands coving the image.
355 The (y, x) offset for the lower left of the image.
361 bands: Sequence |
None =
None,
362 yx0: tuple[int, int] |
None =
None,
364 if bands
is None or len(bands) == 0:
367 assert data.ndim == 2
370 assert data.ndim == 3
371 if data.shape[0] != len(bands):
372 raise ValueError(f
"Array has spectral size {data.shape[0]}, but {bands} bands")
380 def from_box(bbox: Box, bands: tuple |
None =
None, dtype: DTypeLike = float) -> Image:
381 """Initialize an empty image from a bounding Box and optional bands
386 The bounding box that contains the image.
388 The bands for the image.
389 If bands is `None` then a 2D image is created.
391 The numpy dtype of the image.
396 An empty image contained in ``bbox`` with ``bands`` bands.
398 if bands
is not None and len(bands) > 0:
399 shape = (len(bands),) + bbox.shape
402 data = np.zeros(shape, dtype=dtype)
403 return Image(data, bands=bands, yx0=cast(tuple[int, int], bbox.origin))
407 """The shape of the image.
409 This includes the spectral dimension, if there is one.
411 return self.
_data.shape
415 """The numpy dtype of the image."""
416 return self.
_data.dtype
420 """The bands used in the image."""
425 """Number of bands in the image.
427 If `n_bands == 0` then the image is 2D and does not have a spectral
434 """Whether or not the image has a spectral dimension."""
439 """Height of the image."""
440 return self.
shape[-2]
444 """Width of the image."""
445 return self.
shape[-1]
448 def yx0(self) -> tuple[int, int]:
449 """Origin of the image, in numpy/C++ y,x ordering."""
454 """location of the y-offset."""
459 """Location of the x-offset."""
464 """Bounding box for the special dimensions in the image."""
469 """The image viewed as a numpy array."""
474 """Number of dimensions in the image."""
475 return self.
_data.ndim
478 """The indices to extract each band in `bands` in order from the image
480 This converts a band name, or list of band names,
481 into numerical indices that can be used to slice the internal numpy
487 If `bands` is a list of band names, then the result will be an
488 index corresponding to each band, in order.
489 If `bands` is a slice, then the ``start`` and ``stop`` properties
490 should be band names, and the result will be a slice with the
491 appropriate indices to start at `bands.start` and end at
497 Tuple of indices for each band in this image.
499 if isinstance(bands, slice):
502 if bands.start
is None:
505 start = self.
bands.index(bands.start)
506 if bands.stop
is None:
509 stop = self.
bands.index(bands.stop) + 1
510 return slice(start, stop, bands.step)
512 if isinstance(bands, str):
513 return (self.
bands.index(bands),)
515 band_indices = tuple(self.
bands.index(band)
for band
in bands
if band
in self.
bands)
521 ) -> tuple[tuple[int, ...] | slice, tuple[int, ...] | slice]:
522 """Match bands between two images
527 The other image to match spectral indices to.
532 A tuple with a tuple of indices/slices for each dimension,
533 including the spectral dimension.
537 return slice(
None), slice(
None)
541 if self.
n_bands == 0
and other.n_bands > 1:
542 err =
"Attempted to insert a monochromatic image into a mutli-band image"
543 raise ValueError(err)
544 if other.n_bands == 0:
545 err =
"Attempted to insert a multi-band image into a monochromatic image"
546 raise ValueError(err)
549 matched_bands = tuple(self.
bands[bidx]
for bidx
in self_indices)
550 other_indices = cast(tuple[int, ...], other.spectral_indices(matched_bands))
551 return other_indices, self_indices
553 def matched_slices(self, bbox: Box) -> tuple[tuple[slice, ...], tuple[slice, ...]]:
554 """Get the slices to match this image to a given bounding box
559 The bounding box to match this image to.
564 Tuple of indices/slices to match this image to the given bbox.
568 _slice = (slice(
None),) * bbox.ndim
569 return _slice, _slice
576 bands: object | tuple[object] |
None =
None,
577 bbox: Box |
None =
None,
579 """Project this image into a different set of bands
584 Spectral bands to project this image into.
585 Not all bands have to be contained in the image, and not all
586 bands contained in the image have to be used in the projection.
588 A bounding box to project the image into.
593 A new image creating by projecting this image into
598 if not isinstance(bands, tuple):
602 data = self.
data[indices, :]
607 return Image(data, bands=bands, yx0=self.
yx0)
610 image = np.zeros((len(bands),) + bbox.shape, dtype=data.dtype)
611 slices = bbox.overlapped_slices(self.
bbox)
613 image[(slice(
None),) + slices[0]] = data[(slice(
None),) + slices[1]]
614 return Image(image, bands=bands, yx0=cast(tuple[int, int], bbox.origin))
616 image = np.zeros(bbox.shape, dtype=data.dtype)
617 slices = bbox.overlapped_slices(self.
bbox)
618 image[slices[0]] = data[slices[1]]
619 return Image(image, bands=bands, yx0=cast(tuple[int, int], bbox.origin))
623 """Return the slices required to slice a multiband image"""
629 op: Callable = operator.add,
631 """Insert this image into another image in place.
636 The image to insert this image into.
638 The operator to use when combining the images.
643 `image` updated by inserting this instance.
647 def insert(self, image: Image, op: Callable = operator.add) -> Image:
648 """Insert another image into this image in place.
653 The image to insert this image into.
655 The operator to use when combining the images.
660 This instance with `image` inserted.
665 """Project a 2D image into the spectral dimension
670 The bands in the projected image.
675 The 2D image repeated in each band in the spectral dimension.
678 raise ValueError(
"Image.repeat only works with 2D images")
680 np.repeat(self.
data[
None, :, :], len(bands), axis=0),
685 def copy(self, order=None) -> Image:
686 """Make a copy of this image.
691 The ordering to use for storing the bytes.
692 This is unlikely to be needed, and just defaults to
693 the numpy behavior (C) ordering.
698 The copy of this image.
704 data: np.ndarray |
None =
None,
705 order: str |
None =
None,
706 bands: tuple[str, ...] |
None =
None,
707 yx0: tuple[int, int] |
None =
None,
709 """Copy of this image with some parameters updated.
711 Any parameters not specified by the user will be copied from the
717 An update for the data in the image.
719 The ordering for stored bytes, from numpy.copy.
721 The bands that the resulting image will have.
722 The number of bands must be the same as the first dimension
725 The lower-left of the image bounding box.
740 return Image(data, bands, yx0)
742 def trimmed(self, threshold: float = 0) -> Image:
743 """Return a copy of the image trimmed to a threshold.
745 This is essentially the smallest image that contains all of the
746 pixels above the threshold.
751 The threshold to use for trimming the image.
756 A copy of the image trimmed to the threshold.
759 bbox = Box.from_data(data, threshold=threshold)
760 data = data[bbox.slices]
763 return Image(data, yx0=(y0 + self.
y0, x0 + self.
x0))
765 def at(self, y: int, x: int) -> ScalarLike | np.ndarray:
766 """The value of the image at a given location.
768 Image does not implment single index access because the
769 result is a scalar, while indexing an image returns another image.
774 The y-coordinate of the location.
776 The x-coordinate of the location.
781 The value of the image at the given location.
786 return self.
data[_y, _x]
787 return self.
data[:, _y, _x]
789 def _i_update(self, op: Callable, other: Image | ScalarLike) -> Image:
790 """Update the data array in place.
792 This is typically implemented by `__i<op>__` methods,
793 like `__iadd__`, to apply an operator and update this image
794 with the data in place.
799 Operator used to combine this image with the `other` image.
801 The other image that is combined with this one using the operator
807 This image, after being updated by the operator
810 if self.
dtype != dtype:
811 if hasattr(other,
"dtype"):
812 _dtype = cast(np.ndarray, other).dtype
815 msg = f
"Cannot update an array with type {self.dtype} with {_dtype}"
816 raise ValueError(msg)
818 self.
_data[:] = result.data
819 self.
_bands = result.bands
820 self.
_yx0 = result.yx0
824 """Compare this array to another.
826 This performs an element by element equality check.
831 The image to compare this image to.
833 The operator used for the comparision (==, !=, >=, <=).
838 An image made by checking all of the elements in this array with
844 If `other` is not an `Image`.
845 MismatchedBandsError:
846 If `other` has different bands.
848 if `other` exists in a different bounding box.
850 if isinstance(other, Image)
and other.bands == self.
bands and other.bbox == self.
bbox:
853 if not isinstance(other, Image):
854 if type(other)
in ScalarTypes:
856 raise TypeError(f
"Cannot compare images to {type(other)}")
858 if other.bands != self.
bands:
859 msg = f
"Cannot compare images with mismatched bands: {self.bands} vs {other.bands}"
863 f
"Cannot compare images with different bounds boxes: {self.bbox} vs. {other.bbox}"
866 def __eq__(self, other: object) -> Image:
867 """Check if this image is equal to another."""
868 if not isinstance(other, Image)
and not isinstance(other, ScalarTypes):
869 raise TypeError(f
"Cannot compare an Image to {type(other)}.")
872 def __ne__(self, other: object) -> Image:
873 """Check if this image is not equal to another."""
874 return ~self.
__eq__(other)
876 def __ge__(self, other: Image | ScalarLike) -> Image:
877 """Check if this image is greater than or equal to another."""
878 if type(other)
in ScalarTypes:
882 def __le__(self, other: Image | ScalarLike) -> Image:
883 """Check if this image is less than or equal to another."""
884 if type(other)
in ScalarTypes:
888 def __gt__(self, other: Image | ScalarLike) -> Image:
889 """Check if this image is greater than or equal to another."""
890 if type(other)
in ScalarTypes:
894 def __lt__(self, other: Image | ScalarLike) -> Image:
895 """Check if this image is less than or equal to another."""
896 if type(other)
in ScalarTypes:
901 """Take the negative of the image."""
905 """Make a copy using of the image."""
909 """Take the inverse (~) of the image."""
912 def __add__(self, other: Image | ScalarLike) -> Image:
913 """Combine this image and another image using addition."""
916 def __iadd__(self, other: Image | ScalarLike) -> Image:
917 """Combine this image and another image using addition and update
922 def __radd__(self, other: Image | ScalarLike) -> Image:
923 """Combine this image and another image using addition,
924 with this image on the right.
926 if type(other)
in ScalarTypes:
928 return cast(Image, other).__add__(self)
930 def __sub__(self, other: Image | ScalarLike) -> Image:
931 """Combine this image and another image using subtraction."""
934 def __isub__(self, other: Image | ScalarLike) -> Image:
935 """Combine this image and another image using subtraction,
936 with this image on the right.
940 def __rsub__(self, other: Image | ScalarLike) -> Image:
941 """Combine this image and another image using subtraction,
942 with this image on the right.
944 if type(other)
in ScalarTypes:
946 return cast(Image, other).__sub__(self)
948 def __mul__(self, other: Image | ScalarLike) -> Image:
949 """Combine this image and another image using multiplication."""
952 def __imul__(self, other: Image | ScalarLike) -> Image:
953 """Combine this image and another image using multiplication,
954 with this image on the right.
958 def __rmul__(self, other: Image | ScalarLike) -> Image:
959 """Combine this image and another image using multiplication,
960 with this image on the right.
962 if type(other)
in ScalarTypes:
964 return cast(Image, other).__mul__(self)
966 def __truediv__(self, other: Image | ScalarLike) -> Image:
967 """Divide this image by `other`."""
971 """Divide this image by `other` in place."""
975 """Divide this image by `other` with this on the right."""
976 if type(other)
in ScalarTypes:
978 return cast(Image, other).__truediv__(self)
980 def __floordiv__(self, other: Image | ScalarLike) -> Image:
981 """Floor divide this image by `other` in place."""
985 """Floor divide this image by `other` in place."""
989 """Floor divide this image by `other` with this on the right."""
990 if type(other)
in ScalarTypes:
992 return cast(Image, other).__floordiv__(self)
994 def __pow__(self, other: Image | ScalarLike) -> Image:
995 """Raise this image to the `other` power."""
998 def __ipow__(self, other: Image | ScalarLike) -> Image:
999 """Raise this image to the `other` power in place."""
1002 def __rpow__(self, other: Image | ScalarLike) -> Image:
1003 """Raise this other to the power of this image."""
1004 if type(other)
in ScalarTypes:
1006 return cast(Image, other).__pow__(self)
1008 def __mod__(self, other: Image | ScalarLike) -> Image:
1009 """Take the modulus of this % other."""
1012 def __imod__(self, other: Image | ScalarLike) -> Image:
1013 """Take the modulus of this % other in place."""
1016 def __rmod__(self, other: Image | ScalarLike) -> Image:
1017 """Take the modulus of other % this."""
1018 if type(other)
in ScalarTypes:
1020 return cast(Image, other).__mod__(self)
1022 def __and__(self, other: Image | ScalarLike) -> Image:
1023 """Take the bitwise and of this and other."""
1026 def __iand__(self, other: Image | ScalarLike) -> Image:
1027 """Take the bitwise and of this and other in place."""
1030 def __rand__(self, other: Image | ScalarLike) -> Image:
1031 """Take the bitwise and of other and this."""
1032 if type(other)
in ScalarTypes:
1034 return cast(Image, other).__and__(self)
1036 def __or__(self, other: Image | ScalarLike) -> Image:
1037 """Take the binary or of this or other."""
1040 def __ior__(self, other: Image | ScalarLike) -> Image:
1041 """Take the binary or of this or other in place."""
1044 def __ror__(self, other: Image | ScalarLike) -> Image:
1045 """Take the binary or of other or this."""
1046 if type(other)
in ScalarTypes:
1048 return cast(Image, other).__or__(self)
1050 def __xor__(self, other: Image | ScalarLike) -> Image:
1051 """Take the binary xor of this xor other."""
1054 def __ixor__(self, other: Image | ScalarLike) -> Image:
1055 """Take the binary xor of this xor other in place."""
1058 def __rxor__(self, other: Image | ScalarLike) -> Image:
1059 """Take the binary xor of other xor this."""
1060 if type(other)
in ScalarTypes:
1062 return cast(Image, other).__xor__(self)
1065 """Shift this image to the left by other bits."""
1066 if not issubclass(np.dtype(type(other)).type, np.integer):
1067 raise TypeError(
"Bit shifting an image can only be done with integers")
1071 """Shift this image to the left by other bits in place."""
1076 """Shift other to the left by this image bits."""
1080 """Shift this image to the right by other bits."""
1081 if not issubclass(np.dtype(type(other)).type, np.integer):
1082 raise TypeError(
"Bit shifting an image can only be done with integers")
1086 """Shift this image to the right by other bits in place."""
1091 """Shift other to the right by this image bits."""
1095 """Display the image array, bands, and bounding box."""
1096 return f
"Image:\n {str(self.data)}\n bands={self.bands}\n bbox={self.bbox}"
1099 """Check to see if an index is a spectral index.
1104 Either a slice, a tuple, or an element in `Image.bands`.
1109 ``True`` if `index` is band or tuple of bands.
1112 if isinstance(index, slice):
1113 if index.start
in bands
or index.stop
in bands
or (index.start
is None and index.stop
is None):
1116 if index
in self.
bands:
1118 if isinstance(index, tuple)
and index[0]
in self.
bands:
1123 """Get the slices of the image to insert it into the overlapping
1124 region with `bbox`."""
1125 overlap = self.
bbox & bbox
1127 raise IndexError(
"Bounding box is outside of the image")
1128 origin = bbox.origin
1130 y_start = origin[0] - self.
yx0[0]
1131 y_stop = origin[0] + shape[0] - self.
yx0[0]
1132 x_start = origin[1] - self.
yx0[1]
1133 x_stop = origin[1] + shape[1] - self.
yx0[1]
1134 y_index = slice(y_start, y_stop)
1135 x_index = slice(x_start, x_stop)
1136 return y_index, x_index
1138 def _get_sliced(self, indices: Any, value: Image |
None =
None) -> Image:
1139 """Select a subset of an image
1144 The indices to select a subsection of the image.
1145 The spectral index can either be a tuple of indices,
1146 a slice of indices, or a single index used to select a
1147 single-band 2D image.
1148 The spatial index (if present) is a `Box`.
1151 The value used to set this slice of the image.
1152 This allows the single `_get_sliced` method to be used for
1153 both getting a slice of an image and setting it.
1157 result: Image | np.ndarray
1158 The resulting image obtained by selecting subsets of the iamge
1159 based on the `indices`.
1161 if not isinstance(indices, tuple):
1162 indices = (indices,)
1166 if len(indices) > 1
and indices[1]
in self.
bands:
1171 y_index = x_index = slice(
None)
1175 if isinstance(spectral_index, slice):
1176 bands = self.
bands[spectral_index]
1177 elif len(spectral_index) == 1:
1179 spectral_index = spectral_index[0]
1181 bands = tuple(self.
bands[idx]
for idx
in spectral_index)
1182 indices = indices[1:]
1183 if len(indices) == 1:
1185 if not isinstance(indices[0], Box):
1186 raise IndexError(f
"Expected a Box for the spatial index but got {indices[1]}")
1188 elif len(indices) == 0:
1189 y_index = x_index = slice(
None)
1191 raise IndexError(f
"Too many spatial indices, expeected a Box bot got {indices}")
1192 full_index = (spectral_index, y_index, x_index)
1193 elif isinstance(indices[0], Box):
1196 full_index = (slice(
None), y_index, x_index)
1198 error = f
"3D images can only be indexed by spectral indices or bounding boxes, got {indices}"
1199 raise IndexError(error)
1201 if len(indices) != 1
or not isinstance(indices[0], Box):
1202 raise IndexError(f
"2D images can only be sliced by bounding box, got {indices}")
1205 full_index = (y_index, x_index)
1218 yx0 = (y0 + self.
yx0[0], x0 + self.
yx0[1])
1220 data = self.
data[full_index]
1224 return Image(data, yx0=yx0)
1225 return Image(data, bands=bands, yx0=yx0)
1228 self.
_data[full_index] = value.data
1232 """Get the slices needed to insert this image into a bounding box.
1237 The region to insert this image into.
1242 The slice of this image and the slice of the `bbox` required to
1243 insert the overlapping portion of this image.
1248 overlap = (slice(
None),) + overlap[0], (slice(
None),) + overlap[1]
1252 """Get the subset of an image
1257 The indices to select a subsection of the image.
1262 The resulting image obtained by selecting subsets of the iamge
1263 based on the `indices`.
1268 """Set a subset of an image to a given value
1273 The indices to select a subsection of the image.
1275 The value to use for the subset of the image.
1280 The resulting image obtained by selecting subsets of the image
1281 based on the `indices`.
1287 """Perform an operation on two images, that may or may not be spectrally
1288 and spatially aligned.
1293 The image on the LHS of the operation
1295 The image on the RHS of the operation
1297 The operation used to combine the images.
1302 The resulting combined image.
1304 if type(image2)
in ScalarTypes:
1305 return image1.copy_with(data=op(image1.data, image2))
1306 image2 = cast(Image, image2)
1307 if image1.bands == image2.bands
and image1.bbox == image2.bbox:
1309 with np.errstate(divide=
"ignore", invalid=
"ignore"):
1310 result = op(image1.data, image2.data)
1311 return Image(result, bands=image1.bands, yx0=image1.yx0)
1313 if op != operator.add
and op != operator.sub
and image1.bands != image2.bands:
1314 msg =
"Images with different bands can only be combined using addition and subtraction, "
1315 msg += f
"got {op}, with bands {image1.bands}, {image2.bands}"
1316 raise ValueError(msg)
1319 bands = image1.bands
1321 bands = bands + tuple(band
for band
in image2.bands
if band
not in bands)
1323 bbox = image1.bbox | image2.bbox
1326 shape = (len(bands),) + bbox.shape
1330 if op == operator.add
or op == operator.sub:
1332 result =
Image(np.zeros(shape, dtype=dtype), bands=bands, yx0=cast(tuple[int, int], bbox.origin))
1334 image1.insert_into(result, operator.add)
1336 image2.insert_into(result, op)
1339 image1 = image1.project(bbox=bbox)
1340 image2 = image2.project(bbox=bbox)
1341 result = op(image1, image2)
1348 op: Callable = operator.add,
1350 """Insert one image into another image
1355 The image that will have `sub_image` insertd.
1357 The image that is inserted into `main_image`.
1359 The operator to use for insertion
1360 (addition, subtraction, multiplication, etc.).
1365 The `main_image`, with the `sub_image` inserted in place.
1367 if len(main_image.bands) == 0
and len(sub_image.bands) == 0:
1368 slices = sub_image.matched_slices(main_image.bbox)
1369 image_slices = slices[1]
1370 self_slices = slices[0]
1372 band_indices = sub_image.matched_spectral_indices(main_image)
1373 slices = sub_image.matched_slices(main_image.bbox)
1374 image_slices = (band_indices[0],) + slices[1]
1375 self_slices = (band_indices[1],) + slices[0]
1377 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 _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)