ADD INFERENCE ENDPOINTS for images and video
Browse files- .gitattributes +2 -0
- api.py +331 -9
- 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 |
-
|
79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
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 |
-
|
157 |
-
|
158 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
}
|
24 |
|
25 |
-
def
|
26 |
-
"""Test
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
|
28 |
-
|
29 |
-
|
30 |
-
print(f"Health check: {response.status_code} - {response.json()}")
|
31 |
|
32 |
-
|
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 |
-
|
52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
53 |
print(f"❌ Image non trouvée: {IMAGE_PATH}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
54 |
|
55 |
if __name__ == "__main__":
|
56 |
-
|
|
|
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()
|