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) vsSeca(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
Inference Providers NEW
This model isn't deployed by any Inference Provider. π Ask for provider support