Welcome to EORST
Welcome to the EORST blog!
EORST is an open-source Rust library for processing geospatial raster data. Inspired by Python libraries like rasterio and rioxarray, it enables efficient parallel processing of large-scale raster datasets.
New to remote sensing? Start with Getting Started with Geospatial Rust for foundational concepts.
Want a complete workflow? See End-to-End Geospatial Processing with EORST for a code-heavy tutorial.
Benchmarking eorst vs Python+Dask? See Rust vs Python+Dask for NDVI: 23× Faster — real numbers with FMask cloud masking and carbon impact analysis.
Why Rust for Geospatial Processing?
Rust offers several advantages for geospatial computing:
- Performance: Near C-level performance with safety guarantees
- Memory safety: No garbage collection, predictable latency
- Concurrency: Safe parallel processing without data races
- Tooling: First-class cargo ecosystem
Your First EORST Project
Let’s build a complete NDVI (Normalized Difference Vegetation Index) processing pipeline that queries satellite imagery via STAC and uses semantic band selection!
1. Create a new project
cargo new ndvi_processor
cd ndvi_processor
2. Add dependencies to Cargo.toml
[package]
name = "ndvi_processor"
version = "0.1.0"
edition = "2021"
[dependencies]
eorst = "0.3"
rss_core = "0.4"
ndarray = "0.17"
anyhow = "1.0"
chrono = "0.4"
log = "0.4"
env_logger = "0.11"
3. Write the code
Create src/main.rs:
use anyhow::Result;
use chrono::NaiveDate;
use eorst::{
init_logger,
types::BlockSize,
RasterDataset, RasterDatasetBuilder, Select,
};
use log::info;
use ndarray::{Array4, Zip};
use rss_core::{
query::ImageQueryBuilder,
qvf::Collection,
utils::{Cmp, Intersects},
DEA,
};
use std::path::PathBuf;
fn main() -> Result<()> {
init_logger();
info!("Building NDVI processor...");
// Step 1: Query Sentinel-2 data from DEA using canonical band names
let query = ImageQueryBuilder::new(
DEA,
Collection::Sentinel2,
Intersects::Scene(vec!["56jns"]),
)
.canonical_bands(["red", "nir"]) // Semantic band names!
.start_date(NaiveDate::parse_from_str("20210101", "%Y%m%d").unwrap())
.end_date(NaiveDate::parse_from_str("20210601", "%Y%m%d").unwrap())
.cloudcover((Cmp::Less, 5))
.build();
// Step 2: Download data to temporary directory
let tmp_dir = PathBuf::from("/tmp/ndvi_data");
std::fs::create_dir_all(&tmp_dir)?;
let local_stac = query.get(&tmp_dir, None, None)?;
info!("Downloaded {} items", local_stac.items.len());
// Step 3: Build the RasterDataset
let rds: RasterDataset<i16> = RasterDatasetBuilder::from_stac_query(&local_stac)
.block_size(BlockSize { rows: 2048, cols: 2048 })
.build();
info!("Created dataset:\n{}", rds);
// Step 4: Process - compute NDVI using apply() and Select trait
let output_path = PathBuf::from("./ndvi_output.tif");
rds.apply::<i16>(
|block| {
// Select bands by semantic name - no hardcoded indices!
let red = block.select_layers(&["red"])?;
let nir = block.select_layers(&["nir"])?;
let mut ndvi = Array4::zeros(red.data.raw_dim());
Zip::from(&mut ndvi)
.and(&red.data)
.and(&nir.data)
.for_each(|out, &r, &n| {
let val = (n as f32 - r as f32) / (n as f32 + r as f32 + 1e-10);
*out = (val * 10000.0) as i16;
});
Ok(ndvi)
},
4, // number of threads
&output_path,
)?;
info!("NDVI computation complete! Output: {:?}", output_path);
Ok(())
}
4. Run it!
# With Nix (recommended - handles all dependencies)
nix develop
cargo run --release
What Just Happened?
- Query: We asked DEA for Sentinel-2 data for scene “56jns” between Jan-June 2021 with <5% cloud cover, selecting only the red and NIR bands
- Download: STAC client fetched the data and created a local cache
- Build:
RasterDatasetBuildercreated a virtual dataset from the STAC items, aligning bands automatically - Process: The
apply()function applied our worker to every block using the Select trait to pick bands by name - Output: Results saved to a GeoTIFF with proper georeferencing
Key Concepts
| Concept | Description |
|---|---|
RasterDataset |
Main data structure representing a multi-band, multi-temporal raster |
RasterDatasetBuilder |
Fluent API for creating datasets from files, STAC queries, or scratch |
apply |
Parallel processing that passes RasterDataBlock with metadata for name-based selection |
Select trait |
Select bands by semantic name: .select_layers(&["red", "nir"]) |
canonical_bands |
Provider-agnostic band names: ["red", "nir"] → ["nbart_red", "nbart_nir_1"] |
RasterDataBlock<T> |
Typed block with layer names and time indices for metadata-aware processing |
Next Steps
- Try different worker functions (e.g., EVI, SAVI)
- Explore point sampling with
sample_points() - Check out zonal statistics with the
use_polarsfeature - Read the full documentation
Happy processing!