{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.13"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":99552,"databundleVersionId":13851420,"sourceType":"competition"},{"sourceId":12780021,"sourceType":"datasetVersion","datasetId":8079690},{"sourceId":13130191,"sourceType":"datasetVersion","datasetId":8009945},{"sourceId":13262094,"sourceType":"datasetVersion","datasetId":8285566},{"sourceId":13293794,"sourceType":"datasetVersion","datasetId":8299879},{"sourceId":13354474,"sourceType":"datasetVersion","datasetId":8214516},{"sourceId":13372858,"sourceType":"datasetVersion","datasetId":7994218},{"sourceId":13375266,"sourceType":"datasetVersion","datasetId":8325535},{"sourceId":265903317,"sourceType":"kernelVersion"},{"sourceId":267872007,"sourceType":"kernelVersion"},{"sourceId":584390,"sourceType":"modelInstanceVersion","modelInstanceId":436530,"modelId":453217},{"sourceId":585148,"sourceType":"modelInstanceVersion","modelInstanceId":436530,"modelId":453217}],"dockerImageVersionId":31090,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":181.62543,"end_time":"2025-10-03T05:12:22.699968","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-10-03T05:09:21.074538","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"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\nimport sys\nimport json\nimport shutil\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\nimport threading\nimport queue\nimport time\nfrom collections import OrderedDict\n\nwarnings.filterwarnings('ignore')\n\n# Data handling\nimport polars as pl\nimport pandas as pd\nimport joblib\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\nimport pickle\n\n# Transformations\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport sys\nsys.path.insert(0, \"/kaggle/input/ultralytcs-timm-rsna/ultralytics-timm\")\n\n# YOLO\nfrom ultralytics import YOLO\n\n# Competition API\nimport kaggle_evaluation.rsna_inference_server\nfrom tqdm import tqdm\nimport cupy as cp\nfrom cupyx.scipy.ndimage import zoom\nimport lightgbm as lgb\nimport xgboost as xgb\nfrom catboost import CatBoostClassifier, Pool\nfrom sklearn.preprocessing import LabelEncoder\n\n# Set device\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nxgb.set_config(verbosity=0)\n\nwith open('/kaggle/input/2x-yolo-flayer-meta-training-final-ver/label_encoder_sex.pkl', 'rb') as f:\n    le = pickle.load(f)","metadata":{"execution":{"iopub.status.busy":"2025-10-14T02:32:00.5967Z","iopub.execute_input":"2025-10-14T02:32:00.597254Z","iopub.status.idle":"2025-10-14T02:32:53.038849Z","shell.execute_reply.started":"2025-10-14T02:32:00.597197Z","shell.execute_reply":"2025-10-14T02:32:53.03824Z"},"papermill":{"duration":58.742328,"end_time":"2025-10-03T05:10:24.555508","exception":false,"start_time":"2025-10-03T05:09:25.81318","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Optimization settings\ntorch.set_float32_matmul_precision('medium')\ntorch.backends.cudnn.benchmark = True\ntorch.backends.cudnn.deterministic = False\ntorch.backends.cuda.matmul.allow_tf32 = True\ntorch.backends.cudnn.allow_tf32 = True\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]\n\n# YOLO label mappings\nYOLO_LABELS_TO_IDX = {\n    'Anterior Communicating Artery': 0,\n    'Basilar Tip': 1,\n    'Left Anterior Cerebral Artery': 2,\n    'Left Infraclinoid Internal Carotid Artery': 3,\n    'Left Middle Cerebral Artery': 4,\n    'Left Posterior Communicating Artery': 5,\n    'Left Supraclinoid Internal Carotid Artery': 6,\n    'Other Posterior Circulation': 7,\n    'Right Anterior Cerebral Artery': 8,\n    'Right Infraclinoid Internal Carotid Artery': 9,\n    'Right Middle Cerebral Artery': 10,\n    'Right Posterior Communicating Artery': 11,\n    'Right Supraclinoid Internal Carotid Artery': 12\n}\n\nYOLO_LABELS = sorted(list(YOLO_LABELS_TO_IDX.keys()))\n\nVESSEL_LABELS = LABEL_COLS[:-1]\nPRESENCE_LABEL = LABEL_COLS[-1]\n\nensemble_w = [1,  0, 0, 0, 0, 0] #[0.2793572,  0.58535173, 0.00420708, 0.01056496, 0.06308367, 0.05743536]","metadata":{"execution":{"iopub.status.busy":"2025-10-14T02:32:53.039924Z","iopub.execute_input":"2025-10-14T02:32:53.040552Z","iopub.status.idle":"2025-10-14T02:32:53.046763Z","shell.execute_reply.started":"2025-10-14T02:32:53.040531Z","shell.execute_reply":"2025-10-14T02:32:53.046134Z"},"papermill":{"duration":0.011534,"end_time":"2025-10-03T05:10:24.942479","exception":false,"start_time":"2025-10-03T05:10:24.930945","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## meta classifier","metadata":{}},{"cell_type":"code","source":"meta_cls_path = '/kaggle/input/2x-yolo-flayer-meta-training-final-ver'\nmodel_prefix=\"meta_classifier\"\nn_folds = 5\n\n\nlgb_models = {label: [] for label in LABEL_COLS}\nxgb_models = {label: [] for label in LABEL_COLS}\ncat_models = {label: [] for label in LABEL_COLS}\nmeta_models = {'lgb': lgb_models, 'xgb': xgb_models, 'cat': cat_models}\n\n\nfor label in tqdm(LABEL_COLS):\n    for model_file in ['lgb', 'xgb', 'cat']:\n        for fold in range(n_folds):\n            model_path = f\"{meta_cls_path}/{model_file}/{model_prefix}_{label}_fold_fold{fold}.pkl\"\n            model = joblib.load(model_path)\n            meta_models[model_file][label].append(model)\n\ndef predict_prob_lgb(X, fold_id):\n    preds_fold = []\n    for k, model in lgb_models.items():\n        preds_fold.append(model[fold_id].predict_proba(X)[:, 1])\n    preds_fold = np.array(preds_fold)\n    return preds_fold\n\ndef predict_prob_xgb(X, fold_id):\n    preds_fold = []\n    for k, model in xgb_models.items():\n        preds_fold.append(model[fold_id].predict_proba(X)[:, 1])\n    preds_fold = np.array(preds_fold)\n    return preds_fold\n\ndef predict_prob_cat(X, fold_id):\n    preds_fold = []\n    for k, model in cat_models.items():\n        preds_fold.append(model[fold_id].predict_proba(X)[:, 1])\n    preds_fold = np.array(preds_fold)\n    return preds_fold","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-14T02:32:53.047501Z","iopub.execute_input":"2025-10-14T02:32:53.048193Z","iopub.status.idle":"2025-10-14T02:32:55.704127Z","shell.execute_reply.started":"2025-10-14T02:32:53.048172Z","shell.execute_reply":"2025-10-14T02:32:55.70346Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## flayer","metadata":{}},{"cell_type":"code","source":"class FlayerDICOMPreprocessor:\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\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                else:\n                    # Fallback: use InstanceNumber\n                    info['z_position'] = float(info['instance_number'])\n                    #print(\"ImagePositionPatient not found, using InstanceNumber\")\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    #original\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    #YTT \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        \n        # For MR or if windowing is not applied, use statistical normalization\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 + 1e-6)\n            result = (normalized * 255).astype(np.uint8)\n            #print(\"norm\")\n            #print(result)\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 + 1e-6)\n                result = (normalized * 255).astype(np.uint8)\n                return result\n            else:\n                return np.zeros_like(img, dtype=np.uint8)\n    \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        #YTT gemini fix1\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        volume = cp.asarray(volume)\n        \n        # Resize with linear interpolation\n        resized_volume = zoom(volume, zoom_factors, order=1, mode='nearest')\n        resized_volume = resized_volume[:self.target_depth, :self.target_height, :self.target_width]\n        resized_volume = cp.asnumpy(resized_volume)\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        #print(resized_volume)\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        #YTT gemini fix1\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","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-14T02:32:55.705811Z","iopub.execute_input":"2025-10-14T02:32:55.706026Z","iopub.status.idle":"2025-10-14T02:32:55.730441Z","shell.execute_reply.started":"2025-10-14T02:32:55.706009Z","shell.execute_reply":"2025-10-14T02:32:55.729764Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## yolo","metadata":{}},{"cell_type":"code","source":"# ====================================================\n# YOLO DICOM Processing\n# ====================================================\ndef read_dicom_frames_hu(path: Path) -> List[np.ndarray]:\n    \"\"\"Read DICOM file and return HU frames (with slope/intercept conversion)\"\"\"\n    ds = pydicom.dcmread(str(path), force=True)\n    pix = ds.pixel_array\n    slope = float(getattr(ds, \"RescaleSlope\", 1.0))\n    intercept = float(getattr(ds, \"RescaleIntercept\", 0.0))\n    frames: List[np.ndarray] = []\n    if pix.ndim == 2:\n        img = pix.astype(np.float32)\n        frames.append(img * slope + intercept)\n    elif pix.ndim == 3:   \n        for i in range(pix.shape[0]):\n            frm = pix[i].astype(np.float32)\n            frames.append(frm * slope + intercept)\n    else:\n        # Unsupported layout\n        pass\n    return frames\n\n\ndef min_max_normalize(img: np.ndarray) -> np.ndarray:\n    \"\"\"Min-max normalization to 0-255\"\"\"\n    mn, mx = float(img.min()), float(img.max())\n    if mx - mn < 1e-6:\n        return np.zeros_like(img, dtype=np.uint8)\n    norm = (img - mn) / (mx - mn)\n    return (norm * 255.0).clip(0, 255).astype(np.uint8)\n\ndef process_dicom_file_yolo(dcm_path: Path, keep_grayscale: bool = False) -> List[np.ndarray]:\n    \"\"\"Process single DICOM file for YOLO - for parallel processing\"\"\"\n    try:\n        frames = read_dicom_frames_hu(dcm_path)\n        processed_slices = []\n        for f in frames:\n            img_u8 = min_max_normalize(f)\n            processed_slices.append(img_u8)\n        return processed_slices\n    except Exception as e:\n        return []\n\ndef collect_series_slices_sorted(series_dir: Path) -> List[Path]:\n    \"\"\"Collect all DICOM files in series directory and sort by spatial position (match validation script).\"\"\"\n    dicom_files = list(series_dir.glob(\"*.dcm\"))\n\n    if not dicom_files:\n        return []\n\n    # First pass: collect all slices with their spatial information (match validation script)\n    temp_slices = []\n    for filepath in dicom_files:\n        try:\n            ds = pydicom.dcmread(str(filepath), stop_before_pixels=True)\n\n            # Priority order for sorting: SliceLocation > ImagePositionPatient > InstanceNumber\n            if hasattr(ds, \"SliceLocation\"):\n                # SliceLocation is the most reliable for slice ordering\n                sort_val = float(ds.SliceLocation)\n            elif hasattr(ds, \"ImagePositionPatient\") and len(ds.ImagePositionPatient) >= 3:\n                # Fallback to z-coordinate from ImagePositionPatient\n                sort_val = float(ds.ImagePositionPatient[-1])\n            else:\n                # Final fallback to InstanceNumber\n                sort_val = float(getattr(ds, \"InstanceNumber\", 0))\n\n            # Store filepath with its sort value\n            temp_slices.append((sort_val, filepath))\n\n        except Exception as e:\n            # Fallback: use filename as last resort\n            temp_slices.append((str(filepath.name), filepath))\n            continue\n\n    if not temp_slices:\n        return []\n\n    # Sort slices by the determined sort value (spatial order)\n    temp_slices.sort(key=lambda x: x[0])\n\n    # Extract the sorted filepaths\n    sorted_files = [item[1] for item in temp_slices]\n    return sorted_files\n\ndef collect_series_slices(series_dir: Path) -> List[Path]:\n    \"\"\"Collect all DICOM files in a series directory (recursively) - legacy function.\"\"\"\n    return collect_series_slices_sorted(series_dir)\n","metadata":{"execution":{"iopub.status.busy":"2025-10-14T02:32:55.731313Z","iopub.execute_input":"2025-10-14T02:32:55.731574Z","iopub.status.idle":"2025-10-14T02:32:55.753364Z","shell.execute_reply.started":"2025-10-14T02:32:55.731551Z","shell.execute_reply":"2025-10-14T02:32:55.752733Z"},"papermill":{"duration":0.013845,"end_time":"2025-10-03T05:10:24.993772","exception":false,"start_time":"2025-10-03T05:10:24.979927","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ====================================================\n# Configuration\n# ====================================================\nclass FlayerInferenceConfig:\n    # Model settings\n    model_name = \"tf_efficientnetv2_s.in21k_ft_in1k\"\n    size = 448#512#448 #384\n    target_cols = LABEL_COLS\n    num_classes = len(VESSEL_LABELS)\n    heatmap_classes = VESSEL_LABELS\n    in_chans = 1\n    \n    # Preprocessing settings\n    #target_shape = (32, 384, 384)  # (depth, height, width)\n    #target_shape = (32, 448, 448)  \n    #target_shape = (32, 512, 512) \n    #target_shape = (32, 576, 576)\n    #target_shape = (48, 448, 448)\n    target_shape = (64, 448, 448)\n    output_stride_depth = 1\n    output_stride_height = 16#32\n    output_stride_width = 16#32\n    base_channels: int = 32\n    # Inference settings\n    batch_size = 1\n    use_amp = True #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    #model_dir = '/kaggle/input/rsna-iad-model-atom/outputs_init_6336'\n    #model_dir = '/kaggle/input/rsna-iad-model-atom/outputs_flip_swaplabel_6634'\n    #model_dir = '/kaggle/input/rsna-iad-model-atom/outputs_contrast_6697'\n    #model_dir = '/kaggle/input/rsna-iad-model-atom/outputs_default_6670'\n\n    model_dirs = [\n        #\"/kaggle/input/rsna-iad-model-atom/outputs_heatmap_3dnorm_feature_layer2_classx2_74\" #1fold lb71\n        #\"/kaggle/input/rsna-iad-model-atom/heatmap_512_flayer2_3dnorm_7275\", #lb75+\n        #\"/kaggle/input/rsna-iad-model-atom/heatmap_512_flayer2_3dnorm_flipud_7176\"\n        #\"/kaggle/input/rsna-iad-model-atom/heatmap_z48_448_flayer2_fold0_7639\"\n        \"/kaggle/input/iad-model/outputs_heatmap_aux_v1_acc2\"\n    ]\n\n\n    \n    n_fold = 5#1#5\n    #trn_fold = [0, 1, 2, 3, 4]\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\nFLAYER_CFG = FlayerInferenceConfig()\n\n\n# ====================================================\n# YOLO Configuration\n# ====================================================\nIMG_SIZE = 512\nBATCH_SIZE = int(os.getenv(\"YOLO_BATCH_SIZE\", \"32\"))\nMAX_WORKERS = 4\n\nYOLO_MODEL_CONFIGS = [\n    {\n        \"path\": \"/kaggle/input/rsna-yolo-models/yolo-11m-2.5D_fold0/weights/best.pt\",\n        \"fold\": 0,\n        \"weight\": 1.0,\n        \"name\": \"YOLOv11m\"\n    },\n    {\n        \"path\": \"/kaggle/input/rsna-yolo-models/yolo-11m-2.5D_fold1/weights/best.pt\",\n        \"fold\": 1,\n        \"weight\": 1.0,\n        \"name\": \"YOLOv11m\"\n    },\n    {\n         \"path\": \"/kaggle/input/rsna-yolo-models/yolo-11m-2.5D_fold22/weights/best.pt\",\n         \"fold\": 2,\n         \"weight\": 1.0,\n         \"name\": \"YOLOv11m\"\n    }, \n    # {\n    #     \"path\": \"/kaggle/input/rsna-yolo-models/yolo-11m-2.5D_fold3/weights/best.pt\",\n    #     \"fold\": 3,\n    #     \"weight\": 1.0,\n    #     \"name\": \"YOLOv11m\"\n    # },\n    # {\n    #     \"path\": \"/kaggle/input/rsna-yolo-models/yolo-11m-2.5D_fold4/weights/best.pt\",\n    #     \"fold\": 4,\n    #     \"weight\": 1.0,\n    #     \"name\": \"YOLOv11m\"\n    # },\n    ###\n    {\n        \"path\": \"/kaggle/input/rsna-yolo-models/cv_effnetv2_s_drop_path_25d_fold0/weights/best.pt\",\n        \"fold\": 0,\n        \"weight\": 1.0,\n        \"name\": \"effv2s\"\n    },\n    {\n        \"path\": \"/kaggle/input/rsna-yolo-models/cv_effnetv2_s_drop_path_25d_fold1/weights/best.pt\",\n        \"fold\": 1,\n        \"weight\": 1.0,\n        \"name\": \"effv2s\"\n    },\n    {\n         \"path\": \"/kaggle/input/rsna-yolo-models/cv_effnetv2_s_drop_path_25d_fold2/weights/best.pt\",\n         \"fold\": 2,\n         \"weight\": 1.0,\n         \"name\": \"effv2s\"\n    }, \n    # {\n    #     \"path\": \"/kaggle/input/rsna-yolo-models/cv_effnetv2_s_drop_path_25d_fold3/weights/best.pt\",\n    #     \"fold\": 3,\n    #     \"weight\": 1.0,\n    #     \"name\": \"effv2s\"\n    # },\n    # {\n    #     \"path\": \"/kaggle/input/rsna-yolo-models/cv_effnetv2_s_drop_path_25d_fold4/weights/best.pt\",\n    #     \"fold\": 4,\n    #     \"weight\": 1.0,\n    #     \"name\": \"effv2s\"\n    # },\n]\n# ====================================================\n# Model Loading and Inference\n# ====================================================\n# Global variables\nYOLO_MODELS = []\nFLAYER_MODELS = {}\nFLAYER_TRANSFORM = None\nFLAYER_TTA_TRANSFORMS = None\n\n# ====================================================\n# Transforms\n# ====================================================\ndef get_inference_transform():\n    \"\"\"Get inference transformation\"\"\"\n    return A.Compose([\n        A.Resize(FLAYER_CFG.size, FLAYER_CFG.size),\n        A.Normalize(),\n        #A.Normalize(mean=(0.0,), std=(1.0,)),  # no-op for 1-channel\n        ToTensorV2(),\n    ])","metadata":{"execution":{"iopub.status.busy":"2025-10-14T02:32:55.754172Z","iopub.execute_input":"2025-10-14T02:32:55.754448Z","iopub.status.idle":"2025-10-14T02:32:55.771762Z","shell.execute_reply.started":"2025-10-14T02:32:55.754426Z","shell.execute_reply":"2025-10-14T02:32:55.77115Z"},"papermill":{"duration":0.014746,"end_time":"2025-10-03T05:10:25.039561","exception":false,"start_time":"2025-10-03T05:10:25.024815","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"######################################################################\n# 2.5 fate's and atom's model\n######################################################################\n\nclass CenterNet3DInfer(nn.Module):\n    \"\"\"Inference model mirroring training CenterNet3D architecture.\"\"\"\n    def __init__(self):\n        super().__init__()\n        self.backbone = timm.create_model(\n            FLAYER_CFG.model_name,\n            pretrained=False,\n            features_only=True,\n            #out_indices=(-1,),\n            out_indices=(-2,),\n        )\n        info = self.backbone.feature_info\n        self.feature_channels = info.channels()[-1]\n        self.encoder_in_channels = getattr(self.backbone, 'in_chans', None)\n        if self.encoder_in_channels is None:\n            default_input = self.backbone.default_cfg.get('input_size', (3,))\n            if isinstance(default_input, (list, tuple)):\n                self.encoder_in_channels = default_input[0]\n            else:\n                self.encoder_in_channels = int(default_input)\n        head_channels = FLAYER_CFG.base_channels\n        self.temporal_head = nn.Sequential(\n            nn.Conv3d(self.feature_channels, head_channels, kernel_size=3, padding=1),\n            nn.BatchNorm3d(head_channels),\n            nn.ReLU(inplace=True),\n            nn.Conv3d(head_channels, head_channels, kernel_size=3, padding=1),\n            nn.BatchNorm3d(head_channels),\n            nn.ReLU(inplace=True),\n        )\n        self.heatmap_head = nn.Conv3d(head_channels, len(FLAYER_CFG.heatmap_classes), kernel_size=1)\n        self.offset_head = nn.Conv3d(head_channels, 3, kernel_size=1)\n\n    def forward(self, x: torch.Tensor) -> Dict[str, torch.Tensor]:\n        b, c, d, h, w = x.shape\n        x = x.permute(0, 2, 1, 3, 4).reshape(b * d, c, h, w)\n        if x.shape[1] != self.encoder_in_channels:\n            if x.shape[1] == 1 and self.encoder_in_channels == 3:\n                x = x.repeat(1, 3, 1, 1)\n            else:\n                raise ValueError(f\"Input has {x.shape[1]} channels but encoder expects {self.encoder_in_channels}\")\n        feats = self.backbone(x)[0]\n        feat_c, feat_h, feat_w = feats.shape[1:]\n        feats = feats.view(b, d, feat_c, feat_h, feat_w).permute(0, 2, 1, 3, 4)\n        feat3d = self.temporal_head(feats)\n        heatmap = self.heatmap_head(feat3d)\n        offset = self.offset_head(feat3d)\n        return {\"heatmap\": heatmap, \"offset\": offset}\n\n\n@torch.no_grad()\ndef compute_class_logits_from_heatmap(heatmap: torch.Tensor) -> torch.Tensor:\n    b, c, d, h, w = heatmap.shape\n    flat = heatmap.view(b, c, -1)\n    class_logits = flat.max(dim=2).values\n    presence_logits = class_logits.max(dim=1, keepdim=True).values\n    return torch.cat([class_logits, presence_logits], dim=1)\n\n\ndef _resolve_model_dirs() -> list[str]:\n    \"\"\"\n    取得要載入的 model 目錄列表。\n    - 若 FLAYER_CFG.model_dirs 存在且非空，使用之\n    - 否則回退到單一 FLAYER_CFG.model_dir\n    \"\"\"\n    if hasattr(FLAYER_CFG, 'model_dirs') and FLAYER_CFG.model_dirs:\n        return list(FLAYER_CFG.model_dirs)\n    elif hasattr(FLAYER_CFG, 'model_dir') and FLAYER_CFG.model_dir:\n        return [FLAYER_CFG.model_dir]\n    else:\n        raise ValueError(\"Please specify FLAYER_CFG.model_dirs (list) or FLAYER_CFG.model_dir (str).\")\n\ndef _dir_label(path_str: str) -> str:\n    \"\"\"\n    用於建立 FLAYER_MODELS dict 的 key 前綴，避免不同資料夾的 fold key 衝突。\n    會取資料夾名稱當 label。\n    \"\"\"\n    return Path(path_str).name\n\ndef _get_model_name_for_dir(dir_label: str) -> str:\n    \"\"\"\n    若你之後需要不同資料夾有不同 model_name，可在 FLAYER_CFG 內加一個 dict:\n      FLAYER_CFG.dir_model_name_map = {\"expA\": \"tf_efficientnetv2_s\", \"expB\": \"convnext_base\"...}\n    若沒有，則回退使用 FLAYER_CFG.model_name。\n    \"\"\"\n    if hasattr(FLAYER_CFG, 'dir_model_name_map') and dir_label in FLAYER_CFG.dir_model_name_map:\n        return FLAYER_CFG.dir_model_name_map[dir_label]\n    return FLAYER_CFG.model_name\n\n\ndef load_model_fold(model_dir: str, fold: int) -> nn.Module:\n    \"\"\"Load a single fold heatmap model from a specific model_dir\"\"\"\n    dir_label = _dir_label(model_dir)\n    model_name = _get_model_name_for_dir(dir_label)\n\n    model_path = Path(model_dir) / f'{model_name}_fold{fold}_best.pth'\n    if not model_path.exists():\n        raise FileNotFoundError(f\"Model file not found: {model_path}\")\n\n    print(f\"[{dir_label}] Loading fold {fold} model from {model_path}...\")\n    checkpoint = torch.load(model_path, map_location=device, weights_only=False)\n\n    model = CenterNet3DInfer()\n    state = checkpoint['model'] if isinstance(checkpoint, dict) and 'model' in checkpoint else checkpoint\n    model.load_state_dict(state, strict=False)\n    model = model.to(device)\n    model.eval()\n    return model\n\n\ndef load_flayer_models():\n    \"\"\"Load all fold models from all configured model directories\"\"\"\n    global FLAYER_MODELS, FLAYER_TRANSFORM, FLAYER_TTA_TRANSFORMS\n    print(\"Loading all fold models from multiple model_dirs...\")\n\n    model_dirs = _resolve_model_dirs()\n    total_loaded = 0\n\n    for md in model_dirs:\n        dir_label = _dir_label(md)\n        for fold in FLAYER_CFG.trn_fold:\n            try:\n                model = load_model_fold(md, fold)\n                # key: (dir_label, fold)\n                FLAYER_MODELS[(dir_label, fold)] = model\n                total_loaded += 1\n            except Exception as e:\n                print(f\"Warning: Could not load [{dir_label}] fold {fold}: {e}\")\n\n    if not FLAYER_MODELS:\n        raise ValueError(\"No models were loaded successfully\")\n\n    # Initialize transforms (shared)\n    FLAYER_TRANSFORM = get_inference_transform()\n    FLAYER_TTA_TRANSFORMS = None  # TTA disabled due to left/right anatomy\n\n    # Warm up models\n    dummy_volume = torch.randn(1, FLAYER_CFG.in_chans, FLAYER_CFG.target_shape[0], FLAYER_CFG.size, FLAYER_CFG.size).to(device)\n    with torch.no_grad():\n        for (dir_label, fold), model in FLAYER_MODELS.items():\n            _ = model(dummy_volume)\n\n    print(f\"Models ready for inference! Loaded: {total_loaded} models \"\n          f\"from {len(model_dirs)} dirs, folds: {list(FLAYER_CFG.trn_fold)}.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-14T02:32:55.772552Z","iopub.execute_input":"2025-10-14T02:32:55.773135Z","iopub.status.idle":"2025-10-14T02:32:55.793049Z","shell.execute_reply.started":"2025-10-14T02:32:55.77311Z","shell.execute_reply":"2025-10-14T02:32:55.792357Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\ndef load_yolo_models():\n    \"\"\"Load all YOLO models\"\"\"\n    models = []\n    for config in YOLO_MODEL_CONFIGS:\n        model = YOLO(config[\"path\"])\n        model.to(device)\n        \n        model_dict = {\n            \"model\": model,\n            \"weight\": config[\"weight\"],\n            \"name\": config[\"name\"],\n            \"fold\": config[\"fold\"]\n        }\n        models.append(model_dict)\n    return models\n\ndef load_all_models():\n    \"\"\"Load all models (EfficientNet + YOLO)\"\"\"\n    global YOLO_MODELS, FLAYER_MODELS\n    \n\n    # Load YOLO models\n    YOLO_MODELS = load_yolo_models()\n    \n    if not FLAYER_MODELS:\n        load_flayer_models()\n\n    \n    # Warm up models\n    dummy_yolo_image = np.random.randint(0, 255, (512, 512, 3), dtype=np.uint8)\n\n    \n    with torch.no_grad():\n\n        \n        for model_dict in YOLO_MODELS:\n            model = model_dict[\"model\"]\n            _ = model.predict([dummy_yolo_image], verbose=False, device=device)","metadata":{"execution":{"iopub.status.busy":"2025-10-14T02:32:55.793846Z","iopub.execute_input":"2025-10-14T02:32:55.794103Z","iopub.status.idle":"2025-10-14T02:32:55.809263Z","shell.execute_reply.started":"2025-10-14T02:32:55.79408Z","shell.execute_reply":"2025-10-14T02:32:55.80867Z"},"papermill":{"duration":0.023125,"end_time":"2025-10-03T05:10:25.099131","exception":false,"start_time":"2025-10-03T05:10:25.076006","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#ytt avg then sigmoid \ndef flayer_predict_single_model(model: nn.Module, tensor_5d: torch.Tensor) -> torch.Tensor:\n    \"\"\"\n    Run inference for a single model and return LOGITS (torch.Tensor on device).\n    - tensor_5d: (1, 1, D, H, W) 已在 GPU/AMP 準備好的張量\n    \"\"\"\n    # 假設 model 已 .eval()，外層用 inference_mode/autocast\n    outputs = model(tensor_5d)\n\n    # 可能回 dict/tensor，保守處理\n    heatmap = outputs['heatmap'] if isinstance(outputs, dict) else outputs\n    logits = compute_class_logits_from_heatmap(heatmap)\n\n    # 確保 logits 在同一裝置、同一 dtype、且為 1D\n    logits = logits.to(tensor_5d.device, dtype=torch.float32)\n    logits = logits.flatten()  # (num_labels,)\n    return logits\n\n\ndef predict_flayer_ensemble(image: np.ndarray) -> np.ndarray:\n    \"\"\"\n    先對各模型 logits 做加權平均（在 GPU 上累加），最後一次 sigmoid。\n    同時把資料前處理只做一次，避免重工。\n    \"\"\"\n    # --- 一次性前處理到 GPU ---\n    # 原始 volume 是 (D, H, W)，轉成 (H, W, D) 給 FLAYER_TRANSFORM\n    image_hwd = image.transpose(1, 2, 0)  # (H, W, D)\n    transformed = FLAYER_TRANSFORM(image=image_hwd)\n\n    tensor = transformed['image']  # 可能是 numpy 或 torch.Tensor（視 FLAYER_TRANSFORM 實作而定）\n    if not torch.is_tensor(tensor):\n        tensor = torch.from_numpy(tensor)\n\n    # 期望 (D, H, W)\n    if tensor.dim() != 3:\n        raise ValueError(f\"FLAYER_TRANSFORM['image'] should be 3D (D,H,W), got shape {tuple(tensor.shape)}\")\n\n    # 統一 dtype/device，並補上 batch/channel 維度 → (1,1,D,H,W)\n    tensor_5d = tensor.to(device=device, dtype=torch.float32, non_blocking=True).unsqueeze(0).unsqueeze(0)\n\n    def _lookup_weight(dir_label: str, fold: int) -> float:\n        if getattr(FLAYER_CFG, 'ensemble_weights', None) is None:\n            return 1.0\n        ew = FLAYER_CFG.ensemble_weights\n        return (\n            ew.get((dir_label, fold), None)\n            or ew.get(f\"{dir_label}/{fold}\", None)\n            or ew.get(f\"{dir_label}_fold{fold}\", None)\n            or ew.get(f\"fold{fold}\", None)\n            or 1.0\n        )\n\n    sum_logits = None\n    sum_w = 0.0\n\n    # 比 no_grad() 更快的推論模式\n    with torch.inference_mode():\n        # 共享一個 autocast，避免在迴圈中重複建立 context\n        with autocast(enabled=FLAYER_CFG.use_amp):\n            for (dir_label, fold), model in FLAYER_MODELS.items():\n                w = float(_lookup_weight(dir_label, fold))\n                if w == 0.0:\n                    continue\n\n                logits = flayer_predict_single_model(model, tensor_5d)  # torch.Tensor on device, float32, 1D\n\n                # 初始化累加器並做 shape 檢查\n                if sum_logits is None:\n                    sum_logits = torch.zeros_like(logits)  # 確保 shape/dtype/device 一致\n                if sum_logits.shape != logits.shape:\n                    raise ValueError(f\"Logits shape mismatch: got {tuple(logits.shape)}, \"\n                                     f\"expected {tuple(sum_logits.shape)}\")\n\n                # 就地加權累加\n                sum_logits.add_(logits, alpha=w)\n                sum_w += w\n\n    # 邊界情況：沒有模型或權重總和為 0\n    if (sum_logits is None) or (sum_w == 0.0):\n        return np.full(len(LABEL_COLS), 0.5, dtype=np.float32)\n\n    avg_logits = sum_logits / float(sum_w)                    # 仍在 GPU\n    probs = torch.sigmoid(avg_logits).float().cpu().numpy()   # 只在最後搬回 CPU\n    return probs\n\n\n\n\ndef predict_flayer_ensemble(image: np.ndarray) -> np.ndarray:\n    \"\"\"\n    先對各模型 logits 做加權平均（在 GPU 上累加），最後一次 sigmoid。\n    同時把資料前處理只做一次，避免重工。\n    \"\"\"\n    # --- 一次性前處理到 GPU ---\n    # 原始 volume 是 (D, H, W)，轉成 (H, W, D) 給 FLAYER_TRANSFORM\n    image_hwd = image.transpose(1, 2, 0)  # (H, W, D)\n    transformed = FLAYER_TRANSFORM(image=image_hwd)\n\n    tensor = transformed['image']  # 可能是 numpy 或 torch.Tensor（視 FLAYER_TRANSFORM 實作而定）\n    if not torch.is_tensor(tensor):\n        tensor = torch.from_numpy(tensor)\n\n    # 期望 (D, H, W)\n    if tensor.dim() != 3:\n        raise ValueError(f\"FLAYER_TRANSFORM['image'] should be 3D (D,H,W), got shape {tuple(tensor.shape)}\")\n\n    # 統一 dtype/device，並補上 batch/channel 維度 → (1,1,D,H,W)\n    tensor_5d = tensor.to(device=device, dtype=torch.float32, non_blocking=True).unsqueeze(0).unsqueeze(0)\n\n    def _lookup_weight(dir_label: str, fold: int) -> float:\n        if getattr(FLAYER_CFG, 'ensemble_weights', None) is None:\n            return 1.0\n        ew = FLAYER_CFG.ensemble_weights\n        return (\n            ew.get((dir_label, fold), None)\n            or ew.get(f\"{dir_label}/{fold}\", None)\n            or ew.get(f\"{dir_label}_fold{fold}\", None)\n            or ew.get(f\"fold{fold}\", None)\n            or 1.0\n        )\n\n    sum_logits = None\n    sum_w = 0.0\n\n    flayer_preds = []\n\n    # 比 no_grad() 更快的推論模式\n    with torch.inference_mode():\n        # 共享一個 autocast，避免在迴圈中重複建立 context\n        with autocast(enabled=FLAYER_CFG.use_amp):\n            for (dir_label, fold), model in FLAYER_MODELS.items():\n                w = float(_lookup_weight(dir_label, fold))\n                if w == 0.0:\n                    continue\n\n                logits = flayer_predict_single_model(model, tensor_5d)  # torch.Tensor on device, float32, 1D\n\n                # 初始化累加器並做 shape 檢查\n                if sum_logits is None:\n                    sum_logits = torch.zeros_like(logits)  # 確保 shape/dtype/device 一致\n                if sum_logits.shape != logits.shape:\n                    raise ValueError(f\"Logits shape mismatch: got {tuple(logits.shape)}, \"\n                                     f\"expected {tuple(sum_logits.shape)}\")\n\n                # 就地加權累加\n                sum_logits.add_(logits, alpha=w)\n                sum_w += w\n                flayer_preds.append(logits.sigmoid().float().cpu().numpy())\n\n    # 邊界情況：沒有模型或權重總和為 0\n    if (sum_logits is None) or (sum_w == 0.0):\n        return np.full(len(LABEL_COLS), 0.5, dtype=np.float32)\n\n    avg_logits = sum_logits / float(sum_w)                    # 仍在 GPU\n    probs = torch.sigmoid(avg_logits).float().cpu().numpy()   # 只在最後搬回 CPU\n    return probs, flayer_preds\n\n\n@torch.no_grad()\ndef predict_yolo_ensemble(slices, metadata, flayer_fold_preds):\n    \"\"\"Run YOLO inference using all models\"\"\"\n    if not slices:\n        return 0.1, np.ones(len(YOLO_LABELS)) * 0.1\n\n    yolo11m_cls_preds = []\n    yolo11m_loc_preds = []\n    effv2s_cls_preds = []\n    effv2s_loc_preds = []\n    ensemble_cls_preds = []\n    ensemble_loc_preds = []\n    total_weight = 0.0\n    \n    for fold_id, model_dict in enumerate(YOLO_MODELS):\n        model = model_dict[\"model\"]\n        model_name = model_dict[\"name\"]\n        weight = model_dict[\"weight\"]\n        \n        try:\n            max_conf_all = 0.0\n            per_class_max = np.zeros(len(YOLO_LABELS), dtype=np.float32)\n            \n            # Process in batches\n            for i in range(0, len(slices), BATCH_SIZE):\n                batch_slices = slices[i:i+BATCH_SIZE]\n                \n                results = model.predict(\n                    batch_slices, \n                    verbose=False, \n                    batch=len(batch_slices), \n                    device=device, \n                    conf=0.01\n                )\n                \n                for r in results:\n                    if r is None or r.boxes is None or r.boxes.conf is None or len(r.boxes) == 0:\n                        continue\n                    try:\n                        confs = r.boxes.conf\n                        clses = r.boxes.cls\n                        for j in range(len(confs)):\n                            c = float(confs[j].item())\n                            k = int(clses[j].item())\n                            if c > max_conf_all:\n                                max_conf_all = c\n                            if 0 <= k < len(YOLO_LABELS) and c > per_class_max[k]:\n                                per_class_max[k] = c\n                    except Exception:\n                        try:\n                            batch_max = float(r.boxes.conf.max().item())\n                            if batch_max > max_conf_all:\n                                max_conf_all = batch_max\n                        except Exception:\n                            pass\n\n            if model_name == 'YOLOv11m':\n                yolo11m_cls_preds.append(max_conf_all * weight)\n                yolo11m_loc_preds.append(per_class_max * weight)\n            elif model_name == 'effv2s':\n                effv2s_cls_preds.append(max_conf_all * weight)\n                effv2s_loc_preds.append(per_class_max * weight)\n            \n            ensemble_cls_preds.append(max_conf_all * weight)\n            ensemble_loc_preds.append(per_class_max * weight)\n            total_weight += weight\n            \n        except Exception as e:\n            if model_name == 'YOLOv11m':\n                yolo11m_cls_preds.append(0.1 * weight)\n                yolo11m_loc_preds.append(np.ones(len(YOLO_LABELS)) * 0.1 * weight)\n            elif model_name == 'effv2s':\n                effv2s_cls_preds.append(0.1 * weight)\n                effv2s_loc_preds.append(np.ones(len(YOLO_LABELS)) * 0.1 * weight)\n                \n            ensemble_cls_preds.append(0.1 * weight)\n            ensemble_loc_preds.append(np.ones(len(YOLO_LABELS)) * 0.1 * weight)\n            total_weight += weight\n    \n    if total_weight > 0:\n        final_cls_pred = sum(ensemble_cls_preds) / (total_weight)\n        final_loc_preds = sum(ensemble_loc_preds) / (total_weight)\n    else:\n        final_cls_pred = 0.1\n        final_loc_preds = np.ones(len(YOLO_LABELS)) * 0.1\n\n    meta_lgb_preds = []\n    meta_xgb_preds = []\n    meta_cat_preds = []\n\n    for fold_id in range(len(YOLO_MODELS)//2):\n        try:\n            X = np.concatenate([np.array([yolo11m_cls_preds[fold_id]]), yolo11m_loc_preds[fold_id],\n                                np.array([effv2s_cls_preds[fold_id]]), effv2s_loc_preds[fold_id],\n                                flayer_fold_preds[fold_id], metadata], axis=0)[None, :]\n            lgb_pred = predict_prob_lgb(X, fold_id)\n            xgb_pred = predict_prob_xgb(X, fold_id)\n            cat_pred = predict_prob_cat(X, fold_id)\n            meta_lgb_preds.append(lgb_pred)\n            meta_xgb_preds.append(xgb_pred)\n            meta_cat_preds.append(cat_pred)\n        except Exception as e:\n            meta_lgb_preds.append(np.ones(len(YOLO_LABELS) + 1) * 0.1)\n            meta_xgb_preds.append(np.ones(len(YOLO_LABELS) + 1) * 0.1)\n            meta_cat_preds.append(np.ones(len(YOLO_LABELS) + 1) * 0.1)\n    # print(meta_lgb_preds)\n    # print(meta_xgb_preds)\n    # print(meta_cat_preds)\n    meta_lgb_preds = np.mean(meta_lgb_preds, axis=0)[:, 0]\n    meta_xgb_preds = np.mean(meta_xgb_preds, axis=0)[:, 0]\n    meta_cat_preds = np.mean(meta_cat_preds, axis=0)[:, 0]\n    return final_cls_pred, final_loc_preds, meta_lgb_preds, meta_xgb_preds, meta_cat_preds","metadata":{"execution":{"iopub.status.busy":"2025-10-14T02:34:48.404896Z","iopub.execute_input":"2025-10-14T02:34:48.405168Z","iopub.status.idle":"2025-10-14T02:34:48.430302Z","shell.execute_reply.started":"2025-10-14T02:34:48.405149Z","shell.execute_reply":"2025-10-14T02:34:48.429586Z"},"papermill":{"duration":0.018904,"end_time":"2025-10-03T05:10:25.122436","exception":false,"start_time":"2025-10-03T05:10:25.103532","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Safe processing function with memory cleanup\ndef process_dicom_series_for_flayer(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        preprocessor = FlayerDICOMPreprocessor(target_shape=target_shape)\n        return preprocessor.process_series(series_path)\n    finally:\n        gc.collect()\n\n# Test function\ndef test_single_series_flayer(series_path: str, target_shape: Tuple[int, int, int] = (32, 384, 384)):\n    \"\"\"\n    Test processing for single series\n    \"\"\"\n    try:\n\n        volume = process_dicom_series_for_flayer(series_path, target_shape)\n        return volume\n        \n    except Exception as e:\n        #print(f\"✗ Failed to process series: {e}\")\n        return None\n\n\nsex_map = {name[0]: name for name in le.classes_}\ndef parse_meta_data(ds):\n    # ---- Patient Age ----\n    try:\n        age_str = getattr(ds, 'PatientAge', '050Y')\n        age = int(''.join(filter(str.isdigit, age_str[:3])) or '50')\n        age = min(age, 100)\n    except Exception:\n        age = 50\n\n    # ---- Patient Sex ----\n    try:\n        sex = sex_map[getattr(ds, 'PatientSex', 'M')]\n        sex = le.transform(np.array([sex]))[0]\n    except Exception as e:\n        sex = 0\n    metadata = np.array([age, sex])\n    return metadata\n\n\ndef process_dicom_for_yolo(series_path: str, mode: str = \"2.5D\") -> List[np.ndarray]:\n    \"\"\"Process DICOM for YOLO with parallel processing and mode support\"\"\"\n    series_path = Path(series_path)\n    dicom_files = collect_series_slices(series_path)\n    ds = pydicom.dcmread(dicom_files[0], force=True)\n    metadata = parse_meta_data(ds)\n\n    if mode == \"2D\":\n        # For 2D mode, process each DICOM file individually (convert to RGB)\n        all_slices: List[np.ndarray] = []\n        with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:\n            # Submit in order and maintain order\n            futures = [executor.submit(process_dicom_file_yolo, dcm_path, False) \n                      for dcm_path in dicom_files]\n            \n            # Retrieve results in submission order\n            for future in futures:  # ✅ Deterministic order!\n                try:\n                    slices = future.result()\n                    all_slices.extend(slices)\n                except Exception as e:\n                    pass\n        return all_slices, metadata\n\n    elif mode == \"2.5D\":\n        # Similar fix for 2.5D mode\n        if len(dicom_files) < 3:\n            return process_dicom_for_yolo(series_path, \"2D\")\n\n        all_frames = []\n        with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:\n            # Submit in order and maintain order\n            futures = [executor.submit(process_dicom_file_yolo, dcm_path, True) \n                      for dcm_path in dicom_files]\n            \n            # Retrieve results in submission order\n            for future in futures:  # ✅ Deterministic order!\n                try:\n                    slices = future.result()\n                    all_frames.extend(slices)\n                except Exception as e:\n                    pass\n\n        # Rest of 2.5D logic remains the same...\n        if len(all_frames) < 3:\n            print(f\"Warning: Only {len(all_frames)} frames available, need at least 3 for 2.5D\")\n            return (all_frames, metadata) if all_frames else ([], metadata)\n\n        rgb_slices = []\n        for i in range(1, len(all_frames) - 1):\n            try:\n                prev_frame = all_frames[i-1]\n                curr_frame = all_frames[i]\n                next_frame = all_frames[i+1]\n                \n                if prev_frame is None or curr_frame is None or next_frame is None:\n                    continue\n      \n                if not (prev_frame.shape == curr_frame.shape == next_frame.shape):\n                    print(f\"Warning: Frame shape mismatch at index {i}\")\n                    continue\n                \n                rgb_img = np.stack([prev_frame, curr_frame, next_frame], axis=-1)\n\n                if IMG_SIZE > 0 and (rgb_img.shape[0] != IMG_SIZE or rgb_img.shape[1] != IMG_SIZE):\n                    rgb_img = cv2.resize(rgb_img, (IMG_SIZE, IMG_SIZE), interpolation=cv2.INTER_LINEAR)\n                \n                if rgb_img.shape[-1] != 3 or rgb_img.ndim != 3:\n                    print(f\"Warning: Invalid RGB shape {rgb_img.shape} at index {i}\")\n                    continue\n                    \n                rgb_slices.append(rgb_img)\n                \n            except Exception as e:\n                print(f\"Error creating RGB triplet at index {i}: {e}\")\n                continue\n        \n        print(f\"Created {len(rgb_slices)} valid 2.5D slices from {len(all_frames)} frames\")\n        return (rgb_slices, metadata) if rgb_slices else (all_frames, metadata)\n\n    else:\n        raise ValueError(f\"Unsupported YOLO mode: {mode}. Use '2D' or '2.5D'\")","metadata":{"execution":{"iopub.status.busy":"2025-10-14T02:32:55.836724Z","iopub.execute_input":"2025-10-14T02:32:55.836968Z","iopub.status.idle":"2025-10-14T02:32:55.854599Z","shell.execute_reply.started":"2025-10-14T02:32:55.836951Z","shell.execute_reply":"2025-10-14T02:32:55.853823Z"},"papermill":{"duration":0.010055,"end_time":"2025-10-03T05:10:25.135982","exception":false,"start_time":"2025-10-03T05:10:25.125927","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def _predict_inner(series_path: str) -> pl.DataFrame:\n    \"\"\"Main ensemble prediction logic\"\"\"\n    global YOLO_MODELS, FLAYER_MODELS\n    \n    # Load models if not already loaded\n    if  not YOLO_MODELS or not FLAYER_MODELS:\n        load_all_models()\n    try:\n        aneurysm_idx = LABEL_COLS.index('Aneurysm Present')\n        \n        # Process DICOM for both models\n        yolo_slices, metadata = process_dicom_for_yolo(series_path)\n        flayer_volume = process_dicom_series_for_flayer(series_path, FLAYER_CFG.target_shape)\n        print(f\"{flayer_volume.shape=}\")\n        \n        # Get flayer predictions\n        flayer_preds, flayer_fold_preds = predict_flayer_ensemble(flayer_volume)\n        flayer_preds = np.asarray(flayer_preds, dtype=np.float32)\n        if flayer_preds.shape[0] != len(LABEL_COLS):\n            raise ValueError(\"Flayer ensemble output length mismatch\")\n\n        #get yolo predictions\n        yolo_cls_pred, yolo_loc_preds, meta_lgb_preds, meta_xgb_preds, meta_cat_preds = predict_yolo_ensemble(yolo_slices, metadata, flayer_fold_preds)\n\n        # yolo_full_preds has preds in LABEL_COLS order now\n        yolo_full_preds = np.zeros(len(LABEL_COLS))\n        for i, label in enumerate(YOLO_LABELS):\n            if label in LABEL_COLS:\n                label_idx = LABEL_COLS.index(label)\n                yolo_full_preds[label_idx] = yolo_loc_preds[i]\n        yolo_full_preds[aneurysm_idx] = yolo_cls_pred\n\n        ensemble_preds =  ensemble_w[0] * meta_lgb_preds + ensemble_w[1] * meta_xgb_preds + ensemble_w[2] * meta_cat_preds + ensemble_w[3] * flayer_preds +  ensemble_w[4] * yolo_full_preds\n\n        #print(meta_lgb_preds, meta_xgb_preds, meta_cat_preds, flayer_preds, yolo_full_preds)\n        \n        # Create output dataframe\n        predictions_df = pl.DataFrame(\n            data=[ensemble_preds.tolist()],\n            schema=LABEL_COLS,\n            orient='row'\n        )\n        \n        return predictions_df\n        \n    except Exception as e:\n        print(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\ndef predict(series_path: str) -> pl.DataFrame:\n    \"\"\"\n    Top-level prediction function passed to the server.\n    Combines YOLO and EfficientNet with 50/50 weighting.\n    \"\"\"\n    try:\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        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        # Cleanup\n        shared_dir = '/kaggle/shared'\n        shutil.rmtree(shared_dir, ignore_errors=True)\n        os.makedirs(shared_dir, exist_ok=True)\n        \n        # Memory cleanup\n        if torch.cuda.is_available():\n            torch.cuda.empty_cache()\n        gc.collect()","metadata":{"_cell_guid":"71b8dead-f6d2-4299-9e44-c5c6e5492ea5","_uuid":"e29b6075-398a-46c5-98e5-9108702439f4","collapsed":false,"execution":{"iopub.status.busy":"2025-10-14T02:34:56.021167Z","iopub.execute_input":"2025-10-14T02:34:56.02178Z","iopub.status.idle":"2025-10-14T02:34:56.03083Z","shell.execute_reply.started":"2025-10-14T02:34:56.021758Z","shell.execute_reply":"2025-10-14T02:34:56.030123Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.012524,"end_time":"2025-10-03T05:10:25.151964","exception":false,"start_time":"2025-10-03T05:10:25.13944","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if __name__ == \"__main__\":\n    start_time = time.time()\n    \n    # Initialize the inference server\n    inference_server = kaggle_evaluation.rsna_inference_server.RSNAInferenceServer(predict)\n    \n    # Check if running in competition environment\n    if os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n        inference_server.serve()\n    else:\n        inference_server.run_local_gateway()\n        \n        # Display results if in local mode\n        submission_df = pl.read_parquet('/kaggle/working/submission.parquet')\n        print(f\"Submission shape: {submission_df.shape}\")\n        display(submission_df)\n    \n    print(f\"Total execution time: {time.time() - start_time:.2f} seconds\")","metadata":{"execution":{"iopub.status.busy":"2025-10-14T02:34:56.145521Z","iopub.execute_input":"2025-10-14T02:34:56.145828Z","iopub.status.idle":"2025-10-14T02:36:13.099515Z","shell.execute_reply.started":"2025-10-14T02:34:56.145806Z","shell.execute_reply":"2025-10-14T02:36:13.098802Z"},"papermill":{"duration":114.451079,"end_time":"2025-10-03T05:12:19.61779","exception":false,"start_time":"2025-10-03T05:10:25.166711","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}