BAMBI Species Classifiers

Three heads that classify red deer, roe deer and wild boar in nadir UAV imagery from frozen DINOv3 ViT-H+ features: one for thermal, one for RGB, and one for the two together.

They are the species counterpart to cpraschl/bambi-occlusion-classifiers and share its interface exactly, so code that consumes one can consume the other by swapping the file. Both are built for the BAMBI dataset.

Files

directory projection render
non_geo/ native camera perspective (raw video frames) 1024
geo_1k/ orthographic, re-projected onto the terrain DEM 1024
geo_2k/ orthographic, same projection at twice the resolution 2048
alfs_1k/ airborne light-field sampling, 31-shot synthetic aperture 1024
alfs_2k/ the same integration at twice the resolution 2048

Each holds species_thermal.pt (1280-d in), species_rgb.pt (1280-d) and species_matched.pt (2560-d). The projections are not interchangeable: a head expects the feature distribution of the projection it was trained on. No ALFS variant is included.

Usage

The models consume raw DINOv3 ViT-H+ CLS features, not images.

import torch

m = torch.jit.load("non_geo/species_matched.pt")
emb, probs = m(feats)          # feats: (N, 2560) float32, [RGB | thermal]
# emb   : (N, 256)  L2-normalised embedding
# probs : (N, 3)    in the order given by m.classes
print(m.classes)               # ['red_deer', 'roe_deer', 'wild_boar']

Exported attributes: classes, emb_dim (256), in_dim, modality, projection, reduction.

To produce the features, crop the animal square around the box centre at 1.5x the longer side, resize to 224, normalise with ImageNet statistics, and take the CLS token of DINOv3 ViT-H+. Standardisation of the features is inside the model; feed the raw CLS tokens.

Architecture

features -> standardize(mu, sigma) -> Linear(D, 512) -> ReLU
         -> Linear(512, 256) -> L2-normalize -> linear classifier -> softmax

The projection is trained with batch-hard triplet loss on L2 distance; the classifier is then fitted on the frozen 256-d embedding. Batch norm is folded into the preceding linear layer after training, so inference is deterministic without requiring .eval(). The matched model concatenates the two feature vectors before the projection, so the head learns a joint representation rather than combining two separate decisions.

This follows Markoff et al., When One Modality Is Not Enough (arXiv:2608.06973), which used the same construction for sex and life-stage on red deer.

Results

5-fold cross-validation, grouped by flight, on key frames that the matching occlusion head calls clear.

non_geo (perspective): 36,091 boxes, 2,838 tracks, 244 flights.

frame accuracy balanced accuracy per-track (majority vote)
thermal 0.771 ± 0.019 0.742 0.714 ± 0.020
RGB 0.886 ± 0.040 0.881 0.877 ± 0.058
matched 0.912 ± 0.025 0.904 0.889 ± 0.034

geo_1k (orthographic, 1024): 31,730 boxes, 2,792 tracks, 230 flights.

frame accuracy balanced accuracy per-track (majority vote)
thermal 0.776 ± 0.030 0.726 0.754 ± 0.060
RGB 0.848 ± 0.028 0.805 0.869 ± 0.038
matched 0.866 ± 0.052 0.809 0.869 ± 0.054

geo_2k (orthographic, 2048): 32,899 boxes, 2,667 tracks, 231 flights.

frame accuracy balanced accuracy per-track (majority vote)
thermal 0.785 ± 0.042 0.752 0.748 ± 0.067
RGB 0.869 ± 0.042 0.849 0.870 ± 0.030
matched 0.881 ± 0.030 0.854 0.909 ± 0.035

Note the per-track column: geo_2k reaches 0.909, above non_geo's 0.889. Per frame the perspective heads stay ahead, but once a track is resolved by a majority vote over its frames, the orthographic 2k head is the strongest of the three. For geo-referenced tracking, which works in projected space anyway, that is the number that matters.

Fusion beats either sensor alone in perspective (+2.3 points over RGB); in the orthographic view it adds almost nothing (+0.4). Thermal is the weaker modality throughout, which is expected -- species identity lives in texture and outline, and thermal renders an animal as a warm blob.

Confusion for matched (rows = truth, columns = predicted):

perspective red deer roe deer wild boar orthographic red deer roe deer wild boar
red deer 92% 2% 6% red deer 90% 3% 7%
roe deer 9% 87% 3% roe deer 21% 63% 16%
wild boar 7% 1% 92% wild boar 7% 3% 90%

alfs_1k / alfs_2k (light-field integration, unfiltered): 61,516 and 61,536 boxes, 3,772 tracks, 244 flights.

thermal RGB matched per-track
alfs_1k 0.679 0.766 0.781 0.889
alfs_2k 0.706 0.788 0.794 0.893

These are the weakest heads per frame, and the reason is inherent to the method rather than fixable by rendering harder. The synthetic aperture spans ±45 frames at stride 3, roughly three seconds. Static ground sharpens and canopy blurs away, which is the point -- but a moving animal smears across its own track, and species identity lives in the outline and texture the smear destroys. Doubling the render buys +1.3 points, about what it buys the orthographic arm, so resolution is not what limits them.

Where the ALFS heads are the right choice

On frames a perspective occlusion head calls occluded -- the material light-field integration exists for -- the ordering changes:

occluded frames only, matched balanced per-track
non_geo 0.779 0.848
geo_2k 0.736 0.879
alfs_2k 0.734 0.863
alfs_1k 0.725 0.880
geo_1k 0.717 0.836

alfs_1k has the best per-track accuracy of any head on this subset, above perspective. The same occlusion head also flags only 37-38% of ALFS frames as occluded, against 45% of perspective and 50% of orthographic ones, so the integration measurably reduces apparent occlusion rather than merely being robust to it.

So: perspective for general use, ALFS for canopy-heavy material where the animal is intermittently hidden and a track-level answer is what is wanted.

With and without occlusion filtering

Every table above filters frames through the matching head of bambi-occlusion-classifiers and keeps only those it calls clear, which is roughly half of them. Without that step the same heads, trained and evaluated on every frame, score materially lower per frame:

matched, balanced accuracy filtered unfiltered boxes filtered / unfiltered
non_geo 0.904 0.816 36,091 / 65,394
geo_1k 0.809 0.794 31,730 / 61,024
geo_2k 0.854 0.813 32,899 / 61,512
alfs_1k 0.769 0.781 39,422 / 61,516
alfs_2k 0.818 0.794 39,464 / 61,536

Filtering is worth +8.8 points to the perspective arm. It is worth almost nothing to ALFS -- 1.2 points down at 1k, 2.4 up at 2k, both inside the fold spread. That is why the ALFS heads are released trained on unfiltered frames: the filter is not part of their intended pipeline, and light-field integration has already done the job it would do.

Per modality, unfiltered:

thermal RGB matched
non_geo 0.689 0.816 0.816
geo_1k 0.649 0.775 0.794
geo_2k 0.679 0.807 0.813

Filtering is worth most to the perspective arm (+8.8 points) and least to geo_1k (+1.5), and it costs thermal the most to go without -- an occluded animal in thermal is a partial warm blob, which is exactly the case the modality has least margin for.

The per-track picture is different, and worth knowing before switching the filter on. Majority voting over a track benefits from having more frames to vote with, so filtering can cost per-track accuracy even while it improves per-frame accuracy:

matched, per-track filtered unfiltered
non_geo 0.889 0.874
geo_1k 0.869 0.887
geo_2k 0.909 0.868

So filter when labelling individual frames; for track-level decisions on geo_1k, keeping every frame is the better choice.

Why the orthographic heads score lower

Partly resolution, and partly the projection itself. A 70 m ortho window over 1024 px is 6.8 cm/px, which leaves the animal smaller than in the source frame; doubling to 2048 more than reverses that:

median longer box side perspective ortho 1k ortho 2k
red deer 50 px 33 px 65 px
roe deer 43 px 26 px 51 px
wild boar 46 px 27 px 53 px

Rendering at 2048 recovers about half the deficit -- matched balanced accuracy 0.809 -> 0.854 against perspective's 0.904 -- and roe deer, the smallest and weakest class, recovers most: 63% correct at 1k, 77% at 2k, against 87% in perspective. So undersampling was real and worth fixing.

But it is not the whole story. At 2048 the ortho crops are larger in pixels than the perspective ones and still score five points lower. Those extra pixels are resampled from the same source frames, so they carry no new detail: the render can only redistribute the sampling the sensor already made, while adding interpolation blur and whatever error the DEM and pose carry. Rendering beyond 2048 should not be expected to close the remaining gap.

Read the split protocol before comparing numbers

97.4% of BAMBI flights contain exactly one of the three species, so flight identity is very nearly a perfect proxy for the label. Splitting by track leaves other tracks from the same flight in training, and a head can then score well by recognising the session -- its terrain, illumination and thermal calibration -- without learning anything about animals:

grouping matched, balanced accuracy
by track 0.967
by flight 0.904

The flight-grouped number is the one that means something. Track grouping is appropriate for the paper's sex and life-stage tasks, where a single flight contains both classes; it is not appropriate for species.

Interpolated labels were not used

BAMBI is annotated on key frames. Densifying with mot_interpolation.py (step 5, gaps capped at 30 frames) more than doubles the data and makes results worse under flight grouping:

training rows matched, balanced accuracy
key frames only (36,091 boxes) 0.904
key + interpolated (85,069 boxes) 0.872

Under track grouping the two are indistinguishable (0.967 vs 0.969), which is the tell: interpolated boxes are near-duplicates that inflate a track-grouped score while adding drift-induced label noise that only shows up on unseen flights. These models are trained on key frames only.

Training data

266 flights of the BAMBI base release containing the three species: 1,605 red deer, 646 roe deer and 1,810 wild boar tracks. Species labels come from each flight's own <flight>_gt.txt, which carries the species name in plain text.

Thermal boxes are the released annotations. RGB boxes are not -- the two modalities are not spatially aligned, so each thermal box was transferred onto the RGB frame by detecting the animals with OWL and matching detections to box centres with the Hungarian algorithm (transfer_labels.py in the dataset repository). That transfer agrees with the human-accepted corrections of the matched release to a mean centre error of 4.31 px, with 95.9% of boxes at IoU > 0.5.

Frames were filtered by the matching head of bambi-occlusion-classifiers -- non_geo/occlusion_matched.pt for the perspective arm (keeps 55%), and the geo_1k and geo_2k heads for the orthographic ones (52% and 54%). Each set of released weights is retrained on all of its flights; the tables above are the cross-validated estimate of their accuracy, not a measurement of these particular weights.

Orthographic renders were produced with alfs_pytorch at a 70 m window, 1024 px (geo_1k) and 2048 px (geo_2k), projected onto the per-flight terrain DEM. Three flights are absent from the orthographic arm: two whose DEM files are corrupt, and one whose box corners ray-cast onto the DEM tens of metres apart instead of two or three.

Limitations

  • Feature-space locked to DINOv3 ViT-H+. A different backbone, or a different crop convention, will silently degrade accuracy.
  • Projection-specific. The five projections are not interchangeable; each head expects the feature distribution of its own render.
  • The ALFS heads are the weakest per frame and are not a general-purpose choice. They are for canopy-heavy material where the animal is intermittently hidden, and they want unfiltered input.
  • Per frame the perspective heads are the strongest; per track, geo_2k is. Prefer non_geo for frame-level decisions on native imagery, geo_2k for pipelines already working in projected space. geo_1k is kept for continuity with the occlusion release but is superseded by geo_2k at the same inference cost.
  • The matched head needs genuinely co-registered pairs. Feeding it an RGB box that was not transferred onto the animal will hurt more than using the RGB head alone.
  • Three species only. Anything else -- fallow deer, chamois, ibex, birds, humans -- will be forced into one of the three. Fallow deer is the largest excluded class in BAMBI (23,636 boxes) and is a plausible source of confident errors.
  • Roe deer is the weakest class at 87%, and the smallest in the training data (5,218 boxes against 17,038 and 13,835).
  • Balanced accuracy near 0.9 suits triage and pre-labelling, not unreviewed population statistics.

Licence

The heads are MIT. The DINOv3 backbone carries Meta's separate licence terms; verify compliance independently.

Provenance

The species models built for the paper below were unfortunately lost, so these were retrained from scratch on the same data with the same recipe -- frozen DINOv3 ViT-H+ features, a batch-hard triplet projection to 256-d, then a linear classifier.

The retraining was checked against the one component that did survive. Rebuilt with this code, the occlusion heads reproduce the released bambi-occlusion-classifiers closely across all three projections:

occlusion, matched, balanced accuracy retrained here released
non_geo 0.836 0.845
geo_1k 0.810 0.812
geo_2k 0.810 0.804

Within about a point in each case, with the same ordering across modalities, so the pipeline behind these species heads is faithful to the original.

Citation

@misc{markoff2026multimodal,
  title  = {When One Modality Is Not Enough: Multimodal Sex and Life-Stage
            Classification of Red Deer from Aerial RGB-Thermal Video},
  author = {Markoff, Hugo and Praschl, Christoph and Ludo{\v{s}}ki, Ivan and
            Beery, Sara and {\O}rsted, Michael and Schedl, David C.},
  year   = {2026},
  eprint = {2608.06973},
  archivePrefix = {arXiv}
}
Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Collection including cpraschl/bambi-species-classification

Paper for cpraschl/bambi-species-classification