๐ŸŠ GeoPool: From Pixels to Patches

March 26, 2026 ยท View on GitHub

Accepted at the ICLR 2026 ML4RS Workshop in Rio de Janeiro, Brazil ๐Ÿ‡ง๐Ÿ‡ท

Paper Dataset License

Benchmark for evaluating pixel-to-patch pooling methods on geospatial foundation model embeddings.

As geospatial foundation models shift from patch-level to pixel-level embeddings, practitioners must aggregate thousands of pixel vectors into patch representations. We benchmark 13 pooling methods across 3 GFMs (AlphaEarth, OlmoEarth, Tessera) on EuroSAT land-cover classification and release EuroSAT-Embed: 81,000 embedding GeoTIFFs for reproducible pooling research. Across the benchmark, simple distributional pooling methods improve spatial-split performance over mean pooling and substantially reduce the random-to-spatial generalization gap.

๐Ÿ“Š Key Results

Stats pooling (min/max/mean/std) is the strongest default: for linear probes it improves average spatial accuracy from 87.3% to 93.0% and cuts the random-to-spatial gap from 8.8 pp to 3.8 pp. Covariance pooling reaches the best accuracy on two of three encoders when higher dimensionality is acceptable, while mean+max is another strong low-complexity option with a small spatial generalization gap.

Random vs. spatial split accuracy across encoders. Points near the diagonal generalize better under geographic shift.

๐Ÿ’ก Recommendation: Mean Baseline, Stats Default

Use mean as a low-cost baseline. When a modest increase in dimensionality is acceptable, use stats pooling as the default. When maximum accuracy matters more than representation size, use covariance pooling. If you want a lighter-weight alternative to covariance, mean+max is a strong option.

NumPy implementations:

import numpy as np

def stats_pool(x: np.ndarray) -> np.ndarray:
    """Stats pooling over spatial dims. x: (H, W, D) -> (4D,)"""
    mins = x.min(axis=(0, 1))
    maxs = x.max(axis=(0, 1))
    means = x.mean(axis=(0, 1))
    stds = x.std(axis=(0, 1))
    return np.concatenate([mins, maxs, means, stds], axis=-1)


def mean_max_pool(x: np.ndarray) -> np.ndarray:
    """Mean+max pooling over spatial dims. x: (H, W, D) -> (2D,)"""
    means = x.mean(axis=(0, 1))
    maxs = x.max(axis=(0, 1))
    return np.concatenate([means, maxs], axis=-1)


def covariance_pool(x: np.ndarray) -> np.ndarray:
    """Upper-triangular covariance pooling. x: (H, W, D) -> (D(D+1)/2,)"""
    pixels = x.reshape(-1, x.shape[-1])
    centered = pixels - pixels.mean(axis=0, keepdims=True)
    denom = max(pixels.shape[0] - 1, 1)
    cov = centered.T @ centered / denom
    tri = np.triu_indices(cov.shape[0])
    return cov[tri]

๐Ÿ—‚๏ธ Pooling Methods

Training-Free

MethodKeyDimDescription
MeanmeanDGlobal average pooling
MaxmaxDGlobal max pooling
StdstdDGlobal standard deviation
GeMgemDGeneralized mean pooling (p=3)
Center-Weightedcenter_weighted_meanDGaussian-weighted mean (center focus)
Mean+Stdmean_std2DConcatenation of mean and std
Mean+Maxmean_max2DConcatenation of mean and max
Median+IQRmedian_iqr2DMedian and interquartile range
Statsstats4Dmin, max, mean, std concatenated
Percentilespercentiles5D10th, 25th, 50th, 75th, 90th percentiles
Covarianceflattened_covD(D+1)/2Upper triangle of covariance matrix

Parametric (require training data)

MethodKeyDimDescription
PCApca_6464PCA on mean-pooled embeddings
BoVWbovw_128128Bag of Visual Words (k-means clustering)

โš™๏ธ Setup

make install

For the download/embed workflow, install the extra dependency group too:

uv sync --dev --group download

๐Ÿ“ฆ Datasets

The dense pixel embedding variants of EuroSAT and pooled versions are on HuggingFace.

# pooled embeddings
wget https://hf.co/datasets/isaaccorley/eurosat-embed/resolve/main/embeddings-aef-pooled.tar.gz
wget https://hf.co/datasets/isaaccorley/eurosat-embed/resolve/main/embeddings-olmoearth-nano-pooled.tar.gz
wget https://hf.co/datasets/isaaccorley/eurosat-embed/resolve/main/embeddings-tessera-pooled.tar.gz

# pixel embeddings
wget https://hf.co/datasets/isaaccorley/eurosat-embed/resolve/main/eurosat-aef.tar.gz
wget https://hf.co/datasets/isaaccorley/eurosat-embed/resolve/main/eurosat-olmoearth-nano.tar.gz
wget https://hf.co/datasets/isaaccorley/eurosat-embed/resolve/main/eurosat-tessera.tar.gz

๐Ÿงช Evaluation

Once pooled embeddings are downloaded/created in embeddings/:

Run kNN and linear probes

uv run python scripts/knnprobe.py --dataset-name aef
uv run python scripts/linearprobe.py --dataset-name aef

Generate paper tables

uv run python scripts/knn_table.py --output paper/knn_table.tex
uv run python scripts/linear_table.py --output paper/linear_table.tex

Plot results

uv run python scripts/plot_results.py

๐Ÿ”ง (Optional) Generating Pixel Embeddings

All data is available on HuggingFace above. To regenerate from scratch:

Download EuroSAT and splits

uv run python data/download_eurosat.py

Create EuroSAT-AEF from Google Earth Engine (requires GEE auth)

uv run python data/download_eurosat_aef.py
uv run python data/convert_aef.py

Generate EuroSAT-OlmoEarth and EuroSAT-Tessera embeddings

Install the download/embed dependency group first:

uv sync --dev --group download
uv run python scripts/embed_olmoearth.py --model-size nano
uv run python scripts/embed_tessera.py

Cache pooled embeddings

uv run python scripts/pool.py --dataset-name aef
uv run python scripts/pool.py --dataset-name tessera
uv run python scripts/pool.py --dataset-name olmoearth-nano
uv run python scripts/pool.py --dataset-name olmoearth-tiny
uv run python scripts/pool.py --dataset-name olmoearth-base

If running OOM, use scripts/pool-stream.py which streams in batches (slower).

๐Ÿ› ๏ธ Development

make install # install deps
make check  # lint + format + typecheck
make test   # run tests

๐Ÿ“ Citation

@article{corley2026pixels,
  title={From Pixels to Patches: Pooling Strategies for Earth Embeddings},
  author={Corley, Isaac and Robinson, Caleb and Becker-Reshef, Inbal and Ferres, Juan M Lavista},
  journal={arXiv preprint arXiv:2603.02080},
  year={2026}
}