{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":99552,"databundleVersionId":13851420,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":594631,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":445082,"modelId":461556}],"dockerImageVersionId":31089,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"\"\"\"# submission_kaggle_safe.py\nimport os\nimport shutil\nimport gc\nfrom pathlib import Path\n\nimport pydicom\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport torch\nimport kaggle_evaluation.rsna_inference_server\n\n# ===================== CONFIG =====================\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\nLOCATION_MAP = {\n    1: \"Other Posterior Circulation\",\n    2: \"Basilar Tip\",\n    3: \"Right Posterior Communicating Artery\",\n    4: \"Left Posterior Communicating Artery\",\n    5: \"Right Infraclinoid Internal Carotid Artery\",\n    6: \"Left Infraclinoid Internal Carotid Artery\",\n    7: \"Right Supraclinoid Internal Carotid Artery\",\n    8: \"Left Supraclinoid Internal Carotid Artery\",\n    9: \"Right Middle Cerebral Artery\",\n    10: \"Left Middle Cerebral Artery\",\n    11: \"Right Anterior Cerebral Artery\",\n    12: \"Left Anterior Cerebral Artery\",\n    13: \"Anterior Communicating Artery\",\n}\nID_TO_LABEL = {i - 1: v for i, v in LOCATION_MAP.items()}\n\n# ===================== HELPERS =====================\ndef adaptive_windowing(image):\n    img_flat = image.flatten()\n    img_flat = img_flat[img_flat > 0]\n    if len(img_flat) == 0:\n        return np.zeros_like(image, dtype=np.uint8)\n    low_val, high_val = np.percentile(img_flat, [5, 95])\n    img_windowed = np.clip(image, low_val, high_val)\n    img_norm = ((img_windowed - low_val) / (high_val - low_val + 1e-8) * 255).astype(np.uint8)\n    return img_norm\n\ndef process_dicom_series(series_path, image_size=512, num_slices=16):\n    #Load DICOM series safely and reduce slices for memory efficiency.\n    dcm_files = sorted([os.path.join(root, f)\n                        for root, _, files in os.walk(series_path)\n                        for f in files if f.endswith(\".dcm\")])\n    if len(dcm_files) == 0:\n        return np.zeros((num_slices, image_size, image_size), dtype=np.uint8)\n\n    dicom_data = []\n    for f in dcm_files:\n        try:\n            ds = pydicom.dcmread(f, force=True)\n            img = ds.pixel_array.astype(np.float32)\n            if img.ndim == 3 and img.shape[-1] == 3:\n                img = cv2.cvtColor(img.astype(np.uint8), cv2.COLOR_BGR2GRAY).astype(np.float32)\n            img_resized = cv2.resize(img, (image_size, image_size))\n            dicom_data.append(img_resized)\n        except Exception as e:\n            print(f\"Skipping file {f} due to error: {e}\")\n\n    if len(dicom_data) == 0:\n        return np.zeros((num_slices, image_size, image_size), dtype=np.uint8)\n\n    volume = np.stack(dicom_data, axis=0)\n    volume = adaptive_windowing(volume)\n\n    if volume.shape[0] > num_slices:\n        idx = np.linspace(0, volume.shape[0] - 1, num_slices).astype(int)\n        volume = volume[idx]\n    elif volume.shape[0] < num_slices:\n        pad = num_slices - volume.shape[0]\n        volume = np.pad(volume, ((0, pad), (0, 0), (0, 0)), mode=\"edge\")\n\n    return volume\n\ndef create_multichannel_img(volume):\n    depth, h, w = volume.shape\n    start, end = int(depth * 0.15), int(depth * 0.85)\n    mip = np.max(volume[start:end], axis=0)\n\n    slice_means = np.mean(volume, axis=(1, 2))\n    top_percentile = np.percentile(slice_means, 75)\n    high_intensity = slice_means >= top_percentile\n    weighted_avg = np.mean(volume[high_intensity], axis=0) if np.any(high_intensity) else np.mean(volume, axis=0)\n\n    std_proj = np.zeros_like(volume[0])\n    for i in range(depth - 4):\n        window_std = np.std(volume[i:i+5], axis=0)\n        std_proj = np.maximum(std_proj, window_std)\n\n    chans = []\n    for ch in [mip, weighted_avg, std_proj]:\n        # Safe normalization to prevent NaNs\n        if ch.max() == ch.min():\n            ch_norm = np.zeros_like(ch, dtype=np.uint8)\n        else:\n            ch_norm = ((ch - ch.min()) / (ch.max() - ch.min()) * 255).astype(np.uint8)\n        chans.append(ch_norm)\n    return np.stack(chans, axis=-1)\n\ndef preprocess_image(img, target_size=640):\n    img_resized = cv2.resize(img, (target_size, target_size))\n    img_rgb = cv2.cvtColor(img_resized, cv2.COLOR_BGR2RGB)\n    tensor = np.transpose(img_rgb, (2, 0, 1)).astype(np.float32) / 255.0\n    return torch.from_numpy(tensor).unsqueeze(0)\n\ndef sigmoid(x):\n    return 1 / (1 + np.exp(-x))\n\ndef tta_transforms(image):\n    transforms = [image]\n    transforms.append(cv2.flip(image, 1))\n    transforms.append(cv2.flip(image, 0))\n    transforms.append(cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE))\n    return transforms\n\n# ===================== MODEL =====================\nmodel_path = \"/kaggle/input/yolo8_multimodal_modal/pytorch/default/1/best.torchscript\"\nassert os.path.exists(model_path), f\"Model not found at {model_path}\"\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nmodel = torch.jit.load(model_path, map_location=device)\nmodel.eval()\n\n# ===================== PREDICTION =====================\ndef predict(series_path: str) -> pd.DataFrame:\n    try:\n        series_id = os.path.basename(series_path)\n\n        volume = process_dicom_series(series_path)\n        base_image = create_multichannel_img(volume)\n\n        scores_list = []\n        for aug_img in tta_transforms(base_image):\n            try:\n                img_tensor = preprocess_image(aug_img).to(device)\n                with torch.no_grad():\n                    outputs = model(img_tensor)\n                    if outputs is None or len(outputs) == 0:\n                        continue\n                    # Handle different output types safely\n                    if isinstance(outputs, torch.Tensor):\n                        outputs = outputs.cpu().numpy()\n                    elif isinstance(outputs, (list, tuple)) and len(outputs) > 0:\n                        outputs = [o.cpu().numpy() if isinstance(o, torch.Tensor) else o for o in outputs]\n                    else:\n                        continue\n            except Exception as e:\n                print(f\"Skipping TTA image due to error: {e}\")\n                continue\n\n            scores = {label: 0.0 for label in LABEL_COLS}\n            aneurysm_present = 0.0\n\n            for det in outputs[0]:\n                try:\n                    conf_prob = sigmoid(float(det[4]))\n                    cls_id = int(det[5])\n                    label = ID_TO_LABEL.get(cls_id, None)\n                    if label is not None:\n                        scores[label] = max(scores[label], conf_prob)\n                        aneurysm_present = max(aneurysm_present, conf_prob)\n                except Exception:\n                    continue\n\n            scores[\"Aneurysm Present\"] = max(aneurysm_present, max(scores.values()))\n            scores_list.append(scores)\n\n        if len(scores_list) == 0:\n            final_scores = {label: 0.0 for label in LABEL_COLS}\n        else:\n            final_scores = {label: np.mean([s[label] for s in scores_list]) for label in LABEL_COLS}\n\n        # Clean up memory\n        del volume, base_image, scores_list, img_tensor\n        gc.collect()\n\n        row = [series_id] + [float(final_scores[c]) for c in LABEL_COLS]\n        df = pd.DataFrame([row], columns=[ID_COL, *LABEL_COLS])\n        return df\n\n    except Exception as e:\n        print(f\"Failed series {series_path} with error: {e}\")\n        # Return zero predictions for failed series\n        row = [os.path.basename(series_path)] + [0.0]*len(LABEL_COLS)\n        return pd.DataFrame([row], columns=[ID_COL, *LABEL_COLS])\n\n# ===================== SERVER / LOCAL =====================\nshared_dir = \"/kaggle/shared\"\nshutil.rmtree(shared_dir, ignore_errors=True)\nos.makedirs(shared_dir, exist_ok=True)\n\ninference_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    # Generate submission CSV\n    submission_path = \"/kaggle/working/submission.parquet\"\n    if os.path.exists(submission_path):\n        df = pd.read_parquet(submission_path)\n        df.to_csv(\"/kaggle/working/submission.csv\", index=False)\n        display(df)\"\"\"","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-10-01T06:52:33.272265Z","iopub.execute_input":"2025-10-01T06:52:33.2726Z","iopub.status.idle":"2025-10-01T06:52:33.288091Z","shell.execute_reply.started":"2025-10-01T06:52:33.272576Z","shell.execute_reply":"2025-10-01T06:52:33.287283Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\"\"\"# submission.py\nimport os\nimport shutil\nimport pydicom\nimport cv2\nimport numpy as np\nimport torch\nimport pandas as pd\nimport kaggle_evaluation.rsna_inference_server\n\n# ===================== CONFIG =====================\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\nLOCATION_MAP = {\n    1: \"Other Posterior Circulation\",\n    2: \"Basilar Tip\",\n    3: \"Right Posterior Communicating Artery\",\n    4: \"Left Posterior Communicating Artery\",\n    5: \"Right Infraclinoid Internal Carotid Artery\",\n    6: \"Left Infraclinoid Internal Carotid Artery\",\n    7: \"Right Supraclinoid Internal Carotid Artery\",\n    8: \"Left Supraclinoid Internal Carotid Artery\",\n    9: \"Right Middle Cerebral Artery\",\n    10: \"Left Middle Cerebral Artery\",\n    11: \"Right Anterior Cerebral Artery\",\n    12: \"Left Anterior Cerebral Artery\",\n    13: \"Anterior Communicating Artery\",\n}\nID_TO_LABEL = {i - 1: v for i, v in LOCATION_MAP.items()}\n\n# ===================== HELPERS =====================\ndef normalize_dicom(img):\n    img = img.astype(np.float32)\n    img = (img - img.min()) / (img.max() - img.min() + 1e-8)\n    img = (img * 255).astype(np.uint8)\n    return img\n\ndef preprocess_image(img, target_size=640):\n    img_resized = cv2.resize(img, (target_size, target_size))\n    img_rgb = cv2.cvtColor(img_resized, cv2.COLOR_GRAY2RGB)\n    img_tensor = np.transpose(img_rgb, (2, 0, 1)).astype(np.float32) / 255.0\n    img_tensor = torch.from_numpy(img_tensor).unsqueeze(0)\n    return img_tensor\n\ndef sigmoid(x):\n    return 1 / (1 + np.exp(-x))\n\n# ===================== LOAD MODEL =====================\nmodel_path = \"/kaggle/input/yolo8_multimodal_modal/pytorch/default/1/best.torchscript\"\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nmodel = torch.jit.load(model_path, map_location=device)\nmodel.eval()\n\n# ===================== PREDICT =====================\ndef predict(series_path: str) -> pd.DataFrame:\n    series_id = os.path.basename(series_path)\n\n    # Gather DICOM files\n    dcm_files = [\n        os.path.join(root, file)\n        for root, _, files in os.walk(series_path)\n        for file in files if file.endswith(\".dcm\")\n    ]\n    dcm_files.sort()\n\n    # Initialize scores\n    scores = {label: 0.0 for label in LABEL_COLS}\n    aneurysm_present = 0.0\n\n    for filepath in dcm_files:\n        ds = pydicom.dcmread(filepath, force=True)\n        img = normalize_dicom(ds.pixel_array)\n        img_tensor = preprocess_image(img).to(device)\n\n        with torch.no_grad():\n            outputs = model(img_tensor)\n\n        if outputs is not None:\n            outputs = outputs.cpu().numpy()\n            for det in outputs[0]:\n                conf_prob = sigmoid(float(det[4]))\n                cls_id = int(det[5])\n                label = ID_TO_LABEL.get(cls_id, None)\n                if label is not None:\n                    scores[label] = max(scores[label], conf_prob)\n                    aneurysm_present = max(aneurysm_present, conf_prob)\n\n    # Consistency: aneurysm present = max of all detections\n    scores['Aneurysm Present'] = max(aneurysm_present, max(scores.values()))\n\n    # Return pandas DataFrame\n    row = [series_id] + [float(scores[c]) for c in LABEL_COLS]\n    df = pd.DataFrame([row], columns=[ID_COL, *LABEL_COLS])\n    return df\n\n# ===================== SERVER =====================\nshared_dir = \"/kaggle/shared\"\nif os.path.exists(shared_dir):\n    shutil.rmtree(shared_dir)\nos.makedirs(shared_dir, exist_ok=True)\n\ninference_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    df = pd.read_parquet('/kaggle/working/submission.parquet')\n    df.to_csv('/kaggle/working/submission.csv', index=False)\n    display(df)\"\"\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-30T23:07:06.072505Z","iopub.status.idle":"2025-09-30T23:07:06.07283Z","shell.execute_reply.started":"2025-09-30T23:07:06.072675Z","shell.execute_reply":"2025-09-30T23:07:06.07269Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# submission_crashproof_tta_fast.py\nimport os\nimport shutil\nimport gc\nimport pydicom\nimport cv2\nimport numpy as np\nimport torch\nimport pandas as pd\nfrom tqdm import tqdm\nimport kaggle_evaluation.rsna_inference_server\n\n# ===================== CONFIG =====================\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\nLOCATION_MAP = {\n    1: \"Other Posterior Circulation\",\n    2: \"Basilar Tip\",\n    3: \"Right Posterior Communicating Artery\",\n    4: \"Left Posterior Communicating Artery\",\n    5: \"Right Infraclinoid Internal Carotid Artery\",\n    6: \"Left Infraclinoid Internal Carotid Artery\",\n    7: \"Right Supraclinoid Internal Carotid Artery\",\n    8: \"Left Supraclinoid Internal Carotid Artery\",\n    9: \"Right Middle Cerebral Artery\",\n    10: \"Left Middle Cerebral Artery\",\n    11: \"Right Anterior Cerebral Artery\",\n    12: \"Left Anterior Cerebral Artery\",\n    13: \"Anterior Communicating Artery\",\n}\nID_TO_LABEL = {i - 1: v for i, v in LOCATION_MAP.items()}\n\n# Performance / timeout-oriented params\nMAX_SLICES = 32            # sample at most this many slices per series (evenly spaced)\nBATCH_SIZE = 16            # batch size for model inference\nTARGET_SIZE = 640          # model input size\nTTA_MODES = ['none', 'hflip']  # keep TTA small; remove 'hflip' if you want faster\nTHREADS = 1                # reduce CPU thread contention on Kaggle runner\n\n# ===================== HELPERS =====================\ndef normalize_dicom(img):\n    img = img.astype(np.float32)\n    if img.max() == img.min():\n        return np.zeros_like(img, dtype=np.uint8)\n    img = (img - img.min()) / (img.max() - img.min()) * 255.0\n    return img.astype(np.uint8)\n\ndef preprocess_image_np(img, target_size=TARGET_SIZE):\n    # expects grayscale numpy image\n    img_resized = cv2.resize(img, (target_size, target_size), interpolation=cv2.INTER_LINEAR)\n    if img_resized.ndim == 2:\n        img_rgb = cv2.cvtColor(img_resized, cv2.COLOR_GRAY2RGB)\n    else:\n        img_rgb = img_resized\n    tensor = np.transpose(img_rgb, (2,0,1)).astype(np.float32) / 255.0\n    return tensor  # CHW np float32\n\ndef sigmoid(x): \n    return 1.0/(1.0+np.exp(-x))\n\ndef sample_filepaths_evenly(filepaths, max_samples=MAX_SLICES):\n    if len(filepaths) <= max_samples:\n        return filepaths\n    idxs = np.linspace(0, len(filepaths)-1, max_samples, dtype=int)\n    return [filepaths[i] for i in idxs]\n\n# ===================== MODEL =====================\n# Update model_path to the correct path in your input dataset if needed\nmodel_path = \"/kaggle/input/yolo8_multimodal_modal/pytorch/default/1/best.torchscript\"\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# Limit CPU threads to avoid overhead on Kaggle runners\ntorch.set_num_threads(THREADS)\ntorch.set_num_interop_threads(THREADS)\n\ntry:\n    model = torch.jit.load(model_path, map_location=device)\n    model.to(device)\n    model.eval()\nexcept Exception as e:\n    # If model fails to load, we still want to register server (predict will return zeros)\n    model = None\n    print(f\"[ERROR] Could not load model: {e}\")\n\n# ===================== PREDICTION =====================\ndef run_model_on_batch(batch_tensor):\n    \"\"\"\n    batch_tensor: torch.Tensor shape (N,3,H,W)\n    returns: list of outputs per image (each output may be numpy array or list)\n    \"\"\"\n    with torch.no_grad():\n        preds = model(batch_tensor)  # may be list or tensor depending on script\n    # Normalize to list of per-image outputs (np)\n    outputs_list = []\n    if preds is None:\n        return [None] * batch_tensor.shape[0]\n    # Common ultralytics torchscript: returns list-like where preds[i] contains detections\n    try:\n        # If preds is iterable per image\n        for p in preds:\n            if isinstance(p, torch.Tensor):\n                outputs_list.append(p.cpu().numpy())\n            else:\n                # try convert to numpy if possible\n                try:\n                    outputs_list.append(np.array(p))\n                except:\n                    outputs_list.append(p)\n    except TypeError:\n        # preds is a single tensor of shape (N, ...). Split by first dimension.\n        if isinstance(preds, torch.Tensor):\n            for i in range(preds.shape[0]):\n                outputs_list.append(preds[i].unsqueeze(0).cpu().numpy())\n        else:\n            # unknown format - wrap and return\n            outputs_list = [preds] * batch_tensor.shape[0]\n    return outputs_list\n\ndef parse_detections_to_scores(detections, label_scores):\n    \"\"\"\n    detections: numpy array-like for one image: rows of [x1,y1,x2,y2,conf,cls] or similar.\n    Updates label_scores dict in place with max confidences.\n    \"\"\"\n    if detections is None:\n        return\n    try:\n        arr = np.array(detections)\n        if arr.size == 0:\n            return\n        # Support shape (...,6) or list-of-lists\n        if arr.ndim == 3 and arr.shape[0] == 1:\n            arr = arr[0]\n        if arr.ndim == 1:\n            # single detection\n            arr = arr.reshape(1, -1)\n        for det in arr:\n            if det.size < 6:\n                continue\n            conf_prob = float(sigmoid(float(det[4])) ) if (det[4] < -20 or det[4] > 20) else float(det[4])\n            try:\n                cls_id = int(det[5])\n            except:\n                continue\n            label = ID_TO_LABEL.get(cls_id)\n            if label:\n                label_scores[label] = max(label_scores.get(label, 0.0), conf_prob)\n    except Exception:\n        # best-effort: ignore parse errors\n        return\n\ndef predict(series_path: str) -> pd.DataFrame:\n    \"\"\"\n    Must be fast. Returns one-row DataFrame with INDEX ID_COL and LABEL_COLS floats.\n    \"\"\"\n    series_id = os.path.basename(series_path.rstrip(\"/\"))\n    final_scores = {label:0.0 for label in LABEL_COLS}\n\n    # Early return zeros if model not loaded\n    if model is None:\n        row = [series_id] + [0.0 for _ in LABEL_COLS]\n        return pd.DataFrame([row], columns=[ID_COL, *LABEL_COLS])\n\n    try:\n        # Gather DICOM file paths\n        dcm_files = sorted([os.path.join(root, f)\n                            for root, _, files in os.walk(series_path)\n                            for f in files if f.lower().endswith(\".dcm\")])\n        if len(dcm_files) == 0:\n            # no dicoms -> zeros\n            row = [series_id] + [0.0 for _ in LABEL_COLS]\n            return pd.DataFrame([row], columns=[ID_COL, *LABEL_COLS])\n\n        # sample evenly to limit runtime\n        sampled_files = sample_filepaths_evenly(dcm_files, MAX_SLICES)\n\n        # prepare batches for model\n        tensors_for_infer = []\n        # For mapping back: store indices mapping each prepared tensor to original augment type\n        augment_map = []  # list of (file_idx, tta_mode)\n        for fp in sampled_files:\n            try:\n                ds = pydicom.dcmread(fp, force=True)\n                img = normalize_dicom(ds.pixel_array)\n            except Exception:\n                continue\n            base_np = preprocess_image_np(img, TARGET_SIZE)\n            for tta in TTA_MODES:\n                if tta == 'none':\n                    tensors_for_infer.append(base_np)\n                    augment_map.append((fp, 'none'))\n                elif tta == 'hflip':\n                    flipped = np.flip(base_np, axis=2).copy()  # flip horizontally in W axis since CHW\n                    tensors_for_infer.append(flipped)\n                    augment_map.append((fp, 'hflip'))\n                # keep TTA small to save time\n\n        if len(tensors_for_infer) == 0:\n            row = [series_id] + [0.0 for _ in LABEL_COLS]\n            return pd.DataFrame([row], columns=[ID_COL, *LABEL_COLS])\n\n        # Batch inference\n        all_scores_per_prep = []  # list of dicts per prepared image\n        idx = 0\n        N = len(tensors_for_infer)\n        while idx < N:\n            batch_np = tensors_for_infer[idx: idx + BATCH_SIZE]\n            batch_tensor = torch.from_numpy(np.stack(batch_np, axis=0)).to(device)  # (B,3,H,W)\n            # Run model and parse outputs\n            try:\n                outputs_list = run_model_on_batch(batch_tensor)\n            except Exception as e:\n                # if batch inference fails, try per-image fallback to avoid total crash\n                outputs_list = []\n                for single in batch_np:\n                    try:\n                        st = torch.from_numpy(single[None]).to(device)\n                        olist = run_model_on_batch(st)\n                        outputs_list.extend(olist)\n                    except Exception:\n                        outputs_list.append(None)\n\n            # for each output, update slice-level scores\n            for out in outputs_list:\n                slice_scores = {label:0.0 for label in LABEL_COLS}\n                parse_detections_to_scores(out, slice_scores)\n                all_scores_per_prep.append(slice_scores)\n\n            # cleanup\n            del batch_tensor\n            gc.collect()\n            idx += BATCH_SIZE\n\n        # Reduce across TTA and slices: take max across all scores\n        for s in all_scores_per_prep:\n            for k, v in s.items():\n                final_scores[k] = max(final_scores.get(k, 0.0), float(v))\n\n        # Aneurysm Present is the max across location labels (exclude last label if already included)\n        location_only_labels = [l for l in LABEL_COLS if l != 'Aneurysm Present']\n        final_scores['Aneurysm Present'] = max([final_scores.get(l, 0.0) for l in location_only_labels] + [final_scores.get('Aneurysm Present', 0.0)])\n\n    except Exception as e:\n        # On any unexpected error, log and return zeros (fast)\n        print(f\"[ERROR] Series {series_id} failed during predict: {e}\")\n        final_scores = {label:0.0 for label in LABEL_COLS}\n\n    row = [series_id] + [float(final_scores.get(c, 0.0)) for c in LABEL_COLS]\n    return pd.DataFrame([row], columns=[ID_COL, *LABEL_COLS])\n\n# ===================== SERVER =====================\nshared_dir = \"/kaggle/shared\"\nshutil.rmtree(shared_dir, ignore_errors=True)\nos.makedirs(shared_dir, exist_ok=True)\n\nserver = kaggle_evaluation.rsna_inference_server.RSNAInferenceServer(predict)\n\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    # In competition environment the runner will call endpoints\n    server.serve()\nelse:\n    # For local testing in notebook\n    server.run_local_gateway()\n    submission_path = '/kaggle/working/submission.parquet'\n    if os.path.exists(submission_path):\n        df = pd.read_parquet(submission_path)\n        df.to_csv('/kaggle/working/submission.csv', index=False)\n        display(df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T17:06:52.234531Z","iopub.execute_input":"2025-10-01T17:06:52.235287Z","iopub.status.idle":"2025-10-01T17:10:00.301658Z","shell.execute_reply.started":"2025-10-01T17:06:52.235251Z","shell.execute_reply":"2025-10-01T17:10:00.300539Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}