A full-stack system that detects wildfires from satellite imagery using province-specific CNNs, and delivers location-filtered alerts to subscribers within minutes of image capture.
Course: ENG 4000 — Capstone Design Project, York University (Lassonde), 2024–2025 · Grade: A+
Team (Group 12): Eyinojuoluwa Akin-Salami · Kellan Ho · Angelique Izere · Noran Kerret · Parmoun Khalkhali Sharifi · Sara Riazi
Note: this repository is a fork of the team repo. See My Contributions for what I personally built.
This was a 6-person capstone. My ownership was the CNN training pipeline and the image preprocessing workflow.
Specifically, I built:
- The training pipeline — dataset loading with per-class subsampling to correct fire/no-fire imbalance, a two-phase schedule (initial training, then fine-tuning at a lower learning rate) with
EarlyStoppingandReduceLROnPlateaucallbacks. - The model architecture — four convolutional blocks with batch normalization and ReLU, followed by two dense layers, with L1+L2 regularization and dropout for overfitting control.
- The loss and optimization strategy — a custom focal loss to handle class imbalance directly rather than relying on resampling alone, optimized with Adam, tracking AUC / precision / recall / accuracy.
- Threshold tuning — sweeping confidence thresholds against validation predictions and selecting the operating point on balanced accuracy between the fire and no-fire classes, rather than defaulting to 0.5.
- The preprocessing workflow — resizing, normalization to [0, 1], 64×64 patch segmentation, and augmentation (random flips, brightness, contrast).
Backend alert logic, the Flask API, the dashboard, and the mapping layer were built by teammates.
Traditional wildfire detection relies on ground sensors, aerial patrols, and manual observation — methods that fail in remote areas and create dangerous delays. EFDS replaces this with a satellite-fed CNN pipeline:
- Satellite imagery is fetched from NASA GIBS (MODIS/VIIRS) via WMS tile requests for a given province and date
- Preprocessing resizes, normalizes, and segments each image into 64×64 patches
- A province-specific CNN classifies each patch as fire / no-fire with a confidence score
- Fire patches above threshold are mapped to geographic coordinates and written to CSV
- The backend computes Haversine distance to every registered user and emails those within a 15 km radius, suppressing duplicates within 24 hours
- A dashboard renders detections on an interactive Folium map
The detection job runs on a scheduler every 10 minutes, or on demand via /trigger-fetch.
Target users: government fire agencies, environmental organizations, communities in fire-prone regions.
NASA GIBS (MODIS / VIIRS)
│
▼
┌──────────────────┐
│ Preprocessing │ ← resize, normalize [0,1], 64×64 patch segmentation
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Province CNN │ ← 4 conv blocks (BatchNorm + ReLU) → 2 dense layers
│ (Keras / TF) │ focal loss · L1+L2 · dropout · tuned threshold
└────────┬─────────┘
│ fire patches → (lat, lon, confidence) CSV
▼
┌──────────────────────────────────────────┐
│ Flask Backend │
│ - Haversine distance to each subscriber │
│ - 15 km alert radius │
│ - Flask-Mail email, 24 h suppression │
│ - Folium map render → fire_map.html │
└───────────┬──────────────────────────────┘
│
┌──────────▼───────────┐
│ Dashboard (iframe) │
│ live map + feed │
└──────────────────────┘
A custom CNN trained from scratch, one model per province:
Input: 64×64 RGB patch (from satellite imagery)
│
├─ 4 × [ Conv2D → BatchNorm → ReLU → MaxPool ]
├─ Flatten
├─ Dense → Dropout (L1 + L2 regularization)
└─ Dense(1, Sigmoid) → fire probability [0, 1]
Training: custom focal loss (class imbalance), Adam optimizer, two-phase schedule with fine-tuning at reduced learning rate, EarlyStopping + ReduceLROnPlateau. Metrics tracked: AUC, precision, recall, accuracy.
Threshold selection: rather than defaulting to 0.5, a range of confidence thresholds was swept against validation predictions and the operating point chosen for balanced accuracy across both classes. The selected threshold ships alongside the model in JSON.
In the Fall semester we trained a single Canada-wide model on 128×128 patches. It failed to capture local features — fire signatures vary meaningfully between provinces, and a national model averaged those differences away. In the Winter semester we cut patch size to 64×64 and moved to per-province models. Both changes improved spatial resolution and accuracy.
- Source: NASA GIBS satellite imagery (MODIS/VIIRS), labelled fire / no-fire per province
- Balancing: per-class subsampling to prevent convergence bias
- Preprocessing: 64×64 patch extraction, normalization, flip / brightness / contrast augmentation
Validated on a corpus of 115,214 samples.
| Class | Precision | Recall | F1 | Support |
|---|---|---|---|---|
| No Fire | 92% | 90% | 0.91 | 72,089 |
| Fire | 84% | 87% | 0.85 | 43,125 |
| Weighted avg | 89% | 89% | 0.89 | 115,214 |
| Metric | Target | Achieved |
|---|---|---|
| Classification accuracy | ≥ 85.00% | 88.74% |
| Geospatial alert precision | 15.00 km radius | 15.00 km radius |
| End-to-end detection latency | — | 1.968 – 2.289 s |
| Memory usage | — | 56.899 MiB |
Accuracy exceeded the 85% project requirement. Fire-class recall (87%) was deliberately prioritized over precision (84%) — a false positive is a nuisance, a missed fire is a catastrophe — and the confidence threshold was tuned accordingly.
pip install tensorflow keras flask numpy pillow opencv-python foliumfrom tensorflow.keras.models import load_model
from PIL import Image
import numpy as np
model = load_model('<province_model>.h5')
def predict(image_path, threshold):
img = Image.open(image_path).resize((64, 64))
arr = np.expand_dims(np.array(img) / 255.0, axis=0)
prob = model.predict(arr)[0][0]
print(f"{'FIRE' if prob > threshold else 'NO FIRE'} (confidence: {prob:.2%})")cd Backend/
pip install -r requirements.txt
python app.py # → http://localhost:5000Endpoints: POST /detect · GET /alerts · POST /subscribe · POST /trigger-fetch
| Layer | Tool / Service |
|---|---|
| ML Framework | TensorFlow / Keras |
| Data source | NASA GIBS (MODIS, VIIRS) |
| Preprocessing | Python (NumPy, OpenCV, PIL) |
| Backend | Flask, Flask-Mail |
| Frontend | HTML / CSS · Folium (Leaflet.js) |
| Versioning | Git / GitHub |
Patch size is a real design parameter, not a default. Our Fall prototype used 128×128 patches and could not resolve local fire features. Dropping to 64×64 in the Winter semester measurably improved detection of finer-grained patterns. Full-image analysis was rejected outright — it carried excessive processing time, higher memory requirements, poor parallelization, and worse precision on small fires.
Regional variation broke the single-model assumption. A Canada-wide model underperformed because fire patterns differ across provinces. Splitting into province-specific models fixed it, at the cost of a training pipeline that has to produce and version many models instead of one.
Class imbalance needed more than resampling. With roughly 72k no-fire to 43k fire samples, plain resampling still biased the model. Focal loss, combined with subsampling and a tuned decision threshold, was what actually moved fire-class recall.
Recall over precision, deliberately. The costs of the two error types are not symmetric. We chose the operating point on balanced accuracy rather than raw accuracy, and accepted lower fire precision to keep recall high.
Flask is enough for an MVP, not for production. Synchronous endpoints will not survive real concurrent load; async inference behind a queue and a proper API gateway would be the next step.