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/__init__.py
CHANGED
|
@@ -3,8 +3,20 @@
|
|
|
3
3
|
[cogeo]: https://cogeo.org/
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
from . import exceptions
|
|
7
|
+
from ._array import Array
|
|
6
8
|
from ._geotiff import GeoTIFF
|
|
7
9
|
from ._overview import Overview
|
|
10
|
+
from ._tile import Tile
|
|
8
11
|
from ._version import __version__
|
|
12
|
+
from ._windows import Window
|
|
9
13
|
|
|
10
|
-
__all__ = [
|
|
14
|
+
__all__ = [
|
|
15
|
+
"Array",
|
|
16
|
+
"GeoTIFF",
|
|
17
|
+
"Overview",
|
|
18
|
+
"Tile",
|
|
19
|
+
"Window",
|
|
20
|
+
"__version__",
|
|
21
|
+
"exceptions",
|
|
22
|
+
]
|
async_geotiff/_array.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, Self
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from async_tiff.enums import PlanarConfiguration
|
|
8
|
+
from numpy.ma import MaskedArray
|
|
9
|
+
|
|
10
|
+
from async_geotiff._transform import TransformMixin
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from affine import Affine
|
|
14
|
+
from async_tiff import Array as AsyncTiffArray
|
|
15
|
+
from numpy.typing import NDArray
|
|
16
|
+
from pyproj.crs import CRS
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, kw_only=True, eq=False)
|
|
20
|
+
class Array(TransformMixin):
|
|
21
|
+
"""An array representation of data from a GeoTIFF."""
|
|
22
|
+
|
|
23
|
+
data: NDArray
|
|
24
|
+
"""The array data with shape (bands, height, width)."""
|
|
25
|
+
|
|
26
|
+
mask: NDArray[np.bool_] | None
|
|
27
|
+
"""The mask array with shape (height, width), if any.
|
|
28
|
+
|
|
29
|
+
Values of True indicate valid data; False indicates no data.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
width: int
|
|
33
|
+
"""The width of the array in pixels."""
|
|
34
|
+
|
|
35
|
+
height: int
|
|
36
|
+
"""The height of the array in pixels."""
|
|
37
|
+
|
|
38
|
+
count: int
|
|
39
|
+
"""The number of bands in the array."""
|
|
40
|
+
|
|
41
|
+
transform: Affine
|
|
42
|
+
"""The affine transform mapping pixel coordinates to geographic coordinates."""
|
|
43
|
+
|
|
44
|
+
crs: CRS
|
|
45
|
+
"""The coordinate reference system of the array."""
|
|
46
|
+
|
|
47
|
+
nodata: float | None = None
|
|
48
|
+
"""The nodata value for the array, if any."""
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def _create( # noqa: PLR0913
|
|
52
|
+
cls,
|
|
53
|
+
*,
|
|
54
|
+
data: AsyncTiffArray,
|
|
55
|
+
mask: AsyncTiffArray | None,
|
|
56
|
+
planar_configuration: PlanarConfiguration,
|
|
57
|
+
transform: Affine,
|
|
58
|
+
crs: CRS,
|
|
59
|
+
nodata: float | None = None,
|
|
60
|
+
) -> Self:
|
|
61
|
+
"""Create an Array from async_tiff data.
|
|
62
|
+
|
|
63
|
+
Handles axis reordering to ensure data is always in (bands, height, width)
|
|
64
|
+
order, matching rasterio's convention.
|
|
65
|
+
"""
|
|
66
|
+
data_arr = np.asarray(data, copy=False)
|
|
67
|
+
if mask is not None:
|
|
68
|
+
mask_arr = np.asarray(mask, copy=False).astype(np.bool_, copy=False)
|
|
69
|
+
assert mask_arr.ndim == 3 # noqa: S101, PLR2004
|
|
70
|
+
assert mask_arr.shape[2] == 1 # noqa: S101
|
|
71
|
+
# This assumes it's always (height, width, 1)
|
|
72
|
+
mask_arr = np.squeeze(mask_arr, axis=2)
|
|
73
|
+
else:
|
|
74
|
+
mask_arr = None
|
|
75
|
+
|
|
76
|
+
assert data_arr.ndim == 3, f"Expected 3D array, got {data_arr.ndim}D" # noqa: S101, PLR2004
|
|
77
|
+
|
|
78
|
+
# async_tiff returns data in the native TIFF order:
|
|
79
|
+
# - Chunky (pixel interleaved): (height, width, bands)
|
|
80
|
+
# - Planar (band interleaved): (bands, height, width)
|
|
81
|
+
# We always want (bands, height, width) to match rasterio.
|
|
82
|
+
if planar_configuration == PlanarConfiguration.Chunky:
|
|
83
|
+
# Transpose from (H, W, C) to (C, H, W)
|
|
84
|
+
data_arr = np.moveaxis(data_arr, -1, 0)
|
|
85
|
+
|
|
86
|
+
count, height, width = data_arr.shape
|
|
87
|
+
|
|
88
|
+
return cls(
|
|
89
|
+
data=data_arr,
|
|
90
|
+
mask=mask_arr,
|
|
91
|
+
width=width,
|
|
92
|
+
height=height,
|
|
93
|
+
count=count,
|
|
94
|
+
transform=transform,
|
|
95
|
+
crs=crs,
|
|
96
|
+
nodata=nodata,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def as_masked(self) -> MaskedArray:
|
|
100
|
+
"""Return the data as a masked array using the Array mask or nodata value.
|
|
101
|
+
|
|
102
|
+
!!! warning
|
|
103
|
+
In a numpy [`MaskedArray`][numpy.ma.MaskedArray], `True`
|
|
104
|
+
indicates invalid (masked) data and `False` indicates valid data.
|
|
105
|
+
|
|
106
|
+
This is the inverse convention of a GeoTIFF's mask. The boolean array
|
|
107
|
+
[`Array.mask`][async_geotiff.Array.mask] uses `True` for valid data and
|
|
108
|
+
`False` for invalid data.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
A masked array with the same shape as `data`, where invalid data
|
|
112
|
+
(as indicated by the mask) is masked out.
|
|
113
|
+
|
|
114
|
+
"""
|
|
115
|
+
if self.mask is not None:
|
|
116
|
+
return MaskedArray(self.data, mask=~self.mask)
|
|
117
|
+
|
|
118
|
+
if self.nodata is not None:
|
|
119
|
+
return np.ma.masked_equal(self.data, self.nodata)
|
|
120
|
+
|
|
121
|
+
return MaskedArray(self.data)
|
async_geotiff/_fetch.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import TYPE_CHECKING, Protocol
|
|
5
|
+
|
|
6
|
+
from affine import Affine
|
|
7
|
+
|
|
8
|
+
from async_geotiff._array import Array
|
|
9
|
+
from async_geotiff._tile import Tile
|
|
10
|
+
from async_geotiff._transform import HasTransform
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from async_tiff import TIFF, ImageFileDirectory
|
|
14
|
+
from async_tiff import Array as AsyncTiffArray
|
|
15
|
+
from pyproj import CRS
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class HasTiffReference(HasTransform, Protocol):
|
|
19
|
+
"""Protocol for objects that hold a TIFF reference and can request tiles."""
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def _ifd(self) -> ImageFileDirectory:
|
|
23
|
+
"""The data IFD for this image (index, IFD)."""
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def _mask_ifd(self) -> ImageFileDirectory | None:
|
|
28
|
+
"""The mask IFD for this image (index, IFD), if any."""
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def _tiff(self) -> TIFF:
|
|
33
|
+
"""A reference to the underlying TIFF object."""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def crs(self) -> CRS:
|
|
38
|
+
"""The coordinate reference system."""
|
|
39
|
+
...
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def tile_height(self) -> int:
|
|
43
|
+
"""The height of tiles in pixels."""
|
|
44
|
+
...
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def tile_width(self) -> int:
|
|
48
|
+
"""The width of tiles in pixels."""
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def nodata(self) -> int | float | None:
|
|
53
|
+
"""The nodata value for the image, if any."""
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class FetchTileMixin:
|
|
58
|
+
"""Mixin for fetching tiles from a GeoTIFF.
|
|
59
|
+
|
|
60
|
+
Classes using this mixin must implement HasTiffReference.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
async def fetch_tile(
|
|
64
|
+
self: HasTiffReference,
|
|
65
|
+
x: int,
|
|
66
|
+
y: int,
|
|
67
|
+
) -> Tile:
|
|
68
|
+
tile_fut = self._ifd.fetch_tile(x, y)
|
|
69
|
+
|
|
70
|
+
mask_data: AsyncTiffArray | None = None
|
|
71
|
+
if self._mask_ifd is not None:
|
|
72
|
+
mask_fut = self._mask_ifd.fetch_tile(x, y)
|
|
73
|
+
tile, mask = await asyncio.gather(tile_fut, mask_fut)
|
|
74
|
+
tile_data, mask_data = await asyncio.gather(tile.decode(), mask.decode())
|
|
75
|
+
else:
|
|
76
|
+
tile = await tile_fut
|
|
77
|
+
tile_data = await tile.decode()
|
|
78
|
+
|
|
79
|
+
tile_transform = self.transform * Affine.translation(
|
|
80
|
+
x * self.tile_width,
|
|
81
|
+
y * self.tile_height,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
array = Array._create( # noqa: SLF001
|
|
85
|
+
data=tile_data,
|
|
86
|
+
mask=mask_data,
|
|
87
|
+
planar_configuration=self._ifd.planar_configuration,
|
|
88
|
+
crs=self.crs,
|
|
89
|
+
transform=tile_transform,
|
|
90
|
+
nodata=self.nodata,
|
|
91
|
+
)
|
|
92
|
+
return Tile(
|
|
93
|
+
x=x,
|
|
94
|
+
y=y,
|
|
95
|
+
_ifd=self._ifd,
|
|
96
|
+
array=array,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
async def fetch_tiles(
|
|
100
|
+
self: HasTiffReference,
|
|
101
|
+
xs: list[int],
|
|
102
|
+
ys: list[int],
|
|
103
|
+
) -> list[Tile]:
|
|
104
|
+
"""Fetch multiple tiles from this overview.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
xs: The x coordinates of the tiles.
|
|
108
|
+
ys: The y coordinates of the tiles.
|
|
109
|
+
|
|
110
|
+
"""
|
|
111
|
+
tiles_fut = self._ifd.fetch_tiles(xs, ys)
|
|
112
|
+
|
|
113
|
+
decoded_masks: list[AsyncTiffArray | None] = [None] * len(xs)
|
|
114
|
+
if self._mask_ifd is not None:
|
|
115
|
+
masks_fut = self._mask_ifd.fetch_tiles(xs, ys)
|
|
116
|
+
tiles, masks = await asyncio.gather(tiles_fut, masks_fut)
|
|
117
|
+
|
|
118
|
+
decoded_tile_futs = [tile.decode() for tile in tiles]
|
|
119
|
+
decoded_mask_futs = [mask.decode() for mask in masks]
|
|
120
|
+
decoded_tiles = await asyncio.gather(*decoded_tile_futs)
|
|
121
|
+
decoded_masks = await asyncio.gather(*decoded_mask_futs)
|
|
122
|
+
else:
|
|
123
|
+
tiles = await tiles_fut
|
|
124
|
+
decoded_tiles = await asyncio.gather(*[tile.decode() for tile in tiles])
|
|
125
|
+
|
|
126
|
+
final_tiles: list[Tile] = []
|
|
127
|
+
for x, y, tile_data, mask_data in zip(
|
|
128
|
+
xs,
|
|
129
|
+
ys,
|
|
130
|
+
decoded_tiles,
|
|
131
|
+
decoded_masks,
|
|
132
|
+
strict=True,
|
|
133
|
+
):
|
|
134
|
+
tile_transform = self.transform * Affine.translation(
|
|
135
|
+
x * self.tile_width,
|
|
136
|
+
y * self.tile_height,
|
|
137
|
+
)
|
|
138
|
+
array = Array._create( # noqa: SLF001
|
|
139
|
+
data=tile_data,
|
|
140
|
+
mask=mask_data,
|
|
141
|
+
planar_configuration=self._ifd.planar_configuration,
|
|
142
|
+
crs=self.crs,
|
|
143
|
+
transform=tile_transform,
|
|
144
|
+
nodata=self.nodata,
|
|
145
|
+
)
|
|
146
|
+
tile = Tile(
|
|
147
|
+
x=x,
|
|
148
|
+
y=y,
|
|
149
|
+
_ifd=self._ifd,
|
|
150
|
+
array=array,
|
|
151
|
+
)
|
|
152
|
+
final_tiles.append(tile)
|
|
153
|
+
|
|
154
|
+
return final_tiles
|
async_geotiff/_geotiff.py
CHANGED
|
@@ -2,28 +2,38 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
from functools import cached_property
|
|
5
|
-
from typing import TYPE_CHECKING,
|
|
5
|
+
from typing import TYPE_CHECKING, Self
|
|
6
6
|
|
|
7
7
|
from affine import Affine
|
|
8
8
|
from async_tiff import TIFF
|
|
9
9
|
from async_tiff.enums import PhotometricInterpretation
|
|
10
10
|
|
|
11
11
|
from async_geotiff._crs import crs_from_geo_keys
|
|
12
|
+
from async_geotiff._fetch import FetchTileMixin
|
|
12
13
|
from async_geotiff._overview import Overview
|
|
13
|
-
from async_geotiff.
|
|
14
|
+
from async_geotiff._read import ReadMixin
|
|
15
|
+
from async_geotiff._transform import TransformMixin
|
|
16
|
+
from async_geotiff.colormap import Colormap
|
|
14
17
|
|
|
15
18
|
if TYPE_CHECKING:
|
|
16
|
-
from collections.abc import Callable
|
|
17
|
-
|
|
18
|
-
import pyproj
|
|
19
19
|
from async_tiff import GeoKeyDirectory, ImageFileDirectory, ObspecInput
|
|
20
20
|
from async_tiff.store import ObjectStore # type: ignore # noqa: PGH003
|
|
21
|
+
from pyproj.crs import CRS
|
|
22
|
+
|
|
23
|
+
from async_geotiff.enums import Compression, Interleaving
|
|
21
24
|
|
|
22
25
|
|
|
23
26
|
@dataclass(frozen=True, init=False, kw_only=True, repr=False)
|
|
24
|
-
class GeoTIFF:
|
|
27
|
+
class GeoTIFF(ReadMixin, FetchTileMixin, TransformMixin):
|
|
25
28
|
"""A class representing a GeoTIFF image."""
|
|
26
29
|
|
|
30
|
+
_crs: CRS | None = None
|
|
31
|
+
"""A cached CRS instance.
|
|
32
|
+
|
|
33
|
+
We don't use functools.cached_property on the `crs` attribute because of typing
|
|
34
|
+
issues.
|
|
35
|
+
"""
|
|
36
|
+
|
|
27
37
|
_tiff: TIFF
|
|
28
38
|
"""The underlying async-tiff TIFF instance that we wrap.
|
|
29
39
|
"""
|
|
@@ -34,6 +44,10 @@ class GeoTIFF:
|
|
|
34
44
|
Some tags, like most geo tags, only exist on the primary IFD.
|
|
35
45
|
"""
|
|
36
46
|
|
|
47
|
+
_mask_ifd: ImageFileDirectory | None = None
|
|
48
|
+
"""The mask IFD of the full-resolution GeoTIFF, if any.
|
|
49
|
+
"""
|
|
50
|
+
|
|
37
51
|
_gkd: GeoKeyDirectory = field(init=False)
|
|
38
52
|
"""The GeoKeyDirectory of the primary IFD.
|
|
39
53
|
"""
|
|
@@ -42,6 +56,11 @@ class GeoTIFF:
|
|
|
42
56
|
"""A list of overviews for the GeoTIFF.
|
|
43
57
|
"""
|
|
44
58
|
|
|
59
|
+
@property
|
|
60
|
+
def _ifd(self) -> ImageFileDirectory:
|
|
61
|
+
"""An alias for the primary IFD to satisfy _fetch protocol."""
|
|
62
|
+
return self._primary_ifd
|
|
63
|
+
|
|
45
64
|
def __init__(self, tiff: TIFF) -> None:
|
|
46
65
|
"""Create a GeoTIFF from an existing TIFF instance."""
|
|
47
66
|
first_ifd = tiff.ifds[0]
|
|
@@ -59,29 +78,32 @@ class GeoTIFF:
|
|
|
59
78
|
object.__setattr__(self, "_primary_ifd", first_ifd)
|
|
60
79
|
object.__setattr__(self, "_gkd", gkd)
|
|
61
80
|
|
|
62
|
-
#
|
|
63
|
-
|
|
81
|
+
# Separate data IFDs and mask IFDs (skip the primary IFD at index 0)
|
|
82
|
+
# Data IFDs are indexed by (width, height) for matching with masks
|
|
83
|
+
data_ifds: dict[tuple[int, int], ImageFileDirectory] = {}
|
|
84
|
+
mask_ifds: dict[tuple[int, int], ImageFileDirectory] = {}
|
|
85
|
+
|
|
86
|
+
for ifd in tiff.ifds[1:]:
|
|
87
|
+
dims = (ifd.image_width, ifd.image_height)
|
|
88
|
+
if is_mask_ifd(ifd):
|
|
89
|
+
mask_ifds[dims] = ifd
|
|
90
|
+
else:
|
|
91
|
+
data_ifds[dims] = ifd
|
|
92
|
+
|
|
93
|
+
# Find and set the mask for the primary IFD (matches primary dimensions)
|
|
94
|
+
if primary_mask_ifd := mask_ifds.get(
|
|
95
|
+
(first_ifd.image_width, first_ifd.image_height),
|
|
96
|
+
):
|
|
97
|
+
object.__setattr__(self, "_mask_ifd", primary_mask_ifd)
|
|
98
|
+
|
|
99
|
+
# Build overviews, sorted by resolution (highest to lowest, i.e., largest first)
|
|
100
|
+
# Sort by width * height descending
|
|
101
|
+
sorted_dims = sorted(data_ifds.keys(), key=lambda d: d[0] * d[1], reverse=True)
|
|
102
|
+
|
|
64
103
|
overviews: list[Overview] = []
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
except IndexError:
|
|
69
|
-
# No more IFDs
|
|
70
|
-
break
|
|
71
|
-
|
|
72
|
-
ifd_idx += 1
|
|
73
|
-
|
|
74
|
-
mask_ifd = None
|
|
75
|
-
next_ifd = None
|
|
76
|
-
try:
|
|
77
|
-
next_ifd = tiff.ifds[ifd_idx]
|
|
78
|
-
except IndexError:
|
|
79
|
-
# No more IFDs
|
|
80
|
-
pass
|
|
81
|
-
finally:
|
|
82
|
-
if next_ifd is not None and is_mask_ifd(next_ifd):
|
|
83
|
-
mask_ifd = (ifd_idx, next_ifd)
|
|
84
|
-
ifd_idx += 1
|
|
104
|
+
for dims in sorted_dims:
|
|
105
|
+
data_ifd = data_ifds[dims]
|
|
106
|
+
mask_ifd = mask_ifds.get(dims)
|
|
85
107
|
|
|
86
108
|
ovr = Overview._create( # noqa: SLF001
|
|
87
109
|
geotiff=self,
|
|
@@ -169,24 +191,18 @@ class GeoTIFF:
|
|
|
169
191
|
# https://github.com/developmentseed/async-geotiff/issues/12
|
|
170
192
|
raise NotImplementedError
|
|
171
193
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
Args:
|
|
176
|
-
bidx: The 1-based index of the band whose colormap will be returned.
|
|
194
|
+
@property
|
|
195
|
+
def colormap(self) -> Colormap | None:
|
|
196
|
+
"""Return the Colormap stored in the file, if any.
|
|
177
197
|
|
|
178
198
|
Returns:
|
|
179
|
-
|
|
180
|
-
4-element tuple.
|
|
181
|
-
|
|
182
|
-
Raises:
|
|
183
|
-
ValueError
|
|
184
|
-
If no colormap is found for the specified band (NULL color table).
|
|
185
|
-
IndexError
|
|
186
|
-
If no band exists for the provided index.
|
|
199
|
+
A Colormap instance if the dataset has a colormap, else None.
|
|
187
200
|
|
|
188
201
|
"""
|
|
189
|
-
|
|
202
|
+
if upstream_colormap := self._primary_ifd.colormap:
|
|
203
|
+
return Colormap(_cmap=upstream_colormap, _nodata=self.nodata)
|
|
204
|
+
|
|
205
|
+
return None
|
|
190
206
|
|
|
191
207
|
@property
|
|
192
208
|
def compression(self) -> Compression:
|
|
@@ -204,10 +220,15 @@ class GeoTIFF:
|
|
|
204
220
|
"""The number of raster bands in the full image."""
|
|
205
221
|
raise NotImplementedError
|
|
206
222
|
|
|
207
|
-
@
|
|
208
|
-
def crs(self) ->
|
|
223
|
+
@property
|
|
224
|
+
def crs(self) -> CRS:
|
|
209
225
|
"""The dataset's coordinate reference system."""
|
|
210
|
-
|
|
226
|
+
if self._crs is not None:
|
|
227
|
+
return self._crs
|
|
228
|
+
|
|
229
|
+
crs = crs_from_geo_keys(self._gkd)
|
|
230
|
+
object.__setattr__(self, "_crs", crs)
|
|
231
|
+
return crs
|
|
211
232
|
|
|
212
233
|
@property
|
|
213
234
|
def dtypes(self) -> list[str]:
|
|
@@ -222,27 +243,6 @@ class GeoTIFF:
|
|
|
222
243
|
"""The height (number of rows) of the full image."""
|
|
223
244
|
return self._primary_ifd.image_height
|
|
224
245
|
|
|
225
|
-
def index(
|
|
226
|
-
self,
|
|
227
|
-
x: float,
|
|
228
|
-
y: float,
|
|
229
|
-
op: Callable[[float, float], tuple[int, int]] | None = None,
|
|
230
|
-
) -> tuple[int, int]:
|
|
231
|
-
"""Get the (row, col) index of the pixel containing (x, y).
|
|
232
|
-
|
|
233
|
-
Args:
|
|
234
|
-
x: x value in coordinate reference system
|
|
235
|
-
y: y value in coordinate reference system
|
|
236
|
-
op: function, optional (default: numpy.floor)
|
|
237
|
-
Function to convert fractional pixels to whole numbers
|
|
238
|
-
(floor, ceiling, round)
|
|
239
|
-
|
|
240
|
-
Returns:
|
|
241
|
-
(row index, col index)
|
|
242
|
-
|
|
243
|
-
"""
|
|
244
|
-
raise NotImplementedError
|
|
245
|
-
|
|
246
246
|
def indexes(self) -> list[int]:
|
|
247
247
|
"""Return the 1-based indexes of each band in the dataset.
|
|
248
248
|
|
|
@@ -273,7 +273,14 @@ class GeoTIFF:
|
|
|
273
273
|
|
|
274
274
|
@property
|
|
275
275
|
def overviews(self) -> list[Overview]:
|
|
276
|
-
"""A list of overview levels for the dataset.
|
|
276
|
+
"""A list of overview levels for the dataset.
|
|
277
|
+
|
|
278
|
+
Overviews are reduced-resolution versions of the main image used for faster
|
|
279
|
+
rendering at lower zoom levels.
|
|
280
|
+
|
|
281
|
+
This list of overviews is ordered from finest to coarsest resolution. The first
|
|
282
|
+
element of the list is the highest-resolution after the base image.
|
|
283
|
+
"""
|
|
277
284
|
return self._overviews
|
|
278
285
|
|
|
279
286
|
@property
|
|
@@ -283,7 +290,7 @@ class GeoTIFF:
|
|
|
283
290
|
# https://rasterio.readthedocs.io/en/stable/api/rasterio.enums.html#rasterio.enums.PhotometricInterp
|
|
284
291
|
raise NotImplementedError
|
|
285
292
|
|
|
286
|
-
@
|
|
293
|
+
@property
|
|
287
294
|
def res(self) -> tuple[float, float]:
|
|
288
295
|
"""Return the (width, height) of pixels in the units of its CRS."""
|
|
289
296
|
transform = self.transform
|
|
@@ -294,7 +301,17 @@ class GeoTIFF:
|
|
|
294
301
|
"""Get the shape (height, width) of the full image."""
|
|
295
302
|
return (self.height, self.width)
|
|
296
303
|
|
|
297
|
-
@
|
|
304
|
+
@property
|
|
305
|
+
def tile_height(self) -> int:
|
|
306
|
+
"""The height in pixels per tile of the image."""
|
|
307
|
+
return self._primary_ifd.tile_height or self.height
|
|
308
|
+
|
|
309
|
+
@property
|
|
310
|
+
def tile_width(self) -> int:
|
|
311
|
+
"""The width in pixels per tile of the image."""
|
|
312
|
+
return self._primary_ifd.tile_width or self.width
|
|
313
|
+
|
|
314
|
+
@property
|
|
298
315
|
def transform(self) -> Affine:
|
|
299
316
|
"""Return the dataset's georeferencing transformation matrix.
|
|
300
317
|
|
|
@@ -344,46 +361,6 @@ class GeoTIFF:
|
|
|
344
361
|
"""The width (number of columns) of the full image."""
|
|
345
362
|
return self._primary_ifd.image_width
|
|
346
363
|
|
|
347
|
-
def xy(
|
|
348
|
-
self,
|
|
349
|
-
row: int,
|
|
350
|
-
col: int,
|
|
351
|
-
offset: Literal["center", "ul", "ur", "ll", "lr"] | str = "center",
|
|
352
|
-
) -> tuple[float, float]:
|
|
353
|
-
"""Get the coordinates x, y of a pixel at row, col.
|
|
354
|
-
|
|
355
|
-
The pixel's center is returned by default, but a corner can be returned
|
|
356
|
-
by setting `offset` to one of `"ul"`, `"ur"`, `"ll"`, `"lr"`.
|
|
357
|
-
|
|
358
|
-
Args:
|
|
359
|
-
row: Pixel row.
|
|
360
|
-
col: Pixel column.
|
|
361
|
-
offset: Determines if the returned coordinates are for the center of the
|
|
362
|
-
pixel or for a corner.
|
|
363
|
-
|
|
364
|
-
"""
|
|
365
|
-
transform = self.transform
|
|
366
|
-
|
|
367
|
-
if offset == "center":
|
|
368
|
-
c = col + 0.5
|
|
369
|
-
r = row + 0.5
|
|
370
|
-
elif offset == "ul":
|
|
371
|
-
c = col
|
|
372
|
-
r = row
|
|
373
|
-
elif offset == "ur":
|
|
374
|
-
c = col + 1
|
|
375
|
-
r = row
|
|
376
|
-
elif offset == "ll":
|
|
377
|
-
c = col
|
|
378
|
-
r = row + 1
|
|
379
|
-
elif offset == "lr":
|
|
380
|
-
c = col + 1
|
|
381
|
-
r = row + 1
|
|
382
|
-
else:
|
|
383
|
-
raise ValueError(f"Invalid offset value: {offset}")
|
|
384
|
-
|
|
385
|
-
return transform * (c, r)
|
|
386
|
-
|
|
387
364
|
|
|
388
365
|
def has_geokeys(ifd: ImageFileDirectory) -> bool:
|
|
389
366
|
"""Check if an IFD has GeoTIFF keys.
|
|
@@ -398,8 +375,7 @@ def has_geokeys(ifd: ImageFileDirectory) -> bool:
|
|
|
398
375
|
def is_mask_ifd(ifd: ImageFileDirectory) -> bool:
|
|
399
376
|
"""Check if an IFD is a mask IFD."""
|
|
400
377
|
return (
|
|
401
|
-
ifd.
|
|
402
|
-
and ifd.new_subfile_type is not None
|
|
378
|
+
ifd.new_subfile_type is not None
|
|
403
379
|
and ifd.new_subfile_type & 4 != 0
|
|
404
380
|
and ifd.photometric_interpretation == PhotometricInterpretation.TransparencyMask
|
|
405
381
|
)
|