{"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":12780021,"sourceType":"datasetVersion","datasetId":8079690}],"dockerImageVersionId":31090,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## Pipeline\n1. **DICOM → 3D Volume**: Normalize to `(32, 384, 384)`\n2. **EfficientNetV2-S**: 32-channel input, 14 binary outputs\n3. **Ensemble**: Average 5-fold predictions","metadata":{}},{"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\nwarnings.filterwarnings('ignore')\n\nclass DICOMPreprocessorKaggle:\n    \"\"\"\n    DICOM preprocessing system for Kaggle Code Competition\n    Converts original DICOMPreprocessor logic to single series processing\n    \"\"\"\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        \"\"\"\n        Load DICOM series\n        \"\"\"\n        series_path = Path(series_path)\n        series_name = series_path.name\n        \n        # Search for DICOM files\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        #print(f\"Found {len(dicom_files)} DICOM files in series {series_name}\")\n        \n        # Load DICOM datasets\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                #print(f\"Failed to load {filepath}: {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        \"\"\"\n        Extract position information for each slice\n        \"\"\"\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            # Get z-coordinate from ImagePositionPatient and ImageOrientationPatient            \n            try:\n                ipp = np.array(getattr(ds, 'ImagePositionPatient', None))\n                iop = np.array(getattr(ds, 'ImageOrientationPatient', None))\n                n_vec = np.cross(iop[:3],iop[3:])\n                info['z_position'] = -float((ipp*n_vec).sum())\n            except Exception as e:\n                info['z_position'] = float(i)\n                #print(f\"Failed to extract position info: {e}\")\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        \"\"\"\n        Sort slices by z-coordinate\n        \"\"\"\n        # Sort by z-coordinate\n        sorted_slices = sorted(slice_info, key=lambda x: x['z_position'])\n        \n        #print(f\"Sorted {len(sorted_slices)} slices by z-position\")\n        #print(f\"Z-range: {sorted_slices[0]['z_position']:.2f} to {sorted_slices[-1]['z_position']:.2f}\")\n        \n        return sorted_slices\n    \n    def get_windowing_params(self, ds: pydicom.Dataset, img: np.ndarray = None) -> Tuple[Optional[float], Optional[float]]:\n        \"\"\"\n        Get windowing parameters based on modality\n        \"\"\"\n        modality = getattr(ds, 'Modality', 'CT')\n        \n        if modality == 'CT':\n            # For CT, apply CTA (angiography) settings\n            center, width = (50, 350)\n            #print(f\"Using CTA windowing for CT: Center={center}, Width={width}\")\n            # return center, width\n            return \"CT\", \"CT\"\n            \n        elif modality == 'MR':\n            # For MR, skip windowing (statistical normalization only)\n            #print(\"MR modality detected: skipping windowing, using statistical normalization\")\n            return None, None\n            \n        else:\n            # Unexpected modality (safety measure)\n            #print(f\"Unexpected modality '{modality}', using CTA windowing\")\n            #return (50, 350)\n            return None, None\n    \n    def apply_windowing_or_normalize(self, img: np.ndarray, center: Optional[float], width: Optional[float]) -> np.ndarray:\n        \"\"\"\n        Apply windowing or statistical normalization\n        \"\"\"\n        if center is not None and width is not None:\n            # # Windowing processing (for CT/CTA)\n            # img_min = center - width / 2\n            # img_max = center + width / 2\n            \n            # windowed = np.clip(img, img_min, img_max)\n            # windowed = (windowed - img_min) / (img_max - img_min + 1e-7)\n            # result = (windowed * 255).astype(np.uint8)\n            \n            # #print(f\"Applied windowing: [{img_min:.1f}, {img_max:.1f}] → [0, 255]\")\n            # return result\n            \n            # Statistical normalization (for CT as well)\n            # Normalize using 1-99 percentiles\n            p1, p99 = np.percentile(img, [1, 99])\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                \n                #print(f\"Applied statistical normalization: [{p1:.1f}, {p99:.1f}] → [0, 255]\")\n                return result\n            else:\n                # Fallback: min-max normalization\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                    #print(f\"Applied min-max normalization: [{img_min:.1f}, {img_max:.1f}] → [0, 255]\")\n                    return result\n                else:\n                    # If image has no variation\n                    #print(\"Image has no variation, returning zeros\")\n                    return np.zeros_like(img, dtype=np.uint8)\n        \n        else:\n            # Statistical normalization (for MR)\n            # Normalize using 1-99 percentiles\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                \n                #print(f\"Applied statistical normalization: [{p1:.1f}, {p99:.1f}] → [0, 255]\")\n                return result\n            else:\n                # Fallback: min-max normalization\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                    #print(f\"Applied min-max normalization: [{img_min:.1f}, {img_max:.1f}] → [0, 255]\")\n                    return result\n                else:\n                    # If image has no variation\n                    #print(\"Image has no variation, returning zeros\")\n                    return np.zeros_like(img, dtype=np.uint8)\n    \n    def extract_pixel_array(self, ds: pydicom.Dataset) -> np.ndarray:\n        \"\"\"\n        Extract 2D pixel array from DICOM and apply preprocessing (for 2D DICOM series)\n        \"\"\"\n        # Get pixel data\n        img = ds.pixel_array.astype(np.float32)\n        \n        # For 3D volume case (multiple frames) - select middle frame\n        if img.ndim == 3:\n            #print(f\"3D DICOM in 2D processing - using middle frame from shape: {img.shape}\")\n            frame_idx = img.shape[0] // 2\n            img = img[frame_idx]\n            #print(f\"Selected frame {frame_idx} from 3D DICOM\")\n        \n        # Convert color image to grayscale\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            #print(\"Converted color image to grayscale\")\n        \n        # Apply RescaleSlope and RescaleIntercept\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            #print(f\"Applied rescaling: slope={slope}, intercept={intercept}\")\n        \n        return img\n    \n    def resize_volume_3d(self, volume: np.ndarray) -> np.ndarray:\n        \"\"\"\n        Resize 3D volume to target size\n        \"\"\"\n        current_shape = volume.shape\n        target_shape = (self.target_depth, self.target_height, self.target_width)\n        \n        if current_shape == target_shape:\n            return volume\n        \n        #print(f\"Resizing volume from {current_shape} to {target_shape}\")\n        \n        # 3D resizing using scipy.ndimage\n        zoom_factors = [\n            target_shape[i] / current_shape[i] for i in range(3)\n        ]\n        \n        # Resize with linear interpolation\n        resized_volume = ndimage.zoom(volume, zoom_factors, order=1, mode='nearest')\n        \n        # Clip to exact size just in case\n        resized_volume = resized_volume[:self.target_depth, :self.target_height, :self.target_width]\n        \n        # Padding if necessary\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        #print(f\"Final volume shape: {resized_volume.shape}\")\n        return resized_volume.astype(np.uint8)\n    \n    def process_series(self, series_path: str) -> np.ndarray:\n        \"\"\"\n        Process DICOM series and return as NumPy array (for Kaggle: no file saving)\n        \"\"\"\n        try:\n            # 1. Load DICOM files\n            datasets, series_name = self.load_dicom_series(series_path)\n            \n            # Check first DICOM to determine 3D/2D\n            first_ds = datasets[0]\n            first_img = first_ds.pixel_array\n            \n            if len(datasets) == 1 and first_img.ndim == 3:\n                # Case 1: Single 3D DICOM file\n                #print(f\"Processing single 3D DICOM with shape: {first_img.shape}\")\n                return self._process_single_3d_dicom(first_ds, series_name)\n            else:\n                # Case 2: Multiple 2D DICOM files\n                #print(f\"Processing {len(datasets)} 2D DICOM files\")\n                return self._process_multiple_2d_dicoms(datasets, series_name)\n            \n        except Exception as e:\n            #print(f\"Failed to process series {series_path}: {e}\")\n            raise\n    \n    def _process_single_3d_dicom(self, ds: pydicom.Dataset, series_name: str) -> np.ndarray:\n        \"\"\"\n        Process single 3D DICOM file (for Kaggle: no file saving)\n        \"\"\"\n        # Get pixel array\n        volume = ds.pixel_array.astype(np.float32)\n        \n        # Apply RescaleSlope and RescaleIntercept\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            # #print(f\"Applied rescaling: slope={slope}, intercept={intercept}\")\n        \n        # Get windowing settings\n        window_center, window_width = self.get_windowing_params(ds)\n        \n        # Apply windowing to each slice\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        ##print(f\"3D volume shape after windowing: {volume.shape}\")\n        \n        # 3D resize\n        final_volume = self.resize_volume_3d(volume)\n        \n        ##print(f\"Successfully processed 3D DICOM series {series_name}\")\n        return final_volume\n    \n    def _process_multiple_2d_dicoms(self, datasets: List[pydicom.Dataset], series_name: str) -> np.ndarray:\n        \"\"\"\n        Process multiple 2D DICOM files (for Kaggle: no file saving)\n        \"\"\"\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            resized_img = cv2.resize(processed_img, (self.target_width, self.target_height))\n            \n            processed_slices.append(resized_img)\n\n        volume = np.stack(processed_slices, axis=0)\n        ##print(f\"2D slices stacked to volume shape: {volume.shape}\")\n        final_volume = self.resize_volume_3d(volume)\n        \n        ##print(f\"Successfully processed 2D DICOM series {series_name}\")\n        return final_volume\n\ndef process_dicom_series_kaggle(series_path: str, target_shape: Tuple[int, int, int] = (32, 384, 384)) -> np.ndarray:\n    \"\"\"\n    DICOM processing function for Kaggle inference (single series)\n    \n    Args:\n        series_path: Path to DICOM series\n        target_shape: Target volume size (depth, height, width)\n    \n    Returns:\n        np.ndarray: Processed volume\n    \"\"\"\n    preprocessor = DICOMPreprocessorKaggle(target_shape=target_shape)\n    return preprocessor.process_series(series_path)\n\n# Safe processing function with memory cleanup\ndef process_dicom_series_safe(series_path: str, target_shape: Tuple[int, int, int] = (32, 384, 384)) -> np.ndarray:\n    \"\"\"\n    Safe DICOM processing with memory cleanup\n    \n    Args:\n        series_path: Path to DICOM series\n        target_shape: Target volume size (depth, height, width)\n    \n    Returns:\n        np.ndarray: Processed volume\n    \"\"\"\n    try:\n        volume = process_dicom_series_kaggle(series_path, target_shape)\n        return volume\n    finally:\n        # Memory cleanup\n        gc.collect()\n\n# Test function\ndef test_single_series(series_path: str, target_shape: Tuple[int, int, int] = (32, 384, 384)):\n    \"\"\"\n    Test processing for single series\n    \"\"\"\n    try:\n        #print(f\"Testing single series: {series_path}\")\n        \n        # Execute processing\n        volume = process_dicom_series_safe(series_path, target_shape)\n        \n        # Display results\n        #print(f\"✓ Successfully processed series\")\n        #print(f\"  Volume shape: {volume.shape}\")\n        #print(f\"  Volume dtype: {volume.dtype}\")\n        #print(f\"  Volume range: [{volume.min()}, {volume.max()}]\")\n        \n        return volume\n        \n    except Exception as e:\n        #print(f\"✗ Failed to process series: {e}\")\n        return None","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-10-02T17:08:51.846419Z","iopub.execute_input":"2025-10-02T17:08:51.847222Z","iopub.status.idle":"2025-10-02T17:08:51.884844Z","shell.execute_reply.started":"2025-10-02T17:08:51.847196Z","shell.execute_reply":"2025-10-02T17:08:51.88395Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import sys\nimport gc\nimport json\nimport shutil\nimport warnings\nwarnings.filterwarnings('ignore')\nfrom pathlib import Path\nfrom typing import List, Dict, Optional, Tuple\n\n# Data handling\nimport numpy as np\nimport polars as pl\nimport pandas as pd\n\n# Medical imaging\nimport pydicom\nimport cv2\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\n\n# Transformations\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\n# Competition API\nimport kaggle_evaluation.rsna_inference_server\n\n# DICOM preprocessor (DICOMPreprocessorKaggle class defined in previous cell)\n# In actual use, define in the same file or import appropriately\n\n# Set device\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n#print(f\"Using device: {device}\")\n\n# ====================================================\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]\nprint(np.array(LABEL_COLS)[[1,0,3,2,5,4,6,8,7,10,9,11,12,13]])\n\n# ====================================================\n# Configuration\n# ====================================================\nclass InferenceConfig:\n    # Model settings\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    \n    # Preprocessing settings\n    target_shape = (32, 384, 384)  # (depth, height, width)\n    \n    # Inference settings\n    batch_size = 1\n    use_amp = True\n    use_tta = False  # TTA is prohibited due to left/right positional information\n    tta_transforms = 0\n    \n    # Model paths\n    model_dir = '/kaggle/input/rsna2025-effnetv2-32ch'\n    n_fold = 5\n    trn_fold = [0, 1, 2, 3, 4]\n    \n    # Ensemble weights (equal weight for all folds)\n    ensemble_weights = None  # None means equal weights\n\nCFG = InferenceConfig()\n\n# ====================================================\n# Transforms\n# ====================================================\ndef get_inference_transform():\n    \"\"\"Get inference transformation\"\"\"\n    return A.Compose([\n        A.Resize(CFG.size, CFG.size),\n        A.Normalize(),\n        ToTensorV2(),\n    ])\n\n# TTA is not used due to left/right positional information\n# def get_tta_transforms():\n#     \"\"\"TTA is prohibited for brain aneurysms due to left/right positioning\"\"\"\n#     pass\n\n# ====================================================\n# Model Loading Functions\n# ====================================================\n# Global variables\nMODELS = {}\nTRANSFORM = None\nTTA_TRANSFORMS = None\n\ndef load_model_fold(fold: int) -> nn.Module:\n    \"\"\"Load a single fold model\"\"\"\n    model_path = Path(CFG.model_dir) / f'{CFG.model_name}_fold{fold}_best.pth'\n    \n    if not model_path.exists():\n        raise FileNotFoundError(f\"Model file not found: {model_path}\")\n    \n    #print(f\"Loading fold {fold} model from {model_path}...\")\n    \n    # Load checkpoint\n    checkpoint = torch.load(model_path, map_location=device, weights_only=False)\n    \n    # Initialize model with same architecture as training\n    model = timm.create_model(\n        CFG.model_name, \n        num_classes=CFG.num_classes, \n        pretrained=False,  # Don't load pretrained weights\n        in_chans=CFG.in_chans\n    )\n    \n    # Load trained weights\n    model.load_state_dict(checkpoint['model'])\n    model = model.to(device)\n    model.eval()\n    \n    #print(f\"Successfully loaded fold {fold} model\")\n    return model\n\ndef load_models():\n    \"\"\"Load all fold models\"\"\"\n    global MODELS, TRANSFORM, TTA_TRANSFORMS\n    \n    #print(\"Loading all fold models...\")\n    \n    for fold in CFG.trn_fold:\n        try:\n            MODELS[fold] = load_model_fold(fold)\n        except Exception as e:\n            print(f\"Warning: Could not load fold {fold}: {e}\")\n    \n    if not MODELS:\n        raise ValueError(\"No models were loaded successfully\")\n    \n    # Initialize transforms\n    TRANSFORM = get_inference_transform()\n    # TTA is not used due to left/right positioning\n    TTA_TRANSFORMS = None\n    \n    #print(f\"Loaded {len(MODELS)} models: folds {list(MODELS.keys())}\")\n    \n    # Warm up models\n    #print(\"Warming up models...\")\n    dummy_image = torch.randn(1, CFG.in_chans, CFG.size, CFG.size).to(device)\n    \n    with torch.no_grad():\n        for fold, model in MODELS.items():\n            _ = model(dummy_image)\n    \n    #print(\"Models ready for inference!\")\n\n# ====================================================\n# Prediction Functions\n# ====================================================\ndef predict_single_model(model: nn.Module, image: np.ndarray) -> np.ndarray:\n    \"\"\"Make prediction with a single model (NO TTA due to left/right anatomy)\"\"\"\n    \n    # Same processing as training code\n    # image shape: (D, H, W) = (32, 384, 384)\n    image = image.transpose(1, 2, 0)  # (D,H,W) -> (H,W,D) = (384, 384, 32)\n    \n    # Apply same transform as training\n    transformed = TRANSFORM(image=image)\n    image_tensor = transformed['image']  # Shape: (32, 384, 384)\n    image_tensor = image_tensor.unsqueeze(0).to(device)  # (1, 32, 384, 384)\n    \n    with torch.no_grad():\n        with autocast(enabled=CFG.use_amp):\n            output = model(image_tensor)\n            return torch.sigmoid(output).cpu().numpy().squeeze()\n\ndef predict_ensemble(image: np.ndarray) -> np.ndarray:\n    \"\"\"Make ensemble prediction across all folds\"\"\"\n    all_predictions = []\n    weights = []\n    \n    for fold, model in MODELS.items():\n        pred = predict_single_model(model, image)\n        all_predictions.append(pred)\n        \n        # Use equal weights if not specified\n        if CFG.ensemble_weights is not None:\n            weights.append(CFG.ensemble_weights.get(fold, 1.0))\n        else:\n            weights.append(1.0)\n    \n    # Weighted average\n    weights = np.array(weights) / np.sum(weights)\n    predictions = np.array(all_predictions)\n    \n    return np.average(predictions, weights=weights, axis=0)\n\ndef _predict_inner(series_path: str) -> pl.DataFrame:\n    \"\"\"Main prediction logic (internal).\"\"\"\n    global MODELS\n    \n    # Load models if not already loaded\n    if not MODELS:\n        load_models()\n    \n    # Extract series ID\n    series_id = os.path.basename(series_path)\n    \n    try:\n        # Process DICOM series using our preprocessor\n        volume = process_dicom_series_safe(series_path, CFG.target_shape)\n        \n        # Make ensemble prediction\n        final_pred = (predict_ensemble(volume) + predict_ensemble(np.flip(volume,-2))[[1,0,3,2,5,4,6,8,7,10,9,11,12,13]])/2\n        \n        # Create output dataframe\n        predictions_df = pl.DataFrame(\n            data=[[series_id] + final_pred.tolist()],\n            schema=[ID_COL] + LABEL_COLS,\n            orient='row'\n        )\n        \n        # Return without ID column, as required by the API\n        return predictions_df.drop(ID_COL)\n        \n    except Exception as e:\n        #print(f\"Error processing {series_id}: {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\n# ====================================================\n# DICOM Processing (using DICOMPreprocessorKaggle defined in previous cell)\n# ====================================================\ndef process_dicom_series_safe(series_path: str, target_shape: Tuple[int, int, int] = (32, 384, 384)) -> np.ndarray:\n    \"\"\"\n    Safe DICOM processing with memory cleanup\n    Uses DICOMPreprocessorKaggle defined in previous cell\n    \n    Args:\n        series_path: Path to DICOM series\n        target_shape: Target volume size (depth, height, width)\n    \n    Returns:\n        np.ndarray: Processed volume\n    \"\"\"\n    try:\n        preprocessor = DICOMPreprocessorKaggle(target_shape=target_shape)\n        volume = preprocessor.process_series(series_path)\n        return volume\n    finally:\n        # Memory cleanup\n        gc.collect()\n\ndef predict_fallback(series_path: str) -> pl.DataFrame:\n    \"\"\"Fallback prediction function\"\"\"\n    #print(f\"Using fallback predictions for {os.path.basename(series_path)}\")\n    \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    \n    # Clean up\n    shutil.rmtree('/kaggle/shared', ignore_errors=True)\n    \n    return predictions_df\n\ndef predict(series_path: str) -> pl.DataFrame:\n    \"\"\"\n    Top-level prediction function passed to the server.\n    It calls the core logic and guarantees cleanup in a `finally` block.\n    \"\"\"\n    try:\n        # Call the internal prediction logic\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        # Return a fallback dataframe with the correct schema\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        # This code is required to prevent \"out of disk space\" and \"directory not empty\" errors.\n        # It deletes the shared folder and then immediately recreates it, ensuring it's\n        # empty and ready for the next prediction.\n        shared_dir = '/kaggle/shared'\n        shutil.rmtree(shared_dir, ignore_errors=True)\n        os.makedirs(shared_dir, exist_ok=True)\n        \n        # Also perform memory cleanup here\n        if torch.cuda.is_available():\n            torch.cuda.empty_cache()\n        gc.collect()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-02T17:08:51.886286Z","iopub.execute_input":"2025-10-02T17:08:51.886626Z","iopub.status.idle":"2025-10-02T17:08:51.925624Z","shell.execute_reply.started":"2025-10-02T17:08:51.886605Z","shell.execute_reply":"2025-10-02T17:08:51.924755Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ====================================================\n# Main Execution\n# ====================================================\n\n# Load models at startup\nload_models()\n\n# Initialize the inference server with our main `predict` function.\ninference_server = kaggle_evaluation.rsna_inference_server.RSNAInferenceServer(predict)\n\n# Check if the notebook is running in the competition environment or a local session.\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    inference_server.serve()\nelse:\n    inference_server.run_local_gateway()\n    \n    submission_df = pl.read_parquet('/kaggle/working/submission.parquet')\n    display(submission_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-02T17:08:51.926496Z","iopub.execute_input":"2025-10-02T17:08:51.926775Z","iopub.status.idle":"2025-10-02T17:10:00.052527Z","shell.execute_reply.started":"2025-10-02T17:08:51.926755Z","shell.execute_reply":"2025-10-02T17:10:00.051521Z"},"collapsed":true,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null}]}