async-geotiff 0.1.0b3__tar.gz → 0.1.0b5__tar.gz
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-0.1.0b5/PKG-INFO +128 -0
- async_geotiff-0.1.0b5/README.md +95 -0
- {async_geotiff-0.1.0b3 → async_geotiff-0.1.0b5}/pyproject.toml +23 -7
- async_geotiff-0.1.0b5/src/async_geotiff/__init__.py +22 -0
- async_geotiff-0.1.0b5/src/async_geotiff/_array.py +121 -0
- async_geotiff-0.1.0b5/src/async_geotiff/_fetch.py +154 -0
- {async_geotiff-0.1.0b3 → async_geotiff-0.1.0b5}/src/async_geotiff/_geotiff.py +87 -111
- {async_geotiff-0.1.0b3 → async_geotiff-0.1.0b5}/src/async_geotiff/_overview.py +44 -13
- async_geotiff-0.1.0b5/src/async_geotiff/_read.py +185 -0
- async_geotiff-0.1.0b5/src/async_geotiff/_tile.py +26 -0
- async_geotiff-0.1.0b5/src/async_geotiff/_transform.py +90 -0
- async_geotiff-0.1.0b5/src/async_geotiff/_windows.py +76 -0
- async_geotiff-0.1.0b5/src/async_geotiff/colormap.py +104 -0
- async_geotiff-0.1.0b5/src/async_geotiff/exceptions.py +5 -0
- {async_geotiff-0.1.0b3 → async_geotiff-0.1.0b5}/src/async_geotiff/tms.py +2 -2
- async_geotiff-0.1.0b3/PKG-INFO +0 -53
- async_geotiff-0.1.0b3/README.md +0 -20
- async_geotiff-0.1.0b3/src/async_geotiff/__init__.py +0 -10
- {async_geotiff-0.1.0b3 → async_geotiff-0.1.0b5}/LICENSE +0 -0
- {async_geotiff-0.1.0b3 → async_geotiff-0.1.0b5}/src/async_geotiff/_crs.py +0 -0
- {async_geotiff-0.1.0b3 → async_geotiff-0.1.0b5}/src/async_geotiff/_version.py +0 -0
- {async_geotiff-0.1.0b3 → async_geotiff-0.1.0b5}/src/async_geotiff/enums.py +0 -0
- {async_geotiff-0.1.0b3 → async_geotiff-0.1.0b5}/src/async_geotiff/py.typed +0 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: async-geotiff
|
|
3
|
+
Version: 0.1.0b5
|
|
4
|
+
Summary: Async GeoTIFF reader for Python
|
|
5
|
+
Keywords: geotiff,tiff,async,cog,raster,gis
|
|
6
|
+
Author: Kyle Barron
|
|
7
|
+
Author-email: Kyle Barron <kyle@developmentseed.org>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Dist: affine>=2.4.0
|
|
21
|
+
Requires-Dist: async-tiff>=0.5.0b3
|
|
22
|
+
Requires-Dist: numpy>=2.0
|
|
23
|
+
Requires-Dist: pyproj>=3.3.0
|
|
24
|
+
Requires-Dist: morecantile>=7.0,<8.0 ; extra == 'morecantile'
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Project-URL: Changelog, https://github.com/developmentseed/async-geotiff/blob/main/CHANGELOG.md
|
|
27
|
+
Project-URL: Documentation, https://developmentseed.github.io/async-geotiff/
|
|
28
|
+
Project-URL: Homepage, https://github.com/developmentseed/async-geotiff
|
|
29
|
+
Project-URL: Issues, https://github.com/developmentseed/async-geotiff/issues
|
|
30
|
+
Project-URL: Repository, https://github.com/developmentseed/async-geotiff
|
|
31
|
+
Provides-Extra: morecantile
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# async-geotiff
|
|
35
|
+
|
|
36
|
+
Fast, async GeoTIFF and [Cloud-Optimized GeoTIFF][cogeo] (COG) reader for Python, wrapping the Rust-based [Async-TIFF][async-tiff] library.
|
|
37
|
+
|
|
38
|
+
[async-tiff]: https://github.com/developmentseed/async-tiff
|
|
39
|
+
[cogeo]: https://cogeo.org/
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- Read-only support for GeoTIFF and COG formats.
|
|
44
|
+
- High-level, familiar, easy to use API.
|
|
45
|
+
- Performance-focused:
|
|
46
|
+
- Rust core ensures native performance.
|
|
47
|
+
- CPU-bound tasks like image decoding happen in a thread pool, without blocking the async executor.
|
|
48
|
+
- Buffer protocol integration for zero-copy data sharing between Rust and Python.
|
|
49
|
+
- Lightweight with no GDAL dependency.
|
|
50
|
+
- Integration with [obstore] for efficient data access on object stores.
|
|
51
|
+
- Full type hinting for all operations.
|
|
52
|
+
- Broad decompression support: Deflate, LZW, JPEG, JPEG2000, WebP, ZSTD.
|
|
53
|
+
|
|
54
|
+
**Anti-Features** (features explicitly not in scope):
|
|
55
|
+
|
|
56
|
+
- No pixel resampling.
|
|
57
|
+
- No warping/reprojection.
|
|
58
|
+
|
|
59
|
+
Resampling and warping bring significant additional complexity and are out of scope for this library.
|
|
60
|
+
|
|
61
|
+
[obstore]: https://developmentseed.org/obstore/latest/
|
|
62
|
+
[obspec]: https://developmentseed.org/obspec/latest/
|
|
63
|
+
|
|
64
|
+
## Example
|
|
65
|
+
|
|
66
|
+
First create a "store", such as an [`S3Store`][S3Store], [`GCSStore`][GCSStore], [`AzureStore`][AzureStore], or [`LocalStore`][LocalStore] for reading data from AWS S3, Google Cloud, Azure Storage, or local files. Refer to [obstore] documentation for more information.
|
|
67
|
+
|
|
68
|
+
[S3Store]: https://developmentseed.org/obstore/latest/api/store/aws/#obstore.store.S3Store
|
|
69
|
+
[GCSStore]: https://developmentseed.org/obstore/latest/api/store/gcs/#obstore.store.GCSStore
|
|
70
|
+
[AzureStore]: https://developmentseed.org/obstore/latest/api/store/azure/#obstore.store.AzureStore
|
|
71
|
+
[LocalStore]: https://developmentseed.org/obstore/latest/api/store/local/#obstore.store.LocalStore
|
|
72
|
+
|
|
73
|
+
```py
|
|
74
|
+
from obstore.store import S3Store
|
|
75
|
+
|
|
76
|
+
store = S3Store("sentinel-cogs", region="us-west-2", skip_signature=True)
|
|
77
|
+
path = "sentinel-s2-l2a-cogs/12/S/UF/2022/6/S2B_12SUF_20220609_0_L2A/TCI.tif"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Then open a `GeoTIFF`:
|
|
81
|
+
|
|
82
|
+
```py
|
|
83
|
+
from async_geotiff import GeoTIFF
|
|
84
|
+
|
|
85
|
+
geotiff = await GeoTIFF.open(path, store=store)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
On the `GeoTIFF` instance you have metadata about the image, such as its affine transform and Coordinate Reference System:
|
|
89
|
+
|
|
90
|
+
```py
|
|
91
|
+
geotiff.transform
|
|
92
|
+
# Affine(10.0, 0.0, 300000.0,
|
|
93
|
+
# 0.0, -10.0, 4100040.0)
|
|
94
|
+
|
|
95
|
+
geotiff.crs
|
|
96
|
+
# <Projected CRS: EPSG:32612>
|
|
97
|
+
# Name: WGS 84 / UTM zone 12N
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
For a COG, you can access the overviews, or reduced resolution versions, of the image:
|
|
101
|
+
|
|
102
|
+
```py
|
|
103
|
+
# Overviews are ordered from finest to coarsest resolution
|
|
104
|
+
# In this case, access the second-coarsest resolution version of the image
|
|
105
|
+
overview = geotiff.overviews[-2]
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Then we can read data from the image. This loads a 512-pixel square from the
|
|
109
|
+
upper-left corner of the selected overview.
|
|
110
|
+
|
|
111
|
+
```py
|
|
112
|
+
from async_geotiff import Window
|
|
113
|
+
|
|
114
|
+
window = Window(col_off=0, row_off=0, width=512, height=512)
|
|
115
|
+
array = await overview.read(window=window)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
This `Array` instance has `data`, `mask`, and some other metadata about the fetched array data.
|
|
119
|
+
|
|
120
|
+
Plot, using [`rasterio.plot.show`](https://rasterio.readthedocs.io/en/stable/api/rasterio.plot.html#rasterio.plot.show) (requires `matplotlib`):
|
|
121
|
+
|
|
122
|
+
```py
|
|
123
|
+
import rasterio.plot
|
|
124
|
+
|
|
125
|
+
rasterio.plot.show(array.data)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+

|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# async-geotiff
|
|
2
|
+
|
|
3
|
+
Fast, async GeoTIFF and [Cloud-Optimized GeoTIFF][cogeo] (COG) reader for Python, wrapping the Rust-based [Async-TIFF][async-tiff] library.
|
|
4
|
+
|
|
5
|
+
[async-tiff]: https://github.com/developmentseed/async-tiff
|
|
6
|
+
[cogeo]: https://cogeo.org/
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- Read-only support for GeoTIFF and COG formats.
|
|
11
|
+
- High-level, familiar, easy to use API.
|
|
12
|
+
- Performance-focused:
|
|
13
|
+
- Rust core ensures native performance.
|
|
14
|
+
- CPU-bound tasks like image decoding happen in a thread pool, without blocking the async executor.
|
|
15
|
+
- Buffer protocol integration for zero-copy data sharing between Rust and Python.
|
|
16
|
+
- Lightweight with no GDAL dependency.
|
|
17
|
+
- Integration with [obstore] for efficient data access on object stores.
|
|
18
|
+
- Full type hinting for all operations.
|
|
19
|
+
- Broad decompression support: Deflate, LZW, JPEG, JPEG2000, WebP, ZSTD.
|
|
20
|
+
|
|
21
|
+
**Anti-Features** (features explicitly not in scope):
|
|
22
|
+
|
|
23
|
+
- No pixel resampling.
|
|
24
|
+
- No warping/reprojection.
|
|
25
|
+
|
|
26
|
+
Resampling and warping bring significant additional complexity and are out of scope for this library.
|
|
27
|
+
|
|
28
|
+
[obstore]: https://developmentseed.org/obstore/latest/
|
|
29
|
+
[obspec]: https://developmentseed.org/obspec/latest/
|
|
30
|
+
|
|
31
|
+
## Example
|
|
32
|
+
|
|
33
|
+
First create a "store", such as an [`S3Store`][S3Store], [`GCSStore`][GCSStore], [`AzureStore`][AzureStore], or [`LocalStore`][LocalStore] for reading data from AWS S3, Google Cloud, Azure Storage, or local files. Refer to [obstore] documentation for more information.
|
|
34
|
+
|
|
35
|
+
[S3Store]: https://developmentseed.org/obstore/latest/api/store/aws/#obstore.store.S3Store
|
|
36
|
+
[GCSStore]: https://developmentseed.org/obstore/latest/api/store/gcs/#obstore.store.GCSStore
|
|
37
|
+
[AzureStore]: https://developmentseed.org/obstore/latest/api/store/azure/#obstore.store.AzureStore
|
|
38
|
+
[LocalStore]: https://developmentseed.org/obstore/latest/api/store/local/#obstore.store.LocalStore
|
|
39
|
+
|
|
40
|
+
```py
|
|
41
|
+
from obstore.store import S3Store
|
|
42
|
+
|
|
43
|
+
store = S3Store("sentinel-cogs", region="us-west-2", skip_signature=True)
|
|
44
|
+
path = "sentinel-s2-l2a-cogs/12/S/UF/2022/6/S2B_12SUF_20220609_0_L2A/TCI.tif"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Then open a `GeoTIFF`:
|
|
48
|
+
|
|
49
|
+
```py
|
|
50
|
+
from async_geotiff import GeoTIFF
|
|
51
|
+
|
|
52
|
+
geotiff = await GeoTIFF.open(path, store=store)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
On the `GeoTIFF` instance you have metadata about the image, such as its affine transform and Coordinate Reference System:
|
|
56
|
+
|
|
57
|
+
```py
|
|
58
|
+
geotiff.transform
|
|
59
|
+
# Affine(10.0, 0.0, 300000.0,
|
|
60
|
+
# 0.0, -10.0, 4100040.0)
|
|
61
|
+
|
|
62
|
+
geotiff.crs
|
|
63
|
+
# <Projected CRS: EPSG:32612>
|
|
64
|
+
# Name: WGS 84 / UTM zone 12N
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
For a COG, you can access the overviews, or reduced resolution versions, of the image:
|
|
68
|
+
|
|
69
|
+
```py
|
|
70
|
+
# Overviews are ordered from finest to coarsest resolution
|
|
71
|
+
# In this case, access the second-coarsest resolution version of the image
|
|
72
|
+
overview = geotiff.overviews[-2]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Then we can read data from the image. This loads a 512-pixel square from the
|
|
76
|
+
upper-left corner of the selected overview.
|
|
77
|
+
|
|
78
|
+
```py
|
|
79
|
+
from async_geotiff import Window
|
|
80
|
+
|
|
81
|
+
window = Window(col_off=0, row_off=0, width=512, height=512)
|
|
82
|
+
array = await overview.read(window=window)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
This `Array` instance has `data`, `mask`, and some other metadata about the fetched array data.
|
|
86
|
+
|
|
87
|
+
Plot, using [`rasterio.plot.show`](https://rasterio.readthedocs.io/en/stable/api/rasterio.plot.html#rasterio.plot.show) (requires `matplotlib`):
|
|
88
|
+
|
|
89
|
+
```py
|
|
90
|
+
import rasterio.plot
|
|
91
|
+
|
|
92
|
+
rasterio.plot.show(array.data)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+

|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "async-geotiff"
|
|
3
|
-
version = "0.1.0-beta.
|
|
3
|
+
version = "0.1.0-beta.5"
|
|
4
4
|
description = "Async GeoTIFF reader for Python"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "Kyle Barron", email = "kyle@developmentseed.org" }]
|
|
@@ -22,7 +22,7 @@ classifiers = [
|
|
|
22
22
|
keywords = ["geotiff", "tiff", "async", "cog", "raster", "gis"]
|
|
23
23
|
dependencies = [
|
|
24
24
|
"affine>=2.4.0",
|
|
25
|
-
"async-tiff>=0.
|
|
25
|
+
"async-tiff>=0.5.0-beta.3",
|
|
26
26
|
"numpy>=2.0",
|
|
27
27
|
"pyproj>=3.3.0",
|
|
28
28
|
]
|
|
@@ -43,6 +43,7 @@ dev = [
|
|
|
43
43
|
"build>=1.4.0",
|
|
44
44
|
"ipykernel>=7.1.0",
|
|
45
45
|
"jsonschema>=4.26.0",
|
|
46
|
+
"matplotlib>=3.10.8",
|
|
46
47
|
"morecantile>=7.0.2",
|
|
47
48
|
"obstore>=0.8.2",
|
|
48
49
|
"pydantic>=2.12.5",
|
|
@@ -51,6 +52,19 @@ dev = [
|
|
|
51
52
|
"rasterio>=1.4.4",
|
|
52
53
|
"types-jsonschema>=4.26.0.20260109",
|
|
53
54
|
]
|
|
55
|
+
docs = [
|
|
56
|
+
# Workaround for https://github.com/mkdocs/mkdocs/issues/4032
|
|
57
|
+
"click<8.3",
|
|
58
|
+
"mkdocs-material[imaging]>=9.5.49",
|
|
59
|
+
"mkdocs>=1.6.1",
|
|
60
|
+
"mkdocstrings[python]>=1.0",
|
|
61
|
+
"mike>=2.1.3",
|
|
62
|
+
"griffe-inherited-docstrings>=1.0.1",
|
|
63
|
+
# We use ruff format ourselves, but mkdocstrings requires black to be
|
|
64
|
+
# installed to format signatures in the docs
|
|
65
|
+
"black>=26",
|
|
66
|
+
]
|
|
67
|
+
|
|
54
68
|
|
|
55
69
|
[build-system]
|
|
56
70
|
requires = ["uv_build>=0.8.8,<0.9.0"]
|
|
@@ -65,6 +79,7 @@ module = [
|
|
|
65
79
|
# https://github.com/rasterio/affine/issues/135
|
|
66
80
|
"affine.*",
|
|
67
81
|
"async_tiff.store.*",
|
|
82
|
+
"rasterio.*",
|
|
68
83
|
]
|
|
69
84
|
ignore_missing_imports = true
|
|
70
85
|
|
|
@@ -81,9 +96,10 @@ ignore = [
|
|
|
81
96
|
|
|
82
97
|
[tool.ruff.lint.per-file-ignores]
|
|
83
98
|
"tests/*" = [
|
|
84
|
-
"ANN001",
|
|
85
|
-
"ANN201",
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"
|
|
99
|
+
"ANN001", # annotation in function argument
|
|
100
|
+
"ANN201", # return type annotation
|
|
101
|
+
"PLR2004", # Magic value used in comparison
|
|
102
|
+
"S101", # assert
|
|
103
|
+
"SLF001", # private member access
|
|
104
|
+
"D", # docstring
|
|
89
105
|
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Async GeoTIFF and [Cloud-Optimized GeoTIFF][cogeo] (COG) reader for Python.
|
|
2
|
+
|
|
3
|
+
[cogeo]: https://cogeo.org/
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from . import exceptions
|
|
7
|
+
from ._array import Array
|
|
8
|
+
from ._geotiff import GeoTIFF
|
|
9
|
+
from ._overview import Overview
|
|
10
|
+
from ._tile import Tile
|
|
11
|
+
from ._version import __version__
|
|
12
|
+
from ._windows import Window
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"Array",
|
|
16
|
+
"GeoTIFF",
|
|
17
|
+
"Overview",
|
|
18
|
+
"Tile",
|
|
19
|
+
"Window",
|
|
20
|
+
"__version__",
|
|
21
|
+
"exceptions",
|
|
22
|
+
]
|
|
@@ -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)
|
|
@@ -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
|