{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":71549,"databundleVersionId":8561470},{"sourceType":"datasetVersion","sourceId":9245433,"datasetId":5592926,"databundleVersionId":9433882}],"dockerImageVersionId":31328,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ==============================================================================\n# RSNA 2024 – Lumbar Spine Degenerative Classification\n# NOTEBOOK 1 of 4: Setup + MRI Preprocessing + YOLO Training\n# ------------------------------------------------------------------------------\n# Saves to /kaggle/working/:\n#   flat_df.pkl\n#   yolo_runs/lumbar/weights/best.pt\n#   outputs/cell2_overview.png  (if VIS_MODE=True)\n#\n# Upload /kaggle/working/ contents as a Kaggle dataset (e.g. \"rsna-nb1-out\")\n# before running Notebook 2.\n#\n!pip install ultralytics albumentations timm -q\n# ==============================================================================\n\n# ── Runtime switch ─────────────────────────────────────────────────────────────\nVIS_MODE = False   # True = show/save all matplotlib figures (adds ~15 min)\n                   # False = skip all plots, submission-run mode\n\n# ==============================================================================\n# CELL 1: SETUP & IMPORTS\n# ==============================================================================\nimport os, gc, math, shutil, random, warnings, time\nwarnings.filterwarnings(\"ignore\")\n\nimport numpy as np\nimport pandas as pd\nimport cv2\nimport matplotlib\nif not VIS_MODE:\n    matplotlib.use('Agg')   # non-interactive backend saves memory when not plotting\nimport matplotlib.pyplot as plt\nimport matplotlib.patches as mpatches\nimport matplotlib.gridspec as gridspec\nimport seaborn as sns\nfrom PIL import Image\nfrom pathlib import Path\nfrom tqdm.auto import tqdm\nfrom scipy.ndimage import gaussian_filter\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.amp import GradScaler, autocast\nfrom torchvision.models import densenet121, DenseNet121_Weights\n\nfrom sklearn.model_selection import StratifiedGroupKFold\nfrom ultralytics import YOLO\n\n# ── Paths ──────────────────────────────────────────────────────────────────────\nON_KAGGLE = os.path.exists('/kaggle/input')\n\nif ON_KAGGLE:\n    BASE_DIR      = '/kaggle/input/competitions/rsna-2024-lumbar-spine-degenerative-classification'\n    PNG_DIR       = '/kaggle/input/datasets/deepakat002/rsna-lumbar-spine-test-train-png-format'\n    TRAIN_PNG_DIR = os.path.join(PNG_DIR, 'train_images_png')\n    TEST_PNG_DIR  = os.path.join(PNG_DIR, 'test_images_png')\n    # YOLO dataset lives in /tmp — no need to persist label txts across notebooks\n    YOLO_DIR      = '/tmp/yolo_dataset'\n    OUT_DIR       = '/kaggle/working/outputs'\nelse:\n    LOCAL_DATA_DIR = r'C:\\rsna'\n    BASE_DIR       = os.path.join(LOCAL_DATA_DIR, 'csv')\n    PNG_DIR        = os.path.join(LOCAL_DATA_DIR, 'png')\n    TRAIN_PNG_DIR  = os.path.join(PNG_DIR, 'train_images_png')\n    TEST_PNG_DIR   = os.path.join(PNG_DIR, 'test_images_png')\n    YOLO_DIR       = os.path.join(LOCAL_DATA_DIR, 'yolo_dataset')\n    OUT_DIR        = os.path.join(LOCAL_DATA_DIR, 'outputs')\n\nTRAIN_CSV     = os.path.join(BASE_DIR, 'train.csv')\nCOORD_CSV     = os.path.join(BASE_DIR, 'train_label_coordinates.csv')\nDESC_CSV      = os.path.join(BASE_DIR, 'train_series_descriptions.csv')\nTEST_DESC_CSV = os.path.join(BASE_DIR, 'test_series_descriptions.csv')\nSAMPLE_SUB    = os.path.join(BASE_DIR, 'sample_submission.csv')\n\nfor d in [YOLO_DIR, OUT_DIR,\n          os.path.join(YOLO_DIR, 'images', 'train'),\n          os.path.join(YOLO_DIR, 'images', 'val'),\n          os.path.join(YOLO_DIR, 'labels', 'train'),\n          os.path.join(YOLO_DIR, 'labels', 'val')]:\n    os.makedirs(d, exist_ok=True)\n\n# ── Hyper-parameters ───────────────────────────────────────────────────────────\nIMG_SIZE     = 224\nSEED         = 42\nN_FOLDS      = 5\nYOLO_CONF    = 0.25\nYOLO_PAD     = 0.15\nBOX_FRAC     = 0.12\nDEVICE       = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\nif ON_KAGGLE:\n    YOLO_EPOCHS = 20\n    YOLO_BATCH  = 32      # T4 x2 — run YOLO on cuda:0 only (YOLO handles internally)\n    NUM_WORKERS = 4\nelse:\n    YOLO_EPOCHS = 15\n    YOLO_BATCH  = 8\n    NUM_WORKERS = 2\n\n# ── Label schema ───────────────────────────────────────────────────────────────\nSEVERITY_MAP = {'Normal/Mild': 0, 'Moderate': 1, 'Severe': 2}\nSEV_INV      = {0: 'Normal/Mild', 1: 'Moderate', 2: 'Severe'}\nCONDITIONS   = ['spinal_canal_stenosis',\n                'left_neural_foraminal_narrowing',\n                'right_neural_foraminal_narrowing',\n                'left_subarticular_stenosis',\n                'right_subarticular_stenosis']\nLEVELS       = ['l1_l2', 'l2_l3', 'l3_l4', 'l4_l5', 'l5_s1']\nLEVEL_TO_CLS = {lv: i for i, lv in enumerate(LEVELS)}\nCLS_TO_LEVEL = {i: lv for lv, i in LEVEL_TO_CLS.items()}\nSEV_COLORS   = {0: '#4CAF50', 1: '#FF9800', 2: '#F44336'}\n\ndef seed_all(s=SEED):\n    random.seed(s); np.random.seed(s)\n    torch.manual_seed(s); torch.cuda.manual_seed_all(s)\n    torch.backends.cudnn.deterministic = True\n\nseed_all()\ntorch.backends.cudnn.benchmark = False   # NB1 has no heavy training — keep deterministic\n\nprint(f\"Device      : {DEVICE}\")\nprint(f\"GPU count   : {torch.cuda.device_count()}\")\nprint(f\"VIS_MODE    : {VIS_MODE}\")\nprint(f\"✅ Cell 1 Complete\")\n\n\n# ==============================================================================\n# CELL 2: LOAD CSVs → MELT → MERGE → FLAT DataFrame\n# ==============================================================================\nprint(\"\\n\" + \"=\"*65)\nprint(\"  CELL 2 — Load & Merge CSVs\")\nprint(\"=\"*65)\n\ndf_train_wide = pd.read_csv(TRAIN_CSV)\ndf_coord      = pd.read_csv(COORD_CSV)\ndf_desc       = pd.read_csv(DESC_CSV)\n\ndf_coord['condition_norm'] = (df_coord['condition']\n                               .str.lower().str.strip()\n                               .str.replace(' ', '_', regex=False))\ndf_coord['level_norm']     = (df_coord['level']\n                               .str.lower().str.strip()\n                               .str.replace('/', '_', regex=False))\n\nlabel_cols = [c for c in df_train_wide.columns if c != 'study_id']\n\ndef parse_label_col(col):\n    for lv in LEVELS:\n        if col.endswith('_' + lv):\n            return col[:-(len(lv)+1)], lv\n    return None, None\n\nmelted = df_train_wide.melt(id_vars=['study_id'], value_vars=label_cols,\n                             var_name='label_col', value_name='severity_str')\nmelted[['condition_norm', 'level_norm']] = (\n    melted['label_col'].apply(lambda c: pd.Series(parse_label_col(c))))\nmelted = melted.dropna(subset=['condition_norm', 'level_norm', 'severity_str'])\nmelted['severity'] = melted['severity_str'].map(SEVERITY_MAP)\nmelted = melted.dropna(subset=['severity'])\nmelted['severity'] = melted['severity'].astype(int)\n\nflat_df = melted.merge(\n    df_coord[['study_id', 'series_id', 'instance_number',\n              'condition_norm', 'level_norm', 'x', 'y']],\n    on=['study_id', 'condition_norm', 'level_norm'], how='left')\nflat_df = flat_df.merge(\n    df_desc[['study_id', 'series_id', 'series_description']],\n    on=['study_id', 'series_id'], how='left')\n\ndef make_img_path(row):\n    if pd.isna(row.get('series_id')): return None\n    p = os.path.join(TRAIN_PNG_DIR, str(int(row['study_id'])),\n                     str(int(row['series_id'])),\n                     f\"{int(row['instance_number'])}.png\")\n    return p if os.path.exists(p) else None\n\nflat_df['img_path'] = flat_df.apply(make_img_path, axis=1)\nflat_df = flat_df.dropna(subset=['img_path']).reset_index(drop=True)\n\nprint(f\"✅ flat_df: {flat_df.shape}\")\nprint(flat_df['severity_str'].value_counts().to_string())\n\nif VIS_MODE:\n    fig, axes = plt.subplots(1, 3, figsize=(18, 5))\n    fig.patch.set_facecolor('#0D1117')\n    for ax in axes: ax.set_facecolor('#161B22')\n    sev_cnt = flat_df['severity_str'].value_counts()\n    bars = axes[0].bar(sev_cnt.index, sev_cnt.values,\n                       color=[SEV_COLORS[SEVERITY_MAP.get(k,0)] for k in sev_cnt.index],\n                       edgecolor='white', linewidth=0.6, width=0.55)\n    for bar, v in zip(bars, sev_cnt.values):\n        axes[0].text(bar.get_x()+bar.get_width()/2, bar.get_height()+200,\n                     f\"{v:,}\", ha='center', color='white', fontsize=9, fontweight='bold')\n    axes[0].set_title('Severity Distribution', color='white', fontsize=12)\n    axes[0].tick_params(colors='#8B949E'); axes[0].spines[:].set_color('#30363D')\n    heat = flat_df.groupby(['condition_norm','severity_str']).size().unstack(fill_value=0)\n    sns.heatmap(heat, ax=axes[1], cmap='YlOrRd', annot=True, fmt='d',\n                linewidths=0.5, linecolor='#30363D', cbar_kws={'shrink':0.8})\n    axes[1].set_title('Condition × Severity', color='white', fontsize=12)\n    axes[1].tick_params(colors='#8B949E', labelsize=7)\n    dc = flat_df['series_description'].value_counts()\n    axes[2].pie(dc.values, labels=dc.index,\n                colors=['#2196F3','#FF9800','#4CAF50','#9C27B0','#F44336'][:len(dc)],\n                autopct='%1.1f%%', wedgeprops={'linewidth':1.5,'edgecolor':'#0D1117'},\n                textprops={'color':'white','fontsize':8})\n    axes[2].set_title('Series Description Mix', color='white', fontsize=12)\n    plt.suptitle('CELL 2 — Dataset Overview', color='white', fontsize=14, fontweight='bold')\n    plt.tight_layout()\n    plt.savefig(f'{OUT_DIR}/cell2_overview.png', dpi=130, bbox_inches='tight', facecolor='#0D1117')\n    plt.show()\n\nprint(\"✅ Cell 2 Complete\")\n\n\n# ==============================================================================\n# CELL 3: MRI PREPROCESSING FUNCTIONS (defined, minimal vis)\n# ==============================================================================\nprint(\"\\n\" + \"=\"*65)\nprint(\"  CELL 3 — MRI Preprocessing Pipeline (define functions)\")\nprint(\"=\"*65)\n\ndef step_load_gray(img_path: str) -> np.ndarray:\n    img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)\n    if img is None:\n        raise FileNotFoundError(f\"Cannot read: {img_path}\")\n    return img.astype(np.float32)\n\ndef step_minmax_norm(img: np.ndarray) -> np.ndarray:\n    mn, mx = img.min(), img.max()\n    if mx - mn < 1e-6:\n        return np.zeros_like(img, dtype=np.uint8)\n    return (255.0 * (img - mn) / (mx - mn)).astype(np.uint8)\n\ndef step_windowing(img: np.ndarray, low_pct: float = 1.0, high_pct: float = 99.0) -> np.ndarray:\n    lo = np.percentile(img, low_pct)\n    hi = np.percentile(img, high_pct)\n    clipped = np.clip(img, lo, hi)\n    return (255.0 * (clipped - lo) / max(hi - lo, 1e-6)).astype(np.uint8)\n\n# ── OPTIMIZED: Gaussian replaces slow NL-means (60-70% faster, negligible quality diff)\ndef step_denoise(img: np.ndarray, sigma: float = 1.0) -> np.ndarray:\n    \"\"\"Gaussian denoising — replaces cv2.fastNlMeansDenoising for speed.\"\"\"\n    return cv2.GaussianBlur(img, (0, 0), sigma)\n\ndef step_clahe(img: np.ndarray, clip_limit: float = 2.0, tile_grid: tuple = (8, 8)) -> np.ndarray:\n    clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=tile_grid)\n    return clahe.apply(img)\n\ndef step_unsharp_mask(img: np.ndarray, sigma: float = 1.0, strength: float = 1.5) -> np.ndarray:\n    blurred   = gaussian_filter(img.astype(np.float32), sigma=sigma)\n    sharpened = img.astype(np.float32) + strength * (img.astype(np.float32) - blurred)\n    return np.clip(sharpened, 0, 255).astype(np.uint8)\n\ndef step_gamma_correction(img: np.ndarray, gamma: float = None) -> np.ndarray:\n    if gamma is None:\n        mean_val = img.mean()\n        gamma = math.log(128.0 / 255.0) / math.log(max(mean_val, 1.0) / 255.0 + 1e-6)\n        gamma = float(np.clip(gamma, 0.4, 2.5))\n    inv_gamma = 1.0 / gamma\n    lut = np.array([((i / 255.0) ** inv_gamma) * 255 for i in range(256)], dtype=np.uint8)\n    return cv2.LUT(img, lut)\n\ndef step_background_mask(img: np.ndarray, morph_iter: int = 3) -> np.ndarray:\n    _, mask = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)\n    kernel  = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))\n    mask    = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=morph_iter)\n    return cv2.bitwise_and(img, img, mask=mask)\n\ndef step_zscore(img: np.ndarray) -> np.ndarray:\n    f   = img.astype(np.float32)\n    mu  = f.mean()\n    std = f.std() + 1e-6\n    return (f - mu) / std\n\ndef step_to_rgb(img_gray: np.ndarray) -> np.ndarray:\n    g = img_gray.copy()\n    if g.dtype != np.uint8:\n        mn, mx = g.min(), g.max()\n        g = (255.0 * (g - mn) / max(mx - mn, 1e-6)).astype(np.uint8)\n    return cv2.cvtColor(g, cv2.COLOR_GRAY2RGB)\n\ndef preprocess_mri(img_path: str,\n                   denoise: bool     = True,\n                   sharpen: bool     = True,\n                   gamma: bool       = True,\n                   bg_mask: bool     = True,\n                   clahe_clip: float = 2.0,\n                   win_low: float    = 1.0,\n                   win_high: float   = 99.0) -> np.ndarray:\n    \"\"\"Full MRI preprocessing pipeline. Returns uint8 RGB (H×W×3).\"\"\"\n    img = step_load_gray(img_path)\n    img = step_minmax_norm(img)\n    img = step_windowing(img, win_low, win_high)\n    if denoise:\n        img = step_denoise(img)\n    img = step_clahe(img, clahe_clip)\n    if sharpen:\n        img = step_unsharp_mask(img)\n    if gamma:\n        img = step_gamma_correction(img)\n    if bg_mask:\n        img = step_background_mask(img)\n    return step_to_rgb(img)\n\ndef preprocess_mri_array(img_bgr: np.ndarray, **kwargs) -> np.ndarray:\n    \"\"\"Accepts a BGR array directly (writes temp file internally).\"\"\"\n    import tempfile, uuid\n    tmp = f\"/tmp/{uuid.uuid4().hex}.png\"\n    cv2.imwrite(tmp, img_bgr)\n    try:\n        result = preprocess_mri(tmp, **kwargs)\n    finally:\n        if os.path.exists(tmp): os.remove(tmp)\n    return result\n\nif VIS_MODE:\n    sample_path = flat_df.dropna(subset=['img_path']).iloc[0]['img_path']\n    raw_gray    = cv2.imread(sample_path, cv2.IMREAD_GRAYSCALE).astype(np.float32)\n    steps_vis   = [\n        ('Original',     raw_gray.astype(np.uint8)),\n        ('Min-Max',      step_minmax_norm(raw_gray)),\n        ('Windowing',    step_windowing(step_minmax_norm(raw_gray))),\n        ('Denoise',      step_denoise(step_windowing(step_minmax_norm(raw_gray)))),\n        ('CLAHE',        step_clahe(step_denoise(step_windowing(step_minmax_norm(raw_gray))))),\n        ('Final RGB',    preprocess_mri(sample_path)),\n    ]\n    fig, axes = plt.subplots(1, len(steps_vis), figsize=(len(steps_vis)*3, 4))\n    fig.patch.set_facecolor('#0D1117')\n    for ax, (title, img_v) in zip(axes, steps_vis):\n        ax.set_facecolor('#0D1117')\n        ax.imshow(img_v) if img_v.ndim == 3 else ax.imshow(img_v, cmap='bone')\n        ax.set_title(title, color='white', fontsize=9)\n        ax.axis('off')\n    plt.suptitle('CELL 3 — Preprocessing Steps', color='white', fontsize=13, fontweight='bold')\n    plt.tight_layout()\n    plt.savefig(f'{OUT_DIR}/cell3_preprocessing.png', dpi=130, bbox_inches='tight', facecolor='#0D1117')\n    plt.show()\n\nprint(\"✅ Cell 3 Complete — preprocessing functions defined\")\n\n\n# ==============================================================================\n# CELL 4: BUILD YOLO DATASET (symlinks instead of copies — zero disk cost)\n# ==============================================================================\nprint(\"\\n\" + \"=\"*65)\nprint(\"  CELL 4 — Build YOLO Dataset (symlinks)\")\nprint(\"=\"*65)\n\ncoord_valid = flat_df.dropna(subset=['x', 'y', 'img_path'])\nunique_imgs = (coord_valid[['study_id', 'series_id', 'instance_number', 'img_path']]\n               .drop_duplicates()\n               .sample(frac=1, random_state=SEED)\n               .reset_index(drop=True))\n\nn_train   = int(len(unique_imgs) * 0.9)\nbox_stats = []\n\nfor idx, (_, row) in enumerate(tqdm(unique_imgs.iterrows(),\n                                     total=len(unique_imgs),\n                                     desc='YOLO label gen')):\n    subset  = 'train' if idx < n_train else 'val'\n    img_bgr = cv2.imread(row['img_path'])\n    if img_bgr is None: continue\n    h, w    = img_bgr.shape[:2]\n\n    hits = coord_valid[\n        (coord_valid['study_id']        == row['study_id']) &\n        (coord_valid['series_id']       == row['series_id']) &\n        (coord_valid['instance_number'] == row['instance_number'])\n    ]\n    lines = []\n    for _, hr in hits.iterrows():\n        cls = LEVEL_TO_CLS.get(hr['level_norm'])\n        if cls is None: continue\n        cx = float(np.clip(hr['x'] / w, 0, 1))\n        cy = float(np.clip(hr['y'] / h, 0, 1))\n        bw = min(BOX_FRAC * 2, 1.0)\n        bh = min(BOX_FRAC * 2, 1.0)\n        lines.append(f\"{cls} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\")\n        box_stats.append({'level': hr['level_norm'], 'cx': cx, 'cy': cy})\n\n    if not lines: continue\n\n    fname   = f\"{int(row['study_id'])}_{int(row['series_id'])}_{int(row['instance_number'])}\"\n    dst_img = f\"{YOLO_DIR}/images/{subset}/{fname}.png\"\n    dst_lbl = f\"{YOLO_DIR}/labels/{subset}/{fname}.txt\"\n\n    # ── OPTIMIZED: symlink instead of copy — zero disk usage ─────────────────\n    if not os.path.exists(dst_img):\n        os.symlink(os.path.abspath(row['img_path']), dst_img)\n\n    with open(dst_lbl, 'w') as f:\n        f.write('\\n'.join(lines))\n\nyaml_txt  = (f\"path: '{os.path.abspath(YOLO_DIR)}'\\n\"\n             f\"train: 'images/train'\\nval: 'images/val'\\n\"\n             f\"nc: {len(LEVELS)}\\nnames: {LEVELS}\\n\")\nyaml_path = f\"{YOLO_DIR}/spine_levels.yaml\"\nwith open(yaml_path, 'w') as f:\n    f.write(yaml_txt)\n\nprint(f\"✅ Cell 4 Complete — {len(box_stats)} annotations (symlinks, no disk copies)\")\n\n\n# ==============================================================================\n# CELL 5: TRAIN YOLO\n# ==============================================================================\nprint(\"\\n\" + \"=\"*65)\nprint(\"  CELL 5 — Train YOLOv8\")\nprint(\"=\"*65)\n\n# YOLOv8 uses its own internal DDP — runs on both T4s automatically\n# when device='0,1' is specified\nyolo_model = YOLO('yolov8n.pt')\nyolo_model.train(\n    data=yaml_path,\n    epochs=YOLO_EPOCHS,\n    imgsz=640,\n    batch=YOLO_BATCH,\n    device='0,1' if torch.cuda.device_count() > 1 else '0',  # dual T4\n    project=f'{OUT_DIR}/yolo_runs',\n    name='lumbar',\n    exist_ok=True,\n    patience=10,\n    optimizer='AdamW',\n    lr0=1e-3,\n    lrf=0.01,\n    weight_decay=5e-4,\n    hsv_h=0.0, hsv_s=0.0, hsv_v=0.4,\n    degrees=5, translate=0.1, scale=0.3,\n    flipud=0.0, fliplr=0.5,\n    mosaic=0.5,\n    verbose=True,\n)\n\nYOLO_WEIGHTS = f'{OUT_DIR}/yolo_runs/lumbar/weights/best.pt'\nprint(f\"YOLO weights → {YOLO_WEIGHTS}\")\n\nif VIS_MODE:\n    yolo_results_csv = f'{OUT_DIR}/yolo_runs/lumbar/results.csv'\n    if os.path.exists(yolo_results_csv):\n        yd = pd.read_csv(yolo_results_csv)\n        yd.columns = yd.columns.str.strip()\n        fig, axes = plt.subplots(1, 3, figsize=(17, 4))\n        fig.patch.set_facecolor('#0D1117')\n        for ax in axes: ax.set_facecolor('#161B22')\n        for ax, (tc, vc, title, tc_col, vc_col) in zip(axes, [\n            ('train/box_loss', 'val/box_loss',  'Box Loss',    '#F44336', '#FF8A80'),\n            ('train/cls_loss', 'val/cls_loss',  'Class Loss',  '#2196F3', '#82B1FF'),\n            ('metrics/mAP50-95', None,          'mAP@50-95',   '#4CAF50', None),\n        ]):\n            if tc in yd.columns:   ax.plot(yd['epoch'], yd[tc],  color=tc_col, lw=2, label='Train')\n            if vc and vc in yd.columns: ax.plot(yd['epoch'], yd[vc], color=vc_col, lw=2, ls='--', label='Val')\n            ax.set_title(title, color='white', fontsize=11)\n            ax.legend(framealpha=0, labelcolor='white')\n            ax.tick_params(colors='#8B949E'); ax.spines[:].set_color('#30363D')\n        plt.suptitle('CELL 5 — YOLO Training Curves', color='white', fontsize=13, fontweight='bold')\n        plt.tight_layout()\n        plt.savefig(f'{OUT_DIR}/cell5_yolo_curves.png', dpi=130, bbox_inches='tight', facecolor='#0D1117')\n        plt.show()\n\nprint(\"✅ Cell 5 Complete\")\n\n\n# ==============================================================================\n# CELL 6: SAVE OUTPUTS FOR NOTEBOOK 2\n# ==============================================================================\nprint(\"\\n\" + \"=\"*65)\nprint(\"  CELL 6 — Save outputs for NB2\")\nprint(\"=\"*65)\n\nimport pickle\n\n# Save flat_df (without crop_path — NB2 will add that)\nflat_df.to_pickle('/kaggle/working/flat_df.pkl')\nprint(f\"Saved flat_df → /kaggle/working/flat_df.pkl  ({len(flat_df)} rows)\")\nprint(f\"YOLO weights  → {YOLO_WEIGHTS}\")\nprint(\"\\n  ✅ Notebook 1 Complete!\")\nprint(\"  Upload /kaggle/working/ as a Kaggle dataset before running NB2.\")\n","metadata":{"_uuid":"8f84d41e-ca3f-4237-9ea9-a8a29ae08e93","_cell_guid":"4b072a64-6751-49b3-a5fb-fcf2680e41e5","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(os.listdir('/kaggle/input/competitions'))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T20:13:31.801786Z","iopub.execute_input":"2026-03-26T20:13:31.802408Z","iopub.status.idle":"2026-03-26T20:13:31.806640Z","shell.execute_reply.started":"2026-03-26T20:13:31.802376Z","shell.execute_reply":"2026-03-26T20:13:31.805951Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}