{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":99552,"databundleVersionId":13851420,"sourceType":"competition"},{"sourceId":13228807,"sourceType":"datasetVersion","datasetId":8385277},{"sourceId":13350812,"sourceType":"datasetVersion","datasetId":8467273},{"sourceId":605676,"sourceType":"modelInstanceVersion","modelInstanceId":454330,"modelId":470548},{"sourceId":605720,"sourceType":"modelInstanceVersion","modelInstanceId":454370,"modelId":470583},{"sourceId":607859,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":455338,"modelId":471402}],"dockerImageVersionId":31154,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":129.923752,"end_time":"2025-10-12T16:04:55.346384","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-10-12T16:02:45.422632","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install /kaggle/input/wheels-20251001/wheels_20251001/*.whl --no-deps\n!cp -r /kaggle/input/nnunet-2-4-1-20251012-3/nnUNet-2.4.1_20251012_3 /kaggle/nnunet\n!cp -r /kaggle/input/wheels-20251001/wheels_20251001/dicom2nifti_20250917 /kaggle/dicom2nifti\n!cp -r /kaggle/input/wheels-20251001/wheels_20251001/acvl_utils-0.2.5 /kaggle/acvl_utils\n!cp -r /kaggle/input/wheels-20251001/wheels_20251001/batchgenerators-0.25.1 /kaggle/batchgenerators\n!cp -r /kaggle/input/wheels-20251001/wheels_20251001/dynamic_network_architectures-0.3.1 /kaggle/dynamic_network_architectures","metadata":{"execution":{"iopub.status.busy":"2025-10-14T08:12:17.357654Z","iopub.execute_input":"2025-10-14T08:12:17.357927Z","iopub.status.idle":"2025-10-14T08:12:25.596232Z","shell.execute_reply.started":"2025-10-14T08:12:17.357902Z","shell.execute_reply":"2025-10-14T08:12:25.595386Z"},"papermill":{"duration":7.535605,"end_time":"2025-10-12T16:02:56.416251","exception":false,"start_time":"2025-10-12T16:02:48.880646","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import sys\nsys.path.append('/kaggle/nnunet')  # 添加nnUNet安装路径到Python路径\nsys.path.append('/kaggle/dicom2nifti')\nsys.path.append('/kaggle/acvl_utils')\nsys.path.append('/kaggle/batchgenerators')\nsys.path.append('/kaggle/dynamic_network_architectures')","metadata":{"execution":{"iopub.status.busy":"2025-10-14T08:12:25.597259Z","iopub.execute_input":"2025-10-14T08:12:25.59752Z","iopub.status.idle":"2025-10-14T08:12:25.602872Z","shell.execute_reply.started":"2025-10-14T08:12:25.597487Z","shell.execute_reply":"2025-10-14T08:12:25.602192Z"},"papermill":{"duration":0.008742,"end_time":"2025-10-12T16:02:56.427944","exception":false,"start_time":"2025-10-12T16:02:56.419202","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==================================================\n# RSNA Intracranial Aneurysm Challenge - Inference\n# Two-stage pipeline: DICOM → NIfTI → Brain Segmentation → Aneurysm Classification\n# ==================================================\n\"\"\"HOUJING:\nONLY WORK FOR 2 GPUs.\nHow inference tasks are divided between workers are defined by how you call `executor.submit`.\nOnly create the thread pool once and use it for all case prediction, because creating the pool for each case is very time consuming.\nExtension:\nIf you want to use 4 folds with 2 GPUs:\n- Submit fold_0 and fold_1 prediction in parallel\n- Wait till finish\n- Submit fold_2 and fold_3 prediction in parallel\n- Wait till finish\n\"\"\"\nimport os\nimport numpy as np\nimport polars as pl\nimport torch\nimport torch.nn as nn\nimport pydicom\nimport shutil\nimport gc\nimport nibabel as nib\nimport dicom2nifti\nimport kaggle_evaluation.rsna_inference_server\nimport torch.nn.functional as F\nfrom skimage.transform import resize\nfrom pathlib import Path\nfrom typing import Tuple, Union, List, Tuple, Dict, Any\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\nfrom nnunetv2.inference.predict_from_raw_data_2D_orthogonal_planes_fast_0913 import nnUNetPredictor\nfrom nnunetv2.inference.predict_from_raw_data_two_seg_with_cls_no_seg_return_no_filter import nnUNetPredictor as nnUNetPredictorWithCls\nfrom nnunetv2.utilities.helpers import empty_cache, dummy_context\n\n# Constants\nMODEL_PATHS = {\n    'vessel_ROI_seg': \"/kaggle/input/dataset180_2d_vessel_box_seg_stable/pytorch/default/1/Dataset180_2D_vessel_box_seg_stable/nnUNetTrainer__nnUNetPlans__2d\",\n    'aneurysm_cls_1': \"/kaggle/input/dataset660_26classes_resize224_4661/pytorch/default/2/Dataset660_26classes_resize224_4661/onlyMirror01_lr4e3_100epochs_ps224\",\n    'aneurysm_cls_2': \"/kaggle/input/dataset660_26classes_resize224_4661/pytorch/default/2/Dataset660_26classes_resize224_4661/onlyMirror01_lr4e3_250epochs_ps224\",\n    'plane_2d_cls': \"/kaggle/input/resnet34_plane_2d_cls/pytorch/default/1/ResNet34_Plane_2D_cls/checkpoint_best_loss.pth\"\n}\nSHARED_DIR = Path('/kaggle/shared')\nTEMP_DIR = Path('/kaggle/working')\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# 在模块级别定义全局变量\nGLOBAL_VESSEL_ROI_PREDICTOR = None\nGLOBAL_ANEURYSM_PREDICTOR_ALL_FOLDS = None\nCLS_2D_PREDICTOR = None\n\nUSE_NUM_GPUS = 2\nNUM_INFER_WORKERS = 2\nCOMPILE_NETWORK = False\n\nexecutor = ThreadPoolExecutor(max_workers=NUM_INFER_WORKERS)\n\ndef get_device(gpu_id: int = 0) -> torch.device:\n    \"\"\"\n    Get the computation device, with validation for GPU availability.\n    \"\"\"\n    if torch.cuda.is_available() and gpu_id < torch.cuda.device_count():\n        return torch.device(f\"cuda:{gpu_id}\")\n    return torch.device(\"cpu\")\n\n# Get the device (this part of the original code is fine)\nDEVICE = get_device(gpu_id=0)\n\n# ==================================================\n# Plane Classification Model Definition\n# ==================================================\n\nclass BasicBlock(nn.Module):\n    expansion = 1\n    \n    def __init__(self, in_planes, planes, stride=1):\n        super(BasicBlock, self).__init__()\n        self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=3, stride=stride, padding=1, bias=False)\n        self.bn1 = nn.BatchNorm2d(planes)\n        self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=1, padding=1, bias=False)\n        self.bn2 = nn.BatchNorm2d(planes)\n        self.shortcut = nn.Sequential()\n        if stride != 1 or in_planes != self.expansion * planes:\n            self.shortcut = nn.Sequential(\n                nn.Conv2d(in_planes, self.expansion * planes, kernel_size=1, stride=stride, bias=False),\n                nn.BatchNorm2d(self.expansion * planes)\n            )\n    \n    def forward(self, x):\n        out = F.relu(self.bn1(self.conv1(x)))\n        out = self.bn2(self.conv2(out))\n        out += self.shortcut(x)\n        out = F.relu(out)\n        return out\n\n\nclass ResNetEncoder(nn.Module):\n    def __init__(self, block, num_blocks, in_channels=1):\n        super(ResNetEncoder, self).__init__()\n        self.in_planes = 64\n        self.block = block\n        self.conv1 = nn.Conv2d(in_channels, 64, kernel_size=7, stride=2, padding=3, bias=False)\n        self.bn1 = nn.BatchNorm2d(64)\n        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)\n        self.layer1 = self._make_layer(block, 64, num_blocks[0], stride=1)\n        self.layer2 = self._make_layer(block, 128, num_blocks[1], stride=2)\n        self.layer3 = self._make_layer(block, 256, num_blocks[2], stride=2)\n        self.layer4 = self._make_layer(block, 512, num_blocks[3], stride=2) \n        self.embed_dim = 512 * block.expansion\n        \n        self._init_weights()\n    \n    def _init_weights(self):\n        for m in self.modules():\n            if isinstance(m, nn.Conv2d):\n                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')\n            elif isinstance(m, nn.BatchNorm2d):\n                nn.init.constant_(m.weight, 1)\n                nn.init.constant_(m.bias, 0)\n    \n    def _make_layer(self, block, planes, num_blocks, stride):\n        strides = [stride] + [1] * (num_blocks - 1)\n        layers = []\n        for stride in strides:\n            layers.append(block(self.in_planes, planes, stride))\n            self.in_planes = planes * block.expansion\n        return nn.Sequential(*layers)\n    \n    def forward(self, x):\n        out = F.relu(self.bn1(self.conv1(x)))\n        out = self.maxpool(out)\n        out = self.layer1(out)\n        out = self.layer2(out)\n        out = self.layer3(out)\n        out = self.layer4(out)\n        return out\n\n\nclass CrossAttentionPooling(nn.Module):\n    def __init__(self, embed_dim, query_num, num_classes, num_heads=4, dropout=0.0):\n        super(CrossAttentionPooling, self).__init__()\n        self.embed_dim = embed_dim\n        self.num_classes = num_classes\n        self.query_num = query_num\n        self.class_query = nn.Parameter(torch.randn(query_num, embed_dim))\n        self.cross_attention = nn.MultiheadAttention(\n            embed_dim=embed_dim, num_heads=num_heads, dropout=dropout, batch_first=False\n        )\n        self.norm = nn.LayerNorm(embed_dim)\n        self.dropout = nn.Dropout(dropout)\n        self.classifier = nn.Linear(query_num * embed_dim, num_classes) \n        self._init_weights()\n    \n    def _init_weights(self):\n        nn.init.xavier_uniform_(self.class_query)\n        nn.init.xavier_uniform_(self.classifier.weight)\n        nn.init.constant_(self.classifier.bias, 0)\n        \n        for name, param in self.cross_attention.named_parameters():\n            if 'weight' in name:\n                nn.init.xavier_uniform_(param)\n            elif 'bias' in name:\n                nn.init.constant_(param, 0)\n    \n    def forward(self, x):\n        batch_size = x.shape[0]\n        x = x.flatten(2)\n        x = x.permute(2, 0, 1)\n        query = self.class_query.unsqueeze(1).repeat(1, batch_size, 1)\n        \n        attended, _ = self.cross_attention(query=query, key=x, value=x)\n        \n        attended = self.norm(attended)\n        attended = self.dropout(attended)\n        attended_permuted = attended.permute(1, 0, 2)\n        attended_flatten = attended_permuted.flatten(1)\n        logits = self.classifier(attended_flatten) \n        return logits\n\n\nclass ClassificationHead(nn.Module):\n    def __init__(self, embed_dim, query_num, num_classes, dropout=0.0, use_cross_attention=True, num_heads=4):\n        super(ClassificationHead, self).__init__()\n        if use_cross_attention:\n            self.pooling = CrossAttentionPooling(\n                embed_dim=embed_dim, \n                query_num=query_num, \n                num_classes=num_classes, \n                num_heads=num_heads, \n                dropout=dropout\n            )\n        else:\n            self.pooling = nn.Sequential(\n                nn.AdaptiveAvgPool2d(1), \n                nn.Flatten(1), \n                nn.Dropout(dropout), \n                nn.Linear(embed_dim, num_classes)\n            )\n    \n    def forward(self, x):\n        return self.pooling(x)\n\n\nclass PlaneResNet34(nn.Module):\n    \"\"\"单任务模型：仅 Plane 分类 (3分类: AX/SAG/COR)\"\"\"\n    \n    def __init__(self, dropout: float = 0.1):\n        super(PlaneResNet34, self).__init__()\n        \n        self.encoder = ResNetEncoder(BasicBlock, [3, 4, 6, 3], in_channels=1)\n        self.embed_dim = self.encoder.embed_dim\n        \n        self.head_plane = ClassificationHead(\n            embed_dim=self.embed_dim, \n            query_num=3, \n            num_classes=3, \n            dropout=dropout, \n            use_cross_attention=True\n        )\n\n    def forward(self, x):\n        features = self.encoder(x)\n        logits = self.head_plane(features)\n        return logits\n\n\n# ==================================================\n# Plane Classification Predictor\n# ==================================================\n\nclass PlaneClassifier:\n    \"\"\"Axial 切片 Plane 预测\"\"\"\n    \n    # Plane 类别映射\n    PLANE_MAP = {0: 'AX', 1: 'SAG', 2: 'COR'}\n    \n    def __init__(self, checkpoint_path: str, device: str = 'cuda:0', target_size=(256, 256)):\n        \"\"\"\n        初始化推理器\n        \n        Args:\n            checkpoint_path: 模型权重路径\n            device: 推理设备\n            target_size: 目标图像尺寸\n        \"\"\"\n        self.device = device\n        self.target_size = target_size\n        self.model = self._load_model(checkpoint_path)\n    \n    def _load_model(self, checkpoint_path: str) -> PlaneResNet34:\n        \"\"\"加载模型 -  修改：使用 PlaneResNet34\"\"\"\n        if not os.path.exists(checkpoint_path):\n            raise FileNotFoundError(f\"模型文件不存在: {checkpoint_path}\")\n        \n        model = PlaneResNet34(dropout=0.0)\n        \n        # 加载权重\n        checkpoint = torch.load(checkpoint_path, map_location=self.device)\n        model.load_state_dict(checkpoint['state_dict'], strict=True)\n        \n        model.to(self.device)\n        model.eval()\n        \n        return model\n    \n    def preprocess_slice(self, slice_2d: np.ndarray) -> torch.Tensor:\n        \"\"\"预处理 2D 切片\"\"\"\n        # 1. 类型转换和裁剪\n        slice_data = slice_2d\n        \n        # 2. Z-score 标准化\n        mean = slice_data.mean()\n        std = slice_data.std()\n        std = np.clip(std, 1e-8, None)\n        slice_data = (slice_data - mean) / std\n        \n        # 3. Resize\n        resized_slice = resize(slice_data, self.target_size, anti_aliasing=True).astype(np.float32)\n        \n        # 4. 转换为 Tensor\n        tensor = torch.from_numpy(resized_slice).unsqueeze(0).unsqueeze(0)\n        \n        return tensor\n    \n    def predict(self, tensor: torch.Tensor) -> Tuple[str, int, float, torch.Tensor]:\n        \"\"\"\n        执行 Plane 预测 - 修改：适配单任务模型的输出格式\n        \n        Args:\n            tensor: 输入 Tensor [1, 1, H, W]\n            \n        Returns:\n            plane_pred_label: 预测类别名称 ('AX', 'SAG', 'COR')\n            plane_pred: 预测类别索引 (0, 1, 2)\n            plane_prob: 最大概率值\n            plane_prob_list: 所有类别的概率分布 [1, 3]\n        \"\"\"\n        tensor = tensor.to(self.device)\n        \n        with torch.no_grad():\n            logits = self.model(tensor)\n        \n        # Plane 预测\n        plane_pred = logits.argmax(dim=1).item()\n        plane_prob_list = F.softmax(logits, dim=1)\n        plane_prob = plane_prob_list.max().item()\n        plane_pred_label = self.PLANE_MAP[plane_pred]\n        \n        return plane_pred_label, plane_pred, plane_prob, plane_prob_list\n    \n    def inference_from_slice(\n        self, \n        slice_2d: np.ndarray,\n    ) -> Tuple[str, int, float, torch.Tensor]:\n        \"\"\"\n        从 2D 切片推理\n        \n        Returns:\n            plane_pred_label: 预测类别名称\n            plane_pred: 预测类别索引\n            plane_prob: 最大概率\n            plane_prob_list: 概率分布\n        \"\"\"\n        # 1. 预处理\n        tensor = self.preprocess_slice(slice_2d)\n        \n        # 2. 推理\n        plane_pred_label, plane_pred, plane_prob, plane_prob_list = self.predict(tensor)\n        \n        # 3. 打印结果\n        self._print_result(plane_pred_label, plane_pred, plane_prob, plane_prob_list)\n        \n        return plane_pred_label, plane_pred, plane_prob, plane_prob_list\n    \n    def _print_result(self, plane_pred_label: str, plane_pred: int, plane_prob: float, plane_prob_list: torch.Tensor):\n        \"\"\"打印预测结果\"\"\"\n        print(\"=\" * 60)\n        print(\"Plane 预测结果:\")\n        print(f\"  预测类别: {plane_pred_label}\")\n        print(f\"  类别索引: {plane_pred}\")\n        print(f\"  置信度: {plane_prob:.4f} ({plane_prob*100:.2f}%)\")\n        print(f\"  概率分布: {plane_prob_list}\")\n        print(\"=\" * 60)\n\ndef correct_orientation(pixel_array, spacing, plane_id):\n    \"\"\"\n    修正图像方向到标准轴向视图\n    \"\"\"\n    if plane_id == 1:\n        # Sagittal → Axial\n        fixed_array = np.transpose(pixel_array, (1, 2, 0))\n        fixed_array = fixed_array[::-1, :, :]\n        fixed_spacing = [spacing[1], spacing[2], spacing[0]]\n        print(f\"  修正: {pixel_array.shape} → {fixed_array.shape}\")\n        print(f\"  修正: {spacing} → {fixed_spacing}\")\n        return fixed_array, fixed_spacing\n\n    elif plane_id == 2:\n        # Coronal → Axial\n        fixed_array = np.transpose(pixel_array, (1, 0, 2))\n        fixed_array = fixed_array[::-1, :, :]\n        fixed_spacing = [spacing[1], spacing[0], spacing[2]]\n        print(f\"  修正: {pixel_array.shape} → {fixed_array.shape}\")\n        print(f\"  修正: {spacing} → {fixed_spacing}\")\n        return fixed_array, fixed_spacing\n    else:\n        # 已经是Axial或者不需要修正\n        return pixel_array, spacing\n#=====================================================\n\ndef reorient_nii(orig_nii, targ_aff=\"LPS\"):\n    \"\"\"\n    Reorient to the standard LPS+ DICOM coord.\n    \"\"\"\n    if \"\".join(nib.aff2axcodes(orig_nii.affine)) == targ_aff:\n        return orig_nii\n    orig_ornt = nib.io_orientation(orig_nii.affine)\n    targ_ornt = nib.orientations.axcodes2ornt(targ_aff)\n    transform = nib.orientations.ornt_transform(orig_ornt, targ_ornt)\n    img_orient = orig_nii.as_reoriented(transform)\n    return img_orient\n\n# ==================================================\n# 1. 初始化模型（全局单次初始化）\n# ==================================================\n\ndef may_compile_network(network):\n    if COMPILE_NETWORK:\n        return torch.compile(network)\n    return network\n\ndef init_predictors(device):\n    global GLOBAL_VESSEL_ROI_PREDICTOR, GLOBAL_ANEURYSM_PREDICTOR_ALL_FOLDS, CLS_2D_PREDICTOR\n    \n    # 如果已经初始化，直接返回全局实例\n    if GLOBAL_VESSEL_ROI_PREDICTOR is not None and GLOBAL_ANEURYSM_PREDICTOR_ALL_FOLDS is not None and CLS_2D_PREDICTOR is not None:\n        return GLOBAL_VESSEL_ROI_PREDICTOR, GLOBAL_ANEURYSM_PREDICTOR_ALL_FOLDS, CLS_2D_PREDICTOR\n    \n    # Stage1: Vessel ROI segmentation (使用新的多平面预测器)\n    GLOBAL_VESSEL_ROI_PREDICTOR = nnUNetPredictor(\n        tile_step_size=0.5,\n        use_mirroring=False,\n        use_gaussian=True,\n        perform_everything_on_device=True,\n        device=device,\n        allow_tqdm=False\n    )\n    GLOBAL_VESSEL_ROI_PREDICTOR.initialize_from_trained_model_folder(\n        model_training_output_dir=MODEL_PATHS['vessel_ROI_seg'],\n        use_folds=(0,),\n        checkpoint_name='checkpoint_final.pth',\n    )\n    GLOBAL_VESSEL_ROI_PREDICTOR.initialize_network_and_gaussian()\n    GLOBAL_VESSEL_ROI_PREDICTOR.network = may_compile_network(GLOBAL_VESSEL_ROI_PREDICTOR.network)\n\n    # Stage2: Aneurysm classification\n    aneurysm_predictor_f0 = nnUNetPredictorWithCls(\n        tile_step_size=0.5,\n        use_gaussian=True,\n        use_mirroring=False,\n        perform_everything_on_device=True,\n        device=get_device(gpu_id=0),\n        verbose=False\n    )\n\n    aneurysm_predictor_f1 = nnUNetPredictorWithCls(\n        tile_step_size=0.5,\n        use_gaussian=True,\n        use_mirroring=False,\n        perform_everything_on_device=True,\n        device=get_device(gpu_id=1 if USE_NUM_GPUS == 2 else 0),\n        verbose=False\n    )\n\n    aneurysm_predictor_f0.initialize_from_trained_model_folder(\n        model_training_output_dir=MODEL_PATHS['aneurysm_cls_1'],\n        use_folds=(0, ),\n        checkpoint_name=\"checkpoint_final.pth\",\n    )\n    aneurysm_predictor_f0.initialize_network_and_gaussian()\n\n    aneurysm_predictor_f1.initialize_from_trained_model_folder(\n        model_training_output_dir=MODEL_PATHS['aneurysm_cls_2'],\n        use_folds=(1, ),\n        checkpoint_name=\"checkpoint_final.pth\",\n    )\n    aneurysm_predictor_f1.initialize_network_and_gaussian()\n\n    aneurysm_predictor_f0.network = may_compile_network(aneurysm_predictor_f0.network)\n    aneurysm_predictor_f1.network = may_compile_network(aneurysm_predictor_f1.network)\n\n    # ========== 初始化2D方向分类推理器 ==========\n    CLS_2D_PREDICTOR = PlaneClassifier(\n        checkpoint_path=MODEL_PATHS['plane_2d_cls'],\n        device=device,\n        target_size=(256, 256)\n    )\n    CLS_2D_PREDICTOR.model = may_compile_network(CLS_2D_PREDICTOR.model)\n\n    GLOBAL_ANEURYSM_PREDICTOR_ALL_FOLDS = [aneurysm_predictor_f0, aneurysm_predictor_f1]\n    return GLOBAL_VESSEL_ROI_PREDICTOR, GLOBAL_ANEURYSM_PREDICTOR_ALL_FOLDS, CLS_2D_PREDICTOR\n\n    \nGLOBAL_VESSEL_ROI_PREDICTOR, GLOBAL_ANEURYSM_PREDICTOR, CLS_2D_PREDICTOR = init_predictors(DEVICE)\n\ndef group_dicom_files(study_folder_path: str) -> Dict[Tuple, List[str]]:\n    \"\"\"\n    根据 StudyInstanceUID, FrameOfReferenceUID, Modality, 和 ImageOrientationPatient\n    对 DICOM 文件进行分组（模拟序列）。\n    \"\"\"\n    dicom_groups = {}\n    dicom_files = []\n\n    for root, _, files in os.walk(study_folder_path):\n        for file in files:\n            if file.endswith(('.dcm', '.DCM')) or ('.' not in file and len(file) > 1):\n                dicom_files.append(os.path.join(root, file))\n\n    if not dicom_files:\n        print(f\"在路径 {study_folder_path} 中未找到任何 DICOM 文件。\")\n        return dicom_groups\n\n    print(f\"总共找到 {len(dicom_files)} 个文件，开始分组...\")\n\n    for file_path in dicom_files:\n        try:\n            ds = pydicom.dcmread(file_path, stop_before_pixels=True)\n            \n            study_uid = getattr(ds, 'StudyInstanceUID', 'NO_STUDY_UID')\n            frame_uid = getattr(ds, 'FrameOfReferenceUID', 'NO_FRAME_UID')\n            modality = getattr(ds, 'Modality', 'UNKNOWN')\n            \n            # 转换为元组并四舍五入\n            orientation = getattr(ds, 'ImageOrientationPatient', [0, 0, 0, 0, 0, 0])\n            orientation_key = tuple(np.round(orientation, 4)) \n\n            group_key = (study_uid, frame_uid, modality, orientation_key)\n\n            if group_key not in dicom_groups:\n                dicom_groups[group_key] = []\n            dicom_groups[group_key].append(file_path)\n\n        except (pydicom.errors.InvalidDicomError, Exception):\n            continue\n\n    print(\"-\" * 50)\n    print(f\"分组完成。共识别出 {len(dicom_groups)} 个逻辑序列。\")\n    return dicom_groups\n\ndef get_largest_series_files(all_series: Dict[Tuple, List[str]]) -> List[str]:\n    \"\"\"\n    找出层数最多的序列，并返回按 InstanceNumber 排序后的文件列表。\n\n    Args:\n        all_series (dict): group_dicom_files 返回的分组字典。\n\n    Returns:\n        list: 层数最多序列中按 InstanceNumber 排序后的 DICOM 文件路径列表。\n    \"\"\"\n    if not all_series:\n        return []\n\n    # 1. 找出层数最多的序列\n    # (Key, 文件列表)\n    max_layers_series_item = None\n    max_layers = 0\n\n    for group_key, file_list in all_series.items():\n        if len(file_list) > max_layers:\n            max_layers = len(file_list)\n            max_layers_series_item = (group_key, file_list)\n\n    if not max_layers_series_item:\n        print(\"未找到任何有效序列。\")\n        return []\n\n    target_group_key, target_series_files = max_layers_series_item\n    \n    print(\"-\" * 50)\n    print(f\"*** 找到层数最多的序列 *** (层数: {max_layers})\")\n    print(f\"  Modality: {target_group_key[2]}\")\n    print(f\"  方向 (IOP): {target_group_key[3][:3]}...\")\n\n    # 2. 对目标序列进行排序 (使用 InstanceNumber)\n    sorted_files_with_number: List[Tuple[Any, str]] = []\n    \n    # 遍历文件，获取 InstanceNumber\n    for fp in target_series_files:\n        try:\n            ds = pydicom.dcmread(fp, stop_before_pixels=True)\n            # 使用 InstanceNumber 进行排序。如果 InstanceNumber 不存在，则使用 0。\n            # 也可以考虑使用 ImagePositionPatient[2] 来排序\n            instance_number = getattr(ds, 'InstanceNumber', 0)\n            sorted_files_with_number.append((instance_number, fp))\n        except:\n            # 无法读取的文件跳过\n            pass\n\n    # 按 InstanceNumber 排序\n    sorted_files_with_number.sort(key=lambda x: x[0])\n    \n    final_file_paths = [fp for _, fp in sorted_files_with_number]\n    \n    return final_file_paths\n\ndef get_spacing_by_shape(shape):\n    \"\"\"\n    根据每个轴的形状高效映射spacing\n    规则:\n    - 轴 > 300: 0.5mm\n    - 120 < 轴 <= 300: 0.55mm  \n    - 100 < 轴 <= 120: 0.75mm\n    - 80 < 轴 <= 100: 1.0mm\n    - 60 < 轴 <= 80: 1.5mm\n    - 45 < 轴 <= 60: 3.0mm\n    - 轴 <= 45: 5.0mm\n    \"\"\"\n    spacing = []\n    for dim in shape:\n        if dim > 300:\n            spacing.append(0.5)\n        elif dim > 120:\n            spacing.append(0.55)\n        elif dim > 100:\n            spacing.append(0.75)\n        elif dim > 80:\n            spacing.append(1.0)\n        elif dim > 60:\n            spacing.append(1.5)\n        elif dim > 45:\n            spacing.append(3.0)\n        else:\n            spacing.append(5.0)\n    return spacing\n\ndef flip_z(img_tensor: torch.Tensor) -> torch.Tensor:\n    \"\"\"Z轴 (上下) 翻转: 对应 dim=2\"\"\"\n    # 形状 [B, C, D, H, W]，D 对应 dim=2\n    return torch.flip(img_tensor, dims=[2])\n\ndef flip_y(img_tensor: torch.Tensor) -> torch.Tensor:\n    \"\"\"Y轴 (前后) 翻转: 对应 dim=3\"\"\"\n    # 形状 [B, C, D, H, W]，H 对应 dim=3\n    return torch.flip(img_tensor, dims=[3])\n\ndef flip_x(tensor):\n    \"\"\"X轴 (左右) 翻转: 对应 dim=4\"\"\"\n    # 形状 [B, C, D, H, W]，H 对应 dim=3\n    return torch.flip(tensor, dims=[4])\n\n@torch.no_grad()\ndef worker_infer(num_cls_task, tta_batch_size, image_resized, aneurysm_predictor_fold, fold_i, all_fold_mean_logits_by_task):\n    \"\"\"\n    image_resized: can be on whatever device, because `image_resized = image_resized.to(device)` \n        will move it to correct device for inference. \n    Results are on CPU.\n    all_fold_mean_logits_by_task: modified inplace in this function.\n        It is thread-safe if we make sure different threads write to different slots (or sub-slots) of the list.\n    \"\"\"\n    device = aneurysm_predictor_fold.device\n    print(f\"[*] Worker Using Device: {device}\")\n\n    # Task 2 (位置分类) 左右翻转后的索引互换映射 (0-12共13个类别)\n    # [L_ICL, R_ICL, L_SCL, R_SCL, L_MCA, R_MCA, AC, L_AC, R_AC, L_PC, R_PC, BT, OP]\n    # 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12\n    # 互换: 1, 0, 3, 2, 5, 4, 6, 8, 7, 10, 9, 11, 12\n    # 仅针对 Task 2 的 logits (形状是 [N_classes=13])\n    TASK2_SWAP_INDICES = [1, 0, 3, 2, 5, 4, 6, 8, 7, 10, 9, 11, 12]\n\n    image_resized = image_resized.to(device)\n    # ======================================================\n    # TTA 步骤 1: 构造所有 TTA 增强图像列表\n    # 增加 X 轴翻转 (左右翻转)\n    # ======================================================\n    \n    # 1. 原始图像\n    I_orig = image_resized\n    # 2. 增强 A: Y 轴翻转 (前后)\n    I_flip_y = flip_y(I_orig)\n    # 3. 增强 B: Z 轴翻转 (上下)\n    I_flip_z = flip_z(I_orig)\n    # 4. 增强 C: Y 轴 + Z 轴翻转\n    I_flip_yz = flip_y(I_flip_z)\n    \n    # 5. 增强 D: X 轴翻转 (左右)\n    I_flip_x = flip_x(I_orig)\n    # 6. 增强 E: X 轴 + Y 轴翻转\n    I_flip_xy = flip_x(I_flip_y)\n    # 7. 增强 F: X 轴 + Z 轴翻转\n    I_flip_xz = flip_x(I_flip_z)\n    # 8. 增强 G: X 轴 + Y 轴 + Z 轴翻转\n    I_flip_xyz = flip_x(I_flip_yz)\n    \n    # TTA 列表 (总共 8 个增强)\n    tta_images = [\n        I_orig, I_flip_y, I_flip_z, I_flip_yz,\n        I_flip_x, I_flip_xy, I_flip_xz, I_flip_xyz\n    ]\n    \n    # 标记哪些增强执行了 X 轴 (左右) 翻转\n    # 0: 未翻转, 1: X 轴翻转\n    # 对应 tta_images 列表\n    x_flip_masks = [0, 0, 0, 0, 1, 1, 1, 1] \n\n    # ======================================================\n    # TTA 步骤 2: 迭代推理（支持设置 tta_batch_size）\n    # ======================================================\n    \n    num_tta_augments = len(tta_images) # 8\n    # current_fold_tta_logits_by_task[task_i] 是一个列表，存储该任务所有 TTA 图像的 logits\n    current_fold_tta_logits_by_task = [[] for _ in range(num_cls_task)]\n    \n    empty_cache(device)\n    \n    for i in range(0, num_tta_augments, tta_batch_size):\n        # 构造当前批次的 TTA 图像和对应的翻转标记\n        batch_tta_images = tta_images[i:i + tta_batch_size]\n        batch_x_flip_masks = x_flip_masks[i:i + tta_batch_size]\n        \n        # 堆叠成 TTA Batch (形状: [tta_batch_size, 1, 224, 224, 224])\n        image_tta_batch = torch.cat(batch_tta_images, dim=0)\n\n        with torch.autocast(device.type, enabled=True) if device.type == 'cuda' else dummy_context():\n            # predicted_logits_list 形状: [task1_logits, task2_logits], \n            predicted_logits_list = aneurysm_predictor_fold.network(image_tta_batch, only_forward_cls=True)\n            \n            # 收集每个任务的 logits\n            # Task 1 (第一个任务, 索引 0): 不涉及左右互换 (假设是 Aneurysm Present)\n            current_fold_tta_logits_by_task[0].append(predicted_logits_list[0])\n            \n            # Task 2 (第二个任务, 索引 1): 处理左右互换\n            current_task2_logits = predicted_logits_list[1]\n            \n            processed_logits_list = []\n            for j, is_x_flipped in enumerate(batch_x_flip_masks):\n                # 如果当前 TTA 图像进行了 X 轴翻转 (左右翻转)\n                if is_x_flipped == 1:\n                    # 则需要将 Task 2 结果的左右位置对调\n                    flipped_logits = current_task2_logits[j][TASK2_SWAP_INDICES]\n                    processed_logits_list.append(flipped_logits)\n                else:\n                    # 否则保持原始 Logits\n                    processed_logits_list.append(current_task2_logits[j])\n                    \n            # 将处理后的 Task 2 Logits 收集\n            current_fold_tta_logits_by_task[1].append(torch.stack(processed_logits_list))\n        \n        del image_tta_batch\n        empty_cache(device) # 清理释放的显存\n\n\n    # --------------------------------------------------\n    # TTA 步骤 3: 当前 Fold 的结果整合 (Logits 平均)\n    # --------------------------------------------------\n    for task_i in range(num_cls_task):\n        # 合并当前 Fold 的所有批次的 logits，形成一个大的 [num_tta_augments, 类别数] Tensor\n        logits_tta = torch.cat(current_fold_tta_logits_by_task[task_i], dim=0)\n        \n        # Logits 平均 (在第 0 维，即 Batch 维)\n        mean_logit_fold = logits_tta.mean(dim=0, keepdim=False).cpu() # 形状: [类别数]\n        \n        # 收集当前 Fold 的 TTA 平均 Logits\n        all_fold_mean_logits_by_task[task_i][fold_i] = mean_logit_fold\n    \n\ndef predict_aneurysm(input_img_np, original_spacing, device, tta_batch_size=2):\n    \"\"\"\n    动脉瘤预测函数 (增加左右翻转 TTA，并对 Task 2 结果进行左右互换)\n    \n    参数:\n    input_img_np: 输入图像numpy数组\n    original_spacing: 原始图像间距 (x, y, z)\n    device: 计算设备 (cpu/cuda)\n    \n    返回:\n    timepoint_probs: 分类概率数组 [task2概率, task1概率]\n    \"\"\"\n    # Stage1: 血管ROI预测\n    stage_1_target_spacing = np.array([1, 0.55, 0.5])\n    with torch.no_grad():\n        z_min_final, z_max_final, y_min_final, y_max_final, x_min_final, x_max_final = GLOBAL_VESSEL_ROI_PREDICTOR.predict_from_multi_axial_slices(\n            input_img_np, original_spacing, stage_1_target_spacing, max_batch_size=16\n        )\n\n    # 裁剪图像到ROI区域\n    img_cropped_np = input_img_np[0][z_min_final:z_max_final, y_min_final:y_max_final, x_min_final:x_max_final][None]\n\n    del input_img_np \n\n    img_cropped_np = np.ascontiguousarray(img_cropped_np)\n\n    # Stage2: 动脉瘤分类\n    img_cropped_tensor = torch.from_numpy(img_cropped_np).half().to(device)  # HOUJING: added half()\n\n    # 图像归一化\n    image_normed = (img_cropped_tensor - img_cropped_tensor.mean()) / img_cropped_tensor.std().clip(1e-8)\n\n    del img_cropped_tensor\n\n    dst_shape = [224, 224, 224]\n    \n    # 图像重采样\n    image_resized = torch.nn.functional.interpolate(\n        image_normed[None], size=dst_shape, mode='trilinear', align_corners=True\n    )\n\n    del image_normed\n\n    # 初始化一个列表，用于存储每个 TTA 图像的 Logits\n    # all_fold_mean_logits_by_task[task_i] 是一个列表，存储该任务所有 TTA 图像的 logits\n    num_cls_task = 2 # 假设是 2 个任务\n    n_folds = 2\n    all_fold_mean_logits_by_task = [[None for _ in range(n_folds)] for _ in range(num_cls_task)]\n\n    # ======================================================\n    # 推理每个 Fold 模型 in parallel\n    # ======================================================\n    futures = []\n    for fold_i, aneurysm_predictor_fold in enumerate(GLOBAL_ANEURYSM_PREDICTOR_ALL_FOLDS):\n        # This runs asynchronously\n        futures.append(executor.submit(worker_infer, num_cls_task, tta_batch_size, image_resized, aneurysm_predictor_fold, fold_i, all_fold_mean_logits_by_task))\n    # Iterating over as_completed(futures) ensures all tasks finish before moving on\n    for future in as_completed(futures):\n        pass\n        \n    # ======================================================\n    # 最终结果整合 (所有 Fold 的 TTA 平均 Logits 平均)\n    # ======================================================\n    \n    aggregated_probs_list = []\n    \n    for task_i in range(num_cls_task):\n        logits_tta = torch.stack(all_fold_mean_logits_by_task[task_i], dim=0)\n        \n        mean_logit = logits_tta.mean(dim=0, keepdim=False)\n        \n        # Sigmoid 转换为最终概率\n        final_prob = torch.sigmoid(mean_logit)\n        \n        aggregated_probs_list.append(final_prob.to('cpu').numpy().flatten())\n\n    # 合并概率：Task2在前，Task1在后 (保持与原始代码一致)\n    task1_probs = aggregated_probs_list[0] # Task 1 (存在性)\n    task2_probs = aggregated_probs_list[1] # Task 2 (位置)\n\n    return np.concatenate([task2_probs, task1_probs], axis=0)\n\n# ==================================================\n# 2. 两阶段推理\n# ==================================================\ndef process_single_timepoint(orig_nii, time_index=None):\n    \"\"\"\n    处理单个时间点的数据\n    \"\"\"\n    # 提取指定时间点的数据\n    if time_index is not None and orig_nii.ndim == 4 and orig_nii.shape[3] > 1:\n        orig_data = orig_nii.get_fdata()[:, :, :, time_index]\n        orig_nii = nib.Nifti1Image(orig_data, orig_nii.affine, orig_nii.header)\n    \n    # 重新定向\n    img_orient = reorient_nii(orig_nii, targ_aff=\"LPS\")\n    input_img_np = img_orient.get_fdata()\n    \n    input_img_np = input_img_np.transpose(2, 1, 0)[None]\n    original_spacing = img_orient.header.get_zooms()[:3]\n\n    return predict_aneurysm(input_img_np, original_spacing, DEVICE, tta_batch_size=2)\n\n# You can return either a Pandas or Polars dataframe, though Polars is recommended.\ndef predict(series_path: str) -> pl.DataFrame:\n    \"\"\"\n    Make a prediction for a given DICOM series path.\n    This function consolidates the core prediction logic into the required format.\n    \"\"\"\n    series_id = os.path.basename(series_path)\n    \n    # 步骤 1: 快速获取文件列表\n    dicom_files = []\n    for root, _, files in os.walk(series_path):\n        for file in files:\n            # 简化文件类型判断\n            if file.endswith(('.dcm', '.DCM')) or ('.' not in file and len(file) > 1):\n                dicom_files.append(os.path.join(root, file))\n    \n    if len(dicom_files) == 0:\n        probs = np.ones(len(LABEL_COLS)) * 0.5\n\n    elif len(dicom_files) == 1:\n        \n        ds = pydicom.dcmread(dicom_files[0], force=True)\n        \n        # 获取像素数组\n        pixel_array = ds.pixel_array\n        # 像素数组形状: (150, 528, 528)\n        # 计算得到的spacing: [0.55, 0.5, 0.5]\n\n        input_img_np = pixel_array[None]\n        spacing = get_spacing_by_shape(pixel_array.shape)\n        original_spacing = spacing[::-1]\n\n        print(f\"计算得到的spacing: {spacing}\")\n\n        ''' \n        加入异常处理\n        '''\n        # 筛选厚层T2数据\n        if spacing[0] >= 3:\n            print('发现T2厚层数据，进入方位判断......')\n            \n            # ========== 从 2D numpy 数组推理 ==========\n            print(\"\\n从 2D numpy 数组推理\")     \n            img_3d = pixel_array  # 直接使用已读取的 pixel_array\n            D, _ , _ = pixel_array.shape\n            slice_idx = D // 2\n            slice_2d = img_3d[slice_idx, :, :]\n            \n            plane_label, plane_id, plane_conf, plane_probs = CLS_2D_PREDICTOR.inference_from_slice(\n                slice_2d=slice_2d)\n            \n            # 异常处理修正\n            if len(pixel_array.shape) == 3 and plane_id != 0:\n                print('进入异常处理中......')\n                pixel_array, spacing = correct_orientation(pixel_array, spacing, plane_id)\n                print(f\"修正后的像素数组形状: {pixel_array.shape}\")\n                print(f\"修正后spacing: {spacing}\")\n                input_img_np = pixel_array[None]\n                original_spacing = spacing[::-1]\n\n        probs = predict_aneurysm(input_img_np, original_spacing, DEVICE, tta_batch_size=2)\n        \n    else:\n        try: \n            orig_nii = dicom2nifti.dicom_series_to_nifti(series_path, None, reorient_nifti=False)['NII']\n        except:\n            all_series = group_dicom_files(series_path)\n    \n            # 找到层数最多的序列并排序\n            largest_series_files = get_largest_series_files(all_series)\n    \n            # 现在 largest_series_files 就是您需要的、已按 InstanceNumber 排序的 DICOM 文件路径列表\n            if largest_series_files:\n                print(f\"\\n最终目标序列文件总数: {len(largest_series_files)}\")\n                # 您现在可以使用这个列表进行后续的图像加载和处理\n    \n            largest_series_path = \"/kaggle/largest_series_tmp_path\"\n    \n            if os.path.exists(largest_series_path):\n                shutil.rmtree(largest_series_path)\n            os.makedirs(largest_series_path)\n    \n            # 4. 复制文件\n            for file_path in largest_series_files:\n                filename = os.path.basename(file_path)\n                dest_path = os.path.join(largest_series_path, filename)\n                shutil.copy2(file_path, dest_path)\n    \n            orig_nii = dicom2nifti.dicom_series_to_nifti(largest_series_path, None, reorient_nifti=False)['NII']\n            \n        # 处理多时间点数据\n        if orig_nii.ndim == 4 and orig_nii.shape[3] > 1:\n            # 获取时间点数量\n            t = orig_nii.shape[3]\n            \n            # 为每个时间点存储预测概率\n            all_timepoint_probs = []\n            \n            # 对每个时间点进行推理\n            for t_i in range(t):\n                print(f\"处理时间点 {t_i + 1}/{t}\")\n                \n                # 使用复用的处理函数，传入时间索引\n                timepoint_probs = process_single_timepoint(\n                    orig_nii,\n                    time_index=t_i\n                )\n                all_timepoint_probs.append(timepoint_probs)\n            \n            # 将所有时间点的概率合并，取最大值\n            all_timepoint_probs = np.array(all_timepoint_probs)  # shape: (T, prob_length)\n            probs = np.max(all_timepoint_probs, axis=0)\n            \n        else:\n            # 单时间点处理\n            probs = process_single_timepoint(\n                orig_nii\n            )\n\n    pred_df = pl.DataFrame(\n        data=[probs.tolist()],\n        schema=LABEL_COLS,\n        orient='row'\n    )\n        \n    # Perform memory cleanup\n    if torch.cuda.is_available():\n        torch.cuda.empty_cache()\n    gc.collect()\n\n    # ----------------------------- IMPORTANT ------------------------------\n    # You MUST have the following code in your `predict` function\n    # to prevent \"out of disk space\" errors. This is a temporary workaround\n    # as we implement improvements to our evaluation system.\n    shutil.rmtree('/kaggle/shared', ignore_errors=True)\n    # ----------------------------------------------------------------------\n    \n    return pred_df","metadata":{"execution":{"iopub.status.busy":"2025-10-14T08:12:25.605437Z","iopub.execute_input":"2025-10-14T08:12:25.605761Z","iopub.status.idle":"2025-10-14T08:13:34.779487Z","shell.execute_reply.started":"2025-10-14T08:12:25.605744Z","shell.execute_reply":"2025-10-14T08:13:34.7787Z"},"papermill":{"duration":56.958374,"end_time":"2025-10-12T16:03:53.388436","exception":false,"start_time":"2025-10-12T16:02:56.430062","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# from time import time\n# case_paths = [\n#     '/kaggle/input/rsna-intracranial-aneurysm-detection/series/1.2.826.0.1.3680043.8.498.10004044428023505108375152878107656647',\n#     '/kaggle/input/rsna-intracranial-aneurysm-detection/series/1.2.826.0.1.3680043.8.498.10004684224894397679901841656954650085',\n#     '/kaggle/input/rsna-intracranial-aneurysm-detection/series/1.2.826.0.1.3680043.8.498.10005158603912009425635473100344077317',\n#     '/kaggle/input/rsna-intracranial-aneurysm-detection/series/1.2.826.0.1.3680043.8.498.10009383108068795488741533244914370182',\n#     '/kaggle/input/rsna-intracranial-aneurysm-detection/series/1.2.826.0.1.3680043.8.498.10012790035410518400400834395242853657',\n#     '/kaggle/input/rsna-intracranial-aneurysm-detection/series/1.2.826.0.1.3680043.8.498.10014757658335054766479957992112625961',\n#     '/kaggle/input/rsna-intracranial-aneurysm-detection/series/1.2.826.0.1.3680043.8.498.10021411248005513321236647460239137906',\n#     '/kaggle/input/rsna-intracranial-aneurysm-detection/series/1.2.826.0.1.3680043.8.498.10022688097731894079510930966432818105',\n#     '/kaggle/input/rsna-intracranial-aneurysm-detection/series/1.2.826.0.1.3680043.8.498.10022796280698534221758473208024838831',\n#     '/kaggle/input/rsna-intracranial-aneurysm-detection/series/1.2.826.0.1.3680043.8.498.10023411164590664678534044036963716636',\n#     '/kaggle/input/rsna-intracranial-aneurysm-detection/series/1.2.826.0.1.3680043.8.498.10030095840917973694487307992374923817',\n#     '/kaggle/input/rsna-intracranial-aneurysm-detection/series/1.2.826.0.1.3680043.8.498.10030804647049037739144303822498146901',\n#     '/kaggle/input/rsna-intracranial-aneurysm-detection/series/1.2.826.0.1.3680043.8.498.10034081836061566510187499603024895557',\n#     '/kaggle/input/rsna-intracranial-aneurysm-detection/series/1.2.826.0.1.3680043.8.498.10035643165968342618460849823699311381',\n#     '/kaggle/input/rsna-intracranial-aneurysm-detection/series/1.2.826.0.1.3680043.8.498.10035782880104673269567641444954004745'\n# ]\n# st = time()\n# for p in case_paths:\n#     this_st = time()\n#     predict(p)\n#     print(f\"******** One case done, {time() - this_st:.2f}s ********\")\n# total_time = time() - st\n# avg_time = total_time / len(case_paths)\n# print(f\"All Done, {total_time:.2f}s\")\n# print(f\"Avg time: {avg_time:.2f}s\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-14T08:13:34.78032Z","iopub.execute_input":"2025-10-14T08:13:34.780539Z","iopub.status.idle":"2025-10-14T08:13:34.785548Z","shell.execute_reply.started":"2025-10-14T08:13:34.780522Z","shell.execute_reply":"2025-10-14T08:13:34.7848Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"inference_server = kaggle_evaluation.rsna_inference_server.RSNAInferenceServer(predict)\n\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    inference_server.serve()\nelse:\n    inference_server.run_local_gateway()\n    display(pl.read_parquet('/kaggle/working/submission.parquet'))","metadata":{"execution":{"iopub.status.busy":"2025-10-14T08:13:34.786299Z","iopub.execute_input":"2025-10-14T08:13:34.786503Z","iopub.status.idle":"2025-10-14T08:14:22.531908Z","shell.execute_reply.started":"2025-10-14T08:13:34.786477Z","shell.execute_reply":"2025-10-14T08:14:22.531203Z"},"papermill":{"duration":58.367491,"end_time":"2025-10-12T16:04:51.758702","exception":false,"start_time":"2025-10-12T16:03:53.391211","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null}]}