QuercusHealth AI β€” DeepForest Dehesa Fine-Tuned

Domain-adapted tree crown detector and La Seca disease classifier for Quercus ilex (Holm Oak) in the Spanish Dehesa, using aerial satellite imagery.

GitHub: sergioillescascabiro/QuercusHealth-Public
Paper: Available in the repository at report/main.pdf


Models in this Repository

This repo contains two complementary models from a 5-phase pipeline:

File Architecture Phase Task
deepforest_dehesa_finetuned.pt RetinaNet + ResNet-50 (32M params) 3 2-class tree crown detection (Healthy / Seca)
stage2_classifier.pt ResNet-18 (11M params) 4/5 Crop-level health classification (Healthy / Seca)

Phase 3: Fine-Tuned Detector

Description

  • Base: weecology/deepforest-tree (pre-trained on NEON North American forests)
  • Domain: Spanish Dehesa (Mediterranean holm oak savanna, Extremadura)
  • Task: 2-class tree crown detection β€” Healthy (0) vs Seca (1)
  • Training: 30 epochs, LR=1e-4, batch=4, RTX 3060 12GB, seed=42

Performance

Metric Zero-shot baseline Fine-tuned (Phase 3)
F1 Overall 0.320 0.669
F1 Healthy β€” 0.914
F1 Seca 0.000 0.000
Precision 0.510 0.610
Recall 0.230 0.742

Note on Seca F1 = 0.000: Class imbalance (87.9% Healthy / 12.1% Seca) prevented the 2-class head from learning Seca detections. The two-stage pipeline (Phase 4/5) addresses this.

Usage

from deepforest import main as df_main
from huggingface_hub import hf_hub_download
import torch
import os
os.environ['CURL_CA_BUNDLE'] = ''  # if needed for SSL

weights_path = hf_hub_download(
    repo_id="sillescas/deepforest-dehesa-quercus",
    filename="deepforest_dehesa_finetuned.pt"
)

model = df_main.deepforest(config_args={"num_classes": 2})
model.load_state_dict(torch.load(weights_path, map_location="cpu"))
model.label_dict = {"Healthy": 0, "Seca": 1}
model.eval()

predictions = model.predict_image(path="your_dehesa_tile.jpg")
print(predictions)

Phase 4/5: Stage 2 Crop Classifier

Description

  • Architecture: ResNet-18, ImageNet pre-trained, 2-class head (Healthy / Seca)
  • Input: 96Γ—96 px crops extracted from detected tree crowns
  • Training: 5-epoch frozen warmup + 25-epoch fine-tuning, LR=3e-4, batch=64
  • Class imbalance handling: WeightedRandomSampler + class-weighted CrossEntropyLoss

Performance

Metric Phase 3 (detector only) Phase 4 (GT crops) Phase 5 (end-to-end)
F1 Seca 0.000 0.354 0.346
F1 Overall 0.669 0.712 0.636

Usage

from huggingface_hub import hf_hub_download
import torch
import torchvision.models as models
import torchvision.transforms as T
from PIL import Image
import os
os.environ['CURL_CA_BUNDLE'] = ''  # if needed for SSL

# Download classifier
weights_path = hf_hub_download(
    repo_id="sillescas/deepforest-dehesa-quercus",
    filename="stage2_classifier.pt"
)

# Load ResNet-18 classifier
model = models.resnet18(weights=None)
model.fc = torch.nn.Linear(model.fc.in_features, 2)
model.load_state_dict(torch.load(weights_path, map_location="cpu"))
model.eval()

CLASSES = ["Healthy", "Seca"]
transform = T.Compose([
    T.Resize((96, 96)),
    T.ToTensor(),
    T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])

def classify_crop(image_path):
    img = Image.open(image_path).convert("RGB")
    x = transform(img).unsqueeze(0)
    with torch.no_grad():
        logits = model(x)
    return CLASSES[logits.argmax(1).item()]

print(classify_crop("your_crop.jpg"))

Two-Stage End-to-End Pipeline

# Combine Phase 3 detector + Phase 4 classifier for full pipeline
from deepforest import main as df_main
from huggingface_hub import hf_hub_download
import torch, torchvision.models as models, torchvision.transforms as T
from PIL import Image
import os
os.environ['CURL_CA_BUNDLE'] = ''

# Load detector (Phase 3)
det_path = hf_hub_download("sillescas/deepforest-dehesa-quercus", "deepforest_dehesa_finetuned.pt")
detector = df_main.deepforest(config_args={"num_classes": 1})
detector.load_state_dict(torch.load(det_path, map_location="cpu"))
detector.eval()

# Load classifier (Phase 4)
cls_path = hf_hub_download("sillescas/deepforest-dehesa-quercus", "stage2_classifier.pt")
classifier = models.resnet18(weights=None)
classifier.fc = torch.nn.Linear(classifier.fc.in_features, 2)
classifier.load_state_dict(torch.load(cls_path, map_location="cpu"))
classifier.eval()

CLASSES = ["Healthy", "Seca"]
transform = T.Compose([T.Resize((96, 96)), T.ToTensor(),
    T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])

# Run on a tile
image_path = "your_dehesa_tile.jpg"
boxes = detector.predict_image(path=image_path, return_plot=False)
img = Image.open(image_path).convert("RGB")

results = []
for _, row in boxes.iterrows():
    crop = img.crop((int(row.xmin), int(row.ymin), int(row.xmax), int(row.ymax)))
    label = CLASSES[classifier(transform(crop).unsqueeze(0)).argmax(1).item()]
    results.append({**row.to_dict(), "health": label})

print(f"Detected {len(results)} trees: "
      f"{sum(r['health']=='Healthy' for r in results)} Healthy, "
      f"{sum(r['health']=='Seca' for r in results)} Seca")

Dataset

  • Source: Roboflow β€” quercushealth-dehesa-summer2019
  • Train: 243 images (6,449 Healthy + 892 Seca annotations, after 3Γ— augmentation)
  • Val: 18 images
  • Test: 17 images
  • Imagery: Google Earth Pro, ~0.20 m/px, summer 2019, La Dehesa de la Villa (Madrid)
  • Ground truth: Multi-temporal validation (Summer 2019 symptoms β†’ Feb 2024 confirmed dead)

Training Details

  • Domain shift from NEON (USA, temperate conifer forest) to Dehesa (Spain, Mediterranean savanna): KS-test D=0.86, p<0.0001
  • Ablation: LR=1e-2 causes catastrophic forgetting (F1β†’0.000); LR=1e-4 is critical
  • Focal Loss (Ξ±=0.25) for class imbalance in Phase 3
  • WeightedRandomSampler + weighted CrossEntropy for Phase 4 classifier

Citation

Illescas Cabiro, S. (2026). QuercusHealth AI: Automated Detection and Health
Classification of Quercus ilex in the Spanish Dehesa via Domain-Adapted Aerial
Imagery. Illinois Institute of Technology, ITMD-524 Applied AI & Deep Learning.
GitHub: https://github.com/sergioillescascabiro/QuercusHealth-Public
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