{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":99552,"databundleVersionId":13851420,"sourceType":"competition"},{"sourceId":12780021,"sourceType":"datasetVersion","datasetId":8079690},{"sourceId":14045202,"sourceType":"datasetVersion","datasetId":8941532}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport pydicom\nimport cv2\nfrom pathlib import Path\nfrom scipy import ndimage\nimport warnings\nimport gc\nfrom typing import List, Dict, Tuple, Optional\nfrom tqdm.auto import tqdm\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.cuda.amp import autocast, GradScaler\nimport timm\n\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import roc_auc_score\n\nwarnings.filterwarnings('ignore')\n\n# ====================================================\n# DICOM PREPROCESSING\n# ====================================================\nclass DICOMPreprocessorKaggle:\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        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:\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        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                elif hasattr(ds, \"SliceLocation\"):\n                    info['z_position'] = float(getattr(ds, \"SliceLocation\"))\n                else:\n                    info['z_position'] = float(info['instance_number'])\n            except:\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        return sorted(slice_info, key=lambda x: x['z_position'])\n    \n    def get_windowing_params(self, ds: pydicom.Dataset) -> Tuple[Optional[float], Optional[float]]:\n        modality = getattr(ds, 'Modality', 'CT')\n        if modality == 'CT':\n            return 50, 350\n        return None, None\n    \n    def apply_windowing_or_normalize(self, img: np.ndarray) -> np.ndarray:\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            return (normalized * 255).astype(np.uint8)\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                return (normalized * 255).astype(np.uint8)\n            else:\n                return np.zeros_like(img, dtype=np.uint8)\n    \n    def extract_pixel_array(self, ds: pydicom.Dataset) -> np.ndarray:\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 = float(getattr(ds, 'RescaleSlope', 1))\n        intercept = float(getattr(ds, 'RescaleIntercept', 0))\n        img = img * slope + intercept\n        \n        return img\n    \n    def resize_volume_3d(self, volume: np.ndarray) -> np.ndarray:\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        zoom_factors = [target_shape[i] / current_shape[i] for i in range(3)]\n        resized_volume = ndimage.zoom(volume, zoom_factors, order=1, mode='nearest')\n        resized_volume = resized_volume[:self.target_depth, :self.target_height, :self.target_width]\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        return resized_volume.astype(np.uint8)\n    \n    def process_series(self, series_path: str) -> np.ndarray:\n        datasets, series_name = self.load_dicom_series(series_path)\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)\n        else:\n            return self._process_multiple_2d_dicoms(datasets)\n    \n    def _process_single_3d_dicom(self, ds: pydicom.Dataset) -> np.ndarray:\n        volume = ds.pixel_array.astype(np.float32)\n        \n        slope = float(getattr(ds, 'RescaleSlope', 1))\n        intercept = float(getattr(ds, 'RescaleIntercept', 0))\n        volume = volume * slope + intercept\n        \n        processed_slices = []\n        for i in range(volume.shape[0]):\n            processed_img = self.apply_windowing_or_normalize(volume[i])\n            processed_slices.append(processed_img)\n        \n        volume = np.stack(processed_slices, axis=0)\n        return self.resize_volume_3d(volume)\n    \n    def _process_multiple_2d_dicoms(self, datasets: List[pydicom.Dataset]) -> np.ndarray:\n        slice_info = self.extract_slice_info(datasets)\n        sorted_slices = self.sort_slices_by_position(slice_info)\n        \n        processed_slices = []\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)\n            resized_img = cv2.resize(processed_img, (self.target_width, self.target_height))\n            processed_slices.append(resized_img)\n\n        volume = np.stack(processed_slices, axis=0)\n        return self.resize_volume_3d(volume)\n\ndef process_dicom_series_safe(series_path: str, target_shape: Tuple[int, int, int] = (32, 384, 384)) -> np.ndarray:\n    try:\n        preprocessor = DICOMPreprocessorKaggle(target_shape=target_shape)\n        return preprocessor.process_series(series_path)\n    finally:\n        gc.collect()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ====================================================\n# CONFIGURATION - FOLD 4\n# ====================================================\nclass Config:\n    data_dir = '/kaggle/input/rsna-intracranial-aneurysm-detection'\n    series_dir = f'{data_dir}/series'\n    train_csv = f'{data_dir}/train.csv'\n    output_dir = './outputs'\n    \n    # Path to dataset that contains your cached .npy volumes\n    cache_dir = '/kaggle/input/rsna-aneurysm-cache/cache'  # <-- keep or adjust name\n\n    model_name = \"tf_efficientnetv2_s.in21k_ft_in1k\"\n    size = 384\n    in_chans = 32\n    \n    target_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    num_classes = len(target_cols)\n    target_shape = (32, 384, 384)\n    \n    n_fold = 5\n    trn_fold = [4]   # <-- ONLY FOLD 4 NOW\n    epochs = 15\n    batch_size = 10\n    num_workers = 4\n    \n    lr = 3e-4\n    weight_decay = 1e-5\n    max_grad_norm = 1000\n    use_amp = True\n    \n    early_stopping_patience = 15\n    use_cache = True\n    \n    seed = 42\n    device = 'cuda' if torch.cuda.is_available() else 'cpu'\n\nCFG = Config()\nos.makedirs(CFG.output_dir, exist_ok=True)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ====================================================\n# SEED\n# ====================================================\ndef set_seed(seed):\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = False\n    torch.backends.cudnn.benchmark = True\n\nset_seed(CFG.seed)\n\n# ====================================================\n# TRANSFORMS\n# ====================================================\ndef get_train_transform():\n    return A.Compose([\n        A.Resize(CFG.size, CFG.size),\n        A.ShiftScaleRotate(shift_limit=0.05, scale_limit=0.05, rotate_limit=10, p=0.3),\n        A.RandomBrightnessContrast(brightness_limit=0.15, contrast_limit=0.15, p=0.3),\n        A.GaussNoise(var_limit=(10.0, 30.0), p=0.2),\n        A.Normalize(),\n        ToTensorV2(),\n    ])\n\ndef get_valid_transform():\n    return A.Compose([\n        A.Resize(CFG.size, CFG.size),\n        A.Normalize(),\n        ToTensorV2(),\n    ])\n\n# ====================================================\n# DATASET\n# ====================================================\nclass AneurysmDataset(Dataset):\n    def __init__(self, df: pd.DataFrame, transform=None):\n        self.df = df.reset_index(drop=True)\n        self.transform = transform\n        \n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        series_id = row['SeriesInstanceUID']\n        \n        try:\n            cache_path = Path(CFG.cache_dir) / f\"{series_id}.npy\"\n            \n            if CFG.use_cache and cache_path.exists():\n                volume = np.load(cache_path)\n            else:\n                series_path = Path(CFG.series_dir) / series_id\n                volume = process_dicom_series_safe(str(series_path), CFG.target_shape)\n                if CFG.use_cache:\n                    np.save(cache_path, volume)\n            \n            volume = volume.transpose(1, 2, 0)\n            \n            if self.transform:\n                volume = self.transform(image=volume)['image']\n            \n            labels = row[CFG.target_cols].values.astype(np.float32)\n            \n            return {\n                'image': volume,\n                'labels': torch.tensor(labels, dtype=torch.float32),\n            }\n            \n        except Exception as e:\n            return {\n                'image': torch.zeros(CFG.in_chans, CFG.size, CFG.size),\n                'labels': torch.zeros(CFG.num_classes, dtype=torch.float32),\n            }\n\n# ====================================================\n# MODEL\n# ====================================================\ndef build_model():\n    model = timm.create_model(\n        CFG.model_name,\n        pretrained=True,\n        num_classes=CFG.num_classes,\n        in_chans=CFG.in_chans\n    )\n    return model\n\n# ====================================================\n# METRICS\n# ====================================================\ndef calculate_auc(preds, labels):\n    try:\n        preds = preds.cpu().numpy()\n        labels = labels.cpu().numpy()\n        \n        aucs = []\n        for i in range(CFG.num_classes):\n            if len(np.unique(labels[:, i])) > 1:\n                aucs.append(roc_auc_score(labels[:, i], preds[:, i]))\n        \n        return np.mean(aucs) if aucs else 0.0\n    except:\n        return 0.0\n\n# ====================================================\n# TRAINING\n# ====================================================\ndef train_epoch(model, loader, criterion, optimizer, scaler):\n    model.train()\n    losses = []\n    all_preds = []\n    all_labels = []\n    \n    for batch in tqdm(loader, desc='Train'):\n        images = batch['image'].to(CFG.device)\n        labels = batch['labels'].to(CFG.device)\n        \n        with autocast(enabled=CFG.use_amp):\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n        \n        scaler.scale(loss).backward()\n        scaler.unscale_(optimizer)\n        torch.nn.utils.clip_grad_norm_(model.parameters(), CFG.max_grad_norm)\n        scaler.step(optimizer)\n        scaler.update()\n        optimizer.zero_grad()\n        \n        losses.append(loss.item())\n        all_preds.append(torch.sigmoid(outputs).detach())\n        all_labels.append(labels.detach())\n    \n    auc = calculate_auc(torch.cat(all_preds), torch.cat(all_labels))\n    return np.mean(losses), auc\n\ndef valid_epoch(model, loader, criterion):\n    model.eval()\n    losses = []\n    all_preds = []\n    all_labels = []\n    \n    with torch.no_grad():\n        for batch in tqdm(loader, desc='Valid'):\n            images = batch['image'].to(CFG.device)\n            labels = batch['labels'].to(CFG.device)\n            \n            with autocast(enabled=CFG.use_amp):\n                outputs = model(images)\n                loss = criterion(outputs, labels)\n            \n            losses.append(loss.item())\n            all_preds.append(torch.sigmoid(outputs))\n            all_labels.append(labels)\n    \n    auc = calculate_auc(torch.cat(all_preds), torch.cat(all_labels))\n    return np.mean(losses), auc\n\n# ====================================================\n# FOLD TRAINING\n# ====================================================\ndef train_fold(fold, train_df, valid_df):\n    print(f'\\nFold {fold}: Train={len(train_df)}, Valid={len(valid_df)}')\n    \n    train_ds = AneurysmDataset(train_df, transform=get_train_transform())\n    valid_ds = AneurysmDataset(valid_df, transform=get_valid_transform())\n    \n    train_loader = DataLoader(\n        train_ds, \n        batch_size=CFG.batch_size, \n        shuffle=True,\n        num_workers=CFG.num_workers, \n        pin_memory=True, \n        drop_last=True\n    )\n    valid_loader = DataLoader(\n        valid_ds, \n        batch_size=CFG.batch_size, \n        shuffle=False,\n        num_workers=CFG.num_workers, \n        pin_memory=True\n    )\n    \n    model = build_model().to(CFG.device)\n    criterion = nn.BCEWithLogitsLoss()\n    optimizer = torch.optim.AdamW(model.parameters(), lr=CFG.lr, weight_decay=CFG.weight_decay)\n    scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=CFG.epochs, eta_min=1e-6)\n    scaler = GradScaler(enabled=CFG.use_amp)\n    \n    best_auc = 0.0\n    patience = 0\n    \n    for epoch in range(1, CFG.epochs + 1):\n        print(f'\\nEpoch {epoch}/{CFG.epochs}')\n        \n        train_loss, train_auc = train_epoch(model, train_loader, criterion, optimizer, scaler)\n        valid_loss, valid_auc = valid_epoch(model, valid_loader, criterion)\n        scheduler.step()\n        \n        print(f'Train: Loss={train_loss:.4f}, AUC={train_auc:.4f}')\n        print(f'Valid: Loss={valid_loss:.4f}, AUC={valid_auc:.4f}')\n        \n        if valid_auc > best_auc:\n            best_auc = valid_auc\n            patience = 0\n            \n            torch.save({\n                'epoch': epoch,\n                'model': model.state_dict(),\n                'auc': best_auc\n            }, f'{CFG.output_dir}/{CFG.model_name}_fold{fold}_best.pth')\n            \n            print(f'Saved: AUC={best_auc:.4f}')\n        else:\n            patience += 1\n        \n        if patience >= CFG.early_stopping_patience:\n            print('Early stopping')\n            break\n        \n        torch.cuda.empty_cache()\n        gc.collect()\n    \n    print(f'Fold {fold} Best: {best_auc:.4f}')\n    \n    del model, optimizer, scheduler\n    torch.cuda.empty_cache()\n    gc.collect()\n    \n    return best_auc\n\n# ====================================================\n# MAIN\n# ====================================================\ndef main():\n    df = pd.read_csv(CFG.train_csv)\n    print(f'Dataset: {len(df)} samples')\n    \n    skf = StratifiedKFold(n_splits=CFG.n_fold, shuffle=True, random_state=CFG.seed)\n    df['fold'] = -1\n    \n    for fold, (_, val_idx) in enumerate(skf.split(df, df['Aneurysm Present'])):\n        df.loc[val_idx, 'fold'] = fold\n    \n    scores = []\n    for fold in CFG.trn_fold:\n        train_df = df[df['fold'] != fold]\n        valid_df = df[df['fold'] == fold]\n        \n        score = train_fold(fold, train_df, valid_df)\n        scores.append(score)\n    \n    print(f'\\nResults:')\n    for i, fold in enumerate(CFG.trn_fold):\n        print(f'Fold {fold}: {scores[i]:.4f}')\n    print(f'Mean: {np.mean(scores):.4f}')\n\nif __name__ == '__main__':\n    main()","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}