{"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":"none","dataSources":[{"sourceId":99552,"databundleVersionId":13851420,"sourceType":"competition"},{"sourceId":267657842,"sourceType":"kernelVersion"}],"dockerImageVersionId":31192,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-11T17:50:22.856825Z","iopub.execute_input":"2025-11-11T17:50:22.857263Z"}},"outputs":[],"execution_count":null},{"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)\nclass 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# 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":"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) -> np.ndarray:\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\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":"code","source":"# Path to your trained model\nMODEL_PATH = \"/kaggle/input/train-model-for-aneurysm-3/best_model.pth\"  # Update this path\n\n# Create and load model\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nmodel = Simple3DCNN(num_labels=len(LABEL_COLS)).to(device)\n\ntry:\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 {MODEL_PATH}\")\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 = Simple3DCNN(num_labels=len(LABEL_COLS)).to(device)\n    model.eval()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"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 = process_dicom_series(series_path)\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":"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}]}