Celianv2 commited on
Commit
1c9c5b2
·
1 Parent(s): 8a4a092

commentaire et moins de fonctions inutiles

Browse files
Files changed (1) hide show
  1. jericho/main.py +58 -115
jericho/main.py CHANGED
@@ -1,15 +1,5 @@
1
- """
2
- face_analysis.py – compatible MediaPipe 0.10+ (nouvelle API Tasks)
3
- Audio preservé via FFmpeg en fin de traitement.
4
- ======================================================================
5
- Usage :
6
- python face_analysis.py --video chemin/video.mp4
7
- python face_analysis.py --video chemin/video.mp4 --output_dir resultats/
8
- """
9
-
10
  import argparse
11
  import csv
12
- import json
13
  import math
14
  import os
15
  import subprocess
@@ -19,17 +9,14 @@ import urllib.request
19
  import cv2
20
  import mediapipe as mp
21
  import numpy as np
22
- import matplotlib
23
- matplotlib.use("Agg")
24
- import matplotlib.pyplot as plt
25
 
26
  from mediapipe.tasks import python as mp_python
27
  from mediapipe.tasks.python import vision as mp_vision
28
  from mediapipe.tasks.python.vision import FaceLandmarkerOptions, FaceLandmarker
29
 
30
- # ──────────────────────────────────────────────
31
- # Modèle MediaPipe
32
- # ──────────────────────────────────────────────
33
  MODEL_URL = "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task"
34
  MODEL_PATH = "face_landmarker.task"
35
 
@@ -39,53 +26,52 @@ def ensure_model():
39
  urllib.request.urlretrieve(MODEL_URL, MODEL_PATH)
40
  print(" Modele telecharge.")
41
 
42
- # ──────────────────────────────────────────────
43
- # Indices landmarks
44
- # ──────────────────────────────────────────────
45
- LEFT_EYE = [362, 385, 387, 263, 373, 380]
46
  RIGHT_EYE = [33, 160, 158, 133, 153, 144]
47
  MOUTH_OUTER = [61, 291, 0, 17, 37, 267]
48
  MOUTH_L, MOUTH_R = 61, 291
49
  LEFT_PUP, RIGHT_PUP = 468, 473
50
 
51
- EAR_THRESHOLD = 0.20 # en dessous de ce seuil = œil considéré fermé
52
-
53
  HEAD_POSE_IDX = [1, 152, 226, 446, 57, 287]
54
- MODEL_3D = np.array([
55
  [ 0.0, 0.0, 0.0 ],
56
  [ 0.0, -63.6, -12.5],
57
  [-43.3, 32.7, -26.0],
58
  [ 43.3, 32.7, -26.0],
59
  [-28.9, -28.9, -24.1],
60
  [ 28.9, -28.9, -24.1],
61
- ], dtype=np.float64)
 
62
 
63
- # ──────────────────────────────────────────────
64
  # Calculs
65
- # ──────────────────────────────────────────────
66
 
67
- def _pts(lms, indices, w, h):
 
68
  return np.array([[lms[i].x * w, lms[i].y * h] for i in indices])
69
 
70
- def ear(lms, indices, w, h):
71
- p = _pts(lms, indices, w, h)
72
- A = np.linalg.norm(p[1]-p[5]); B = np.linalg.norm(p[2]-p[4])
73
  return (A+B) / (2*np.linalg.norm(p[0]-p[3])+1e-6)
74
 
75
- def mar(lms, indices, w, h):
76
  p = _pts(lms, indices, w, h)
77
  A = np.linalg.norm(p[1]-p[5]); B = np.linalg.norm(p[2]-p[4])
78
  return (A+B) / (2*np.linalg.norm(p[0]-p[3])+1e-6)
79
 
80
- def smile_index(lms, w, h):
81
- ml = np.array([lms[MOUTH_L].x*w, lms[MOUTH_L].y*h])
82
  mr = np.array([lms[MOUTH_R].x*w, lms[MOUTH_R].y*h])
83
- pl = np.array([lms[LEFT_PUP].x*w, lms[LEFT_PUP].y*h])
84
  pr = np.array([lms[RIGHT_PUP].x*w, lms[RIGHT_PUP].y*h])
85
- return np.linalg.norm(mr-ml) / (np.linalg.norm(pr-pl)+1e-6)
86
 
87
- def head_pose(lms, w, h):
88
- img_pts = np.array([[lms[i].x*w, lms[i].y*h] for i in HEAD_POSE_IDX], dtype=np.float64)
89
  cam = np.array([[w,0,w/2],[0,w,h/2],[0,0,1]], dtype=np.float64)
90
  dist = np.zeros((4,1))
91
  ok, rvec, _ = cv2.solvePnP(MODEL_3D, img_pts, cam, dist, flags=cv2.SOLVEPNP_ITERATIVE)
@@ -103,75 +89,43 @@ def head_pose(lms, w, h):
103
  yaw = 0.0
104
  return yaw, pitch, roll
105
 
106
- def face_bbox(lms, w, h):
107
- xs = [lm.x for lm in lms]; ys = [lm.y for lm in lms]
108
  xmn,xmx,ymn,ymx = min(xs),max(xs),min(ys),max(ys)
109
- return (xmn+xmx)/2,(ymn+ymx)/2,xmx-xmn,ymx-ymn,xmn,ymn,xmx,ymx
110
 
111
- # ──────────────────────────────────────────────
112
  # Annotation
113
- # ──────────────────────────────────────────────
114
 
115
- def annotate_frame(frame, lms, sig, w, h):
 
116
  x1,y1 = int(sig["bbox_xmin"]*w), int(sig["bbox_ymin"]*h)
117
  x2,y2 = int(sig["bbox_xmax"]*w), int(sig["bbox_ymax"]*h)
118
- cv2.rectangle(frame,(x1,y1),(x2,y2),(0,255,0),2)
119
- for idx in LEFT_EYE+RIGHT_EYE+MOUTH_OUTER:
120
  cv2.circle(frame,(int(lms[idx].x*w),int(lms[idx].y*h)),2,(255,0,255),-1)
121
  for idx in [LEFT_PUP, RIGHT_PUP]:
122
  cv2.circle(frame,(int(lms[idx].x*w),int(lms[idx].y*h)),3,(0,200,255),-1)
123
- lines = [
124
  f"Yaw:{sig['yaw']:+.1f} Pitch:{sig['pitch']:+.1f} Roll:{sig['roll']:+.1f}",
125
  f"EAR L:{sig['ear_left']:.3f} R:{sig['ear_right']:.3f}",
126
  f"MAR:{sig['mar']:.3f} Smile:{sig['smile']:.3f}",
127
  f"T:{sig['timestamp_s']:.2f}s",
128
  ]
129
- for i, line in enumerate(lines):
130
  cv2.putText(frame, line, (x1, max(y1-10-i*18, 14)),
131
  cv2.FONT_HERSHEY_SIMPLEX, 0.48, (0,255,0), 1, cv2.LINE_AA)
132
  nose = (int(lms[1].x*w), int(lms[1].y*h))
133
  tip = (int(nose[0]+60*math.sin(math.radians(sig["yaw"]))),
134
  int(nose[1]-60*math.sin(math.radians(sig["pitch"]))))
135
- cv2.arrowedLine(frame, nose, tip, (0,100,255), 2, tipLength=0.3)
136
  return frame
137
 
138
- # ──────────────────────────────────────────────
139
- # Tracés temporels
140
- # ──────────────────────────────────────────────
141
-
142
- def make_plots(records, path):
143
- ts = [r["timestamp_s"] for r in records]
144
- fig, axes = plt.subplots(4,1, figsize=(14,12), sharex=True)
145
- fig.suptitle("Signaux faciaux extraits", fontsize=14, fontweight="bold")
146
- axes[0].plot(ts,[r["yaw"] for r in records],label="Yaw")
147
- axes[0].plot(ts,[r["pitch"] for r in records],label="Pitch")
148
- axes[0].plot(ts,[r["roll"] for r in records],label="Roll")
149
- axes[0].set_ylabel("Orientation (deg)"); axes[0].legend(); axes[0].grid(alpha=.3)
150
- axes[1].plot(ts,[r["ear_left"] for r in records],label="EAR gauche")
151
- axes[1].plot(ts,[r["ear_right"] for r in records],label="EAR droite")
152
- axes[1].axhline(0.20,ls="--",color="gray",label="seuil ~0.20")
153
- axes[1].set_ylabel("EAR"); axes[1].legend(); axes[1].grid(alpha=.3)
154
- axes[2].plot(ts,[r["mar"] for r in records],label="MAR bouche")
155
- axes[2].plot(ts,[r["smile"] for r in records],label="Sourire")
156
- axes[2].set_ylabel("MAR / Sourire"); axes[2].legend(); axes[2].grid(alpha=.3)
157
- axes[3].plot(ts,[r["face_cx"] for r in records],label="Centre X")
158
- axes[3].plot(ts,[r["face_cy"] for r in records],label="Centre Y")
159
- axes[3].set_ylabel("Position (norm.)"); axes[3].set_xlabel("Temps (s)")
160
- axes[3].legend(); axes[3].grid(alpha=.3)
161
- plt.tight_layout(); plt.savefig(path, dpi=130); plt.close(fig)
162
- print(f" -> Traces : {path}")
163
-
164
- # ──────────────────────────────────────────────
165
  # Fusion audio via FFmpeg
166
- # ──────────────────────────────────────────────
167
-
168
- def merge_audio(original_video, silent_video, output_path):
169
- """
170
- Copie la piste audio de original_video dans silent_video.
171
- Nécessite que ffmpeg soit dans le PATH.
172
- Produit output_path.
173
- """
174
- # Vérifie si ffmpeg est disponible
175
  try:
176
  subprocess.run(
177
  ["ffmpeg", "-version"],
@@ -203,60 +157,60 @@ def merge_audio(original_video, silent_video, output_path):
203
  print(f" [ERREUR FFmpeg] {result.stderr[-300:]}")
204
  return False
205
 
206
- # ──────────────────────────────────────────────
207
  # Pipeline principal
208
- # ──────────────────────────────────────────────
209
 
210
  def process_video(video_path, output_dir):
211
  os.makedirs(output_dir, exist_ok=True)
212
- ensure_model()
213
 
214
- cap = cv2.VideoCapture(video_path)
215
  if not cap.isOpened():
216
  sys.exit(f"[ERREUR] Impossible d'ouvrir : {video_path}")
217
- fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
218
- W = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
219
  H = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
220
- total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
221
  print(f"Video : {W}x{H} {fps:.1f} fps {total} frames")
222
 
223
- base_opts = mp_python.BaseOptions(model_asset_path=MODEL_PATH)
224
  options = FaceLandmarkerOptions(
225
  base_options=base_opts,
226
  running_mode=mp_vision.RunningMode.VIDEO,
227
- num_faces=1,
228
  min_face_detection_confidence=0.5,
229
  min_face_presence_confidence=0.5,
230
  min_tracking_confidence=0.5,
231
  output_face_blendshapes=False,
232
  output_facial_transformation_matrixes=False,
233
  )
234
- landmarker = FaceLandmarker.create_from_options(options)
235
 
236
  # Fichier intermédiaire sans audio
237
  silent_path = os.path.join(output_dir, "_annotated_silent.mp4")
238
  final_path = os.path.join(output_dir, "output_annotated.mp4")
239
 
240
  writer = cv2.VideoWriter(
241
- silent_path, cv2.VideoWriter_fourcc(*"mp4v"), fps, (W, H)
242
  )
243
 
244
- records = []
245
- frame_idx = 0
246
 
247
  while True:
248
- ret, frame = cap.read()
249
  if not ret:
250
  break
251
 
252
- ts_ms = int(frame_idx * 1000 / fps)
253
- ts_s = frame_idx / fps
254
 
255
  rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
256
- mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb)
257
- result = landmarker.detect_for_video(mp_image, ts_ms)
258
 
259
- if result.face_landmarks:
260
  lms = result.face_landmarks[0]
261
  cx,cy,bw,bh,xmn,ymn,xmx,ymx = face_bbox(lms, W, H)
262
  yaw,pitch,roll = head_pose(lms, W, H)
@@ -290,7 +244,7 @@ def process_video(video_path, output_dir):
290
  pct = 100*frame_idx//total if total else 0
291
  print(f" Frame {frame_idx}/{total} ({pct}%)")
292
 
293
- cap.release(); writer.release(); landmarker.close()
294
  print(f" -> Video sans audio : {silent_path}")
295
 
296
  # ── Recoller l'audio avec FFmpeg ──────────────
@@ -313,17 +267,6 @@ def process_video(video_path, output_dir):
313
  wc.writeheader(); wc.writerows(records)
314
  print(f" -> CSV : {csv_path}")
315
 
316
- # ── JSON ─────────────────────────────────────
317
- json_path = os.path.join(output_dir, "signals.json")
318
- with open(json_path,"w",encoding="utf-8") as f:
319
- json.dump(records, f, indent=2, ensure_ascii=False)
320
- print(f" -> JSON : {json_path}")
321
-
322
- # ── Tracés ───────────────────────��───────────
323
- detected = [r for r in records if r.get("face_detected")]
324
- if detected:
325
- make_plots(detected, os.path.join(output_dir, "plots.png"))
326
-
327
  print("\n Traitement termine.")
328
  print(f" Fichiers dans : {os.path.abspath(output_dir)}/")
329
 
 
 
 
 
 
 
 
 
 
 
1
  import argparse
2
  import csv
 
3
  import math
4
  import os
5
  import subprocess
 
9
  import cv2
10
  import mediapipe as mp
11
  import numpy as np
 
 
 
12
 
13
  from mediapipe.tasks import python as mp_python
14
  from mediapipe.tasks.python import vision as mp_vision
15
  from mediapipe.tasks.python.vision import FaceLandmarkerOptions, FaceLandmarker
16
 
17
+
18
+ # MediaPipe
19
+
20
  MODEL_URL = "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task"
21
  MODEL_PATH = "face_landmarker.task"
22
 
 
26
  urllib.request.urlretrieve(MODEL_URL, MODEL_PATH)
27
  print(" Modele telecharge.")
28
 
29
+ # Indices visage
30
+
31
+ LEFT_EYE = [362, 385, 387, 263, 373, 380] # point qui contoure l oeil
 
32
  RIGHT_EYE = [33, 160, 158, 133, 153, 144]
33
  MOUTH_OUTER = [61, 291, 0, 17, 37, 267]
34
  MOUTH_L, MOUTH_R = 61, 291
35
  LEFT_PUP, RIGHT_PUP = 468, 473
36
 
37
+ EAR_THRESHOLD = 0.20 # en dessous de ce seuil l'oeil considéré fermé
38
+ #EAR = eye aspect ratio
39
  HEAD_POSE_IDX = [1, 152, 226, 446, 57, 287]
40
+ MODEL_3D = np.array([# genere un model 3D d'une tete d'humain
41
  [ 0.0, 0.0, 0.0 ],
42
  [ 0.0, -63.6, -12.5],
43
  [-43.3, 32.7, -26.0],
44
  [ 43.3, 32.7, -26.0],
45
  [-28.9, -28.9, -24.1],
46
  [ 28.9, -28.9, -24.1],
47
+ ], dtype=np.float64)#float 64 oblige a etre tres orecis
48
+
49
 
 
50
  # Calculs
 
51
 
52
+
53
+ def _pts(lms, indices, w, h): # convertit les coordonnées en pixel
54
  return np.array([[lms[i].x * w, lms[i].y * h] for i in indices])
55
 
56
+ def ear(lms, indices, w, h): # ca calule le degres d'ouverture de l'oeil
57
+ p = _pts(lms, indices, w, h)# recupere les pixels de l oeil
58
+ A = np.linalg.norm(p[1]-p[5]); B = np.linalg.norm(p[2]-p[4]) # distance verticale des paupiere
59
  return (A+B) / (2*np.linalg.norm(p[0]-p[3])+1e-6)
60
 
61
+ def mar(lms, indices, w, h):# calcul le degres d'ouverture de la bouche
62
  p = _pts(lms, indices, w, h)
63
  A = np.linalg.norm(p[1]-p[5]); B = np.linalg.norm(p[2]-p[4])
64
  return (A+B) / (2*np.linalg.norm(p[0]-p[3])+1e-6)
65
 
66
+ def smile_index(lms, w, h):# calcul a quel point la personne sourit
67
+ ml = np.array([lms[MOUTH_L].x*w, lms[MOUTH_L].y*h])# coordonne coin de bouche droit et gauche
68
  mr = np.array([lms[MOUTH_R].x*w, lms[MOUTH_R].y*h])
69
+ pl = np.array([lms[LEFT_PUP].x*w, lms[LEFT_PUP].y*h])#coordonne pupilles
70
  pr = np.array([lms[RIGHT_PUP].x*w, lms[RIGHT_PUP].y*h])
71
+ return np.linalg.norm(mr-ml) / (np.linalg.norm(pr-pl)+1e-6)# divise largeur bouche par largeur pupille car elle se dilate en souriant
72
 
73
+ def head_pose(lms, w, h): # fonction qui estime l'orientation de la tete
74
+ img_pts = np.array([[lms[i].x*w, lms[i].y*h] for i in HEAD_POSE_IDX], dtype=np.float64)# extrait les coordonnées des points de references
75
  cam = np.array([[w,0,w/2],[0,w,h/2],[0,0,1]], dtype=np.float64)
76
  dist = np.zeros((4,1))
77
  ok, rvec, _ = cv2.solvePnP(MODEL_3D, img_pts, cam, dist, flags=cv2.SOLVEPNP_ITERATIVE)
 
89
  yaw = 0.0
90
  return yaw, pitch, roll
91
 
92
+ def face_bbox(lms, w, h): # focntion pour calculer une boite qui enveloppe le visage
93
+ xs = [lm.x for lm in lms]; ys = [lm.y for lm in lms]# ca creer une liste de tout les X et Y du visage
94
  xmn,xmx,ymn,ymx = min(xs),max(xs),min(ys),max(ys)
95
+ return (xmn+xmx)/2,(ymn+ymx)/2,xmx-xmn,ymx-ymn,xmn,ymn,xmx,ymx # retourne tout les infos sur la boites
96
 
 
97
  # Annotation
 
98
 
99
+
100
+ def annotate_frame(frame, lms, sig, w, h): # fonction qui va dessiner sur l video
101
  x1,y1 = int(sig["bbox_xmin"]*w), int(sig["bbox_ymin"]*h)
102
  x2,y2 = int(sig["bbox_xmax"]*w), int(sig["bbox_ymax"]*h)
103
+ cv2.rectangle(frame,(x1,y1),(x2,y2),(0,255,0),2) # fait un carre vert autour du visage
104
+ for idx in LEFT_EYE+RIGHT_EYE+MOUTH_OUTER: # dessine des points sur les differents endroit du visage
105
  cv2.circle(frame,(int(lms[idx].x*w),int(lms[idx].y*h)),2,(255,0,255),-1)
106
  for idx in [LEFT_PUP, RIGHT_PUP]:
107
  cv2.circle(frame,(int(lms[idx].x*w),int(lms[idx].y*h)),3,(0,200,255),-1)
108
+ lines = [ # affiche les lignes texte qui contient les differents
109
  f"Yaw:{sig['yaw']:+.1f} Pitch:{sig['pitch']:+.1f} Roll:{sig['roll']:+.1f}",
110
  f"EAR L:{sig['ear_left']:.3f} R:{sig['ear_right']:.3f}",
111
  f"MAR:{sig['mar']:.3f} Smile:{sig['smile']:.3f}",
112
  f"T:{sig['timestamp_s']:.2f}s",
113
  ]
114
+ for i, line in enumerate(lines):# ecrit les ligne de texte au dessus de la boite
115
  cv2.putText(frame, line, (x1, max(y1-10-i*18, 14)),
116
  cv2.FONT_HERSHEY_SIMPLEX, 0.48, (0,255,0), 1, cv2.LINE_AA)
117
  nose = (int(lms[1].x*w), int(lms[1].y*h))
118
  tip = (int(nose[0]+60*math.sin(math.radians(sig["yaw"]))),
119
  int(nose[1]-60*math.sin(math.radians(sig["pitch"]))))
120
+ cv2.arrowedLine(frame, nose, tip, (0,100,255), 2, tipLength=0.3)# dessine feche partant du nez montrant orientation tete
121
  return frame
122
 
123
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  # Fusion audio via FFmpeg
125
+
126
+
127
+ def merge_audio(original_video, silent_video, output_path):# fonction qui reassemble le son de la video avec la video
128
+
 
 
 
 
 
129
  try:
130
  subprocess.run(
131
  ["ffmpeg", "-version"],
 
157
  print(f" [ERREUR FFmpeg] {result.stderr[-300:]}")
158
  return False
159
 
160
+
161
  # Pipeline principal
162
+
163
 
164
  def process_video(video_path, output_dir):
165
  os.makedirs(output_dir, exist_ok=True)
166
+ ensure_model() # telecharge le modele si il n'est pas deja present
167
 
168
+ cap = cv2.VideoCapture(video_path) # ouvre le fichier video pour le lire image par image
169
  if not cap.isOpened():
170
  sys.exit(f"[ERREUR] Impossible d'ouvrir : {video_path}")
171
+ fps = cap.get(cv2.CAP_PROP_FPS) or 30.0 # nombre d'image par seconde
172
+ W = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) # dimension de la video
173
  H = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
174
+ total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) # nombre total d'image dans la video
175
  print(f"Video : {W}x{H} {fps:.1f} fps {total} frames")
176
 
177
+ base_opts = mp_python.BaseOptions(model_asset_path=MODEL_PATH) # charge MediaPipe et configure les options du modele
178
  options = FaceLandmarkerOptions(
179
  base_options=base_opts,
180
  running_mode=mp_vision.RunningMode.VIDEO,
181
+ num_faces=1, # cherche qu'un seul visage
182
  min_face_detection_confidence=0.5,
183
  min_face_presence_confidence=0.5,
184
  min_tracking_confidence=0.5,
185
  output_face_blendshapes=False,
186
  output_facial_transformation_matrixes=False,
187
  )
188
+ landmarker = FaceLandmarker.create_from_options(options) # cree le detecteur de visage
189
 
190
  # Fichier intermédiaire sans audio
191
  silent_path = os.path.join(output_dir, "_annotated_silent.mp4")
192
  final_path = os.path.join(output_dir, "output_annotated.mp4")
193
 
194
  writer = cv2.VideoWriter(
195
+ silent_path, cv2.VideoWriter_fourcc(*"mp4v"), fps, (W, H) # ouvre un fichier de sortie pour ecrire les frames annotées sans audio
196
  )
197
 
198
+ records = [] # liste pour stocker les données extraites de chaque frame
199
+ frame_idx = 0 # index de la frame actuelle
200
 
201
  while True:
202
+ ret, frame = cap.read() # lit la frame suivante de la video
203
  if not ret:
204
  break
205
 
206
+ ts_ms = int(frame_idx * 1000 / fps) # calcule le timestamp en millisecondes pour la frame actuelle
207
+ ts_s = frame_idx / fps # calcule le timestamp en secondes pour la frame actuelle
208
 
209
  rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
210
+ mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb) # convertit la frame en format RGB et crée une image MediaPipe à partir de celle-ci
211
+ result = landmarker.detect_for_video(mp_image, ts_ms) # applique le modèle de détection de visage à l'image et obtient les résultats pour la frame actuelle
212
 
213
+ if result.face_landmarks: # si un visage est détecté, extraire les informations et les stocker dans un dictionnaire
214
  lms = result.face_landmarks[0]
215
  cx,cy,bw,bh,xmn,ymn,xmx,ymx = face_bbox(lms, W, H)
216
  yaw,pitch,roll = head_pose(lms, W, H)
 
244
  pct = 100*frame_idx//total if total else 0
245
  print(f" Frame {frame_idx}/{total} ({pct}%)")
246
 
247
+ cap.release(); writer.release(); landmarker.close() # libère les ressources utilisées pour la lecture de la vidéo, l'écriture de la vidéo annotée et le modèle de détection de visage
248
  print(f" -> Video sans audio : {silent_path}")
249
 
250
  # ── Recoller l'audio avec FFmpeg ──────────────
 
267
  wc.writeheader(); wc.writerows(records)
268
  print(f" -> CSV : {csv_path}")
269
 
 
 
 
 
 
 
 
 
 
 
 
270
  print("\n Traitement termine.")
271
  print(f" Fichiers dans : {os.path.abspath(output_dir)}/")
272