{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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"},"jupytext":{"cell_metadata_filter":"-all","main_language":"python","notebook_metadata_filter":"-all"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":99552,"databundleVersionId":13190393,"sourceType":"competition"}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport sys\nimport gc\nimport json\nimport shutil\nimport warnings\nwarnings.filterwarnings('ignore')\nfrom pathlib import Path\nfrom collections import defaultdict\nfrom typing import List, Dict, Optional, Tuple\nfrom IPython.display import display\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# Set device\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"Using device: {device}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-04T10:16:23.61098Z","iopub.execute_input":"2025-08-04T10:16:23.611328Z","iopub.status.idle":"2025-08-04T10:16:23.619617Z","shell.execute_reply.started":"2025-08-04T10:16:23.611304Z","shell.execute_reply":"2025-08-04T10:16:23.618012Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The evaluation API requires that you set up a server which will respond to inference requests. We have already defined the server; you just need write the predict function. When we evaluate your submission on the hidden test set the client defined in `rsna_gateway` will run in a different container with direct access to the hidden test set and hand off the data series by series.\n\nYour code will always have access to the published copies of the files.","metadata":{}},{"cell_type":"code","source":"import os\nimport pydicom\nimport pandas as pd\nfrom collections import defaultdict\nimport random\nimport time\n\ndef analyze_random_sample(root_dir, max_minutes=10, sample_size=50):\n    \"\"\"Analyze a random sample of DICOM folders within time constraints\"\"\"\n    start_time = time.time()\n    max_seconds = max_minutes * 60\n    \n    # Get all series folders and shuffle them\n    all_folders = [f for f in os.listdir(root_dir) if os.path.isdir(os.path.join(root_dir, f))]\n    random.shuffle(all_folders)\n    \n    # Initialize data structures\n    series_data = []\n    size_stats = defaultdict(lambda: defaultdict(int))\n    processed_count = 0\n    \n    for folder in all_folders:\n        if time.time() - start_time > max_seconds:\n            break\n            \n        series_path = os.path.join(root_dir, folder)\n        dicom_files = [f for f in os.listdir(series_path) if f.endswith('.dcm')]\n        \n        if not dicom_files:\n            continue\n            \n        # Only check 5 random files per folder to save time\n        sample_files = random.sample(dicom_files, min(5, len(dicom_files)))\n        dimensions = set()\n        modality = None\n        \n        for filename in sample_files:\n            try:\n                ds = pydicom.dcmread(os.path.join(series_path, filename))\n                rows = getattr(ds, 'Rows', 0)\n                cols = getattr(ds, 'Columns', 0)\n                dimensions.add((rows, cols))\n                \n                if modality is None:\n                    modality = getattr(ds, 'Modality', 'UNKNOWN')\n                    \n            except Exception as e:\n                continue\n        \n        if modality and dimensions:\n            size = f\"{next(iter(dimensions))[0]}x{next(iter(dimensions))[1]}\"\n            consistent = len(dimensions) == 1\n            \n            series_data.append({\n                'Series': folder,\n                'Modality': modality,\n                'Size': size,\n                'ConsistentDimensions': consistent,\n                'FilesChecked': len(sample_files),\n                'DimensionVariations': len(dimensions)\n            })\n            \n            size_stats[modality][size] += 1\n            processed_count += 1\n            \n            # Early exit if we've processed enough samples\n            if processed_count >= sample_size:\n                break\n    \n    # Create summary\n    elapsed = (time.time() - start_time) / 60\n    print(f\"Processed {processed_count} folders in {elapsed:.1f} minutes\")\n    \n    return pd.DataFrame(series_data), size_stats\n\ndef print_summary(df, stats):\n    \"\"\"Print condensed summary\"\"\"\n    print(\"\\n=== RANDOM SAMPLE SUMMARY ===\")\n    print(f\"Total folders analyzed: {len(df)}\")\n    \n    print(\"\\nModality Distribution:\")\n    for modality, sizes in stats.items():\n        print(f\"- {modality}: {sum(sizes.values())} folders\")\n        for size, count in sizes.items():\n            print(f\"  {size}: {count}\")\n    \n    inconsistent = df[~df['ConsistentDimensions']]\n    if not inconsistent.empty:\n        print(\"\\nInconsistent Folders Found:\")\n        print(inconsistent[['Series', 'Modality', 'DimensionVariations']])\n    else:\n        print(\"\\nAll sampled folders had consistent dimensions\")\n\n# Example usage\n\ninput_path = '/kaggle/input/rsna-intracranial-aneurysm-detection/series'\n# Run analysis (will stop after 10 minutes or 50 folders, whichever comes first)\ndf, stats = analyze_random_sample(input_path, max_minutes=10, sample_size=50)\n    \n# Print and save results\nprint_summary(df, stats)\ndf.to_csv('/kaggle/working/random_sample_analysis.csv', index=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-04T11:17:57.557101Z","iopub.execute_input":"2025-08-04T11:17:57.557496Z","iopub.status.idle":"2025-08-04T11:19:02.166356Z","shell.execute_reply.started":"2025-08-04T11:17:57.55747Z","shell.execute_reply":"2025-08-04T11:19:02.164943Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Constant configuration","metadata":{}},{"cell_type":"code","source":"# Competition constants\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]\n\n# Model selection - Change this to select which model to use for inference\n# Options: 'tf_efficientnetv2_s', 'convnext_small', 'swin_small_patch4_window7_224', 'ensemble'\nSELECTED_MODEL = 'ensemble' \n\n\n# Model paths configuration\nMODEL_PATHS = {\n    'tf_efficientnetv2_s': '/kaggle/input/rsna-iad-trained-models/models/tf_efficientnetv2_s_fold0_best.pth',\n    'convnext_small': '/kaggle/input/rsna-iad-trained-models/models/convnext_small_fold0_best.pth',\n    'swin_small_patch4_window7_224': '/kaggle/input/rsna-iad-trained-models/models/swin_small_patch4_window7_224_fold0_best.pth'\n}\n\nclass InferenceConfig:\n    # Model selection\n    model_selection = SELECTED_MODEL\n    use_ensemble = (SELECTED_MODEL == 'ensemble')\n    \n    # Default model settings (will be overridden by checkpoint)\n    image_size = 512\n    num_slices = 32\n    use_windowing = True\n    \n    # Inference settings\n    batch_size = 1\n    use_amp = True\n    use_tta = True\n    tta_transforms = 4\n    \n    # Ensemble weights (if using ensemble)\n    ensemble_weights = {\n        'tf_efficientnetv2_s': 0.1,\n        'convnext_small': 0.8,\n        'swin_small_patch4_window7_224': 0.1\n    }\n\nCFG = InferenceConfig()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-04T12:13:24.853176Z","iopub.execute_input":"2025-08-04T12:13:24.853723Z","iopub.status.idle":"2025-08-04T12:13:29.574241Z","shell.execute_reply.started":"2025-08-04T12:13:24.85368Z","shell.execute_reply":"2025-08-04T12:13:29.572489Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class MultiBackboneModel(nn.Module):\n    \"\"\"Flexible model that can use different backbones\"\"\"\n    def __init__(self, model_name, num_classes=14, pretrained=True, \n                 drop_rate=0.3, drop_path_rate=0.2):\n        super().__init__()\n        \n        self.model_name = model_name\n        \n        if 'swin' in model_name:\n            # Swin transformer requires 224x224 by default\n            self.backbone = timm.create_model(\n                model_name, \n                pretrained=pretrained,\n                in_chans=3,\n                drop_rate=drop_rate,\n                drop_path_rate=drop_path_rate,\n                img_size=CFG.image_size,  # Override default size\n                num_classes=0,  # Remove classifier head\n                global_pool=''  # Remove global pooling\n            )\n        else:\n            self.backbone = timm.create_model(\n                model_name, \n                pretrained=pretrained,\n                in_chans=3,\n                drop_rate=drop_rate,\n                drop_path_rate=drop_path_rate,\n                num_classes=0,  # Remove classifier head\n                global_pool=''  # Remove global pooling\n            )\n        \n        with torch.no_grad():\n            dummy_input = torch.zeros(1, 3, CFG.image_size, CFG.image_size)\n            features = self.backbone(dummy_input)\n            \n            if len(features.shape) == 4:\n                # Conv features (batch, channels, height, width)\n                num_features = features.shape[1]\n                self.needs_pool = True\n            elif len(features.shape) == 3:\n                # Transformer features (batch, sequence, features)\n                num_features = features.shape[-1]\n                self.needs_pool = False\n                self.needs_seq_pool = True\n            else:\n                # Already flat features (batch, features)\n                num_features = features.shape[1]\n                self.needs_pool = False\n                self.needs_seq_pool = False\n        \n        print(f\"Model {model_name}: detected {num_features} features, output shape: {features.shape}\")\n        \n        # Add global pooling for models that output spatial features\n        if self.needs_pool:\n            self.global_pool = nn.AdaptiveAvgPool2d(1)\n        \n        # Metadata processing\n        self.meta_fc = nn.Sequential(\n            nn.Linear(2, 16),\n            nn.ReLU(),\n            nn.Dropout(0.2),\n            nn.Linear(16, 32),\n            nn.ReLU()\n        )\n        \n        # Combined classifier with batch norm for stability\n        self.classifier = nn.Sequential(\n            nn.Linear(num_features + 32, 512),\n            nn.BatchNorm1d(512),\n            nn.ReLU(),\n            nn.Dropout(drop_rate),\n            nn.Linear(512, 256),\n            nn.BatchNorm1d(256),\n            nn.ReLU(),\n            nn.Dropout(drop_rate),\n            nn.Linear(256, num_classes)\n        )\n\n    def forward(self, image, meta):\n        # Extract image features\n        img_features = self.backbone(image)\n        \n        # Apply appropriate pooling based on model type\n        if hasattr(self, 'needs_pool') and self.needs_pool:\n            # Conv features - apply global pooling\n            img_features = self.global_pool(img_features)\n            img_features = img_features.flatten(1)\n        elif hasattr(self, 'needs_seq_pool') and self.needs_seq_pool:\n            # Transformer features - average across sequence dimension\n            img_features = img_features.mean(dim=1)\n        elif len(img_features.shape) == 4:\n            # Fallback for any 4D output\n            img_features = F.adaptive_avg_pool2d(img_features, 1).flatten(1)\n        elif len(img_features.shape) == 3:\n            # Fallback for any 3D output\n            img_features = img_features.mean(dim=1)\n        \n        # Process metadata\n        meta_features = self.meta_fc(meta)\n        \n        # Combine features\n        combined = torch.cat([img_features, meta_features], dim=1)\n        \n        # Classification\n        output = self.classifier(combined)\n        \n        return output","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-04T12:13:33.6624Z","iopub.execute_input":"2025-08-04T12:13:33.662771Z","iopub.status.idle":"2025-08-04T12:13:33.670992Z","shell.execute_reply.started":"2025-08-04T12:13:33.662741Z","shell.execute_reply":"2025-08-04T12:13:33.669717Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"model training","metadata":{}},{"cell_type":"code","source":"def apply_dicom_windowing(img: np.ndarray, window_center: float, window_width: float) -> np.ndarray:\n    \"\"\"Apply DICOM windowing\"\"\"\n    img_min = window_center - window_width // 2\n    img_max = window_center + window_width // 2\n    img = np.clip(img, img_min, img_max)\n    img = (img - img_min) / (img_max - img_min + 1e-7)\n    return (img * 255).astype(np.uint8)\n\ndef get_windowing_params(modality: str) -> Tuple[float, float]:\n    \"\"\"Get appropriate windowing for different modalities\"\"\"\n    windows = {\n        'CT': (40, 80),\n        'CTA': (50, 350),\n        'MRA': (600, 1200),\n        'MRI': (40, 80),\n    }\n    return windows.get(modality, (40, 80))\n\ndef process_dicom_series(series_path: str) -> Tuple[np.ndarray, Dict]:\n    \"\"\"Process a DICOM series and extract metadata\"\"\"\n    series_path = Path(series_path)\n    \n    # Find all DICOM files\n    all_filepaths = []\n    for root, _, files in os.walk(series_path):\n        for file in files:\n            if file.endswith('.dcm'):\n                all_filepaths.append(os.path.join(root, file))\n    all_filepaths.sort()\n    \n    if len(all_filepaths) == 0:\n        # Return default values\n        volume = np.zeros((CFG.num_slices, CFG.image_size, CFG.image_size), dtype=np.uint8)\n        metadata = {'age': 50, 'sex': 0, 'modality': 'CT'}\n        return volume, metadata\n    \n    # Process DICOM files\n    slices = []\n    metadata = {}\n    \n    for i, filepath in enumerate(all_filepaths):\n        try:\n            ds = pydicom.dcmread(filepath, force=True)\n            img = ds.pixel_array.astype(np.float32)\n            \n            # Handle multi-frame or color images\n            if img.ndim == 3:\n                if img.shape[-1] == 3:\n                    img = cv2.cvtColor(img.astype(np.uint8), cv2.COLOR_BGR2GRAY).astype(np.float32)\n                else:\n                    img = img[:, :, 0]\n            \n            # Extract metadata from first file\n            if i == 0:\n                metadata['modality'] = getattr(ds, 'Modality', 'CT')\n                \n                try:\n                    age_str = getattr(ds, 'PatientAge', '050Y')\n                    age = int(''.join(filter(str.isdigit, age_str[:3])) or '50')\n                    metadata['age'] = min(age, 100)\n                except:\n                    metadata['age'] = 50\n                \n                try:\n                    sex = getattr(ds, 'PatientSex', 'M')\n                    metadata['sex'] = 1 if sex == 'M' else 0\n                except:\n                    metadata['sex'] = 0\n            \n            # Apply rescale if available\n            if hasattr(ds, 'RescaleSlope') and hasattr(ds, 'RescaleIntercept'):\n                img = img * ds.RescaleSlope + ds.RescaleIntercept\n            \n            # Apply windowing\n            if CFG.use_windowing:\n                window_center, window_width = get_windowing_params(metadata['modality'])\n                img = apply_dicom_windowing(img, window_center, window_width)\n            else:\n                img_min, img_max = img.min(), img.max()\n                if img_max > img_min:\n                    img = ((img - img_min) / (img_max - img_min) * 255).astype(np.uint8)\n                else:\n                    img = np.zeros_like(img, dtype=np.uint8)\n            \n            # Resize\n            img = cv2.resize(img, (CFG.image_size, CFG.image_size))\n            slices.append(img)\n            \n        except Exception as e:\n            print(f\"Error processing {filepath}: {e}\")\n            continue\n    \n    # Handle slice sampling\n    if len(slices) == 0:\n        volume = np.zeros((CFG.num_slices, CFG.image_size, CFG.image_size), dtype=np.uint8)\n    else:\n        volume = np.array(slices)\n        if len(slices) > CFG.num_slices:\n            indices = np.linspace(0, len(slices) - 1, CFG.num_slices).astype(int)\n            volume = volume[indices]\n        elif len(slices) < CFG.num_slices:\n            pad_size = CFG.num_slices - len(slices)\n            volume = np.pad(volume, ((0, pad_size), (0, 0), (0, 0)), mode='edge')\n    \n    return volume, metadata","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-04T12:13:38.561676Z","iopub.execute_input":"2025-08-04T12:13:38.562018Z","iopub.status.idle":"2025-08-04T12:13:38.569961Z","shell.execute_reply.started":"2025-08-04T12:13:38.561997Z","shell.execute_reply":"2025-08-04T12:13:38.568661Z"}},"outputs":[],"execution_count":null}]}