Spaces:
Sleeping
Sleeping
Commit
·
8c6f511
1
Parent(s):
293fffd
oko
Browse files- app.py +666 -0
- requirements.txt +13 -0
app.py
ADDED
@@ -0,0 +1,666 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# backend.py
|
2 |
+
|
3 |
+
import os
|
4 |
+
import cv2
|
5 |
+
import numpy as np
|
6 |
+
import tensorflow as tf
|
7 |
+
import smtplib
|
8 |
+
|
9 |
+
from fastapi import FastAPI, File, UploadFile, Form, HTTPException
|
10 |
+
from fastapi.responses import JSONResponse, StreamingResponse
|
11 |
+
from fastapi.middleware.cors import CORSMiddleware
|
12 |
+
|
13 |
+
from typing import Dict, Any
|
14 |
+
from datetime import datetime, timezone
|
15 |
+
from io import BytesIO
|
16 |
+
|
17 |
+
# SQLAlchemy imports
|
18 |
+
from sqlalchemy import create_engine, Column, Integer, String, DateTime, ForeignKey, Text, func
|
19 |
+
from sqlalchemy.orm import sessionmaker, relationship, declarative_base, Session
|
20 |
+
|
21 |
+
# ReportLab (PDF generation)
|
22 |
+
from reportlab.lib.pagesizes import A4
|
23 |
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image as RLImage, Table, TableStyle
|
24 |
+
from reportlab.lib.styles import getSampleStyleSheet
|
25 |
+
from reportlab.lib import colors
|
26 |
+
|
27 |
+
# Matplotlib (Chart generation)
|
28 |
+
import matplotlib
|
29 |
+
matplotlib.use('Agg')
|
30 |
+
import matplotlib.pyplot as plt
|
31 |
+
|
32 |
+
# YOLO-related imports
|
33 |
+
from src.yolo3.model import yolo_body
|
34 |
+
from src.yolo3.detect import detection
|
35 |
+
from src.utils.image import letterbox_image
|
36 |
+
from src.utils.fixes import fix_tf_gpu
|
37 |
+
from tensorflow.keras.layers import Input
|
38 |
+
|
39 |
+
|
40 |
+
##############################################################################
|
41 |
+
# Database Setup (SQLite)
|
42 |
+
##############################################################################
|
43 |
+
|
44 |
+
DB_URL = "sqlite:///./safety_monitor.db"
|
45 |
+
|
46 |
+
engine = create_engine(
|
47 |
+
DB_URL, connect_args={"check_same_thread": False} # for single-threaded SQLite
|
48 |
+
)
|
49 |
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
50 |
+
Base = declarative_base()
|
51 |
+
|
52 |
+
class Upload(Base):
|
53 |
+
"""
|
54 |
+
Stores information about each upload (image or video), plus the user's email.
|
55 |
+
"""
|
56 |
+
__tablename__ = "uploads"
|
57 |
+
|
58 |
+
id = Column(Integer, primary_key=True, index=True)
|
59 |
+
filename = Column(String)
|
60 |
+
filepath = Column(String)
|
61 |
+
timestamp = Column(DateTime)
|
62 |
+
approach = Column(Integer)
|
63 |
+
user_email = Column(String) # The user’s email address
|
64 |
+
total_workers = Column(Integer, default=0)
|
65 |
+
total_helmets = Column(Integer, default=0)
|
66 |
+
total_vests = Column(Integer, default=0)
|
67 |
+
# We'll store worker_images as a comma-separated string for simplicity
|
68 |
+
worker_images = Column(Text, default="")
|
69 |
+
|
70 |
+
# Relationship to SafetyDetection
|
71 |
+
detections = relationship("SafetyDetection", back_populates="upload", cascade="all, delete-orphan")
|
72 |
+
|
73 |
+
|
74 |
+
class SafetyDetection(Base):
|
75 |
+
"""
|
76 |
+
Stores individual safety gear detections (e.g., bounding boxes for helmets/vests).
|
77 |
+
"""
|
78 |
+
__tablename__ = "safety_detections"
|
79 |
+
|
80 |
+
id = Column(Integer, primary_key=True, index=True)
|
81 |
+
label = Column(String) # e.g. 'H', 'V'
|
82 |
+
box = Column(String) # bounding box as string, e.g. "x1,y1,x2,y2"
|
83 |
+
timestamp = Column(DateTime)
|
84 |
+
|
85 |
+
upload_id = Column(Integer, ForeignKey("uploads.id"))
|
86 |
+
upload = relationship("Upload", back_populates="detections")
|
87 |
+
|
88 |
+
|
89 |
+
Base.metadata.create_all(bind=engine)
|
90 |
+
|
91 |
+
|
92 |
+
##############################################################################
|
93 |
+
# FastAPI App & Configuration
|
94 |
+
##############################################################################
|
95 |
+
|
96 |
+
app = FastAPI(
|
97 |
+
title="Industrial Safety Monitor (FastAPI + SQLite)",
|
98 |
+
description="A YOLO-based safety gear detection app. Three endpoints: upload, results, dashboard.",
|
99 |
+
version="1.0.0",
|
100 |
+
)
|
101 |
+
|
102 |
+
# Allow cross-origin requests (optional)
|
103 |
+
app.add_middleware(
|
104 |
+
CORSMiddleware,
|
105 |
+
allow_origins=["*"],
|
106 |
+
allow_credentials=True,
|
107 |
+
allow_methods=["*"],
|
108 |
+
allow_headers=["*"],
|
109 |
+
)
|
110 |
+
|
111 |
+
# Directories
|
112 |
+
UPLOAD_FOLDER = "static/uploads"
|
113 |
+
PROCESSED_FOLDER = "static/processed"
|
114 |
+
WORKER_FOLDER = "static/workers"
|
115 |
+
CHARTS_FOLDER = "static/charts"
|
116 |
+
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'mp4'}
|
117 |
+
|
118 |
+
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
119 |
+
os.makedirs(PROCESSED_FOLDER, exist_ok=True)
|
120 |
+
os.makedirs(WORKER_FOLDER, exist_ok=True)
|
121 |
+
os.makedirs(CHARTS_FOLDER, exist_ok=True)
|
122 |
+
|
123 |
+
##############################################################################
|
124 |
+
# YOLO Model Setup
|
125 |
+
##############################################################################
|
126 |
+
|
127 |
+
input_shape = (416, 416)
|
128 |
+
class_names = []
|
129 |
+
anchor_boxes = None
|
130 |
+
num_classes = 0
|
131 |
+
num_anchors = 0
|
132 |
+
model = None
|
133 |
+
|
134 |
+
def prepare_model(approach: int):
|
135 |
+
"""
|
136 |
+
Prepares the YOLO model for the selected approach (1, 2, or 3).
|
137 |
+
"""
|
138 |
+
global input_shape, class_names, anchor_boxes
|
139 |
+
global num_classes, num_anchors
|
140 |
+
|
141 |
+
if approach not in [1, 2, 3]:
|
142 |
+
raise NotImplementedError("Approach must be 1, 2, or 3")
|
143 |
+
|
144 |
+
# Classes: H=Helmet, V=Vest, W=Worker
|
145 |
+
class_names[:] = ['H', 'V', 'W']
|
146 |
+
|
147 |
+
# Anchor boxes by approach
|
148 |
+
if approach == 1:
|
149 |
+
anchor_boxes = np.array(
|
150 |
+
[
|
151 |
+
np.array([[76, 59], [84, 136], [188, 225]]) / 32,
|
152 |
+
np.array([[25, 15], [46, 29], [27, 56]]) / 16,
|
153 |
+
np.array([[5, 3], [10, 8], [12, 26]]) / 8
|
154 |
+
],
|
155 |
+
dtype='float64'
|
156 |
+
)
|
157 |
+
elif approach == 2:
|
158 |
+
anchor_boxes = np.array(
|
159 |
+
[
|
160 |
+
np.array([[73, 158], [128, 209], [224, 246]]) / 32,
|
161 |
+
np.array([[32, 50], [40, 104], [76, 73]]) / 16,
|
162 |
+
np.array([[6, 11], [11, 23], [19, 36]]) / 8
|
163 |
+
],
|
164 |
+
dtype='float64'
|
165 |
+
)
|
166 |
+
else: # approach == 3
|
167 |
+
anchor_boxes = np.array(
|
168 |
+
[
|
169 |
+
np.array([[76, 59], [84, 136], [188, 225]]) / 32,
|
170 |
+
np.array([[25, 15], [46, 29], [27, 56]]) / 16,
|
171 |
+
np.array([[5, 3], [10, 8], [12, 26]]) / 8
|
172 |
+
],
|
173 |
+
dtype='float64'
|
174 |
+
)
|
175 |
+
|
176 |
+
num_classes = len(class_names)
|
177 |
+
num_anchors = anchor_boxes.shape[0] * anchor_boxes.shape[1]
|
178 |
+
|
179 |
+
input_tensor = Input(shape=(input_shape[0], input_shape[1], 3))
|
180 |
+
num_out_filters = (num_anchors // 3) * (5 + num_classes)
|
181 |
+
_model = yolo_body(input_tensor, num_out_filters)
|
182 |
+
|
183 |
+
weight_path = f"model-data/weights/pictor-ppe-v302-a{approach}-yolo-v3-weights.h5"
|
184 |
+
if not os.path.exists(weight_path):
|
185 |
+
raise FileNotFoundError(f"Weight file not found: {weight_path}")
|
186 |
+
|
187 |
+
_model.load_weights(weight_path)
|
188 |
+
return _model
|
189 |
+
|
190 |
+
##############################################################################
|
191 |
+
# Utility & Detection Logic
|
192 |
+
##############################################################################
|
193 |
+
|
194 |
+
def allowed_file(filename: str) -> bool:
|
195 |
+
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
196 |
+
|
197 |
+
def get_db() -> Session:
|
198 |
+
"""
|
199 |
+
Yields a database session.
|
200 |
+
"""
|
201 |
+
db = SessionLocal()
|
202 |
+
try:
|
203 |
+
yield db
|
204 |
+
finally:
|
205 |
+
db.close()
|
206 |
+
|
207 |
+
def run_detection_on_frame(frame: np.ndarray,
|
208 |
+
approach: int,
|
209 |
+
upload_id: int,
|
210 |
+
db: Session) -> np.ndarray:
|
211 |
+
"""
|
212 |
+
Runs YOLO detection on a single frame, updates DB counters/detections,
|
213 |
+
and returns the annotated frame.
|
214 |
+
"""
|
215 |
+
global model, anchor_boxes, class_names, input_shape
|
216 |
+
|
217 |
+
ih, iw = frame.shape[:2]
|
218 |
+
resized = letterbox_image(frame, input_shape)
|
219 |
+
resized_expanded = np.expand_dims(resized, 0)
|
220 |
+
image_data = np.array(resized_expanded) / 255.0
|
221 |
+
|
222 |
+
prediction = model.predict(image_data)
|
223 |
+
boxes = detection(
|
224 |
+
prediction,
|
225 |
+
anchor_boxes,
|
226 |
+
len(class_names),
|
227 |
+
image_shape=(ih, iw),
|
228 |
+
input_shape=input_shape,
|
229 |
+
max_boxes=50,
|
230 |
+
score_threshold=0.3,
|
231 |
+
iou_threshold=0.45,
|
232 |
+
classes_can_overlap=False
|
233 |
+
)[0].numpy()
|
234 |
+
|
235 |
+
# Tally
|
236 |
+
workers, helmets, vests = [], [], []
|
237 |
+
for box in boxes:
|
238 |
+
x1, y1, x2, y2, score, cls_id = box
|
239 |
+
x1, y1, x2, y2 = map(int, [x1, y1, x2, y2])
|
240 |
+
cls_id = int(cls_id)
|
241 |
+
label = class_names[cls_id]
|
242 |
+
|
243 |
+
if label == 'W':
|
244 |
+
workers.append((x1, y1, x2, y2))
|
245 |
+
color = (0, 255, 0)
|
246 |
+
elif label == 'H':
|
247 |
+
helmets.append((x1, y1, x2, y2))
|
248 |
+
color = (255, 0, 0)
|
249 |
+
elif label == 'V':
|
250 |
+
vests.append((x1, y1, x2, y2))
|
251 |
+
color = (0, 0, 255)
|
252 |
+
else:
|
253 |
+
color = (255, 255, 0)
|
254 |
+
|
255 |
+
cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
|
256 |
+
cv2.putText(frame, label, (x1, y1 - 10),
|
257 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.9, color, 2)
|
258 |
+
|
259 |
+
upload_obj = db.query(Upload).filter(Upload.id == upload_id).first()
|
260 |
+
if upload_obj:
|
261 |
+
upload_obj.total_workers += len(workers)
|
262 |
+
upload_obj.total_helmets += len(helmets)
|
263 |
+
upload_obj.total_vests += len(vests)
|
264 |
+
db.commit()
|
265 |
+
|
266 |
+
# Insert SafetyDetection for helmets/vests
|
267 |
+
now_utc = datetime.now(timezone.utc)
|
268 |
+
for (hx1, hy1, hx2, hy2) in helmets:
|
269 |
+
db.add(SafetyDetection(
|
270 |
+
label='H',
|
271 |
+
box=f"{hx1},{hy1},{hx2},{hy2}",
|
272 |
+
timestamp=now_utc,
|
273 |
+
upload_id=upload_id
|
274 |
+
))
|
275 |
+
for (vx1, vy1, vx2, vy2) in vests:
|
276 |
+
db.add(SafetyDetection(
|
277 |
+
label='V',
|
278 |
+
box=f"{vx1},{vy1},{vx2},{vy2}",
|
279 |
+
timestamp=now_utc,
|
280 |
+
upload_id=upload_id
|
281 |
+
))
|
282 |
+
db.commit()
|
283 |
+
|
284 |
+
# Also save worker crops
|
285 |
+
worker_images_list = []
|
286 |
+
for idx, (wx1, wy1, wx2, wy2) in enumerate(workers, start=1):
|
287 |
+
crop = frame[wy1:wy2, wx1:wx2]
|
288 |
+
if crop.size == 0:
|
289 |
+
continue
|
290 |
+
worker_filename = f"worker_{upload_id}_{idx}.jpg"
|
291 |
+
worker_path = os.path.join(WORKER_FOLDER, worker_filename)
|
292 |
+
cv2.imwrite(worker_path, crop)
|
293 |
+
worker_images_list.append(worker_path)
|
294 |
+
|
295 |
+
# Append new worker images
|
296 |
+
existing_imgs = upload_obj.worker_images.split(",") if upload_obj.worker_images else []
|
297 |
+
all_imgs = existing_imgs + worker_images_list
|
298 |
+
upload_obj.worker_images = ",".join([w for w in all_imgs if w])
|
299 |
+
db.commit()
|
300 |
+
|
301 |
+
return frame
|
302 |
+
|
303 |
+
def generate_and_email_pdf(upload_obj: Upload, db: Session):
|
304 |
+
"""
|
305 |
+
Generates a PDF report for a single upload, then emails it to upload_obj.user_email.
|
306 |
+
"""
|
307 |
+
# We’ll produce a single-page-ish PDF with the detection summary for this upload.
|
308 |
+
|
309 |
+
# Grab top-level stats
|
310 |
+
total_workers = upload_obj.total_workers
|
311 |
+
total_helmets = upload_obj.total_helmets
|
312 |
+
total_vests = upload_obj.total_vests
|
313 |
+
worker_images = upload_obj.worker_images.split(",") if upload_obj.worker_images else []
|
314 |
+
|
315 |
+
# Create a PDF
|
316 |
+
buffer = BytesIO()
|
317 |
+
doc = SimpleDocTemplate(buffer, pagesize=A4)
|
318 |
+
elements = []
|
319 |
+
styles = getSampleStyleSheet()
|
320 |
+
|
321 |
+
# Title
|
322 |
+
elements.append(Paragraph("Industrial Safety Monitor Report", styles["Title"]))
|
323 |
+
elements.append(Paragraph(f"Upload ID: {upload_obj.id}", styles["Normal"]))
|
324 |
+
elements.append(Paragraph(f"Filename: {upload_obj.filename}", styles["Normal"]))
|
325 |
+
elements.append(Paragraph(f"Timestamp: {upload_obj.timestamp.strftime('%Y-%m-%d %H:%M:%S')}", styles["Normal"]))
|
326 |
+
elements.append(Paragraph(f"Approach: {upload_obj.approach}", styles["Normal"]))
|
327 |
+
elements.append(Paragraph(f"User Email: {upload_obj.user_email}", styles["Normal"]))
|
328 |
+
elements.append(Spacer(1, 12))
|
329 |
+
|
330 |
+
# Table of basic detection metrics
|
331 |
+
data = [
|
332 |
+
["Total Workers", total_workers],
|
333 |
+
["Total Helmets", total_helmets],
|
334 |
+
["Total Vests", total_vests]
|
335 |
+
]
|
336 |
+
table = Table(data, colWidths=[200, 200])
|
337 |
+
table.setStyle(TableStyle([
|
338 |
+
("BACKGROUND", (0, 0), (-1, 0), colors.grey),
|
339 |
+
("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke),
|
340 |
+
("ALIGN", (0, 0), (-1, -1), "CENTER"),
|
341 |
+
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
342 |
+
("FONTSIZE", (0, 0), (-1, 0), 12),
|
343 |
+
("BOTTOMPADDING", (0, 0), (-1, 0), 12),
|
344 |
+
("BACKGROUND", (0, 1), (-1, -1), colors.beige),
|
345 |
+
("GRID", (0, 0), (-1, -1), 1, colors.black),
|
346 |
+
]))
|
347 |
+
elements.append(table)
|
348 |
+
elements.append(Spacer(1, 12))
|
349 |
+
|
350 |
+
# Show worker crops, if any
|
351 |
+
if worker_images:
|
352 |
+
elements.append(Paragraph("Detected Workers:", styles["Heading3"]))
|
353 |
+
elements.append(Spacer(1, 12))
|
354 |
+
for wimg in worker_images:
|
355 |
+
wimg = wimg.strip()
|
356 |
+
if wimg and os.path.exists(wimg):
|
357 |
+
elements.append(RLImage(wimg, width=100, height=75))
|
358 |
+
elements.append(Spacer(1, 12))
|
359 |
+
|
360 |
+
doc.build(elements)
|
361 |
+
buffer.seek(0)
|
362 |
+
pdf_data = buffer.getvalue()
|
363 |
+
|
364 |
+
# Email the PDF
|
365 |
+
receiver_email = upload_obj.user_email
|
366 |
+
if not receiver_email:
|
367 |
+
print("No email to send to.")
|
368 |
+
return # skip emailing if no user email
|
369 |
+
|
370 |
+
# Adjust credentials
|
371 |
+
sender_email = "[email protected]"
|
372 |
+
sender_password = "aobh rdgp iday bpwg"
|
373 |
+
subject = "Industrial Safety Monitor - Your Detection Report"
|
374 |
+
body = (
|
375 |
+
"Hello,\n\n"
|
376 |
+
"Please find attached the Industrial Safety Monitor detection report.\n"
|
377 |
+
"Regards,\nISM Bot"
|
378 |
+
)
|
379 |
+
|
380 |
+
from email.mime.multipart import MIMEMultipart
|
381 |
+
from email.mime.text import MIMEText
|
382 |
+
from email.mime.application import MIMEApplication
|
383 |
+
|
384 |
+
msg = MIMEMultipart()
|
385 |
+
msg["From"] = sender_email
|
386 |
+
msg["To"] = receiver_email
|
387 |
+
msg["Subject"] = subject
|
388 |
+
msg.attach(MIMEText(body, "plain"))
|
389 |
+
|
390 |
+
part = MIMEApplication(pdf_data, _subtype="pdf")
|
391 |
+
part.add_header("Content-Disposition", "attachment", filename="ISM_Report.pdf")
|
392 |
+
msg.attach(part)
|
393 |
+
|
394 |
+
try:
|
395 |
+
with smtplib.SMTP("smtp.gmail.com", 587) as server:
|
396 |
+
server.starttls()
|
397 |
+
server.login(sender_email, sender_password)
|
398 |
+
server.send_message(msg)
|
399 |
+
print(f"Email sent successfully to {receiver_email}!")
|
400 |
+
except Exception as e:
|
401 |
+
print(f"Error sending email: {e}")
|
402 |
+
|
403 |
+
|
404 |
+
##############################################################################
|
405 |
+
# 1) /upload
|
406 |
+
##############################################################################
|
407 |
+
|
408 |
+
@app.post("/upload", summary="Upload image/video + email; run detection, send PDF to email.")
|
409 |
+
async def upload_file(
|
410 |
+
approach: int = Form(...),
|
411 |
+
file: UploadFile = File(...),
|
412 |
+
user_email: str = Form(...),
|
413 |
+
):
|
414 |
+
"""
|
415 |
+
1) User uploads an image/video with approach + email.
|
416 |
+
2) We run YOLO detection.
|
417 |
+
3) We store results in DB.
|
418 |
+
4) We generate a PDF and email it to `user_email`.
|
419 |
+
5) Return detection counts in JSON.
|
420 |
+
"""
|
421 |
+
global model
|
422 |
+
|
423 |
+
db = SessionLocal()
|
424 |
+
|
425 |
+
# Prepare YOLO model for the chosen approach
|
426 |
+
try:
|
427 |
+
if (model is None) or (approach not in [1, 2, 3]):
|
428 |
+
model = prepare_model(approach)
|
429 |
+
except Exception as e:
|
430 |
+
db.close()
|
431 |
+
raise HTTPException(status_code=500, detail=str(e))
|
432 |
+
|
433 |
+
# Check file type
|
434 |
+
filename = file.filename
|
435 |
+
if not allowed_file(filename):
|
436 |
+
db.close()
|
437 |
+
raise HTTPException(
|
438 |
+
status_code=400,
|
439 |
+
detail="Unsupported file type. Allowed: .png, .jpg, .jpeg, .gif, .mp4",
|
440 |
+
)
|
441 |
+
|
442 |
+
# Save the uploaded file
|
443 |
+
filepath = os.path.join(UPLOAD_FOLDER, filename)
|
444 |
+
with open(filepath, "wb") as f:
|
445 |
+
f.write(await file.read())
|
446 |
+
|
447 |
+
# Create an Upload record
|
448 |
+
upload_obj = Upload(
|
449 |
+
filename=filename,
|
450 |
+
filepath=filepath,
|
451 |
+
timestamp=datetime.now(timezone.utc),
|
452 |
+
approach=approach,
|
453 |
+
user_email=user_email,
|
454 |
+
total_workers=0,
|
455 |
+
total_helmets=0,
|
456 |
+
total_vests=0,
|
457 |
+
worker_images=""
|
458 |
+
)
|
459 |
+
db.add(upload_obj)
|
460 |
+
db.commit()
|
461 |
+
db.refresh(upload_obj)
|
462 |
+
upload_id = upload_obj.id
|
463 |
+
|
464 |
+
# If it's an image
|
465 |
+
if filename.lower().endswith((".png", ".jpg", ".jpeg", ".gif")):
|
466 |
+
img = cv2.imread(filepath)
|
467 |
+
if img is None:
|
468 |
+
db.close()
|
469 |
+
raise HTTPException(status_code=400, detail="Failed to read the image file.")
|
470 |
+
|
471 |
+
# Run detection on the single image
|
472 |
+
annotated_frame = run_detection_on_frame(img, approach, upload_id, db)
|
473 |
+
|
474 |
+
# Save processed image
|
475 |
+
processed_filename = f"processed_{filename}"
|
476 |
+
processed_path = os.path.join(PROCESSED_FOLDER, processed_filename)
|
477 |
+
cv2.imwrite(processed_path, annotated_frame)
|
478 |
+
|
479 |
+
# If it's a video
|
480 |
+
elif filename.lower().endswith(".mp4"):
|
481 |
+
video = cv2.VideoCapture(filepath)
|
482 |
+
if not video.isOpened():
|
483 |
+
db.close()
|
484 |
+
raise HTTPException(status_code=400, detail="Failed to read the video file.")
|
485 |
+
|
486 |
+
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
487 |
+
processed_filename = f"processed_{filename}"
|
488 |
+
processed_path = os.path.join(PROCESSED_FOLDER, processed_filename)
|
489 |
+
|
490 |
+
original_width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))
|
491 |
+
original_height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
492 |
+
fps = video.get(cv2.CAP_PROP_FPS)
|
493 |
+
frame_count = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
|
494 |
+
|
495 |
+
out = cv2.VideoWriter(
|
496 |
+
processed_path, fourcc, fps, (original_width, original_height)
|
497 |
+
)
|
498 |
+
|
499 |
+
current_frame = 0
|
500 |
+
while True:
|
501 |
+
ret, frame = video.read()
|
502 |
+
if not ret:
|
503 |
+
break
|
504 |
+
current_frame += 1
|
505 |
+
print(f"Processing frame {current_frame}/{frame_count} (Upload ID={upload_id})")
|
506 |
+
|
507 |
+
annotated_frame = run_detection_on_frame(frame, approach, upload_id, db)
|
508 |
+
out.write(annotated_frame)
|
509 |
+
|
510 |
+
video.release()
|
511 |
+
out.release()
|
512 |
+
|
513 |
+
# Now fetch updated counts
|
514 |
+
db.refresh(upload_obj)
|
515 |
+
|
516 |
+
# Generate & email PDF
|
517 |
+
generate_and_email_pdf(upload_obj, db)
|
518 |
+
|
519 |
+
counts = {
|
520 |
+
"total_workers": upload_obj.total_workers,
|
521 |
+
"total_helmets": upload_obj.total_helmets,
|
522 |
+
"total_vests": upload_obj.total_vests
|
523 |
+
}
|
524 |
+
|
525 |
+
db.close()
|
526 |
+
return {
|
527 |
+
"message": f"File uploaded, detection done, PDF emailed to {user_email}.",
|
528 |
+
"upload_id": upload_id,
|
529 |
+
"counts": counts
|
530 |
+
}
|
531 |
+
|
532 |
+
|
533 |
+
##############################################################################
|
534 |
+
# 2) /results
|
535 |
+
##############################################################################
|
536 |
+
|
537 |
+
@app.get("/results", summary="Fetch the most recent upload’s details.")
|
538 |
+
def get_results():
|
539 |
+
"""
|
540 |
+
Returns the details (counts, file paths, worker_images) of the most recent upload.
|
541 |
+
"""
|
542 |
+
db = SessionLocal()
|
543 |
+
latest = db.query(Upload).order_by(Upload.timestamp.desc()).first()
|
544 |
+
if not latest:
|
545 |
+
db.close()
|
546 |
+
return {"message": "No uploads found in the database."}
|
547 |
+
|
548 |
+
processed_filename = f"processed_{latest.filename}"
|
549 |
+
processed_path = os.path.join(PROCESSED_FOLDER, processed_filename)
|
550 |
+
data = {
|
551 |
+
"upload_id": latest.id,
|
552 |
+
"filename": latest.filename,
|
553 |
+
"original_path": latest.filepath,
|
554 |
+
"processed_path": processed_path if os.path.exists(processed_path) else None,
|
555 |
+
"approach": latest.approach,
|
556 |
+
"user_email": latest.user_email,
|
557 |
+
"total_workers": latest.total_workers,
|
558 |
+
"total_helmets": latest.total_helmets,
|
559 |
+
"total_vests": latest.total_vests,
|
560 |
+
"worker_images": (latest.worker_images.split(",") if latest.worker_images else []),
|
561 |
+
"timestamp": latest.timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
562 |
+
}
|
563 |
+
db.close()
|
564 |
+
return data
|
565 |
+
|
566 |
+
|
567 |
+
##############################################################################
|
568 |
+
# 3) /dashboard
|
569 |
+
##############################################################################
|
570 |
+
|
571 |
+
@app.get("/dashboard", summary="Get aggregated statistics for a dashboard.")
|
572 |
+
def dashboard():
|
573 |
+
"""
|
574 |
+
Returns aggregated stats (uploads, detection sums, time-series, approach usage) in JSON.
|
575 |
+
"""
|
576 |
+
db = SessionLocal()
|
577 |
+
|
578 |
+
# Total uploads
|
579 |
+
total_uploads = db.query(Upload).count()
|
580 |
+
|
581 |
+
# Summation of detections
|
582 |
+
agg = db.query(
|
583 |
+
func.sum(Upload.total_workers).label("tw"),
|
584 |
+
func.sum(Upload.total_helmets).label("th"),
|
585 |
+
func.sum(Upload.total_vests).label("tv")
|
586 |
+
).one()
|
587 |
+
total_workers = agg.tw or 0
|
588 |
+
total_helmets = agg.th or 0
|
589 |
+
total_vests = agg.tv or 0
|
590 |
+
|
591 |
+
# Time-series by day
|
592 |
+
day_rows = db.query(
|
593 |
+
func.date(Upload.timestamp).label("day"),
|
594 |
+
func.count(Upload.id).label("uploads"),
|
595 |
+
func.sum(Upload.total_workers).label("workers"),
|
596 |
+
func.sum(Upload.total_helmets).label("helmets"),
|
597 |
+
func.sum(Upload.total_vests).label("vests")
|
598 |
+
).group_by(func.date(Upload.timestamp)).order_by(func.date(Upload.timestamp)).all()
|
599 |
+
|
600 |
+
dates = []
|
601 |
+
uploads_per_day = []
|
602 |
+
workers_per_day = []
|
603 |
+
helmets_per_day = []
|
604 |
+
vests_per_day = []
|
605 |
+
|
606 |
+
for row in day_rows:
|
607 |
+
dates.append(row.day)
|
608 |
+
uploads_per_day.append(row.uploads or 0)
|
609 |
+
workers_per_day.append(row.workers or 0)
|
610 |
+
helmets_per_day.append(row.helmets or 0)
|
611 |
+
vests_per_day.append(row.vests or 0)
|
612 |
+
|
613 |
+
# Approach usage
|
614 |
+
approach_rows = db.query(
|
615 |
+
Upload.approach,
|
616 |
+
func.count(Upload.id).label("count")
|
617 |
+
).group_by(Upload.approach).all()
|
618 |
+
approach_data = []
|
619 |
+
for ar in approach_rows:
|
620 |
+
approach_data.append({
|
621 |
+
"approach": f"Approach-{ar.approach}",
|
622 |
+
"count": ar.count
|
623 |
+
})
|
624 |
+
|
625 |
+
# Basic distribution of helmets vs. vests
|
626 |
+
safety_gear_labels = ["Helmets", "Vests"]
|
627 |
+
safety_gear_counts = [total_helmets, total_vests]
|
628 |
+
|
629 |
+
db.close()
|
630 |
+
return {
|
631 |
+
"total_uploads": total_uploads,
|
632 |
+
"total_workers": total_workers,
|
633 |
+
"total_helmets": total_helmets,
|
634 |
+
"total_vests": total_vests,
|
635 |
+
"time_series": {
|
636 |
+
"dates": dates,
|
637 |
+
"uploads_per_day": uploads_per_day,
|
638 |
+
"workers_per_day": workers_per_day,
|
639 |
+
"helmets_per_day": helmets_per_day,
|
640 |
+
"vests_per_day": vests_per_day
|
641 |
+
},
|
642 |
+
"approach_usage": approach_data,
|
643 |
+
"safety_gear_distribution": {
|
644 |
+
"labels": safety_gear_labels,
|
645 |
+
"counts": safety_gear_counts
|
646 |
+
}
|
647 |
+
}
|
648 |
+
|
649 |
+
|
650 |
+
##############################################################################
|
651 |
+
# Startup (Load YOLO Model)
|
652 |
+
##############################################################################
|
653 |
+
|
654 |
+
@app.on_event("startup")
|
655 |
+
def on_startup():
|
656 |
+
fix_tf_gpu()
|
657 |
+
global model
|
658 |
+
try:
|
659 |
+
# Load default approach=1 at startup (optional)
|
660 |
+
model_local = prepare_model(approach=1)
|
661 |
+
model = model_local
|
662 |
+
print("YOLO model (Approach=1) loaded successfully.")
|
663 |
+
except FileNotFoundError as e:
|
664 |
+
print(f"Model file not found on startup: {e}")
|
665 |
+
except Exception as e:
|
666 |
+
print(f"Error preparing model on startup: {e}")
|
requirements.txt
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FastAPI
|
2 |
+
Flask
|
3 |
+
matplotlib
|
4 |
+
numpy
|
5 |
+
opencv-python
|
6 |
+
opencv-python-headless
|
7 |
+
pymongo
|
8 |
+
reportlab
|
9 |
+
starlette
|
10 |
+
tensorflow
|
11 |
+
tensorflow-intel
|
12 |
+
uvicorn
|
13 |
+
Werkzeug
|