{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","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"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"8785ffda","cell_type":"markdown","source":"# RSNA Pneumonia — Notebook 1 : Préparation des données + entraînement YOLO26x\n\nPipeline modulaire en 2 étapes (approche parallèle) :\n1. **YOLO26x** détecte les opacités (bounding boxes) — *ce notebook*.\n2. **MedGemma 1.5 4B** décrit les détections — *notebook 2*.\n\nCe notebook : conversion DICOM→image (CLAHE), conversion des boxes au format YOLO,\nsplit stratifié, entraînement `yolo26x` @ 1024, évaluation (mAP) + visualisation.\n\n**Config choisie** : yolo26x @ 1024 · dataset équilibré (tous positifs + autant de négatifs) · 1 classe `pneumonia`.","metadata":{}},{"id":"d714b94c","cell_type":"markdown","source":"### Prérequis Kaggle\n- **Accélérateur** : GPU T4 (Settings → Accelerator).\n- **Internet** : activé (pip + téléchargement des poids YOLO26).\n- **Dataset** : ajoute *RSNA Pneumonia Detection Challenge*.\n\n> Exécute les cellules de haut en bas.","metadata":{}},{"id":"750a66f8","cell_type":"code","source":"# --- Installations (Kaggle) ---\n# Ultralytics apporte le support YOLO26. On (re)fige Pillow 11.3.0 APRÈS coup :\n# un upgrade casse l'extension _imaging sur l'image Kaggle.\n!pip install -q -U ultralytics\n!pip install -q pydicom\n!pip install -q --force-reinstall --no-deps \"pillow==11.3.0\"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"04fcfc3a","cell_type":"code","source":"import os, glob, random, warnings\nfrom pathlib import Path\nimport numpy as np\nimport pandas as pd\nimport cv2, pydicom\nfrom tqdm.auto import tqdm\nfrom sklearn.model_selection import train_test_split\nimport matplotlib.pyplot as plt\nimport matplotlib.patches as patches\n\nwarnings.filterwarnings(\"ignore\")\nSEED = 42\nrandom.seed(SEED); np.random.seed(SEED)\n\n# --- Chemins RSNA (le dossier varie selon la façon dont le dataset est ajouté) ---\nCANDIDATES = [\n    \"/kaggle/input/rsna-pneumonia-detection-challenge\",\n    \"/kaggle/input/competitions/rsna-pneumonia-detection-challenge\",\n]\nBASE_DIR = next((p for p in CANDIDATES if os.path.exists(p)), None)\nassert BASE_DIR is not None, \"Dataset RSNA introuvable — vérifie l'ajout du dataset.\"\nprint(\"BASE_DIR :\", BASE_DIR)\n\nTRAIN_DCM_DIR = os.path.join(BASE_DIR, \"stage_2_train_images\")\nCLASS_CSV     = os.path.join(BASE_DIR, \"stage_2_detailed_class_info.csv\")\nLABELS_CSV    = os.path.join(BASE_DIR, \"stage_2_train_labels.csv\")\n\n# --- Sortie / paramètres ---\nOUT_DIR       = Path(\"/kaggle/working/rsna_yolo\")\nIMG_SIZE_SAVE = 1024          # on sauvegarde en résolution native\nIMGSZ_TRAIN   = 1024          # résolution d'entraînement YOLO\nVAL_FRAC      = 0.15          # split 85/15\nCLAHE_CLIP    = 2.0\nCLAHE_GRID    = (8, 8)\n\nfor sub in [\"images/train\", \"images/val\", \"labels/train\", \"labels/val\"]:\n    (OUT_DIR / sub).mkdir(parents=True, exist_ok=True)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"3be3223d","cell_type":"code","source":"df_cls = pd.read_csv(CLASS_CSV)\ndf_box = pd.read_csv(LABELS_CSV)\nprint(df_box.Target.value_counts(), \"\\n\")\nprint(df_cls[\"class\"].value_counts())\n\n# Boxes groupées par patient (une ligne = une box quand Target==1)\npos_boxes = (df_box[df_box.Target == 1]\n             .groupby(\"patientId\")[[\"x\", \"y\", \"width\", \"height\"]]\n             .apply(lambda g: g.values.tolist())\n             .to_dict())\n\npos_ids = sorted(pos_boxes.keys())                              # patients AVEC opacité\nneg_ids = sorted(df_box.loc[df_box.Target == 0, \"patientId\"].unique())\nprint(f\"\\nPositifs : {len(pos_ids)} · Négatifs disponibles : {len(neg_ids)}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"c635ee70","cell_type":"code","source":"# Dataset équilibré : tous les positifs + autant de négatifs (échantillonnés)\nrandom.shuffle(neg_ids)\nneg_sel  = neg_ids[:len(pos_ids)]\nselected = [(pid, 1) for pid in pos_ids] + [(pid, 0) for pid in neg_sel]\nrandom.shuffle(selected)\nprint(f\"Total sélectionné : {len(selected)} images \"\n      f\"({len(pos_ids)} positifs + {len(neg_sel)} négatifs)\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"4063ebfc","cell_type":"code","source":"def dicom_to_rgb(dcm_path, size=IMG_SIZE_SAVE):\n    # DICOM -> normalisation Min-Max 8 bits -> CLAHE -> RGB (prétraitement cohérent)\n    dcm = pydicom.dcmread(dcm_path)\n    img = dcm.pixel_array.astype(np.float32)\n    img = (img - img.min()) / (img.max() - img.min() + 1e-8)\n    img = (img * 255.0).astype(np.uint8)\n    if img.shape[0] != size:\n        img = cv2.resize(img, (size, size), interpolation=cv2.INTER_AREA)\n    clahe = cv2.createCLAHE(clipLimit=CLAHE_CLIP, tileGridSize=CLAHE_GRID)\n    img = clahe.apply(img)\n    return cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)   # HxWx3\n\n\ndef boxes_to_yolo(boxes, img_size=1024):\n    # (x, y, w, h) pixels sur 1024 -> lignes YOLO \"classe cx cy w h\" normalisées\n    lines = []\n    for (x, y, w, h) in boxes:\n        cx = (x + w / 2) / img_size\n        cy = (y + h / 2) / img_size\n        lines.append(f\"0 {cx:.6f} {cy:.6f} {w/img_size:.6f} {h/img_size:.6f}\")\n    return lines","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"7b937ded","cell_type":"code","source":"# Split stratifié par présence d'opacité\nids  = [pid for pid, _ in selected]\nlabs = [lab for _, lab in selected]\ntr_ids, va_ids = train_test_split(ids, test_size=VAL_FRAC,\n                                  stratify=labs, random_state=SEED)\nsplit_of = {pid: \"train\" for pid in tr_ids}\nsplit_of.update({pid: \"val\" for pid in va_ids})\nprint(f\"train : {len(tr_ids)} · val : {len(va_ids)}\")\n\ndef materialize(pid):\n    split = split_of[pid]\n    dcm   = os.path.join(TRAIN_DCM_DIR, pid + \".dcm\")\n    rgb   = dicom_to_rgb(dcm)\n    cv2.imwrite(str(OUT_DIR / \"images\" / split / f\"{pid}.png\"), rgb)\n    # label : lignes si positif, fichier vide si négatif (background)\n    boxes = pos_boxes.get(pid, [])\n    lines = boxes_to_yolo(boxes) if boxes else []\n    (OUT_DIR / \"labels\" / split / f\"{pid}.txt\").write_text(\"\\n\".join(lines))\n\nfor pid, _ in tqdm(selected, desc=\"DICOM->PNG + labels\"):\n    materialize(pid)\nprint(\"Conversion terminée.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"78ccdeb8","cell_type":"code","source":"data_yaml = OUT_DIR / \"data.yaml\"\ndata_yaml.write_text(\n    f\"path: {OUT_DIR}\\n\"\n    \"train: images/train\\n\" \n    \"val: images/val\\n\"\n    \"nc: 1\\n\"\n    \"names: ['pneumonia']\\n\"\n)\nprint(data_yaml.read_text())","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"6a6dc5fb","cell_type":"code","source":"from ultralytics import YOLO\n\n# yolo26x = variante extra-large ; poids COCO téléchargés au 1er usage.\nmodel = YOLO(\"yolo26m.pt\")\n\n# NOTE TEMPS : yolo26x @ 1024 sur un seul T4 est lent (~20-40 min/epoch).\n#  - Pour ~diviser le temps par 2 si 2 T4 dispo : device=[0, 1]\n#  - Session interactive Kaggle ~9 h ; \"Save & Run All (Commit)\" ~12 h\n#  - Si la session coupe : reprends avec resume=True (cellule suivante)\n#  - Si OOM : baisse batch à 2\nresults = model.train(\n    data=str(data_yaml),\n    epochs=10,\n    imgsz=IMGSZ_TRAIN,\n    batch=16,\n    device=[0,1],            # -> [0, 1] pour utiliser les 2 T4\n    workers=8,\n    cache=False,\n    patience=8,          # early stopping\n    close_mosaic=10,\n    project=\"/kaggle/working/runs\",\n    name=\"yolo26m_rsna\",\n    exist_ok=True,\n    seed=SEED,\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"8a918768","cell_type":"markdown","source":"### Reprise après coupure de session\nSi Kaggle stoppe avant la fin, relance depuis le dernier checkpoint :\n```python\nmodel = YOLO(\"/kaggle/working/runs/yolo26x_rsna/weights/last.pt\")\nmodel.train(resume=True)\n```","metadata":{}},{"id":"05c2abf1","cell_type":"code","source":"metrics = model.val()\nprint(f\"mAP@50    : {metrics.box.map50:.4f}\")\nprint(f\"mAP@50-95 : {metrics.box.map:.4f}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"71d449fd","cell_type":"code","source":"# Visualisation : boxes prédites (rouge) vs réelles (vert) sur des images de val\nval_imgs = sorted(glob.glob(str(OUT_DIR / \"images/val/*.png\")))\nrandom.shuffle(val_imgs)\nshow = val_imgs[:50]\n\ndef gt_boxes_for(img_path):\n    pid = Path(img_path).stem\n    lbl = OUT_DIR / \"labels/val\" / f\"{pid}.txt\"\n    out = []\n    if lbl.exists() and lbl.read_text().strip():\n        for line in lbl.read_text().strip().splitlines():\n            _, cx, cy, w, h = map(float, line.split())\n            out.append([(cx - w/2)*1024, (cy - h/2)*1024, w*1024, h*1024])\n    return out\n\nfig, axes = plt.subplots(2, 3, figsize=(16, 10))\nfor ax, img_path in zip(axes.ravel(), show):\n    img = cv2.imread(img_path)[:, :, ::-1]\n    res = model.predict(img_path, conf=0.25, imgsz=1024, verbose=False)[0]\n    ax.imshow(img); ax.axis(\"off\")\n    for (x, y, w, h) in gt_boxes_for(img_path):          # vérité terrain (vert)\n        ax.add_patch(patches.Rectangle((x, y), w, h, fill=False, edgecolor=\"lime\", lw=2))\n    for b in res.boxes:                                   # prédictions (rouge)\n        x1, y1, x2, y2 = b.xyxy[0].tolist()\n        ax.add_patch(patches.Rectangle((x1, y1), x2-x1, y2-y1, fill=False, edgecolor=\"red\", lw=2))\n        ax.text(x1, y1-4, f\"{float(b.conf[0]):.2f}\", color=\"red\", fontsize=9,\n                bbox=dict(facecolor=\"white\", alpha=0.6, pad=1))\n    ax.set_title(Path(img_path).stem[:12])\nplt.suptitle(\"Vert = vérité terrain · Rouge = YOLO26x\", y=0.92)\nplt.tight_layout(); plt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"39ecf6e0","cell_type":"markdown","source":"### Sorties\n- Poids entraînés : `/kaggle/working/runs/yolo26x_rsna/weights/best.pt`\n- Courbes & matrices : `/kaggle/working/runs/yolo26x_rsna/` (`results.png`, `confusion_matrix.png`, `val_batch*.jpg`)\n\n**Pour le notebook 2** : le plus simple est de *committer* ce notebook, puis d'ajouter sa sortie\n(ou juste `best.pt`) comme **dataset / Kaggle model** d'entrée du notebook 2.","metadata":{}}]}