Tree counting with satellite imagery
June 30, 2026 · View on GitHub
This repository contains the code and data for the upcoming ECCV26 paper, Counting Trees from Satellite Imagery with Noisy Supervision:
- TinyTrees — a multi-sensor tree counting benchmark with point-level annotations across three geographic regions and satellite sensors.
- TreeMatch — a training method for tree density estimation that leverages optimal transport to learn from both strong and weak annotations.
TinyTrees Dataset
TinyTrees provides georeferenced satellite imagery with per-tree point annotations across three regions, sensors, and resolutions:
| Region | Sensor | GSD | Train (strong) | Train (weak) | Test | Total trees | Total area |
|---|---|---|---|---|---|---|---|
| Rwanda | PlanetScope | 3.0 m | 231 tiles / 309k trees | 73 tiles / 3.4M trees | 645 tiles / 237k trees | 3.9M | 283 km² |
| China | Gaofen-2 | 0.8 m | 446 tiles / 55k trees | 16,364 tiles / 7.7M trees | 2,189 tiles / 70k trees | 7.8M | 2,344 km² |
| France | SPOT-6 | 1.5 m | 492 tiles / 11k trees | CHM-derived (via Open-Canopy) | 493 tiles / 11k trees | 22k | 0.7 km² |
Each tile is a 5-band GeoTIFF (4 spectral bands + 1 binary validity mask). Point annotations are stored in a single GeoPackage per split with a tile column linking each point to its image.
SPOT weak labels are CHM-derived pseudolabels bundled in spot/train_weak/. The corresponding SPOT-6 imagery is not redistributed; to use it, download the Open-Canopy dataset and point spot_imagery_root at its canopy_height/ directory:
canopy_height/
├── 2021/spot/compressed_pansharpened_*.tif
├── 2022/spot/compressed_pansharpened_*.tif
└── 2023/spot/compressed_pansharpened_*.tif
Add to conf/local/local.yaml:
spot_imagery_root: /path/to/open-canopy/datasets/canopy_height
Download
The dataset is hosted on HuggingFace and mirrored at https://sid.erda.dk/cgi-sid/ls.py?share_id=ET2ix678WL. PlanetScope and Gaofen imagery are under the CC BY-NC 4.0 license: usage is reserved for research and education only.
tinytrees/
├── ps/ # PlanetScope (Rwanda)
│ ├── train_strong/ # *.tif + points.gpkg
│ ├── train_weak/
│ └── test/
├── gf/ # Gaofen-2 (China)
│ ├── train_strong/
│ ├── train_weak/
│ └── test/
└── spot/ # SPOT-6 (France)
├── train_strong/
├── train_weak/ # pseudolabels only (imagery from Open-Canopy)
└── test/
Loading data
from data.ps import PlanetScopeStrong
from data.gf import GaofenStrong
from data.spot import SPOTStrong
# Each dataset returns (image, valid_mask, count_map)
# image: (C+1, H, W) float tensor — C normalized bands + 1 validity channel
# valid_mask: (1, H, W) binary tensor
# count_map: (1, H, W) sparse 0/1 tensor with tree locations
ds = PlanetScopeStrong(imsize=64, split="train_strong", root="/path/to/tinytrees/ps")
ds = GaofenStrong(imsize=64, split="train_strong", root="/path/to/tinytrees/gf")
ds = SPOTStrong(imsize=64, split="train_strong", root="/path/to/tinytrees/spot")
TreeMatch
TreeMatch is an optimal-transport-based training method for tree density estimation that supports mixed supervision from strong (expert) and weak (e.g. CHM-derived) point annotations. It uses unbalanced optimal transport to match predicted density maps to point annotations, with a slack mechanism to down-weight weak labels.
Pretrained models
Pretrained TreeMatch checkpoints for all three sensors are available on HuggingFace.
from hub_model import UNetR50
import torch
model = UNetR50.from_pretrained("dgominski/TinyTrees", subfolder="ps") # Rwanda / PlanetScope
# model = UNetR50.from_pretrained("dgominski/TinyTrees", subfolder="gf") # China / Gaofen-2
# model = UNetR50.from_pretrained("dgominski/TinyTrees", subfolder="spot") # France / SPOT-6
model.eval()
# Input: (B, 5, H, W) — RGBI + 1 binary validity mask (last channel)
x = torch.randn(1, 4, 128, 128)
valid = torch.ones(1, 1, 128, 128) # 1 = valid pixel, 0 = nodata
density = model(torch.cat([x, valid], dim=1)) # (B, 1, H, W), trees/pixel
Training
Training is configured via Hydra. The main entry point is train.py:
# Train TreeMatch on PlanetScope with strong labels only
python train.py dataset=ps model=treematch train.strong_ratio=1.0 model.lr=8e-05
# Train with 80% strong + 20% weak labels
python train.py dataset=ps model=treematch train.strong_ratio=0.8 model.lr=8e-05
# Train density regression baseline
python train.py dataset=ps model=density_regressor
# Train DM-Count baseline
python train.py dataset=ps model=dm_count
Override dataset paths for your machine in conf/local/local.yaml:
# @package _global_
dataset:
root: ${dataset_roots.${hydra:runtime.choices.dataset}}
dataset_weak:
root: ${dataset_roots.${hydra:runtime.choices.dataset_weak}}
dataset_roots:
ps: /path/to/tinytrees/ps
gf: /path/to/tinytrees/gf
spot: /path/to/tinytrees/spot
ps_weak: /path/to/tinytrees/ps/train_weak
gf_weak: /path/to/tinytrees/gf/train_weak
spot_weak: /path/to/tinytrees/spot/train_weak
# SPOT weak labels use imagery from Open-Canopy (not bundled in TinyTrees)
spot_imagery_root: /path/to/open-canopy/canopy_height
Available models
| Model | Config | Description |
|---|---|---|
| TreeMatch | model=treematch | Unbalanced OT density matching with slack for weak labels |
| Density Regression | model=density_regressor | Gaussian density map regression (MSE loss) |
| DM-Count | model=dm_count | Balanced OT with total variation loss |
| CenterNet | model=centernet | Keypoint detection with heatmap regression |
| P2PNet | model=p2p | Point-to-point matching |
Available backbones
| Backbone | Config | Architecture |
|---|---|---|
| ResNet-50 U-Net | backbone=resnet50 | segmentation_models_pytorch U-Net with ResNet-50 encoder |
| Swin Transformer | backbone=swint | SwinV2-Small with FPN decoder |
| ViT | backbone=vit | Vision Transformer with FPN decoder |
Rwanda-Tanzania application
We trained a PlanetScope+Sentinel-1+Sentinel-2 (20-band composite) model to count trees in Rwanda and Tanzania.
Reference
@inproceedings{gominski2026counting,
title = {Counting Trees from Satellite Imagery with Noisy Supervision},
author = {Gominski, Dimitri and Mugabowindekwe, Maurice and Xu, Qiue and Tong, Xiaowei and Brandt, Martin and Le, Hieu and Fensholt, Rasmus and Samaras, Dimitris and Landrieu, Loic},
booktitle = {European Conference on Computer Vision},
year = {2026}
}
,