{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":99552,"databundleVersionId":13851420,"sourceType":"competition"},{"sourceId":9114148,"sourceType":"datasetVersion","datasetId":5501147},{"sourceId":13130191,"sourceType":"datasetVersion","datasetId":8009945},{"sourceId":13190707,"sourceType":"datasetVersion","datasetId":8299879},{"sourceId":13201731,"sourceType":"datasetVersion","datasetId":8325535},{"sourceId":13211505,"sourceType":"datasetVersion","datasetId":7994218},{"sourceId":227202990,"sourceType":"kernelVersion"},{"sourceId":595133,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":445521,"modelId":461989}],"dockerImageVersionId":31090,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"#!tar xfvz /kaggle/input/ultralytics-offlineinstall-yolo12-weights/archive.tar.gz\n#!pip install --no-index --find-links=./packages ultralytics\n#!rm -rf ./packages","metadata":{"_uuid":"8e9fa6b9-c8fb-475d-bcde-a037152b60ce","_cell_guid":"7d86274d-180b-4fa0-9e7b-c81b5d0f5ff2","trusted":true,"_kg_hide-output":true,"_kg_hide-input":true,"execution":{"iopub.status.busy":"2025-10-01T13:59:34.207001Z","iopub.execute_input":"2025-10-01T13:59:34.207698Z","iopub.status.idle":"2025-10-01T13:59:34.211462Z","shell.execute_reply.started":"2025-10-01T13:59:34.207673Z","shell.execute_reply":"2025-10-01T13:59:34.210597Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!cp -r /kaggle/input/timm-3d/* .","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T13:59:34.213929Z","iopub.execute_input":"2025-10-01T13:59:34.214165Z","iopub.status.idle":"2025-10-01T13:59:36.43567Z","shell.execute_reply.started":"2025-10-01T13:59:34.214144Z","shell.execute_reply":"2025-10-01T13:59:36.434874Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pydicom\nimport cv2\nfrom pathlib import Path\nfrom typing import List, Tuple, Dict, Optional\nfrom scipy import ndimage\nimport warnings\nimport gc\nimport sys\nimport json\nimport shutil\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\nimport threading\nimport queue\nimport time\nfrom collections import OrderedDict\n\nwarnings.filterwarnings('ignore')\n\n# Data handling\nimport polars as pl\nimport pandas as pd\nimport math\nimport pywt\n\n# ML/DL\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.cuda.amp import autocast\nimport timm\nfrom tqdm import tqdm\n\nimport cupy as cp\nfrom cupyx.scipy.ndimage import zoom\n\n\n# Transformations\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\n# YOLO\nsys.path.insert(0, \"/kaggle/input/ultralytcs-timm-rsna/ultralytics-timm\")\n# YOLO\nfrom ultralytics import YOLO\n\nimport timm_3d\n\n# Competition API\nimport kaggle_evaluation.rsna_inference_server\n\n# Set device\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nMAX_WORKERS = 4\n\n# Optimization settings\ntorch.set_float32_matmul_precision('medium')\ntorch.backends.cudnn.benchmark = True\ntorch.backends.cudnn.deterministic = False\ntorch.backends.cuda.matmul.allow_tf32 = True\ntorch.backends.cudnn.allow_tf32 = True","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T13:59:36.437188Z","iopub.execute_input":"2025-10-01T13:59:36.437531Z","iopub.status.idle":"2025-10-01T14:00:26.75634Z","shell.execute_reply.started":"2025-10-01T13:59:36.437506Z","shell.execute_reply":"2025-10-01T14:00:26.75577Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Multi-View PatchModel","metadata":{}},{"cell_type":"code","source":"class PatchModelConfig:\n    model_name= \"tf_efficientnetv2_b0.in1k\"\n    pretrained= False\n    drop_rate= 0.5\n    drop_path_rate= 0.2\n    model_dim= 512\n    transformer_layers= 2\n    transformer_heads= 4\n    classifier_hidden= 256\n    dropout= 0.2\n\n    #process\n    vol_size = (8, 128, 128)\n    patch_size = 64\n    patch_depth = 15\n    iou_thresh = 5.0\n    k_candi = 2\n    wavelet = 'haar'\n\n#patch_model_ckpts = [\n#     ['/kaggle/input/multi-view-patch-model-rsna-iad/pytorch/tf_efficientnetv2_b0.in1k/1/logpolar-mip-vol-3-views--tf_efficientnetv2_b0.in1k-epoch03-val_cls_auroc_30.7141_fold_id0.ckpt'],\n#     ['/kaggle/input/multi-view-patch-model-rsna-iad/pytorch/tf_efficientnetv2_b0.in1k/1/logpolar-mip-vol-3-views--tf_efficientnetv2_b0.in1k-epoch04-val_cls_auroc_30.7630_fold_id1.ckpt']\n#]\n\npatch_model_ckpts = [\n    '/kaggle/input/patch-wavelet-from-yolo-rsna-iad/pytorch/tf_efficientnetv2_b0.in1k-haar/1/logpolar-mip-vol-3-views--tf_efficientnetv2_b0.in1k-epoch05-val_cls_auroc0.7238_fold_id0_haar.ckpt',\n    '/kaggle/input/patch-wavelet-from-yolo-rsna-iad/pytorch/tf_efficientnetv2_b0.in1k-haar/1/logpolar-mip-vol-3-views--tf_efficientnetv2_b0.in1k-epoch05-val_cls_auroc0.7509_fold_id3_haar.ckpt',\n    '/kaggle/input/patch-wavelet-from-yolo-rsna-iad/pytorch/tf_efficientnetv2_b0.in1k-haar/1/logpolar-mip-vol-3-views--tf_efficientnetv2_b0.in1k-epoch04-val_cls_auroc0.7760_fold_id4_haar.ckpt'\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T14:00:26.757067Z","iopub.execute_input":"2025-10-01T14:00:26.75749Z","iopub.status.idle":"2025-10-01T14:00:26.762251Z","shell.execute_reply.started":"2025-10-01T14:00:26.757471Z","shell.execute_reply":"2025-10-01T14:00:26.761503Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_dicom_series_for_patch(series_dir: Path):\n    # Collect DICOMs\n    dcm_paths = [Path(series_dir) / f for f in os.listdir(series_dir) if f.lower().endswith(\".dcm\")]\n    if not dcm_paths:\n        raise FileNotFoundError(f\"No DICOM files found in {series_dir}\")\n\n    slices = [pydicom.dcmread(str(p), force=True) for p in dcm_paths]\n\n    # --- Orientation ---\n    orientation = slices[0].get(\"ImageOrientationPatient\", [1, 0, 0, 0, 1, 0])\n    orientation = np.array(orientation, dtype=np.float32).reshape(2, 3)\n    row_cos, col_cos = orientation\n    normal = np.cross(row_cos, col_cos)\n\n    # --- Sorting ---\n    if hasattr(slices[0], \"ImagePositionPatient\"):\n        slices.sort(key=lambda ds: np.dot(ds.get(\"ImagePositionPatient\", [0, 0, 0]), normal))\n    else:\n        # fallback: sort by InstanceNumber if available\n        slices.sort(key=lambda ds: getattr(ds, \"InstanceNumber\", 0))\n\n    # --- HU scaling ---\n    slope = float(getattr(slices[0], \"RescaleSlope\", 1.0))\n    intercept = float(getattr(slices[0], \"RescaleIntercept\", 0.0))\n    volume = np.stack([ds.pixel_array for ds in slices]).astype(np.float32)\n    volume = volume * slope + intercept\n\n    if volume.ndim == 4 and volume.shape[0] == 1:\n        volume = volume[0]\n    return volume\n\n\ndef normalize_vol(vol):\n    p2, p98 = np.percentile(vol, (2, 98))\n    mask = (vol >= p2) & (vol <= p98)\n    mean = np.mean(vol[mask])\n    std = np.std(vol[mask]) + 1e-6\n\n    vol = (vol - mean) / std\n    vol = np.clip((vol - vol.min()) / (vol.max() - vol.min() + 1e-6), 0, 1)\n    return vol","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T14:00:26.764045Z","iopub.execute_input":"2025-10-01T14:00:26.764288Z","iopub.status.idle":"2025-10-01T14:00:26.796877Z","shell.execute_reply.started":"2025-10-01T14:00:26.764267Z","shell.execute_reply":"2025-10-01T14:00:26.796176Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport torch\nimport cv2\nfrom concurrent.futures import ThreadPoolExecutor\n\nclass AneurysmVolumeProcessor3Planes:\n    def __init__(self, N=96, K_axial=5, K_sagittal=15, K_coronal=15,\n                 Nr=64, Ntheta=128, augment=False, device='cpu', n_workers=4):\n        self.N = N\n        self.K_axial = K_axial\n        self.K_sagittal = K_sagittal\n        self.K_coronal = K_coronal\n        self.Nr = Nr\n        self.Ntheta = Ntheta\n        self.augment = augment\n        self.device = device\n        self.n_workers = n_workers\n\n    def __call__(self, volume, yolo_points):\n        def process_point(point):\n            x, y, z = map(int, map(round, point))\n            planes = {}\n\n            # --- Axial ---\n            K = self.K_axial\n            z_min, z_max = max(0, z-K//2), min(volume.shape[0], z+K//2+1)\n            y_min, y_max = max(0, y-self.N//2), min(volume.shape[1], y+self.N//2)\n            x_min, x_max = max(0, x-self.N//2), min(volume.shape[2], x+self.N//2)\n            axial_patch = volume[z_min:z_max, y_min:y_max, x_min:x_max].copy()\n            axial_patch = self._pad_patch(axial_patch, (K, self.N, self.N))\n            planes['axial'] = axial_patch\n\n            # --- Sagittal ---\n            K = self.K_sagittal\n            x_min, x_max = max(0, x-K//2), min(volume.shape[2], x+K//2+1)\n            z_min, z_max = max(0, z-self.N//2), min(volume.shape[0], z+self.N//2)\n            y_min, y_max = max(0, y-self.N//2), min(volume.shape[1], y+self.N//2)\n            sag_patch = volume[z_min:z_max, y_min:y_max, x_min:x_max].copy()\n            sag_patch = np.transpose(sag_patch, (2, 0, 1))  # (x, z, y)\n            sag_patch = self._pad_patch(sag_patch, (K, self.N, self.N))\n            planes['sagittal'] = sag_patch\n\n            # --- Coronal ---\n            K = self.K_coronal\n            y_min, y_max = max(0, y-K//2), min(volume.shape[1], y+K//2+1)\n            z_min, z_max = max(0, z-self.N//2), min(volume.shape[0], z+self.N//2)\n            x_min, x_max = max(0, x-self.N//2), min(volume.shape[2], x+self.N//2)\n            cor_patch = volume[z_min:z_max, y_min:y_max, x_min:x_max].copy()\n            cor_patch = np.transpose(cor_patch, (1, 0, 2))  # (y, z, x)\n            cor_patch = self._pad_patch(cor_patch, (K, self.N, self.N))\n            planes['coronal'] = cor_patch\n\n            # --- Cartesian & Log-Polar features ---\n            cartesian_channels, logpolar_channels = [], []\n            for plane_name, patch in planes.items():\n                center_slice = patch[patch.shape[0] // 2]\n                mip = np.max(patch, axis=0)\n\n                # stack [center_slice, mip] only\n                cartesian_channels.append(np.stack([center_slice, mip], axis=0))\n\n                cx, cy = self.N / 2, self.N / 2\n                logpolar_channels.append(np.stack([\n                    self._logpolar(center_slice, cx, cy),\n                    self._logpolar(mip, cx, cy)\n                ], axis=0))\n\n            return {\n                'cartesian': torch.from_numpy(np.stack(cartesian_channels, axis=0)).float(),\n                'logpolar': torch.from_numpy(np.stack(logpolar_channels, axis=0)).float(),\n                'axial': torch.from_numpy(planes['axial']).float(),\n                'sagittal': torch.from_numpy(planes['sagittal']).float(),\n                'coronal': torch.from_numpy(planes['coronal']).float()\n            }\n\n        # ✅ order preserved\n        with ThreadPoolExecutor(max_workers=self.n_workers) as executor:\n            outputs = list(executor.map(process_point, yolo_points))\n\n        return outputs\n        \n    def _pad_patch(self, patch, shape):\n        K, N, _ = shape\n        padded = np.zeros(shape, dtype=patch.dtype)\n        dz, dy, dx = patch.shape\n        padded[:dz, :dy, :dx] = patch\n        return padded\n\n    def _logpolar(self, img, cx, cy):\n        img = img.astype(np.float32)\n        max_radius = np.sqrt(\n            max(cx, img.shape[1]-cx)**2 + \n            max(cy, img.shape[0]-cy)**2\n        )\n        logpolar_img = cv2.logPolar(\n            img, center=(cx, cy),\n            M=self.Nr / np.log(max_radius + 1e-6),\n            flags=cv2.INTER_LINEAR + cv2.WARP_FILL_OUTLIERS\n        )\n        return cv2.resize(logpolar_img, (self.Ntheta, self.Nr))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T14:00:26.797498Z","iopub.execute_input":"2025-10-01T14:00:26.797708Z","iopub.status.idle":"2025-10-01T14:00:26.813226Z","shell.execute_reply.started":"2025-10-01T14:00:26.797694Z","shell.execute_reply":"2025-10-01T14:00:26.812589Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ------------------------\n# MultiViewPatchModel\n# ------------------------\nclass AttentionPool(nn.Module):\n    \"\"\"Attention pooling across a token dimension.\n       Input: x (B, n_tokens, D)\n       Output: pooled (B, D) or (B, n_groups, D) if grouped beforehand.\n    \"\"\"\n    def __init__(self, dim):\n        super().__init__()\n        self.score = nn.Linear(dim, 1)\n\n    def forward(self, x, mask=None):\n        # x: (B, T, D)\n        scores = self.score(x).squeeze(-1)  # (B, T)\n        if mask is not None:\n            scores = scores.masked_fill(~mask, -1e9)\n        attn = torch.softmax(scores, dim=-1).unsqueeze(-1)  # (B, T, 1)\n        pooled = (x * attn).sum(dim=1)  # (B, D)\n        return pooled #, attn.squeeze(-1)  # return pooled and weights for debugging\n\n\nclass SinusoidalPosEmb(nn.Module):\n    def __init__(self, dim: int = 512, M: int = 10000):\n        super().__init__()\n        self.dim = dim\n        self.M = M\n\n    def forward(self, seq_len: int, device=None):\n        device = device or torch.device(\"cpu\")\n        half_dim = self.dim // 2\n        emb = math.log(self.M) / half_dim\n        emb = torch.exp(torch.arange(half_dim, device=device) * (-emb))\n        positions = torch.arange(seq_len, device=device).unsqueeze(1)\n        emb = positions * emb[None, :]\n        emb = torch.cat([emb.sin(), emb.cos()], dim=-1)\n        if self.dim % 2 == 1:\n            emb = torch.cat([emb, torch.zeros(seq_len, 1, device=device)], dim=-1)\n        return emb.unsqueeze(0)  # (1, seq_len, dim)\n\nclass MultiViewWaveletModel(nn.Module):\n    def __init__(\n        self,\n        model_name: str,\n        pretrained: bool = True,\n        drop_rate: float = 0.3,\n        drop_path_rate: float = 0.2,\n        k_candi: int = 2,\n        model_dim: int = 512,\n        transformer_layers: int = 2,\n        transformer_heads: int = 8,\n        classifier_hidden: int = 256,\n        dropout: float = 0.2,\n    ):\n        super().__init__()\n        self.k_candi = k_candi\n        self.model_keys = [\n            \"axial_vol\",\n            \"sagittal_vol\",\n            \"coronal_vol\",\n        ]\n        in_chans_list = [8 * 8] * 3\n        assert len(in_chans_list) == len(self.model_keys)\n\n        self.num_modules = len(self.model_keys)\n\n        # Create backbone dict\n        self.backboneDict = nn.ModuleDict()\n        for key, in_ch in zip(self.model_keys, in_chans_list):\n            backbone = timm.create_model(\n                model_name,\n                pretrained=pretrained,\n                in_chans=in_ch,\n                features_only=True,\n                drop_rate=drop_rate,\n                drop_path_rate=drop_path_rate,\n            )\n            self.backboneDict[key] = backbone\n\n        # Determine backbone output dim from last stage feature channels\n        sample_backbone = next(iter(self.backboneDict.values()))\n        out_channels = sample_backbone.feature_info.channels()[-1] \\\n            if hasattr(sample_backbone, \"feature_info\") else 512\n        self.backbone_out_dim = out_channels\n        self.model_dim = model_dim\n\n        # Linear projection to shared embedding space\n        self.project = nn.Sequential(\n            nn.Linear(self.backbone_out_dim * 2, self.model_dim),\n            nn.GELU(),\n            nn.LayerNorm(self.model_dim),\n        )\n\n        # Positional embedding\n        self.seq_len = self.num_modules * self.k_candi\n        self.pos_emb = SinusoidalPosEmb(self.model_dim)\n\n        # Transformer encoder\n        encoder_layer = nn.TransformerEncoderLayer(\n            d_model=self.model_dim,\n            nhead=transformer_heads,\n            dim_feedforward=self.model_dim * 4,\n            activation=nn.GELU(),\n            batch_first=True,\n            dropout=0\n        )\n        self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=transformer_layers)\n\n        # Classifier head\n        self.classifier = nn.Sequential(\n            AttentionPool(self.model_dim),\n            nn.Dropout(dropout),\n            nn.Linear(self.model_dim, 1))\n\n        self._init_weights()\n\n    def _init_weights(self):\n        for m in self.modules():\n            if isinstance(m, nn.Linear):\n                nn.init.trunc_normal_(m.weight, std=0.02)\n                if m.bias is not None:\n                    nn.init.zeros_(m.bias)\n\n    def _flatten_input_for_backbone(self, x):\n        if x.dim() == 5:\n            B, K, C, H, W = x.shape\n            return x.view(B * K, C, H, W), B\n        elif x.dim() == 4:\n            return x, None\n        else:\n            raise ValueError(\"Unsupported input shape.\")\n\n    def forward(self, patch: dict):\n        device = next(self.parameters()).device\n        feats_per_key = []\n        inferred_B = None\n\n        for key in self.model_keys:\n            if key not in patch:\n                raise KeyError(f\"Missing key '{key}' in patch dict.\")\n            x = patch[key].to(device)\n            x_flat, maybe_B = self._flatten_input_for_backbone(x)\n            if inferred_B is None and maybe_B is not None:\n                inferred_B = maybe_B\n\n            feat_maps = self.backboneDict[key](x_flat)\n            feat = feat_maps[-1]  # (B*K, C, H, W)\n            feat = torch.cat(\n                [feat.mean(dim=(2, 3)), feat.amax(dim=(2, 3))],\n                dim=1\n            )  # (B*K, 2*C)\n            feats_per_key.append(feat)\n\n        if inferred_B is None:\n            N_flat = feats_per_key[0].size(0)\n            if (N_flat % self.k_candi) != 0:\n                raise ValueError(\"Cannot infer batch size.\")\n            inferred_B = N_flat // self.k_candi\n\n        B = inferred_B\n        K = self.k_candi\n\n        stacked = torch.stack(feats_per_key, dim=1)\n        stacked = stacked.view(B, K, len(self.model_keys), 2 * self.backbone_out_dim)\n        tokens = stacked.view(B, K * len(self.model_keys), 2 * self.backbone_out_dim)\n\n        tokens = self.project(tokens)  # (B, 3*K, model_dim)\n        seq_len = tokens.size(1)\n        pos = self.pos_emb(seq_len, device=tokens.device)\n        tokens = tokens + pos\n        tokens = self.transformer(tokens)  # (B, 3*K, model_dim)\n        logits = self.classifier(tokens)  # (B, 1)\n        return logits\n\n\ndef load_patch_model():\n    patch_models = []\n    for ckpt_path in tqdm(patch_model_ckpts):\n        checkpoint = torch.load(ckpt_path, map_location=device, weights_only=False)\n        state_dict = checkpoint['state_dict']\n        new_state_dict = OrderedDict()\n        for k, v in state_dict.items():\n            new_key = k.replace(\"model.\", \"\") if k.startswith(\"model.\") else k\n            new_state_dict[new_key] = v\n        patch_model = MultiViewWaveletModel(\n            model_name=PatchModelConfig.model_name,\n            pretrained=PatchModelConfig.pretrained,\n            drop_rate=PatchModelConfig.drop_rate,\n            drop_path_rate=PatchModelConfig.drop_path_rate,\n            k_candi=PatchModelConfig.k_candi,\n            model_dim=PatchModelConfig.model_dim,\n            transformer_layers=PatchModelConfig.transformer_layers,\n            transformer_heads=PatchModelConfig.transformer_heads,\n            classifier_hidden=PatchModelConfig.classifier_hidden,\n            dropout=PatchModelConfig.dropout\n        )\n        patch_model.load_state_dict(new_state_dict)\n        patch_model = patch_model.to(device)\n        patch_model.eval()\n        patch_models.append(patch_model)\n    return patch_models","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T14:01:07.074416Z","iopub.execute_input":"2025-10-01T14:01:07.075254Z","iopub.status.idle":"2025-10-01T14:01:07.100592Z","shell.execute_reply.started":"2025-10-01T14:01:07.075216Z","shell.execute_reply":"2025-10-01T14:01:07.099837Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"patch_models = load_patch_model()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T14:01:07.311388Z","iopub.execute_input":"2025-10-01T14:01:07.311681Z","iopub.status.idle":"2025-10-01T14:01:15.254975Z","shell.execute_reply.started":"2025-10-01T14:01:07.31166Z","shell.execute_reply":"2025-10-01T14:01:15.254155Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"len(patch_models)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T14:01:16.974237Z","iopub.execute_input":"2025-10-01T14:01:16.974784Z","iopub.status.idle":"2025-10-01T14:01:16.979732Z","shell.execute_reply.started":"2025-10-01T14:01:16.974759Z","shell.execute_reply":"2025-10-01T14:01:16.979086Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"aneurysm_proc = AneurysmVolumeProcessor3Planes(N=PatchModelConfig.patch_size,\n                 K_axial=PatchModelConfig.patch_depth, K_sagittal=PatchModelConfig.patch_depth,\n                                               K_coronal=PatchModelConfig.patch_depth,\n                 Nr=PatchModelConfig.patch_size, Ntheta=PatchModelConfig.patch_size,\n                                               augment=False, device=device)\n\npatch_transforms = A.Compose(\n            [A.Resize(PatchModelConfig.vol_size[1], PatchModelConfig.vol_size[2]), ToTensorV2()],\n        )\n\n\ndef apply_3d_dwt(x):\n    coeffs = pywt.dwtn(x, wavelet=PatchModelConfig.wavelet, axes=(0,1,2))\n    #Z-normalization\n    normalized_coeffs = {}\n    for band, band_data in coeffs.items():\n        normalized_coeffs[band] = (band_data - np.mean(band_data)) / (np.std(band_data) + 1e-10)\n    bands = np.stack([normalized_coeffs[k] for k in normalized_coeffs.keys()], axis=0)\n    bands = bands.reshape(-1, bands.shape[-2], bands.shape[-1]) #(13 * 8, 37, 37)\n    return  bands\n\ndef resize_vol3d(vol, vol_size):\n    target_d, target_h, target_w = vol_size[0] * 8, vol_size[1], vol_size[2]\n    if vol.ndim == 4:\n        vol = vol.unsqueeze(1)\n    return F.interpolate(\n            vol,\n            size=(target_d, target_h, target_w),\n            mode=\"trilinear\",\n            align_corners=False\n        ).squeeze(1)\n\ndef multi_view_patch_prediction(series_path, location_preds):\n    vol = load_dicom_series_for_patch(series_path)\n    vol_norm = normalize_vol(vol)\n    all_folds_probs = []\n    for fold_idx, (key, value) in enumerate(location_preds.items()):\n        if len(value) < PatchModelConfig.k_candi:\n            continue\n        yolo_points = location_preds[key][:, [2, 1, 0]].astype('int32')\n        outputs = aneurysm_proc(vol_norm, yolo_points) #list:[patch0, ...]\n        patch_data = []\n        for patch_id, output in enumerate(outputs):\n            axial = output['axial'].numpy()\n            sagittal = output['sagittal'].numpy()\n            coronal = output['coronal'].numpy()\n            patch_data.append({\n                    \"axial\": apply_3d_dwt(axial.astype(np.float32)),           # (31,128,128)\n                    \"sagittal\": apply_3d_dwt(sagittal.astype(np.float32)),\n                    \"coronal\": apply_3d_dwt(coronal.astype(np.float32)),\n                })\n        # Stack across patches\n        data = {\n            \"axial_vol\": torch.stack([torch.from_numpy(p[\"axial\"]) for p in patch_data], dim=0),\n            \"sagittal_vol\": torch.stack([torch.from_numpy(p[\"sagittal\"]) for p in patch_data], dim=0),\n            \"coronal_vol\": torch.stack([torch.from_numpy(p[\"coronal\"]) for p in patch_data], dim=0),\n        }\n        \n        for k in [\"axial_vol\", \"sagittal_vol\", \"coronal_vol\"]:\n            data[k] = resize_vol3d(data[k], PatchModelConfig.vol_size)[None, ]\n\n        #fold -> k-fold\n        logits = patch_models[fold_idx](data)\n        prob = logits.sigmoid()[:, 0]\n        all_folds_probs.append(prob)\n        \n    if len(all_folds_probs)==0:\n        return 0.\n    all_folds_probs = torch.mean(torch.tensor(all_folds_probs))\n    return all_folds_probs.cpu().item()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T14:01:17.432102Z","iopub.execute_input":"2025-10-01T14:01:17.432654Z","iopub.status.idle":"2025-10-01T14:01:17.445773Z","shell.execute_reply.started":"2025-10-01T14:01:17.432632Z","shell.execute_reply":"2025-10-01T14:01:17.445129Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Auxiliary 2.5D CNN","metadata":{}},{"cell_type":"code","source":"def encode_for_resnet(e, x, B, depth_scaling=[2,2,2,2,1]):\n\n    def pool_in_depth(x, depth_scaling):\n        bd, c, h, w = x.shape\n        x1 = x.reshape(B, -1, c, h, w).permute(0, 2, 1, 3, 4)\n        x1 = F.avg_pool3d(x1, kernel_size=(depth_scaling, 1, 1), stride=(depth_scaling, 1, 1), padding=0)\n        x = x1.permute(0, 2, 1, 3, 4).reshape(-1, c, h, w)\n        return x, x1\n\n    encode=[]\n    x = e.conv1(x)\n    x = e.bn1(x)\n    x = e.act1(x)\n    x, x1 = pool_in_depth(x, depth_scaling[0])\n    encode.append(x1)\n    #x = e.maxpool(x)\n    x = F.avg_pool2d(x,kernel_size=2,stride=2)\n\n    x = e.layer1(x)\n    x, x1 = pool_in_depth(x, depth_scaling[1])\n    encode.append(x1)\n\n    x = e.layer2(x)\n    x, x1 = pool_in_depth(x, depth_scaling[2])\n    encode.append(x1)\n\n    x = e.layer3(x)\n    x, x1 = pool_in_depth(x, depth_scaling[3])\n    encode.append(x1)\n\n    x = e.layer4(x)\n    x, x1 = pool_in_depth(x, depth_scaling[4])\n    encode.append(x1)\n\n    return encode\n\ndef encode_for_convnext(e, x, B, depth_scaling=[2,2,2,2,1]):\n\n    def pool_in_depth(x, depth_scaling):\n        bd, c, h, w = x.shape\n        x1 = x.reshape(B, -1, c, h, w).permute(0, 2, 1, 3, 4)\n        x1 = F.avg_pool3d(x1, kernel_size=(depth_scaling, 1, 1), stride=(depth_scaling, 1, 1), padding=0)\n        x = x1.permute(0, 2, 1, 3, 4).reshape(-1, c, h, w)\n        return x, x1\n\n    encode=[]\n    x = e.stem_0(x)\n    x = e.stem_1(x)\n    \n   \n    x = e.stages_0(x)\n    x, x1 = pool_in_depth(x, depth_scaling[0])\n    encode.append(x1)\n\n    x = e.stages_1(x)\n    x, x1 = pool_in_depth(x, depth_scaling[1])\n    encode.append(x1)\n    \n    x = e.stages_2(x)\n    x, x1 = pool_in_depth(x, depth_scaling[2])\n    encode.append(x1)\n    \n    x = e.stages_3(x)\n    x, x1 = pool_in_depth(x, depth_scaling[3])\n    encode.append(x1)\n    \n\n    return encode\n\nclass DecoderBlock(nn.Module):\n    \"\"\"\n    U-Net-style decoder block with skip connections.\n    It upsamples the feature map and concatenates it with the corresponding\n    feature map from the encoder, followed by two convolutional layers.\n    \"\"\"\n    def __init__(self, in_channels, skip_channels, out_channels):\n        super().__init__()\n        # Upsampling layer followed by a convolution to adjust channels\n        self.upsample = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)\n        # We combine the upsampled channels with the skip connection channels\n        combined_in_channels = in_channels + skip_channels\n        self.conv = nn.Sequential(\n            nn.Conv2d(combined_in_channels, out_channels, kernel_size=3, padding=1, bias=False),\n            nn.BatchNorm2d(out_channels),\n            nn.ReLU(inplace=True),\n            nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1, bias=False),\n            nn.BatchNorm2d(out_channels),\n            nn.ReLU(inplace=True)\n        )\n\n    def forward(self, x, skip):\n        x = self.upsample(x)\n        # Concatenate along the channel dimension\n        if skip is not None:\n             x = torch.cat([x, skip], dim=1)\n        x = self.conv(x)\n        return x\n\nclass SegmentationClassifier(nn.Module):\n    \"\"\"\n    A multi-task model for binary classification and binary segmentation.\n    Uses a timm model as the encoder.\n    \"\"\"\n    def __init__(self, pooler, model_name, pretrained=False):\n        super().__init__()\n        \n        # 1. Encoder (from timm)\n        # We use features_only=True to get intermediate feature maps\n        self.encoder = timm.create_model(\n            model_name,\n            pretrained=pretrained,\n            features_only=True,\n            in_chans=3\n        )\n        self.pooler = pooler\n        \n        # Get the channel sizes of the feature maps from the encoder\n        encoder_channels = self.encoder.feature_info.channels()\n        # e.g., for resnet34: [64, 64, 128, 256, 512]\n        \n        # 2. Classification Head\n        self.avgpool = nn.AdaptiveAvgPool2d(1)\n        self.flatten = nn.Flatten()\n        # The input to the linear layer is the number of channels in the last feature map\n        self.classification_head = nn.Linear(encoder_channels[-1], 1)\n\n        # 3. Segmentation Head (Decoder)\n        # We work backwards from the last feature map\n        self.decoder_blocks = nn.ModuleList()\n        \n        reversed_encoder_channels = list(reversed(encoder_channels))\n        # Example for resnet34: [512, 256, 128, 64, 64]\n        \n        # The first decoder block takes the last feature map\n        in_ch = reversed_encoder_channels[0] \n        # The first decoder block does not have a skip connection from a deeper layer\n        skip_ch = 0 \n        out_ch = in_ch // 2 # Halve the channels\n        \n        self.center = nn.Identity() # Placeholder for the deepest features\n        \n        for i in range(len(reversed_encoder_channels) - 1):\n            in_ch = reversed_encoder_channels[i] if i == 0 else out_ch\n            skip_ch = reversed_encoder_channels[i+1]\n            out_ch = reversed_encoder_channels[i+1]\n            \n            self.decoder_blocks.append(DecoderBlock(in_ch, skip_ch, out_ch))\n\n        # Final 1x1 convolution to get a single-channel mask\n        # The number of input channels is the output of the last decoder block\n        last_decoder_out_channels = reversed_encoder_channels[-1]\n        self.segmentation_head = nn.Conv2d(last_decoder_out_channels, 1, kernel_size=1)\n\n    def forward(self, x):\n\n        # Bx96x384x384 -> \n        B, D, H, W = x.shape\n        x = x.reshape(B*D, 1, H, W)\n        x = x.expand(-1, 3, -1, -1)\n        features = self.pooler(self.encoder, x, B, depth_scaling=[2,2,2,2,1])\n\n        for i in range(len(features)):\n            features[i] = features[i].amax(dim=2)\n\n\n        # --- Classification Path ---\n        # Use the last and most abstract feature map for classification\n        last_feature = features[-1]\n        pooled_features = self.avgpool(last_feature)\n        flat_features = self.flatten(pooled_features)\n        classification_output = self.classification_head(flat_features)\n\n        return classification_output","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T14:01:21.170952Z","iopub.execute_input":"2025-10-01T14:01:21.171515Z","iopub.status.idle":"2025-10-01T14:01:21.189085Z","shell.execute_reply.started":"2025-10-01T14:01:21.171492Z","shell.execute_reply":"2025-10-01T14:01:21.18839Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Yolo11 + 32ch EFF Model + 3D EFF Model","metadata":{}},{"cell_type":"code","source":"# ====================================================\n# Competition constants\n# ====================================================\nID_COL = 'SeriesInstanceUID'\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# YOLO label mappings\nYOLO_LABELS_TO_IDX = {\n    'Anterior Communicating Artery': 0,\n    'Basilar Tip': 1,\n    'Left Anterior Cerebral Artery': 2,\n    'Left Infraclinoid Internal Carotid Artery': 3,\n    'Left Middle Cerebral Artery': 4,\n    'Left Posterior Communicating Artery': 5,\n    'Left Supraclinoid Internal Carotid Artery': 6,\n    'Other Posterior Circulation': 7,\n    'Right Anterior Cerebral Artery': 8,\n    'Right Infraclinoid Internal Carotid Artery': 9,\n    'Right Middle Cerebral Artery': 10,\n    'Right Posterior Communicating Artery': 11,\n    'Right Supraclinoid Internal Carotid Artery': 12\n}\n\nYOLO_LABELS = sorted(list(YOLO_LABELS_TO_IDX.keys()))\n\n\nEFF_LABELS_TO_IDX = {\n    'Aneurysm Present': 0,\n    'Anterior Communicating Artery': 1,\n    'Basilar Tip': 2,\n    'Left Anterior Cerebral Artery': 3,\n    'Left Infraclinoid Internal Carotid Artery': 4,\n    'Left Middle Cerebral Artery': 5,\n    'Left Posterior Communicating Artery': 6,\n    'Left Supraclinoid Internal Carotid Artery': 7,\n    'Other Posterior Circulation': 8,\n    'Right Anterior Cerebral Artery': 9,\n    'Right Infraclinoid Internal Carotid Artery': 10,\n    'Right Middle Cerebral Artery': 11,\n    'Right Posterior Communicating Artery': 12,\n    'Right Supraclinoid Internal Carotid Artery': 13\n}\n\nEFF_LABELS = sorted(list(EFF_LABELS_TO_IDX.keys()))","metadata":{"trusted":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2025-10-01T14:01:24.087936Z","iopub.execute_input":"2025-10-01T14:01:24.088596Z","iopub.status.idle":"2025-10-01T14:01:24.094244Z","shell.execute_reply.started":"2025-10-01T14:01:24.08857Z","shell.execute_reply":"2025-10-01T14:01:24.093457Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ====================================================\n# DICOM Preprocessor for EfficientNet\n# ====================================================\nclass DICOMPreprocessorKaggle:\n    \"\"\"DICOM preprocessing system for EfficientNet\"\"\"\n    \n    def __init__(self, target_shape: Tuple[int, int, int] = (32, 384, 384)):\n        self.target_depth, self.target_height, self.target_width = target_shape\n        \n    def load_dicom_series(self, series_path: str) -> Tuple[List[pydicom.Dataset], str]:\n        \"\"\"Load DICOM series\"\"\"\n        series_path = Path(series_path)\n        series_name = series_path.name\n        \n        dicom_files = []\n        for root, _, files in os.walk(series_path):\n            for file in files:\n                if file.endswith('.dcm'):\n                    dicom_files.append(os.path.join(root, file))\n        \n        if not dicom_files:\n            raise ValueError(f\"No DICOM files found in {series_path}\")\n        \n        datasets = []\n        for filepath in dicom_files:\n            try:\n                ds = pydicom.dcmread(filepath, force=True)\n                datasets.append(ds)\n            except Exception as e:\n                continue\n        \n        if not datasets:\n            raise ValueError(f\"No valid DICOM files in {series_path}\")\n        \n        return datasets, series_name\n    \n    def extract_slice_info(self, datasets: List[pydicom.Dataset]) -> List[Dict]:\n        \"\"\"Extract position information for each slice\"\"\"\n        slice_info = []\n        \n        for i, ds in enumerate(datasets):\n            info = {\n                'dataset': ds,\n                'index': i,\n                'instance_number': getattr(ds, 'InstanceNumber', i),\n            }\n            \n            try:\n                position = getattr(ds, 'ImagePositionPatient', None)\n                if position is not None and len(position) >= 3:\n                    info['z_position'] = float(position[2])\n                else:\n                    info['z_position'] = float(info['instance_number'])\n            except Exception as e:\n                info['z_position'] = float(i)\n            \n            slice_info.append(info)\n        \n        return slice_info\n    \n    def sort_slices_by_position(self, slice_info: List[Dict]) -> List[Dict]:\n        \"\"\"Sort slices by z-coordinate\"\"\"\n        return sorted(slice_info, key=lambda x: x['z_position'])\n    \n    def get_windowing_params(self, ds: pydicom.Dataset, img: np.ndarray = None) -> Tuple[Optional[float], Optional[float]]:\n        \"\"\"Get windowing parameters based on modality\"\"\"\n        modality = getattr(ds, 'Modality', 'CT')\n        \n        if modality == 'CT':\n            return \"CT\", \"CT\"\n        else:\n            return None, None\n    \n    def apply_windowing_or_normalize(self, img: np.ndarray, center: Optional[float], width: Optional[float]) -> np.ndarray:\n        \"\"\"Apply windowing or statistical normalization\"\"\"\n        if center is not None and width is not None:\n            p1, p99 = 0, 500\n            \n            if p99 > p1:\n                normalized = np.clip(img, p1, p99)\n                normalized = (normalized - p1) / (p99 - p1)\n                result = (normalized * 255).astype(np.uint8)\n                return result\n            else:\n                img_min, img_max = img.min(), img.max()\n                if img_max > img_min:\n                    normalized = (img - img_min) / (img_max - img_min)\n                    result = (normalized * 255).astype(np.uint8)\n                    return result\n                else:\n                    return np.zeros_like(img, dtype=np.uint8)\n        else:\n            p1, p99 = np.percentile(img, [1, 99])\n            \n            if p99 > p1:\n                normalized = np.clip(img, p1, p99)\n                normalized = (normalized - p1) / (p99 - p1)\n                result = (normalized * 255).astype(np.uint8)\n                return result\n            else:\n                img_min, img_max = img.min(), img.max()\n                if img_max > img_min:\n                    normalized = (img - img_min) / (img_max - img_min)\n                    result = (normalized * 255).astype(np.uint8)\n                    return result\n                else:\n                    return np.zeros_like(img, dtype=np.uint8)\n    \n    def extract_pixel_array(self, ds: pydicom.Dataset) -> np.ndarray:\n        \"\"\"Extract 2D pixel array from DICOM\"\"\"\n        img = ds.pixel_array.astype(np.float32)\n        \n        if img.ndim == 3:\n            frame_idx = img.shape[0] // 2\n            img = img[frame_idx]\n        \n        if img.ndim == 3 and img.shape[-1] == 3:\n            img = cv2.cvtColor(img.astype(np.uint8), cv2.COLOR_RGB2GRAY).astype(np.float32)\n        \n        slope = getattr(ds, 'RescaleSlope', 1)\n        intercept = getattr(ds, 'RescaleIntercept', 0)\n        slope, intercept = 1, 0\n        if slope != 1 or intercept != 0:\n            img = img * float(slope) + float(intercept)\n        \n        return img\n    \n    def resize_volume_3d(self, volume: np.ndarray) -> np.ndarray:\n        \"\"\"Resize 3D volume to target size\"\"\"\n        current_shape = volume.shape\n        \n        \n        target_shape = (self.target_depth, self.target_height, self.target_width)\n        target_effnet_aux_shape = (96,384,384)\n        \n        \n        if current_shape == target_shape:\n            return volume\n        \n        zoom_factors = [\n            target_shape[i] / current_shape[i] for i in range(3)\n        ]\n\n        eff_aux_zoom_factors = [\n            target_effnet_aux_shape[i] / current_shape[i] for i in range(3)\n        ]\n\n        volume = cp.asarray(volume)\n        resized_volume = zoom(volume, zoom_factors, order=1, mode='nearest')\n        resized_volume = resized_volume[:self.target_depth, :self.target_height, :self.target_width]\n        resized_volume = cp.asnumpy(resized_volume)\n\n        pad_width = [\n            (0, max(0, self.target_depth - resized_volume.shape[0])),\n            (0, max(0, self.target_height - resized_volume.shape[1])),\n            (0, max(0, self.target_width - resized_volume.shape[2]))\n        ]\n        \n        if any(pw[1] > 0 for pw in pad_width):\n            resized_volume = np.pad(resized_volume, pad_width, mode='edge')\n\n        eff_aux_resized_volume = zoom(volume, eff_aux_zoom_factors, order=1, mode='nearest')\n        eff_aux_resized_volume = eff_aux_resized_volume[:96, :384, :384]\n        eff_aux_resized_volume = cp.asnumpy(eff_aux_resized_volume)\n        \n        pad_width = [\n            (0, max(0, 96 - eff_aux_resized_volume.shape[0])),\n            (0, max(0, 384 - eff_aux_resized_volume.shape[1])),\n            (0, max(0, 384 - eff_aux_resized_volume.shape[2]))\n        ]\n        \n        if any(pw[1] > 0 for pw in pad_width):\n            eff_aux_resized_volume = np.pad(eff_aux_resized_volume, pad_width, mode='edge')\n        return resized_volume.astype(np.uint8), eff_aux_resized_volume.astype(np.uint8)\n    \n    def process_series(self, series_path: str) -> np.ndarray:\n        \"\"\"Process DICOM series and return as NumPy array\"\"\"\n        try:\n            datasets, series_name = self.load_dicom_series(series_path)\n            \n            first_ds = datasets[0]\n            first_img = first_ds.pixel_array\n            \n            if len(datasets) == 1 and first_img.ndim == 3:\n                return self._process_single_3d_dicom(first_ds, series_name)\n            else:\n                return self._process_multiple_2d_dicoms(datasets, series_name)\n            \n        except Exception as e:\n            raise\n    \n    def _process_single_3d_dicom(self, ds: pydicom.Dataset, series_name: str) -> np.ndarray:\n        \"\"\"Process single 3D DICOM file\"\"\"\n        volume = ds.pixel_array.astype(np.float32)\n        \n        slope = getattr(ds, 'RescaleSlope', 1)\n        intercept = getattr(ds, 'RescaleIntercept', 0)\n        slope, intercept = 1, 0\n        if slope != 1 or intercept != 0:\n            volume = volume * float(slope) + float(intercept)\n        \n        window_center, window_width = self.get_windowing_params(ds)\n        \n        processed_slices = []\n        for i in range(volume.shape[0]):\n            slice_img = volume[i]\n            processed_img = self.apply_windowing_or_normalize(slice_img, window_center, window_width)\n            processed_slices.append(processed_img)\n        \n        volume = np.stack(processed_slices, axis=0)\n        final_volume = self.resize_volume_3d(volume)\n        \n        return final_volume\n    \n    def _process_multiple_2d_dicoms(self, datasets: List[pydicom.Dataset], series_name: str) -> np.ndarray:\n        \"\"\"Process multiple 2D DICOM files\"\"\"\n        slice_info = self.extract_slice_info(datasets)\n        sorted_slices = self.sort_slices_by_position(slice_info)\n        first_img = self.extract_pixel_array(sorted_slices[0]['dataset'])\n        window_center, window_width = self.get_windowing_params(sorted_slices[0]['dataset'], first_img)\n        processed_slices = []\n        \n        for slice_data in sorted_slices:\n            ds = slice_data['dataset']\n            img = self.extract_pixel_array(ds)\n            processed_img = self.apply_windowing_or_normalize(img, window_center, window_width)\n            processed_slices.append(processed_img)\n\n        volume = np.stack(processed_slices, axis=0)\n        final_volume = self.resize_volume_3d(volume)\n        \n        return final_volume","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T14:01:24.272721Z","iopub.execute_input":"2025-10-01T14:01:24.273289Z","iopub.status.idle":"2025-10-01T14:01:24.2971Z","shell.execute_reply.started":"2025-10-01T14:01:24.273267Z","shell.execute_reply":"2025-10-01T14:01:24.29639Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ====================================================\n# YOLO DICOM Processing\n# ====================================================\ndef read_dicom_frames_hu(path: Path) -> List[Tuple[float, np.ndarray]]:\n    \"\"\"Read DICOM file and return list of (slice_position, HU frame)\"\"\"\n    ds = pydicom.dcmread(str(path), force=True)\n    pix = ds.pixel_array\n    slope = float(getattr(ds, 'RescaleSlope', 1.0))\n    intercept = float(getattr(ds, 'RescaleIntercept', 0.0))\n\n    # Compute slice location using orientation + position\n    try:\n        orientation = np.array(ds.ImageOrientationPatient).reshape(2, 3)\n        row_cos, col_cos = orientation\n        normal = np.cross(row_cos, col_cos)  # slice normal vector\n        position = np.array(ds.ImagePositionPatient)\n        slice_loc = float(np.dot(position, normal))  # projection along normal\n    except Exception:\n        # Fallback: SliceLocation / InstanceNumber\n        slice_loc = float(getattr(ds, \"SliceLocation\", getattr(ds, \"InstanceNumber\", 0.0)))\n\n    frames: List[Tuple[float, np.ndarray]] = []\n\n    if pix.ndim == 2:\n        img = pix.astype(np.float32)\n        frames.append((slice_loc, img * slope + intercept))\n    elif pix.ndim == 3:\n        # RGB or multi-frame\n        if pix.shape[-1] == 3 and pix.shape[0] != 3:\n            try:\n                gray = cv2.cvtColor(pix.astype(np.uint8), cv2.COLOR_BGR2GRAY).astype(np.float32)\n            except Exception:\n                gray = pix[..., 0].astype(np.float32)\n            frames.append((slice_loc, gray * slope + intercept))\n        else:\n            for i in range(pix.shape[0]):\n                frm = pix[i].astype(np.float32)\n                # tiny offset ensures consistent ordering for multi-frame\n                frames.append((slice_loc + i * 1e-3, frm * slope + intercept))\n    return frames\n\n\ndef min_max_normalize(img: np.ndarray) -> np.ndarray:\n    \"\"\"Min-max normalization to 0-255 with optional flipping\"\"\"\n    mn, mx = float(img.min()), float(img.max())\n    if mx - mn < 1e-6:\n        norm = np.zeros_like(img, dtype=np.uint8)\n    else:\n        norm = (img - mn) / (mx - mn)\n        norm = (norm * 255.0).clip(0, 255).astype(np.uint8)\n    return norm\n\n\ndef process_dicom_file(dcm_path: Path) -> List[Tuple[float, np.ndarray]]:\n    \"\"\"Process single DICOM file -> list of (slice_loc, image) tuples\"\"\"\n    try:\n        frames = read_dicom_frames_hu(dcm_path)\n        processed_slices = []\n        for loc, f in frames:\n            img_u8 = min_max_normalize(f)\n            if img_u8.ndim == 2:\n                img_u8 = cv2.cvtColor(img_u8, cv2.COLOR_GRAY2BGR)\n            processed_slices.append((loc, img_u8))\n        return processed_slices\n    except Exception as e:\n        print(f\"Failed processing {dcm_path.name}: {e}\")\n        return []\n\n\ndef collect_series_slices(series_dir: Path) -> List[Path]:\n    \"\"\"Collect all DICOM files in a series directory (recursively).\"\"\"\n    dcm_paths: List[Path] = []\n    try:\n        for root, _, files in os.walk(series_dir):\n            for f in files:\n                if f.lower().endswith('.dcm'):\n                    dcm_paths.append(Path(root) / f)\n    except Exception as e:\n        print(f\"Failed to walk series dir {series_dir}: {e}\")\n    return dcm_paths\n\n\ndef slice_sort_key(path: Path) -> float:\n    \"\"\"Compute a robust slice sort key (orientation + position) for a single DICOM file\"\"\"\n    try:\n        ds = pydicom.dcmread(str(path), stop_before_pixels=True, force=True)\n        orientation = np.array(ds.ImageOrientationPatient).reshape(2, 3)\n        row_cos, col_cos = orientation\n        normal = np.cross(row_cos, col_cos)\n        position = np.array(ds.ImagePositionPatient)\n        return float(np.dot(position, normal))\n    except Exception:\n        # fallback\n        try:\n            return float(getattr(ds, \"SliceLocation\", getattr(ds, \"InstanceNumber\", 0.0)))\n        except:\n            return 0.0\n\n\n# ====================================================\n# TIMM 3D\n# ====================================================\ndef scale_intensity(x, minv=0.0, maxv=1.0):\n    \"\"\"\n    Rescale tensor intensities to [minv, maxv].\n    \"\"\"\n    x_min, x_max = x.min(), x.max()\n    if x_max == x_min:\n        return np.full_like(x, minv)  # avoid divide by zero\n    return (x - x_min) / (x_max - x_min) * (maxv - minv) + minv","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T14:01:27.178343Z","iopub.execute_input":"2025-10-01T14:01:27.178648Z","iopub.status.idle":"2025-10-01T14:01:27.193195Z","shell.execute_reply.started":"2025-10-01T14:01:27.178626Z","shell.execute_reply":"2025-10-01T14:01:27.192581Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ====================================================\n# EfficientNet Configuration\n# ====================================================\nclass EfficientNetConfig:\n    model_name = \"tf_efficientnetv2_s.in21k_ft_in1k\"\n    size = 384\n    target_cols = LABEL_COLS\n    num_classes = len(target_cols)\n    in_chans = 32\n    target_shape = (32, 384, 384)\n    batch_size = 1\n    use_amp = True\n    model_dir = '/kaggle/input/rsna2025-effnetv2-32ch'\n    n_fold = 5\n    trn_fold = [0, 1, 2, 3, 4]\n\nEFFNET_CFG = EfficientNetConfig()\n\n\n# ====================================================\n# EfficientNet Configuration\n# ====================================================\nclass EfficientNet3DConfig:\n    model_name = \"tf_efficientnetv2_s.in21k_ft_in1k\"\n    size = 384\n    target_cols = LABEL_COLS\n    num_classes = len(target_cols)\n    in_chans = 1\n    target_shape = (32, 384, 384)\n    batch_size = 1\n    use_amp = True\n    model_dir = '/kaggle/input/rsna2025-effnetv2-32ch'\n    n_fold = 5\n    trn_fold = [0, 1, 2, 3, 4]\n\nEFFNET_3D_CFG = EfficientNet3DConfig()\n\n# ====================================================\n# EfficientNet B0 AuxLoss Configuration\n# ====================================================\nclass EfficientNetAuxLossConfig:\n    model_name = \"efficientnet_b0\"\n    size = 384\n    target_cols = LABEL_COLS\n    num_classes = len(target_cols)\n    in_chans = 96\n    target_shape = (96, 384, 384)\n    batch_size = 1\n    use_amp = True\n    model_dir = '/kaggle/input/rsna-iad-modelzoo/'\n    n_fold = 5\n    trn_fold = [0, 1, 2, 3, 4]\n\nEFFNET_AUX_LOSS_CFG = EfficientNetAuxLossConfig()\n\n# ====================================================\n# YOLO Configuration\n# ====================================================\nIMG_SIZE = 512\nBATCH_SIZE = int(os.getenv(\"YOLO_BATCH_SIZE\", \"32\"))\nMAX_WORKERS = 4\n\nYOLO_MODEL_CONFIGS = [\n    {\n        \"path\": \"/kaggle/input/rsna-yolo-models/cv_y11m_more_negatives_fold02/weights/best.pt\",\n        \"fold\": \"0\",\n        \"weight\": 1.0,\n        \"name\": \"YOLOv11m_fold0\"\n    },\n    # {\n    #     \"path\": \"/kaggle/input/rsna-yolo-models/cv_mobilenet_more_negatives_fold1/weights/best.pt\",\n    #     \"fold\": \"1\",\n    #     \"weight\": 1.0,\n    #     \"name\": \"mobile_net_more_negatives\"\n    # },\n    {\n        \"path\": \"/kaggle/input/rsna-yolo-models/cv_y11m_more_negatives_fold3/weights/best.pt\",\n        \"fold\": \"3\",\n        \"weight\": 1.0,\n        \"name\": \"YOLOv11m_fold1\"\n    },  \n    {\n        \"path\": \"/kaggle/input/rsna-yolo-models/cv_y11m_more_negatives_fold4/weights/best.pt\",\n        \"fold\": \"4\",\n        \"weight\": 1.0,\n        \"name\": \"YOLOv11m_fold1\"\n    }, \n    #{\n    #    \"path\": \"/kaggle/input/rsna-sergio-models/cv_y11m_with_mix_up_mosaic_fold2/weights/best.pt\",\n    #    \"fold\": \"2\",\n    #    \"weight\": 1.0,\n    #    \"name\": \"YOLOv11n_fold2\"\n    #}\n]\n\n\n# ====================================================\n# Model Loading and Inference\n# ====================================================\n# Global variables\nEFFNET_MODELS = {}\nYOLO_MODELS = []\nEFFNET_AUX_MODELS = {}\nEFFNET_TRANSFORM = None\nEFFNET_AUX_TRANSFORM = None\n\nEFFNET_CKPTS = [\n    '/kaggle/input/rsna-iad-32ch-efficientnet/pytorch/tf_efficientnetv2_s.in21k_ft_in1k/2/32-ch-cnn-epoch04-kaggle_score0.6727_fold_id0.ckpt',\n    '/kaggle/input/rsna-iad-32ch-efficientnet/pytorch/tf_efficientnetv2_s.in21k_ft_in1k/2/32-ch-cnn-epoch05-kaggle_score0.6883_fold_id1.ckpt',\n    '/kaggle/input/rsna-iad-32ch-efficientnet/pytorch/tf_efficientnetv2_s.in21k_ft_in1k/2/32-ch-cnn-epoch13-kaggle_score0.6891_fold_id2.ckpt',\n    '/kaggle/input/rsna-iad-32ch-efficientnet/pytorch/tf_efficientnetv2_s.in21k_ft_in1k/1/32-ch-cnn-epoch03-kaggle_score0.7006_fold_id3.ckpt',\n    '/kaggle/input/rsna-iad-32ch-efficientnet/pytorch/tf_efficientnetv2_s.in21k_ft_in1k/2/32-ch-cnn-epoch16-kaggle_score0.6686_fold_id4.ckpt'\n]\n\nEFFNET_AUX_LOSS_CKPTS = {\n    'resnet18d': [\n        '/kaggle/input/rsna-iad-modelzoo/ch32_segcls_resnet18d_2.5D-epoch08-val_cls_auroc0.6936_fold_id4.pth',\n        '/kaggle/input/rsna-iad-modelzoo/ch32_segcls_resnet18d_2.5D-epoch08-val_cls_auroc0.7130_fold_id2.pth',\n        '/kaggle/input/rsna-iad-modelzoo/ch32_segcls_resnet18d_2.5D-epoch09-val_cls_auroc0.7113_fold_id1.pth',\n        '/kaggle/input/rsna-iad-modelzoo/ch32_segcls_resnet18d_2.5D-epoch09-val_cls_auroc0.7226_fold_id3.pth',\n        '/kaggle/input/rsna-iad-modelzoo/ch32_segcls_resnet18d_2.5D-epoch09-val_cls_auroc0.7393_fold_id0.pth'\n    ],\n    # 'convnext_tiny.fb_in22k':[\n    #     '/kaggle/input/rsna-iad-modelzoo/ch32_segcls_convnext_tiny_2.5D-epoch05-val_cls_auroc0.7067_fold_id4.pth',\n    #     '/kaggle/input/rsna-iad-modelzoo/ch32_segcls_convnext_tiny_2.5D-epoch06-val_cls_auroc0.7264_fold_id2.pth',\n    #     '/kaggle/input/rsna-iad-modelzoo/ch32_segcls_convnext_tiny_2.5D-epoch06-val_cls_auroc0.7422_fold_id0.pth',\n    #     '/kaggle/input/rsna-iad-modelzoo/ch32_segcls_convnext_tiny_2.5D-epoch06-val_cls_auroc0.7424_fold_id3.pth',\n    #     '/kaggle/input/rsna-iad-modelzoo/ch32_segcls_convnext_tiny_2.5D-epoch07-val_cls_auroc0.7210_fold_id1.pth'\n    # ]\n}\n\n\npoolers = {'resnet18d': encode_for_resnet,\n           'convnext_tiny.fb_in22k': encode_for_convnext}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T14:01:27.314047Z","iopub.execute_input":"2025-10-01T14:01:27.314568Z","iopub.status.idle":"2025-10-01T14:01:27.323747Z","shell.execute_reply.started":"2025-10-01T14:01:27.314522Z","shell.execute_reply":"2025-10-01T14:01:27.322919Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_inference_transform():\n    \"\"\"Get inference transformation for EfficientNet\"\"\"\n    return A.Compose([\n        A.Resize(EFFNET_CFG.size, EFFNET_CFG.size),\n        A.Normalize(),\n        ToTensorV2(),\n    ])\n\ndef load_effnet_model_fold(fold: int) -> nn.Module:\n    \"\"\"Load a single EfficientNet fold model\"\"\"\n    #model_path = Path(EFFNET_CFG.model_dir) / f'{EFFNET_CFG.model_name}_fold{fold}_best.pth'\n    checkpoint = torch.load(EFFNET_CKPTS[fold], map_location=device, weights_only=False)\n    state_dict = checkpoint['state_dict']\n    new_state_dict = OrderedDict()\n    for k, v in state_dict.items():\n        # remove \"model.\" prefix if it exists\n        new_key = k.replace(\"model.\", \"\") if k.startswith(\"model.\") else k\n        new_state_dict[new_key] = v\n    \n    model = timm.create_model(\n        EFFNET_CFG.model_name, \n        num_classes=EFFNET_CFG.num_classes, \n        pretrained=False,\n        in_chans=EFFNET_CFG.in_chans\n    )\n    \n    model.load_state_dict(new_state_dict)\n    model = model.to(device)\n    model.eval()\n    return model\n\ndef load_effnet_aux_model_soup():\n    for i, (model_name, checkpoints) in enumerate(EFFNET_AUX_LOSS_CKPTS.items()):\n        model = SegmentationClassifier(poolers[model_name], model_name)\n        state_dicts = []\n        for checkpoint in checkpoints:\n            state_dict = torch.load(checkpoint, map_location=device, weights_only=False)\n            state_dicts.append(state_dict)\n        soup_state_dict = OrderedDict()\n        for key in state_dicts[0].keys():\n            tensors = [sd[key].float() for sd in state_dicts]\n            soup_state_dict[key] = torch.mean(torch.stack(tensors, dim=0), dim=0)\n        model.load_state_dict(soup_state_dict)\n        model = model.to(device)\n        model.eval()\n        EFFNET_AUX_MODELS[model_name] = model\n\ndef load_yolo_models():\n    \"\"\"Load all YOLO models\"\"\"\n    models = []\n    for config in YOLO_MODEL_CONFIGS:\n        model = YOLO(config[\"path\"])\n        model.to(device)\n        \n        model_dict = {\n            \"model\": model,\n            \"weight\": config[\"weight\"],\n            \"name\": config[\"name\"],\n            \"fold\": config[\"fold\"]\n        }\n        models.append(model_dict)\n    return models\n\ndef load_all_models():\n    \"\"\"Load all models (EfficientNet + YOLO)\"\"\"\n    global EFFNET_MODELS, YOLO_MODELS, EFFNET_TRANSFORM, EFFNET_AUX_MODELS, EFFNET_AUX_TRANSFORM\n    \n    # # Load EfficientNet models\n    # for fold in EFFNET_CFG.trn_fold:\n    #     try:\n    #         EFFNET_MODELS[fold] = load_effnet_model_fold(fold)\n    #     except Exception as e:\n    #         print(f\"Warning: Could not load EfficientNet fold {fold}: {e}\")\n\n    #aux models\n    # load_effnet_aux_model_soup()\n    \n    # if not EFFNET_MODELS:\n    #     raise ValueError(\"No EfficientNet models were loaded successfully\")\n    \n    # Load YOLO models\n    YOLO_MODELS = load_yolo_models()\n    \n    # Initialize transforms\n    EFFNET_TRANSFORM = get_inference_transform()\n    EFFNET_AUX_TRANSFORM = A.Compose([ A.Normalize(mean=(0.5,) * 96, std=(0.5,) * 96), ToTensorV2() ])\n    \n    # Warm up models\n    dummy_effnet_image = torch.randn(1, EFFNET_CFG.in_chans, EFFNET_CFG.size, EFFNET_CFG.size).to(device)\n    dummy_yolo_image = np.random.randint(0, 255, (512, 512, 3), dtype=np.uint8)\n    dummy_effnet_aux_image = torch.randn(1, EFFNET_AUX_LOSS_CFG.in_chans, EFFNET_AUX_LOSS_CFG.size, EFFNET_AUX_LOSS_CFG.size).to(device)\n\n    \n    with torch.no_grad():\n        # for fold, model in EFFNET_MODELS.items():\n        #     _ = model(dummy_effnet_image)\n\n        # for fold, model in EFFNET_AUX_MODELS.items():\n        #     _ = model(dummy_effnet_aux_image)\n        \n        for model_dict in YOLO_MODELS:\n            model = model_dict[\"model\"]\n            _ = model.predict([dummy_yolo_image], verbose=False, device=device)\n\n\ndef predict_effnet_single_model(model: nn.Module, image: np.ndarray) -> np.ndarray:\n    \"\"\"Make prediction with a single EfficientNet model\"\"\"\n    image = image.transpose(1, 2, 0)  # (D,H,W) -> (H,W,D)\n    \n    transformed = EFFNET_TRANSFORM(image=image)\n    image_tensor = transformed['image']\n    image_tensor = image_tensor.unsqueeze(0).to(device)\n    \n    with torch.no_grad():\n        with autocast(enabled=EFFNET_CFG.use_amp):\n            output = model(image_tensor)\n            return torch.sigmoid(output).cpu().numpy().squeeze()\n\n\ndef predict_effnet_single_model(model: nn.Module, image_tensor) -> np.ndarray:\n    \"\"\"Make prediction with a single EfficientNet model\"\"\"\n    \n    with torch.no_grad():\n        with autocast(enabled=EFFNET_CFG.use_amp):\n            output = model(image_tensor)\n            return torch.sigmoid(output).cpu().numpy().squeeze()\n\ndef predict_effnet_ensemble(image: np.ndarray) -> np.ndarray:\n    \"\"\"Make EfficientNet ensemble prediction across all folds\"\"\"\n    all_predictions = []\n    weights = []\n\n    image = image.transpose(1, 2, 0)  # (D,H,W) -> (H,W,D)\n    transformed = EFFNET_TRANSFORM(image=image)\n    image_tensor = transformed['image']\n    image_tensor = image_tensor.unsqueeze(0).to(device)\n    \n    for fold, model in EFFNET_MODELS.items():\n        pred = predict_effnet_single_model(model, image_tensor)\n        all_predictions.append(pred)\n        weights.append(1.0)\n    \n    weights = np.array(weights) / np.sum(weights)\n    predictions = np.array(all_predictions)\n    return np.average(predictions, weights=weights, axis=0)\n\n\ndef predict_effnet_aux_single_model(model: nn.Module, image_tensor) -> np.ndarray:\n    \"\"\"Make prediction with a single EfficientNet model\"\"\"\n    \n    with torch.no_grad():\n        with autocast(enabled=EFFNET_AUX_LOSS_CFG.use_amp):\n            output = model(image_tensor)\n            return torch.sigmoid(output).cpu().item()\n\ndef predict_effnet_aux_ensemble(image: np.ndarray) -> np.ndarray:\n    \"\"\"Make EfficientNet ensemble prediction across all folds\"\"\"\n    all_predictions = []\n    weights = []\n\n    image = image.transpose(1, 2, 0)  # (D,H,W) -> (H,W,D)\n    transformed = EFFNET_AUX_TRANSFORM(image=image)\n    image_tensor = transformed['image']\n    image_tensor = image_tensor.unsqueeze(0).to(device)\n    \n    for fold, model in EFFNET_AUX_MODELS.items():\n        pred = predict_effnet_aux_single_model(model, image_tensor)\n        all_predictions.append(pred)\n        weights.append(1.0)\n    \n    weights = np.array(weights) / np.sum(weights)\n    predictions = np.array(all_predictions)\n    return np.average(predictions, weights=weights, axis=0)\n\ndef process_dicom_for_effnet(series_path: str) -> np.ndarray:\n    \"\"\"Process DICOM for EfficientNet with memory cleanup\"\"\"\n    try:\n        preprocessor = DICOMPreprocessorKaggle(target_shape=EFFNET_CFG.target_shape)\n        volume = preprocessor.process_series(series_path)\n        return volume\n    finally:\n        gc.collect()\n\ndef process_dicom_for_yolo(series_path):\n    series_path = Path(series_path)\n    dicom_files = collect_series_slices(series_path)\n\n    # Sort DICOM files by orientation+position before processing\n    dicom_files.sort(key=slice_sort_key)\n\n    with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:\n        results = list(executor.map(process_dicom_file, dicom_files))\n\n    # Flatten into (loc, img)\n    all_slices_with_loc = [item for sublist in results for item in sublist]\n\n    # Already sorted by dicom_files order, but double-check (safe)\n    all_slices_with_loc.sort(key=lambda x: x[0])\n\n    # Extract just the images\n    all_slices = [img for _, img in all_slices_with_loc]\n\n    # Now dicom_files matches the sorted slices\n    dcm_list = [f.stem for f in dicom_files]\n    return all_slices","metadata":{"_uuid":"e29b6075-398a-46c5-98e5-9108702439f4","_cell_guid":"71b8dead-f6d2-4299-9e44-c5c6e5492ea5","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-10-01T14:01:29.2733Z","iopub.execute_input":"2025-10-01T14:01:29.274015Z","iopub.status.idle":"2025-10-01T14:01:29.292813Z","shell.execute_reply.started":"2025-10-01T14:01:29.273991Z","shell.execute_reply":"2025-10-01T14:01:29.292115Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def nms_3d_points(points, iou_thresh=2.0):\n    if len(points) == 0:\n        return []\n\n    keep = []\n    pts = points[:, :3]  # z,y,x\n    scores = points[:, 3]\n    order = scores.argsort()[::-1]\n\n    suppressed = np.zeros(len(points), dtype=bool)\n\n    for idx in order:\n        if suppressed[idx]:\n            continue\n        keep.append(idx)\n        dists = np.linalg.norm(pts - pts[idx], axis=1)\n        suppressed[dists < iou_thresh] = True\n        suppressed[idx] = False  # keep current\n    return keep\n\n@torch.no_grad()\ndef predict_yolo_ensemble(series_path, slices, iou_thresh=2.0, k_candi = 3):\n    \"\"\"Run YOLO inference using all models\"\"\"\n    if not slices:\n        return 0.1, 0.1, np.ones(len(YOLO_LABELS)) * 0.1\n    \n    ensemble_cls_preds = []\n    ensemble_loc_preds = []\n    location_preds = {f'MODEL{i}': [] for i in range(len(YOLO_MODELS))}\n    total_weight = 0.0\n    \n    for model_idx, model_dict in enumerate(YOLO_MODELS):\n        model = model_dict[\"model\"]\n        weight = model_dict[\"weight\"]\n        \n        try:\n            max_conf_all = 0.0\n            per_class_max = np.zeros(len(YOLO_LABELS), dtype=np.float32)\n            \n            # Process in batches\n            for i in range(0, len(slices), BATCH_SIZE):\n                batch_slices = slices[i:i+BATCH_SIZE]\n                z_idxes = [i + batch_idx for batch_idx in range(len(batch_slices))]\n                results = model.predict(\n                    batch_slices, \n                    verbose=False, \n                    batch=len(batch_slices), \n                    device=device, \n                    conf=0.01\n                )\n                \n                for z_idx, r in enumerate(results):\n                    if r is None or r.boxes is None or r.boxes.conf is None or len(r.boxes) == 0:\n                        continue\n                    try:\n                        confs = r.boxes.conf\n                        clses = r.boxes.cls\n                        confs_ = r.boxes.conf.cpu().numpy()\n                        clses_ = r.boxes.cls.cpu().numpy()\n                        xyxy = r.boxes.xyxy.cpu().numpy()\n                        for j in range(len(confs)):\n                            #get yolo preds\n                            c = float(confs[j].item())\n                            k = int(clses[j].item())\n                            if c > max_conf_all:\n                                max_conf_all = c\n                            if 0 <= k < len(YOLO_LABELS) and c > per_class_max[k]:\n                                per_class_max[k] = c\n                                \n                            #get location preds\n                            x1, y1, x2, y2 = xyxy[j]\n                            x_center = (x1 + x2)/2\n                            y_center = (y1 + y2)/2\n                            point = np.array([z_idxes[z_idx], y_center, x_center, confs_[j], clses_[j], model_idx])\n                            location_preds[f'MODEL{model_idx}'].append(point)\n                    except Exception as e1:\n                        try:\n                            batch_max = float(r.boxes.conf.max().item())\n                            if batch_max > max_conf_all:\n                                max_conf_all = batch_max\n                        except Exception as e2:\n                            pass\n            \n            ensemble_cls_preds.append(max_conf_all * weight)\n            ensemble_loc_preds.append(per_class_max * weight)\n            total_weight += weight\n            \n        except Exception as e:\n            ensemble_cls_preds.append(0.1 * weight)\n            ensemble_loc_preds.append(np.ones(len(YOLO_LABELS)) * 0.1 * weight)\n            total_weight += weight\n\n    #yolo final predictions\n    if total_weight > 0:\n        final_cls_pred = sum(ensemble_cls_preds) / total_weight\n        final_loc_preds = sum(ensemble_loc_preds) / total_weight\n    else:\n        final_cls_pred = 0.1\n        final_loc_preds = np.ones(len(YOLO_LABELS)) * 0.1\n\n    # multi-view patch prediction\n    final_preds = {f'MODEL{i}': [] for i in range(len(YOLO_MODELS))}\n    patch_cls_preds = []\n    try:\n        for fold_id, (model_key, points) in enumerate(location_preds.items()):\n            points = np.array(points)\n            if points.shape[0] < PatchModelConfig.k_candi:\n                continue\n            for cls in np.unique(points[:,4]):\n                cls_points = points[points[:,4]==cls]\n                keep_idx = nms_3d_points(cls_points, iou_thresh=iou_thresh)\n                final_preds[model_key].extend(cls_points[keep_idx])\n            if len(final_preds[model_key])<PatchModelConfig.k_candi:\n                continue\n            model_preds = torch.tensor(final_preds[model_key])\n            values, indices = model_preds[:, -3].topk(k_candi)   # get top-k values & indices\n            final_preds[model_key] = model_preds[indices].numpy()\n        patch_cls_preds = multi_view_patch_prediction(series_path, final_preds)\n    except Exception as ee:\n        patch_cls_preds = 0.\n        print(ee)\n    return patch_cls_preds, final_cls_pred, final_loc_preds","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T14:01:32.605164Z","iopub.execute_input":"2025-10-01T14:01:32.605677Z","iopub.status.idle":"2025-10-01T14:01:32.620169Z","shell.execute_reply.started":"2025-10-01T14:01:32.605654Z","shell.execute_reply":"2025-10-01T14:01:32.619337Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def _predict_inner(series_path: str) -> pl.DataFrame:\n    \"\"\"Main ensemble prediction logic\"\"\"\n    global EFFNET_MODELS, YOLO_MODELS, EFFNET_AUX_MODELS\n    \n    # Load models if not already loaded\n    if not EFFNET_MODELS or not YOLO_MODELS or not EFFNET_AUX_MODELS:\n        load_all_models()\n    \n    try:\n        # Process DICOM for both models\n        # effnet_volume, effnet_aux_volume = process_dicom_for_effnet(series_path)\n        yolo_slices = process_dicom_for_yolo(series_path)\n        \n        # Get EfficientNet predictions\n        #effnet_preds = predict_effnet_ensemble(effnet_volume)\n\n        # # Get 3d EfficientNet predictions\n        # effnet_aux_cls_pred = predict_effnet_aux_ensemble(effnet_aux_volume)\n        \n        # Get YOLO predictions\n        patch_cls_pred, yolo_cls_pred, yolo_loc_preds = predict_yolo_ensemble(series_path, yolo_slices,\n                                                              PatchModelConfig.iou_thresh, \n                                                              PatchModelConfig.k_candi)\n        #print(patch_cls_pred, yolo_cls_pred)\n\n        #generate yolo preds match the label cols\n        yolo_full_preds = np.zeros(len(LABEL_COLS))\n        for i, label in enumerate(YOLO_LABELS):\n            if label in LABEL_COLS:\n                label_idx = LABEL_COLS.index(label)\n                yolo_full_preds[label_idx] = yolo_loc_preds[i]\n        aneurysm_idx = LABEL_COLS.index('Aneurysm Present')\n        #yolo_full_preds[aneurysm_idx] = patch_cls_pred if patch_cls_pred!=0 else yolo_cls_pred\n        yolo_full_preds[aneurysm_idx] = 0.7 * yolo_cls_pred + 0.3 * patch_cls_pred\\\n                       if patch_cls_pred!=0 else yolo_cls_pred\n\n        # generate eff preds match the label cols\n        #eff_full_preds = effnet_3d_preds\n        #eff_full_preds = np.zeros(len(LABEL_COLS))\n        #eff_full_preds_3d = np.zeros(len(LABEL_COLS))\n        #for i, label in enumerate(EFF_LABELS):\n        #     if label in LABEL_COLS:\n        #         label_idx = LABEL_COLS.index(label)\n        #         eff_full_preds[label_idx] = effnet_preds[i]\n        #         #eff_full_preds_3d[label_idx] = effnet_3d_preds[i]\n\n        #print(eff_full_preds)\n        #print(eff_full_preds_3d)\n        #print(yolo_full_preds)\n        # Ensemble: 50% EfficientNet + 50% YOLO  \n        #ensemble_preds = 0.3 * eff_full_preds + 0.7 * yolo_full_preds\n        ensemble_preds = yolo_full_preds\n        \n        # Create output dataframe\n        predictions_df = pl.DataFrame(\n            data=[ensemble_preds.tolist()],\n            schema=LABEL_COLS,\n            orient='row'\n        )\n        \n        return predictions_df\n\n        \n    except Exception as e:\n        print(e)\n        # Return conservative predictions\n        conservative_preds = [0.1] * len(LABEL_COLS)\n        predictions_df = pl.DataFrame(\n            data=[conservative_preds],\n            schema=LABEL_COLS,\n            orient='row'\n        )\n        return predictions_df\n\ndef predict(series_path: str) -> pl.DataFrame:\n    \"\"\"\n    Top-level prediction function passed to the server.\n    Combines YOLO and EfficientNet with 50/50 weighting.\n    \"\"\"\n    try:\n        return _predict_inner(series_path)\n    except Exception as e:\n        print(f\"Error during prediction for {os.path.basename(series_path)}: {e}\")\n        print(\"Using fallback predictions.\")\n        conservative_preds = [0.1] * len(LABEL_COLS)\n        predictions = pl.DataFrame(\n            data=[conservative_preds],\n            schema=LABEL_COLS,\n            orient='row'\n        )\n        return predictions\n    finally:\n        # Cleanup\n        shared_dir = '/kaggle/shared'\n        shutil.rmtree(shared_dir, ignore_errors=True)\n        os.makedirs(shared_dir, exist_ok=True)\n        \n        # Memory cleanup\n        if torch.cuda.is_available():\n            torch.cuda.empty_cache()\n        gc.collect()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T14:01:32.760529Z","iopub.execute_input":"2025-10-01T14:01:32.760825Z","iopub.status.idle":"2025-10-01T14:01:32.769866Z","shell.execute_reply.started":"2025-10-01T14:01:32.760805Z","shell.execute_reply":"2025-10-01T14:01:32.76916Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!ls /kaggle/input/rsna-intracranial-aneurysm-detection","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T14:01:34.733149Z","iopub.execute_input":"2025-10-01T14:01:34.733425Z","iopub.status.idle":"2025-10-01T14:01:34.888589Z","shell.execute_reply.started":"2025-10-01T14:01:34.733405Z","shell.execute_reply":"2025-10-01T14:01:34.88784Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if __name__ == \"__main__\":\n    start_time = time.time()\n    \n    # Initialize the inference server\n    inference_server = kaggle_evaluation.rsna_inference_server.RSNAInferenceServer(predict)\n    \n    # Check if running in competition environment\n    if os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n        inference_server.serve()\n    else:\n        inference_server.run_local_gateway()\n        \n        # Display results if in local mode\n        submission_df = pl.read_parquet('/kaggle/working/submission.parquet')\n        print(f\"Submission shape: {submission_df.shape}\")\n        display(submission_df)\n    \n    print(f\"Total execution time: {time.time() - start_time:.2f} seconds\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T14:01:34.891161Z","iopub.execute_input":"2025-10-01T14:01:34.891414Z","iopub.status.idle":"2025-10-01T14:02:53.638016Z","shell.execute_reply.started":"2025-10-01T14:01:34.891392Z","shell.execute_reply":"2025-10-01T14:02:53.637189Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}