Leaderboard¶
Tasks¶
Out-of-distribution¶
The first task evaluates generalization across geography and acquisition conditions. Selected source datasets are held out from the train split; models are fine-tuned on the remaining sources and evaluated on the held-out test sources (no images from test localities appear in train).
Within-distribution¶
The second task is to create the best global detector for individual trees given a set of training and test data. Datasets are split randomly, reflecting information within localities. This is consistent with how most applied users engage with models, by fine-tuning backbone models with sample data from a desired locality.
Cross-geometry¶
Off the shelf tools often limit users for a single annotation type. We have ‘point’ models, ‘box’ models and ‘polygon’ models. To create truly global models for biological inference, we need models that can use all available data, not just one annotation geometry. In particular, polygon annotations are very time consuming to create, but are often desirable for downstream usecases. We opted against polygon training sources, for example polygons to points, as this is an unrealistic, or atleast, very uncommon downstream use case.
Boxes to Polygons¶
All box sources are used to train and predict all polygon sources. There is no local data from the test localities in train.
Points to Polygons¶
All point sources are used to train and predict all polygon sources
Points to Boxes¶
All point sources are used to train and predict all box sources.
Results¶
All scores below are computed on MillionTrees v0.23, on the test split of the named
split scheme. Every table in this page is generated from the evaluation result files by
scripts/make_benchmark_table.py, which reads an explicit registry of published runs — so
the numbers, and the configuration table that accompanies them, always describe the same run.
Fine-tuned panel figures (within-distribution + out-of-distribution splits, ground truth vs
prediction) are generated from training checkpoints via
scripts/create_finetuned_visualizations.py (PNG + SVG under docs/).
See repository_structure.md.
How to read the metrics¶
Recall is geometry-specific: detection recall (boxes), keypoint accuracy (points), or mask recall (polygons).
Mask-aware precision is partial-annotation aware. An unmatched prediction that lands on tree-covered pixels is ignored rather than counted as a false positive, because most MillionTrees sources are incompletely annotated. This is why precision reads high, and it is why precision here is not comparable to a standard COCO precision.
F1 is the harmonic mean of that recall and that mask-aware precision, computed post-hoc.
AP is AP40 — average precision at IoU 0.4, the same match threshold recall and mask-aware precision use, so AP and F1 agree on what counts as a match. There is no AP50 column; AP40 runs roughly +0.04 to +0.11 above AP50 on the same predictions, so these numbers are not comparable to MillionTrees tables published before 2026-08-04. See notes/ap50_vs_ap40_existing_models.md.
Point matching uses a GSD-normalized radius per source (a fixed ground distance, not a fixed pixel count), so a point prediction is matched at the same real-world tolerance regardless of the source’s resolution.
Counting MAE is computed only over completely-annotated sources. TreePolygons emits no counting metric, so polygon tables have no counting column.
Reproducing a row¶
Two things determine a number as much as the model does, and both are recorded in the Run configuration table at the bottom of this page:
Dataset version. The loaders default to the newest key in their
_versions_dict, so the same command run after a release produces different numbers. Pin it explicitly —TreeBoxesDataset(version="0.23")— when reproducing anything here. All rows on this page are v0.23, supervised sources only (include_unsupervised=False, the default).Score threshold. The standardized operating point is 0.10. Two exceptions are marked in the configuration table: the CanopyRS box and polygon rows use 0.30, the best-F1 point from a full-test-set sweep (at 0.10 they are badly recall-heavy), and the Detectron2 polygon model uses its built-in 0.15
ROI_HEADS.SCORE_THRESH_TEST. These are per-model tuned exceptions, not a re-standardization.
Beyond those, the configuration table lists the checkpoint, evaluation image size, Python
environment, and full training hyperparameters per row. Note that several published runs
deliberately differ from the script defaults — the point rows run at image size 896 (default
448) with --loss-preset pretrain, and the box rows use different early-stopping patience on
each split — so running a bare command from this page without those flags will not reproduce
the number.
Known caveats¶
Neither the box nor the point trainer sets a random seed, so those rows are not bit-reproducible; expect run-to-run variation. The polygon trainers use seed 42.
The fine-tuned polygon Detectron2 weights predate v0.23; the model is scored on v0.23 data but was trained on an earlier packaging of it. A v0.23 retrain is pending.
Cross-geometry is defined as predicting polygons from another annotation geometry. It is not applicable to box or point prediction, so those tables are absent rather than zero-filled.
TreeFormer fine-tunes well and generalizes poorly¶
The point rows show a clear gradient, and it is a result rather than an artifact. Fine-tuning TreeFormer helps in proportion to how closely the evaluation resembles the training data, and hurts once it does not:
Evaluation |
Relationship to train |
Fine-tuned |
Pretrained |
|---|---|---|---|
Within-distribution test |
same sources |
F1 0.782, counting nMAE 0.214 |
F1 0.726, nMAE 1.611 |
Out-of-distribution test |
held-out aerial sources |
F1 0.675, nMAE 0.350 |
F1 0.750, nMAE 0.489 |
Validation (held-out TLS) |
different reference geometry |
keypoint acc 0.265, Allen MAE 136.5 (nMAE 2.035) |
Allen MAE 46.9 |
Within-distribution, fine-tuning is decisively better on both detection and counting. Out-of-distribution it already loses detection F1 while still improving counting. On the held-out TLS validation split it loses badly, and the per-image count slope turns negative (−0.309 on Allen), meaning predicted counts are anti-correlated with truth. The conclusion is that TreeFormer’s parameters adapt to the specific sources they are fine-tuned on rather than learning a transferable notion of tree density — it is a strong fine-tuning target and a weak generalizer. Applied users fine-tuning on local data should expect the within-distribution column; nobody should expect the validation column to follow from it.
The source-level breakdown says the same thing. Amirkolaee is the within-distribution source where fine-tuning moves both axes the way the split does (recall 0.31 → 0.61, precision 0.78 → 0.92, counting MAE 112 → 43). OFO is 1387 of the 3260 out-of-distribution test images and is what drives that split’s aggregate precision drop: the fine-tune finds nearly every tree (recall 0.95 → 0.98) while precision falls 0.44 → 0.18, which is over-prediction, not a detection failure. Allen carries the validation counting collapse (MAE 47 → 136).
Precision figures here are mask-aware: an unmatched prediction landing on tree-covered pixels is exempted rather than counted as a false positive, so precision swings image to image on closed-canopy sources and only the source-level aggregate is meaningful.



Submissions¶
Submit to the leaderboard¶
Once you have trained a model and evaluated its performance, you can submit your results to the MillionTrees leaderboard. Here’s how:
Create a public repository with your code and model training scripts. Make sure to include:
Clear instructions for reproducing your results
Requirements file listing all dependencies
Training configuration files/parameters
Code for data preprocessing and augmentation
Model architecture definition
Evaluation code
Generate predictions on the test split, pinning the dataset version explicitly:
dataset = TreeBoxesDataset(version="0.23") # pin the version; do not rely on the default test_dataset = dataset.get_subset("test") # Use test split test_loader = get_eval_loader("standard", test_dataset, batch_size=16) predictions = [] for metadata, images, _ in test_loader: pred = model(images) predictions.append(pred)
Report the run configuration. A submission is only comparable to the published rows if it states each of the following — these are exactly the columns of the Run configuration table below:
Field
Why it matters
Dataset version and whether unsupervised sources were included
Scores are not comparable across releases
Score threshold
The single largest lever on the recall/precision balance; standard is 0.10
Weights / checkpoint the score came from
Distinguishes a run from its neighbours in a sweep
Evaluation image size and tiling / inference mode
Changes effective object scale
Training hyperparameters: batch size, GPUs, learning rate, epochs, LR schedule, early stopping / checkpoint-selection criterion, augmentation
Needed to retrain
Random seed, or an explicit statement that none was set
Bounds how much of a difference is noise
Software environment (framework versions, any non-release branch)
Several published rows require specific branches
Submit a pull request to the MillionTrees repository with:
Link to your code repository
Model description and approach
Performance metrics on test set, with the configuration from step 3
Example prediction visualizations
Instructions for reproducing results
Benchmark Results¶
Fine-tuned models (trained on the MillionTrees train split) vs. pretrained models evaluated zero-shot, all on MillionTrees v0.23. All AP is AP40 (IoU 0.4), the same match threshold behind recall and mask-aware precision; F1 is their harmonic mean. Rows are generated from the result files by scripts/make_benchmark_table.py – see the run configuration table below for what produced each number.
Split: within-distribution¶
TreeBoxes¶
Model |
Fine-tuned |
DetectionRecall |
MaskAwarePrecision |
F1 |
AP40 |
CountingMAE |
|---|---|---|---|---|---|---|
CanopyRS DINO Swin-L |
✗ |
0.664 |
0.776 |
0.716 |
0.488 |
14.495 |
DeepForest (RetinaNet) |
✓ |
0.635 |
0.611 |
0.623 |
0.467 |
15.570 |
SAM3 |
✗ |
0.578 |
0.476 |
0.522 |
0.390 |
38.164 |
DeepForest (release weights) |
✗ |
0.388 |
0.598 |
0.471 |
0.241 |
11.440 |
TreePoints¶
Model |
Fine-tuned |
KeypointAccuracy |
MaskAwarePrecision |
F1 |
CountingMAE |
|---|---|---|---|---|---|
TreeFormer (count-loss fix) |
✓ |
0.779 |
0.786 |
0.782 |
19.993 |
TreeFormer (release weights) |
✗ |
0.741 |
0.711 |
0.726 |
118.665 |
SAM3 |
✗ |
0.679 |
0.599 |
0.636 |
43.537 |
TreePolygons¶
Model |
Fine-tuned |
MaskRecall |
MaskAwarePrecision |
F1 |
MaskAccuracy |
AP40 |
|---|---|---|---|---|---|---|
CanopyRS DINO + SAM3 (SelvaMask) |
✗ |
0.762 |
0.874 |
0.814 |
0.268 |
0.375 |
Mask R-CNN (Detectron2) |
✓ |
0.652 |
0.921 |
0.763 |
0.368 |
0.453 |
SAM3 |
✗ |
0.576 |
0.621 |
0.598 |
0.176 |
0.290 |
detectree2 |
✗ |
0.530 |
0.604 |
0.565 |
0.137 |
0.223 |
DeepForest Mask R-CNN |
✓ |
0.432 |
0.707 |
0.536 |
0.090 |
0.075 |
Split: out-of-distribution¶
TreeBoxes¶
Model |
Fine-tuned |
DetectionRecall |
MaskAwarePrecision |
F1 |
AP40 |
CountingMAE |
|---|---|---|---|---|---|---|
CanopyRS DINO Swin-L |
✗ |
0.799 |
0.865 |
0.831 |
0.662 |
13.792 |
DeepForest (RetinaNet) |
✓ |
0.644 |
0.759 |
0.697 |
0.480 |
18.060 |
SAM3 |
✗ |
0.725 |
0.581 |
0.645 |
0.503 |
40.154 |
DeepForest (release weights) |
✗ |
0.465 |
0.781 |
0.583 |
0.305 |
13.104 |
TreePoints¶
Model |
Fine-tuned |
KeypointAccuracy |
MaskAwarePrecision |
F1 |
CountingMAE |
|---|---|---|---|---|---|
TreeFormer (release weights) |
✗ |
0.763 |
0.737 |
0.750 |
66.771 |
TreeFormer (count-loss fix) |
✓ |
0.688 |
0.662 |
0.675 |
38.398 |
SAM3 |
✗ |
0.687 |
0.624 |
0.654 |
37.385 |
TreePolygons¶
Model |
Fine-tuned |
MaskRecall |
MaskAwarePrecision |
F1 |
MaskAccuracy |
AP40 |
|---|---|---|---|---|---|---|
CanopyRS DINO + SAM3 (SelvaMask) |
✗ |
0.819 |
0.861 |
0.839 |
0.291 |
0.417 |
Mask R-CNN (Detectron2) |
✓ |
0.555 |
0.904 |
0.688 |
0.331 |
0.393 |
detectree2 |
✗ |
0.504 |
0.633 |
0.561 |
0.170 |
0.253 |
SAM3 |
✗ |
0.465 |
0.668 |
0.548 |
0.171 |
0.249 |
DeepForest Mask R-CNN |
✓ |
0.498 |
0.561 |
0.528 |
0.078 |
0.120 |
Split: crossgeometry¶
TreePolygons¶
Model |
Fine-tuned |
MaskRecall |
MaskAwarePrecision |
F1 |
MaskAccuracy |
AP40 |
|---|---|---|---|---|---|---|
TreeFormer + SAM2 |
✗ |
0.464 |
0.653 |
0.543 |
0.179 |
0.211 |
Run configuration¶
Everything needed to reproduce a row beyond the dataset version. Score thresholds are the standardized 0.10 unless marked otherwise.
Model |
Geometry |
Fine-tuned |
Weights / checkpoint |
Score threshold |
Eval image size |
Environment |
Training configuration |
|---|---|---|---|---|---|---|---|
DeepForest (RetinaNet) |
TreeBoxes |
✓ |
trained from the DeepForest release backbone |
0.10 |
native tile |
shared |
|
CanopyRS DINO Swin-L |
TreeBoxes |
✗ |
CanopyRS DINO Swin-L release |
0.30 (per-model tuned; see |
CanopyRS default tiling |
|
not trained on MillionTrees |
DeepForest (release weights) |
TreeBoxes |
✗ |
DeepForest release weights |
0.10 |
native tile |
deepforest venv |
not trained on MillionTrees |
SAM3 |
TreeBoxes |
✗ |
|
0.10 |
SAM3 default |
sam3 venv |
not trained on MillionTrees |
TreeFormer (count-loss fix) |
TreePoints |
✓ |
|
0.10 ( |
896 |
frozen |
|
SAM3 |
TreePoints |
✗ |
|
0.10 |
SAM3 default |
sam3 venv |
not trained on MillionTrees |
TreeFormer (release weights) |
TreePoints |
✗ |
|
0.10 ( |
896 |
frozen |
not trained on MillionTrees |
DeepForest Mask R-CNN |
TreePolygons |
✓ |
torchvision Mask R-CNN, COCO init |
0.10 |
448, tiled stream eval |
shared |
|
Mask R-CNN (Detectron2) |
TreePolygons |
✓ |
COCO Mask R-CNN R50-FPN 3x; checkpoint |
0.15 ( |
448 ( |
CanopyRS uv venv (from-source Detectron2) |
|
CanopyRS DINO + SAM3 (SelvaMask) |
TreePolygons |
✗ |
CanopyRS DINO detector + SAM3 SelvaMask segmenter |
0.30 (per-model tuned) |
CanopyRS default tiling |
|
not trained on MillionTrees |
SAM3 |
TreePolygons |
✗ |
|
0.10 |
SAM3 default |
sam3 venv |
not trained on MillionTrees |
TreeFormer + SAM2 |
TreePolygons |
✗ |
TreeFormer points -> SAM2 mask prompting |
0.10 |
896 (points stage) |
frozen |
not trained on MillionTrees |
detectree2 |
TreePolygons |
✗ |
|
0.10 |
detectree2 default tiling |
detectree2 venv |
not trained on MillionTrees |