Spaces:
Runtime error
Runtime error
| import argparse | |
| import sys | |
| import os | |
| from pathlib import Path | |
| from typing import Optional, Union | |
| import cv2 | |
| import mediapipe as mp | |
| import numpy as np | |
| import torch | |
| from mediapipe.tasks import python | |
| from mediapipe.tasks.python import vision as mp_vision | |
| from PIL import Image | |
| # --- GESTION DES CHEMINS (Correctif Docker) --- | |
| # Dans Docker, le fichier est dans /app/predict.py | |
| # parent = /app | |
| CURRENT_DIR = Path(__file__).resolve().parent | |
| # On définit ROOT avec sécurité pour Docker | |
| try: | |
| if os.path.exists("/app"): | |
| ROOT = Path("/app") | |
| else: | |
| ROOT = CURRENT_DIR.parents[1] # Remonte de oily/ vers skinmatch/ | |
| except IndexError: | |
| ROOT = CURRENT_DIR | |
| # Ajout au sys.path pour que Python trouve les modules 'oily' | |
| if str(CURRENT_DIR) not in sys.path: | |
| sys.path.insert(0, str(CURRENT_DIR)) | |
| # On ajuste les imports pour Docker (si oily est le dossier courant) | |
| try: | |
| from transforms import get_inference_transforms | |
| from mobilenetv2 import build_model, CLASS_NAMES, NUM_CLASSES | |
| except ImportError: | |
| from oily.transforms import get_inference_transforms | |
| from oily.mobilenetv2 import build_model, CLASS_NAMES, NUM_CLASSES | |
| # Chemin vers le modèle de détection de visage | |
| # Note : Assure-toi que ce fichier .tflite est bien copié dans ton image Docker ! | |
| BLAZEFACE_MODEL = CURRENT_DIR / "models" / "blaze_face_short_range.tflite" | |
| def load_model(model_path: str, device: Optional[str] = None) -> tuple[torch.nn.Module, dict]: | |
| resolved_device: str = device if device is not None else ( | |
| "cuda" if torch.cuda.is_available() else "cpu" | |
| ) | |
| # Ajout de weights_only=True par sécurité (bonne pratique PyTorch récente) | |
| checkpoint: dict = torch.load(model_path, map_location=resolved_device, weights_only=False) | |
| metadata: dict = checkpoint.get("metadata", {}) | |
| num_classes: int = int(metadata.get("num_classes", NUM_CLASSES)) | |
| # On reconstruit le modèle | |
| model = build_model(num_classes=num_classes, freeze_backbone=False) | |
| model.load_state_dict(checkpoint["model_state_dict"]) | |
| model = model.to(resolved_device) | |
| model.eval() | |
| print(f"Modèle chargé depuis : {model_path}") | |
| return model, metadata | |
| def preprocess_image(image_input: Union[str, Path, Image.Image], use_face_detection: bool = True) -> Image.Image: | |
| # Conversion en format OpenCV (RGB) | |
| if isinstance(image_input, (str, Path)): | |
| image_bgr = cv2.imread(str(image_input)) | |
| else: | |
| # Conversion PIL -> OpenCV RGB | |
| image_bgr = cv2.cvtColor(np.array(image_input), cv2.COLOR_RGB2BGR) | |
| if image_bgr is None: | |
| raise ValueError("Impossible de charger l'image.") | |
| h, w = image_bgr.shape[:2] | |
| # 2. Détection de visage (Fallback si le modèle .tflite manque) | |
| if use_face_detection and BLAZEFACE_MODEL.exists(): | |
| try: | |
| base_options = python.BaseOptions(model_asset_path=str(BLAZEFACE_MODEL)) | |
| options = mp_vision.FaceDetectorOptions( | |
| base_options=base_options, | |
| running_mode=mp_vision.RunningMode.IMAGE, | |
| ) | |
| with mp_vision.FaceDetector.create_from_options(options) as detector: | |
| # Conversion pour Mediapipe | |
| mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)) | |
| result = detector.detect(mp_image) | |
| if result.detections: | |
| bbox = result.detections[0].bounding_box | |
| margin = 0.2 | |
| x = max(0, int(bbox.origin_x - bbox.width * margin)) | |
| y = max(0, int(bbox.origin_y - bbox.height * margin)) | |
| w_crop = int(bbox.width * (1 + margin * 2)) | |
| h_crop = int(bbox.height * (1 + margin * 2)) | |
| crop = image_bgr[y:y+h_crop, x:x+w_crop] | |
| if crop.size > 0: | |
| image_bgr = crop | |
| except Exception as e: | |
| print(f"Face detection failed: {e}. Using original image.") | |
| # Resize final et retour en PIL RGB | |
| resized = cv2.resize(image_bgr, (224, 224), interpolation=cv2.INTER_CUBIC) | |
| return Image.fromarray(cv2.cvtColor(resized, cv2.COLOR_BGR2RGB)) | |
| def predict_image( | |
| model: torch.nn.Module, | |
| image_input: Union[str, Path, Image.Image], | |
| device: Optional[str] = None, | |
| use_face_detection: bool = True, | |
| class_names: Optional[list[str]] = None, | |
| ) -> dict: | |
| resolved_device: str = device if device is not None else str(next(model.parameters()).device) | |
| resolved_classes: list[str] = class_names if class_names is not None else CLASS_NAMES | |
| # 1. Prétraitement | |
| pil_image = preprocess_image(image_input, use_face_detection) | |
| # 2. Transforms (Normalisation ImageNet) | |
| transform = get_inference_transforms() | |
| tensor = transform(pil_image).unsqueeze(0).to(resolved_device) | |
| # 3. Inférence | |
| with torch.no_grad(): | |
| logits = model(tensor) | |
| probs = torch.softmax(logits, dim=1).squeeze(0) | |
| # 4. Formatage | |
| predicted_idx = int(probs.argmax().item()) | |
| confidence = float(probs[predicted_idx].item()) | |
| scores = { | |
| cls: round(float(probs[i].item()), 4) | |
| for i, cls in enumerate(resolved_classes) | |
| } | |
| return { | |
| "class": resolved_classes[predicted_idx], | |
| "confidence": round(confidence, 4), | |
| "scores": scores, | |
| } | |
| # --- Bloc main inchangé mais sécurisé --- | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--model", type=str, required=True) | |
| parser.add_argument("--image", type=str, required=True) | |
| args = parser.parse_args() | |
| model, meta = load_model(args.model) | |
| names = meta.get("class_names", meta.get("class_names", CLASS_NAMES)) | |
| res = predict_image(model, args.image, class_names=names) | |
| print(res) |