OpelSpeedster commited on
Commit
1feed70
·
verified ·
1 Parent(s): e9b1e71

Update the Project

Browse files
app.py CHANGED
@@ -1,69 +1,323 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
- from huggingface_hub import InferenceClient
 
3
 
 
 
4
 
5
- def respond(
6
- message,
7
- history: list[dict[str, str]],
8
- system_message,
9
- max_tokens,
10
- temperature,
11
- top_p,
12
- hf_token: gr.OAuthToken,
13
- ):
14
- """
15
- For more information on `huggingface_hub` Inference API support, please check the docs: https://huggingface.co/docs/huggingface_hub/v0.22.2/en/guides/inference
16
- """
17
- client = InferenceClient(token=hf_token.token, model="openai/gpt-oss-20b")
18
 
19
- messages = [{"role": "system", "content": system_message}]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
- messages.extend(history)
22
 
23
- messages.append({"role": "user", "content": message})
24
 
25
- response = ""
26
 
27
- for message in client.chat_completion(
28
- messages,
29
- max_tokens=max_tokens,
30
- stream=True,
31
- temperature=temperature,
32
- top_p=top_p,
33
- ):
34
- choices = message.choices
35
- token = ""
36
- if len(choices) and choices[0].delta.content:
37
- token = choices[0].delta.content
 
 
 
 
 
 
 
 
 
38
 
39
- response += token
40
- yield response
41
 
 
 
 
 
 
42
 
43
- """
44
- For information on how to customize the ChatInterface, peruse the gradio docs: https://www.gradio.app/docs/chatinterface
45
- """
46
- chatbot = gr.ChatInterface(
47
- respond,
48
- additional_inputs=[
49
- gr.Textbox(value="You are a friendly Chatbot.", label="System message"),
50
- gr.Slider(minimum=1, maximum=2048, value=512, step=1, label="Max new tokens"),
51
- gr.Slider(minimum=0.1, maximum=4.0, value=0.7, step=0.1, label="Temperature"),
52
- gr.Slider(
53
- minimum=0.1,
54
- maximum=1.0,
55
- value=0.95,
56
- step=0.05,
57
- label="Top-p (nucleus sampling)",
58
- ),
59
- ],
60
- )
61
-
62
- with gr.Blocks() as demo:
63
- with gr.Sidebar():
64
- gr.LoginButton()
65
- chatbot.render()
66
-
67
-
68
- if __name__ == "__main__":
69
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ app.py - Gradio Web Application for Forest Fire Detection
3
+ Provides a web interface for image classification, video analysis, and model info.
4
+ """
5
+ # `spaces` must be imported before any CUDA-touching library (tensorflow is
6
+ # imported transitively below via `src.inference`) for Hugging Face ZeroGPU.
7
+ import spaces
8
+ import os
9
+ os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
10
+
11
+ # Fix invalid SSL_CERT_FILE on Windows (points to non-existent path)
12
+ ssl_cert = os.environ.get('SSL_CERT_FILE', '')
13
+ if ssl_cert and not os.path.exists(ssl_cert):
14
+ del os.environ['SSL_CERT_FILE']
15
+
16
+ import tempfile
17
+ import time
18
+ from pathlib import Path
19
+
20
+ import cv2
21
  import gradio as gr
22
+ import numpy as np
23
+ from PIL import Image
24
 
25
+ from src.inference import FireNetInference
26
+ from src.model import FireNetModel
27
 
28
+ # Default model path (override with MODEL_PATH env var, e.g. as a Space secret)
29
+ DEFAULT_MODEL = os.environ.get('MODEL_PATH', 'models/FirenetCNN1.h5')
 
 
 
 
 
 
 
 
 
 
 
30
 
31
+ # Class descriptions
32
+ CLASS_INFO = {
33
+ 'fire': {
34
+ 'color': 'Red',
35
+ 'description': 'Active fire detected with high confidence. Immediate attention required.',
36
+ 'icon': '🔥'
37
+ },
38
+ 'no_fire': {
39
+ 'color': 'Green',
40
+ 'description': 'No fire detected. Scene appears safe.',
41
+ 'icon': '✅'
42
+ },
43
+ 'smoke': {
44
+ 'color': 'Orange',
45
+ 'description': 'Smoke detected. May indicate early-stage fire or controlled burn.',
46
+ 'icon': '💨'
47
+ }
48
+ }
49
 
 
50
 
51
+ _engine = None
52
 
 
53
 
54
+ def get_inference_engine() -> FireNetInference:
55
+ """Get or create the cached inference engine (loaded once per process)."""
56
+ global _engine
57
+ if _engine is None:
58
+ model_path = DEFAULT_MODEL
59
+ if not os.path.exists(model_path):
60
+ # Try alternative paths
61
+ alternatives = [
62
+ 'models/FirenetCNN.keras',
63
+ 'models/FirenetCNN.h5',
64
+ 'models/firenet_model.h5',
65
+ 'FirenetCNN1.h5',
66
+ 'FirenetCNN.h5',
67
+ ]
68
+ for alt in alternatives:
69
+ if os.path.exists(alt):
70
+ model_path = alt
71
+ break
72
+ _engine = FireNetInference(model_path)
73
+ return _engine
74
 
 
 
75
 
76
+ @spaces.GPU(duration=30)
77
+ def predict_image(image, apply_gradcam=True):
78
+ """Predict on a single image with optional Grad-CAM."""
79
+ if image is None:
80
+ return None, "Please upload an image.", None, None
81
 
82
+ try:
83
+ engine = get_inference_engine()
84
+
85
+ # Convert PIL to temp file for inference
86
+ with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as tmp:
87
+ if isinstance(image, np.ndarray):
88
+ cv2.imwrite(tmp.name, cv2.cvtColor(image, cv2.COLOR_RGB2BGR))
89
+ else:
90
+ image.save(tmp.name)
91
+ tmp_path = tmp.name
92
+
93
+ result = engine.predict_image(tmp_path, apply_gradcam=apply_gradcam)
94
+
95
+ # Cleanup
96
+ os.unlink(tmp_path)
97
+
98
+ # Prepare outputs
99
+ label = result['label']
100
+ confidence = result['confidence']
101
+ probs = result['probability_array']
102
+
103
+ # Create annotated image
104
+ if result.get('annotated_image') is not None:
105
+ annotated = cv2.cvtColor(result['annotated_image'], cv2.COLOR_BGR2RGB)
106
+ elif result.get('heatmap') is not None and result['has_gradcam']:
107
+ original = cv2.imread(tmp_path) if os.path.exists(tmp_path) else None
108
+ if original is not None:
109
+ rgb_orig = cv2.cvtColor(original, cv2.COLOR_BGR2RGB)
110
+ heatmap = np.array(result['heatmap'])
111
+ annotated = engine.gradcam.overlay_heatmap(rgb_orig, heatmap, alpha=0.5)
112
+ else:
113
+ annotated = np.array(image)
114
+ else:
115
+ annotated = np.array(image)
116
+
117
+ # Create probability chart
118
+ prob_dict = {cls: float(probs[i]) for i, cls in enumerate(FireNetModel.CLASS_LABELS)}
119
+
120
+ # Format result text
121
+ info = CLASS_INFO.get(label, {})
122
+ result_text = f"## {info.get('icon', '')} Prediction: **{label.upper()}**\n"
123
+ result_text += f"**Confidence:** {confidence*100:.2f}%\n\n"
124
+ result_text += f"**Details:** {info.get('description', 'N/A')}\n\n"
125
+ result_text += "### Class Probabilities\n"
126
+ for cls, prob in prob_dict.items():
127
+ bar = '█' * int(prob * 20)
128
+ result_text += f"- **{cls}**: {prob*100:.1f}% {bar}\n"
129
+
130
+ return annotated, result_text, prob_dict, None
131
+
132
+ except Exception as e:
133
+ return None, f"Error: {str(e)}", None, None
134
+
135
+
136
+ @spaces.GPU(duration=120)
137
+ def predict_video(video_path, skip_frames=5, apply_gradcam=True):
138
+ """Process a video file frame-by-frame."""
139
+ if video_path is None:
140
+ return None, "Please upload a video.", None
141
+
142
+ try:
143
+ engine = get_inference_engine()
144
+
145
+ # Create output path
146
+ output_path = tempfile.mktemp(suffix='.mp4')
147
+
148
+ stats = engine.predict_video(
149
+ video_path,
150
+ output_path=output_path,
151
+ skip_frames=skip_frames,
152
+ apply_gradcam=apply_gradcam
153
+ )
154
+
155
+ # Format stats
156
+ result_text = f"## Video Analysis Complete\n\n"
157
+ result_text += f"**Total Frames:** {stats['total_frames']}\n"
158
+ result_text += f"**Processed Frames:** {stats['processed_frames']}\n"
159
+ result_text += f"**Processing Time:** {stats['processing_time_seconds']:.2f}s\n"
160
+ result_text += f"**FPS:** {stats['processed_frames']/max(stats['processing_time_seconds'],0.001):.1f}\n\n"
161
+
162
+ # Class distribution
163
+ labels = [f['label'] for f in stats['frame_by_frame'] if f['processed']]
164
+ if labels:
165
+ from collections import Counter
166
+ counts = Counter(labels)
167
+ result_text += "### Detection Summary\n"
168
+ for cls, count in counts.most_common():
169
+ pct = count / len(labels) * 100
170
+ result_text += f"- **{cls}**: {count} frames ({pct:.1f}%)\n"
171
+
172
+ return output_path, result_text, stats
173
+
174
+ except Exception as e:
175
+ return None, f"Error: {str(e)}", None
176
+
177
+
178
+ def get_model_info():
179
+ """Get model information and statistics."""
180
+ try:
181
+ engine = get_inference_engine()
182
+ config = FireNetModel.get_model_config()
183
+
184
+ info_text = f"## Model Information\n\n"
185
+ info_text += f"**Architecture:** {config['architecture']}\n"
186
+ info_text += f"**Input Shape:** {config['input_shape']}\n"
187
+ info_text += f"**Number of Classes:** {config['num_classes']}\n"
188
+ info_text += f"**Class Labels:** {', '.join(config['class_labels'])}\n"
189
+ info_text += f"**Learning Rate:** {config['learning_rate']}\n"
190
+ info_text += f"**Image Size:** {config['image_size']}\n"
191
+ info_text += f"**Batch Size:** {config['batch_size']}\n"
192
+ info_text += f"**Grad-CAM Layer:** {config['last_conv_layer']}\n\n"
193
+
194
+ info_text += "### Model Files\n"
195
+ models_dir = Path('models')
196
+ if models_dir.exists():
197
+ for f in models_dir.glob('*'):
198
+ if f.suffix in ('.h5', '.keras'):
199
+ size_mb = f.stat().st_size / (1024 * 1024)
200
+ info_text += f"- **{f.name}**: {size_mb:.1f} MB\n"
201
+
202
+ info_text += "\n### Class Reference\n"
203
+ for cls, details in CLASS_INFO.items():
204
+ info_text += f"- {details['icon']} **{cls}**: {details['description']}\n"
205
+
206
+ return info_text
207
+
208
+ except Exception as e:
209
+ return f"Error loading model info: {str(e)}"
210
+
211
+
212
+ # Build Gradio interface
213
+ with gr.Blocks(title="Forest Fire Detection - FirenetCNN", theme=gr.themes.Soft()) as demo:
214
+ gr.Markdown(
215
+ """
216
+ # 🔥 Forest Fire Detection using FirenetCNN and XAI Techniques
217
+
218
+ Detect and classify forest fires from images and videos using deep learning with explainable AI (Grad-CAM).
219
+
220
+ **Classes:** `fire` | `no_fire` | `smoke`
221
+ """
222
+ )
223
+
224
+ with gr.Tabs():
225
+ # Tab 1: Image Classification
226
+ with gr.Tab("📷 Image Classification"):
227
+ with gr.Row():
228
+ with gr.Column(scale=1):
229
+ image_input = gr.Image(type="pil", label="Upload Image")
230
+ gradcam_check = gr.Checkbox(label="Apply Grad-CAM", value=True)
231
+ predict_btn = gr.Button("Predict", variant="primary")
232
+
233
+ with gr.Column(scale=1):
234
+ image_output = gr.Image(label="Annotated Result")
235
+ result_text = gr.Markdown(label="Prediction")
236
+ prob_chart = gr.Label(label="Probabilities")
237
+
238
+ predict_btn.click(
239
+ fn=predict_image,
240
+ inputs=[image_input, gradcam_check],
241
+ outputs=[image_output, result_text, prob_chart, gr.State()]
242
+ )
243
+
244
+ # Tab 2: Video Analysis
245
+ with gr.Tab("🎥 Video Analysis"):
246
+ with gr.Row():
247
+ with gr.Column(scale=1):
248
+ video_input = gr.Video(label="Upload Video")
249
+ skip_frames = gr.Slider(
250
+ minimum=1, maximum=30, value=5, step=1,
251
+ label="Process every Nth frame"
252
+ )
253
+ video_gradcam = gr.Checkbox(label="Apply Grad-CAM", value=True)
254
+ video_btn = gr.Button("Analyze Video", variant="primary")
255
+
256
+ with gr.Column(scale=1):
257
+ video_output = gr.Video(label="Processed Video")
258
+ video_stats = gr.Markdown(label="Statistics")
259
+
260
+ video_btn.click(
261
+ fn=predict_video,
262
+ inputs=[video_input, skip_frames, video_gradcam],
263
+ outputs=[video_output, video_stats, gr.State()]
264
+ )
265
+
266
+ # Tab 3: Webcam Inference
267
+ with gr.Tab("📹 Webcam Inference"):
268
+ gr.Markdown(
269
+ """
270
+ ### Live Webcam Detection
271
+
272
+ Click **Start Camera** to begin real-time fire/smoke detection.
273
+
274
+ **Note:** Webcam inference runs locally in your browser.
275
+ """
276
+ )
277
+ webcam_input = gr.Image(label="Webcam Feed")
278
+ webcam_output = gr.Image(label="Detection Result")
279
+
280
+ # Webcam processing would need real-time streaming
281
+ # For now, provide a static image upload alternative
282
+ gr.Markdown("*For live webcam detection, use the Python API directly:*")
283
+ gr.Markdown(
284
+ "```python\n"
285
+ "from src.inference import FireNetInference\n"
286
+ "engine = FireNetInference('models/FirenetCNN1.h5')\n"
287
+ "engine.predict_webcam()\n"
288
+ "```"
289
+ )
290
+
291
+ # Tab 4: Model Information
292
+ with gr.Tab("📊 Model Information"):
293
+ model_info = gr.Markdown(value=get_model_info)
294
+
295
+ gr.Markdown(
296
+ """
297
+ ### Evaluation Metrics (Test Set)
298
+
299
+ | Class | Precision | Recall | F1-Score | Support |
300
+ |-------|-----------|--------|----------|---------|
301
+ | fire | 0.92 | 0.81 | 0.86 | 121 |
302
+ | no_fire | 0.76 | 0.98 | 0.86 | 146 |
303
+ | smoke | 0.84 | 0.67 | 0.75 | 138 |
304
+ | **accuracy** | | | **0.82** | **405** |
305
+ | macro avg | 0.84 | 0.82 | 0.82 | 405 |
306
+ | weighted avg | 0.83 | 0.82 | 0.82 | 405 |
307
+ """
308
+ )
309
+
310
+ gr.Markdown(
311
+ """
312
+ ---
313
+ *Built with FirenetCNN (MobileNetV2) + Grad-CAM | [GitHub](https://github.com/OpelSpeedster/Forest-Fire-Detection-Using-FirenetCNN-and-XAI-Techniques)*
314
+ """
315
+ )
316
+
317
+
318
+ if __name__ == '__main__':
319
+ demo.queue().launch(
320
+ server_name="0.0.0.0",
321
+ server_port=int(os.environ.get("PORT", 7860)),
322
+ share=False,
323
+ )
config.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Development configuration and utilities for Forest Fire Detection project
3
+ """
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Dict, Any
7
+ class Config:
8
+ """Project configuration management"""
9
+
10
+ def __init__(self):
11
+ self.project_root = Path.cwd()
12
+ self.data_dir = self.project_root / "data"
13
+ self.models_dir = self.project_root / "models"
14
+ self.assets_dir = self.project_root / "assets"
15
+ self.notebooks_dir = self.project_root / "notebooks"
16
+ self.src_dir = self.project_root / "src"
17
+
18
+ # Load or create configuration
19
+ self.config_path = self.project_root / "config.json"
20
+ self.load_config()
21
+
22
+ def load_config(self) -> Dict[str, Any]:
23
+ """Load configuration from file or create default"""
24
+ if self.config_path.exists():
25
+ with open(self.config_path, 'r') as f:
26
+ self.config = json.load(f)
27
+ else:
28
+ self.config = self._get_default_config()
29
+ self.save_config()
30
+
31
+ return self.config
32
+
33
+ def _get_default_config(self) -> Dict[str, Any]:
34
+ """Get default project configuration"""
35
+ return {
36
+ "project_name": "Forest Fire Detection using FirenetCNN and XAI Techniques",
37
+ "version": "1.0.0",
38
+ "description": "Detect and classify forest fires with explainable AI",
39
+ "model": {
40
+ "input_size": [224, 224],
41
+ "num_classes": 3,
42
+ "class_labels": ["fire", "no_fire", "smoke"],
43
+ "reverse_class_map": {"fire": 0, "no_fire": 1, "smoke": 2}
44
+ },
45
+ "gradcam": {
46
+ "target_layer": "out_relu",
47
+ "alpha": 0.5,
48
+ "colormap": "cv2.COLORMAP_JET"
49
+ },
50
+ "app": {
51
+ "title": "Forest Fire Detection - FirenetCNN",
52
+ "port": 7860,
53
+ "share": True
54
+ },
55
+ "paths": {
56
+ "data_dir": "data",
57
+ "models_dir": "models",
58
+ "assets_dir": "assets",
59
+ "notebooks_dir": "notebooks",
60
+ "src_dir": "src"
61
+ }
62
+ }
63
+
64
+ def save_config(self) -> None:
65
+ """Save configuration to file"""
66
+ with open(self.config_path, 'w') as f:
67
+ json.dump(self.config, f, indent=2)
68
+
69
+ def get_model_paths(self) -> Dict[str, str]:
70
+ """Get all model file paths"""
71
+ return {
72
+ "modern": str(self.models_dir / "FirenetCNN.keras"),
73
+ "legacy": str(self.models_dir / "FirenetCNN1.h5"),
74
+ "alternative": str(self.models_dir / "firenet_model.h5")
75
+ }
76
+
77
+ def setup_directories(self) -> None:
78
+ """Create necessary directories"""
79
+ directories = [
80
+ self.models_dir,
81
+ self.assets_dir,
82
+ self.notebooks_dir,
83
+ self.src_dir,
84
+ self.data_dir
85
+ ]
86
+
87
+ for directory in directories:
88
+ directory.mkdir(parents=True, exist_ok=True)
89
+
90
+ def validate_environment(self) -> bool:
91
+ """Validate that required files and directories exist"""
92
+ required_items = [
93
+ self.notebooks_dir / "Fire_PredCopy.ipynb"
94
+ ]
95
+
96
+ for item in required_items:
97
+ if not item.exists():
98
+ print(f"⚠️ Warning: Required item not found: {item}")
99
+
100
+ return all(item.exists() for item in required_items)
101
+
102
+ def print_summary(self) -> None:
103
+ """Print project structure summary"""
104
+ print("=" * 60)
105
+ print(f"🚀 {self.config['project_name']}")
106
+ print("=" * 60)
107
+ print(f"📝 Version: {self.config['version']}")
108
+ print(f"📝 Description: {self.config['description']}")
109
+ print()
110
+ print("📂 Project Structure:")
111
+ print(f" • Models: {self.models_dir}/")
112
+ print(f" • Assets: {self.assets_dir}/")
113
+ print(f" • Notebooks: {self.notebooks_dir}/")
114
+ print(f" • Source: {self.src_dir}/")
115
+ print(f" • Data: {self.data_dir}/")
116
+ print()
117
+ print("🔧 Key Features:")
118
+ print(" • FirenetCNN (MobileNetV2 based)")
119
+ print(" • Grad-CAM Explainable AI")
120
+ print(" • Gradio Web Interface")
121
+ print(" • Image & Video Analysis")
122
+ print(" • Model Conversion Tools")
123
+ print(" • Full Documentation")
124
+ print("=" * 60)
125
+ if __name__ == "__main__":
126
+ config = Config()
127
+ config.print_summary()
models/FirenetCNN.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c580135a798d9855914e77031e3028112c3b993d94c058536cfe419ff5353554
3
+ size 25352336
models/FirenetCNN1.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d3bc11fdafb36cf0e43ea1af44015097a62a67118b43d27f0badd2b5867c8525
3
+ size 25352336
models/firenet_model.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6f39472ed93085dc6aa77b3c48d1cc741f40ab693b68eb003981b585b7277b7d
3
+ size 2113688
packages.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ ffmpeg
2
+ libgl1
3
+ libglib2.0-0
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ tensorflow[and-cuda]>=2.15.0,<2.20.0
2
+ opencv-python-headless>=4.8.0
3
+ numpy>=1.24.0
4
+ Pillow>=10.0.0
5
+ matplotlib>=3.7.0
6
+ seaborn>=0.12.0
7
+ scikit-learn>=1.3.0
8
+ gradio>=4.44.0
9
+ spaces>=0.30.0
10
+ requests>=2.31.0
11
+ python-dateutil>=2.8.0
src/__init__.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Forest Fire Detection using FirenetCNN and XAI Techniques
3
+ src/__init__.py
4
+ """
5
+
6
+ __version__ = "1.0.0"
7
+
8
+ from .model import FireNetModel
9
+ from .gradcam import GradCAM
10
+ from .inference import FireNetInference
11
+
12
+ __all__ = ["FireNetModel", "GradCAM", "FireNetInference"]
src/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (497 Bytes). View file
 
src/__pycache__/gradcam.cpython-313.pyc ADDED
Binary file (8.23 kB). View file
 
src/__pycache__/inference.cpython-313.pyc ADDED
Binary file (21 kB). View file
 
src/__pycache__/model.cpython-313.pyc ADDED
Binary file (8.67 kB). View file
 
src/__pycache__/training.cpython-313.pyc ADDED
Binary file (6.38 kB). View file
 
src/gradcam.py ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ src/gradcam.py
3
+ Grad-CAM implementation for visualizing model decisions
4
+ """
5
+ import cv2
6
+ import numpy as np
7
+ import tensorflow as tf
8
+ from typing import Optional, Tuple, Union
9
+ class GradCAM:
10
+ """
11
+ Gradient-weighted Class Activation Mapping (Grad-CAM)
12
+
13
+ Visualizes which regions of an image the model focuses on for its predictions.
14
+ Based on the original Grad-CAM paper: "Grad-CAM: Visual Explanations from Deep Neural Networks via Gradient-based Localization"
15
+ """
16
+
17
+ def __init__(self, model, last_conv_layer_name: str = 'out_relu'):
18
+ """
19
+ Initialize Grad-CAM with a Keras model.
20
+
21
+ Args:
22
+ model: Keras model
23
+ last_conv_layer_name: Name of the last convolutional layer to use
24
+ """
25
+ self.model = model
26
+ self.last_conv_layer_name = last_conv_layer_name
27
+
28
+ # Create the gradient model once
29
+ self.grad_model = self._create_gradient_model()
30
+
31
+ def _create_gradient_model(self):
32
+ """
33
+ Create a model that connects the last conv layer and model output.
34
+
35
+ Returns:
36
+ Keras Model that outputs both the last conv layer and predictions
37
+ """
38
+ return tf.keras.Model(
39
+ [self.model.inputs],
40
+ [self.model.get_layer(self.last_conv_layer_name).output, self.model.output]
41
+ )
42
+
43
+ def generate_heatmap(self, image_array: np.ndarray, class_index: int) -> np.ndarray:
44
+ """
45
+ Generate a Grad-CAM heatmap for a given image and class.
46
+
47
+ Args:
48
+ image_array: Preprocessed image array (with batch dimension)
49
+ class_index: Index of the class to generate heatmap for
50
+
51
+ Returns:
52
+ Generated heatmap (numpy array, shape: H x W)
53
+ """
54
+ with tf.GradientTape() as tape:
55
+ last_conv_layer_output, preds = self.grad_model(image_array)
56
+ class_channel = preds[:, class_index]
57
+
58
+ grads = tape.gradient(class_channel, last_conv_layer_output)
59
+
60
+ # Pool gradients across spatial dimensions
61
+ pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2))
62
+
63
+ # Get the output of the last conv layer
64
+ last_conv_layer_output = last_conv_layer_output[0]
65
+
66
+ # Compute the weighted combination of feature maps
67
+ heatmap = last_conv_layer_output @ pooled_grads[..., tf.newaxis]
68
+ heatmap = tf.squeeze(heatmap)
69
+
70
+ # Apply ReLU to get positive weights
71
+ heatmap = tf.maximum(heatmap, 0)
72
+
73
+ # Normalize heatmap to [0, 1]
74
+ max_val = tf.math.reduce_max(heatmap)
75
+ if max_val > 0:
76
+ heatmap = heatmap / max_val
77
+
78
+ return heatmap.numpy()
79
+
80
+ def overlay_heatmap(self, original_image: np.ndarray, heatmap: np.ndarray,
81
+ alpha: float = 0.5) -> np.ndarray:
82
+ """
83
+ Overlay heatmap on original image using JET colormap.
84
+
85
+ Args:
86
+ original_image: Original image (numpy array, RGB)
87
+ heatmap: Grad-CAM heatmap (normalized to [0, 1])
88
+ alpha: Weight for heatmap in overlay (0-1)
89
+
90
+ Returns:
91
+ Image with heatmap overlay (numpy array, RGB)
92
+ """
93
+ # Resize heatmap to match original image dimensions
94
+ heatmap = cv2.resize(heatmap, (original_image.shape[1], original_image.shape[0]))
95
+
96
+ # Convert heatmap to 8-bit format and apply JET colormap
97
+ heatmap = np.uint8(255 * heatmap)
98
+ heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)
99
+
100
+ # Convert original image to float for blending
101
+ original_float = original_image.astype(np.float32)
102
+ heatmap_float = heatmap.astype(np.float32)
103
+
104
+ # Blend original image with heatmap
105
+ superimposed_img = heatmap_float * alpha + original_float
106
+ superimposed_img = np.clip(superimposed_img, 0, 255).astype(np.uint8)
107
+
108
+ return superimposed_img
109
+
110
+ @staticmethod
111
+ def preprocess_frame(frame: np.ndarray, target_size: Tuple[int, int] = (224, 224)):
112
+ """
113
+ Preprocess a video frame for model inference.
114
+
115
+ Args:
116
+ frame: Input frame (numpy array, BGR format from OpenCV)
117
+ target_size: Target size for model input
118
+
119
+ Returns:
120
+ Preprocessed frame (numpy array)
121
+ """
122
+ # Resize to model input size
123
+ resized = cv2.resize(frame, target_size)
124
+ # Normalize to [0, 1]
125
+ normalized = resized / 255.0
126
+ # Add batch dimension
127
+ batched = np.expand_dims(normalized, axis=0)
128
+
129
+ return batched
130
+
131
+ @staticmethod
132
+ def get_confidence_and_prediction(predictions: np.ndarray,
133
+ class_labels: list) -> Tuple[str, float, np.ndarray]:
134
+ """
135
+ Get prediction label, confidence, and full probability array.
136
+
137
+ Args:
138
+ predictions: Model output probabilities (array of shape [1, 3])
139
+ class_labels: List of class labels
140
+
141
+ Returns:
142
+ Tuple of (predicted_label, confidence, probability_array)
143
+ """
144
+ prob_array = predictions[0]
145
+ max_idx = np.argmax(prob_array)
146
+ confidence = prob_array[max_idx]
147
+ label = class_labels[max_idx]
148
+
149
+ return label, confidence, prob_array
150
+
151
+ @staticmethod
152
+ def get_text_overlay_params(label: str, color_map: dict):
153
+ """
154
+ Get parameters for text overlay on frames.
155
+
156
+ Args:
157
+ label: Predicted class label
158
+ color_map: Dictionary mapping labels to colors
159
+
160
+ Returns:
161
+ Tuple of (text_color, background_color)
162
+ """
163
+ text_color = color_map.get(label, (255, 255, 255)) # White default
164
+ background_color = (0, 0, 0) # Black background
165
+ return text_color, background_color
166
+
167
+ @staticmethod
168
+ def create_text_overlay(frame: np.ndarray, text: str,
169
+ text_color: Tuple[int, int, int],
170
+ bg_color: Tuple[int, int, int] = (0, 0, 0)):
171
+ """
172
+ Create a text overlay with background on the frame.
173
+
174
+ Args:
175
+ frame: Input frame
176
+ text: Text to display
177
+ text_color: Color of the text
178
+ bg_color: Background color
179
+
180
+ Returns:
181
+ Frame with text overlay
182
+ """
183
+ # Get text size
184
+ font = cv2.FONT_HERSHEY_SIMPLEX
185
+ font_scale = 0.7
186
+ thickness = 2
187
+ (text_width, text_height), baseline = cv2.getTextSize(text, font, font_scale, thickness)
188
+
189
+ # Define text position with padding
190
+ text_x, text_y = 10, 40
191
+ rect_x1, rect_y1 = text_x - 5, text_y - text_height - baseline - 5
192
+ rect_x2, rect_y2 = text_x + text_width + 5, text_y + baseline + 5
193
+
194
+ # Draw background rectangle
195
+ cv2.rectangle(frame, (rect_x1, rect_y1), (rect_x2, rect_y2), bg_color, -1)
196
+
197
+ # Draw text
198
+ cv2.putText(frame, text, (text_x, text_y), font, font_scale, text_color, thickness)
199
+
200
+ return frame
src/inference.py ADDED
@@ -0,0 +1,539 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ src/inference.py
3
+ Unified inference interface for FirenetCNN
4
+ Supports single-image, video, and webcam inference with Grad-CAM visualization
5
+ """
6
+ import cv2
7
+ import numpy as np
8
+ import tensorflow as tf
9
+ from pathlib import Path
10
+ from typing import Optional, Tuple, Dict, Any
11
+ from PIL import Image
12
+ import time
13
+
14
+ from .model import FireNetModel
15
+ from .gradcam import GradCAM
16
+ class FireNetInference:
17
+ """
18
+ Unified inference engine for FirenetCNN
19
+
20
+ Provides interface for:
21
+ - Single image inference with optional Grad-CAM
22
+ - Video inference with frame-by-frame processing
23
+ - Real-time webcam inference
24
+ - Model evaluation and reporting
25
+ """
26
+
27
+ def __init__(self, model_path: str = 'models/FirenetCNN.keras'):
28
+ """
29
+ Initialize the inference engine.
30
+
31
+ Args:
32
+ model_path: Path to the trained model file
33
+ """
34
+ self.model_path = model_path
35
+ self.class_labels = FireNetModel.CLASS_LABELS
36
+ self.reverse_class_map = FireNetModel.REVERSE_CLASS_MAP
37
+ self.color_map = FireNetModel.TEXT_COLOR
38
+
39
+ # Load model and Grad-CAM components
40
+ self.model_wrapper = FireNetModel(model_path)
41
+ self.model = self.model_wrapper.load_pretrained_model()
42
+ self.gradcam = GradCAM(self.model, self.model_wrapper.last_conv_layer_name)
43
+
44
+ # Create gradient model for Grad-CAM
45
+ self.grad_model = tf.keras.Model(
46
+ [self.model.inputs],
47
+ [self.model.get_layer(self.model_wrapper.last_conv_layer_name).output, self.model.output]
48
+ )
49
+
50
+ def predict_image(self, image_path: str,
51
+ apply_gradcam: bool = True,
52
+ output_path: Optional[str] = None) -> Dict[str, Any]:
53
+ """
54
+ Perform inference on a single image.
55
+
56
+ Args:
57
+ image_path: Path to input image
58
+ apply_gradcam: Whether to generate Grad-CAM heatmap
59
+ output_path: Optional path to save annotated image
60
+
61
+ Returns:
62
+ Dictionary with prediction results and annotations
63
+ """
64
+ # Load image
65
+ image = cv2.imread(str(image_path))
66
+ if image is None:
67
+ raise ValueError(f"Could not load image: {image_path}")
68
+
69
+ # Store original for potential output
70
+ original_image = image.copy()
71
+ height, width = image.shape[:2]
72
+
73
+ # Preprocess for model
74
+ input_frame = GradCAM.preprocess_frame(image)
75
+
76
+ # Get predictions
77
+ predictions = self.model.predict(input_frame, verbose=0)
78
+ label, confidence, prob_array = GradCAM.get_confidence_and_prediction(
79
+ predictions, self.class_labels
80
+ )
81
+
82
+ # Generate Grad-CAM if requested and prediction is fire or smoke
83
+ heatmap = None
84
+ superimposed_img = None
85
+
86
+ if apply_gradcam and label in ['fire', 'smoke']:
87
+ # Convert BGR to RGB for Grad-CAM processing
88
+ rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
89
+
90
+ # Prepare input for Grad-CAM
91
+ img_array = tf.expand_dims(tf.convert_to_tensor(rgb_image / 255.0), 0)
92
+ class_index = self.reverse_class_map[label]
93
+
94
+ # Generate heatmap
95
+ heatmap = self.gradcam.generate_heatmap(img_array, class_index)
96
+
97
+ # Overlay heatmap
98
+ superimposed_img = self.gradcam.overlay_heatmap(
99
+ original_image, heatmap, alpha=0.5
100
+ )
101
+
102
+ # Create text overlay parameters
103
+ text_color, bg_color = GradCAM.get_text_overlay_params(label, self.color_map)
104
+ result_text = f"Class: {label} ({confidence*100:.2f}%)"
105
+
106
+ # Add text overlay to image
107
+ annotated_image = GradCAM.create_text_overlay(
108
+ superimposed_img if superimposed_img is not None else original_image,
109
+ result_text, text_color, bg_color
110
+ )
111
+
112
+ # Save annotated image if output path provided
113
+ if output_path:
114
+ Path(output_path).parent.mkdir(parents=True, exist_ok=True)
115
+ cv2.imwrite(str(output_path), annotated_image)
116
+
117
+ # Return results as dictionary
118
+ result = {
119
+ 'image_path': image_path,
120
+ 'label': label,
121
+ 'confidence': confidence,
122
+ 'probability_array': prob_array.tolist(),
123
+ 'class_labels': self.class_labels,
124
+ 'has_gradcam': heatmap is not None,
125
+ 'heatmap': heatmap.tolist() if heatmap is not None else None,
126
+ 'annotated_image': annotated_image if output_path is None else None,
127
+ 'processing_time': 0 # Will be measured externally if needed
128
+ }
129
+
130
+ return result
131
+
132
+ def predict_video(self, video_path: str,
133
+ output_path: Optional[str] = None,
134
+ skip_frames: int = 5,
135
+ apply_gradcam: bool = True) -> Dict[str, Any]:
136
+ """
137
+ Process a video file frame-by-frame.
138
+
139
+ Args:
140
+ video_path: Path to input video file
141
+ output_path: Optional path to save processed video
142
+ skip_frames: Process every Nth frame (for performance)
143
+ apply_gradcam: Whether to generate Grad-CAM for fire/smoke frames
144
+
145
+ Returns:
146
+ Dictionary with video processing statistics
147
+ """
148
+ cap = cv2.VideoCapture(video_path)
149
+ if not cap.isOpened():
150
+ raise ValueError(f"Could not open video file: {video_path}")
151
+
152
+ # Get video properties
153
+ frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
154
+ fps = cap.get(cv2.CAP_PROP_FPS)
155
+ width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
156
+ height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
157
+
158
+ # Setup video writer if output path provided
159
+ video_writer = None
160
+ if output_path:
161
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
162
+ video_writer = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
163
+
164
+ # Statistics
165
+ stats = {
166
+ 'total_frames': frame_count,
167
+ 'processed_frames': 0,
168
+ 'predictions': [],
169
+ 'frame_by_frame': [],
170
+ 'final_frame': None,
171
+ 'processing_time_seconds': 0
172
+ }
173
+
174
+ # State tracking across frames
175
+ last_label = "Initializing..."
176
+ last_confidence = 0
177
+ last_heatmap = None
178
+ last_color = (255, 255, 255) # White
179
+
180
+ start_time = time.time()
181
+ frame_number = 0
182
+
183
+ while cap.isOpened() and frame_number < frame_count:
184
+ ret, frame = cap.read()
185
+ if not ret:
186
+ break
187
+
188
+ # Process only every Nth frame for performance
189
+ if frame_number % skip_frames == 0 or frame_number == 0:
190
+ # Preprocess frame
191
+ input_frame = GradCAM.preprocess_frame(frame)
192
+
193
+ # Get predictions
194
+ predictions = self.model.predict(input_frame, verbose=0)
195
+ label, confidence, prob_array = GradCAM.get_confidence_and_prediction(
196
+ predictions, self.class_labels
197
+ )
198
+
199
+ # Update state
200
+ last_label = label
201
+ last_confidence = confidence
202
+
203
+ # Generate Grad-CAM if requested and prediction is fire or smoke
204
+ if apply_gradcam and label in ['fire', 'smoke']:
205
+ rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
206
+ img_array = tf.expand_dims(tf.convert_to_tensor(rgb_image / 255.0), 0)
207
+ class_index = self.reverse_class_map[label]
208
+
209
+ last_heatmap = self.gradcam.generate_heatmap(img_array, class_index)
210
+ last_color = self.color_map.get(label, (255, 255, 255))
211
+ else:
212
+ last_heatmap = None
213
+ last_color = self.color_map.get(label, (255, 255, 255))
214
+
215
+ # Update statistics
216
+ stats['processed_frames'] += 1
217
+
218
+ # Apply Grad-CAM if available
219
+ display_frame = frame.copy()
220
+ if last_heatmap is not None:
221
+ display_frame = self.gradcam.overlay_heatmap(frame, last_heatmap, alpha=0.5)
222
+
223
+ # Create text overlay
224
+ text_color, bg_color = GradCAM.get_text_overlay_params(last_label, self.color_map)
225
+ result_text = f"Class: {last_label} ({last_confidence*100:.2f}%)"
226
+ display_frame = GradCAM.create_text_overlay(display_frame, result_text, text_color, bg_color)
227
+
228
+ # Save frame if video writer provided
229
+ if video_writer:
230
+ video_writer.write(display_frame)
231
+
232
+ # Store frame info
233
+ frame_info = {
234
+ 'frame_number': frame_number,
235
+ 'label': last_label,
236
+ 'confidence': last_confidence,
237
+ 'processed': (frame_number % skip_frames == 0 or frame_number == 0),
238
+ 'has_gradcam': last_heatmap is not None,
239
+ 'color_bgr': last_color
240
+ }
241
+ stats['frame_by_frame'].append(frame_info)
242
+
243
+ # Store last frame for final output
244
+ stats['final_frame'] = display_frame.copy()
245
+
246
+ frame_number += 1
247
+
248
+ # Cleanup
249
+ cap.release()
250
+ if video_writer:
251
+ video_writer.release()
252
+
253
+ # Calculate processing time
254
+ processing_time = time.time() - start_time
255
+ stats['processing_time_seconds'] = processing_time
256
+
257
+ return stats
258
+
259
+ def predict_webcam(self, window_name: str = "Live Webcam Inference (Slow)",
260
+ apply_gradcam: bool = False,
261
+ max_frames: Optional[int] = None) -> Dict[str, Any]:
262
+ """
263
+ Perform real-time inference using webcam.
264
+
265
+ Args:
266
+ window_name: Name of the display window
267
+ apply_gradcam: Whether to generate Grad-CAM (normally False for webcam)
268
+ max_frames: Maximum number of frames to process (None for infinite)
269
+
270
+ Returns:
271
+ Dictionary with webcam inference results
272
+ """
273
+ cap = cv2.VideoCapture(0)
274
+ if not cap.isOpened():
275
+ raise RuntimeError("Could not open webcam.")
276
+
277
+ # Statistics
278
+ stats = {
279
+ 'frames_processed': 0,
280
+ 'predictions': [],
281
+ 'detected_classes': set(),
282
+ 'processing_times': [],
283
+ 'live_feed_active': True
284
+ }
285
+
286
+ frame_count = 0
287
+
288
+ print(f"Webcam started. Press 'q' to quit.")
289
+
290
+ try:
291
+ while cap.isOpened() and (max_frames is None or frame_count < max_frames):
292
+ ret, frame = cap.read()
293
+ if not ret:
294
+ print("Error: Failed to capture frame.")
295
+ break
296
+
297
+ # Start timing
298
+ start_time = time.time()
299
+
300
+ # Preprocess frame
301
+ input_frame = GradCAM.preprocess_frame(frame)
302
+
303
+ # Get predictions
304
+ predictions = self.model.predict(input_frame, verbose=0)
305
+ label, confidence, prob_array = GradCAM.get_confidence_and_prediction(
306
+ predictions, self.class_labels
307
+ )
308
+
309
+ # End timing
310
+ processing_time = time.time() - start_time
311
+ stats['processing_times'].append(processing_time)
312
+
313
+ # Update stats
314
+ stats['frames_processed'] += 1
315
+ stats['detected_classes'].add(label)
316
+ frame_info = {
317
+ 'frame_number': frame_count,
318
+ 'label': label,
319
+ 'confidence': confidence,
320
+ 'processing_time': processing_time
321
+ }
322
+ stats['predictions'].append(frame_info)
323
+
324
+ # Generate Grad-CAM if requested and prediction is fire or smoke
325
+ heatmap = None
326
+ if apply_gradcam and label in ['fire', 'smoke']:
327
+ rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
328
+ img_array = tf.expand_dims(tf.convert_to_tensor(rgb_image / 255.0), 0)
329
+ class_index = self.reverse_class_map[label]
330
+
331
+ heatmap = self.gradcam.generate_heatmap(img_array, class_index)
332
+
333
+ # Create text overlay
334
+ text_color, bg_color = GradCAM.get_text_overlay_params(label, self.color_map)
335
+ result_text = f"Class: {label} ({confidence*100:.2f}%)"
336
+ display_frame = GradCAM.create_text_overlay(frame, result_text, text_color, bg_color)
337
+
338
+ # Apply Grad-CAM if available
339
+ if heatmap is not None:
340
+ display_frame = self.gradcam.overlay_heatmap(display_frame, heatmap, alpha=0.5)
341
+
342
+ # Display the frame
343
+ cv2.imshow(window_name, display_frame)
344
+
345
+ # Check for quit
346
+ if cv2.waitKey(1) & 0xFF == ord('q'):
347
+ break
348
+
349
+ frame_count += 1
350
+
351
+ finally:
352
+ # Cleanup
353
+ cap.release()
354
+ cv2.destroyAllWindows()
355
+ stats['live_feed_active'] = False
356
+
357
+ return stats
358
+
359
+ def create_gradcam_demo_image(self, image_path: str, class_to_highlight: Optional[str] = None) -> Dict[str, Any]:
360
+ """
361
+ Create a demonstration image with Grad-CAM for all classes.
362
+
363
+ Args:
364
+ image_path: Path to input image
365
+ class_to_highlight: Optional specific class to highlight (default: auto-select best)
366
+
367
+ Returns:
368
+ Dictionary with demo images for each class
369
+ """
370
+ # Load and preprocess image
371
+ image = cv2.imread(str(image_path))
372
+ if image is None:
373
+ raise ValueError(f"Could not load image: {image_path}")
374
+
375
+ rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
376
+ img_array = tf.expand_dims(tf.convert_to_tensor(rgb_image / 255.0), 0)
377
+
378
+ # Get predictions
379
+ predictions = self.model.predict(img_array, verbose=0)
380
+ prob_array = predictions[0]
381
+
382
+ # Determine which class to highlight (default: class with highest probability)
383
+ if class_to_highlight is None:
384
+ max_idx = np.argmax(prob_array)
385
+ class_to_highlight = self.class_labels[max_idx]
386
+
387
+ # Create demo images for each class
388
+ demo_images = {}
389
+
390
+ for class_label in self.class_labels:
391
+ # Get class index
392
+ class_index = self.reverse_class_map[class_label]
393
+
394
+ # Generate heatmap
395
+ heatmap = self.gradcam.generate_heatmap(img_array, class_index)
396
+
397
+ # Overlay heatmap
398
+ if class_label == class_to_highlight:
399
+ # For highlighted class, show the original heatmap
400
+ overlaid = self.gradcam.overlay_heatmap(
401
+ rgb_image, heatmap, alpha=0.6
402
+ )
403
+ overlaid = cv2.cvtColor(overlaid, cv2.COLOR_RGB2BGR)
404
+ else:
405
+ # For other classes, show grayscale heatmap
406
+ heatmap_bgr = cv2.applyColorMap(
407
+ np.uint8(255 * heatmap), cv2.COLORMAP_JET
408
+ )
409
+ overlaid = heatmap_bgr
410
+
411
+ # Create text overlay
412
+ text_color, bg_color = GradCAM.get_text_overlay_params(class_label, self.color_map)
413
+ result_text = f"{class_label} ({prob_array[class_index]*100:.1f}%)"
414
+ demo_images[class_label] = GradCAM.create_text_overlay(
415
+ overlaid, result_text, text_color, bg_color
416
+ )
417
+
418
+ return {
419
+ 'original_image': cv2.cvtColor(rgb_image, cv2.COLOR_RGB2BGR),
420
+ 'prediction': {
421
+ 'predicted_class': self.class_labels[np.argmax(prob_array)],
422
+ 'confidence': float(np.max(prob_array)),
423
+ 'probabilities': prob_array.tolist()
424
+ },
425
+ 'demo_images': demo_images,
426
+ 'class_to_highlight': class_to_highlight
427
+ }
428
+
429
+ @classmethod
430
+ def convert_model_format(cls, input_path: str, output_path: str) -> None:
431
+ """
432
+ Convert model from legacy format to modern Keras format.
433
+
434
+ Args:
435
+ input_path: Path to input model (HDF5 format)
436
+ output_path: Path to save converted model
437
+ """
438
+ from tensorflow.keras.models import load_model
439
+
440
+ model = load_model(input_path, compile=False)
441
+ model.save(output_path, save_format='keras')
442
+ print(f"Model converted and saved to: {output_path}")
443
+
444
+ @classmethod
445
+ def evaluate_model_on_dataset(cls, model_path: str,
446
+ test_dir: str,
447
+ output_report: Optional[str] = None) -> Dict[str, Any]:
448
+ """
449
+ Evaluate model performance on a dataset.
450
+
451
+ Args:
452
+ model_path: Path to model file
453
+ test_dir: Path to test dataset directory (with class subdirectories)
454
+ output_report: Optional path to save evaluation report
455
+
456
+ Returns:
457
+ Dictionary with evaluation results
458
+ """
459
+ from sklearn.metrics import classification_report, confusion_matrix
460
+ import matplotlib.pyplot as plt
461
+ import seaborn as sns
462
+
463
+ # Load model
464
+ model_wrapper = FireNetModel(model_path)
465
+ model = model_wrapper.load_pretrained_model()
466
+
467
+ # Create data generators
468
+ test_datagen = ImageDataGenerator(rescale=1./255.)
469
+ test_generator = test_datagen.flow_from_directory(
470
+ test_dir,
471
+ target_size=FireNetModel.IMAGE_SIZE,
472
+ batch_size=FireNetModel.BATCH_SIZE,
473
+ class_mode='categorical',
474
+ shuffle=False
475
+ )
476
+
477
+ # Make predictions
478
+ y_pred = model.predict(test_generator, verbose=0)
479
+ y_pred_classes = np.argmax(y_pred, axis=1)
480
+
481
+ # Get true labels
482
+ y_true = test_generator.classes
483
+
484
+ # Generate classification report
485
+ report = classification_report(
486
+ y_true,
487
+ y_pred_classes,
488
+ target_names=FireNetModel.CLASS_LABELS,
489
+ output_dict=True
490
+ )
491
+
492
+ # Generate confusion matrix
493
+ cm = confusion_matrix(y_true, y_pred_classes)
494
+
495
+ # Create and save visualization
496
+ plt.figure(figsize=(10, 8))
497
+ sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
498
+ xticklabels=FireNetModel.CLASS_LABELS,
499
+ yticklabels=FireNetModel.CLASS_LABELS)
500
+ plt.title('Confusion Matrix')
501
+ plt.ylabel('True Label')
502
+ plt.xlabel('Predicted Label')
503
+
504
+ if output_report:
505
+ plt.savefig(output_report, dpi=150, bbox_inches='tight')
506
+ plt.close()
507
+
508
+ # Save report as text file if requested
509
+ if output_report and output_report.endswith('.txt'):
510
+ with open(output_report, 'w') as f:
511
+ f.write("Forest Fire Detection Model Evaluation Report\n")
512
+ f.write("=" * 60 + "\n\n")
513
+ f.write(f"Total samples: {len(y_true)}\n")
514
+ f.write(f"Classes: {', '.join(FireNetModel.CLASS_LABELS)}\n\n")
515
+ f.write("Classification Report:\n")
516
+ f.write(classification_report(y_true, y_pred_classes,
517
+ target_names=FireNetModel.CLASS_LABELS) + "\n\n")
518
+ f.write("Confusion Matrix:\n")
519
+ f.write(str(cm) + "\n")
520
+
521
+ # Add detailed metrics
522
+ f.write("\nDetailed Metrics:\n")
523
+ for i, class_name in enumerate(FireNetModel.CLASS_LABELS):
524
+ precision = report[class_name]['precision']
525
+ recall = report[class_name]['recall']
526
+ f1 = report[class_name]['f1-score']
527
+ support = report[class_name]['support']
528
+ f.write(f"{class_name:10} - Precision: {precision:.3f}, "
529
+ f"Recall: {recall:.3f}, F1-score: {f1:.3f}, "
530
+ f"Support: {support}\n")
531
+
532
+ return {
533
+ 'classification_report': report,
534
+ 'confusion_matrix': cm.tolist(),
535
+ 'total_samples': len(y_true),
536
+ 'accuracy': report['accuracy'],
537
+ 'macro_avg_f1': report['macro avg']['f1-score'],
538
+ 'weighted_avg_f1': report['weighted avg']['f1-score']
539
+ }
src/model.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ src/model.py
3
+ FirenetCNN model definition and utilities
4
+ """
5
+ import json
6
+ import numpy as np
7
+ from pathlib import Path
8
+ from typing import Dict, Tuple, Optional, List
9
+ import tensorflow as tf
10
+ from tensorflow.keras.applications import MobileNetV2
11
+ from tensorflow.keras.layers import Dense, GlobalAveragePooling2D, Dropout
12
+ from tensorflow.keras.models import Model, load_model
13
+ from tensorflow.keras.optimizers import Adam
14
+ from tensorflow.keras.preprocessing.image import ImageDataGenerator
15
+ class FireNetModel:
16
+ """FirenetCNN model implementation using MobileNetV2 transfer learning"""
17
+
18
+ # Model configuration constants
19
+ IMAGE_SIZE = (224, 224)
20
+ BATCH_SIZE = 32
21
+ LEARNING_RATE = 0.0001
22
+ EPOCHS = 100
23
+ CLASS_LABELS = ['fire', 'no_fire', 'smoke'] # Model's actual output order
24
+ REVERSE_CLASS_MAP = {'fire': 0, 'no_fire': 1, 'smoke': 2}
25
+
26
+ # Text overlay styling (OpenCV BGR format)
27
+ TEXT_COLOR = {
28
+ 'fire': (0, 0, 255), # Red
29
+ 'smoke': (0, 255, 255), # Yellow
30
+ 'no_fire': (0, 255, 0), # Green
31
+ }
32
+
33
+ def __init__(self, model_path: str = 'models/FirenetCNN.keras'):
34
+ """
35
+ Initialize the FirenetCNN model.
36
+
37
+ Args:
38
+ model_path: Path to the trained Keras model (.keras or .h5 format)
39
+ """
40
+ self.model_path = Path(model_path)
41
+ self.model = None
42
+ self.last_conv_layer_name = 'out_relu'
43
+
44
+ # Ensure model directory exists
45
+ self.model_path.parent.mkdir(parents=True, exist_ok=True)
46
+
47
+ @staticmethod
48
+ def build_model(input_shape: Tuple[int, int, int] = (224, 224, 3)) -> Model:
49
+ """
50
+ Build the FirenetCNN architecture.
51
+
52
+ Args:
53
+ input_shape: Input shape for the model (default: 224x224x3)
54
+
55
+ Returns:
56
+ Compiled Keras model
57
+ """
58
+ base_model = MobileNetV2(weights='imagenet', include_top=False,
59
+ input_shape=input_shape)
60
+
61
+ # Freeze the base model layers
62
+ base_model.trainable = False
63
+
64
+ # Classification head
65
+ x = base_model.output
66
+ x = GlobalAveragePooling2D()(x)
67
+ x = Dense(1024, activation='relu')(x)
68
+ x = Dropout(0.5)(x)
69
+ predictions = Dense(3, activation='softmax')(x)
70
+
71
+ model = Model(inputs=base_model.input, outputs=predictions)
72
+
73
+ model.compile(optimizer=Adam(learning_rate=0.0001),
74
+ loss='categorical_crossentropy',
75
+ metrics=['accuracy'])
76
+
77
+ return model
78
+
79
+ @staticmethod
80
+ def create_data_generators(train_dir: str, val_dir: str):
81
+ """
82
+ Create data generators for training and validation.
83
+
84
+ Args:
85
+ train_dir: Path to training data directory
86
+ val_dir: Path to validation data directory
87
+
88
+ Returns:
89
+ Tuple of (train_generator, validation_generator)
90
+ """
91
+ # Training data with augmentation
92
+ train_datagen = ImageDataGenerator(
93
+ rescale=1./255.,
94
+ rotation_range=40,
95
+ width_shift_range=0.2,
96
+ height_shift_range=0.2,
97
+ shear_range=0.2,
98
+ zoom_range=0.2,
99
+ horizontal_flip=True,
100
+ fill_mode='nearest'
101
+ )
102
+
103
+ # Validation data without augmentation
104
+ val_test_datagen = ImageDataGenerator(rescale=1./255.)
105
+
106
+ train_generator = train_datagen.flow_from_directory(
107
+ train_dir,
108
+ target_size=FireNetModel.IMAGE_SIZE,
109
+ batch_size=FireNetModel.BATCH_SIZE,
110
+ class_mode='categorical'
111
+ )
112
+
113
+ validation_generator = val_test_datagen.flow_from_directory(
114
+ val_dir,
115
+ target_size=FireNetModel.IMAGE_SIZE,
116
+ batch_size=FireNetModel.BATCH_SIZE,
117
+ class_mode='categorical'
118
+ )
119
+
120
+ return train_generator, validation_generator
121
+
122
+ def load_pretrained_model(self, model_path: Optional[str] = None) -> Model:
123
+ """
124
+ Load a pretrained model from file.
125
+
126
+ Args:
127
+ model_path: Path to model file (optional, uses instance path if None)
128
+
129
+ Returns:
130
+ Loaded Keras model
131
+
132
+ Raises:
133
+ FileNotFoundError: If model file does not exist
134
+ """
135
+ path = Path(model_path) if model_path else self.model_path
136
+
137
+ if not path.exists():
138
+ raise FileNotFoundError(f"Model file not found: {path}")
139
+
140
+ try:
141
+ # Try to load as modern Keras .keras format
142
+ self.model = load_model(str(path), compile=False)
143
+ return self.model
144
+ except Exception:
145
+ # Fallback to legacy .h5 format
146
+ if path.suffix == '.h5':
147
+ self.model = load_model(str(path), compile=False)
148
+ return self.model
149
+ raise ValueError(f"Unsupported model format or file not found: {path}")
150
+
151
+ def save_model(self, path: str) -> None:
152
+ """
153
+ Save the model to file.
154
+
155
+ Args:
156
+ path: Path to save the model
157
+ """
158
+ if self.model is None:
159
+ raise ValueError("Model not loaded. Call load_model() first.")
160
+
161
+ self.model.save(path)
162
+
163
+ @staticmethod
164
+ def preprocess_image(image_path: str) -> tf.Tensor:
165
+ """
166
+ Preprocess a single image for inference.
167
+
168
+ Args:
169
+ image_path: Path to the image file
170
+
171
+ Returns:
172
+ Preprocessed image tensor
173
+ """
174
+ img = tf.keras.utils.load_img(image_path, target_size=FireNetModel.IMAGE_SIZE)
175
+ img_array = tf.keras.utils.img_to_array(img)
176
+ img_array = tf.expand_dims(img_array, 0) # Add batch dimension
177
+ img_array = img_array / 255.0
178
+ return img_array
179
+
180
+ @staticmethod
181
+ def get_model_config() -> Dict:
182
+ """
183
+ Get model configuration metadata.
184
+
185
+ Returns:
186
+ Dictionary with model configuration
187
+ """
188
+ return {
189
+ 'input_shape': (*FireNetModel.IMAGE_SIZE, 3),
190
+ 'num_classes': len(FireNetModel.CLASS_LABELS),
191
+ 'class_labels': FireNetModel.CLASS_LABELS,
192
+ 'reverse_class_map': FireNetModel.REVERSE_CLASS_MAP,
193
+ 'learning_rate': FireNetModel.LEARNING_RATE,
194
+ 'image_size': FireNetModel.IMAGE_SIZE,
195
+ 'batch_size': FireNetModel.BATCH_SIZE,
196
+ 'architecture': 'FirenetCNN (MobileNetV2 + custom classifier head)',
197
+ 'last_conv_layer': 'out_relu'
198
+ }
199
+
200
+ @classmethod
201
+ def save_model_config(cls, path: str) -> None:
202
+ """
203
+ Save model configuration to JSON.
204
+
205
+ Args:
206
+ path: Path to save configuration JSON
207
+ """
208
+ config = cls.get_model_config()
209
+ with open(path, 'w') as f:
210
+ json.dump(config, f, indent=2)
211
+
212
+ @classmethod
213
+ def load_model_config(cls, path: str) -> Dict:
214
+ """
215
+ Load model configuration from JSON.
216
+
217
+ Args:
218
+ path: Path to configuration JSON
219
+
220
+ Returns:
221
+ Dictionary with model configuration
222
+ """
223
+ with open(path, 'r') as f:
224
+ return json.load(f)
src/training.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ src/training.py
3
+ Training pipeline for FirenetCNN model
4
+ """
5
+ import tensorflow as tf
6
+ from pathlib import Path
7
+ from typing import Optional, Tuple, Dict, Any
8
+
9
+ from .model import FireNetModel
10
+
11
+
12
+ def train(
13
+ train_dir: str,
14
+ val_dir: str,
15
+ model_save_path: str = 'models/FirenetCNN.keras',
16
+ epochs: int = 100,
17
+ learning_rate: float = 0.0001,
18
+ batch_size: int = 32,
19
+ image_size: Tuple[int, int] = (224, 224),
20
+ fine_tune: bool = False,
21
+ fine_tune_epochs: int = 10,
22
+ fine_tune_lr: float = 1e-5,
23
+ callback_save_best: bool = True,
24
+ callback_early_stopping: bool = True,
25
+ early_stopping_patience: int = 10,
26
+ ) -> Dict[str, Any]:
27
+ """
28
+ Train the FirenetCNN model.
29
+
30
+ Args:
31
+ train_dir: Path to training data directory
32
+ val_dir: Path to validation data directory
33
+ model_save_path: Path to save the trained model
34
+ epochs: Number of training epochs
35
+ learning_rate: Learning rate for initial training
36
+ batch_size: Training batch size
37
+ image_size: Input image size (height, width)
38
+ fine_tune: Whether to fine-tune the base model after initial training
39
+ fine_tune_epochs: Number of fine-tuning epochs
40
+ fine_tune_lr: Learning rate for fine-tuning
41
+ callback_save_best: Save model with best validation accuracy
42
+ callback_early_stopping: Stop training if validation loss stops improving
43
+ early_stopping_patience: Patience for early stopping
44
+
45
+ Returns:
46
+ Dictionary with training history and results
47
+ """
48
+ # Build model
49
+ model = FireNetModel.build_model(input_shape=(*image_size, 3))
50
+
51
+ # Create data generators
52
+ train_gen, val_gen = FireNetModel.create_data_generators(train_dir, val_dir)
53
+
54
+ # Setup callbacks
55
+ callbacks = []
56
+
57
+ save_path = Path(model_save_path)
58
+ save_path.parent.mkdir(parents=True, exist_ok=True)
59
+
60
+ if callback_save_best:
61
+ best_model_path = save_path.with_name(save_path.stem + '_best' + save_path.suffix)
62
+ callbacks.append(tf.keras.callbacks.ModelCheckpoint(
63
+ str(best_model_path),
64
+ monitor='val_accuracy',
65
+ save_best_only=True,
66
+ mode='max',
67
+ verbose=1
68
+ ))
69
+
70
+ if callback_early_stopping:
71
+ callbacks.append(tf.keras.callbacks.EarlyStopping(
72
+ monitor='val_loss',
73
+ patience=early_stopping_patience,
74
+ restore_best_weights=True,
75
+ verbose=1
76
+ ))
77
+
78
+ # Initial training (frozen base model)
79
+ print(f"Phase 1: Training with frozen base for {epochs} epochs...")
80
+ history = model.fit(
81
+ train_gen,
82
+ epochs=epochs,
83
+ validation_data=val_gen,
84
+ callbacks=callbacks,
85
+ verbose=1
86
+ )
87
+
88
+ # Fine-tuning phase
89
+ if fine_tune:
90
+ print(f"\nPhase 2: Fine-tuning for {fine_tune_epochs} epochs...")
91
+
92
+ # Unfreeze the base model
93
+ base_model = None
94
+ for layer in model.layers:
95
+ if hasattr(layer, 'layers') and len(layer.layers) > 50:
96
+ base_model = layer
97
+ break
98
+
99
+ if base_model is not None:
100
+ base_model.trainable = True
101
+
102
+ # Recompile with lower learning rate
103
+ model.compile(
104
+ optimizer=tf.keras.optimizers.Adam(learning_rate=fine_tune_lr),
105
+ loss='categorical_crossentropy',
106
+ metrics=['accuracy']
107
+ )
108
+
109
+ # Continue training
110
+ history_fine = model.fit(
111
+ train_gen,
112
+ epochs=fine_tune_epochs,
113
+ validation_data=val_gen,
114
+ callbacks=callbacks,
115
+ verbose=1
116
+ )
117
+
118
+ # Merge histories
119
+ for key in history_fine.history:
120
+ history.history[key].extend(history_fine.history[key])
121
+
122
+ # Save final model
123
+ model.save(str(save_path))
124
+ print(f"\nModel saved to: {save_path}")
125
+
126
+ return {
127
+ 'history': history.history,
128
+ 'epochs_completed': len(history.history['accuracy']),
129
+ 'final_train_acc': history.history['accuracy'][-1],
130
+ 'final_val_acc': history.history['val_accuracy'][-1],
131
+ 'model_path': str(save_path)
132
+ }
133
+
134
+
135
+ if __name__ == '__main__':
136
+ import argparse
137
+
138
+ parser = argparse.ArgumentParser(description='Train FirenetCNN model')
139
+ parser.add_argument('--train-dir', default='data/forestfire-classifier-dataset/train',
140
+ help='Training data directory')
141
+ parser.add_argument('--val-dir', default='data/forestfire-classifier-dataset/val',
142
+ help='Validation data directory')
143
+ parser.add_argument('--model-path', default='models/FirenetCNN.keras',
144
+ help='Path to save trained model')
145
+ parser.add_argument('--epochs', type=int, default=100,
146
+ help='Number of training epochs')
147
+ parser.add_argument('--fine-tune', action='store_true',
148
+ help='Enable fine-tuning phase')
149
+ parser.add_argument('--fine-tune-epochs', type=int, default=10,
150
+ help='Number of fine-tuning epochs')
151
+
152
+ args = parser.parse_args()
153
+
154
+ results = train(
155
+ train_dir=args.train_dir,
156
+ val_dir=args.val_dir,
157
+ model_save_path=args.model_path,
158
+ epochs=args.epochs,
159
+ fine_tune=args.fine_tune,
160
+ fine_tune_epochs=args.fine_tune_epochs
161
+ )
162
+
163
+ print(f"\nTraining complete!")
164
+ print(f"Final train accuracy: {results['final_train_acc']:.4f}")
165
+ print(f"Final val accuracy: {results['final_val_acc']:.4f}")