{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":71549,"databundleVersionId":8561470,"sourceType":"competition"}],"dockerImageVersionId":31192,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## Import + config","metadata":{}},{"cell_type":"code","source":"import os\nimport cv2\nimport numpy as np\nimport pandas as pd\nfrom glob import glob\nfrom tqdm import tqdm\nimport pydicom\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-14T12:03:24.452367Z","iopub.execute_input":"2025-12-14T12:03:24.452704Z","iopub.status.idle":"2025-12-14T12:03:28.191484Z","shell.execute_reply.started":"2025-12-14T12:03:24.452678Z","shell.execute_reply":"2025-12-14T12:03:28.190236Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Path config","metadata":{}},{"cell_type":"code","source":"# Kaggle input dataset \nDATA_DIR = \"/kaggle/input/rsna-2024-lumbar-spine-degenerative-classification\"\n\n# Output \n# OUT_DIR = \"/kaggle/working/processed_volumes\"\nOUT_25D = \"/kaggle/working/processed_25d\"\n\n# os.makedirs(OUT_DIR, exist_ok=True)\nos.makedirs(OUT_25D, exist_ok=True)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-14T12:03:28.193855Z","iopub.execute_input":"2025-12-14T12:03:28.194311Z","iopub.status.idle":"2025-12-14T12:03:28.200521Z","shell.execute_reply.started":"2025-12-14T12:03:28.194285Z","shell.execute_reply":"2025-12-14T12:03:28.199047Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Load metadata","metadata":{}},{"cell_type":"code","source":"series_df = pd.read_csv(\n    os.path.join(DATA_DIR, \"train_series_descriptions.csv\")\n)\nseries_df = series_df[\n    series_df.series_description == \"Sagittal T2/STIR\"\n].reset_index(drop=True)\n\nprint(\"Sagittal T2/STIR series:\", len(series_df))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-14T12:03:28.201957Z","iopub.execute_input":"2025-12-14T12:03:28.202233Z","iopub.status.idle":"2025-12-14T12:03:28.279340Z","shell.execute_reply.started":"2025-12-14T12:03:28.202211Z","shell.execute_reply":"2025-12-14T12:03:28.278291Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Helper functions","metadata":{}},{"cell_type":"code","source":"\ndef normalize_volume(vol):\n    p1, p99 = np.percentile(vol, (1, 99))\n    vol = np.clip(vol, p1, p99)\n    vol = (vol - p1) / (p99 - p1 + 1e-6)\n    return vol#.astype(np.float32)\n\n\ndef fix_depth(vol, target_depth=32):\n    d = vol.shape[0]\n    if d >= target_depth:\n        idx = np.linspace(0, d - 1, target_depth).astype(int)\n        return vol[idx]\n    pad = target_depth - d\n    return np.pad(vol, ((0, pad), (0, 0), (0, 0)), mode=\"edge\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-14T12:03:28.280844Z","iopub.execute_input":"2025-12-14T12:03:28.281172Z","iopub.status.idle":"2025-12-14T12:03:28.288052Z","shell.execute_reply.started":"2025-12-14T12:03:28.281139Z","shell.execute_reply":"2025-12-14T12:03:28.286945Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2.5D window function","metadata":{}},{"cell_type":"code","source":"def make_25d_windows(vol, k=3, max_windows=8):\n\n    half = k // 2\n\n    wins = []\n    for i in range(half, vol.shape[0] - half):\n        wins.append(vol[i - half:i + half + 1])\n\n    wins = np.stack(wins, axis=0)\n\n    if len(wins) > max_windows:\n        idx = np.linspace(0, len(wins) - 1, max_windows).astype(int)\n        wins = wins[idx]\n\n    return wins#.astype(np.float32)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-14T12:03:28.288999Z","iopub.execute_input":"2025-12-14T12:03:28.289301Z","iopub.status.idle":"2025-12-14T12:03:28.313069Z","shell.execute_reply.started":"2025-12-14T12:03:28.289279Z","shell.execute_reply":"2025-12-14T12:03:28.311451Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Dicom -> 2.5D Dataset","metadata":{}},{"cell_type":"code","source":"def process_series_to_25d(series_path, img_size=256):\n    files = glob(os.path.join(series_path, \"*.dcm\"))\n    if len(files) == 0:\n        return None\n\n    files = sorted(\n        files,\n        key=lambda f: int(os.path.splitext(os.path.basename(f))[0])\n    )\n\n    imgs = []\n    for f in files:\n        try:\n            ds = pydicom.dcmread(f)\n            img = ds.pixel_array.astype(np.float32)\n\n            img = cv2.resize(\n                img, (img_size, img_size),\n                interpolation=cv2.INTER_AREA\n            )\n\n            imgs.append(img)\n\n        except Exception:\n            continue\n\n    if len(imgs) < 5:\n        return None\n\n    vol = np.stack(imgs, axis=0)        \n    vol = normalize_volume(vol)\n    vol = fix_depth(vol, 32)\n\n    wins = make_25d_windows(vol)       \n    return wins.astype(np.float16)     \n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-14T12:03:28.314239Z","iopub.execute_input":"2025-12-14T12:03:28.314541Z","iopub.status.idle":"2025-12-14T12:03:28.336778Z","shell.execute_reply.started":"2025-12-14T12:03:28.314517Z","shell.execute_reply":"2025-12-14T12:03:28.335217Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"rows = []\n\nfor _, r in tqdm(series_df.iterrows(), total=len(series_df)):\n    series_path = os.path.join(\n        DATA_DIR,\n        \"train_images\",\n        str(int(r.study_id)),\n        str(int(r.series_id))\n    )\n\n    wins = process_series_to_25d(series_path)\n    if wins is None:\n        continue\n\n    out_path = os.path.join(\n        OUT_25D,\n        f\"{int(r.study_id)}_{int(r.series_id)}.npy\"\n    )\n    np.save(out_path, wins)\n\n    rows.append({\n        \"study_id\": int(r.study_id),\n        \"series_id\": int(r.series_id),\n        \"series_description\": r.series_description,\n        \"path_25d\": out_path\n    })\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-14T12:03:28.340405Z","iopub.execute_input":"2025-12-14T12:03:28.340841Z","iopub.status.idle":"2025-12-14T12:16:21.430454Z","shell.execute_reply.started":"2025-12-14T12:03:28.340691Z","shell.execute_reply":"2025-12-14T12:16:21.428501Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_25d = pd.DataFrame(rows)\ndf_25d.to_csv(\n    os.path.join(OUT_25D, \"series_25d.csv\"),\n    index=False\n)\n\nprint(\"Saved 2.5D samples:\", len(df_25d))\ndf_25d.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-14T12:16:21.432089Z","iopub.execute_input":"2025-12-14T12:16:21.432482Z","iopub.status.idle":"2025-12-14T12:16:21.495779Z","shell.execute_reply.started":"2025-12-14T12:16:21.432446Z","shell.execute_reply":"2025-12-14T12:16:21.494449Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df = pd.read_csv(os.path.join(DATA_DIR, \"train.csv\"))\n\nseries_25d = pd.read_csv(os.path.join(OUT_25D, \"series_25d.csv\"))\n\n# merge label vào series_25d theo study_id\ndataset = series_25d.merge(train_df, on=\"study_id\", how=\"left\")\n\n# sanity: check label missing\nmissing = dataset.filter(like=\"stenosis\").isna().any(axis=1).sum()\nprint(\"Rows with any missing labels:\", missing)\n\ndataset.to_csv(os.path.join(OUT_25D, \"dataset.csv\"), index=False)\nprint(\"Saved final dataset:\", len(dataset))\ndataset.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-14T12:16:21.497285Z","iopub.execute_input":"2025-12-14T12:16:21.497588Z","iopub.status.idle":"2025-12-14T12:16:21.606689Z","shell.execute_reply.started":"2025-12-14T12:16:21.497565Z","shell.execute_reply":"2025-12-14T12:16:21.605535Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"dataset = dataset.dropna().reset_index(drop=True)\nprint(\"Final dataset size:\", len(dataset))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-14T12:16:21.607997Z","iopub.execute_input":"2025-12-14T12:16:21.608294Z","iopub.status.idle":"2025-12-14T12:16:21.623023Z","shell.execute_reply.started":"2025-12-14T12:16:21.608274Z","shell.execute_reply":"2025-12-14T12:16:21.621641Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Sanity check","metadata":{}},{"cell_type":"code","source":"df = pd.read_csv(os.path.join(OUT_25D, \"dataset.csv\"))\nx = np.load(df.iloc[0].path_25d)\n\nprint(\"X shape:\", x.shape, \"dtype:\", x.dtype)  \nprint(\"First row study_id:\", df.iloc[0].study_id)\nprint(\"Label columns example:\", [c for c in df.columns if \"stenosis\" in c.lower()][:10])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-14T12:16:21.624409Z","iopub.execute_input":"2025-12-14T12:16:21.624827Z","iopub.status.idle":"2025-12-14T12:16:21.695381Z","shell.execute_reply.started":"2025-12-14T12:16:21.624794Z","shell.execute_reply":"2025-12-14T12:16:21.694380Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"LABEL_MAP = {\n    \"Normal/Mild\": 0,\n    \"Moderate\": 1,\n    \"Severe\": 2\n}\n\nmeta_cols = [\n    \"study_id\",\n    \"series_id\",\n    \"series_description\",\n    \"path_25d\"\n]\n\nlabel_cols = [c for c in dataset.columns if c not in meta_cols]\n\ndef parse_label_column(col):\n    \"\"\"\n    Expected patterns:\n    spinal_canal_stenosis_L4_L5\n    left_neural_foraminal_stenosis_L5_S1\n    right_subarticular_stenosis_L3_L4\n    \"\"\"\n    parts = col.split(\"_\")\n\n    if parts[0] == \"spinal\":\n        side = \"spinal\"\n        task = \"canal\"\n        level = \"_\".join(parts[-2:])\n\n    else:\n        side = parts[0]                 \n        task = parts[1]                 \n        level = \"_\".join(parts[-2:])     \n\n    return side, task, level\n\n\nrecords = []\n\nfor _, row in tqdm(dataset.iterrows(), total=len(dataset)):\n    npy_path = row[\"path_25d\"]\n\n    for col in label_cols:\n        label_str = row[col]\n\n        if pd.isna(label_str):\n            continue\n        if label_str not in LABEL_MAP:\n            continue\n\n        side, task, level = parse_label_column(col)\n\n        records.append({\n            \"npy_path\": npy_path,\n            \"level\": level,\n            \"side\": side,\n            \"task\": task,\n            \"label\": LABEL_MAP[label_str]\n        })\n\ntrain_df = pd.DataFrame(records)\n\nTRAIN_DF_PATH = os.path.join(OUT_25D, \"train_df.csv\")\ntrain_df.to_csv(TRAIN_DF_PATH, index=False)\n\nprint(\"Saved:\", TRAIN_DF_PATH)\nprint(\"Total training samples:\", len(train_df))\ntrain_df.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-14T12:16:21.696599Z","iopub.execute_input":"2025-12-14T12:16:21.697078Z","iopub.status.idle":"2025-12-14T12:16:22.220079Z","shell.execute_reply.started":"2025-12-14T12:16:21.697054Z","shell.execute_reply":"2025-12-14T12:16:22.218871Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}