Adit1Sharma commited on
Commit
8e6f164
·
0 Parent(s):

Initial commit: FastAPI Incident Analyzer for HF Spaces

Browse files
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .env
5
+ .ipynb_checkpoints/
6
+ *.bat
Dockerfile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /code
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ build-essential \
8
+ libgl1-mesa-glx \
9
+ libglib2.0-0 \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # Copy requirements and install dependencies
13
+ COPY ./requirements.txt /code/requirements.txt
14
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
15
+ RUN pip install --no-cache-dir accelerate
16
+
17
+ # Copy application files
18
+ COPY ./api /code/api
19
+ COPY ./app.py /code/app.py
20
+
21
+ # Expose port 7860 (default port for Hugging Face Spaces)
22
+ ENV PORT=7860
23
+ EXPOSE 7860
24
+
25
+ # Run the FastAPI server
26
+ CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Emergency Incident Detection API
3
+ emoji: 🚨
4
+ colorFrom: red
5
+ colorTo: blue
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
10
+
11
+ # Emergency Incident Detection & Severity Analyzer API
12
+
13
+ A REST API wrapper around Hugging Face's `grounding-dino-base` zero-shot object detection model for analyzing emergency incidents, blockages, and hazards.
14
+
15
+ ## API Endpoints
16
+
17
+ ### 1. Root Endpoint
18
+ - **GET** `/`
19
+ - Returns a welcome message.
20
+
21
+ ### 2. Health Check
22
+ - **GET** `/health`
23
+ - Returns the service health status and confirms whether the model has loaded successfully.
24
+
25
+ ### 3. Analyze Image
26
+ - **POST** `/api/v1/incidents/analyze`
27
+ - Upload an image file under key `file`.
28
+ - **Response Schema**:
29
+ ```json
30
+ {
31
+ "success": true,
32
+ "incident_type": "road_accident",
33
+ "severity": "high",
34
+ "severity_score": 78,
35
+ "keywords": [
36
+ "car",
37
+ "truck",
38
+ "ambulance",
39
+ "person"
40
+ ],
41
+ "counts": {
42
+ "car": 2,
43
+ "truck": 1,
44
+ "ambulance": 1,
45
+ "person": 4
46
+ },
47
+ "detections": [
48
+ {
49
+ "label": "car",
50
+ "confidence": 0.91,
51
+ "box": [10.2, 50.4, 250.7, 400.1]
52
+ }
53
+ ]
54
+ }
55
+ ```
api/config.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ class Settings:
4
+ MODEL_ID: str = os.getenv("MODEL_ID", "IDEA-Research/grounding-dino-base")
5
+ BOX_THRESHOLD: float = float(os.getenv("BOX_THRESHOLD", 0.35))
6
+ TEXT_THRESHOLD: float = float(os.getenv("TEXT_THRESHOLD", 0.25))
7
+ MIN_CONFIDENCE: float = float(os.getenv("MIN_CONFIDENCE", 0.45))
8
+
9
+ TEXT_LABELS: list[str] = [
10
+ "car",
11
+ "truck",
12
+ "bus",
13
+ "motorcycle",
14
+ "person",
15
+ "ambulance",
16
+ "police vehicle",
17
+ "fire",
18
+ "smoke",
19
+ "tree",
20
+ "water",
21
+ "building",
22
+ "road",
23
+ "debris",
24
+ "damaged vehicle",
25
+ "traffic congestion",
26
+ "collapsed structure",
27
+ "construction barrier"
28
+ ]
29
+
30
+ settings = Settings()
api/main.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from contextlib import asynccontextmanager
2
+ from fastapi import FastAPI
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ import torch
5
+ from api.services.dino_service import GroundingDinoService
6
+ from api.routers.analysis import router as analysis_router
7
+
8
+ # Define the lifespan context manager for startup and shutdown events
9
+ @asynccontextmanager
10
+ async def lifespan(app: FastAPI):
11
+ # Initialize the Grounding DINO Service (downloads model if not cached and loads onto device)
12
+ dino_service = GroundingDinoService()
13
+ app.state.dino_service = dino_service
14
+ yield
15
+ # Clean up and free device memory
16
+ if hasattr(app.state, "dino_service"):
17
+ del app.state.dino_service
18
+ if torch.cuda.is_available():
19
+ torch.cuda.empty_cache()
20
+
21
+ # Create FastAPI application instance
22
+ app = FastAPI(
23
+ title="Emergency Incident Detector & Analyzer API",
24
+ description="A REST API wrapper around Grounding DINO for real-time disaster and accident scene analysis.",
25
+ version="1.0.0",
26
+ lifespan=lifespan
27
+ )
28
+
29
+ # Set up CORS middleware to support local testing and front-end integration
30
+ app.add_middleware(
31
+ CORSMiddleware,
32
+ allow_origins=["*"],
33
+ allow_credentials=True,
34
+ allow_methods=["*"],
35
+ allow_headers=["*"],
36
+ )
37
+
38
+ # Include incident routers
39
+ app.include_router(analysis_router)
40
+
41
+ # Basic health-check endpoint
42
+ @app.get("/health", tags=["Health"])
43
+ def health_check():
44
+ return {
45
+ "status": "healthy",
46
+ "model_loaded": hasattr(app.state, "dino_service") and app.state.dino_service is not None
47
+ }
48
+
49
+ @app.get("/", tags=["Root"])
50
+ def read_root():
51
+ return {
52
+ "message": "Welcome to the Emergency Incident Detection API. Go to /docs for the interactive Swagger API documentation."
53
+ }
api/routers/analysis.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ from fastapi import APIRouter, File, UploadFile, HTTPException, Request
3
+ from PIL import Image
4
+ from api.services.analyzer_service import IncidentAnalyzer
5
+
6
+ router = APIRouter(prefix="/api/v1/incidents", tags=["Incidents"])
7
+
8
+ @router.post("/analyze", response_model=dict)
9
+ async def analyze_incident_image(request: Request, file: UploadFile = File(...)):
10
+ """
11
+ Accepts an emergency incident image, runs zero-shot object detection using
12
+ Grounding DINO, and computes an incident type and severity score.
13
+ """
14
+ # Validate uploaded file type
15
+ if not file.content_type or not file.content_type.startswith("image/"):
16
+ raise HTTPException(
17
+ status_code=400,
18
+ detail="Invalid file format. Please upload an image file."
19
+ )
20
+
21
+ try:
22
+ # Read file contents and open as PIL Image
23
+ file_bytes = await file.read()
24
+ image = Image.open(io.BytesIO(file_bytes)).convert("RGB")
25
+ except Exception as e:
26
+ raise HTTPException(
27
+ status_code=400,
28
+ detail=f"Failed to process image file: {str(e)}"
29
+ )
30
+
31
+ # Get Grounding DINO service instance from app state
32
+ dino_service = getattr(request.app.state, "dino_service", None)
33
+ if dino_service is None:
34
+ raise HTTPException(
35
+ status_code=503,
36
+ detail="Model service is currently initializing. Please try again shortly."
37
+ )
38
+
39
+ try:
40
+ # Run inference
41
+ detections = dino_service.detect(image)
42
+ except Exception as e:
43
+ raise HTTPException(
44
+ status_code=500,
45
+ detail=f"Error executing object detection: {str(e)}"
46
+ )
47
+
48
+ # Count frequencies of each detected label
49
+ counts = {}
50
+ for detection in detections:
51
+ label = detection["label"]
52
+ counts[label] = counts.get(label, 0) + 1
53
+
54
+ # Extract unique labels list
55
+ keywords = list(counts.keys())
56
+
57
+ # Analyze incident characteristics (severity and type classification)
58
+ analysis_result = IncidentAnalyzer.analyze(keywords)
59
+
60
+ # Return structured API response
61
+ return {
62
+ "success": True,
63
+ "incident_type": analysis_result["incident_type"],
64
+ "severity": analysis_result["severity"],
65
+ "severity_score": analysis_result["severity_score"],
66
+ "keywords": keywords,
67
+ "counts": counts,
68
+ "detections": detections
69
+ }
api/services/analyzer_service.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class IncidentAnalyzer:
2
+ @staticmethod
3
+ def classify_incident(labels: set[str]) -> str:
4
+ """
5
+ Determines the specific incident classification type based on the detected labels.
6
+ """
7
+ vehicles = {
8
+ "car",
9
+ "truck",
10
+ "bus",
11
+ "motorcycle",
12
+ "damaged vehicle"
13
+ }
14
+ vehicle_count = len(labels.intersection(vehicles))
15
+
16
+ # ==========================
17
+ # FIRE RELATED
18
+ # ==========================
19
+ if "fire" in labels and "building" in labels:
20
+ return "building_fire"
21
+ if "fire" in labels and vehicle_count > 0:
22
+ return "vehicle_fire"
23
+ if "fire" in labels:
24
+ return "fire_incident"
25
+ if "smoke" in labels and "building" in labels:
26
+ return "possible_building_fire"
27
+ if "smoke" in labels:
28
+ return "smoke_hazard"
29
+
30
+ # ==========================
31
+ # FLOOD RELATED
32
+ # ==========================
33
+ if "water" in labels and "road" in labels:
34
+ return "road_flooding"
35
+ if "water" in labels and "building" in labels:
36
+ return "urban_flooding"
37
+ if "water" in labels and "tree" in labels:
38
+ return "storm_damage"
39
+ if "water" in labels:
40
+ return "water_hazard"
41
+
42
+ # ==========================
43
+ # BUILDING DAMAGE
44
+ # ==========================
45
+ if "collapsed structure" in labels and "person" in labels:
46
+ return "major_building_collapse"
47
+ if "collapsed structure" in labels:
48
+ return "building_collapse"
49
+
50
+ # ==========================
51
+ # ROAD ACCIDENTS
52
+ # ==========================
53
+ if "damaged vehicle" in labels and "ambulance" in labels:
54
+ return "critical_road_accident"
55
+ if vehicle_count >= 2 and "person" in labels:
56
+ return "road_accident"
57
+ if vehicle_count >= 2:
58
+ return "possible_vehicle_collision"
59
+ if "motorcycle" in labels and "ambulance" in labels:
60
+ return "motorcycle_accident"
61
+ if "truck" in labels and "ambulance" in labels:
62
+ return "truck_accident"
63
+ if "bus" in labels and "ambulance" in labels:
64
+ return "bus_accident"
65
+
66
+ # ==========================
67
+ # TRAFFIC RELATED
68
+ # ==========================
69
+ if "traffic congestion" in labels and vehicle_count >= 2:
70
+ return "heavy_traffic"
71
+ if "construction barrier" in labels and "road" in labels:
72
+ return "road_construction"
73
+
74
+ # ==========================
75
+ # ROAD BLOCKAGE
76
+ # ==========================
77
+ if "tree" in labels and "road" in labels:
78
+ return "fallen_tree_blockage"
79
+ if "debris" in labels and "road" in labels:
80
+ return "road_debris"
81
+ if "tree" in labels and "debris" in labels:
82
+ return "storm_road_blockage"
83
+
84
+ # ==========================
85
+ # MEDICAL
86
+ # ==========================
87
+ if "ambulance" in labels and "person" in labels:
88
+ return "medical_emergency"
89
+
90
+ # ==========================
91
+ # LAW ENFORCEMENT
92
+ # ==========================
93
+ if "police vehicle" in labels and vehicle_count > 0:
94
+ return "traffic_enforcement"
95
+ if "police vehicle" in labels:
96
+ return "police_activity"
97
+
98
+ # ==========================
99
+ # GENERAL EVENTS
100
+ # ==========================
101
+ if vehicle_count > 0:
102
+ return "vehicle_activity"
103
+ if "building" in labels:
104
+ return "building_related_event"
105
+ if "person" in labels:
106
+ return "crowd_activity"
107
+
108
+ return "unknown_incident"
109
+
110
+ @classmethod
111
+ def analyze(cls, labels_found: list[str]) -> dict:
112
+ """
113
+ Calculates severity scores, severity categorizations, and determines
114
+ the incident type classification.
115
+ """
116
+ labels = set(x.lower().strip() for x in labels_found)
117
+ score = 0
118
+
119
+ # Object weight mapping for severity scoring
120
+ weights = {
121
+ "ambulance": 35,
122
+ "fire": 40,
123
+ "smoke": 20,
124
+ "water": 20,
125
+ "person": 5,
126
+ "debris": 15,
127
+ "tree": 10,
128
+ "police vehicle": 15,
129
+ "damaged vehicle": 25
130
+ }
131
+
132
+ # Accumulate weights of detected labels
133
+ for label in labels:
134
+ score += weights.get(label, 0)
135
+
136
+ vehicles = {
137
+ "car",
138
+ "truck",
139
+ "bus",
140
+ "motorcycle",
141
+ "damaged vehicle"
142
+ }
143
+ vehicle_count = len(labels.intersection(vehicles))
144
+
145
+ # Scoring modifiers based on incident context
146
+ # Fire
147
+ if "fire" in labels:
148
+ score += 20
149
+ # Flood
150
+ elif "water" in labels and "road" in labels:
151
+ score += 20
152
+ # Accident
153
+ elif vehicle_count >= 2 and "person" in labels:
154
+ score += 25
155
+ elif "ambulance" in labels and vehicle_count >= 1:
156
+ score += 30
157
+ # Obstruction
158
+ elif "tree" in labels and "road" in labels:
159
+ score += 15
160
+
161
+ # Cap score at 100
162
+ score = min(score, 100)
163
+
164
+ # Categorize severity based on score thresholds
165
+ if score >= 80:
166
+ severity = "critical"
167
+ elif score >= 60:
168
+ severity = "high"
169
+ elif score >= 30:
170
+ severity = "medium"
171
+ else:
172
+ severity = "low"
173
+
174
+ # Get classified incident type
175
+ incident_type = cls.classify_incident(labels)
176
+
177
+ return {
178
+ "incident_type": incident_type,
179
+ "severity": severity,
180
+ "severity_score": score
181
+ }
api/services/dino_service.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from PIL import Image
3
+ from transformers import AutoProcessor, AutoModelForZeroShotObjectDetection
4
+ from api.config import settings
5
+
6
+ class GroundingDinoService:
7
+ def __init__(self):
8
+ self.model_id = settings.MODEL_ID
9
+ print(f"Initializing Grounding DINO model '{self.model_id}'...")
10
+
11
+ # Load processor and model
12
+ self.processor = AutoProcessor.from_pretrained(self.model_id)
13
+ self.model = AutoModelForZeroShotObjectDetection.from_pretrained(
14
+ self.model_id,
15
+ device_map="auto"
16
+ )
17
+ print("Grounding DINO model loaded successfully!")
18
+
19
+ def detect(self, image: Image.Image) -> list[dict]:
20
+ """
21
+ Executes object detection on the PIL Image using the configured labels and thresholds.
22
+ Returns a list of detections containing label, confidence score, and bounding boxes.
23
+ """
24
+ # Format labels as expected by processor: nested list containing label strings
25
+ formatted_labels = [settings.TEXT_LABELS]
26
+
27
+ # Prepare inputs
28
+ inputs = self.processor(
29
+ images=image,
30
+ text=formatted_labels,
31
+ return_tensors="pt"
32
+ ).to(self.model.device)
33
+
34
+ # Run inference
35
+ with torch.no_grad():
36
+ outputs = self.model(**inputs)
37
+
38
+ # Post-process detections
39
+ results = self.processor.post_process_grounded_object_detection(
40
+ outputs,
41
+ inputs.input_ids,
42
+ threshold=settings.BOX_THRESHOLD,
43
+ text_threshold=settings.TEXT_THRESHOLD,
44
+ target_sizes=[image.size[::-1]]
45
+ )
46
+
47
+ detections = []
48
+ result = results[0]
49
+
50
+ # Iterate over output boxes, scores, and labels
51
+ for box, score, label in zip(
52
+ result["boxes"],
53
+ result["scores"],
54
+ result["labels"]
55
+ ):
56
+ confidence = round(score.item(), 3)
57
+
58
+ # Filter detections by minimum confidence threshold
59
+ if confidence < settings.MIN_CONFIDENCE:
60
+ continue
61
+
62
+ # Convert box coordinates to float list and round
63
+ box_coords = [round(x, 2) for x in box.tolist()]
64
+
65
+ detections.append({
66
+ "label": label,
67
+ "confidence": confidence,
68
+ "box": box_coords
69
+ })
70
+
71
+ return detections
app.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uvicorn
2
+ import os
3
+
4
+ if __name__ == "__main__":
5
+ port = int(os.getenv("PORT", 8000))
6
+ host = os.getenv("HOST", "0.0.0.0")
7
+
8
+ print(f"Starting FastAPI server on http://{host}:{port}...")
9
+ uvicorn.run(
10
+ "api.main:app",
11
+ host=host,
12
+ port=port,
13
+ reload=False # Disabled by default for ML models to prevent duplicate model loads in memory
14
+ )
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.110.0
2
+ uvicorn>=0.28.0
3
+ python-multipart>=0.0.9
4
+ transformers>=4.38.0
5
+ torch>=2.0.0
6
+ pillow>=10.0.0
7
+ requests>=2.31.0
8
+ icrawler>=0.6.0