import os import json import tempfile import numpy as np import torch import matplotlib.pyplot as plt import segmentation_models_pytorch as smp import mlflow import re import ast import time # Visualisation import matplotlib.patches as mpatches from matplotlib.colors import ListedColormap # Preprocessing/Images import albumentations as A from albumentations.pytorch import ToTensorV2 def load_cityscapes_config(config_path="../cityscapes_config.json", verbose=True): """ Charge la configuration Cityscapes depuis un fichier JSON. INCHANGÉ - Compatible PyTorch """ with open(config_path, "r", encoding="utf-8") as f: config = json.load(f) class_groups = config["class_groups"] cityscapes_mapping = {int(k): v for k, v in config["cityscapes_mapping"].items()} id_to_label = {int(k): v for k, v in config["id_to_label"].items()} group_colors = np.array(config["group_colors"], dtype=np.uint8) group_names = config["group_names"] id_to_group = np.zeros(256, dtype=np.uint8) for id_, group_id in cityscapes_mapping.items(): id_to_group[id_] = group_id num_classes = len(group_names) if verbose: print("== Groupes de classes Cityscapes ==") for group, classes in class_groups.items(): print(f" - {group} : {classes}") print("\n== Mapping ID Cityscapes → Groupe ==") for id_ in sorted(cityscapes_mapping.keys()): label = id_to_label.get(id_, "Label inconnu") group_id = cityscapes_mapping[id_] print(f" ID {id_:2d} ('{label}') → groupe '{group_names[group_id]}' ({group_id})") print("\n== Liste des groupes et couleurs associées ==") for i, (name, color) in enumerate(zip(group_names, group_colors)): print(f" Groupe {i}: {name} - Couleur RGB : {color}") print(f"\nNombre total de groupes/classes : {num_classes}") return { "class_groups": class_groups, "cityscapes_mapping": cityscapes_mapping, "id_to_label": id_to_label, "group_colors": group_colors, "group_names": group_names, "id_to_group": id_to_group, "num_classes": num_classes } def map_mask_ids(mask, id_to_group_mapping): """ Applique le mapping des IDs Cityscapes vers les groupes ADAPTÉ PYTORCH : Utilise torch au lieu de tf """ if isinstance(mask, np.ndarray): mask = torch.from_numpy(mask) if isinstance(id_to_group_mapping, np.ndarray): if len(id_to_group_mapping) != 256: raise ValueError(f"id_to_group_mapping doit avoir 256 éléments, reçu: {len(id_to_group_mapping)}") mapping_tensor = torch.tensor(id_to_group_mapping, dtype=torch.long) else: mapping_tensor = torch.tensor(list(id_to_group_mapping), dtype=torch.long) # S'assurer que les valeurs du masque sont dans la plage valide [0, 255] mask_clipped = torch.clamp(mask, 0, 255) mapped_mask = mapping_tensor[mask_clipped.long()] return mapped_mask def colorize_mask(mask, group_colors): """ Applique la colorisation du masque avec les couleurs définies INCHANGÉ - Compatible PyTorch (prend numpy en entrée) """ if isinstance(mask, torch.Tensor): mask = mask.numpy() colored_mask = np.zeros((mask.shape[0], mask.shape[1], 3), dtype=np.uint8) num_classes = len(group_colors) for group_id in range(num_classes): colored_mask[mask == group_id] = group_colors[group_id] return colored_mask def get_preprocessing_fn(img_size=(512, 512), encoder=None): """ Retourne une fonction qui prétraite à la fois les images et les masques pour l'inférence, en reproduisant exactement le pipeline CityscapesDataset. """ if encoder is None: mean, std = [0.0, 0.0, 0.0], [1.0, 1.0, 1.0] else: mean, std = [0.485, 0.456, 0.406], [0.229, 0.224, 0.225] # Pipeline unifié (gérera image ET masque ensemble) transform = A.Compose([ A.Resize(height=img_size[0], width=img_size[1]), A.Normalize(mean=mean, std=std), ToTensorV2() ]) def preprocess(image_np, mask_np=None): if mask_np is not None: # Traitement simultané image + masque transformed = transform(image=image_np, mask=mask_np) return transformed['image'], transformed['mask'].long() else: # Image seule transformed = transform(image=image_np) return transformed['image'], None return preprocess # Version simplifiée de preprocess_image_and_mask utilisant la nouvelle get_preprocessing_fn def preprocess_image_and_mask(image_path, mask_path=None, img_size=(512, 512), encoder_name=None, id_to_group_mapping=None): """ Version simplifiée utilisant get_preprocessing_fn pour la cohérence avec CityscapesDataset """ import numpy as np from PIL import Image import torch print(f"🔄 Préprocessing de l'image: {os.path.basename(image_path)}") # Chargement des données image_pil = Image.open(image_path).convert('RGB') image_np = np.array(image_pil) img_display = np.array(image_pil.resize(img_size, Image.BILINEAR)) # Pour affichage mask_np = None if mask_path and os.path.exists(mask_path): print(f"🔄 Préprocessing du masque: {os.path.basename(mask_path)}") mask_pil = Image.open(mask_path).convert('L') mask_np = np.array(mask_pil) print(f" Valeurs uniques avant mapping: {np.unique(mask_np)[:10]}...") # Utiliser notre preprocessing unifié preprocess_fn = get_preprocessing_fn(img_size=img_size, encoder=encoder_name) img_preprocessed, mask_preprocessed = preprocess_fn(image_np, mask_np) # Appliquer le mapping si fourni ET si on a un masque if mask_preprocessed is not None and id_to_group_mapping is not None: mask_preprocessed = map_mask_ids(mask_preprocessed, id_to_group_mapping) print(f" Valeurs uniques après mapping: {torch.unique(mask_preprocessed)}") return img_preprocessed, img_display, mask_preprocessed # Version mise à jour de run_inference_and_visualize def run_inference_and_visualize(image_path, model, encoder_name, img_size, mapping_config, mask_path=None, save_dir=None, show_stats=True, show_plot=True): """ Version simplifiée utilisant preprocess_image_and_mask """ print(f"=== INFÉRENCE PYTORCH SUR {os.path.basename(image_path)} ===") # Utiliser la version simplifiée du preprocessing img_preprocessed, img_display, mask_gt = preprocess_image_and_mask( image_path=image_path, mask_path=mask_path, img_size=img_size or (512, 512), encoder_name=encoder_name, id_to_group_mapping=mapping_config.get('id_to_group') ) print("🔮 Exécution de l'inférence PyTorch...") model.eval() device = next(model.parameters()).device with torch.no_grad(): # Ajouter dimension batch img_batch = img_preprocessed.unsqueeze(0).to(device) # [1, 3, H, W] # ⏱️ MESURE DU TEMPS D'INFÉRENCE start_time = time.time() predictions = model(img_batch) # [1, num_classes, H, W] end_time = time.time() inference_time = end_time - start_time predicted_mask = torch.argmax(predictions[0], dim=0).cpu().numpy() # [H, W] print(f"✅ Inférence terminée - Shape: {predicted_mask.shape}") print(f"⏱️ Temps d'inférence: {inference_time*1000:.1f} ms ({inference_time:.4f} s)") # [Le reste du code de visualisation reste identique] # Visualisation num_plots = 2 if mask_gt is None else 4 fig, axes = plt.subplots(1, num_plots, figsize=(5*num_plots, 5)) if num_plots == 1: axes = [axes] axes[0].imshow(img_display) axes[0].set_title("Image Originale") axes[0].axis('off') predicted_colored = colorize_mask(predicted_mask, mapping_config['group_colors']) axes[1].imshow(predicted_colored) axes[1].set_title("Masque Prédit") axes[1].axis('off') if mask_gt is not None: mask_gt_np = mask_gt.numpy() if isinstance(mask_gt, torch.Tensor) else mask_gt gt_colored = colorize_mask(mask_gt_np, mapping_config['group_colors']) axes[2].imshow(gt_colored) axes[2].set_title("Masque de Vérité") axes[2].axis('off') # Visualisation des erreurs diff_mask = (mask_gt_np != predicted_mask).astype(float) axes[3].imshow(img_display) axes[3].imshow(diff_mask, alpha=0.5, cmap='Reds') axes[3].set_title("Erreurs (en rouge)") axes[3].axis('off') colors_normalized = mapping_config['group_colors'] / 255.0 custom_cmap = ListedColormap(colors_normalized) legend_elements = [] for i, (name, color) in enumerate(zip(mapping_config['group_names'], colors_normalized)): legend_elements.append(mpatches.Patch(color=color, label=f'{i}: {name}')) fig.legend(handles=legend_elements, loc='center left', bbox_to_anchor=(1.01, 0.5), fontsize=9, frameon=True, fancybox=True, shadow=True) plt.tight_layout() fig.subplots_adjust(right=0.95) if save_dir: os.makedirs(save_dir, exist_ok=True) img_name = os.path.splitext(os.path.basename(image_path))[0] fig_path = os.path.join(save_dir, f'{img_name}_inference.png') fig.savefig(fig_path, dpi=150, bbox_inches='tight') print(f"💾 Résultat sauvegardé: {fig_path}") # Statistiques if show_stats: print("\n📊 Statistiques du masque prédit:") unique_classes, counts = np.unique(predicted_mask, return_counts=True) for class_id, count in zip(unique_classes, counts): if class_id < len(mapping_config['group_names']): percentage = (count / predicted_mask.size) * 100 print(f" {mapping_config['group_names'][class_id]}: {count} pixels ({percentage:.1f}%)") if mask_gt is not None: mask_gt_np = mask_gt.numpy() if isinstance(mask_gt, torch.Tensor) else mask_gt print("\n📊 Statistiques du masque de vérité:") unique_classes, counts = np.unique(mask_gt_np, return_counts=True) for class_id, count in zip(unique_classes, counts): if class_id < len(mapping_config['group_names']): percentage = (count / mask_gt_np.size) * 100 print(f" {mapping_config['group_names'][class_id]}: {count} pixels ({percentage:.1f}%)") if show_plot: plt.show() return predicted_mask, inference_time else: return predicted_mask, inference_time, fig def load_model(run_id=None, experiment_name="OC Projet 9", metric="test_mean_iou", top_n=1): """ Charge un modèle PyTorch depuis MLflow avec plusieurs méthodes de sélection Args: run_id (str, optional): Run ID spécifique (prioritaire si fourni) experiment_name (str): Nom de l'expérience MLflow metric (str): Métrique pour classer les runs top_n (int): Position dans le classement (1=meilleur, 2=deuxième, etc.) Returns: tuple: (model, source_run_id, encoder_name, script_img_size) """ print(f"\n🔍 Chargement d'un modèle PyTorch depuis MLflow...") client = mlflow.tracking.MlflowClient() # 1. Sélection du run selon les paramètres if run_id: print(f"🎯 Mode run_id spécifique: {run_id}") try: run = mlflow.get_run(run_id) source_run_id = run_id print(f"✅ Run trouvé: {run.data.tags.get('mlflow.runName', 'Unknown')}") except Exception as e: raise RuntimeError(f"❌ Run {run_id} introuvable: {e}") else: print(f"🏆 Mode classement: top_{top_n} de '{metric}' dans '{experiment_name}'") try: # Récupérer l'expérience experiment = client.get_experiment_by_name(experiment_name) if experiment is None: raise RuntimeError(f"❌ Expérience '{experiment_name}' non trouvée") # Rechercher les runs avec la métrique, triés par ordre décroissant filter_string = f"status = 'FINISHED' and metrics.{metric} >= 0" order_by = f"metrics.{metric} DESC" runs_df = mlflow.search_runs( experiment_ids=[experiment.experiment_id], filter_string=filter_string, order_by=[order_by], max_results=top_n ) if runs_df.empty: raise RuntimeError(f"❌ Aucun run avec '{metric}' trouvé dans '{experiment_name}'") if len(runs_df) < top_n: raise RuntimeError(f"❌ Seulement {len(runs_df)} runs trouvés, impossible d'obtenir le top_{top_n}") # Prendre le run à la position top_n selected_run = runs_df.iloc[top_n - 1] source_run_id = selected_run['run_id'] metric_value = selected_run[f'metrics.{metric}'] run_name = selected_run.get('tags.mlflow.runName', 'Unknown') print(f"✅ Run sélectionné (position #{top_n}):") print(f" • Nom: {run_name}") print(f" • Run ID: {source_run_id}") print(f" • {metric}: {metric_value:.4f}") # Afficher le contexte print(f"📊 Contexte du classement:") for i, (_, row) in enumerate(runs_df.head(min(5, len(runs_df))).iterrows()): indicator = "👑" if i == (top_n - 1) else f"{i+1}." name = row.get('tags.mlflow.runName', 'Unknown')[:30] value = row[f'metrics.{metric}'] print(f" {indicator} {name}: {value:.4f}") run = mlflow.get_run(source_run_id) except Exception as e: raise RuntimeError(f"❌ Erreur lors de la sélection: {e}") # 2-5. [Reste du code identique - extraction params, détection variante, chargement...] try: params = run.data.params encoder_name = params.get("encoder_name", None) architecture = params.get("architecture", "FPN") num_classes = int(params.get("num_classes", 8)) script_img_size_raw = params.get("script_img_size", None) run_name = run.data.tags.get('mlflow.runName', 'Unknown') # Parser script_img_size script_img_size = None if script_img_size_raw: try: script_img_size = ast.literal_eval(script_img_size_raw) if isinstance(script_img_size, (tuple, list)) and len(script_img_size) == 2: script_img_size = tuple(script_img_size) except: script_img_size = None print(f"\n📋 Paramètres du modèle:") print(f"🏗️ Architecture: {architecture}") print(f"🔧 Encodeur: {encoder_name}") print(f"📊 Nombre de classes: {num_classes}") print(f"🖼️ Script img size: {script_img_size}") except Exception as e: raise RuntimeError(f"❌ Erreur lors de la récupération des paramètres: {e}") # Détecter la variante SegFormer def detect_segformer_variant_from_name(run_name, params): segformer_pattern = r'SegFormer[_-]?b(\d+)' match = re.search(segformer_pattern, run_name, re.IGNORECASE) if match: variant_num = match.group(1) detected = f"mit_b{variant_num}" print(f"🔍 Variante détectée: 'SegFormer_b{variant_num}' → {detected}") return detected if encoder_name and encoder_name.startswith("mit_b"): print(f"🔍 Variante depuis paramètres: {encoder_name}") return encoder_name b_pattern = r'\bb(\d+)\b' match = re.search(b_pattern, run_name, re.IGNORECASE) if match: variant_num = match.group(1) detected = f"mit_b{variant_num}" print(f"🔍 Pattern détecté: 'b{variant_num}' → {detected}") return detected print(f"🔍 Aucune variante détectée, utilisation de mit_b0") return "mit_b0" detected_variant = detect_segformer_variant_from_name(run_name, params) # Télécharger et charger le modèle with tempfile.TemporaryDirectory() as tmpdir: try: artifacts = client.list_artifacts(source_run_id) model_artifact_candidates = [a.path for a in artifacts if a.path.endswith('.pth')] if not model_artifact_candidates: raise RuntimeError("❌ Aucun fichier .pth trouvé") model_artifact = model_artifact_candidates[0] print(f"⬇️ Téléchargement de {model_artifact}...") local_path = mlflow.artifacts.download_artifacts( run_id=source_run_id, artifact_path=model_artifact, dst_path=tmpdir ) checkpoint = torch.load(local_path, map_location='cpu') if isinstance(checkpoint, dict) and 'state_dict' in checkpoint: state_dict = checkpoint['state_dict'] else: state_dict = checkpoint except Exception as e: raise RuntimeError(f"❌ Erreur lors du téléchargement: {e}") # Créer le modèle try: is_transformer = any('patch_embed' in key for key in state_dict.keys()) if is_transformer: print(f"🔧 Création SegFormer ({detected_variant})") model = smp.Segformer(encoder_name=detected_variant, classes=num_classes) encoder_name = detected_variant else: print("🔧 Création modèle CNN") if encoder_name is None or encoder_name == "None": encoder_name = "efficientnet-b0" if architecture.lower() == "fpn": model = smp.FPN(encoder_name=encoder_name, classes=num_classes) elif architecture.lower() == "unet": model = smp.Unet(encoder_name=encoder_name, classes=num_classes) elif architecture.lower() == "deeplabv3plus": model = smp.DeepLabV3Plus(encoder_name=encoder_name, classes=num_classes) else: raise ValueError(f"Architecture {architecture} non supportée") print("🔄 Chargement des poids...") model.load_state_dict(state_dict, strict=True) model.eval() print("✅ Modèle chargé avec succès!") except Exception as e: try: model.load_state_dict(state_dict, strict=False) model.eval() print("✅ Chargement partiel réussi!") except Exception as e2: raise RuntimeError(f"❌ Erreur lors du chargement: {e2}") print(f"🎉 Modèle chargé depuis le run {source_run_id}") return model, source_run_id, encoder_name, script_img_size def load_best_model_from_registry(model_name): """ Charge le meilleur modèle depuis le MLflow Model Registry (Version refactorisée - wrapper autour de load_model) Args: model_name (str): Nom du modèle dans le Registry Returns: tuple: (model, source_run_id, encoder_name, script_img_size) """ print(f"🏛️ Récupération du modèle '{model_name}' depuis le Registry...") client = mlflow.tracking.MlflowClient() try: # Récupérer la version en Production versions = client.search_model_versions(f"name='{model_name}'") prod_versions = [v for v in versions if v.current_stage == "Production"] if not prod_versions: raise RuntimeError(f"❌ Aucune version en Production trouvée pour {model_name}") latest_prod = max(prod_versions, key=lambda v: int(v.version)) print(f"📦 Version Registry: {latest_prod.version} (Stage: {latest_prod.current_stage})") # Récupérer le source_run_id depuis le tag du Registry source_run_id = latest_prod.tags.get("source_run_id") if not source_run_id: raise RuntimeError(f"❌ Tag 'source_run_id' absent pour la version {latest_prod.version}") print(f"🔗 Source run détecté: {source_run_id}") except Exception as e: raise RuntimeError(f"❌ Erreur lors de l'accès au Registry: {e}") # Déléguer le chargement à load_model avec le run_id trouvé print("➡️ Délégation vers load_model...") return load_model(run_id=source_run_id) # Fonctions de convenance def load_best_model(experiment_name="OC Projet 9", metric="test_mean_iou"): """Charge le meilleur modèle selon une métrique""" return load_model(experiment_name=experiment_name, metric=metric, top_n=1) def load_second_best_model(experiment_name="OC Projet 9", metric="test_mean_iou"): """Charge le deuxième meilleur modèle selon une métrique""" return load_model(experiment_name=experiment_name, metric=metric, top_n=2) def load_model_by_run_id(run_id): """Charge un modèle par run_id spécifique""" return load_model(run_id=run_id)