{"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":266728311,"sourceType":"kernelVersion"},{"sourceId":266728536,"sourceType":"kernelVersion"}],"dockerImageVersionId":31090,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# <div style=\"color:#18ad4c; font-family: 'Segoe UI'; text-align: center; border-top:5px solid #3e3ec7; padding-left:10px; background-color:#F8F9F9; padding:10px; border-radius:5px;font-weight: bold\">🩻 3️⃣🇩 CNN Neural Network</div>","metadata":{}},{"cell_type":"markdown","source":"# <div style=\"color:#18ad4c; font-family: 'Segoe UI'; text-align: center; border-top:5px solid #3e3ec7; padding-left:10px; background-color:#F8F9F9; padding:10px; border-radius:5px;font-weight: bold\">🗂️ Step 1: Setup - Import Required Libraries</div>","metadata":{}},{"cell_type":"code","source":"# Basic imports\nimport os\nimport numpy as np\nimport pydicom\nfrom scipy import ndimage\nimport torch\nimport torch.nn as nn\nfrom torch import sigmoid\nimport polars as pl\n\n# Competition-specific imports\nimport kaggle_evaluation.rsna_inference_server\n\n# # Our model architecture (must match training)\n# class Simple3DCNN(nn.Module):\n#     def __init__(self, num_labels=14):\n#         super(Simple3DCNN, self).__init__()\n        \n#         # Same architecture as in training\n#         self.conv1 = nn.Conv3d(1, 8, kernel_size=3, padding=1)\n#         self.pool1 = nn.MaxPool3d(2)\n        \n#         self.conv2 = nn.Conv3d(8, 16, kernel_size=3, padding=1)\n#         self.pool2 = nn.MaxPool3d(2)\n        \n#         self.conv3 = nn.Conv3d(16, 32, kernel_size=3, padding=1)\n#         self.pool3 = nn.MaxPool3d(2)\n        \n#         self.flatten = nn.Flatten()\n#         self.fc1 = nn.Linear(32 * 8 * 8 * 8, 128)\n#         self.fc2 = nn.Linear(128, num_labels)\n    \n#     def forward(self, x):\n#         x = self.pool1(torch.relu(self.conv1(x)))\n#         x = self.pool2(torch.relu(self.conv2(x)))\n#         x = self.pool3(torch.relu(self.conv3(x)))\n#         x = self.flatten(x)\n#         x = torch.relu(self.fc1(x))\n#         x = self.fc2(x)\n#         return x\n\n# Our IMPROVED model architecture (must match training)\nclass ResidualBlock3D(nn.Module):\n    \"\"\"Properly handles channel dimension changes in residual connections\"\"\"\n    def __init__(self, in_channels, out_channels, stride=1):\n        super(ResidualBlock3D, self).__init__()\n        \n        self.conv1 = nn.Conv3d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)\n        self.bn1 = nn.BatchNorm3d(out_channels)\n        self.relu = nn.ReLU(inplace=True)\n        self.conv2 = nn.Conv3d(out_channels, out_channels, kernel_size=3, padding=1, bias=False)\n        self.bn2 = nn.BatchNorm3d(out_channels)\n        \n        # Projection shortcut if dimensions change\n        self.projection = None\n        if stride != 1 or in_channels != out_channels:\n            self.projection = nn.Sequential(\n                nn.Conv3d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),\n                nn.BatchNorm3d(out_channels)\n            )\n    \n    def forward(self, x):\n        identity = x\n        \n        out = self.conv1(x)\n        out = self.bn1(out)\n        out = self.relu(out)\n        \n        out = self.conv2(out)\n        out = self.bn2(out)\n        \n        if self.projection is not None:\n            identity = self.projection(x)\n            \n        out += identity\n        out = self.relu(out)\n        \n        return out\n\nclass Improved3DModel(nn.Module):\n    \"\"\"Fixed architecture with proper residual connections\"\"\"\n    def __init__(self, num_labels=14):\n        super(Improved3DModel, self).__init__()\n        \n        # Initial feature extraction\n        self.initial = nn.Sequential(\n            nn.Conv3d(1, 32, kernel_size=3, stride=1, padding=1, bias=False),\n            nn.BatchNorm3d(32),\n            nn.ReLU(inplace=True),\n            nn.Conv3d(32, 32, kernel_size=3, stride=1, padding=1, bias=False),\n            nn.BatchNorm3d(32),\n            nn.ReLU(inplace=True)\n        )\n        \n        # Residual blocks with proper downsampling\n        self.res1 = ResidualBlock3D(32, 32, stride=1)\n        self.pool1 = nn.MaxPool3d(2)\n        \n        self.res2 = ResidualBlock3D(32, 64, stride=1)\n        self.pool2 = nn.MaxPool3d(2)\n        \n        self.res3 = ResidualBlock3D(64, 128, stride=1)\n        self.pool3 = nn.MaxPool3d(2)\n        \n        self.res4 = ResidualBlock3D(128, 256, stride=1)\n        \n        # Global pooling and classification\n        self.global_pool = nn.AdaptiveAvgPool3d(1)\n        self.dropout = nn.Dropout(0.5)\n        self.fc1 = nn.Linear(256, 128)\n        self.relu = nn.ReLU()\n        self.dropout2 = nn.Dropout(0.3)\n        \n        # Separate heads for main label and locations\n        self.main_head = nn.Linear(128, 1)\n        self.location_head = nn.Linear(128, 13)\n    \n    def forward(self, x):\n        x = self.initial(x)\n        x = self.res1(x)\n        x = self.pool1(x)\n        x = self.res2(x)\n        x = self.pool2(x)\n        x = self.res3(x)\n        x = self.pool3(x)\n        x = self.res4(x)\n        \n        x = self.global_pool(x)\n        x = x.view(x.size(0), -1)\n        x = self.dropout(x)\n        \n        x = self.fc1(x)\n        x = self.relu(x)\n        x = self.dropout2(x)\n        \n        main_pred = self.main_head(x)\n        location_preds = self.location_head(x)\n        \n        return torch.cat([location_preds, main_pred], dim=1)\n\n# Constants (must match preprocessing)\nTARGET_SIZE = (64, 64, 64)\nCTA_WINDOW = (300.0, 700.0)\nMRI_Z_CLIP = 3.0\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# Check if we're running in competition environment\nIS_COMPETITION = os.getenv('KAGGLE_IS_COMPETITION_RERUN') is not None\nprint(f\"Running in {'competition' if IS_COMPETITION else 'local test'} mode\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# <div style=\"color:#18ad4c; font-family: 'Segoe UI'; text-align: center; border-top:5px solid #3e3ec7; padding-left:10px; background-color:#F8F9F9; padding:10px; border-radius:5px;font-weight: bold\">💎 Step 2: DICOM Processing for Inference</div>","metadata":{}},{"cell_type":"code","source":"# Simplified DICOM processor for inference\ndef _safe_zoom(volume: np.ndarray, zoom_factors: tuple, order: int = 1) -> np.ndarray:\n    \"\"\"Safely resize a volume.\"\"\"\n    volume = np.nan_to_num(volume, copy=False)\n    zf = tuple(max(1e-6, f) for f in zoom_factors)\n    if len(zf) != volume.ndim:\n        if len(zf) > volume.ndim:\n            zf = zf[:volume.ndim]\n        else:\n            zf = (1.0,) * (volume.ndim - len(zf)) + zf\n    return ndimage.zoom(volume, zf, order=1)\n\ndef _resize_slice(arr: np.ndarray, out_h: int, out_w: int) -> np.ndarray:\n    \"\"\"Resize a 2D slice.\"\"\"\n    h, w = arr.shape\n    if h == out_h and w == out_w:\n        return arr.astype(np.float32, copy=False)\n    zy = out_h / max(h, 1)\n    zx = out_w / max(w, 1)\n    return _safe_zoom(arr, (zy, zx), order=1).astype(np.float32, copy=False)\n\ndef process_dicom_series(series_path: str) -> tuple:\n    \"\"\"Process a single DICOM series into a normalized 64x64x64 volume.\"\"\"\n    try:\n        # Collect DICOM files\n        dicoms = []\n        for root, _, files in os.walk(series_path):\n            for f in files:\n                if f.endswith(\".dcm\"):\n                    try:\n                        ds = pydicom.dcmread(os.path.join(root, f), force=True)\n                        if hasattr(ds, \"PixelData\"):\n                            dicoms.append(ds)\n                    except:\n                        continue\n        \n        if not dicoms:\n            raise ValueError(\"No valid DICOM files found\")\n        \n        # Sort slices\n        try:\n            orient = np.array(dicoms[0].ImageOrientationPatient, dtype=np.float32)\n            row = orient[:3]\n            col = orient[3:]\n            normal = np.cross(row, col)\n            def sort_key(ds):\n                ipp = np.array(getattr(ds, \"ImagePositionPatient\", [0, 0, 0]), dtype=np.float32)\n                return float(np.dot(ipp, normal))\n            dicoms = sorted(dicoms, key=sort_key)\n        except:\n            dicoms = sorted(dicoms, key=lambda ds: getattr(ds, \"InstanceNumber\", 0))\n        \n        # Get spacing\n        try:\n            dy, dx = map(float, dicoms[0].PixelSpacing)\n        except:\n            dy, dx = 1.0, 1.0\n            \n        zs = []\n        for i in range(1, len(dicoms)):\n            p0 = np.array(getattr(dicoms[i-1], \"ImagePositionPatient\", [0, 0, 0]), dtype=np.float32)\n            p1 = np.array(getattr(dicoms[i], \"ImagePositionPatient\", [0, 0, 0]), dtype=np.float32)\n            d = np.linalg.norm(p1 - p0)\n            if d > 0:\n                zs.append(d)\n        dz = float(np.median(zs)) if zs else float(getattr(dicoms[0], \"SliceThickness\", 1.0))\n        \n        # Choose base shape\n        shapes = []\n        for ds in dicoms:\n            try:\n                h, w = int(ds.Rows), int(ds.Columns)\n            except:\n                arr = ds.pixel_array\n                h, w = arr.shape[-2], arr.shape[-1]\n            shapes.append((h, w))\n        base_h, base_w = max(shapes, key=shapes.count)\n        \n        # Process slices\n        vol_slices = []\n        modality_tag = (getattr(dicoms[0], \"Modality\", \"\") or \"\").upper()\n        \n        for ds in dicoms:\n            arr = ds.pixel_array\n            if arr.ndim >= 3:\n                h, w = arr.shape[-2], arr.shape[-1]\n                n = int(np.prod(arr.shape[:-2]))\n                arr = arr.reshape(n, h, w)\n                frames = arr\n            else:\n                frames = arr[np.newaxis, ...]\n            \n            for sl in frames:\n                sl = sl.astype(np.float32)\n                \n                # Handle MONOCHROME1\n                if getattr(ds, \"PhotometricInterpretation\", \"MONOCHROME2\") == \"MONOCHROME1\":\n                    sl = sl.max() - sl\n                \n                # Apply rescaling\n                slope = float(getattr(ds, \"RescaleSlope\", 1.0))\n                intercept = float(getattr(ds, \"RescaleIntercept\", 0.0))\n                sl = sl * slope + intercept\n                sl = np.nan_to_num(sl, copy=False)\n                \n                # Resize\n                sl = _resize_slice(sl, base_h, base_w)\n                vol_slices.append(sl)\n        \n        if not vol_slices:\n            raise ValueError(\"No valid slices extracted\")\n        \n        volume = np.stack(vol_slices, axis=0).astype(np.float32)\n        \n        # Normalize by modality\n        if modality_tag == \"CT\":\n            c, w = CTA_WINDOW\n            lo, hi = c - w / 2.0, c + w / 2.0\n            volume = np.clip(volume, lo, hi)\n            volume = (volume - lo) / (hi - lo + 1e-6)\n        else:\n            mean = float(volume.mean())\n            std = float(volume.std() + 1e-6)\n            volume = (volume - mean) / std\n            volume = np.clip(volume, -MRI_Z_CLIP, MRI_Z_CLIP)\n            volume = (volume + MRI_Z_CLIP) / (2.0 * MRI_Z_CLIP)\n        \n        # Resample to target size\n        tz, ty, tx = TARGET_SIZE\n        z, y, x = volume.shape\n        volume = _safe_zoom(volume, (tz/z, ty/y, tx/x), order=1).astype(np.float32)\n        \n        return volume, modality_tag\n    \n    except Exception as e:\n        # Return a zero volume if processing fails\n        print(f\"Error processing {series_path}: {str(e)}\")\n        return np.zeros(TARGET_SIZE, dtype=np.float32)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# <div style=\"color:#18ad4c; font-family: 'Segoe UI'; text-align: center; border-top:5px solid #3e3ec7; padding-left:10px; background-color:#F8F9F9; padding:10px; border-radius:5px;font-weight: bold\">💎 Step 3: Load Trained Model</div>","metadata":{}},{"cell_type":"code","source":"# Set Device\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\ntry:\n    # Load CT model\n    ct_model_path = \"/kaggle/input/ct-train-model-for-aneurysm-3/final_full_model.pth\"  # CT model path\n    ct_model = Improved3DModel(num_labels=len(LABEL_COLS)).to(device)\n    ct_model.load_state_dict(torch.load(ct_model_path, map_location=device))\n    ct_model.eval()\n    \n    # Load MR model\n    mr_model_path = \"/kaggle/input/mr-train-model-for-aneurysm-3/final_full_model.pth\"  # MR model path\n    mr_model = Improved3DModel(num_labels=len(LABEL_COLS)).to(device)\n    mr_model.load_state_dict(torch.load(mr_model_path, map_location=device))\n    mr_model.eval()\n    # # Try to load the model\n    # state_dict = torch.load(MODEL_PATH, map_location=device)\n    # model.load_state_dict(state_dict)\n    # model.eval()  # Set to evaluation mode\n    print(f\"Model loaded successfully from\")\nexcept Exception as e:\n    print(f\"Error loading model: {str(e)}\")\n    print(\"Creating a random model for demonstration (will not score well)\")\n    model = Improved3DModel(num_labels=len(LABEL_COLS)).to(device)\n    model.eval()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# <div style=\"color:#18ad4c; font-family: 'Segoe UI'; text-align: center; border-top:5px solid #3e3ec7; padding-left:10px; background-color:#F8F9F9; padding:10px; border-radius:5px;font-weight: bold\">🎯 Step 4: Prediction Function </div>","metadata":{}},{"cell_type":"code","source":"# PREDICT FUNCTION DEFINITION\ndef predict(series_path: str) -> pl.DataFrame:\n    \"\"\"\n    Process a DICOM series and return predictions.\n    \n    Args:\n        series_path: Path to the DICOM series directory\n        \n    Returns:\n        Polars DataFrame with predictions for all 14 labels\n    \"\"\"\n    # Extract SeriesInstanceUID from the path\n    series_id = os.path.basename(series_path)\n    \n    # Process the DICOM series\n    volume, modality = process_dicom_series(series_path)\n\n    # Select model based on modality\n    if modality == \"CT\":\n        model = ct_model\n        # print(f\"Using CT model for series {series_id}\")\n    else:  # For MR, CR, etc.\n        model = mr_model\n        # print(f\"Using MR model for series {series_id} (modality: {modality})\")\n    \n    # Prepare for model input\n    volume_tensor = torch.FloatTensor(volume).unsqueeze(0).unsqueeze(0).to(device)  # (1,1,D,H,W)\n    \n    # Get predictions\n    with torch.no_grad():\n        outputs = model(volume_tensor)\n        probs = sigmoid(outputs).cpu().numpy()[0]  # Convert to probabilities\n    \n    # Create DataFrame with predictions\n    # Must have exactly the 14 label columns\n    predictions = {\n        'SeriesInstanceUID': [series_id],\n    }\n    \n    # Add all label predictions\n    for i, col in enumerate(LABEL_COLS):\n        predictions[col] = [float(probs[i])]\n    \n    return pl.DataFrame(predictions)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# <div style=\"color:#18ad4c; font-family: 'Segoe UI'; text-align: center; border-top:5px solid #3e3ec7; padding-left:10px; background-color:#F8F9F9; padding:10px; border-radius:5px;font-weight: bold\">🖥️ Step 5: Inference Server Setup</div>","metadata":{}},{"cell_type":"code","source":"# INFERENCE SERVER\ninference_server = kaggle_evaluation.rsna_inference_server.RSNAInferenceServer(predict)\n\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    # This runs when submitting to the competition\n    print(\"Running in competition mode - serving predictions\")\n    inference_server.serve()\nelse:\n    # This runs when testing locally\n    print(\"Running in local test mode\")\n    inference_server.run_local_gateway()\n    \n    # Display the submission file for verification\n    submission_path = '/kaggle/working/submission.parquet'\n    if os.path.exists(submission_path):\n        print(\"\\nLocal test submission results:\")\n        display(pl.read_parquet(submission_path))\n        print(\"\\nSubmission file saved to:\", submission_path)\n    else:\n        print(\"Warning: Submission file not created. Check your predict function.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}