{"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":"gpu","dataSources":[{"sourceId":99552,"databundleVersionId":13851420,"sourceType":"competition"},{"sourceId":4125181,"sourceType":"datasetVersion","datasetId":2437947},{"sourceId":5824224,"sourceType":"datasetVersion","datasetId":3347112},{"sourceId":6109993,"sourceType":"datasetVersion","datasetId":2437951},{"sourceId":13322423,"sourceType":"datasetVersion","datasetId":8427848},{"sourceId":261515368,"sourceType":"kernelVersion"},{"sourceId":262889198,"sourceType":"kernelVersion"},{"sourceId":263100519,"sourceType":"kernelVersion"},{"sourceId":264523006,"sourceType":"kernelVersion"},{"sourceId":264523029,"sourceType":"kernelVersion"},{"sourceId":265978919,"sourceType":"kernelVersion"},{"sourceId":267019335,"sourceType":"kernelVersion"},{"sourceId":267019367,"sourceType":"kernelVersion"},{"sourceId":267198042,"sourceType":"kernelVersion"},{"sourceId":267198958,"sourceType":"kernelVersion"}],"dockerImageVersionId":31154,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport timm\nimport shutil\nfrom collections import defaultdict\n\nimport pandas as pd\nimport polars as pl\nimport pydicom\nimport torch\n\nimport numpy as np\nimport torch.nn as nn\nimport matplotlib.pyplot as plt\nimport torchvision.transforms as transforms\n\nfrom tqdm import tqdm\nfrom torch.utils.data import Dataset, DataLoader\n\nimport kaggle_evaluation.rsna_inference_server\n\nimport sys\nsys.path.extend([\n    \"../input/pretrained-models-pytorch\",\n    \"../input/efficientnet-pytorch\",\n    \"../input/smp-github/segmentation_models.pytorch-master\"\n])\nimport segmentation_models_pytorch as smp\n\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-12T11:01:52.048961Z","iopub.execute_input":"2025-10-12T11:01:52.049531Z","iopub.status.idle":"2025-10-12T11:02:05.842401Z","shell.execute_reply.started":"2025-10-12T11:01:52.049507Z","shell.execute_reply":"2025-10-12T11:02:05.84179Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"BS = 8      # 2D scan batch size\nSSIZE = 512 # Slice resizing\nWSIZE = 256 # Slice window size\nCSIZE = 128 # Slice core size\nRADIUS = 16 # Expected aneurysm radius","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-12T11:02:05.843482Z","iopub.execute_input":"2025-10-12T11:02:05.843894Z","iopub.status.idle":"2025-10-12T11:02:05.847558Z","shell.execute_reply.started":"2025-10-12T11:02:05.843875Z","shell.execute_reply":"2025-10-12T11:02:05.846827Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ID_COL = 'SeriesInstanceUID'\n\nLABEL_COLS = [\n    'Other Posterior Circulation',\n    'Basilar Tip',\n    'Right Posterior Communicating Artery',\n    'Left Posterior Communicating Artery',\n    'Right Infraclinoid Internal Carotid Artery',\n    'Left Infraclinoid Internal Carotid Artery',\n    'Right Supraclinoid Internal Carotid Artery',\n    'Left Supraclinoid Internal Carotid Artery',\n    'Right Middle Cerebral Artery',\n    'Left Middle Cerebral Artery',\n    'Right Anterior Cerebral Artery',\n    'Left Anterior Cerebral Artery',\n    'Anterior Communicating Artery',\n    'Aneurysm Present'\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-12T11:02:05.8483Z","iopub.execute_input":"2025-10-12T11:02:05.848516Z","iopub.status.idle":"2025-10-12T11:02:05.89178Z","shell.execute_reply.started":"2025-10-12T11:02:05.84848Z","shell.execute_reply":"2025-10-12T11:02:05.891119Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train = pd.read_csv('/kaggle/input/rsna-intracranial-aneurysm-detection/train.csv')\nobserved_ratios = train[LABEL_COLS].values.sum(0) / len(train)\nobserved_ratios = observed_ratios.tolist()\nobserved_ratios","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-12T11:02:05.892387Z","iopub.execute_input":"2025-10-12T11:02:05.892546Z","iopub.status.idle":"2025-10-12T11:02:05.94066Z","shell.execute_reply.started":"2025-10-12T11:02:05.892532Z","shell.execute_reply":"2025-10-12T11:02:05.940139Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class dcm_reading_ds(Dataset):\n    def __init__(\n        self,\n        series_path\n    ):\n        \"\"\"\n        \n        \"\"\"\n        self.paths = [series_path + f for f in os.listdir(series_path)]\n\n    def __len__(self):\n        return len(self.paths)\n\n    def __getitem__(self, idx):  \n        dcm = pydicom.dcmread(self.paths[idx])\n        img = dcm.pixel_array\n        shp = img.shape\n        if len(shp) == 2:\n            try:\n                ipp = dcm.ImagePositionPatient\n            except:\n                ipp = [dcm.InstanceNumber]*3\n            try:\n                iop = dcm.ImageOrientationPatient\n            except:\n                iop = [1, 0, 0, 0, 1, 0] # Axial IOP\n            try:\n                ps = dcm.PixelSpacing\n            except:\n                ps = 1\n            try:\n                st = dcm.SliceThickness\n            except:\n                st = np.mean(ps)\n\n        else:\n            try:\n                iop = np.array(dcm.SharedFunctionalGroupsSequence[0].PlaneOrientationSequence[0].ImageOrientationPatient)\n            except:\n                iop = [1, 0, 0, 0, 1, 0] # Axial IOP\n            ipp = []\n            try:\n                ps = np.array(dcm.SharedFunctionalGroupsSequence[0].PixelMeasuresSequence[0].PixelSpacing).mean()\n            except:\n                ps = 1\n            try:\n                st = np.array(dcm.SharedFunctionalGroupsSequence[0].PixelMeasuresSequence[0].SliceThickness)\n            except:\n                st = np.mean(ps)\n        \n        return {\n            'img':img,\n            'shp':shp,\n            'iop':iop,\n            'ipp':ipp,\n            'ps':ps,\n            'st':st\n        }\n\ndef list_collate(batch):\n    \"\"\"\n    Collate function that returns lists of values instead of trying to stack them.\n    \"\"\"\n    if isinstance(batch[0], dict):\n        result = {}\n        for key in batch[0].keys():\n            result[key] = [item[key] for item in batch]\n        return result\n    else:\n        return list(batch)\n\ndef seg3D(v,axis, models):\n#   Reorientation to axial perspective\n    if axis == 0: v = torch.rot90(torch.rot90(v,1,(-3,-2)),-1,(-2,-1))\n    if axis == 1: v = torch.rot90(v,1,(-3,-2))\n    with torch.no_grad():\n        out = 0\n        for model in models:\n            out += model(v).softmax(1)\n#           Flip TTA needs left-rigth labels remapping\n            out += model(v.flip(-1)).flip(-1).softmax(1)[:,[0,1,2,4,3,6,5,8,7,10,9,12,11,13]]\n#   Reorientation to original perspective\n    if axis == 0: out = torch.rot90(torch.rot90(out,1,(-2,-1)),-1,(-3,-2))\n    if axis == 1: out = torch.rot90(out,-1,(-3,-2))\n    \n    return out.argmax(1)[0]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-12T11:02:05.942057Z","iopub.execute_input":"2025-10-12T11:02:05.942314Z","iopub.status.idle":"2025-10-12T11:02:05.952477Z","shell.execute_reply.started":"2025-10-12T11:02:05.942299Z","shell.execute_reply":"2025-10-12T11:02:05.95178Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def convert_2d_to_3d(model, is_top_level=True):\n    for name, module in model.named_children():\n        # Recursively convert child modules first\n        convert_2d_to_3d(module, False)\n\n        # Replace Conv2d with Conv3d\n        if isinstance(module, nn.Conv2d):\n            # Handle cases where kernel_size/stride/padding are ints (not tuples)\n            kernel_size = module.kernel_size[0]\n            stride = module.stride[0]\n            padding = module.padding[0]\n\n            # New Conv3d layer with expanded kernel\n            new_conv = nn.Conv3d(\n                in_channels=module.in_channels,\n                out_channels=module.out_channels,\n                kernel_size=kernel_size,\n                stride=stride,\n                padding=padding,\n                bias=False\n            )\n            \n            # Initialize weights: tile 2D weights along depth and average\n            weight_2d = module.weight.data\n            weight_3d = weight_2d.unsqueeze(2).repeat(1, 1, kernel_size, 1, 1) / kernel_size\n            new_conv.weight.data = weight_3d\n            \n            setattr(model, name, new_conv)\n\n        # Replace BatchNorm2d with BatchNorm3d\n        elif isinstance(module, nn.BatchNorm2d):\n            new_bn = nn.BatchNorm3d(\n                num_features=module.num_features,\n                eps=module.eps,\n                momentum=module.momentum,\n                affine=module.affine,\n                track_running_stats=module.track_running_stats\n            ).to(device)\n            # Copy existing parameters\n            new_bn.load_state_dict(module.state_dict())\n            setattr(model, name, new_bn)\n\n        # Replace MaxPool2d with MaxPool3d (anisotropic)\n        elif isinstance(module, nn.MaxPool2d):\n            # Handle int vs. tuple for kernel_size, stride, padding\n            kernel_size = module.kernel_size\n            stride = module.stride\n            padding = module.padding\n\n            new_pool = nn.MaxPool3d(\n                kernel_size=kernel_size,\n                stride=stride,\n                padding=padding,\n                dilation=1,\n                ceil_mode=False\n            )\n            setattr(model, name, new_pool)\n\n    if is_top_level and hasattr(model, 'segmentation_head'):\n        old_weight = model.segmentation_head[0].weight.data\n        new_weight = torch.cat([\n            old_weight[0:1],  # Class 0 (foreground, unchanged)\n            old_weight[1:2].repeat(13, 1, *[1] * (old_weight.dim() - 2))  # Repeat Class 1 for 13 new positives\n        ], dim=0)\n        model.segmentation_head[0] = nn.Conv3d(\n            16,\n            14,\n            kernel_size=3,\n            stride=1,\n            padding=1,\n            bias=False\n        )\n        model.segmentation_head[0].weight.data = new_weight\n\n    return model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-12T11:02:05.953097Z","iopub.execute_input":"2025-10-12T11:02:05.953261Z","iopub.status.idle":"2025-10-12T11:02:05.969965Z","shell.execute_reply.started":"2025-10-12T11:02:05.953246Z","shell.execute_reply":"2025-10-12T11:02:05.969334Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"resize = transforms.Resize((SSIZE,SSIZE), interpolation=transforms.InterpolationMode.BILINEAR).to(device)\n\nmodel2D = smp.Unet(\n        encoder_name=\"resnet18\",\n        encoder_weights=None,\n        in_channels=1,\n        classes=2\n).to(device)\nmodel2D.load_state_dict(torch.load(f'/kaggle/input/rsna-2d-binary-segmentation-training-{0}/best_model_{0}.pth'))\nmodel2D.eval()\n\nmodels3D = []\nfor fold in [0]:#[0,1,2,3]:\n    model = convert_2d_to_3d(smp.Unet(\n        encoder_name=\"resnet18\",\n        encoder_weights=None,\n        in_channels=1,\n        classes=2\n    )).to(device)\n    model.load_state_dict(torch.load(f'/kaggle/input/rsna-from-2d-binary-to-3d-full-segmentation-{fold}/best_3d_model_{fold}.pth'))\n    model.eval()\n    models3D.append(model)\nmodel = convert_2d_to_3d(smp.Unet(\n    encoder_name=\"resnet18\",\n    encoder_weights=None,\n    in_channels=1,\n    classes=2\n)).to(device)\nmodel.load_state_dict(torch.load('/kaggle/input/rsna-models/best_3d_model_4.pth'))\nmodel.eval()\nmodels3D.append(model)\n\npredictors = []\nfor fold in [0,1,2,3,4]:#[0,1,2,3,4]:\n    model = smp.Unet(\n        encoder_name=\"resnet18\",\n        encoder_weights=None,\n        in_channels=1,\n        classes=2\n    ).to(device)\n    model = convert_2d_to_3d(model)\n    model.load_state_dict(torch.load(f'/kaggle/input/rsna-aneurysm-3d-full-segmentation-{fold}/best_aneurysm_3d_model_{fold}.pth'))\n    model.eval()\n    predictors.append(model)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-12T11:02:05.970548Z","iopub.execute_input":"2025-10-12T11:02:05.970726Z","iopub.status.idle":"2025-10-12T11:02:23.196452Z","shell.execute_reply.started":"2025-10-12T11:02:05.970712Z","shell.execute_reply":"2025-10-12T11:02:23.195655Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict(series_path: str) -> pl.DataFrame | pd.DataFrame:\n    \"\"\"Make a prediction.\"\"\"\n    ds = dcm_reading_ds(series_path + '/')\n    dl = DataLoader(\n        ds,\n        batch_size=1,\n        num_workers=4,\n        collate_fn=list_collate\n    )\n    images = []\n    shapes = []\n    ipp = []\n    iop = []\n    ps = []\n    st = []\n    for d in dl:\n        images = images + d['img']\n        shapes = shapes + d['shp']\n        ipp = ipp + d['ipp']\n        iop = iop + d['iop']\n        ps = ps + d['ps']\n        st = st + d['st']\n\n    if len(images) > 1:\n        shape_counts = {}\n        for s in shapes:\n            try:\n                shape_counts[s] += 1\n            except:\n                shape_counts[s] = 1\n\n        all_shapes = list(shape_counts.keys())\n        cmax = 0\n        smax = all_shapes[0]\n        for s in all_shapes[1:]:\n            if shape_counts[s] > cmax:\n                smax = s\n                cmax = shape_counts[s]\n\n        images = [images[k] for k in range(len(shapes)) if shapes[k] == smax]\n        ipp = [ipp[k] for k in range(len(shapes)) if shapes[k] == smax]\n        iop = [iop[k] for k in range(len(shapes)) if shapes[k] == smax]\n        iop = np.mean(iop,0)\n        n_vec = np.cross(iop[:3],iop[3:])\n        axis = np.argmax(abs(n_vec))\n        sign = np.sign(n_vec[axis])\n        depth = (np.array(ipp)*n_vec).sum(1)\n        images = [images[k] for k in np.argsort(depth)]\n        volume = np.stack(images)        \n        ps = np.mean([ps[k] for k in range(len(shapes)) if shapes[k] == smax])\n        st = np.mean([st[k] for k in range(len(shapes)) if shapes[k] == smax])\n\n    else:\n        iop = np.mean(iop,0)\n        n_vec = np.cross(iop[:3],iop[3:])\n        axis = np.argmax(abs(n_vec))\n        sign = np.sign(n_vec[axis])\n        volume = images[0]\n        ps = ps[0]\n        st = st[0]\n\n    pmin,pmax = np.percentile(volume,(1,99))\n    D,H,W = volume.shape\n    s = min(H,W)\n    h_ = (H - s)//2\n    w_ = (W - s)//2\n    ZYX = []\n    resized_vol = []\n    STEPS = D//BS\n    if D%BS: STEPS += 1\n    with torch.no_grad():\n#       First 2D binary segmentation depth scan\n        for k in range(STEPS):\n            v = torch.from_numpy(volume[\n                BS*k:BS*(k+1),\n                h_:h_+s,\n                w_:w_+s\n            ].astype(np.float32)).to(device)\n            v = (v - pmin) / (pmax - pmin)\n            v = resize(v.view(-1,1,s,s))\n            resized_vol.append(v)\n            ZYX.append(model2D(v[::2]).argmax(1))\n#       3D reconstructed binary mask\n        ZYX = torch.cat(ZYX)\n#       First ROI estimation\n        _,h,w = torch.where(ZYX)\n        if len(h) > 0:\n#           Center on centroid mask\n            h0 = min(max(0,np.rint(h.float().mean().cpu()).long().item() - WSIZE//2),SSIZE-WSIZE)\n            w0 = min(max(0,np.rint(w.float().mean().cpu()).long().item() - WSIZE//2),SSIZE-WSIZE)\n        else:\n#           Center on slice\n            h0 = w0 = (SSIZE - WSIZE)//2\n#       3D reconstructed resized volume window\n        sf = SSIZE/s\n        resized_vol = torch.nn.functional.interpolate(\n            torch.concat(resized_vol)[...,h0:h0+WSIZE,w0:w0+WSIZE].view(1,1,-1,WSIZE,WSIZE),\n            scale_factor=(sf,1,1),\n            mode='trilinear'\n        )[0,0]\n        DD = resized_vol.size(0)\n#       Core mask extraction\n        zyx = ZYX[\n            :,\n            h0+(WSIZE-CSIZE)//2:h0+(WSIZE-CSIZE)//2+CSIZE,\n            w0+(WSIZE-CSIZE)//2:w0+(WSIZE-CSIZE)//2+CSIZE\n        ]\n        d = torch.where(zyx.sum((1,2)) > 0)[0]\n        if len(d) > 0:\n#           2D ROI checkpoint\n            d_ = np.rint(2*d.float().mean().cpu()).long().item()\n            d0 = 2*d.min().item()\n            d = 2*(d.max().item() + 1)\n            dd = d - d0\n        else:\n            d_ = DD/2\n            d0 = 0\n            d = dd = DD\n        d_ = np.rint(d_ - WSIZE/2).astype(int)\n#       Second 3D full general scan\n        pad_d = dd%CSIZE\n        if pad_d > 0:\n            pad = CSIZE - pad_d\n            pad_d0 = pad_d//2\n            pad_d = pad - pad_d0\n            d0 -= pad_d0\n            d += pad_d\n            pad_d0 = max(0, -d0)\n            pad_d = max(0, d - DD)\n            d0 = max(0, d0)\n            d = min(d,DD)\n        else:\n            pad_d0 = pad_d = 0\n        rv = torch.nn.functional.pad(\n            resized_vol[d0:d],\n            (0,0,0,0,pad_d0,pad_d)\n        ).unsqueeze(0).unsqueeze(0)\n        N = rv.size(2)\n        d_unique = torch.zeros(N,13).to(device)\n        STEPS = N//CSIZE\n        for k in range(STEPS):\n#           3D segmentation with flip TTA\n            out = seg3D(rv[:,:,k*CSIZE:(k+1)*CSIZE],axis,models3D[:1])\n            for kk in range(1,14):\n                d_unique[k*CSIZE:(k+1)*CSIZE,kk-1][torch.where(out == kk)[0]] = 1\n#       Second ROI estimation\n        d_unique_counts = torch.tensor([(d_unique[k:k+CSIZE].sum(0) > 0).sum(0).item() for k in range(N-CSIZE+1)])\n        if d_unique_counts.sum() > 0: d_ = d_unique_counts.argmax().item() + CSIZE//2 + d0 - pad_d0 - WSIZE//2\n#       Last 3D full focused scan\n        pad_before = max(0, -d_)\n        pad_after = max(0, d_ + WSIZE - DD)\n        d_ = max(0, d_)\n        rv = torch.nn.functional.pad(resized_vol[\n            d_:d_ + WSIZE - pad_before\n        ],(0, 0, 0, 0, pad_before, pad_after)).unsqueeze(0).unsqueeze(0)\n#       3D segmentation with flip TTA\n        out = seg3D(rv,axis,models3D[:1]) > 0\n#       Almost Definitive ROI estimation\n        z,y,x = torch.where(out)\n        o = observed_ratios\n        if len(z) > 0:\n            d0 = z.min().item() - RADIUS\n            h0 = y.min().item() - RADIUS\n            w0 = x.min().item() - RADIUS\n            \n            d = z.max().item() + RADIUS + 1\n            h = y.max().item() + RADIUS + 1\n            w = x.max().item() + RADIUS + 1\n            \n            dd = d - d0\n            hh = h - h0\n            ww = w - w0\n            \n            pad_d = 32 - dd%32\n            pad_h = 32 - hh%32\n            pad_w = 32 - ww%32\n            \n            pad_d += max(0,CSIZE - dd - pad_d)\n            pad_h += max(0,CSIZE - hh - pad_h)\n            pad_w += max(0,CSIZE - ww - pad_w)\n            \n            pad_d0 = pad_d//2\n            pad_h0 = pad_h//2\n            pad_w0 = pad_w//2\n            \n            pad_d -= pad_d0\n            pad_h -= pad_h0\n            pad_w -= pad_w0\n            \n            d0 -= pad_d0\n            h0 -= pad_h0\n            w0 -= pad_w0\n            \n            d += pad_d\n            h += pad_h\n            w += pad_w\n\n            d0 += d_ - pad_before\n            d += d_ - pad_before\n            \n            pad_d0 = max(0,-d0)\n            pad_h0 = max(0,-h0)\n            pad_w0 = max(0,-w0)\n            \n            pad_d = max(0,d - DD)\n            pad_h = max(0,h - WSIZE)\n            pad_w = max(0,w - WSIZE)\n            \n            d0 = max(0,d0)\n            h0 = max(0,h0)\n            w0 = max(0,w0)\n            \n            d = min(d,DD)\n            h = min(h,WSIZE)\n            w = min(w,WSIZE)\n            \n            v = resized_vol[d0:d,h0:h,w0:w]\n            v = torch.nn.functional.pad(\n                v,\n                (\n                    pad_w0,pad_w,\n                    pad_h0,pad_h,\n                    pad_d0,pad_d\n                )\n            ).unsqueeze(0).unsqueeze(0)\n            seg_mask = seg3D(v,axis,models3D)\n#           Reorientation to axial perspective\n            if axis == 0:\n                v = torch.rot90(torch.rot90(v,1,(-3,-2)),-1,(-2,-1))\n                seg_mask = torch.rot90(torch.rot90(seg_mask,1,(-3,-2)),-1,(-2,-1))\n            if axis == 1:\n                v = torch.rot90(v,1,(-3,-2))\n                seg_mask = torch.rot90(seg_mask,1,(-3,-2))\n            mask = seg_mask > 0\n            D,H,W = mask.shape\n#           I swear, Definitive ROI estimation\n            z,y,x = torch.where(mask)\n            if len(z) > 0:\n                d0 = max(0, z.min().item() - RADIUS)\n                h0 = max(0, y.min().item() - RADIUS)\n                w0 = max(0, x.min().item() - RADIUS)\n            \n                d = min(z.max().item() + RADIUS + 1,D)\n                h = min(y.max().item() + RADIUS + 1,H)\n                w = min(x.max().item() + RADIUS + 1,W)\n\n                dd = d - d0\n                hh = h - h0\n                ww = w - w0\n\n                if dd < CSIZE:\n                    pad_d = CSIZE - dd\n                    pad_d0 = pad_d//2\n                    pad_d -= pad_d0\n                    d0 -= pad_d0\n                    d += pad_d\n                    \n                    if d0 < 0:\n                        d -= d0\n                        d0 = 0\n\n                    if d > D:\n                        d0 -= d - D\n                        d = D\n\n                    trim_d0 = trim_d = 0\n                else:\n                    trim_d = dd%32\n                    trim_d0 = trim_d//2\n                    trim_d -= trim_d0\n\n                if hh < CSIZE:\n                    pad_h = CSIZE - hh\n                    pad_h0 = pad_h//2\n                    pad_h -= pad_h0\n                    h0 -= pad_h0\n                    h += pad_h\n\n                    if h0 < 0:\n                        h -= h0\n                        h0 = 0\n                    if h > H:\n                        h0 -= h - H\n                        h = H\n\n                    trim_h0 = trim_h = 0\n                else:\n                    trim_h = hh%32\n                    trim_h0 = trim_h//2\n                    trim_h -= trim_h0\n\n                if ww < CSIZE:\n                    pad_w = CSIZE - ww\n                    pad_w0 = pad_w//2\n                    pad_w -= pad_w0\n                    w0 -= pad_w0\n                    w += pad_w\n\n                    if w0 < 0:\n                        w -= w0\n                        w0 = 0\n                    if w > W:\n                        w0 -= w - W\n                        w = W\n\n                    trim_w0 = trim_w = 0\n                else:\n                    trim_w = ww%32\n                    trim_w0 = trim_w//2\n                    trim_w -= trim_w0\n\n                v = v[\n                    ...,\n                    d0 + trim_d0:d - trim_d,\n                    h0 + trim_h0:h - trim_h,\n                    w0 + trim_w0:w - trim_w\n                ]\n                out = 0\n                for model in predictors:\n                    out += model(v).softmax(1)\n                    out += model(v.flip(-1)).flip(-1).softmax(1)[:,[0,1,2,4,3,6,5,8,7,10,9,12,11,13]]\n                out = out[0] / (2*len(predictors))\n                o = out.view(14,-1)\n                o = o[1:].max(-1)[0].tolist() + [(1 - o[0].min()).item()]\n    \n    series_id = os.path.basename(series_path)\n    predictions = pl.DataFrame(\n        data=[[series_id] + o],\n        schema=[ID_COL, *LABEL_COLS],\n        orient='row',\n    )\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-12T11:02:23.197391Z","iopub.execute_input":"2025-10-12T11:02:23.197787Z","iopub.status.idle":"2025-10-12T11:02:23.233224Z","shell.execute_reply.started":"2025-10-12T11:02:23.197756Z","shell.execute_reply":"2025-10-12T11:02:23.23243Z"}},"outputs":[],"execution_count":null},{"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-12T11:02:23.234051Z","iopub.execute_input":"2025-10-12T11:02:23.23427Z","iopub.status.idle":"2025-10-12T11:03:00.32461Z","shell.execute_reply.started":"2025-10-12T11:02:23.234253Z","shell.execute_reply":"2025-10-12T11:03:00.323867Z"}},"outputs":[],"execution_count":null}]}