15/09, 2025

Automating habitat classifications from LUCAS (EU)

Context

  • LUCAS (EU-wide Land Use/Cover Area frame Survey) collects ground-based images (GBIs) every ~3 years since the early 2000s.
  • 240,000 stratified sample points on a 2-km grid; 5 photos per point (center + N/E/S/W).
  • In 2020, the database included >5.4 million geolocated photos and metadata with up to 106 variables, following a standardized, hierarchical nomenclature.
  • In 2025: 6 time-slices 2006, 2009, 2012, 2015, 2018, 2022

How it works

Why it matters

  • Labeled LUCAS GBIs form a high-quality training set for automated image recognition to reduce manual labeling.
  • Can accelerate ground-truthing for land-cover maps from airborne/remote sensing and extend to GBIs beyond LUCAS.
  • Probabilistic outputs from classifiers can feed species distribution models (SDMs) as proportions.

Model & training (Keras / TensorFlow)

  • Architecture: Xception base pretrained on ImageNet (include_top = FALSE), frozen conv layers; head = Global Average Pooling → Dense (1024, ReLU) → Dropout (0.2) → Dense (4, Softmax).
  • Input: 224×224 RGB, rescaled to [0,1] with image_data_generator(rescale = 1/255).
  • Loss: categorical cross‑entropy (multi‑class).
  • Optimizer: Adam (learning rate = 0.001).
  • Metric: accuracy.
  • Batch / epochs: batch = 32; epochs = 10; steps_per_epoch = floor(n_train / batch_size).
  • Validation: held‑out images via your split; report accuracy/κ on validation.
  • Note: Epoch = one full pass over training data. The “input ran out of data” warning appears if steps_per_epoch*epochs exceeds generator batches; reduce steps or use repeat(). ]

Current dataset

  • Train: 15,676
  • Test: 4,297

Pilot: CNN classification on LUCAS images (Denmark)

Setup

  • Balanced subset: 1,093 LUCAS images (Denmark).
  • Four habitat classes: croplands, grassland, artificial land use, woody vegetation.

Results

  • Overall accuracy: 0.734
  • Cohen’s κ: 0.650

Next steps

  • Improve via more training images, and majority voting across the five photos per point.

Results

Python translation

Using torch and sklearn

jupyter notebook

def build_model(num_classes: int, arch: str = "xception", dropout: float = 0.2, freeze_backbone: bool = True) -> nn.Module:
    # Try timm/xception
    backbone = None
    in_feats = None

    if arch.lower() == "xception":
        try:
            import timm
            backbone = timm.create_model("xception", pretrained=True, num_classes=0, global_pool="avg")
            in_feats = backbone.num_features
        except Exception as e:
            print(f"[warn] timm/xception not available ({e}). Falling back to torchvision resnet50.")

    if backbone is None:
        m = tvm.resnet50(weights=tvm.ResNet50_Weights.IMAGENET1K_V2)
        in_feats = m.fc.in_features
        backbone = nn.Sequential(*(list(m.children())[:-1]))  # (B, 2048, 1, 1)

    if freeze_backbone:
        for p in backbone.parameters():
            p.requires_grad = False

    head = nn.Sequential(
        nn.Flatten(),
        nn.Linear(in_feats, 1024),
        nn.ReLU(inplace=True),
        nn.Dropout(p=dropout),
        nn.Linear(1024, num_classes),
    )

    class Net(nn.Module):
        def __init__(self, backbone, head):
            super().__init__()
            self.backbone = backbone
            self.head = head
        def forward(self, x):
            if hasattr(self.backbone, "forward_features"):  # timm models
                feats = self.backbone.forward_features(x)
                if feats.ndim == 4:
                    feats = feats.mean(dim=(-2, -1))  # GAP
            else:
                feats = self.backbone(x)
            return self.head(feats)

    model = Net(backbone, head).to(DEVICE)
    return model

Interactive test

The code (not the data in github)