{"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":"gpu","dataSources":[{"sourceId":99552,"databundleVersionId":13851420,"sourceType":"competition"}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    break\n    for filename in filenames:\n        continue\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-10-05T19:06:21.886216Z","iopub.execute_input":"2025-10-05T19:06:21.886443Z","iopub.status.idle":"2025-10-05T19:06:23.548312Z","shell.execute_reply.started":"2025-10-05T19:06:21.886416Z","shell.execute_reply":"2025-10-05T19:06:23.547498Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\ntrain_df = pd.read_csv(\"/kaggle/input/rsna-intracranial-aneurysm-detection/train.csv\")\nlocalizer_df = pd.read_csv(\"/kaggle/input/rsna-intracranial-aneurysm-detection/train_localizers.csv\")\n\nprint(train_df.head())\nprint(localizer_df.head())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T19:06:23.550025Z","iopub.execute_input":"2025-10-05T19:06:23.550525Z","iopub.status.idle":"2025-10-05T19:06:23.613756Z","shell.execute_reply.started":"2025-10-05T19:06:23.550506Z","shell.execute_reply":"2025-10-05T19:06:23.61303Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pydicom\nimport os\nimport numpy as np\n\n# Example: load one scan series\nseries_uid = train_df[\"SeriesInstanceUID\"].iloc[0]\nseries_path = f\"/kaggle/input/rsna-intracranial-aneurysm-detection/series/{series_uid}/\"\n\n# Read all slices\nslices = []\nfor fname in sorted(os.listdir(series_path)):\n    dcm = pydicom.dcmread(os.path.join(series_path, fname))\n    img = dcm.pixel_array\n    slices.append(img)\n\n# Convert to 3D volume (H, W, D)\nvolume = np.stack(slices, axis=-1)\nprint(volume.shape)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T19:06:23.614543Z","iopub.execute_input":"2025-10-05T19:06:23.614808Z","iopub.status.idle":"2025-10-05T19:06:28.402936Z","shell.execute_reply.started":"2025-10-05T19:06:23.614785Z","shell.execute_reply":"2025-10-05T19:06:28.402254Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import nibabel as nib\n\nseg_path = \"/kaggle/input/rsna-intracranial-aneurysm-detection/segmentations/1.2.826.0.1.3680043.8.498.10035643165968342618460849823699311381.nii\"\nseg = nib.load(seg_path)\nseg_data = seg.get_fdata()\n\nprint(seg_data.shape)  # 3D mask volume\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T19:06:28.403686Z","iopub.execute_input":"2025-10-05T19:06:28.403937Z","iopub.status.idle":"2025-10-05T19:06:31.056579Z","shell.execute_reply.started":"2025-10-05T19:06:28.403917Z","shell.execute_reply":"2025-10-05T19:06:31.055949Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.model_selection import train_test_split\n\ntrain_ids, val_ids = train_test_split(train_df[\"SeriesInstanceUID\"], test_size=0.2, random_state=42)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T19:06:31.057308Z","iopub.execute_input":"2025-10-05T19:06:31.057912Z","iopub.status.idle":"2025-10-05T19:06:31.621985Z","shell.execute_reply.started":"2025-10-05T19:06:31.057892Z","shell.execute_reply":"2025-10-05T19:06:31.621218Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# complete_rewrite_aneurysm_dataset_fixed.py\nimport os\nimport random\nimport ast\nimport re\nfrom glob import glob\n\nimport numpy as np\nimport pandas as pd\nimport pydicom\nimport nibabel as nib\nfrom scipy.ndimage import zoom\nfrom skimage.transform import resize\n\nimport torch\nfrom torch.utils.data import Dataset, DataLoader\n\nimport matplotlib.pyplot as plt\n\n# -------------------------\n# Utilities\n# -------------------------\ndef parse_coords_field(coords):\n    \"\"\"\n    Robustly parse the 'coordinates' field from train_localizers.csv.\n    Accepts:\n      - dict-like string: \"{'x': 258.36, 'y': 261.36}\"\n      - list-like string: \"[258.36, 261.36]\"\n      - tuple-like string: \"(258.36, 261.36)\"\n      - actual dict/list/tuple/np array\n      - fallback: extract first two floats from string\n    Returns (x, y) floats or None.\n    \"\"\"\n    if coords is None:\n        return None\n    if isinstance(coords, (list, tuple, np.ndarray)):\n        try:\n            return float(coords[0]), float(coords[1])\n        except Exception:\n            return None\n    if isinstance(coords, dict):\n        try:\n            x = coords.get(\"x\") or coords.get(\"X\")\n            y = coords.get(\"y\") or coords.get(\"Y\")\n            return float(x), float(y)\n        except Exception:\n            return None\n    if isinstance(coords, str):\n        s = coords.strip()\n        try:\n            val = ast.literal_eval(s)\n            return parse_coords_field(val)\n        except Exception:\n            nums = re.findall(r\"[-+]?\\d*\\.?\\d+|\\d+\", s)\n            if len(nums) >= 2:\n                return float(nums[0]), float(nums[1])\n            return None\n    return None\n\n\ndef load_dicom_series(series_path, sort_by_instance=True):\n    \"\"\"\n    Load DICOM files from a series folder.\n    Returns:\n      - volume: numpy array (D, H, W) dtype float32\n      - sop_uids: list of SOPInstanceUID aligned with slices\n      - meta_list: list of pydicom Dataset objects (ordered)\n    \"\"\"\n    files = sorted(glob(os.path.join(series_path, \"*.dcm\")))\n    if len(files) == 0:\n        raise FileNotFoundError(f\"No DICOMs found in {series_path}\")\n\n    slices = []\n    instance_nums = []\n    sop_uids = []\n    meta_list = []\n\n    for f in files:\n        d = pydicom.dcmread(f, force=True)\n        img = d.pixel_array.astype(np.float32)\n        intercept = float(getattr(d, \"RescaleIntercept\", 0.0))\n        slope = float(getattr(d, \"RescaleSlope\", 1.0))\n        img = img * slope + intercept\n        slices.append(img)\n        instance_nums.append(getattr(d, \"InstanceNumber\", None))\n        sop = getattr(d, \"SOPInstanceUID\", None) or os.path.basename(f).replace(\".dcm\", \"\")\n        sop_uids.append(sop)\n        meta_list.append(d)\n\n    if sort_by_instance and any(x is not None for x in instance_nums):\n        paired = sorted(zip(instance_nums, slices, sop_uids, meta_list),\n                        key=lambda x: (x[0] if x[0] is not None else 0))\n        slices = [p[1] for p in paired]\n        sop_uids = [p[2] for p in paired]\n        meta_list = [p[3] for p in paired]\n\n    volume = np.stack(slices, axis=0)  # (D, H, W)\n    return volume, sop_uids, meta_list\n\n\ndef load_nifti_mask(seg_dir, series_uid, target_shape=None):\n    \"\"\"\n    Robustly find and load segmentation for a series UID.\n    Accepts files like:\n      {series_uid}.nii(.gz)  OR  {series_uid}_cowseg.nii  OR any file starting with series_uid\n    Returns:\n      mask numpy array shaped (D, H, W) dtype int16 OR None\n    \"\"\"\n    if seg_dir is None:\n        return None\n\n    # first try exact names\n    candidates = []\n    exact1 = os.path.join(seg_dir, f\"{series_uid}.nii\")\n    exact2 = os.path.join(seg_dir, f\"{series_uid}.nii.gz\")\n    if os.path.exists(exact1):\n        candidates.append(exact1)\n    if os.path.exists(exact2):\n        candidates.append(exact2)\n\n    # try any file starting with series_uid\n    if len(candidates) == 0:\n        patt = os.path.join(seg_dir, f\"{series_uid}*.nii*\")\n        candidates = glob(patt)\n\n    # fallback: any file that contains series_uid in its basename\n    if len(candidates) == 0:\n        all_files = glob(os.path.join(seg_dir, \"*.nii*\"))\n        candidates = [p for p in all_files if series_uid in os.path.basename(p)]\n\n    if len(candidates) == 0:\n        return None\n\n    # prefer exact if present, else pick first sorted candidate\n    chosen = None\n    for c in candidates:\n        base = os.path.basename(c)\n        if base == f\"{series_uid}.nii\" or base == f\"{series_uid}.nii.gz\":\n            chosen = c\n            break\n    if chosen is None:\n        candidates = sorted(candidates)\n        chosen = candidates[0]\n\n    try:\n        nii = nib.load(chosen)\n        mask = nii.get_fdata().astype(np.int16)\n    except Exception as e:\n        print(f\"[load_nifti_mask] failed to load {chosen}: {e}\")\n        return None\n\n    if mask.ndim != 3:\n        return None\n\n    # If target_shape provided (expected D,H,W from dicom stack) try to align\n    if target_shape is not None:\n        td, th, tw = target_shape  # expected D,H,W\n        # common case: mask is H,W,D -> transpose\n        if mask.shape == (th, tw, td):\n            mask = np.transpose(mask, (2, 0, 1))\n        elif mask.shape == (td, th, tw):\n            pass  # already (D,H,W)\n        else:\n            # heuristics: check if any axis equals td (depth)\n            if mask.shape[0] == td:\n                pass\n            elif mask.shape[2] == td:\n                mask = np.transpose(mask, (2, 0, 1))\n            elif mask.shape[1] == td:\n                mask = np.transpose(mask, (1, 2, 0))\n            else:\n                # final fallback: resample by zoom to target_shape\n                try:\n                    factors = (td / mask.shape[0], th / mask.shape[1], tw / mask.shape[2])\n                    mask = zoom(mask, factors, order=0)\n                except Exception:\n                    pass\n    else:\n        # if no target provided, try to guess and put depth first if last axis is smallest\n        if mask.shape[2] < mask.shape[0] and mask.shape[2] < mask.shape[1]:\n            mask = np.transpose(mask, (2, 0, 1))\n\n    return mask\n\n\ndef find_sop_index(sop_csv, sop_list):\n    \"\"\"\n    Robustly match a SOPInstanceUID from CSV to the sop_list from DICOM loading.\n    Returns index (int) or None.\n    Strategies tried in order:\n      - exact match\n      - suffix/prefix match\n      - last-N chars match\n      - substring containment\n    \"\"\"\n    if sop_csv is None:\n        return None\n    sop_csv_s = str(sop_csv).strip()\n\n    # exact\n    for i, s in enumerate(sop_list):\n        if s is None:\n            continue\n        if sop_csv_s == str(s).strip():\n            return i\n\n    # suffix/prefix\n    for i, s in enumerate(sop_list):\n        if s is None:\n            continue\n        s_s = str(s).strip()\n        if sop_csv_s.endswith(s_s) or s_s.endswith(sop_csv_s):\n            return i\n\n    # last-N chars\n    N = 32\n    csv_tail = sop_csv_s[-N:]\n    for i, s in enumerate(sop_list):\n        if s is None:\n            continue\n        if csv_tail == str(s)[-N:]:\n            return i\n\n    # substring\n    for i, s in enumerate(sop_list):\n        if s is None:\n            continue\n        if str(s) in sop_csv_s or sop_csv_s in str(s):\n            return i\n\n    return None\n\n\ndef normalize_image_slice(slice_2d, clip_pct=(0.5, 99.5)):\n    low, high = np.percentile(slice_2d, clip_pct)\n    img = np.clip(slice_2d, low, high)\n    img = (img - img.min()) / (img.max() - img.min() + 1e-8)\n    return img.astype(np.float32)\n\n\n# -------------------------\n# Dataset\n# -------------------------\nclass AneurysmDataset(Dataset):\n    LOCATION_COLS = [\n        \"Left Infraclinoid Internal Carotid Artery\",\n        \"Right Infraclinoid Internal Carotid Artery\",\n        \"Left Supraclinoid Internal Carotid Artery\",\n        \"Right Supraclinoid Internal Carotid Artery\",\n        \"Left Middle Cerebral Artery\",\n        \"Right Middle Cerebral Artery\",\n        \"Anterior Communicating Artery\",\n        \"Left Anterior Cerebral Artery\",\n        \"Right Anterior Cerebral Artery\",\n        \"Left Posterior Communicating Artery\",\n        \"Right Posterior Communicating Artery\",\n        \"Basilar Tip\",\n        \"Other Posterior Circulation\"\n    ]\n\n    def __init__(self, base_dir, df, localizer_df=None, seg_dir=None,\n                 max_slices=32, slice_strategy=\"center\", target_hw=(512,512),\n                 transforms=None, debug=False):\n        \"\"\"\n        debug: when True, prints helpful diagnostics for SOP/mask matching\n        \"\"\"\n        self.base_dir = base_dir\n        self.series_dir = os.path.join(base_dir, \"series\")\n        self.seg_dir = seg_dir\n        self.df = df.reset_index(drop=True)\n        self.localizer_df = localizer_df\n        self.max_slices = max_slices\n        self.slice_strategy = slice_strategy\n        self.target_hw = target_hw\n        self.transforms = transforms\n        self.debug = debug\n\n    def __len__(self):\n        return len(self.df)\n\n    def _get_row(self, idx):\n        return self.df.iloc[idx]\n\n    def _get_localizers_for_series(self, series_uid):\n        if self.localizer_df is None:\n            return []\n        rows = self.localizer_df[self.localizer_df[\"SeriesInstanceUID\"] == series_uid]\n        out = []\n        for _, r in rows.iterrows():\n            coords_raw = r.get(\"coordinates\", None)\n            coords_xy = parse_coords_field(coords_raw)\n            out.append({\n                \"SOPInstanceUID\": r.get(\"SOPInstanceUID\", None),\n                \"coordinates_orig\": coords_xy,\n                \"location\": r.get(\"location\", None)\n            })\n        return out\n\n    def __getitem__(self, idx):\n        row = self._get_row(idx)\n        series_uid = row[\"SeriesInstanceUID\"]\n        series_path = os.path.join(self.series_dir, series_uid)\n\n        # load volume & sop list\n        volume, sop_list, meta_list = load_dicom_series(series_path)\n        volume = volume.squeeze()\n        orig_D, orig_H, orig_W = volume.shape\n\n        # load mask (if any)\n        mask = None\n        if self.seg_dir is not None:\n            mask = load_nifti_mask(self.seg_dir, series_uid, target_shape=(orig_D, orig_H, orig_W))\n            if self.debug:\n                print(f\"[DEBUG] series {series_uid} mask found: {mask is not None}\")\n\n        # depth handling\n        if self.max_slices is not None and self.slice_strategy != \"full\":\n            D, H, W = volume.shape\n            if D == self.max_slices:\n                vol_crop = volume\n                mask_crop = mask\n                sop_crop = sop_list\n            elif D > self.max_slices:\n                if self.slice_strategy == \"center\":\n                    start = max(0, (D - self.max_slices)//2)\n                elif self.slice_strategy == \"random\":\n                    start = random.randint(0, D - self.max_slices)\n                else:\n                    start = 0\n                vol_crop = volume[start: start + self.max_slices]\n                mask_crop = mask[start: start + self.max_slices] if mask is not None else None\n                sop_crop = sop_list[start: start + self.max_slices]\n            else:\n                factor = self.max_slices / float(D)\n                vol_crop = zoom(volume, (factor, 1, 1), order=1)\n                mask_crop = zoom(mask, (factor, 1, 1), order=0) if mask is not None else None\n                sop_crop = [None] * vol_crop.shape[0]\n            volume = vol_crop\n            mask = mask_crop\n            sop_list = sop_crop\n\n        # record pre-resize dims\n        D_before, H_before, W_before = volume.shape\n\n        # resize in-plane to target_hw\n        if self.target_hw is not None:\n            target_h, target_w = self.target_hw\n            D_now, H_now, W_now = volume.shape\n            vol_resized = np.zeros((D_now, target_h, target_w), dtype=volume.dtype)\n            for i in range(D_now):\n                vol_resized[i] = resize(volume[i], (target_h, target_w), order=1, preserve_range=True, anti_aliasing=True)\n            volume = vol_resized\n            if mask is not None:\n                mask_resized = np.zeros((volume.shape[0], target_h, target_w), dtype=mask.dtype)\n                for i in range(mask.shape[0]):\n                    mask_resized[i] = resize(mask[i], (target_h, target_w), order=0, preserve_range=True, anti_aliasing=False)\n                mask = mask_resized\n\n        # after-resize dims\n        D_after, H_after, W_after = volume.shape\n\n        # scaling factors for coords\n        scale_x = (W_after / float(W_before)) if W_before and W_after else 1.0\n        scale_y = (H_after / float(H_before)) if H_before and H_after else 1.0\n\n        # localizers mapping and scaling\n        localizers = self._get_localizers_for_series(series_uid)\n        for loc in localizers:\n            sop_csv = loc.get(\"SOPInstanceUID\", None)\n            idx_found = find_sop_index(sop_csv, sop_list)\n            loc[\"slice_index\"] = int(idx_found) if idx_found is not None else None\n            orig = loc.get(\"coordinates_orig\", None)\n            if orig is not None:\n                x0, y0 = orig\n                x_scaled = x0 * scale_x\n                y_scaled = y0 * scale_y\n                loc[\"coordinates_scaled\"] = (x_scaled, y_scaled)\n                loc[\"coordinates_norm\"] = (x_scaled / float(W_after), y_scaled / float(H_after))\n            else:\n                loc[\"coordinates_scaled\"] = None\n                loc[\"coordinates_norm\"] = None\n            if self.debug:\n                print(f\"[DEBUG] loc sop_csv={str(sop_csv)[:60]} found_idx={loc['slice_index']} scaled={loc['coordinates_scaled']}\")\n\n        # normalize and to tensor\n        volume_norm = np.stack([normalize_image_slice(s) for s in volume], axis=0)  # (D,H,W)\n        volume_norm = np.expand_dims(volume_norm, axis=0).astype(np.float32)        # (1,D,H,W)\n        volume_t = torch.from_numpy(volume_norm)\n\n        mask_t = torch.from_numpy(mask.astype(np.int16)) if mask is not None else None\n\n        # labels & meta\n        label = int(row.get(\"Aneurysm Present\", 0))\n        locations = {col: int(row.get(col, 0)) for col in self.LOCATION_COLS}\n        metadata = {\n            \"PatientAge\": row.get(\"PatientAge\", None),\n            \"PatientSex\": row.get(\"PatientSex\", None),\n            \"Modality\": row.get(\"Modality\", None)\n        }\n\n        item = {\n            \"series_uid\": series_uid,\n            \"volume\": volume_t,\n            \"label\": label,\n            \"locations\": locations,\n            \"localizers\": localizers,\n            \"mask\": mask_t,  # (D,H,W) tensor or None\n            \"meta\": metadata\n        }\n\n        if self.transforms is not None:\n            item = self.transforms(item)\n        return item\n\n\n# -------------------------\n# Collate and plotting\n# -------------------------\ndef aneurysm_collate(batch):\n    volumes = torch.stack([b[\"volume\"] for b in batch], dim=0)  # (B, C, D, H, W)\n    labels = torch.tensor([b[\"label\"] for b in batch], dtype=torch.long)\n    series_uids = [b[\"series_uid\"] for b in batch]\n    metas = [b.get(\"meta\", {}) for b in batch]\n    locations = [b[\"locations\"] for b in batch]\n    localizers = [b[\"localizers\"] for b in batch]\n\n    masks_list = [b[\"mask\"] for b in batch]\n    if all(m is not None for m in masks_list):\n        masks_stacked = torch.stack([m for m in masks_list], dim=0)  # (B, D, H, W)\n    else:\n        masks_stacked = masks_list\n\n    return {\n        \"volume\": volumes,\n        \"label\": labels,\n        \"series_uid\": series_uids,\n        \"meta\": metas,\n        \"locations\": locations,\n        \"localizers\": localizers,\n        \"mask\": masks_stacked\n    }\n\n\ndef plot_two_random_slices_with_annotations(item, figsize=(10, 6), seed=None):\n    if seed is not None:\n        random.seed(seed)\n\n    vol = item[\"volume\"].numpy()[0]  # (D,H,W)\n    D, H, W = vol.shape\n    if D == 0:\n        raise ValueError(\"Empty volume\")\n\n    if D == 1:\n        idxs = [0, 0]\n    else:\n        idxs = random.sample(range(D), k=2 if D >= 2 else 1)\n\n    fig, axes = plt.subplots(2, 2, figsize=figsize)\n    fig.suptitle(f\"Series: {item['series_uid']}  |  Aneurysm: {item['label']}\", fontsize=13)\n\n    coords_by_slice = {}\n    for loc in item.get(\"localizers\", []):\n        slice_idx = loc.get(\"slice_index\", None)\n        coords_scaled = loc.get(\"coordinates_scaled\", None)\n        if slice_idx is not None and coords_scaled is not None:\n            coords_by_slice.setdefault(int(slice_idx), []).append((coords_scaled, loc.get(\"location\", None)))\n        elif slice_idx is None and coords_scaled is not None:\n            mid = D // 2\n            coords_by_slice.setdefault(mid, []).append((coords_scaled, loc.get(\"location\", None)))\n\n    for row_i, slice_idx in enumerate(idxs):\n        img = vol[slice_idx]\n        ax_raw = axes[row_i, 0]\n        ax_overlay = axes[row_i, 1]\n\n        ax_raw.imshow(img, cmap=\"gray\")\n        ax_raw.set_title(f\"Slice {slice_idx} (raw)\")\n        ax_raw.axis(\"off\")\n\n        ax_overlay.imshow(img, cmap=\"gray\")\n        mask = item.get(\"mask\", None)\n        if mask is not None and isinstance(mask, torch.Tensor):\n            m = mask.numpy()\n            if m.ndim == 3 and m.shape[0] == D:\n                mask_slice = m[slice_idx]\n                ax_overlay.imshow(np.ma.masked_where(mask_slice == 0, mask_slice), alpha=0.45, cmap=\"jet\")\n\n        coords_list = coords_by_slice.get(slice_idx, [])\n        for (coords_scaled, location_name) in coords_list:\n            x, y = coords_scaled\n            ax_overlay.scatter([x], [y], s=80, marker='x', color='yellow')\n            if location_name is not None:\n                ax_overlay.text(x + 5, y + 5, location_name, color='yellow', fontsize=8, backgroundcolor='black')\n\n        ax_overlay.set_title(f\"Slice {slice_idx} (overlay)\")\n        ax_overlay.axis(\"off\")\n\n    locs = item.get(\"locations\", {})\n    pos = [k for k, v in locs.items() if v == 1]\n    pos_txt = \", \".join(pos) if len(pos) > 0 else \"No positive location flags\"\n    meta = item.get(\"meta\", {})\n    info_txt = f\"Age: {meta.get('PatientAge', 'N/A')}   Sex: {meta.get('PatientSex','N/A')}   Modality: {meta.get('Modality','N/A')}\"\n    plt.figtext(0.5, 0.02, f\"{info_txt}  |  Positive location flags: {pos_txt}\", wrap=True, ha=\"center\", fontsize=10)\n    plt.tight_layout(rect=[0, 0.03, 1, 0.95])\n    plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T19:22:48.698674Z","iopub.execute_input":"2025-10-05T19:22:48.699356Z","iopub.status.idle":"2025-10-05T19:22:48.773991Z","shell.execute_reply.started":"2025-10-05T19:22:48.699309Z","shell.execute_reply":"2025-10-05T19:22:48.773356Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -------------------------\n# Example usage snippet (adjust base_dir as appropriate)\n# -------------------------\nif __name__ == \"__main__\":\n    base_dir = \"/kaggle/input/rsna-intracranial-aneurysm-detection\"\n    df = pd.read_csv(os.path.join(base_dir, \"train.csv\"))\n    local_df = pd.read_csv(os.path.join(base_dir, \"train_localizers.csv\"))\n    seg_dir = os.path.join(base_dir, \"segmentations\")\n\n    ds = AneurysmDataset(base_dir=base_dir, df=df, localizer_df=local_df, seg_dir=seg_dir,\n                         max_slices=32, slice_strategy=\"center\", target_hw=(512, 512), debug=True)\n    loader = DataLoader(ds, batch_size=4, shuffle=True, collate_fn=aneurysm_collate, num_workers=2, pin_memory=True)\n\n    # load a sample and plot\n    sample_item = ds[2]\n    print(\"Series:\", sample_item[\"series_uid\"])\n    print(\"Volume shape (C,D,H,W):\", sample_item[\"volume\"].shape)\n    print(\"Label:\", sample_item[\"label\"])\n    print(\"Localizers (first):\", sample_item[\"localizers\"][:2])\n    plot_two_random_slices_with_annotations(sample_item, seed=42)\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T19:22:50.489476Z","iopub.execute_input":"2025-10-05T19:22:50.489904Z","iopub.status.idle":"2025-10-05T19:22:55.429506Z","shell.execute_reply.started":"2025-10-05T19:22:50.489868Z","shell.execute_reply":"2025-10-05T19:22:55.428718Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sample_item","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T19:22:55.430558Z","iopub.execute_input":"2025-10-05T19:22:55.43079Z","iopub.status.idle":"2025-10-05T19:22:55.440654Z","shell.execute_reply.started":"2025-10-05T19:22:55.430772Z","shell.execute_reply":"2025-10-05T19:22:55.439858Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df.iloc[0]['SeriesInstanceUID']","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T19:22:55.4414Z","iopub.execute_input":"2025-10-05T19:22:55.44169Z","iopub.status.idle":"2025-10-05T19:22:55.451126Z","shell.execute_reply.started":"2025-10-05T19:22:55.441674Z","shell.execute_reply":"2025-10-05T19:22:55.450402Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df[df['SeriesInstanceUID'] == '1.2.826.0.1.3680043.8.498.10004044428023505108375152878107656647']","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T19:22:55.452744Z","iopub.execute_input":"2025-10-05T19:22:55.453301Z","iopub.status.idle":"2025-10-05T19:22:55.472573Z","shell.execute_reply.started":"2025-10-05T19:22:55.453282Z","shell.execute_reply":"2025-10-05T19:22:55.471858Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T19:22:55.473383Z","iopub.execute_input":"2025-10-05T19:22:55.47386Z","iopub.status.idle":"2025-10-05T19:22:55.493878Z","shell.execute_reply.started":"2025-10-05T19:22:55.473842Z","shell.execute_reply":"2025-10-05T19:22:55.493326Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"len(df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T19:22:55.494635Z","iopub.execute_input":"2025-10-05T19:22:55.494859Z","iopub.status.idle":"2025-10-05T19:22:55.507388Z","shell.execute_reply.started":"2025-10-05T19:22:55.494842Z","shell.execute_reply":"2025-10-05T19:22:55.506643Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"np.unique(df['Modality'],return_counts=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T19:22:55.508118Z","iopub.execute_input":"2025-10-05T19:22:55.508375Z","iopub.status.idle":"2025-10-05T19:22:55.52304Z","shell.execute_reply.started":"2025-10-05T19:22:55.508359Z","shell.execute_reply":"2025-10-05T19:22:55.522556Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df['Modality'].value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T19:22:55.523971Z","iopub.execute_input":"2025-10-05T19:22:55.524206Z","iopub.status.idle":"2025-10-05T19:22:55.538646Z","shell.execute_reply.started":"2025-10-05T19:22:55.52419Z","shell.execute_reply":"2025-10-05T19:22:55.538118Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df['SeriesInstanceUID'].nunique()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T19:22:55.539226Z","iopub.execute_input":"2025-10-05T19:22:55.539403Z","iopub.status.idle":"2025-10-05T19:22:55.554748Z","shell.execute_reply.started":"2025-10-05T19:22:55.539389Z","shell.execute_reply":"2025-10-05T19:22:55.554046Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df['Modality'].unique()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T19:22:55.556396Z","iopub.execute_input":"2025-10-05T19:22:55.556654Z","iopub.status.idle":"2025-10-05T19:22:55.569245Z","shell.execute_reply.started":"2025-10-05T19:22:55.556636Z","shell.execute_reply":"2025-10-05T19:22:55.568682Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"local_df['SeriesInstanceUID'].nunique()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T19:22:55.569869Z","iopub.execute_input":"2025-10-05T19:22:55.57014Z","iopub.status.idle":"2025-10-05T19:22:55.584645Z","shell.execute_reply.started":"2025-10-05T19:22:55.570117Z","shell.execute_reply":"2025-10-05T19:22:55.584096Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"local_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T19:22:55.585264Z","iopub.execute_input":"2025-10-05T19:22:55.585442Z","iopub.status.idle":"2025-10-05T19:22:55.60115Z","shell.execute_reply.started":"2025-10-05T19:22:55.585421Z","shell.execute_reply":"2025-10-05T19:22:55.600396Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\n\n# Load the pretrained 3D ResNet model\nmodel = torch.hub.load(\"Warvito/MedicalNet-models\", 'medicalnet_resnet10_23datasets')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T19:22:55.601919Z","iopub.execute_input":"2025-10-05T19:22:55.602432Z","iopub.status.idle":"2025-10-05T19:22:56.147839Z","shell.execute_reply.started":"2025-10-05T19:22:55.602414Z","shell.execute_reply":"2025-10-05T19:22:56.146932Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!wget https://huggingface.co/TencentMedicalNet/MedicalNet-Resnet10/resolve/main/resnet_10_23dataset.pth","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T19:06:48.136117Z","iopub.execute_input":"2025-10-05T19:06:48.136645Z","iopub.status.idle":"2025-10-05T19:06:48.70529Z","shell.execute_reply.started":"2025-10-05T19:06:48.136619Z","shell.execute_reply":"2025-10-05T19:06:48.704337Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nfrom torchvision import models\n\nclass AneurysmNet(nn.Module):\n    def __init__(self, n_modalities=4, metadata_dim=2, pretrained=True):\n        super().__init__()\n\n        # Load 3D ResNet backbone from MedicalNet\n        self.backbone = torch.hub.load(\n            \"Warvito/MedicalNet-models\", \n            'medicalnet_resnet10_23datasets', \n        )\n\n        if pretrained:\n            pretrained_weights = torch.load(\n                \"/kaggle/working/resnet_10_23dataset.pth\", \n                map_location=\"cpu\"\n            )\n            self.backbone.load_state_dict(pretrained_weights, strict=False)\n\n        # Remove classifier head (fc) -> keep feature extractor\n        self.backbone.fc = nn.Identity()\n\n        # Modality embedding\n        self.modality_emb = nn.Embedding(n_modalities, 32)\n\n        # Metadata branch\n        self.meta_fc = nn.Sequential(\n            nn.Linear(metadata_dim, 32),\n            nn.ReLU(),\n            nn.Linear(32, 32),\n            nn.ReLU()\n        )\n\n        # Final classifier\n        self.classifier = nn.Sequential(\n            nn.Linear(512 + 32 + 32, 128),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(128, 1)\n        )\n\n    def forward(self, x, modality_idx=None, metadata=None):\n        B = x.shape[0]\n\n        # MedicalNet expects 3D input: [B, C, D, H, W]\n        feat = self.backbone(x)  # [B, 512]\n        # Global average pooling over D,H,W\n        feat = torch.mean(feat, dim=[2,3,4])  # now feat: [B, C]\n\n        if modality_idx is not None:\n            m_emb = self.modality_emb(modality_idx)  # [B, 32]\n        else:\n            m_emb = torch.zeros(B, 32, device=x.device)\n\n        if metadata is not None:\n            meta_feat = self.meta_fc(metadata)  # [B, 32]\n        else:\n            meta_feat = torch.zeros(B, 32, device=x.device)\n\n        combined = torch.cat([feat, m_emb, meta_feat], dim=1)  # [B, 576]\n        out = self.classifier(combined)\n        return torch.sigmoid(out)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T19:22:09.967821Z","iopub.execute_input":"2025-10-05T19:22:09.968665Z","iopub.status.idle":"2025-10-05T19:22:09.977104Z","shell.execute_reply.started":"2025-10-05T19:22:09.968635Z","shell.execute_reply":"2025-10-05T19:22:09.976382Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"local_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T19:22:58.441879Z","iopub.execute_input":"2025-10-05T19:22:58.442655Z","iopub.status.idle":"2025-10-05T19:22:58.451197Z","shell.execute_reply.started":"2025-10-05T19:22:58.442626Z","shell.execute_reply":"2025-10-05T19:22:58.450572Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T19:22:58.571507Z","iopub.execute_input":"2025-10-05T19:22:58.571684Z","iopub.status.idle":"2025-10-05T19:22:58.583312Z","shell.execute_reply.started":"2025-10-05T19:22:58.57167Z","shell.execute_reply":"2025-10-05T19:22:58.582529Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import DataLoader\nfrom sklearn.model_selection import train_test_split\nfrom tqdm import tqdm\n\n# -------------------------------\n# Map modalities to indices\n# -------------------------------\nmodality_map = {\"CTA\":0, \"MRA\":1, \"MRI T2\":2, \"MRI T1post\":3}\n\nbase_dir = \"/kaggle/input/rsna-intracranial-aneurysm-detection\"\ndf = pd.read_csv(os.path.join(base_dir, \"train.csv\"))\nlocal_df = pd.read_csv(os.path.join(base_dir, \"train_localizers.csv\"))\n\n# -------------------------------\n# Helper: prepare batch\n# -------------------------------\ndef prepare_batch(batch):\n    volumes = batch['volume'].float()                    # [B,1,D,H,W]\n    labels = batch['label'].float().unsqueeze(1)         # [B,1]\n\n    # modality encoding\n    modality_idx = torch.tensor(\n        [modality_map[m['Modality']] for m in batch['meta']],\n        device=volumes.device\n    )\n\n    # metadata: Age normalized + Sex encoded\n    # metadata: Age normalized + Sex encoded\n    ages = torch.tensor(\n        [m.get('PatientAge', 0) / 100. for m in batch['meta']],\n        device=volumes.device,\n        dtype=torch.float32   # <--- enforce float32\n    ).unsqueeze(1)\n    \n    sexes = torch.tensor(\n        [0 if m.get('PatientSex','Male')=='Male' else 1 for m in batch['meta']],\n        device=volumes.device,\n        dtype=torch.float32   # <--- enforce float32\n    ).unsqueeze(1)\n    \n    metadata = torch.cat([ages, sexes], dim=1)\n\n    return volumes, modality_idx, metadata, labels\n\n# -------------------------------\n# Dataset split\n# -------------------------------\ntrain_df, val_df = train_test_split(\n    df, test_size=0.2, random_state=42, stratify=df['Aneurysm Present']\n)\ntrain_loc_df = local_df[local_df['SeriesInstanceUID'].isin(train_df['SeriesInstanceUID'])]\nval_loc_df   = local_df[local_df['SeriesInstanceUID'].isin(val_df['SeriesInstanceUID'])]\n\nseg_dir = os.path.join(base_dir, \"segmentations\")\n\ntrain_dataset = AneurysmDataset(base_dir=base_dir, localizer_df=train_loc_df, df=train_df, seg_dir=seg_dir)\nval_dataset   = AneurysmDataset(base_dir=base_dir, localizer_df=val_loc_df, df=val_df, seg_dir=seg_dir)\n\ntrain_loader = DataLoader(train_dataset, batch_size=2, shuffle=True, num_workers=4, collate_fn=aneurysm_collate)\nval_loader   = DataLoader(val_dataset, batch_size=2, shuffle=False, num_workers=2, collate_fn=aneurysm_collate)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T19:22:58.660569Z","iopub.execute_input":"2025-10-05T19:22:58.66077Z","iopub.status.idle":"2025-10-05T19:22:58.706878Z","shell.execute_reply.started":"2025-10-05T19:22:58.660755Z","shell.execute_reply":"2025-10-05T19:22:58.706388Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -------------------------------\n# Model, optimizer, loss\n# -------------------------------\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nmodel = AneurysmNet(n_modalities=4, metadata_dim=2, pretrained=True).to(device)\n\noptimizer = torch.optim.Adam(model.parameters(), lr=1e-4)\ncriterion = nn.BCELoss()\n\n# -------------------------------\n# Training loop\n# -------------------------------\nn_epochs = 3\ntrain_losses, val_losses, val_accs = [], [], []\n\nfor epoch in range(n_epochs):\n    model.train()\n    train_loss = 0.0\n    pbar = tqdm(train_loader, desc=f\"Epoch {epoch+1}/{n_epochs}\")\n\n    for batch in pbar:\n        volumes, modality_idx, metadata, labels = prepare_batch(batch)\n        volumes, modality_idx, metadata, labels = (\n            volumes.to(device), modality_idx.to(device), metadata.to(device), labels.to(device)\n        )\n\n        optimizer.zero_grad()\n        outputs = model(volumes, modality_idx, metadata)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n\n        train_loss += loss.item() * volumes.size(0)\n        pbar.set_postfix({\"loss\": loss.item()})\n\n    train_loss /= len(train_loader.dataset)\n\n    # -------------------------------\n    # Validation\n    # -------------------------------\n    model.eval()\n    val_loss, correct, total = 0.0, 0, 0\n    all_preds, all_labels = [], []\n    with torch.no_grad():\n        for batch in val_loader:\n            volumes, modality_idx, metadata, labels = prepare_batch(batch)\n            volumes, modality_idx, metadata, labels = (\n                volumes.to(device), modality_idx.to(device), metadata.to(device), labels.to(device)\n            )\n\n            outputs = model(volumes, modality_idx, metadata)\n            loss = criterion(outputs, labels)\n            val_loss += loss.item() * volumes.size(0)\n\n            preds = (outputs > 0.5).long()\n            correct += (preds == labels.long()).sum().item()\n            total += labels.size(0)\n\n            all_preds.extend(preds.cpu().numpy().flatten())\n            all_labels.extend(labels.cpu().numpy().flatten())\n\n    val_loss /= len(val_loader.dataset)\n    val_acc = correct / total\n    train_losses.append(train_loss)\n    val_losses.append(val_loss)\n    val_accs.append(val_acc)\n\n    print(f\"Epoch {epoch+1} | Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f} | Val Acc: {val_acc:.4f}\")\n\n    # -------------------------------\n    # Plot predictions vs labels\n    # -------------------------------\n    plt.figure(figsize=(8,5))\n    plt.plot(all_labels[:100], \"g.-\", label=\"Ground Truth\")\n    plt.plot(all_preds[:100], \"r.-\", label=\"Predictions\")\n    plt.title(f\"Predicted vs Ground Truth (Epoch {epoch+1})\")\n    plt.xlabel(\"Sample idx\")\n    plt.ylabel(\"Label\")\n    plt.legend()\n    plt.show()\n\n    # -------------------------------\n    # Visualize some volumes + predictions\n    # -------------------------------\n    example_vol = volumes[0,0].cpu().numpy()   # pick first sample, channel 0\n    mid_slice = example_vol[example_vol.shape[0]//2]  # middle slice along depth\n    plt.figure(figsize=(6,6))\n    plt.imshow(mid_slice, cmap=\"gray\")\n    plt.title(f\"Example slice | True: {labels[0].item()} | Pred: {preds[0].item()}\")\n    plt.axis(\"off\")\n    plt.show()\n\n# -------------------------------\n# Plot training curves\n# -------------------------------\nplt.figure(figsize=(10,5))\nplt.plot(train_losses, label=\"Train Loss\")\nplt.plot(val_losses, label=\"Val Loss\")\nplt.plot(val_accs, label=\"Val Acc\")\nplt.xlabel(\"Epoch\")\nplt.legend()\nplt.title(\"Training Curves\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T19:22:58.812713Z","iopub.execute_input":"2025-10-05T19:22:58.812903Z","execution_failed":"2025-10-05T19:23:31.979Z"}},"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}]}