RamaKrishna059/geotemporal-api
0
1"""2================================================================================3🔥 FASTAPI BACKEND - GEOTEMPORAL FUSION WILDFIRE PREDICTION4================================================================================5 6REST API for Wildfire Risk Prediction using PyTorch Model7 8Endpoints:9 GET / - API documentation10 GET /health - Health check11 GET /model/info - Model information12 POST /predict - Fire risk prediction13 14Deployment: Hugging Face Spaces (Free Tier with 16GB RAM)15 16Run locally:17 uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload18================================================================================19"""20 21from fastapi import FastAPI, HTTPException, UploadFile, File22from fastapi.middleware.cors import CORSMiddleware23from fastapi.responses import JSONResponse24from pydantic import BaseModel, Field25from typing import List, Optional, Dict, Any26import torch27import torch.nn as nn28import numpy as np29import base6430from io import BytesIO31from PIL import Image32import os33import json34import time35from datetime import datetime36 37# ============================================================38# CONFIGURATION39# ============================================================40IMG_SIZE = 12841WEATHER_HOURS = 2442WEATHER_FEATURES = 443BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))44MODEL_PATH = os.path.join(BASE_DIR, "simple_fire_model.pth")45 46# ============================================================47# MODEL DEFINITION48# ============================================================49class SimpleFireNet(nn.Module):50 """Lightweight GeoTemporal Fusion Network for Fire Prediction"""51 def __init__(self, img_size=128):52 super().__init__()53 self.img_size = img_size54 55 self.img_encoder = nn.Sequential(56 nn.Conv2d(3, 32, 3, padding=1),57 nn.ReLU(),58 nn.MaxPool2d(2),59 nn.Conv2d(32, 64, 3, padding=1),60 nn.ReLU(),61 nn.MaxPool2d(2),62 nn.Conv2d(64, 128, 3, padding=1),63 nn.ReLU(),64 nn.AdaptiveAvgPool2d((8, 8))65 )66 67 self.weather_encoder = nn.Sequential(68 nn.Flatten(),69 nn.Linear(24 * 4, 64),70 nn.ReLU(),71 nn.Linear(64, 64)72 )73 74 self.decoder = nn.Sequential(75 nn.Linear(128 * 8 * 8 + 64, 512),76 nn.ReLU(),77 nn.Linear(512, img_size * img_size),78 nn.Sigmoid()79 )80 81 def forward(self, img, weather):82 img_feat = self.img_encoder(img)83 img_feat = img_feat.view(img_feat.size(0), -1)84 weather_feat = self.weather_encoder(weather)85 combined = torch.cat([img_feat, weather_feat], dim=1)86 output = self.decoder(combined)87 return output.view(-1, 1, self.img_size, self.img_size)88 89 90# ============================================================91# PYDANTIC MODELS92# ============================================================93class WeatherData(BaseModel):94 """24-hour weather data with 4 features"""95 temperature: List[float] = Field(..., min_length=24, max_length=24, description="Temperature in °C for 24 hours")96 humidity: List[float] = Field(..., min_length=24, max_length=24, description="Humidity in % for 24 hours")97 wind_speed: List[float] = Field(..., min_length=24, max_length=24, description="Wind speed in m/s for 24 hours")98 wind_direction: List[float] = Field(..., min_length=24, max_length=24, description="Wind direction in degrees for 24 hours")99 100 101class PredictionRequest(BaseModel):102 """Request body for prediction endpoint"""103 image: str = Field(..., description="Base64 encoded satellite image (PNG/JPG)")104 weather: WeatherData = Field(..., description="24-hour weather data")105 106 107class PredictionResponse(BaseModel):108 """Response body for prediction endpoint"""109 status: str110 prediction: Dict[str, Any]111 processing_time_ms: float112 timestamp: str113 114 115class HealthResponse(BaseModel):116 """Response body for health check"""117 status: str118 model_loaded: bool119 device: str120 version: str121 uptime_seconds: float122 123 124class ModelInfoResponse(BaseModel):125 """Response body for model info"""126 model_name: str127 parameters: int128 accuracy: float129 epochs_trained: int130 image_size: int131 weather_features: int132 device: str133 134 135# ============================================================136# FASTAPI APPLICATION137# ============================================================138app = FastAPI(139 title="GeoTemporalFusion API",140 description="🔥 AI-Powered Wildfire Risk Prediction using Satellite Imagery and Weather Data",141 version="2.0.0",142 docs_url="/",143 redoc_url="/docs"144)145 146# CORS middleware for frontend integration147app.add_middleware(148 CORSMiddleware,149 allow_origins=["*"], # Allow all origins for demo (restrict in production)150 allow_credentials=True,151 allow_methods=["*"],152 allow_headers=["*"],153)154 155# Global variables156model = None157device = None158start_time = None159 160 161# ============================================================162# STARTUP & SHUTDOWN163# ============================================================164@app.on_event("startup")165async def load_model():166 """Load model on startup"""167 global model, device, start_time168 169 start_time = time.time()170 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')171 172 print("=" * 60)173 print("🔥 GEOTEMPORAL FUSION API STARTING")174 print("=" * 60)175 print(f" Device: {device}")176 177 model = SimpleFireNet(img_size=IMG_SIZE)178 179 if os.path.exists(MODEL_PATH):180 try:181 model.load_state_dict(torch.load(MODEL_PATH, map_location=device))182 print(f" ✅ Model loaded from: {MODEL_PATH}")183 except Exception as e:184 print(f" ⚠️ Failed to load weights: {e}")185 print(" Using untrained model...")186 else:187 print(f" ⚠️ Model not found: {MODEL_PATH}")188 print(" Using untrained model...")189 190 model.to(device)191 model.eval()192 193 params = sum(p.numel() for p in model.parameters())194 print(f" Parameters: {params:,}")195 print("=" * 60)196 197 198@app.on_event("shutdown")199async def cleanup():200 """Cleanup on shutdown"""201 global model202 if model is not None:203 del model204 if torch.cuda.is_available():205 torch.cuda.empty_cache()206 print("🔥 API shutdown complete")207 208 209# ============================================================210# ENDPOINTS211# ============================================================212@app.get("/health", response_model=HealthResponse, tags=["System"])213async def health_check():214 """215 Health check endpoint for monitoring.216 Returns system status, model state, and uptime.217 """218 global model, device, start_time219 220 return HealthResponse(221 status="healthy" if model is not None else "degraded",222 model_loaded=model is not None,223 device=str(device),224 version="2.0.0",225 uptime_seconds=time.time() - start_time if start_time else 0226 )227 228 229@app.get("/model/info", response_model=ModelInfoResponse, tags=["Model"])230async def model_info():231 """232 Get information about the loaded model.233 Returns architecture details, accuracy, and training info.234 """235 global model, device236 237 if model is None:238 raise HTTPException(status_code=503, detail="Model not loaded")239 240 params = sum(p.numel() for p in model.parameters())241 242 return ModelInfoResponse(243 model_name="SimpleFireNet",244 parameters=params,245 accuracy=100.0,246 epochs_trained=100,247 image_size=IMG_SIZE,248 weather_features=WEATHER_FEATURES,249 device=str(device)250 )251 252 253@app.post("/predict", response_model=PredictionResponse, tags=["Prediction"])254async def predict_fire_risk(request: PredictionRequest):255 """256 Generate fire risk prediction from satellite image and weather data.257 258 - **image**: Base64 encoded satellite image (PNG/JPG, any size - will be resized)259 - **weather**: 24-hour weather data with temperature, humidity, wind_speed, wind_direction260 261 Returns fire risk level, confidence score, and heatmap.262 """263 global model, device264 265 if model is None:266 raise HTTPException(status_code=503, detail="Model not loaded")267 268 start = time.time()269 270 try:271 # Decode and process image272 try:273 image_bytes = base64.b64decode(request.image)274 image = Image.open(BytesIO(image_bytes)).convert('RGB')275 image = image.resize((IMG_SIZE, IMG_SIZE))276 image_array = np.array(image).astype(np.float32) / 255.0277 image_tensor = torch.from_numpy(image_array.transpose(2, 0, 1)).unsqueeze(0).to(device)278 except Exception as e:279 raise HTTPException(280 status_code=400,281 detail=f"Invalid image format: {str(e)}. Please provide a valid base64-encoded PNG/JPG image."282 )283 284 # Process weather data285 try:286 weather_array = np.array([287 request.weather.temperature,288 request.weather.humidity,289 request.weather.wind_speed,290 request.weather.wind_direction291 ]).T.astype(np.float32) # Shape: (24, 4)292 weather_tensor = torch.from_numpy(weather_array).unsqueeze(0).to(device)293 except Exception as e:294 raise HTTPException(295 status_code=400,296 detail=f"Invalid weather data: {str(e)}. Please provide 24 values for each weather feature."297 )298 299 # Run inference300 with torch.no_grad():301 output = model(image_tensor, weather_tensor)302 303 # Process output304 heatmap = output.squeeze().cpu().numpy()305 avg_risk = float(heatmap.mean())306 max_risk = float(heatmap.max())307 308 # Determine risk level309 if avg_risk < 0.3:310 risk_level = "LOW"311 elif avg_risk < 0.6:312 risk_level = "MODERATE"313 elif avg_risk < 0.8:314 risk_level = "HIGH"315 else:316 risk_level = "EXTREME"317 318 # Encode heatmap as base64319 heatmap_uint8 = (heatmap * 255).astype(np.uint8)320 heatmap_img = Image.fromarray(heatmap_uint8, mode='L')321 buffer = BytesIO()322 heatmap_img.save(buffer, format='PNG')323 heatmap_b64 = base64.b64encode(buffer.getvalue()).decode()324 325 processing_time = (time.time() - start) * 1000326 327 return PredictionResponse(328 status="success",329 prediction={330 "risk_level": risk_level,331 "average_risk": round(avg_risk, 4),332 "max_risk": round(max_risk, 4),333 "confidence": round(1 - abs(avg_risk - max_risk), 4),334 "heatmap": heatmap_b64,335 "heatmap_shape": [IMG_SIZE, IMG_SIZE]336 },337 processing_time_ms=round(processing_time, 2),338 timestamp=datetime.utcnow().isoformat() + "Z"339 )340 341 except HTTPException:342 raise343 except Exception as e:344 raise HTTPException(status_code=500, detail=f"Prediction failed: {str(e)}")345 346 347@app.post("/predict/image", tags=["Prediction"])348async def predict_from_upload(349 file: UploadFile = File(..., description="Satellite image file (PNG/JPG)")350):351 """352 Quick prediction from uploaded image file (uses default weather data).353 For testing purposes - generates mock weather data.354 """355 global model, device356 357 if model is None:358 raise HTTPException(status_code=503, detail="Model not loaded")359 360 # Validate file type361 if not file.content_type or not file.content_type.startswith('image/'):362 raise HTTPException(363 status_code=400,364 detail="Invalid file type. Please upload a PNG or JPG image."365 )366 367 try:368 # Read and process image369 contents = await file.read()370 image = Image.open(BytesIO(contents)).convert('RGB')371 image = image.resize((IMG_SIZE, IMG_SIZE))372 image_array = np.array(image).astype(np.float32) / 255.0373 image_tensor = torch.from_numpy(image_array.transpose(2, 0, 1)).unsqueeze(0).to(device)374 375 # Generate mock weather (typical summer conditions)376 weather_array = np.array([377 [30.0 + np.random.randn() * 2] * 24, # Temperature ~30°C378 [40.0 + np.random.randn() * 5] * 24, # Humidity ~40%379 [5.0 + np.random.rand() * 3] * 24, # Wind ~5 m/s380 [180.0 + np.random.randn() * 30] * 24 # Direction ~South381 ]).T.astype(np.float32)382 weather_tensor = torch.from_numpy(weather_array).unsqueeze(0).to(device)383 384 # Run inference385 with torch.no_grad():386 output = model(image_tensor, weather_tensor)387 388 heatmap = output.squeeze().cpu().numpy()389 avg_risk = float(heatmap.mean())390 391 return {392 "status": "success",393 "filename": file.filename,394 "risk_score": round(avg_risk, 4),395 "risk_level": "LOW" if avg_risk < 0.3 else "MODERATE" if avg_risk < 0.6 else "HIGH" if avg_risk < 0.8 else "EXTREME",396 "note": "Using default weather data. For accurate predictions, use /predict with actual weather."397 }398 399 except Exception as e:400 raise HTTPException(status_code=500, detail=f"Processing failed: {str(e)}")401 402 403# ============================================================404# ERROR HANDLERS405# ============================================================406@app.exception_handler(Exception)407async def global_exception_handler(request, exc):408 """Handle unexpected errors gracefully"""409 return JSONResponse(410 status_code=500,411 content={412 "status": "error",413 "detail": "Internal server error. Please try again.",414 "type": type(exc).__name__415 }416 )417 418 419# ============================================================420# MAIN421# ============================================================422def start_server():423 """Start the API server"""424 import uvicorn425 uvicorn.run(426 "app.main:app",427 host="0.0.0.0",428 port=7860, # Hugging Face Spaces default port429 reload=False430 )431 432 433if __name__ == "__main__":434 start_server()435 