1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
import streamlit as st
import cv2
import numpy as np
import pandas as pd
import time
import dlib
import os
import glob
import base64
import face_recognition
import winsound
import threading
from scipy.spatial import distance as dist
from imutils import face_utils
from datetime import datetime
import matplotlib.pyplot as plt
import imutils
from PIL import Image
import io
# Configuration de la page Streamlit
st.set_page_config(
page_title="Système de Détection de Somnolence",
page_icon="😴",
layout="wide",
initial_sidebar_state="expanded"
)
# Styles CSS personnalisés
st.markdown("""
<style>
.main-title {
font-size: 2.5rem;
font-weight: bold;
color: #1E88E5;
text-align: center;
margin-bottom: 1rem;
}
.sub-title {
font-size: 1.5rem;
font-weight: bold;
color: #333;
margin-top: 1rem;
}
.alert-box {
padding: 1rem;
border-radius: 0.5rem;
margin: 1rem 0;
}
.alert-green {
background-color: rgba(76, 175, 80, 0.2);
border: 1px solid #4CAF50;
}
.alert-yellow {
background-color: rgba(255, 193, 7, 0.2);
border: 1px solid #FFC107;
}
.alert-red {
background-color: rgba(244, 67, 54, 0.2);
border: 1px solid #F44336;
}
.student-card {
border: 1px solid #ddd;
border-radius: 0.5rem;
padding: 0.5rem;
margin-bottom: 0.5rem;
transition: all 0.3s;
}
.student-card:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.student-alert {
border-left: 5px solid #4CAF50;
}
.student-drowsy {
border-left: 5px solid #F44336;
}
.metric-card {
background-color: #f8f9fa;
border-radius: 0.5rem;
padding: 1rem;
text-align: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.metric-value {
font-size: 2rem;
font-weight: bold;
}
.metric-label {
font-size: 0.9rem;
color: #666;
}
.stButton button {
width: 100%;
}
.table-container {
border: 1px solid #ddd;
border-radius: 0.5rem;
padding: 1rem;
background-color: white;
margin-top: 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.stDataFrame {
padding: 0 !important;
}
.download-btn {
margin-top: 0.5rem;
text-align: right;
}
</style>
""", unsafe_allow_html=True)
# Fonctions utilitaires
def sound_alarm(path):
"""Play an alarm sound using winsound"""
try:
winsound.PlaySound(path, winsound.SND_FILENAME)
except:
st.error("Impossible de jouer le son d'alarme")
def eye_aspect_ratio(eye):
# Compute the euclidean distances between the two sets of
# vertical eye landmarks (x, y)-coordinates
A = dist.euclidean(eye[1], eye[5])
B = dist.euclidean(eye[2], eye[4])
# Compute the euclidean distance between the horizontal
# eye landmark (x, y)-coordinates
C = dist.euclidean(eye[0], eye[3])
# Compute the eye aspect ratio
ear = (A + B) / (2.0 * C)
# Return the eye aspect ratio
return ear
def get_adaptive_eye_threshold(ear_values, default_thresh=0.25):
"""Calcule un seuil adaptatif basé sur l'historique des valeurs EAR"""
if not ear_values or len(ear_values) < 5:
return default_thresh
# Calculer un seuil personnalisé basé sur les valeurs récentes
mean_ear = np.mean(ear_values)
std_ear = np.std(ear_values)
# Le seuil est défini comme la moyenne moins 1.3 fois l'écart-type
# pour une détection plus sensible
adaptive_thresh = mean_ear - 1.3 * std_ear
# Limiter le seuil à des valeurs raisonnables
return max(0.2, min(adaptive_thresh, 0.3))
def load_known_faces(students_folder):
"""Charge les visages connus depuis le dossier des étudiants"""
st.sidebar.write("Chargement des visages connus...")
known_face_encodings = []
known_face_names = []
known_face_images = {} # Pour stocker les chemins d'images pour chaque étudiant
# Si le dossier n'existe pas, le créer
if not os.path.exists(students_folder):
os.makedirs(students_folder)
st.sidebar.warning(f"Dossier {students_folder} créé. Veuillez y ajouter des photos d'étudiants.")
return known_face_encodings, known_face_names, known_face_images
# Parcourir tous les fichiers image dans le dossier
image_files = glob.glob(os.path.join(students_folder, "*.jpg")) + \
glob.glob(os.path.join(students_folder, "*.jpeg")) + \
glob.glob(os.path.join(students_folder, "*.png"))
progress_bar = st.sidebar.progress(0)
for i, image_file in enumerate(image_files):
# Mettre à jour la barre de progression
progress = (i + 1) / len(image_files)
progress_bar.progress(progress)
# Extraire le nom à partir du nom de fichier
filename = os.path.basename(image_file)
student_name = os.path.splitext(filename)[0].replace("_", " ")
try:
# Charger l'image et encoder le visage
image = face_recognition.load_image_file(image_file)
face_encodings = face_recognition.face_encodings(image)
if face_encodings:
known_face_encodings.append(face_encodings[0])
known_face_names.append(student_name)
known_face_images[student_name] = image_file # Stocker le chemin d'image
st.sidebar.success(f"Étudiant chargé: {student_name}")
else:
st.sidebar.warning(f"Aucun visage détecté dans {image_file}")
except Exception as e:
st.sidebar.error(f"Erreur lors du traitement de {image_file}: {e}")
progress_bar.empty()
st.sidebar.success(f"{len(known_face_names)} visages chargés.")
return known_face_encodings, known_face_names, known_face_images
def get_image_base64(image_path):
"""Convertit une image en base64 pour l'affichage HTML"""
try:
with open(image_path, "rb") as img_file:
return base64.b64encode(img_file.read()).decode('utf-8')
except Exception as e:
st.error(f"Erreur de chargement d'image: {e}")
return None
def add_student_to_database(student_name, uploaded_file, students_folder):
"""Ajoute un nouvel étudiant à la base de données"""
try:
# Créer le dossier s'il n'existe pas
if not os.path.exists(students_folder):
os.makedirs(students_folder)
# Sauvegarder l'image téléchargée
filename = f"{student_name.replace(' ', '_')}.jpg"
output_path = os.path.join(students_folder, filename)
# Convertir le fichier téléchargé en image
image = Image.open(uploaded_file)
image.save(output_path)
# Charger et encoder l'image
face_image = face_recognition.load_image_file(output_path)
encodings = face_recognition.face_encodings(face_image)
if encodings:
st.session_state.known_face_encodings.append(encodings[0])
st.session_state.known_face_names.append(student_name)
st.session_state.known_face_images[student_name] = output_path
# Ajouter l'étudiant à notre structure de données
st.session_state.students_info[student_name] = StudentInfo(student_name, output_path)
return True
else:
st.warning(f"Aucun visage détecté dans l'image téléchargée")
# Supprimer le fichier s'il n'y a pas de visage
if os.path.exists(output_path):
os.remove(output_path)
return False
except Exception as e:
st.error(f"Erreur lors de l'ajout de l'étudiant: {e}")
return False
# Fonction pour générer un CSV des données des étudiants
def generate_csv_data():
data = []
for student_id, info in st.session_state.students_info.items():
# Calculer la durée de présence actualisée
attendance_duration = time.time() - info.first_seen
attendance_minutes = int(attendance_duration // 60)
attendance_seconds = int(attendance_duration % 60)
# Formater la durée de somnolence en minutes:secondes
drowsy_minutes = int(info.total_drowsy_time // 60)
drowsy_seconds = int(info.total_drowsy_time % 60)
# Ajouter les données de l'étudiant
data.append({
"Nom": info.name,
"État": "Somnolent" if info.is_drowsy else "Alerte",
"Durée_Somnolence_Secondes": info.total_drowsy_time,
"Durée_Somnolence": f"{drowsy_minutes:02d}:{drowsy_seconds:02d}",
"Épisodes_Somnolence": info.drowsy_episodes,
"Durée_Présence_Secondes": attendance_duration,
"Durée_Présence": f"{attendance_minutes:02d}:{attendance_seconds:02d}",
"EAR_Actuel": info.ear,
"Seuil_EAR": info.adaptive_threshold
})
return pd.DataFrame(data)
# Classe pour stocker les informations des étudiants
class StudentInfo:
def __init__(self, name, image_path=None):
self.name = name
self.image_path = image_path
self.is_drowsy = False
self.counter = 0
self.ear = 0
self.ear_history = [] # Pour stocker l'historique des valeurs EAR
self.adaptive_threshold = 0.25 # Seuil adaptatif personnalisé pour chaque étudiant
self.position = (0, 0)
self.last_seen = time.time()
self.color = (0, 255, 0)
self.total_drowsy_time = 0 # Temps total de somnolence en secondes
self.drowsy_start_time = None # Pour calculer la durée de chaque épisode de somnolence
self.drowsy_episodes = 0 # Nombre d'épisodes de somnolence
self.attendance_duration = 0 # Durée totale de présence
self.first_seen = time.time() # Première fois qu'on a vu l'étudiant
# Initialisation des variables de session
if 'start_time' not in st.session_state:
st.session_state.start_time = time.time()
st.session_state.students_info = {}
st.session_state.active_students = set()
st.session_state.drowsiness_log = []
st.session_state.known_face_encodings = []
st.session_state.known_face_names = []
st.session_state.known_face_images = {}
st.session_state.frame_count = 0
st.session_state.is_running = False
st.session_state.camera_index = 0
st.session_state.EYE_AR_THRESH = 0.25
st.session_state.EYE_AR_CONSEC_FRAMES = 8
st.session_state.SKIP_FRAMES = 1
st.session_state.FACE_TRACKING_TIMEOUT = 10
st.session_state.FACE_RECOGNITION_TOLERANCE = 0.6
st.session_state.alarm_path = "alarm.wav"
st.session_state.shape_predictor_path = "shape_predictor_68_face_landmarks.dat"
st.session_state.last_update_time = time.time()
# Clé unique pour le bouton de téléchargement
st.session_state.download_key = f"download-csv-{int(time.time())}"
# Titre principal
st.markdown('<div class="main-title">Système de Détection de Somnolence</div>', unsafe_allow_html=True)
# Barre latérale pour les paramètres
with st.sidebar:
st.markdown("### Paramètres")
# Sélection de la caméra
st.session_state.camera_index = st.number_input("Index de la caméra", min_value=0, value=st.session_state.camera_index)
# Paramètres de détection
st.session_state.SKIP_FRAMES = st.slider("Sauter des frames", 0, 5, st.session_state.SKIP_FRAMES, 1)
# Ajout d'un nouvel étudiant
st.markdown("### Ajouter un nouvel étudiant")
new_student_name = st.text_input("Nom de l'étudiant")
uploaded_file = st.file_uploader("Photo de l'étudiant", type=["jpg", "jpeg", "png"])
if st.button("Ajouter l'étudiant") and new_student_name and uploaded_file:
if add_student_to_database(new_student_name, uploaded_file, "./etudiants"):
st.success(f"Étudiant {new_student_name} ajouté avec succès!")
else:
st.error("Échec de l'ajout de l'étudiant")
# Bouton pour démarrer/arrêter la détection
if not st.session_state.is_running:
if st.button("Démarrer la détection"):
st.session_state.is_running = True
# Charger les visages connus si ce n'est pas déjà fait
if not st.session_state.known_face_encodings:
st.session_state.known_face_encodings, st.session_state.known_face_names, st.session_state.known_face_images = load_known_faces("./etudiants")
else:
if st.button("Arrêter la détection"):
st.session_state.is_running = False
# Disposition principale en colonnes
col1, col2 = st.columns([3, 2])
# Colonne 1: Affichage vidéo
with col1:
# Placeholder pour l'affichage vidéo
video_placeholder = st.empty()
# Colonne 2: Statistiques et tableau des étudiants
with col2:
# Métriques principales (une seule fois)
metrics_container = st.container()
# Graphiques
charts_container = st.container()
# Tableau des étudiants
st.markdown('<div class="sub-title">Tableau des étudiants</div>', unsafe_allow_html=True)
# Container pour le tableau avec bordure
table_container = st.container()
# Recommandations
recommendations = st.empty()
# Fonction pour mettre à jour les métriques et les graphiques
def update_metrics_and_charts():
# Calculer les statistiques
total_students = len(st.session_state.active_students)
drowsy_students = sum(1 for student_id in st.session_state.active_students
if student_id in st.session_state.students_info
and st.session_state.students_info[student_id].is_drowsy)
alert_students = total_students - drowsy_students
if total_students > 0:
drowsy_percentage = (drowsy_students / total_students) * 100
else:
drowsy_percentage = 0
# Mettre à jour les métriques principales
with metrics_container:
# Effacer le contenu précédent
metrics_container.empty()
# Afficher les métriques principales
col1, col2, col3 = metrics_container.columns(3)
with col1:
st.markdown(f"""
<div class="metric-card">
<div class="metric-value">{total_students}</div>
<div class="metric-label">Étudiants détectés</div>
</div>
""", unsafe_allow_html=True)
with col2:
st.markdown(f"""
<div class="metric-card">
<div class="metric-value" style="color: #4CAF50;">{alert_students}</div>
<div class="metric-label">Étudiants alertes</div>
</div>
""", unsafe_allow_html=True)
with col3:
st.markdown(f"""
<div class="metric-card">
<div class="metric-value" style="color: #F44336;">{drowsy_students}</div>
<div class="metric-label">Étudiants somnolents</div>
</div>
""", unsafe_allow_html=True)
# Mettre à jour les graphiques
with charts_container:
# Effacer le contenu précédent
charts_container.empty()
# Créer deux colonnes pour les graphiques
chart_col1, chart_col2 = charts_container.columns(2)
with chart_col1:
# Graphique camembert de l'état des étudiants
if total_students > 0:
fig, ax = plt.subplots(figsize=(4, 4))
labels = ['Alertes', 'Somnolents']
sizes = [alert_students, drowsy_students]
colors = ['#4CAF50', '#F44336']
ax.pie(sizes, labels=labels, colors=colors, autopct='%1.1f%%', startangle=90)
ax.axis('equal')
st.pyplot(fig)
else:
st.info("Aucun étudiant détecté")
with chart_col2:
# Graphique d'évolution de la somnolence
if st.session_state.drowsiness_log:
fig, ax = plt.subplots(figsize=(4, 4))
timestamps = [log["timestamp"] for log in st.session_state.drowsiness_log]
drowsy_counts = [log["drowsy_count"] for log in st.session_state.drowsiness_log]
ax.plot(timestamps, drowsy_counts, 'r-')
ax.set_title('Évolution de la somnolence')
ax.set_ylabel('Nb. étudiants somnolents')
plt.xticks(rotation=45)
plt.tight_layout()
st.pyplot(fig)
else:
st.info("Pas assez de données pour le graphique")
# Fonction pour mettre à jour le tableau des étudiants
def update_students_table():
with table_container:
# Effacer le contenu précédent
table_container.empty()
# Créer un DataFrame pour le tableau
data = []
for student_id, info in st.session_state.students_info.items():
# Calculer la durée de présence actualisée
attendance_duration = time.time() - info.first_seen
attendance_minutes = int(attendance_duration // 60)
attendance_seconds = int(attendance_duration % 60)
# Formater la durée de somnolence en minutes:secondes
drowsy_minutes = int(info.total_drowsy_time // 60)
drowsy_seconds = int(info.total_drowsy_time % 60)
# Ajouter les données de l'étudiant
data.append({
"Nom": info.name,
"État": "Somnolent" if info.is_drowsy else "Alerte",
"Somnolence": f"{drowsy_minutes:02d}:{drowsy_seconds:02d}",
"Épisodes": info.drowsy_episodes,
"Présence": f"{attendance_minutes:02d}:{attendance_seconds:02d}",
"EAR": f"{info.ear:.2f}",
"Seuil": f"{info.adaptive_threshold:.2f}",
"_is_drowsy": info.is_drowsy # Champ caché pour le style
})
if data:
# Créer un DataFrame
df = pd.DataFrame(data)
# Supprimer la colonne cachée pour l'affichage
display_df = df.drop(columns=["_is_drowsy"])
# Créer un conteneur avec bordure pour le tableau
st.markdown('<div class="table-container">', unsafe_allow_html=True)
# Afficher le tableau avec style
st.dataframe(
display_df,
hide_index=True,
use_container_width=True
)
# Ajouter un bouton de téléchargement CSV avec une clé fixe
csv_data = generate_csv_data()
csv = csv_data.to_csv(index=False).encode('utf-8-sig')
st.download_button(
label="Télécharger CSV",
data=csv,
file_name=f"detection_somnolence_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
mime="text/csv",
key=st.session_state.download_key
)
st.markdown('</div>', unsafe_allow_html=True)
else:
st.info("Aucun étudiant détecté")
# Fonction pour mettre à jour les recommandations
def update_recommendations():
# Calculer le pourcentage d'étudiants somnolents
total_students = len(st.session_state.active_students)
drowsy_students = sum(1 for student_id in st.session_state.active_students
if student_id in st.session_state.students_info
and st.session_state.students_info[student_id].is_drowsy)
if total_students > 0:
drowsy_percentage = (drowsy_students / total_students) * 100
else:
drowsy_percentage = 0
# Générer les recommandations en fonction du pourcentage
if drowsy_percentage > 50:
recommendations.markdown("""
<div class="alert-box alert-red">
<h3>⚠️ ALERTE: Niveau de somnolence élevé</h3>
<p>Plus de 50% des étudiants montrent des signes de somnolence.</p>
<p><strong>Recommandation:</strong> Faites une pause ou changez d'activité immédiatement.</p>
</div>
""", unsafe_allow_html=True)
elif drowsy_percentage > 30:
recommendations.markdown("""
<div class="alert-box alert-yellow">
<h3>⚠️ ATTENTION: Niveau de somnolence modéré</h3>
<p>Un nombre significatif d'étudiants montrent des signes de somnolence.</p>
<p><strong>Recommandation:</strong> Envisagez un exercice interactif ou une discussion de groupe.</p>
</div>
""", unsafe_allow_html=True)
elif drowsy_percentage > 0:
recommendations.markdown("""
<div class="alert-box alert-yellow">
<h3>⚠️ INFORMATION: Quelques étudiants somnolents</h3>
<p>Quelques étudiants montrent des signes de somnolence.</p>
<p><strong>Recommandation:</strong> Surveillez ces étudiants et envisagez de les impliquer davantage.</p>
</div>
""", unsafe_allow_html=True)
else:
recommendations.markdown("""
<div class="alert-box alert-green">
<h3>✅ EXCELLENT: Tous les étudiants sont alertes</h3>
<p>Aucun signe de somnolence détecté.</p>
<p><strong>Recommandation:</strong> Continuez avec le cours actuel.</p>
</div>
""", unsafe_allow_html=True)
# Fonction principale de détection
def run_detection():
# Initialiser le détecteur de visages et le prédicteur de points de repère
detector = dlib.get_frontal_face_detector()
try:
predictor = dlib.shape_predictor(st.session_state.shape_predictor_path)
except:
video_placeholder.error("Erreur: Impossible de charger le fichier shape_predictor_68_face_landmarks.dat")
st.session_state.is_running = False
return
# Obtenir les indices des points de repère pour les yeux
(lStart, lEnd) = face_utils.FACIAL_LANDMARKS_IDXS["left_eye"]
(rStart, rEnd) = face_utils.FACIAL_LANDMARKS_IDXS["right_eye"]
# Ouvrir la webcam
cap = cv2.VideoCapture(st.session_state.camera_index)
if not cap.isOpened():
video_placeholder.error(f"Impossible d'accéder à la webcam (index: {st.session_state.camera_index})")
st.session_state.is_running = False
return
# Boucle principale
while st.session_state.is_running:
ret, frame = cap.read()
if not ret:
video_placeholder.error("Erreur de lecture de la webcam")
break
st.session_state.frame_count += 1
current_time = time.time()
# Traiter une image sur N pour améliorer les performances
if st.session_state.frame_count % (st.session_state.SKIP_FRAMES + 1) != 0:
continue
# Redimensionner l'image pour de meilleures performances
frame = imutils.resize(frame, width=700)
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# Détecter les visages avec dlib (pour le ratio d'aspect des yeux)
rects = detector(gray, 0)
# Détecter les visages avec face_recognition (pour la reconnaissance d'identité)
face_locations = face_recognition.face_locations(rgb_frame)
face_encodings = face_recognition.face_encodings(rgb_frame, face_locations)
# Effacer l'ensemble des étudiants actifs pour cette frame
current_frame_students = set()
# Traiter chaque visage avec face_recognition
for (top, right, bottom, left), face_encoding in zip(face_locations, face_encodings):
# Essayer de reconnaître le visage
name = "Inconnu"
if st.session_state.known_face_encodings:
matches = face_recognition.compare_faces(
st.session_state.known_face_encodings,
face_encoding,
tolerance=st.session_state.FACE_RECOGNITION_TOLERANCE
)
face_distances = face_recognition.face_distance(st.session_state.known_face_encodings, face_encoding)
if len(face_distances) > 0:
best_match_index = np.argmin(face_distances)
if matches[best_match_index]:
name = st.session_state.known_face_names[best_match_index]
# Créer un ID unique pour ce visage (en utilisant le nom comme ID)
student_id = name
current_frame_students.add(student_id)
st.session_state.active_students.add(student_id)
# Initialiser les données de l'étudiant si c'est un nouveau visage
if student_id not in st.session_state.students_info:
image_path = st.session_state.known_face_images.get(student_id) if student_id in st.session_state.known_face_images else None
st.session_state.students_info[student_id] = StudentInfo(name, image_path)
# Mettre à jour le timestamp de dernière vue
st.session_state.students_info[student_id].last_seen = current_time
# Convertir les face_locations au format rectangle dlib pour la détection de points de repère
dlib_rect = dlib.rectangle(left, top, right, bottom)
# Obtenir les points de repère faciaux
shape = predictor(gray, dlib_rect)
shape = face_utils.shape_to_np(shape)
# Extraire les coordonnées des yeux
leftEye = shape[lStart:lEnd]
rightEye = shape[rStart:rEnd]
# Calculer l'EAR
leftEAR = eye_aspect_ratio(leftEye)
rightEAR = eye_aspect_ratio(rightEye)
ear = (leftEAR + rightEAR) / 2.0
# Stocker la valeur EAR
st.session_state.students_info[student_id].ear = ear
# Stocker la position du visage (centre de la boîte englobante)
st.session_state.students_info[student_id].position = ((left + right)//2, (top + bottom)//2)
# Dessiner les contours des yeux
leftEyeHull = cv2.convexHull(leftEye)
rightEyeHull = cv2.convexHull(rightEye)
cv2.drawContours(frame, [leftEyeHull], -1, (0, 255, 0), 1)
cv2.drawContours(frame, [rightEyeHull], -1, (0, 255, 0), 1)
# Mettre à jour l'historique EAR et calculer le seuil adaptatif
st.session_state.students_info[student_id].ear_history.append(ear)
# Garder seulement les 30 dernières valeurs pour l'historique
if len(st.session_state.students_info[student_id].ear_history) > 30:
st.session_state.students_info[student_id].ear_history.pop(0)
# Calculer le seuil adaptatif pour cet étudiant
st.session_state.students_info[student_id].adaptive_threshold = get_adaptive_eye_threshold(
st.session_state.students_info[student_id].ear_history,
default_thresh=st.session_state.EYE_AR_THRESH
)
# Détecter la somnolence avec un seuil personnalisé
if ear < st.session_state.students_info[student_id].adaptive_threshold:
st.session_state.students_info[student_id].counter += 1
# Réinitialiser le compteur si la valeur EAR est nettement supérieure au seuil
# pour éviter les faux positifs lors des clignements
if ear > st.session_state.students_info[student_id].adaptive_threshold * 1.5:
st.session_state.students_info[student_id].counter = max(0, st.session_state.students_info[student_id].counter - 2)
if st.session_state.students_info[student_id].counter >= st.session_state.EYE_AR_CONSEC_FRAMES:
if not st.session_state.students_info[student_id].is_drowsy:
st.session_state.students_info[student_id].is_drowsy = True
st.session_state.students_info[student_id].color = (0, 0, 255) # Rouge
st.session_state.students_info[student_id].drowsy_episodes += 1
st.session_state.students_info[student_id].drowsy_start_time = current_time
# Jouer l'alarme une seule fois lorsqu'un étudiant devient somnolent
if os.path.exists(st.session_state.alarm_path) and not any(
info.is_drowsy for sid, info in st.session_state.students_info.items() if sid != student_id
):
t = threading.Thread(target=sound_alarm, args=(st.session_state.alarm_path,))
t.daemon = True
t.start()
else:
# Réduire progressivement le compteur plus rapidement
st.session_state.students_info[student_id].counter = max(0, st.session_state.students_info[student_id].counter - 2)
# Si le compteur est suffisamment bas, marquer comme non somnolent
if st.session_state.students_info[student_id].counter < st.session_state.EYE_AR_CONSEC_FRAMES // 3:
# Si l'étudiant était somnolent et ne l'est plus, mettre à jour son temps total de somnolence
if st.session_state.students_info[student_id].is_drowsy:
drowsy_duration = current_time - st.session_state.students_info[student_id].drowsy_start_time
st.session_state.students_info[student_id].total_drowsy_time += drowsy_duration
st.session_state.students_info[student_id].drowsy_start_time = None
st.session_state.students_info[student_id].is_drowsy = False
st.session_state.students_info[student_id].color = (0, 255, 0) # Vert
# Dessiner un rectangle autour du visage avec la couleur appropriée
color = st.session_state.students_info[student_id].color
cv2.rectangle(frame, (left, top), (right, bottom), color, 2)
# Afficher le nom et la valeur EAR au-dessus de la tête
text = f"{name}: EAR={ear:.2f} | Seuil={st.session_state.students_info[student_id].adaptive_threshold:.2f}"
# Positionner le texte au-dessus de la tête (plutôt qu'en dessous)
cv2.putText(frame, text, (left, top - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
# Afficher le texte "SOMNOLENT" si nécessaire
if st.session_state.students_info[student_id].is_drowsy:
cv2.putText(frame, "SOMNOLENT", (left, bottom + 20), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
# Mettre à jour le tableau et les statistiques à intervalles réguliers
if current_time - st.session_state.last_update_time >= 0.5: # Mise à jour toutes les 0.5 secondes
# Mettre à jour les statistiques de somnolence
drowsy_count = sum(1 for sid in st.session_state.active_students if sid in st.session_state.students_info and st.session_state.students_info[sid].is_drowsy)
st.session_state.drowsiness_log.append({
"timestamp": datetime.now(),
"drowsy_count": drowsy_count,
"total_students": len(st.session_state.active_students)
})
# Limiter la taille du log
if len(st.session_state.drowsiness_log) > 120: # Garder seulement les 60 dernières secondes
st.session_state.drowsiness_log.pop(0)
# Mettre à jour les statistiques et le graphique
update_metrics_and_charts()
update_students_table()
update_recommendations()
st.session_state.last_update_time = current_time
# Mettre à jour la durée totale de somnolence pour les étudiants actuellement somnolents
for student_id in st.session_state.students_info:
if st.session_state.students_info[student_id].is_drowsy and st.session_state.students_info[student_id].drowsy_start_time is not None:
# Calculer la durée actuelle de l'épisode de somnolence
current_drowsy_duration = current_time - st.session_state.students_info[student_id].drowsy_start_time
# Mettre à jour temporairement pour l'affichage
temp_total = st.session_state.students_info[student_id].total_drowsy_time + current_drowsy_duration
st.session_state.students_info[student_id].total_drowsy_time = temp_total
# Réinitialiser le temps de début pour éviter le double comptage
st.session_state.students_info[student_id].drowsy_start_time = current_time
# Supprimer les étudiants qui n'ont pas été vus depuis plus de 10 secondes
inactive_students = []
for student_id in st.session_state.students_info:
if current_time - st.session_state.students_info[student_id].last_seen > st.session_state.FACE_TRACKING_TIMEOUT:
inactive_students.append(student_id)
for student_id in inactive_students:
if student_id in st.session_state.active_students:
st.session_state.active_students.remove(student_id)
# Afficher l'image avec tous les visages détectés et les informations
video_placeholder.image(frame, channels="BGR", use_container_width=True)
# Libérer les ressources
cap.release()
# Exécuter la détection si elle est activée
if st.session_state.is_running:
run_detection()
else:
# Afficher un message d'accueil lorsque l'application n'est pas en cours d'exécution
video_placeholder.markdown("""
<div style="display: flex; justify-content: center; align-items: center; height: 400px; background-color: #f0f2f6; border-radius: 10px;">
<div style="text-align: center;">
<h2 style="color: #1E88E5;">Bienvenue dans le Système de Détection de Somnolence</h2>
<p>Cliquez sur "Démarrer la détection" dans la barre latérale pour commencer.</p>
<p style="font-size: 5rem;">😴 👀 😃</p>
</div>
</div>
""", unsafe_allow_html=True)
# Afficher des exemples de statistiques
with metrics_container:
col1, col2, col3 = st.columns(3)
with col1:
st.markdown("""
<div class="metric-card">
<div class="metric-value">0</div>
<div class="metric-label">Étudiants détectés</div>
</div>
""", unsafe_allow_html=True)
with col2:
st.markdown("""
<div class="metric-card">
<div class="metric-value" style="color: #4CAF50;">0</div>
<div class="metric-label">Étudiants alertes</div>
</div>
""", unsafe_allow_html=True)
with col3:
st.markdown("""
<div class="metric-card">
<div class="metric-value" style="color: #F44336;">0</div>
<div class="metric-label">Étudiants somnolents</div>
</div>
""", unsafe_allow_html=True)
# Afficher un tableau vide
with table_container:
st.markdown('<div class="table-container">', unsafe_allow_html=True)
st.info("Aucune donnée disponible. Démarrez la détection pour voir les étudiants.")
st.markdown('</div>', unsafe_allow_html=True)
# Afficher des instructions
with st.expander("Instructions d'utilisation"):
st.markdown("""
### Comment utiliser ce système
1. **Préparation**:
- Assurez-vous que votre webcam est connectée et fonctionne correctement
- Vérifiez que le fichier `shape_predictor_68_face_landmarks.dat` est présent dans le répertoire
2. **Ajout d'étudiants**:
- Utilisez le formulaire dans la barre latérale pour ajouter des étudiants
- Chaque étudiant doit avoir une photo claire de son visage
3. **Configuration**:
- Ajustez les paramètres de détection selon vos besoins
- Le seuil EAR contrôle la sensibilité de la détection de somnolence
4. **Démarrage**:
- Cliquez sur "Démarrer la détection" pour commencer la surveillance
- Les statistiques et le tableau des étudiants seront mis à jour en temps réel
5. **Interprétation**:
- Suivez les recommandations affichées en bas à droite
- Le système vous alertera si un nombre significatif d'étudiants montre des signes de somnolence
""")
# Afficher les informations de session en bas de page (une seule fois)
session_duration = time.time() - st.session_state.start_time
hours = int(session_duration // 3600)
minutes = int((session_duration % 3600) // 60)
seconds = int(session_duration % 60)
st.markdown(f"""
<div style="text-align: center; margin-top: 20px; color: #666;">
Durée de session: {hours:02d}:{minutes:02d}:{seconds:02d} |
Dernière mise à jour: {datetime.now().strftime('%H:%M:%S')}
</div>
""", unsafe_allow_html=True)No Output
Run the code to generate an output.