{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":99552,"databundleVersionId":13851420,"sourceType":"competition"},{"sourceId":13033841,"sourceType":"datasetVersion","datasetId":8252931},{"sourceId":267726899,"sourceType":"kernelVersion"}],"dockerImageVersionId":31089,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# --- 1. Imports and Setup ---\nimport os\nimport gc\nimport warnings\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport pydicom\nimport timm\nimport torch\nimport torch.nn as nn\nfrom pathlib import Path\nfrom scipy import ndimage\nfrom typing import List, Tuple\n\nwarnings.filterwarnings('ignore')\n\n# --- 2. Configuration ---\nclass CFG:\n    # !!! IMPORTANT: EDIT THIS PATH !!!\n    # Point this to the .pth file from your training notebook's output\n    MODEL_PATH = \"/kaggle/input/final-train/checkpoints/best_model_fold_0.pth\"\n    \n    # Device and model parameters must match your training setup\n    DEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    MODEL_NAME = \"tf_efficientnetv2_s.in21k_ft_in1k\"\n    TARGET_SHAPE = (32, 384, 384) # (Depth/Channels, Height, Width)\n    \n    # Target columns must be in the same order as in training\n    TARGET_COLUMNS = [\n        'Left Infraclinoid Internal Carotid Artery', 'Right Infraclinoid Internal Carotid Artery',\n        'Left Supraclinoid Internal Carotid Artery', 'Right Supraclinoid Internal Carotid Artery',\n        'Left Middle Cerebral Artery', 'Right Middle Cerebral Artery',\n        'Anterior Communicating Artery', 'Left Anterior Cerebral Artery',\n        'Right Anterior Cerebral Artery', 'Left Posterior Communicating Artery',\n        'Right Posterior Communicating Artery', 'Basilar Tip',\n        'Other Posterior Circulation', 'Aneurysm Present'\n    ]\n\nprint(f\"Using device: {CFG.DEVICE}\")\n\n# --- 3. Model Definition ---\n# This function must be identical to the one in your training script\ndef build_model():\n    model = timm.create_model(\n        CFG.MODEL_NAME,\n        pretrained=False, # We are loading our own trained weights\n        num_classes=len(CFG.TARGET_COLUMNS),\n        in_chans=CFG.TARGET_SHAPE[0]\n    )\n    return model\n\n# Load the trained model weights\nmodel = build_model().to(CFG.DEVICE)\nmodel.load_state_dict(torch.load(CFG.MODEL_PATH, map_location=CFG.DEVICE))\nmodel.eval()\nprint(\"Model loaded successfully.\")\n\n# --- 4. Preprocessing Pipeline ---\n# This class MUST be an exact copy of the one from your successful preprocessing notebook\nclass DICOMPreprocessor:\n    def __init__(self, target_shape: Tuple[int, int, int] = CFG.TARGET_SHAPE):\n        self.target_depth, self.target_height, self.target_width = target_shape\n\n    def load_dicom_series(self, series_path: str) -> List[pydicom.Dataset]:\n        dicom_files = list(Path(series_path).rglob('*.dcm'))\n        if not dicom_files: raise ValueError(f\"No DICOM files in {series_path}\")\n        datasets = [pydicom.dcmread(fp, force=True) for fp in dicom_files]\n        return datasets\n\n    def sort_slices(self, datasets: List[pydicom.Dataset]) -> List[pydicom.Dataset]:\n        slice_info = [{'dataset': ds, 'z_pos': float(ds.ImagePositionPatient[2]) if hasattr(ds, 'ImagePositionPatient') else int(ds.InstanceNumber)} for i, ds in enumerate(datasets)]\n        return [info['dataset'] for info in sorted(slice_info, key=lambda x: x['z_pos'])]\n\n    def apply_windowing(self, img: np.ndarray) -> np.ndarray:\n        window_min, window_max = 0, 500\n        img = np.clip(img, window_min, window_max)\n        return (img - window_min) / (window_max - window_min)\n\n    def extract_pixel_array(self, ds: pydicom.Dataset) -> np.ndarray:\n        img = ds.pixel_array.astype(np.float32)\n        slope = getattr(ds, 'RescaleSlope', 1.0)\n        intercept = getattr(ds, 'RescaleIntercept', 0.0)\n        img = img * float(slope) + float(intercept)\n        if img.ndim == 3 and img.shape[-1] == 3:\n            img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)\n        return img if img.size > 0 else None\n\n    def resize_depth(self, volume: np.ndarray) -> np.ndarray:\n        if volume.shape[0] == self.target_depth: return volume\n        zoom_factor = self.target_depth / volume.shape[0]\n        return ndimage.zoom(volume, (zoom_factor, 1, 1), order=1, mode='nearest')\n\n    def process_series(self, series_path: str) -> np.ndarray:\n        datasets = self.load_dicom_series(series_path)\n        if not datasets: raise ValueError(\"Could not read DICOMs.\")\n        sorted_datasets = self.sort_slices(datasets)\n        processed_slices = []\n        for ds in sorted_datasets:\n            img = self.extract_pixel_array(ds)\n            if img is None: continue\n            windowed_img = self.apply_windowing(img)\n            resized_img = cv2.resize(windowed_img, (self.target_width, self.target_height))\n            processed_slices.append(resized_img)\n        if not processed_slices: raise ValueError(\"No valid slices processed.\")\n        volume = np.stack(processed_slices, axis=0)\n        final_volume = self.resize_depth(volume)\n        return (final_volume * 255).astype(np.uint8)\n\ndef preprocess_for_inference(series_path: str) -> torch.Tensor:\n    \"\"\"Orchestrates the full preprocessing pipeline for a single series.\"\"\"\n    preprocessor = DICOMPreprocessor()\n    \n    # 1. DICOM to NumPy array (uint8)\n    volume_np = preprocessor.process_series(series_path)\n    \n    # 2. NumPy to PyTorch Tensor, scale to [0, 1]\n    volume_tensor = torch.from_numpy(volume_np).float() / 255.0\n    \n    # 3. Apply the same normalization as in training (mean=0.5, std=0.5)\n    mean = torch.tensor([0.5] * CFG.TARGET_SHAPE[0]).view(CFG.TARGET_SHAPE[0], 1, 1)\n    std = torch.tensor([0.5] * CFG.TARGET_SHAPE[0]).view(CFG.TARGET_SHAPE[0], 1, 1)\n    volume_tensor = (volume_tensor - mean) / std\n    \n    # 4. Add batch dimension for the model\n    return volume_tensor.unsqueeze(0)\n\n# --- 5. Prediction Function (Kaggle API) ---\ndef predict(test_series_path: str) -> pd.DataFrame:\n    \"\"\"Processes a DICOM series, runs inference with TTA, and returns results.\"\"\"\n    print(f\"--> Processing series: {os.path.basename(test_series_path)}\")\n    try:\n        # Preprocess the original test scan\n        scan_tensor_original = preprocess_for_inference(test_series_path).to(CFG.DEVICE)\n        \n        # TTA: Create a horizontally flipped version (W-axis is the last one)\n        scan_tensor_flipped = torch.flip(scan_tensor_original, dims=[-1])\n\n        with torch.no_grad():\n            # Get predictions for both versions\n            logits_original = model(scan_tensor_original)\n            probs_original = torch.sigmoid(logits_original)\n            \n            logits_flipped = model(scan_tensor_flipped)\n            probs_flipped = torch.sigmoid(logits_flipped)\n\n        # TTA: \"Un-flip\" the predictions from the flipped scan\n        probs_flipped_corrected = probs_flipped.clone()\n        swap_map = {0: 1, 2: 3, 4: 5, 7: 8, 9: 10} # Indices of Left/Right pairs\n        for left_idx, right_idx in swap_map.items():\n            probs_flipped_corrected[0, left_idx] = probs_flipped[0, right_idx]\n            probs_flipped_corrected[0, right_idx] = probs_flipped[0, left_idx]\n            \n        # TTA: Average the two sets of probabilities for a more robust prediction\n        final_probabilities = (probs_original + probs_flipped_corrected) / 2.0\n        \n        # Format the final results\n        results = {'SeriesInstanceUID': os.path.basename(test_series_path)}\n        for i, col in enumerate(CFG.TARGET_COLUMNS):\n            results[col] = float(final_probabilities[0, i].item())\n            \n        return pd.DataFrame([results])\n\n    except Exception as e:\n        # Fallback: If anything fails, return a DataFrame of zeros\n        print(f\"!!! FAILED processing {test_series_path}: {e}\")\n        results = {'SeriesInstanceUID': os.path.basename(test_series_path)}\n        for col in CFG.TARGET_COLUMNS:\n            results[col] = 0.0\n        return pd.DataFrame([results])\n    finally:\n        gc.collect()\n\nprint(\"Inference pipeline with TTA is ready.\")\n\n# --- 6. Kaggle Submission Execution ---\n# This part of the code is provided by Kaggle to run your `predict` function\nfrom kaggle_evaluation import rsna_inference_server\n\ninference_server = rsna_inference_server.RSNAInferenceServer(predict)\n\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    inference_server.serve()\nelse:\n    # This runs in the interactive notebook, using a local test set\n    inference_server.run_local_gateway()\n    try:\n        display(pd.read_parquet('/kaggle/working/submission.parquet'))\n    except FileNotFoundError:\n        print(\"Local run complete. No submission.parquet generated.\")\n\nprint(\"Submission script finished.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-13T12:01:23.983102Z","iopub.execute_input":"2025-10-13T12:01:23.983373Z","iopub.status.idle":"2025-10-13T12:01:55.32643Z","shell.execute_reply.started":"2025-10-13T12:01:23.983351Z","shell.execute_reply":"2025-10-13T12:01:55.325854Z"}},"outputs":[],"execution_count":null}]}