Spaces:
Paused
Paused
| """ | |
| app.py - Gradio Web Application for Forest Fire Detection | |
| Provides a web interface for image classification, video analysis, and model info. | |
| """ | |
| # `spaces` must be imported before any CUDA-touching library (tensorflow is | |
| # imported transitively below via `src.inference`) for Hugging Face ZeroGPU. | |
| import spaces | |
| import os | |
| os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' | |
| # Fix invalid SSL_CERT_FILE on Windows (points to non-existent path) | |
| ssl_cert = os.environ.get('SSL_CERT_FILE', '') | |
| if ssl_cert and not os.path.exists(ssl_cert): | |
| del os.environ['SSL_CERT_FILE'] | |
| import tempfile | |
| import time | |
| from pathlib import Path | |
| import cv2 | |
| import gradio as gr | |
| import numpy as np | |
| from PIL import Image | |
| from src.inference import FireNetInference | |
| from src.model import FireNetModel | |
| # Default model path (override with MODEL_PATH env var, e.g. as a Space secret) | |
| DEFAULT_MODEL = os.environ.get('MODEL_PATH', 'models/FirenetCNN1.h5') | |
| # Class descriptions | |
| CLASS_INFO = { | |
| 'fire': { | |
| 'color': 'Red', | |
| 'description': 'Active fire detected with high confidence. Immediate attention required.', | |
| 'icon': '🔥' | |
| }, | |
| 'no_fire': { | |
| 'color': 'Green', | |
| 'description': 'No fire detected. Scene appears safe.', | |
| 'icon': '✅' | |
| }, | |
| 'smoke': { | |
| 'color': 'Orange', | |
| 'description': 'Smoke detected. May indicate early-stage fire or controlled burn.', | |
| 'icon': '💨' | |
| } | |
| } | |
| _engine = None | |
| def get_inference_engine() -> FireNetInference: | |
| """Get or create the cached inference engine (loaded once per process).""" | |
| global _engine | |
| if _engine is None: | |
| model_path = DEFAULT_MODEL | |
| if not os.path.exists(model_path): | |
| # Try alternative paths | |
| alternatives = [ | |
| 'models/FirenetCNN.keras', | |
| 'models/FirenetCNN.h5', | |
| 'models/firenet_model.h5', | |
| 'FirenetCNN1.h5', | |
| 'FirenetCNN.h5', | |
| ] | |
| for alt in alternatives: | |
| if os.path.exists(alt): | |
| model_path = alt | |
| break | |
| _engine = FireNetInference(model_path) | |
| return _engine | |
| def predict_image(image, apply_gradcam=True): | |
| """Predict on a single image with optional Grad-CAM.""" | |
| if image is None: | |
| return None, "Please upload an image.", None, None | |
| try: | |
| engine = get_inference_engine() | |
| # Convert PIL to temp file for inference | |
| with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as tmp: | |
| if isinstance(image, np.ndarray): | |
| cv2.imwrite(tmp.name, cv2.cvtColor(image, cv2.COLOR_RGB2BGR)) | |
| else: | |
| image.save(tmp.name) | |
| tmp_path = tmp.name | |
| result = engine.predict_image(tmp_path, apply_gradcam=apply_gradcam) | |
| # Cleanup | |
| os.unlink(tmp_path) | |
| # Prepare outputs | |
| label = result['label'] | |
| confidence = result['confidence'] | |
| probs = result['probability_array'] | |
| # Create annotated image | |
| if result.get('annotated_image') is not None: | |
| annotated = cv2.cvtColor(result['annotated_image'], cv2.COLOR_BGR2RGB) | |
| elif result.get('heatmap') is not None and result['has_gradcam']: | |
| original = cv2.imread(tmp_path) if os.path.exists(tmp_path) else None | |
| if original is not None: | |
| rgb_orig = cv2.cvtColor(original, cv2.COLOR_BGR2RGB) | |
| heatmap = np.array(result['heatmap']) | |
| annotated = engine.gradcam.overlay_heatmap(rgb_orig, heatmap, alpha=0.5) | |
| else: | |
| annotated = np.array(image) | |
| else: | |
| annotated = np.array(image) | |
| # Create probability chart | |
| prob_dict = {cls: float(probs[i]) for i, cls in enumerate(FireNetModel.CLASS_LABELS)} | |
| # Format result text | |
| info = CLASS_INFO.get(label, {}) | |
| result_text = f"## {info.get('icon', '')} Prediction: **{label.upper()}**\n" | |
| result_text += f"**Confidence:** {confidence*100:.2f}%\n\n" | |
| result_text += f"**Details:** {info.get('description', 'N/A')}\n\n" | |
| result_text += "### Class Probabilities\n" | |
| for cls, prob in prob_dict.items(): | |
| bar = '█' * int(prob * 20) | |
| result_text += f"- **{cls}**: {prob*100:.1f}% {bar}\n" | |
| return annotated, result_text, prob_dict, None | |
| except Exception as e: | |
| return None, f"Error: {str(e)}", None, None | |
| def predict_video(video_path, skip_frames=5, apply_gradcam=True): | |
| """Process a video file frame-by-frame.""" | |
| if video_path is None: | |
| return None, "Please upload a video.", None | |
| try: | |
| engine = get_inference_engine() | |
| # Create output path | |
| output_path = tempfile.mktemp(suffix='.mp4') | |
| stats = engine.predict_video( | |
| video_path, | |
| output_path=output_path, | |
| skip_frames=skip_frames, | |
| apply_gradcam=apply_gradcam | |
| ) | |
| # Format stats | |
| result_text = f"## Video Analysis Complete\n\n" | |
| result_text += f"**Total Frames:** {stats['total_frames']}\n" | |
| result_text += f"**Processed Frames:** {stats['processed_frames']}\n" | |
| result_text += f"**Processing Time:** {stats['processing_time_seconds']:.2f}s\n" | |
| result_text += f"**FPS:** {stats['processed_frames']/max(stats['processing_time_seconds'],0.001):.1f}\n\n" | |
| # Class distribution | |
| labels = [f['label'] for f in stats['frame_by_frame'] if f['processed']] | |
| if labels: | |
| from collections import Counter | |
| counts = Counter(labels) | |
| result_text += "### Detection Summary\n" | |
| for cls, count in counts.most_common(): | |
| pct = count / len(labels) * 100 | |
| result_text += f"- **{cls}**: {count} frames ({pct:.1f}%)\n" | |
| return output_path, result_text, stats | |
| except Exception as e: | |
| return None, f"Error: {str(e)}", None | |
| def get_model_info(): | |
| """Get model information and statistics.""" | |
| try: | |
| engine = get_inference_engine() | |
| config = FireNetModel.get_model_config() | |
| info_text = f"## Model Information\n\n" | |
| info_text += f"**Architecture:** {config['architecture']}\n" | |
| info_text += f"**Input Shape:** {config['input_shape']}\n" | |
| info_text += f"**Number of Classes:** {config['num_classes']}\n" | |
| info_text += f"**Class Labels:** {', '.join(config['class_labels'])}\n" | |
| info_text += f"**Learning Rate:** {config['learning_rate']}\n" | |
| info_text += f"**Image Size:** {config['image_size']}\n" | |
| info_text += f"**Batch Size:** {config['batch_size']}\n" | |
| info_text += f"**Grad-CAM Layer:** {config['last_conv_layer']}\n\n" | |
| info_text += "### Model Files\n" | |
| models_dir = Path('models') | |
| if models_dir.exists(): | |
| for f in models_dir.glob('*'): | |
| if f.suffix in ('.h5', '.keras'): | |
| size_mb = f.stat().st_size / (1024 * 1024) | |
| info_text += f"- **{f.name}**: {size_mb:.1f} MB\n" | |
| info_text += "\n### Class Reference\n" | |
| for cls, details in CLASS_INFO.items(): | |
| info_text += f"- {details['icon']} **{cls}**: {details['description']}\n" | |
| return info_text | |
| except Exception as e: | |
| return f"Error loading model info: {str(e)}" | |
| # Build Gradio interface | |
| with gr.Blocks(title="Forest Fire Detection - FirenetCNN") as demo: | |
| gr.Markdown( | |
| """ | |
| # 🔥 Forest Fire Detection using FirenetCNN and XAI Techniques | |
| Detect and classify forest fires from images and videos using deep learning with explainable AI (Grad-CAM). | |
| **Classes:** `fire` | `no_fire` | `smoke` | |
| """ | |
| ) | |
| with gr.Tabs(): | |
| # Tab 1: Image Classification | |
| with gr.Tab("📷 Image Classification"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| image_input = gr.Image(type="pil", label="Upload Image") | |
| gradcam_check = gr.Checkbox(label="Apply Grad-CAM", value=True) | |
| predict_btn = gr.Button("Predict", variant="primary") | |
| with gr.Column(scale=1): | |
| image_output = gr.Image(label="Annotated Result") | |
| result_text = gr.Markdown(label="Prediction") | |
| prob_chart = gr.Label(label="Probabilities") | |
| predict_btn.click( | |
| fn=predict_image, | |
| inputs=[image_input, gradcam_check], | |
| outputs=[image_output, result_text, prob_chart, gr.State()] | |
| ) | |
| # Tab 2: Video Analysis | |
| with gr.Tab("🎥 Video Analysis"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| video_input = gr.Video(label="Upload Video") | |
| skip_frames = gr.Slider( | |
| minimum=1, maximum=30, value=5, step=1, | |
| label="Process every Nth frame" | |
| ) | |
| video_gradcam = gr.Checkbox(label="Apply Grad-CAM", value=True) | |
| video_btn = gr.Button("Analyze Video", variant="primary") | |
| with gr.Column(scale=1): | |
| video_output = gr.Video(label="Processed Video") | |
| video_stats = gr.Markdown(label="Statistics") | |
| video_btn.click( | |
| fn=predict_video, | |
| inputs=[video_input, skip_frames, video_gradcam], | |
| outputs=[video_output, video_stats, gr.State()] | |
| ) | |
| # Tab 3: Webcam Inference | |
| with gr.Tab("📹 Webcam Inference"): | |
| gr.Markdown( | |
| """ | |
| ### Live Webcam Detection | |
| Click **Start Camera** to begin real-time fire/smoke detection. | |
| **Note:** Webcam inference runs locally in your browser. | |
| """ | |
| ) | |
| webcam_input = gr.Image(label="Webcam Feed") | |
| webcam_output = gr.Image(label="Detection Result") | |
| # Webcam processing would need real-time streaming | |
| # For now, provide a static image upload alternative | |
| gr.Markdown("*For live webcam detection, use the Python API directly:*") | |
| gr.Markdown( | |
| "```python\n" | |
| "from src.inference import FireNetInference\n" | |
| "engine = FireNetInference('models/FirenetCNN1.h5')\n" | |
| "engine.predict_webcam()\n" | |
| "```" | |
| ) | |
| # Tab 4: Model Information | |
| with gr.Tab("📊 Model Information"): | |
| model_info = gr.Markdown(value=get_model_info) | |
| gr.Markdown( | |
| """ | |
| ### Evaluation Metrics (Test Set) | |
| | Class | Precision | Recall | F1-Score | Support | | |
| |-------|-----------|--------|----------|---------| | |
| | fire | 0.92 | 0.81 | 0.86 | 121 | | |
| | no_fire | 0.76 | 0.98 | 0.86 | 146 | | |
| | smoke | 0.84 | 0.67 | 0.75 | 138 | | |
| | **accuracy** | | | **0.82** | **405** | | |
| | macro avg | 0.84 | 0.82 | 0.82 | 405 | | |
| | weighted avg | 0.83 | 0.82 | 0.82 | 405 | | |
| """ | |
| ) | |
| gr.Markdown( | |
| """ | |
| --- | |
| *Built with FirenetCNN (MobileNetV2) + Grad-CAM | [GitHub](https://github.com/OpelSpeedster/Forest-Fire-Detection-Using-FirenetCNN-and-XAI-Techniques)* | |
| """ | |
| ) | |
| if __name__ == '__main__': | |
| demo.queue().launch( | |
| server_name="0.0.0.0", | |
| server_port=int(os.environ.get("PORT", 7860)), | |
| share=False, | |
| theme=gr.themes.Soft(), | |
| ) | |