{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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"},"jupytext":{"cell_metadata_filter":"-all","main_language":"python","notebook_metadata_filter":"-all"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":99552,"databundleVersionId":13190393,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":12689617,"sourceType":"datasetVersion","datasetId":8019198},{"sourceId":254325926,"sourceType":"kernelVersion"}],"dockerImageVersionId":31089,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport shutil\nfrom collections import defaultdict\n\nimport pandas as pd\nimport polars as pl\nimport pydicom\nimport numpy as np\nimport kaggle_evaluation.rsna_inference_server","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T10:16:42.569644Z","iopub.execute_input":"2025-08-06T10:16:42.569898Z","iopub.status.idle":"2025-08-06T10:16:46.309532Z","shell.execute_reply.started":"2025-08-06T10:16:42.56987Z","shell.execute_reply":"2025-08-06T10:16:46.308662Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The evaluation API requires that you set up a server which will respond to inference requests. We have already defined the server; you just need write the predict function. When we evaluate your submission on the hidden test set the client defined in `rsna_gateway` will run in a different container with direct access to the hidden test set and hand off the data series by series.\n\nYour code will always have access to the published copies of the files.","metadata":{}},{"cell_type":"code","source":"!pip install /kaggle/input/monai-library/monai-1.5.0-py3-none-any.whl --no-deps","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T10:16:46.310379Z","iopub.execute_input":"2025-08-06T10:16:46.310738Z","iopub.status.idle":"2025-08-06T10:16:51.383199Z","shell.execute_reply.started":"2025-08-06T10:16:46.310711Z","shell.execute_reply":"2025-08-06T10:16:51.382178Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"os.environ[\"PYTORCH_CUDA_ALLOC_CONF\"] = \"expandable_segments:True\"\n\nimport gc\nfrom monai.transforms.utils import create_scale\nimport warnings\nfrom monai.networks.nets import DenseNet121\nfrom monai.transforms import Resize, Spacing, EnsureChannelFirst\nfrom monai.data import MetaTensor\nfrom skimage.morphology import dilation, disk, erosion, binary_closing, binary_opening, binary_dilation, binary_erosion\nfrom skimage.morphology import remove_small_objects\nimport scipy.ndimage as ndimage\nfrom skimage.filters import sobel\nfrom skimage.segmentation import watershed\nimport scipy.ndimage\nfrom skimage.transform import resize\nimport cv2\nimport pydicom\nfrom tqdm import tqdm\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import roc_auc_score\n\nimport torch\nfrom torch.utils import data\nimport torch.nn as nn\nimport torchvision.transforms as transforms\nimport torchvision.transforms.functional as F\nfrom torch.utils.data import DataLoader\nimport torch.optim as optim\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T10:16:51.385673Z","iopub.execute_input":"2025-08-06T10:16:51.385936Z","iopub.status.idle":"2025-08-06T10:17:24.447751Z","shell.execute_reply.started":"2025-08-06T10:16:51.385908Z","shell.execute_reply":"2025-08-06T10:17:24.446964Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CFG:\n    labels_cols = ['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', 'Right Middle Cerebral Artery',\n       'Anterior Communicating Artery', 'Left Anterior Cerebral Artery',\n       'Right Anterior Cerebral Artery', 'Left Posterior Communicating Artery',\n       'Right Posterior Communicating Artery', 'Basilar Tip',\n       'Other Posterior Circulation', 'Aneurysm Present']\n    bs = 64\n    lr = 1e-4\n    num_workers = 4\n    num_epochs = 10\n    img_shape = (64, 64, 64)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T10:17:24.448564Z","iopub.execute_input":"2025-08-06T10:17:24.44931Z","iopub.status.idle":"2025-08-06T10:17:24.454378Z","shell.execute_reply.started":"2025-08-06T10:17:24.449284Z","shell.execute_reply":"2025-08-06T10:17:24.453571Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_windowing_params(modality: str):\n    \"\"\"\n    The windowing function has been taken from the following Kaggle Notebook:\n        https://www.kaggle.com/code/zshashz/rsna-iad-efficientnetv2-lb#7.-Prediction-Functions\n    \"\"\"\n    windows = {\n        'CT': (40, 80),\n        'CTA': (50, 350),\n        'MRA': (600, 1200),\n        'MRI': (40, 80),\n    }\n    return windows.get(modality, (40, 80))\n\ndef is_valid_spacing(spacing):\n    return all([\n        isinstance(s, (int, float)) and\n        np.isfinite(s) and\n        s > 0 for s in spacing\n    ])\n\ndef apply_windowing(image, window_center, window_width):\n    \"\"\"Apply DICOM windowing to improve contrast.\"\"\"\n    min_value = window_center - (window_width / 2)\n    max_value = window_center + (window_width / 2)\n\n    # Clip and normalize image\n    windowed_image = np.clip(image, min_value, max_value)\n    windowed_image = (windowed_image - min_value) / (max_value - min_value) * 255\n    return windowed_image.astype(np.uint8)\n\n\ndef load_dicom_images(dicom_files, windowing_val, apply_skull_stripping=True, target_shape=(64, 64)):\n    \"\"\"Loads single- or multi-frame DICOM(s), applies windowing, and stacks into 3D volume.\"\"\"\n    \n    default_spacing = (0.488281, 0.488281, 0.625)\n    \n    if len(dicom_files) == 1:\n        # Case 1: Single multi-frame DICOM\n        print(\"Single multi-frame DICOM detected.\")\n        dcm = pydicom.dcmread(dicom_files[0])\n        if 'PixelData' not in dcm:\n            raise ValueError(\"No pixel data found in DICOM.\")\n\n        vol = dcm.pixel_array  # Shape: (D, H, W)\n        try:\n            x_spacing, y_spacing = map(float, dcm.PixelSpacing)\n        except Exception:\n            x_spacing, y_spacing = default_spacing[:2]\n            warnings.warn(\"PixelSpacing missing in DICOM. Using default.\")\n        try:\n            z_spacing = float(dcm.SliceThickness)\n        except Exception:\n            z_spacing = default_spacing[2]\n            warnings.warn(\"SliceThickness missing in DICOM. Using default.\")\n\n        # Apply windowing and resize\n        volume = np.zeros((target_shape[0], target_shape[1], vol.shape[0]), dtype=np.uint8)\n        for i in range(vol.shape[0]):\n            slice_img = vol[i, :, :].astype(np.float32)\n            slice_img = apply_windowing(slice_img, *windowing_val)\n            slice_img = cv2.resize(slice_img, target_shape, interpolation=cv2.INTER_LINEAR)\n            volume[:, :, i] = slice_img\n\n    else:\n        # Case 2: Multi-file 2D slices\n        dicoms = []\n        for f in dicom_files:\n            dcm = pydicom.dcmread(f)\n            z = getattr(dcm, 'ImagePositionPatient', [None, None, 0])[2]\n            dicoms.append((dcm, z))\n\n        dicoms.sort(key=lambda x: x[1])\n        dicoms = [d[0] for d in dicoms]\n\n        try:\n            x_spacing, y_spacing = map(float, dicoms[0].PixelSpacing)\n        except Exception:\n            x_spacing, y_spacing = default_spacing[:2]\n            warnings.warn(\"PixelSpacing missing in DICOM. Using default.\")\n\n        try:\n            if len(dicoms) > 1:\n                z_positions = [float(getattr(d, 'ImagePositionPatient', [None, None, 0])[2]) for d in dicoms]\n                z_spacing = np.abs(z_positions[1] - z_positions[0])\n            else:\n                z_spacing = float(dicoms[0].SliceThickness)\n        except Exception:\n            z_spacing = default_spacing[2]\n            warnings.warn(\"SliceThickness missing in DICOM. Using default.\")\n\n        volume = np.zeros((target_shape[0], target_shape[1], len(dicoms)), dtype=np.uint8)\n        for i, dcm in enumerate(dicoms):\n            slice_img = dcm.pixel_array.astype(np.float32)\n\n            if hasattr(dcm, 'RescaleSlope') and hasattr(dcm, 'RescaleIntercept'):\n                slice_img = slice_img * dcm.RescaleSlope + dcm.RescaleIntercept\n\n            slice_img = apply_windowing(slice_img, *windowing_val)\n            slice_img = cv2.resize(slice_img, target_shape, interpolation=cv2.INTER_LINEAR)\n\n            volume[:, :, i] = slice_img\n\n    return volume, (x_spacing, y_spacing, z_spacing)\n\n\ndef resample_and_resize_monai(volume, original_spacing,\n                               target_spacing=(0.488281, 0.488281, 0.625),\n                               target_shape=CFG.img_shape,\n                               is_mask=False):\n    \"\"\"\n    Resample and resize a 3D volume using MONAI transforms.\n\n    Args:\n        volume (np.ndarray): Input volume in shape (H, W, D)\n        original_spacing (tuple): (x_spacing, y_spacing, z_spacing)\n        target_spacing (tuple): Desired spacing (x, y, z)\n        target_shape (tuple): Desired output shape (D, H, W)\n        is_mask (bool): Whether it's a mask\n\n    Returns:\n        np.ndarray: Transformed volume in shape (D, H, W)\n    \"\"\"\n    # Convert to (D, H, W)\n    volume = np.transpose(volume, (2, 0, 1))\n\n    # Convert to tensor and add channel dim: (1, D, H, W)\n    dtype = torch.uint8 if is_mask else torch.float32\n    volume_tensor = torch.tensor(volume, dtype=dtype).unsqueeze(0)\n\n    if not is_valid_spacing(original_spacing):\n        warnings.warn(f\"Invalid spacing: {original_spacing}. Using default.\")\n        original_spacing = (0.488281, 0.488281, 0.625)\n\n    # Create affine matrix for MetaTensor using original_spacing (Z, Y, X)\n    z, y, x = original_spacing[2], original_spacing[1], original_spacing[0]\n    affine = torch.tensor([\n        [z, 0, 0, 0],\n        [0, y, 0, 0],\n        [0, 0, x, 0],\n        [0, 0, 0, 1]\n    ], dtype=torch.float32)\n\n    # Wrap with MetaTensor including affine\n    volume_tensor = MetaTensor(volume_tensor, affine=affine)\n\n    # Create transforms\n    interp_mode = \"nearest\" if is_mask else \"bilinear\"\n    resample = Spacing(pixdim=(target_spacing[2], target_spacing[1], target_spacing[0]), mode=interp_mode)\n    resize = Resize(spatial_size=target_shape, mode=interp_mode, align_corners=False if interp_mode == \"bilinear\" else None)\n\n    # Apply transforms\n    volume_tensor = resample(volume_tensor)\n    volume_tensor = resize(volume_tensor)\n\n    # Remove channel dim and return as numpy array\n    result = volume_tensor.squeeze(0).numpy()\n\n    return result.astype(np.uint8 if is_mask else np.float32)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T10:17:24.455393Z","iopub.execute_input":"2025-08-06T10:17:24.455706Z","iopub.status.idle":"2025-08-06T10:17:24.489025Z","shell.execute_reply.started":"2025-08-06T10:17:24.455678Z","shell.execute_reply":"2025-08-06T10:17:24.488131Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\ndevice","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T10:17:24.489953Z","iopub.execute_input":"2025-08-06T10:17:24.490249Z","iopub.status.idle":"2025-08-06T10:17:24.50752Z","shell.execute_reply.started":"2025-08-06T10:17:24.49022Z","shell.execute_reply":"2025-08-06T10:17:24.506674Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = DenseNet121(\n    spatial_dims=3,\n    in_channels=1,\n    out_channels=len(CFG.labels_cols),\n    norm='instance'\n).to(device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T10:17:24.508513Z","iopub.execute_input":"2025-08-06T10:17:24.509126Z","iopub.status.idle":"2025-08-06T10:17:24.780392Z","shell.execute_reply.started":"2025-08-06T10:17:24.509083Z","shell.execute_reply":"2025-08-06T10:17:24.779631Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load the .pth file\ncheckpoint_path = '/kaggle/input/simple-training/best_model.pth'  # Replace with your .pth file path\ncheckpoint = torch.load(checkpoint_path, map_location=device)\n\n# Load the weights into the model\nmodel.load_state_dict(checkpoint)\n\n# Set the model to evaluation mode if you're doing inference\nmodel.eval()\n\nprint(\"Loaded the model for evaluation\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T10:17:24.781559Z","iopub.execute_input":"2025-08-06T10:17:24.781822Z","iopub.status.idle":"2025-08-06T10:17:25.327141Z","shell.execute_reply.started":"2025-08-06T10:17:24.781794Z","shell.execute_reply":"2025-08-06T10:17:25.326388Z"}},"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\n# All tags (other than PixelData and SeriesInstanceUID) that may be in a test set dcm file\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\n# Replace this function with your inference code.\n# You can return either a Pandas or Polars dataframe, though Polars is recommended.\n# Each prediction (except the very first) must be returned within 30 minutes of the series being provided.\ndef predict(series_path: str) -> pl.DataFrame | pd.DataFrame:\n    \"\"\"Make a prediction.\"\"\"\n    try:\n        # --------- Replace this section with your own prediction code ---------\n        series_id = os.path.basename(series_path)\n        \n        all_filepaths = []\n        for root, _, files in os.walk(series_path):\n            for file in files:\n                if file.endswith('.dcm'):\n                    all_filepaths.append(os.path.join(root, file))\n        all_filepaths.sort()\n        \n        # Collect tags from the dicoms\n        tags = defaultdict(list)\n        tags['SeriesInstanceUID'] = series_id\n        global dcms\n        for filepath in all_filepaths:\n            ds = pydicom.dcmread(filepath, force=True)\n            tags['filepath'].append(filepath)\n            for tag in DICOM_TAG_ALLOWLIST:\n                tags[tag].append(getattr(ds, tag, None))\n            # The image is in ds.PixelData\n    \n        ######################### My code begins here #############################\n        modality = tags[\"Modality\"][0]\n        wndw = get_windowing_params(modality)\n\n        with torch.no_grad():\n            img_vol, orig_spacings = load_dicom_images(all_filepaths, wndw, apply_skull_stripping=True)\n            img_vol = resample_and_resize_monai(img_vol, orig_spacings, is_mask=False)\n        \n            chosen_ct_tensor = torch.tensor(img_vol, dtype=torch.float32) / 255.0\n            chosen_ct_tensor = chosen_ct_tensor.unsqueeze(0)  # shape: (C, D, H, W) = (1, D, H, W)\n            chosen_ct_tensor = chosen_ct_tensor.unsqueeze(0)  # shape: (1, 1, D, H, W)\n            outputs = model(chosen_ct_tensor.to(device))             # shape: [B, 16] = [1, 16]\n            probs = torch.sigmoid(outputs) \n            probs = probs.cpu().numpy().flatten()\n    \n        # Create result DataFrame\n        result_data = [[series_id] + probs.tolist()]\n        result_df = pl.DataFrame(\n            data=result_data,\n            schema=[ID_COL] + LABEL_COLS,\n            orient='row'\n        )\n        \n        # Clean up memory\n        del probs\n        torch.cuda.empty_cache()\n        gc.collect()\n        ######################### My code ends here #############################\n\n    except Exception as e:\n        print(f\"Error predicting for series {series_id}: {e}\")\n        # Return baseline predictions (0.5 for all classes)\n        result_data = [[series_id] + [0.5] * len(LABEL_COLS)]\n        result_df = pl.DataFrame(\n            data=result_data,\n            schema=[ID_COL] + LABEL_COLS,\n            orient='row')\n    \n    if isinstance(result_df, pl.DataFrame):\n        assert result_df.columns == [ID_COL, *LABEL_COLS]\n    elif isinstance(result_df, pd.DataFrame):\n        assert (result_df.columns == [ID_COL, *LABEL_COLS]).all()\n    else:\n        raise TypeError('The predict function must return a DataFrame')\n\n    # print(\"Files in /kaggle/shared at predict call:\", os.listdir(\"/kaggle/shared\"))\n    # Mandatory cleanup\n    shutil.rmtree('/kaggle/shared', ignore_errors=True)\n    \n    return result_df.drop(ID_COL)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T10:17:25.329441Z","iopub.execute_input":"2025-08-06T10:17:25.32974Z","iopub.status.idle":"2025-08-06T10:17:25.342048Z","shell.execute_reply.started":"2025-08-06T10:17:25.329721Z","shell.execute_reply":"2025-08-06T10:17:25.341203Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"When your notebook is run on the hidden test set, `inference_server.serve` must be called within 15 minutes of the notebook starting or the gateway will throw an error. If you need more than 15 minutes to load your model you can do so during the very first `predict` call.","metadata":{}},{"cell_type":"code","source":"inference_server = kaggle_evaluation.rsna_inference_server.RSNAInferenceServer(predict)\n\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    inference_server.serve()\nelse:\n    inference_server.run_local_gateway()\n    display(pl.read_parquet('/kaggle/working/submission.parquet'))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T10:17:25.343Z","iopub.execute_input":"2025-08-06T10:17:25.343317Z","iopub.status.idle":"2025-08-06T10:17:45.687207Z","shell.execute_reply.started":"2025-08-06T10:17:25.343289Z","shell.execute_reply":"2025-08-06T10:17:45.686411Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}