{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport cv2\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torch.optim as optim\n\nfrom torch.utils.data import Dataset, DataLoader, ConcatDataset\nfrom torch.utils.data import WeightedRandomSampler\n\nfrom torchvision import transforms, models\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import cohen_kappa_score\nfrom sklearn.utils.class_weight import compute_class_weight\n\nfrom tqdm.auto import tqdm\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nprint(\"Device :\", device)\n\ntorch.backends.cudnn.benchmark = True","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T12:46:56.673384Z","iopub.execute_input":"2026-08-08T12:46:56.673574Z","iopub.status.idle":"2026-08-08T12:47:07.686483Z","shell.execute_reply.started":"2026-08-08T12:46:56.673555Z","shell.execute_reply":"2026-08-08T12:47:07.685751Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cellule 2 — Charger APTOS","metadata":{}},{"cell_type":"code","source":"base_dir = \"/kaggle/input/competitions/aptos2019-blindness-detection\"\n\ntrain_csv = os.path.join(base_dir, \"train.csv\")\ntrain_dir = os.path.join(base_dir, \"train_images\")\n\ndf = pd.read_csv(train_csv)\n\nprint(df.head())\nprint(df[\"diagnosis\"].value_counts())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T12:47:07.687918Z","iopub.execute_input":"2026-08-08T12:47:07.688238Z","iopub.status.idle":"2026-08-08T12:47:07.726976Z","shell.execute_reply.started":"2026-08-08T12:47:07.688216Z","shell.execute_reply":"2026-08-08T12:47:07.726246Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cellule 3 — Split","metadata":{}},{"cell_type":"code","source":"train_df, val_df = train_test_split(\n    df,\n    test_size=0.2,\n    stratify=df[\"diagnosis\"],\n    random_state=42\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T12:47:07.727916Z","iopub.execute_input":"2026-08-08T12:47:07.728288Z","iopub.status.idle":"2026-08-08T12:47:07.834592Z","shell.execute_reply.started":"2026-08-08T12:47:07.728263Z","shell.execute_reply":"2026-08-08T12:47:07.833753Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cellule 4 — Oversampling x3 pour classes rares (2, 3, 4)","metadata":{}},{"cell_type":"code","source":"# Oversampling x3 pour les classes minoritaires\nminority_df = train_df[train_df[\"diagnosis\"].isin([2, 3, 4])]\ntrain_df_augmented = pd.concat([train_df, minority_df, minority_df]).reset_index(drop=True)\n\nprint(\"Distribution après oversampling :\")\nprint(train_df_augmented[\"diagnosis\"].value_counts().sort_index())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T12:47:07.835779Z","iopub.execute_input":"2026-08-08T12:47:07.836077Z","iopub.status.idle":"2026-08-08T12:47:07.848581Z","shell.execute_reply.started":"2026-08-08T12:47:07.836053Z","shell.execute_reply":"2026-08-08T12:47:07.847793Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cellule 5 — Crop + CLAHE","metadata":{}},{"cell_type":"code","source":"def crop_image(img):\n\n    gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)\n\n    thresh = cv2.threshold(\n        gray,\n        10,\n        255,\n        cv2.THRESH_BINARY\n    )[1]\n\n    coords = cv2.findNonZero(thresh)\n\n    if coords is not None:\n\n        x, y, w, h = cv2.boundingRect(coords)\n\n        img = img[y:y+h, x:x+w]\n\n    return img\n\n\ndef apply_clahe(img):\n\n    lab = cv2.cvtColor(img, cv2.COLOR_RGB2LAB)\n\n    l, a, b = cv2.split(lab)\n\n    clahe = cv2.createCLAHE(\n        clipLimit=2.0,\n        tileGridSize=(8,8)\n    )\n\n    l = clahe.apply(l)\n\n    lab = cv2.merge((l,a,b))\n\n    img = cv2.cvtColor(\n        lab,\n        cv2.COLOR_LAB2RGB\n    )\n\n    return img","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T12:47:07.850141Z","iopub.execute_input":"2026-08-08T12:47:07.850395Z","iopub.status.idle":"2026-08-08T12:47:07.859365Z","shell.execute_reply.started":"2026-08-08T12:47:07.850372Z","shell.execute_reply":"2026-08-08T12:47:07.858746Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cellule 6 — Augmentation + Resize + Normalize (IMG_SIZE=380 pour EfficientNet-B4)","metadata":{}},{"cell_type":"code","source":"IMG_SIZE = 380   # natif EfficientNet-B4 (vs 224 avant)\n\ntrain_transform = transforms.Compose([\n\n    transforms.ToPILImage(),\n\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n\n    transforms.RandomHorizontalFlip(),\n\n    transforms.RandomVerticalFlip(),\n\n    transforms.RandomRotation(15),\n\n    transforms.ColorJitter(\n        brightness=0.2,\n        contrast=0.2,\n        saturation=0.2\n    ),\n\n    transforms.ToTensor(),\n\n    transforms.Normalize(\n        [0.485,0.456,0.406],\n        [0.229,0.224,0.225]\n    )\n])\n\n\nval_transform = transforms.Compose([\n\n    transforms.ToPILImage(),\n\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n\n    transforms.ToTensor(),\n\n    transforms.Normalize(\n        [0.485,0.456,0.406],\n        [0.229,0.224,0.225]\n    )\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T12:47:07.860775Z","iopub.execute_input":"2026-08-08T12:47:07.861078Z","iopub.status.idle":"2026-08-08T12:47:07.87452Z","shell.execute_reply.started":"2026-08-08T12:47:07.861056Z","shell.execute_reply":"2026-08-08T12:47:07.873855Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cellule 7 — Dataset","metadata":{}},{"cell_type":"code","source":"class AptosDataset(Dataset):\n\n    def __init__(\n        self,\n        dataframe,\n        image_dir,\n        transform=None\n    ):\n        self.df = dataframe.reset_index(drop=True)\n        self.image_dir = image_dir\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n\n        img_name = self.df.loc[idx, \"id_code\"]\n\n        label = self.df.loc[idx, \"diagnosis\"]\n\n        path = os.path.join(\n            self.image_dir,\n            img_name + \".png\"\n        )\n\n        image = cv2.imread(path)\n\n        image = cv2.cvtColor(\n            image,\n            cv2.COLOR_BGR2RGB\n        )\n\n        image = crop_image(image)\n\n        image = apply_clahe(image)\n\n        if self.transform:\n            image = self.transform(image)\n\n        return image, label","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T12:47:07.876693Z","iopub.execute_input":"2026-08-08T12:47:07.877006Z","iopub.status.idle":"2026-08-08T12:47:07.893978Z","shell.execute_reply.started":"2026-08-08T12:47:07.876967Z","shell.execute_reply":"2026-08-08T12:47:07.893216Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cellule 8 — Weighted Random Sampler (appliqué sur train_df_augmented)","metadata":{}},{"cell_type":"code","source":"class_counts = (\n    train_df_augmented[\"diagnosis\"]\n    .value_counts()\n    .sort_index()\n)\n\nweights = 1.0 / class_counts\n\nsample_weights = [\n    weights[label]\n    for label in train_df_augmented[\"diagnosis\"]\n]\n\nsampler = WeightedRandomSampler(\n    weights=torch.tensor(sample_weights, dtype=torch.float),\n    num_samples=len(sample_weights),\n    replacement=True\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T12:47:07.89482Z","iopub.execute_input":"2026-08-08T12:47:07.894996Z","iopub.status.idle":"2026-08-08T12:47:07.956742Z","shell.execute_reply.started":"2026-08-08T12:47:07.894978Z","shell.execute_reply":"2026-08-08T12:47:07.955915Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cellule 9 — Focal Loss (remplace CrossEntropyLoss)","metadata":{}},{"cell_type":"code","source":"class_weights_np = compute_class_weight(\n    class_weight=\"balanced\",\n    classes=np.unique(train_df_augmented[\"diagnosis\"]),\n    y=train_df_augmented[\"diagnosis\"]\n)\n\nclass_weights = torch.tensor(\n    class_weights_np,\n    dtype=torch.float\n).to(device)\n\n\nclass FocalLoss(nn.Module):\n    \"\"\"\n    Focal Loss — pénalise davantage les erreurs sur les classes difficiles (2 et 3).\n    gamma=2 est la valeur standard recommandée par Lin et al. (2017).\n    \"\"\"\n    def __init__(self, gamma=2, weight=None):\n        super().__init__()\n        self.gamma = gamma\n        self.weight = weight\n\n    def forward(self, inputs, targets):\n        ce = F.cross_entropy(\n            inputs,\n            targets,\n            weight=self.weight,\n            reduction='none'\n        )\n        pt = torch.exp(-ce)\n        return ((1 - pt) ** self.gamma * ce).mean()\n\n\ncriterion = FocalLoss(gamma=2, weight=class_weights)\nprint(\"Focal Loss initialisée avec gamma=2 et class_weights.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T12:47:07.957742Z","iopub.execute_input":"2026-08-08T12:47:07.95802Z","iopub.status.idle":"2026-08-08T12:47:08.280261Z","shell.execute_reply.started":"2026-08-08T12:47:07.95799Z","shell.execute_reply":"2026-08-08T12:47:08.27951Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cellule 10 — DataLoaders","metadata":{}},{"cell_type":"code","source":"BATCH_SIZE = 16\n\ntrain_dataset = AptosDataset(\n    train_df_augmented,   # dataset augmenté avec oversampling\n    train_dir,\n    train_transform\n)\n\nval_dataset = AptosDataset(\n    val_df,\n    train_dir,\n    val_transform\n)\n\ntrain_loader = DataLoader(\n    train_dataset,\n    batch_size=BATCH_SIZE,\n    sampler=sampler,\n    num_workers=2,\n    pin_memory=True\n)\n\nval_loader = DataLoader(\n    val_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    num_workers=2,\n    pin_memory=True\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T12:47:08.281229Z","iopub.execute_input":"2026-08-08T12:47:08.281554Z","iopub.status.idle":"2026-08-08T12:47:08.286987Z","shell.execute_reply.started":"2026-08-08T12:47:08.281531Z","shell.execute_reply":"2026-08-08T12:47:08.286141Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cellule 11 — Création du modèle (EfficientNet-B4 upgrade)","metadata":{}},{"cell_type":"code","source":"def create_model(model_name):\n\n    if model_name == \"resnet50\":\n\n        model = models.resnet50(\n            weights=models.ResNet50_Weights.DEFAULT\n        )\n\n        num_features = model.fc.in_features\n\n        model.fc = nn.Linear(\n            num_features,\n            5\n        )\n\n    elif model_name == \"densenet121\":\n\n        model = models.densenet121(\n            weights=models.DenseNet121_Weights.DEFAULT\n        )\n\n        num_features = model.classifier.in_features\n\n        model.classifier = nn.Linear(\n            num_features,\n            5\n        )\n\n    elif model_name == \"efficientnet_b4\":   # ← upgrade B0 → B4\n\n        model = models.efficientnet_b4(\n            weights=models.EfficientNet_B4_Weights.DEFAULT\n        )\n\n        num_features = model.classifier[1].in_features\n\n        model.classifier[1] = nn.Linear(\n            num_features,\n            5\n        )\n\n    return model.to(device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T12:47:08.287974Z","iopub.execute_input":"2026-08-08T12:47:08.288284Z","iopub.status.idle":"2026-08-08T12:47:08.302963Z","shell.execute_reply.started":"2026-08-08T12:47:08.288261Z","shell.execute_reply":"2026-08-08T12:47:08.302135Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cellule 12 — Fine Tuning + Optimizer","metadata":{}},{"cell_type":"code","source":"def setup_finetuning(model_name, model):\n\n    for param in model.parameters():\n        param.requires_grad = False\n\n    if model_name == \"resnet50\":\n\n        for param in model.layer4.parameters():\n            param.requires_grad = True\n\n        for param in model.fc.parameters():\n            param.requires_grad = True\n\n        optimizer = optim.AdamW(\n            filter(lambda p: p.requires_grad, model.parameters()),\n            lr=1e-4\n        )\n\n    elif model_name == \"densenet121\":\n\n        for param in model.features.denseblock4.parameters():\n            param.requires_grad = True\n\n        for param in model.classifier.parameters():\n            param.requires_grad = True\n\n        optimizer = optim.AdamW(\n            filter(lambda p: p.requires_grad, model.parameters()),\n            lr=5e-5\n        )\n\n    elif model_name == \"efficientnet_b4\":\n\n        for param in model.features[-2:].parameters():\n            param.requires_grad = True\n\n        for param in model.classifier.parameters():\n            param.requires_grad = True\n\n        optimizer = optim.AdamW(\n            filter(lambda p: p.requires_grad, model.parameters()),\n            lr=3e-5\n        )\n\n    scheduler = optim.lr_scheduler.ReduceLROnPlateau(\n        optimizer,\n        mode='max',\n        factor=0.5,\n        patience=2\n    )\n\n    return optimizer, scheduler","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T12:47:08.304154Z","iopub.execute_input":"2026-08-08T12:47:08.304439Z","iopub.status.idle":"2026-08-08T12:47:08.320073Z","shell.execute_reply.started":"2026-08-08T12:47:08.304409Z","shell.execute_reply":"2026-08-08T12:47:08.319197Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cellule 13 — Paramètres","metadata":{}},{"cell_type":"code","source":"EPOCHS = 10\n\nMODELS = [\n    \"resnet50\",\n    \"densenet121\",\n    \"efficientnet_b4\"   # ← upgrade\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T12:47:08.320954Z","iopub.execute_input":"2026-08-08T12:47:08.321281Z","iopub.status.idle":"2026-08-08T12:47:08.335595Z","shell.execute_reply.started":"2026-08-08T12:47:08.321251Z","shell.execute_reply":"2026-08-08T12:47:08.334922Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cellule 14 — Optimisation des seuils par classe (remplace argmax brutal)","metadata":{}},{"cell_type":"code","source":"def optimize_thresholds(probs, y_true):\n    \"\"\"\n    Cherche les seuils optimaux sur la validation pour maximiser le Kappa quadratique.\n    Remplace le argmax simple qui pénalise les classes 2 et 3.\n    \"\"\"\n    best_kappa = 0\n    best_thresholds = [0.5, 1.5, 2.5, 3.5]\n\n    for t1 in np.arange(0.3, 0.7, 0.05):\n        for t2 in np.arange(1.3, 1.8, 0.05):\n            for t3 in np.arange(2.3, 2.8, 0.05):\n                thresholds = [t1, t2, t3, 3.5]\n                # Régression ordinale : nombre de seuils dépassés = classe prédite\n                preds = np.sum(probs > np.array(thresholds), axis=1)\n                preds = np.clip(preds, 0, 4)\n                kappa = cohen_kappa_score(y_true, preds, weights=\"quadratic\")\n                if kappa > best_kappa:\n                    best_kappa = kappa\n                    best_thresholds = thresholds.copy()\n\n    return best_thresholds, best_kappa","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T12:47:08.336485Z","iopub.execute_input":"2026-08-08T12:47:08.336764Z","iopub.status.idle":"2026-08-08T12:47:08.348925Z","shell.execute_reply.started":"2026-08-08T12:47:08.336735Z","shell.execute_reply":"2026-08-08T12:47:08.348078Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cellule 15 — Validation","metadata":{}},{"cell_type":"code","source":"def validate(model):\n\n    model.eval()\n\n    preds = []\n    labels_list = []\n\n    with torch.no_grad():\n\n        for images, labels in val_loader:\n\n            images = images.to(device)\n\n            outputs = model(images)\n\n            pred = outputs.argmax(1)\n\n            preds.extend(pred.cpu().numpy())\n\n            labels_list.extend(labels.numpy())\n\n    kappa = cohen_kappa_score(\n        labels_list,\n        preds,\n        weights=\"quadratic\"\n    )\n\n    return kappa","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T12:47:08.349779Z","iopub.execute_input":"2026-08-08T12:47:08.350581Z","iopub.status.idle":"2026-08-08T12:47:08.365515Z","shell.execute_reply.started":"2026-08-08T12:47:08.350558Z","shell.execute_reply":"2026-08-08T12:47:08.364732Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cellule 16 — Génération des probabilités pour Soft Voting","metadata":{}},{"cell_type":"code","source":"def get_probabilities(model):\n\n    model.eval()\n\n    probs = []\n    labels_list = []\n\n    with torch.no_grad():\n\n        for images, labels in val_loader:\n\n            images = images.to(device)\n\n            outputs = model(images)\n\n            p = torch.softmax(\n                outputs,\n                dim=1\n            )\n\n            probs.append(\n                p.cpu().numpy()\n            )\n\n            labels_list.extend(\n                labels.numpy()\n            )\n\n    probs = np.concatenate(probs)\n\n    return probs, np.array(labels_list)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T12:47:08.366389Z","iopub.execute_input":"2026-08-08T12:47:08.366697Z","iopub.status.idle":"2026-08-08T12:47:08.378354Z","shell.execute_reply.started":"2026-08-08T12:47:08.366644Z","shell.execute_reply":"2026-08-08T12:47:08.377805Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cellule 17 — Entraînement complet + Sauvegarde","metadata":{}},{"cell_type":"code","source":"all_probabilities = {}\n\nfor model_name in MODELS:\n\n    print(f\"\\n{'='*50}\")\n    print(f\"Training {model_name}\")\n    print(f\"{'='*50}\")\n\n    model = create_model(model_name)\n\n    optimizer, scheduler = setup_finetuning(\n        model_name,\n        model\n    )\n\n    best_kappa = -1\n\n    for epoch in range(EPOCHS):\n\n        model.train()\n\n        running_loss = 0\n\n        loop = tqdm(\n            train_loader,\n            leave=False\n        )\n\n        for images, labels in loop:\n\n            images = images.to(device)\n            labels = labels.to(device)\n\n            optimizer.zero_grad()\n\n            outputs = model(images)\n\n            loss = criterion(\n                outputs,\n                labels\n            )\n\n            loss.backward()\n\n            optimizer.step()\n\n            running_loss += loss.item()\n\n        avg_loss = (\n            running_loss /\n            len(train_loader)\n        )\n\n        kappa = validate(model)\n\n        scheduler.step(kappa)\n\n        print(\n            f\"Epoch [{epoch+1}/{EPOCHS}] \"\n            f\"Loss={avg_loss:.4f} \"\n            f\"Kappa={kappa:.4f}\"\n        )\n\n        if kappa > best_kappa:\n\n            best_kappa = kappa\n\n            torch.save(\n                model.state_dict(),\n                f\"{model_name}_best.pth\"\n            )\n\n            print(\n                f\"✓ Best model saved\"\n            )\n\n    print(\n        f\"Best Kappa = {best_kappa:.4f}\"\n    )\n\n    model.load_state_dict(\n        torch.load(\n            f\"{model_name}_best.pth\"\n        )\n    )\n\n    probs, y_true = get_probabilities(model)\n\n    np.save(\n        f\"{model_name}_probs.npy\",\n        probs\n    )\n\n    all_probabilities[model_name] = probs","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T12:47:08.379465Z","iopub.execute_input":"2026-08-08T12:47:08.379769Z","iopub.status.idle":"2026-08-08T19:46:57.924303Z","shell.execute_reply.started":"2026-08-08T12:47:08.379736Z","shell.execute_reply":"2026-08-08T19:46:57.923374Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cellule 18 — Soft Voting + Seuils Optimisés","metadata":{}},{"cell_type":"code","source":"resnet_probs = np.load(\"resnet50_probs.npy\")\ndense_probs  = np.load(\"densenet121_probs.npy\")\neff_probs    = np.load(\"efficientnet_b4_probs.npy\")   # ← B4\n\n# Kappas réels obtenus pendant l'entraînement\nkappas = np.array([0.8304, 0.8144, 0.7690])  # resnet, dense, eff_b4\n\n# Poids proportionnels aux kappas (somme = 1)\nweights_vote = kappas / kappas.sum()\nprint(f\"Poids → ResNet: {weights_vote[0]:.4f} | DenseNet: {weights_vote[1]:.4f} | EfficientNet-B4: {weights_vote[2]:.4f}\")\n\nsoft_probs = (\n      weights_vote[0] * resnet_probs\n    + weights_vote[1] * dense_probs\n    + weights_vote[2] * eff_probs\n)\n\n# ── Argmax standard (baseline) ──────────────────────────\nsoft_preds_argmax = np.argmax(soft_probs, axis=1)\n\nsoft_kappa_argmax = cohen_kappa_score(\n    y_true,\n    soft_preds_argmax,\n    weights=\"quadratic\"\n)\nprint(f\"Soft Voting Kappa (argmax)     = {soft_kappa_argmax:.4f}\")\n\n# ── Seuils optimisés ────────────────────────────────────\nbest_thresholds, best_kappa_thresh = optimize_thresholds(soft_probs, y_true)\n\nprint(f\"Seuils optimaux : {[round(t,3) for t in best_thresholds]}\")\nprint(f\"Soft Voting Kappa (seuils opt) = {best_kappa_thresh:.4f}\")\n\n# Prédictions finales avec seuils optimisés\nsoft_preds = np.sum(soft_probs > np.array(best_thresholds), axis=1)\nsoft_preds = np.clip(soft_preds, 0, 4)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T19:47:10.202291Z","iopub.execute_input":"2026-08-08T19:47:10.202755Z","iopub.status.idle":"2026-08-08T19:47:10.220133Z","shell.execute_reply.started":"2026-08-08T19:47:10.202704Z","shell.execute_reply":"2026-08-08T19:47:10.218833Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cellule 19 — Télécharger les modèles","metadata":{}},{"cell_type":"code","source":"from IPython.display import FileLink\n\nFileLink(\"resnet50_best.pth\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T19:47:17.075491Z","iopub.execute_input":"2026-08-08T19:47:17.076326Z","iopub.status.idle":"2026-08-08T19:47:17.082032Z","shell.execute_reply.started":"2026-08-08T19:47:17.076295Z","shell.execute_reply":"2026-08-08T19:47:17.081357Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"FileLink(\"densenet121_best.pth\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T19:47:19.982134Z","iopub.execute_input":"2026-08-08T19:47:19.982604Z","iopub.status.idle":"2026-08-08T19:47:19.989461Z","shell.execute_reply.started":"2026-08-08T19:47:19.982575Z","shell.execute_reply":"2026-08-08T19:47:19.988909Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"FileLink(\"efficientnet_b4_best.pth\")   # ← B4","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T19:47:24.437524Z","iopub.execute_input":"2026-08-08T19:47:24.438304Z","iopub.status.idle":"2026-08-08T19:47:24.444078Z","shell.execute_reply.started":"2026-08-08T19:47:24.438273Z","shell.execute_reply":"2026-08-08T19:47:24.443322Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cellule 20 — Télécharger les probabilités","metadata":{}},{"cell_type":"code","source":"FileLink(\"resnet50_probs.npy\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T19:47:27.171884Z","iopub.execute_input":"2026-08-08T19:47:27.17261Z","iopub.status.idle":"2026-08-08T19:47:27.178092Z","shell.execute_reply.started":"2026-08-08T19:47:27.172578Z","shell.execute_reply":"2026-08-08T19:47:27.177328Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"FileLink(\"densenet121_probs.npy\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T19:47:29.485747Z","iopub.execute_input":"2026-08-08T19:47:29.486299Z","iopub.status.idle":"2026-08-08T19:47:29.491428Z","shell.execute_reply.started":"2026-08-08T19:47:29.48627Z","shell.execute_reply":"2026-08-08T19:47:29.490568Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"FileLink(\"efficientnet_b4_probs.npy\")   # ← B4","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T19:47:32.024204Z","iopub.execute_input":"2026-08-08T19:47:32.024651Z","iopub.status.idle":"2026-08-08T19:47:32.03017Z","shell.execute_reply.started":"2026-08-08T19:47:32.024622Z","shell.execute_reply":"2026-08-08T19:47:32.029234Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cellule 21 — Matrice de confusion","metadata":{}},{"cell_type":"code","source":"import numpy as np\nfrom sklearn.metrics import confusion_matrix, cohen_kappa_score\nimport seaborn as sns\nimport matplotlib.pyplot as plt\n\n# ── Chargement des probabilités ─────────────────────────\nresnet_probs = np.load(\"resnet50_probs.npy\")\ndense_probs  = np.load(\"densenet121_probs.npy\")\neff_probs    = np.load(\"efficientnet_b4_probs.npy\")\n\n# ── Vérification que y_true est défini ──────────────────\n# y_true = np.load(\"y_true.npy\")  # ← décommente si nécessaire\n\n# ── Poids proportionnels aux kappas ─────────────────────\nkappas = np.array([0.8304, 0.8144, 0.7690])\nweights_vote = kappas / kappas.sum()\nprint(f\"Poids → ResNet: {weights_vote[0]:.4f} | DenseNet: {weights_vote[1]:.4f} | EfficientNet-B4: {weights_vote[2]:.4f}\")\n\n# ── Soft Voting ──────────────────────────────────────────\nsoft_probs = (\n      weights_vote[0] * resnet_probs\n    + weights_vote[1] * dense_probs\n    + weights_vote[2] * eff_probs\n)\n\n# ── Argmax baseline ──────────────────────────────────────\nsoft_preds_argmax = np.argmax(soft_probs, axis=1)\nsoft_kappa_argmax = cohen_kappa_score(y_true, soft_preds_argmax, weights=\"quadratic\")\nprint(f\"Soft Voting Kappa (argmax)     = {soft_kappa_argmax:.4f}\")\n\n# ── Seuils optimisés ─────────────────────────────────────\nbest_thresholds, best_kappa_thresh = optimize_thresholds(soft_probs, y_true)\nprint(f\"Seuils optimaux : {[round(t,3) for t in best_thresholds]}\")\nprint(f\"Soft Voting Kappa (seuils opt) = {best_kappa_thresh:.4f}\")\n\n# ── Prédictions finales ───────────────────────────────────\nsoft_preds = np.sum(soft_probs > np.array(best_thresholds), axis=1)\nsoft_preds = np.clip(soft_preds, 0, 4)\n\n# ── Matrice de confusion ──────────────────────────────────\ncm = confusion_matrix(y_true, soft_preds)\n\nplt.figure(figsize=(8, 6))\nsns.heatmap(cm, annot=True, fmt=\"d\", cmap=\"Blues\",\n            xticklabels=range(5), yticklabels=range(5))\nplt.xlabel(\"Predicted\")\nplt.ylabel(\"True\")\nplt.title(\"Matrice de confusion — Soft Voting + Seuils Optimisés\")\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T19:47:33.762075Z","iopub.execute_input":"2026-08-08T19:47:33.762503Z","iopub.status.idle":"2026-08-08T19:47:34.043108Z","shell.execute_reply.started":"2026-08-08T19:47:33.762473Z","shell.execute_reply":"2026-08-08T19:47:34.041992Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nfrom sklearn.metrics import confusion_matrix, cohen_kappa_score\nimport seaborn as sns\nimport matplotlib.pyplot as plt\n\n# ──────────────────────────────────────────────────────────────\n# Fonction d'optimisation des seuils\n# ──────────────────────────────────────────────────────────────\n\ndef optimize_thresholds(probs, y_true):\n    \"\"\"\n    Cherche les seuils optimaux sur la validation\n    pour maximiser le Quadratic Weighted Kappa.\n    Compatible avec les probabilités (N,5).\n    \"\"\"\n\n    # Conversion des probabilités en score ordinal\n    scores = probs @ np.arange(probs.shape[1])\n\n    best_kappa = -1\n    best_thresholds = [0.5, 1.5, 2.5, 3.5]\n\n    for t1 in np.arange(0.3, 0.7, 0.05):\n        for t2 in np.arange(1.3, 1.8, 0.05):\n            for t3 in np.arange(2.3, 2.8, 0.05):\n\n                preds = np.zeros(len(scores), dtype=int)\n                preds[scores >= t1] = 1\n                preds[scores >= t2] = 2\n                preds[scores >= t3] = 3\n                preds[scores >= 3.5] = 4\n\n                kappa = cohen_kappa_score(\n                    y_true,\n                    preds,\n                    weights=\"quadratic\"\n                )\n\n                if kappa > best_kappa:\n                    best_kappa = kappa\n                    best_thresholds = [t1, t2, t3, 3.5]\n\n    return best_thresholds, best_kappa\n\n\n# ──────────────────────────────────────────────────────────────\n# Chargement des probabilités\n# ──────────────────────────────────────────────────────────────\n\nresnet_probs = np.load(\"resnet50_probs.npy\")\ndense_probs = np.load(\"densenet121_probs.npy\")\neff_probs = np.load(\"efficientnet_b4_probs.npy\")\n\n# y_true doit déjà être chargé\n# y_true = np.load(\"y_true.npy\")\n\n# ──────────────────────────────────────────────────────────────\n# Poids du Soft Voting\n# ──────────────────────────────────────────────────────────────\n\nkappas = np.array([0.8304, 0.8144, 0.7690])\nweights_vote = kappas / kappas.sum()\n\nprint(f\"Poids → ResNet: {weights_vote[0]:.4f} | DenseNet: {weights_vote[1]:.4f} | EfficientNet-B4: {weights_vote[2]:.4f}\")\n\n# ──────────────────────────────────────────────────────────────\n# Soft Voting\n# ──────────────────────────────────────────────────────────────\n\nsoft_probs = (\n    weights_vote[0] * resnet_probs +\n    weights_vote[1] * dense_probs +\n    weights_vote[2] * eff_probs\n)\n\n# ──────────────────────────────────────────────────────────────\n# Résultat Argmax\n# ──────────────────────────────────────────────────────────────\n\nsoft_preds_argmax = np.argmax(soft_probs, axis=1)\n\nsoft_kappa_argmax = cohen_kappa_score(\n    y_true,\n    soft_preds_argmax,\n    weights=\"quadratic\"\n)\n\nprint(f\"Soft Voting Kappa (argmax) = {soft_kappa_argmax:.4f}\")\n\n# ──────────────────────────────────────────────────────────────\n# Optimisation des seuils\n# ──────────────────────────────────────────────────────────────\n\nbest_thresholds, best_kappa_thresh = optimize_thresholds(\n    soft_probs,\n    y_true\n)\n\nprint(f\"Seuils optimaux : {[round(t,3) for t in best_thresholds]}\")\nprint(f\"Soft Voting Kappa (seuils optimisés) = {best_kappa_thresh:.4f}\")\n\n# ──────────────────────────────────────────────────────────────\n# Prédictions finales\n# ──────────────────────────────────────────────────────────────\n\nscores = soft_probs @ np.arange(5)\n\nsoft_preds = np.zeros(len(scores), dtype=int)\nsoft_preds[scores >= best_thresholds[0]] = 1\nsoft_preds[scores >= best_thresholds[1]] = 2\nsoft_preds[scores >= best_thresholds[2]] = 3\nsoft_preds[scores >= best_thresholds[3]] = 4\n\n# ──────────────────────────────────────────────────────────────\n# Matrice de confusion\n# ──────────────────────────────────────────────────────────────\n\ncm = confusion_matrix(y_true, soft_preds)\n\nplt.figure(figsize=(8,6))\nsns.heatmap(\n    cm,\n    annot=True,\n    fmt=\"d\",\n    cmap=\"Blues\",\n    xticklabels=range(5),\n    yticklabels=range(5)\n)\n\nplt.xlabel(\"Predicted\")\nplt.ylabel(\"True\")\nplt.title(\"Matrice de confusion — Soft Voting + Seuils optimisés\")\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T20:04:57.94983Z","iopub.execute_input":"2026-08-08T20:04:57.950108Z","iopub.status.idle":"2026-08-08T20:04:58.824164Z","shell.execute_reply.started":"2026-08-08T20:04:57.950086Z","shell.execute_reply":"2026-08-08T20:04:58.823474Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import confusion_matrix\nimport seaborn as sns\nimport matplotlib.pyplot as plt\n\n# ✅ Définir soft_preds à partir des probabilités et d'un seuil\nthreshold = 0.5  # ajuste selon ton seuil optimisé\nsoft_preds = (soft_proba[:, 1] >= threshold).astype(int)\n\ncm = confusion_matrix(y_true, soft_preds)\n\nplt.figure(figsize=(8, 6))\nsns.heatmap(cm, annot=True, fmt=\"d\", cmap=\"Blues\")\nplt.xlabel(\"Predicted\")\nplt.ylabel(\"True\")\nplt.title(\"Matrice de confusion — Soft Voting + Seuils Optimisés\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T19:47:49.829009Z","iopub.execute_input":"2026-08-08T19:47:49.829436Z","iopub.status.idle":"2026-08-08T19:47:49.837497Z","shell.execute_reply.started":"2026-08-08T19:47:49.829408Z","shell.execute_reply":"2026-08-08T19:47:49.836466Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import cohen_kappa_score\n\n# Prédictions individuelles sur la validation\ndef get_preds(model, loader):\n    model.eval()\n    all_preds, all_labels = [], []\n    with torch.no_grad():\n        for images, labels in loader:\n            images = images.to(device)\n            outputs = model(images)\n            preds = outputs.argmax(dim=1).cpu().numpy()\n            all_preds.extend(preds)\n            all_labels.extend(labels.numpy())\n    return np.array(all_preds), np.array(all_labels)\n\npreds_resnet, labels = get_preds(resnet_model, val_loader)\npreds_dense,  _      = get_preds(dense_model,  val_loader)\npreds_eff,    _      = get_preds(eff_model,     val_loader)\n\nkappa_resnet = cohen_kappa_score(labels, preds_resnet, weights=\"quadratic\")\nkappa_dense  = cohen_kappa_score(labels, preds_dense,  weights=\"quadratic\")\nkappa_eff    = cohen_kappa_score(labels, preds_eff,    weights=\"quadratic\")\n\nprint(f\"Kappa ResNet-50       : {kappa_resnet:.4f}\")\nprint(f\"Kappa DenseNet-121    : {kappa_dense:.4f}\")\nprint(f\"Kappa EfficientNet-B4 : {kappa_eff:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T20:05:18.714449Z","iopub.execute_input":"2026-08-08T20:05:18.714877Z","iopub.status.idle":"2026-08-08T20:05:18.723699Z","shell.execute_reply.started":"2026-08-08T20:05:18.714847Z","shell.execute_reply":"2026-08-08T20:05:18.722724Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def optimize_thresholds(probs, y_true):\n    \"\"\"\n    Accepte probs de shape (N, C) ou score de shape (N,).\n    Si (N, C) → convertit en score continu via argmax pondéré.\n    \"\"\"\n    # ── Conversion en score continu si matrice de probs ───\n    if probs.ndim == 2:\n        scores = probs @ np.arange(probs.shape[1])  # (N,) — ex: (733,)\n    else:\n        scores = probs  # déjà 1D\n\n    best_kappa = 0\n    best_thresholds = [0.5, 1.5, 2.5, 3.5]\n\n    for t1 in np.arange(0.3, 0.7, 0.05):\n        for t2 in np.arange(1.3, 1.8, 0.05):\n            for t3 in np.arange(2.3, 2.8, 0.05):\n                thresholds = [t1, t2, t3, 3.5]\n                # Score 1D comparé à 4 seuils scalaires ✅\n                preds = np.zeros(len(scores), dtype=int)\n                preds[scores >= t1] = 1\n                preds[scores >= t2] = 2\n                preds[scores >= t3] = 3\n                preds[scores >= 3.5] = 4\n                preds = np.clip(preds, 0, 4)\n\n                kappa = cohen_kappa_score(y_true, preds, weights=\"quadratic\")\n                if kappa > best_kappa:\n                    best_kappa = kappa\n                    best_thresholds = thresholds.copy()\n\n    return best_thresholds, best_kappa\n\n\n# ── Application des seuils (même logique) ─────────────────\ndef apply_thresholds(probs, thresholds):\n    if probs.ndim == 2:\n        scores = probs @ np.arange(probs.shape[1])\n    else:\n        scores = probs\n\n    t1, t2, t3, t4 = thresholds\n    preds = np.zeros(len(scores), dtype=int)\n    preds[scores >= t1] = 1\n    preds[scores >= t2] = 2\n    preds[scores >= t3] = 3\n    preds[scores >= t4] = 4\n    return np.clip(preds, 0, 4)\n\n\n# ── Pipeline complet ──────────────────────────────────────\nbest_thresholds, best_kappa_thresh = optimize_thresholds(soft_probs, y_true)\nprint(f\"Seuils optimaux : {[round(t,3) for t in best_thresholds]}\")\nprint(f\"Soft Voting Kappa (seuils opt) = {best_kappa_thresh:.4f}\")\n\nsoft_preds = apply_thresholds(soft_probs, best_thresholds)\n\n# ── Matrice de confusion ──────────────────────────────────\ncm = confusion_matrix(y_true, soft_preds)\nplt.figure(figsize=(8, 6))\nsns.heatmap(cm, annot=True, fmt=\"d\", cmap=\"Blues\",\n            xticklabels=range(5), yticklabels=range(5))\nplt.xlabel(\"Predicted\")\nplt.ylabel(\"True\")\nplt.title(\"Matrice de confusion — Soft Voting + Seuils Optimisés\")\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cellule 22 — Accuracy, Precision, Recall, F1","metadata":{}},{"cell_type":"code","source":"from sklearn.metrics import classification_report\n\nprint(\n    classification_report(\n        y_true,\n        soft_preds\n    )\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T20:05:27.852083Z","iopub.execute_input":"2026-08-08T20:05:27.852842Z","iopub.status.idle":"2026-08-08T20:05:27.866102Z","shell.execute_reply.started":"2026-08-08T20:05:27.85281Z","shell.execute_reply":"2026-08-08T20:05:27.865427Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torchvision.models as models\n\n# ── 1. Instancier les architectures ───────────────────────\nresnet_model = models.resnet50(weights=None)\nresnet_model.fc = torch.nn.Linear(resnet_model.fc.in_features, 5)\n\ndense_model = models.densenet121(weights=None)\ndense_model.classifier = torch.nn.Linear(dense_model.classifier.in_features, 5)\n\nfrom torchvision.models import efficientnet_b4\neff_model = efficientnet_b4(weights=None)\neff_model.classifier[1] = torch.nn.Linear(eff_model.classifier[1].in_features, 5)\n\n# ── 2. Charger les poids sauvegardés ─────────────────────\nresnet_model.load_state_dict(torch.load(\"resnet50_best.pth\",      map_location=device))\ndense_model.load_state_dict(torch.load(\"densenet121_best.pth\",    map_location=device))\neff_model.load_state_dict(torch.load(\"efficientnet_b4_best.pth\",  map_location=device))\n\n# ── 3. Mettre en mode évaluation ──────────────────────────\nresnet_model = resnet_model.to(device).eval()\ndense_model  = dense_model.to(device).eval()\neff_model    = eff_model.to(device).eval()\n\nprint(\"✅ Modèles chargés et prêts\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T20:05:31.051449Z","iopub.execute_input":"2026-08-08T20:05:31.051911Z","iopub.status.idle":"2026-08-08T20:05:32.420921Z","shell.execute_reply.started":"2026-08-08T20:05:31.05188Z","shell.execute_reply":"2026-08-08T20:05:32.420215Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from PIL import Image\nimport torch\nimport numpy as np\nimport matplotlib.pyplot as plt\n\n# ── 1. Charger l'image .tif ───────────────────────────────\nimage_path = \"/kaggle/input/datasets/andrewmvd/drive-digital-retinal-images-for-vessel-extraction/DRIVE/test/images/01_test.tif\"\n\ntransform = transforms.Compose([\n    transforms.Resize((380, 380)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406],\n                         [0.229, 0.224, 0.225])\n])\n\nimg      = Image.open(image_path).convert(\"RGB\")\nimg_tensor = transform(img).unsqueeze(0)  # (1, 3, 380, 380)\n\n# ── 2. Probabilités de chaque modèle ─────────────────────\ndef predict_single(model, tensor):\n    model.eval()\n    with torch.no_grad():\n        output = model(tensor.to(device))\n        return torch.softmax(output, dim=1).cpu().numpy()  # (1, 5)\n\nresnet_p = predict_single(resnet_model, img_tensor)\ndense_p  = predict_single(dense_model,  img_tensor)\neff_p    = predict_single(eff_model,    img_tensor)\n\n# ── 3. Soft Voting ────────────────────────────────────────\nsoft_p = (\n      weights_vote[0] * resnet_p\n    + weights_vote[1] * dense_p\n    + weights_vote[2] * eff_p\n)\n\n# ── 4. Classe prédite via seuils optimisés ────────────────\npred_class = apply_thresholds(soft_p, best_thresholds)[0]\n\n# ── 5. Résultat ───────────────────────────────────────────\nclass_names = [\"No DR\", \"Mild\", \"Moderate\", \"Severe\", \"Proliferative DR\"]\nprint(f\"Classe prédite : {pred_class} — {class_names[pred_class]}\")\nprint(f\"Probabilités   :\")\nfor i, c in enumerate(class_names):\n    print(f\"  {c:<20} : {soft_p[0][i]:.4f}\")\n\n# ── 6. Visualisation ──────────────────────────────────────\nfig, axes = plt.subplots(1, 2, figsize=(12, 4))\n\naxes[0].imshow(img)\naxes[0].set_title(f\"01_test.tif\\nPrédiction : {class_names[pred_class]}\", fontsize=13)\naxes[0].axis(\"off\")\n\nbars = axes[1].bar(class_names, soft_p[0], color=[\"green\" if i == pred_class else \"steelblue\" for i in range(5)])\naxes[1].set_ylabel(\"Probabilité\")\naxes[1].set_title(\"Distribution Soft Voting\")\naxes[1].set_ylim(0, 1)\naxes[1].tick_params(axis='x', rotation=15)\nfor bar, val in zip(bars, soft_p[0]):\n    axes[1].text(bar.get_x() + bar.get_width()/2, val + 0.01, f\"{val:.3f}\", ha=\"center\", fontsize=9)\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T20:05:35.530428Z","iopub.execute_input":"2026-08-08T20:05:35.531192Z","iopub.status.idle":"2026-08-08T20:05:38.483968Z","shell.execute_reply.started":"2026-08-08T20:05:35.531163Z","shell.execute_reply":"2026-08-08T20:05:38.482852Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from PIL import Image\nimport torch\nimport torch.nn.functional as F\nimport numpy as np\nimport matplotlib.pyplot as plt\n\n# ── 1. Charger l'image .tif ───────────────────────────────\nimage_path = \"/kaggle/input/datasets/andrewmvd/drive-digital-retinal-images-for-vessel-extraction/DRIVE/test/images/01_test.tif\"\n\ntransform = transforms.Compose([\n    transforms.Resize((380, 380)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406],\n                         [0.229, 0.224, 0.225])\n])\n\nimg        = Image.open(image_path).convert(\"RGB\")\nimg_tensor = transform(img).unsqueeze(0)\nimg_show   = img.resize((380, 380))  # pour l'affichage\n\n# ── 2. Grad-CAM ───────────────────────────────────────────\nclass GradCAM:\n    def __init__(self, model, target_layer):\n        self.model       = model\n        self.activations = None\n        self.gradients   = None\n        target_layer.register_forward_hook(self._save_activation)\n        target_layer.register_full_backward_hook(self._save_gradient)\n\n    def _save_activation(self, module, input, output):\n        self.activations = output.detach()\n\n    def _save_gradient(self, module, grad_input, grad_output):\n        self.gradients = grad_output[0].detach()\n\n    def generate(self, tensor, class_idx=None):\n        self.model.eval()\n        output = self.model(tensor.to(device))\n        if class_idx is None:\n            class_idx = output.argmax(dim=1).item()\n        self.model.zero_grad()\n        output[0, class_idx].backward()\n        weights = self.gradients.mean(dim=[2, 3], keepdim=True)\n        cam     = F.relu((weights * self.activations).sum(dim=1, keepdim=True))\n        cam     = F.interpolate(cam, size=(380, 380), mode=\"bilinear\", align_corners=False)\n        cam     = cam.squeeze().cpu().numpy()\n        cam     = (cam - cam.min()) / (cam.max() - cam.min() + 1e-8)\n        return cam\n\ndef get_gradcam_overlay(cam, img_pil):\n    heatmap = plt.cm.jet(cam)[:, :, :3]          # (380,380,3) float\n    base    = np.array(img_pil) / 255.0\n    overlay = 0.5 * base + 0.5 * heatmap\n    return np.clip(overlay, 0, 1)\n\n# Couches cibles\ngcam_resnet = GradCAM(resnet_model, resnet_model.layer4[-1])\ngcam_dense  = GradCAM(dense_model,  dense_model.features.denseblock4)\ngcam_eff    = GradCAM(eff_model,    eff_model.features[-1])\n\n# ── 3. Probabilités de chaque modèle ─────────────────────\ndef predict_single(model, tensor):\n    model.eval()\n    with torch.no_grad():\n        output = model(tensor.to(device))\n        return torch.softmax(output, dim=1).cpu().numpy()\n\nresnet_p = predict_single(resnet_model, img_tensor)\ndense_p  = predict_single(dense_model,  img_tensor)\neff_p    = predict_single(eff_model,    img_tensor)\n\n# ── 4. Soft Voting ────────────────────────────────────────\nsoft_p = (\n      weights_vote[0] * resnet_p\n    + weights_vote[1] * dense_p\n    + weights_vote[2] * eff_p\n)\n\n# ── 5. Classe prédite via seuils optimisés ────────────────\npred_class  = apply_thresholds(soft_p, best_thresholds)[0]\nclass_names = [\"No DR\", \"Mild\", \"Moderate\", \"Severe\", \"Proliferative DR\"]\n\n# Générer les cartes Grad-CAM pour la classe prédite\ncam_resnet  = gcam_resnet.generate(img_tensor, class_idx=pred_class)\ncam_dense   = gcam_dense.generate(img_tensor,  class_idx=pred_class)\ncam_eff     = gcam_eff.generate(img_tensor,    class_idx=pred_class)\n\noverlay_resnet = get_gradcam_overlay(cam_resnet, img_show)\noverlay_dense  = get_gradcam_overlay(cam_dense,  img_show)\noverlay_eff    = get_gradcam_overlay(cam_eff,    img_show)\n\n# ── 6. Résultat terminal ──────────────────────────────────\nprint(f\"Classe prédite : {pred_class} — {class_names[pred_class]}\")\nprint(\"Probabilités   :\")\nfor i, c in enumerate(class_names):\n    print(f\"  {c:<20} : {soft_p[0][i]:.4f}\")\n\n# ── 7. Visualisation ──────────────────────────────────────\nfig, axes = plt.subplots(2, 4, figsize=(18, 9))\nfig.suptitle(f\"01_test.tif  —  Prédiction : Grade {pred_class} ({class_names[pred_class]})\",\n             fontsize=14, fontweight=\"bold\")\n\n# Ligne 0 : image originale + overlays Grad-CAM\naxes[0, 0].imshow(img_show)\naxes[0, 0].set_title(\"Image originale\", fontsize=11)\naxes[0, 0].axis(\"off\")\n\nfor ax, overlay, title in zip(\n    axes[0, 1:],\n    [overlay_resnet, overlay_dense, overlay_eff],\n    [\"Grad-CAM ResNet-50\", \"Grad-CAM DenseNet-121\", \"Grad-CAM EfficientNet-B4\"]\n):\n    ax.imshow(overlay)\n    ax.set_title(title, fontsize=10)\n    ax.axis(\"off\")\n\n# Ligne 1 : distribution softmax (soft voting) + probabilités individuelles\nbar_colors = [\"green\" if i == pred_class else \"steelblue\" for i in range(5)]\n\ntitles_probs = [\n    (\"Soft Voting (final)\", soft_p[0]),\n    (\"ResNet-50\",           resnet_p[0]),\n    (\"DenseNet-121\",        dense_p[0]),\n    (\"EfficientNet-B4\",     eff_p[0]),\n]\n\nfor ax, (title, probs) in zip(axes[1], titles_probs):\n    bars = ax.bar(range(5), probs, color=bar_colors, edgecolor=\"black\", linewidth=0.5)\n    bars[pred_class].set_edgecolor(\"red\")\n    bars[pred_class].set_linewidth(2.5)\n    ax.set_xticks(range(5))\n    ax.set_xticklabels([\"G0\", \"G1\", \"G2\", \"G3\", \"G4\"], fontsize=9)\n    ax.set_ylim(0, 1)\n    ax.set_ylabel(\"Probabilité\", fontsize=9)\n    ax.set_title(title, fontsize=10)\n    for bar, val in zip(bars, probs):\n        ax.text(bar.get_x() + bar.get_width() / 2, val + 0.02,\n                f\"{val:.2f}\", ha=\"center\", fontsize=8)\n\nplt.tight_layout()\nplt.savefig(\"gradcam_drive_01_test.png\", dpi=150, bbox_inches=\"tight\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T20:05:47.677714Z","iopub.execute_input":"2026-08-08T20:05:47.678125Z","iopub.status.idle":"2026-08-08T20:05:47.781954Z","shell.execute_reply.started":"2026-08-08T20:05:47.678096Z","shell.execute_reply":"2026-08-08T20:05:47.780961Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ======================= INSTALLATION ==========================\n!pip install -q grad-cam\n\n# ======================= IMPORTS ===============================\nimport cv2\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport torch\n\nfrom pytorch_grad_cam import GradCAM\nfrom pytorch_grad_cam.utils.image import show_cam_on_image\n\n# ===============================================================\n# Choisir le modèle\n# Remplacer par :\n# model = resnet_model\n# model = densenet_model\n# model = efficientnet_model\n# ===============================================================\n\nmodel.eval()\n\n# ===============================================================\n# Détection automatique de la dernière couche convolutive\n# ===============================================================\n\nmodel_name = model.__class__.__name__.lower()\n\nif \"resnet\" in model_name:\n    target_layers = [model.layer4[-1]]\n\nelif \"densenet\" in model_name:\n    target_layers = [model.features[-1]]\n\nelif \"efficientnet\" in model_name:\n    target_layers = [model.features[-1]]\n\nelse:\n    raise ValueError(\"Architecture non supportée.\")\n\n# ===============================================================\n# Image\n# ===============================================================\n\nimage_path = \"/kaggle/input/aptos2019-blindness-detection/train_images/000c1434d8d7.png\"\n\nimg = cv2.imread(image_path)\nimg = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\nimg = cv2.resize(img,(224,224))\n\nrgb_img = img.astype(np.float32)/255.\n\n# ===============================================================\n# Prétraitement\n# ===============================================================\n\nmean = np.array([0.485,0.456,0.406])\nstd = np.array([0.229,0.224,0.225])\n\ninput_tensor = (rgb_img-mean)/std\ninput_tensor = np.transpose(input_tensor,(2,0,1))\ninput_tensor = torch.tensor(input_tensor,dtype=torch.float32).unsqueeze(0).to(device)\n\n# ===============================================================\n# Prédiction\n# ===============================================================\n\nwith torch.no_grad():\n    output = model(input_tensor)\n    pred = torch.argmax(output,dim=1).item()\n\nprint(\"Classe prédite :\",pred)\n\n# ===============================================================\n# GradCAM\n# ===============================================================\n\ncam = GradCAM(\n    model=model,\n    target_layers=target_layers\n)\n\ngrayscale_cam = cam(\n    input_tensor=input_tensor\n)[0]\n\nvisualization = show_cam_on_image(\n    rgb_img,\n    grayscale_cam,\n    use_rgb=True\n)\n\n# ===============================================================\n# Affichage\n# ===============================================================\n\nplt.figure(figsize=(12,5))\n\nplt.subplot(1,2,1)\nplt.imshow(rgb_img)\nplt.title(\"Image originale\")\nplt.axis(\"off\")\n\nplt.subplot(1,2,2)\nplt.imshow(visualization)\nplt.title(\"Grad-CAM\")\nplt.axis(\"off\")\n\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T20:12:19.51203Z","iopub.execute_input":"2026-08-08T20:12:19.51281Z","iopub.status.idle":"2026-08-08T20:12:31.691316Z","shell.execute_reply.started":"2026-08-08T20:12:19.51278Z","shell.execute_reply":"2026-08-08T20:12:31.690154Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ══════════════════════════════════════════════════════════\n# CELLULE AUTONOME — Grad-CAM (tout le nécessaire est ici)\n# ══════════════════════════════════════════════════════════\n\nimport os\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom PIL import Image\nfrom torchvision import transforms\nimport torchvision.models as models\nfrom torchvision.models import efficientnet_b4\n\n# ── 0. Device ──────────────────────────────────────────────\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# ── 1. Chemins des poids sauvegardés ────────────────────────\nRESNET_PATH = \"resnet50_best.pth\"\nDENSE_PATH  = \"densenet121_best.pth\"\nEFF_PATH    = \"efficientnet_b4_best.pth\"\n\n# ── 2. Reconstruire les architectures ──────────────────────\nresnet_model = models.resnet50(weights=None)\nresnet_model.fc = nn.Linear(resnet_model.fc.in_features, 5)\n\ndense_model = models.densenet121(weights=None)\ndense_model.classifier = nn.Linear(dense_model.classifier.in_features, 5)\n\neff_model = efficientnet_b4(weights=None)\neff_model.classifier[1] = nn.Linear(eff_model.classifier[1].in_features, 5)\n\n# ── 3. Charger les poids ────────────────────────────────────\nresnet_model.load_state_dict(torch.load(RESNET_PATH, map_location=device))\ndense_model.load_state_dict(torch.load(DENSE_PATH,  map_location=device))\neff_model.load_state_dict(torch.load(EFF_PATH,     map_location=device))\n\nresnet_model.to(device).eval()\ndense_model.to(device).eval()\neff_model.to(device).eval()\n\n# ── 4. Poids du soft voting (kappas de l'entraînement) ──────\nkappas = np.array([0.8304, 0.8144, 0.7690])  # resnet, dense, eff_b4\nweights_vote = kappas / kappas.sum()\nprint(f\"Poids → ResNet: {weights_vote[0]:.4f} | DenseNet: {weights_vote[1]:.4f} | EfficientNet-B4: {weights_vote[2]:.4f}\")\n\n# ── 5. Seuils optimisés (tes vraies valeurs) ────────────────\ndef apply_thresholds(probs, thresholds):\n    if probs.ndim == 2:\n        scores = probs @ np.arange(probs.shape[1])\n    else:\n        scores = probs\n    t1, t2, t3, t4 = thresholds\n    preds = np.zeros(len(scores), dtype=int)\n    preds[scores >= t1] = 1\n    preds[scores >= t2] = 2\n    preds[scores >= t3] = 3\n    preds[scores >= t4] = 4\n    return np.clip(preds, 0, 4)\n\nbest_thresholds = [0.6, 1.3, 2.75, 3.5]\n\n# ── 6. Classe Grad-CAM (avec retrait des hooks) ─────────────\nclass GradCAM:\n    def __init__(self, model, target_layer):\n        self.model = model\n        self.activations = None\n        self.gradients = None\n        self._fwd_handle = target_layer.register_forward_hook(self._save_activation)\n        self._bwd_handle = target_layer.register_full_backward_hook(self._save_gradient)\n\n    def _save_activation(self, module, input, output):\n        self.activations = output.detach()\n\n    def _save_gradient(self, module, grad_input, grad_output):\n        self.gradients = grad_output[0].detach()\n\n    def generate(self, tensor, class_idx=None):\n        self.model.eval()\n        output = self.model(tensor.to(device))\n        if class_idx is None:\n            class_idx = output.argmax(dim=1).item()\n        self.model.zero_grad()\n        output[0, class_idx].backward()\n        weights = self.gradients.mean(dim=[2, 3], keepdim=True)\n        cam = F.relu((weights * self.activations).sum(dim=1, keepdim=True))\n        cam = F.interpolate(cam, size=(380, 380), mode=\"bilinear\", align_corners=False)\n        cam = cam.squeeze().cpu().numpy()\n        cam = (cam - cam.min()) / (cam.max() - cam.min() + 1e-8)\n        return cam\n\n    def remove_hooks(self):\n        self._fwd_handle.remove()\n        self._bwd_handle.remove()\n\n\ndef get_gradcam_overlay(cam, img_pil):\n    heatmap = plt.cm.jet(cam)[:, :, :3]\n    base = np.array(img_pil) / 255.0\n    overlay = 0.5 * base + 0.5 * heatmap\n    return np.clip(overlay, 0, 1)\n\n# ══════════════════════════════════════════════════════════\n# CELLULE GRAD-CAM\n# ══════════════════════════════════════════════════════════\n\n# ── 7. Charger l'image .tif ─────────────────────────────────\nimage_path = \"/kaggle/input/datasets/andrewmvd/drive-digital-retinal-images-for-vessel-extraction/DRIVE/test/images/01_test.tif\"\n\ntransform = transforms.Compose([\n    transforms.Resize((380, 380)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406],\n                          [0.229, 0.224, 0.225])\n])\n\nimg = Image.open(image_path).convert(\"RGB\")\nimg_tensor = transform(img).unsqueeze(0)\nimg_show = img.resize((380, 380))\n\n# ── 8. Instancier Grad-CAM sur chaque modèle ────────────────\ngcam_resnet = GradCAM(resnet_model, resnet_model.layer4[-1])\ngcam_dense  = GradCAM(dense_model,  dense_model.features.denseblock4)\ngcam_eff    = GradCAM(eff_model,    eff_model.features[-1])\n\n# ── 9. Probabilités de chaque modèle ────────────────────────\ndef predict_single(model, tensor):\n    model.eval()\n    with torch.no_grad():\n        output = model(tensor.to(device))\n        return torch.softmax(output, dim=1).cpu().numpy()\n\nresnet_p = predict_single(resnet_model, img_tensor)\ndense_p  = predict_single(dense_model,  img_tensor)\neff_p    = predict_single(eff_model,    img_tensor)\n\n# ── 10. Soft Voting + classe prédite ────────────────────────\nsoft_p = (\n      weights_vote[0] * resnet_p\n    + weights_vote[1] * dense_p\n    + weights_vote[2] * eff_p\n)\n\npred_class  = apply_thresholds(soft_p, best_thresholds)[0]\nclass_names = [\"No DR\", \"Mild\", \"Moderate\", \"Severe\", \"Proliferative DR\"]\n\n# ── 11. Cartes Grad-CAM pour la classe prédite ──────────────\ncam_resnet = gcam_resnet.generate(img_tensor, class_idx=pred_class)\ncam_dense  = gcam_dense.generate(img_tensor,  class_idx=pred_class)\ncam_eff    = gcam_eff.generate(img_tensor,    class_idx=pred_class)\n\noverlay_resnet = get_gradcam_overlay(cam_resnet, img_show)\noverlay_dense  = get_gradcam_overlay(cam_dense,  img_show)\noverlay_eff    = get_gradcam_overlay(cam_eff,    img_show)\n\n# ── 12. Nettoyage des hooks ──────────────────────────────────\ngcam_resnet.remove_hooks()\ngcam_dense.remove_hooks()\ngcam_eff.remove_hooks()\n\n# ── 13. Résultat terminal ───────────────────────────────────\nprint(f\"Classe prédite : {pred_class} — {class_names[pred_class]}\")\nprint(\"Probabilités   :\")\nfor i, c in enumerate(class_names):\n    print(f\"  {c:<20} : {soft_p[0][i]:.4f}\")\n\n# ── 14. Visualisation ───────────────────────────────────────\nfig, axes = plt.subplots(2, 4, figsize=(18, 9))\nfig.suptitle(f\"01_test.tif  —  Prédiction : Grade {pred_class} ({class_names[pred_class]})\",\n             fontsize=14, fontweight=\"bold\")\n\naxes[0, 0].imshow(img_show)\naxes[0, 0].set_title(\"Image originale\", fontsize=11)\naxes[0, 0].axis(\"off\")\n\nfor ax, overlay, title in zip(\n    axes[0, 1:],\n    [overlay_resnet, overlay_dense, overlay_eff],\n    [\"Grad-CAM ResNet-50\", \"Grad-CAM DenseNet-121\", \"Grad-CAM EfficientNet-B4\"]\n):\n    ax.imshow(overlay)\n    ax.set_title(title, fontsize=10)\n    ax.axis(\"off\")\n\nbar_colors = [\"green\" if i == pred_class else \"steelblue\" for i in range(5)]\ntitles_probs = [\n    (\"Soft Voting (final)\", soft_p[0]),\n    (\"ResNet-50\",           resnet_p[0]),\n    (\"DenseNet-121\",        dense_p[0]),\n    (\"EfficientNet-B4\",     eff_p[0]),\n]\n\nfor ax, (title, probs) in zip(axes[1], titles_probs):\n    bars = ax.bar(range(5), probs, color=bar_colors, edgecolor=\"black\", linewidth=0.5)\n    bars[pred_class].set_edgecolor(\"red\")\n    bars[pred_class].set_linewidth(2.5)\n    ax.set_xticks(range(5))\n    ax.set_xticklabels([\"G0\", \"G1\", \"G2\", \"G3\", \"G4\"], fontsize=9)\n    ax.set_ylim(0, 1)\n    ax.set_ylabel(\"Probabilité\", fontsize=9)\n    ax.set_title(title, fontsize=10)\n    for bar, val in zip(bars, probs):\n        ax.text(bar.get_x() + bar.get_width() / 2, val + 0.02,\n                f\"{val:.2f}\", ha=\"center\", fontsize=8)\n\nplt.tight_layout()\nplt.savefig(\"gradcam_drive_01_test.png\", dpi=150, bbox_inches=\"tight\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T20:30:28.561985Z","iopub.execute_input":"2026-08-08T20:30:28.562506Z","iopub.status.idle":"2026-08-08T20:31:38.848578Z","shell.execute_reply.started":"2026-08-08T20:30:28.562469Z","shell.execute_reply":"2026-08-08T20:31:38.847736Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ══════════════════════════════════════════════════════════\n# CELLULE AUTONOME — Grad-CAM (tout le nécessaire est ici)\n# ══════════════════════════════════════════════════════════\n\nimport os\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom PIL import Image\nfrom torchvision import transforms\nimport torchvision.models as models\nfrom torchvision.models import efficientnet_b4\n\n# ── 0. Device ──────────────────────────────────────────────\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# ── 1. Chemins des poids sauvegardés ────────────────────────\nRESNET_PATH = \"resnet50_best.pth\"\nDENSE_PATH  = \"densenet121_best.pth\"\nEFF_PATH    = \"efficientnet_b4_best.pth\"\n\n# ── 2. Reconstruire les architectures ──────────────────────\nresnet_model = models.resnet50(weights=None)\nresnet_model.fc = nn.Linear(resnet_model.fc.in_features, 5)\n\ndense_model = models.densenet121(weights=None)\ndense_model.classifier = nn.Linear(dense_model.classifier.in_features, 5)\n\neff_model = efficientnet_b4(weights=None)\neff_model.classifier[1] = nn.Linear(eff_model.classifier[1].in_features, 5)\n\n# ── 3. Charger les poids ────────────────────────────────────\nresnet_model.load_state_dict(torch.load(RESNET_PATH, map_location=device))\ndense_model.load_state_dict(torch.load(DENSE_PATH,  map_location=device))\neff_model.load_state_dict(torch.load(EFF_PATH,     map_location=device))\n\nresnet_model.to(device).eval()\ndense_model.to(device).eval()\neff_model.to(device).eval()\n\n# ── 4. Poids du soft voting (kappas de l'entraînement) ──────\nkappas = np.array([0.8304, 0.8144, 0.7690])  # resnet, dense, eff_b4\nweights_vote = kappas / kappas.sum()\nprint(f\"Poids → ResNet: {weights_vote[0]:.4f} | DenseNet: {weights_vote[1]:.4f} | EfficientNet-B4: {weights_vote[2]:.4f}\")\n\n# ── 5. Seuils optimisés (tes vraies valeurs) ────────────────\ndef apply_thresholds(probs, thresholds):\n    if probs.ndim == 2:\n        scores = probs @ np.arange(probs.shape[1])\n    else:\n        scores = probs\n    t1, t2, t3, t4 = thresholds\n    preds = np.zeros(len(scores), dtype=int)\n    preds[scores >= t1] = 1\n    preds[scores >= t2] = 2\n    preds[scores >= t3] = 3\n    preds[scores >= t4] = 4\n    return np.clip(preds, 0, 4)\n\nbest_thresholds = [0.6, 1.3, 2.75, 3.5]\n\n# ── 6. Classe Grad-CAM (avec retrait des hooks) ─────────────\nclass GradCAM:\n    def __init__(self, model, target_layer):\n        self.model = model\n        self.activations = None\n        self.gradients = None\n        self._fwd_handle = target_layer.register_forward_hook(self._save_activation)\n        self._bwd_handle = target_layer.register_full_backward_hook(self._save_gradient)\n\n    def _save_activation(self, module, input, output):\n        self.activations = output.detach()\n\n    def _save_gradient(self, module, grad_input, grad_output):\n        self.gradients = grad_output[0].detach()\n\n    def generate(self, tensor, class_idx=None):\n        self.model.eval()\n        output = self.model(tensor.to(device))\n        if class_idx is None:\n            class_idx = output.argmax(dim=1).item()\n        self.model.zero_grad()\n        output[0, class_idx].backward()\n        weights = self.gradients.mean(dim=[2, 3], keepdim=True)\n        cam = F.relu((weights * self.activations).sum(dim=1, keepdim=True))\n        cam = F.interpolate(cam, size=(380, 380), mode=\"bilinear\", align_corners=False)\n        cam = cam.squeeze().cpu().numpy()\n        cam = (cam - cam.min()) / (cam.max() - cam.min() + 1e-8)\n        return cam\n\n    def remove_hooks(self):\n        self._fwd_handle.remove()\n        self._bwd_handle.remove()\n\n\ndef get_gradcam_overlay(cam, img_pil):\n    heatmap = plt.cm.jet(cam)[:, :, :3]\n    base = np.array(img_pil) / 255.0\n    overlay = 0.5 * base + 0.5 * heatmap\n    return np.clip(overlay, 0, 1)\n\n# ══════════════════════════════════════════════════════════\n# CELLULE GRAD-CAM — image 06_test.tif\n# ══════════════════════════════════════════════════════════\n\n# ── 7. Charger l'image .tif ─────────────────────────────────\nimage_path = \"/kaggle/input/datasets/andrewmvd/drive-digital-retinal-images-for-vessel-extraction/DRIVE/test/images/06_test.tif\"\n\ntransform = transforms.Compose([\n    transforms.Resize((380, 380)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406],\n                          [0.229, 0.224, 0.225])\n])\n\nimg = Image.open(image_path).convert(\"RGB\")\nimg_tensor = transform(img).unsqueeze(0)\nimg_show = img.resize((380, 380))\n\n# ── 8. Instancier Grad-CAM sur chaque modèle ────────────────\ngcam_resnet = GradCAM(resnet_model, resnet_model.layer4[-1])\ngcam_dense  = GradCAM(dense_model,  dense_model.features.denseblock4)\ngcam_eff    = GradCAM(eff_model,    eff_model.features[-1])\n\n# ── 9. Probabilités de chaque modèle ────────────────────────\ndef predict_single(model, tensor):\n    model.eval()\n    with torch.no_grad():\n        output = model(tensor.to(device))\n        return torch.softmax(output, dim=1).cpu().numpy()\n\nresnet_p = predict_single(resnet_model, img_tensor)\ndense_p  = predict_single(dense_model,  img_tensor)\neff_p    = predict_single(eff_model,    img_tensor)\n\n# ── 10. Soft Voting + classe prédite ────────────────────────\nsoft_p = (\n      weights_vote[0] * resnet_p\n    + weights_vote[1] * dense_p\n    + weights_vote[2] * eff_p\n)\n\npred_class  = apply_thresholds(soft_p, best_thresholds)[0]\nclass_names = [\"No DR\", \"Mild\", \"Moderate\", \"Severe\", \"Proliferative DR\"]\n\n# ── 11. Cartes Grad-CAM pour la classe prédite ──────────────\ncam_resnet = gcam_resnet.generate(img_tensor, class_idx=pred_class)\ncam_dense  = gcam_dense.generate(img_tensor,  class_idx=pred_class)\ncam_eff    = gcam_eff.generate(img_tensor,    class_idx=pred_class)\n\noverlay_resnet = get_gradcam_overlay(cam_resnet, img_show)\noverlay_dense  = get_gradcam_overlay(cam_dense,  img_show)\noverlay_eff    = get_gradcam_overlay(cam_eff,    img_show)\n\n# ── 12. Nettoyage des hooks ──────────────────────────────────\ngcam_resnet.remove_hooks()\ngcam_dense.remove_hooks()\ngcam_eff.remove_hooks()\n\n# ── 13. Résultat terminal ───────────────────────────────────\nprint(f\"Classe prédite : {pred_class} — {class_names[pred_class]}\")\nprint(\"Probabilités   :\")\nfor i, c in enumerate(class_names):\n    print(f\"  {c:<20} : {soft_p[0][i]:.4f}\")\n\n# ── 14. Visualisation ───────────────────────────────────────\nfig, axes = plt.subplots(2, 4, figsize=(18, 9))\nfig.suptitle(f\"06_test.tif  —  Prédiction : Grade {pred_class} ({class_names[pred_class]})\",\n             fontsize=14, fontweight=\"bold\")\n\naxes[0, 0].imshow(img_show)\naxes[0, 0].set_title(\"Image originale\", fontsize=11)\naxes[0, 0].axis(\"off\")\n\nfor ax, overlay, title in zip(\n    axes[0, 1:],\n    [overlay_resnet, overlay_dense, overlay_eff],\n    [\"Grad-CAM ResNet-50\", \"Grad-CAM DenseNet-121\", \"Grad-CAM EfficientNet-B4\"]\n):\n    ax.imshow(overlay)\n    ax.set_title(title, fontsize=10)\n    ax.axis(\"off\")\n\nbar_colors = [\"green\" if i == pred_class else \"steelblue\" for i in range(5)]\ntitles_probs = [\n    (\"Soft Voting (final)\", soft_p[0]),\n    (\"ResNet-50\",           resnet_p[0]),\n    (\"DenseNet-121\",        dense_p[0]),\n    (\"EfficientNet-B4\",     eff_p[0]),\n]\n\nfor ax, (title, probs) in zip(axes[1], titles_probs):\n    bars = ax.bar(range(5), probs, color=bar_colors, edgecolor=\"black\", linewidth=0.5)\n    bars[pred_class].set_edgecolor(\"red\")\n    bars[pred_class].set_linewidth(2.5)\n    ax.set_xticks(range(5))\n    ax.set_xticklabels([\"G0\", \"G1\", \"G2\", \"G3\", \"G4\"], fontsize=9)\n    ax.set_ylim(0, 1)\n    ax.set_ylabel(\"Probabilité\", fontsize=9)\n    ax.set_title(title, fontsize=10)\n    for bar, val in zip(bars, probs):\n        ax.text(bar.get_x() + bar.get_width() / 2, val + 0.02,\n                f\"{val:.2f}\", ha=\"center\", fontsize=8)\n\nplt.tight_layout()\nplt.savefig(\"gradcam_drive_06_test.png\", dpi=150, bbox_inches=\"tight\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T20:33:51.736531Z","iopub.execute_input":"2026-08-08T20:33:51.737203Z","iopub.status.idle":"2026-08-08T20:33:55.820242Z","shell.execute_reply.started":"2026-08-08T20:33:51.737173Z","shell.execute_reply":"2026-08-08T20:33:55.819336Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ══════════════════════════════════════════════════════════\n# CELLULE AUTONOME — Grad-CAM (tout le nécessaire est ici)\n# ══════════════════════════════════════════════════════════\n\nimport os\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom PIL import Image\nfrom torchvision import transforms\nimport torchvision.models as models\nfrom torchvision.models import efficientnet_b4\n\n# ── 0. Device ──────────────────────────────────────────────\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# ── 1. Chemins des poids sauvegardés ────────────────────────\nRESNET_PATH = \"resnet50_best.pth\"\nDENSE_PATH  = \"densenet121_best.pth\"\nEFF_PATH    = \"efficientnet_b4_best.pth\"\n\n# ── 2. Reconstruire les architectures ──────────────────────\nresnet_model = models.resnet50(weights=None)\nresnet_model.fc = nn.Linear(resnet_model.fc.in_features, 5)\n\ndense_model = models.densenet121(weights=None)\ndense_model.classifier = nn.Linear(dense_model.classifier.in_features, 5)\n\neff_model = efficientnet_b4(weights=None)\neff_model.classifier[1] = nn.Linear(eff_model.classifier[1].in_features, 5)\n\n# ── 3. Charger les poids ────────────────────────────────────\nresnet_model.load_state_dict(torch.load(RESNET_PATH, map_location=device))\ndense_model.load_state_dict(torch.load(DENSE_PATH,  map_location=device))\neff_model.load_state_dict(torch.load(EFF_PATH,     map_location=device))\n\nresnet_model.to(device).eval()\ndense_model.to(device).eval()\neff_model.to(device).eval()\n\n# ── 4. Poids du soft voting (kappas de l'entraînement) ──────\nkappas = np.array([0.8304, 0.8144, 0.7690])  # resnet, dense, eff_b4\nweights_vote = kappas / kappas.sum()\nprint(f\"Poids → ResNet: {weights_vote[0]:.4f} | DenseNet: {weights_vote[1]:.4f} | EfficientNet-B4: {weights_vote[2]:.4f}\")\n\n# ── 5. Seuils optimisés (tes vraies valeurs) ────────────────\ndef apply_thresholds(probs, thresholds):\n    if probs.ndim == 2:\n        scores = probs @ np.arange(probs.shape[1])\n    else:\n        scores = probs\n    t1, t2, t3, t4 = thresholds\n    preds = np.zeros(len(scores), dtype=int)\n    preds[scores >= t1] = 1\n    preds[scores >= t2] = 2\n    preds[scores >= t3] = 3\n    preds[scores >= t4] = 4\n    return np.clip(preds, 0, 4)\n\nbest_thresholds = [0.6, 1.3, 2.75, 3.5]\n\n# ── 6. Classe Grad-CAM (avec retrait des hooks) ─────────────\nclass GradCAM:\n    def __init__(self, model, target_layer):\n        self.model = model\n        self.activations = None\n        self.gradients = None\n        self._fwd_handle = target_layer.register_forward_hook(self._save_activation)\n        self._bwd_handle = target_layer.register_full_backward_hook(self._save_gradient)\n\n    def _save_activation(self, module, input, output):\n        self.activations = output.detach()\n\n    def _save_gradient(self, module, grad_input, grad_output):\n        self.gradients = grad_output[0].detach()\n\n    def generate(self, tensor, class_idx=None):\n        self.model.eval()\n        output = self.model(tensor.to(device))\n        if class_idx is None:\n            class_idx = output.argmax(dim=1).item()\n        self.model.zero_grad()\n        output[0, class_idx].backward()\n        weights = self.gradients.mean(dim=[2, 3], keepdim=True)\n        cam = F.relu((weights * self.activations).sum(dim=1, keepdim=True))\n        cam = F.interpolate(cam, size=(380, 380), mode=\"bilinear\", align_corners=False)\n        cam = cam.squeeze().cpu().numpy()\n        cam = (cam - cam.min()) / (cam.max() - cam.min() + 1e-8)\n        return cam\n\n    def remove_hooks(self):\n        self._fwd_handle.remove()\n        self._bwd_handle.remove()\n\n\ndef get_gradcam_overlay(cam, img_pil):\n    heatmap = plt.cm.jet(cam)[:, :, :3]\n    base = np.array(img_pil) / 255.0\n    overlay = 0.5 * base + 0.5 * heatmap\n    return np.clip(overlay, 0, 1)\n\n# ══════════════════════════════════════════════════════════\n# CELLULE GRAD-CAM — image 18_test.tif\n# ══════════════════════════════════════════════════════════\n\n# ── 7. Charger l'image .tif ─────────────────────────────────\nimage_path = \"/kaggle/input/datasets/andrewmvd/drive-digital-retinal-images-for-vessel-extraction/DRIVE/test/images/18_test.tif\"\n\ntransform = transforms.Compose([\n    transforms.Resize((380, 380)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406],\n                          [0.229, 0.224, 0.225])\n])\n\nimg = Image.open(image_path).convert(\"RGB\")\nimg_tensor = transform(img).unsqueeze(0)\nimg_show = img.resize((380, 380))\n\n# ── 8. Instancier Grad-CAM sur chaque modèle ────────────────\ngcam_resnet = GradCAM(resnet_model, resnet_model.layer4[-1])\ngcam_dense  = GradCAM(dense_model,  dense_model.features.denseblock4)\ngcam_eff    = GradCAM(eff_model,    eff_model.features[-1])\n\n# ── 9. Probabilités de chaque modèle ────────────────────────\ndef predict_single(model, tensor):\n    model.eval()\n    with torch.no_grad():\n        output = model(tensor.to(device))\n        return torch.softmax(output, dim=1).cpu().numpy()\n\nresnet_p = predict_single(resnet_model, img_tensor)\ndense_p  = predict_single(dense_model,  img_tensor)\neff_p    = predict_single(eff_model,    img_tensor)\n\n# ── 10. Soft Voting + classe prédite ────────────────────────\nsoft_p = (\n      weights_vote[0] * resnet_p\n    + weights_vote[1] * dense_p\n    + weights_vote[2] * eff_p\n)\n\npred_class  = apply_thresholds(soft_p, best_thresholds)[0]\nclass_names = [\"No DR\", \"Mild\", \"Moderate\", \"Severe\", \"Proliferative DR\"]\n\n# ── 11. Cartes Grad-CAM pour la classe prédite ──────────────\ncam_resnet = gcam_resnet.generate(img_tensor, class_idx=pred_class)\ncam_dense  = gcam_dense.generate(img_tensor,  class_idx=pred_class)\ncam_eff    = gcam_eff.generate(img_tensor,    class_idx=pred_class)\n\noverlay_resnet = get_gradcam_overlay(cam_resnet, img_show)\noverlay_dense  = get_gradcam_overlay(cam_dense,  img_show)\noverlay_eff    = get_gradcam_overlay(cam_eff,    img_show)\n\n# ── 12. Nettoyage des hooks ──────────────────────────────────\ngcam_resnet.remove_hooks()\ngcam_dense.remove_hooks()\ngcam_eff.remove_hooks()\n\n# ── 13. Résultat terminal ───────────────────────────────────\nprint(f\"Classe prédite : {pred_class} — {class_names[pred_class]}\")\nprint(\"Probabilités   :\")\nfor i, c in enumerate(class_names):\n    print(f\"  {c:<20} : {soft_p[0][i]:.4f}\")\n\n# ── 14. Visualisation ───────────────────────────────────────\nfig, axes = plt.subplots(2, 4, figsize=(18, 9))\nfig.suptitle(f\"18_test.tif  —  Prédiction : Grade {pred_class} ({class_names[pred_class]})\",\n             fontsize=14, fontweight=\"bold\")\n\naxes[0, 0].imshow(img_show)\naxes[0, 0].set_title(\"Image originale\", fontsize=11)\naxes[0, 0].axis(\"off\")\n\nfor ax, overlay, title in zip(\n    axes[0, 1:],\n    [overlay_resnet, overlay_dense, overlay_eff],\n    [\"Grad-CAM ResNet-50\", \"Grad-CAM DenseNet-121\", \"Grad-CAM EfficientNet-B4\"]\n):\n    ax.imshow(overlay)\n    ax.set_title(title, fontsize=10)\n    ax.axis(\"off\")\n\nbar_colors = [\"green\" if i == pred_class else \"steelblue\" for i in range(5)]\ntitles_probs = [\n    (\"Soft Voting (final)\", soft_p[0]),\n    (\"ResNet-50\",           resnet_p[0]),\n    (\"DenseNet-121\",        dense_p[0]),\n    (\"EfficientNet-B4\",     eff_p[0]),\n]\n\nfor ax, (title, probs) in zip(axes[1], titles_probs):\n    bars = ax.bar(range(5), probs, color=bar_colors, edgecolor=\"black\", linewidth=0.5)\n    bars[pred_class].set_edgecolor(\"red\")\n    bars[pred_class].set_linewidth(2.5)\n    ax.set_xticks(range(5))\n    ax.set_xticklabels([\"G0\", \"G1\", \"G2\", \"G3\", \"G4\"], fontsize=9)\n    ax.set_ylim(0, 1)\n    ax.set_ylabel(\"Probabilité\", fontsize=9)\n    ax.set_title(title, fontsize=10)\n    for bar, val in zip(bars, probs):\n        ax.text(bar.get_x() + bar.get_width() / 2, val + 0.02,\n                f\"{val:.2f}\", ha=\"center\", fontsize=8)\n\nplt.tight_layout()\nplt.savefig(\"gradcam_drive_18_test.png\", dpi=150, bbox_inches=\"tight\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T20:35:19.734514Z","iopub.execute_input":"2026-08-08T20:35:19.735229Z","iopub.status.idle":"2026-08-08T20:35:23.872112Z","shell.execute_reply.started":"2026-08-08T20:35:19.735198Z","shell.execute_reply":"2026-08-08T20:35:23.868968Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ══════════════════════════════════════════════════════════\n# CELLULE AUTONOME — Grad-CAM (tout le nécessaire est ici)\n# ══════════════════════════════════════════════════════════\n\nimport os\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom PIL import Image\nfrom torchvision import transforms\nimport torchvision.models as models\nfrom torchvision.models import efficientnet_b4\n\n# ── 0. Device ──────────────────────────────────────────────\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# ── 1. Chemins des poids sauvegardés ────────────────────────\nRESNET_PATH = \"resnet50_best.pth\"\nDENSE_PATH  = \"densenet121_best.pth\"\nEFF_PATH    = \"efficientnet_b4_best.pth\"\n\n# ── 2. Reconstruire les architectures ──────────────────────\nresnet_model = models.resnet50(weights=None)\nresnet_model.fc = nn.Linear(resnet_model.fc.in_features, 5)\n\ndense_model = models.densenet121(weights=None)\ndense_model.classifier = nn.Linear(dense_model.classifier.in_features, 5)\n\neff_model = efficientnet_b4(weights=None)\neff_model.classifier[1] = nn.Linear(eff_model.classifier[1].in_features, 5)\n\n# ── 3. Charger les poids ────────────────────────────────────\nresnet_model.load_state_dict(torch.load(RESNET_PATH, map_location=device))\ndense_model.load_state_dict(torch.load(DENSE_PATH,  map_location=device))\neff_model.load_state_dict(torch.load(EFF_PATH,     map_location=device))\n\nresnet_model.to(device).eval()\ndense_model.to(device).eval()\neff_model.to(device).eval()\n\n# ── 4. Poids du soft voting (kappas de l'entraînement) ──────\nkappas = np.array([0.8304, 0.8144, 0.7690])  # resnet, dense, eff_b4\nweights_vote = kappas / kappas.sum()\nprint(f\"Poids → ResNet: {weights_vote[0]:.4f} | DenseNet: {weights_vote[1]:.4f} | EfficientNet-B4: {weights_vote[2]:.4f}\")\n\n# ── 5. Seuils optimisés (tes vraies valeurs) ────────────────\ndef apply_thresholds(probs, thresholds):\n    if probs.ndim == 2:\n        scores = probs @ np.arange(probs.shape[1])\n    else:\n        scores = probs\n    t1, t2, t3, t4 = thresholds\n    preds = np.zeros(len(scores), dtype=int)\n    preds[scores >= t1] = 1\n    preds[scores >= t2] = 2\n    preds[scores >= t3] = 3\n    preds[scores >= t4] = 4\n    return np.clip(preds, 0, 4)\n\nbest_thresholds = [0.6, 1.3, 2.75, 3.5]\n\n# ── 6. Classe Grad-CAM (avec retrait des hooks) ─────────────\nclass GradCAM:\n    def __init__(self, model, target_layer):\n        self.model = model\n        self.activations = None\n        self.gradients = None\n        self._fwd_handle = target_layer.register_forward_hook(self._save_activation)\n        self._bwd_handle = target_layer.register_full_backward_hook(self._save_gradient)\n\n    def _save_activation(self, module, input, output):\n        self.activations = output.detach()\n\n    def _save_gradient(self, module, grad_input, grad_output):\n        self.gradients = grad_output[0].detach()\n\n    def generate(self, tensor, class_idx=None):\n        self.model.eval()\n        output = self.model(tensor.to(device))\n        if class_idx is None:\n            class_idx = output.argmax(dim=1).item()\n        self.model.zero_grad()\n        output[0, class_idx].backward()\n        weights = self.gradients.mean(dim=[2, 3], keepdim=True)\n        cam = F.relu((weights * self.activations).sum(dim=1, keepdim=True))\n        cam = F.interpolate(cam, size=(380, 380), mode=\"bilinear\", align_corners=False)\n        cam = cam.squeeze().cpu().numpy()\n        cam = (cam - cam.min()) / (cam.max() - cam.min() + 1e-8)\n        return cam\n\n    def remove_hooks(self):\n        self._fwd_handle.remove()\n        self._bwd_handle.remove()\n\n\ndef get_gradcam_overlay(cam, img_pil):\n    heatmap = plt.cm.jet(cam)[:, :, :3]\n    base = np.array(img_pil) / 255.0\n    overlay = 0.5 * base + 0.5 * heatmap\n    return np.clip(overlay, 0, 1)\n\n# ══════════════════════════════════════════════════════════\n# CELLULE GRAD-CAM — image APTOS 03be80919be4.png\n# ══════════════════════════════════════════════════════════\n\n# ── 7. Charger l'image .png ─────────────────────────────────\nimage_path = \"/kaggle/input/competitions/aptos2019-blindness-detection/test_images/03be80919be4.png\"\n\ntransform = transforms.Compose([\n    transforms.Resize((380, 380)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406],\n                          [0.229, 0.224, 0.225])\n])\n\nimg = Image.open(image_path).convert(\"RGB\")\nimg_tensor = transform(img).unsqueeze(0)\nimg_show = img.resize((380, 380))\n\n# ── 8. Instancier Grad-CAM sur chaque modèle ────────────────\ngcam_resnet = GradCAM(resnet_model, resnet_model.layer4[-1])\ngcam_dense  = GradCAM(dense_model,  dense_model.features.denseblock4)\ngcam_eff    = GradCAM(eff_model,    eff_model.features[-1])\n\n# ── 9. Probabilités de chaque modèle ────────────────────────\ndef predict_single(model, tensor):\n    model.eval()\n    with torch.no_grad():\n        output = model(tensor.to(device))\n        return torch.softmax(output, dim=1).cpu().numpy()\n\nresnet_p = predict_single(resnet_model, img_tensor)\ndense_p  = predict_single(dense_model,  img_tensor)\neff_p    = predict_single(eff_model,    img_tensor)\n\n# ── 10. Soft Voting + classe prédite ────────────────────────\nsoft_p = (\n      weights_vote[0] * resnet_p\n    + weights_vote[1] * dense_p\n    + weights_vote[2] * eff_p\n)\n\npred_class  = apply_thresholds(soft_p, best_thresholds)[0]\nclass_names = [\"No DR\", \"Mild\", \"Moderate\", \"Severe\", \"Proliferative DR\"]\n\n# ── 11. Cartes Grad-CAM pour la classe prédite ──────────────\ncam_resnet = gcam_resnet.generate(img_tensor, class_idx=pred_class)\ncam_dense  = gcam_dense.generate(img_tensor,  class_idx=pred_class)\ncam_eff    = gcam_eff.generate(img_tensor,    class_idx=pred_class)\n\noverlay_resnet = get_gradcam_overlay(cam_resnet, img_show)\noverlay_dense  = get_gradcam_overlay(cam_dense,  img_show)\noverlay_eff    = get_gradcam_overlay(cam_eff,    img_show)\n\n# ── 12. Nettoyage des hooks ──────────────────────────────────\ngcam_resnet.remove_hooks()\ngcam_dense.remove_hooks()\ngcam_eff.remove_hooks()\n\n# ── 13. Résultat terminal ───────────────────────────────────\nprint(f\"Classe prédite : {pred_class} — {class_names[pred_class]}\")\nprint(\"Probabilités   :\")\nfor i, c in enumerate(class_names):\n    print(f\"  {c:<20} : {soft_p[0][i]:.4f}\")\n\n# ── 14. Visualisation ───────────────────────────────────────\nfig, axes = plt.subplots(2, 4, figsize=(18, 9))\nfig.suptitle(f\"03be80919be4.png  —  Prédiction : Grade {pred_class} ({class_names[pred_class]})\",\n             fontsize=14, fontweight=\"bold\")\n\naxes[0, 0].imshow(img_show)\naxes[0, 0].set_title(\"Image originale\", fontsize=11)\naxes[0, 0].axis(\"off\")\n\nfor ax, overlay, title in zip(\n    axes[0, 1:],\n    [overlay_resnet, overlay_dense, overlay_eff],\n    [\"Grad-CAM ResNet-50\", \"Grad-CAM DenseNet-121\", \"Grad-CAM EfficientNet-B4\"]\n):\n    ax.imshow(overlay)\n    ax.set_title(title, fontsize=10)\n    ax.axis(\"off\")\n\nbar_colors = [\"green\" if i == pred_class else \"steelblue\" for i in range(5)]\ntitles_probs = [\n    (\"Soft Voting (final)\", soft_p[0]),\n    (\"ResNet-50\",           resnet_p[0]),\n    (\"DenseNet-121\",        dense_p[0]),\n    (\"EfficientNet-B4\",     eff_p[0]),\n]\n\nfor ax, (title, probs) in zip(axes[1], titles_probs):\n    bars = ax.bar(range(5), probs, color=bar_colors, edgecolor=\"black\", linewidth=0.5)\n    bars[pred_class].set_edgecolor(\"red\")\n    bars[pred_class].set_linewidth(2.5)\n    ax.set_xticks(range(5))\n    ax.set_xticklabels([\"G0\", \"G1\", \"G2\", \"G3\", \"G4\"], fontsize=9)\n    ax.set_ylim(0, 1)\n    ax.set_ylabel(\"Probabilité\", fontsize=9)\n    ax.set_title(title, fontsize=10)\n    for bar, val in zip(bars, probs):\n        ax.text(bar.get_x() + bar.get_width() / 2, val + 0.02,\n                f\"{val:.2f}\", ha=\"center\", fontsize=8)\n\nplt.tight_layout()\nplt.savefig(\"gradcam_aptos_03be80919be4.png\", dpi=150, bbox_inches=\"tight\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T20:36:48.682392Z","iopub.execute_input":"2026-08-08T20:36:48.682861Z","iopub.status.idle":"2026-08-08T20:36:52.946038Z","shell.execute_reply.started":"2026-08-08T20:36:48.68283Z","shell.execute_reply":"2026-08-08T20:36:52.945211Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ══════════════════════════════════════════════════════════\n# CELLULE AUTONOME — Grad-CAM++ (meilleure localisation)\n# ══════════════════════════════════════════════════════════\n\nimport os\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom PIL import Image\nfrom torchvision import transforms\nimport torchvision.models as models\nfrom torchvision.models import efficientnet_b4\n\n# ── 0. Device ──────────────────────────────────────────────\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# ── 1. Chemins des poids sauvegardés ────────────────────────\nRESNET_PATH = \"resnet50_best.pth\"\nDENSE_PATH  = \"densenet121_best.pth\"\nEFF_PATH    = \"efficientnet_b4_best.pth\"\n\n# ── 2. Reconstruire les architectures ──────────────────────\nresnet_model = models.resnet50(weights=None)\nresnet_model.fc = nn.Linear(resnet_model.fc.in_features, 5)\n\ndense_model = models.densenet121(weights=None)\ndense_model.classifier = nn.Linear(dense_model.classifier.in_features, 5)\n\neff_model = efficientnet_b4(weights=None)\neff_model.classifier[1] = nn.Linear(eff_model.classifier[1].in_features, 5)\n\n# ── 3. Charger les poids ────────────────────────────────────\nresnet_model.load_state_dict(torch.load(RESNET_PATH, map_location=device))\ndense_model.load_state_dict(torch.load(DENSE_PATH,  map_location=device))\neff_model.load_state_dict(torch.load(EFF_PATH,     map_location=device))\n\nresnet_model.to(device).eval()\ndense_model.to(device).eval()\neff_model.to(device).eval()\n\n# ── 4. Poids du soft voting (kappas de l'entraînement) ──────\nkappas = np.array([0.8304, 0.8144, 0.7690])  # resnet, dense, eff_b4\nweights_vote = kappas / kappas.sum()\nprint(f\"Poids → ResNet: {weights_vote[0]:.4f} | DenseNet: {weights_vote[1]:.4f} | EfficientNet-B4: {weights_vote[2]:.4f}\")\n\n# ── 5. Seuils optimisés (tes vraies valeurs) ────────────────\ndef apply_thresholds(probs, thresholds):\n    if probs.ndim == 2:\n        scores = probs @ np.arange(probs.shape[1])\n    else:\n        scores = probs\n    t1, t2, t3, t4 = thresholds\n    preds = np.zeros(len(scores), dtype=int)\n    preds[scores >= t1] = 1\n    preds[scores >= t2] = 2\n    preds[scores >= t3] = 3\n    preds[scores >= t4] = 4\n    return np.clip(preds, 0, 4)\n\nbest_thresholds = [0.6, 1.3, 2.75, 3.5]\n\n# ── 6. Classe Grad-CAM++ (pondération d'ordre 2 — plus précise) ─\nclass GradCAMPlusPlus:\n    def __init__(self, model, target_layer):\n        self.model = model\n        self.activations = None\n        self.gradients = None\n        self._fwd_handle = target_layer.register_forward_hook(self._save_activation)\n        self._bwd_handle = target_layer.register_full_backward_hook(self._save_gradient)\n\n    def _save_activation(self, module, input, output):\n        self.activations = output.detach()\n\n    def _save_gradient(self, module, grad_input, grad_output):\n        self.gradients = grad_output[0].detach()\n\n    def generate(self, tensor, class_idx=None):\n        self.model.eval()\n        output = self.model(tensor.to(device))\n        if class_idx is None:\n            class_idx = output.argmax(dim=1).item()\n        self.model.zero_grad()\n        output[0, class_idx].backward()\n\n        grads = self.gradients          # (1, C, H, W)\n        acts  = self.activations        # (1, C, H, W)\n\n        grads_sq  = grads ** 2\n        grads_cub = grads ** 3\n        sum_acts  = acts.sum(dim=[2, 3], keepdim=True)\n\n        alpha_num   = grads_sq\n        alpha_denom = 2 * grads_sq + sum_acts * grads_cub\n        alpha_denom = torch.where(alpha_denom != 0, alpha_denom, torch.ones_like(alpha_denom))\n        alpha = alpha_num / alpha_denom\n\n        weights = (alpha * F.relu(grads)).sum(dim=[2, 3], keepdim=True)\n\n        cam = F.relu((weights * acts).sum(dim=1, keepdim=True))\n        cam = F.interpolate(cam, size=(380, 380), mode=\"bilinear\", align_corners=False)\n        cam = cam.squeeze().cpu().numpy()\n        cam = (cam - cam.min()) / (cam.max() - cam.min() + 1e-8)\n        return cam\n\n    def remove_hooks(self):\n        self._fwd_handle.remove()\n        self._bwd_handle.remove()\n\n\ndef get_gradcam_overlay(cam, img_pil):\n    heatmap = plt.cm.jet(cam)[:, :, :3]\n    base = np.array(img_pil) / 255.0\n    overlay = 0.5 * base + 0.5 * heatmap\n    return np.clip(overlay, 0, 1)\n\n# ══════════════════════════════════════════════════════════\n# CELLULE GRAD-CAM++ — image APTOS 03be80919be4.png\n# ══════════════════════════════════════════════════════════\n\n# ── 7. Charger l'image ───────────────────────────────────────\nimage_path = \"/kaggle/input/competitions/aptos2019-blindness-detection/test_images/03be80919be4.png\"\n\ntransform = transforms.Compose([\n    transforms.Resize((380, 380)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406],\n                          [0.229, 0.224, 0.225])\n])\n\nimg = Image.open(image_path).convert(\"RGB\")\nimg_tensor = transform(img).unsqueeze(0)\nimg_show = img.resize((380, 380))\n\n# ── 8. Instancier Grad-CAM++ sur chaque modèle ──────────────\ngcam_resnet = GradCAMPlusPlus(resnet_model, resnet_model.layer4[-1])\ngcam_dense  = GradCAMPlusPlus(dense_model,  dense_model.features.denseblock4)\ngcam_eff    = GradCAMPlusPlus(eff_model,    eff_model.features[-1])\n\n# ── 9. Probabilités de chaque modèle ────────────────────────\ndef predict_single(model, tensor):\n    model.eval()\n    with torch.no_grad():\n        output = model(tensor.to(device))\n        return torch.softmax(output, dim=1).cpu().numpy()\n\nresnet_p = predict_single(resnet_model, img_tensor)\ndense_p  = predict_single(dense_model,  img_tensor)\neff_p    = predict_single(eff_model,    img_tensor)\n\n# ── 10. Soft Voting + classe prédite ────────────────────────\nsoft_p = (\n      weights_vote[0] * resnet_p\n    + weights_vote[1] * dense_p\n    + weights_vote[2] * eff_p\n)\n\npred_class  = apply_thresholds(soft_p, best_thresholds)[0]\nclass_names = [\"No DR\", \"Mild\", \"Moderate\", \"Severe\", \"Proliferative DR\"]\n\n# ── 11. Cartes Grad-CAM++ pour la classe prédite ────────────\ncam_resnet = gcam_resnet.generate(img_tensor, class_idx=pred_class)\ncam_dense  = gcam_dense.generate(img_tensor,  class_idx=pred_class)\ncam_eff    = gcam_eff.generate(img_tensor,    class_idx=pred_class)\n\noverlay_resnet = get_gradcam_overlay(cam_resnet, img_show)\noverlay_dense  = get_gradcam_overlay(cam_dense,  img_show)\noverlay_eff    = get_gradcam_overlay(cam_eff,    img_show)\n\n# ── 12. Nettoyage des hooks ──────────────────────────────────\ngcam_resnet.remove_hooks()\ngcam_dense.remove_hooks()\ngcam_eff.remove_hooks()\n\n# ── 13. Résultat terminal ───────────────────────────────────\nprint(f\"Classe prédite : {pred_class} — {class_names[pred_class]}\")\nprint(\"Probabilités   :\")\nfor i, c in enumerate(class_names):\n    print(f\"  {c:<20} : {soft_p[0][i]:.4f}\")\n\n# ── 14. Visualisation ───────────────────────────────────────\nfig, axes = plt.subplots(2, 4, figsize=(18, 9))\nfig.suptitle(f\"03be80919be4.png  —  Prédiction : Grade {pred_class} ({class_names[pred_class]})\",\n             fontsize=14, fontweight=\"bold\")\n\naxes[0, 0].imshow(img_show)\naxes[0, 0].set_title(\"Image originale\", fontsize=11)\naxes[0, 0].axis(\"off\")\n\nfor ax, overlay, title in zip(\n    axes[0, 1:],\n    [overlay_resnet, overlay_dense, overlay_eff],\n    [\"Grad-CAM++ ResNet-50\", \"Grad-CAM++ DenseNet-121\", \"Grad-CAM++ EfficientNet-B4\"]\n):\n    ax.imshow(overlay)\n    ax.set_title(title, fontsize=10)\n    ax.axis(\"off\")\n\nbar_colors = [\"green\" if i == pred_class else \"steelblue\" for i in range(5)]\ntitles_probs = [\n    (\"Soft Voting (final)\", soft_p[0]),\n    (\"ResNet-50\",           resnet_p[0]),\n    (\"DenseNet-121\",        dense_p[0]),\n    (\"EfficientNet-B4\",     eff_p[0]),\n]\n\nfor ax, (title, probs) in zip(axes[1], titles_probs):\n    bars = ax.bar(range(5), probs, color=bar_colors, edgecolor=\"black\", linewidth=0.5)\n    bars[pred_class].set_edgecolor(\"red\")\n    bars[pred_class].set_linewidth(2.5)\n    ax.set_xticks(range(5))\n    ax.set_xticklabels([\"G0\", \"G1\", \"G2\", \"G3\", \"G4\"], fontsize=9)\n    ax.set_ylim(0, 1)\n    ax.set_ylabel(\"Probabilité\", fontsize=9)\n    ax.set_title(title, fontsize=10)\n    for bar, val in zip(bars, probs):\n        ax.text(bar.get_x() + bar.get_width() / 2, val + 0.02,\n                f\"{val:.2f}\", ha=\"center\", fontsize=8)\n\nplt.tight_layout()\nplt.savefig(\"gradcamplusplus_aptos_03be80919be4.png\", dpi=150, bbox_inches=\"tight\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T20:40:48.506082Z","iopub.execute_input":"2026-08-08T20:40:48.506385Z","iopub.status.idle":"2026-08-08T20:40:52.531145Z","shell.execute_reply.started":"2026-08-08T20:40:48.50636Z","shell.execute_reply":"2026-08-08T20:40:52.530361Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ══════════════════════════════════════════════════════════\n# CELLULE AUTONOME — Score-CAM (sans gradients, plus net)\n# ══════════════════════════════════════════════════════════\n\nimport os\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom PIL import Image\nfrom torchvision import transforms\nimport torchvision.models as models\nfrom torchvision.models import efficientnet_b4\n\n# ── 0. Device ──────────────────────────────────────────────\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# ── 1. Chemins des poids sauvegardés ────────────────────────\nRESNET_PATH = \"resnet50_best.pth\"\nDENSE_PATH  = \"densenet121_best.pth\"\nEFF_PATH    = \"efficientnet_b4_best.pth\"\n\n# ── 2. Reconstruire les architectures ──────────────────────\nresnet_model = models.resnet50(weights=None)\nresnet_model.fc = nn.Linear(resnet_model.fc.in_features, 5)\n\ndense_model = models.densenet121(weights=None)\ndense_model.classifier = nn.Linear(dense_model.classifier.in_features, 5)\n\neff_model = efficientnet_b4(weights=None)\neff_model.classifier[1] = nn.Linear(eff_model.classifier[1].in_features, 5)\n\n# ── 3. Charger les poids ────────────────────────────────────\nresnet_model.load_state_dict(torch.load(RESNET_PATH, map_location=device))\ndense_model.load_state_dict(torch.load(DENSE_PATH,  map_location=device))\neff_model.load_state_dict(torch.load(EFF_PATH,     map_location=device))\n\nresnet_model.to(device).eval()\ndense_model.to(device).eval()\neff_model.to(device).eval()\n\n# ── 4. Poids du soft voting (kappas de l'entraînement) ──────\nkappas = np.array([0.8304, 0.8144, 0.7690])  # resnet, dense, eff_b4\nweights_vote = kappas / kappas.sum()\nprint(f\"Poids → ResNet: {weights_vote[0]:.4f} | DenseNet: {weights_vote[1]:.4f} | EfficientNet-B4: {weights_vote[2]:.4f}\")\n\n# ── 5. Seuils optimisés (tes vraies valeurs) ────────────────\ndef apply_thresholds(probs, thresholds):\n    if probs.ndim == 2:\n        scores = probs @ np.arange(probs.shape[1])\n    else:\n        scores = probs\n    t1, t2, t3, t4 = thresholds\n    preds = np.zeros(len(scores), dtype=int)\n    preds[scores >= t1] = 1\n    preds[scores >= t2] = 2\n    preds[scores >= t3] = 3\n    preds[scores >= t4] = 4\n    return np.clip(preds, 0, 4)\n\nbest_thresholds = [0.6, 1.3, 2.75, 3.5]\n\n# ── 6. Classe Score-CAM (sans gradients) ────────────────────\nclass ScoreCAM:\n    def __init__(self, model, target_layer, top_k=64, batch_size=16):\n        self.model = model\n        self.activations = None\n        self.top_k = top_k          # nb de cartes d'activation testées (perf)\n        self.batch_size = batch_size\n        self._fwd_handle = target_layer.register_forward_hook(self._save_activation)\n\n    def _save_activation(self, module, input, output):\n        self.activations = output.detach()\n\n    def generate(self, tensor, class_idx=None):\n        self.model.eval()\n        tensor = tensor.to(device)\n        input_size = tensor.shape[-2:]\n\n        with torch.no_grad():\n            output = self.model(tensor)\n        if class_idx is None:\n            class_idx = output.argmax(dim=1).item()\n\n        acts = self.activations[0]                      # (C, h, w)\n        C = acts.shape[0]\n\n        # ── Sélection des top_k canaux les plus \"actifs\" (perf) ──\n        act_energy = acts.view(C, -1).sum(dim=1)\n        k = min(self.top_k, C)\n        top_idx = torch.topk(act_energy, k).indices\n\n        # ── Upsampler + normaliser chaque carte sélectionnée ────\n        norm_maps = []\n        for i in top_idx:\n            m = acts[i:i+1].unsqueeze(0)                # (1,1,h,w)\n            m = F.interpolate(m, size=input_size, mode=\"bilinear\", align_corners=False).squeeze()\n            if m.max() - m.min() > 1e-8:\n                m = (m - m.min()) / (m.max() - m.min())\n            else:\n                m = torch.zeros_like(m)\n            norm_maps.append(m)\n        norm_maps = torch.stack(norm_maps)               # (k, H, W)\n\n        # ── Masquer l'image, faire des forward passes par batch ──\n        scores = []\n        with torch.no_grad():\n            for i in range(0, k, self.batch_size):\n                batch_masks = norm_maps[i:i + self.batch_size].to(device)      # (b,H,W)\n                masked = tensor * batch_masks.unsqueeze(1)                      # (b,3,H,W)\n                out = self.model(masked)\n                probs = torch.softmax(out, dim=1)[:, class_idx]\n                scores.append(probs.cpu())\n        scores = torch.cat(scores)                        # (k,)\n        weights = torch.softmax(scores, dim=0)             # normaliser les contributions\n\n        cam = torch.zeros(input_size)\n        for w, m in zip(weights, norm_maps):\n            cam += w.item() * m.cpu()\n\n        cam = F.relu(cam).numpy()\n        cam = (cam - cam.min()) / (cam.max() - cam.min() + 1e-8)\n        return cam\n\n    def remove_hooks(self):\n        self._fwd_handle.remove()\n\n\ndef get_gradcam_overlay(cam, img_pil):\n    heatmap = plt.cm.jet(cam)[:, :, :3]\n    base = np.array(img_pil) / 255.0\n    overlay = 0.5 * base + 0.5 * heatmap\n    return np.clip(overlay, 0, 1)\n\n# ══════════════════════════════════════════════════════════\n# CELLULE SCORE-CAM — image APTOS 03be80919be4.png\n# ══════════════════════════════════════════════════════════\n\n# ── 7. Charger l'image ───────────────────────────────────────\nimage_path = \"/kaggle/input/competitions/aptos2019-blindness-detection/test_images/03be80919be4.png\"\n\ntransform = transforms.Compose([\n    transforms.Resize((380, 380)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406],\n                          [0.229, 0.224, 0.225])\n])\n\nimg = Image.open(image_path).convert(\"RGB\")\nimg_tensor = transform(img).unsqueeze(0)\nimg_show = img.resize((380, 380))\n\n# ── 8. Instancier Score-CAM sur chaque modèle ───────────────\nscam_resnet = ScoreCAM(resnet_model, resnet_model.layer4[-1])\nscam_dense  = ScoreCAM(dense_model,  dense_model.features.denseblock4)\nscam_eff    = ScoreCAM(eff_model,    eff_model.features[-1])\n\n# ── 9. Probabilités de chaque modèle ────────────────────────\ndef predict_single(model, tensor):\n    model.eval()\n    with torch.no_grad():\n        output = model(tensor.to(device))\n        return torch.softmax(output, dim=1).cpu().numpy()\n\nresnet_p = predict_single(resnet_model, img_tensor)\ndense_p  = predict_single(dense_model,  img_tensor)\neff_p    = predict_single(eff_model,    img_tensor)\n\n# ── 10. Soft Voting + classe prédite ────────────────────────\nsoft_p = (\n      weights_vote[0] * resnet_p\n    + weights_vote[1] * dense_p\n    + weights_vote[2] * eff_p\n)\n\npred_class  = apply_thresholds(soft_p, best_thresholds)[0]\nclass_names = [\"No DR\", \"Mild\", \"Moderate\", \"Severe\", \"Proliferative DR\"]\n\n# ── 11. Cartes Score-CAM pour la classe prédite (peut prendre 1-2 min) ──\ncam_resnet = scam_resnet.generate(img_tensor, class_idx=pred_class)\ncam_dense  = scam_dense.generate(img_tensor,  class_idx=pred_class)\ncam_eff    = scam_eff.generate(img_tensor,    class_idx=pred_class)\n\noverlay_resnet = get_gradcam_overlay(cam_resnet, img_show)\noverlay_dense  = get_gradcam_overlay(cam_dense,  img_show)\noverlay_eff    = get_gradcam_overlay(cam_eff,    img_show)\n\n# ── 12. Nettoyage des hooks ──────────────────────────────────\nscam_resnet.remove_hooks()\nscam_dense.remove_hooks()\nscam_eff.remove_hooks()\n\n# ── 13. Résultat terminal ───────────────────────────────────\nprint(f\"Classe prédite : {pred_class} — {class_names[pred_class]}\")\nprint(\"Probabilités   :\")\nfor i, c in enumerate(class_names):\n    print(f\"  {c:<20} : {soft_p[0][i]:.4f}\")\n\n# ── 14. Visualisation ───────────────────────────────────────\nfig, axes = plt.subplots(2, 4, figsize=(18, 9))\nfig.suptitle(f\"03be80919be4.png  —  Prédiction : Grade {pred_class} ({class_names[pred_class]})\",\n             fontsize=14, fontweight=\"bold\")\n\naxes[0, 0].imshow(img_show)\naxes[0, 0].set_title(\"Image originale\", fontsize=11)\naxes[0, 0].axis(\"off\")\n\nfor ax, overlay, title in zip(\n    axes[0, 1:],\n    [overlay_resnet, overlay_dense, overlay_eff],\n    [\"Score-CAM ResNet-50\", \"Score-CAM DenseNet-121\", \"Score-CAM EfficientNet-B4\"]\n):\n    ax.imshow(overlay)\n    ax.set_title(title, fontsize=10)\n    ax.axis(\"off\")\n\nbar_colors = [\"green\" if i == pred_class else \"steelblue\" for i in range(5)]\ntitles_probs = [\n    (\"Soft Voting (final)\", soft_p[0]),\n    (\"ResNet-50\",           resnet_p[0]),\n    (\"DenseNet-121\",        dense_p[0]),\n    (\"EfficientNet-B4\",     eff_p[0]),\n]\n\nfor ax, (title, probs) in zip(axes[1], titles_probs):\n    bars = ax.bar(range(5), probs, color=bar_colors, edgecolor=\"black\", linewidth=0.5)\n    bars[pred_class].set_edgecolor(\"red\")\n    bars[pred_class].set_linewidth(2.5)\n    ax.set_xticks(range(5))\n    ax.set_xticklabels([\"G0\", \"G1\", \"G2\", \"G3\", \"G4\"], fontsize=9)\n    ax.set_ylim(0, 1)\n    ax.set_ylabel(\"Probabilité\", fontsize=9)\n    ax.set_title(title, fontsize=10)\n    for bar, val in zip(bars, probs):\n        ax.text(bar.get_x() + bar.get_width() / 2, val + 0.02,\n                f\"{val:.2f}\", ha=\"center\", fontsize=8)\n\nplt.tight_layout()\nplt.savefig(\"scorecam_aptos_03be80919be4.png\", dpi=150, bbox_inches=\"tight\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T20:42:49.572622Z","iopub.execute_input":"2026-08-08T20:42:49.573408Z","iopub.status.idle":"2026-08-08T20:42:55.301978Z","shell.execute_reply.started":"2026-08-08T20:42:49.573376Z","shell.execute_reply":"2026-08-08T20:42:55.300555Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ══════════════════════════════════════════════════════════\n# CELLULE AUTONOME — Grad-CAM++ sur couche intermédiaire\n# (résolution spatiale plus fine → localisation plus précise)\n# ══════════════════════════════════════════════════════════\n\nimport os\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom PIL import Image\nfrom torchvision import transforms\nimport torchvision.models as models\nfrom torchvision.models import efficientnet_b4\n\n# ── 0. Device ──────────────────────────────────────────────\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# ── 1. Chemins des poids sauvegardés ────────────────────────\nRESNET_PATH = \"resnet50_best.pth\"\nDENSE_PATH  = \"densenet121_best.pth\"\nEFF_PATH    = \"efficientnet_b4_best.pth\"\n\n# ── 2. Reconstruire les architectures ──────────────────────\nresnet_model = models.resnet50(weights=None)\nresnet_model.fc = nn.Linear(resnet_model.fc.in_features, 5)\n\ndense_model = models.densenet121(weights=None)\ndense_model.classifier = nn.Linear(dense_model.classifier.in_features, 5)\n\neff_model = efficientnet_b4(weights=None)\neff_model.classifier[1] = nn.Linear(eff_model.classifier[1].in_features, 5)\n\n# ── 3. Charger les poids ────────────────────────────────────\nresnet_model.load_state_dict(torch.load(RESNET_PATH, map_location=device))\ndense_model.load_state_dict(torch.load(DENSE_PATH,  map_location=device))\neff_model.load_state_dict(torch.load(EFF_PATH,     map_location=device))\n\nresnet_model.to(device).eval()\ndense_model.to(device).eval()\neff_model.to(device).eval()\n\n# ── 4. Poids du soft voting (kappas de l'entraînement) ──────\nkappas = np.array([0.8304, 0.8144, 0.7690])  # resnet, dense, eff_b4\nweights_vote = kappas / kappas.sum()\nprint(f\"Poids → ResNet: {weights_vote[0]:.4f} | DenseNet: {weights_vote[1]:.4f} | EfficientNet-B4: {weights_vote[2]:.4f}\")\n\n# ── 5. Seuils optimisés (tes vraies valeurs) ────────────────\ndef apply_thresholds(probs, thresholds):\n    if probs.ndim == 2:\n        scores = probs @ np.arange(probs.shape[1])\n    else:\n        scores = probs\n    t1, t2, t3, t4 = thresholds\n    preds = np.zeros(len(scores), dtype=int)\n    preds[scores >= t1] = 1\n    preds[scores >= t2] = 2\n    preds[scores >= t3] = 3\n    preds[scores >= t4] = 4\n    return np.clip(preds, 0, 4)\n\nbest_thresholds = [0.6, 1.3, 2.75, 3.5]\n\n# ── 6. Classe Grad-CAM++ ─────────────────────────────────────\nclass GradCAMPlusPlus:\n    def __init__(self, model, target_layer):\n        self.model = model\n        self.activations = None\n        self.gradients = None\n        self._fwd_handle = target_layer.register_forward_hook(self._save_activation)\n        self._bwd_handle = target_layer.register_full_backward_hook(self._save_gradient)\n\n    def _save_activation(self, module, input, output):\n        self.activations = output.detach()\n\n    def _save_gradient(self, module, grad_input, grad_output):\n        self.gradients = grad_output[0].detach()\n\n    def generate(self, tensor, class_idx=None):\n        self.model.eval()\n        output = self.model(tensor.to(device))\n        if class_idx is None:\n            class_idx = output.argmax(dim=1).item()\n        self.model.zero_grad()\n        output[0, class_idx].backward()\n\n        grads = self.gradients\n        acts  = self.activations\n\n        grads_sq  = grads ** 2\n        grads_cub = grads ** 3\n        sum_acts  = acts.sum(dim=[2, 3], keepdim=True)\n\n        alpha_num   = grads_sq\n        alpha_denom = 2 * grads_sq + sum_acts * grads_cub\n        alpha_denom = torch.where(alpha_denom != 0, alpha_denom, torch.ones_like(alpha_denom))\n        alpha = alpha_num / alpha_denom\n\n        weights = (alpha * F.relu(grads)).sum(dim=[2, 3], keepdim=True)\n\n        cam = F.relu((weights * acts).sum(dim=1, keepdim=True))\n        cam = F.interpolate(cam, size=(380, 380), mode=\"bilinear\", align_corners=False)\n        cam = cam.squeeze().cpu().numpy()\n        cam = (cam - cam.min()) / (cam.max() - cam.min() + 1e-8)\n        return cam\n\n    def remove_hooks(self):\n        self._fwd_handle.remove()\n        self._bwd_handle.remove()\n\n\ndef get_gradcam_overlay(cam, img_pil):\n    heatmap = plt.cm.jet(cam)[:, :, :3]\n    base = np.array(img_pil) / 255.0\n    overlay = 0.5 * base + 0.5 * heatmap\n    return np.clip(overlay, 0, 1)\n\n# ══════════════════════════════════════════════════════════\n# CELLULE GRAD-CAM++ — COUCHES INTERMÉDIAIRES (plus de détail)\n# ══════════════════════════════════════════════════════════\n\n# ── 7. Charger l'image ───────────────────────────────────────\nimage_path = \"/kaggle/input/competitions/aptos2019-blindness-detection/test_images/03be80919be4.png\"\n\ntransform = transforms.Compose([\n    transforms.Resize((380, 380)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406],\n                          [0.229, 0.224, 0.225])\n])\n\nimg = Image.open(image_path).convert(\"RGB\")\nimg_tensor = transform(img).unsqueeze(0)\nimg_show = img.resize((380, 380))\n\n# ── 8. Instancier Grad-CAM++ sur des couches INTERMÉDIAIRES ──\n#    (résolution ~24×24 au lieu de ~12×12 pour les couches finales)\ngcam_resnet = GradCAMPlusPlus(resnet_model, resnet_model.layer3[-1])\ngcam_dense  = GradCAMPlusPlus(dense_model,  dense_model.features.denseblock3)\ngcam_eff    = GradCAMPlusPlus(eff_model,    eff_model.features[5])\n\n# ── 9. Probabilités de chaque modèle ────────────────────────\ndef predict_single(model, tensor):\n    model.eval()\n    with torch.no_grad():\n        output = model(tensor.to(device))\n        return torch.softmax(output, dim=1).cpu().numpy()\n\nresnet_p = predict_single(resnet_model, img_tensor)\ndense_p  = predict_single(dense_model,  img_tensor)\neff_p    = predict_single(eff_model,    img_tensor)\n\n# ── 10. Soft Voting + classe prédite ────────────────────────\nsoft_p = (\n      weights_vote[0] * resnet_p\n    + weights_vote[1] * dense_p\n    + weights_vote[2] * eff_p\n)\n\npred_class  = apply_thresholds(soft_p, best_thresholds)[0]\nclass_names = [\"No DR\", \"Mild\", \"Moderate\", \"Severe\", \"Proliferative DR\"]\n\n# ── 11. Cartes Grad-CAM++ pour la classe prédite ────────────\ncam_resnet = gcam_resnet.generate(img_tensor, class_idx=pred_class)\ncam_dense  = gcam_dense.generate(img_tensor,  class_idx=pred_class)\ncam_eff    = gcam_eff.generate(img_tensor,    class_idx=pred_class)\n\noverlay_resnet = get_gradcam_overlay(cam_resnet, img_show)\noverlay_dense  = get_gradcam_overlay(cam_dense,  img_show)\noverlay_eff    = get_gradcam_overlay(cam_eff,    img_show)\n\n# ── 12. Nettoyage des hooks ──────────────────────────────────\ngcam_resnet.remove_hooks()\ngcam_dense.remove_hooks()\ngcam_eff.remove_hooks()\n\n# ── 13. Résultat terminal ───────────────────────────────────\nprint(f\"Classe prédite : {pred_class} — {class_names[pred_class]}\")\nprint(\"Probabilités   :\")\nfor i, c in enumerate(class_names):\n    print(f\"  {c:<20} : {soft_p[0][i]:.4f}\")\n\n# ── 14. Visualisation ───────────────────────────────────────\nfig, axes = plt.subplots(2, 4, figsize=(18, 9))\nfig.suptitle(f\"03be80919be4.png  —  Prédiction : Grade {pred_class} ({class_names[pred_class]})  —  couches intermédiaires\",\n             fontsize=13, fontweight=\"bold\")\n\naxes[0, 0].imshow(img_show)\naxes[0, 0].set_title(\"Image originale\", fontsize=11)\naxes[0, 0].axis(\"off\")\n\nfor ax, overlay, title in zip(\n    axes[0, 1:],\n    [overlay_resnet, overlay_dense, overlay_eff],\n    [\"Grad-CAM++ ResNet-50 (layer3)\", \"Grad-CAM++ DenseNet-121 (denseblock3)\", \"Grad-CAM++ EfficientNet-B4 (features[5])\"]\n):\n    ax.imshow(overlay)\n    ax.set_title(title, fontsize=9)\n    ax.axis(\"off\")\n\nbar_colors = [\"green\" if i == pred_class else \"steelblue\" for i in range(5)]\ntitles_probs = [\n    (\"Soft Voting (final)\", soft_p[0]),\n    (\"ResNet-50\",           resnet_p[0]),\n    (\"DenseNet-121\",        dense_p[0]),\n    (\"EfficientNet-B4\",     eff_p[0]),\n]\n\nfor ax, (title, probs) in zip(axes[1], titles_probs):\n    bars = ax.bar(range(5), probs, color=bar_colors, edgecolor=\"black\", linewidth=0.5)\n    bars[pred_class].set_edgecolor(\"red\")\n    bars[pred_class].set_linewidth(2.5)\n    ax.set_xticks(range(5))\n    ax.set_xticklabels([\"G0\", \"G1\", \"G2\", \"G3\", \"G4\"], fontsize=9)\n    ax.set_ylim(0, 1)\n    ax.set_ylabel(\"Probabilité\", fontsize=9)\n    ax.set_title(title, fontsize=10)\n    for bar, val in zip(bars, probs):\n        ax.text(bar.get_x() + bar.get_width() / 2, val + 0.02,\n                f\"{val:.2f}\", ha=\"center\", fontsize=8)\n\nplt.tight_layout()\nplt.savefig(\"gradcamplusplus_intermediate_03be80919be4.png\", dpi=150, bbox_inches=\"tight\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T20:44:49.649542Z","iopub.execute_input":"2026-08-08T20:44:49.650349Z","iopub.status.idle":"2026-08-08T20:44:53.944255Z","shell.execute_reply.started":"2026-08-08T20:44:49.650309Z","shell.execute_reply":"2026-08-08T20:44:53.943368Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ══════════════════════════════════════════════════════════\n# CELLULE AUTONOME — Occlusion Sensitivity (sans gradients, sans couche cachée)\n# ══════════════════════════════════════════════════════════\n\nimport os\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom PIL import Image\nfrom torchvision import transforms\nimport torchvision.models as models\nfrom torchvision.models import efficientnet_b4\n\n# ── 0. Device ──────────────────────────────────────────────\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# ── 1. Chemins des poids sauvegardés ────────────────────────\nRESNET_PATH = \"resnet50_best.pth\"\nDENSE_PATH  = \"densenet121_best.pth\"\nEFF_PATH    = \"efficientnet_b4_best.pth\"\n\n# ── 2. Reconstruire les architectures ──────────────────────\nresnet_model = models.resnet50(weights=None)\nresnet_model.fc = nn.Linear(resnet_model.fc.in_features, 5)\n\ndense_model = models.densenet121(weights=None)\ndense_model.classifier = nn.Linear(dense_model.classifier.in_features, 5)\n\neff_model = efficientnet_b4(weights=None)\neff_model.classifier[1] = nn.Linear(eff_model.classifier[1].in_features, 5)\n\n# ── 3. Charger les poids ────────────────────────────────────\nresnet_model.load_state_dict(torch.load(RESNET_PATH, map_location=device))\ndense_model.load_state_dict(torch.load(DENSE_PATH,  map_location=device))\neff_model.load_state_dict(torch.load(EFF_PATH,     map_location=device))\n\nresnet_model.to(device).eval()\ndense_model.to(device).eval()\neff_model.to(device).eval()\n\n# ── 4. Poids du soft voting (kappas de l'entraînement) ──────\nkappas = np.array([0.8304, 0.8144, 0.7690])  # resnet, dense, eff_b4\nweights_vote = kappas / kappas.sum()\nprint(f\"Poids → ResNet: {weights_vote[0]:.4f} | DenseNet: {weights_vote[1]:.4f} | EfficientNet-B4: {weights_vote[2]:.4f}\")\n\n# ── 5. Seuils optimisés (tes vraies valeurs) ────────────────\ndef apply_thresholds(probs, thresholds):\n    if probs.ndim == 2:\n        scores = probs @ np.arange(probs.shape[1])\n    else:\n        scores = probs\n    t1, t2, t3, t4 = thresholds\n    preds = np.zeros(len(scores), dtype=int)\n    preds[scores >= t1] = 1\n    preds[scores >= t2] = 2\n    preds[scores >= t3] = 3\n    preds[scores >= t4] = 4\n    return np.clip(preds, 0, 4)\n\nbest_thresholds = [0.6, 1.3, 2.75, 3.5]\n\n# ── 6. Occlusion Sensitivity ─────────────────────────────────\ndef occlusion_sensitivity(model, tensor, class_idx, patch_size=40, stride=20, baseline=0.0):\n    \"\"\"\n    Fait glisser un patch masqué sur l'image et mesure la chute de probabilité\n    pour la classe prédite. Aucun gradient, aucune couche cachée requise.\n    \"\"\"\n    model.eval()\n    tensor = tensor.to(device)\n    _, C, H, W = tensor.shape\n\n    with torch.no_grad():\n        base_prob = torch.softmax(model(tensor), dim=1)[0, class_idx].item()\n\n    heatmap = np.zeros((H, W))\n    counts  = np.zeros((H, W))\n\n    positions = [(y, x) for y in range(0, H - patch_size + 1, stride)\n                         for x in range(0, W - patch_size + 1, stride)]\n\n    batch_size = 16\n    with torch.no_grad():\n        for i in range(0, len(positions), batch_size):\n            batch_pos = positions[i:i + batch_size]\n            batch = tensor.repeat(len(batch_pos), 1, 1, 1).clone()\n            for j, (y, x) in enumerate(batch_pos):\n                batch[j, :, y:y+patch_size, x:x+patch_size] = baseline\n            probs = torch.softmax(model(batch), dim=1)[:, class_idx].cpu().numpy()\n            for (y, x), p in zip(batch_pos, probs):\n                drop = base_prob - p          # chute de proba = importance de la zone\n                heatmap[y:y+patch_size, x:x+patch_size] += drop\n                counts[y:y+patch_size, x:x+patch_size]  += 1\n\n    counts[counts == 0] = 1\n    heatmap = heatmap / counts\n    heatmap = np.clip(heatmap, 0, None)                        # garder seulement les chutes positives\n    heatmap = (heatmap - heatmap.min()) / (heatmap.max() - heatmap.min() + 1e-8)\n    return heatmap\n\n\ndef get_gradcam_overlay(cam, img_pil):\n    heatmap = plt.cm.jet(cam)[:, :, :3]\n    base = np.array(img_pil) / 255.0\n    overlay = 0.5 * base + 0.5 * heatmap\n    return np.clip(overlay, 0, 1)\n\n# ══════════════════════════════════════════════════════════\n# CELLULE OCCLUSION SENSITIVITY — image APTOS 03be80919be4.png\n# ══════════════════════════════════════════════════════════\n\n# ── 7. Charger l'image ───────────────────────────────────────\nimage_path = \"/kaggle/input/competitions/aptos2019-blindness-detection/test_images/03be80919be4.png\"\n\ntransform = transforms.Compose([\n    transforms.Resize((380, 380)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406],\n                          [0.229, 0.224, 0.225])\n])\n\nimg = Image.open(image_path).convert(\"RGB\")\nimg_tensor = transform(img).unsqueeze(0)\nimg_show = img.resize((380, 380))\n\n# ── 8. Probabilités de chaque modèle ────────────────────────\ndef predict_single(model, tensor):\n    model.eval()\n    with torch.no_grad():\n        output = model(tensor.to(device))\n        return torch.softmax(output, dim=1).cpu().numpy()\n\nresnet_p = predict_single(resnet_model, img_tensor)\ndense_p  = predict_single(dense_model,  img_tensor)\neff_p    = predict_single(eff_model,    img_tensor)\n\n# ── 9. Soft Voting + classe prédite ─────────────────────────\nsoft_p = (\n      weights_vote[0] * resnet_p\n    + weights_vote[1] * dense_p\n    + weights_vote[2] * eff_p\n)\n\npred_class  = apply_thresholds(soft_p, best_thresholds)[0]\nclass_names = [\"No DR\", \"Mild\", \"Moderate\", \"Severe\", \"Proliferative DR\"]\n\n# ── 10. Cartes d'Occlusion Sensitivity (peut prendre 30-60s par modèle) ──\ncam_resnet = occlusion_sensitivity(resnet_model, img_tensor, pred_class, patch_size=40, stride=20)\ncam_dense  = occlusion_sensitivity(dense_model,  img_tensor, pred_class, patch_size=40, stride=20)\ncam_eff    = occlusion_sensitivity(eff_model,    img_tensor, pred_class, patch_size=40, stride=20)\n\noverlay_resnet = get_gradcam_overlay(cam_resnet, img_show)\noverlay_dense  = get_gradcam_overlay(cam_dense,  img_show)\noverlay_eff    = get_gradcam_overlay(cam_eff,    img_show)\n\n# ── 11. Résultat terminal ───────────────────────────────────\nprint(f\"Classe prédite : {pred_class} — {class_names[pred_class]}\")\nprint(\"Probabilités   :\")\nfor i, c in enumerate(class_names):\n    print(f\"  {c:<20} : {soft_p[0][i]:.4f}\")\n\n# ── 12. Visualisation ───────────────────────────────────────\nfig, axes = plt.subplots(2, 4, figsize=(18, 9))\nfig.suptitle(f\"03be80919be4.png  —  Prédiction : Grade {pred_class} ({class_names[pred_class]})  —  Occlusion Sensitivity\",\n             fontsize=13, fontweight=\"bold\")\n\naxes[0, 0].imshow(img_show)\naxes[0, 0].set_title(\"Image originale\", fontsize=11)\naxes[0, 0].axis(\"off\")\n\nfor ax, overlay, title in zip(\n    axes[0, 1:],\n    [overlay_resnet, overlay_dense, overlay_eff],\n    [\"Occlusion ResNet-50\", \"Occlusion DenseNet-121\", \"Occlusion EfficientNet-B4\"]\n):\n    ax.imshow(overlay)\n    ax.set_title(title, fontsize=10)\n    ax.axis(\"off\")\n\nbar_colors = [\"green\" if i == pred_class else \"steelblue\" for i in range(5)]\ntitles_probs = [\n    (\"Soft Voting (final)\", soft_p[0]),\n    (\"ResNet-50\",           resnet_p[0]),\n    (\"DenseNet-121\",        dense_p[0]),\n    (\"EfficientNet-B4\",     eff_p[0]),\n]\n\nfor ax, (title, probs) in zip(axes[1], titles_probs):\n    bars = ax.bar(range(5), probs, color=bar_colors, edgecolor=\"black\", linewidth=0.5)\n    bars[pred_class].set_edgecolor(\"red\")\n    bars[pred_class].set_linewidth(2.5)\n    ax.set_xticks(range(5))\n    ax.set_xticklabels([\"G0\", \"G1\", \"G2\", \"G3\", \"G4\"], fontsize=9)\n    ax.set_ylim(0, 1)\n    ax.set_ylabel(\"Probabilité\", fontsize=9)\n    ax.set_title(title, fontsize=10)\n    for bar, val in zip(bars, probs):\n        ax.text(bar.get_x() + bar.get_width() / 2, val + 0.02,\n                f\"{val:.2f}\", ha=\"center\", fontsize=8)\n\nplt.tight_layout()\nplt.savefig(\"occlusion_aptos_03be80919be4.png\", dpi=150, bbox_inches=\"tight\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T20:46:40.632757Z","iopub.execute_input":"2026-08-08T20:46:40.633373Z","iopub.status.idle":"2026-08-08T20:46:54.874356Z","shell.execute_reply.started":"2026-08-08T20:46:40.633342Z","shell.execute_reply":"2026-08-08T20:46:54.873451Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ══════════════════════════════════════════════════════════\n# CELLULE — Courbes ROC multi-classes (ResNet, DenseNet, EffNet, Soft Voting)\n# ══════════════════════════════════════════════════════════\n\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom sklearn.metrics import roc_curve, auc\nfrom sklearn.preprocessing import label_binarize\n\n# ── Prérequis : y_true, resnet_probs, dense_probs, eff_probs, soft_probs\n#    (déjà calculés dans tes cellules précédentes — probs de VALIDATION, shape (N, 5))\n\nn_classes = 5\ny_true_bin = label_binarize(y_true, classes=list(range(n_classes)))\n\nmodels_probs = {\n    \"ResNet-50\":       resnet_probs,\n    \"DenseNet-121\":    dense_probs,\n    \"EfficientNet-B4\": eff_probs,\n    \"Soft Voting\":     soft_probs,\n}\n\nclass_names = [\"No DR\", \"Mild\", \"Moderate\", \"Severe\", \"Proliferative DR\"]\n\nfig, axes = plt.subplots(1, 4, figsize=(22, 5))\n\nfor ax, (model_name, probs) in zip(axes, models_probs.items()):\n    for c in range(n_classes):\n        fpr, tpr, _ = roc_curve(y_true_bin[:, c], probs[:, c])\n        roc_auc = auc(fpr, tpr)\n        ax.plot(fpr, tpr, label=f\"{class_names[c]} (AUC={roc_auc:.3f})\")\n    ax.plot([0, 1], [0, 1], \"k--\", linewidth=0.8)\n    ax.set_title(model_name, fontsize=11, fontweight=\"bold\")\n    ax.set_xlabel(\"Taux de faux positifs\")\n    ax.set_ylabel(\"Taux de vrais positifs\")\n    ax.legend(fontsize=7, loc=\"lower right\")\n    ax.set_xlim([0, 1])\n    ax.set_ylim([0, 1.02])\n\nplt.tight_layout()\nplt.savefig(\"roc_curves_comparison.png\", dpi=150, bbox_inches=\"tight\")\nplt.show()\n\n# ── AUC macro-moyenné par modèle (résumé chiffré pour ta thèse) ──\nprint(\"AUC macro-moyenné par modèle :\")\nfor model_name, probs in models_probs.items():\n    aucs = [auc(*roc_curve(y_true_bin[:, c], probs[:, c])[:2]) for c in range(n_classes)]\n    print(f\"  {model_name:<18} : {np.mean(aucs):.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T20:49:49.227837Z","iopub.execute_input":"2026-08-08T20:49:49.22871Z","iopub.status.idle":"2026-08-08T20:49:50.589032Z","shell.execute_reply.started":"2026-08-08T20:49:49.228643Z","shell.execute_reply":"2026-08-08T20:49:50.58839Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ══════════════════════════════════════════════════════════\n# CELLULE — Détection des EXSUDATS (lésions claires/jaunâtres)\n# Méthode : top-hat morphologique + seuillage adaptatif\n# ══════════════════════════════════════════════════════════\n\nimport cv2\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom PIL import Image\n\ndef detect_exudates(img_rgb):\n    \"\"\"\n    Détecte les exsudats (lésions claires, riches en lipides) via\n    top-hat morphologique sur le canal vert (meilleur contraste pour le fond d'œil).\n    \"\"\"\n    green = img_rgb[:, :, 1]  # canal vert = meilleur contraste vasculaire/lésionnel\n\n    # ── CLAHE pour renforcer le contraste local ─────────────\n    clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))\n    green_eq = clahe.apply(green)\n\n    # ── Top-hat morphologique : isole les structures claires ──\n    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15))\n    tophat = cv2.morphologyEx(green_eq, cv2.MORPH_TOPHAT, kernel)\n\n    # ── Seuillage adaptatif (Otsu) ───────────────────────────\n    _, mask = cv2.threshold(tophat, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)\n\n    # ── Nettoyage : enlever le bruit isolé ───────────────────\n    mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN,\n                             cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)))\n\n    return mask\n\n\ndef detect_red_lesions(img_rgb):\n    \"\"\"\n    Détecte les lésions rouges (microanévrismes + hémorragies) via\n    black-hat morphologique sur le canal vert inversé.\n    \"\"\"\n    green = img_rgb[:, :, 1]\n    green_inv = 255 - green\n\n    clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))\n    green_inv_eq = clahe.apply(green_inv)\n\n    # ── Black-hat : isole les petites structures sombres (rouges dans l'original) ──\n    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (11, 11))\n    blackhat = cv2.morphologyEx(green_inv_eq, cv2.MORPH_BLACKHAT, kernel)\n\n    _, mask = cv2.threshold(blackhat, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)\n    mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN,\n                             cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2, 2)))\n\n    return mask\n\n\ndef overlay_lesions(img_rgb, exudate_mask, red_mask):\n    \"\"\"Superpose exsudats (jaune) et lésions rouges (rouge) sur l'image originale.\"\"\"\n    overlay = img_rgb.copy().astype(float) / 255.0\n\n    exu_color = np.zeros_like(overlay)\n    exu_color[:, :, 0] = 1.0   # rouge\n    exu_color[:, :, 1] = 1.0   # + vert = jaune\n\n    red_color = np.zeros_like(overlay)\n    red_color[:, :, 0] = 1.0   # rouge pur\n\n    exu_norm = (exudate_mask / 255.0)[:, :, None]\n    red_norm = (red_mask / 255.0)[:, :, None]\n\n    overlay = overlay * (1 - exu_norm * 0.7) + exu_color * exu_norm * 0.7\n    overlay = overlay * (1 - red_norm * 0.7) + red_color * red_norm * 0.7\n\n    return np.clip(overlay, 0, 1)\n\n\n# ══════════════════════════════════════════════════════════\n# APPLICATION — image APTOS 03be80919be4.png\n# ══════════════════════════════════════════════════════════\n\nimage_path = \"/kaggle/input/competitions/aptos2019-blindness-detection/test_images/03be80919be4.png\"\n\nimg_pil = Image.open(image_path).convert(\"RGB\").resize((380, 380))\nimg_rgb = np.array(img_pil)\n\nexudate_mask   = detect_exudates(img_rgb)\nred_lesion_mask = detect_red_lesions(img_rgb)\noverlay         = overlay_lesions(img_rgb, exudate_mask, red_lesion_mask)\n\nfig, axes = plt.subplots(1, 4, figsize=(20, 5))\n\naxes[0].imshow(img_rgb)\naxes[0].set_title(\"Image originale\", fontsize=11)\naxes[0].axis(\"off\")\n\naxes[1].imshow(exudate_mask, cmap=\"gray\")\naxes[1].set_title(\"Masque exsudats (jaune)\", fontsize=11)\naxes[1].axis(\"off\")\n\naxes[2].imshow(red_lesion_mask, cmap=\"gray\")\naxes[2].set_title(\"Masque lésions rouges (MA + hémorragies)\", fontsize=11)\naxes[2].axis(\"off\")\n\naxes[3].imshow(overlay)\naxes[3].set_title(\"Superposition finale\", fontsize=11)\naxes[3].axis(\"off\")\n\nplt.tight_layout()\nplt.savefig(\"lesion_detection_03be80919be4.png\", dpi=150, bbox_inches=\"tight\")\nplt.show()\n\n# ── Statistiques pour ta thèse (surface occupée par les lésions) ──\ntotal_pixels = img_rgb.shape[0] * img_rgb.shape[1]\nprint(f\"Surface exsudats     : {100*np.sum(exudate_mask>0)/total_pixels:.2f}% de l'image\")\nprint(f\"Surface lésions rouges : {100*np.sum(red_lesion_mask>0)/total_pixels:.2f}% de l'image\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T20:52:49.727341Z","iopub.execute_input":"2026-08-08T20:52:49.728306Z","iopub.status.idle":"2026-08-08T20:52:51.354304Z","shell.execute_reply.started":"2026-08-08T20:52:49.72826Z","shell.execute_reply":"2026-08-08T20:52:51.353434Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ══════════════════════════════════════════════════════════\n# CELLULE — Détection des lésions (VERSION CORRIGÉE)\n# Ajout : suppression des vaisseaux + filtrage par forme/taille\n# ══════════════════════════════════════════════════════════\n\nimport cv2\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom PIL import Image\n\n\ndef get_fundus_mask(img_rgb):\n    \"\"\"Masque du fond d'œil (exclut le fond noir hors rétine).\"\"\"\n    gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY)\n    _, mask = cv2.threshold(gray, 15, 255, cv2.THRESH_BINARY)\n    mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE,\n                             cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15)))\n    return mask\n\n\ndef segment_vessels(green_eq):\n    \"\"\"\n    Segmente les vaisseaux sanguins (structures fines et allongées)\n    pour pouvoir les EXCLURE de la détection de lésions.\n    \"\"\"\n    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9, 9))\n    blackhat = cv2.morphologyEx(255 - green_eq, cv2.MORPH_BLACKHAT, kernel)\n    _, vessels = cv2.threshold(blackhat, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)\n    # Dilater légèrement pour bien couvrir toute la largeur des vaisseaux\n    vessels = cv2.dilate(vessels, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)))\n    return vessels\n\n\ndef filter_by_shape(mask, min_area=8, max_area=400, min_circularity=0.35):\n    \"\"\"\n    Ne garde que les composantes rondes et de petite taille\n    (typique des microanévrismes/hémorragies ponctuelles),\n    élimine les formes allongées (résidus de vaisseaux).\n    \"\"\"\n    contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n    filtered = np.zeros_like(mask)\n\n    for cnt in contours:\n        area = cv2.contourArea(cnt)\n        if area < min_area or area > max_area:\n            continue\n        perimeter = cv2.arcLength(cnt, True)\n        if perimeter == 0:\n            continue\n        circularity = 4 * np.pi * area / (perimeter ** 2)   # 1.0 = cercle parfait\n        if circularity >= min_circularity:\n            cv2.drawContours(filtered, [cnt], -1, 255, -1)\n\n    return filtered\n\n\ndef detect_exudates(img_rgb, fundus_mask):\n    green = img_rgb[:, :, 1]\n    clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))\n    green_eq = clahe.apply(green)\n\n    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15))\n    tophat = cv2.morphologyEx(green_eq, cv2.MORPH_TOPHAT, kernel)\n    _, mask = cv2.threshold(tophat, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)\n    mask = cv2.bitwise_and(mask, fundus_mask)\n\n    # Exsudats = taches plus grandes/irrégulières que MA, seuil de taille différent\n    mask = filter_by_shape(mask, min_area=10, max_area=1500, min_circularity=0.25)\n    return mask\n\n\ndef detect_red_lesions(img_rgb, fundus_mask):\n    green = img_rgb[:, :, 1]\n    clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))\n    green_eq = clahe.apply(green)\n\n    # ── 1. Segmenter et exclure les vaisseaux ────────────────\n    vessels = segment_vessels(green_eq)\n\n    # ── 2. Détecter toutes les structures sombres ────────────\n    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (11, 11))\n    blackhat = cv2.morphologyEx(255 - green_eq, cv2.MORPH_BLACKHAT, kernel)\n    _, raw_mask = cv2.threshold(blackhat, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)\n\n    # ── 3. Soustraire les vaisseaux + limiter au fond d'œil ──\n    mask = cv2.bitwise_and(raw_mask, cv2.bitwise_not(vessels))\n    mask = cv2.bitwise_and(mask, fundus_mask)\n\n    # ── 4. Filtrer par forme : ne garder que le petit et rond ──\n    mask = filter_by_shape(mask, min_area=4, max_area=150, min_circularity=0.4)\n\n    return mask\n\n\ndef overlay_lesions(img_rgb, exudate_mask, red_mask):\n    overlay = img_rgb.copy().astype(float) / 255.0\n\n    exu_color = np.zeros_like(overlay)\n    exu_color[:, :, 0] = 1.0\n    exu_color[:, :, 1] = 1.0\n\n    red_color = np.zeros_like(overlay)\n    red_color[:, :, 0] = 1.0\n\n    exu_norm = (exudate_mask / 255.0)[:, :, None]\n    red_norm = (red_mask / 255.0)[:, :, None]\n\n    overlay = overlay * (1 - exu_norm) + exu_color * exu_norm\n    overlay = overlay * (1 - red_norm) + red_color * red_norm\n\n    return np.clip(overlay, 0, 1)\n\n\n# ══════════════════════════════════════════════════════════\n# APPLICATION — image APTOS 03be80919be4.png\n# ══════════════════════════════════════════════════════════\n\nimage_path = \"/kaggle/input/competitions/aptos2019-blindness-detection/test_images/03be80919be4.png\"\n\nimg_pil = Image.open(image_path).convert(\"RGB\").resize((380, 380))\nimg_rgb = np.array(img_pil)\n\nfundus_mask     = get_fundus_mask(img_rgb)\nexudate_mask    = detect_exudates(img_rgb, fundus_mask)\nred_lesion_mask = detect_red_lesions(img_rgb, fundus_mask)\noverlay         = overlay_lesions(img_rgb, exudate_mask, red_lesion_mask)\n\nfig, axes = plt.subplots(1, 4, figsize=(20, 5))\n\naxes[0].imshow(img_rgb)\naxes[0].set_title(\"Image originale\", fontsize=11)\naxes[0].axis(\"off\")\n\naxes[1].imshow(exudate_mask, cmap=\"gray\")\naxes[1].set_title(\"Exsudats détectés\", fontsize=11)\naxes[1].axis(\"off\")\n\naxes[2].imshow(red_lesion_mask, cmap=\"gray\")\naxes[2].set_title(\"Lésions rouges (vaisseaux exclus)\", fontsize=11)\naxes[2].axis(\"off\")\n\naxes[3].imshow(overlay)\naxes[3].set_title(\"Superposition finale\", fontsize=11)\naxes[3].axis(\"off\")\n\nplt.tight_layout()\nplt.savefig(\"lesion_detection_corrected_03be80919be4.png\", dpi=150, bbox_inches=\"tight\")\nplt.show()\n\ntotal_pixels = np.sum(fundus_mask > 0)\nprint(f\"Surface exsudats       : {100*np.sum(exudate_mask>0)/total_pixels:.2f}% du fond d'œil\")\nprint(f\"Surface lésions rouges : {100*np.sum(red_lesion_mask>0)/total_pixels:.2f}% du fond d'œil\")\nprint(f\"Nb composantes exsudats : {len(cv2.findContours(exudate_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0])}\")\nprint(f\"Nb composantes lésions rouges : {len(cv2.findContours(red_lesion_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0])}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T20:55:00.182267Z","iopub.execute_input":"2026-08-08T20:55:00.183167Z","iopub.status.idle":"2026-08-08T20:55:01.597092Z","shell.execute_reply.started":"2026-08-08T20:55:00.183134Z","shell.execute_reply":"2026-08-08T20:55:01.596264Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ══════════════════════════════════════════════════════════\n# CELLULE — Lésions rouges (VERSION RÉÉQUILIBRÉE)\n# Assouplit la circularité + réduit la dilatation des vaisseaux\n# ══════════════════════════════════════════════════════════\n\ndef segment_vessels(green_eq, dilate_size=2):\n    \"\"\"dilate_size réduit (2 au lieu de 3) pour épargner les MA proches des vaisseaux.\"\"\"\n    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9, 9))\n    blackhat = cv2.morphologyEx(255 - green_eq, cv2.MORPH_BLACKHAT, kernel)\n    _, vessels = cv2.threshold(blackhat, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)\n    vessels = cv2.dilate(vessels, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (dilate_size, dilate_size)))\n    return vessels\n\n\ndef filter_by_shape(mask, min_area=8, max_area=400, min_circularity=0.35):\n    contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n    filtered = np.zeros_like(mask)\n    for cnt in contours:\n        area = cv2.contourArea(cnt)\n        if area < min_area or area > max_area:\n            continue\n        perimeter = cv2.arcLength(cnt, True)\n        if perimeter == 0:\n            continue\n        circularity = 4 * np.pi * area / (perimeter ** 2)\n        if circularity >= min_circularity:\n            cv2.drawContours(filtered, [cnt], -1, 255, -1)\n    return filtered\n\n\ndef detect_red_lesions(img_rgb, fundus_mask):\n    green = img_rgb[:, :, 1]\n    clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))\n    green_eq = clahe.apply(green)\n\n    # ── Dilatation réduite pour ne pas effacer les MA proches des vaisseaux ──\n    vessels = segment_vessels(green_eq, dilate_size=2)\n\n    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (11, 11))\n    blackhat = cv2.morphologyEx(255 - green_eq, cv2.MORPH_BLACKHAT, kernel)\n    _, raw_mask = cv2.threshold(blackhat, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)\n\n    mask = cv2.bitwise_and(raw_mask, cv2.bitwise_not(vessels))\n    mask = cv2.bitwise_and(mask, fundus_mask)\n\n    # ── Circularité assouplie (0.4 → 0.25) pour ne pas rejeter les vraies MA ──\n    mask = filter_by_shape(mask, min_area=3, max_area=200, min_circularity=0.25)\n\n    return mask\n\n\n# ══════════════════════════════════════════════════════════\n# RÉ-APPLICATION avec les fonctions mises à jour\n# ══════════════════════════════════════════════════════════\n\nred_lesion_mask = detect_red_lesions(img_rgb, fundus_mask)\noverlay = overlay_lesions(img_rgb, exudate_mask, red_lesion_mask)\n\nfig, axes = plt.subplots(1, 4, figsize=(20, 5))\n\naxes[0].imshow(img_rgb)\naxes[0].set_title(\"Image originale\", fontsize=11)\naxes[0].axis(\"off\")\n\naxes[1].imshow(exudate_mask, cmap=\"gray\")\naxes[1].set_title(\"Exsudats détectés (inchangé)\", fontsize=11)\naxes[1].axis(\"off\")\n\naxes[2].imshow(red_lesion_mask, cmap=\"gray\")\naxes[2].set_title(\"Lésions rouges (rééquilibré)\", fontsize=11)\naxes[2].axis(\"off\")\n\naxes[3].imshow(overlay)\naxes[3].set_title(\"Superposition finale\", fontsize=11)\naxes[3].axis(\"off\")\n\nplt.tight_layout()\nplt.savefig(\"lesion_detection_rebalanced_03be80919be4.png\", dpi=150, bbox_inches=\"tight\")\nplt.show()\n\ntotal_pixels = np.sum(fundus_mask > 0)\nn_exu = len(cv2.findContours(exudate_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0])\nn_red = len(cv2.findContours(red_lesion_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0])\n\nprint(f\"Surface exsudats       : {100*np.sum(exudate_mask>0)/total_pixels:.2f}% du fond d'œil\")\nprint(f\"Surface lésions rouges : {100*np.sum(red_lesion_mask>0)/total_pixels:.2f}% du fond d'œil\")\nprint(f\"Nb composantes exsudats       : {n_exu}\")\nprint(f\"Nb composantes lésions rouges : {n_red}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T20:56:39.80219Z","iopub.execute_input":"2026-08-08T20:56:39.803044Z","iopub.status.idle":"2026-08-08T20:56:41.202582Z","shell.execute_reply.started":"2026-08-08T20:56:39.803013Z","shell.execute_reply":"2026-08-08T20:56:41.201976Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ══════════════════════════════════════════════════════════\n# CELLULE — Validation : nb de lésions détectées vs sévérité DR\n# ══════════════════════════════════════════════════════════\n\nimport pandas as pd\n\n# ── Prends quelques images APTOS de grades différents connus ──\n# (adapte les id_code depuis ton train.csv, un exemple par grade)\nsample_df = df.groupby(\"diagnosis\").apply(lambda x: x.sample(1, random_state=42)).reset_index(drop=True)\n\nresults = []\nfor _, row in sample_df.iterrows():\n    img_path = os.path.join(train_dir, row[\"id_code\"] + \".png\")\n    img_pil = Image.open(img_path).convert(\"RGB\").resize((380, 380))\n    img_rgb = np.array(img_pil)\n\n    fundus_mask     = get_fundus_mask(img_rgb)\n    exudate_mask    = detect_exudates(img_rgb, fundus_mask)\n    red_lesion_mask = detect_red_lesions(img_rgb, fundus_mask)\n\n    n_exu = len(cv2.findContours(exudate_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0])\n    n_red = len(cv2.findContours(red_lesion_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0])\n\n    results.append({\n        \"id_code\": row[\"id_code\"],\n        \"diagnosis\": row[\"diagnosis\"],\n        \"grade_name\": class_names[row[\"diagnosis\"]],\n        \"nb_exsudats\": n_exu,\n        \"nb_lesions_rouges\": n_red,\n    })\n\nresults_df = pd.DataFrame(results).sort_values(\"diagnosis\")\nprint(results_df.to_string(index=False))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ══════════════════════════════════════════════════════════\n# CELLULE — Validation : nb de lésions détectées vs sévérité DR\n# ══════════════════════════════════════════════════════════\n\nimport pandas as pd\n\n# ── Prends quelques images APTOS de grades différents connus ──\n# (adapte les id_code depuis ton train.csv, un exemple par grade)\nsample_df = df.groupby(\"diagnosis\").apply(lambda x: x.sample(1, random_state=42)).reset_index(drop=True)\n\nresults = []\nfor _, row in sample_df.iterrows():\n    img_path = os.path.join(train_dir, row[\"id_code\"] + \".png\")\n    img_pil = Image.open(img_path).convert(\"RGB\").resize((380, 380))\n    img_rgb = np.array(img_pil)\n\n    fundus_mask     = get_fundus_mask(img_rgb)\n    exudate_mask    = detect_exudates(img_rgb, fundus_mask)\n    red_lesion_mask = detect_red_lesions(img_rgb, fundus_mask)\n\n    n_exu = len(cv2.findContours(exudate_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0])\n    n_red = len(cv2.findContours(red_lesion_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0])\n\n    results.append({\n        \"id_code\": row[\"id_code\"],\n        \"diagnosis\": row[\"diagnosis\"],\n        \"grade_name\": class_names[row[\"diagnosis\"]],\n        \"nb_exsudats\": n_exu,\n        \"nb_lesions_rouges\": n_red,\n    })\n\nresults_df = pd.DataFrame(results).sort_values(\"diagnosis\")\nprint(results_df.to_string(index=False))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T20:58:32.728425Z","iopub.execute_input":"2026-08-08T20:58:32.729253Z","iopub.status.idle":"2026-08-08T20:58:33.594863Z","shell.execute_reply.started":"2026-08-08T20:58:32.729219Z","shell.execute_reply":"2026-08-08T20:58:33.594023Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ══════════════════════════════════════════════════════════\n# CELLULE — Validation robuste : moyenne sur N images par grade\n# ══════════════════════════════════════════════════════════\n\nimport pandas as pd\n\nN_PER_CLASS = 15  # augmente si tu as le temps (30-50s pour ~75 images)\n\nsample_df = (\n    df.groupby(\"diagnosis\")\n    .apply(lambda x: x.sample(min(N_PER_CLASS, len(x)), random_state=42))\n    .reset_index(drop=True)\n)\n\nresults = []\nfor _, row in sample_df.iterrows():\n    img_path = os.path.join(train_dir, row[\"id_code\"] + \".png\")\n    img_pil = Image.open(img_path).convert(\"RGB\").resize((380, 380))\n    img_rgb = np.array(img_pil)\n\n    fundus_mask     = get_fundus_mask(img_rgb)\n    exudate_mask    = detect_exudates(img_rgb, fundus_mask)\n    red_lesion_mask = detect_red_lesions(img_rgb, fundus_mask)\n\n    n_exu = len(cv2.findContours(exudate_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0])\n    n_red = len(cv2.findContours(red_lesion_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0])\n\n    results.append({\n        \"id_code\": row[\"id_code\"],\n        \"diagnosis\": row[\"diagnosis\"],\n        \"nb_exsudats\": n_exu,\n        \"nb_lesions_rouges\": n_red,\n    })\n\nresults_df = pd.DataFrame(results)\n\n# ── Moyenne + écart-type par grade (résultat à présenter) ──\nsummary = results_df.groupby(\"diagnosis\").agg(\n    nb_exsudats_moy=(\"nb_exsudats\", \"mean\"),\n    nb_exsudats_std=(\"nb_exsudats\", \"std\"),\n    nb_lesions_rouges_moy=(\"nb_lesions_rouges\", \"mean\"),\n    nb_lesions_rouges_std=(\"nb_lesions_rouges\", \"std\"),\n    n_images=(\"nb_exsudats\", \"count\"),\n).reset_index()\nsummary[\"grade_name\"] = summary[\"diagnosis\"].map(dict(enumerate(class_names)))\n\nprint(summary.to_string(index=False))\n\n# ── Visualisation avec barres d'erreur ──\nfig, axes = plt.subplots(1, 2, figsize=(14, 5))\n\naxes[0].bar(summary[\"grade_name\"], summary[\"nb_exsudats_moy\"],\n            yerr=summary[\"nb_exsudats_std\"], capsize=5, color=\"goldenrod\")\naxes[0].set_title(\"Exsudats moyens par grade\")\naxes[0].set_ylabel(\"Nombre moyen de composantes\")\naxes[0].tick_params(axis=\"x\", rotation=30)\n\naxes[1].bar(summary[\"grade_name\"], summary[\"nb_lesions_rouges_moy\"],\n            yerr=summary[\"nb_lesions_rouges_std\"], capsize=5, color=\"firebrick\")\naxes[1].set_title(\"Lésions rouges moyennes par grade\")\naxes[1].set_ylabel(\"Nombre moyen de composantes\")\naxes[1].tick_params(axis=\"x\", rotation=30)\n\nplt.tight_layout()\nplt.savefig(\"lesion_validation_by_grade.png\", dpi=150, bbox_inches=\"tight\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T20:59:31.49526Z","iopub.execute_input":"2026-08-08T20:59:31.496214Z","iopub.status.idle":"2026-08-08T20:59:45.171483Z","shell.execute_reply.started":"2026-08-08T20:59:31.496172Z","shell.execute_reply":"2026-08-08T20:59:45.170803Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ══════════════════════════════════════════════════════════\n# CELLULE — Corrélation quantitative sévérité vs nb lésions\n# ══════════════════════════════════════════════════════════\n\nfrom scipy.stats import spearmanr\n\nrho_exu, p_exu = spearmanr(results_df[\"diagnosis\"], results_df[\"nb_exsudats\"])\nrho_red, p_red = spearmanr(results_df[\"diagnosis\"], results_df[\"nb_lesions_rouges\"])\n\nprint(f\"Corrélation sévérité ↔ exsudats       : ρ = {rho_exu:.3f}  (p = {p_exu:.4f})\")\nprint(f\"Corrélation sévérité ↔ lésions rouges : ρ = {rho_red:.3f}  (p = {p_red:.4f})\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T21:01:41.863819Z","iopub.execute_input":"2026-08-08T21:01:41.864222Z","iopub.status.idle":"2026-08-08T21:01:41.87201Z","shell.execute_reply.started":"2026-08-08T21:01:41.864194Z","shell.execute_reply":"2026-08-08T21:01:41.871199Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELLULE 1 — Frangi + LoG (Fonctions de base)\n# ============================================================\n\n!pip -q install scikit-image\n\nimport cv2\nimport numpy as np\nimport matplotlib.pyplot as plt\n\nfrom PIL import Image\n\nfrom skimage.filters import frangi\nfrom skimage.feature import blob_log\nfrom skimage.measure import label, regionprops\n\n\n# ------------------------------------------------------------\n# Masque du fond d'œil\n# ------------------------------------------------------------\n\ndef get_fundus_mask(img):\n\n    gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)\n\n    _, mask = cv2.threshold(\n        gray,\n        15,\n        255,\n        cv2.THRESH_BINARY\n    )\n\n    kernel = cv2.getStructuringElement(\n        cv2.MORPH_ELLIPSE,\n        (15,15)\n    )\n\n    mask = cv2.morphologyEx(\n        mask,\n        cv2.MORPH_CLOSE,\n        kernel\n    )\n\n    return mask\n\n\n# ------------------------------------------------------------\n# CLAHE\n# ------------------------------------------------------------\n\ndef enhance_green(img):\n\n    green = img[:,:,1]\n\n    clahe = cv2.createCLAHE(\n        clipLimit=3.0,\n        tileGridSize=(8,8)\n    )\n\n    return clahe.apply(green)\n\n\n# ------------------------------------------------------------\n# Segmentation des vaisseaux (Frangi)\n# ------------------------------------------------------------\n\ndef segment_vessels(green):\n\n    vesselness = frangi(\n        green.astype(np.float32)/255.0,\n        sigmas=range(1,6),\n        black_ridges=False\n    )\n\n    vesselness = (\n        vesselness /\n        vesselness.max()\n        *255\n    ).astype(np.uint8)\n\n    _, vessels = cv2.threshold(\n        vesselness,\n        25,\n        255,\n        cv2.THRESH_BINARY\n    )\n\n    kernel = cv2.getStructuringElement(\n        cv2.MORPH_ELLIPSE,\n        (3,3)\n    )\n\n    vessels = cv2.dilate(\n        vessels,\n        kernel\n    )\n\n    return vessels\n\n\n# ------------------------------------------------------------\n# Détection des exsudats\n# ------------------------------------------------------------\n\ndef detect_exudates(img,fundus):\n\n    green = enhance_green(img)\n\n    kernels=[9,15,25]\n\n    masks=[]\n\n    for k in kernels:\n\n        kernel=cv2.getStructuringElement(\n            cv2.MORPH_ELLIPSE,\n            (k,k)\n        )\n\n        th=cv2.morphologyEx(\n            green,\n            cv2.MORPH_TOPHAT,\n            kernel\n        )\n\n        _,m=cv2.threshold(\n            th,\n            0,\n            255,\n            cv2.THRESH_BINARY+cv2.THRESH_OTSU\n        )\n\n        masks.append(m)\n\n    mask=np.maximum.reduce(masks)\n\n    mask=cv2.bitwise_and(mask,fundus)\n\n    return mask\n\n\n# ------------------------------------------------------------\n# Détection des lésions rouges (LoG)\n# ------------------------------------------------------------\n\ndef detect_red_lesions(img,fundus):\n\n    green=enhance_green(img)\n\n    vessels=segment_vessels(green)\n\n    inv=255-green\n\n    blobs=blob_log(\n\n        inv,\n\n        min_sigma=1,\n\n        max_sigma=6,\n\n        num_sigma=10,\n\n        threshold=0.05\n\n    )\n\n    mask=np.zeros_like(green)\n\n    for y,x,r in blobs:\n\n        cv2.circle(\n\n            mask,\n\n            (int(x),int(y)),\n\n            int(r*np.sqrt(2)),\n\n            255,\n\n            -1\n\n        )\n\n    mask=cv2.bitwise_and(\n\n        mask,\n\n        cv2.bitwise_not(vessels)\n\n    )\n\n    mask=cv2.bitwise_and(\n\n        mask,\n\n        fundus\n\n    )\n\n    return mask\n\n\n# ------------------------------------------------------------\n# Overlay\n# ------------------------------------------------------------\n\ndef overlay(img,exu,red):\n\n    out=img.copy().astype(np.float32)/255\n\n    yellow=np.zeros_like(out)\n\n    yellow[:,:,0]=1\n    yellow[:,:,1]=1\n\n    redc=np.zeros_like(out)\n\n    redc[:,:,0]=1\n\n    ex=(exu/255)[:,:,None]\n\n    rd=(red/255)[:,:,None]\n\n    out=out*(1-ex)+yellow*ex\n\n    out=out*(1-rd)+redc*rd\n\n    return np.clip(out,0,1)\n\n\nprint(\"✅ Cellule 1 exécutée\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T21:59:08.302166Z","iopub.execute_input":"2026-08-08T21:59:08.302858Z","iopub.status.idle":"2026-08-08T21:59:11.713529Z","shell.execute_reply.started":"2026-08-08T21:59:08.302825Z","shell.execute_reply":"2026-08-08T21:59:11.712743Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELLULE 2 — Détection sur une image APTOS\n# ============================================================\n\nimport os\n\n# -------- Choisir automatiquement une image du train --------\n\nimage_id = df.iloc[0][\"id_code\"]          # première image du train\n# ou :\n# image_id = df.sample(1, random_state=42).iloc[0][\"id_code\"]\n\nprint(\"Image utilisée :\", image_id)\n\nimage_path = os.path.join(\n    train_dir,\n    image_id + \".png\"\n)\n\nprint(\"Chemin :\", image_path)\n\nimg = Image.open(image_path).convert(\"RGB\")\nimg = img.resize((512,512))\nimg_rgb = np.array(img)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T22:01:01.887114Z","iopub.execute_input":"2026-08-08T22:01:01.887394Z","iopub.status.idle":"2026-08-08T22:01:02.144893Z","shell.execute_reply.started":"2026-08-08T22:01:01.887372Z","shell.execute_reply":"2026-08-08T22:01:02.14396Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def detect_red_lesions(img_rgb, fundus_mask):\n\n    green = img_rgb[:,:,1]\n\n    clahe = cv2.createCLAHE(clipLimit=2.5, tileGridSize=(8,8))\n    green = clahe.apply(green)\n\n    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(15,15))\n    blackhat = cv2.morphologyEx(255-green, cv2.MORPH_BLACKHAT, kernel)\n\n    _,mask = cv2.threshold(\n        blackhat,\n        35,\n        255,\n        cv2.THRESH_BINARY\n    )\n\n    mask = cv2.bitwise_and(mask,fundus_mask)\n\n    nb, labels, stats, _ = cv2.connectedComponentsWithStats(mask)\n\n    clean = np.zeros_like(mask)\n\n    for i in range(1,nb):\n\n        area = stats[i,cv2.CC_STAT_AREA]\n\n        if area < 8:\n            continue\n\n        if area > 60:\n            continue\n\n        component = (labels==i).astype(np.uint8)*255\n\n        cnts,_ = cv2.findContours(\n            component,\n            cv2.RETR_EXTERNAL,\n            cv2.CHAIN_APPROX_SIMPLE\n        )\n\n        if len(cnts)==0:\n            continue\n\n        cnt=cnts[0]\n\n        perimeter=cv2.arcLength(cnt,True)\n\n        if perimeter==0:\n            continue\n\n        circularity=4*np.pi*area/(perimeter**2)\n\n        if circularity<0.65:\n            continue\n\n        cv2.drawContours(clean,[cnt],-1,255,-1)\n\n    return clean","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Détection\n# ============================================================\n\nfundus_mask = get_fundus_mask(img_rgb)\n\nexudate_mask = detect_exudates(\n    img_rgb,\n    fundus_mask\n)\n\nred_mask = detect_red_lesions(\n    img_rgb,\n    fundus_mask\n)\n\noverlay_img = overlay(\n    img_rgb,\n    exudate_mask,\n    red_mask\n)\n\n# ============================================================\n# Comptage des composantes\n# ============================================================\n\nlabels_exu = label(exudate_mask)\nlabels_red = label(red_mask)\n\nregions_exu = regionprops(labels_exu)\nregions_red = regionprops(labels_red)\n\nn_exu = len(regions_exu)\nn_red = len(regions_red)\n\nsurface_exu = (\n    np.sum(exudate_mask > 0)\n    /\n    np.sum(fundus_mask > 0)\n) * 100\n\nsurface_red = (\n    np.sum(red_mask > 0)\n    /\n    np.sum(fundus_mask > 0)\n) * 100\n\nprint(\"=\" * 60)\nprint(\"RESULTATS\")\nprint(\"=\" * 60)\n\nprint(f\"Nombre d'exsudats détectés       : {n_exu}\")\nprint(f\"Nombre de lésions rouges         : {n_red}\")\nprint(f\"Surface des exsudats             : {surface_exu:.2f}%\")\nprint(f\"Surface des lésions rouges       : {surface_red:.2f}%\")\n\n# ============================================================\n# Affichage\n# ============================================================\n\nfig, axes = plt.subplots(1,4,figsize=(22,6))\n\naxes[0].imshow(img_rgb)\naxes[0].set_title(\"Image originale\")\naxes[0].axis(\"off\")\n\naxes[1].imshow(exudate_mask,cmap=\"gray\")\naxes[1].set_title(\"Exsudats\")\naxes[1].axis(\"off\")\n\naxes[2].imshow(red_mask,cmap=\"gray\")\naxes[2].set_title(\"Lésions rouges (LoG)\")\naxes[2].axis(\"off\")\n\naxes[3].imshow(overlay_img)\naxes[3].set_title(\"Overlay\")\naxes[3].axis(\"off\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T22:09:07.784195Z","iopub.execute_input":"2026-08-08T22:09:07.785033Z","iopub.status.idle":"2026-08-08T22:09:08.450702Z","shell.execute_reply.started":"2026-08-08T22:09:07.785004Z","shell.execute_reply":"2026-08-08T22:09:08.449914Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from skimage.filters import frangi\n\nprint(\"Frangi disponible ✔\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T22:14:24.11912Z","iopub.execute_input":"2026-08-08T22:14:24.119869Z","iopub.status.idle":"2026-08-08T22:14:24.124254Z","shell.execute_reply.started":"2026-08-08T22:14:24.119837Z","shell.execute_reply":"2026-08-08T22:14:24.123373Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELLULE 1\n# Prétraitement + segmentation robuste des vaisseaux\n# ============================================================\n\nimport cv2\nimport numpy as np\nimport matplotlib.pyplot as plt\n\nfrom PIL import Image\n\nfrom skimage.filters import frangi\nfrom skimage.measure import label, regionprops\n\n# ------------------------------------------------------------\n# Masque du fond d'œil\n# ------------------------------------------------------------\n\ndef get_fundus_mask(img_rgb):\n\n    gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY)\n\n    _, mask = cv2.threshold(\n        gray,\n        15,\n        255,\n        cv2.THRESH_BINARY\n    )\n\n    kernel = cv2.getStructuringElement(\n        cv2.MORPH_ELLIPSE,\n        (15,15)\n    )\n\n    mask = cv2.morphologyEx(\n        mask,\n        cv2.MORPH_CLOSE,\n        kernel\n    )\n\n    mask = cv2.morphologyEx(\n        mask,\n        cv2.MORPH_OPEN,\n        kernel\n    )\n\n    return mask\n\n\n# ------------------------------------------------------------\n# CLAHE\n# ------------------------------------------------------------\n\ndef enhance_green_channel(img_rgb):\n\n    green = img_rgb[:,:,1]\n\n    clahe = cv2.createCLAHE(\n        clipLimit=2.5,\n        tileGridSize=(8,8)\n    )\n\n    green = clahe.apply(green)\n\n    return green\n\n\n# ------------------------------------------------------------\n# Segmentation Frangi\n# ------------------------------------------------------------\n\ndef segment_vessels_frangi(green):\n\n    vesselness = frangi(\n        green.astype(np.float32)/255.0,\n        sigmas=range(1,5),\n        black_ridges=True\n    )\n\n    vesselness = (\n        vesselness -\n        vesselness.min()\n    ) / (\n        vesselness.max() -\n        vesselness.min() +\n        1e-8\n    )\n\n    vessels = np.zeros_like(green)\n\n    vessels[\n        vesselness > 0.20\n    ] = 255\n\n    kernel = cv2.getStructuringElement(\n        cv2.MORPH_ELLIPSE,\n        (3,3)\n    )\n\n    vessels = cv2.dilate(\n        vessels,\n        kernel,\n        iterations=1\n    )\n\n    vessels = cv2.morphologyEx(\n        vessels,\n        cv2.MORPH_CLOSE,\n        kernel\n    )\n\n    return vessels\n\n\n# ------------------------------------------------------------\n# Visualisation\n# ------------------------------------------------------------\n\ndef show_preprocessing(img_rgb):\n\n    green = enhance_green_channel(img_rgb)\n\n    fundus = get_fundus_mask(img_rgb)\n\n    vessels = segment_vessels_frangi(green)\n\n    fig,ax = plt.subplots(1,4,figsize=(20,5))\n\n    ax[0].imshow(img_rgb)\n    ax[0].set_title(\"Image originale\")\n    ax[0].axis(\"off\")\n\n    ax[1].imshow(green,cmap=\"gray\")\n    ax[1].set_title(\"Canal vert + CLAHE\")\n    ax[1].axis(\"off\")\n\n    ax[2].imshow(fundus,cmap=\"gray\")\n    ax[2].set_title(\"Masque rétine\")\n    ax[2].axis(\"off\")\n\n    ax[3].imshow(vessels,cmap=\"gray\")\n    ax[3].set_title(\"Vaisseaux (Frangi)\")\n    ax[3].axis(\"off\")\n\n    plt.tight_layout()\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T22:18:17.879736Z","iopub.execute_input":"2026-08-08T22:18:17.880147Z","iopub.status.idle":"2026-08-08T22:18:17.891694Z","shell.execute_reply.started":"2026-08-08T22:18:17.880119Z","shell.execute_reply":"2026-08-08T22:18:17.890842Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELLULE 2\n# Détection robuste des exsudats et des lésions rouges\n# ============================================================\n\nimport cv2\nimport numpy as np\n\n\n# ------------------------------------------------------------\n# Filtrage des composantes connexes\n# ------------------------------------------------------------\n\ndef filter_components(mask,\n                      min_area,\n                      max_area,\n                      min_circularity):\n\n    output = np.zeros_like(mask)\n\n    contours, _ = cv2.findContours(\n        mask,\n        cv2.RETR_EXTERNAL,\n        cv2.CHAIN_APPROX_SIMPLE\n    )\n\n    for cnt in contours:\n\n        area = cv2.contourArea(cnt)\n\n        if area < min_area:\n            continue\n\n        if area > max_area:\n            continue\n\n        perimeter = cv2.arcLength(cnt, True)\n\n        if perimeter == 0:\n            continue\n\n        circularity = 4*np.pi*area/(perimeter*perimeter)\n\n        if circularity < min_circularity:\n            continue\n\n        cv2.drawContours(\n            output,\n            [cnt],\n            -1,\n            255,\n            -1\n        )\n\n    return output\n\n\n# ------------------------------------------------------------\n# Détection des exsudats\n# ------------------------------------------------------------\n\ndef detect_exudates(img_rgb,\n                    fundus_mask):\n\n    green = enhance_green_channel(img_rgb)\n\n    kernel = cv2.getStructuringElement(\n        cv2.MORPH_ELLIPSE,\n        (19,19)\n    )\n\n    tophat = cv2.morphologyEx(\n        green,\n        cv2.MORPH_TOPHAT,\n        kernel\n    )\n\n    _, mask = cv2.threshold(\n        tophat,\n        0,\n        255,\n        cv2.THRESH_BINARY + cv2.THRESH_OTSU\n    )\n\n    mask = cv2.bitwise_and(\n        mask,\n        fundus_mask\n    )\n\n    mask = filter_components(\n        mask,\n        min_area=12,\n        max_area=1500,\n        min_circularity=0.20\n    )\n\n    return mask\n\n\n# ------------------------------------------------------------\n# Détection des lésions rouges\n# ------------------------------------------------------------\n\ndef detect_red_lesions(img_rgb,\n                       fundus_mask):\n\n    green = enhance_green_channel(img_rgb)\n\n    vessels = segment_vessels_frangi(green)\n\n    kernel = cv2.getStructuringElement(\n        cv2.MORPH_ELLIPSE,\n        (15,15)\n    )\n\n    blackhat = cv2.morphologyEx(\n        255-green,\n        cv2.MORPH_BLACKHAT,\n        kernel\n    )\n\n    _, mask = cv2.threshold(\n        blackhat,\n        0,\n        255,\n        cv2.THRESH_BINARY + cv2.THRESH_OTSU\n    )\n\n    mask = cv2.bitwise_and(\n        mask,\n        cv2.bitwise_not(vessels)\n    )\n\n    mask = cv2.bitwise_and(\n        mask,\n        fundus_mask\n    )\n\n    mask = filter_components(\n        mask,\n        min_area=6,\n        max_area=180,\n        min_circularity=0.55\n    )\n\n    return mask\n\n\n# ------------------------------------------------------------\n# Superposition couleur\n# ------------------------------------------------------------\n\ndef overlay_lesions(img_rgb,\n                    exudate_mask,\n                    red_mask):\n\n    overlay = img_rgb.copy().astype(np.float32)\n\n    overlay[exudate_mask > 0] = [255,255,0]\n\n    overlay[red_mask > 0] = [255,0,0]\n\n    return overlay.astype(np.uint8)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T22:19:05.447239Z","iopub.execute_input":"2026-08-08T22:19:05.447699Z","iopub.status.idle":"2026-08-08T22:19:05.458518Z","shell.execute_reply.started":"2026-08-08T22:19:05.447631Z","shell.execute_reply":"2026-08-08T22:19:05.457612Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELLULE 3\n# Application de la pipeline complète\n# ============================================================\n\nimport random\nfrom PIL import Image\n\n# ------------------------------------------------------------\n# Choisir une image\n# ------------------------------------------------------------\n\nrow = df.sample(1, random_state=42).iloc[0]\n\nimage_path = os.path.join(\n    train_dir,\n    row[\"id_code\"] + \".png\"\n)\n\nprint(\"Image :\", row[\"id_code\"])\nprint(\"Grade réel :\", row[\"diagnosis\"])\n\n# ------------------------------------------------------------\n# Chargement\n# ------------------------------------------------------------\n\nimg_rgb = np.array(\n    Image.open(image_path)\n    .convert(\"RGB\")\n)\n\nimg_rgb = cv2.resize(img_rgb,(512,512))\n\n# ------------------------------------------------------------\n# Prétraitement\n# ------------------------------------------------------------\n\nfundus_mask = get_fundus_mask(img_rgb)\n\ngreen = enhance_green_channel(img_rgb)\n\nvessels = segment_vessels_frangi(green)\n\n# ------------------------------------------------------------\n# Détection\n# ------------------------------------------------------------\n\nexudate_mask = detect_exudates(\n    img_rgb,\n    fundus_mask\n)\n\nred_mask = detect_red_lesions(\n    img_rgb,\n    fundus_mask\n)\n\noverlay = overlay_lesions(\n    img_rgb,\n    exudate_mask,\n    red_mask\n)\n\n# ------------------------------------------------------------\n# Statistiques\n# ------------------------------------------------------------\n\ntotal_pixels = np.sum(fundus_mask > 0)\n\nn_exu = len(\n    cv2.findContours(\n        exudate_mask,\n        cv2.RETR_EXTERNAL,\n        cv2.CHAIN_APPROX_SIMPLE\n    )[0]\n)\n\nn_red = len(\n    cv2.findContours(\n        red_mask,\n        cv2.RETR_EXTERNAL,\n        cv2.CHAIN_APPROX_SIMPLE\n    )[0]\n)\n\nsurface_exu = (\n    100*np.sum(exudate_mask>0)\n    / total_pixels\n)\n\nsurface_red = (\n    100*np.sum(red_mask>0)\n    / total_pixels\n)\n\n# ------------------------------------------------------------\n# Affichage\n# ------------------------------------------------------------\n\nfig,ax = plt.subplots(\n    2,\n    3,\n    figsize=(18,12)\n)\n\nax[0,0].imshow(img_rgb)\nax[0,0].set_title(\"Image originale\")\nax[0,0].axis(\"off\")\n\nax[0,1].imshow(green,cmap=\"gray\")\nax[0,1].set_title(\"Canal vert + CLAHE\")\nax[0,1].axis(\"off\")\n\nax[0,2].imshow(vessels,cmap=\"gray\")\nax[0,2].set_title(\"Vaisseaux (Frangi)\")\nax[0,2].axis(\"off\")\n\nax[1,0].imshow(exudate_mask,cmap=\"gray\")\nax[1,0].set_title(\"Exsudats\")\nax[1,0].axis(\"off\")\n\nax[1,1].imshow(red_mask,cmap=\"gray\")\nax[1,1].set_title(\"Lésions rouges\")\nax[1,1].axis(\"off\")\n\nax[1,2].imshow(overlay)\nax[1,2].set_title(\"Overlay final\")\nax[1,2].axis(\"off\")\n\nplt.tight_layout()\n\nplt.show()\n\nprint(\"=\"*60)\n\nprint(f\"Grade réel                : {row['diagnosis']}\")\n\nprint(f\"Nombre d'exsudats         : {n_exu}\")\n\nprint(f\"Nombre de lésions rouges  : {n_red}\")\n\nprint(f\"Surface exsudats          : {surface_exu:.2f}%\")\n\nprint(f\"Surface lésions rouges    : {surface_red:.2f}%\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T22:19:49.871207Z","iopub.execute_input":"2026-08-08T22:19:49.871928Z","iopub.status.idle":"2026-08-08T22:19:52.004176Z","shell.execute_reply.started":"2026-08-08T22:19:49.871894Z","shell.execute_reply":"2026-08-08T22:19:52.003492Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELLULE 4\n# Validation quantitative sur 15 images par grade\n# ============================================================\n\nresults = []\n\nN = 15\n\nfor grade in sorted(df[\"diagnosis\"].unique()):\n\n    print(f\"Grade {grade}\")\n\n    subset = (\n        df[df[\"diagnosis\"] == grade]\n        .sample(N, random_state=42)\n    )\n\n    for _, row in subset.iterrows():\n\n        path = os.path.join(\n            train_dir,\n            row[\"id_code\"] + \".png\"\n        )\n\n        img = np.array(\n            Image.open(path)\n            .convert(\"RGB\")\n        )\n\n        img = cv2.resize(img,(512,512))\n\n        fundus_mask = get_fundus_mask(img)\n\n        exu = detect_exudates(\n            img,\n            fundus_mask\n        )\n\n        red = detect_red_lesions(\n            img,\n            fundus_mask\n        )\n\n        total = np.sum(fundus_mask > 0)\n\n        results.append({\n\n            \"diagnosis\": grade,\n\n            \"nb_exsudats\":\n            len(\n                cv2.findContours(\n                    exu,\n                    cv2.RETR_EXTERNAL,\n                    cv2.CHAIN_APPROX_SIMPLE\n                )[0]\n            ),\n\n            \"nb_lesions_rouges\":\n            len(\n                cv2.findContours(\n                    red,\n                    cv2.RETR_EXTERNAL,\n                    cv2.CHAIN_APPROX_SIMPLE\n                )[0]\n            ),\n\n            \"surface_exsudats\":\n            100*np.sum(exu>0)/total,\n\n            \"surface_lesions_rouges\":\n            100*np.sum(red>0)/total\n\n        })\n\nresults_df = pd.DataFrame(results)\n\nresults_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T22:21:10.020033Z","iopub.execute_input":"2026-08-08T22:21:10.020949Z","iopub.status.idle":"2026-08-08T22:22:09.569317Z","shell.execute_reply.started":"2026-08-08T22:21:10.020904Z","shell.execute_reply":"2026-08-08T22:22:09.568684Z"}},"outputs":[],"execution_count":null}]}