{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":71549,"databundleVersionId":8561470,"sourceType":"competition"},{"sourceId":12249891,"sourceType":"datasetVersion","datasetId":7718540}],"dockerImageVersionId":31041,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"**CLASSIFER with test case**","metadata":{}},{"cell_type":"code","source":"# === RSNA 2024: ONE SAMPLE PER CONDITION (from provided CSVs + competition images) ===\n# Inputs:\n#   - /kaggle/input/csv-files/*.csv (one CSV per condition, with study_id, series_id, instance_number, x, y, score/label)\n#   - /kaggle/input/rsna-2024-lumbar-spine-degenerative-classification/train_images/<study>/<series>/<instance>.dcm\n#\n# Outputs (per condition):\n#   - /kaggle/working/condition_samples/<condition>_full.png  (full slice + red marker)\n#   - /kaggle/working/condition_samples/<condition>_crop_224.png (224×224 crop around (x,y))\n#   - /kaggle/working/condition_samples/condition_samples_manifest.csv\n\nimport os, glob, re\nfrom pathlib import Path\nimport numpy as np\nimport pandas as pd\nimport pydicom\nimport matplotlib.pyplot as plt\nfrom PIL import Image, ImageDraw\n\nCSV_DIR      = Path(\"/kaggle/input/csv-files\")\nIMG_ROOT     = Path(\"/kaggle/input/rsna-2024-lumbar-spine-degenerative-classification/train_images\")\nOUT_DIR      = Path(\"/kaggle/working/condition_samples\")\nCROP_SIZE    = 224  # output crop size\nMARKER_PX    = 6\nOUT_DIR.mkdir(parents=True, exist_ok=True)\n\ndef read_dicom_rgb(dcm_path: Path) -> np.ndarray:\n    ds = pydicom.dcmread(str(dcm_path))\n    arr = ds.pixel_array.astype(np.float32)\n    if arr.ndim == 3:  # multiframe → take mid-frame\n        arr = arr[arr.shape[0]//2]\n    vmin, vmax = np.percentile(arr, [1, 99])\n    arr = np.clip((arr - vmin) / max(vmax - vmin, 1e-6), 0, 1) * 255.0\n    arr = arr.astype(\"uint8\")\n    return np.stack([arr]*3, axis=-1)  # RGB\n\ndef safe_crop_box(w, h, cx, cy, box):\n    half = box // 2\n    left   = int(max(0, min(cx - half, w - box)))\n    top    = int(max(0, min(cy - half, h - box)))\n    right  = left + box\n    bottom = top  + box\n    return left, top, right, bottom\n\ndef save_full_with_marker(img_rgb: np.ndarray, x: float, y: float, out_path: Path, marker=MARKER_PX):\n    im = Image.fromarray(img_rgb)\n    d  = ImageDraw.Draw(im)\n    d.line((x-marker, y, x+marker, y), fill=(255,0,0), width=2)\n    d.line((x, y-marker, x, y+marker), fill=(255,0,0), width=2)\n    im.save(out_path, format=\"PNG\", optimize=True)\n\ndef save_crop(img_rgb: np.ndarray, x: float, y: float, out_path: Path, crop_size=CROP_SIZE):\n    H, W = img_rgb.shape[:2]\n    left, top, right, bottom = safe_crop_box(W, H, x, y, min(crop_size, min(W, H)))\n    patch = Image.fromarray(img_rgb[top:bottom, left:right])\n    if patch.size != (crop_size, crop_size):\n        patch = patch.resize((crop_size, crop_size), Image.BICUBIC)\n    patch.save(out_path, format=\"PNG\", optimize=True)\n\ndef normalize_colnames(df: pd.DataFrame) -> pd.DataFrame:\n    # Make common cols accessible regardless of exact casing\n    cmap = {c.lower(): c for c in df.columns}\n    # Required columns (with fallback aliases)\n    need = {\n        \"study_id\": [\"study_id\",\"study\",\"StudyInstanceUID\"],\n        \"series_id\": [\"series_id\",\"series\",\"SeriesInstanceUID\"],\n        \"instance_number\": [\"instance_number\",\"image_id\",\"sop_instance_uid\",\"instance\"],\n        \"x\": [\"x\",\"X\",\"coord_x\",\"cx\"],\n        \"y\": [\"y\",\"Y\",\"coord_y\",\"cy\"],\n    }\n    rename = {}\n    for std, alts in need.items():\n        for a in alts:\n            if a.lower() in cmap:\n                rename[cmap[a.lower()]] = std\n                break\n    df = df.rename(columns=rename)\n    missing = [k for k in [\"study_id\",\"series_id\",\"instance_number\",\"x\",\"y\"] if k not in df.columns]\n    if missing:\n        raise ValueError(f\"CSV missing required columns: {missing}\")\n    return df\n\ndef resolve_instance_path(study_id: str, series_id: str, instance_id: str) -> Path:\n    # Most RSNA slices are \"<instance_number>.dcm\". Some are SOPInstanceUID.dcm or without extension.\n    candidates = [\n        IMG_ROOT / study_id / series_id / f\"{instance_id}.dcm\",\n        IMG_ROOT / study_id / series_id / f\"{int(float(instance_id))}.dcm\" if instance_id.replace('.','',1).isdigit() else None,\n        IMG_ROOT / study_id / series_id / f\"{instance_id}\",\n    ]\n    for c in candidates:\n        if c and c.exists() and c.is_file():\n            return c\n    # Fallback: first file that contains instance_id in name, else any .dcm\n    series_dir = IMG_ROOT / study_id / series_id\n    if series_dir.exists():\n        hits = sorted([p for p in series_dir.glob(\"*\") if instance_id in p.name])\n        if hits:\n            return hits[0]\n        dcm_any = sorted(series_dir.glob(\"*.dcm\"))\n        if dcm_any:\n            return dcm_any[len(dcm_any)//2]  # mid-slice fallback\n    raise FileNotFoundError(f\"Could not locate DICOM for {study_id}/{series_id}/{instance_id}\")\n\n# Gather CSVs (each assumed to correspond to a condition)\ncsv_files = sorted(glob.glob(str(CSV_DIR / \"*.csv\")))\nif not csv_files:\n    raise FileNotFoundError(\"No CSVs found in /kaggle/input/csv-files. Please add the dataset ‘reeyav/csv-files’ as an input.\")\n\nmanifest = []\nfor csv_path in csv_files:\n    cond_raw = Path(csv_path).stem  # use filename as condition name\n    df = pd.read_csv(csv_path)\n    df = df.dropna(subset=[c for c in df.columns if c.lower() in {\"study_id\",\"study\",\"series_id\",\"series\",\"instance_number\",\"image_id\",\"x\",\"y\"}])\n    df = normalize_colnames(df)\n\n    if df.empty:\n        print(f\"[WARN] {cond_raw}: CSV has no usable rows.\")\n        continue\n\n    # Choose one representative row (first). If you prefer, filter by score/label here.\n    row = df.iloc[0]\n    sid = str(row[\"study_id\"])\n    seid = str(row[\"series_id\"])\n    inst = str(row[\"instance_number\"])\n    x = float(row[\"x\"]); y = float(row[\"y\"])\n\n    # Resolve and load image\n    try:\n        dcm_path = resolve_instance_path(sid, seid, inst)\n    except Exception as e:\n        print(f\"[WARN] {cond_raw}: {e}\")\n        continue\n\n    try:\n        img = read_dicom_rgb(dcm_path)\n    except Exception as e:\n        print(f\"[WARN] {cond_raw}: failed reading {dcm_path} — {e}\")\n        continue\n\n    # Save images\n    safe_cond = re.sub(r\"[^a-zA-Z0-9_]+\",\"_\", cond_raw)[:60] or \"condition\"\n    full_png = OUT_DIR / f\"{safe_cond}_full.png\"\n    crop_png = OUT_DIR / f\"{safe_cond}_crop_{CROP_SIZE}.png\"\n    save_full_with_marker(img, x, y, full_png)\n    save_crop(img, x, y, crop_png, CROP_SIZE)\n\n    print(f\"Saved {cond_raw}:\")\n    print(f\"  Full: {full_png}\")\n    print(f\"  Crop: {crop_png}\")\n\n    manifest.append({\n        \"condition\": cond_raw,\n        \"study_id\": sid,\n        \"series_id\": seid,\n        \"instance_number\": inst,\n        \"x\": x, \"y\": y,\n        \"dicom_path\": str(dcm_path),\n        \"full_png\": str(full_png),\n        \"crop_png\": str(crop_png),\n    })\n\n# Write manifest and show a quick gallery of full-slices\nman_csv = OUT_DIR / \"condition_samples_manifest.csv\"\npd.DataFrame(manifest).to_csv(man_csv, index=False)\nprint(\"\\nManifest:\", man_csv)\n\nif manifest:\n    cols = 3\n    rows = int(np.ceil(len(manifest)/cols))\n    plt.figure(figsize=(cols*4, rows*4))\n    for i, rec in enumerate(manifest, 1):\n        img = plt.imread(rec[\"full_png\"])\n        plt.subplot(rows, cols, i)\n        plt.imshow(img)\n        plt.title(rec[\"condition\"], fontsize=10)\n        plt.axis('off')\n    plt.tight_layout()\n    plt.show()\nelse:\n    print(\"No samples were saved — check CSV contents/column names.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-07T07:12:54.087337Z","iopub.execute_input":"2025-09-07T07:12:54.087613Z","iopub.status.idle":"2025-09-07T07:12:58.203757Z","shell.execute_reply.started":"2025-09-07T07:12:54.087591Z","shell.execute_reply":"2025-09-07T07:12:58.203029Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# === One cropped ROI per condition (VGG16-style) ============================\n# Inputs:\n#   - /kaggle/input/csv-files/*.csv (one CSV per condition; must include study_id, series_id, instance_number, x, y)\n#   - /kaggle/input/rsna-2024-lumbar-spine-degenerative-classification/train_images/<study>/<series>/<instance>.dcm\n#\n# Output (per condition):\n#   /kaggle/working/vgg16_style_crops/<condition>_crop_224.png\n#   plus a small gallery\n\nimport os, re, glob\nfrom pathlib import Path\nimport numpy as np\nimport pandas as pd\nimport pydicom\nimport matplotlib.pyplot as plt\nfrom PIL import Image\n\nCSV_DIR   = Path(\"/kaggle/input/csv-files\")\nIMG_ROOT  = Path(\"/kaggle/input/rsna-2024-lumbar-spine-degenerative-classification/train_images\")\nOUT_DIR   = Path(\"/kaggle/working/vgg16_style_crops\")\nOUT_DIR.mkdir(parents=True, exist_ok=True)\n\n# --- Helpers ---------------------------------------------------------------\n\ndef normalize_cols(df: pd.DataFrame) -> pd.DataFrame:\n    \"\"\"Map common aliases -> required cols.\"\"\"\n    cmap = {c.lower(): c for c in df.columns}\n    need = {\n        \"study_id\": [\"study_id\",\"study\",\"StudyInstanceUID\"],\n        \"series_id\": [\"series_id\",\"series\",\"SeriesInstanceUID\"],\n        \"instance_number\": [\"instance_number\",\"image_id\",\"sop_instance_uid\",\"instance\"],\n        \"x\": [\"x\",\"cx\",\"coord_x\"],\n        \"y\": [\"y\",\"cy\",\"coord_y\"],\n    }\n    rename = {}\n    for std, alts in need.items():\n        for a in alts:\n            if a.lower() in cmap:\n                rename[cmap[a.lower()]] = std\n                break\n    df = df.rename(columns=rename)\n    missing = [k for k in [\"study_id\",\"series_id\",\"instance_number\",\"x\",\"y\"] if k not in df.columns]\n    if missing:\n        raise ValueError(f\"CSV missing required columns: {missing}\")\n    return df\n\ndef resolve_dicom(study_id: str, series_id: str, inst_id: str) -> Path:\n    \"\"\"Find the DICOM file for (study, series, instance).\"\"\"\n    cands = [\n        IMG_ROOT / study_id / series_id / f\"{inst_id}.dcm\",\n        IMG_ROOT / study_id / series_id / f\"{inst_id}\",\n    ]\n    if inst_id.replace('.','',1).isdigit():\n        cands.insert(1, IMG_ROOT / study_id / series_id / f\"{int(float(inst_id))}.dcm\")\n    for c in cands:\n        if c.exists():\n            return c\n    serdir = IMG_ROOT / study_id / series_id\n    if serdir.exists():\n        hits = sorted([p for p in serdir.glob(\"*\") if inst_id in p.name])\n        if hits: return hits[0]\n        dcms = sorted(serdir.glob(\"*.dcm\"))\n        if dcms: return dcms[len(dcms)//2]\n    raise FileNotFoundError(f\"No DICOM for {study_id}/{series_id}/{inst_id}\")\n\ndef read_dicom_uint8_rgb(path: Path) -> np.ndarray:\n    \"\"\"Load DICOM → robust 1–99% normalization → uint8 RGB (H,W,3).\"\"\"\n    ds = pydicom.dcmread(str(path))\n    arr = ds.pixel_array.astype(np.float32)\n    if arr.ndim == 3:  # multi-frame\n        arr = arr[arr.shape[0]//2]\n    lo, hi = np.percentile(arr, [1, 99])\n    arr = np.clip((arr - lo) / max(hi - lo, 1e-6), 0, 1) * 255.0\n    img = arr.astype(\"uint8\")\n    return np.stack([img]*3, axis=-1)\n\ndef clamp_crop_box(w, h, cx, cy, box):\n    half = box // 2\n    left   = int(max(0, min(cx - half, w - box)))\n    top    = int(max(0, min(cy - half, h - box)))\n    right  = left + box\n    bottom = top  + box\n    return left, top, right, bottom\n\ndef crop_around_xy_to_224(img_rgb: np.ndarray, x: float, y: float,\n                          crop_box: int = 128, out_size=(224,224)) -> Image.Image:\n    \"\"\"Center crop a square around (x,y) with clamping, then resize to 224×224 RGB.\"\"\"\n    H, W = img_rgb.shape[:2]\n    box = min(crop_box, min(W, H))\n    left, top, right, bottom = clamp_crop_box(W, H, x, y, box)\n    patch = Image.fromarray(img_rgb[top:bottom, left:right])\n    if patch.size != out_size:\n        patch = patch.resize(out_size, Image.BICUBIC)\n    if patch.mode != \"RGB\":\n        patch = patch.convert(\"RGB\")\n    return patch\n\n# --- Main: one cropped sample per condition --------------------------------\n\ncsv_files = sorted(glob.glob(str(CSV_DIR / \"*.csv\")))\nif not csv_files:\n    raise FileNotFoundError(\"No CSVs found in /kaggle/input/csv-files\")\n\nsaved = []\nfor cpath in csv_files:\n    cond_name = Path(cpath).stem\n    try:\n        df = pd.read_csv(cpath)\n        df = df.dropna(how=\"any\")  # ensure required fields present\n        df = normalize_cols(df)\n        if df.empty:\n            print(f\"[WARN] {cond_name}: CSV empty after cleaning\")\n            continue\n\n        # pick ONE example (first row)\n        row = df.iloc[0]\n        sid, seid, inst = str(row[\"study_id\"]), str(row[\"series_id\"]), str(row[\"instance_number\"])\n        x, y = float(row[\"x\"]), float(row[\"y\"])\n\n        dcm = resolve_dicom(sid, seid, inst)\n        img = read_dicom_uint8_rgb(dcm)\n        crop = crop_around_xy_to_224(img, x, y, crop_box=128, out_size=(224,224))\n\n        safe = re.sub(r\"[^a-zA-Z0-9_]+\",\"_\", cond_name)[:60] or \"condition\"\n        out_png = OUT_DIR / f\"{safe}_crop_224.png\"\n        crop.save(out_png, format=\"PNG\", optimize=True)\n        print(f\"Saved: {cond_name} -> {out_png}\")\n        saved.append((cond_name, str(out_png)))\n    except Exception as e:\n        print(f\"[WARN] Skipping {cond_name}: {e}\")\n\n# --- Tiny gallery -----------------------------------------------------------\n\nif saved:\n    cols = 3\n    rows = int(np.ceil(len(saved)/cols))\n    plt.figure(figsize=(cols*4, rows*4))\n    for i, (name, p) in enumerate(saved, 1):\n        plt.subplot(rows, cols, i)\n        plt.imshow(plt.imread(p))\n        plt.title(name, fontsize=10)\n        plt.axis('off')\n    plt.tight_layout()\n    plt.show()\nelse:\n    print(\"No crops saved. Check CSV columns and dataset paths.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-07T07:43:00.820438Z","iopub.execute_input":"2025-09-07T07:43:00.820734Z","iopub.status.idle":"2025-09-07T07:43:02.578473Z","shell.execute_reply.started":"2025-09-07T07:43:00.820712Z","shell.execute_reply":"2025-09-07T07:43:02.577600Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# === RSNA 2024: One cropped ROI per condition =====================\n# Conditions: Spinal Canal Stenosis, Left/Right Neural Foraminal Narrowing,\n#             Left/Right Subarticular Stenosis\n#\n# Input:\n#   - /kaggle/input/csv-files/*.csv  (one per condition; must include study_id, series_id, instance_number, x, y)\n#   - /kaggle/input/rsna-2024-lumbar-spine-degenerative-classification/train_images/<study>/<series>/<instance>.dcm\n#\n# Output:\n#   - /kaggle/working/rsna_condition_crops/<condition>_crop_224.png\n#   - plus a gallery plot\n# ==================================================================\n\nimport os, glob, re\nfrom pathlib import Path\nimport numpy as np\nimport pandas as pd\nimport pydicom\nimport matplotlib.pyplot as plt\nfrom PIL import Image\n\nCSV_DIR   = Path(\"/kaggle/input/csv-files\")\nIMG_ROOT  = Path(\"/kaggle/input/rsna-2024-lumbar-spine-degenerative-classification/train_images\")\nOUT_DIR   = Path(\"/kaggle/working/rsna_condition_crops\")\nOUT_DIR.mkdir(parents=True, exist_ok=True)\n\n# ---- Helpers ------------------------------------------------------\n\ndef normalize_cols(df: pd.DataFrame) -> pd.DataFrame:\n    cmap = {c.lower(): c for c in df.columns}\n    need = {\n        \"study_id\": [\"study_id\",\"study\",\"StudyInstanceUID\"],\n        \"series_id\": [\"series_id\",\"series\",\"SeriesInstanceUID\"],\n        \"instance_number\": [\"instance_number\",\"image_id\",\"sop_instance_uid\",\"instance\"],\n        \"x\": [\"x\",\"coord_x\",\"cx\"],\n        \"y\": [\"y\",\"coord_y\",\"cy\"],\n    }\n    rename = {}\n    for std, alts in need.items():\n        for a in alts:\n            if a.lower() in cmap:\n                rename[cmap[a.lower()]] = std\n                break\n    df = df.rename(columns=rename)\n    return df\n\ndef resolve_dicom(study_id: str, series_id: str, inst_id: str) -> Path:\n    candidates = [\n        IMG_ROOT / study_id / series_id / f\"{inst_id}.dcm\",\n        IMG_ROOT / study_id / series_id / f\"{inst_id}\",\n    ]\n    if inst_id.replace('.','',1).isdigit():\n        candidates.insert(1, IMG_ROOT / study_id / series_id / f\"{int(float(inst_id))}.dcm\")\n    for c in candidates:\n        if c.exists():\n            return c\n    serdir = IMG_ROOT / study_id / series_id\n    if serdir.exists():\n        hits = [p for p in serdir.glob(\"*\") if inst_id in p.name]\n        if hits: return hits[0]\n        dcms = sorted(serdir.glob(\"*.dcm\"))\n        if dcms: return dcms[len(dcms)//2]\n    raise FileNotFoundError(f\"No DICOM for {study_id}/{series_id}/{inst_id}\")\n\ndef read_dicom_uint8_rgb(path: Path) -> np.ndarray:\n    ds = pydicom.dcmread(str(path))\n    arr = ds.pixel_array.astype(np.float32)\n    if arr.ndim == 3:\n        arr = arr[arr.shape[0]//2]\n    lo, hi = np.percentile(arr, [1, 99])\n    arr = np.clip((arr - lo) / max(hi - lo, 1e-6), 0, 1) * 255.0\n    img = arr.astype(\"uint8\")\n    return np.stack([img]*3, axis=-1)\n\ndef clamp_crop_box(w, h, cx, cy, box):\n    half = box // 2\n    left   = int(max(0, min(cx - half, w - box)))\n    top    = int(max(0, min(cy - half, h - box)))\n    right  = left + box\n    bottom = top  + box\n    return left, top, right, bottom\n\ndef crop_around_xy(img_rgb: np.ndarray, x: float, y: float,\n                   crop_box: int = 128, out_size=(224,224)) -> Image.Image:\n    H, W = img_rgb.shape[:2]\n    box = min(crop_box, min(W, H))\n    left, top, right, bottom = clamp_crop_box(W, H, x, y, box)\n    patch = Image.fromarray(img_rgb[top:bottom, left:right])\n    if patch.size != out_size:\n        patch = patch.resize(out_size, Image.BICUBIC)\n    return patch.convert(\"RGB\")\n\n# ---- Main ---------------------------------------------------------\n\ntarget_conditions = {\n    \"Spinal_Canal_Stenosis\": \"Spinal Canal Stenosis\",\n    \"Left_Neural_Foraminal_Narrowing\": \"Left Neural Foraminal Narrowing\",\n    \"Right_Neural_Foraminal_Narrowing\": \"Right Neural Foraminal Narrowing\",\n    \"Left_Subarticular_Stenosis\": \"Left Subarticular Stenosis\",\n    \"Right_Subarticular_Stenosis\": \"Right Subarticular Stenosis\",\n}\n\nsaved = []\nfor csv_path in glob.glob(str(CSV_DIR / \"*.csv\")):\n    cond_file = Path(csv_path).stem\n    if cond_file not in target_conditions:\n        continue\n    df = pd.read_csv(csv_path)\n    df = df.dropna(how=\"any\")\n    df = normalize_cols(df)\n    if df.empty: \n        continue\n    row = df.iloc[0]\n    sid, seid, inst = str(row[\"study_id\"]), str(row[\"series_id\"]), str(row[\"instance_number\"])\n    x, y = float(row[\"x\"]), float(row[\"y\"])\n\n    dcm = resolve_dicom(sid, seid, inst)\n    img = read_dicom_uint8_rgb(dcm)\n    crop = crop_around_xy(img, x, y, crop_box=128, out_size=(224,224))\n\n    out_name = cond_file + \"_crop_224.png\"\n    out_path = OUT_DIR / out_name\n    crop.save(out_path, format=\"PNG\", optimize=True)\n\n    print(f\"Saved {cond_file} -> {out_path}\")\n    saved.append((target_conditions[cond_file], str(out_path)))\n\n# ---- Gallery ------------------------------------------------------\n\nif saved:\n    cols = 3\n    rows = int(np.ceil(len(saved)/cols))\n    plt.figure(figsize=(cols*4, rows*4))\n    for i, (name, p) in enumerate(saved, 1):\n        plt.subplot(rows, cols, i)\n        plt.imshow(plt.imread(p))\n        plt.title(name, fontsize=10)\n        plt.axis('off')\n    plt.tight_layout()\n    plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-07T08:04:29.293964Z","iopub.execute_input":"2025-09-07T08:04:29.294504Z","iopub.status.idle":"2025-09-07T08:04:29.583466Z","shell.execute_reply.started":"2025-09-07T08:04:29.294479Z","shell.execute_reply":"2025-09-07T08:04:29.582771Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ---- Main (fuzzy filename match; pick ONE sample per base condition) ----\nimport re, glob\nfrom pathlib import Path\n\npatterns = {\n    r\"\\bspinal[_ ]?canal[_ ]?stenosis\\b\": \"Spinal Canal Stenosis\",\n    r\"\\bneural[_ ]?foram(inal)?[_ ]?narrowing\\b\": \"Neural Foraminal Narrowing\",\n    r\"\\bsubarticular[_ ]?stenosis\\b\": \"Subarticular Stenosis\",\n}\n\n# track if we've already saved a sample for a base condition\ngot = {pretty: False for pretty in patterns.values()}\nsaved = []\n\ncsv_paths = sorted(glob.glob(str(CSV_DIR / \"*.csv\")))\nfor csv_path in csv_paths:\n    stem = Path(csv_path).stem.lower()\n\n    # find which base condition this file belongs to (if any)\n    pretty = None\n    for pat, label in patterns.items():\n        if re.search(pat, stem, flags=re.IGNORECASE):\n            pretty = label\n            break\n    if pretty is None or got[pretty]:\n        continue  # skip non-condition CSVs or already satisfied\n\n    # load, normalize, choose one row, save crop\n    df = pd.read_csv(csv_path)\n    df = df.dropna(how=\"any\")\n    df = normalize_cols(df)\n    if df.empty:\n        continue\n\n    row = df.iloc[0]\n    sid, seid, inst = str(row[\"study_id\"]), str(row[\"series_id\"]), str(row[\"instance_number\"])\n    x, y = float(row[\"x\"]), float(row[\"y\"])\n\n    dcm = resolve_dicom(sid, seid, inst)\n    img = read_dicom_uint8_rgb(dcm)\n    crop = crop_around_xy(img, x, y, crop_box=128, out_size=(224,224))\n\n    safe = re.sub(r\"[^a-zA-Z0-9_]+\",\"_\", pretty)[:60]\n    out_path = OUT_DIR / f\"{safe}_crop_224.png\"\n    crop.save(out_path, format=\"PNG\", optimize=True)\n\n    print(f\"Saved {pretty} -> {out_path}  (from {Path(csv_path).name})\")\n    saved.append((pretty, str(out_path)))\n    got[pretty] = True\n\n# gallery (unchanged)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-07T08:08:35.618568Z","iopub.execute_input":"2025-09-07T08:08:35.619081Z","iopub.status.idle":"2025-09-07T08:08:35.953798Z","shell.execute_reply.started":"2025-09-07T08:08:35.619059Z","shell.execute_reply":"2025-09-07T08:08:35.953175Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ---- Five-condition saver: left/right aware --------------------------------\nimport re, glob\nfrom pathlib import Path\n\n# base patterns -> canonical base name\nbase_patterns = {\n    r\"\\bspinal[_ ]?canal[_ ]?stenosis\\b\": \"Spinal Canal Stenosis\",\n    r\"\\bneural[_ ]?foram(inal)?[_ ]?narrowing\\b\": \"Neural Foraminal Narrowing\",\n    r\"\\bsubarticular[_ ]?stenosis\\b\": \"Subarticular Stenosis\",\n}\nsides = [\"left\",\"right\"]\n\n# Track what we've already saved (avoid duplicates)\ndone = set()\nsaved = []\n\nfor csv_path in sorted(glob.glob(str(CSV_DIR / \"*.csv\"))):\n    stem = Path(csv_path).stem.lower()\n\n    # Which base condition?\n    base = None\n    for pat, label in base_patterns.items():\n        if re.search(pat, stem, flags=re.I):\n            base = label\n            break\n    if base is None:\n        continue\n\n    # Side from filename if present\n    side = None\n    for s in sides:\n        if re.search(rf\"\\b{s}\\b\", stem, flags=re.I):\n            side = s.capitalize()\n            break\n\n    # Load/normalize\n    df = pd.read_csv(csv_path)\n    df = df.dropna(how=\"any\")\n    df = normalize_cols(df)\n\n    # Try to read side from a column if not in filename\n    if side is None:\n        for col in df.columns:\n            if col.lower() in {\"side\",\"laterality\"}:\n                val = str(df.iloc[0][col]).strip().lower()\n                if val in {\"left\",\"right\"}:\n                    side = val.capitalize()\n                break\n\n    # Pick one row\n    if df.empty:\n        continue\n    row = df.iloc[0]\n    sid, seid, inst = str(row[\"study_id\"]), str(row[\"series_id\"]), str(row[\"instance_number\"])\n    x, y = float(row[\"x\"]), float(row[\"y\"])\n\n    # Build label: include side when applicable\n    label = f\"{side} {base}\" if (side and base != \"Spinal Canal Stenosis\") else base\n\n    # Skip if we already saved this label\n    if label in done:\n        continue\n\n    # Save crop\n    dcm = resolve_dicom(sid, seid, inst)\n    img = read_dicom_uint8_rgb(dcm)\n    crop = crop_around_xy(img, x, y, crop_box=128, out_size=(224,224))\n\n    safe = re.sub(r\"[^a-zA-Z0-9_]+\",\"_\", label)[:60]\n    out_path = OUT_DIR / f\"{safe}_crop_224.png\"\n    crop.save(out_path, format=\"PNG\", optimize=True)\n\n    print(f\"Saved {label} -> {out_path}  (from {Path(csv_path).name})\")\n    saved.append((label, str(out_path)))\n    done.add(label)\n\n# quick gallery\nif saved:\n    cols = 3\n    rows = int(np.ceil(len(saved)/cols))\n    plt.figure(figsize=(cols*4, rows*4))\n    for i, (name, p) in enumerate(saved, 1):\n        plt.subplot(rows, cols, i)\n        plt.imshow(plt.imread(p))\n        plt.title(name, fontsize=10)\n        plt.axis('off')\n    plt.tight_layout()\n    plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-07T08:13:07.671366Z","iopub.execute_input":"2025-09-07T08:13:07.671670Z","iopub.status.idle":"2025-09-07T08:13:08.469910Z","shell.execute_reply.started":"2025-09-07T08:13:07.671647Z","shell.execute_reply":"2025-09-07T08:13:08.469306Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}