{"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":99552,"databundleVersionId":13851420,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":668232,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":505913,"modelId":520716},{"sourceId":672921,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":508860,"modelId":523531}],"dockerImageVersionId":31192,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# Standard Library\nimport ast\nimport multiprocessing as mp\nimport os\nimport random\nimport shutil\nfrom collections import Counter, defaultdict\nfrom typing import List, Tuple\n\n# Data Processing\nimport numpy as np\nimport pandas as pd\nimport polars as pl\n\n# Medical Imaging\nimport pydicom as dicom\n\n# Visualization\nimport matplotlib.pyplot as plt\n\n# SciPy / Image Processing\nimport scipy.ndimage as ndi\nfrom scipy import ndimage\nfrom scipy.ndimage import (zoom as ndi_zoom, binary_dilation, binary_closing, generate_binary_structure,)\n\n# ML / Preprocessing\nfrom sklearn.model_selection import (train_test_split, StratifiedShuffleSplit,)\nfrom sklearn.preprocessing import StandardScaler\n\n# PyTorch\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, Subset\n\n# Kaggle Evaluation Tools\nimport kaggle_evaluation.rsna_inference_server","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-06T23:09:15.249249Z","iopub.execute_input":"2025-12-06T23:09:15.249554Z","iopub.status.idle":"2025-12-06T23:09:25.245893Z","shell.execute_reply.started":"2025-12-06T23:09:15.249529Z","shell.execute_reply":"2025-12-06T23:09:25.24462Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ID_COL = 'SeriesInstanceUID'\n\nLABEL_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    'Aneurysm Present',\n]\n\nDICOM_TAG_ALLOWLIST = [\n    'BitsAllocated',\n    'BitsStored',\n    'Columns',\n    'FrameOfReferenceUID',\n    'HighBit',\n    'ImageOrientationPatient',\n    'ImagePositionPatient',\n    'InstanceNumber',\n    'Modality',\n    'PatientID',\n    'PhotometricInterpretation',\n    'PixelRepresentation',\n    'PixelSpacing',\n    'PlanarConfiguration',\n    'RescaleIntercept',\n    'RescaleSlope',\n    'RescaleType',\n    'Rows',\n    'SOPClassUID',\n    'SOPInstanceUID',\n    'SamplesPerPixel',\n    'SliceThickness',\n    'SpacingBetweenSlices',\n    'StudyInstanceUID',\n    'TransferSyntaxUID',\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T23:09:25.24794Z","iopub.execute_input":"2025-12-06T23:09:25.24838Z","iopub.status.idle":"2025-12-06T23:09:25.25668Z","shell.execute_reply.started":"2025-12-06T23:09:25.248354Z","shell.execute_reply":"2025-12-06T23:09:25.255708Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Model configuration\nTARGET_SIZE = (160, 160, 160)  # Reduced size for memory efficiency\nTARGET_ISO_BRAIN = 1.25\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T23:09:25.2589Z","iopub.execute_input":"2025-12-06T23:09:25.259586Z","iopub.status.idle":"2025-12-06T23:09:25.416726Z","shell.execute_reply.started":"2025-12-06T23:09:25.259554Z","shell.execute_reply":"2025-12-06T23:09:25.415757Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Image Pre-Processing:","metadata":{}},{"cell_type":"markdown","source":"## Helper Functions","metadata":{}},{"cell_type":"code","source":"def _get_slice_order(slice_sample):\n    \"\"\"\n    INPUT: \n    slice_sample: dicom read single slice.\n    OUPTUT:\n    z_cord: slice's Z axis index for ordering.\n    \"\"\"\n    #check if the slice/dicom_file has ImageOrientationPatient\n    iop = getattr(slice_sample, \"ImageOrientationPatient\", None)\n    ipp = getattr(slice_sample, \"ImagePositionPatient\", None)\n\n    if iop is not None and ipp is not None and len(iop)>=6 and len(ipp)>=3:\n        x_cord = np.array(iop[:3], float)      # rows dir (x)\n        y_cord = np.array(iop[3:6], float)     # cols dir (y)\n        z_cord = np.cross(x_cord, y_cord)      # slice normal (z)\n        ipp = np.array(ipp[:3], float)\n        return float(np.dot(ipp, z_cord))\n\n    ins_num = getattr(slice_sample, \"InstanceNumber\", None)\n    if ins_num is not None:\n        z_cord = int(ins_num)\n        return z_cord \n\ndef _slice_by_slice_correct_rescale(list_of_slices):\n    \"\"\"\n    INPUT: \n    list_of_slices: List of dicom read slices (order does not matter).\n    OUPTUT:\n    corrected_slices: 1 channel, propper gray scale and corrected list of dicom slices in Array form.\n    \"\"\"\n    corrected_slices = []\n    for slce in list_of_slices:\n        slce_array = slce.pixel_array.astype(np.float32)\n        \n        #Fix the contrast of some images in case the smaller values are white instead of black for consistency\n        # https://dicom.innolitics.com/ciods/rt-dose/image-pixel/00280004\n        # https://dgobbi.github.io/vtk-dicom/doc/api/imageDisplay.html?utm_source=chatgpt.com\n        phot = str(getattr(slce, \"PhotometricInterpretation\", \"MONOCHROME2\")).upper()\n        if phot == \"MONOCHROME1\":\n            maxv = (2**int(getattr(slce, \"BitsStored\", arr.dtype.itemsize*8))) - 1\n            slce_array = maxv - slce_array\n\n        # https://collectiveminds.health/articles/the-ultimate-guide-to-preprocessing-medical-images-techniques-tools-and-best-practices-for-enhanced-diagnosis\n        #Collapse channels if there are more than 1, maybe just CT, but lets be sure.\n        spp = int(getattr(slce, \"SamplesPerPixel\", 1))\n        if spp == 3 and slce_array.ndim == 3:\n            slce_array = slce_array.mean(axis=-1)\n        \n        # Apply DICOM linear HU rescale because we are using different modalities: https://www.mathworks.com/help/medical-imaging/ug/overview-medical-image-preprocessing.html \n        slope = float(getattr(slce, \"RescaleSlope\", 1.0))\n        inter = float(getattr(slce, \"RescaleIntercept\", 0.0))\n        corrected_slices.append(slce_array * slope + inter)\n    return corrected_slices\n\ndef _get_slices_spacing(list_of_slices):\n    \"\"\"\n    INPUT: \n    list_of_slices: List of dicom read slices (order does not matter).\n    OUPTUT:\n    spacing_zyx: the pixel spacing from the dicom files in Array form\n    \"\"\"\n    sy, sx = map(float, getattr(list_of_slices[0], \"PixelSpacing\", [1.0,1.0]))\n    sz = None\n\n    st = getattr(list_of_slices[0], \"SliceThickness\", None)\n    sbs = getattr(list_of_slices[0], \"SpacingBetweenSlices\", None)\n    if st is not None:\n        sz = float(st)\n    if sbs is not None:\n        sz = float(sbs)\n\n    if sz is None:\n        iop = getattr(list_of_slices[0], \"ImageOrientationPatient\", None)\n        if iop is not None and len(iop) >= 6:\n            row = np.array(iop[:3], dtype=float)\n            col = np.array(iop[3:6], dtype=float)\n            norm   = np.cross(row, col)\n            scalars = []\n            for slc in list_of_slices:\n                ipp = getattr(slc, \"ImagePositionPatient\", None)\n                if ipp is not None and len(ipp)>=3:\n                    scalars.append(np.dot(np.array(ipp[:3], float), norm))\n            if len(scalars)>=2:\n                diffs = np.diff(np.sort(scalars))\n                sz = float(np.median(np.abs(diffs)))\n        if sz is None:\n            sz = 1.0\n    spacing_zyx = (sz, sy, sx)\n    return spacing_zyx\n\ndef load_dicom_series(dicom_patient_dir):\n    \"\"\"\n    INPUT: \n    dicom_patient_dir: Patient folder directory with dicom files per slice.\n    OUPTUT:\n    dicom_vol: Ordered and rescaled read dicom slices stacked to make a volume\n    modality: MRI, CTA, etc....\n\n    Variables Cheat Sheet\n    list_slices_files: The list of PATHS for each slice in the patient folder.\n    dicom_slices: dicom read files with data.\n    \"\"\"\n    list_slices_files = [\n        os.path.join(dicom_patient_dir, f) \n        for f in os.listdir(dicom_patient_dir) \n        if f.lower().endswith(\".dcm\")]\n    \n    if not list_slices_files: \n        raise ValueError(f\"No DICOM slice files found in patient folder: {dicom_patient_dir}.\")\n    \n    dicom_slices = []\n    for slice_file in list_slices_files:\n        dicom_slice = dicom.dcmread(\n            slice_file, stop_before_pixels=False, force=True)\n        dicom_slices.append(dicom_slice)\n\n    dicom_slices.sort(key=_get_slice_order)\n    modality = getattr(dicom_slices[0], 'Modality')\n    the_series_uid = getattr(dicom_slices[0], \"SeriesInstanceUID\")\n\n    # Calculate the Spacing using the dicom metadata of one slice.\n    spacing_zyx = _get_slices_spacing(dicom_slices)\n\n    \n    slices = _slice_by_slice_correct_rescale(dicom_slices)\n    dicom_vol = np.stack(slices, axis=0)\n\n    return modality, spacing_zyx, dicom_vol, the_series_uid, dicom_slices\n\n\n\ndef resample_isotropic(vol_zyx, spacing_zyx, target_iso=1, order=1):\n    \"\"\"\n    Resample a [Z,H,W] volume to isotropic spacing (target_iso in mm).\n    INPUT:\n      vol_zyx: np.ndarray [Z,H,W]\n      spacing_zyx: tuple (sz, sy, sx) in mm\n      target_iso: desired isotropic spacing in mm \n      order: interpolation order (1 = linear)\n    OUPTUT:\n      vol_iso: np.ndarray resampled volume\n      new_spacing_zyx: (target_iso, target_iso, target_iso)\n    \"\"\"\n    sz, sy, sx = map(float, spacing_zyx)\n    tz = ty = tx = float(target_iso)\n\n    # zoom factors are (old_spacing / new_spacing)\n    zoom_factors = (sz / tz, sy / ty, sx / tx)\n    vol_iso = ndi_zoom(vol_zyx, zoom=zoom_factors, order=order)\n    spacing_iso_zyx = (tz, ty, tx)\n    return spacing_iso_zyx, vol_iso.astype(np.float32)\n\ndef modality_specific_preprocessing(vol, modality, cta_startegy=\"clip\"):\n    \"\"\"\n    Clip the extremes values of a volume (HU scaled)\n    INPUT:\n        volume: full sampled volume\n    OUPTUT: \n        volume: extremes clipped volume\n    Variables Cheat Sheet:\n        cta_startegy: clip or hu_window\n    \"\"\"\n    if modality == 'CTA' or modality == 'CT':\n        if cta_startegy == 'clip':\n            vol = _clip_by_percentile_per_volume(vol)\n        elif cta_startegy == 'hu_window':\n            vol = _apply_hu_window(vol)\n        elif cta_startegy == 'hu_range':\n            vol = _apply_hu_range(vol)\n        else:\n            vol = _clip_by_percentile_per_volume(vol)\n        \n        vol = _apply_normalization(vol, norm='min-max')\n        return vol\n    else:\n        vol = _apply_normalization(vol, norm='z-score')\n        return vol\n\ndef _clip_by_percentile_per_volume(vol, low_p=0.5, high_p=99.5):\n    \"\"\"\n    Clip the HU value of the CT volume\n    INPUT: \n        vol: volume tensor\n        low: lower percentile value to clip\n        high: higher percentile value to clip\n    \n    OUPTUT:\n        vol: clipped vol\n    \"\"\"\n    lo = np.percentile(vol, low_p)\n    hi = np.percentile(vol, high_p)\n    vol = np.clip(vol, lo, hi)\n    return ((vol - lo) / (hi - lo + 1e-6)).astype(np.float32)\n\ndef _apply_hu_window(vol, center=125, width=550):\n    \"\"\"\n    INPUT: \n        vol: volume tensor\n        center: center of the HU window\n        width: width of the HU unit window [center-width, center+width]\n    \n    OUPTUT:\n        vol: volume scaled to the defined window.\n    \"\"\"\n    lo = center - width/2.0\n    hi = center + width/2.0\n    vol = np.clip(vol, lo, hi)\n    return ((vol - lo) / (hi - lo + 1e-6)).astype(np.float32)\n\ndef _apply_hu_range(vol, hounsfield_range=(100, 600)):\n    \"\"\"\n    INPUT: \n        vol: volume tensor\n        center: center of the HU window\n        width: width of the HU unit window [center-width, center+width]\n    \n    OUPTUT:\n        range_vol: a mask of only the boders\n    \"\"\"\n    mask = (vol >= hounsfield_range[0]) & (vol <= hounsfield_range[1])\n    range_vol = np.where(mask, vol, 0)\n    return range_vol\n\ndef _apply_normalization(vol, norm='z-score'):\n    \"\"\"\n    INPUT: \n        vol: volume tenso\n        norm: normalization strategy (max-min for CTA, z-score for MRI)\n    \n    OUPTUT:\n        vol: Normalized volume tensor.\n    \"\"\"\n    if norm == 'min-max':\n        return (vol - vol.min()) / (vol.max() - vol.min() + 1e-8)\n    elif norm == 'z-score':\n        return (vol - vol.mean()) / vol.std()\n\ndef center_crop_or_pad(vol, target_shape, z_start=0.55, x_start=0.5, y_start=0.5):\n    vol_z, vol_y, vol_x = vol.shape\n    tz, ty, tx = target_shape\n    padz = max(tz - vol_z, 0)\n    pady = max(ty - vol_y, 0)\n    padx = max(tx - vol_x, 0)\n    vol = np.pad(vol,\n                 ((padz//2, padz - padz//2),\n                  (pady//2, pady - pady//2),\n                  (padx//2, padx - padx//2)),\n                 mode='constant')\n    z, y, x = vol.shape\n    cz = int(round(z_start * (z - 1)))\n    cy = int(round(y_start * (y - 1)))\n    cx = int(round(x_start * (x - 1)))\n    \n    startz, starty, startx = cz-tz//2, cy-ty//2, cx-tx//2\n    startz = max(0, min(z - tz, startz))\n    starty = max(0, min(y - ty, starty))\n    startx = max(0, min(x - tx, startx))\n                       \n    endz, endy, endx = startz + tz, starty + ty, startx + tx\n    return vol[startz:endz, starty:endy, startx:endx]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T23:09:25.417788Z","iopub.execute_input":"2025-12-06T23:09:25.418459Z","iopub.status.idle":"2025-12-06T23:09:25.451851Z","shell.execute_reply.started":"2025-12-06T23:09:25.418418Z","shell.execute_reply":"2025-12-06T23:09:25.450834Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Main Function","metadata":{}},{"cell_type":"code","source":"def load_and_preprocess_series(dicom_dir):\n    \"\"\"\n    Full pipeline:\n    DICOM folder -> ordered + HU-rescaled slices -> \n    modality-specific normalization -> isotropic resample -> center crop/pad.\n    Returns:\n        vol_iso: np.ndarray [Z,H,W] float32 normalized, ready for U-Net.\n        modality: modality string (\"CT\", \"CTA\", \"MR\", ...)\n        spacing_iso_zyx: (1,1,1) after resample (for reference only)\n    \"\"\"\n    # 1) Load dicom series\n    modality, spacing_zyx_raw, sample_volume_raw, series_uid, dicom_slices = load_dicom_series(dicom_dir)\n\n    # 2) Modality-specific preprocessing (HU window/clip + normalization)\n    sample_volume_pre = modality_specific_preprocessing(sample_volume_raw, modality, cta_startegy=\"hu_window\")\n\n    # Handle unnecessary frame dim if present\n    if sample_volume_pre.ndim == 4 and sample_volume_pre.shape[0] == 1:\n        sample_volume_pre = np.squeeze(sample_volume_pre, axis=0)\n\n    # 3) Resample to isotropic spacing\n    spacing_zyx_iso, volume_iso = resample_isotropic(sample_volume_pre, spacing_zyx_raw, target_iso=TARGET_ISO_BRAIN)\n\n    # 4) Center crop or pad to fixed shape\n    volume_iso = center_crop_or_pad(volume_iso, TARGET_SIZE).astype(np.float32)\n\n    return volume_iso, modality, spacing_zyx_iso","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T23:09:25.452832Z","iopub.execute_input":"2025-12-06T23:09:25.453473Z","iopub.status.idle":"2025-12-06T23:09:25.474194Z","shell.execute_reply.started":"2025-12-06T23:09:25.453439Z","shell.execute_reply":"2025-12-06T23:09:25.473042Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Models Used","metadata":{}},{"cell_type":"markdown","source":"## U-Net","metadata":{}},{"cell_type":"code","source":"class ConvBlock3D(nn.Module):\n    \"\"\"\n    3D convolutional downsampling block:\n    Based on: \n    github.com/ahsan-83/Deep-Learning-Specialization-Coursera/blob/main/Convolutional%20Neural%20Networks/Assignment/Week%203-Programming%20Assignment%20Image%20Segmentation%20with%20U-Net/Image_segmentation_Unet_v2.ipynb\n    Tweaked for 3D\n    Arguments:\n        inputs -- Input tensor\n        n_filters -- Number of filters for the convolutional layers\n        dropout_prob -- Dropout probability\n        max_pooling -- Use MaxPooling2D to reduce the spatial dimensions of the output volume\n    Returns:\n      next_layer, skip_connection --  Next layer and skip connection outputs\n    \"\"\"\n    def __init__(self, in_channels, out_channels, dropout_prob=0.0, use_maxpool=True):\n        super().__init__()\n        self.use_maxpool = use_maxpool\n\n        self.conv1 = nn.Conv3d(in_channels, out_channels, kernel_size=3, padding=1)\n        self.conv2 = nn.Conv3d(out_channels, out_channels, kernel_size=3, padding=1)\n        self.bn1 = nn.BatchNorm3d(out_channels)\n        self.bn2 = nn.BatchNorm3d(out_channels)\n\n        self.dropout = nn.Dropout3d(dropout_prob) if dropout_prob > 0 else None\n        self.pool = nn.MaxPool3d(kernel_size=2, stride=2) if use_maxpool else None\n\n    def forward(self, x):\n        x = self.conv1(x)\n        x = self.bn1(x)\n        x = F.relu(x, inplace=True)\n\n        x = self.conv2(x)\n        x = self.bn2(x)\n        x = F.relu(x, inplace=True)\n\n        if self.dropout is not None:\n            x = self.dropout(x)\n            \n        if self.use_maxpool:\n            next_layer = self.pool(x)\n        else:\n            next_layer = x\n\n        skip = x  # skip connection\n\n        return next_layer, skip\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T23:09:25.475149Z","iopub.execute_input":"2025-12-06T23:09:25.475576Z","iopub.status.idle":"2025-12-06T23:09:25.49191Z","shell.execute_reply.started":"2025-12-06T23:09:25.475551Z","shell.execute_reply":"2025-12-06T23:09:25.490869Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class UpBlock3D(nn.Module):\n    \"\"\"\n    Convolutional upsampling block\n    Based on: \n    github.com/ahsan-83/Deep-Learning-Specialization-Coursera/blob/main/Convolutional%20Neural%20Networks/Assignment/Week%203-Programming%20Assignment%20Image%20Segmentation%20with%20U-Net/Image_segmentation_Unet_v2.ipynb\n    Tweaked for 3D\n    Arguments:\n        expansive_input -- Input tensor from previous layer\n        contractive_input -- Input tensor from previous skip layer\n        n_filters -- Number of filters for the convolutional layers\n    Returns: \n        conv -- Tensor output\n    \"\"\"\n    def __init__(self, in_channels, out_channels):\n        super().__init__()\n\n        # upsample by factor 2 in D, H, W\n        self.upconv = nn.ConvTranspose3d(\n            in_channels, out_channels,\n            kernel_size=2, stride=2\n        )\n\n        # Merge the previous output and the contractive_input\n        self.conv1 = nn.Conv3d(out_channels * 2, out_channels, kernel_size=3, padding=1)\n        self.conv2 = nn.Conv3d(out_channels, out_channels, kernel_size=3, padding=1)\n        self.bn1 = nn.BatchNorm3d(out_channels)\n        self.bn2 = nn.BatchNorm3d(out_channels)\n\n    def forward(self, expansive_input, contractive_input):\n        x = self.upconv(expansive_input)\n        x = torch.cat([contractive_input, x], dim=1)\n        x = self.conv1(x)\n        x = self.bn1(x)\n        x = F.relu(x, inplace=True)\n        x = self.conv2(x)\n        x = self.bn2(x)\n        x = F.relu(x, inplace=True)\n\n        return x","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T23:09:25.494751Z","iopub.execute_input":"2025-12-06T23:09:25.495029Z","iopub.status.idle":"2025-12-06T23:09:25.513472Z","shell.execute_reply.started":"2025-12-06T23:09:25.495005Z","shell.execute_reply":"2025-12-06T23:09:25.512503Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class UNet3D(nn.Module):\n    def __init__(self, in_channels=1, n_filters=16, n_classes=1):\n        \"\"\"\n        in_channels: channels of input volume (e.g., 1 for CT/MRI)\n        n_filters: base number of filters (you may want 16 or 32 depending on GPU)\n        n_classes: output channels (1 for binary mask)\n        \"\"\"\n        super().__init__()\n\n        # Encoder: Down\n        self.enc1 = ConvBlock3D(in_channels,      n_filters,      dropout_prob=0.0, use_maxpool=True)\n        self.enc2 = ConvBlock3D(n_filters,        n_filters * 2,  dropout_prob=0.0, use_maxpool=True)\n        self.enc3 = ConvBlock3D(n_filters * 2,    n_filters * 4,  dropout_prob=0.0, use_maxpool=True)\n        self.enc4 = ConvBlock3D(n_filters * 4,    n_filters * 8,  dropout_prob=0.0, use_maxpool=True)\n        self.bottleneck = ConvBlock3D(n_filters * 8, n_filters * 16,\n                                      dropout_prob=0.0, use_maxpool=False)\n\n        # Decoder: Up\n        self.up4 = UpBlock3D(n_filters * 16, n_filters * 8)  \n        self.up3 = UpBlock3D(n_filters * 8,  n_filters * 4)   \n        self.up2 = UpBlock3D(n_filters * 4,  n_filters * 2)   \n        self.up1 = UpBlock3D(n_filters * 2,  n_filters * 1)   \n        self.final_conv = nn.Conv3d(n_filters, n_classes, kernel_size=1)\n\n    def forward(self, x):\n\n        # Encoding\n        x1, skip1 = self.enc1(x)\n        x2, skip2 = self.enc2(x1)\n        x3, skip3 = self.enc3(x2)\n        x4, skip4 = self.enc4(x3)\n\n        # Bottleneck\n        bottleneck, _ = self.bottleneck(x4)\n\n        # Decoding\n        d4 = self.up4(bottleneck, skip4)\n        d3 = self.up3(d4,        skip3)\n        d2 = self.up2(d3,        skip2)\n        d1 = self.up1(d2,        skip1)\n\n        logits = self.final_conv(d1)\n        return logits\n    \n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T23:09:25.514444Z","iopub.execute_input":"2025-12-06T23:09:25.514769Z","iopub.status.idle":"2025-12-06T23:09:25.54013Z","shell.execute_reply.started":"2025-12-06T23:09:25.514738Z","shell.execute_reply":"2025-12-06T23:09:25.538969Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_roi_from_pred_mask(vol_tensor, mask_logits, padding=5, size=(64,64,64)):\n    Dz, Dy, Dx = size\n    \n    #Make binay mask\n    probs = torch.sigmoid(mask_logits)\n    mask  = (probs > 0.5).float()\n\n    B, C, D, H, W = vol_tensor.shape\n\n    #Get the center depending the case\n    #Center crop if no mask was predicted, to work around false positives returned by U-Net\n    if mask.sum() == 0:\n        cz, cy, cx = D//2, H//2, W//2\n    else:\n        coords = mask.nonzero(as_tuple=False).float()   \n        cz, cy, cx = coords[:, 2:].mean(dim=0).tolist() \n        cz, cy, cx = int(cz), int(cy), int(cx)\n\n    #get ROI coordinates\n    z_min = max(0, cz - Dz // 2)\n    y_min = max(0, cy - Dy // 2)\n    x_min = max(0, cx - Dx // 2)\n\n    z_max = min(D, z_min + Dz)\n    y_max = min(H, y_min + Dy)\n    x_max = min(W, x_min + Dx)\n\n    #get ROI\n    roi = vol_tensor[:, :, z_min:z_max, y_min:y_max, x_min:x_max]\n    return roi, (cz, cy, cx)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T23:09:25.541246Z","iopub.execute_input":"2025-12-06T23:09:25.541579Z","iopub.status.idle":"2025-12-06T23:09:25.560749Z","shell.execute_reply.started":"2025-12-06T23:09:25.54155Z","shell.execute_reply":"2025-12-06T23:09:25.55976Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Classification Head","metadata":{}},{"cell_type":"code","source":"class AneurysmClassifier3D(nn.Module):\n    def __init__(self, in_channels=1, num_classes=16):\n        super().__init__()\n        \n        # Conv3D -> BN -> ReLU -> MaxPool\n        self.features = nn.Sequential(\n            self._conv_block(in_channels, 16),\n            nn.MaxPool3d(2), # 64 -> 32\n            \n            self._conv_block(16, 32),\n            nn.MaxPool3d(2), # 32 -> 16\n            \n            self._conv_block(32, 64),\n            nn.MaxPool3d(2), # 16 -> 8\n            \n            self._conv_block(64, 128),\n            nn.MaxPool3d(2), # 8 -> 4\n        )\n        \n        # Classification\n        self.classifier = nn.Sequential(\n            nn.AdaptiveAvgPool3d(1), # Flattens [128, 4, 4, 4] -> [128, 1, 1, 1]\n            nn.Flatten(),\n            nn.Linear(128, 64),\n            nn.ReLU(),\n            nn.Dropout(0.5),\n            nn.Linear(64, num_classes)\n        )\n\n    def _conv_block(self, in_c, out_c):\n        return nn.Sequential(\n            nn.Conv3d(in_c, out_c, kernel_size=3, padding=1),\n            nn.BatchNorm3d(out_c),\n            nn.ReLU(inplace=True)\n        )\n\n    def forward(self, x):\n        x = self.features(x)\n        x = self.classifier(x)\n        return x","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T23:09:25.5616Z","iopub.execute_input":"2025-12-06T23:09:25.561886Z","iopub.status.idle":"2025-12-06T23:09:25.586171Z","shell.execute_reply.started":"2025-12-06T23:09:25.561865Z","shell.execute_reply":"2025-12-06T23:09:25.584439Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Loading Models","metadata":{}},{"cell_type":"code","source":"def load_models():\n    \n    global UNET, CLS\n    UNET = UNet3D(in_channels=1, n_filters=8, n_classes=1).to(DEVICE)\n    UNET.load_state_dict(torch.load(\"/kaggle/input/unet-bce-and-dice-loss-checkpoint-7epochs/pytorch/default/1/epoch6_unet3d_seg (3).pth\", map_location=DEVICE))\n    UNET.eval()\n\n    CLS = AneurysmClassifier3D(num_classes=len(LABEL_COLS)).to(DEVICE)\n    CLS.load_state_dict(torch.load(\"/kaggle/input/roi-classifier-3d-cnn/tensorflow2/default/5/aneurysm_roi_classifier_simple.pth\", map_location=DEVICE))\n    CLS.eval()\n    return UNET, CLS","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T23:09:25.58735Z","iopub.execute_input":"2025-12-06T23:09:25.587611Z","iopub.status.idle":"2025-12-06T23:09:25.613852Z","shell.execute_reply.started":"2025-12-06T23:09:25.58759Z","shell.execute_reply":"2025-12-06T23:09:25.612502Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Predict","metadata":{}},{"cell_type":"code","source":"def predict(series_path: str, slice_to_view = None):\n\n    #Get the Whole path to a user's folder with dicom files.\n    series_id = os.path.basename(series_path)\n    #Preprocess the Volume of the user based only on the DICOM files in their folder\n    vol_iso, modality, spacing_iso = load_and_preprocess_series(series_path)\n\n    #Get the volume: FOR VISUALIZATION ONLY\n    vol_t = torch.from_numpy(vol_iso).unsqueeze(0).unsqueeze(0).to(DEVICE)\n    plt.figure(figsize=(20,4))\n    print(vol_t.shape)\n    \n    #Load Both Models\n    unet, cls = load_models()\n    \n    with torch.no_grad():\n        #Get the predicted logits of the mask using the volume as input for the U-Net\n        mask_logits = unet(vol_t)  # [1, 1, D, H, W]\n        probs = torch.sigmoid(mask_logits)\n\n        #Get the mask and reduze single dimensions. FOR VISUALIZATION ONLY\n        mask  = (probs > 0.5).float() \n        probs_2d_max = probs.max(dim=4)[0].max(dim=3)[0]  \n        probs_1d = probs_2d_max.squeeze(0).squeeze(0)  \n        mask_slices = (probs_1d > 0.01).nonzero(as_tuple=True)[0] \n        print(\"U-Net's predicted mask shape:\", mask_logits.shape)\n        print(\"U-Net's mask's slices containing pixels:\", mask_slices.tolist())\n        print(\"Number of slices with positive pixels:\", len(mask_slices))\n        \n        # Build ROI using the U-Net's generated Mask's centroid\n        roi, centroid = build_roi_from_pred_mask(vol_t, \n                                       mask_logits, \n                                       padding=5, \n                                       size=(64, 64, 64))\n\n        # VISUALZIE THE OBTAINED MASK, and ROI\n        plt.subplot(1,4,1)\n        plt.imshow(vol_t[0, 0, centroid[0]].numpy())\n        plt.scatter([centroid[2]], [centroid[1]], c='red', s=40)\n        plt.title(\"Original Volume\")\n        print(\"Visualizing slice with mask' center. Slice No.:\", centroid[2])\n        plt.subplot(1,4,2)\n        plt.imshow(mask_logits[0, 0, centroid[0]].numpy())\n        plt.scatter([centroid[2]], [centroid[1]], c='red', s=40)\n        plt.title(\"U-net's probs\")\n        plt.subplot(1,4,3)\n        plt.imshow(mask[0, 0, centroid[0]].numpy())\n        plt.scatter([centroid[2]], [centroid[1]], c='red', s=40)\n        plt.title(f\"U-net's binary mask with center: {centroid}\")\n        plt.subplot(1,4,4)\n        plt.imshow(roi[0, 0, 32].numpy())\n        plt.title(f\"Center of the generated ROI\")\n\n        # Classify ROI\n        logits = cls(roi)[0]  # [num_labels]\n        probs_t = torch.sigmoid(logits)\n        probs = probs_t.detach().cpu().numpy().astype(np.float32)\n    \n    predictions = pd.DataFrame([[series_id, *probs]], columns=[ID_COL, *LABEL_COLS])\n    return predictions.drop(columns=[ID_COL])\n   ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T23:09:25.614872Z","iopub.execute_input":"2025-12-06T23:09:25.615559Z","iopub.status.idle":"2025-12-06T23:09:25.637556Z","shell.execute_reply.started":"2025-12-06T23:09:25.615496Z","shell.execute_reply":"2025-12-06T23:09:25.636609Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Visualize Samples of Confirmed Aneurysms","metadata":{}},{"cell_type":"code","source":"localizer_csv = \"/kaggle/input/rsna-intracranial-aneurysm-detection/train_localizers.csv\"\ndf = pd.read_csv(localizer_csv)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T23:11:44.666367Z","iopub.execute_input":"2025-12-06T23:11:44.666644Z","iopub.status.idle":"2025-12-06T23:11:44.684785Z","shell.execute_reply.started":"2025-12-06T23:11:44.666625Z","shell.execute_reply":"2025-12-06T23:11:44.683546Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## False Negative","metadata":{}},{"cell_type":"code","source":"SERIES_EXAMPLE_NUMBER = df['SeriesInstanceUID'].iloc[10]\nprint(df.iloc[10])\nSERIES_EXAMPLE = '/kaggle/input/rsna-intracranial-aneurysm-detection/series/'\nSERIES_EXAMPLE_FULL = SERIES_EXAMPLE + SERIES_EXAMPLE_NUMBER\npredictions = predict(series_path= SERIES_EXAMPLE_FULL, slice_to_view=84)\npredictions","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T23:12:34.901649Z","iopub.execute_input":"2025-12-06T23:12:34.902Z","iopub.status.idle":"2025-12-06T23:12:41.181249Z","shell.execute_reply.started":"2025-12-06T23:12:34.901977Z","shell.execute_reply":"2025-12-06T23:12:41.180265Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# True Positive","metadata":{}},{"cell_type":"code","source":"SERIES_EXAMPLE_NUMBER = df['SeriesInstanceUID'].iloc[15]\nprint(df.iloc[15])\nSERIES_EXAMPLE = '/kaggle/input/rsna-intracranial-aneurysm-detection/series/'\nSERIES_EXAMPLE_FULL = SERIES_EXAMPLE + SERIES_EXAMPLE_NUMBER\npredictions = predict(series_path= SERIES_EXAMPLE_FULL, slice_to_view=84)\npredictions","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T23:11:52.669603Z","iopub.execute_input":"2025-12-06T23:11:52.669965Z","iopub.status.idle":"2025-12-06T23:12:02.774663Z","shell.execute_reply.started":"2025-12-06T23:11:52.669941Z","shell.execute_reply":"2025-12-06T23:12:02.773794Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Visualize Samples of the Whole Dataset","metadata":{}},{"cell_type":"code","source":"train_csv = '/kaggle/input/rsna-intracranial-aneurysm-detection/train.csv'\ndf = pd.read_csv(train_csv)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T23:09:38.968237Z","iopub.execute_input":"2025-12-06T23:09:38.968528Z","iopub.status.idle":"2025-12-06T23:09:38.994468Z","shell.execute_reply.started":"2025-12-06T23:09:38.968499Z","shell.execute_reply":"2025-12-06T23:09:38.993448Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## True Negative","metadata":{}},{"cell_type":"code","source":"SERIES_EXAMPLE_NUMBER = df['SeriesInstanceUID'].iloc[4]\nprint(df.iloc[4])\nSERIES_EXAMPLE = '/kaggle/input/rsna-intracranial-aneurysm-detection/series/'\nSERIES_EXAMPLE_FULL = SERIES_EXAMPLE + SERIES_EXAMPLE_NUMBER\npredictions = predict(series_path= SERIES_EXAMPLE_FULL)\npredictions  ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T23:11:14.427319Z","iopub.execute_input":"2025-12-06T23:11:14.427647Z","iopub.status.idle":"2025-12-06T23:11:22.543812Z","shell.execute_reply.started":"2025-12-06T23:11:14.427623Z","shell.execute_reply":"2025-12-06T23:11:22.542917Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## False Positive","metadata":{}},{"cell_type":"code","source":"SERIES_EXAMPLE_NUMBER = df['SeriesInstanceUID'].iloc[3]\nprint(df.iloc[3])\nSERIES_EXAMPLE = '/kaggle/input/rsna-intracranial-aneurysm-detection/series/'\nSERIES_EXAMPLE_FULL = SERIES_EXAMPLE + SERIES_EXAMPLE_NUMBER\npredictions = predict(series_path= SERIES_EXAMPLE_FULL)\npredictions  ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T23:09:46.673747Z","iopub.execute_input":"2025-12-06T23:09:46.674105Z","iopub.status.idle":"2025-12-06T23:10:01.970388Z","shell.execute_reply.started":"2025-12-06T23:09:46.674071Z","shell.execute_reply":"2025-12-06T23:10:01.96909Z"}},"outputs":[],"execution_count":null}]}