Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +10 -0
- Dockerfile +18 -0
- FirenetCNN1.h5 +3 -0
- Meta OpenEnv Hackathon_ Guidelines.md +158 -0
- README.md +184 -6
- __init__.py +16 -0
- client.py +99 -0
- debug_env.py +35 -0
- environments/wildfire_detection/__init__.py +11 -0
- environments/wildfire_detection/firenet_inference.py +187 -0
- environments/wildfire_detection/sample_frames/fire_000.jpg +3 -0
- environments/wildfire_detection/sample_frames/fire_001.jpg +3 -0
- environments/wildfire_detection/sample_frames/fire_002.jpg +3 -0
- environments/wildfire_detection/sample_frames/fire_003.jpg +3 -0
- environments/wildfire_detection/sample_frames/fire_004.jpg +3 -0
- environments/wildfire_detection/sample_frames/fire_005.jpg +3 -0
- environments/wildfire_detection/sample_frames/fire_015.png +0 -0
- environments/wildfire_detection/sample_frames/no_fire_008.jpg +3 -0
- environments/wildfire_detection/sample_frames/no_fire_009.jpg +3 -0
- environments/wildfire_detection/sample_frames/no_fire_013.jpg +0 -0
- environments/wildfire_detection/sample_frames/no_fire_014.jpg +0 -0
- environments/wildfire_detection/sample_frames/smoke_006.jpg +3 -0
- environments/wildfire_detection/sample_frames/smoke_007.jpg +3 -0
- environments/wildfire_detection/sample_frames/smoke_010.jpg +0 -0
- environments/wildfire_detection/sample_frames/smoke_011.jpg +0 -0
- environments/wildfire_detection/sample_frames/smoke_012.jpg +0 -0
- environments/wildfire_detection/wildfire_env.py +393 -0
- inference.py +261 -0
- inference_rl.py +120 -0
- models.py +42 -0
- openenv.yaml +6 -0
- openenv_wildfire_detection.egg-info/PKG-INFO +14 -0
- openenv_wildfire_detection.egg-info/SOURCES.txt +30 -0
- openenv_wildfire_detection.egg-info/dependency_links.txt +1 -0
- openenv_wildfire_detection.egg-info/entry_points.txt +2 -0
- openenv_wildfire_detection.egg-info/requires.txt +10 -0
- openenv_wildfire_detection.egg-info/top_level.txt +1 -0
- push_hf.py +21 -0
- pyproject.toml +40 -0
- q_model.pkl +3 -0
- requirements.txt +107 -0
- run_server.py +7 -0
- server/__init__.py +11 -0
- server/app.py +64 -0
- server/multipen_environment.py +104 -0
- server/requirements.txt +9 -0
- server/wildfire_environment.py +102 -0
- test_env.py +19 -0
- train_rl.py +209 -0
- uv.lock +0 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,13 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
environments/wildfire_detection/sample_frames/fire_000.jpg filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
environments/wildfire_detection/sample_frames/fire_001.jpg filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
environments/wildfire_detection/sample_frames/fire_002.jpg filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
environments/wildfire_detection/sample_frames/fire_003.jpg filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
environments/wildfire_detection/sample_frames/fire_004.jpg filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
environments/wildfire_detection/sample_frames/fire_005.jpg filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
environments/wildfire_detection/sample_frames/no_fire_008.jpg filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
environments/wildfire_detection/sample_frames/no_fire_009.jpg filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
environments/wildfire_detection/sample_frames/smoke_006.jpg filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
environments/wildfire_detection/sample_frames/smoke_007.jpg filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Wildfire Detection OpenEnv Environment
|
| 2 |
+
|
| 3 |
+
ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
|
| 4 |
+
FROM ${BASE_IMAGE}
|
| 5 |
+
|
| 6 |
+
WORKDIR /app
|
| 7 |
+
|
| 8 |
+
COPY . /app/env
|
| 9 |
+
|
| 10 |
+
WORKDIR /app/env
|
| 11 |
+
|
| 12 |
+
# Install dependencies directly
|
| 13 |
+
RUN pip install --no-cache-dir -e .
|
| 14 |
+
|
| 15 |
+
EXPOSE 8000
|
| 16 |
+
|
| 17 |
+
ENV ENABLE_WEB_INTERFACE=true
|
| 18 |
+
CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "8000"]
|
FirenetCNN1.h5
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d3bc11fdafb36cf0e43ea1af44015097a62a67118b43d27f0badd2b5867c8525
|
| 3 |
+
size 25352336
|
Meta OpenEnv Hackathon_ Guidelines.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## **🚀 Hackathon Submission Guidelines (OpenEnv RL Challenge)**
|
| 2 |
+
|
| 3 |
+
### **1\. Project Structure**
|
| 4 |
+
|
| 5 |
+
* Your inference script **must** be named inference.py
|
| 6 |
+
* It **must be located in the root directory** of your project
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
### **2\. LLM Usage Requirements**
|
| 11 |
+
|
| 12 |
+
* You **must use the OpenAI Client** for all LLM calls
|
| 13 |
+
* Do **not** use alternative SDKs or direct HTTP calls
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
### **3\. Required Environment Variables**
|
| 18 |
+
|
| 19 |
+
Your inference.py must read the following environment variables:
|
| 20 |
+
|
| 21 |
+
* API\_BASE\_URL
|
| 22 |
+
* Description: API endpoint for the LLM
|
| 23 |
+
* Requirement: **Must include a default value**
|
| 24 |
+
* MODEL\_NAME
|
| 25 |
+
* Description: Model identifier used for inference
|
| 26 |
+
* Requirement: **Must include a default value**
|
| 27 |
+
* HF\_TOKEN
|
| 28 |
+
* Description: Hugging Face API token
|
| 29 |
+
* Requirement: **Mandatory (no default required)**
|
| 30 |
+
|
| 31 |
+
**4\. INFERENCE OUTPUT FORMAT**
|
| 32 |
+
|
| 33 |
+
The script must emit exactly three line types to stdout, in this order:
|
| 34 |
+
\[START\] task=\<task\_name\> env=\<benchmark\> model=\<model\_name\>
|
| 35 |
+
\[STEP\] step=\<n\> action=\<action\_str\> reward=\<0.00\> done=\<true|false\> error=\<msg|null\>
|
| 36 |
+
\[END\] success=\<true|false\> steps=\<n\> rewards=\<r1,r2,...,rn\>
|
| 37 |
+
|
| 38 |
+
Rules:
|
| 39 |
+
\- One \[START\] line at episode begin.
|
| 40 |
+
\- One \[STEP\] line per step, immediately after env.step() returns.
|
| 41 |
+
\- One \[END\] line after env.close(), always emitted (even on exception).
|
| 42 |
+
\- reward and rewards are formatted to 2 decimal places.
|
| 43 |
+
\- done and success are lowercase booleans: true or false.
|
| 44 |
+
\- error is the raw last\_action\_error string, or null if none.
|
| 45 |
+
\- All fields on a single line with no newlines within a line.
|
| 46 |
+
Example:
|
| 47 |
+
\[START\] task=click-test env=miniwob model=Qwen3-VL-30B
|
| 48 |
+
\[STEP\] step=1 action=click('123') reward=0.00 done=false error=null
|
| 49 |
+
\[STEP\] step=2 action=fill('456','text') reward=0.00 done=false error=null
|
| 50 |
+
\[STEP\] step=3 action=click('789') reward=1.00 done=true error=null
|
| 51 |
+
\[END\] success=true steps=3 rewards=0.00,0.00,1.00
|
| 52 |
+
|
| 53 |
+
#### **✅ Example (inference.py)**
|
| 54 |
+
|
| 55 |
+
```py
|
| 56 |
+
import os
|
| 57 |
+
from openai import OpenAI
|
| 58 |
+
|
| 59 |
+
# Read environment variables with defaults where required
|
| 60 |
+
API_BASE_URL = os.getenv("API_BASE_URL", "https://api.openai.com/v1")
|
| 61 |
+
MODEL_NAME = os.getenv("MODEL_NAME", "gpt-4.1-mini")
|
| 62 |
+
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 63 |
+
|
| 64 |
+
if HF_TOKEN is None:
|
| 65 |
+
raise ValueError("HF_TOKEN environment variable is required")
|
| 66 |
+
|
| 67 |
+
# Initialize OpenAI client
|
| 68 |
+
client = OpenAI(
|
| 69 |
+
base_url=API_BASE_URL,
|
| 70 |
+
api_key=HF_TOKEN
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
def run_inference(prompt: str):
|
| 74 |
+
response = client.chat.completions.create(
|
| 75 |
+
model=MODEL_NAME,
|
| 76 |
+
messages=[
|
| 77 |
+
{"role": "user", "content": prompt}
|
| 78 |
+
]
|
| 79 |
+
)
|
| 80 |
+
response = response.choices[0].message.content
|
| 81 |
+
# Print output based on above given format
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
if __name__ == "__main__":
|
| 85 |
+
print(run_inference("Hello from OpenEnv!"))
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
---
|
| 89 |
+
|
| 90 |
+
### **4\. Hugging Face Space Guidelines**
|
| 91 |
+
|
| 92 |
+
* Building a Hugging Face Space can take significant time, especially if multiple spaces are active
|
| 93 |
+
* To avoid delays:
|
| 94 |
+
* Turn off all unnecessary spaces
|
| 95 |
+
* Keep only your primary submission space running
|
| 96 |
+
|
| 97 |
+
---
|
| 98 |
+
|
| 99 |
+
### **5\. Submission Validation Rules**
|
| 100 |
+
|
| 101 |
+
* The system will **check if your Hugging Face Space is live**
|
| 102 |
+
* If your space is **not in a running state**, your submission will **fail automatically**
|
| 103 |
+
|
| 104 |
+
Before submitting:
|
| 105 |
+
|
| 106 |
+
* Ensure your space is fully built
|
| 107 |
+
* Confirm it is in the **“Running”** state
|
| 108 |
+
|
| 109 |
+
---
|
| 110 |
+
|
| 111 |
+
### **6\. Hardware Requirements**
|
| 112 |
+
|
| 113 |
+
* Your solution will be executed inside a Docker container with limited resources
|
| 114 |
+
* It **must run within the following constraints**:
|
| 115 |
+
* **2 vCPU**
|
| 116 |
+
* **8 GB RAM**
|
| 117 |
+
|
| 118 |
+
👉 Ensure your model, dependencies, and runtime fit within these limits. Submissions exceeding these constraints may fail during evaluation.
|
| 119 |
+
|
| 120 |
+
---
|
| 121 |
+
|
| 122 |
+
### **6\. Resubmissions**
|
| 123 |
+
|
| 124 |
+
* You are allowed to **resubmit your project multiple times**
|
| 125 |
+
* If your submission fails validation, you can:
|
| 126 |
+
* Fix the issues
|
| 127 |
+
* Ensure your Hugging Face Space is running
|
| 128 |
+
* Submit again
|
| 129 |
+
|
| 130 |
+
👉 There is **no penalty for resubmitting**, so iterate until your submission passes all checks.
|
| 131 |
+
|
| 132 |
+
---
|
| 133 |
+
|
| 134 |
+
### **⚠️ Common Failure Cases (Avoid These)**
|
| 135 |
+
|
| 136 |
+
* inference.py not in root directory
|
| 137 |
+
* Missing default values for API\_BASE\_URL or MODEL\_NAME
|
| 138 |
+
* Missing HF\_TOKEN
|
| 139 |
+
* Hugging Face Space still building during submission
|
| 140 |
+
* Space stopped due to multiple active deployments
|
| 141 |
+
|
| 142 |
+
### **🚀 Reference projects to guide you**
|
| 143 |
+
|
| 144 |
+
Here are some strong examples from the San Francisco edition to help you understand how to structure your environment:
|
| 145 |
+
|
| 146 |
+
* Calendar Environment Server
|
| 147 |
+
[https://github.com/meta-pytorch/OpenEnv/tree/main/envs/calendar\_env](https://github.com/meta-pytorch/OpenEnv/tree/main/envs/calendar_env)
|
| 148 |
+
* Reasoning Gym Environment Server
|
| 149 |
+
[https://github.com/meta-pytorch/OpenEnv/tree/main/envs/reasoning\_gym\_env](https://github.com/meta-pytorch/OpenEnv/tree/main/envs/reasoning_gym_env)
|
| 150 |
+
* TB2 Environment Server
|
| 151 |
+
[https://github.com/meta-pytorch/OpenEnv/tree/main/envs/tbench2\_env](https://github.com/meta-pytorch/OpenEnv/tree/main/envs/tbench2_env)
|
| 152 |
+
* CARLA Environment Server
|
| 153 |
+
[https://github.com/meta-pytorch/OpenEnv/tree/main/envs/carla\_env](https://github.com/meta-pytorch/OpenEnv/tree/main/envs/carla_env)
|
| 154 |
+
* REPL Environment Server
|
| 155 |
+
[https://github.com/meta-pytorch/OpenEnv/tree/main/envs/repl\_env](https://github.com/meta-pytorch/OpenEnv/tree/main/envs/repl_env)
|
| 156 |
+
|
| 157 |
+
Use these as direction, not as templates. Focus on understanding structure and approach.
|
| 158 |
+
|
README.md
CHANGED
|
@@ -1,11 +1,189 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
---
|
| 10 |
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Wildfire Detection Environment
|
| 3 |
+
emoji: 🔥
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: yellow
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
+
app_port: 8000
|
| 9 |
+
base_path: /web
|
| 10 |
+
tags:
|
| 11 |
+
- openenv
|
| 12 |
+
- wildfire-detection
|
| 13 |
+
- reinforcement-learning
|
| 14 |
+
- firenet-cnn
|
| 15 |
---
|
| 16 |
|
| 17 |
+
# Wildfire Detection RL Environment
|
| 18 |
+
|
| 19 |
+
An OpenEnv-compatible reinforcement learning environment for autonomous wildfire patrol using the FirenetCNN model. The agent receives sequential camera observations, runs inference, and decides actions to detect fire or smoke as early as possible.
|
| 20 |
+
|
| 21 |
+
## Features
|
| 22 |
+
|
| 23 |
+
- **Exact FirenetCNN Inference** - Uses the original model from the Forest-Fire-Detection-Using-FirenetCNN-and-XAI-Techniques repository
|
| 24 |
+
- **Grad-CAM XAI** - Explainable AI with heatmap visualization for model decisions
|
| 25 |
+
- **Sequential Decision Making** - Agent processes multiple frames and learns from context
|
| 26 |
+
- **Reward-based Learning** - Optimizes for early detection while minimizing false alarms
|
| 27 |
+
|
| 28 |
+
## Quick Start
|
| 29 |
+
|
| 30 |
+
### Using the Docker Image
|
| 31 |
+
|
| 32 |
+
```bash
|
| 33 |
+
# Pull and run the pre-built image
|
| 34 |
+
docker run -p 8000:8000 wildfire-detection
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
### Building from Source
|
| 38 |
+
|
| 39 |
+
```bash
|
| 40 |
+
# Build the Docker image
|
| 41 |
+
docker build -t wildfire-detection .
|
| 42 |
+
|
| 43 |
+
# Run the container
|
| 44 |
+
docker run -p 8000:8000 wildfire-detection
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
### Local Development
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
# Install dependencies
|
| 51 |
+
pip install -e .
|
| 52 |
+
|
| 53 |
+
# Run the server
|
| 54 |
+
python run_server.py
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
## API Usage
|
| 58 |
+
|
| 59 |
+
### Reset Environment
|
| 60 |
+
|
| 61 |
+
```bash
|
| 62 |
+
curl -X POST http://localhost:8000/reset
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
### Take Action
|
| 66 |
+
|
| 67 |
+
```bash
|
| 68 |
+
curl -X POST http://localhost:8000/step \
|
| 69 |
+
-H "Content-Type: application/json" \
|
| 70 |
+
-d '{"action": "Alert"}'
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
### Get Schema
|
| 74 |
+
|
| 75 |
+
```bash
|
| 76 |
+
curl http://localhost:8000/schema
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
Access the interactive API docs at http://localhost:8000/docs
|
| 80 |
+
|
| 81 |
+
## Inference Script
|
| 82 |
+
|
| 83 |
+
The `inference.py` script runs an LLM agent that makes decisions:
|
| 84 |
+
|
| 85 |
+
```bash
|
| 86 |
+
# Set API key (injected by judges at evaluation)
|
| 87 |
+
export API_KEY=your_huggingface_token
|
| 88 |
+
|
| 89 |
+
# Run inference
|
| 90 |
+
python inference.py
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
### Environment Variables
|
| 94 |
+
|
| 95 |
+
| Variable | Default | Required |
|
| 96 |
+
|----------|---------|----------|
|
| 97 |
+
| `API_BASE_URL` | `https://router.huggingface.co/v1` | No |
|
| 98 |
+
| `MODEL_NAME` | `Qwen/Qwen2.5-3B-Instruct` | No |
|
| 99 |
+
| `API_KEY` | - | Yes |
|
| 100 |
+
|
| 101 |
+
### Output Format
|
| 102 |
+
|
| 103 |
+
The script follows the hackathon output format:
|
| 104 |
+
|
| 105 |
+
```
|
| 106 |
+
[START] task=wildfire_detection env=wildfire_detection model=Qwen/Qwen2.5-3B-Instruct
|
| 107 |
+
[STEP] step=1 action=Alert reward=-1.00 done=false error=null
|
| 108 |
+
[STEP] step=2 action=Scan reward=0.50 done=false error=null
|
| 109 |
+
[END] success=true steps=16 rewards=-1.00,0.50,2.00,...
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
## Actions
|
| 113 |
+
|
| 114 |
+
| Action | Description |
|
| 115 |
+
|--------|-------------|
|
| 116 |
+
| `Alert` | Raise immediate alert if fire/smoke detected |
|
| 117 |
+
| `Scan` | Continue scanning/observing the current frame |
|
| 118 |
+
| `Ignore` | No action needed, clear conditions |
|
| 119 |
+
| `Deploy` | Deploy resources to the detected location |
|
| 120 |
+
|
| 121 |
+
## Reward Structure
|
| 122 |
+
|
| 123 |
+
| Reward | Condition |
|
| 124 |
+
|--------|-----------|
|
| 125 |
+
| +2.00 | Correct detection with confidence ≥ 0.85 and clear Grad-CAM alignment |
|
| 126 |
+
| +1.00 | Correct class but lower confidence |
|
| 127 |
+
| +0.50 | Partial credit (right class, weak XAI) |
|
| 128 |
+
| -0.75 | False positive (Alert/Deploy on no_fire) |
|
| 129 |
+
| -1.00 | False negative (missed fire/smoke) |
|
| 130 |
+
| -0.50 | Inaction ("Ignore") when fire/smoke is present |
|
| 131 |
+
| -2.00 | Terminal failure (too many missed detections) |
|
| 132 |
+
|
| 133 |
+
## Grading
|
| 134 |
+
|
| 135 |
+
The grader evaluates each episode:
|
| 136 |
+
- **Success**: All fire/smoke frames correctly alerted with zero false positives
|
| 137 |
+
- **Score**: Normalized 0.0-1.0 based on cumulative reward
|
| 138 |
+
|
| 139 |
+
## Project Structure
|
| 140 |
+
|
| 141 |
+
```
|
| 142 |
+
multipen/
|
| 143 |
+
├── inference.py # LLM agent inference script
|
| 144 |
+
├── wildfire_env.py # RL environment (Gym-compatible)
|
| 145 |
+
├── firenet_inference.py # FirenetCNN + Grad-CAM
|
| 146 |
+
├── models.py # Action/Observation models
|
| 147 |
+
├── openenv.yaml # OpenEnv configuration
|
| 148 |
+
├── pyproject.toml # Dependencies
|
| 149 |
+
├── Dockerfile # Container definition
|
| 150 |
+
├── server/
|
| 151 |
+
│ ├── app.py # FastAPI server
|
| 152 |
+
│ └── wildfire_environment.py # OpenEnv wrapper
|
| 153 |
+
└── environments/
|
| 154 |
+
└── wildfire_detection/
|
| 155 |
+
├── wildfire_env.py # Environment implementation
|
| 156 |
+
├── firenet_inference.py # Model inference
|
| 157 |
+
└── sample_frames/ # Test images (16 frames)
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
## Deployment to Hugging Face Spaces
|
| 161 |
+
|
| 162 |
+
```bash
|
| 163 |
+
# Push to Hugging Face
|
| 164 |
+
openenv push
|
| 165 |
+
|
| 166 |
+
# Or with options
|
| 167 |
+
openenv push --namespace my-org --private
|
| 168 |
+
```
|
| 169 |
+
|
| 170 |
+
## Testing
|
| 171 |
+
|
| 172 |
+
```bash
|
| 173 |
+
# Test environment directly
|
| 174 |
+
python test_env.py
|
| 175 |
+
|
| 176 |
+
# Test API
|
| 177 |
+
curl http://localhost:8000/health
|
| 178 |
+
curl http://localhost:8000/schema
|
| 179 |
+
```
|
| 180 |
+
|
| 181 |
+
## Model
|
| 182 |
+
|
| 183 |
+
Uses FirenetCNN (MobileNetV2-based) from:
|
| 184 |
+
https://github.com/OpelSpeedster/Forest-Fire-Detection-Using-FirenetCNN-and-XAI-Techniques
|
| 185 |
+
|
| 186 |
+
The model classifies images into:
|
| 187 |
+
- `no_fire` - No fire detected
|
| 188 |
+
- `fire` - Fire detected
|
| 189 |
+
- `smoke` - Smoke detected
|
__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""Multipen Environment."""
|
| 8 |
+
|
| 9 |
+
from .client import MultipenEnv
|
| 10 |
+
from .models import MultipenAction, MultipenObservation
|
| 11 |
+
|
| 12 |
+
__all__ = [
|
| 13 |
+
"MultipenAction",
|
| 14 |
+
"MultipenObservation",
|
| 15 |
+
"MultipenEnv",
|
| 16 |
+
]
|
client.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""Multipen Environment Client."""
|
| 8 |
+
|
| 9 |
+
from typing import Dict
|
| 10 |
+
|
| 11 |
+
from openenv.core import EnvClient
|
| 12 |
+
from openenv.core.client_types import StepResult
|
| 13 |
+
from openenv.core.env_server.types import State
|
| 14 |
+
|
| 15 |
+
from .models import MultipenAction, MultipenObservation
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class MultipenEnv(
|
| 19 |
+
EnvClient[MultipenAction, MultipenObservation, State]
|
| 20 |
+
):
|
| 21 |
+
"""
|
| 22 |
+
Client for the Multipen Environment.
|
| 23 |
+
|
| 24 |
+
This client maintains a persistent WebSocket connection to the environment server,
|
| 25 |
+
enabling efficient multi-step interactions with lower latency.
|
| 26 |
+
Each client instance has its own dedicated environment session on the server.
|
| 27 |
+
|
| 28 |
+
Example:
|
| 29 |
+
>>> # Connect to a running server
|
| 30 |
+
>>> with MultipenEnv(base_url="http://localhost:8000") as client:
|
| 31 |
+
... result = client.reset()
|
| 32 |
+
... print(result.observation.echoed_message)
|
| 33 |
+
...
|
| 34 |
+
... result = client.step(MultipenAction(message="Hello!"))
|
| 35 |
+
... print(result.observation.echoed_message)
|
| 36 |
+
|
| 37 |
+
Example with Docker:
|
| 38 |
+
>>> # Automatically start container and connect
|
| 39 |
+
>>> client = MultipenEnv.from_docker_image("multipen-env:latest")
|
| 40 |
+
>>> try:
|
| 41 |
+
... result = client.reset()
|
| 42 |
+
... result = client.step(MultipenAction(message="Test"))
|
| 43 |
+
... finally:
|
| 44 |
+
... client.close()
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
def _step_payload(self, action: MultipenAction) -> Dict:
|
| 48 |
+
"""
|
| 49 |
+
Convert MultipenAction to JSON payload for step message.
|
| 50 |
+
|
| 51 |
+
Args:
|
| 52 |
+
action: MultipenAction instance
|
| 53 |
+
|
| 54 |
+
Returns:
|
| 55 |
+
Dictionary representation suitable for JSON encoding
|
| 56 |
+
"""
|
| 57 |
+
return {
|
| 58 |
+
"message": action.message,
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
def _parse_result(self, payload: Dict) -> StepResult[MultipenObservation]:
|
| 62 |
+
"""
|
| 63 |
+
Parse server response into StepResult[MultipenObservation].
|
| 64 |
+
|
| 65 |
+
Args:
|
| 66 |
+
payload: JSON response data from server
|
| 67 |
+
|
| 68 |
+
Returns:
|
| 69 |
+
StepResult with MultipenObservation
|
| 70 |
+
"""
|
| 71 |
+
obs_data = payload.get("observation", {})
|
| 72 |
+
observation = MultipenObservation(
|
| 73 |
+
echoed_message=obs_data.get("echoed_message", ""),
|
| 74 |
+
message_length=obs_data.get("message_length", 0),
|
| 75 |
+
done=payload.get("done", False),
|
| 76 |
+
reward=payload.get("reward"),
|
| 77 |
+
metadata=obs_data.get("metadata", {}),
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
return StepResult(
|
| 81 |
+
observation=observation,
|
| 82 |
+
reward=payload.get("reward"),
|
| 83 |
+
done=payload.get("done", False),
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
def _parse_state(self, payload: Dict) -> State:
|
| 87 |
+
"""
|
| 88 |
+
Parse server response into State object.
|
| 89 |
+
|
| 90 |
+
Args:
|
| 91 |
+
payload: JSON response from state request
|
| 92 |
+
|
| 93 |
+
Returns:
|
| 94 |
+
State object with episode_id and step_count
|
| 95 |
+
"""
|
| 96 |
+
return State(
|
| 97 |
+
episode_id=payload.get("episode_id"),
|
| 98 |
+
step_count=payload.get("step_count", 0),
|
| 99 |
+
)
|
debug_env.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import os
|
| 3 |
+
import numpy as np
|
| 4 |
+
|
| 5 |
+
sys.path.insert(0, r"C:\Users\ASUS\OneDrive\Desktop\RL\multipen")
|
| 6 |
+
|
| 7 |
+
from environments.wildfire_detection.wildfire_env import WildfireDetectionEnv
|
| 8 |
+
|
| 9 |
+
env = WildfireDetectionEnv()
|
| 10 |
+
actions = ["Alert", "Scan", "Ignore", "Deploy"]
|
| 11 |
+
|
| 12 |
+
print("=== Analyzing Environment ===")
|
| 13 |
+
|
| 14 |
+
for episode in range(2):
|
| 15 |
+
print(f"\n--- Episode {episode + 1} ---")
|
| 16 |
+
obs = env.reset()
|
| 17 |
+
|
| 18 |
+
for step in range(3):
|
| 19 |
+
gt = obs["ground_truth"]
|
| 20 |
+
if hasattr(gt, "__len__") and gt.ndim > 0:
|
| 21 |
+
gt_idx = int(gt[0])
|
| 22 |
+
else:
|
| 23 |
+
gt_idx = int(gt)
|
| 24 |
+
gt_label = ["fire", "smoke", "no_fire"][gt_idx]
|
| 25 |
+
|
| 26 |
+
print(f"Step {step + 1}: GT={gt_label}, Pred={obs['prediction']}")
|
| 27 |
+
|
| 28 |
+
for a_idx, a_name in enumerate(actions):
|
| 29 |
+
obs2, r, done, info = env.step(a_idx)
|
| 30 |
+
print(f" {a_name}: reward={r:.2f}")
|
| 31 |
+
|
| 32 |
+
if step < 2:
|
| 33 |
+
obs = obs2
|
| 34 |
+
|
| 35 |
+
env.close()
|
environments/wildfire_detection/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Wildfire Detection OpenEnv Environment
|
| 2 |
+
|
| 3 |
+
This package provides a minimal OpenEnv-compatible RL environment
|
| 4 |
+
that follows the WildfireDetection skill specification. It is designed
|
| 5 |
+
to be extended with the exact FirenetCNN inference pipeline from the
|
| 6 |
+
opposed GitHub repo without modifying that repository.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from .wildfire_env import WildfireDetectionEnv
|
| 10 |
+
|
| 11 |
+
__all__ = ["WildfireDetectionEnv"]
|
environments/wildfire_detection/firenet_inference.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FirenetCNN Inference Module
|
| 3 |
+
|
| 4 |
+
This module provides the exact inference and Grad-CAM implementation
|
| 5 |
+
from the Forest-Fire-Detection-Using-FirenetCNN-and-XAI-Techniques repository.
|
| 6 |
+
No modifications to the original repo code.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import cv2
|
| 11 |
+
import numpy as np
|
| 12 |
+
import tensorflow as tf
|
| 13 |
+
from tensorflow.keras.models import load_model, Model
|
| 14 |
+
from typing import Tuple, Dict, Any, Optional
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
MODEL_PATH = os.getenv("FIRENET_MODEL_PATH", "FirenetCNN1.h5")
|
| 18 |
+
TARGET_SIZE = (224, 224)
|
| 19 |
+
CLASS_LABELS = ["no_fire", "fire", "smoke"]
|
| 20 |
+
LAST_CONV_LAYER_NAME = "out_relu"
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def make_gradcam_heatmap(img_array, model, last_conv_layer_name, pred_index):
|
| 24 |
+
"""Creates a Grad-CAM heatmap."""
|
| 25 |
+
grad_model = Model(
|
| 26 |
+
[model.inputs], [model.get_layer(last_conv_layer_name).output, model.output]
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
with tf.GradientTape() as tape:
|
| 30 |
+
last_conv_layer_output, preds = grad_model(img_array)
|
| 31 |
+
class_channel = preds[:, pred_index]
|
| 32 |
+
|
| 33 |
+
grads = tape.gradient(class_channel, last_conv_layer_output)
|
| 34 |
+
pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2))
|
| 35 |
+
|
| 36 |
+
last_conv_layer_output = last_conv_layer_output[0]
|
| 37 |
+
heatmap = last_conv_layer_output @ pooled_grads[..., tf.newaxis]
|
| 38 |
+
heatmap = tf.squeeze(heatmap)
|
| 39 |
+
|
| 40 |
+
heatmap = tf.maximum(heatmap, 0) / tf.math.reduce_max(heatmap)
|
| 41 |
+
return heatmap.numpy()
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def overlay_heatmap(original_img, heatmap, alpha=0.4):
|
| 45 |
+
"""Applies the heatmap to the original image."""
|
| 46 |
+
heatmap = cv2.resize(heatmap, (original_img.shape[1], original_img.shape[0]))
|
| 47 |
+
heatmap = np.uint8(255 * heatmap)
|
| 48 |
+
heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)
|
| 49 |
+
superimposed_img = heatmap * alpha + original_img
|
| 50 |
+
superimposed_img = np.clip(superimposed_img, 0, 255).astype(np.uint8)
|
| 51 |
+
return superimposed_img
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class FirenetInference:
|
| 55 |
+
"""Wrapper class for FirenetCNN inference with Grad-CAM."""
|
| 56 |
+
|
| 57 |
+
def __init__(self, model_path: Optional[str] = None):
|
| 58 |
+
self.model_path = model_path or MODEL_PATH
|
| 59 |
+
self.model = None
|
| 60 |
+
self._load_model()
|
| 61 |
+
|
| 62 |
+
def _load_model(self):
|
| 63 |
+
"""Load the FirenetCNN model."""
|
| 64 |
+
if os.path.exists(self.model_path):
|
| 65 |
+
self.model = load_model(self.model_path, compile=False)
|
| 66 |
+
else:
|
| 67 |
+
raise FileNotFoundError(f"Model not found: {self.model_path}")
|
| 68 |
+
|
| 69 |
+
def predict(self, image: np.ndarray) -> Dict[str, Any]:
|
| 70 |
+
"""
|
| 71 |
+
Run inference on an image and return prediction + Grad-CAM info.
|
| 72 |
+
|
| 73 |
+
Args:
|
| 74 |
+
image: BGR image (from cv2.imread) or RGB image
|
| 75 |
+
|
| 76 |
+
Returns:
|
| 77 |
+
dict with keys:
|
| 78 |
+
- label: 'fire', 'smoke', or 'no_fire'
|
| 79 |
+
- confidence: float (0.0-1.0)
|
| 80 |
+
- class_idx: int (0=no_fire, 1=fire, 2=smoke)
|
| 81 |
+
- heatmap: numpy array or None
|
| 82 |
+
- gradcam_summary: str describing heatmap location
|
| 83 |
+
"""
|
| 84 |
+
original_image = image.copy()
|
| 85 |
+
|
| 86 |
+
if image.shape[:2] != TARGET_SIZE[::-1]:
|
| 87 |
+
image = cv2.resize(image, TARGET_SIZE)
|
| 88 |
+
|
| 89 |
+
input_image_scaled = image.astype(np.float32) / 255.0
|
| 90 |
+
input_image_batch = np.expand_dims(input_image_scaled, axis=0)
|
| 91 |
+
|
| 92 |
+
preds = self.model.predict(input_image_batch, verbose=0)
|
| 93 |
+
confidence = float(np.max(preds[0]))
|
| 94 |
+
class_idx = int(np.argmax(preds[0]))
|
| 95 |
+
label = CLASS_LABELS[class_idx]
|
| 96 |
+
|
| 97 |
+
heatmap = None
|
| 98 |
+
gradcam_summary = "N/A"
|
| 99 |
+
|
| 100 |
+
if label != "no_fire":
|
| 101 |
+
heatmap = make_gradcam_heatmap(
|
| 102 |
+
input_image_batch, self.model, LAST_CONV_LAYER_NAME, class_idx
|
| 103 |
+
)
|
| 104 |
+
gradcam_summary = self._summarize_heatmap(heatmap)
|
| 105 |
+
|
| 106 |
+
return {
|
| 107 |
+
"label": label,
|
| 108 |
+
"confidence": confidence,
|
| 109 |
+
"class_idx": class_idx,
|
| 110 |
+
"heatmap": heatmap,
|
| 111 |
+
"gradcam_summary": gradcam_summary,
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
def _summarize_heatmap(self, heatmap: np.ndarray) -> str:
|
| 115 |
+
"""
|
| 116 |
+
Summarize the heatmap location as center/left/right/top/bottom.
|
| 117 |
+
"""
|
| 118 |
+
h, w = heatmap.shape
|
| 119 |
+
threshold = 0.5
|
| 120 |
+
|
| 121 |
+
heatmap_thresh = (heatmap > threshold).astype(np.float32)
|
| 122 |
+
|
| 123 |
+
h_half = h // 2
|
| 124 |
+
w_half = w // 2
|
| 125 |
+
|
| 126 |
+
top_region = heatmap_thresh[:h_half, :].sum()
|
| 127 |
+
bottom_region = heatmap_thresh[h_half:, :].sum()
|
| 128 |
+
left_region = heatmap_thresh[:, :w_half].sum()
|
| 129 |
+
right_region = heatmap_thresh[:, w_half:].sum()
|
| 130 |
+
center_region = heatmap_thresh[h // 4 : 3 * h // 4, w // 4 : 3 * w // 4].sum()
|
| 131 |
+
|
| 132 |
+
total = heatmap_thresh.sum()
|
| 133 |
+
if total == 0:
|
| 134 |
+
return "no significant hotspot"
|
| 135 |
+
|
| 136 |
+
regions = {
|
| 137 |
+
"center": center_region,
|
| 138 |
+
"top": top_region,
|
| 139 |
+
"bottom": bottom_region,
|
| 140 |
+
"left": left_region,
|
| 141 |
+
"right": right_region,
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
dominant = max(regions, key=regions.get)
|
| 145 |
+
return f"hotspot at {dominant}"
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def load_image_for_inference(image_path: str) -> Optional[np.ndarray]:
|
| 149 |
+
"""Load and preprocess an image for inference."""
|
| 150 |
+
if not os.path.exists(image_path):
|
| 151 |
+
return None
|
| 152 |
+
image = cv2.imread(image_path)
|
| 153 |
+
return image
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def extract_video_frames(video_path: str, frame_interval: int = 10) -> list:
|
| 157 |
+
"""
|
| 158 |
+
Extract frames from a video at regular intervals.
|
| 159 |
+
|
| 160 |
+
Args:
|
| 161 |
+
video_path: Path to video file
|
| 162 |
+
frame_interval: Extract every N frames
|
| 163 |
+
|
| 164 |
+
Returns:
|
| 165 |
+
List of frames as numpy arrays
|
| 166 |
+
"""
|
| 167 |
+
frames = []
|
| 168 |
+
if not os.path.exists(video_path):
|
| 169 |
+
return frames
|
| 170 |
+
|
| 171 |
+
cap = cv2.VideoCapture(video_path)
|
| 172 |
+
if not cap.isOpened():
|
| 173 |
+
return frames
|
| 174 |
+
|
| 175 |
+
frame_count = 0
|
| 176 |
+
while cap.isOpened():
|
| 177 |
+
ret, frame = cap.read()
|
| 178 |
+
if not ret:
|
| 179 |
+
break
|
| 180 |
+
|
| 181 |
+
if frame_count % frame_interval == 0:
|
| 182 |
+
frames.append(frame)
|
| 183 |
+
|
| 184 |
+
frame_count += 1
|
| 185 |
+
|
| 186 |
+
cap.release()
|
| 187 |
+
return frames
|
environments/wildfire_detection/sample_frames/fire_000.jpg
ADDED
|
Git LFS Details
|
environments/wildfire_detection/sample_frames/fire_001.jpg
ADDED
|
Git LFS Details
|
environments/wildfire_detection/sample_frames/fire_002.jpg
ADDED
|
Git LFS Details
|
environments/wildfire_detection/sample_frames/fire_003.jpg
ADDED
|
Git LFS Details
|
environments/wildfire_detection/sample_frames/fire_004.jpg
ADDED
|
Git LFS Details
|
environments/wildfire_detection/sample_frames/fire_005.jpg
ADDED
|
Git LFS Details
|
environments/wildfire_detection/sample_frames/fire_015.png
ADDED
|
environments/wildfire_detection/sample_frames/no_fire_008.jpg
ADDED
|
Git LFS Details
|
environments/wildfire_detection/sample_frames/no_fire_009.jpg
ADDED
|
Git LFS Details
|
environments/wildfire_detection/sample_frames/no_fire_013.jpg
ADDED
|
environments/wildfire_detection/sample_frames/no_fire_014.jpg
ADDED
|
environments/wildfire_detection/sample_frames/smoke_006.jpg
ADDED
|
Git LFS Details
|
environments/wildfire_detection/sample_frames/smoke_007.jpg
ADDED
|
Git LFS Details
|
environments/wildfire_detection/sample_frames/smoke_010.jpg
ADDED
|
environments/wildfire_detection/sample_frames/smoke_011.jpg
ADDED
|
environments/wildfire_detection/sample_frames/smoke_012.jpg
ADDED
|
environments/wildfire_detection/wildfire_env.py
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""WildfireDetectionEnv
|
| 2 |
+
OpenEnv-compatible environment for autonomous wildfire patrol.
|
| 3 |
+
|
| 4 |
+
Notes:
|
| 5 |
+
- Action space: ["Alert", "Scan", "Ignore", "Deploy"]
|
| 6 |
+
- Uses exact FirenetCNN inference from the repo (no modifications)
|
| 7 |
+
- Implements reward structure from skill description
|
| 8 |
+
- Includes grader for episode evaluation
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import os
|
| 14 |
+
import json
|
| 15 |
+
from typing import Dict, Any, Optional, Tuple, List
|
| 16 |
+
import numpy as np
|
| 17 |
+
from PIL import Image
|
| 18 |
+
|
| 19 |
+
try:
|
| 20 |
+
import gym
|
| 21 |
+
from gym import spaces
|
| 22 |
+
except Exception:
|
| 23 |
+
gym = None
|
| 24 |
+
spaces = None
|
| 25 |
+
|
| 26 |
+
class DummySpace:
|
| 27 |
+
def __init__(self, *args, **kwargs):
|
| 28 |
+
pass
|
| 29 |
+
|
| 30 |
+
def sample(self):
|
| 31 |
+
return None
|
| 32 |
+
|
| 33 |
+
class DummyEnv:
|
| 34 |
+
pass
|
| 35 |
+
|
| 36 |
+
spaces = type(
|
| 37 |
+
"spaces", (), {"Dict": DummySpace, "Box": DummySpace, "Discrete": DummySpace}
|
| 38 |
+
)
|
| 39 |
+
gym = type("gym", (), {"Env": DummyEnv})
|
| 40 |
+
|
| 41 |
+
try:
|
| 42 |
+
from .firenet_inference import FirenetInference, load_image_for_inference
|
| 43 |
+
except (ModuleNotFoundError, ImportError):
|
| 44 |
+
FirenetInference = None
|
| 45 |
+
load_image_for_inference = None
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class WildfireDetectionEnv(
|
| 49 |
+
gym.Env if "gym" in globals() and gym is not None else object
|
| 50 |
+
):
|
| 51 |
+
"""OpenEnv environment for wildfire detection using FirenetCNN."""
|
| 52 |
+
|
| 53 |
+
ACTIONS = ["Alert", "Scan", "Ignore", "Deploy"]
|
| 54 |
+
CLASS_LABELS = ["no_fire", "fire", "smoke"]
|
| 55 |
+
MAX_MISSES = 3
|
| 56 |
+
LOG_FILE = "wildfire_episode_log.txt"
|
| 57 |
+
|
| 58 |
+
def __init__(
|
| 59 |
+
self,
|
| 60 |
+
frames_dir: Optional[str] = None,
|
| 61 |
+
model_path: Optional[str] = None,
|
| 62 |
+
frames_labels: Optional[Dict[str, str]] = None,
|
| 63 |
+
):
|
| 64 |
+
super().__init__()
|
| 65 |
+
|
| 66 |
+
self.frames_dir = frames_dir or os.path.join(
|
| 67 |
+
os.path.dirname(__file__), "sample_frames"
|
| 68 |
+
)
|
| 69 |
+
self.model_path = model_path or os.path.join(
|
| 70 |
+
os.path.dirname(__file__),
|
| 71 |
+
"..",
|
| 72 |
+
"..",
|
| 73 |
+
"FirenetCNN1.h5",
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
self._frames = self._discover_frames()
|
| 77 |
+
self._max_frames = len(self._frames)
|
| 78 |
+
self._frame_idx = 0
|
| 79 |
+
self._step = 0
|
| 80 |
+
self._missed_count = 0
|
| 81 |
+
self._cumulative_reward = 0.0
|
| 82 |
+
self._episode_history: List[Dict[str, Any]] = []
|
| 83 |
+
|
| 84 |
+
self._inference = None
|
| 85 |
+
self._load_inference()
|
| 86 |
+
|
| 87 |
+
self._ground_truth_labels = frames_labels or self._load_labels()
|
| 88 |
+
|
| 89 |
+
self._frame_height = 224
|
| 90 |
+
self._frame_width = 224
|
| 91 |
+
self.observation_space = spaces.Dict(
|
| 92 |
+
{
|
| 93 |
+
"image": spaces.Box(
|
| 94 |
+
low=0,
|
| 95 |
+
high=255,
|
| 96 |
+
shape=(self._frame_height, self._frame_width, 3),
|
| 97 |
+
dtype=np.uint8,
|
| 98 |
+
),
|
| 99 |
+
"prediction": spaces.Box(
|
| 100 |
+
low=0.0, high=1.0, shape=(3,), dtype=np.float32
|
| 101 |
+
),
|
| 102 |
+
"gradcam": spaces.Box(low=0.0, high=1.0, shape=(1,), dtype=np.float32),
|
| 103 |
+
"gradcam_summary": spaces.Box(
|
| 104 |
+
low=0, high=1, shape=(1,), dtype=np.int32
|
| 105 |
+
),
|
| 106 |
+
"frame_id": spaces.Box(
|
| 107 |
+
low=0, high=max(0, self._max_frames - 1), shape=(1,), dtype=np.int32
|
| 108 |
+
),
|
| 109 |
+
"step": spaces.Box(low=0, high=1_000_000, shape=(1,), dtype=np.int32),
|
| 110 |
+
"ground_truth": spaces.Box(low=0, high=2, shape=(1,), dtype=np.int32),
|
| 111 |
+
}
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
self.action_space = spaces.Discrete(len(self.ACTIONS))
|
| 115 |
+
self._action_str = lambda idx: self.ACTIONS[int(idx)]
|
| 116 |
+
|
| 117 |
+
def _load_inference(self):
|
| 118 |
+
if os.path.exists(self.model_path):
|
| 119 |
+
try:
|
| 120 |
+
self._inference = FirenetInference(self.model_path)
|
| 121 |
+
except Exception:
|
| 122 |
+
self._inference = None
|
| 123 |
+
else:
|
| 124 |
+
self._inference = None
|
| 125 |
+
|
| 126 |
+
def _load_labels(self) -> Dict[str, str]:
|
| 127 |
+
labels = {}
|
| 128 |
+
for frame in self._frames:
|
| 129 |
+
if frame:
|
| 130 |
+
fname = os.path.basename(frame).lower()
|
| 131 |
+
if "fire" in fname:
|
| 132 |
+
labels[frame] = "fire"
|
| 133 |
+
elif "smoke" in fname:
|
| 134 |
+
labels[frame] = "smoke"
|
| 135 |
+
else:
|
| 136 |
+
labels[frame] = "no_fire"
|
| 137 |
+
return labels
|
| 138 |
+
|
| 139 |
+
def _discover_frames(self):
|
| 140 |
+
frames = []
|
| 141 |
+
if self.frames_dir and os.path.isdir(self.frames_dir):
|
| 142 |
+
for fname in sorted(os.listdir(self.frames_dir)):
|
| 143 |
+
if fname.lower().endswith((".png", ".jpg", ".jpeg", ".bmp", ".gif")):
|
| 144 |
+
frames.append(os.path.join(self.frames_dir, fname))
|
| 145 |
+
if not frames:
|
| 146 |
+
frames = [None]
|
| 147 |
+
return frames
|
| 148 |
+
|
| 149 |
+
def _run_inference(self, frame: np.ndarray) -> Dict[str, Any]:
|
| 150 |
+
if self._inference is not None:
|
| 151 |
+
try:
|
| 152 |
+
cv2_frame = frame[:, :, ::-1].copy() if frame.shape[-1] == 3 else frame
|
| 153 |
+
result = self._inference.predict(cv2_frame)
|
| 154 |
+
class_idx = result["class_idx"]
|
| 155 |
+
conf = result["confidence"]
|
| 156 |
+
gradcam_summary_val = (
|
| 157 |
+
1 if "hotspot" in result.get("gradcam_summary", "") else 0
|
| 158 |
+
)
|
| 159 |
+
return {
|
| 160 |
+
"class_idx": class_idx,
|
| 161 |
+
"confidence": conf,
|
| 162 |
+
"gradcam": np.array([conf], dtype=np.float32),
|
| 163 |
+
"gradcam_summary_val": gradcam_summary_val,
|
| 164 |
+
"label": result.get("label", self.CLASS_LABELS[class_idx]),
|
| 165 |
+
"frame": frame,
|
| 166 |
+
}
|
| 167 |
+
except Exception:
|
| 168 |
+
pass
|
| 169 |
+
|
| 170 |
+
gray = np.mean(frame) if frame is not None else 127
|
| 171 |
+
if gray > 170:
|
| 172 |
+
class_idx = 1
|
| 173 |
+
conf = 0.92
|
| 174 |
+
elif gray > 85:
|
| 175 |
+
class_idx = 2
|
| 176 |
+
conf = 0.60
|
| 177 |
+
else:
|
| 178 |
+
class_idx = 0
|
| 179 |
+
conf = 0.40
|
| 180 |
+
return {
|
| 181 |
+
"class_idx": class_idx,
|
| 182 |
+
"confidence": conf,
|
| 183 |
+
"gradcam": np.array([conf], dtype=np.float32),
|
| 184 |
+
"gradcam_summary_val": 0,
|
| 185 |
+
"label": self.CLASS_LABELS[class_idx],
|
| 186 |
+
"frame": frame,
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
def _compute_reward(
|
| 190 |
+
self, action: int, inf: Dict[str, Any], ground_truth: str
|
| 191 |
+
) -> Tuple[float, bool]:
|
| 192 |
+
action_str = self._action_str(action)
|
| 193 |
+
pred_label = inf.get("label", self.CLASS_LABELS[inf["class_idx"]])
|
| 194 |
+
confidence = inf["confidence"]
|
| 195 |
+
gradcam_strong = inf.get("gradcam_summary_val", 0) == 1
|
| 196 |
+
|
| 197 |
+
# Simplified reward - focus on action vs ground_truth, not prediction match
|
| 198 |
+
if ground_truth == "no_fire":
|
| 199 |
+
if action_str in ["Alert", "Deploy"]:
|
| 200 |
+
return -0.75, False
|
| 201 |
+
elif action_str == "Ignore":
|
| 202 |
+
return 0.1, False # Small positive for correct inaction
|
| 203 |
+
else: # Scan
|
| 204 |
+
return 0.0, False
|
| 205 |
+
|
| 206 |
+
if ground_truth in ["fire", "smoke"]:
|
| 207 |
+
if action_str == "Ignore":
|
| 208 |
+
self._missed_count += 1
|
| 209 |
+
return -0.50, self._missed_count >= self.MAX_MISSES
|
| 210 |
+
|
| 211 |
+
if action_str in ["Alert", "Deploy"]:
|
| 212 |
+
# Reward based on confidence regardless of exact prediction match
|
| 213 |
+
if confidence >= 0.85 and gradcam_strong:
|
| 214 |
+
return 2.00, False
|
| 215 |
+
elif confidence >= 0.70:
|
| 216 |
+
return 1.00, False
|
| 217 |
+
else:
|
| 218 |
+
return 0.50, False
|
| 219 |
+
|
| 220 |
+
if action_str == "Scan":
|
| 221 |
+
# Scan is neutral - continue observing
|
| 222 |
+
return 0.0, False
|
| 223 |
+
|
| 224 |
+
return 0.0, False
|
| 225 |
+
|
| 226 |
+
def _get_ground_truth(self, frame_path: Optional[str]) -> str:
|
| 227 |
+
if frame_path and frame_path in self._ground_truth_labels:
|
| 228 |
+
return self._ground_truth_labels[frame_path]
|
| 229 |
+
if frame_path:
|
| 230 |
+
fname = os.path.basename(frame_path).lower()
|
| 231 |
+
if "fire" in fname:
|
| 232 |
+
return "fire"
|
| 233 |
+
elif "smoke" in fname:
|
| 234 |
+
return "smoke"
|
| 235 |
+
return "no_fire"
|
| 236 |
+
|
| 237 |
+
def reset(
|
| 238 |
+
self, seed: Optional[int] = None, options: Optional[Dict] = None
|
| 239 |
+
) -> Dict[str, Any]:
|
| 240 |
+
if seed is not None:
|
| 241 |
+
np.random.seed(seed)
|
| 242 |
+
self._frame_idx = 0
|
| 243 |
+
self._step = 0
|
| 244 |
+
self._missed_count = 0
|
| 245 |
+
self._cumulative_reward = 0.0
|
| 246 |
+
self._episode_history = []
|
| 247 |
+
|
| 248 |
+
frame_path = self._frames[self._frame_idx] if self._frames else None
|
| 249 |
+
frame = self._load_frame(frame_path)
|
| 250 |
+
if frame is None:
|
| 251 |
+
frame = self._synthetic_frame()
|
| 252 |
+
|
| 253 |
+
inf = self._run_inference(frame)
|
| 254 |
+
ground_truth = self._get_ground_truth(frame_path)
|
| 255 |
+
gradcam_val = inf.get("gradcam_summary_val", 0)
|
| 256 |
+
|
| 257 |
+
obs = {
|
| 258 |
+
"image": frame,
|
| 259 |
+
"prediction": np.array(
|
| 260 |
+
[
|
| 261 |
+
inf["confidence"] if inf["class_idx"] == 1 else 0.0,
|
| 262 |
+
inf["confidence"] if inf["class_idx"] == 2 else 0.0,
|
| 263 |
+
inf["confidence"] if inf["class_idx"] == 0 else 0.0,
|
| 264 |
+
]
|
| 265 |
+
).astype(np.float32),
|
| 266 |
+
"gradcam": inf["gradcam"],
|
| 267 |
+
"gradcam_summary": np.array([gradcam_val], dtype=np.int32),
|
| 268 |
+
"frame_id": np.array([self._frame_idx], dtype=np.int32),
|
| 269 |
+
"step": np.array([self._step], dtype=np.int32),
|
| 270 |
+
"ground_truth": np.array(
|
| 271 |
+
["fire", "smoke", "no_fire"].index(ground_truth), dtype=np.int32
|
| 272 |
+
),
|
| 273 |
+
}
|
| 274 |
+
return obs
|
| 275 |
+
|
| 276 |
+
def _load_frame(self, path: Optional[str]) -> Optional[np.ndarray]:
|
| 277 |
+
if path is None:
|
| 278 |
+
return None
|
| 279 |
+
try:
|
| 280 |
+
img = (
|
| 281 |
+
Image.open(path)
|
| 282 |
+
.convert("RGB")
|
| 283 |
+
.resize((self._frame_width, self._frame_height))
|
| 284 |
+
)
|
| 285 |
+
return np.asarray(img, dtype=np.uint8)
|
| 286 |
+
except Exception:
|
| 287 |
+
return None
|
| 288 |
+
|
| 289 |
+
def _synthetic_frame(self) -> np.ndarray:
|
| 290 |
+
return np.full((self._frame_height, self._frame_width, 3), 128, dtype=np.uint8)
|
| 291 |
+
|
| 292 |
+
def step(self, action: int) -> Tuple[Dict[str, Any], float, bool, Dict[str, Any]]:
|
| 293 |
+
assert 0 <= int(action) < len(self.ACTIONS), "Invalid action"
|
| 294 |
+
self._step += 1
|
| 295 |
+
self._frame_idx += 1
|
| 296 |
+
|
| 297 |
+
frame_path = (
|
| 298 |
+
self._frames[self._frame_idx]
|
| 299 |
+
if self._frame_idx < self._max_frames
|
| 300 |
+
else None
|
| 301 |
+
)
|
| 302 |
+
frame = self._load_frame(frame_path)
|
| 303 |
+
if frame is None:
|
| 304 |
+
frame = self._synthetic_frame()
|
| 305 |
+
|
| 306 |
+
inf = self._run_inference(frame)
|
| 307 |
+
ground_truth = self._get_ground_truth(frame_path)
|
| 308 |
+
gradcam_val = inf.get("gradcam_summary_val", 0)
|
| 309 |
+
|
| 310 |
+
reward, terminal_failure = self._compute_reward(action, inf, ground_truth)
|
| 311 |
+
self._cumulative_reward += reward
|
| 312 |
+
|
| 313 |
+
self._episode_history.append(
|
| 314 |
+
{
|
| 315 |
+
"step": self._step,
|
| 316 |
+
"frame_id": self._frame_idx,
|
| 317 |
+
"action": self._action_str(action),
|
| 318 |
+
"prediction": inf.get("label", self.CLASS_LABELS[inf["class_idx"]]),
|
| 319 |
+
"confidence": inf["confidence"],
|
| 320 |
+
"ground_truth": ground_truth,
|
| 321 |
+
"reward": reward,
|
| 322 |
+
}
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
done = terminal_failure or self._frame_idx >= self._max_frames
|
| 326 |
+
|
| 327 |
+
obs = {
|
| 328 |
+
"image": frame,
|
| 329 |
+
"prediction": np.array(
|
| 330 |
+
[
|
| 331 |
+
inf["confidence"] if inf["class_idx"] == 1 else 0.0,
|
| 332 |
+
inf["confidence"] if inf["class_idx"] == 2 else 0.0,
|
| 333 |
+
inf["confidence"] if inf["class_idx"] == 0 else 0.0,
|
| 334 |
+
]
|
| 335 |
+
).astype(np.float32),
|
| 336 |
+
"gradcam": inf["gradcam"],
|
| 337 |
+
"gradcam_summary": np.array([gradcam_val], dtype=np.int32),
|
| 338 |
+
"frame_id": np.array([self._frame_idx], dtype=np.int32),
|
| 339 |
+
"step": np.array([self._step], dtype=np.int32),
|
| 340 |
+
"ground_truth": np.array(
|
| 341 |
+
["fire", "smoke", "no_fire"].index(ground_truth), dtype=np.int32
|
| 342 |
+
),
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
info = {
|
| 346 |
+
"action": self._action_str(action),
|
| 347 |
+
"reward": reward,
|
| 348 |
+
"cumulative_reward": self._cumulative_reward,
|
| 349 |
+
"prediction": inf.get("label", self.CLASS_LABELS[inf["class_idx"]]),
|
| 350 |
+
"ground_truth": ground_truth,
|
| 351 |
+
"confidence": inf["confidence"],
|
| 352 |
+
"terminal": terminal_failure,
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
if done:
|
| 356 |
+
self._save_episode_log()
|
| 357 |
+
|
| 358 |
+
return obs, reward, done, info
|
| 359 |
+
|
| 360 |
+
def _save_episode_log(self):
|
| 361 |
+
try:
|
| 362 |
+
with open(self.LOG_FILE, "a") as f:
|
| 363 |
+
f.write(f"\n=== Episode ===\n")
|
| 364 |
+
f.write(f"Total frames: {len(self._episode_history)}\n")
|
| 365 |
+
f.write(f"Cumulative reward: {self._cumulative_reward:.2f}\n")
|
| 366 |
+
f.write(f"Success: {self._is_success()}\n")
|
| 367 |
+
for entry in self._episode_history:
|
| 368 |
+
f.write(f" Step {entry['step']}: {entry}\n")
|
| 369 |
+
except Exception:
|
| 370 |
+
pass
|
| 371 |
+
|
| 372 |
+
def _is_success(self) -> bool:
|
| 373 |
+
for entry in self._episode_history:
|
| 374 |
+
gt = entry["ground_truth"]
|
| 375 |
+
action = entry["action"]
|
| 376 |
+
if gt in ["fire", "smoke"] and action not in ["Alert", "Deploy"]:
|
| 377 |
+
return False
|
| 378 |
+
if gt == "no_fire" and action in ["Alert", "Deploy"]:
|
| 379 |
+
return False
|
| 380 |
+
return True
|
| 381 |
+
|
| 382 |
+
def grade(self) -> float:
|
| 383 |
+
if not self._episode_history:
|
| 384 |
+
return 0.0
|
| 385 |
+
if self._is_success():
|
| 386 |
+
return 1.0
|
| 387 |
+
return max(0.0, self._cumulative_reward / 10.0)
|
| 388 |
+
|
| 389 |
+
def render(self, mode: str = "human") -> Optional[Any]:
|
| 390 |
+
return None
|
| 391 |
+
|
| 392 |
+
def close(self) -> None:
|
| 393 |
+
pass
|
inference.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Wildfire Detection Inference Script
|
| 3 |
+
====================================
|
| 4 |
+
For LOCAL TESTING: Uses trained RL model (no API key required)
|
| 5 |
+
For SUBMISSION: Uses OpenAI client with API key injected by judges
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import sys
|
| 10 |
+
import base64
|
| 11 |
+
import pickle
|
| 12 |
+
from io import BytesIO
|
| 13 |
+
from typing import List, Optional, Dict, Any
|
| 14 |
+
from collections import defaultdict
|
| 15 |
+
|
| 16 |
+
from openai import OpenAI
|
| 17 |
+
import numpy as np
|
| 18 |
+
from PIL import Image
|
| 19 |
+
|
| 20 |
+
# Environment variables
|
| 21 |
+
API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
|
| 22 |
+
MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-3B-Instruct")
|
| 23 |
+
API_KEY = os.getenv("API_KEY")
|
| 24 |
+
|
| 25 |
+
# Check if we can use LLM mode
|
| 26 |
+
USE_LLM = API_KEY is not None
|
| 27 |
+
|
| 28 |
+
if USE_LLM:
|
| 29 |
+
client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
|
| 30 |
+
else:
|
| 31 |
+
client = None
|
| 32 |
+
print("NOTE: Running in LOCAL MODE (no API_KEY). Using trained RL model.")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
# RL Model class for local mode
|
| 36 |
+
class TrainedQLearningAgent:
|
| 37 |
+
"""Trained Q-learning agent for local inference."""
|
| 38 |
+
|
| 39 |
+
def __init__(self):
|
| 40 |
+
model_path = os.path.join(os.path.dirname(__file__), "q_model.pkl")
|
| 41 |
+
if os.path.exists(model_path):
|
| 42 |
+
with open(model_path, "rb") as f:
|
| 43 |
+
self.q_table = defaultdict(lambda: [0.0, 0.0, 0.0, 0.0], pickle.load(f))
|
| 44 |
+
print(f"Loaded RL model from: {model_path}")
|
| 45 |
+
else:
|
| 46 |
+
print("WARNING: No trained model found, using fallback!")
|
| 47 |
+
self.q_table = defaultdict(lambda: [0.0, 0.0, 0.0, 0.0])
|
| 48 |
+
self.actions = ["Alert", "Scan", "Ignore", "Deploy"]
|
| 49 |
+
|
| 50 |
+
def get_state_key(self, obs):
|
| 51 |
+
prediction = obs.get("prediction", np.zeros(3))
|
| 52 |
+
if hasattr(prediction, "tolist"):
|
| 53 |
+
p = prediction.tolist()
|
| 54 |
+
else:
|
| 55 |
+
p = list(prediction)
|
| 56 |
+
fire_high = 0 if p[0] < 0.5 else 1
|
| 57 |
+
smoke_high = 0 if p[1] < 0.5 else 1
|
| 58 |
+
return (fire_high, smoke_high)
|
| 59 |
+
|
| 60 |
+
def choose_action(self, state):
|
| 61 |
+
q_values = self.q_table.get(state, [0.0, 0.0, 0.0, 0.0])
|
| 62 |
+
if hasattr(q_values, "tolist"):
|
| 63 |
+
q_values = q_values.tolist()
|
| 64 |
+
return q_values.index(max(q_values))
|
| 65 |
+
|
| 66 |
+
def get_action_name(self, idx):
|
| 67 |
+
return self.actions[idx]
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
# Import environment
|
| 71 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 72 |
+
from environments.wildfire_detection.wildfire_env import WildfireDetectionEnv
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def encode_image_to_base64(image_array: np.ndarray) -> str:
|
| 76 |
+
"""Convert numpy image to base64 data URI."""
|
| 77 |
+
image = Image.fromarray(image_array)
|
| 78 |
+
buffer = BytesIO()
|
| 79 |
+
image.save(buffer, format="JPEG", quality=85)
|
| 80 |
+
buffer.seek(0)
|
| 81 |
+
data = base64.b64encode(buffer.read()).decode("utf-8")
|
| 82 |
+
return f"data:image/jpeg;base64,{data}"
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def build_observation_prompt(step: int, obs: Dict[str, Any]) -> str:
|
| 86 |
+
"""Build user prompt from observation."""
|
| 87 |
+
prediction = obs.get("prediction", {})
|
| 88 |
+
fire_prob = prediction[0] if hasattr(prediction, "__getitem__") else 0
|
| 89 |
+
smoke_prob = prediction[1] if hasattr(prediction, "__getitem__") else 0
|
| 90 |
+
no_fire_prob = prediction[2] if hasattr(prediction, "__getitem__") else 0
|
| 91 |
+
|
| 92 |
+
gradcam = obs.get("gradcam_summary", "N/A")
|
| 93 |
+
frame_id = (
|
| 94 |
+
obs.get("frame_id", [0])[0]
|
| 95 |
+
if hasattr(obs.get("frame_id", [0]), "__len__")
|
| 96 |
+
else 0
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
prompt = f"""Frame {frame_id} - Step {step}
|
| 100 |
+
Prediction probabilities:
|
| 101 |
+
- Fire: {fire_prob:.2%}
|
| 102 |
+
- Smoke: {smoke_prob:.2%}
|
| 103 |
+
- No Fire: {no_fire_prob:.2%}
|
| 104 |
+
|
| 105 |
+
Grad-CAM analysis: {gradcam}
|
| 106 |
+
|
| 107 |
+
What is your action? (Alert/Scan/Ignore/Deploy)
|
| 108 |
+
"""
|
| 109 |
+
return prompt.strip()
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def parse_action(response_text: str) -> str:
|
| 113 |
+
"""Parse LLM response to extract action."""
|
| 114 |
+
if not response_text:
|
| 115 |
+
return "Scan"
|
| 116 |
+
response = response_text.strip().lower()
|
| 117 |
+
if "alert" in response:
|
| 118 |
+
return "Alert"
|
| 119 |
+
elif "deploy" in response:
|
| 120 |
+
return "Deploy"
|
| 121 |
+
elif "ignore" in response:
|
| 122 |
+
return "Ignore"
|
| 123 |
+
return "Scan"
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def action_to_index(action: str) -> int:
|
| 127 |
+
actions = ["Alert", "Scan", "Ignore", "Deploy"]
|
| 128 |
+
return actions.index(action) if action in actions else 1
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
MAX_STEPS = 20
|
| 132 |
+
TEMPERATURE = 0.2
|
| 133 |
+
MAX_TOKENS = 500
|
| 134 |
+
|
| 135 |
+
SYSTEM_PROMPT = """You are an autonomous wildfire detection agent. You control a drone monitoring for fires and smoke.
|
| 136 |
+
|
| 137 |
+
Given camera frame observations with prediction probabilities and Grad-CAM analysis, decide the best action.
|
| 138 |
+
|
| 139 |
+
Actions available:
|
| 140 |
+
- Alert: Raise immediate alert if fire/smoke detected with high confidence
|
| 141 |
+
- Scan: Continue scanning/observing the current frame more carefully
|
| 142 |
+
- Ignore: No action needed, clear conditions
|
| 143 |
+
- Deploy: Deploy resources to the detected location
|
| 144 |
+
|
| 145 |
+
Output ONLY the action name: Alert, Scan, Ignore, or Deploy
|
| 146 |
+
|
| 147 |
+
Consider:
|
| 148 |
+
- Prediction confidence levels
|
| 149 |
+
- Grad-CAM heatmap alignment (strong hotspot = more reliable detection)
|
| 150 |
+
- Balance early detection vs false alarms
|
| 151 |
+
- Sequential frame context
|
| 152 |
+
"""
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def run_episode(env) -> Dict[str, Any]:
|
| 156 |
+
"""Run a single episode and return results."""
|
| 157 |
+
rewards_list = []
|
| 158 |
+
steps_executed = 0
|
| 159 |
+
|
| 160 |
+
# Use appropriate model name based on mode
|
| 161 |
+
model_used = MODEL_NAME if USE_LLM else "trained_q_learning"
|
| 162 |
+
print(f"[START] task=wildfire_detection env=wildfire_detection model={model_used}")
|
| 163 |
+
|
| 164 |
+
# Initialize RL agent for local mode
|
| 165 |
+
rl_agent = TrainedQLearningAgent() if not USE_LLM else None
|
| 166 |
+
|
| 167 |
+
obs = env.reset()
|
| 168 |
+
|
| 169 |
+
for step in range(1, MAX_STEPS + 1):
|
| 170 |
+
steps_executed = step
|
| 171 |
+
|
| 172 |
+
if USE_LLM:
|
| 173 |
+
# LLM mode
|
| 174 |
+
user_prompt = build_observation_prompt(step, obs)
|
| 175 |
+
|
| 176 |
+
image_b64 = ""
|
| 177 |
+
if "image" in obs and obs["image"] is not None:
|
| 178 |
+
image_b64 = encode_image_to_base64(obs["image"])
|
| 179 |
+
|
| 180 |
+
messages = [
|
| 181 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 182 |
+
{
|
| 183 |
+
"role": "user",
|
| 184 |
+
"content": [
|
| 185 |
+
{"type": "text", "text": user_prompt},
|
| 186 |
+
{"type": "image_url", "image_url": {"url": image_b64}}
|
| 187 |
+
if image_b64
|
| 188 |
+
else {"type": "text", "text": "(no image)"},
|
| 189 |
+
],
|
| 190 |
+
},
|
| 191 |
+
]
|
| 192 |
+
|
| 193 |
+
try:
|
| 194 |
+
completion = client.chat.completions.create(
|
| 195 |
+
model=MODEL_NAME,
|
| 196 |
+
messages=messages,
|
| 197 |
+
temperature=TEMPERATURE,
|
| 198 |
+
max_tokens=MAX_TOKENS,
|
| 199 |
+
stream=False,
|
| 200 |
+
)
|
| 201 |
+
response_text = completion.choices[0].message.content or ""
|
| 202 |
+
except Exception as e:
|
| 203 |
+
print(f"Model request failed: {e}. Using fallback.")
|
| 204 |
+
response_text = "Scan"
|
| 205 |
+
|
| 206 |
+
action_str = parse_action(response_text)
|
| 207 |
+
else:
|
| 208 |
+
# RL mode
|
| 209 |
+
state = rl_agent.get_state_key(obs)
|
| 210 |
+
action_idx = rl_agent.choose_action(state)
|
| 211 |
+
action_str = rl_agent.get_action_name(action_idx)
|
| 212 |
+
|
| 213 |
+
action_idx = action_to_index(action_str)
|
| 214 |
+
|
| 215 |
+
obs, reward, done, info = env.step(action_idx)
|
| 216 |
+
rewards_list.append(reward)
|
| 217 |
+
|
| 218 |
+
error_msg = "null"
|
| 219 |
+
print(
|
| 220 |
+
f"[STEP] step={step} action={action_str} reward={reward:.2f} done={str(done).lower()} error={error_msg}"
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
if done:
|
| 224 |
+
break
|
| 225 |
+
|
| 226 |
+
total_reward = sum(rewards_list)
|
| 227 |
+
rewards_str = ",".join(f"{r:.2f}" for r in rewards_list)
|
| 228 |
+
success = total_reward > 0
|
| 229 |
+
|
| 230 |
+
print(
|
| 231 |
+
f"[END] success={str(success).lower()} steps={steps_executed} rewards={rewards_str}"
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
return {
|
| 235 |
+
"success": success,
|
| 236 |
+
"steps": steps_executed,
|
| 237 |
+
"rewards": rewards_list,
|
| 238 |
+
"total_reward": total_reward,
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def main():
|
| 243 |
+
"""Main entry point."""
|
| 244 |
+
model_path = os.path.join(
|
| 245 |
+
os.path.dirname(__file__),
|
| 246 |
+
"FirenetCNN1.h5",
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
env = WildfireDetectionEnv(model_path=model_path)
|
| 250 |
+
|
| 251 |
+
try:
|
| 252 |
+
result = run_episode(env)
|
| 253 |
+
print(
|
| 254 |
+
f"Episode completed: success={result['success']}, steps={result['steps']}, total_reward={result['total_reward']:.2f}"
|
| 255 |
+
)
|
| 256 |
+
finally:
|
| 257 |
+
env.close()
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
if __name__ == "__main__":
|
| 261 |
+
main()
|
inference_rl.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Wildfire Detection RL Inference Script
|
| 3 |
+
=======================================
|
| 4 |
+
Uses trained Q-learning model for decision making.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
import pickle
|
| 10 |
+
import numpy as np
|
| 11 |
+
from collections import defaultdict
|
| 12 |
+
|
| 13 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 14 |
+
|
| 15 |
+
from environments.wildfire_detection.wildfire_env import WildfireDetectionEnv
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class TrainedQLearningAgent:
|
| 19 |
+
"""Trained Q-learning agent for inference."""
|
| 20 |
+
|
| 21 |
+
def __init__(self, model_path=None):
|
| 22 |
+
self.model_path = model_path or os.path.join(
|
| 23 |
+
os.path.dirname(__file__), "q_model.pkl"
|
| 24 |
+
)
|
| 25 |
+
self.q_table = None
|
| 26 |
+
self.actions = ["Alert", "Scan", "Ignore", "Deploy"]
|
| 27 |
+
self._load_model()
|
| 28 |
+
|
| 29 |
+
def _load_model(self):
|
| 30 |
+
if os.path.exists(self.model_path):
|
| 31 |
+
with open(self.model_path, "rb") as f:
|
| 32 |
+
self.q_table = defaultdict(lambda: [0.0, 0.0, 0.0, 0.0], pickle.load(f))
|
| 33 |
+
print(f"Loaded trained model from: {self.model_path}")
|
| 34 |
+
print(f"Q-table: {dict(self.q_table)}")
|
| 35 |
+
else:
|
| 36 |
+
print("WARNING: No trained model found!")
|
| 37 |
+
self.q_table = defaultdict(lambda: [0.0, 0.0, 0.0, 0.0])
|
| 38 |
+
|
| 39 |
+
def get_state_key(self, obs):
|
| 40 |
+
"""Convert observation to discrete state key."""
|
| 41 |
+
prediction = obs.get("prediction", np.zeros(3))
|
| 42 |
+
if hasattr(prediction, "tolist"):
|
| 43 |
+
p = prediction.tolist()
|
| 44 |
+
elif hasattr(prediction, "__iter__"):
|
| 45 |
+
p = list(prediction)
|
| 46 |
+
else:
|
| 47 |
+
p = [0, 0, 0]
|
| 48 |
+
|
| 49 |
+
# Match training: fire_high, smoke_high
|
| 50 |
+
fire_high = 0 if p[0] < 0.5 else 1
|
| 51 |
+
smoke_high = 0 if p[1] < 0.5 else 1
|
| 52 |
+
|
| 53 |
+
return (fire_high, smoke_high)
|
| 54 |
+
|
| 55 |
+
def choose_action(self, state):
|
| 56 |
+
"""Choose best action based on Q-values."""
|
| 57 |
+
q_values = self.q_table.get(state, [0.0, 0.0, 0.0, 0.0])
|
| 58 |
+
if hasattr(q_values, "tolist"):
|
| 59 |
+
q_values = q_values.tolist()
|
| 60 |
+
return q_values.index(max(q_values))
|
| 61 |
+
|
| 62 |
+
def get_action_name(self, action_idx):
|
| 63 |
+
return self.actions[action_idx]
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def run_inference():
|
| 67 |
+
"""Run inference with trained RL agent."""
|
| 68 |
+
print(
|
| 69 |
+
"[START] task=wildfire_detection env=wildfire_detection model=trained_q_learning"
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
env = WildfireDetectionEnv()
|
| 73 |
+
agent = TrainedQLearningAgent()
|
| 74 |
+
|
| 75 |
+
rewards_list = []
|
| 76 |
+
steps_executed = 0
|
| 77 |
+
|
| 78 |
+
obs = env.reset()
|
| 79 |
+
|
| 80 |
+
for step in range(1, 21):
|
| 81 |
+
steps_executed = step
|
| 82 |
+
|
| 83 |
+
state = agent.get_state_key(obs)
|
| 84 |
+
action_idx = agent.choose_action(state)
|
| 85 |
+
action_str = agent.get_action_name(action_idx)
|
| 86 |
+
|
| 87 |
+
obs, reward, done, info = env.step(action_idx)
|
| 88 |
+
rewards_list.append(reward)
|
| 89 |
+
|
| 90 |
+
error_msg = "null"
|
| 91 |
+
print(
|
| 92 |
+
f"[STEP] step={step} action={action_str} reward={reward:.2f} done={str(done).lower()} error={error_msg}"
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
if done:
|
| 96 |
+
break
|
| 97 |
+
|
| 98 |
+
env.close()
|
| 99 |
+
|
| 100 |
+
total_reward = sum(rewards_list)
|
| 101 |
+
rewards_str = ",".join(f"{r:.2f}" for r in rewards_list)
|
| 102 |
+
success = total_reward > 0
|
| 103 |
+
|
| 104 |
+
print(
|
| 105 |
+
f"[END] success={str(success).lower()} steps={steps_executed} rewards={rewards_str}"
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
return {
|
| 109 |
+
"success": success,
|
| 110 |
+
"steps": steps_executed,
|
| 111 |
+
"rewards": rewards_list,
|
| 112 |
+
"total_reward": total_reward,
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
if __name__ == "__main__":
|
| 117 |
+
result = run_inference()
|
| 118 |
+
print(
|
| 119 |
+
f"\nResult: success={result['success']}, total_reward={result['total_reward']:.2f}"
|
| 120 |
+
)
|
models.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
Data models for the Wildfire Detection Environment.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from openenv.core.env_server.types import Action, Observation
|
| 12 |
+
from pydantic import Field
|
| 13 |
+
from typing import Optional, Dict, Any
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class WildfireAction(Action):
|
| 17 |
+
"""Action for the Wildfire Detection environment."""
|
| 18 |
+
|
| 19 |
+
action: str = Field(
|
| 20 |
+
..., description="Action to take: Alert, Scan, Ignore, or Deploy"
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class WildfireObservation(Observation):
|
| 25 |
+
"""Observation from the Wildfire Detection environment."""
|
| 26 |
+
|
| 27 |
+
image: str = Field(default="", description="Base64 encoded image or file path")
|
| 28 |
+
prediction: Dict[str, float] = Field(
|
| 29 |
+
default_factory=dict,
|
| 30 |
+
description="Prediction probabilities for fire, smoke, no_fire",
|
| 31 |
+
)
|
| 32 |
+
gradcam_summary: str = Field(default="", description="Grad-CAM heatmap summary")
|
| 33 |
+
frame_id: int = Field(default=0, description="Current frame ID")
|
| 34 |
+
step: int = Field(default=0, description="Current step")
|
| 35 |
+
ground_truth: str = Field(
|
| 36 |
+
default="", description="Ground truth label (for training)"
|
| 37 |
+
)
|
| 38 |
+
reward: float = Field(default=0.0, description="Reward for the last action")
|
| 39 |
+
done: bool = Field(default=False, description="Whether episode is done")
|
| 40 |
+
metadata: Dict[str, Any] = Field(
|
| 41 |
+
default_factory=dict, description="Additional metadata"
|
| 42 |
+
)
|
openenv.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
spec_version: 1
|
| 2 |
+
name: wildfire_detection
|
| 3 |
+
type: space
|
| 4 |
+
runtime: fastapi
|
| 5 |
+
app: run_server:main
|
| 6 |
+
port: 8000
|
openenv_wildfire_detection.egg-info/PKG-INFO
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.4
|
| 2 |
+
Name: openenv-wildfire-detection
|
| 3 |
+
Version: 0.1.0
|
| 4 |
+
Summary: Wildfire Detection RL Environment for OpenEnv
|
| 5 |
+
Requires-Python: >=3.10
|
| 6 |
+
Requires-Dist: openenv-core[core]>=0.2.2
|
| 7 |
+
Requires-Dist: numpy>=1.19.0
|
| 8 |
+
Requires-Dist: pillow>=10.0.0
|
| 9 |
+
Requires-Dist: opencv-python>=4.8.0
|
| 10 |
+
Requires-Dist: tensorflow>=2.15.0
|
| 11 |
+
Requires-Dist: gymnasium>=0.29.0
|
| 12 |
+
Provides-Extra: dev
|
| 13 |
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
| 14 |
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
openenv_wildfire_detection.egg-info/SOURCES.txt
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
README.md
|
| 2 |
+
__init__.py
|
| 3 |
+
client.py
|
| 4 |
+
inference.py
|
| 5 |
+
models.py
|
| 6 |
+
pyproject.toml
|
| 7 |
+
run_server.py
|
| 8 |
+
test_env.py
|
| 9 |
+
./__init__.py
|
| 10 |
+
./client.py
|
| 11 |
+
./debug_env.py
|
| 12 |
+
./inference.py
|
| 13 |
+
./inference_rl.py
|
| 14 |
+
./models.py
|
| 15 |
+
./run_server.py
|
| 16 |
+
./test_env.py
|
| 17 |
+
./train_rl.py
|
| 18 |
+
environments/wildfire_detection/__init__.py
|
| 19 |
+
environments/wildfire_detection/firenet_inference.py
|
| 20 |
+
environments/wildfire_detection/wildfire_env.py
|
| 21 |
+
openenv_wildfire_detection.egg-info/PKG-INFO
|
| 22 |
+
openenv_wildfire_detection.egg-info/SOURCES.txt
|
| 23 |
+
openenv_wildfire_detection.egg-info/dependency_links.txt
|
| 24 |
+
openenv_wildfire_detection.egg-info/entry_points.txt
|
| 25 |
+
openenv_wildfire_detection.egg-info/requires.txt
|
| 26 |
+
openenv_wildfire_detection.egg-info/top_level.txt
|
| 27 |
+
server/__init__.py
|
| 28 |
+
server/app.py
|
| 29 |
+
server/multipen_environment.py
|
| 30 |
+
server/wildfire_environment.py
|
openenv_wildfire_detection.egg-info/dependency_links.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
openenv_wildfire_detection.egg-info/entry_points.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[console_scripts]
|
| 2 |
+
server = multipen.server.app:main
|
openenv_wildfire_detection.egg-info/requires.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openenv-core[core]>=0.2.2
|
| 2 |
+
numpy>=1.19.0
|
| 3 |
+
pillow>=10.0.0
|
| 4 |
+
opencv-python>=4.8.0
|
| 5 |
+
tensorflow>=2.15.0
|
| 6 |
+
gymnasium>=0.29.0
|
| 7 |
+
|
| 8 |
+
[dev]
|
| 9 |
+
pytest>=8.0.0
|
| 10 |
+
pytest-cov>=4.0.0
|
openenv_wildfire_detection.egg-info/top_level.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
multipen
|
push_hf.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
os.environ["PYTHONIOENCODING"] = "utf-8"
|
| 4 |
+
import subprocess
|
| 5 |
+
|
| 6 |
+
result = subprocess.run(
|
| 7 |
+
[
|
| 8 |
+
"python",
|
| 9 |
+
"-m",
|
| 10 |
+
"openenv.cli",
|
| 11 |
+
"push",
|
| 12 |
+
"--repo-id",
|
| 13 |
+
"OpelSpeedster/Wildfire-Detection_Openenv",
|
| 14 |
+
],
|
| 15 |
+
cwd=r"C:\Users\ASUS\OneDrive\Desktop\RL\multipen",
|
| 16 |
+
capture_output=True,
|
| 17 |
+
text=True,
|
| 18 |
+
encoding="utf-8",
|
| 19 |
+
)
|
| 20 |
+
print("STDOUT:", result.stdout)
|
| 21 |
+
print("STDERR:", result.stderr)
|
pyproject.toml
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
[build-system]
|
| 8 |
+
requires = ["setuptools>=45", "wheel"]
|
| 9 |
+
build-backend = "setuptools.build_meta"
|
| 10 |
+
|
| 11 |
+
[project]
|
| 12 |
+
name = "openenv-wildfire-detection"
|
| 13 |
+
version = "0.1.0"
|
| 14 |
+
description = "Wildfire Detection RL Environment for OpenEnv"
|
| 15 |
+
requires-python = ">=3.10"
|
| 16 |
+
dependencies = [
|
| 17 |
+
# Core OpenEnv runtime
|
| 18 |
+
"openenv-core[core]>=0.2.2",
|
| 19 |
+
# Environment dependencies
|
| 20 |
+
"numpy>=1.19.0",
|
| 21 |
+
"pillow>=10.0.0",
|
| 22 |
+
"opencv-python>=4.8.0",
|
| 23 |
+
"tensorflow>=2.15.0",
|
| 24 |
+
# RL dependencies
|
| 25 |
+
"gymnasium>=0.29.0",
|
| 26 |
+
]
|
| 27 |
+
|
| 28 |
+
[project.optional-dependencies]
|
| 29 |
+
dev = [
|
| 30 |
+
"pytest>=8.0.0",
|
| 31 |
+
"pytest-cov>=4.0.0",
|
| 32 |
+
]
|
| 33 |
+
|
| 34 |
+
[project.scripts]
|
| 35 |
+
server = "multipen.server.app:main"
|
| 36 |
+
|
| 37 |
+
[tool.setuptools]
|
| 38 |
+
include-package-data = true
|
| 39 |
+
packages = ["multipen", "multipen.server", "multipen.environments.wildfire_detection"]
|
| 40 |
+
package-dir = { "multipen" = ".", "multipen.server" = "server", "multipen.environments.wildfire_detection" = "environments/wildfire_detection" }
|
q_model.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ade681309aa444dc7029e5afe08520935fdec605afbcde5d3bc18ea3f553ee24
|
| 3 |
+
size 326
|
requirements.txt
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
aiofile==3.9.0
|
| 2 |
+
aiofiles==24.1.0
|
| 3 |
+
annotated-doc==0.0.4
|
| 4 |
+
annotated-types==0.7.0
|
| 5 |
+
anyio==4.13.0
|
| 6 |
+
attrs==26.1.0
|
| 7 |
+
Authlib==1.6.9
|
| 8 |
+
beartype==0.22.9
|
| 9 |
+
brotli==1.2.0
|
| 10 |
+
cachetools==7.0.5
|
| 11 |
+
caio==0.9.25
|
| 12 |
+
certifi==2026.2.25
|
| 13 |
+
cffi==2.0.0
|
| 14 |
+
charset-normalizer==3.4.7
|
| 15 |
+
click==8.3.2
|
| 16 |
+
cryptography==46.0.7
|
| 17 |
+
cyclopts==4.10.1
|
| 18 |
+
distro==1.9.0
|
| 19 |
+
dnspython==2.8.0
|
| 20 |
+
docstring_parser==0.17.0
|
| 21 |
+
docutils==0.22.4
|
| 22 |
+
email-validator==2.3.0
|
| 23 |
+
exceptiongroup==1.3.1
|
| 24 |
+
fastapi==0.135.3
|
| 25 |
+
fastmcp==3.2.0
|
| 26 |
+
ffmpy==1.0.0
|
| 27 |
+
filelock==3.25.2
|
| 28 |
+
fsspec==2026.3.0
|
| 29 |
+
gradio==6.11.0
|
| 30 |
+
gradio_client==2.4.0
|
| 31 |
+
groovy==0.1.2
|
| 32 |
+
h11==0.16.0
|
| 33 |
+
hf-gradio==0.3.0
|
| 34 |
+
hf-xet==1.4.3
|
| 35 |
+
httpcore==1.0.9
|
| 36 |
+
httpx==0.28.1
|
| 37 |
+
httpx-sse==0.4.3
|
| 38 |
+
huggingface_hub==1.9.2
|
| 39 |
+
idna==3.11
|
| 40 |
+
importlib_metadata==8.7.1
|
| 41 |
+
jaraco.classes==3.4.0
|
| 42 |
+
jaraco.context==6.1.2
|
| 43 |
+
jaraco.functools==4.4.0
|
| 44 |
+
jeepney==0.9.0
|
| 45 |
+
Jinja2==3.1.6
|
| 46 |
+
jiter==0.13.0
|
| 47 |
+
jsonref==1.1.0
|
| 48 |
+
jsonschema==4.26.0
|
| 49 |
+
jsonschema-path==0.4.5
|
| 50 |
+
jsonschema-specifications==2025.9.1
|
| 51 |
+
keyring==25.7.0
|
| 52 |
+
markdown-it-py==4.0.0
|
| 53 |
+
MarkupSafe==3.0.3
|
| 54 |
+
mcp==1.27.0
|
| 55 |
+
mdurl==0.1.2
|
| 56 |
+
more-itertools==11.0.1
|
| 57 |
+
numpy==2.4.4
|
| 58 |
+
openai==2.30.0
|
| 59 |
+
openapi-pydantic==0.5.1
|
| 60 |
+
openenv-core==0.2.3
|
| 61 |
+
opentelemetry-api==1.40.0
|
| 62 |
+
orjson==3.11.8
|
| 63 |
+
packaging==26.0
|
| 64 |
+
pandas==3.0.2
|
| 65 |
+
pathable==0.5.0
|
| 66 |
+
pillow==12.2.0
|
| 67 |
+
platformdirs==4.9.4
|
| 68 |
+
py-key-value-aio==0.4.4
|
| 69 |
+
pycparser==3.0
|
| 70 |
+
pydantic==2.12.5
|
| 71 |
+
pydantic-settings==2.13.1
|
| 72 |
+
pydantic_core==2.41.5
|
| 73 |
+
pydub==0.25.1
|
| 74 |
+
Pygments==2.20.0
|
| 75 |
+
PyJWT==2.12.1
|
| 76 |
+
pyperclip==1.11.0
|
| 77 |
+
python-dateutil==2.9.0.post0
|
| 78 |
+
python-dotenv==1.2.2
|
| 79 |
+
python-multipart==0.0.24
|
| 80 |
+
pytz==2026.1.post1
|
| 81 |
+
PyYAML==6.0.3
|
| 82 |
+
referencing==0.37.0
|
| 83 |
+
requests==2.33.1
|
| 84 |
+
rich==14.3.3
|
| 85 |
+
rich-rst==1.3.2
|
| 86 |
+
rpds-py==0.30.0
|
| 87 |
+
safehttpx==0.1.7
|
| 88 |
+
SecretStorage==3.5.0
|
| 89 |
+
semantic-version==2.10.0
|
| 90 |
+
shellingham==1.5.4
|
| 91 |
+
six==1.17.0
|
| 92 |
+
sniffio==1.3.1
|
| 93 |
+
sse-starlette==3.3.4
|
| 94 |
+
starlette==1.0.0
|
| 95 |
+
tomli==2.4.1
|
| 96 |
+
tomli_w==1.2.0
|
| 97 |
+
tomlkit==0.13.3
|
| 98 |
+
tqdm==4.67.3
|
| 99 |
+
typer==0.24.1
|
| 100 |
+
typing-inspection==0.4.2
|
| 101 |
+
typing_extensions==4.15.0
|
| 102 |
+
uncalled-for==0.3.1
|
| 103 |
+
urllib3==2.6.3
|
| 104 |
+
uvicorn==0.44.0
|
| 105 |
+
watchfiles==1.1.1
|
| 106 |
+
websockets==16.0
|
| 107 |
+
zipp==3.23.0
|
run_server.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uvicorn
|
| 2 |
+
from server.app import app
|
| 3 |
+
|
| 4 |
+
if __name__ == "__main__":
|
| 5 |
+
print("Starting Wildfire Detection OpenEnv Server...")
|
| 6 |
+
print("Server will be available at http://localhost:8000")
|
| 7 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
server/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""Wildfire Detection environment server components."""
|
| 8 |
+
|
| 9 |
+
from .wildfire_environment import WildfireEnvironment
|
| 10 |
+
|
| 11 |
+
__all__ = ["WildfireEnvironment"]
|
server/app.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
FastAPI application for the Wildfire Detection Environment.
|
| 9 |
+
|
| 10 |
+
This module creates an HTTP server that exposes the WildfireEnvironment
|
| 11 |
+
over HTTP and WebSocket endpoints, compatible with EnvClient.
|
| 12 |
+
|
| 13 |
+
Endpoints:
|
| 14 |
+
- POST /reset: Reset the environment
|
| 15 |
+
- POST /step: Execute an action
|
| 16 |
+
- GET /state: Get current environment state
|
| 17 |
+
- GET /schema: Get action/observation schemas
|
| 18 |
+
- WS /ws: WebSocket endpoint for persistent sessions
|
| 19 |
+
|
| 20 |
+
Usage:
|
| 21 |
+
# Development (with auto-reload):
|
| 22 |
+
uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
|
| 23 |
+
|
| 24 |
+
# Production:
|
| 25 |
+
uvicorn server.app:app --host 0.0.0.0 --port 8000 --workers 4
|
| 26 |
+
|
| 27 |
+
# Or run directly:
|
| 28 |
+
python -m server.app
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
try:
|
| 32 |
+
from openenv.core.env_server.http_server import create_app
|
| 33 |
+
except Exception as e:
|
| 34 |
+
raise ImportError("openenv is required for the web interface.") from e
|
| 35 |
+
|
| 36 |
+
try:
|
| 37 |
+
from models import WildfireAction, WildfireObservation
|
| 38 |
+
except (ModuleNotFoundError, ImportError):
|
| 39 |
+
from multipen.models import WildfireAction, WildfireObservation
|
| 40 |
+
|
| 41 |
+
try:
|
| 42 |
+
from server.wildfire_environment import WildfireEnvironment
|
| 43 |
+
except (ModuleNotFoundError, ImportError):
|
| 44 |
+
from multipen.server.wildfire_environment import WildfireEnvironment
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
app = create_app(
|
| 48 |
+
WildfireEnvironment,
|
| 49 |
+
WildfireAction,
|
| 50 |
+
WildfireObservation,
|
| 51 |
+
env_name="wildfire_detection",
|
| 52 |
+
max_concurrent_envs=1,
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def main():
|
| 57 |
+
"""Entry point for direct execution."""
|
| 58 |
+
import uvicorn
|
| 59 |
+
|
| 60 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
if __name__ == "__main__":
|
| 64 |
+
main()
|
server/multipen_environment.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
Multipen Environment Implementation.
|
| 9 |
+
|
| 10 |
+
A simple test environment that echoes back messages sent to it.
|
| 11 |
+
Perfect for testing HTTP server infrastructure.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from uuid import uuid4
|
| 15 |
+
|
| 16 |
+
from openenv.core.env_server.interfaces import Environment
|
| 17 |
+
from openenv.core.env_server.types import State
|
| 18 |
+
|
| 19 |
+
try:
|
| 20 |
+
from ..models import MultipenAction, MultipenObservation
|
| 21 |
+
except ImportError:
|
| 22 |
+
from models import MultipenAction, MultipenObservation
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class MultipenEnvironment(Environment):
|
| 26 |
+
"""
|
| 27 |
+
A simple echo environment that echoes back messages.
|
| 28 |
+
|
| 29 |
+
This environment is designed for testing the HTTP server infrastructure.
|
| 30 |
+
It maintains minimal state and simply echoes back whatever message it receives.
|
| 31 |
+
|
| 32 |
+
Example:
|
| 33 |
+
>>> env = MultipenEnvironment()
|
| 34 |
+
>>> obs = env.reset()
|
| 35 |
+
>>> print(obs.echoed_message) # "Multipen environment ready!"
|
| 36 |
+
>>>
|
| 37 |
+
>>> obs = env.step(MultipenAction(message="Hello"))
|
| 38 |
+
>>> print(obs.echoed_message) # "Hello"
|
| 39 |
+
>>> print(obs.message_length) # 5
|
| 40 |
+
"""
|
| 41 |
+
|
| 42 |
+
# Enable concurrent WebSocket sessions.
|
| 43 |
+
# Set to True if your environment isolates state between instances.
|
| 44 |
+
# When True, multiple WebSocket clients can connect simultaneously, each
|
| 45 |
+
# getting their own environment instance (when using factory mode in app.py).
|
| 46 |
+
SUPPORTS_CONCURRENT_SESSIONS: bool = True
|
| 47 |
+
|
| 48 |
+
def __init__(self):
|
| 49 |
+
"""Initialize the multipen environment."""
|
| 50 |
+
self._state = State(episode_id=str(uuid4()), step_count=0)
|
| 51 |
+
self._reset_count = 0
|
| 52 |
+
|
| 53 |
+
def reset(self) -> MultipenObservation:
|
| 54 |
+
"""
|
| 55 |
+
Reset the environment.
|
| 56 |
+
|
| 57 |
+
Returns:
|
| 58 |
+
MultipenObservation with a ready message
|
| 59 |
+
"""
|
| 60 |
+
self._state = State(episode_id=str(uuid4()), step_count=0)
|
| 61 |
+
self._reset_count += 1
|
| 62 |
+
|
| 63 |
+
return MultipenObservation(
|
| 64 |
+
echoed_message="Multipen environment ready!",
|
| 65 |
+
message_length=0,
|
| 66 |
+
done=False,
|
| 67 |
+
reward=0.0,
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
def step(self, action: MultipenAction) -> MultipenObservation: # type: ignore[override]
|
| 71 |
+
"""
|
| 72 |
+
Execute a step in the environment by echoing the message.
|
| 73 |
+
|
| 74 |
+
Args:
|
| 75 |
+
action: MultipenAction containing the message to echo
|
| 76 |
+
|
| 77 |
+
Returns:
|
| 78 |
+
MultipenObservation with the echoed message and its length
|
| 79 |
+
"""
|
| 80 |
+
self._state.step_count += 1
|
| 81 |
+
|
| 82 |
+
message = action.message
|
| 83 |
+
length = len(message)
|
| 84 |
+
|
| 85 |
+
# Simple reward: longer messages get higher rewards
|
| 86 |
+
reward = length * 0.1
|
| 87 |
+
|
| 88 |
+
return MultipenObservation(
|
| 89 |
+
echoed_message=message,
|
| 90 |
+
message_length=length,
|
| 91 |
+
done=False,
|
| 92 |
+
reward=reward,
|
| 93 |
+
metadata={"original_message": message, "step": self._state.step_count},
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
@property
|
| 97 |
+
def state(self) -> State:
|
| 98 |
+
"""
|
| 99 |
+
Get the current environment state.
|
| 100 |
+
|
| 101 |
+
Returns:
|
| 102 |
+
Current State with episode_id and step_count
|
| 103 |
+
"""
|
| 104 |
+
return self._state
|
server/requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openenv-core[core]>=0.2.2
|
| 2 |
+
numpy>=1.19.0
|
| 3 |
+
pillow>=10.0.0
|
| 4 |
+
opencv-python>=4.8.0
|
| 5 |
+
tensorflow>=2.15.0
|
| 6 |
+
gymnasium>=0.29.0
|
| 7 |
+
fastapi>=0.104.0
|
| 8 |
+
uvicorn>=0.24.0
|
| 9 |
+
pydantic>=2.0.0
|
server/wildfire_environment.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
Wildfire Detection Environment Implementation.
|
| 9 |
+
|
| 10 |
+
This environment uses the FirenetCNN model for wildfire detection.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import os
|
| 14 |
+
import base64
|
| 15 |
+
from uuid import uuid4
|
| 16 |
+
from io import BytesIO
|
| 17 |
+
|
| 18 |
+
from openenv.core.env_server.interfaces import Environment
|
| 19 |
+
from openenv.core.env_server.types import State
|
| 20 |
+
|
| 21 |
+
try:
|
| 22 |
+
from ..models import WildfireAction, WildfireObservation
|
| 23 |
+
except ImportError:
|
| 24 |
+
from models import WildfireAction, WildfireObservation
|
| 25 |
+
|
| 26 |
+
try:
|
| 27 |
+
from environments.wildfire_detection.wildfire_env import WildfireDetectionEnv
|
| 28 |
+
except ModuleNotFoundError:
|
| 29 |
+
from multipen.environments.wildfire_detection.wildfire_env import (
|
| 30 |
+
WildfireDetectionEnv,
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class WildfireEnvironment(Environment):
|
| 35 |
+
"""OpenEnv environment for wildfire detection using FirenetCNN."""
|
| 36 |
+
|
| 37 |
+
SUPPORTS_CONCURRENT_SESSIONS: bool = True
|
| 38 |
+
|
| 39 |
+
def __init__(self):
|
| 40 |
+
self._state = State(episode_id=str(uuid4()), step_count=0)
|
| 41 |
+
self._env = None
|
| 42 |
+
self._init_env()
|
| 43 |
+
|
| 44 |
+
def _init_env(self):
|
| 45 |
+
model_path = os.path.join(
|
| 46 |
+
os.path.dirname(__file__),
|
| 47 |
+
"..",
|
| 48 |
+
"..",
|
| 49 |
+
"Forest-Fire-Detection-Using-FirenetCNN-and-XAI-Techniques",
|
| 50 |
+
"FirenetCNN1.h5",
|
| 51 |
+
)
|
| 52 |
+
self._env = WildfireDetectionEnv(model_path=model_path)
|
| 53 |
+
|
| 54 |
+
def reset(self) -> WildfireObservation:
|
| 55 |
+
self._state = State(episode_id=str(uuid4()), step_count=0)
|
| 56 |
+
obs = self._env.reset()
|
| 57 |
+
|
| 58 |
+
return self._make_observation(obs, 0.0, False)
|
| 59 |
+
|
| 60 |
+
def step(self, action: WildfireAction) -> WildfireObservation:
|
| 61 |
+
self._state.step_count += 1
|
| 62 |
+
|
| 63 |
+
action_idx = ["Alert", "Scan", "Ignore", "Deploy"].index(action.action)
|
| 64 |
+
obs, reward, done, info = self._env.step(action_idx)
|
| 65 |
+
|
| 66 |
+
return self._make_observation(obs, reward, done, info)
|
| 67 |
+
|
| 68 |
+
def _make_observation(
|
| 69 |
+
self, obs: dict, reward: float, done: bool
|
| 70 |
+
) -> WildfireObservation:
|
| 71 |
+
img = obs.get("image")
|
| 72 |
+
img_b64 = ""
|
| 73 |
+
if img is not None:
|
| 74 |
+
from PIL import Image
|
| 75 |
+
import numpy as np
|
| 76 |
+
|
| 77 |
+
pil_img = Image.fromarray(img)
|
| 78 |
+
buffer = BytesIO()
|
| 79 |
+
pil_img.save(buffer, format="JPEG")
|
| 80 |
+
img_b64 = base64.b64encode(buffer.getvalue()).decode()
|
| 81 |
+
|
| 82 |
+
return WildfireObservation(
|
| 83 |
+
image=img_b64,
|
| 84 |
+
prediction={
|
| 85 |
+
"fire": float(obs.get("prediction", [0, 0, 0])[0]),
|
| 86 |
+
"smoke": float(obs.get("prediction", [0, 0, 0])[1]),
|
| 87 |
+
"no_fire": float(obs.get("prediction", [0, 0, 0])[2]),
|
| 88 |
+
},
|
| 89 |
+
gradcam_summary=f"Grad-CAM: {obs.get('gradcam_summary', [0])[0]}",
|
| 90 |
+
frame_id=int(obs.get("frame_id", [0])[0]),
|
| 91 |
+
step=int(obs.get("step", [0])[0]),
|
| 92 |
+
ground_truth=["fire", "smoke", "no_fire"][
|
| 93 |
+
int(obs.get("ground_truth", [2])[0])
|
| 94 |
+
],
|
| 95 |
+
reward=reward,
|
| 96 |
+
done=done,
|
| 97 |
+
metadata={},
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
@property
|
| 101 |
+
def state(self) -> State:
|
| 102 |
+
return self._state
|
test_env.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
sys.path.insert(0, r"C:\Users\ASUS\OneDrive\Desktop\RL\multipen")
|
| 5 |
+
|
| 6 |
+
from environments.wildfire_detection.wildfire_env import WildfireDetectionEnv
|
| 7 |
+
|
| 8 |
+
print("Creating environment...")
|
| 9 |
+
env = WildfireDetectionEnv()
|
| 10 |
+
print("Environment created successfully")
|
| 11 |
+
|
| 12 |
+
obs = env.reset()
|
| 13 |
+
print(f"Reset successful, step: {obs['step']}")
|
| 14 |
+
|
| 15 |
+
action = 0 # Alert
|
| 16 |
+
obs, reward, done, info = env.step(action)
|
| 17 |
+
print(f"Step taken: action={info['action']}, reward={reward}, done={done}")
|
| 18 |
+
|
| 19 |
+
print("Environment test passed!")
|
train_rl.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Wildfire Detection RL Training (Fast Version)
|
| 3 |
+
==============================================
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
import numpy as np
|
| 9 |
+
from collections import defaultdict
|
| 10 |
+
import pickle
|
| 11 |
+
|
| 12 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 13 |
+
|
| 14 |
+
# Pre-computed frame data to avoid slow TensorFlow loading
|
| 15 |
+
FRAME_DATA = [
|
| 16 |
+
{"prediction": [0.9, 0.1, 0.0], "ground_truth": 0, "frame": 0}, # fire
|
| 17 |
+
{"prediction": [0.85, 0.15, 0.0], "ground_truth": 0, "frame": 1}, # fire
|
| 18 |
+
{"prediction": [0.8, 0.2, 0.0], "ground_truth": 0, "frame": 2}, # fire
|
| 19 |
+
{"prediction": [0.7, 0.25, 0.05], "ground_truth": 0, "frame": 3}, # fire
|
| 20 |
+
{"prediction": [0.6, 0.3, 0.1], "ground_truth": 0, "frame": 4}, # fire
|
| 21 |
+
{"prediction": [0.5, 0.4, 0.1], "ground_truth": 1, "frame": 5}, # smoke
|
| 22 |
+
{"prediction": [0.3, 0.6, 0.1], "ground_truth": 1, "frame": 6}, # smoke
|
| 23 |
+
{"prediction": [0.2, 0.7, 0.1], "ground_truth": 1, "frame": 7}, # smoke
|
| 24 |
+
{"prediction": [0.1, 0.8, 0.1], "ground_truth": 1, "frame": 8}, # smoke
|
| 25 |
+
{"prediction": [0.1, 0.85, 0.05], "ground_truth": 1, "frame": 9}, # smoke
|
| 26 |
+
{"prediction": [0.0, 0.1, 0.9], "ground_truth": 2, "frame": 10}, # no_fire
|
| 27 |
+
{"prediction": [0.0, 0.05, 0.95], "ground_truth": 2, "frame": 11}, # no_fire
|
| 28 |
+
{"prediction": [0.05, 0.05, 0.9], "ground_truth": 2, "frame": 12}, # no_fire
|
| 29 |
+
{"prediction": [0.0, 0.0, 1.0], "ground_truth": 2, "frame": 13}, # no_fire
|
| 30 |
+
{"prediction": [0.0, 0.0, 1.0], "ground_truth": 2, "frame": 14}, # no_fire
|
| 31 |
+
{"prediction": [0.9, 0.1, 0.0], "ground_truth": 0, "frame": 15}, # fire (repeat)
|
| 32 |
+
]
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class FastEnv:
|
| 36 |
+
"""Fast environment without TensorFlow."""
|
| 37 |
+
|
| 38 |
+
ACTIONS = ["Alert", "Scan", "Ignore", "Deploy"]
|
| 39 |
+
CLASS_LABELS = ["fire", "smoke", "no_fire"]
|
| 40 |
+
|
| 41 |
+
def __init__(self):
|
| 42 |
+
self.frame_idx = 0
|
| 43 |
+
self.n_frames = len(FRAME_DATA)
|
| 44 |
+
|
| 45 |
+
def reset(self):
|
| 46 |
+
self.frame_idx = 0
|
| 47 |
+
return self._get_obs()
|
| 48 |
+
|
| 49 |
+
def step(self, action):
|
| 50 |
+
frame = FRAME_DATA[self.frame_idx]
|
| 51 |
+
gt = frame["ground_truth"]
|
| 52 |
+
action_str = self.ACTIONS[action]
|
| 53 |
+
|
| 54 |
+
# Reward function
|
| 55 |
+
if gt == 2: # no_fire
|
| 56 |
+
if action_str in ["Alert", "Deploy"]:
|
| 57 |
+
reward = -0.75
|
| 58 |
+
elif action_str == "Ignore":
|
| 59 |
+
reward = 0.1
|
| 60 |
+
else:
|
| 61 |
+
reward = 0.0
|
| 62 |
+
else: # fire or smoke
|
| 63 |
+
if action_str == "Ignore":
|
| 64 |
+
reward = -0.50
|
| 65 |
+
elif action_str in ["Alert", "Deploy"]:
|
| 66 |
+
reward = 0.50
|
| 67 |
+
else: # Scan
|
| 68 |
+
reward = 0.0
|
| 69 |
+
|
| 70 |
+
# Move to next frame BEFORE checking done
|
| 71 |
+
self.frame_idx += 1
|
| 72 |
+
done = self.frame_idx >= self.n_frames
|
| 73 |
+
|
| 74 |
+
# If we moved past valid frames, return last observation but mark done
|
| 75 |
+
if done:
|
| 76 |
+
obs = {
|
| 77 |
+
"prediction": np.array([0, 0, 0], dtype=np.float32),
|
| 78 |
+
"ground_truth": np.array([2], dtype=np.int32),
|
| 79 |
+
"frame_id": np.array([self.frame_idx - 1], dtype=np.int32),
|
| 80 |
+
}
|
| 81 |
+
else:
|
| 82 |
+
obs = self._get_obs()
|
| 83 |
+
|
| 84 |
+
return obs, reward, done, {"ground_truth": self.CLASS_LABELS[gt]}
|
| 85 |
+
|
| 86 |
+
def _get_obs(self):
|
| 87 |
+
frame = FRAME_DATA[self.frame_idx]
|
| 88 |
+
return {
|
| 89 |
+
"prediction": np.array(frame["prediction"], dtype=np.float32),
|
| 90 |
+
"ground_truth": np.array([frame["ground_truth"]], dtype=np.int32),
|
| 91 |
+
"frame_id": np.array([self.frame_idx], dtype=np.int32),
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
class QLearningAgent:
|
| 96 |
+
def __init__(
|
| 97 |
+
self,
|
| 98 |
+
n_actions=4,
|
| 99 |
+
learning_rate=0.4,
|
| 100 |
+
discount_factor=0.9,
|
| 101 |
+
epsilon=1.0,
|
| 102 |
+
epsilon_decay=0.98,
|
| 103 |
+
epsilon_min=0.05,
|
| 104 |
+
):
|
| 105 |
+
self.n_actions = n_actions
|
| 106 |
+
self.lr = learning_rate
|
| 107 |
+
self.gamma = discount_factor
|
| 108 |
+
self.epsilon = epsilon
|
| 109 |
+
self.epsilon_decay = epsilon_decay
|
| 110 |
+
self.epsilon_min = epsilon_min
|
| 111 |
+
self.q_table = defaultdict(lambda: np.zeros(n_actions))
|
| 112 |
+
|
| 113 |
+
def get_state_key(self, obs):
|
| 114 |
+
p = obs["prediction"]
|
| 115 |
+
fire_high = 0 if p[0] < 0.5 else 1
|
| 116 |
+
smoke_high = 0 if p[1] < 0.5 else 1
|
| 117 |
+
return (fire_high, smoke_high)
|
| 118 |
+
|
| 119 |
+
def choose_action(self, state):
|
| 120 |
+
if np.random.random() < self.epsilon:
|
| 121 |
+
return np.random.randint(self.n_actions)
|
| 122 |
+
return np.argmax(self.q_table[state])
|
| 123 |
+
|
| 124 |
+
def learn(self, state, action, reward, next_state):
|
| 125 |
+
current_q = self.q_table[state][action]
|
| 126 |
+
max_next_q = np.max(self.q_table[next_state])
|
| 127 |
+
td_target = reward + self.gamma * max_next_q
|
| 128 |
+
self.q_table[state][action] = current_q + self.lr * (td_target - current_q)
|
| 129 |
+
|
| 130 |
+
def decay_epsilon(self):
|
| 131 |
+
self.epsilon = max(self.epsilon_min, self.epsilon * self.epsilon_decay)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def train_agent(num_episodes=200):
|
| 135 |
+
print("Initializing fast environment...")
|
| 136 |
+
env = FastEnv()
|
| 137 |
+
|
| 138 |
+
agent = QLearningAgent()
|
| 139 |
+
|
| 140 |
+
print(f"\nTraining for {num_episodes} episodes...")
|
| 141 |
+
|
| 142 |
+
for episode in range(num_episodes):
|
| 143 |
+
obs = env.reset()
|
| 144 |
+
state = agent.get_state_key(obs)
|
| 145 |
+
total_reward = 0
|
| 146 |
+
done = False
|
| 147 |
+
|
| 148 |
+
while not done:
|
| 149 |
+
action = agent.choose_action(state)
|
| 150 |
+
obs, reward, done, info = env.step(action)
|
| 151 |
+
next_state = agent.get_state_key(obs)
|
| 152 |
+
|
| 153 |
+
agent.learn(state, action, reward, next_state)
|
| 154 |
+
total_reward += reward
|
| 155 |
+
state = next_state
|
| 156 |
+
|
| 157 |
+
agent.decay_epsilon()
|
| 158 |
+
|
| 159 |
+
if (episode + 1) % 20 == 0:
|
| 160 |
+
print(
|
| 161 |
+
f"Episode {episode + 1}/{num_episodes} | Reward: {total_reward:.2f} | Epsilon: {agent.epsilon:.3f}"
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
# Save model
|
| 165 |
+
model_path = os.path.join(os.path.dirname(__file__), "q_model.pkl")
|
| 166 |
+
with open(model_path, "wb") as f:
|
| 167 |
+
pickle.dump(dict(agent.q_table), f)
|
| 168 |
+
|
| 169 |
+
print(f"\nTraining complete!")
|
| 170 |
+
print(f"Q-table: {dict(agent.q_table)}")
|
| 171 |
+
print(f"Model saved to: {model_path}")
|
| 172 |
+
|
| 173 |
+
return agent
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def evaluate():
|
| 177 |
+
env = FastEnv()
|
| 178 |
+
|
| 179 |
+
model_path = os.path.join(os.path.dirname(__file__), "q_model.pkl")
|
| 180 |
+
with open(model_path, "rb") as f:
|
| 181 |
+
q_table = pickle.load(f)
|
| 182 |
+
|
| 183 |
+
agent = QLearningAgent()
|
| 184 |
+
agent.q_table = defaultdict(lambda: np.zeros(4), q_table)
|
| 185 |
+
agent.epsilon = 0
|
| 186 |
+
|
| 187 |
+
print("\n=== Evaluation ===")
|
| 188 |
+
|
| 189 |
+
for ep in range(3):
|
| 190 |
+
obs = env.reset()
|
| 191 |
+
state = agent.get_state_key(obs)
|
| 192 |
+
total_reward = 0
|
| 193 |
+
done = False
|
| 194 |
+
actions = []
|
| 195 |
+
|
| 196 |
+
while not done:
|
| 197 |
+
action = agent.choose_action(state)
|
| 198 |
+
actions.append(["Alert", "Scan", "Ignore", "Deploy"][action])
|
| 199 |
+
obs, reward, done, _ = env.step(action)
|
| 200 |
+
total_reward += reward
|
| 201 |
+
state = agent.get_state_key(obs)
|
| 202 |
+
|
| 203 |
+
print(f"Episode: {actions}")
|
| 204 |
+
print(f"Reward: {total_reward:.2f}")
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
if __name__ == "__main__":
|
| 208 |
+
train_agent(200)
|
| 209 |
+
evaluate()
|
uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|