2nzi commited on
Commit
fe10d2a
·
1 Parent(s): 5938d36

ADD INFERENCE ENDPOINTS for images and video

Browse files
Files changed (3) hide show
  1. .gitattributes +2 -0
  2. api.py +331 -9
  3. test-api.py +266 -13
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ models/SV_FT_TSWC_kp filter=lfs diff=lfs merge=lfs -text
37
+ models/SV_FT_TSWC_lines filter=lfs diff=lfs merge=lfs -text
api.py CHANGED
@@ -1,15 +1,27 @@
1
  from fastapi import FastAPI, HTTPException, UploadFile, File, Form
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from pydantic import BaseModel
4
- from typing import Dict, List, Any
5
  import json
6
  import tempfile
7
  import os
8
  from PIL import Image
9
  import numpy as np
 
 
 
 
 
 
10
 
11
  from get_camera_params import get_camera_parameters
12
 
 
 
 
 
 
 
13
  app = FastAPI(
14
  title="Football Vision Calibration API",
15
  description="API pour la calibration de caméras à partir de lignes de terrain de football",
@@ -25,6 +37,80 @@ app.add_middleware(
25
  allow_headers=["*"],
26
  )
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  # Modèles Pydantic pour la validation des données
29
  class Point(BaseModel):
30
  x: float
@@ -42,6 +128,19 @@ class CalibrationResponse(BaseModel):
42
  input_lines: Dict[str, List[Point]]
43
  message: str
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  @app.get("/")
46
  async def root():
47
  return {
@@ -49,6 +148,8 @@ async def root():
49
  "version": "1.0.0",
50
  "endpoints": {
51
  "/calibrate": "POST - Calibrer une caméra à partir d'une image et de lignes",
 
 
52
  "/health": "GET - Vérifier l'état de l'API"
53
  }
54
  }
@@ -74,9 +175,20 @@ async def calibrate_camera(
74
  Paramètres de calibration de la caméra et lignes d'entrée
75
  """
76
  try:
77
- # Validation du format d'image
78
- if not image.content_type.startswith('image/'):
79
- raise HTTPException(status_code=400, detail="Le fichier doit être une image")
 
 
 
 
 
 
 
 
 
 
 
80
 
81
  # Parse des données de lignes
82
  try:
@@ -114,7 +226,8 @@ async def calibrate_camera(
114
  validated_lines[line_name] = validated_points
115
 
116
  # Sauvegarde temporaire de l'image
117
- with tempfile.NamedTemporaryFile(delete=False, suffix=f".{image.filename.split('.')[-1]}") as temp_file:
 
118
  content = await image.read()
119
  temp_file.write(content)
120
  temp_image_path = temp_file.name
@@ -153,10 +266,219 @@ async def calibrate_camera(
153
  except Exception as e:
154
  raise HTTPException(status_code=500, detail=f"Erreur interne: {str(e)}")
155
 
156
- # if __name__ == "__main__":
157
- # import uvicorn
158
- # uvicorn.run(app, host="0.0.0.0", port=8000)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
- # Ajoutez ceci à la place :
161
  # Point d'entrée pour Vercel
162
  app_instance = app
 
1
  from fastapi import FastAPI, HTTPException, UploadFile, File, Form
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from pydantic import BaseModel
4
+ from typing import Dict, List, Any, Optional
5
  import json
6
  import tempfile
7
  import os
8
  from PIL import Image
9
  import numpy as np
10
+ import cv2
11
+ import torch
12
+ import torchvision.transforms as T
13
+ import torchvision.transforms.functional as f
14
+ import yaml
15
+ from tqdm import tqdm
16
 
17
  from get_camera_params import get_camera_parameters
18
 
19
+ # Imports pour l'inférence automatique
20
+ from model.cls_hrnet import get_cls_net
21
+ from model.cls_hrnet_l import get_cls_net as get_cls_net_l
22
+ from utils.utils_calib import FramebyFrameCalib
23
+ from utils.utils_heatmap import get_keypoints_from_heatmap_batch_maxpool, get_keypoints_from_heatmap_batch_maxpool_l, complete_keypoints, coords_to_dict
24
+
25
  app = FastAPI(
26
  title="Football Vision Calibration API",
27
  description="API pour la calibration de caméras à partir de lignes de terrain de football",
 
37
  allow_headers=["*"],
38
  )
39
 
40
+ # Paramètres par défaut pour l'inférence
41
+ WEIGHTS_KP = "models/SV_FT_TSWC_kp"
42
+ WEIGHTS_LINE = "models/SV_FT_TSWC_lines"
43
+ DEVICE = "cuda:0"
44
+ KP_THRESHOLD = 0.15
45
+ LINE_THRESHOLD = 0.15
46
+ PNL_REFINE = True
47
+ FRAME_STEP = 5
48
+
49
+ # Cache pour les modèles (éviter de les recharger à chaque requête)
50
+ _models_cache = None
51
+
52
+ def load_inference_models():
53
+ """Charge les modèles d'inférence (avec cache)"""
54
+ global _models_cache
55
+
56
+ if _models_cache is not None:
57
+ return _models_cache
58
+
59
+ device = torch.device(DEVICE if torch.cuda.is_available() else 'cpu')
60
+
61
+ # Charger les configurations
62
+ cfg = yaml.safe_load(open("config/hrnetv2_w48.yaml", 'r'))
63
+ cfg_l = yaml.safe_load(open("config/hrnetv2_w48_l.yaml", 'r'))
64
+
65
+ # Modèle keypoints
66
+ model = get_cls_net(cfg)
67
+ model.load_state_dict(torch.load(WEIGHTS_KP, map_location=device))
68
+ model.to(device)
69
+ model.eval()
70
+
71
+ # Modèle lignes
72
+ model_l = get_cls_net_l(cfg_l)
73
+ model_l.load_state_dict(torch.load(WEIGHTS_LINE, map_location=device))
74
+ model_l.to(device)
75
+ model_l.eval()
76
+
77
+ _models_cache = (model, model_l, device)
78
+ return _models_cache
79
+
80
+ def process_frame_inference(frame, model, model_l, device, frame_width, frame_height):
81
+ """Traite une frame et retourne les paramètres de caméra"""
82
+ transform = T.Resize((540, 960))
83
+
84
+ # Préparer la frame pour l'inférence
85
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
86
+ frame_pil = Image.fromarray(frame_rgb)
87
+ frame_tensor = f.to_tensor(frame_pil).float().unsqueeze(0)
88
+
89
+ if frame_tensor.size()[-1] != 960:
90
+ frame_tensor = transform(frame_tensor)
91
+
92
+ frame_tensor = frame_tensor.to(device)
93
+ b, c, h, w = frame_tensor.size()
94
+
95
+ # Inférence
96
+ with torch.no_grad():
97
+ heatmaps = model(frame_tensor)
98
+ heatmaps_l = model_l(frame_tensor)
99
+
100
+ # Extraire les keypoints et lignes
101
+ kp_coords = get_keypoints_from_heatmap_batch_maxpool(heatmaps[:,:-1,:,:])
102
+ line_coords = get_keypoints_from_heatmap_batch_maxpool_l(heatmaps_l[:,:-1,:,:])
103
+ kp_dict = coords_to_dict(kp_coords, threshold=KP_THRESHOLD)
104
+ lines_dict = coords_to_dict(line_coords, threshold=LINE_THRESHOLD)
105
+ kp_dict, lines_dict = complete_keypoints(kp_dict[0], lines_dict[0], w=w, h=h, normalize=True)
106
+
107
+ # Calibration
108
+ cam = FramebyFrameCalib(iwidth=frame_width, iheight=frame_height, denormalize=True)
109
+ cam.update(kp_dict, lines_dict)
110
+ final_params_dict = cam.heuristic_voting(refine_lines=PNL_REFINE)
111
+
112
+ return final_params_dict
113
+
114
  # Modèles Pydantic pour la validation des données
115
  class Point(BaseModel):
116
  x: float
 
128
  input_lines: Dict[str, List[Point]]
129
  message: str
130
 
131
+ class InferenceImageResponse(BaseModel):
132
+ status: str
133
+ camera_parameters: Optional[Dict[str, Any]]
134
+ image_info: Dict[str, Any]
135
+ message: str
136
+
137
+ class InferenceVideoResponse(BaseModel):
138
+ status: str
139
+ camera_parameters: List[Dict[str, Any]]
140
+ video_info: Dict[str, Any]
141
+ frames_processed: int
142
+ message: str
143
+
144
  @app.get("/")
145
  async def root():
146
  return {
 
148
  "version": "1.0.0",
149
  "endpoints": {
150
  "/calibrate": "POST - Calibrer une caméra à partir d'une image et de lignes",
151
+ "/inference/image": "POST - Extraire les paramètres de caméra d'une image automatiquement",
152
+ "/inference/video": "POST - Extraire les paramètres de caméra d'une vidéo automatiquement",
153
  "/health": "GET - Vérifier l'état de l'API"
154
  }
155
  }
 
175
  Paramètres de calibration de la caméra et lignes d'entrée
176
  """
177
  try:
178
+ # Validation du format d'image - version robuste
179
+ content_type = getattr(image, 'content_type', None) or ""
180
+ filename = getattr(image, 'filename', "") or ""
181
+
182
+ # Vérifier le type MIME ou l'extension du fichier
183
+ image_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif']
184
+ is_image_content = content_type.startswith('image/') if content_type else False
185
+ is_image_extension = any(filename.lower().endswith(ext) for ext in image_extensions)
186
+
187
+ if not is_image_content and not is_image_extension:
188
+ raise HTTPException(
189
+ status_code=400,
190
+ detail=f"Le fichier doit être une image. Type détecté: {content_type}, Fichier: {filename}"
191
+ )
192
 
193
  # Parse des données de lignes
194
  try:
 
226
  validated_lines[line_name] = validated_points
227
 
228
  # Sauvegarde temporaire de l'image
229
+ file_extension = os.path.splitext(filename)[1] if filename else '.jpg'
230
+ with tempfile.NamedTemporaryFile(delete=False, suffix=file_extension) as temp_file:
231
  content = await image.read()
232
  temp_file.write(content)
233
  temp_image_path = temp_file.name
 
266
  except Exception as e:
267
  raise HTTPException(status_code=500, detail=f"Erreur interne: {str(e)}")
268
 
269
+ @app.post("/inference/image", response_model=InferenceImageResponse)
270
+ async def inference_image(
271
+ image: UploadFile = File(..., description="Image du terrain de football"),
272
+ kp_threshold: float = Form(KP_THRESHOLD, description="Seuil pour les keypoints"),
273
+ line_threshold: float = Form(LINE_THRESHOLD, description="Seuil pour les lignes")
274
+ ):
275
+ """
276
+ Extraire automatiquement les paramètres de caméra à partir d'une image.
277
+
278
+ Args:
279
+ image: Image du terrain de football (formats: jpg, jpeg, png)
280
+ kp_threshold: Seuil pour la détection des keypoints (défaut: 0.15)
281
+ line_threshold: Seuil pour la détection des lignes (défaut: 0.15)
282
+
283
+ Returns:
284
+ Paramètres de calibration de la caméra extraits automatiquement
285
+ """
286
+ try:
287
+ # Validation du format d'image - version robuste
288
+ content_type = getattr(image, 'content_type', None) or ""
289
+ filename = getattr(image, 'filename', "") or ""
290
+
291
+ # Vérifier le type MIME ou l'extension du fichier
292
+ image_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif']
293
+ is_image_content = content_type.startswith('image/') if content_type else False
294
+ is_image_extension = any(filename.lower().endswith(ext) for ext in image_extensions)
295
+
296
+ if not is_image_content and not is_image_extension:
297
+ raise HTTPException(
298
+ status_code=400,
299
+ detail=f"Le fichier doit être une image. Type détecté: {content_type}, Fichier: {filename}"
300
+ )
301
+
302
+ # Sauvegarde temporaire de l'image
303
+ file_extension = os.path.splitext(filename)[1] if filename else '.jpg'
304
+ with tempfile.NamedTemporaryFile(delete=False, suffix=file_extension) as temp_file:
305
+ content = await image.read()
306
+ temp_file.write(content)
307
+ temp_image_path = temp_file.name
308
+
309
+ try:
310
+ # Charger les modèles
311
+ model, model_l, device = load_inference_models()
312
+
313
+ # Lire l'image
314
+ frame = cv2.imread(temp_image_path)
315
+ if frame is None:
316
+ raise HTTPException(status_code=400, detail="Impossible de lire l'image")
317
+
318
+ frame_height, frame_width = frame.shape[:2]
319
+
320
+ # Mettre à jour les seuils globaux
321
+ global KP_THRESHOLD, LINE_THRESHOLD
322
+ KP_THRESHOLD = kp_threshold
323
+ LINE_THRESHOLD = line_threshold
324
+
325
+ # Traitement
326
+ params = process_frame_inference(frame, model, model_l, device, frame_width, frame_height)
327
+ # Formatage de la réponse
328
+ response = InferenceImageResponse(
329
+ status="success" if params is not None else "failed",
330
+ camera_parameters=params,
331
+ image_info={
332
+ "filename": filename,
333
+ "width": frame_width,
334
+ "height": frame_height,
335
+ "kp_threshold": kp_threshold,
336
+ "line_threshold": line_threshold
337
+ },
338
+ message="Paramètres extraits avec succès" if params is not None else "Échec de l'extraction des paramètres"
339
+ )
340
+
341
+ return response
342
+
343
+ except Exception as e:
344
+ raise HTTPException(
345
+ status_code=500,
346
+ detail=f"Erreur lors de l'inférence: {str(e)} \n params:\n{params}"
347
+ )
348
+
349
+ finally:
350
+ # Nettoyage du fichier temporaire
351
+ if os.path.exists(temp_image_path):
352
+ os.unlink(temp_image_path)
353
+
354
+ except HTTPException:
355
+ raise
356
+ except Exception as e:
357
+ raise HTTPException(status_code=500, detail=f"Erreur interne: {str(e)}")
358
+
359
+ @app.post("/inference/video", response_model=InferenceVideoResponse)
360
+ async def inference_video(
361
+ video: UploadFile = File(..., description="Vidéo du terrain de football"),
362
+ kp_threshold: float = Form(KP_THRESHOLD, description="Seuil pour les keypoints"),
363
+ line_threshold: float = Form(LINE_THRESHOLD, description="Seuil pour les lignes"),
364
+ frame_step: int = Form(FRAME_STEP, description="Traiter 1 frame sur N")
365
+ ):
366
+ """
367
+ Extraire automatiquement les paramètres de caméra à partir d'une vidéo.
368
+
369
+ Args:
370
+ video: Vidéo du terrain de football (formats: mp4, avi, mov, etc.)
371
+ kp_threshold: Seuil pour la détection des keypoints (défaut: 0.15)
372
+ line_threshold: Seuil pour la détection des lignes (défaut: 0.15)
373
+ frame_step: Traiter 1 frame sur N pour accélérer le traitement (défaut: 5)
374
+
375
+ Returns:
376
+ Liste des paramètres de calibration de la caméra pour chaque frame traitée
377
+ """
378
+ try:
379
+ # Validation du format vidéo - version robuste
380
+ content_type = getattr(video, 'content_type', None) or ""
381
+ filename = getattr(video, 'filename', "") or ""
382
+
383
+ video_extensions = ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv']
384
+ is_video_content = content_type.startswith('video/') if content_type else False
385
+ is_video_extension = any(filename.lower().endswith(ext) for ext in video_extensions)
386
+
387
+ if not is_video_content and not is_video_extension:
388
+ raise HTTPException(
389
+ status_code=400,
390
+ detail=f"Le fichier doit être une vidéo. Type détecté: {content_type}, Fichier: {filename}"
391
+ )
392
+
393
+ # Sauvegarde temporaire de la vidéo
394
+ file_extension = os.path.splitext(filename)[1] if filename else '.mp4'
395
+ with tempfile.NamedTemporaryFile(delete=False, suffix=file_extension) as temp_file:
396
+ content = await video.read()
397
+ temp_file.write(content)
398
+ temp_video_path = temp_file.name
399
+
400
+ try:
401
+ # Charger les modèles
402
+ model, model_l, device = load_inference_models()
403
+
404
+ # Ouvrir la vidéo
405
+ cap = cv2.VideoCapture(temp_video_path)
406
+ if not cap.isOpened():
407
+ raise HTTPException(status_code=400, detail="Impossible d'ouvrir la vidéo")
408
+
409
+ frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
410
+ frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
411
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
412
+ fps = int(cap.get(cv2.CAP_PROP_FPS))
413
+
414
+ # Mettre à jour les seuils globaux
415
+ global KP_THRESHOLD, LINE_THRESHOLD
416
+ KP_THRESHOLD = kp_threshold
417
+ LINE_THRESHOLD = line_threshold
418
+
419
+ all_params = []
420
+ frame_count = 0
421
+ processed_count = 0
422
+
423
+ while cap.isOpened():
424
+ ret, frame = cap.read()
425
+ if not ret:
426
+ break
427
+
428
+ # Traiter seulement 1 frame sur frame_step
429
+ if frame_count % frame_step != 0:
430
+ frame_count += 1
431
+ continue
432
+
433
+ # Traitement
434
+ params = process_frame_inference(frame, model, model_l, device, frame_width, frame_height)
435
+
436
+ if params is not None:
437
+ params['frame_number'] = frame_count
438
+ params['timestamp_seconds'] = frame_count / fps
439
+ all_params.append(params)
440
+ processed_count += 1
441
+
442
+ frame_count += 1
443
+
444
+ cap.release()
445
+
446
+ # Formatage de la réponse
447
+ response = InferenceVideoResponse(
448
+ status="success" if all_params else "failed",
449
+ camera_parameters=all_params,
450
+ video_info={
451
+ "filename": filename,
452
+ "width": frame_width,
453
+ "height": frame_height,
454
+ "total_frames": total_frames,
455
+ "fps": fps,
456
+ "duration_seconds": total_frames / fps,
457
+ "kp_threshold": kp_threshold,
458
+ "line_threshold": line_threshold,
459
+ "frame_step": frame_step
460
+ },
461
+ frames_processed=processed_count,
462
+ message=f"Paramètres extraits de {processed_count} frames" if all_params else "Aucun paramètre extrait"
463
+ )
464
+
465
+ return response
466
+
467
+ except Exception as e:
468
+ raise HTTPException(
469
+ status_code=500,
470
+ detail=f"Erreur lors de l'inférence vidéo: {str(e)}"
471
+ )
472
+
473
+ finally:
474
+ # Nettoyage du fichier temporaire
475
+ if os.path.exists(temp_video_path):
476
+ os.unlink(temp_video_path)
477
+
478
+ except HTTPException:
479
+ raise
480
+ except Exception as e:
481
+ raise HTTPException(status_code=500, detail=f"Erreur interne: {str(e)}")
482
 
 
483
  # Point d'entrée pour Vercel
484
  app_instance = app
test-api.py CHANGED
@@ -5,6 +5,8 @@ from pathlib import Path
5
  # Configuration
6
  API_URL = "http://localhost:8000"
7
  IMAGE_PATH = "examples/input/cam3.jpg" # Adaptez selon votre structure
 
 
8
 
9
  # Données d'exemple (votre cam3_line_dict)
10
  cam3_line_dict = {
@@ -19,38 +21,289 @@ cam3_line_dict = {
19
  {"x": 349.8767728435256, "y": 500.9610345717304},
20
  {"x": 32.736572890025556, "y": 397.21988189225624}
21
  ],
22
- # ... ajoutez les autres lignes si nécessaire
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  }
24
 
25
- def test_api():
26
- """Test de l'API de calibration"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
- # Test du health check
29
- response = requests.get(f"{API_URL}/health")
30
- print(f"Health check: {response.status_code} - {response.json()}")
31
 
32
- # Test de calibration
33
- if Path(IMAGE_PATH).exists():
34
  with open(IMAGE_PATH, 'rb') as image_file:
35
- files = {'image': image_file}
36
  data = {'lines_data': json.dumps(cam3_line_dict)}
37
 
 
38
  response = requests.post(
39
  f"{API_URL}/calibrate",
40
  files=files,
41
  data=data
42
  )
43
 
 
 
44
  if response.status_code == 200:
45
  result = response.json()
46
  print("✅ Calibration réussie!")
47
  print("Paramètres de la caméra:")
48
  print(json.dumps(result['camera_parameters'], indent=2))
49
  else:
50
- print(f"❌ Erreur: {response.status_code}")
51
- print(response.json())
52
- else:
 
 
 
 
 
 
 
 
 
 
53
  print(f"❌ Image non trouvée: {IMAGE_PATH}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
  if __name__ == "__main__":
56
- test_api()
 
5
  # Configuration
6
  API_URL = "http://localhost:8000"
7
  IMAGE_PATH = "examples/input/cam3.jpg" # Adaptez selon votre structure
8
+ # IMAGE_PATH = "examples/input/FootDrone.jpg" # Adaptez selon votre structure
9
+ VIDEO_PATH = "examples/input/FootDrone.mp4" # Adaptez selon votre structure
10
 
11
  # Données d'exemple (votre cam3_line_dict)
12
  cam3_line_dict = {
 
21
  {"x": 349.8767728435256, "y": 500.9610345717304},
22
  {"x": 32.736572890025556, "y": 397.21988189225624}
23
  ],
24
+ "Big rect. right bottom": [
25
+ {"x": 32.736572890025556, "y": 397.21988189225624},
26
+ {"x": 0.3753980224568448, "y": 407.0286292126068}
27
+ ],
28
+ "Small rect. right top": [
29
+ {"x": 312.24913494809687, "y": 1075.6461846681693},
30
+ {"x": 426.66666666666663, "y": 999.9279904137233}
31
+ ],
32
+ "Small rect. right main": [
33
+ {"x": 426.66666666666663, "y": 999.9279904137233},
34
+ {"x": 0, "y": 769.079837198949}
35
+ ],
36
+ "Circle right": [
37
+ {"x": 828.6491513601493, "y": 668.8579000924583},
38
+ {"x": 821.7759602949911, "y": 612.2830792373484},
39
+ {"x": 782.8739995106773, "y": 564.5621490047902},
40
+ {"x": 722.6387053930304, "y": 529.3993583071158},
41
+ {"x": 623.5014504910696, "y": 503.02726528386006},
42
+ {"x": 494.24654853028534, "y": 492.980753655953},
43
+ {"x": 349.8767728435256, "y": 500.9610345717304}
44
+ ],
45
+ "Side line bottom": [
46
+ {"x": 2.0193824656299317, "y": 266.2605192109321},
47
+ {"x": 399.0443993689428, "y": 186.14824976426013},
48
+ {"x": 645.5533017804819, "y": 132.93313314748357},
49
+ {"x": 1001.1088573360372, "y": 53.39824942655338},
50
+ {"x": 1208.1676808654488, "y": 7.351737798646435}
51
+ ],
52
+ "Middle line": [
53
+ {"x": 645.5533017804819, "y": 132.93313314748357},
54
+ {"x": 1106.0585089650835, "y": 200.22939899146556},
55
+ {"x": 1580.7388158704541, "y": 269.8451725000601},
56
+ {"x": 1917.6527118636336, "y": 318.9857185061268}
57
+ ],
58
+ "Circle central": [
59
+ {"x": 1580.7388158704541, "y": 269.8451725000601},
60
+ {"x": 1580.7388158704541, "y": 269.8451725000601},
61
+ {"x": 1533.8366024891266, "y": 288.8643838246303},
62
+ {"x": 1441.810458698277, "y": 302.46903498742097},
63
+ {"x": 1316.3202626198458, "y": 304.5620582432349},
64
+ {"x": 1219.0653606590615, "y": 292.0039187083512},
65
+ {"x": 1135.4052299401073, "y": 274.2132210339326},
66
+ {"x": 1069.522876998931, "y": 237.5853140571884},
67
+ {"x": 1106.0585089650835, "y": 200.22939899146556},
68
+ {"x": 1139.5882364760548, "y": 189.4457791734675},
69
+ {"x": 1224.2941188289963, "y": 177.9341512664908},
70
+ {"x": 1314.2287593518718, "y": 174.79461638276985},
71
+ {"x": 1392.6601319008914, "y": 180.02717452230473},
72
+ {"x": 1465.8627462799764, "y": 190.49229080137454},
73
+ {"x": 1529.6535959531789, "y": 204.09694196416518},
74
+ {"x": 1581.9411776525253, "y": 230.2597326618396},
75
+ {"x": 1580.7388158704541, "y": 269.8451725000601}
76
+ ],
77
+ "Side line left": [
78
+ {"x": 1208.1676808654488, "y": 7.351737798646435},
79
+ {"x": 1401.9652021886754, "y": 20.565213248502545},
80
+ {"x": 1582.3573590514204, "y": 30.37625976013045},
81
+ {"x": 1679.416182580832, "y": 34.300678364781604},
82
+ {"x": 1824.5142217965183, "y": 41.23091697692868},
83
+ {"x": 1918.6318688553417, "y": 42.21202162809147}
84
+ ],
85
+ "Big rect. left bottom": [
86
+ {"x": 1401.9652021886754, "y": 20.565213248502545},
87
+ {"x": 1283.3377512082834, "y": 53.98527744204496}
88
+ ],
89
+ "Big rect. left main": [
90
+ {"x": 1283.3377512082834, "y": 53.98527744204496},
91
+ {"x": 1510.7887316004399, "y": 73.60737046530076},
92
+ {"x": 1808.8279472867146, "y": 94.21056813971936},
93
+ {"x": 1918.6318688553417, "y": 100.0971960466961}
94
+ ],
95
+ "Circle left": [
96
+ {"x": 1510.7887316004399, "y": 73.60737046530076},
97
+ {"x": 1548.0436335612244, "y": 86.36173093041702},
98
+ {"x": 1620.5926531690673, "y": 95.19167279088215},
99
+ {"x": 1681.3769668945574, "y": 97.15388209320773},
100
+ {"x": 1746.0828492474989, "y": 100.0971960466961},
101
+ {"x": 1808.8279472867146, "y": 94.21056813971936}
102
+ ],
103
+ "Small rect. left bottom": [
104
+ {"x": 1550.9848100318127, "y": 42.21202162809147},
105
+ {"x": 1582.3573590514204, "y": 30.37625976013045}
106
+ ],
107
+ "Small rect. left main": [
108
+ {"x": 1550.9848100318127, "y": 42.21202162809147},
109
+ {"x": 1918.418689198772, "y": 60.49417894940041}
110
+ ]
111
  }
112
 
113
+ def test_health():
114
+ """Test du health check"""
115
+ try:
116
+ response = requests.get(f"{API_URL}/health")
117
+ print(f"Health check: {response.status_code} - {response.json()}")
118
+ return response.status_code == 200
119
+ except requests.exceptions.ConnectionError:
120
+ print("❌ Impossible de se connecter à l'API. Vérifiez qu'elle est démarrée.")
121
+ return False
122
+ except Exception as e:
123
+ print(f"❌ Erreur health check: {e}")
124
+ return False
125
+
126
+ def test_calibration():
127
+ """Test de l'API de calibration avec lignes manuelles"""
128
+ if not Path(IMAGE_PATH).exists():
129
+ print(f"❌ Image non trouvée: {IMAGE_PATH}")
130
+ print(f" Chemin absolu: {Path(IMAGE_PATH).absolute()}")
131
+ return
132
 
133
+ print(f"📁 Test avec l'image: {IMAGE_PATH}")
134
+ print(f" Taille du fichier: {Path(IMAGE_PATH).stat().st_size} bytes")
 
135
 
136
+ try:
 
137
  with open(IMAGE_PATH, 'rb') as image_file:
138
+ files = {'image': (Path(IMAGE_PATH).name, image_file, 'image/jpeg')}
139
  data = {'lines_data': json.dumps(cam3_line_dict)}
140
 
141
+ print("🚀 Envoi de la requête de calibration...")
142
  response = requests.post(
143
  f"{API_URL}/calibrate",
144
  files=files,
145
  data=data
146
  )
147
 
148
+ print(f"📡 Réponse reçue: {response.status_code}")
149
+
150
  if response.status_code == 200:
151
  result = response.json()
152
  print("✅ Calibration réussie!")
153
  print("Paramètres de la caméra:")
154
  print(json.dumps(result['camera_parameters'], indent=2))
155
  else:
156
+ print(f"❌ Erreur calibration: {response.status_code}")
157
+ try:
158
+ error_detail = response.json()
159
+ print(f"Détail de l'erreur: {error_detail}")
160
+ except:
161
+ print(f"Réponse brute: {response.text}")
162
+
163
+ except Exception as e:
164
+ print(f"❌ Exception lors du test de calibration: {e}")
165
+
166
+ def test_inference_image():
167
+ """Test de l'inférence automatique sur image"""
168
+ if not Path(IMAGE_PATH).exists():
169
  print(f"❌ Image non trouvée: {IMAGE_PATH}")
170
+ print(f" Chemin absolu: {Path(IMAGE_PATH).absolute()}")
171
+ return
172
+
173
+ print(f"📁 Test inférence avec l'image: {IMAGE_PATH}")
174
+ print(f" Taille du fichier: {Path(IMAGE_PATH).stat().st_size} bytes")
175
+
176
+ try:
177
+ with open(IMAGE_PATH, 'rb') as image_file:
178
+ files = {'image': (Path(IMAGE_PATH).name, image_file, 'image/jpeg')}
179
+ data = {
180
+ 'kp_threshold': 0.15,
181
+ 'line_threshold': 0.15
182
+ }
183
+
184
+ print("🚀 Envoi de la requête d'inférence image...")
185
+ response = requests.post(
186
+ f"{API_URL}/inference/image",
187
+ files=files,
188
+ data=data
189
+ )
190
+
191
+ print(f"📡 Réponse reçue: {response.status_code}")
192
+ print(f"📡 Réponse reçue: {response.json()}")
193
+
194
+ if response.status_code == 200 and response.json()['status'] == 'success':
195
+ result = response.json()
196
+ print("✅ Inférence image réussie!")
197
+ print(f"Status: {result['status']}")
198
+ print(f"Image info: {result['image_info']}")
199
+ if result['camera_parameters']:
200
+ print("Paramètres de la caméra:")
201
+ cam_params = result['camera_parameters'].get('cam_params', {})
202
+ print(f" Position: {cam_params.get('position_meters', 'N/A')}")
203
+ print(f" Focale X: {cam_params.get('x_focal_length', 'N/A')}")
204
+ print(f" Focale Y: {cam_params.get('y_focal_length', 'N/A')}")
205
+ else:
206
+ print(f"❌ Erreur inférence image: {response.status_code}")
207
+ try:
208
+ error_detail = response.json()
209
+ print(f"Détail de l'erreur: {error_detail}")
210
+ except:
211
+ print(f"Réponse brute: {response.text}")
212
+
213
+ except Exception as e:
214
+ print(f"❌ Exception lors du test d'inférence image: {e}")
215
+
216
+ def test_inference_video():
217
+ """Test de l'inférence automatique sur vidéo"""
218
+ if not Path(VIDEO_PATH).exists():
219
+ print(f"❌ Vidéo non trouvée: {VIDEO_PATH}")
220
+ print(f" Chemin absolu: {Path(VIDEO_PATH).absolute()}")
221
+ return
222
+
223
+ print(f"📁 Test inférence avec la vidéo: {VIDEO_PATH}")
224
+ print(f" Taille du fichier: {Path(VIDEO_PATH).stat().st_size} bytes")
225
+ print("🎬 Test inférence vidéo (peut prendre du temps...)")
226
+
227
+ try:
228
+ with open(VIDEO_PATH, 'rb') as video_file:
229
+ files = {'video': (Path(VIDEO_PATH).name, video_file, 'video/mp4')}
230
+ data = {
231
+ 'kp_threshold': 0.15,
232
+ 'line_threshold': 0.15,
233
+ 'frame_step': 200 # Traiter 1 frame sur 10 pour le test
234
+ }
235
+
236
+ print("🚀 Envoi de la requête d'inférence vidéo...")
237
+ response = requests.post(
238
+ f"{API_URL}/inference/video",
239
+ files=files,
240
+ data=data
241
+ )
242
+
243
+ print(f"📡 Réponse reçue: {response.status_code}")
244
+
245
+ if response.status_code == 200:
246
+ result = response.json()
247
+ print("✅ Inférence vidéo réussie!")
248
+ print(f"Status: {result['status']}")
249
+ print(f"Frames traitées: {result['frames_processed']}")
250
+ print(f"Vidéo info: {result['video_info']}")
251
+
252
+ if result['camera_parameters']:
253
+ print(f"\n=== Exemples de paramètres ===")
254
+ for i, params in enumerate(result['camera_parameters'][:3]):
255
+ frame_num = params.get('frame_number', i)
256
+ timestamp = params.get('timestamp_seconds', 0)
257
+ print(f"Frame {frame_num} (t={timestamp:.2f}s):")
258
+ if 'cam_params' in params:
259
+ cam_params = params['cam_params']
260
+ print(f" Position: {cam_params.get('position_meters', 'N/A')}")
261
+ print(f" Focales: X={cam_params.get('x_focal_length', 'N/A')}, Y={cam_params.get('y_focal_length', 'N/A')}")
262
+
263
+ if len(result['camera_parameters']) > 3:
264
+ print(f"... et {len(result['camera_parameters']) - 3} autres frames")
265
+ else:
266
+ print(f"❌ Erreur inférence vidéo: {response.status_code}")
267
+ try:
268
+ error_detail = response.json()
269
+ print(f"Détail de l'erreur: {error_detail}")
270
+ except:
271
+ print(f"Réponse brute: {response.text}")
272
+
273
+ except Exception as e:
274
+ print(f"❌ Exception lors du test d'inférence vidéo: {e}")
275
+
276
+ def test_all():
277
+ """Lance tous les tests"""
278
+ print("=== TEST DE L'API FOOTBALL VISION ===\n")
279
+
280
+ # Test 1: Health check
281
+ # print("1. Test Health Check")
282
+ # if not test_health():
283
+ # print("❌ API non accessible, arrêt des tests")
284
+ # return
285
+ # print()
286
+
287
+ # # Vérifier les chemins des fichiers
288
+ # print("2. Vérification des fichiers")
289
+ # print(f" Image: {'✅' if Path(IMAGE_PATH).exists() else '❌'} {IMAGE_PATH}")
290
+ # print(f" Vidéo: {'✅' if Path(VIDEO_PATH).exists() else '❌'} {VIDEO_PATH}")
291
+ # print()
292
+
293
+ # # Test 2: Calibration avec lignes manuelles
294
+ # print("3. Test Calibration (lignes manuelles)")
295
+ # test_calibration()
296
+ # print()
297
+
298
+ # Test 3: Inférence image
299
+ print("4. Test Inférence Image (automatique)")
300
+ test_inference_image()
301
+ print()
302
+
303
+ # # Test 4: Inférence vidéo
304
+ # print("5. Test Inférence Vidéo (automatique)")
305
+ # test_inference_video()
306
+ # print()
307
 
308
  if __name__ == "__main__":
309
+ test_all()