async-geotiff 0.1.0b3__py3-none-any.whl → 0.1.0b5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- async_geotiff/__init__.py +13 -1
- async_geotiff/_array.py +121 -0
- async_geotiff/_fetch.py +154 -0
- async_geotiff/_geotiff.py +87 -111
- async_geotiff/_overview.py +44 -13
- async_geotiff/_read.py +185 -0
- async_geotiff/_tile.py +26 -0
- async_geotiff/_transform.py +90 -0
- async_geotiff/_windows.py +76 -0
- async_geotiff/colormap.py +104 -0
- async_geotiff/exceptions.py +5 -0
- async_geotiff/tms.py +2 -2
- async_geotiff-0.1.0b5.dist-info/METADATA +128 -0
- async_geotiff-0.1.0b5.dist-info/RECORD +20 -0
- async_geotiff-0.1.0b3.dist-info/METADATA +0 -53
- async_geotiff-0.1.0b3.dist-info/RECORD +0 -12
- {async_geotiff-0.1.0b3.dist-info → async_geotiff-0.1.0b5.dist-info}/WHEEL +0 -0
- {async_geotiff-0.1.0b3.dist-info → async_geotiff-0.1.0b5.dist-info}/licenses/LICENSE +0 -0
async_geotiff/_overview.py
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from functools import cached_property
|
|
5
4
|
from typing import TYPE_CHECKING
|
|
6
5
|
|
|
7
6
|
from affine import Affine
|
|
8
7
|
|
|
8
|
+
from async_geotiff._fetch import FetchTileMixin
|
|
9
|
+
from async_geotiff._read import ReadMixin
|
|
10
|
+
from async_geotiff._transform import TransformMixin
|
|
11
|
+
|
|
9
12
|
if TYPE_CHECKING:
|
|
10
|
-
from async_tiff import GeoKeyDirectory, ImageFileDirectory
|
|
13
|
+
from async_tiff import TIFF, GeoKeyDirectory, ImageFileDirectory
|
|
14
|
+
from pyproj.crs import CRS
|
|
11
15
|
|
|
12
16
|
from async_geotiff import GeoTIFF
|
|
13
17
|
|
|
18
|
+
# ruff: noqa: SLF001
|
|
19
|
+
|
|
14
20
|
|
|
15
21
|
@dataclass(init=False, frozen=True, kw_only=True, eq=False, repr=False)
|
|
16
|
-
class Overview:
|
|
22
|
+
class Overview(ReadMixin, FetchTileMixin, TransformMixin):
|
|
17
23
|
"""An overview level of a Cloud-Optimized GeoTIFF image."""
|
|
18
24
|
|
|
19
25
|
_geotiff: GeoTIFF
|
|
@@ -24,13 +30,13 @@ class Overview:
|
|
|
24
30
|
"""The GeoKeyDirectory of the primary IFD.
|
|
25
31
|
"""
|
|
26
32
|
|
|
27
|
-
_ifd:
|
|
33
|
+
_ifd: ImageFileDirectory
|
|
28
34
|
"""The IFD for this overview level.
|
|
29
35
|
|
|
30
36
|
(positional index of the IFD in the TIFF file, IFD object)
|
|
31
37
|
"""
|
|
32
38
|
|
|
33
|
-
_mask_ifd:
|
|
39
|
+
_mask_ifd: ImageFileDirectory | None
|
|
34
40
|
"""The IFD for the mask associated with this overview level, if any.
|
|
35
41
|
|
|
36
42
|
(positional index of the IFD in the TIFF file, IFD object)
|
|
@@ -42,8 +48,8 @@ class Overview:
|
|
|
42
48
|
*,
|
|
43
49
|
geotiff: GeoTIFF,
|
|
44
50
|
gkd: GeoKeyDirectory,
|
|
45
|
-
ifd:
|
|
46
|
-
mask_ifd:
|
|
51
|
+
ifd: ImageFileDirectory,
|
|
52
|
+
mask_ifd: ImageFileDirectory | None,
|
|
47
53
|
) -> Overview:
|
|
48
54
|
instance = cls.__new__(cls)
|
|
49
55
|
|
|
@@ -55,13 +61,38 @@ class Overview:
|
|
|
55
61
|
|
|
56
62
|
return instance
|
|
57
63
|
|
|
64
|
+
@property
|
|
65
|
+
def _tiff(self) -> TIFF:
|
|
66
|
+
"""A reference to the underlying TIFF object."""
|
|
67
|
+
return self._geotiff._tiff
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def crs(self) -> CRS:
|
|
71
|
+
"""The coordinate reference system of the overview."""
|
|
72
|
+
return self._geotiff.crs
|
|
73
|
+
|
|
58
74
|
@property
|
|
59
75
|
def height(self) -> int:
|
|
60
76
|
"""The height of the overview in pixels."""
|
|
61
|
-
return self._ifd
|
|
77
|
+
return self._ifd.image_height
|
|
62
78
|
|
|
63
|
-
@
|
|
64
|
-
def
|
|
79
|
+
@property
|
|
80
|
+
def nodata(self) -> int | float | None:
|
|
81
|
+
"""The nodata value for the overview, if any."""
|
|
82
|
+
return self._geotiff.nodata
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def tile_height(self) -> int:
|
|
86
|
+
"""The height in pixels per tile of the overview."""
|
|
87
|
+
return self._ifd.tile_height or self.height
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def tile_width(self) -> int:
|
|
91
|
+
"""The width in pixels per tile of the overview."""
|
|
92
|
+
return self._ifd.tile_width or self.width
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def transform(self) -> Affine: # type: ignore[override]
|
|
65
96
|
"""The affine transform mapping pixel coordinates to geographic coordinates.
|
|
66
97
|
|
|
67
98
|
Returns:
|
|
@@ -70,9 +101,9 @@ class Overview:
|
|
|
70
101
|
"""
|
|
71
102
|
full_transform = self._geotiff.transform
|
|
72
103
|
|
|
73
|
-
overview_width = self._ifd
|
|
104
|
+
overview_width = self._ifd.image_width
|
|
74
105
|
full_width = self._geotiff.width
|
|
75
|
-
overview_height = self._ifd
|
|
106
|
+
overview_height = self._ifd.image_height
|
|
76
107
|
full_height = self._geotiff.height
|
|
77
108
|
|
|
78
109
|
scale_x = full_width / overview_width
|
|
@@ -83,4 +114,4 @@ class Overview:
|
|
|
83
114
|
@property
|
|
84
115
|
def width(self) -> int:
|
|
85
116
|
"""The width of the overview in pixels."""
|
|
86
|
-
return self._ifd
|
|
117
|
+
return self._ifd.image_width
|
async_geotiff/_read.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Higher-level read utilities for cross-tile operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Protocol
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from affine import Affine
|
|
9
|
+
|
|
10
|
+
from async_geotiff._array import Array
|
|
11
|
+
from async_geotiff._fetch import HasTiffReference
|
|
12
|
+
from async_geotiff._windows import Window
|
|
13
|
+
from async_geotiff.exceptions import WindowError
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from numpy.typing import NDArray
|
|
17
|
+
|
|
18
|
+
from async_geotiff._tile import Tile
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CanFetchTiles(HasTiffReference, Protocol):
|
|
22
|
+
"""Protocol for objects that can fetch tiles."""
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def height(self) -> int:
|
|
26
|
+
"""The height of the image in pixels."""
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def width(self) -> int:
|
|
31
|
+
"""The width of the image in pixels."""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
async def fetch_tiles(
|
|
35
|
+
self,
|
|
36
|
+
xs: list[int],
|
|
37
|
+
ys: list[int],
|
|
38
|
+
) -> list[Tile]: ...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ReadMixin:
|
|
42
|
+
async def read(
|
|
43
|
+
self: CanFetchTiles,
|
|
44
|
+
*,
|
|
45
|
+
window: Window | None = None,
|
|
46
|
+
) -> Array:
|
|
47
|
+
"""Read pixel data for a window region.
|
|
48
|
+
|
|
49
|
+
This method fetches all tiles that intersect the given window and
|
|
50
|
+
stitches them together, returning only the pixels within the window.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
window: A Window object defining the pixel region to read.
|
|
54
|
+
If None, the entire image is read.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
An Array containing the pixel data for the requested window.
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
WindowError: If the window extends outside the image bounds.
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
return await read(self, window=window)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def read(
|
|
67
|
+
self: CanFetchTiles,
|
|
68
|
+
*,
|
|
69
|
+
window: Window | None = None,
|
|
70
|
+
) -> Array:
|
|
71
|
+
if isinstance(window, Window):
|
|
72
|
+
win = window
|
|
73
|
+
else:
|
|
74
|
+
win = Window(col_off=0, row_off=0, width=self.width, height=self.height)
|
|
75
|
+
|
|
76
|
+
# Most validation occurred in construction of Window; here we just check against
|
|
77
|
+
# image size
|
|
78
|
+
if win.col_off + win.width > self.width or win.row_off + win.height > self.height:
|
|
79
|
+
raise WindowError(
|
|
80
|
+
f"Window extends outside image bounds.\n"
|
|
81
|
+
f"Window: cols={win.col_off}:{win.col_off + win.width}, "
|
|
82
|
+
f"rows={win.row_off}:{win.row_off + win.height}.\n"
|
|
83
|
+
f"Image size: {self.height}x{self.width}",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Calculate which tiles we need to fetch
|
|
87
|
+
tile_x_start = win.col_off // self.tile_width
|
|
88
|
+
tile_x_stop = (win.col_off + win.width - 1) // self.tile_width + 1
|
|
89
|
+
tile_y_start = win.row_off // self.tile_height
|
|
90
|
+
tile_y_stop = (win.row_off + win.height - 1) // self.tile_height + 1
|
|
91
|
+
|
|
92
|
+
# Build list of tile coordinates
|
|
93
|
+
xs: list[int] = []
|
|
94
|
+
ys: list[int] = []
|
|
95
|
+
for tx in range(tile_x_start, tile_x_stop):
|
|
96
|
+
for ty in range(tile_y_start, tile_y_stop):
|
|
97
|
+
xs.append(tx)
|
|
98
|
+
ys.append(ty)
|
|
99
|
+
|
|
100
|
+
tiles = await self.fetch_tiles(xs, ys)
|
|
101
|
+
|
|
102
|
+
num_bands = tiles[0].array.count
|
|
103
|
+
dtype = tiles[0].array.data.dtype
|
|
104
|
+
|
|
105
|
+
# Create output array and mask array
|
|
106
|
+
output_data = np.empty((num_bands, win.height, win.width), dtype=dtype)
|
|
107
|
+
output_mask: NDArray[np.bool_] | None = None
|
|
108
|
+
if self._mask_ifd is not None:
|
|
109
|
+
output_mask = np.ones((win.height, win.width), dtype=np.bool_)
|
|
110
|
+
|
|
111
|
+
assemble_tiles(
|
|
112
|
+
tiles=tiles,
|
|
113
|
+
window=win,
|
|
114
|
+
tile_width=self.tile_width,
|
|
115
|
+
tile_height=self.tile_height,
|
|
116
|
+
output_data=output_data,
|
|
117
|
+
output_mask=output_mask,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
window_transform = self.transform * Affine.translation(
|
|
121
|
+
win.col_off,
|
|
122
|
+
win.row_off,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return Array(
|
|
126
|
+
data=output_data,
|
|
127
|
+
mask=output_mask,
|
|
128
|
+
width=win.width,
|
|
129
|
+
height=win.height,
|
|
130
|
+
count=num_bands,
|
|
131
|
+
transform=window_transform,
|
|
132
|
+
crs=self.crs,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def assemble_tiles( # noqa: PLR0913
|
|
137
|
+
*,
|
|
138
|
+
tiles: list[Tile],
|
|
139
|
+
window: Window,
|
|
140
|
+
tile_width: int,
|
|
141
|
+
tile_height: int,
|
|
142
|
+
output_data: NDArray[np.generic],
|
|
143
|
+
output_mask: NDArray[np.bool_] | None,
|
|
144
|
+
) -> None:
|
|
145
|
+
"""Assemble multiple tiles into output arrays.
|
|
146
|
+
|
|
147
|
+
This function copies data from tiles into the appropriate positions
|
|
148
|
+
in the output arrays, handling partial tiles at window boundaries.
|
|
149
|
+
"""
|
|
150
|
+
for tile in tiles:
|
|
151
|
+
# Create a window for this tile's position in image coordinates
|
|
152
|
+
tile_window = Window(
|
|
153
|
+
col_off=tile.x * tile_width,
|
|
154
|
+
row_off=tile.y * tile_height,
|
|
155
|
+
width=tile.array.width,
|
|
156
|
+
height=tile.array.height,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Calculate the intersection between tile and target window
|
|
160
|
+
overlap = window.intersection(tile_window)
|
|
161
|
+
|
|
162
|
+
# Calculate source slice within the tile
|
|
163
|
+
src_col_start = overlap.col_off - tile_window.col_off
|
|
164
|
+
src_col_stop = src_col_start + overlap.width
|
|
165
|
+
src_row_start = overlap.row_off - tile_window.row_off
|
|
166
|
+
src_row_stop = src_row_start + overlap.height
|
|
167
|
+
|
|
168
|
+
# Calculate destination slice within the output
|
|
169
|
+
dst_col_start = overlap.col_off - window.col_off
|
|
170
|
+
dst_col_stop = dst_col_start + overlap.width
|
|
171
|
+
dst_row_start = overlap.row_off - window.row_off
|
|
172
|
+
dst_row_stop = dst_row_start + overlap.height
|
|
173
|
+
|
|
174
|
+
# Copy data and mask if present
|
|
175
|
+
output_data[
|
|
176
|
+
:,
|
|
177
|
+
dst_row_start:dst_row_stop,
|
|
178
|
+
dst_col_start:dst_col_stop,
|
|
179
|
+
] = tile.array.data[:, src_row_start:src_row_stop, src_col_start:src_col_stop]
|
|
180
|
+
|
|
181
|
+
if output_mask is not None and tile.array.mask is not None:
|
|
182
|
+
output_mask[
|
|
183
|
+
dst_row_start:dst_row_stop,
|
|
184
|
+
dst_col_start:dst_col_stop,
|
|
185
|
+
] = tile.array.mask[src_row_start:src_row_stop, src_col_start:src_col_stop]
|
async_geotiff/_tile.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from async_tiff import ImageFileDirectory
|
|
8
|
+
|
|
9
|
+
from async_geotiff._array import Array
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, kw_only=True, eq=False)
|
|
13
|
+
class Tile:
|
|
14
|
+
"""A tile from a GeoTIFF, containing array data and grid position."""
|
|
15
|
+
|
|
16
|
+
x: int
|
|
17
|
+
"""The tile column index in the GeoTIFF or overview."""
|
|
18
|
+
|
|
19
|
+
y: int
|
|
20
|
+
"""The tile row index in the GeoTIFF or overview."""
|
|
21
|
+
|
|
22
|
+
_ifd: ImageFileDirectory
|
|
23
|
+
"""A reference to the IFD this tile belongs to."""
|
|
24
|
+
|
|
25
|
+
array: Array
|
|
26
|
+
"""The array data for this tile."""
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Mixin class for coordinate transformation methods."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from math import floor
|
|
6
|
+
from typing import TYPE_CHECKING, Literal, Protocol
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
|
|
11
|
+
from affine import Affine
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HasTransform(Protocol):
|
|
15
|
+
"""Protocol for objects that have an affine transform."""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def transform(self) -> Affine: ...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TransformMixin:
|
|
22
|
+
"""Mixin providing coordinate transformation methods.
|
|
23
|
+
|
|
24
|
+
Classes using this mixin must implement HasTransform.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def index(
|
|
28
|
+
self: HasTransform,
|
|
29
|
+
x: float,
|
|
30
|
+
y: float,
|
|
31
|
+
op: Callable[[float], int] = floor,
|
|
32
|
+
) -> tuple[int, int]:
|
|
33
|
+
"""Get the (row, col) index of the pixel containing (x, y).
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
x: x value in coordinate reference system.
|
|
37
|
+
y: y value in coordinate reference system.
|
|
38
|
+
op: Function to convert fractional pixels to whole numbers
|
|
39
|
+
(floor, ceiling, round). Defaults to math.floor.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
(row index, col index)
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
inv_transform = ~self.transform
|
|
46
|
+
# Affine * (x, y) returns tuple[float, float] for 2D coordinates
|
|
47
|
+
col_frac, row_frac = inv_transform * (x, y) # type: ignore[misc]
|
|
48
|
+
|
|
49
|
+
return (op(row_frac), op(col_frac))
|
|
50
|
+
|
|
51
|
+
def xy(
|
|
52
|
+
self: HasTransform,
|
|
53
|
+
row: int,
|
|
54
|
+
col: int,
|
|
55
|
+
offset: Literal["center", "ul", "ur", "ll", "lr"] = "center",
|
|
56
|
+
) -> tuple[float, float]:
|
|
57
|
+
"""Get the coordinates (x, y) of a pixel at (row, col).
|
|
58
|
+
|
|
59
|
+
The pixel's center is returned by default, but a corner can be returned
|
|
60
|
+
by setting `offset` to one of `"ul"`, `"ur"`, `"ll"`, `"lr"`.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
row: Pixel row.
|
|
64
|
+
col: Pixel column.
|
|
65
|
+
offset: Determines if the returned coordinates are for the center of the
|
|
66
|
+
pixel or for a corner.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
(x, y) coordinates in the dataset's CRS.
|
|
70
|
+
|
|
71
|
+
"""
|
|
72
|
+
if offset == "center":
|
|
73
|
+
c = col + 0.5
|
|
74
|
+
r = row + 0.5
|
|
75
|
+
elif offset == "ul":
|
|
76
|
+
c = col
|
|
77
|
+
r = row
|
|
78
|
+
elif offset == "ur":
|
|
79
|
+
c = col + 1
|
|
80
|
+
r = row
|
|
81
|
+
elif offset == "ll":
|
|
82
|
+
c = col
|
|
83
|
+
r = row + 1
|
|
84
|
+
elif offset == "lr":
|
|
85
|
+
c = col + 1
|
|
86
|
+
r = row + 1
|
|
87
|
+
else:
|
|
88
|
+
raise ValueError(f"Invalid offset value: {offset}")
|
|
89
|
+
|
|
90
|
+
return self.transform * (c, r)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Window utilities for defining rectangular subsets of rasters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from async_geotiff.exceptions import WindowError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class Window:
|
|
12
|
+
"""A rectangular subset of a raster.
|
|
13
|
+
|
|
14
|
+
Windows define pixel regions using column/row offsets and dimensions.
|
|
15
|
+
This class is similar to rasterio's [Window][rasterio.windows.Window] but supports
|
|
16
|
+
integer offsets and ranges only.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
col_off: int
|
|
20
|
+
"""The column offset (x position of the left edge)."""
|
|
21
|
+
|
|
22
|
+
row_off: int
|
|
23
|
+
"""The row offset (y position of the top edge)."""
|
|
24
|
+
|
|
25
|
+
width: int
|
|
26
|
+
"""The width in pixels (number of columns)."""
|
|
27
|
+
|
|
28
|
+
height: int
|
|
29
|
+
"""The height in pixels (number of rows)."""
|
|
30
|
+
|
|
31
|
+
def __post_init__(self) -> None:
|
|
32
|
+
"""Validate window dimensions."""
|
|
33
|
+
if self.col_off < 0 or self.row_off < 0:
|
|
34
|
+
raise WindowError(
|
|
35
|
+
f"Window start indices must be non-negative, "
|
|
36
|
+
f"got col_off={self.col_off}, row_off={self.row_off}",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if self.width <= 0:
|
|
40
|
+
raise WindowError(f"Window width must be positive, got {self.width}")
|
|
41
|
+
|
|
42
|
+
if self.height <= 0:
|
|
43
|
+
raise WindowError(f"Window height must be positive, got {self.height}")
|
|
44
|
+
|
|
45
|
+
def __repr__(self) -> str:
|
|
46
|
+
"""Return a nicely formatted representation string."""
|
|
47
|
+
return (
|
|
48
|
+
f"async_geotiff.Window(col_off={self.col_off}, row_off={self.row_off}, "
|
|
49
|
+
f"width={self.width}, height={self.height})"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def intersection(self, other: Window) -> Window:
|
|
53
|
+
"""Compute the intersection with another window.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
other: Another Window object.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
A new Window representing the overlapping region.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
WindowError: If windows do not intersect.
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
col_off = max(self.col_off, other.col_off)
|
|
66
|
+
row_off = max(self.row_off, other.row_off)
|
|
67
|
+
col_stop = min(self.col_off + self.width, other.col_off + other.width)
|
|
68
|
+
row_stop = min(self.row_off + self.height, other.row_off + other.height)
|
|
69
|
+
|
|
70
|
+
width = col_stop - col_off
|
|
71
|
+
height = row_stop - row_off
|
|
72
|
+
|
|
73
|
+
if width <= 0 or height <= 0:
|
|
74
|
+
raise WindowError(f"Windows do not intersect: {self} and {other}")
|
|
75
|
+
|
|
76
|
+
return Window(col_off=col_off, row_off=row_off, width=width, height=height)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""High-level Colormap class for GeoTIFF colormaps."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from async_tiff import Colormap as AsyncTiffColormap
|
|
12
|
+
from numpy.typing import NDArray
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, kw_only=True, eq=False)
|
|
16
|
+
class Colormap:
|
|
17
|
+
"""A representation of a GeoTIFF colormap.
|
|
18
|
+
|
|
19
|
+
GeoTIFF colormaps
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
_cmap: AsyncTiffColormap
|
|
23
|
+
"""The colormap data held in Rust, accessible via the buffer protocol.
|
|
24
|
+
|
|
25
|
+
Has shape `(N, 3)` and is of data type uint16.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
_nodata: int | float | None
|
|
29
|
+
"""The nodata value from gdal_nodata, if set."""
|
|
30
|
+
|
|
31
|
+
def as_array(self, *, dtype: type[np.uint8 | np.uint16] = np.uint8) -> NDArray:
|
|
32
|
+
"""Return the colormap as a NumPy array with shape (N, 3) and dtype uint16.
|
|
33
|
+
|
|
34
|
+
Each row corresponds to a color entry in the colormap, with columns
|
|
35
|
+
representing the Red, Green, and Blue components respectively.
|
|
36
|
+
|
|
37
|
+
This is the most efficient way to access and apply the colormap data.
|
|
38
|
+
|
|
39
|
+
```py
|
|
40
|
+
geotiff = await GeoTIFF.open(...)
|
|
41
|
+
array = await geotiff.fetch_tile(0, 0)
|
|
42
|
+
|
|
43
|
+
colormap = geotiff.colormap
|
|
44
|
+
colormap_array = colormap.as_array()
|
|
45
|
+
|
|
46
|
+
rgb_data = colormap_array[array.data[0]]
|
|
47
|
+
# A 3D array with shape (height, width, 3)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
A NumPy array representation of the colormap.
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
cmap_array = np.asarray(self._cmap)
|
|
55
|
+
if dtype == np.uint8:
|
|
56
|
+
return (cmap_array >> 8).astype(np.uint8)
|
|
57
|
+
if dtype == np.uint16:
|
|
58
|
+
return cmap_array
|
|
59
|
+
raise ValueError("dtype must be either np.uint8 or np.uint16.")
|
|
60
|
+
|
|
61
|
+
def as_dict(
|
|
62
|
+
self,
|
|
63
|
+
*,
|
|
64
|
+
dtype: type[np.uint8 | np.uint16] = np.uint8,
|
|
65
|
+
) -> dict[int, tuple[int, int, int]]:
|
|
66
|
+
"""Return the colormap as a dictionary mapping indices to RGB tuples.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
A dictionary where keys are indices and values are tuples of
|
|
70
|
+
(Red, Green, Blue) components.
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
cmap_array = self.as_array(dtype=dtype)
|
|
74
|
+
return {
|
|
75
|
+
int(idx): (int(r), int(g), int(b))
|
|
76
|
+
for idx, (r, g, b) in enumerate(cmap_array)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
def as_rasterio(self) -> dict[int, tuple[int, int, int, int]]:
|
|
80
|
+
"""Return the colormap as a mapping to 8-bit RGBA colors.
|
|
81
|
+
|
|
82
|
+
This returns a colormap in the same format as rasterio's
|
|
83
|
+
[`DatasetReader.colormap`][rasterio.io.DatasetReader.colormap] method.
|
|
84
|
+
|
|
85
|
+
This is the same as
|
|
86
|
+
[`Colormap.as_dict`][async_geotiff.colormap.Colormap.as_dict] with:
|
|
87
|
+
|
|
88
|
+
- `dtype` set to `np.uint8`
|
|
89
|
+
- an added alpha channel set to 255, **except** for the nodata value, if
|
|
90
|
+
defined, which has an alpha of 0.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Mapping of color index value (starting at 0) to RGBA color as a 4-element
|
|
94
|
+
tuple.
|
|
95
|
+
|
|
96
|
+
"""
|
|
97
|
+
cmap_array = self.as_array(dtype=np.uint8)
|
|
98
|
+
cmap_dict: dict[int, tuple[int, int, int, int]] = {}
|
|
99
|
+
|
|
100
|
+
for idx, (r, g, b) in enumerate(cmap_array):
|
|
101
|
+
alpha = 255 if self._nodata is None or idx != self._nodata else 0
|
|
102
|
+
cmap_dict[int(idx)] = (int(r), int(g), int(b), alpha)
|
|
103
|
+
|
|
104
|
+
return cmap_dict
|
async_geotiff/tms.py
CHANGED
|
@@ -63,8 +63,8 @@ def generate_tms(
|
|
|
63
63
|
|
|
64
64
|
for idx, overview in enumerate(reversed(geotiff.overviews)):
|
|
65
65
|
overview_tr = overview.transform
|
|
66
|
-
blockxsize = overview._ifd
|
|
67
|
-
blockysize = overview._ifd
|
|
66
|
+
blockxsize = overview._ifd.tile_width # noqa: SLF001
|
|
67
|
+
blockysize = overview._ifd.tile_height # noqa: SLF001
|
|
68
68
|
|
|
69
69
|
if blockxsize is None or blockysize is None:
|
|
70
70
|
raise ValueError("GeoTIFF overviews must be tiled to generate a TMS.")
|