Skip to content

Commit 365271a

Browse files
committed
Merge branch 'main' into kyle/as-masked
2 parents dc32c76 + 910818d commit 365271a

File tree

18 files changed

+992
-48
lines changed

18 files changed

+992
-48
lines changed

DEVELOP.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ Build locally:
1111
```
1212
uv run --group docs mkdocs serve
1313
```
14+
15+
## References
16+
17+
- aiocogeo: https://github.com/geospatial-jeff/aiocogeo

README.md

Lines changed: 86 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,95 @@
11
# async-geotiff
22

3-
Async GeoTIFF and [Cloud-Optimized GeoTIFF][cogeo] (COG) reader for Python, wrapping [`async-tiff`][async-tiff].
3+
Fast, async GeoTIFF and [Cloud-Optimized GeoTIFF][cogeo] (COG) reader for Python, wrapping the Rust-based [Async-TIFF][async-tiff] library.
44

55
[async-tiff]: https://github.com/developmentseed/async-tiff
66
[cogeo]: https://cogeo.org/
77

8-
## Project Goals:
8+
## Features
99

10-
- Support only for GeoTIFF and Cloud-Optimized GeoTIFF (COG) formats
11-
- Support for reading only, no writing support
12-
- Full type hinting.
13-
- API similar to rasterio where possible.
14-
- We won't support the full rasterio API, but we'll try to when it's possible to implement rasterio APIs with straightforward maintenance requirements.
15-
- For methods where we do intentionally try to match with rasterio, the tests should match against rasterio.
16-
- Initially, we'll try to support a core set of GeoTIFF formats. Obscure GeoTIFF files may not be supported.
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.
1720

18-
## References
21+
**Anti-Features** (features explicitly not in scope):
1922

20-
- aiocogeo: https://github.com/geospatial-jeff/aiocogeo
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+
![](assets/sentinel_2_plot.jpg)

assets/sentinel_2_plot.jpg

61.6 KB
Loading

docs/assets/sentinel_2_plot.jpg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../assets/sentinel_2_plot.jpg

mkdocs.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,6 @@ plugins:
9393
python:
9494
paths: [src]
9595
options:
96-
# We set allow_inspection: false to ensure that all docstrings come
97-
# from the pyi files, not the Rust-facing doc comments.
98-
allow_inspection: false
9996
docstring_section_style: list
10097
docstring_style: google
10198
inherited_members: true

pyproject.toml

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ dev = [
4343
"build>=1.4.0",
4444
"ipykernel>=7.1.0",
4545
"jsonschema>=4.26.0",
46+
"matplotlib>=3.10.8",
4647
"morecantile>=7.0.2",
4748
"obstore>=0.8.2",
4849
"pydantic>=2.12.5",
@@ -52,6 +53,8 @@ dev = [
5253
"types-jsonschema>=4.26.0.20260109",
5354
]
5455
docs = [
56+
# Workaround for https://github.com/mkdocs/mkdocs/issues/4032
57+
"click<8.3",
5558
"mkdocs-material[imaging]>=9.5.49",
5659
"mkdocs>=1.6.1",
5760
"mkdocstrings[python]>=1.0",
@@ -76,6 +79,7 @@ module = [
7679
# https://github.com/rasterio/affine/issues/135
7780
"affine.*",
7881
"async_tiff.store.*",
82+
"rasterio.*",
7983
]
8084
ignore_missing_imports = true
8185

@@ -92,9 +96,10 @@ ignore = [
9296

9397
[tool.ruff.lint.per-file-ignores]
9498
"tests/*" = [
95-
"ANN001", # annotation in function argument
96-
"ANN201", # return type annotation
97-
"S101", # assert
98-
"SLF001", # private member access
99-
"D", # docstring
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
100105
]

src/async_geotiff/__init__.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,20 @@
33
[cogeo]: https://cogeo.org/
44
"""
55

6+
from . import exceptions
67
from ._array import Array
78
from ._geotiff import GeoTIFF
89
from ._overview import Overview
10+
from ._tile import Tile
911
from ._version import __version__
12+
from ._windows import Window
1013

11-
__all__ = ["Array", "GeoTIFF", "Overview", "__version__"]
14+
__all__ = [
15+
"Array",
16+
"GeoTIFF",
17+
"Overview",
18+
"Tile",
19+
"Window",
20+
"__version__",
21+
"exceptions",
22+
]

src/async_geotiff/_fetch.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
from affine import Affine
77

8-
from async_geotiff import Array
8+
from async_geotiff._array import Array
9+
from async_geotiff._tile import Tile
910
from async_geotiff._transform import HasTransform
1011

1112
if TYPE_CHECKING:
@@ -63,7 +64,7 @@ async def fetch_tile(
6364
self: HasTiffReference,
6465
x: int,
6566
y: int,
66-
) -> Array:
67+
) -> Tile:
6768
tile_fut = self._ifd.fetch_tile(x, y)
6869

6970
mask_data: AsyncTiffArray | None = None
@@ -80,20 +81,26 @@ async def fetch_tile(
8081
y * self.tile_height,
8182
)
8283

83-
return Array._create( # noqa: SLF001
84+
array = Array._create( # noqa: SLF001
8485
data=tile_data,
8586
mask=mask_data,
8687
planar_configuration=self._ifd.planar_configuration,
8788
crs=self.crs,
8889
transform=tile_transform,
8990
nodata=self.nodata,
9091
)
92+
return Tile(
93+
x=x,
94+
y=y,
95+
_ifd=self._ifd,
96+
array=array,
97+
)
9198

9299
async def fetch_tiles(
93100
self: HasTiffReference,
94101
xs: list[int],
95102
ys: list[int],
96-
) -> list[Array]:
103+
) -> list[Tile]:
97104
"""Fetch multiple tiles from this overview.
98105
99106
Args:
@@ -116,7 +123,7 @@ async def fetch_tiles(
116123
tiles = await tiles_fut
117124
decoded_tiles = await asyncio.gather(*[tile.decode() for tile in tiles])
118125

119-
arrays: list[Array] = []
126+
final_tiles: list[Tile] = []
120127
for x, y, tile_data, mask_data in zip(
121128
xs,
122129
ys,
@@ -136,6 +143,12 @@ async def fetch_tiles(
136143
transform=tile_transform,
137144
nodata=self.nodata,
138145
)
139-
arrays.append(array)
146+
tile = Tile(
147+
x=x,
148+
y=y,
149+
_ifd=self._ifd,
150+
array=array,
151+
)
152+
final_tiles.append(tile)
140153

141-
return arrays
154+
return final_tiles

src/async_geotiff/_geotiff.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from async_geotiff._crs import crs_from_geo_keys
1212
from async_geotiff._fetch import FetchTileMixin
1313
from async_geotiff._overview import Overview
14+
from async_geotiff._read import ReadMixin
1415
from async_geotiff._transform import TransformMixin
1516
from async_geotiff.colormap import Colormap
1617

@@ -23,7 +24,7 @@
2324

2425

2526
@dataclass(frozen=True, init=False, kw_only=True, repr=False)
26-
class GeoTIFF(FetchTileMixin, TransformMixin):
27+
class GeoTIFF(ReadMixin, FetchTileMixin, TransformMixin):
2728
"""A class representing a GeoTIFF image."""
2829

2930
_crs: CRS | None = None
@@ -272,7 +273,14 @@ def nodata(self) -> float | None:
272273

273274
@property
274275
def overviews(self) -> list[Overview]:
275-
"""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+
"""
276284
return self._overviews
277285

278286
@property

src/async_geotiff/_overview.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from affine import Affine
77

88
from async_geotiff._fetch import FetchTileMixin
9+
from async_geotiff._read import ReadMixin
910
from async_geotiff._transform import TransformMixin
1011

1112
if TYPE_CHECKING:
@@ -18,7 +19,7 @@
1819

1920

2021
@dataclass(init=False, frozen=True, kw_only=True, eq=False, repr=False)
21-
class Overview(FetchTileMixin, TransformMixin):
22+
class Overview(ReadMixin, FetchTileMixin, TransformMixin):
2223
"""An overview level of a Cloud-Optimized GeoTIFF image."""
2324

2425
_geotiff: GeoTIFF

0 commit comments

Comments
 (0)