{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":113558,"databundleVersionId":14456136,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":13746211,"sourceType":"datasetVersion","datasetId":8746873},{"sourceId":13747057,"sourceType":"datasetVersion","datasetId":8747501},{"sourceId":13747854,"sourceType":"datasetVersion","datasetId":8748029},{"sourceId":13751233,"sourceType":"datasetVersion","datasetId":8750011},{"sourceId":13756766,"sourceType":"datasetVersion","datasetId":8754058},{"sourceId":13760694,"sourceType":"datasetVersion","datasetId":8756805}],"dockerImageVersionId":31192,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nfrom sklearn.model_selection import train_test_split\n# import cv2 # Vous n'avez pas besoin de cv2 si vous utilisez PIL/Pillow pour une lecture robuste\n\n# --- 0. Définition des chemins de base ---\nDATA_DIR = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/\"\nTRAIN_IMAGES_DIR = os.path.join(DATA_DIR, \"train_images\")\nTRAIN_MASKS_DIR = os.path.join(DATA_DIR, \"train_masks\")\nSUPPLEMENTAL_IMAGES_DIR = os.path.join(DATA_DIR, \"supplemental_images\")\nSUPPLEMENTAL_MASKS_DIR = os.path.join(DATA_DIR, \"supplemental_masks\")\n\n# Fonction de recherche des masques (format corrigé : [case_id].npy)\ndef find_mask_paths(case_id, all_mask_files, mask_dir_list):\n    \"\"\"\n    Cherche le masque NPY unique au format '[case_id].npy' en vérifiant tous les répertoires de masques.\n    \"\"\"\n    mask_file_name = str(case_id) + '.npy'\n    \n    # 1. Parcourir la liste combinée des fichiers de masques\n    if mask_file_name in all_mask_files:\n        \n        # 2. Trouver dans quel répertoire il se trouve\n        for mask_dir in mask_dir_list:\n            full_path = os.path.join(mask_dir, mask_file_name)\n            if os.path.exists(full_path):\n                return [full_path]\n    return []\n\n# --- 1. Lister tous les fichiers PNG d'entraînement (Inclus les supplémentaires) ---\nall_train_images = []\nimage_dirs = [\n    os.path.join(TRAIN_IMAGES_DIR, 'authentic'),\n    os.path.join(TRAIN_IMAGES_DIR, 'forged'),\n    SUPPLEMENTAL_IMAGES_DIR \n]\n\nprint(\"--- Indexation des Images ---\")\nfor img_dir in image_dirs:\n    if os.path.exists(img_dir):\n        is_forged = ('forged' in img_dir)\n        \n        for img_name in os.listdir(img_dir):\n            if img_name.endswith('.png'):\n                case_id = img_name.replace('.png', '')\n                \n                # Pour les supplemental, on ne peut pas savoir si c'est forged sans le masque\n                is_forged_final = is_forged if 'train_images' in img_dir else False \n                \n                all_train_images.append({'case_id': case_id, \n                                         'path': os.path.join(img_dir, img_name),\n                                         'is_forged': is_forged_final})\n\n# --- 2. Créer le DataFrame et gérer les doublons (Priorité au cas 'forged') ---\ntrain_df = pd.DataFrame(all_train_images)\n\n# Trier pour mettre les cas Forged en dernier pour que drop_duplicates les conserve\ntrain_df = train_df.sort_values(by='is_forged').reset_index(drop=True)\ntrain_df = train_df.drop_duplicates(subset=['case_id'], keep='last').reset_index(drop=True)\n\n# --- 3. Intégrer les chemins complets des masques ---\n\nmask_dir_list = [TRAIN_MASKS_DIR, SUPPLEMENTAL_MASKS_DIR]\nall_mask_files = []\nfor mask_dir in mask_dir_list:\n    if os.path.exists(mask_dir):\n        all_mask_files.extend(os.listdir(mask_dir))\n\nprint(f\"\\nTotal des fichiers de masques (train + supplemental) trouvés : {len(all_mask_files)}\")\n\n# Appliquer la fonction pour ajouter les chemins des masques au DataFrame\ntrain_df['mask_paths'] = train_df['case_id'].astype(str).apply(\n    lambda x: find_mask_paths(x, all_mask_files, mask_dir_list))\ntrain_df['mask_count'] = train_df['mask_paths'].apply(len)\n\n# CORRECTION: Mettre à jour 'is_forged' pour toutes les images ayant un masque\ntrain_df.loc[train_df['mask_count'] > 0, 'is_forged'] = True\n\n# Filtrer les images incohérentes (déclarées forged mais sans masque)\ntrain_df_clean = train_df[~((train_df['is_forged'] == True) & (train_df['mask_count'] == 0))].reset_index(drop=True)\n\nprint(f\"\\nTotal des images d'entraînement uniques et utilisables : {len(train_df_clean)}\")\nprint(f\"Images avec masques (Forged) : {train_df_clean['is_forged'].sum()}\")\nprint(f\"Images sans masques (Authentic) : {len(train_df_clean) - train_df_clean['is_forged'].sum()}\")\n\n\n# --- 4. Séparation Train/Validation (Stratifiée) ---\nVALID_SIZE = 0.20\nSEED = 42 \n\n# S'assurer que la stratification fonctionne sur 'is_forged'\ntrain_ids, valid_ids = train_test_split(\n    train_df_clean['case_id'].values,\n    test_size=VALID_SIZE,\n    random_state=SEED,\n    # La stratification est essentielle car les classes Forged/Authentic sont déséquilibrées\n    stratify=train_df_clean['is_forged'].values\n)\n\ntrain_df_final = train_df_clean[train_df_clean['case_id'].isin(train_ids)].reset_index(drop=True)\nvalid_df_final = train_df_clean[train_df_clean['case_id'].isin(valid_ids)].reset_index(drop=True)\n\nprint(\"\\n--- Répartition des Jeux de Données ---\")\nprint(f\"Jeu d'entraînement (Train) : {len(train_df_final)} images\")\nprint(f\"Jeu de validation (Validation) : {len(valid_df_final)} images\")\nprint(f\"Proportion Forged/Total dans le Train : {train_df_final['is_forged'].sum() / len(train_df_final):.3f}\")\nprint(f\"Proportion Forged/Total dans la Validation : {valid_df_final['is_forged'].sum() / len(valid_df_final):.3f}\")\n\nprint(\"\\nLes DataFrames train_df_final et valid_df_final sont prêts à être utilisés.\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# FIXE CRITIQUE : Installation de la version 3.20.1 compatible Python 3.\n# Remplacez 'chemin-vers-votre-dataset' par le chemin exact.\n\n!pip install --upgrade --force-reinstall /kaggle/input/protobuf-3-20-1/protobuf-3.20.1-py2.py3-none-any.whl\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-16T16:26:22.936044Z","iopub.execute_input":"2025-11-16T16:26:22.936286Z","iopub.status.idle":"2025-11-16T16:26:29.638254Z","shell.execute_reply.started":"2025-11-16T16:26:22.936264Z","shell.execute_reply":"2025-11-16T16:26:29.636946Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import sys\nimport os\nimport shutil\n\n# --- 1. Définition des chemins ---\n# Chemins d'entrée (Input Datasets)\nSEG_INPUT_PATH = '/kaggle/input/segmentation-models'\nCLS_INPUT_PATH = '/kaggle/input/classification-models'\n\n# Chemins de travail corrigés (avec underscore et dans /kaggle/working/)\nSEG_WORKING_PATH = '/kaggle/working/segmentation_models'\nCLS_WORKING_PATH = '/kaggle/working/classification_models'\n\n# --- 2. Copie et Renommage Forcé (Contournement de la Lecture Seule) ---\n\nprint(\"Début de la copie des packages vers /kaggle/working/...\")\n\n# A. Copie du package de segmentation\nif os.path.isdir(SEG_INPUT_PATH):\n    # Supprimer l'ancienne version si elle existe (pour une copie propre)\n    if os.path.exists(SEG_WORKING_PATH):\n        shutil.rmtree(SEG_WORKING_PATH)\n    \n    # Créer le nouveau dossier correctement nommé\n    os.makedirs(SEG_WORKING_PATH, exist_ok=True)\n    \n    # Copier le contenu\n    !cp -r $SEG_INPUT_PATH/* $SEG_WORKING_PATH\n    print(f\"✅ Segmentation package copié dans {SEG_WORKING_PATH}\")\nelse:\n    print(f\"⚠️ Avertissement : {SEG_INPUT_PATH} n'a pas été trouvé.\")\n\n\n# B. Copie du package de classification\nif os.path.isdir(CLS_INPUT_PATH):\n    # Supprimer l'ancienne version si elle existe\n    if os.path.exists(CLS_WORKING_PATH):\n        shutil.rmtree(CLS_WORKING_PATH)\n        \n    # Créer le nouveau dossier correctement nommé\n    os.makedirs(CLS_WORKING_PATH, exist_ok=True)\n    \n    # Copier le contenu\n    !cp -r $CLS_INPUT_PATH/* $CLS_WORKING_PATH\n    print(f\"✅ Classification package copié dans {CLS_WORKING_PATH}\")\nelse:\n    print(f\"⚠️ Avertissement : {CLS_INPUT_PATH} n'a pas été trouvé.\")\n\n\n# --- 3. Ajout des chemins corrigés à sys.path ---\n\n# Ajouter les chemins corrigés à sys.path\nif SEG_WORKING_PATH not in sys.path:\n    sys.path.append(SEG_WORKING_PATH)\n\nif CLS_WORKING_PATH not in sys.path:\n    sys.path.append(CLS_WORKING_PATH)\n\nprint(\"✅ Chemins du répertoire de travail ajoutés à sys.path.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-16T16:29:20.263323Z","iopub.execute_input":"2025-11-16T16:29:20.263708Z","iopub.status.idle":"2025-11-16T16:29:20.646477Z","shell.execute_reply.started":"2025-11-16T16:29:20.263677Z","shell.execute_reply":"2025-11-16T16:29:20.644978Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- 1. IMPORTATION DES LIBRAIRIES ET DES PACKAGES KAGGLE ---\nimport os\nimport sys\nimport tensorflow as tf\nfrom tensorflow.keras import backend as K\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.models import load_model\nfrom PIL import Image\n\n# Chemins des packages et des poids du modèle dans les Datasets Kaggle\nPACKAGE_DIR = \"/kaggle/input/\"\n# VÉRIFIEZ CE CHEMIN. L'erreur indique que le fichier .h5 est introuvable ici.\nMODEL_WEIGHTS_DIR = \"/kaggle/input/unet-renet34-phase2-final-plus-h5/\" \n\n# --- 2. GESTION DES CHEMINS (ASSUMANT LA COPIE PRÉALABLE) ---\n# Ces chemins pointent vers les dossiers dans /kaggle/working/ après la commande !cp -r\n\nCLASSIFICATION_PATH = '/kaggle/working/classification_models'  \nSEGMENTATION_PATH = '/kaggle/working/segmentation_models'  \n\nprint(\"Ajout des chemins des dépendances locales...\")\n\n# Ajouter les chemins corrigés à sys.path (uniquement s'ils ne sont pas déjà là)\nif CLASSIFICATION_PATH not in sys.path:\n    sys.path.append(CLASSIFICATION_PATH)\nif SEGMENTATION_PATH not in sys.path:\n    sys.path.append(SEGMENTATION_PATH)\n\n\n# --- 3. IMPORTATION DES PACKAGES ---\ntry:\n    # L'importation cherche dans les chemins ajoutés ci-dessus\n    import segmentation_models as sm\n    import classification_models as cm\n    print(\"✅ Packages 'segmentation_models' et 'classification_models' importés avec succès.\")\nexcept ImportError as e:\n    print(f\"❌ ÉCHEC DE L'IMPORTATION. CAUSE: {e}\")\n\n\n# --- 2. CONFIGURATION DES HYPERPARAMÈTRES (Stage 2: Fine-Tuning) ---\nIMG_SIZE = 256\nBATCH_SIZE = 1 \nTHRESHOLD = 0.5\nLR_FINE_TUNE = 1e-5 \n\n# --- NOUVEAU : Définition du BACKBONE pour la construction de secours ---\nBACKBONE = 'resnet34' \n\nPOSITIVE_WEIGHT = 30.0\nNEGATIVE_WEIGHT = 1.0\nALPHA_LOSS = 0.5\n\nprint(f\"✅ Configuration initiale : IMG_SIZE={IMG_SIZE}, BATCH_SIZE={BATCH_SIZE}, LR={LR_FINE_TUNE}.\")\nprint(f\"✅ Pondération de la perte : Positif/Négatif = {POSITIVE_WEIGHT}/{NEGATIVE_WEIGHT}.\")\n\n\n# --- 3. DÉFINITION DES FONCTIONS DE PERTE ET DE MÉTRIQUE ---\n\n# 3.1 Fonction Métrique: Dice Coefficient (doit être définie avant la perte combinée)\ndef dice_coef(y_true, y_pred, smooth=1e-6):\n    y_true_f = K.flatten(y_true)\n    y_pred_f = K.flatten(y_pred)\n    intersection = K.sum(y_true_f * y_pred_f)\n    return (2. * intersection + smooth) / (K.sum(y_true_f) + K.sum(y_pred_f) + smooth)\n\n# 3.2 Nouvelle Fonction de Perte: Weighted Combined Loss\ndef weighted_combined_loss(y_true, y_pred):\n    \"\"\"Perte combinée (Pondérée BCE + Dice Loss).\"\"\"\n    \n    # --- Weighted Binary Cross-Entropy ---\n    class_weights = tf.constant([NEGATIVE_WEIGHT, POSITIVE_WEIGHT])\n    # Calculer les poids d'échantillon (sample_weights)\n    sample_weights = y_true * class_weights[1] + (1. - y_true) * class_weights[0]\n    \n    # Calcul de la BCE brute\n    bce = tf.keras.losses.BinaryCrossentropy(from_logits=False, reduction=tf.keras.losses.Reduction.NONE)\n    bce_loss_raw = bce(y_true, y_pred)\n    \n    # Appliquer les poids et prendre la moyenne pondérée\n    weighted_bce = tf.reduce_mean(bce_loss_raw * sample_weights)\n    \n    # --- Dice Loss ---\n    dice_l = 1.0 - dice_coef(y_true, y_pred)\n    \n    # --- Combinaison ---\n    return ALPHA_LOSS * dice_l + (1 - ALPHA_LOSS) * weighted_bce\n\n# Objets personnalisés requis pour la compilation/chargement du modèle\ncustom_objects = {\n    'dice_coef': dice_coef,\n    # 💥 CORRECTION 1 : Le nom de la clé doit correspondre au nom de la fonction\n    'weighted_combined_loss': weighted_combined_loss, \n    # 💥 CORRECTION 2 : Ajout de K pour la stabilité de la désérialisation\n    'K': K \n}\n\nprint(\"✅ Objets personnalisés (dice_coef, weighted_combined_loss, K) définis et corrigés.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-16T16:29:25.173706Z","iopub.execute_input":"2025-11-16T16:29:25.174120Z","iopub.status.idle":"2025-11-16T16:29:25.190444Z","shell.execute_reply.started":"2025-11-16T16:29:25.174082Z","shell.execute_reply":"2025-11-16T16:29:25.189368Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport warnings\n\n# --- Définition des Paramètres Globaux Manquants ---\n# (Assurez-vous que IMG_SIZE est cohérent avec la Cellule 2)\nIMG_SIZE = 256 \n# ----------------------------------------------------\n\n# Supprimer les avertissements qui peuvent être générés par np.load()\nwarnings.filterwarnings(\"ignore\", category=UserWarning)\n\n# --- Fonction de Vérification du Masque ---\ndef is_mask_corrupted(mask_path, img_size):\n    \"\"\"Vérifie si le fichier masque est chargeable et a des dimensions valides.\"\"\"\n    try:\n        mask_data = np.load(mask_path)\n        \n        # Logique de combinaison des canaux (pour obtenir un masque 2D)\n        if mask_data.ndim == 3 and mask_data.shape[0] in [2, 3]:\n            # Si le masque est 3D (ex: [3, H, W] ou [2, H, W]), on le réduit à 2D\n            mask_2d = np.max(mask_data, axis=0) \n        else:\n            mask_2d = mask_data\n            \n        # Vérification de la taille et de la forme\n        # On vérifie que la taille n'est pas zéro et que la dimension est au moins 2.\n        if not (mask_2d.size > 0 and mask_2d.ndim >= 2 and mask_2d.shape[0] > 0 and mask_2d.shape[1] > 0):\n            return True # Masque vide ou de forme non valide\n\n        return False\n    except Exception:\n        return True # Le fichier ne peut pas être chargé (corrompu)\n\n# --- Nettoyage du DataFrame ---\n\nprint(f\"Début de la vérification de {len(train_df_final)} échantillons...\")\nrows_to_drop = []\n\n# Il est important de ne nettoyer que le jeu d'entraînement.\nfor index, row in train_df_final.iterrows():\n    # Nous vérifions le premier masque seulement, car il est le seul utilisé\n    # Nous vérifions uniquement si l'image est marquée comme 'forged'\n    if row['is_forged'] and row['mask_paths']:\n        mask_path = row['mask_paths'][0] \n        \n        if is_mask_corrupted(mask_path, IMG_SIZE):\n            rows_to_drop.append(index)\n            \nif rows_to_drop:\n    # Suppression des lignes corrompues\n    train_df_final = train_df_final.drop(rows_to_drop).reset_index(drop=True)\n    print(f\"\\n✅ Nettoyage terminé. {len(rows_to_drop)} échantillons corrompus ont été retirés.\")\nelse:\n    print(\"\\n✅ Nettoyage terminé. Aucune ligne corrompue trouvée.\")\n\nprint(f\"Taille finale du jeu d'entraînement : {len(train_df_final)}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- NOUVELLE CELLULE D'INSTALLATION (à placer au début du notebook) ---\n\n# Vérifier si 'albumentations' est déjà installé\ntry:\n    import albumentations\n    print(\"Albumentations est déjà installé.\")\nexcept ImportError:\n    print(\"Installation de Albumentations depuis le répertoire d'entrée...\")\n    \n    # Commande pour installer le package à partir du répertoire local sur Kaggle\n    # Le '/*' permet de cibler spécifiquement les fichiers .whl si le dataset les contient.\n    # Le '-q' signifie 'quiet' (silencieux).\n    !pip install /kaggle/input/albumentations/* -q \n    \n    print(\"✅ Installation locale terminée.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-16T17:22:57.235680Z","iopub.execute_input":"2025-11-16T17:22:57.236028Z","iopub.status.idle":"2025-11-16T17:23:37.647327Z","shell.execute_reply.started":"2025-11-16T17:22:57.235991Z","shell.execute_reply":"2025-11-16T17:23:37.645723Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- INSTALLATION NÉCESSAIRE (Si ce n'est pas déjà fait) ---\nfrom tensorflow.keras.utils import Sequence\nfrom PIL import Image \nimport numpy as np\nimport albumentations as A\nimport cv2 \n\n# --- Paramètres (Doivent être cohérents avec la Cellule 2) ---\nIMG_SIZE = 256 \nBATCH_SIZE = 1 \n# ---\n\n# --- DÉFINITION DE LA PIPELINE D'AUGMENTATION AGRESSIVE ---\ndef get_training_augmentation(img_size):\n    \"\"\"Pipeline d'augmentation pour le jeu d'entraînement (accent sur la robustèse).\"\"\"\n    train_transform = A.Compose([\n        A.HorizontalFlip(p=0.5),\n        A.VerticalFlip(p=0.5),\n        A.ShiftScaleRotate(scale_limit=0.15, rotate_limit=15, shift_limit=0.15, p=0.7, \n                           border_mode=cv2.BORDER_CONSTANT),\n        A.GaussNoise(var_limit=(10.0, 50.0), p=0.4), \n        A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.5),\n        A.HueSaturationValue(hue_shift_limit=10, sat_shift_limit=15, val_shift_limit=10, p=0.3),\n        A.CoarseDropout(max_holes=8, max_height=8, max_width=8, p=0.2), \n    ], p=1.0, is_check_shapes=False)\n    return train_transform\n\n# --- CLASSE DE GÉNÉRATEUR AVEC LECTURE ROBUSTE (PIL) ---\nclass ScientificImageGenerator(Sequence):\n    def __init__(self, df, batch_size=BATCH_SIZE, img_size=IMG_SIZE, shuffle=True, augment=False):\n        self.df = df\n        self.batch_size = batch_size\n        self.img_size = img_size\n        self.shuffle = shuffle\n        self.augment = augment\n        self.on_epoch_end()\n        \n        if self.augment:\n            self.transform = get_training_augmentation(self.img_size)\n        else:\n            self.transform = A.Compose([\n                A.Resize(self.img_size, self.img_size, interpolation=cv2.INTER_NEAREST), \n            ], is_check_shapes=False) \n\n    def __len__(self):\n        return int(np.floor(len(self.df) / self.batch_size))\n\n    def on_epoch_end(self):\n        self.indices = np.arange(len(self.df))\n        if self.shuffle == True:\n            np.random.shuffle(self.indices)\n\n    def load_image_robust(self, path):\n        \"\"\"Lit l'image PNG de manière robuste avec PIL et assure la conversion RGB.\"\"\"\n        try:\n            img = Image.open(path)\n            if img.mode != 'RGB':\n                img = img.convert('RGB')\n            return np.array(img).astype(np.uint8)\n        except Exception as e:\n            return np.zeros((self.img_size, self.img_size, 3), dtype=np.uint8)\n\n    def __getitem__(self, index):\n        indices = self.indices[index * self.batch_size:(index + 1) * self.batch_size]\n        batch_df = self.df.iloc[indices]\n        \n        X = np.empty((self.batch_size, self.img_size, self.img_size, 3), dtype=np.float32)\n        y = np.empty((self.batch_size, self.img_size, self.img_size, 1), dtype=np.float32)\n\n        for i_local, row in enumerate(batch_df.itertuples()):\n            \n            # --- 1. Chargement et vérification de l'Image ---\n            img_raw = self.load_image_robust(row.path)\n            if img_raw.shape[0] != self.img_size or img_raw.shape[1] != self.img_size:\n                img_raw = cv2.resize(img_raw, (self.img_size, self.img_size), interpolation=cv2.INTER_LINEAR)\n            \n            # --- 2. Chargement et correction du Masque (RÉSOLUTION 1024x1024) ---\n            mask_data = np.zeros((self.img_size, self.img_size), dtype=np.float32)\n            if row.is_forged and row.mask_paths:\n                try:\n                    mask_data_raw = np.load(row.mask_paths[0])\n                    \n                    # 1. Nettoyage initial : Forcer la forme 2D (Résout les masques 1024x1024 mal gérés)\n                    mask_2d_temp = np.squeeze(mask_data_raw) \n                    \n                    if mask_2d_temp.ndim == 3:\n                        # Fusionner les canaux s'ils existent (prend la valeur max)\n                        mask_2d = np.max(mask_2d_temp, axis=-1) \n                    elif mask_2d_temp.ndim == 2:\n                        mask_2d = mask_2d_temp\n                    else:\n                        # Si le masque est < 2D, on le crée comme une matrice de zéros 2D\n                        mask_2d = np.zeros((self.img_size, self.img_size), dtype=mask_2d_temp.dtype)\n                        \n                    mask_data = mask_2d.astype(np.uint8)  \n                    \n                    # 2. Redimensionnement forcé à IMG_SIZE (Résout l'erreur 1024x1024)\n                    if mask_data.shape[0] != self.img_size or mask_data.shape[1] != self.img_size:\n                        mask_data = cv2.resize(\n                            mask_data, \n                            (self.img_size, self.img_size), \n                            interpolation=cv2.INTER_NEAREST\n                        )\n                        \n                except Exception:\n                    pass\n\n            # --- 3. APPLICATION DE L'AUGMENTATION / REDIMENSIONNEMENT ---\n            interpolation_mask = cv2.INTER_NEAREST\n            \n            augmented = self.transform(image=img_raw, mask=mask_data, interpolation=interpolation_mask)\n            img_aug = augmented['image']\n            mask_aug = augmented['mask']\n            \n            # Finalisation et Normalisation\n            X[i_local] = img_aug.astype(np.float32) / 255.0  \n            \n            # 🚀 CORRECTION FINALE : Troncature Forcée de la 3ème dimension (Résout l'erreur 256x256xN)\n            mask_cleaned = np.squeeze(mask_aug)\n            \n            if mask_cleaned.ndim > 2:\n                print(f\"⚠️ AVERTISSEMENT: Masque encore {mask_cleaned.shape} pour {row.path}. Forçage à 2D.\")\n                # Troncature pour forcer la suppression de la 3ème dimension\n                mask_2d_final = mask_cleaned[:self.img_size, :self.img_size, 0] \n            elif mask_cleaned.ndim == 2:\n                mask_2d_final = mask_cleaned\n            else:\n                mask_2d_final = np.zeros((self.img_size, self.img_size), dtype=mask_cleaned.dtype)\n                \n            # 3. Remodeler le masque pour garantir (H, W, 1) et l'insérer dans y\n            y[i_local] = np.expand_dims(mask_2d_final > 0, axis=-1).astype(np.float32)\n            \n        return X, y\n\nprint(\"✅ Classe ScientificImageGenerator définie. Toutes les corrections de formes (initiales et après augmentation) sont intégrées.\")\n\n# --- Initialisation des Générateurs ---\nif 'train_df_final' in locals() and 'valid_df_final' in locals():\n    train_gen = ScientificImageGenerator(train_df_final, batch_size=BATCH_SIZE, shuffle=True, augment=True)\n    valid_gen = ScientificImageGenerator(valid_df_final, batch_size=BATCH_SIZE, shuffle=False, augment=False) \n\n    print(f\"\\n--- Initialisation des Générateurs pour le STAGE 2 ---\")\n    print(f\"Générateur d'entraînement (avec augmentation) : Lots par époque = {len(train_gen)} (BATCH_SIZE={BATCH_SIZE})\")\n    print(f\"Générateur de validation (sans augmentation) : Lots par époque = {len(valid_gen)} (BATCH_SIZE={BATCH_SIZE})\")\nelse:\n    print(\"❌ ATTENTION: Les DataFrames 'train_df_final' ou 'valid_df_final' ne sont pas définis.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import google.protobuf\nprint(f\"Version de Protobuf utilisée : {google.protobuf.__version__}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- 1. CHEMIN ET PRÉPARATION ---\nimport os\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.models import load_model\nfrom tensorflow.keras import backend as K\n\n# Le chemin du dossier (MODEL_WEIGHTS_DIR) est défini en Cellule 2.\n# Correction du nom du fichier pour correspondre à 'unet_resnet34...'\nMODEL_FILE_NAME = \"unet_resnet34_phase2_final_plus.h5\"\nMODEL_FILE_PATH = os.path.join(MODEL_WEIGHTS_DIR, MODEL_FILE_NAME)\n\n# --- 2. CHARGEMENT DU MODÈLE ENTRAÎNÉ (STAGE 1) ---\nmodel = None # Initialisation sécurisée\nis_loaded = False\n\ntry:\n    print(f\"Chargement du modèle pré-entraîné depuis : {MODEL_FILE_PATH}...\")\n    \n    # Tentative de chargement du modèle\n    model = load_model(MODEL_FILE_PATH, custom_objects=custom_objects)\n    is_loaded = True\n    \n    print(\"✅ Modèle chargé avec succès.\")\n\nexcept Exception as e:\n    print(f\"❌ ERREUR FATALE lors du chargement du modèle (Fichier H5 introuvable ?) : {e}\")\n    \n    # --- SOLUTION DE SECOURS : Construction U-Net à partir des poids ImageNet ---\n    print(\"\\nTentative de construction du modèle U-Net à partir des poids ImageNet...\")\n    \n    try:\n        # Nécessite que 'sm', IMG_SIZE, BACKBONE soient définis\n        model = sm.Unet(\n            BACKBONE, \n            encoder_weights='imagenet',  \n            input_shape=(IMG_SIZE, IMG_SIZE, 3),  \n            classes=1,  \n            activation='sigmoid'        \n        )\n        is_loaded = True\n        print(f\"✅ Modèle U-Net ({BACKBONE}) construit à partir des poids ImageNet.\")\n        \n    except Exception as build_error:\n        print(f\"❌ ERREUR LORS DE LA CONSTRUCTION : Le processus s'arrête ici. {build_error}\")\n\n\n# --- 3. RECOMPILATION POUR LE FINE-TUNING (STAGE 2) ---\n\nif model is not None:\n    print(f\"\\nRecompilation du modèle avec LR = {LR_FINE_TUNE} et la Perte Pondérée...\")\n\n    # Recompiler le modèle avec les paramètres du Stage 2:\n    model.compile(\n        optimizer=Adam(learning_rate=LR_FINE_TUNE), \n        loss=weighted_combined_loss, \n        metrics=[dice_coef, 'accuracy'] \n    )\n\n    print(f\"✅ Modèle recompilé pour le Fine-Tuning.\")\n    \n    # LIGNE CORRIGÉE : Utiliser 'learning_rate' au lieu de 'lr' pour éviter l'AttributeError\n    print(f\"Taux d'apprentissage actuel : {K.eval(model.optimizer.learning_rate)}\") \n\n    # --- 4. PRÉPARATION DES CALLBACKS ---\n    from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping, ReduceLROnPlateau\n\n    model_checkpoint_callback = ModelCheckpoint(\n        'unet_resnet34_stage2_finetuned.h5', \n        monitor='val_dice_coef', \n        mode='max', \n        save_best_only=True, \n        verbose=1\n    )\n\n    early_stopping_callback = EarlyStopping(\n        monitor='val_dice_coef', \n        mode='max', \n        patience=5,\n        restore_best_weights=True, \n        verbose=1\n    )\n\n    reduce_lr_callback = ReduceLROnPlateau(\n        monitor='val_dice_coef', \n        mode='max', \n        factor=0.5, \n        patience=2, \n        min_lr=1e-7, \n        verbose=1\n    )\n\n    callbacks = [model_checkpoint_callback, early_stopping_callback, reduce_lr_callback]\n\n    print(\"✅ Callbacks définis.\")\nelse:\n    print(\"❌ Processus arrêté. Le modèle n'a pu être ni chargé, ni construit.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- VÉRIFICATION DU PREMIER LOT ---\n\n# 1. Vérification du générateur d'entraînement\nX_train_test, y_train_test = train_gen[0]\n\nprint(\"--- 🧪 Inspection du Générateur d'Entraînement (Lot 0) ---\")\nprint(f\"Forme de l'Image X (Entrée du Modèle) : {X_train_test.shape}\")\nprint(f\"Forme du Masque y (Vérité Terrain) : {y_train_test.shape}\")\n\n# 2. Vérification du générateur de validation\nX_valid_test, y_valid_test = valid_gen[0]\n\nprint(\"\\n--- 🔬 Inspection du Générateur de Validation (Lot 0) ---\")\nprint(f\"Forme de l'Image X (Entrée du Modèle) : {X_valid_test.shape}\")\nprint(f\"Forme du Masque y (Vérité Terrain) : {y_valid_test.shape}\")\n\n# 3. Vérification des types de données\nprint(\"\\n--- Types de Données ---\")\nprint(f\"Type X : {X_train_test.dtype}\")\nprint(f\"Type y : {y_train_test.dtype}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# # La variable K est importée dans la Cellule 2, ainsi que POSITIVE_WEIGHT\n# import tensorflow.keras.backend as K\n\n# # --- Vérification finale des paramètres ---\n# print(\"--- Démarrage du Fine-Tuning (Stage 2) ---\")\n# # LIGNE CORRIGÉE : Utiliser 'learning_rate'\n# print(f\"Modèle : U-Net (ResNet34), Optimiseur : Adam (LR={K.eval(model.optimizer.learning_rate)})\")\n# # Affichage des poids pour confirmer la stratégie anti-asymétrie\n# print(f\"Poids de la Perte Positive (Anti-Asymétrie) : {POSITIVE_WEIGHT}\") \n# print(f\"Lots d'entraînement par époque (BATCH_SIZE=1) : {len(train_gen)}\")\n\n# # Lancement de l'entraînement du modèle\n# # Nous utilisons un nombre d'époques suffisant (15) pour laisser l'EarlyStopping\n# # (avec patience=5) trouver le meilleur point de convergence.\n# history_stage2 = model.fit(\n#     train_gen,\n#     # Utilisation du jeu de validation complet pour le suivi\n#     validation_data=valid_gen, \n#     epochs=15, \n#     callbacks=callbacks,\n#     verbose=1\n# )\n\n# # print(\"\\n✅ Fine-Tuning du Stage 2 terminé.\")\n# # print(\"Le meilleur modèle (basé sur 'val_dice_coef') est sauvegardé dans 'unet_resnet34_stage2_finetuned.h5'.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"## 🛠️ CELLULE D'INSPECTION (À EXÉCUTER AVANT LOAD_MODEL)\n\n# 1. Vérification du dictionnaire custom_objects (la cause principale de l'erreur)\nprint(\"--- 1. Inspection de custom_objects ---\")\ntry:\n    print(f\"custom_objects contient {len(custom_objects)} entrées:\")\n    for key, value in custom_objects.items():\n        print(f\"  - Clé enregistrée : '{key}' -> Objet: {value.__name__}\")\n    \n    # Vérification critique de la clé de la perte et du backend K\n    if 'weighted_combined_loss' in custom_objects and 'K' in custom_objects:\n        print(\"✅ Statut des clés : OK (Noms de perte corrigés et K inclus).\")\n    else:\n        print(\"❌ Statut des clés : ATTENTION ! La perte ou 'K' pourrait manquer.\")\n\nexcept NameError:\n    print(\"❌ ERREUR: 'custom_objects' n'est pas défini (Exécutez la cellule de définition).\")\nexcept AttributeError:\n     print(\"❌ ERREUR: Un objet dans custom_objects n'est pas une fonction ou un module valide.\")\nexcept Exception as e:\n    print(f\"❌ Erreur lors de l'inspection des custom_objects: {e}\")\n    \nprint(\"\\n--- 2. Inspection du Chemin du Modèle ---\")\n# Assurez-vous d'utiliser la variable du chemin que vous utilisez dans la cellule suivante\nBEST_MODEL_PATH = '/kaggle/input/unet-resnet34-stage2-finetuned-h5/unet_resnet34_stage2_finetuned.h5'\nprint(f\"Chemin du modèle : {BEST_MODEL_PATH}\")\nprint(\"Vérifiez l'exactitude de ce chemin (doit être un fichier .h5 existant).\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nfrom tensorflow.keras.models import load_model\nfrom tensorflow.keras import backend as K\nfrom tensorflow.keras.utils import Sequence\nfrom PIL import Image\nimport albumentations as A\nimport cv2\n\n# =================================================================\n# !!! CONFIGURATION CRUCIALE !!!\n# Vous devez définir ces variables une seule fois au début du notebook.\n# Elles sont nécessaires pour que le code ci-dessous fonctionne.\nIMG_SIZE = 256\nDATA_DIR = '/kaggle/input/recodai-luc-scientific-image-forgery-detection' \n# custom_objects doit être défini dans la cellule précédente\n# =================================================================\n\n\n# --- 1. CHARGEMENT DU MODÈLE FINAL ---\nBEST_MODEL_PATH = '/kaggle/input/unet-resnet34-stage2-finetuned-h5/unet_resnet34_stage2_finetuned.h5'\n\ntry:\n    print(f\"Chargement du meilleur modèle pour la prédiction : {BEST_MODEL_PATH}\")\n    model_final = load_model(BEST_MODEL_PATH, custom_objects=custom_objects)\n    print(\"✅ Modèle final chargé avec succès.\")\nexcept Exception as e:\n    print(f\"❌ ERREUR LORS DU CHARGEMENT DU MODÈLE : {e}\")\n    print(\"!!! VÉRIFICATION REQUISE : custom_objects ou le chemin du modèle est incorrect.\")\n\n\n# --- 2. PRÉPARATION DU DATAFRAME DE TEST (CORRIGÉE & INSPECTÉE) ---\n# Le dossier réel des images de test\nTEST_IMAGES_DIR = os.path.join(DATA_DIR, \"test_images\")\n\ntest_images = []\nif os.path.isdir(TEST_IMAGES_DIR):\n    for img_name in os.listdir(TEST_IMAGES_DIR):\n        if img_name.endswith('.png'):\n            # Enregistrement de l'ID complet (nom du fichier sans extension: '45')\n            full_image_id = img_name.replace('.png', '')\n            \n            test_images.append({\n                'case_id': full_image_id, \n                'path': os.path.join(TEST_IMAGES_DIR, img_name)\n            })\n\ntest_df = pd.DataFrame(test_images)\nprint(f\"\\nTotal des images de test à traiter : {len(test_df)}\")\n\n### 🔑 Inspection du DataFrame 🔑 ###\nprint(\"\\n--- INSPECTION test_df ---\")\nif not test_df.empty:\n    print(f\"Le premier 'case_id' est : {test_df.iloc[0]['case_id']}\")\n    print(f\"Le chemin du premier fichier est : {test_df.iloc[0]['path']}\")\n    if test_df.iloc[0]['case_id'] == '45':\n        print(\"✅ CONFIANCE : Le 'case_id' est bien '45' dans le DataFrame.\")\n    else:\n        print(\"❌ ALERTE : Le 'case_id' n'est PAS '45' (vérifiez le nom du fichier lu).\")\nelse:\n    print(\"❌ ALERTE : Le DataFrame de test est vide.\")\n# --------------------------------- #\n\n\n# --- 3. DÉFINITION DU GÉNÉRATEUR DE TEST ---\nclass TestImageGenerator(Sequence):\n    \"\"\"Générateur simplifié pour le jeu de test (lit seulement les images).\"\"\"\n    def __init__(self, df, img_size=IMG_SIZE, batch_size=1):\n        self.df = df\n        self.img_size = img_size\n        self.batch_size = batch_size\n        self.indices = np.arange(len(self.df))\n        self.transform = A.Resize(img_size, img_size, interpolation=cv2.INTER_CUBIC)\n\n    def __len__(self):\n        return int(np.ceil(len(self.df) / self.batch_size))\n\n    def load_image_robust(self, path):\n        \"\"\"Lit l'image PNG de manière robuste avec PIL.\"\"\"\n        try:\n            img = Image.open(path)\n            if img.mode != 'RGB':\n                img = img.convert('RGB')\n            return np.array(img).astype(np.uint8)\n        except Exception:\n            return np.zeros((self.img_size, self.img_size, 3), dtype=np.uint8)\n\n    def __getitem__(self, index):\n        indices = self.indices[index * self.batch_size:(index + 1) * self.batch_size]\n        batch_df = self.df.iloc[indices]\n        \n        X = np.empty((len(batch_df), self.img_size, self.img_size, 3), dtype=np.float32)\n\n        for i_local, row in enumerate(batch_df.itertuples()):\n            img_raw = self.load_image_robust(row.path)\n            augmented = self.transform(image=img_raw)\n            img_aug = augmented['image']\n            X[i_local] = img_aug.astype(np.float32) / 255.0\n\n        return X\n\n# Initialisation du générateur de test\ntest_gen = TestImageGenerator(test_df, img_size=IMG_SIZE, batch_size=1)\n\n\n# --- 4. EXÉCUTION DES PRÉDICTIONS ---\nif 'model_final' in locals():\n    print(\"\\nDémarrage des prédictions (cela peut prendre quelques minutes)...\")\n    predictions_raw = model_final.predict(test_gen, verbose=1) \n    print(f\"✅ Prédictions brutes terminées. Forme : {predictions_raw.shape}\")\nelse:\n    print(\"\\n⚠️ Prédictions non exécutées car le modèle n'a pas été chargé.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- 1. DÉFINITION DES LIBRAIRIES/FONCTIONS ESSENTIELLES ---\nimport numpy as np\nimport pandas as pd\nimport json # Essentiel pour sérialiser la liste RLE en JSON\nimport os\n\n# --- Définition RLE standard (qui retourne une chaîne brute) ---\ndef rle_encode_raw(mask):\n    \"\"\"\n    Encode un masque binaire (H, W) en codage RLE (Run-Length Encoding) en chaîne brute.\n    \"\"\"\n    pixels = mask.T.flatten()\n    pixels = np.concatenate([[0], pixels, [0]])\n    runs = np.where(pixels[1:] != pixels[:-1])[0] + 1\n    run_lengths = runs[1::2] - runs[0::2]\n    starts = runs[0::2]\n    rle = list(zip(starts, run_lengths))\n    return ' '.join(str(x) for p in rle for x in p)\n\n\n# --- 2. POST-TRAITEMENT ET ENCODAGE ---\n\n# THRESHOLD = 0.5 (Doit être défini précédemment)\nsubmission_data = []\n\nprint(\"\\nDémarrage de la création du fichier de soumission...\")\n\nfor i, row in test_df.iterrows():\n    # 💥 SOLUTION : UTILISER DIRECTEMENT LE 'case_id' CONFIRMÉ COMME ÉTANT CORRECT\n    final_case_id = row['case_id'] \n\n    # Récupérer la prédiction et seuiller\n    pred_mask_norm = predictions_raw[i, :, :, 0]\n    pred_mask_binary = (pred_mask_norm > THRESHOLD).astype(np.uint8)\n    \n    \n    if np.sum(pred_mask_binary) > 0:\n        # Cas 1 : Fraude détectée -> Encodage RLE JSON\n        rle_string = rle_encode_raw(pred_mask_binary)\n        \n        if rle_string:\n            # Conversion au format JSON requis\n            rle_list = [int(x) for x in rle_string.split(' ')]\n            annotation_value = json.dumps(rle_list)\n        else:\n            annotation_value = 'authentic'\n            \n    else:\n        # Cas 2 : Masque vide -> 'authentic'\n        annotation_value = 'authentic'\n        \n    submission_data.append({\n        'case_id': final_case_id, # C'est ici que '45' sera utilisé\n        'annotation': annotation_value \n    })\n\n# --- 3. CRÉATION DU FICHIER DE SOUMISSION ---\n\nsubmission_df = pd.DataFrame(submission_data)\n\n# S'assurer que les colonnes sont dans l'ordre attendu\nsubmission_df = submission_df[['case_id', 'annotation']]\n\nSUBMISSION_FILE_NAME = 'submission.csv'\nsubmission_df.to_csv(SUBMISSION_FILE_NAME, index=False)\n\nprint(f\"\\n✅ Fichier de soumission créé : {SUBMISSION_FILE_NAME}\")\nprint(f\"Aperçu des premières lignes (VÉRIFICATION FINALE DU case_id) :\\n{submission_df.head()}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}