Spaces:
Running on Zero
Running on Zero
Kinzaaa commited on
Commit Β·
3fd6082
0
Parent(s):
Initial deployment: Attention UNet + ZoeDepth + Grad-CAM + Risk Assessment
Browse files- .gitattributes +3 -0
- README.md +315 -0
- app.py +364 -0
- app/__init__.py +6 -0
- app/main.py +105 -0
- app/model_utils.py +501 -0
- models/flood_best_model_export.keras +3 -0
- models/flood_model_production.pkl +3 -0
- requirements.txt +8 -0
.gitattributes
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.keras filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Flood Risk Detection
|
| 3 |
+
emoji: π
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: red
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: "6.14.0"
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: true
|
| 10 |
+
license: mit
|
| 11 |
+
short_description: Attention UNet flood segmentation + ZoeDepth + risk assessment
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
# π Flood Detection & Depth-Aware Risk Assessment
|
| 15 |
+
|
| 16 |
+
An end-to-end deep learning pipeline for flood detection, explainability, depth estimation, and risk scoring from satellite/aerial imagery.
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
## ποΈ Project Structure
|
| 21 |
+
|
| 22 |
+
```
|
| 23 |
+
βββ app.py β Gradio UI (HF Spaces entry point)
|
| 24 |
+
βββ app/
|
| 25 |
+
β βββ __init__.py
|
| 26 |
+
β βββ main.py β FastAPI REST backend
|
| 27 |
+
β βββ model_utils.py β Core engine (model, Grad-CAM, ZoeDepth, risk)
|
| 28 |
+
βββ models/
|
| 29 |
+
β βββ flood_best_model_export.keras β Attention UNet weights
|
| 30 |
+
β βββ flood_model_production.pkl β Metadata (thresholds, metrics, layer names)
|
| 31 |
+
βββ requirements.txt
|
| 32 |
+
βββ run_all.py β Local launcher (FastAPI + Gradio together)
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
---
|
| 36 |
+
|
| 37 |
+
## βοΈ Full Pipeline
|
| 38 |
+
|
| 39 |
+
```
|
| 40 |
+
Input Image
|
| 41 |
+
β
|
| 42 |
+
βΌ
|
| 43 |
+
1. Preprocessing
|
| 44 |
+
ββ Convert to RGB β resize 512Γ512 β normalise [0,1]
|
| 45 |
+
β
|
| 46 |
+
βΌ
|
| 47 |
+
2. Attention UNet (flood_best_model_export.keras)
|
| 48 |
+
ββ Predicts per-pixel flood probability map (512Γ512)
|
| 49 |
+
ββ OOD detection: colour check + spatial coherence + coverage cap
|
| 50 |
+
ββ Threshold 0.5 β binary mask (1=flooded, 0=dry)
|
| 51 |
+
β
|
| 52 |
+
βΌ
|
| 53 |
+
3. Grad-CAM (layer: conv2d_130)
|
| 54 |
+
ββ GradientTape β gradients w.r.t. last conv feature maps
|
| 55 |
+
ββ Weighted sum β activation map β JET colormap overlay
|
| 56 |
+
β
|
| 57 |
+
βΌ
|
| 58 |
+
4. ZoeDepth (Intel/zoedepth-nyu, CPU)
|
| 59 |
+
ββ Monocular depth estimation β relative depth map
|
| 60 |
+
ββ Normalise to [0,3m] β stats within flooded pixels only
|
| 61 |
+
β
|
| 62 |
+
βΌ
|
| 63 |
+
5. Risk Assessment
|
| 64 |
+
ββ flood_pct + avg_depth_m β Low / Moderate / High / Critical
|
| 65 |
+
ββ Score = flood_pctΓ0.7 + depth_scoreΓ0.3
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
---
|
| 69 |
+
|
| 70 |
+
## π Deploy to Hugging Face Spaces
|
| 71 |
+
|
| 72 |
+
### Step 1 β Clone your Space
|
| 73 |
+
```bash
|
| 74 |
+
git clone https://huggingface.co/spaces/YOUR_USERNAME/flood-risk-detection
|
| 75 |
+
cd flood-risk-detection
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
### Step 2 β Set up Git LFS (for large model files)
|
| 79 |
+
```bash
|
| 80 |
+
git lfs install
|
| 81 |
+
git lfs track "*.keras" "*.pkl"
|
| 82 |
+
git add .gitattributes
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
### Step 3 β Copy project files
|
| 86 |
+
Copy these into the cloned folder:
|
| 87 |
+
```
|
| 88 |
+
app.py
|
| 89 |
+
app/__init__.py
|
| 90 |
+
app/model_utils.py
|
| 91 |
+
models/flood_best_model_export.keras
|
| 92 |
+
models/flood_model_production.pkl
|
| 93 |
+
requirements.txt
|
| 94 |
+
README.md
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
### Step 4 β Add HF_TOKEN as a Secret
|
| 98 |
+
In your Space β **Settings β Variables and Secrets β New Secret**:
|
| 99 |
+
```
|
| 100 |
+
Name: HF_TOKEN
|
| 101 |
+
Value: your_huggingface_token
|
| 102 |
+
```
|
| 103 |
+
This is used by ZoeDepth to download model weights from the Hub.
|
| 104 |
+
|
| 105 |
+
### Step 5 β Push
|
| 106 |
+
```bash
|
| 107 |
+
git add .
|
| 108 |
+
git commit -m "Deploy flood detection app"
|
| 109 |
+
git push
|
| 110 |
+
```
|
| 111 |
+
HF Spaces will automatically install `requirements.txt` and launch `app.py`.
|
| 112 |
+
|
| 113 |
+
> **Note:** First inference is slow (~60s) β ZoeDepth downloads ~1.4GB weights on first run, then caches them.
|
| 114 |
+
|
| 115 |
+
---
|
| 116 |
+
|
| 117 |
+
## π» Run Locally
|
| 118 |
+
|
| 119 |
+
```bash
|
| 120 |
+
# Install dependencies
|
| 121 |
+
pip install -r requirements.txt
|
| 122 |
+
|
| 123 |
+
# Set environment variables
|
| 124 |
+
set HF_TOKEN=your_token # Windows CMD
|
| 125 |
+
$env:HF_TOKEN="your_token" # PowerShell
|
| 126 |
+
|
| 127 |
+
# Start both servers
|
| 128 |
+
python run_all.py
|
| 129 |
+
|
| 130 |
+
# OR individually:
|
| 131 |
+
python app.py # Gradio β http://localhost:7860
|
| 132 |
+
uvicorn app.main:app --reload --port 8000 # FastAPI β http://localhost:8000/docs
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
---
|
| 136 |
+
|
| 137 |
+
## π‘ API Reference
|
| 138 |
+
|
| 139 |
+
### `POST /predict`
|
| 140 |
+
```bash
|
| 141 |
+
curl -X POST http://localhost:8000/predict \
|
| 142 |
+
-F "file=@flood_image.jpg"
|
| 143 |
+
```
|
| 144 |
+
Response:
|
| 145 |
+
```json
|
| 146 |
+
{
|
| 147 |
+
"mask_b64": "<base64 PNG β binary flood mask>",
|
| 148 |
+
"overlay_b64": "<base64 PNG β blue flood overlay>",
|
| 149 |
+
"gradcam_b64": "<base64 PNG β JET heatmap>",
|
| 150 |
+
"depth_map_b64": "<base64 PNG β PLASMA depth map>",
|
| 151 |
+
"depth_flood_b64": "<base64 PNG β depth in flood region>",
|
| 152 |
+
"depth_info": {
|
| 153 |
+
"avg_depth_m": 0.82,
|
| 154 |
+
"max_depth_m": 1.45,
|
| 155 |
+
"depth_category": "Moderate (30 cm β 80 cm)"
|
| 156 |
+
},
|
| 157 |
+
"risk": {
|
| 158 |
+
"risk_level": "High",
|
| 159 |
+
"risk_score": 43.5,
|
| 160 |
+
"flood_pct": 38.4,
|
| 161 |
+
"avg_depth_m": 0.82,
|
| 162 |
+
"recommendations": ["..."]
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
```
|
| 166 |
+
|
| 167 |
+
### `GET /health`
|
| 168 |
+
```json
|
| 169 |
+
{"status": "ok"}
|
| 170 |
+
```
|
| 171 |
+
|
| 172 |
+
---
|
| 173 |
+
|
| 174 |
+
## π Model Performance
|
| 175 |
+
|
| 176 |
+
| Model | Pixel Accuracy | IoU (Jaccard) | Dice / F1 | Precision | Recall |
|
| 177 |
+
|-------|---------------|---------------|-----------|-----------|--------|
|
| 178 |
+
| UNet (baseline) | 82.55% | 63.94% | 78.00% | 78.75% | 77.26% |
|
| 179 |
+
| **Attention UNet (deployed)** | **89.36%** | **76.91%** | **86.95%** | **85.41%** | **88.54%** |
|
| 180 |
+
|
| 181 |
+
---
|
| 182 |
+
|
| 183 |
+
## π Viva Questions & Answers
|
| 184 |
+
|
| 185 |
+
### Architecture
|
| 186 |
+
|
| 187 |
+
**Q: Why did you choose UNet for flood segmentation?**
|
| 188 |
+
UNet is a fully convolutional encoder-decoder architecture with skip connections. The skip connections preserve spatial detail lost during downsampling β critical for pixel-accurate segmentation. The encoder captures semantic context (what is flooded) while the decoder reconstructs spatial resolution (where exactly).
|
| 189 |
+
|
| 190 |
+
**Q: What is the difference between UNet and Attention UNet?**
|
| 191 |
+
Attention UNet adds attention gates at each skip connection. These gates learn to suppress feature responses in irrelevant regions (dry land, buildings) and amplify responses in relevant regions (water, flooded areas). This is done by computing a soft attention coefficient per spatial location using the decoder signal as a query. Result: IoU improved from 63.94% to 76.91%.
|
| 192 |
+
|
| 193 |
+
**Q: How does the attention gate work mathematically?**
|
| 194 |
+
Given encoder feature `x` and decoder signal `g`:
|
| 195 |
+
```
|
| 196 |
+
attention = sigmoid(W_x(x) + W_g(g) + b)
|
| 197 |
+
output = x * attention
|
| 198 |
+
```
|
| 199 |
+
The sigmoid produces values 0β1 per pixel. Multiplying by the encoder feature selectively passes only relevant spatial information to the decoder.
|
| 200 |
+
|
| 201 |
+
**Q: Why 512Γ512 input size?**
|
| 202 |
+
Balance between spatial resolution and memory. UNet memory scales quadratically with input size due to skip connections storing intermediate feature maps. 512Γ512 gives sufficient detail for flood boundary detection while fitting in GPU/CPU memory.
|
| 203 |
+
|
| 204 |
+
**Q: What loss function did you use and why?**
|
| 205 |
+
Combined BCE + Dice loss:
|
| 206 |
+
- **Binary Cross-Entropy (BCE)**: pixel-level classification loss, handles each pixel independently
|
| 207 |
+
- **Dice Loss**: `1 - (2Γintersection)/(sum+sum)` β directly optimises the overlap metric, handles class imbalance (flooded pixels are often a minority in the image)
|
| 208 |
+
- Combined: `L = BCE + Dice` β BCE provides stable gradients early in training, Dice refines the boundary
|
| 209 |
+
|
| 210 |
+
**Q: What is IoU and how is it calculated?**
|
| 211 |
+
Intersection over Union (Jaccard Index):
|
| 212 |
+
```
|
| 213 |
+
IoU = |Predicted β© Ground Truth| / |Predicted βͺ Ground Truth|
|
| 214 |
+
```
|
| 215 |
+
Ranges 0β1. Measures overlap between predicted and actual flood regions. More robust than accuracy for imbalanced segmentation tasks.
|
| 216 |
+
|
| 217 |
+
---
|
| 218 |
+
|
| 219 |
+
### Explainability
|
| 220 |
+
|
| 221 |
+
**Q: What is Grad-CAM and why did you use it?**
|
| 222 |
+
Gradient-weighted Class Activation Mapping. It computes the gradient of the output with respect to the feature maps of a target convolutional layer, then takes a weighted sum of those feature maps. The result is a spatial heatmap showing which image regions most influenced the prediction.
|
| 223 |
+
|
| 224 |
+
Used because: (1) native to CNNs, (2) computationally cheap β one forward + one backward pass, (3) produces spatial maps that directly overlay on the image, (4) no model modification needed.
|
| 225 |
+
|
| 226 |
+
**Q: Which layer did you target for Grad-CAM and why?**
|
| 227 |
+
`conv2d_130` β the last convolutional layer before the output head. This layer has the best trade-off: it has learned high-level semantic features (water texture, flood patterns) while still retaining spatial resolution sufficient for meaningful visualisation.
|
| 228 |
+
|
| 229 |
+
**Q: Why JET colormap for Grad-CAM?**
|
| 230 |
+
JET (blueβcyanβgreenβyellowβred) is intuitive: red = high attention (model is confident about flooding here), blue = low attention. It matches the mental model of "heat" = importance.
|
| 231 |
+
|
| 232 |
+
---
|
| 233 |
+
|
| 234 |
+
### Depth Estimation
|
| 235 |
+
|
| 236 |
+
**Q: What is ZoeDepth and how does it work?**
|
| 237 |
+
ZoeDepth (Zero-shot Depth Estimation) is a monocular depth estimation model from Intel. It uses a DPT (Dense Prediction Transformer) backbone pretrained on large datasets, then fine-tuned for metric depth. It takes a single RGB image and outputs a per-pixel depth map in metres β no stereo cameras or LiDAR needed.
|
| 238 |
+
|
| 239 |
+
**Q: Why use ZoeDepth instead of a simpler depth estimate?**
|
| 240 |
+
Rule-based depth estimates (e.g., "flood coverage Γ constant") are not spatially aware. ZoeDepth provides actual per-pixel depth variation β a flood scene may have shallow water at the edges and deep water in the centre. This spatial depth information makes the risk assessment more accurate.
|
| 241 |
+
|
| 242 |
+
**Q: ZoeDepth outputs relative depth β how do you convert to metres?**
|
| 243 |
+
ZoeDepth's `Intel/zoedepth-nyu` model outputs relative disparity values. We normalise to [0,1] then map to [0, 3m] β a realistic maximum flood depth for the scenarios in our training data. The depth is only computed within flooded pixels (masked by the UNet output).
|
| 244 |
+
|
| 245 |
+
---
|
| 246 |
+
|
| 247 |
+
### Risk Assessment
|
| 248 |
+
|
| 249 |
+
**Q: How is the risk level determined?**
|
| 250 |
+
Two signals combined:
|
| 251 |
+
1. **Flood coverage %** β what fraction of the image is flooded
|
| 252 |
+
2. **Average depth (m)** β mean ZoeDepth value within flooded pixels
|
| 253 |
+
|
| 254 |
+
```
|
| 255 |
+
level = max(level_from_pct(flood_pct), level_from_depth(avg_depth_m))
|
| 256 |
+
score = flood_pct Γ 0.7 + depth_score Γ 0.3
|
| 257 |
+
```
|
| 258 |
+
|
| 259 |
+
Thresholds: Low (<15%, <0.3m) β Moderate (15β35%, 0.3β0.8m) β High (35β60%, 0.8β1.5m) β Critical (>60%, >1.5m)
|
| 260 |
+
|
| 261 |
+
**Q: Why does depth only contribute when flood_pct β₯ 2%?**
|
| 262 |
+
Below 2% coverage, the flooded pixels are likely noise or false positives. Using depth on noise pixels would produce meaningless depth values that could incorrectly escalate the risk level.
|
| 263 |
+
|
| 264 |
+
---
|
| 265 |
+
|
| 266 |
+
### OOD Detection
|
| 267 |
+
|
| 268 |
+
**Q: What is out-of-distribution (OOD) detection and why is it needed?**
|
| 269 |
+
The model was trained only on flood/water imagery. When given a completely different image (car, building, portrait), the model produces garbage predictions β it has never seen these inputs during training. OOD detection identifies such inputs and suppresses the results rather than showing false flood detections.
|
| 270 |
+
|
| 271 |
+
**Q: How does your OOD detection work?**
|
| 272 |
+
Four signals:
|
| 273 |
+
1. **Brightness gate** β rejects pitch-black (<15) or overexposed (>240) images
|
| 274 |
+
2. **Spatial coherence** β real flood masks are large connected blobs; OOD produces >50 fragmented disconnected regions with the largest blob <25% of total flood pixels
|
| 275 |
+
3. **Coverage cap** β >90% flood coverage is physically implausible
|
| 276 |
+
4. **Colour-coverage cross-check** β if flood coverage >40% but <15% of pixels have water-like colours (blue hue 100β130Β°, grey low-saturation, brown 8β22Β°) β OOD
|
| 277 |
+
|
| 278 |
+
**Q: Why not use a separate classifier for OOD?**
|
| 279 |
+
A separate classifier would require additional training data and labels. Our heuristic approach is training-free, interpretable, and works well for the specific failure modes of this model (non-water textures being misclassified as flood).
|
| 280 |
+
|
| 281 |
+
---
|
| 282 |
+
|
| 283 |
+
### Deployment
|
| 284 |
+
|
| 285 |
+
**Q: Why FastAPI for the backend?**
|
| 286 |
+
FastAPI is async, type-safe (Pydantic models), auto-generates OpenAPI docs, and has excellent performance. The Pydantic response models also prevent schema issues with Gradio's API introspection.
|
| 287 |
+
|
| 288 |
+
**Q: Why Gradio for the frontend?**
|
| 289 |
+
Gradio is purpose-built for ML demos, integrates directly with Python functions (no JavaScript needed), and deploys natively to Hugging Face Spaces with zero configuration.
|
| 290 |
+
|
| 291 |
+
**Q: What is the PKL file and what does it store?**
|
| 292 |
+
The `.pkl` file stores metadata only β not the model weights. Contents: threshold (0.5), Grad-CAM target layer name (conv2d_130), risk thresholds, model performance metrics, and custom object keys. The actual weights are in the `.keras` file.
|
| 293 |
+
|
| 294 |
+
**Q: How would you scale this to production?**
|
| 295 |
+
- Replace CPU inference with GPU (CUDA) for 10β50Γ speedup
|
| 296 |
+
- Add a message queue (Redis/Celery) for async processing of multiple requests
|
| 297 |
+
- Cache ZoeDepth results for similar images
|
| 298 |
+
- Add authentication to the API
|
| 299 |
+
- Deploy on Kubernetes with horizontal scaling
|
| 300 |
+
- Add monitoring (Prometheus/Grafana) for inference latency and error rates
|
| 301 |
+
|
| 302 |
+
---
|
| 303 |
+
|
| 304 |
+
## π¦ Dependencies
|
| 305 |
+
|
| 306 |
+
| Package | Purpose |
|
| 307 |
+
|---------|---------|
|
| 308 |
+
| `tensorflow-cpu` | Attention UNet inference |
|
| 309 |
+
| `torch` | ZoeDepth (PyTorch model) |
|
| 310 |
+
| `transformers` | ZoeDepth pipeline from HuggingFace |
|
| 311 |
+
| `opencv-python-headless` | Image processing, Grad-CAM, colormaps |
|
| 312 |
+
| `gradio` | Frontend UI |
|
| 313 |
+
| `fastapi` + `uvicorn` | REST API backend |
|
| 314 |
+
| `Pillow` | Image loading/conversion |
|
| 315 |
+
| `numpy` | Array operations |
|
app.py
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Gradio frontend β Flood Detection + ZoeDepth depth estimation.
|
| 3 |
+
Improved UI: dark theme, styled cards, progress steps, metric badges.
|
| 4 |
+
"""
|
| 5 |
+
import gradio as gr
|
| 6 |
+
import numpy as np
|
| 7 |
+
from PIL import Image
|
| 8 |
+
import sys, os
|
| 9 |
+
|
| 10 |
+
sys.path.insert(0, os.path.dirname(__file__))
|
| 11 |
+
from app.model_utils import run_pipeline
|
| 12 |
+
|
| 13 |
+
# ββ Risk config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 14 |
+
RISK_CFG = {
|
| 15 |
+
"Low": {"colour": "#27ae60", "bg": "#eafaf1", "icon": "π’", "bar": 20},
|
| 16 |
+
"Moderate": {"colour": "#f39c12", "bg": "#fef9e7", "icon": "π‘", "bar": 50},
|
| 17 |
+
"High": {"colour": "#e67e22", "bg": "#fdf2e9", "icon": "π ", "bar": 75},
|
| 18 |
+
"Critical": {"colour": "#e74c3c", "bg": "#fdedec", "icon": "π΄", "bar": 100},
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
CSS = """
|
| 22 |
+
/* ββ page ββ */
|
| 23 |
+
body, .gradio-container { background: #0f1117 !important; color: #e8eaf0 !important; }
|
| 24 |
+
|
| 25 |
+
/* ββ header band ββ */
|
| 26 |
+
#header-band {
|
| 27 |
+
background: linear-gradient(135deg, #0d47a1 0%, #1565c0 40%, #0277bd 100%);
|
| 28 |
+
border-radius: 16px; padding: 28px 32px 20px; margin-bottom: 20px;
|
| 29 |
+
box-shadow: 0 4px 24px rgba(0,0,0,0.5);
|
| 30 |
+
}
|
| 31 |
+
#header-band h1 { color: #fff; font-size: 2em; margin: 0 0 6px; }
|
| 32 |
+
#header-band p { color: #bbdefb; margin: 0; font-size: 0.97em; }
|
| 33 |
+
|
| 34 |
+
/* ββ metric pill row ββ */
|
| 35 |
+
.metric-pill {
|
| 36 |
+
display: inline-block; background: rgba(255,255,255,0.08);
|
| 37 |
+
border: 1px solid rgba(255,255,255,0.18); border-radius: 20px;
|
| 38 |
+
padding: 4px 14px; margin: 4px; font-size: 0.88em; color: #e3f2fd;
|
| 39 |
+
}
|
| 40 |
+
.metric-pill strong { color: #64b5f6; }
|
| 41 |
+
|
| 42 |
+
/* ββ upload + button panel ββ */
|
| 43 |
+
#left-panel { background: #1a1d27; border-radius: 14px; padding: 16px; }
|
| 44 |
+
|
| 45 |
+
/* ββ analyse button ββ */
|
| 46 |
+
#analyse-btn {
|
| 47 |
+
background: linear-gradient(90deg, #1565c0, #0277bd) !important;
|
| 48 |
+
color: #fff !important; border: none !important;
|
| 49 |
+
border-radius: 10px !important; font-size: 1.05em !important;
|
| 50 |
+
padding: 12px !important; margin-top: 10px !important;
|
| 51 |
+
box-shadow: 0 3px 12px rgba(2,119,189,0.45) !important;
|
| 52 |
+
transition: transform .15s, box-shadow .15s !important;
|
| 53 |
+
}
|
| 54 |
+
#analyse-btn:hover {
|
| 55 |
+
transform: translateY(-2px) !important;
|
| 56 |
+
box-shadow: 0 6px 20px rgba(2,119,189,0.6) !important;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
/* ββ section labels ββ */
|
| 60 |
+
.section-label {
|
| 61 |
+
font-size: 0.78em; font-weight: 700; letter-spacing: .08em;
|
| 62 |
+
text-transform: uppercase; color: #64b5f6; margin: 18px 0 6px;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
/* ββ image cards ββ */
|
| 66 |
+
.image-card { background: #1a1d27 !important; border-radius: 12px !important;
|
| 67 |
+
border: 1px solid #2a2d3a !important; overflow: hidden; }
|
| 68 |
+
|
| 69 |
+
/* ββ risk card ββ */
|
| 70 |
+
#risk-card { border-radius: 14px; overflow: hidden; }
|
| 71 |
+
|
| 72 |
+
/* ββ how-it-works table ββ */
|
| 73 |
+
.how-table { width:100%; border-collapse:collapse; font-size:0.9em; }
|
| 74 |
+
.how-table th { background:#1565c0; color:#fff; padding:8px 12px; text-align:left; }
|
| 75 |
+
.how-table td { padding:8px 12px; border-bottom:1px solid #2a2d3a; color:#cfd8dc; }
|
| 76 |
+
.how-table tr:last-child td { border-bottom: none; }
|
| 77 |
+
"""
|
| 78 |
+
|
| 79 |
+
# ββ helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 80 |
+
def _score_bar(score: float, colour: str) -> str:
|
| 81 |
+
"""Animated CSS progress bar."""
|
| 82 |
+
return f"""
|
| 83 |
+
<div style="background:#2a2d3a; border-radius:8px; height:10px; margin:8px 0 14px; overflow:hidden;">
|
| 84 |
+
<div style="width:{score}%; background:linear-gradient(90deg,{colour}99,{colour});
|
| 85 |
+
height:100%; border-radius:8px;
|
| 86 |
+
transition: width 1s ease;"></div>
|
| 87 |
+
</div>"""
|
| 88 |
+
|
| 89 |
+
def _stat_row(icon, label, value, colour) -> str:
|
| 90 |
+
return f"""
|
| 91 |
+
<div style="display:flex; justify-content:space-between; align-items:center;
|
| 92 |
+
padding:7px 0; border-bottom:1px solid rgba(255,255,255,0.07);">
|
| 93 |
+
<span style="color:#90a4ae;">{icon} {label}</span>
|
| 94 |
+
<span style="font-weight:700; color:{colour};">{value}</span>
|
| 95 |
+
</div>"""
|
| 96 |
+
|
| 97 |
+
def build_risk_html(risk: dict, depth_info: dict) -> str:
|
| 98 |
+
level = risk["risk_level"]
|
| 99 |
+
cfg = RISK_CFG.get(level, RISK_CFG["Low"])
|
| 100 |
+
c = cfg["colour"]
|
| 101 |
+
score = risk["risk_score"]
|
| 102 |
+
conf = risk.get("confidence", 100)
|
| 103 |
+
warning = risk.get("warning", "")
|
| 104 |
+
|
| 105 |
+
# ββ Out-of-domain warning banner ββββββββββββββββββββββββββββββββββββββββββ
|
| 106 |
+
warning_html = ""
|
| 107 |
+
if warning:
|
| 108 |
+
warning_html = f"""
|
| 109 |
+
<div style="background:#7f1d1d; border:1px solid #ef4444; border-radius:8px;
|
| 110 |
+
padding:10px 14px; margin-bottom:14px; font-size:0.9em; color:#fca5a5;">
|
| 111 |
+
{warning}
|
| 112 |
+
</div>"""
|
| 113 |
+
|
| 114 |
+
recs_html = "".join(
|
| 115 |
+
f'<li style="margin:5px 0; color:#cfd8dc;">{r}</li>'
|
| 116 |
+
for r in risk["recommendations"]
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
metrics = risk.get("model_metrics", {})
|
| 120 |
+
metrics_html = ""
|
| 121 |
+
if metrics:
|
| 122 |
+
pills = "".join(
|
| 123 |
+
f'<span class="metric-pill">{k}: <strong>{v}</strong></span>'
|
| 124 |
+
for k, v in metrics.items() if k != "Model"
|
| 125 |
+
)
|
| 126 |
+
metrics_html = f"""
|
| 127 |
+
<div style="margin-top:14px; padding-top:12px;
|
| 128 |
+
border-top:1px solid rgba(255,255,255,0.1);">
|
| 129 |
+
<div style="font-size:0.78em; text-transform:uppercase; letter-spacing:.08em;
|
| 130 |
+
color:#64b5f6; margin-bottom:6px;">π Model Performance</div>
|
| 131 |
+
{pills}
|
| 132 |
+
</div>"""
|
| 133 |
+
|
| 134 |
+
return f"""
|
| 135 |
+
<div id="risk-card" style="background:linear-gradient(160deg,#1a1d27 60%,{c}18);
|
| 136 |
+
border:2px solid {c}; border-radius:14px; padding:20px; font-family:sans-serif;">
|
| 137 |
+
|
| 138 |
+
{warning_html}
|
| 139 |
+
|
| 140 |
+
<!-- header -->
|
| 141 |
+
<div style="display:flex; align-items:center; gap:12px; margin-bottom:4px;">
|
| 142 |
+
<span style="font-size:2.2em;">{cfg['icon']}</span>
|
| 143 |
+
<div>
|
| 144 |
+
<div style="font-size:0.75em; text-transform:uppercase; letter-spacing:.1em;
|
| 145 |
+
color:{c}; font-weight:700;">Risk Level</div>
|
| 146 |
+
<div style="font-size:1.9em; font-weight:800; color:{c}; line-height:1.1;">
|
| 147 |
+
{level}
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
<div style="margin-left:auto; text-align:right;">
|
| 151 |
+
<div style="font-size:2.4em; font-weight:900; color:{c};">{score}</div>
|
| 152 |
+
<div style="font-size:0.75em; color:#78909c;">/ 100</div>
|
| 153 |
+
</div>
|
| 154 |
+
</div>
|
| 155 |
+
|
| 156 |
+
{_score_bar(score, c)}
|
| 157 |
+
|
| 158 |
+
<!-- confidence row -->
|
| 159 |
+
<div style="display:flex; justify-content:space-between; align-items:center;
|
| 160 |
+
padding:5px 0 10px; border-bottom:1px solid rgba(255,255,255,0.07);">
|
| 161 |
+
<span style="color:#90a4ae; font-size:0.88em;">π― Model Confidence</span>
|
| 162 |
+
<span style="font-weight:700; color:{'#27ae60' if conf >= 50 else '#e74c3c'};">
|
| 163 |
+
{conf}%
|
| 164 |
+
</span>
|
| 165 |
+
</div>
|
| 166 |
+
|
| 167 |
+
<!-- stats grid -->
|
| 168 |
+
{_stat_row("π", "Flood Coverage", f"{risk['flood_pct']}%", c)}
|
| 169 |
+
{_stat_row("π", "Avg Flood Depth", f"{risk['avg_depth_m']} m", c)}
|
| 170 |
+
{_stat_row("π", "Max Flood Depth", f"{depth_info['max_depth_m']} m", c)}
|
| 171 |
+
{_stat_row("π΅", "Depth Category", depth_info['depth_category'], c)}
|
| 172 |
+
|
| 173 |
+
<!-- recommendations -->
|
| 174 |
+
<div style="margin-top:14px; padding-top:12px;
|
| 175 |
+
border-top:1px solid rgba(255,255,255,0.1);">
|
| 176 |
+
<div style="font-size:0.78em; text-transform:uppercase; letter-spacing:.08em;
|
| 177 |
+
color:#64b5f6; margin-bottom:8px;">π Recommendations</div>
|
| 178 |
+
<ul style="margin:0; padding-left:18px; line-height:1.7;">
|
| 179 |
+
{recs_html}
|
| 180 |
+
</ul>
|
| 181 |
+
</div>
|
| 182 |
+
|
| 183 |
+
{metrics_html}
|
| 184 |
+
</div>"""
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
# ββ inference βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 188 |
+
def predict(image: Image.Image):
|
| 189 |
+
if image is None:
|
| 190 |
+
placeholder = """
|
| 191 |
+
<div style="background:#1a1d27; border:2px dashed #2a2d3a; border-radius:14px;
|
| 192 |
+
padding:40px; text-align:center; color:#546e7a; font-family:sans-serif;">
|
| 193 |
+
<div style="font-size:2.5em; margin-bottom:10px;">π</div>
|
| 194 |
+
<div style="font-size:1.1em;">Upload an image and click <strong>Analyse</strong></div>
|
| 195 |
+
<div style="font-size:0.85em; margin-top:6px; color:#37474f;">
|
| 196 |
+
Supports satellite, aerial, or ground-level flood images
|
| 197 |
+
</div>
|
| 198 |
+
</div>"""
|
| 199 |
+
return None, None, None, None, None, placeholder
|
| 200 |
+
|
| 201 |
+
result = run_pipeline(image)
|
| 202 |
+
risk = result["risk"]
|
| 203 |
+
depth_info = result["depth_info"]
|
| 204 |
+
|
| 205 |
+
overlay = Image.fromarray(result["overlay"])
|
| 206 |
+
gradcam = Image.fromarray(result["gradcam"])
|
| 207 |
+
depth_map = Image.fromarray(result["depth_map"])
|
| 208 |
+
depth_flood = Image.fromarray(result["depth_overlay"])
|
| 209 |
+
|
| 210 |
+
# B&W mask: white = flooded, black = dry β clean and crisp
|
| 211 |
+
mask_bw = Image.fromarray((result["mask"] * 255).astype(np.uint8), mode="L")
|
| 212 |
+
|
| 213 |
+
risk_html = build_risk_html(risk, depth_info)
|
| 214 |
+
return overlay, mask_bw, gradcam, depth_map, depth_flood, risk_html
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
# ββ UI layout βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 218 |
+
with gr.Blocks(title="π Flood Detection AI", theme=gr.themes.Soft()) as demo:
|
| 219 |
+
|
| 220 |
+
# ββ Header ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 221 |
+
gr.HTML("""
|
| 222 |
+
<div id="header-band">
|
| 223 |
+
<h1>π Flood Detection & Risk Assessment</h1>
|
| 224 |
+
<p>
|
| 225 |
+
Attention UNet segmentation Β· Grad-CAM explainability
|
| 226 |
+
Β· ZoeDepth metric depth Β· AI-powered risk scoring
|
| 227 |
+
</p>
|
| 228 |
+
<div style="margin-top:12px;">
|
| 229 |
+
<span class="metric-pill">IoU <strong>76.91%</strong></span>
|
| 230 |
+
<span class="metric-pill">Dice/F1 <strong>86.95%</strong></span>
|
| 231 |
+
<span class="metric-pill">Pixel Acc <strong>89.36%</strong></span>
|
| 232 |
+
<span class="metric-pill">Precision <strong>85.41%</strong></span>
|
| 233 |
+
<span class="metric-pill">Recall <strong>88.54%</strong></span>
|
| 234 |
+
</div>
|
| 235 |
+
</div>
|
| 236 |
+
""")
|
| 237 |
+
|
| 238 |
+
# ββ Main row: upload + risk card ββββββββββββββββββββββββββββββββββββββββ
|
| 239 |
+
with gr.Row(equal_height=False):
|
| 240 |
+
|
| 241 |
+
with gr.Column(scale=4, elem_id="left-panel"):
|
| 242 |
+
gr.HTML('<div class="section-label">π· Input Image</div>')
|
| 243 |
+
input_image = gr.Image(
|
| 244 |
+
type="pil",
|
| 245 |
+
label="",
|
| 246 |
+
elem_classes=["image-card"],
|
| 247 |
+
show_label=False,
|
| 248 |
+
)
|
| 249 |
+
run_btn = gr.Button(
|
| 250 |
+
"π Analyse Flood Risk",
|
| 251 |
+
variant="primary",
|
| 252 |
+
elem_id="analyse-btn",
|
| 253 |
+
)
|
| 254 |
+
gr.HTML("""
|
| 255 |
+
<div style="margin-top:12px; padding:10px 14px; background:#12151f;
|
| 256 |
+
border-radius:10px; border-left:3px solid #1565c0;">
|
| 257 |
+
<div style="font-size:0.78em; color:#64b5f6; font-weight:700;
|
| 258 |
+
text-transform:uppercase; letter-spacing:.07em; margin-bottom:6px;">
|
| 259 |
+
Pipeline Steps
|
| 260 |
+
</div>
|
| 261 |
+
<div style="font-size:0.85em; color:#90a4ae; line-height:2;">
|
| 262 |
+
1οΈβ£ Preprocess β 512Γ512 RGB, normalise<br>
|
| 263 |
+
2οΈβ£ Attention UNet β binary flood mask<br>
|
| 264 |
+
3οΈβ£ Grad-CAM β attention heatmap<br>
|
| 265 |
+
4οΈβ£ ZoeDepth β per-pixel depth (metres)<br>
|
| 266 |
+
5οΈβ£ Risk engine β level + score + advice
|
| 267 |
+
</div>
|
| 268 |
+
</div>
|
| 269 |
+
""")
|
| 270 |
+
|
| 271 |
+
with gr.Column(scale=6):
|
| 272 |
+
gr.HTML('<div class="section-label">β οΈ Risk Assessment</div>')
|
| 273 |
+
risk_display = gr.HTML(
|
| 274 |
+
value="""
|
| 275 |
+
<div style="background:#1a1d27; border:2px dashed #2a2d3a;
|
| 276 |
+
border-radius:14px; padding:40px; text-align:center;
|
| 277 |
+
color:#546e7a; font-family:sans-serif;">
|
| 278 |
+
<div style="font-size:2.5em; margin-bottom:10px;">π</div>
|
| 279 |
+
<div style="font-size:1.1em;">
|
| 280 |
+
Upload an image and click <strong>Analyse</strong>
|
| 281 |
+
</div>
|
| 282 |
+
</div>"""
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
# ββ Visual outputs βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 286 |
+
gr.HTML('<div class="section-label">πΊοΈ Segmentation & Explainability</div>')
|
| 287 |
+
with gr.Row():
|
| 288 |
+
overlay_out = gr.Image(
|
| 289 |
+
label="π΅ Flood Mask Overlay",
|
| 290 |
+
elem_classes=["image-card"],
|
| 291 |
+
)
|
| 292 |
+
mask_bw_out = gr.Image(
|
| 293 |
+
label="β¬ Binary Flood Mask (White = Flooded)",
|
| 294 |
+
elem_classes=["image-card"],
|
| 295 |
+
image_mode="L",
|
| 296 |
+
)
|
| 297 |
+
gradcam_out = gr.Image(
|
| 298 |
+
label="π₯ Grad-CAM (TURBO)",
|
| 299 |
+
elem_classes=["image-card"],
|
| 300 |
+
)
|
| 301 |
+
|
| 302 |
+
gr.HTML('<div class="section-label">π ZoeDepth Estimation</div>')
|
| 303 |
+
with gr.Row():
|
| 304 |
+
depth_map_out = gr.Image(
|
| 305 |
+
label="π Full Depth Map (PLASMA)",
|
| 306 |
+
elem_classes=["image-card"],
|
| 307 |
+
)
|
| 308 |
+
depth_flood_out = gr.Image(
|
| 309 |
+
label="π Flood-Region Depth",
|
| 310 |
+
elem_classes=["image-card"],
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
# ββ How it works βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 314 |
+
with gr.Accordion("βοΈ How it works", open=False):
|
| 315 |
+
gr.HTML("""
|
| 316 |
+
<table class="how-table">
|
| 317 |
+
<tr>
|
| 318 |
+
<th>Step</th><th>Component</th><th>What it does</th>
|
| 319 |
+
</tr>
|
| 320 |
+
<tr>
|
| 321 |
+
<td>1</td>
|
| 322 |
+
<td><strong>Preprocessing</strong></td>
|
| 323 |
+
<td>Resize to 512Γ512, convert to RGB, normalise to [0,1]</td>
|
| 324 |
+
</tr>
|
| 325 |
+
<tr>
|
| 326 |
+
<td>2</td>
|
| 327 |
+
<td><strong>Attention UNet</strong></td>
|
| 328 |
+
<td>Predicts binary flood mask β attention gates suppress irrelevant features</td>
|
| 329 |
+
</tr>
|
| 330 |
+
<tr>
|
| 331 |
+
<td>3</td>
|
| 332 |
+
<td><strong>Grad-CAM</strong></td>
|
| 333 |
+
<td>Gradient-weighted class activation map at conv2d_130 β TURBO colormap with contour</td>
|
| 334 |
+
</tr>
|
| 335 |
+
<tr>
|
| 336 |
+
<td>4</td>
|
| 337 |
+
<td><strong>ZoeDepth (Intel/zoedepth-nyu)</strong></td>
|
| 338 |
+
<td>Monocular metric depth estimation β outputs depth in metres per pixel</td>
|
| 339 |
+
</tr>
|
| 340 |
+
<tr>
|
| 341 |
+
<td>5</td>
|
| 342 |
+
<td><strong>Risk Engine</strong></td>
|
| 343 |
+
<td>Combines flood coverage % + avg depth β Low / Moderate / High / Critical</td>
|
| 344 |
+
</tr>
|
| 345 |
+
</table>
|
| 346 |
+
<div style="margin-top:14px; padding:12px 16px; background:#12151f;
|
| 347 |
+
border-radius:10px; font-size:0.88em; color:#90a4ae;">
|
| 348 |
+
<strong style="color:#64b5f6;">Risk thresholds:</strong>
|
| 349 |
+
π’ Low (<15% flood, <0.8m)
|
| 350 |
+
π‘ Moderate (15β35%, 0.8β1.5m)
|
| 351 |
+
π High (35β60%, 1.5β2.2m)
|
| 352 |
+
π΄ Critical (>60% or >2.2m)
|
| 353 |
+
</div>
|
| 354 |
+
""")
|
| 355 |
+
|
| 356 |
+
# ββ Wire up βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 357 |
+
run_btn.click(
|
| 358 |
+
fn=predict,
|
| 359 |
+
inputs=[input_image],
|
| 360 |
+
outputs=[overlay_out, mask_bw_out, gradcam_out, depth_map_out, depth_flood_out, risk_display],
|
| 361 |
+
)
|
| 362 |
+
|
| 363 |
+
if __name__ == "__main__":
|
| 364 |
+
demo.launch(server_name="0.0.0.0", server_port=7860, css=CSS)
|
app/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FastAPI backend for Flood Detection + ZoeDepth.
|
| 3 |
+
Endpoints:
|
| 4 |
+
POST /predict β full pipeline (mask + gradcam + depth + risk)
|
| 5 |
+
GET /health β liveness check
|
| 6 |
+
"""
|
app/main.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FastAPI backend for Flood Detection + ZoeDepth.
|
| 3 |
+
Endpoints:
|
| 4 |
+
POST /predict β full pipeline (mask + gradcam + depth + risk)
|
| 5 |
+
GET /health β liveness check
|
| 6 |
+
"""
|
| 7 |
+
from fastapi import FastAPI, File, UploadFile, HTTPException
|
| 8 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 9 |
+
from pydantic import BaseModel
|
| 10 |
+
from typing import List, Optional
|
| 11 |
+
import numpy as np
|
| 12 |
+
from PIL import Image
|
| 13 |
+
import io
|
| 14 |
+
import base64
|
| 15 |
+
import uvicorn
|
| 16 |
+
|
| 17 |
+
from app.model_utils import run_pipeline
|
| 18 |
+
|
| 19 |
+
app = FastAPI(
|
| 20 |
+
title="Flood Detection API",
|
| 21 |
+
description="Attention UNet + ZoeDepth flood segmentation with Grad-CAM and risk assessment.",
|
| 22 |
+
version="2.0.0",
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
app.add_middleware(
|
| 26 |
+
CORSMiddleware,
|
| 27 |
+
allow_origins=["*"],
|
| 28 |
+
allow_methods=["*"],
|
| 29 |
+
allow_headers=["*"],
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# ββ Response models ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 34 |
+
class DepthInfo(BaseModel):
|
| 35 |
+
avg_depth_m: float
|
| 36 |
+
max_depth_m: float
|
| 37 |
+
depth_category: str
|
| 38 |
+
|
| 39 |
+
class RiskResult(BaseModel):
|
| 40 |
+
risk_level: str
|
| 41 |
+
risk_score: float
|
| 42 |
+
colour: str
|
| 43 |
+
flood_pct: float
|
| 44 |
+
avg_depth_m: float
|
| 45 |
+
recommendations: List[str]
|
| 46 |
+
model_metrics: Optional[dict] = None
|
| 47 |
+
|
| 48 |
+
class PredictResponse(BaseModel):
|
| 49 |
+
mask_b64: str
|
| 50 |
+
overlay_b64: str
|
| 51 |
+
gradcam_b64: str
|
| 52 |
+
depth_map_b64: str
|
| 53 |
+
depth_flood_b64: str
|
| 54 |
+
depth_info: DepthInfo
|
| 55 |
+
risk: RiskResult
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _ndarray_to_b64(arr: np.ndarray) -> str:
|
| 59 |
+
img = Image.fromarray(arr.astype(np.uint8))
|
| 60 |
+
buf = io.BytesIO()
|
| 61 |
+
img.save(buf, format="PNG")
|
| 62 |
+
return base64.b64encode(buf.getvalue()).decode("utf-8")
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
@app.get("/health")
|
| 66 |
+
def health():
|
| 67 |
+
return {"status": "ok"}
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
@app.post("/predict", response_model=PredictResponse)
|
| 71 |
+
async def predict(file: UploadFile = File(...)):
|
| 72 |
+
if not file.content_type.startswith("image/"):
|
| 73 |
+
raise HTTPException(status_code=400, detail="File must be an image.")
|
| 74 |
+
|
| 75 |
+
try:
|
| 76 |
+
contents = await file.read()
|
| 77 |
+
image = Image.open(io.BytesIO(contents))
|
| 78 |
+
except Exception as e:
|
| 79 |
+
raise HTTPException(status_code=400, detail=f"Cannot read image: {e}")
|
| 80 |
+
|
| 81 |
+
try:
|
| 82 |
+
result = run_pipeline(image)
|
| 83 |
+
except Exception as e:
|
| 84 |
+
raise HTTPException(status_code=500, detail=f"Inference error: {e}")
|
| 85 |
+
|
| 86 |
+
mask_uint8 = (result["mask"] * 255).astype(np.uint8)
|
| 87 |
+
depth_info = result["depth_info"]
|
| 88 |
+
|
| 89 |
+
return PredictResponse(
|
| 90 |
+
mask_b64 = _ndarray_to_b64(mask_uint8),
|
| 91 |
+
overlay_b64 = _ndarray_to_b64(result["overlay"]),
|
| 92 |
+
gradcam_b64 = _ndarray_to_b64(result["gradcam"]),
|
| 93 |
+
depth_map_b64 = _ndarray_to_b64(result["depth_map"]),
|
| 94 |
+
depth_flood_b64 = _ndarray_to_b64(result["depth_overlay"]),
|
| 95 |
+
depth_info = DepthInfo(
|
| 96 |
+
avg_depth_m = depth_info["avg_depth_m"],
|
| 97 |
+
max_depth_m = depth_info["max_depth_m"],
|
| 98 |
+
depth_category = depth_info["depth_category"],
|
| 99 |
+
),
|
| 100 |
+
risk = RiskResult(**result["risk"]),
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
if __name__ == "__main__":
|
| 105 |
+
uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=False)
|
app/model_utils.py
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
model_utils.py β loads the .pkl metadata + .keras model and exposes inference helpers.
|
| 3 |
+
ZoeDepth is loaded from HuggingFace Hub (CPU) for flood depth estimation.
|
| 4 |
+
|
| 5 |
+
PKL structure (from flood_model_production.pkl):
|
| 6 |
+
model_name, model_keras_path, input_shape, output_shape, threshold,
|
| 7 |
+
preprocessing, gradcam_layer, metrics, risk_thresholds, custom_objects_keys
|
| 8 |
+
"""
|
| 9 |
+
import os
|
| 10 |
+
import pickle
|
| 11 |
+
import numpy as np
|
| 12 |
+
import cv2
|
| 13 |
+
import tensorflow as tf
|
| 14 |
+
from PIL import Image
|
| 15 |
+
import torch
|
| 16 |
+
|
| 17 |
+
# ββ HuggingFace token (set via env var β never hardcode in production) βββββββββ
|
| 18 |
+
HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
| 19 |
+
|
| 20 |
+
# ββ Custom Keras objects βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 21 |
+
def iou_metric(y_true, y_pred):
|
| 22 |
+
y_pred = tf.cast(y_pred > 0.5, tf.float32)
|
| 23 |
+
intersection = tf.reduce_sum(y_true * y_pred)
|
| 24 |
+
union = tf.reduce_sum(y_true) + tf.reduce_sum(y_pred) - intersection
|
| 25 |
+
return intersection / (union + 1e-7)
|
| 26 |
+
|
| 27 |
+
def dice_loss(y_true, y_pred, smooth=1e-7):
|
| 28 |
+
intersection = tf.reduce_sum(y_true * y_pred)
|
| 29 |
+
return 1.0 - (2.0 * intersection + smooth) / (
|
| 30 |
+
tf.reduce_sum(y_true) + tf.reduce_sum(y_pred) + smooth
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
def bce_dice_loss(y_true, y_pred):
|
| 34 |
+
bce = tf.keras.losses.binary_crossentropy(y_true, y_pred)
|
| 35 |
+
dice = dice_loss(y_true, y_pred)
|
| 36 |
+
return bce + dice
|
| 37 |
+
|
| 38 |
+
CUSTOM_OBJECTS = {
|
| 39 |
+
"iou_metric": iou_metric,
|
| 40 |
+
"dice_loss": dice_loss,
|
| 41 |
+
"bce_dice_loss": bce_dice_loss,
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
IMAGE_SIZE = (512, 512)
|
| 45 |
+
|
| 46 |
+
# ββ Singletons βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 47 |
+
_seg_model = None # Attention UNet (TF/Keras)
|
| 48 |
+
_zoe_model = None # ZoeDepth (PyTorch, CPU)
|
| 49 |
+
_metadata = None # dict from pkl
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
# ββ Metadata βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 53 |
+
def _load_metadata() -> dict:
|
| 54 |
+
global _metadata
|
| 55 |
+
if _metadata is not None:
|
| 56 |
+
return _metadata
|
| 57 |
+
pkl_path = os.environ.get("MODEL_PKL", "models/flood_model_production.pkl")
|
| 58 |
+
if os.path.exists(pkl_path):
|
| 59 |
+
with open(pkl_path, "rb") as f:
|
| 60 |
+
_metadata = pickle.load(f)
|
| 61 |
+
else:
|
| 62 |
+
_metadata = {}
|
| 63 |
+
return _metadata
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
# ββ Attention UNet loader ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 67 |
+
def load_seg_model():
|
| 68 |
+
global _seg_model
|
| 69 |
+
if _seg_model is not None:
|
| 70 |
+
return _seg_model
|
| 71 |
+
keras_path = os.environ.get("MODEL_KERAS", "models/flood_best_model_export.keras")
|
| 72 |
+
if not os.path.exists(keras_path):
|
| 73 |
+
raise FileNotFoundError(
|
| 74 |
+
f"Keras model not found at '{keras_path}'. "
|
| 75 |
+
"Set MODEL_KERAS env var to the correct path."
|
| 76 |
+
)
|
| 77 |
+
_seg_model = tf.keras.models.load_model(keras_path, custom_objects=CUSTOM_OBJECTS)
|
| 78 |
+
return _seg_model
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# ββ ZoeDepth loader (CPU, HF Hub) βββββββββββββββββββββββββββββββββββββββββββββ
|
| 82 |
+
def load_zoe_model():
|
| 83 |
+
"""
|
| 84 |
+
Loads ZoeDepth (ZoeD_N) from HuggingFace Hub on CPU.
|
| 85 |
+
Uses the transformers pipeline for simplicity and reliability.
|
| 86 |
+
Model: Intel/zoedepth-nyu (indoor/outdoor depth estimation)
|
| 87 |
+
"""
|
| 88 |
+
global _zoe_model
|
| 89 |
+
if _zoe_model is not None:
|
| 90 |
+
return _zoe_model
|
| 91 |
+
|
| 92 |
+
from transformers import pipeline as hf_pipeline
|
| 93 |
+
|
| 94 |
+
token = HF_TOKEN or os.environ.get("HF_TOKEN", "")
|
| 95 |
+
|
| 96 |
+
print("[ZoeDepth] Loading from HuggingFace Hub (CPU)...")
|
| 97 |
+
_zoe_model = hf_pipeline(
|
| 98 |
+
task="depth-estimation",
|
| 99 |
+
model="Intel/zoedepth-nyu",
|
| 100 |
+
device="cpu", # force CPU
|
| 101 |
+
token=token if token else None,
|
| 102 |
+
)
|
| 103 |
+
print("[ZoeDepth] Loaded OK.")
|
| 104 |
+
return _zoe_model
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
# ββ Preprocessing ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 108 |
+
def preprocess_image(image: Image.Image) -> np.ndarray:
|
| 109 |
+
"""PIL Image β normalised (1, 512, 512, 3) float32 array."""
|
| 110 |
+
img = image.convert("RGB").resize(IMAGE_SIZE)
|
| 111 |
+
arr = np.array(img, dtype=np.float32) / 255.0
|
| 112 |
+
return np.expand_dims(arr, axis=0) # (1, H, W, 3)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
# ββ OOD / scene validation βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 116 |
+
def _is_flood_scene(image: Image.Image, pred: np.ndarray, threshold: float) -> tuple:
|
| 117 |
+
"""
|
| 118 |
+
Multi-signal check to detect out-of-distribution (non-flood) images.
|
| 119 |
+
|
| 120 |
+
Signals used:
|
| 121 |
+
1. Colour distribution β flood/water images have blue/grey/brown tones.
|
| 122 |
+
Non-flood scenes (cars, indoor, etc.) have very different hue distributions.
|
| 123 |
+
2. Spatial coherence β real flood masks are spatially connected large blobs.
|
| 124 |
+
OOD masks are fragmented noise scattered across the image.
|
| 125 |
+
3. Raw flood coverage β if >85% of image is "flooded" it's almost certainly OOD
|
| 126 |
+
(real floods rarely cover the entire frame uniformly).
|
| 127 |
+
|
| 128 |
+
Returns:
|
| 129 |
+
is_valid : bool β True if image looks like a flood scene
|
| 130 |
+
reason : str β explanation if invalid
|
| 131 |
+
"""
|
| 132 |
+
img_rgb = np.array(image.convert("RGB").resize((512, 512)), dtype=np.float32)
|
| 133 |
+
mask = (pred > threshold).astype(np.uint8)
|
| 134 |
+
|
| 135 |
+
# ββ Signal 1: colour check βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 136 |
+
# Flood scenes have significant blue/grey channel presence.
|
| 137 |
+
# Compute mean of blue channel relative to red β water is blue-dominant.
|
| 138 |
+
r_mean = img_rgb[:, :, 0].mean()
|
| 139 |
+
g_mean = img_rgb[:, :, 1].mean()
|
| 140 |
+
b_mean = img_rgb[:, :, 2].mean()
|
| 141 |
+
brightness = (r_mean + g_mean + b_mean) / 3.0
|
| 142 |
+
|
| 143 |
+
# Very dark images (night/indoor) or very bright (overexposed) are suspicious
|
| 144 |
+
if brightness < 15 or brightness > 240:
|
| 145 |
+
return False, "Image too dark or too bright for flood analysis"
|
| 146 |
+
|
| 147 |
+
# ββ Signal 2: spatial coherence of the predicted mask βββββββββββββββββββββ
|
| 148 |
+
# Real flood masks = large connected regions. OOD = scattered small blobs.
|
| 149 |
+
flood_pct = float(mask.mean() * 100)
|
| 150 |
+
|
| 151 |
+
if flood_pct > 0.5: # only check if there's something to check
|
| 152 |
+
num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(
|
| 153 |
+
mask, connectivity=8
|
| 154 |
+
)
|
| 155 |
+
if num_labels > 1:
|
| 156 |
+
# Largest component area (excluding background label 0)
|
| 157 |
+
component_areas = stats[1:, cv2.CC_STAT_AREA]
|
| 158 |
+
largest_area = int(component_areas.max())
|
| 159 |
+
total_flood_px = int(mask.sum())
|
| 160 |
+
# Coherence = fraction of flood pixels in the largest blob
|
| 161 |
+
coherence = largest_area / (total_flood_px + 1e-8)
|
| 162 |
+
|
| 163 |
+
# If flood pixels are highly fragmented (coherence < 0.3 AND many components)
|
| 164 |
+
# it's likely OOD noise
|
| 165 |
+
if coherence < 0.25 and num_labels > 50:
|
| 166 |
+
return False, f"Flood mask is fragmented ({num_labels} disconnected regions) β likely not a flood scene"
|
| 167 |
+
|
| 168 |
+
# ββ Signal 3: implausibly high coverage βββββββββββββββββββββββββββββββββββ
|
| 169 |
+
if flood_pct > 90:
|
| 170 |
+
return False, f"Flood coverage {flood_pct:.1f}% is implausibly high β image may not be a flood scene"
|
| 171 |
+
|
| 172 |
+
# ββ Signal 4: colour-coverage cross-check βββββββββββββββββββββββββββββββββ
|
| 173 |
+
# High flood coverage (>40%) on an image that is NOT predominantly
|
| 174 |
+
# blue/grey/brown (water colours) is suspicious.
|
| 175 |
+
if flood_pct > 40:
|
| 176 |
+
# Convert to HSV to check hue distribution
|
| 177 |
+
img_uint8 = img_rgb.astype(np.uint8)
|
| 178 |
+
hsv = cv2.cvtColor(img_uint8, cv2.COLOR_RGB2HSV)
|
| 179 |
+
hue = hsv[:, :, 0] # 0-179 in OpenCV
|
| 180 |
+
sat = hsv[:, :, 1] # 0-255
|
| 181 |
+
|
| 182 |
+
# Water/flood hues: blue (100-130), grey (any hue, low sat), brown (10-20)
|
| 183 |
+
blue_mask = ((hue >= 100) & (hue <= 130) & (sat > 30))
|
| 184 |
+
grey_mask = (sat < 40)
|
| 185 |
+
brown_mask = ((hue >= 8) & (hue <= 22) & (sat > 40))
|
| 186 |
+
water_px = float((blue_mask | grey_mask | brown_mask).mean() * 100)
|
| 187 |
+
|
| 188 |
+
# If less than 15% of the image has water-like colours but >40% is "flooded"
|
| 189 |
+
# β almost certainly OOD (car, building, vegetation, etc.)
|
| 190 |
+
if water_px < 15:
|
| 191 |
+
return False, (
|
| 192 |
+
f"High flood coverage ({flood_pct:.1f}%) but only {water_px:.1f}% "
|
| 193 |
+
"water-like colours detected β image does not appear to be a flood scene"
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
return True, ""
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
# ββ Segmentation prediction ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 200 |
+
def predict_mask(image: Image.Image) -> tuple:
|
| 201 |
+
"""
|
| 202 |
+
Returns:
|
| 203 |
+
mask : (H, W) binary float32 array (0 or 1)
|
| 204 |
+
flood_pct : percentage of image covered by flood (0-100)
|
| 205 |
+
confidence : float 0-100 (display percentage)
|
| 206 |
+
low_conf : bool, True if OOD detected
|
| 207 |
+
ood_reason : str, explanation if OOD
|
| 208 |
+
"""
|
| 209 |
+
meta = _load_metadata()
|
| 210 |
+
threshold = meta.get("threshold", 0.5)
|
| 211 |
+
model = load_seg_model()
|
| 212 |
+
inp = preprocess_image(image)
|
| 213 |
+
pred = model.predict(inp, verbose=0)[0, :, :, 0] # (H, W)
|
| 214 |
+
|
| 215 |
+
# Raw confidence metric (how decisive the predictions are)
|
| 216 |
+
confidence_raw = float(np.mean(np.abs(pred - 0.5)) * 2)
|
| 217 |
+
|
| 218 |
+
# OOD detection using multi-signal heuristics
|
| 219 |
+
is_valid, ood_reason = _is_flood_scene(image, pred, threshold)
|
| 220 |
+
|
| 221 |
+
if not is_valid:
|
| 222 |
+
mask = np.zeros_like(pred, dtype=np.float32)
|
| 223 |
+
flood_pct = 0.0
|
| 224 |
+
low_conf = True
|
| 225 |
+
else:
|
| 226 |
+
mask = (pred > threshold).astype(np.float32)
|
| 227 |
+
flood_pct = float(mask.mean() * 100)
|
| 228 |
+
low_conf = False
|
| 229 |
+
|
| 230 |
+
confidence_pct = round(confidence_raw * 100, 1)
|
| 231 |
+
return mask, flood_pct, confidence_pct, low_conf, ood_reason
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
# ββ Grad-CAM βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 235 |
+
def compute_gradcam(image: Image.Image) -> np.ndarray:
|
| 236 |
+
"""
|
| 237 |
+
Returns an RGB uint8 heatmap overlaid on the original image.
|
| 238 |
+
Uses conv2d_130 (from PKL) β the last conv before the output head.
|
| 239 |
+
"""
|
| 240 |
+
meta = _load_metadata()
|
| 241 |
+
model = load_seg_model()
|
| 242 |
+
inp = preprocess_image(image)
|
| 243 |
+
target_layer = meta.get("gradcam_layer", "conv2d_130")
|
| 244 |
+
|
| 245 |
+
layer_names = [l.name for l in model.layers]
|
| 246 |
+
if target_layer not in layer_names:
|
| 247 |
+
# fallback to last Conv2D
|
| 248 |
+
target_layer = None
|
| 249 |
+
for layer in reversed(model.layers):
|
| 250 |
+
if isinstance(layer, tf.keras.layers.Conv2D):
|
| 251 |
+
target_layer = layer.name
|
| 252 |
+
break
|
| 253 |
+
|
| 254 |
+
if target_layer is None:
|
| 255 |
+
return np.array(image.convert("RGB").resize(IMAGE_SIZE))
|
| 256 |
+
|
| 257 |
+
grad_model = tf.keras.models.Model(
|
| 258 |
+
inputs=model.inputs,
|
| 259 |
+
outputs=[model.get_layer(target_layer).output, model.output]
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
with tf.GradientTape() as tape:
|
| 263 |
+
inp_tensor = tf.cast(inp, tf.float32)
|
| 264 |
+
conv_out, predictions = grad_model(inp_tensor)
|
| 265 |
+
loss = tf.reduce_mean(predictions)
|
| 266 |
+
|
| 267 |
+
grads = tape.gradient(loss, conv_out)
|
| 268 |
+
pooled = tf.reduce_mean(grads, axis=(0, 1, 2))
|
| 269 |
+
cam = tf.reduce_sum(conv_out[0] * pooled, axis=-1).numpy()
|
| 270 |
+
cam = np.maximum(cam, 0)
|
| 271 |
+
cam = cam / (cam.max() + 1e-8)
|
| 272 |
+
|
| 273 |
+
# Smooth for the classic soft-blob look (like the reference image)
|
| 274 |
+
cam_resized = cv2.resize(cam, IMAGE_SIZE)
|
| 275 |
+
cam_smooth = cv2.GaussianBlur(cam_resized, (15, 15), 0)
|
| 276 |
+
|
| 277 |
+
# JET: blue(low) β cyan β green β yellow β red(high) β exactly the reference
|
| 278 |
+
heatmap_bgr = cv2.applyColorMap(np.uint8(255 * cam_smooth), cv2.COLORMAP_JET)
|
| 279 |
+
heatmap_rgb = cv2.cvtColor(heatmap_bgr, cv2.COLOR_BGR2RGB).astype(np.float32)
|
| 280 |
+
|
| 281 |
+
# Blend 55% heatmap over original β enough to see the image underneath
|
| 282 |
+
orig = np.array(image.convert("RGB").resize(IMAGE_SIZE), dtype=np.float32)
|
| 283 |
+
blended = (orig * 0.45 + heatmap_rgb * 0.55).clip(0, 255).astype(np.uint8)
|
| 284 |
+
return blended
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
# ββ ZoeDepth flood depth estimation βββββββββββββββββββββββββββββββββββββββββββ
|
| 288 |
+
def estimate_flood_depth(image: Image.Image, mask: np.ndarray) -> dict:
|
| 289 |
+
"""
|
| 290 |
+
Runs ZoeDepth on the image, then analyses depth only within flooded pixels.
|
| 291 |
+
|
| 292 |
+
Returns a dict with:
|
| 293 |
+
depth_map_vis : (H, W, 3) uint8 β colourised depth map
|
| 294 |
+
depth_overlay : (H, W, 3) uint8 β depth map masked to flood region
|
| 295 |
+
avg_depth_m : float β mean depth in flooded area (metres)
|
| 296 |
+
max_depth_m : float β max depth in flooded area (metres)
|
| 297 |
+
depth_category : str β shallow / moderate / deep / very deep
|
| 298 |
+
"""
|
| 299 |
+
zoe = load_zoe_model()
|
| 300 |
+
|
| 301 |
+
# ZoeDepth expects a PIL RGB image (any size β it handles resize internally)
|
| 302 |
+
rgb_img = image.convert("RGB")
|
| 303 |
+
|
| 304 |
+
# Run depth estimation
|
| 305 |
+
depth_result = zoe(rgb_img)
|
| 306 |
+
depth_pil = depth_result["depth"] # PIL Image (grayscale, float-like)
|
| 307 |
+
depth_arr = np.array(depth_pil, dtype=np.float32) # (H, W)
|
| 308 |
+
|
| 309 |
+
# Resize depth map to 512Γ512 to match mask
|
| 310 |
+
depth_512 = cv2.resize(depth_arr, IMAGE_SIZE, interpolation=cv2.INTER_LINEAR)
|
| 311 |
+
|
| 312 |
+
# ββ Normalise to [0, 1] relative scale ββββββββββββββββββββββββββββββββββββ
|
| 313 |
+
# ZoeDepth returns relative disparity values (not calibrated metres).
|
| 314 |
+
# We normalise across the whole image so values are comparable.
|
| 315 |
+
d_min, d_max = depth_512.min(), depth_512.max()
|
| 316 |
+
depth_norm = (depth_512 - d_min) / (d_max - d_min + 1e-8) # 0β1
|
| 317 |
+
|
| 318 |
+
# Map relative depth to an estimated flood depth in metres (0β3 m scale).
|
| 319 |
+
# Higher relative depth in flooded pixels β deeper water estimate.
|
| 320 |
+
FLOOD_DEPTH_MAX_M = 3.0
|
| 321 |
+
depth_metres = depth_norm * FLOOD_DEPTH_MAX_M # (H, W), values 0β3 m
|
| 322 |
+
|
| 323 |
+
# Colourised full depth map (PLASMA colormap)
|
| 324 |
+
depth_vis = cv2.applyColorMap(np.uint8(255 * depth_norm), cv2.COLORMAP_PLASMA)
|
| 325 |
+
depth_vis = cv2.cvtColor(depth_vis, cv2.COLOR_BGR2RGB)
|
| 326 |
+
|
| 327 |
+
# Depth stats within flooded pixels only
|
| 328 |
+
flood_mask_bool = mask.astype(bool)
|
| 329 |
+
flooded_depths = depth_metres[flood_mask_bool]
|
| 330 |
+
|
| 331 |
+
if len(flooded_depths) == 0 or flood_mask_bool.sum() < 50:
|
| 332 |
+
# Fewer than 50 flooded pixels β treat as no flood
|
| 333 |
+
avg_depth_m = 0.0
|
| 334 |
+
max_depth_m = 0.0
|
| 335 |
+
else:
|
| 336 |
+
avg_depth_m = float(flooded_depths.mean())
|
| 337 |
+
max_depth_m = float(flooded_depths.max())
|
| 338 |
+
|
| 339 |
+
# Depth category (ZoeDepth outputs relative depth in metres approx)
|
| 340 |
+
if avg_depth_m < 0.3:
|
| 341 |
+
depth_category = "Shallow (< 30 cm)"
|
| 342 |
+
elif avg_depth_m < 0.8:
|
| 343 |
+
depth_category = "Moderate (30 cm β 80 cm)"
|
| 344 |
+
elif avg_depth_m < 1.5:
|
| 345 |
+
depth_category = "Deep (80 cm β 1.5 m)"
|
| 346 |
+
else:
|
| 347 |
+
depth_category = "Very Deep (> 1.5 m β life-threatening)"
|
| 348 |
+
|
| 349 |
+
# Flood-region depth overlay: blend depth_vis with blue flood mask
|
| 350 |
+
flood_depth_overlay = depth_vis.copy()
|
| 351 |
+
# Highlight non-flooded areas in grey
|
| 352 |
+
grey = np.full_like(depth_vis, 128)
|
| 353 |
+
flood_depth_overlay[~flood_mask_bool] = grey[~flood_mask_bool]
|
| 354 |
+
|
| 355 |
+
return {
|
| 356 |
+
"depth_map_vis": depth_vis, # (H, W, 3) uint8
|
| 357 |
+
"depth_overlay": flood_depth_overlay, # (H, W, 3) uint8
|
| 358 |
+
"avg_depth_m": round(avg_depth_m, 3),
|
| 359 |
+
"max_depth_m": round(max_depth_m, 3),
|
| 360 |
+
"depth_category": depth_category,
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
|
| 364 |
+
# ββ Risk assessment (uses thresholds from PKL + ZoeDepth depth) βββββββββββββββ
|
| 365 |
+
def assess_risk(flood_pct: float, avg_depth_m: float = 0.0) -> dict:
|
| 366 |
+
"""
|
| 367 |
+
Combined risk from flood coverage % AND average depth from ZoeDepth.
|
| 368 |
+
Thresholds from PKL:
|
| 369 |
+
low : flood_pct < 15 AND avg_depth_m < 0.80
|
| 370 |
+
medium : flood_pct < 35 AND avg_depth_m < 1.50
|
| 371 |
+
high : flood_pct < 60 AND avg_depth_m < 2.20
|
| 372 |
+
critical : above all
|
| 373 |
+
"""
|
| 374 |
+
meta = _load_metadata()
|
| 375 |
+
thresholds = meta.get("risk_thresholds", {
|
| 376 |
+
"low": {"max_flood_pct": 15, "max_avg_depth_m": 80},
|
| 377 |
+
"medium": {"max_flood_pct": 35, "max_avg_depth_m": 150},
|
| 378 |
+
"high": {"max_flood_pct": 60, "max_avg_depth_m": 220},
|
| 379 |
+
"critical": {"above_all": True},
|
| 380 |
+
})
|
| 381 |
+
|
| 382 |
+
# PKL stores depth in cm β convert to metres for comparison
|
| 383 |
+
low_pct = thresholds.get("low", {}).get("max_flood_pct", 15)
|
| 384 |
+
med_pct = thresholds.get("medium", {}).get("max_flood_pct", 35)
|
| 385 |
+
high_pct = thresholds.get("high", {}).get("max_flood_pct", 60)
|
| 386 |
+
|
| 387 |
+
# Depth thresholds in metres (realistic flood water levels)
|
| 388 |
+
low_dep = 0.3 # < 30 cm β Low
|
| 389 |
+
med_dep = 0.8 # < 80 cm β Moderate
|
| 390 |
+
high_dep = 1.5 # < 150 cm β High (> 1.5 m β Critical)
|
| 391 |
+
|
| 392 |
+
# Depth only contributes to risk when there is meaningful flood coverage.
|
| 393 |
+
# If flood_pct < 2%, ignore depth entirely (noise / false positives).
|
| 394 |
+
effective_depth = avg_depth_m if flood_pct >= 2.0 else 0.0
|
| 395 |
+
|
| 396 |
+
def _level_from_pct(p):
|
| 397 |
+
if p < low_pct: return 0
|
| 398 |
+
if p < med_pct: return 1
|
| 399 |
+
if p < high_pct: return 2
|
| 400 |
+
return 3
|
| 401 |
+
|
| 402 |
+
def _level_from_dep(d):
|
| 403 |
+
if d < low_dep: return 0
|
| 404 |
+
if d < med_dep: return 1
|
| 405 |
+
if d < high_dep: return 2
|
| 406 |
+
return 3
|
| 407 |
+
|
| 408 |
+
level_idx = max(_level_from_pct(flood_pct), _level_from_dep(effective_depth))
|
| 409 |
+
levels = ["Low", "Moderate", "High", "Critical"]
|
| 410 |
+
colours = ["#2ecc71", "#f39c12", "#e67e22", "#e74c3c"]
|
| 411 |
+
level = levels[level_idx]
|
| 412 |
+
colour = colours[level_idx]
|
| 413 |
+
|
| 414 |
+
# Score: flood coverage drives 70%, depth drives 30%
|
| 415 |
+
pct_score = min(flood_pct / 100 * 100, 100)
|
| 416 |
+
depth_score = min(effective_depth / 3.0 * 100, 100) # 3 m = max realistic scale
|
| 417 |
+
score = round(pct_score * 0.7 + depth_score * 0.3, 1)
|
| 418 |
+
|
| 419 |
+
recs_map = {
|
| 420 |
+
"Low": [
|
| 421 |
+
"Monitor water levels periodically.",
|
| 422 |
+
"Ensure drainage channels are clear.",
|
| 423 |
+
"No immediate evacuation required.",
|
| 424 |
+
],
|
| 425 |
+
"Moderate": [
|
| 426 |
+
"Alert local emergency services.",
|
| 427 |
+
"Move valuables to higher ground.",
|
| 428 |
+
"Prepare emergency kit.",
|
| 429 |
+
"Monitor weather forecasts closely.",
|
| 430 |
+
],
|
| 431 |
+
"High": [
|
| 432 |
+
"Initiate partial evacuation of vulnerable populations.",
|
| 433 |
+
"Deploy flood barriers where possible.",
|
| 434 |
+
"Activate emergency response teams.",
|
| 435 |
+
"Avoid flooded roads and areas.",
|
| 436 |
+
],
|
| 437 |
+
"Critical": [
|
| 438 |
+
"IMMEDIATE EVACUATION REQUIRED.",
|
| 439 |
+
"Contact emergency services (911 / local disaster hotline).",
|
| 440 |
+
"Do not attempt to cross flooded areas.",
|
| 441 |
+
"Seek shelter on highest available ground.",
|
| 442 |
+
"Follow official evacuation routes only.",
|
| 443 |
+
],
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
deployed_metrics = meta.get("metrics", {}).get("deployed", {})
|
| 447 |
+
|
| 448 |
+
return {
|
| 449 |
+
"risk_level": level,
|
| 450 |
+
"risk_score": score,
|
| 451 |
+
"colour": colour,
|
| 452 |
+
"flood_pct": round(flood_pct, 2),
|
| 453 |
+
"avg_depth_m": round(avg_depth_m, 3),
|
| 454 |
+
"recommendations": recs_map[level],
|
| 455 |
+
"model_metrics": deployed_metrics,
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
|
| 459 |
+
# ββ Full pipeline ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 460 |
+
def run_pipeline(image: Image.Image) -> dict:
|
| 461 |
+
"""
|
| 462 |
+
End-to-end:
|
| 463 |
+
1. Attention UNet β flood mask (with confidence gate)
|
| 464 |
+
2. Grad-CAM β explainability heatmap
|
| 465 |
+
3. ZoeDepth β per-pixel depth map, flood depth stats
|
| 466 |
+
4. Risk assessment β level, score, recommendations
|
| 467 |
+
"""
|
| 468 |
+
# Step 1 β segmentation + OOD detection
|
| 469 |
+
mask, flood_pct, confidence, low_conf, ood_reason = predict_mask(image)
|
| 470 |
+
|
| 471 |
+
# Step 2 β Grad-CAM
|
| 472 |
+
gradcam_img = compute_gradcam(image)
|
| 473 |
+
|
| 474 |
+
# Step 3 β ZoeDepth depth estimation
|
| 475 |
+
depth_info = estimate_flood_depth(image, mask)
|
| 476 |
+
|
| 477 |
+
# Step 4 β risk
|
| 478 |
+
risk = assess_risk(flood_pct, avg_depth_m=depth_info["avg_depth_m"])
|
| 479 |
+
|
| 480 |
+
# Add OOD info to risk dict for display
|
| 481 |
+
risk["confidence"] = confidence
|
| 482 |
+
risk["low_conf"] = low_conf
|
| 483 |
+
risk["warning"] = (
|
| 484 |
+
f"β οΈ Out-of-distribution image detected: {ood_reason}. "
|
| 485 |
+
"Results suppressed."
|
| 486 |
+
) if low_conf else ""
|
| 487 |
+
|
| 488 |
+
# Blue-tinted flood overlay on original
|
| 489 |
+
orig_arr = np.array(image.convert("RGB").resize(IMAGE_SIZE), dtype=np.uint8)
|
| 490 |
+
mask_3ch = np.stack([mask * 0, mask * 100, mask * 255], axis=-1).astype(np.uint8)
|
| 491 |
+
overlay = cv2.addWeighted(orig_arr, 0.7, mask_3ch, 0.3, 0)
|
| 492 |
+
|
| 493 |
+
return {
|
| 494 |
+
"mask": mask,
|
| 495 |
+
"overlay": overlay,
|
| 496 |
+
"gradcam": gradcam_img,
|
| 497 |
+
"depth_map": depth_info["depth_map_vis"],
|
| 498 |
+
"depth_overlay": depth_info["depth_overlay"],
|
| 499 |
+
"depth_info": depth_info,
|
| 500 |
+
"risk": risk,
|
| 501 |
+
}
|
models/flood_best_model_export.keras
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0406e44d9a989c2899823a60646ed9076c01b884c264953e87a9bc9ec48c1bf8
|
| 3 |
+
size 26297341
|
models/flood_model_production.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2934609d3bc1473cab0599d6288a632547a51d773abc5b636990cd3f9e2c21b1
|
| 3 |
+
size 873
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
tensorflow-cpu>=2.16.0
|
| 2 |
+
numpy>=1.24.0
|
| 3 |
+
opencv-python-headless>=4.8.0
|
| 4 |
+
Pillow>=10.0.0
|
| 5 |
+
gradio>=6.0.0
|
| 6 |
+
torch>=2.1.0
|
| 7 |
+
transformers>=4.40.0,<4.50.0
|
| 8 |
+
timm>=0.9.0
|