{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","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":"nvidiaTeslaT4","dataSources":[{"sourceId":113558,"databundleVersionId":14878066,"sourceType":"competition"}],"dockerImageVersionId":31193,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# =========================\n# Core libraries\n# =========================\nimport os\nimport warnings\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport cv2\n\nfrom tqdm import tqdm\n\n# =========================\n# Deep Learning (PyTorch)\n# =========================\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\n\nimport torchvision.transforms as transforms\nimport torchvision.models as models\n\n# =========================\n# Reproducibility & performance\n# =========================\nSEED = 42\n\ndef seed_everything(seed=42):\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = False\n    torch.backends.cudnn.benchmark = True  # ⚡ speed boost on GPU\n\nseed_everything(SEED)\n\nwarnings.filterwarnings(\"ignore\")\n\n# =========================\n# Device configuration\n# =========================\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nprint(f\"Using device: {device}\")\nif device.type == \"cuda\":\n    print(f\"GPU: {torch.cuda.get_device_name(0)}\")\n    print(f\"CUDA: {torch.version.cuda}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T07:26:02.047795Z","iopub.execute_input":"2026-01-07T07:26:02.048095Z","iopub.status.idle":"2026-01-07T07:26:11.427612Z","shell.execute_reply.started":"2026-01-07T07:26:02.048070Z","shell.execute_reply":"2026-01-07T07:26:11.426947Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n# Dataset paths (Kaggle-safe)\n# =========================\nINPUT_DIR = Path(\"/kaggle/input/recodai-luc-scientific-image-forgery-detection\")\nWORKING_DIR = Path(\"/kaggle/working\")\n\n# Train\nTRAIN_DIR = INPUT_DIR / \"train_images\"\nTRAIN_AUTHENTIC_DIR = TRAIN_DIR / \"authentic\"\nTRAIN_FORGED_DIR = TRAIN_DIR / \"forged\"\nTRAIN_MASKS_DIR = INPUT_DIR / \"train_masks\"\n\n# Test\nTEST_IMAGES_DIR = INPUT_DIR / \"test_images\"\n\n# Outputs\nMODEL_PATH = WORKING_DIR / \"best_model.pth\"\nSUBMISSION_PATH = WORKING_DIR / \"submission.csv\"\n\n# =========================\n# Utilities\n# =========================\ndef count_files(path, ext=\".png\"):\n    if not path.exists():\n        return 0\n    return len(list(path.glob(f\"*{ext}\")))\n\n# =========================\n# Sanity checks\n# =========================\nauthentic_count = count_files(TRAIN_AUTHENTIC_DIR)\nforged_count = count_files(TRAIN_FORGED_DIR)\nmask_count = count_files(TRAIN_MASKS_DIR, \".npy\")\ntest_count = count_files(TEST_IMAGES_DIR)\n\nprint(\"=\" * 50)\nprint(\"📂 DATASET SUMMARY\")\nprint(\"=\" * 50)\nprint(f\"Authentic train images : {authentic_count}\")\nprint(f\"Forged train images    : {forged_count}\")\nprint(f\"Forgery masks (.npy)   : {mask_count}\")\nprint(f\"Test images            : {test_count}\")\nprint(\"=\" * 50)\n\n# Hard stop if dataset is broken\nassert authentic_count > 0, \"❌ No authentic training images found\"\nassert forged_count > 0, \"❌ No forged training images found\"\nassert mask_count > 0, \"❌ No mask files found\"\nassert test_count > 0, \"❌ No test images found\"\n\nprint(\"✅ Dataset structure looks valid\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T07:26:11.428648Z","iopub.execute_input":"2026-01-07T07:26:11.428974Z","iopub.status.idle":"2026-01-07T07:26:11.541324Z","shell.execute_reply.started":"2026-01-07T07:26:11.428956Z","shell.execute_reply":"2026-01-07T07:26:11.540702Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n# Mask statistics (CRUCIAL)\n# =========================\n\ndef analyze_masks(masks_dir, sample_size=200):\n    \"\"\"\n    Analyze how much of the image is actually forged.\n    This directly impacts thresholding strategy.\n    \"\"\"\n    mask_files = list(masks_dir.glob(\"*.npy\"))\n    np.random.shuffle(mask_files)\n    mask_files = mask_files[:sample_size]\n\n    ratios = []\n\n    for m in tqdm(mask_files, desc=\"Analyzing masks\"):\n        mask = np.load(m)\n        ratio = mask.sum() / mask.size\n        ratios.append(ratio)\n\n    ratios = np.array(ratios)\n\n    print(\"\\n🧠 MASK STATISTICS\")\n    print(\"-\" * 40)\n    print(f\"Samples analyzed      : {len(ratios)}\")\n    print(f\"Mean forged ratio     : {ratios.mean():.4f}\")\n    print(f\"Median forged ratio   : {np.median(ratios):.4f}\")\n    print(f\"Min forged ratio      : {ratios.min():.4f}\")\n    print(f\"Max forged ratio      : {ratios.max():.4f}\")\n    print(\"-\" * 40)\n\n    return ratios\n\nmask_ratios = analyze_masks(TRAIN_MASKS_DIR, sample_size=300)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T07:26:11.541974Z","iopub.execute_input":"2026-01-07T07:26:11.542147Z","iopub.status.idle":"2026-01-07T07:26:15.839089Z","shell.execute_reply.started":"2026-01-07T07:26:11.542133Z","shell.execute_reply":"2026-01-07T07:26:15.838431Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n# Image resolution analysis (DECISION BLOCK)\n# =========================\n\ndef analyze_image_shapes(image_dir, max_samples=300):\n    shapes = []\n\n    image_files = list(image_dir.glob(\"*.png\"))\n    np.random.shuffle(image_files)\n    image_files = image_files[:max_samples]\n\n    for img_path in tqdm(image_files, desc=f\"Scanning {image_dir.name}\"):\n        img = cv2.imread(str(img_path))\n        if img is None:\n            continue\n        h, w, _ = img.shape\n        shapes.append((h, w))\n\n    shapes = np.array(shapes)\n\n    print(\"\\n📐 IMAGE SHAPE STATS\")\n    print(\"-\" * 40)\n    print(f\"Samples        : {len(shapes)}\")\n    print(f\"Height  min/max: {shapes[:,0].min()} / {shapes[:,0].max()}\")\n    print(f\"Width   min/max: {shapes[:,1].min()} / {shapes[:,1].max()}\")\n    print(f\"Median (H, W)  : {int(np.median(shapes[:,0]))}, {int(np.median(shapes[:,1]))}\")\n    print(\"-\" * 40)\n\n    return shapes\n\n\nauth_shapes = analyze_image_shapes(TRAIN_AUTHENTIC_DIR)\nforg_shapes = analyze_image_shapes(TRAIN_FORGED_DIR)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T07:26:15.840837Z","iopub.execute_input":"2026-01-07T07:26:15.841123Z","iopub.status.idle":"2026-01-07T07:26:38.768780Z","shell.execute_reply.started":"2026-01-07T07:26:15.841101Z","shell.execute_reply":"2026-01-07T07:26:38.767900Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n# Global training parameters (DECIDED)\n# =========================\n\nIMG_SIZE = 640          # chosen from dataset statistics\nBATCH_SIZE = 4          # T4-safe for segmentation\nNUM_WORKERS = 2\nPIN_MEMORY = True\n\n# Thresholds (from mask statistics)\nFORGED_PIXEL_THRESHOLD = 0.01   # 1% pixels -> forged\nMASK_THRESHOLD = 0.5            # sigmoid output threshold\n\nprint(\"📌 TRAINING CONFIG\")\nprint(\"-\" * 40)\nprint(f\"Image size              : {IMG_SIZE} x {IMG_SIZE}\")\nprint(f\"Batch size              : {BATCH_SIZE}\")\nprint(f\"Forged pixel threshold  : {FORGED_PIXEL_THRESHOLD}\")\nprint(f\"Mask binarization thr   : {MASK_THRESHOLD}\")\nprint(\"-\" * 40)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T07:26:38.769702Z","iopub.execute_input":"2026-01-07T07:26:38.769927Z","iopub.status.idle":"2026-01-07T07:26:38.775347Z","shell.execute_reply.started":"2026-01-07T07:26:38.769908Z","shell.execute_reply":"2026-01-07T07:26:38.774501Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n# FINAL SAFE DATASET (NO CV2 CRASH)\n# =========================\nimport torch\nfrom torch.utils.data import Dataset, DataLoader\nimport numpy as np\nimport cv2\nfrom pathlib import Path\nfrom torchvision import transforms\n\nclass ForgeryDataset(Dataset):\n    def __init__(self, samples, img_size=640, augment=False):\n        self.samples = samples\n        self.img_size = img_size\n        self.augment = augment\n        self.image_transform = transforms.ToTensor()\n\n    def _load_image(self, path):\n        img = cv2.imread(str(path))\n        if img is None: raise RuntimeError(f\"Image unreadable: {path}\")\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        img = cv2.resize(img, (self.img_size, self.img_size))\n        return img\n\n    def _load_mask(self, path):\n        if path is None:\n            return np.zeros((self.img_size, self.img_size), dtype=np.float32)\n        \n        try:\n            mask = np.load(path)\n            \n            # --- NETTOYAGE RADICAL DES DIMENSIONS ---\n            # Si le masque est (1, 512, 711, 256) ou (512, 711, 1), etc.\n            # On ne garde que les deux premières dimensions qui ont de la taille\n            if mask.ndim > 2:\n                # On cherche les dimensions qui ne sont pas \"1\" ou \"256\" (souvent une erreur de canal)\n                # Mais le plus simple est de forcer le passage en 2D si c'est possible\n                mask = mask[..., 0] if mask.shape[-1] > 1 and mask.ndim == 3 else mask\n                while mask.ndim > 2:\n                    mask = mask.squeeze()\n            \n            # Si après squeeze c'est encore bizarre, on force le redimensionnement\n            mask = mask.astype(np.float32)\n            \n            # On s'assure qu'on a bien un tableau 2D avant le resize\n            # (OpenCV ne peut pas redimensionner un truc à 4 dimensions)\n            mask = cv2.resize(mask, (self.img_size, self.img_size), interpolation=cv2.INTER_NEAREST)\n            \n            return mask\n        except Exception as e:\n            # En cas d'erreur, on retourne un masque vide de la BONNE taille\n            return np.zeros((self.img_size, self.img_size), dtype=np.float32)\n            \n    def __len__(self):\n        return len(self.samples)\n\n    def __getitem__(self, idx):\n        sample = self.samples[idx]\n        \n        # Chargement via les fonctions utilitaires\n        img = self._load_image(sample[\"image_path\"])\n        mask = self._load_mask(sample[\"mask_path\"])\n\n        # Augmentation de données (Flips)\n        if self.augment:\n            if np.random.rand() < 0.5:\n                img = np.fliplr(img).copy()\n                mask = np.fliplr(mask).copy()\n            if np.random.rand() < 0.5:\n                img = np.flipud(img).copy()\n                mask = np.flipud(mask).copy()\n\n        # Transformation de l'image en Tensor PyTorch (C, H, W) -> (3, 640, 640)\n        img = self.image_transform(img)\n        \n        # --- SÉCURISATION DU MASQUE POUR ÉVITER L'ERREUR RUNTIME ---\n        \n        # 1. Si le masque a 3 dimensions (ex: H, W, C), on ne garde que le premier canal\n        if mask.ndim == 3:\n            mask = mask[:, :, 0]\n            \n        # 2. On transforme en Tensor PyTorch\n        mask = torch.from_numpy(mask).float()\n        \n        # 3. On force la forme (1, H, W)\n        if mask.ndim == 2:\n            mask = mask.unsqueeze(0)\n        elif mask.ndim == 3:\n            # Si par erreur on a encore plusieurs canaux (ex: 256, 640, 640)\n            # On ne garde que le premier : (1, 640, 640)\n            mask = mask[:1, :, :]\n\n        return img, mask","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T07:26:38.776144Z","iopub.execute_input":"2026-01-07T07:26:38.776387Z","iopub.status.idle":"2026-01-07T07:26:38.793292Z","shell.execute_reply.started":"2026-01-07T07:26:38.776362Z","shell.execute_reply":"2026-01-07T07:26:38.792639Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pathlib import Path\nimport numpy as np\n\ntrain_authentic_dir = Path(\"/kaggle/input/recodai-luc-scientific-image-forgery-detection/train_images/authentic\")\ntrain_forged_dir = Path(\"/kaggle/input/recodai-luc-scientific-image-forgery-detection/train_images/forged\")\nsupplemental_images = Path(\"/kaggle/input/recodai-luc-scientific-image-forgery-detection/supplemental_images\")\ntrain_masks_dir = Path(\"/kaggle/input/recodai-luc-scientific-image-forgery-detection/train_masks\")\nsupplemental_masks = Path(\"/kaggle/input/recodai-luc-scientific-image-forgery-detection/supplemental_masks\")\n\ntrain_data = []\nfound_masks = 0\n\n# 1. Images authentiques (Pas de masque)\nfor img in train_authentic_dir.glob(\"*.png\"):\n    train_data.append({\"image_path\": img, \"mask_path\": None})\n\n# 2. Images Forged (On force la vérification)\nfor img in train_forged_dir.glob(\"*.png\"):\n    # On teste les deux emplacements possibles\n    m1 = train_masks_dir / f\"{img.stem}.npy\"\n    m2 = Path(\"/kaggle/input/recodai-luc-scientific-image-forgery-detection/supplemental_masks\") / f\"{img.stem}.npy\"\n    \n    selected_mask = None\n    if m1.exists():\n        selected_mask = m1\n    elif m2.exists():\n        selected_mask = m2\n        \n    if selected_mask:\n        train_data.append({\"image_path\": img, \"mask_path\": selected_mask})\n        found_masks += 1\n    else:\n        # Optionnel: On peut quand même ajouter l'image mais elle sera vue comme \"saine\"\n        # Pour l'instant on ne l'ajoute pas pour purger le dataset\n        pass\n\nprint(f\"Total images chargées : {len(train_data)}\")\nprint(f\"Total masques reliés avec succès : {found_masks}\")\n\n# Mélange et Split\nnp.random.shuffle(train_data)\nsplit = int(0.8 * len(train_data))\ntrain_list = train_data[:split]\nval_list = train_data[split:]\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T07:26:38.793971Z","iopub.execute_input":"2026-01-07T07:26:38.794166Z","iopub.status.idle":"2026-01-07T07:26:41.492081Z","shell.execute_reply.started":"2026-01-07T07:26:38.794150Z","shell.execute_reply":"2026-01-07T07:26:41.491326Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# On définit la taille explicitement pour être raccord avec le modèle\nIMG_SIZE = 640 \nbatch_size = 8 if torch.cuda.is_available() else 4\n\ntrain_loader = DataLoader(\n    # Ajout de img_size=IMG_SIZE pour être sûr\n    ForgeryDataset(train_list, img_size=IMG_SIZE, augment=True),\n    batch_size=batch_size,\n    shuffle=True,\n    num_workers=0,  # Garde 0 sur Kaggle pour éviter les freezes au début\n    pin_memory=True if torch.cuda.is_available() else False\n)\n\nval_loader = DataLoader(\n    ForgeryDataset(val_list, img_size=IMG_SIZE),\n    batch_size=batch_size,\n    shuffle=False,\n    num_workers=0,\n    pin_memory=True if torch.cuda.is_available() else False\n)\n\nprint(f\"Loader prêt : {len(train_loader)} batches d'entraînement.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T07:26:41.492864Z","iopub.execute_input":"2026-01-07T07:26:41.493225Z","iopub.status.idle":"2026-01-07T07:26:41.498709Z","shell.execute_reply.started":"2026-01-07T07:26:41.493199Z","shell.execute_reply":"2026-01-07T07:26:41.497966Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 1. On s'assure que la liste est la bonne\ntrain_loader = DataLoader(\n    ForgeryDataset(train_list, img_size=IMG_SIZE, augment=True), # Vérifie bien IMG_SIZE\n    batch_size=batch_size,\n    shuffle=True,\n    num_workers=0\n)\n\n# 2. Test de vérification avec un seuil de tolérance\nempty_masks = 0\nvalid_masks = 0\n\nprint(\"Démarrage de la vérification du Loader...\")\n\nfor i, (img, mask) in enumerate(train_loader):\n    # On regarde si la somme des pixels est supérieure à un micro-seuil\n    # (car après resize, un pixel à 1 peut devenir 0.0001)\n    sums = mask.view(mask.size(0), -1).sum(dim=1) \n    \n    e = (sums <= 1e-6).sum().item()\n    v = (sums > 1e-6).sum().item()\n    \n    empty_masks += e\n    valid_masks += v\n    \n    if i == 5: # On check juste les 5 premiers batches pour aller vite\n        break\n\nprint(f\"Sur les 5 premiers batches :\")\nprint(f\"EMPTY masks: {empty_masks} | VALID masks: {valid_masks}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T07:26:41.500094Z","iopub.execute_input":"2026-01-07T07:26:41.500362Z","iopub.status.idle":"2026-01-07T07:26:43.977453Z","shell.execute_reply.started":"2026-01-07T07:26:41.500345Z","shell.execute_reply":"2026-01-07T07:26:43.976721Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Diagnostic rapide des noms\nprint(f\"Exemple image forged: {list(train_forged_dir.glob('*.png'))[0].name}\")\nmask_samples = list(train_masks_dir.glob('*.npy'))\nif mask_samples:\n    print(f\"Exemple masque: {mask_samples[0].name}\")\nelse:\n    print(\"❌ AUCUN fichier .npy trouvé dans train_masks_dir\")\n\n# Vérification du lien\nimg_test = list(train_forged_dir.glob('*.png'))[0].stem\nexpected_mask = train_masks_dir / f\"{img_test}.npy\"\nprint(f\"Recherche du masque pour {img_test} à l'adresse : {expected_mask}\")\nprint(f\"Trouvé ? {'OUI' if expected_mask.exists() else 'NON'}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T07:26:43.979416Z","iopub.execute_input":"2026-01-07T07:26:43.979739Z","iopub.status.idle":"2026-01-07T07:26:44.003157Z","shell.execute_reply.started":"2026-01-07T07:26:43.979718Z","shell.execute_reply":"2026-01-07T07:26:44.002437Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\n\n# On prend le premier masque trouvé\nmask_path = list(TRAIN_MASKS_DIR.glob('*.npy'))[0]\nmask_data = np.load(mask_path)\n\nprint(f\"Analyse du masque : {mask_path.name}\")\nprint(f\"Shape du masque : {mask_data.shape}\")\nprint(f\"Valeur Max dans le masque : {mask_data.max()}\")\nprint(f\"Nombre de pixels non nuls : {np.count_nonzero(mask_data)}\")\n\nif mask_data.max() == 0:\n    print(\"❌ ALERTE : Le fichier masque est vide (que des zéros) !\")\nelse:\n    print(\"✅ Le masque contient bien des données de falsification.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T07:26:44.003941Z","iopub.execute_input":"2026-01-07T07:26:44.004572Z","iopub.status.idle":"2026-01-07T07:26:44.021874Z","shell.execute_reply.started":"2026-01-07T07:26:44.004547Z","shell.execute_reply":"2026-01-07T07:26:44.021160Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch.nn.functional as F\n\nclass UNetResNet(nn.Module):\n    def __init__(self):\n        super(UNetResNet, self).__init__()\n        # On charge sans pretrained=True pour éviter l'erreur Internet Off à la soumission\n        # Si tu as ajouté le dataset 'pytorch-pretrained-models', on peut charger les poids locaux ici\n        base = models.resnet18(pretrained=False)\n        \n        self.first = nn.Sequential(base.conv1, base.bn1, base.relu, base.maxpool)\n        self.enc1 = base.layer1 \n        self.enc2 = base.layer2 \n        self.enc3 = base.layer3 \n        \n        self.up1 = nn.ConvTranspose2d(256, 128, 2, 2)\n        self.up2 = nn.ConvTranspose2d(128, 64, 2, 2)\n        self.up3 = nn.ConvTranspose2d(64, 32, 2, 2)\n        self.final = nn.Conv2d(32, 1, 1)\n\n    def forward(self, x):\n        e1 = self.first(x)\n        e2 = self.enc1(e1)\n        e3 = self.enc2(e2)\n        e4 = self.enc3(e3) \n        \n        d1 = F.relu(self.up1(e4))\n        d2 = F.relu(self.up2(d1))\n        d3 = F.relu(self.up3(d2))\n        # On s'assure que la sortie fait exactement la taille IMG_SIZE\n        out = F.interpolate(self.final(d3), size=(IMG_SIZE, IMG_SIZE), mode='bilinear')\n        return torch.sigmoid(out)\n\n# Fonction de perte combinée (BCE + Dice) pour les petites zones falsifiées\ndef competition_loss(pred, target):\n    bce = nn.BCELoss()(pred, target)\n    smooth = 1.\n    intersection = (pred * target).sum(dim=(2, 3))\n    dice = 1 - ((2. * intersection + smooth) / (pred.sum(dim=(2, 3)) + target.sum(dim=(2, 3)) + smooth))\n    return bce + dice.mean()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T07:26:44.022680Z","iopub.execute_input":"2026-01-07T07:26:44.023212Z","iopub.status.idle":"2026-01-07T07:26:44.030351Z","shell.execute_reply.started":"2026-01-07T07:26:44.023185Z","shell.execute_reply":"2026-01-07T07:26:44.029682Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = UNetResNet().to(device)\noptimizer = optim.AdamW(model.parameters(), lr=2e-4, weight_decay=1e-2)\ncriterion = competition_loss\n\nbest_loss = float('inf')\nEPOCHS = 20","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T07:26:44.031127Z","iopub.execute_input":"2026-01-07T07:26:44.031433Z","iopub.status.idle":"2026-01-07T07:26:44.360042Z","shell.execute_reply.started":"2026-01-07T07:26:44.031411Z","shell.execute_reply":"2026-01-07T07:26:44.359461Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\n# --- FONCTION DE VISUALISATION ---\ndef visualize_predictions(model, loader, device, num_samples=3):\n    model.eval()\n    images, masks = next(iter(loader)) # On prend un batch\n    images, masks = images.to(device), masks.to(device)\n    \n    with torch.no_grad():\n        preds = model(images)\n    \n    plt.figure(figsize=(15, 5 * num_samples))\n    for i in range(num_samples):\n        # Image originale\n        plt.subplot(num_samples, 3, i*3 + 1)\n        plt.imshow(images[i].cpu().permute(1, 2, 0))\n        plt.title(\"Image Originale\")\n        plt.axis('off')\n        \n        # Vrai Masque\n        plt.subplot(num_samples, 3, i*3 + 2)\n        plt.imshow(masks[i].cpu().squeeze(), cmap='gray')\n        plt.title(\"Vrai Masque (GT)\")\n        plt.axis('off')\n        \n        # Prédiction du modèle (Aléatoire car non entraîné)\n        plt.subplot(num_samples, 3, i*3 + 3)\n        plt.imshow(preds[i].cpu().squeeze(), cmap='magma')\n        plt.title(\"Prédiction (Avant Train)\")\n        plt.axis('off')\n    \n    plt.tight_layout()\n    plt.show()\n\n# --- ÉTAPE 1 : VISUALISATION ---\nprint(\"🖼️ Visualisation du modèle avant l'entraînement...\")\nvisualize_predictions(model, train_loader, device)\n\n# --- ÉTAPE 2 : CONFIGURATION ---\noptimizer = optim.AdamW(model.parameters(), lr=2e-4, weight_decay=1e-2)\ncriterion = competition_loss\nbest_loss = float('inf')\n\n# --- ÉTAPE 3 : BOUCLE D'ENTRAÎNEMENT ---\nprint(\"\\n🚀 Lancement de l'entraînement...\")\nfor epoch in range(EPOCHS):\n    model.train()\n    train_loss = 0.0\n    pbar = tqdm(train_loader, desc=f\"Epoch {epoch+1}/{EPOCHS}\")\n    \n    for images, masks in pbar:\n        images, masks = images.to(device), masks.to(device)\n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, masks)\n        loss.backward()\n        optimizer.step()\n        train_loss += loss.item()\n        pbar.set_postfix(loss=f\"{loss.item():.4f}\")\n\n    # Validation\n    model.eval()\n    val_loss = 0.0\n    with torch.no_grad():\n        for images, masks in val_loader:\n            images, masks = images.to(device), masks.to(device)\n            outputs = model(images)\n            v_loss = criterion(outputs, masks)\n            val_loss += v_loss.item()\n    \n    avg_train = train_loss / len(train_loader)\n    avg_val = val_loss / len(val_loader)\n    print(f\"Epoch {epoch+1} | Train Loss: {avg_train:.4f} | Val Loss: {avg_val:.4f}\")\n    \n    if avg_val < best_loss:\n        best_loss = avg_val\n        torch.save(model.state_dict(), \"best_model.pth\")\n        print(\"⭐ Nouveau record ! Modèle sauvegardé.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T07:26:44.360854Z","iopub.execute_input":"2026-01-07T07:26:44.361357Z","iopub.status.idle":"2026-01-07T09:16:30.179373Z","shell.execute_reply.started":"2026-01-07T07:26:44.361332Z","shell.execute_reply":"2026-01-07T09:16:30.178523Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def visualize_final_results(model, loader, device, num_samples=5):\n    model.eval()\n    images, masks = next(iter(loader))\n    images, masks = images.to(device), masks.to(device)\n    \n    with torch.no_grad():\n        preds = model(images)\n    \n    plt.figure(figsize=(18, 5 * num_samples))\n    for i in range(num_samples):\n        # Image\n        plt.subplot(num_samples, 3, i*3 + 1)\n        plt.imshow(images[i].cpu().permute(1, 2, 0))\n        plt.title(f\"Image Test {i+1}\")\n        plt.axis('off')\n        \n        # Vrai Masque\n        plt.subplot(num_samples, 3, i*3 + 2)\n        plt.imshow(masks[i].cpu().squeeze(), cmap='gray')\n        plt.title(\"Zone Falsifiée Réelle\")\n        plt.axis('off')\n        \n        # Prédiction\n        plt.subplot(num_samples, 3, i*3 + 3)\n        plt.imshow(preds[i].cpu().squeeze(), cmap='jet')\n        plt.title(\"Détection du Modèle\")\n        plt.axis('off')\n    \n    plt.tight_layout()\n    plt.show()\n\n# Appeler la visualisation\nvisualize_final_results(model, val_loader, device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T09:19:40.499289Z","iopub.execute_input":"2026-01-07T09:19:40.499633Z","iopub.status.idle":"2026-01-07T09:19:43.179180Z","shell.execute_reply.started":"2026-01-07T09:19:40.499607Z","shell.execute_reply":"2026-01-07T09:19:43.178196Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 1. Recharger les meilleurs poids obtenus\nmodel.load_state_dict(torch.load(\"best_model.pth\"))\nmodel.eval()\n\n# 2. Lister les images de test\ntest_image_paths = list(TEST_IMAGES_DIR.glob(\"*.png\"))\nsubmission_results = []\n\nprint(f\"🔍 Génération des prédictions pour {len(test_image_paths)} images...\")\n\nwith torch.no_grad():\n    for path in tqdm(test_image_paths):\n        # Prétraitement identique à l'entraînement\n        img = cv2.imread(str(path))\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        img = cv2.resize(img, (IMG_SIZE, IMG_SIZE))\n        img_t = transforms.ToTensor()(img).unsqueeze(0).to(device)\n        \n        # Prédiction\n        output = model(img_t)\n        \n        # STRATÉGIE DE SCORE : \n        # On considère l'image \"Forged\" (1) si au moins un certain nombre \n        # de pixels dépassent un seuil de probabilité.\n        # Ici : si le pixel le plus suspect est > 0.5\n        prediction_score = output.max().item()\n        is_forged = 1 if prediction_score > 0.5 else 0\n        \n        submission_results.append({\n            \"image_id\": path.stem,\n            \"class\": is_forged\n        })\n\n# 3. Création du fichier CSV\nsubmission_df = pd.DataFrame(submission_results)\n\n# /!\\ IMPORTANT : Vérifie l'ordre ou le nom des colonnes requis par ta compétition\n# Typiquement : image_id, class\nsubmission_df.to_csv(\"submission.csv\", index=False)\n\nprint(\"\\n🚀 Bravo ! 'submission.csv' est prêt pour l'envoi.\")\nsubmission_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T09:20:50.386039Z","iopub.execute_input":"2026-01-07T09:20:50.386344Z","iopub.status.idle":"2026-01-07T09:20:50.479703Z","shell.execute_reply.started":"2026-01-07T09:20:50.386323Z","shell.execute_reply":"2026-01-07T09:20:50.479048Z"}},"outputs":[],"execution_count":null}]}