tutosutiles commited on
Commit
2bd8ec6
·
verified ·
1 Parent(s): b078c6a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +938 -0
app.py CHANGED
@@ -0,0 +1,938 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import tempfile
4
+ import json
5
+ from zipfile import ZipFile
6
+ import smtplib
7
+ from email.mime.text import MIMEText
8
+ from email.mime.multipart import MIMEMultipart
9
+
10
+ import cv2
11
+ import numpy as np
12
+ import pandas as pd
13
+ from PIL import Image
14
+ import streamlit as st
15
+ from fpdf import FPDF
16
+ from st_aggrid import AgGrid
17
+ from st_aggrid.grid_options_builder import GridOptionsBuilder
18
+
19
+ from ultralytics import YOLO, RTDETR
20
+ from streamlit_webrtc import webrtc_streamer, VideoTransformerBase
21
+
22
+ ###############################################################################
23
+ # FONCTIONS LIEES AU MODELE
24
+ ###############################################################################
25
+
26
+ @st.cache_resource()
27
+ def load_model(model_choice, custom_model_path=None):
28
+ """
29
+ Chargement du modèle YOLO/RT-DETR en fonction du choix utilisateur,
30
+ avec mise en cache pour accélérer le rechargement.
31
+ """
32
+ detection_models = [
33
+ "yolov5nu", "yolov5s", "yolov5m", "yolov5l", "yolov5x",
34
+ "yolov8n", "yolov8s", "yolov8m", "yolov8l", "yolov8x",
35
+ "yolov9c", "yolov9e",
36
+ "yolov10n", "yolov10s", "yolov10m", "yolov10l", "yolov10x",
37
+ "yolo11n", "yolo11s", "yolo11m", "yolo11l", "yolo11x",
38
+ "yolo12n", "yolo12s", "yolo12m", "yolo12l", "yolo12x",
39
+ "rtdetr-l", "rtdetr-x"
40
+ ]
41
+ segmentation_models = [
42
+ "yolov8n-seg", "yolov8s-seg", "yolov8m-seg", "yolov8l-seg", "yolov8x-seg",
43
+ "yolov9c-seg", "yolov9e-seg",
44
+ "yolo11n-seg", "yolo11s-seg", "yolo11m-seg", "yolo11l-seg", "yolo11x-seg"
45
+ ]
46
+ pose_models = [
47
+ "yolov8n-pose", "yolov8s-pose", "yolov8m-pose", "yolov8l-pose", "yolov8x-pose",
48
+ "yolo11n-pose", "yolo11s-pose", "yolo11m-pose", "yolo11l-pose", "yolo11x-pose"
49
+ ]
50
+
51
+ # Choix du modèle
52
+ if model_choice in detection_models + segmentation_models + pose_models:
53
+ return YOLO(f"{model_choice}.pt")
54
+ elif model_choice == 'custom' and custom_model_path:
55
+ return YOLO(custom_model_path)
56
+ else:
57
+ # Exemple pour gérer plusieurs PT nommés 'best.pt' ...
58
+ model_paths = ["best.pt", "best2.pt", "best3.pt", "best5.pt"]
59
+ model_names = ["model1", "model2", "model3", "model4"]
60
+ idx = model_names.index(model_choice)
61
+ return YOLO(model_paths[idx])
62
+
63
+ def detect_objects(model,
64
+ image,
65
+ model_type,
66
+ conf,
67
+ iou,
68
+ classes_to_detect=None,
69
+ max_det=1000,
70
+ line_width=2,
71
+ agnostic_nms=False):
72
+ """
73
+ Détection sur une image unique, en tenant compte des filtres de classe,
74
+ du paramètre max_det, de l'épaisseur de bounding box, et du agnostic_nms.
75
+ """
76
+ image_np = np.array(image) if not isinstance(image, np.ndarray) else image
77
+
78
+ # Inférence YOLO/RT-DETR
79
+ results = model(
80
+ image_np,
81
+ conf=conf,
82
+ iou=iou,
83
+ classes=classes_to_detect if classes_to_detect else None,
84
+ max_det=max_det,
85
+ agnostic_nms=agnostic_nms
86
+ )
87
+ annotated_image = results[0].plot(line_width=line_width)
88
+ return annotated_image, results
89
+
90
+ def count_objects(results, model_type, class_names):
91
+ """
92
+ Compte le nombre d'objets détectés par classe.
93
+ """
94
+ object_counts = {}
95
+ classes = results[0].boxes.cls.cpu().numpy()
96
+ for cls_id in classes:
97
+ name = class_names[int(cls_id)]
98
+ object_counts[name] = object_counts.get(name, 0) + 1
99
+ return object_counts
100
+
101
+ ###############################################################################
102
+ # FONCTIONS D'EXPORT (PDF, ZIP, CSV, JSON)
103
+ ###############################################################################
104
+
105
+ def export_pdf(images):
106
+ """
107
+ Exporte une liste d'images PIL en un seul PDF avec un bouton de téléchargement.
108
+ """
109
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmpfile:
110
+ pdf_path = tmpfile.name
111
+ images[0].save(pdf_path, save_all=True, append_images=images[1:])
112
+ with open(pdf_path, "rb") as f:
113
+ st.download_button("📄 Télécharger le PDF", data=f, file_name="resultats.pdf")
114
+
115
+ def export_zip(images):
116
+ """
117
+ Exporte une liste d'images PIL dans un ZIP avec un bouton de téléchargement.
118
+ """
119
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".zip") as tmpfile:
120
+ zip_path = tmpfile.name
121
+ with ZipFile(zip_path, 'w') as zipf:
122
+ for i, img in enumerate(images):
123
+ img_filename = f"image_{i}.png"
124
+ img.save(img_filename)
125
+ zipf.write(img_filename)
126
+ os.remove(img_filename)
127
+ with open(zip_path, "rb") as f:
128
+ st.download_button("🗜️ Télécharger le ZIP", data=f, file_name="resultats.zip")
129
+
130
+ def export_csv_rows(csv_rows):
131
+ """
132
+ Exporte les détections dans un CSV avec un bouton de téléchargement.
133
+ """
134
+ df = pd.DataFrame(csv_rows)
135
+ csv_data = df.to_csv(index=False).encode("utf-8")
136
+ st.download_button("📤 Télécharger CSV des détections", data=csv_data,
137
+ file_name="detections.csv", mime="text/csv")
138
+
139
+ ###############################################################################
140
+ # AFFICHAGE DU TABLEAU (st_aggrid)
141
+ ###############################################################################
142
+
143
+ def show_table(data, key=None):
144
+ """
145
+ Affiche un tableau récapitulatif (classe / nombre d'objets détectés).
146
+ """
147
+ df = pd.DataFrame(list(data.items()), columns=["Classe", "Nombre"])
148
+ gb = GridOptionsBuilder.from_dataframe(df)
149
+ gb.configure_pagination()
150
+ gb.configure_default_column(editable=False, groupable=True)
151
+ gb.configure_selection('multiple', use_checkbox=True)
152
+ grid_options = gb.build()
153
+ AgGrid(df, gridOptions=grid_options, theme="streamlit", key=key)
154
+
155
+ ###############################################################################
156
+ # FONCTIONS ENVOI EMAIL & SAUVEGARDE SUR LE CLOUD
157
+ ###############################################################################
158
+
159
+ def send_notification_smtp(to_email, message):
160
+ """
161
+ Envoi d'un email via SMTP, nécessite des identifiants valides dans st.secrets.
162
+ """
163
+ smtp_server = st.secrets.get("SMTP_SERVER", "smtp.gmail.com")
164
+ smtp_port = int(st.secrets.get("SMTP_PORT", 587))
165
+ smtp_user = st.secrets.get("SMTP_USER", "your_email@gmail.com")
166
+ smtp_pass = st.secrets.get("SMTP_PASS", "your_password")
167
+
168
+ try:
169
+ msg = MIMEMultipart("alternative")
170
+ msg["Subject"] = "YOLO Detection Notification"
171
+ msg["From"] = smtp_user
172
+ msg["To"] = to_email
173
+
174
+ part = MIMEText(message, "plain")
175
+ msg.attach(part)
176
+
177
+ with smtplib.SMTP(smtp_server, smtp_port) as server:
178
+ server.starttls()
179
+ server.login(smtp_user, smtp_pass)
180
+ server.sendmail(smtp_user, to_email, msg.as_string())
181
+
182
+ st.success(f"Email envoyé à {to_email} avec succès!")
183
+ except Exception as e:
184
+ st.error(f"Erreur lors de l'envoi du mail: {e}")
185
+
186
+ def save_to_cloud(file_data, service):
187
+ """
188
+ Fonction illustrative pour sauvegarder des données sur Google Drive / Dropbox / OneDrive.
189
+ Remplacer avec du code d'API réel selon le service.
190
+ """
191
+ if service == "Google Drive":
192
+ st.info("Exemple : utiliser PyDrive ou Google Drive API.")
193
+ elif service == "Dropbox":
194
+ st.info("Exemple : utiliser le SDK Dropbox pour l'upload.")
195
+ elif service == "OneDrive":
196
+ st.info("Exemple : utiliser le SDK OneDrive (MS Graph).")
197
+
198
+ st.success(f"Sauvegarde simulée sur {service} réalisée avec succès !")
199
+
200
+ ###############################################################################
201
+ # TRANSFORMER POUR STREAMLIT_WEBRTC (VIDEO TEMPS REEL)
202
+ ###############################################################################
203
+
204
+ class VideoTransformer(VideoTransformerBase):
205
+ def __init__(
206
+ self,
207
+ model,
208
+ conf=0.25,
209
+ iou=0.45,
210
+ show_fps=False,
211
+ auto_snapshot=False,
212
+ snapshot_interval=5,
213
+ output_format="RGB",
214
+ apply_filters=False,
215
+ advanced_filters=False,
216
+ rotation_angle=0,
217
+ resize_width=None,
218
+ resize_height=None,
219
+ detection_zone=None,
220
+ notification_email=None,
221
+ save_to_cloud=False,
222
+ morphological_ops=False,
223
+ equalize_hist=False,
224
+ classes_to_detect=None,
225
+ max_det=1000,
226
+ line_width=2,
227
+ record_output=False,
228
+ agnostic_nms=False
229
+ ):
230
+ """
231
+ Gère chaque frame de la webcam en temps réel, applique YOLO,
232
+ différents filtres, la rotation, la sauvegarde locale, etc.
233
+ """
234
+ self.model = model
235
+ self.conf = conf
236
+ self.iou = iou
237
+ self.show_fps = show_fps
238
+ self.auto_snapshot = auto_snapshot
239
+ self.snapshot_interval = snapshot_interval
240
+ self.output_format = output_format.upper()
241
+ self.apply_filters = apply_filters
242
+ self.advanced_filters = advanced_filters
243
+ self.rotation_angle = rotation_angle
244
+ self.resize_width = resize_width
245
+ self.resize_height = resize_height
246
+ self.detection_zone = detection_zone
247
+ self.notification_email = notification_email
248
+ self.save_to_cloud = save_to_cloud
249
+
250
+ self.morphological_ops = morphological_ops
251
+ self.equalize_hist = equalize_hist
252
+ self.classes_to_detect = classes_to_detect
253
+ self.max_det = max_det
254
+ self.line_width = line_width
255
+ self.record_output = record_output
256
+ self.agnostic_nms = agnostic_nms
257
+
258
+ self.last_time = time.time()
259
+ self.last_snapshot_time = time.time()
260
+ self.latest_snapshot = None
261
+ self.last_frame = None
262
+
263
+ # Configuration pour l'enregistrement local si besoin
264
+ self.video_writer = None
265
+ if self.record_output:
266
+ self.output_filename = os.path.join(tempfile.gettempdir(),
267
+ f"webcam_record_{time.time()}.mp4")
268
+
269
+ def transform(self, frame):
270
+ image = frame.to_ndarray(format="bgr24")
271
+ self.last_frame = image.copy()
272
+
273
+ # Initialiser la sauvegarde si besoin
274
+ if self.record_output and self.video_writer is None:
275
+ fourcc = cv2.VideoWriter_fourcc(*"mp4v")
276
+ h, w, _ = image.shape
277
+ self.video_writer = cv2.VideoWriter(self.output_filename, fourcc, 20.0, (w, h))
278
+
279
+ # Application de filtres simples
280
+ if self.apply_filters:
281
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
282
+ image = cv2.GaussianBlur(image, (5, 5), 0)
283
+ image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
284
+
285
+ # Filtres avancés (contraste, luminosité)
286
+ if self.advanced_filters:
287
+ image = cv2.convertScaleAbs(image, alpha=1.5, beta=30)
288
+
289
+ # Opérations morphologiques
290
+ if self.morphological_ops:
291
+ kernel = np.ones((3,3), np.uint8)
292
+ image = cv2.morphologyEx(image, cv2.MORPH_OPEN, kernel)
293
+ image = cv2.morphologyEx(image, cv2.MORPH_CLOSE, kernel)
294
+
295
+ # Egalisation d'histogramme
296
+ if self.equalize_hist:
297
+ yuv = cv2.cvtColor(image, cv2.COLOR_BGR2YUV)
298
+ yuv[:, :, 0] = cv2.equalizeHist(yuv[:, :, 0])
299
+ image = cv2.cvtColor(yuv, cv2.COLOR_YUV2BGR)
300
+
301
+ # Rotation
302
+ if self.rotation_angle != 0:
303
+ image = self.rotate_image(image, self.rotation_angle)
304
+
305
+ # Redimensionnement
306
+ if self.resize_width and self.resize_height:
307
+ image = cv2.resize(image, (self.resize_width, self.resize_height))
308
+
309
+ # Zone de détection
310
+ if self.detection_zone:
311
+ x, y, w, h = self.detection_zone
312
+ image = image[y:y+h, x:x+w]
313
+
314
+ # Inférence YOLO
315
+ results = self.model(
316
+ image,
317
+ conf=self.conf,
318
+ iou=self.iou,
319
+ classes=self.classes_to_detect if self.classes_to_detect else None,
320
+ max_det=self.max_det,
321
+ agnostic_nms=self.agnostic_nms
322
+ )
323
+ annotated_frame = results[0].plot(line_width=self.line_width)
324
+
325
+ # Affichage FPS
326
+ current_time = time.time()
327
+ dt = current_time - self.last_time
328
+ fps = 1.0 / dt if dt > 0 else 0.0
329
+ self.last_time = current_time
330
+ if self.show_fps:
331
+ cv2.putText(annotated_frame, f"FPS: {fps:.2f}", (10, 30),
332
+ cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
333
+
334
+ # Auto Snapshot
335
+ if self.auto_snapshot and (current_time - self.last_snapshot_time >= self.snapshot_interval):
336
+ self.last_snapshot_time = current_time
337
+ self.latest_snapshot = annotated_frame.copy()
338
+
339
+ # Enregistrement local
340
+ if self.record_output and self.video_writer is not None:
341
+ self.video_writer.write(annotated_frame)
342
+
343
+ # Format de sortie (RGB vs BGR)
344
+ if self.output_format == "RGB":
345
+ display_frame = cv2.cvtColor(annotated_frame, cv2.COLOR_BGR2RGB)
346
+ else:
347
+ display_frame = annotated_frame
348
+
349
+ # Notification email
350
+ if self.notification_email and any(results):
351
+ send_notification_smtp(self.notification_email, "Détection réalisée sur le flux webcam !")
352
+
353
+ # Sauvegarde sur le cloud si détection
354
+ if self.save_to_cloud and any(results):
355
+ _, buffer = cv2.imencode('.png', annotated_frame)
356
+ save_to_cloud(buffer.tobytes(), "Google Drive")
357
+
358
+ return display_frame
359
+
360
+ def rotate_image(self, image, angle):
361
+ (h, w) = image.shape[:2]
362
+ center = (w / 2, h / 2)
363
+ M = cv2.getRotationMatrix2D(center, angle, 1.0)
364
+ return cv2.warpAffine(image, M, (w, h))
365
+
366
+ def __del__(self):
367
+ if self.video_writer:
368
+ self.video_writer.release()
369
+
370
+ ###############################################################################
371
+ # TRAITEMENT DE VIDEOS (FICHIER LOCAL)
372
+ ###############################################################################
373
+
374
+ def process_video_file(
375
+ video_path,
376
+ model,
377
+ conf,
378
+ iou,
379
+ export_type,
380
+ classes_to_detect=None,
381
+ max_det=1000,
382
+ line_width=2,
383
+ agnostic_nms=False
384
+ ):
385
+ """
386
+ Traite une vidéo en local, affiche certaines frames annotées,
387
+ et propose l'exportation des résultats.
388
+ """
389
+ cap = cv2.VideoCapture(video_path)
390
+ if not cap.isOpened():
391
+ st.error("🚫 Impossible d'ouvrir la vidéo.")
392
+ return
393
+
394
+ frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
395
+ st.info(f"🎞️ Vidéo chargée, {frame_count} frames trouvées.")
396
+
397
+ process_only_first = st.checkbox("Traiter seulement la première frame", value=True)
398
+ output_images = []
399
+ csv_rows = []
400
+
401
+ if process_only_first:
402
+ ret, frame = cap.read()
403
+ if ret:
404
+ pil_img = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
405
+ annotated_frame, results = detect_objects(
406
+ model=model,
407
+ image=pil_img,
408
+ model_type="Vidéo",
409
+ conf=conf,
410
+ iou=iou,
411
+ classes_to_detect=classes_to_detect,
412
+ max_det=max_det,
413
+ line_width=line_width,
414
+ agnostic_nms=agnostic_nms
415
+ )
416
+ st.image(annotated_frame, channels="BGR", caption="🖼️ Première frame annotée")
417
+ output_images.append(Image.fromarray(cv2.cvtColor(annotated_frame, cv2.COLOR_BGR2RGB)))
418
+
419
+ counts = count_objects(results, "Vidéo", model.names if hasattr(model, 'names') else [])
420
+ for cls_name, count_val in counts.items():
421
+ csv_rows.append({"Image": "frame_0", "Classe": cls_name, "Nombre": count_val})
422
+ else:
423
+ st.error("🚫 Impossible de lire la première frame.")
424
+ else:
425
+ num_frames_to_process = st.slider("Nombre de frames à traiter", 1, min(frame_count, 50), 10)
426
+ frame_idx = 0
427
+ processed = 0
428
+ interval = max(1, frame_count // num_frames_to_process)
429
+ while processed < num_frames_to_process:
430
+ ret, frame = cap.read()
431
+ if not ret:
432
+ break
433
+ if frame_idx % interval == 0:
434
+ pil_img = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
435
+ annotated_frame, results = detect_objects(
436
+ model=model,
437
+ image=pil_img,
438
+ model_type="Vidéo",
439
+ conf=conf,
440
+ iou=iou,
441
+ classes_to_detect=classes_to_detect,
442
+ max_det=max_det,
443
+ line_width=line_width,
444
+ agnostic_nms=agnostic_nms
445
+ )
446
+ st.image(annotated_frame, channels="BGR", caption=f"🖼️ Frame {frame_idx} annotée")
447
+ output_images.append(Image.fromarray(cv2.cvtColor(annotated_frame, cv2.COLOR_BGR2RGB)))
448
+
449
+ counts = count_objects(results, "Vidéo", model.names if hasattr(model, 'names') else [])
450
+ for cls_name, count_val in counts.items():
451
+ csv_rows.append({"Image": f"frame_{frame_idx}", "Classe": cls_name, "Nombre": count_val})
452
+ processed += 1
453
+ frame_idx += 1
454
+ cap.release()
455
+
456
+ # Export
457
+ if output_images:
458
+ if export_type == "PDF":
459
+ export_pdf(output_images)
460
+ elif export_type == "ZIP":
461
+ export_zip(output_images)
462
+ elif export_type == "CSV":
463
+ export_csv_rows(csv_rows)
464
+ elif export_type == "JSON":
465
+ json_data = json.dumps(csv_rows, indent=4)
466
+ st.download_button("📥 Télécharger JSON des détections",
467
+ data=json_data,
468
+ file_name="detections.json",
469
+ mime="application/json")
470
+
471
+ ###############################################################################
472
+ # GESTION OPTIMISEE DES WEBCAMS: AFFICHER PLUSIEURS CAMERAS
473
+ ###############################################################################
474
+
475
+ def display_webcam_streams(
476
+ selected_devices,
477
+ model,
478
+ conf,
479
+ iou,
480
+ show_fps,
481
+ auto_snapshot,
482
+ snapshot_interval,
483
+ output_format,
484
+ apply_filters,
485
+ advanced_filters,
486
+ rotation_angle,
487
+ resize_width,
488
+ resize_height,
489
+ detection_zone,
490
+ notification_email,
491
+ save_to_cloud,
492
+ morphological_ops,
493
+ equalize_hist,
494
+ classes_to_detect,
495
+ max_det,
496
+ line_width,
497
+ record_output,
498
+ agnostic_nms
499
+ ):
500
+ """
501
+ Affiche plusieurs webcams simultanément, organisées en lignes de 4 caméras max.
502
+ Chaque webcam possède sa propre instance de VideoTransformer.
503
+ """
504
+ if not selected_devices:
505
+ st.warning("Aucune webcam sélectionnée.")
506
+ return
507
+
508
+ # Découper la liste de caméras en groupes de 4 pour l'affichage en grille
509
+ for row_start in range(0, len(selected_devices), 4):
510
+ row_devices = selected_devices[row_start:row_start+4]
511
+ cols = st.columns(len(row_devices))
512
+
513
+ for i, device_index in enumerate(row_devices):
514
+ with cols[i]:
515
+ st.markdown(f"**Caméra {device_index}**")
516
+ ctx = webrtc_streamer(
517
+ key=f"webcam-{device_index}",
518
+ video_transformer_factory=lambda m=model, c=conf, iou_val=iou: VideoTransformer(
519
+ m,
520
+ conf=c,
521
+ iou=iou_val,
522
+ show_fps=show_fps,
523
+ auto_snapshot=auto_snapshot,
524
+ snapshot_interval=snapshot_interval,
525
+ output_format=output_format,
526
+ apply_filters=apply_filters,
527
+ advanced_filters=advanced_filters,
528
+ rotation_angle=rotation_angle,
529
+ resize_width=resize_width,
530
+ resize_height=resize_height,
531
+ detection_zone=detection_zone,
532
+ notification_email=notification_email,
533
+ save_to_cloud=save_to_cloud,
534
+ morphological_ops=morphological_ops,
535
+ equalize_hist=equalize_hist,
536
+ classes_to_detect=classes_to_detect,
537
+ max_det=max_det,
538
+ line_width=line_width,
539
+ record_output=record_output,
540
+ agnostic_nms=agnostic_nms
541
+ ),
542
+ rtc_configuration={"iceServers": [{"urls": ["stun:stun.l.google.com:19302"]}]},
543
+ media_stream_constraints={
544
+ "video": {"deviceId": {"exact": str(device_index)}},
545
+ "audio": False
546
+ }
547
+ )
548
+
549
+ # Bouton de snapshot manuel
550
+ if st.button(f"📸 Snapshot Caméra {device_index}", key=f"snap-{device_index}"):
551
+ if ctx.video_transformer:
552
+ frame = ctx.video_transformer.last_frame
553
+ if frame is not None:
554
+ st.image(frame, caption=f"Snapshot Caméra {device_index}", channels="BGR")
555
+ else:
556
+ st.warning("🚫 Aucune image capturée pour cette caméra.")
557
+
558
+ # Snapshot automatique téléchargeable
559
+ if auto_snapshot and ctx.video_transformer and ctx.video_transformer.latest_snapshot is not None:
560
+ ret, buffer = cv2.imencode('.png', ctx.video_transformer.latest_snapshot)
561
+ if ret:
562
+ snapshot_bytes = buffer.tobytes()
563
+ st.download_button(
564
+ label="📥 Télécharger Snapshot Auto",
565
+ data=snapshot_bytes,
566
+ file_name=f"snapshot_cam_{device_index}.png",
567
+ key=f"auto_snap_{device_index}"
568
+ )
569
+
570
+ ###############################################################################
571
+ # GESTION CAMERA IP
572
+ ###############################################################################
573
+
574
+ def display_ip_camera(
575
+ ip_url,
576
+ model,
577
+ conf,
578
+ iou,
579
+ show_fps,
580
+ auto_snapshot,
581
+ snapshot_interval,
582
+ output_format,
583
+ classes_to_detect=None,
584
+ max_det=1000,
585
+ line_width=2,
586
+ agnostic_nms=False
587
+ ):
588
+ """
589
+ Lit des frames depuis une caméra IP (RTSP), applique YOLO, et les affiche en temps réel.
590
+ """
591
+ cap = cv2.VideoCapture(ip_url)
592
+ if not cap.isOpened():
593
+ st.error("🚫 Impossible d'ouvrir la caméra IP.")
594
+ return
595
+
596
+ frame_placeholder = st.empty()
597
+ last_time = time.time()
598
+ last_snapshot_time = time.time()
599
+ stop_button = st.button("⏹️ Arrêter le streaming IP")
600
+
601
+ while True:
602
+ if stop_button:
603
+ st.info("Arrêt du streaming IP.")
604
+ break
605
+
606
+ ret, frame = cap.read()
607
+ if not ret:
608
+ st.error("🚫 Erreur de lecture du flux IP.")
609
+ break
610
+
611
+ # Inférence YOLO
612
+ results = model(
613
+ frame,
614
+ conf=conf,
615
+ iou=iou,
616
+ classes=classes_to_detect if classes_to_detect else None,
617
+ max_det=max_det,
618
+ agnostic_nms=agnostic_nms
619
+ )
620
+ annotated_frame = results[0].plot(line_width=line_width)
621
+
622
+ # FPS
623
+ current_time = time.time()
624
+ dt = current_time - last_time
625
+ fps = 1.0 / dt if dt > 0 else 0.0
626
+ last_time = current_time
627
+ if show_fps:
628
+ cv2.putText(annotated_frame, f"FPS: {fps:.2f}", (10, 30),
629
+ cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
630
+
631
+ # Auto-snapshot
632
+ if auto_snapshot and (current_time - last_snapshot_time >= snapshot_interval):
633
+ last_snapshot_time = current_time
634
+ snapshot_bytes = cv2.imencode('.png', annotated_frame)[1].tobytes()
635
+ st.download_button(
636
+ "📥 Télécharger Snapshot Auto",
637
+ data=snapshot_bytes,
638
+ file_name="ip_snapshot.png",
639
+ key=f"ip_snapshot_{time.time()}"
640
+ )
641
+
642
+ # Format de sortie
643
+ if output_format.upper() == "RGB":
644
+ annotated_frame = cv2.cvtColor(annotated_frame, cv2.COLOR_BGR2RGB)
645
+
646
+ frame_placeholder.image(
647
+ annotated_frame,
648
+ channels="RGB" if output_format.upper()=="RGB" else "BGR"
649
+ )
650
+
651
+ cap.release()
652
+
653
+ ###############################################################################
654
+ # APPLICATION PRINCIPALE
655
+ ###############################################################################
656
+
657
+ def main():
658
+ st.set_page_config(page_title="Plateforme de Vision par Ordinateur - TECHSOLUT", layout="wide")
659
+ st.title("👁️ Vision par Ordinateur - TECHSOLUT (Multi-Webcams Optimisé)")
660
+
661
+ # 1) Choix du modèle
662
+ model_versions = {
663
+ "Détection": {
664
+ "YOLOv5": ["yolov5nu", "yolov5s", "yolov5m", "yolov5l", "yolov5x"],
665
+ "YOLOv8": ["yolov8n", "yolov8s", "yolov8m", "yolov8l", "yolov8x"],
666
+ "YOLOv9": ["yolov9c", "yolov9e"],
667
+ "YOLOv10": ["yolov10n", "yolov10s", "yolov10m", "yolov10l", "yolov10x"],
668
+ "YOLO11": ["yolo11n", "yolo11s", "yolo11m", "yolo11l", "yolo11x"],
669
+ "YOLO12": ["yolo12n", "yolo12s", "yolo12m", "yolo12l", "yolo12x"],
670
+ "RT-DETR": ["rtdetr-l", "rtdetr-x"]
671
+ },
672
+ "Segmentation": {
673
+ "YOLOv8": ["yolov8n-seg", "yolov8s-seg", "yolov8m-seg", "yolov8l-seg", "yolov8x-seg"],
674
+ "YOLOv9": ["yolov9c-seg", "yolov9e-seg"],
675
+ "YOLO11": ["yolo11n-seg", "yolo11s-seg", "yolo11m-seg", "yolo11l-seg", "yolo11x-seg"]
676
+ },
677
+ "Estimation de pose": {
678
+ "YOLOv8": ["yolov8n-pose", "yolov8s-pose", "yolov8m-pose", "yolov8l-pose", "yolov8x-pose"],
679
+ "YOLO11": ["yolo11n-pose", "yolo11s-pose", "yolo11m-pose", "yolo11l-pose", "yolo11x-pose"]
680
+ },
681
+ "Personnalisé": {
682
+ "Custom": ["custom"]
683
+ }
684
+ }
685
+
686
+ with st.sidebar:
687
+ with st.expander("🧠 Choix du modèle"):
688
+ task_type = st.selectbox("Type de tâche", list(model_versions.keys()))
689
+ model_family = st.selectbox("Famille de modèle", list(model_versions[task_type].keys()))
690
+ selected_model = st.selectbox("Version du modèle", model_versions[task_type][model_family])
691
+
692
+ custom_model_path = None
693
+ if selected_model == "custom":
694
+ uploaded_file = st.file_uploader("📥 Charger un modèle (.pt)", type=["pt"])
695
+ if uploaded_file:
696
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".pt") as tmp:
697
+ tmp.write(uploaded_file.read())
698
+ custom_model_path = tmp.name
699
+
700
+ with st.expander("🎯 Paramètres"):
701
+ conf = st.slider("Confiance (confidence threshold)", 0.0, 1.0, 0.25)
702
+ iou = st.slider("IoU threshold", 0.0, 1.0, 0.45)
703
+ input_size = st.selectbox("Taille d'entrée du modèle",
704
+ options=[320, 416, 512, 640, 960, 1280],
705
+ index=3)
706
+ processing_mode = st.radio("Mode de traitement",
707
+ options=["Image par image", "Traitement par lot"],
708
+ horizontal=True)
709
+ show_fps = st.checkbox("Afficher le FPS sur la vidéo", value=False)
710
+ auto_snapshot = st.checkbox("Téléchargement automatique des snapshots", value=False)
711
+ output_format = st.selectbox("Format de sortie des frames", options=["BGR", "RGB"], index=1)
712
+ snapshot_interval = st.number_input("Sauvegarder une frame toutes les X secondes",
713
+ min_value=1, max_value=60, value=5, step=1)
714
+
715
+ apply_filters = st.checkbox("Appliquer des filtres (grayscale + flou)", value=False)
716
+ advanced_filters = st.checkbox("Appliquer des filtres avancés (contraste + luminosité)", value=False)
717
+ rotation_angle = st.number_input("Angle de rotation", min_value=0, max_value=360, value=0, step=1)
718
+ resize_width = st.number_input("Largeur de redimensionnement", min_value=1, value=640, step=1)
719
+ resize_height = st.number_input("Hauteur de redimensionnement", min_value=1, value=480, step=1)
720
+
721
+ detection_zone = st.checkbox("Définir une zone de détection")
722
+ if detection_zone:
723
+ x = st.number_input("Zone X", min_value=0, value=0, step=1)
724
+ y = st.number_input("Zone Y", min_value=0, value=0, step=1)
725
+ w = st.number_input("Zone Largeur", min_value=1, value=640, step=1)
726
+ h = st.number_input("Zone Hauteur", min_value=1, value=480, step=1)
727
+ detection_zone = (x, y, w, h)
728
+ else:
729
+ detection_zone = None
730
+
731
+ notification_email = st.text_input("Email pour notifications")
732
+ save_to_cloud_flag = st.checkbox("Sauvegarder les résultats sur le cloud")
733
+
734
+ with st.expander("🧩 Options Avancées"):
735
+ morphological_ops = st.checkbox("Opérations morphologiques (opening/closing)")
736
+ equalize_hist = st.checkbox("Égaliser l'histogramme (améliorer contraste)")
737
+
738
+ custom_classes = st.text_input("Lister les classes (ID, séparés par des virgules) à détecter ou laisser vide")
739
+ if custom_classes.strip():
740
+ classes_to_detect = [int(c.strip()) for c in custom_classes.split(",") if c.strip().isdigit()]
741
+ else:
742
+ classes_to_detect = None
743
+
744
+ max_det = st.number_input("Max Detections autorisées", min_value=1, max_value=10000, value=1000, step=50)
745
+ line_width = st.slider("Épaisseur des bounding boxes", 1, 10, 2)
746
+ record_output = st.checkbox("Enregistrer les flux webcam en local (format MP4)")
747
+ agnostic_nms = st.checkbox("Agnostic NMS (ignorer les classes lors du NMS)")
748
+
749
+ with st.expander("🖍️ Post-traitement"):
750
+ export_type = st.selectbox("Exporter sous", ["PDF", "ZIP", "CSV", "JSON"])
751
+
752
+ with st.expander("☁️ Sauvegarde Cloud"):
753
+ cloud_service = st.selectbox("Choisir un service", ["Google Drive", "Dropbox", "OneDrive"])
754
+ cloud_file = st.file_uploader("📤 Sélectionner un fichier à sauvegarder",
755
+ type=["pdf", "zip", "csv", "jpg", "png", "json"])
756
+ if cloud_file:
757
+ if st.button(f"Sauvegarder sur {cloud_service}"):
758
+ save_to_cloud(cloud_file.read(), cloud_service)
759
+
760
+ # -------------------------------------------------------------------------
761
+ # 2) CHARGEMENT DU MODELE
762
+ # -------------------------------------------------------------------------
763
+ model = load_model(selected_model, custom_model_path)
764
+ class_names = model.names if hasattr(model, 'names') else []
765
+
766
+ # -------------------------------------------------------------------------
767
+ # 3) SECTION CENTRALE: CHOIX DE LA SOURCE
768
+ # -------------------------------------------------------------------------
769
+ st.subheader("Source : 🖼️ Image, 🎥 Vidéo, 📷 Webcam, 🌐 Caméra IP ou 🔄 Relecture")
770
+ input_type = st.radio("Source", ["Image", "Vidéo", "Webcam", "Caméra IP", "Relecture"], horizontal=True)
771
+
772
+ # ================== 1) IMAGE ==================
773
+ if input_type == "Image":
774
+ uploaded_images = st.file_uploader("📁 Choisir des images", type=["jpg", "png"], accept_multiple_files=True)
775
+ if uploaded_images:
776
+ output_images = []
777
+ csv_rows = []
778
+ for img_file in uploaded_images:
779
+ image = Image.open(img_file).convert("RGB")
780
+ st.image(image, caption=f"🖼️ {img_file.name}")
781
+
782
+ annotated_image, results = detect_objects(
783
+ model=model,
784
+ image=image,
785
+ model_type=task_type,
786
+ conf=conf,
787
+ iou=iou,
788
+ classes_to_detect=classes_to_detect,
789
+ max_det=max_det,
790
+ line_width=line_width,
791
+ agnostic_nms=agnostic_nms
792
+ )
793
+ st.image(annotated_image, caption="🖼️ Image annotée")
794
+
795
+ # Convert BGR->RGB PIL pour stockage
796
+ output_images.append(Image.fromarray(cv2.cvtColor(annotated_image, cv2.COLOR_BGR2RGB)))
797
+
798
+ counts = count_objects(results, task_type, class_names)
799
+ for cls_name, count_val in counts.items():
800
+ csv_rows.append({"Image": img_file.name, "Classe": cls_name, "Nombre": count_val})
801
+
802
+ show_table(counts)
803
+
804
+ # Export
805
+ if output_images:
806
+ if export_type == "PDF":
807
+ export_pdf(output_images)
808
+ elif export_type == "ZIP":
809
+ export_zip(output_images)
810
+ elif export_type == "CSV":
811
+ export_csv_rows(csv_rows)
812
+ elif export_type == "JSON":
813
+ json_data = json.dumps(csv_rows, indent=4)
814
+ st.download_button("📥 Télécharger JSON des détections",
815
+ data=json_data,
816
+ file_name="detections.json",
817
+ mime="application/json")
818
+ if save_to_cloud_flag:
819
+ st.info("Exemple: sauvegarde ZIP des images sur Cloud.")
820
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".zip") as tmpfile:
821
+ zip_path = tmpfile.name
822
+ with ZipFile(zip_path, 'w') as zipf:
823
+ for i, img in enumerate(output_images):
824
+ img_filename = f"cloud_image_{i}.png"
825
+ img.save(img_filename)
826
+ zipf.write(img_filename)
827
+ os.remove(img_filename)
828
+ with open(zip_path, "rb") as f:
829
+ zip_data = f.read()
830
+ save_to_cloud(zip_data, cloud_service)
831
+ os.remove(zip_path)
832
+
833
+ # ================== 2) VIDEO ==================
834
+ elif input_type == "Vidéo":
835
+ uploaded_video = st.file_uploader("📁 Choisir une vidéo", type=["mp4", "avi", "mov"], accept_multiple_files=False)
836
+ if uploaded_video is not None:
837
+ tfile = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4")
838
+ tfile.write(uploaded_video.read())
839
+ tfile.close()
840
+
841
+ process_video_file(
842
+ video_path=tfile.name,
843
+ model=model,
844
+ conf=conf,
845
+ iou=iou,
846
+ export_type=export_type,
847
+ classes_to_detect=classes_to_detect,
848
+ max_det=max_det,
849
+ line_width=line_width,
850
+ agnostic_nms=agnostic_nms
851
+ )
852
+ os.unlink(tfile.name)
853
+
854
+ # ================== 3) MULTI-WEBCAM ==================
855
+ elif input_type == "Webcam":
856
+ st.info("Recherche de toutes les webcams disponibles...")
857
+ available_devices = []
858
+ # Scanner 0..20 pour découvrir potentiellement plus de webcams
859
+ for index in range(21):
860
+ cap = cv2.VideoCapture(index)
861
+ if cap.isOpened():
862
+ ret, _ = cap.read()
863
+ if ret:
864
+ available_devices.append(index)
865
+ cap.release()
866
+
867
+ if len(available_devices) == 0:
868
+ st.error("🚫 Aucune webcam détectée ou accessible.")
869
+ else:
870
+ st.success(f"Webcams détectées : {available_devices}")
871
+ selected_devices = st.multiselect(
872
+ "Sélectionner les caméras à utiliser (max 4 affichées par rangée)",
873
+ options=available_devices,
874
+ default=available_devices[:1],
875
+ format_func=lambda x: f"Caméra {x}"
876
+ )
877
+ if selected_devices:
878
+ display_webcam_streams(
879
+ selected_devices=selected_devices,
880
+ model=model,
881
+ conf=conf,
882
+ iou=iou,
883
+ show_fps=show_fps,
884
+ auto_snapshot=auto_snapshot,
885
+ snapshot_interval=snapshot_interval,
886
+ output_format=output_format,
887
+ apply_filters=apply_filters,
888
+ advanced_filters=advanced_filters,
889
+ rotation_angle=rotation_angle,
890
+ resize_width=resize_width,
891
+ resize_height=resize_height,
892
+ detection_zone=detection_zone,
893
+ notification_email=notification_email,
894
+ save_to_cloud=save_to_cloud_flag,
895
+ morphological_ops=morphological_ops,
896
+ equalize_hist=equalize_hist,
897
+ classes_to_detect=classes_to_detect,
898
+ max_det=max_det,
899
+ line_width=line_width,
900
+ record_output=record_output,
901
+ agnostic_nms=agnostic_nms
902
+ )
903
+
904
+ # ================== 4) CAMERA IP ==================
905
+ elif input_type == "Caméra IP":
906
+ st.info("Activation de la caméra IP...")
907
+ ip_url = st.text_input("Entrez l'URL RTSP", value="rtsp://")
908
+ if st.button("Démarrer le streaming IP"):
909
+ display_ip_camera(
910
+ ip_url=ip_url,
911
+ model=model,
912
+ conf=conf,
913
+ iou=iou,
914
+ show_fps=show_fps,
915
+ auto_snapshot=auto_snapshot,
916
+ snapshot_interval=snapshot_interval,
917
+ output_format=output_format,
918
+ classes_to_detect=classes_to_detect,
919
+ max_det=max_det,
920
+ line_width=line_width,
921
+ agnostic_nms=agnostic_nms
922
+ )
923
+
924
+ # ================== 5) RELECTURE ==================
925
+ elif input_type == "Relecture":
926
+ st.info("Relecture de vidéos enregistrées")
927
+ recorded_video = st.file_uploader("📁 Charger une vidéo enregistrée",
928
+ type=["mp4", "avi", "mov"],
929
+ accept_multiple_files=False)
930
+ if recorded_video is not None:
931
+ st.video(recorded_video)
932
+
933
+ ###############################################################################
934
+ # LANCEMENT DU SCRIPT
935
+ ###############################################################################
936
+
937
+ if __name__ == "__main__":
938
+ main()