{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":99552,"databundleVersionId":13851420,"sourceType":"competition"},{"sourceId":608565,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":456837,"modelId":472817}],"dockerImageVersionId":31089,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import shutil\n\nimport polars as pl\n\nimport kaggle_evaluation.rsna_inference_server","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-14T20:43:32.611801Z","iopub.execute_input":"2025-10-14T20:43:32.612049Z","iopub.status.idle":"2025-10-14T20:43:35.173758Z","shell.execute_reply.started":"2025-10-14T20:43:32.612025Z","shell.execute_reply":"2025-10-14T20:43:35.173036Z"}},"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":"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]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-14T20:43:35.17447Z","iopub.execute_input":"2025-10-14T20:43:35.174833Z","iopub.status.idle":"2025-10-14T20:43:35.180121Z","shell.execute_reply.started":"2025-10-14T20:43:35.174808Z","shell.execute_reply":"2025-10-14T20:43:35.179399Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Loading Data","metadata":{}},{"cell_type":"code","source":"import os\nimport pydicom\n\ndef dicom_serie_load(serie_path):\n\n    instances_filename = os.listdir(serie_path)\n    \n    ds_l = []\n    for root, _, filenames in os.walk(serie_path):\n        for filename in filenames:\n            if filename.endswith(\".dcm\"):\n                ds = pydicom.dcmread(f\"{root}/{filename}\")\n                ds_l.append(ds)\n\n    return ds_l\n\n\ndef dicom_get_zposition(ds):\n    \n    if getattr(ds, \"ImagePositionPatient\", None) and len(ds.ImagePositionPatient) >= 3:\n        z_position = ds.ImagePositionPatient[2]\n    else:  # in case the tag is missing or does not contain the z axis position\n        z_position = getattr(ds, \"InstanceNumber\", 0.0)\n\n    return float(z_position)\n\n\ndef dicom_get_rescale_factors(ds):\n\n    slope = getattr(ds, \"RescaleSlope\", 1.0)\n    intercept = getattr(ds, \"RescaleIntercept\", 0.0)\n    \n    return float(slope), float(intercept)\n    \n\nimport torch\nfrom copy import deepcopy\n\ndef dicom_split_array_from_metadata(ds):  # process a single DICOM\n\n    ds_copy = deepcopy(ds)  # copy the dicom object\n    pixel_pt = torch.from_numpy(ds_copy.pixel_array)\n    del ds_copy.PixelData  # remove pixel data from the dicom object copy\n    \n    return pixel_pt, ds_copy\n\n\ndef dicom_serie_process(ds_l):\n\n    n_ds = len(ds_l)\n\n    pixel_pt, ds_metadata = dicom_split_array_from_metadata(ds_l[0])  # Get first instance\n\n    if n_ds == 1:  # one dicom with the whole volume\n        volume = pixel_pt\n        ds_metadata_l = [ds_metadata]\n    else:  # each dicom with a slice\n        volume = torch.zeros((n_ds, *pixel_pt.shape), dtype=torch.float32)\n        ds_metadata_l = [None] * n_ds\n        volume[0] = pixel_pt\n        ds_metadata_l[0] = ds_metadata\n\n        # To later sort the slices\n        zpositions = torch.zeros((n_ds,), dtype=torch.float32)\n        zpositions[0] = dicom_get_zposition(ds_l[0])\n        \n        for i, ds in enumerate(ds_l[1:], start=1):\n            volume[i], ds_metadata_l[i] = dicom_split_array_from_metadata(ds)\n            zpositions[i] = dicom_get_zposition(ds)\n\n\t\t# sort slices in the volume\n        zpositions_argsort = torch.argsort(zpositions)\n        volume = volume[zpositions_argsort]\n\t\n    # rescale volume\n    slope, intercept = dicom_get_rescale_factors(ds_l[0])\n    volume = volume * slope + intercept\n    \n    return volume, ds_metadata_l\n\n\ndef dicom_get_spacing(ds):\n\n    pixel_spacing = getattr(ds, \"PixelSpacing\", None)\n    slice_thickness = getattr(ds, \"SliceThickness\", None)\n\n    if (pixel_spacing is None) or (slice_thickness is None):\n        shared_functional_groups_sequence = getattr(ds, \"SharedFunctionalGroupsSequence\", None)\n        if shared_functional_groups_sequence is not None:\n            pixel_measures_sequence = getattr(shared_functional_groups_sequence[0], \"PixelMeasuresSequence\", None)\n            if pixel_measures_sequence is not None:\n                if pixel_spacing is None:\n                    pixel_spacing = getattr(pixel_measures_sequence[0], \"PixelSpacing\", None)\n                if slice_thickness is None:\n                    slice_thickness = getattr(pixel_measures_sequence[0], \"SliceThickness\", None)\n    \n    if pixel_spacing is None:\n        pixel_spacing = [0.0, 0.0]\n    else:\n        pixel_spacing = [float(axis_spacing) for axis_spacing in pixel_spacing]\n    \n    if slice_thickness is None:\n        slice_thickness = 0.0\n    else:\n        slice_thickness = float(slice_thickness)\n    \n    spacing = [*pixel_spacing, slice_thickness]\n\n    return spacing\n\n\ndef dicom_serie_get_spacing(ds_l):\n\n    spacings = torch.zeros((len(ds_l), 3), requires_grad=False)\n    for i, ds in enumerate(ds_l):\n        spacings[i] = torch.tensor(dicom_get_spacing(ds))\n\n    return spacings.mode(dim=0).values","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-14T20:43:35.181956Z","iopub.execute_input":"2025-10-14T20:43:35.182671Z","iopub.status.idle":"2025-10-14T20:43:40.006098Z","shell.execute_reply.started":"2025-10-14T20:43:35.18264Z","shell.execute_reply":"2025-10-14T20:43:40.005336Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Transforms","metadata":{}},{"cell_type":"code","source":"import torch.nn.functional as F\n\nclass NormalizeSpacing:\n\n    def __init__(self, interp_mode, domain_spacings_dict, get_metadata):\n        self.interp_mode = interp_mode\n        self.domain_spacings_dict = domain_spacings_dict\n        self.get_metadata = get_metadata        \n\n    def transform(self, volume, domain, spacing):\n\n        volume = volume.to(torch.float32)\n\n        if domain in self.domain_spacings_dict:\n            domain_spacing = torch.tensor(self.domain_spacings_dict[domain])\n        else:\n            domain_spacing = spacing\n        \n        target_size = (torch.tensor(volume.shape[2:], dtype=torch.float32) / spacing * domain_spacing).to(torch.int32)\n        volume = F.interpolate(volume, size=tuple(target_size), mode=self.interp_mode)\n    \n        return volume\n\n\nclass NormalizeSizeInterp:\n\n    get_metadata = None\n    \n    def __init__(self, target_size, mode):\n        self.target_size = target_size\n        self.mode = mode    \n\n    def transform(self, volume):\n    \n        volume = volume.to(torch.float32)\n        \n        volume = F.interpolate(volume, size=self.target_size, mode=self.mode)\n        return volume\n\nimport numpy as np\n\nclass PercentileCropIntensity:\n\n    get_metadata = None\n\n    def __init__(self, percentiles):\n        self.percentiles = percentiles\n    \n    def transform(self, volume):\n        percentiles = np.percentile(volume.flatten().detach().cpu().numpy(), self.percentiles)\n        volume = torch.clamp(volume, min=percentiles[0], max=percentiles[1])\n        return volume\n\n\nclass StandardizeIntensity:\n\n    get_metadata = None\n    \n    def transform(self, volume):\n\n        volume = volume.to(torch.float32)\n    \n        volume = (volume - volume.mean()) / volume.std()\n        \n        return volume","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-14T20:43:40.006917Z","iopub.execute_input":"2025-10-14T20:43:40.007349Z","iopub.status.idle":"2025-10-14T20:43:40.0158Z","shell.execute_reply.started":"2025-10-14T20:43:40.007321Z","shell.execute_reply":"2025-10-14T20:43:40.015094Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Model","metadata":{}},{"cell_type":"code","source":"from torch import nn\nfrom torch.nn import functional as F\n\n# -------------------- Copied from https://github.com/Warvito/MedicalNet-models/blob/main/medicalnet_models/models/resnet.py\ndef conv3x3x3(in_planes: int, out_planes: int, stride: int = 1, dilation: int = 1) -> nn.Conv3d:\n    \"\"\"3x3x3 convolution with padding\"\"\"\n    return nn.Conv3d(\n        in_planes,\n        out_planes,\n        kernel_size=3,\n        dilation=dilation,\n        stride=stride,\n        padding=dilation,\n        bias=False,\n    )\n\n\ndef conv1x1x1(in_planes: int, out_planes: int, stride: int = 1) -> nn.Conv3d:\n    \"\"\"1x1x1 convolution\"\"\"\n    return nn.Conv3d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)\n\n\nclass BasicBlock(nn.Module):\n    expansion: int = 1\n\n    def __init__(\n        self,\n        inplanes: int,\n        planes: int,\n        stride: int = 1,\n        downsample: nn.Module = None,\n        dilation: int = 1,\n    ) -> None:\n        super().__init__()\n        self.conv1 = conv3x3x3(inplanes, planes, stride=stride, dilation=dilation)\n        self.bn1 = nn.BatchNorm3d(planes)\n        self.relu = nn.ReLU(inplace=True)\n        self.conv2 = conv3x3x3(planes, planes, dilation=dilation)\n        self.bn2 = nn.BatchNorm3d(planes)\n        self.downsample = downsample\n        self.stride = stride\n        self.dilation = dilation\n\n    def forward(self, x):\n        residual = x\n\n        out = self.conv1(x)\n        out = self.bn1(out)\n        out = self.relu(out)\n        out = self.conv2(out)\n        out = self.bn2(out)\n\n        if self.downsample is not None:\n            residual = self.downsample(x)\n\n        out += residual\n        out = self.relu(out)\n\n        return out\n\nclass Bottleneck(nn.Module):\n    expansion: int = 4\n\n    def __init__(\n        self,\n        inplanes: int,\n        planes: int,\n        stride: int = 1,\n        downsample: nn.Module = None,\n        dilation: int = 1,\n    ) -> None:\n        super().__init__()\n        self.conv1 = conv1x1x1(inplanes, planes)\n        self.bn1 = nn.BatchNorm3d(planes)\n        self.conv2 = conv3x3x3(\n            planes,\n            planes,\n            stride=stride,\n            dilation=dilation,\n        )\n        self.bn2 = nn.BatchNorm3d(planes)\n        self.conv3 = conv1x1x1(planes, planes * 4)\n        self.bn3 = nn.BatchNorm3d(planes * 4)\n        self.relu = nn.ReLU(inplace=True)\n        self.downsample = downsample\n        self.stride = stride\n        self.dilation = dilation\n\n    def forward(self, x):\n        residual = x\n\n        out = self.conv1(x)\n        out = self.bn1(out)\n        out = self.relu(out)\n\n        out = self.conv2(out)\n        out = self.bn2(out)\n        out = self.relu(out)\n\n        out = self.conv3(out)\n        out = self.bn3(out)\n\n        if self.downsample is not None:\n            residual = self.downsample(x)\n\n        out += residual\n        out = self.relu(out)\n\n        return out\n\nclass ResNet(nn.Module):\n    def __init__(\n        self,\n        block,\n        layers,\n    ) -> None:\n        super().__init__()\n\n        self.inplanes = 64\n        self.layers = layers\n\n        self.conv1 = nn.Conv3d(1, self.inplanes, kernel_size=7, stride=2, padding=3, bias=False)\n        self.bn1 = nn.BatchNorm3d(self.inplanes)\n        self.relu = nn.ReLU(inplace=True)\n        self.maxpool = nn.MaxPool3d(kernel_size=3, stride=2, padding=1)\n        self.layer1 = self._make_layer(block, 64, layers[0])\n        self.layer2 = self._make_layer(block, 128, layers[1], stride=2)\n        self.layer3 = self._make_layer(block, 256, layers[2], stride=1, dilation=2)\n        self.layer4 = self._make_layer(block, 512, layers[3], stride=1, dilation=4)\n\n    def _make_layer(\n        self, block, planes: int, blocks: int, stride: int = 1, dilation: int = 1\n    ):\n        downsample = None\n        if stride != 1 or self.inplanes != planes * block.expansion:\n            downsample = nn.Sequential(\n                conv1x1x1(\n                    self.inplanes,\n                    planes * block.expansion,\n                    stride=stride,\n                ),\n                nn.BatchNorm3d(planes * block.expansion),\n            )\n\n        layers = []\n        layers.append(\n            block(\n                self.inplanes,\n                planes,\n                stride=stride,\n                dilation=dilation,\n                downsample=downsample,\n            )\n        )\n        self.inplanes = planes * block.expansion\n        for i in range(1, blocks):\n            layers.append(block(self.inplanes, planes, dilation=dilation))\n\n        return nn.Sequential(*layers)\n\n    def forward(self, x):\n        x = self.conv1(x)\n        x = self.bn1(x)\n        x = self.relu(x)\n        x = self.maxpool(x)\n\n        x = self.layer1(x)\n        x = self.layer2(x)\n        x = self.layer3(x)\n        x = self.layer4(x)\n\n        return x\n# --------------------\n\nclass MedResNet(nn.Module):\n\n\n    def __init__(self, out_features):\n        super().__init__()\n        self.pretrained = ResNet(Bottleneck, [3, 4, 6, 3])\n        self.global_avg_pooling = nn.AdaptiveAvgPool3d((1, 1, 1))\n        self.linear = nn.Linear(2048, out_features, bias=False)\n        self.linear.weight = nn.init.xavier_normal_(self.linear.weight)\n\n    def forward(self, x):\n        x = self.pretrained.forward(x)\n        x = self.global_avg_pooling(x)\n        x = x.squeeze(dim=(2,3,4))\n        x = self.linear(x)\n        if not self.training:  # training uses BCEWithLogitsLoss\n            x = F.sigmoid(x)\n\n        return x","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-14T20:43:40.016786Z","iopub.execute_input":"2025-10-14T20:43:40.017047Z","iopub.status.idle":"2025-10-14T20:43:40.047869Z","shell.execute_reply.started":"2025-10-14T20:43:40.017024Z","shell.execute_reply":"2025-10-14T20:43:40.047116Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Inference","metadata":{}},{"cell_type":"code","source":"# Constants\n\n# -- Transforms\nvolume_domain_median_spacing_dict = {\n    \"CT\": (0.46875, 0.46875, 0.8),\n    \"MR\": (0.410156, 0.410156, 0.6),\n}\n\ndef get_metadata_dicom(ds_metadata_l):\n    modality = ds_metadata_l[0].Modality\n    spacing = dicom_serie_get_spacing(ds_metadata_l)\n    return modality, spacing\n\ntransforms = [\n    NormalizeSpacing(\"trilinear\", volume_domain_median_spacing_dict, get_metadata_dicom),\n    PercentileCropIntensity(percentiles=(0.5, 99.5)),\n    StandardizeIntensity(), \n    NormalizeSizeInterp((32, 224, 224), \"nearest\")\n]\n\n# -- Device\nif torch.cuda.is_available():\n    device = \"cuda\"\nelse:\n    device = \"cpu\"\n\n# -- Model\nmodel_resnet50 = MedResNet(14)\nmodel_resnet50.load_state_dict(torch.load(\"/kaggle/input/med3d-resnet50-rsna-iad/pytorch/default/1/resnet50.pth\", weights_only=True))\nmodel_resnet50.eval()\nmodel_resnet50 = model_resnet50.to(device)\n\ndef _predict(series_path: str):\n    \n    # -- Load and Transform\n    ds_l = dicom_serie_load(series_path)\n    volume, ds_metadata_l = dicom_serie_process(ds_l)\n\n    ndim = len(volume.shape)\n    if ndim < 5:\n        for _ in range(5 - ndim):\n            volume = volume.unsqueeze(0)\n    \n    for transform in transforms:\n        if transform.get_metadata is not None:\n            transform_metadata = transform.get_metadata(ds_metadata_l)\n            volume = transform.transform(volume, *transform_metadata)\n        else:\n            volume = transform.transform(volume)\n    \n    # -- Predict\n    with torch.no_grad():\n        volume = volume.to(device)\n        volume = volume.nan_to_num(posinf=0.0, neginf=0.0)\n        scores = model_resnet50.forward(volume)\n        \n        if (scores[:, :-1] > 0.5).any().item():\n            scores[:, -1] = 1.0\n        else:\n            scores[:, -1] = 0.0\n\n    # -- Make polars DataFrame\n    series_id = os.path.basename(series_path)\n    predictions = pl.DataFrame(\n        data=[[series_id] + scores[0].tolist()],\n        schema=[ID_COL, *LABEL_COLS],\n        orient='row',\n    )\n\n    return predictions","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-14T20:43:40.048724Z","iopub.execute_input":"2025-10-14T20:43:40.049007Z","iopub.status.idle":"2025-10-14T20:43:42.213498Z","shell.execute_reply.started":"2025-10-14T20:43:40.048982Z","shell.execute_reply":"2025-10-14T20:43:42.212945Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def _fallback(series_path: str):\n\n    series_id = os.path.basename(series_path)\n    predictions = pl.DataFrame(\n        data=[[series_id] + [0.5] * len(LABEL_COLS)],\n        schema=[ID_COL, *LABEL_COLS],\n        orient='row',\n    )\n\n    return predictions","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-14T20:43:42.214219Z","iopub.execute_input":"2025-10-14T20:43:42.214457Z","iopub.status.idle":"2025-10-14T20:43:42.218655Z","shell.execute_reply.started":"2025-10-14T20:43:42.214431Z","shell.execute_reply":"2025-10-14T20:43:42.217934Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 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:\n    \"\"\"Make a prediction.\"\"\"\n    \n    try:\n        predictions = _predict(series_path)\n    except Exception:\n        predictions = _fallback(series_path)\n\n    if isinstance(predictions, pl.DataFrame):\n        assert predictions.columns == [ID_COL, *LABEL_COLS]\n    elif isinstance(predictions, pd.DataFrame):\n        assert (predictions.columns == [ID_COL, *LABEL_COLS]).all()\n    else:\n        raise TypeError('The predict function must return a DataFrame')\n\n    # ----------------------------- IMPORTANT ------------------------------\n    # You MUST have the following code in your `predict` function\n    # to prevent \"out of disk space\" errors. This is a temporary workaround\n    # as we implement improvements to our evaluation system.\n    shutil.rmtree('/kaggle/shared', ignore_errors=True)\n    # ----------------------------------------------------------------------\n    \n    return predictions.drop(ID_COL)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-14T20:43:42.219338Z","iopub.execute_input":"2025-10-14T20:43:42.219626Z","iopub.status.idle":"2025-10-14T20:43:42.241515Z","shell.execute_reply.started":"2025-10-14T20:43:42.2196Z","shell.execute_reply":"2025-10-14T20:43:42.240949Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Small test\n#predict(\"/kaggle/input/rsna-intracranial-aneurysm-detection/series/1.2.826.0.1.3680043.8.498.10012790035410518400400834395242853657\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-14T20:43:42.243281Z","iopub.execute_input":"2025-10-14T20:43:42.243668Z","iopub.status.idle":"2025-10-14T20:43:42.255677Z","shell.execute_reply.started":"2025-10-14T20:43:42.243649Z","shell.execute_reply":"2025-10-14T20:43:42.254959Z"}},"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-10-14T20:43:42.256552Z","iopub.execute_input":"2025-10-14T20:43:42.256781Z","iopub.status.idle":"2025-10-14T20:44:03.374076Z","shell.execute_reply.started":"2025-10-14T20:43:42.256758Z","shell.execute_reply":"2025-10-14T20:44:03.373479Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}