Docs add async geotiff tutorial#528
Conversation
Adds a tutorial walking through pixel-level Cloud Optimized GeoTIFF reads with async-geotiff — no GDAL dependency, async-first, Rust core. Companion notebook lives in PlanetaryComputerExamples at quickstarts/async-geotiff.ipynb and is wired in via external_docs_config. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nshots Replace nonexistent APIs (obstore async_=True, GeoTIFF.open(full_href), generate_tms, lonboard RasterLayer.from_stac) with the shipping equivalents: from_asset(asset) + empty open() path, the Sentinel-2 visual RGB asset, and a BitmapTileLayer fed by the Planetary Computer tiler. Add the window-preview and Lonboard-scene screenshots. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| @@ -0,0 +1,164 @@ | |||
| # Reading Planetary Computer COGs with async-geotiff | |||
|
|
|||
| [async-geotiff](https://github.com/developmentseed/async-geotiff) is a Python Cloud Optimized GeoTIFF reader with no GDAL dependency. The core is Rust, image decoding runs in a thread pool, buffers are zero-copy, and every API is fully type-hinted. Use it when you want async I/O for pixel-level analysis without putting GDAL on the system. | |||
There was a problem hiding this comment.
| [async-geotiff](https://github.com/developmentseed/async-geotiff) is a Python Cloud Optimized GeoTIFF reader with no GDAL dependency. The core is Rust, image decoding runs in a thread pool, buffers are zero-copy, and every API is fully type-hinted. Use it when you want async I/O for pixel-level analysis without putting GDAL on the system. | |
| [async-geotiff](https://github.com/developmentseed/async-geotiff) is a Python [Cloud Optimized GeoTIFF](https://cogeo.org/) reader with no GDAL dependency. The core is Rust, image decoding runs in a thread pool, buffers are zero-copy, and every API is fully type-hinted. Use it when you want async I/O for pixel-level analysis without putting GDAL on the system. |
| uv add async-geotiff obstore planetary-computer pystac-client lonboard matplotlib | ||
| ``` | ||
|
|
||
| `async-geotiff` is the user-facing library. `async-tiff` is the lower-level Rust core. Use it directly only if you're building library infrastructure on top. |
There was a problem hiding this comment.
| `async-geotiff` is the user-facing library. `async-tiff` is the lower-level Rust core. Use it directly only if you're building library infrastructure on top. | |
| `async-geotiff` is the high-level library for reading GeoTIFF and COG files. `async-tiff` is the lower-level Rust core for generically reading TIFF files. It shouldn't be necessary to touch for most users. |
| print(geotiff.overviews) # finest → coarsest | ||
| ``` | ||
|
|
||
| The header read is a single range request. This is the same pattern used by [obstore.](./obstore.md) |
There was a problem hiding this comment.
| The header read is a single range request. This is the same pattern used by [obstore.](./obstore.md) | |
| The header read usually fits in one or two range requests, facilitated by [obstore](./obstore.md). |
|
|
||
| ## Pick an overview | ||
|
|
||
| `geotiff.overviews` is ordered finest-to-coarsest. Index `0` is the full-resolution image. A coarser overview is the right choice for previews or zoomed-out work: |
There was a problem hiding this comment.
No, the geotiff itself is the full-resolution image. Index 0 of the overviews is the finest resolution after the full-resolution data.
| array = await full_res.read(window=window) | ||
| ``` | ||
|
|
||
| The returned `Array` has: |
There was a problem hiding this comment.
The returned class is named RasterArray https://developmentseed.org/async-geotiff/latest/api/raster-array/
| import numpy as np | ||
| import matplotlib.pyplot as plt | ||
|
|
||
| plt.imshow(np.transpose(array.data, (1, 2, 0))) |
There was a problem hiding this comment.
In case it's clearer, this is shipped as reshape_as_image so that users don't have to remember the band ordering (1, 2, 0) is easy to forget or get wrong IMO
|
|
||
| ## Visualize the scene with Lonboard | ||
|
|
||
| For an interactive map view of the same Sentinel-2 item, stream its COG tiles through the Planetary Computer tiler into a [Lonboard](https://developmentseed.org/lonboard/) `BitmapTileLayer`: |
There was a problem hiding this comment.
As a note, the BitmapTileLayer uses titiler on the backend to send formatted PNG tiles, this doesn't read the COG data directly. That's fine if you'd like to point out how this integrates with the planetary computer titiler server!
In case you want an example of how to read the COG data directly through async-geotiff, you'd want to use RasterLayer.from_geotiff which natively integrates with async-geotiff.
| Each `read()` is independent. Fire many at once with `asyncio.gather` | ||
|
|
||
| async-geotiff issues range requests in parallel and decodes them on the Rust thread pool: | ||
|
|
||
| ```python | ||
| import asyncio | ||
|
|
||
| windows = [ | ||
| Window(c, r, 256, 256) | ||
| for c in range(0, 2048, 256) for r in range(0, 2048, 256) | ||
| ] | ||
| arrays = await asyncio.gather( | ||
| *[full_res.read(window=w) for w in windows] | ||
| ) | ||
| ``` |
There was a problem hiding this comment.
Similar to #527 (comment), this isn't an ideal example to suggest to people, because we're instructing them to make entirely independent requests for each window of the file.
But this is something that read handles automatically, and it'll be faster because it can minimize the total number of requests that need to be made.
So here, a single
window = Window(0, 0, 2048, 2048)
full_res.read(window=window)would be a lot better than making many independent window reads
|
|
||
| ## When to use something else | ||
|
|
||
| - For resampling, reprojection, or warping, hand the array to [rasterio](https://rasterio.readthedocs.io/). |
There was a problem hiding this comment.
| - For resampling, reprojection, or warping, hand the array to [rasterio](https://rasterio.readthedocs.io/). | |
| - For resampling, reprojection, or warping, use [rasterio](https://rasterio.readthedocs.io/), either alone or in combination with async-geotiff. |
| - For resampling, reprojection, or warping, hand the array to [rasterio](https://rasterio.readthedocs.io/). | ||
| - For interactive visualization, see [Lonboard](https://developmentseed.org/lonboard/). | ||
| - For the raw-bytes layer beneath async-geotiff, see [obstore](https://developmentseed.org/obstore/). | ||
| - For library authors building on the Rust core, drop to [async-tiff](https://github.com/developmentseed/async-tiff). |
There was a problem hiding this comment.
Python library authors who want to build on top of GeoTIFF should still be using async-geotiff, not async-tiff. It's really only people who want generic TIFF support, and don't want to specialize their code to support only GeoTIFF, who should be using async-tiff.
Apply Kyle Barron's review feedback: - Link to cogeo.org in intro for "Cloud Optimized GeoTIFF". - Clarify async-tiff is generic TIFF, async-geotiff is the right high-level entry for COG/GeoTIFF work. - Simplify header-read description and reference obstore. - Fix overviews framing: geotiff is full-res; overviews[0] is the finest overview below it, not full-res itself. - Rename returned class Array -> RasterArray; rename example variable array -> raster_array. - Swap manual (1,2,0) transpose for the reshape_as_image helper. - Replace the BitmapTileLayer + titiler-backed visualization with RasterLayer.from_geotiff + a render_tile callback so the example actually showcases async-geotiff. - Replace the asyncio.gather fan-out with a single Window(0,0,2048, 2048) read, because read() coalesces range requests itself. - Rephrase rasterio and async-tiff guidance in the closing list. - Add pillow to install (used by the render_tile callback). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
kylebarron
left a comment
There was a problem hiding this comment.
Just one question about the example but overall looks good!
| ## Pick a resolution | ||
|
|
||
| `geotiff.overviews` is ordered finest-to-coarsest. Index `0` is the full-resolution image. A coarser overview is the right choice for previews or zoomed-out work: | ||
| `geotiff` itself is the full-resolution image. `geotiff.overviews` is the resolution pyramid *below* the full-resolution data, ordered finest-to-coarsest — index `0` is the finest overview, index `-1` is the coarsest. For zoomed-out previews or quick checks, read from a coarser overview; for analysis, read from `geotiff` directly: |
| return EncodedImage(data=buf.getvalue(), media_type="image/png") | ||
|
|
||
|
|
||
| Map(RasterLayer.from_geotiff(geotiff, render_tile=render_visual_tile), height=800) |
There was a problem hiding this comment.
Previously the example was end-to-end, because tilejson loaded from a URL. Should we make this example end-to-end too? geotiff isn't created anywhere
Based on the outline here
Related notebook: microsoft/PlanetaryComputerExamples#315