{"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":"gpu","dataSources":[{"sourceType":"competition","sourceId":99552,"databundleVersionId":13851420},{"sourceType":"datasetVersion","sourceId":13279616,"datasetId":8214516,"databundleVersionId":13979625},{"sourceType":"datasetVersion","sourceId":13248734,"datasetId":7994218,"databundleVersionId":13945599},{"sourceType":"datasetVersion","sourceId":13262094,"datasetId":8285566,"databundleVersionId":13960420},{"sourceType":"datasetVersion","sourceId":13130191,"datasetId":8009945,"databundleVersionId":13816297},{"sourceType":"datasetVersion","sourceId":13269865,"datasetId":8325535,"databundleVersionId":13968941},{"sourceType":"datasetVersion","sourceId":12780021,"datasetId":8079690,"databundleVersionId":13404554},{"sourceType":"datasetVersion","sourceId":13269754,"datasetId":8299879,"databundleVersionId":13968819},{"sourceType":"modelInstanceVersion","sourceId":584390,"databundleVersionId":13787942,"modelInstanceId":436530},{"sourceType":"modelInstanceVersion","sourceId":585148,"databundleVersionId":13797020,"modelInstanceId":436530},{"sourceType":"modelInstanceVersion","sourceId":598193,"databundleVersionId":13963229,"modelInstanceId":447628},{"sourceType":"kernelVersion","sourceId":255206151},{"sourceType":"kernelVersion","sourceId":265692636}],"dockerImageVersionId":31090,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install /kaggle/input/pip-install-pyg-v2/torch_spline_conv-1.2.2+pt26cu124-cp311-cp311-linux_x86_64.whl\n!pip install /kaggle/input/pip-install-pyg-v2/torch_sparse-0.6.18+pt26cu124-cp311-cp311-linux_x86_64.whl\n!pip install /kaggle/input/pip-install-pyg-v2/pyg_lib-0.4.0+pt26cu124-cp311-cp311-linux_x86_64.whl\n!pip install /kaggle/input/pip-install-pyg-v2/torch_cluster-1.6.3+pt26cu124-cp311-cp311-linux_x86_64.whl\n!pip install /kaggle/input/pip-install-pyg-v2/torch_geometric-2.6.1-py3-none-any.whl","metadata":{"trusted":true,"_kg_hide-input":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2025-10-06T12:53:47.301457Z","iopub.execute_input":"2025-10-06T12:53:47.302049Z","iopub.status.idle":"2025-10-06T12:54:05.404456Z","shell.execute_reply.started":"2025-10-06T12:53:47.302015Z","shell.execute_reply":"2025-10-06T12:54:05.403687Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport sys\nimport gc\nimport json\nimport time\nimport shutil\nimport threading\nimport queue\nimport warnings\nfrom pathlib import Path\nfrom typing import List, Tuple, Dict, Optional, Any\nfrom collections import OrderedDict\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\n\nimport numpy as np\nimport cv2\nimport pydicom\nfrom scipy import ndimage\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\n\n# Transformations\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\n# Competition API\nimport kaggle_evaluation.rsna_inference_server\n\n# GPU array processing\nimport cupy as cp\nfrom cupyx.scipy.ndimage import zoom\n\n# Gradient boosting\nimport lightgbm as lgb\nimport xgboost as xgb\n\n# Custom path addition\nsys.path.insert(0, \"/kaggle/input/ultralytcs-timm-rsna/ultralytics-timm\")\n\n# YOLO\nfrom ultralytics import YOLO\n\nwarnings.filterwarnings('ignore')\n\n# Default workers and device setup\nDEFAULT_DICOM_WORKERS = os.cpu_count() or 4\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-06T12:54:05.406097Z","iopub.execute_input":"2025-10-06T12:54:05.4063Z","iopub.status.idle":"2025-10-06T12:54:57.716524Z","shell.execute_reply.started":"2025-10-06T12:54:05.406279Z","shell.execute_reply":"2025-10-06T12:54:57.715913Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Setting","metadata":{}},{"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\n\nEFF_LABELS_TO_IDX = {\n    'Aneurysm Present': 0,\n    'Anterior Communicating Artery': 1,\n    'Basilar Tip': 2,\n    'Left Anterior Cerebral Artery': 3,\n    'Left Infraclinoid Internal Carotid Artery': 4,\n    'Left Middle Cerebral Artery': 5,\n    'Left Posterior Communicating Artery': 6,\n    'Left Supraclinoid Internal Carotid Artery': 7,\n    'Other Posterior Circulation': 8,\n    'Right Anterior Cerebral Artery': 9,\n    'Right Infraclinoid Internal Carotid Artery': 10,\n    'Right Middle Cerebral Artery': 11,\n    'Right Posterior Communicating Artery': 12,\n    'Right Supraclinoid Internal Carotid Artery': 13\n}\n\nEFF_LABELS = sorted(list(EFF_LABELS_TO_IDX.keys()))\nVESSEL_LABELS = LABEL_COLS[:-1]\nPRESENCE_LABEL = LABEL_COLS[-1]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-06T12:54:57.717445Z","iopub.execute_input":"2025-10-06T12:54:57.718218Z","iopub.status.idle":"2025-10-06T12:54:57.724946Z","shell.execute_reply.started":"2025-10-06T12:54:57.718188Z","shell.execute_reply":"2025-10-06T12:54:57.72425Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Meta GNN","metadata":{}},{"cell_type":"code","source":"from torch_geometric.nn.models import GraphSAGE, GAT\nimport torch.nn as nn\nimport torch\nfrom torch_geometric.nn import LayerNorm, global_max_pool, global_mean_pool\nimport torch.nn.functional as F\nimport torchvision.transforms as transforms\nfrom torch_geometric.nn.conv import TransformerConv\nfrom torch_geometric.data import Data\nfrom torch_cluster import knn_graph\nfrom torch_geometric.nn import radius_graph\nfrom torch_geometric.transforms import AddRandomWalkPE\nfrom scipy.spatial import Delaunay\nimport random","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-06T12:54:57.72586Z","iopub.execute_input":"2025-10-06T12:54:57.726153Z","iopub.status.idle":"2025-10-06T12:55:00.899203Z","shell.execute_reply.started":"2025-10-06T12:54:57.726127Z","shell.execute_reply":"2025-10-06T12:55:00.898399Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class GNN_CONIFG:\n    \n    #data\n    k_neibs = 15\n    radius = 30\n    num_samples = 10\n    walk_length = 8\n    \n    #model\n    hidden_channels= 128\n    num_layers=4\n    jk= \"lstm\"\n    walk_length=4\n    use_pe = False\n    dropout = 0.3\n    pooling = \"mean\"\n    tau= 0.1\n    ckpt_paths = [\n        #'/kaggle/input/yolo-gnn-rsna-iad/pytorch/knn_graph_4l_128_lstm_mean/2/knn_graph_15_False_4l_128_lstm_mean_fold0-epoch12-val_loss0.5609-val_cls_auroc0.7690.ckpt',\n        '/kaggle/input/yolo-gnn-rsna-iad/pytorch/knn_graph_4l_128_lstm_mean/2/knn_graph_15_False_4l_128_lstm_mean_fold1-epoch07-val_loss0.5637-val_cls_auroc0.8019.ckpt',\n        #'/kaggle/input/yolo-gnn-rsna-iad/pytorch/knn_graph_4l_128_lstm_mean/2/knn_graph_15_False_4l_128_lstm_mean_fold2-epoch09-val_loss0.5485-val_cls_auroc0.8016.ckpt',\n        '/kaggle/input/yolo-gnn-rsna-iad/pytorch/knn_graph_4l_128_lstm_mean/2/knn_graph_15_False_4l_128_lstm_mean_fold3-epoch12-val_loss0.5642-val_cls_auroc0.7926.ckpt',\n        '/kaggle/input/yolo-gnn-rsna-iad/pytorch/knn_graph_4l_128_lstm_mean/2/knn_graph_15_False_4l_128_lstm_mean_fold4-epoch14-val_loss0.5162-val_cls_auroc0.8312.ckpt'\n    ]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-06T12:55:00.901083Z","iopub.execute_input":"2025-10-06T12:55:00.901607Z","iopub.status.idle":"2025-10-06T12:55:00.906332Z","shell.execute_reply.started":"2025-10-06T12:55:00.901587Z","shell.execute_reply":"2025-10-06T12:55:00.905547Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class GraphModel(nn.Module):\n    def __init__(self, hidden_channels=256, num_layers=8, jk='lstm',\n                 walk_length=8, use_pe=True, dropout=0.3, pooling=\"noisy_or\", tau=0.1):\n        super().__init__()\n        in_dim = 256 + walk_length if use_pe else 256\n        self.gnn = GraphSAGE(\n            in_channels=in_dim,\n            hidden_channels=hidden_channels,\n            num_layers=num_layers,\n            out_channels=hidden_channels,\n            jk=jk,\n            dropout=dropout,\n            norm=LayerNorm(in_channels=hidden_channels)\n        )\n\n        self.meta_map = nn.Sequential(\n            nn.Linear(13, 64),\n            nn.ReLU(),\n            nn.Linear(64, 64)\n        )\n\n        self.to_logits = nn.Linear(hidden_channels + 64, 1)\n\n        self.pooling = pooling\n        self.tau = tau  # temperature for logsumexp pooling\n\n    def forward(self, data):\n        x, edge_index, batch = data.x, data.edge_index, data.batch\n        xx = data.xx\n\n        edge_index = edge_index.cuda()\n        batch = batch.cuda()\n        x = x.cuda()\n        xx = xx.cuda()\n\n        if edge_index.shape[0] == 0:\n            edge_index = torch.tensor([[0, 0]]).T.cuda()\n\n        # Node logits\n        node_embs = self.gnn(x, edge_index, batch=batch)  # (N, 1)\n        meta_embs = self.meta_map(xx)\n\n        # Graph-level pooling\n        if self.pooling == \"max\":\n            graph_embs = global_max_pool(node_embs, batch)\n            graph_embs = torch.cat([graph_embs, meta_embs], dim=-1)\n\n        elif self.pooling == \"mean\":\n            graph_embs = global_mean_pool(node_embs, batch)\n            #print(graph_embs.shape)\n            graph_embs = torch.cat([graph_embs, meta_embs], dim=-1)\n        graph_logits = self.to_logits(graph_embs)\n        return graph_logits","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-06T12:55:00.907362Z","iopub.execute_input":"2025-10-06T12:55:00.907647Z","iopub.status.idle":"2025-10-06T12:55:00.927045Z","shell.execute_reply.started":"2025-10-06T12:55:00.907622Z","shell.execute_reply":"2025-10-06T12:55:00.926265Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_gnn_models(cfg):\n    gnn_models = []\n    for gnn_ckpt in cfg.ckpt_paths:\n        gnn_model = GraphModel(cfg.hidden_channels,\n                               cfg.num_layers, \n                               cfg.jk,\n                               cfg.walk_length, \n                               cfg.use_pe, \n                               cfg.dropout, \n                               cfg.pooling, \n                               cfg.tau)\n        \n        gnn_ckpt = torch.load(gnn_ckpt, weights_only=False)\n        state_dict = gnn_ckpt['state_dict']\n        new_state_dict = OrderedDict()\n        \n        for k, v in state_dict.items():\n            new_key = k.replace(\"model.\", \"\") if k.startswith(\"model.\") else k\n            new_state_dict[new_key] = v\n        \n        gnn_model.load_state_dict(new_state_dict, strict=False)\n        gnn_model.to(\"cuda:0\")\n        gnn_model.eval()\n        gnn_models.append(gnn_model)\n    return gnn_models\n\ngnn_models = load_gnn_models(GNN_CONIFG)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-06T12:55:00.928073Z","iopub.execute_input":"2025-10-06T12:55:00.928256Z","iopub.status.idle":"2025-10-06T12:55:01.809201Z","shell.execute_reply.started":"2025-10-06T12:55:00.928242Z","shell.execute_reply":"2025-10-06T12:55:01.808381Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_feature_map(model):\n    features = {}\n    def make_hook(name):\n        def hook(module, input, output):\n            features[name] = output\n        return hook\n    model.model.model[16].register_forward_hook(make_hook(\"C3K2\"))\n    return features\n\n\ndef delaunay_graph(x):\n    points = x.cpu().numpy()  # assuming x is a tensor of shape [num_points, dims]\n    tri = Delaunay(points)\n    edges = set()\n    for simplex in tri.simplices:\n        for i in range(len(simplex)):\n            for j in range(i + 1, len(simplex)):\n                edges.add(tuple(sorted((simplex[i], simplex[j]))))\n    edge_index = torch.tensor(list(edges), dtype=torch.long).t()\n    return edge_index\n\n\n# def assign_feat(x, feat, tomo_id, vol_size):\n#     d, h, w = vol_size\n#     fd, fh, fw = feat.shape[0], feat.shape[2], feat.shape[3]\n#     z = x[:, 0].astype('int32')\n#     y = ((x[:, 1]/h) * fh).astype('int32')\n#     x = ((x[:, 2]/w) * fw).astype('int32')\n#     extract_feat = feat[z, :, y, x]\n#     return extract_feat\n\n\n# def sample_uniform_3d_ball(points, vol_size, radius=30, num_samples=20):\n#     Z, Y, X = vol_size\n#     N = points.shape[0]\n#     def uniform_ball(n):\n#         vec = np.random.randn(n, 3)\n#         vec /= np.linalg.norm(vec, axis=1, keepdims=True)\n#         r = np.random.rand(n) ** (1/3)\n#         return vec * (r[:, None] * radius)\n\n#     result = []\n#     np.random.seed(42)\n#     for center in points:\n#         attempts = 0\n#         accepted = []\n\n#         # Accept valid samples until we have enough or hit retry limit\n#         while len(accepted) < num_samples and attempts < num_samples * 10:\n#             samples = uniform_ball(num_samples)\n#             candidates = samples + center  # shifted samples\n\n#             # Keep only those within volume bounds\n#             mask = (\n#                 (candidates[:, 0] >= 0) & (candidates[:, 0] < Z) &\n#                 (candidates[:, 1] >= 0) & (candidates[:, 1] < Y) &\n#                 (candidates[:, 2] >= 0) & (candidates[:, 2] < X)\n#             )\n#             accepted.extend(candidates[mask])\n#             attempts += 1\n\n#         # If not enough valid, pad with center point\n#         if len(accepted) < num_samples:\n#             accepted.extend([center] * (num_samples - len(accepted)))\n\n#         result.append(np.array(accepted[:num_samples]))\n\n#     result = np.concatenate(result, axis=0)\n#     return result\n\n\n# def assign_feat(x, feat, vol_size):\n#     d, h, w = vol_size\n#     fd, fh, fw = feat.shape[0], feat.shape[2], feat.shape[3]\n#     z = x[:, 0].astype('int32')\n#     y = ((x[:, 1]/h) * fh).astype('int32')\n#     x = ((x[:, 2]/w) * fw).astype('int32')\n#     extract_feat = feat[z, :, y, x]\n#     return extract_feat\n\ntransform_pe = AddRandomWalkPE(walk_length=GNN_CONIFG.walk_length, attr_name=None)\n\n# def extract_tomo(all_locations, all_features, yolo_loc_preds, vol_size, k_neibs = 15, radius = 30,  num_samples = 20):\n#     points = sample_uniform_3d_ball(all_locations, vol_size, radius, num_samples)\n#     extract_feat = assign_feat(points, all_features, vol_size)\n#     #convert to torch\n#     points = torch.from_numpy(points)\n#     extract_feat = torch.from_numpy(extract_feat)\n#     batch = torch.zeros(points.shape[0], dtype=torch.int64)\n\n#     #multi-graph\n#     edge_index = knn_graph(points, k=k_neibs, loop=False)\n#     #edge_index = delaunay_graph(points)\n    \n#     #loc preds\n#     yolo_loc_preds = torch.from_numpy(yolo_loc_preds)\n    \n#     data = Data(points = points, x = extract_feat, xx = yolo_loc_preds[None, ],\n#                 edge_index = edge_index, batch = batch)\n#     return data\n\ndef set_seed(seed: int = 42):\n    # Python built-in RNG\n    random.seed(seed)\n\n    # NumPy\n    np.random.seed(seed)\n\n    # PyTorch\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n\n    # Make CuDNN deterministic (slower but reproducible)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-06T12:55:01.810095Z","iopub.execute_input":"2025-10-06T12:55:01.810382Z","iopub.status.idle":"2025-10-06T12:55:01.818884Z","shell.execute_reply.started":"2025-10-06T12:55:01.810352Z","shell.execute_reply":"2025-10-06T12:55:01.818049Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def sample_uniform_3d_ball_torch(centers: torch.Tensor, radius: float, num_samples: int, vol_size):\n    \"\"\"\n    Vectorized sampling: for N centers, produce N * num_samples sample points.\n    centers: (N,3) float tensor (z,y,x)\n    returns: samples (N*num_samples, 3) float tensor\n    \"\"\"\n    N = centers.shape[0]\n    # Generate unit directions (N * num_samples, 3)\n    tot = N * num_samples\n    # Normal random vectors\n    vec = torch.randn(tot, 3, device=centers.device)\n    vec = vec / vec.norm(dim=1, keepdim=True).clamp(min=1e-8)\n    # Radius distribution ~ r^1/3 for uniform in ball: sample u in [0,1] -> r = u^(1/3)*R\n    u = torch.rand(tot, device=centers.device)\n    r = (u ** (1.0/3.0)) * radius\n    offsets = vec * r.unsqueeze(1)  # (tot,3)\n    # Repeat centers\n    centers_rep = centers.unsqueeze(1).repeat(1, num_samples, 1).view(-1, 3)\n    samples = centers_rep + offsets\n    # clamp to volume bounds\n    Z, Y, X = vol_size\n    samples[:, 0].clamp_(0, Z - 1)\n    samples[:, 1].clamp_(0, Y - 1)\n    samples[:, 2].clamp_(0, X - 1)\n    return samples  # float tensor (tot,3)\n\ndef assign_feat_torch(points: torch.Tensor, feat: torch.Tensor, vol_size):\n    \"\"\"\n    points: (M,3) float (z,y,x) in voxel coords (z,y,x)\n    feat: feature map tensor, shape assumed (D, C, H, W) OR (batch, C, H, W)\n    This implementation assumes feat is (D, C, H, W), matching original code.\n    Returns: (M, C) tensor\n    \"\"\"\n    # feat: (D, C, H, W)\n    fd, C, fh, fw = feat.shape[0], feat.shape[1], feat.shape[2], feat.shape[3]\n    Z, Y, X = vol_size\n    # map point coordinates to feature spatial coords\n    z = points[:, 0].long().clamp(0, fd - 1)  # but fd should be equal to Z or similar\n    y = ((points[:, 1] / float(Y)) * float(fh)).long().clamp(0, fh - 1)\n    x = ((points[:, 2] / float(X)) * float(fw)).long().clamp(0, fw - 1)\n    # feat indexing: feat[z, :, y, x] -> produce (M, C)\n    # using advanced indexing:\n    out = feat[z, :, y, x]  # shape (M, C)\n    # If feat indexing returns (M, C) already, ok. If not, transpose accordingly.\n    return out  # (M, C)\n\ndef extract_tomo(all_locations, all_features, yolo_loc_preds_np, vol_size,\n                      k_neibs=15, radius=30, num_samples=20):\n    # Accept numpy input for backward compatibility, convert once to torch on device\n    centers = all_locations.float().to(device)  # (N,3)\n    feat = all_features.float().to(device)     # assume (D, C, H, W)\n    yolo_loc_preds = torch.from_numpy(yolo_loc_preds_np).float().to(device)\n\n    # 1) vectorized sampling\n    samples = sample_uniform_3d_ball_torch(centers, radius, num_samples, vol_size)  # (N*num_samples, 3)\n\n    # 2) sample features (vectorized)\n    extract_feat = assign_feat_torch(samples, feat, vol_size)  # (M, C)\n\n    # 3) build edges using GPU knn\n    # If you have multiple graphs per original detection, you may want to add a `batch` vector\n    M = samples.shape[0]\n    batch = torch.zeros(M, dtype=torch.long, device=device)  # single graph\n    # knn_graph from torch_cluster expects pos (M, dim) and returns edge_index\n    edge_index = knn_graph(samples, k=k_neibs, batch=batch, loop=False)  # runs on GPU\n\n    # 4) prepare Data\n    # rename fields to match your model: x=extract_feat, points= samples, xx = yolo metadata\n    data = Data(points=samples, x=extract_feat, xx=yolo_loc_preds[None, :], edge_index=edge_index, batch=batch)\n    # Move data to device (should already be device)\n    data = data.to(device)\n    return data","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-06T12:55:01.819719Z","iopub.execute_input":"2025-10-06T12:55:01.819963Z","iopub.status.idle":"2025-10-06T12:55:01.855852Z","shell.execute_reply.started":"2025-10-06T12:55:01.81994Z","shell.execute_reply":"2025-10-06T12:55:01.854916Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Meta Classifier","metadata":{}},{"cell_type":"code","source":"meta_cls_path = '/kaggle/input/yolo-aneurysm-predictor-from-oof-tom'\nmodel_prefix=\"meta_regressor_fold\"\nn_folds = 5\n\nlgb_models = []\nxgb_models = []\nfor fold in range(n_folds):\n    model_path = f\"{meta_cls_path}/{model_prefix}_{fold}.pkl\"\n    model = joblib.load(model_path)\n    lgb_models.append(model)\n    model_path = f\"{meta_cls_path}/{model_prefix}_{fold}_xgb.pkl\"\n    model = joblib.load(model_path)\n    xgb_models.append(model)\n\n\ndef predict_prob_lgb(X, fold_id):\n    model = lgb_models[fold_id]\n    preds_fold = model.predict(X, num_iteration=model.best_iteration_)\n    return preds_fold\n\n\ndef predict_prob_xgb(X, fold_id):\n    model = xgb_models[fold]\n    preds_fold = model.predict(X, iteration_range=(0, model.best_iteration + 1))\n    return preds_fold","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-06T12:55:01.856872Z","iopub.execute_input":"2025-10-06T12:55:01.857198Z","iopub.status.idle":"2025-10-06T12:55:02.152002Z","shell.execute_reply.started":"2025-10-06T12:55:01.85717Z","shell.execute_reply":"2025-10-06T12:55:02.151369Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Flayer","metadata":{}},{"cell_type":"code","source":"def _parallel_map(items, fn, max_workers: int):\n    items = list(items)\n    if max_workers <= 1 or len(items) <= 1:\n        return [fn(item) for item in items]\n    with ThreadPoolExecutor(max_workers=max_workers) as executor:\n        return list(executor.map(fn, items))\n\nclass FlayerDICOMPreprocessor:\n    \"\"\"\n    DICOM preprocessing system for Kaggle Code Competition\n    Converts original DICOMPreprocessor logic to single series processing\n    \"\"\"\n    \n    def __init__(\n        self,\n        target_shape: Tuple[int, int, int] = (32, 384, 384),\n        max_workers: Optional[int] = None,\n    ):\n        self.target_depth, self.target_height, self.target_width = target_shape\n        self.max_workers = max_workers if max_workers is not None else DEFAULT_DICOM_WORKERS\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    # 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    #     if center is not None and width is not None and center != \"CT\": # Check for specific window values\n    #         # Windowing processing for CT/CTA\n    #         img_min = center - width / 2\n    #         img_max = center + width / 2\n            \n    #         windowed = np.clip(img, img_min, img_max)\n    #         # Normalize to 0-1 range, then scale to 0-255\n    #         windowed = (windowed - img_min) / (img_max - img_min + 1e-6)\n    #         result = (windowed * 255).astype(np.uint8)\n            \n    #         return result\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    #         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    # # Also, update get_windowing_params to return specific values for CT\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    #         # These values are chosen to enhance vessel contrast.\n    #         center, width = (100, 700) \n    #         return center, width\n            \n    #     elif modality == 'MR':\n    #         # For MR, skip windowing (statistical normalization only)\n    #         return None, None\n            \n    #     else:\n    #         # Unexpected modality, default to no windowing\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        #remove ct windows\n        # if center is not None and width is not None and center != \"CT\": # Check for specific window values\n        #     # Windowing processing for CT/CTA\n        #     img_min = center - width / 2\n        #     img_max = center + width / 2\n            \n        #     windowed = np.clip(img, img_min, img_max)\n        #     # Normalize to 0-1 range, then scale to 0-255\n        #     windowed = (windowed - img_min) / (img_max - img_min + 1e-6)\n        #     result = (windowed * 255).astype(np.uint8)\n            \n        #     return result\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    # #original\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    #     if center is not None and width is not None:\n    #         # # Windowing processing (for CT/CTA)\n    #         # img_min = center - width / 2\n    #         # img_max = center + width / 2\n            \n    #         # windowed = np.clip(img, img_min, img_max)\n    #         # windowed = (windowed - img_min) / (img_max - img_min + 1e-7)\n    #         # result = (windowed * 255).astype(np.uint8)\n            \n    #         # #print(f\"Applied windowing: [{img_min:.1f}, {img_max:.1f}] → [0, 255]\")\n    #         # return result\n            \n    #         # Statistical normalization (for CT as well)\n    #         # Normalize using 1-99 percentiles\n    #         p1, p99 = np.percentile(img, [1, 99])\n    #         #p1, p99 = 0, 500\n            \n    #         if p99 > p1:\n    #             normalized = np.clip(img, p1, p99)\n    #             normalized = (normalized - p1) / (p99 - p1)\n    #             result = (normalized * 255).astype(np.uint8)\n                \n    #             #print(f\"CT:Applied statistical normalization: [{p1:.1f}, {p99:.1f}] → [0, 255]\")\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)\n    #                 result = (normalized * 255).astype(np.uint8)\n    #                 print(f\" CT Applied min-max normalization: [{img_min:.1f}, {img_max:.1f}] → [0, 255]\")\n    #                 return result\n    #             else:\n    #                 # If image has no variation\n    #                 #print(\"Image has no variation, returning zeros\")\n    #                 return np.zeros_like(img, dtype=np.uint8)\n        \n    #     else:\n    #         # Statistical normalization (for MR)\n    #         # Normalize using 1-99 percentiles\n    #         p1, p99 = np.percentile(img, [1, 99])\n            \n    #         if p99 > p1:\n    #             normalized = np.clip(img, p1, p99)\n    #             normalized = (normalized - p1) / (p99 - p1)\n    #             result = (normalized * 255).astype(np.uint8)\n                \n    #             #print(f\"MRI Applied statistical normalization: [{p1:.1f}, {p99:.1f}] → [0, 255]\")\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)\n    #                 result = (normalized * 255).astype(np.uint8)\n    #                 #print(f\"Applied min-max normalization: [{img_min:.1f}, {img_max:.1f}] → [0, 255]\")\n    #                 return result\n    #             else:\n    #                 # If image has no variation\n    #                 #print(\"Image has no variation, returning zeros\")\n    #                 return np.zeros_like(img, dtype=np.uint8)\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        \n        # Resize with linear interpolation\n        resized_volume = ndimage.zoom(volume, zoom_factors, order=1, mode='nearest')\n        \n        # Clip to exact size just in case\n        resized_volume = resized_volume[:self.target_depth, :self.target_height, :self.target_width]\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(\n        self,\n        series_path: str,\n        datasets: Optional[List[pydicom.Dataset]] = None,\n        series_name: Optional[str] = None,\n    ) -> np.ndarray:\n        \"\"\"\n        Process DICOM series and return as NumPy array (for Kaggle: no file saving)\n        \"\"\"\n        try:\n            if datasets is None:\n                datasets, series_name = self.load_dicom_series(series_path)\n            else:\n                series_name = series_name or Path(series_path).name\n\n            if not datasets:\n                raise ValueError(f\"No DICOM datasets available for {series_path}\")\n\n            return self.process_loaded_series(datasets, series_name)\n        except Exception as e:\n            raise\n\n    def process_loaded_series(\n        self,\n        datasets: List[pydicom.Dataset],\n        series_name: str,\n    ) -> np.ndarray:\n        \"\"\"\n        Process already loaded DICOM datasets\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            return self._process_single_3d_dicom(first_ds, series_name)\n        else:\n            # Case 2: Multiple 2D DICOM files\n            return self._process_multiple_2d_dicoms(datasets, series_name)\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        indices = list(range(volume.shape[0]))\n\n        def _process_slice(idx: int) -> np.ndarray:\n            slice_img = volume[idx]\n            return self.apply_windowing_or_normalize(slice_img, window_center, window_width)\n\n        processed_slices = _parallel_map(indices, _process_slice, self.max_workers)\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        datasets_in_order = [slice_data['dataset'] for slice_data in sorted_slices]\n\n        def _process_dataset(ds: pydicom.Dataset) -> np.ndarray:\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            return resized_img\n\n        processed_slices = _parallel_map(datasets_in_order, _process_dataset, self.max_workers)\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\ndef process_dicom_series_kaggle(\n    series_path: str,\n    target_shape: Tuple[int, int, int] = (32, 384, 384),\n    preloaded: Optional[Dict[str, Any]] = None,\n) -> np.ndarray:\n    \"\"\"\n    DICOM processing function for Kaggle inference (single series)\n    \n    Args:\n        series_path: Path to DICOM series\n        target_shape: Target volume size (depth, height, width)\n        preloaded: Optional preloaded DICOM dataset bundle\n    \n    Returns:\n        np.ndarray: Processed volume\n    \"\"\"\n    preprocessor = FlayerDICOMPreprocessor(\n        target_shape=target_shape,\n        max_workers=DEFAULT_DICOM_WORKERS,\n    )\n    datasets = preloaded.get(\"datasets\") if preloaded else None\n    series_name = preloaded.get(\"series_name\") if preloaded else None\n    if datasets is not None:\n        return preprocessor.process_series(\n            series_path,\n            datasets=datasets,\n            series_name=series_name,\n        )\n    return preprocessor.process_series(series_path)\n\n# Safe processing function with memory cleanup\ndef process_dicom_series_for_flayer(\n    series_path: str,\n    target_shape: Tuple[int, int, int] = (32, 384, 384),\n    preloaded: Optional[Dict[str, Any]] = None,\n) -> 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        preloaded: Optional preloaded DICOM dataset bundle\n    \n    Returns:\n        np.ndarray: Processed volume\n    \"\"\"\n    try:\n        volume = process_dicom_series_kaggle(series_path, target_shape, preloaded=preloaded)\n        return volume\n    finally:\n        # Memory cleanup\n        gc.collect()\n\n# Test function\ndef test_single_series(series_path: str, target_shape: Tuple[int, int, int] = (32, 384, 384)):\n    \"\"\"\n    Test processing for single series\n    \"\"\"\n    try:\n        #print(f\"Testing single series: {series_path}\")\n        \n        # Execute processing\n        volume = process_dicom_series_for_flayer(series_path, target_shape)\n        \n        # Display results\n        #print(f\"✓ Successfully processed series\")\n        #print(f\"  Volume shape: {volume.shape}\")\n        #print(f\"  Volume dtype: {volume.dtype}\")\n        #print(f\"  Volume range: [{volume.min()}, {volume.max()}]\")\n        \n        return volume\n        \n    except Exception as e:\n        #print(f\"✗ Failed to process series: {e}\")\n        return None","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-06T12:55:02.153008Z","iopub.execute_input":"2025-10-06T12:55:02.153296Z","iopub.status.idle":"2025-10-06T12:55:02.185001Z","shell.execute_reply.started":"2025-10-06T12:55:02.153275Z","shell.execute_reply":"2025-10-06T12:55:02.184167Z"}},"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\"#lb75\n        #\"/kaggle/input/rsna-iad-model-atom/heatmap_z48_448_flayer2_gsig2_7349\"\n        \"/kaggle/input/iad-model/tf_efficientnetv2_s.in21k_ft_in1k_64ch\"\n    ]\n\n\n    \n    n_fold = 5#1#5\n    trn_fold = [0, 1, 2, 3, 4]\n    #trn_fold = [0]\n    \n    # Ensemble weights (equal weight for all folds)\n    #ensemble_weights = None  # None means equal weights\n    ensemble_weights = None\n\n\nFLAYER_CFG = FlayerInferenceConfig()\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\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    ])\n\n# TTA is not used due to left/right positional information\n# def get_tta_transforms():\n#     \"\"\"TTA is prohibited for brain aneurysms due to left/right positioning\"\"\"\n#     pass\n\n# ====================================================\n# Model Loading Functions\n# ====================================================\n# Global variables\nFLAYER_MODELS = {}\nFLAYER_TRANSFORM = None\nFLAYER_TTA_TRANSFORMS = None\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_flayer_model_soup(model_dir: str, folds: list[int], device='cuda') -> nn.Module:\n    \"\"\"Load all fold models from model_dir and average their weights into a single 'model soup'.\"\"\"\n    dir_label = _dir_label(model_dir)\n    model_name = _get_model_name_for_dir(dir_label)\n\n    print(f\"[{dir_label}] Creating model soup from folds: {folds}\")\n\n    # Initialize base model and average state\n    base_model = CenterNet3DInfer().to(device)\n    avg_state = {k: torch.zeros_like(v, dtype=v.dtype, device=device)\n                 for k, v in base_model.state_dict().items()}\n    n_models = 0\n\n    # Accumulate weights\n    for fold in folds:\n        model_path = Path(model_dir) / f\"{model_name}_fold{fold}_best.pth\"\n        if not model_path.exists():\n            print(f\"  ⚠️  Missing model: {model_path}\")\n            continue\n\n        checkpoint = torch.load(model_path, map_location=device)\n        state = checkpoint['model'] if isinstance(checkpoint, dict) and 'model' in checkpoint else checkpoint\n\n        for k in avg_state.keys():\n            if k in state:\n                t = state[k].to(device)\n                # Only add if tensor is float type\n                if torch.is_floating_point(t):\n                    avg_state[k] += t\n                else:\n                    # For non-float tensors (long, bool), just copy from first model\n                    if n_models == 0:\n                        avg_state[k] = t.clone()\n        n_models += 1\n\n    if n_models == 0:\n        raise FileNotFoundError(f\"No valid fold models found in {model_dir}\")\n\n    # Average only float tensors\n    for k in avg_state.keys():\n        if torch.is_floating_point(avg_state[k]):\n            avg_state[k] /= n_models\n\n    base_model.load_state_dict(avg_state, strict=False)\n    base_model.eval()\n\n    print(f\"✅ Model soup ready for {dir_label} ({n_models} folds averaged)\")\n    return base_model\n\n\ndef load_flayer_models():\n    \"\"\"Load averaged (souped) models from all configured model directories.\"\"\"\n    global FLAYER_MODELS, FLAYER_TRANSFORM, FLAYER_TTA_TRANSFORMS\n    print(\"Loading all model soups 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        model = load_flayer_model_soup(md, FLAYER_CFG.trn_fold, device)\n        FLAYER_MODELS[dir_label] = model  # one per dir_label\n        total_loaded += 1\n        # try:\n        #     model = load_flayer_model_soup(md, FLAYER_CFG.trn_fold, device)\n        #     FLAYER_MODELS[dir_label] = model  # one per dir_label\n        #     total_loaded += 1\n        # except Exception as e:\n        #     print(f\"Warning: Could not soup models for [{dir_label}]: {e}\")\n\n    if not FLAYER_MODELS:\n        raise ValueError(\"No model soups were created successfully\")\n\n    # Shared transforms\n    FLAYER_TRANSFORM = get_inference_transform()\n    FLAYER_TTA_TRANSFORMS = None  # TTA disabled\n\n    # Warm-up for inference\n    dummy_volume = torch.randn(\n        1, FLAYER_CFG.in_chans,\n        FLAYER_CFG.target_shape[0],\n        FLAYER_CFG.size,\n        FLAYER_CFG.size\n    ).to(device)\n\n    with torch.no_grad():\n        for dir_label, model in FLAYER_MODELS.items():\n            _ = model(dummy_volume)\n\n    print(f\"✅ Model soups ready! Loaded {total_loaded} models from {len(model_dirs)} dirs \"\n          f\"(folds averaged: {list(FLAYER_CFG.trn_fold)}).\")\n\n\n#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), model in FLAYER_MODELS.items():\n                w = 1\n                #print(\"check label folder\")\n                #print(dir_label)\n                #print(fold)\n                #print(w)\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# # original ok code====================================================\n# # Prediction Functions\n# # ====================================================\n# def flayer_flayer_predict_single_model(model: nn.Module, volume: np.ndarray) -> np.ndarray:\n#     \"\"\"Run inference for a single model on one volume.\"\"\"\n#     #print(\"volume\")\n#     #print(volume)\n#     # volume shape: (D, H, W)\n#     image = volume.transpose(1, 2, 0)\n#     transformed = FLAYER_TRANSFORM(image=image)\n#     tensor = transformed['image']  # (D, H, W)\n#     #ytt try\n#     #print(\"hihi\")\n#     # print(volume.shape)#(32,448,448)\n#     #tensor = torch.from_numpy(volume)\n#     #print(tensor.shape)#(32,448,448)\n#     tensor = tensor.unsqueeze(0).unsqueeze(0).to(device)  # (1, 1, D, H, W)\n#     #print(tensor)\n#     #print(tensor.shape)#(1,1,32,448,448)\n#     with torch.no_grad():\n#         with autocast(enabled=FLAYER_CFG.use_amp):\n#             outputs = model(tensor)\n#             logits = compute_class_logits_from_heatmap(outputs['heatmap'])\n#             probs = torch.sigmoid(logits).cpu().numpy().squeeze()\n#     return probs\n\n# def predict_flayer_ensemble(image: np.ndarray) -> np.ndarray:\n#     \"\"\"\n#     Make ensemble prediction across ALL loaded models.\n#     - 支援等權重或自訂加權\n#     - ensemble_weights 的 key 可用:\n#         1) tuple key: (dir_label, fold)\n#         2) 字串 key:  \"dir_label/fold\"\n#         3) 字串 key:  f\"fold{fold}\" （僅在單一目錄時方便）\n#     - 若沒對應到就用 1.0\n#     \"\"\"\n#     all_predictions = []\n#     weights = []\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#         # 支援多種 key 形式\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#     for (dir_label, fold), model in FLAYER_MODELS.items():\n#         pred = flayer_predict_single_model(model, image)\n#         all_predictions.append(pred)\n#         weights.append(_lookup_weight(dir_label, fold))\n\n#     weights = np.array(weights, dtype=np.float32)\n#     weights = weights / (weights.sum() if weights.sum() > 0 else 1.0)\n#     predictions = np.array(all_predictions)  # shape: (N_models, num_labels)\n\n#     return np.average(predictions, weights=weights, axis=0)\n\n\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    Uses FlayerDICOMPreprocessor defined in previous cell\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        volume = preprocessor.process_series(series_path)\n        return volume\n    finally:\n        # Memory cleanup\n        gc.collect()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-06T12:55:02.185915Z","iopub.execute_input":"2025-10-06T12:55:02.186184Z","iopub.status.idle":"2025-10-06T12:55:02.216564Z","shell.execute_reply.started":"2025-10-06T12:55:02.186158Z","shell.execute_reply":"2025-10-06T12:55:02.215933Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## YOLO","metadata":{}},{"cell_type":"code","source":"# ====================================================\n# DICOM Preprocessor for EfficientNet\n# ====================================================\nclass DICOMPreprocessorKaggle:\n    \"\"\"DICOM preprocessing system for EfficientNet\"\"\"\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        \"\"\"Load DICOM series\"\"\"\n        series_path = Path(series_path)\n        series_name = series_path.name\n        \n        dicom_files = []\n        for root, _, files in os.walk(series_path):\n            for file in files:\n                if file.endswith('.dcm'):\n                    dicom_files.append(os.path.join(root, file))\n        \n        if not dicom_files:\n            raise ValueError(f\"No DICOM files found in {series_path}\")\n        \n        datasets = []\n        for filepath in dicom_files:\n            try:\n                ds = pydicom.dcmread(filepath, force=True)\n                datasets.append(ds)\n            except Exception as 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        \"\"\"Extract position information for each slice\"\"\"\n        slice_info = []\n        \n        for i, ds in enumerate(datasets):\n            info = {\n                'dataset': ds,\n                'index': i,\n                'instance_number': getattr(ds, 'InstanceNumber', i),\n            }\n            \n            try:\n                position = getattr(ds, 'ImagePositionPatient', None)\n                if position is not None and len(position) >= 3:\n                    info['z_position'] = float(position[2])\n                else:\n                    info['z_position'] = float(info['instance_number'])\n            except Exception as e:\n                info['z_position'] = float(i)\n            \n            slice_info.append(info)\n        \n        return slice_info\n    \n    def sort_slices_by_position(self, slice_info: List[Dict]) -> List[Dict]:\n        \"\"\"Sort slices by z-coordinate\"\"\"\n        return sorted(slice_info, key=lambda x: x['z_position'])\n    \n    def get_windowing_params(self, ds: pydicom.Dataset, img: np.ndarray = None) -> Tuple[Optional[float], Optional[float]]:\n        \"\"\"Get windowing parameters based on modality\"\"\"\n        modality = getattr(ds, 'Modality', 'CT')\n        \n        if modality == 'CT':\n            return \"CT\", \"CT\"\n        else:\n            return None, None\n    \n    def apply_windowing_or_normalize(self, img: np.ndarray, center: Optional[float], width: Optional[float]) -> np.ndarray:\n        \"\"\"Apply windowing or statistical normalization\"\"\"\n        if center is not None and width is not None:\n            p1, p99 = 0, 500\n            \n            if p99 > p1:\n                normalized = np.clip(img, p1, p99)\n                normalized = (normalized - p1) / (p99 - p1)\n                result = (normalized * 255).astype(np.uint8)\n                return result\n            else:\n                img_min, img_max = img.min(), img.max()\n                if img_max > img_min:\n                    normalized = (img - img_min) / (img_max - img_min)\n                    result = (normalized * 255).astype(np.uint8)\n                    return result\n                else:\n                    return np.zeros_like(img, dtype=np.uint8)\n        else:\n            p1, p99 = np.percentile(img, [1, 99])\n            \n            if p99 > p1:\n                normalized = np.clip(img, p1, p99)\n                normalized = (normalized - p1) / (p99 - p1)\n                result = (normalized * 255).astype(np.uint8)\n                return result\n            else:\n                img_min, img_max = img.min(), img.max()\n                if img_max > img_min:\n                    normalized = (img - img_min) / (img_max - img_min)\n                    result = (normalized * 255).astype(np.uint8)\n                    return result\n                else:\n                    return np.zeros_like(img, dtype=np.uint8)\n    \n    def extract_pixel_array(self, ds: pydicom.Dataset) -> np.ndarray:\n        \"\"\"Extract 2D pixel array from DICOM\"\"\"\n        img = ds.pixel_array.astype(np.float32)\n        \n        if img.ndim == 3:\n            frame_idx = img.shape[0] // 2\n            img = img[frame_idx]\n        \n        if img.ndim == 3 and img.shape[-1] == 3:\n            img = cv2.cvtColor(img.astype(np.uint8), cv2.COLOR_RGB2GRAY).astype(np.float32)\n        \n        slope = getattr(ds, 'RescaleSlope', 1)\n        intercept = getattr(ds, 'RescaleIntercept', 0)\n        slope, intercept = 1, 0\n        if slope != 1 or intercept != 0:\n            img = img * float(slope) + float(intercept)\n        \n        return img\n    \n    def resize_volume_3d(self, volume: np.ndarray) -> np.ndarray:\n        \"\"\"Resize 3D volume to target size\"\"\"\n        current_shape = volume.shape\n        \n        \n        target_shape = (self.target_depth, self.target_height, self.target_width)\n        target_effnet_aux_shape = (96,384,384)\n        \n        \n        if current_shape == target_shape:\n            return volume\n        \n        zoom_factors = [\n            target_shape[i] / current_shape[i] for i in range(3)\n        ]\n\n        eff_aux_zoom_factors = [\n            target_effnet_aux_shape[i] / current_shape[i] for i in range(3)\n        ]\n\n        volume = cp.asarray(volume)\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        pad_width = [\n            (0, max(0, self.target_depth - resized_volume.shape[0])),\n            (0, max(0, self.target_height - resized_volume.shape[1])),\n            (0, max(0, self.target_width - resized_volume.shape[2]))\n        ]\n        \n        if any(pw[1] > 0 for pw in pad_width):\n            resized_volume = np.pad(resized_volume, pad_width, mode='edge')\n\n        eff_aux_resized_volume = zoom(volume, eff_aux_zoom_factors, order=1, mode='nearest')\n        eff_aux_resized_volume = eff_aux_resized_volume[:96, :384, :384]\n        eff_aux_resized_volume = cp.asnumpy(eff_aux_resized_volume)\n        \n        pad_width = [\n            (0, max(0, 96 - eff_aux_resized_volume.shape[0])),\n            (0, max(0, 384 - eff_aux_resized_volume.shape[1])),\n            (0, max(0, 384 - eff_aux_resized_volume.shape[2]))\n        ]\n        \n        if any(pw[1] > 0 for pw in pad_width):\n            eff_aux_resized_volume = np.pad(eff_aux_resized_volume, pad_width, mode='edge')\n        return resized_volume.astype(np.uint8), eff_aux_resized_volume.astype(np.uint8)\n    \n    def process_series(self, series_path: str) -> np.ndarray:\n        \"\"\"Process DICOM series and return as NumPy array\"\"\"\n        try:\n            datasets, series_name = self.load_dicom_series(series_path)\n            \n            first_ds = datasets[0]\n            first_img = first_ds.pixel_array\n            \n            if len(datasets) == 1 and first_img.ndim == 3:\n                return self._process_single_3d_dicom(first_ds, series_name)\n            else:\n                return self._process_multiple_2d_dicoms(datasets, series_name)\n            \n        except Exception as e:\n            raise\n    \n    def _process_single_3d_dicom(self, ds: pydicom.Dataset, series_name: str) -> np.ndarray:\n        \"\"\"Process single 3D DICOM file\"\"\"\n        volume = ds.pixel_array.astype(np.float32)\n        \n        slope = getattr(ds, 'RescaleSlope', 1)\n        intercept = getattr(ds, 'RescaleIntercept', 0)\n        slope, intercept = 1, 0\n        if slope != 1 or intercept != 0:\n            volume = volume * float(slope) + float(intercept)\n        \n        window_center, window_width = self.get_windowing_params(ds)\n        \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        final_volume = self.resize_volume_3d(volume)\n        \n        return final_volume\n    \n    def _process_multiple_2d_dicoms(self, datasets: List[pydicom.Dataset], series_name: str) -> np.ndarray:\n        \"\"\"Process multiple 2D DICOM files\"\"\"\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            processed_slices.append(processed_img)\n\n        volume = np.stack(processed_slices, axis=0)\n        final_volume = self.resize_volume_3d(volume)\n        \n        return final_volume","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-06T12:55:02.217355Z","iopub.execute_input":"2025-10-06T12:55:02.217611Z","iopub.status.idle":"2025-10-06T12:55:02.241277Z","shell.execute_reply.started":"2025-10-06T12:55:02.217586Z","shell.execute_reply":"2025-10-06T12:55:02.240571Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ====================================================\n# YOLO DICOM Processing\n# ====================================================\ndef read_dicom_frames_hu(path: Path) -> List[Tuple[float, np.ndarray]]:\n    \"\"\"Read DICOM file and return list of (slice_position, HU frame)\"\"\"\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\n    # Compute slice location using orientation + position\n    try:\n        orientation = np.array(ds.ImageOrientationPatient).reshape(2, 3)\n        row_cos, col_cos = orientation\n        normal = np.cross(row_cos, col_cos)  # slice normal vector\n        position = np.array(ds.ImagePositionPatient)\n        slice_loc = float(np.dot(position, normal))  # projection along normal\n    except Exception:\n        # Fallback: SliceLocation / InstanceNumber\n        slice_loc = float(getattr(ds, \"SliceLocation\", getattr(ds, \"InstanceNumber\", 0.0)))\n\n    frames: List[Tuple[float, np.ndarray]] = []\n\n    if pix.ndim == 2:\n        img = pix.astype(np.float32)\n        frames.append((slice_loc, img * slope + intercept))\n    elif pix.ndim == 3:\n        # RGB or multi-frame\n        if pix.shape[-1] == 3 and pix.shape[0] != 3:\n            try:\n                gray = cv2.cvtColor(pix.astype(np.uint8), cv2.COLOR_BGR2GRAY).astype(np.float32)\n            except Exception:\n                gray = pix[..., 0].astype(np.float32)\n            frames.append((slice_loc, gray * slope + intercept))\n        else:\n            for i in range(pix.shape[0]):\n                frm = pix[i].astype(np.float32)\n                # tiny offset ensures consistent ordering for multi-frame\n                frames.append((slice_loc + i * 1e-3, frm * slope + intercept))\n    return frames\n\n\ndef min_max_normalize(img: np.ndarray) -> np.ndarray:\n    \"\"\"Min-max normalization to 0-255 with optional flipping\"\"\"\n    mn, mx = float(img.min()), float(img.max())\n    if mx - mn < 1e-6:\n        norm = np.zeros_like(img, dtype=np.uint8)\n    else:\n        norm = (img - mn) / (mx - mn)\n        norm = (norm * 255.0).clip(0, 255).astype(np.uint8)\n    return norm\n\n\ndef process_dicom_file(dcm_path: Path) -> List[Tuple[float, np.ndarray]]:\n    \"\"\"Process single DICOM file -> list of (slice_loc, image) tuples\"\"\"\n    try:\n        frames = read_dicom_frames_hu(dcm_path)\n        processed_slices = []\n        for loc, f in frames:\n            img_u8 = min_max_normalize(f)\n            if img_u8.ndim == 2:\n                img_u8 = cv2.cvtColor(img_u8, cv2.COLOR_GRAY2BGR)\n            processed_slices.append((loc, img_u8))\n        return processed_slices\n    except Exception as e:\n        print(f\"Failed processing {dcm_path.name}: {e}\")\n        return []\n\n\ndef collect_series_slices(series_dir: Path) -> List[Path]:\n    \"\"\"Collect all DICOM files in a series directory (recursively).\"\"\"\n    dcm_paths: List[Path] = []\n    try:\n        for root, _, files in os.walk(series_dir):\n            for f in files:\n                if f.lower().endswith('.dcm'):\n                    dcm_paths.append(Path(root) / f)\n    except Exception as e:\n        print(f\"Failed to walk series dir {series_dir}: {e}\")\n    return dcm_paths\n\n\ndef slice_sort_key(path: Path) -> float:\n    \"\"\"Compute a robust slice sort key (orientation + position) for a single DICOM file\"\"\"\n    try:\n        ds = pydicom.dcmread(str(path), stop_before_pixels=True, force=True)\n        orientation = np.array(ds.ImageOrientationPatient).reshape(2, 3)\n        row_cos, col_cos = orientation\n        normal = np.cross(row_cos, col_cos)\n        position = np.array(ds.ImagePositionPatient)\n        return float(np.dot(position, normal))\n    except Exception:\n        # fallback\n        try:\n            return float(getattr(ds, \"SliceLocation\", getattr(ds, \"InstanceNumber\", 0.0)))\n        except:\n            return 0.0","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-06T12:55:02.243728Z","iopub.execute_input":"2025-10-06T12:55:02.243967Z","iopub.status.idle":"2025-10-06T12:55:02.260509Z","shell.execute_reply.started":"2025-10-06T12:55:02.24395Z","shell.execute_reply":"2025-10-06T12:55:02.259849Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# %load ../src/models/segmentation_classification.py\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport timm\n\ndef encode_for_resnet(e, x, B, depth_scaling=[2,2,2,2,1]):\n\n    def pool_in_depth(x, depth_scaling):\n        bd, c, h, w = x.shape\n        x1 = x.reshape(B, -1, c, h, w).permute(0, 2, 1, 3, 4)\n        x1 = F.avg_pool3d(x1, kernel_size=(depth_scaling, 1, 1), stride=(depth_scaling, 1, 1), padding=0)\n        x = x1.permute(0, 2, 1, 3, 4).reshape(-1, c, h, w)\n        return x, x1\n\n    encode=[]\n    x = e.conv1(x)\n    x = e.bn1(x)\n    x = e.act1(x)\n    x, x1 = pool_in_depth(x, depth_scaling[0])\n    encode.append(x1)\n    #x = e.maxpool(x)\n    x = F.avg_pool2d(x,kernel_size=2,stride=2)\n\n    x = e.layer1(x)\n    x, x1 = pool_in_depth(x, depth_scaling[1])\n    encode.append(x1)\n\n    x = e.layer2(x)\n    x, x1 = pool_in_depth(x, depth_scaling[2])\n    encode.append(x1)\n\n    x = e.layer3(x)\n    x, x1 = pool_in_depth(x, depth_scaling[3])\n    encode.append(x1)\n\n    x = e.layer4(x)\n    x, x1 = pool_in_depth(x, depth_scaling[4])\n    encode.append(x1)\n\n    return encode\n\ndef encode_for_convnext(e, x, B, depth_scaling=[2,2,2,2,1]):\n\n    def pool_in_depth(x, depth_scaling):\n        bd, c, h, w = x.shape\n        x1 = x.reshape(B, -1, c, h, w).permute(0, 2, 1, 3, 4)\n        x1 = F.avg_pool3d(x1, kernel_size=(depth_scaling, 1, 1), stride=(depth_scaling, 1, 1), padding=0)\n        x = x1.permute(0, 2, 1, 3, 4).reshape(-1, c, h, w)\n        return x, x1\n\n    encode=[]\n    x = e.stem_0(x)\n    x = e.stem_1(x)\n    \n   \n    x = e.stages_0(x)\n    x, x1 = pool_in_depth(x, depth_scaling[0])\n    encode.append(x1)\n\n    x = e.stages_1(x)\n    x, x1 = pool_in_depth(x, depth_scaling[1])\n    encode.append(x1)\n    \n    x = e.stages_2(x)\n    x, x1 = pool_in_depth(x, depth_scaling[2])\n    encode.append(x1)\n    \n    x = e.stages_3(x)\n    x, x1 = pool_in_depth(x, depth_scaling[3])\n    encode.append(x1)\n    \n\n    return encode\n\nclass DecoderBlock(nn.Module):\n    \"\"\"\n    U-Net-style decoder block with skip connections.\n    It upsamples the feature map and concatenates it with the corresponding\n    feature map from the encoder, followed by two convolutional layers.\n    \"\"\"\n    def __init__(self, in_channels, skip_channels, out_channels):\n        super().__init__()\n        # Upsampling layer followed by a convolution to adjust channels\n        self.upsample = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)\n        # We combine the upsampled channels with the skip connection channels\n        combined_in_channels = in_channels + skip_channels\n        self.conv = nn.Sequential(\n            nn.Conv2d(combined_in_channels, out_channels, kernel_size=3, padding=1, bias=False),\n            nn.BatchNorm2d(out_channels),\n            nn.ReLU(inplace=True),\n            nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1, bias=False),\n            nn.BatchNorm2d(out_channels),\n            nn.ReLU(inplace=True)\n        )\n\n    def forward(self, x, skip):\n        x = self.upsample(x)\n        # Concatenate along the channel dimension\n        if skip is not None:\n             x = torch.cat([x, skip], dim=1)\n        x = self.conv(x)\n        return x\n\nclass SegmentationClassifier(nn.Module):\n    \"\"\"\n    A multi-task model for binary classification and binary segmentation.\n    Uses a timm model as the encoder.\n    \"\"\"\n    def __init__(self, pooler, model_name, pretrained=False):\n        super().__init__()\n        \n        # 1. Encoder (from timm)\n        # We use features_only=True to get intermediate feature maps\n        self.encoder = timm.create_model(\n            model_name,\n            pretrained=pretrained,\n            features_only=True,\n            in_chans=3\n        )\n        self.pooler = pooler\n        \n        # Get the channel sizes of the feature maps from the encoder\n        encoder_channels = self.encoder.feature_info.channels()\n        # e.g., for resnet34: [64, 64, 128, 256, 512]\n        \n        # 2. Classification Head\n        self.avgpool = nn.AdaptiveAvgPool2d(1)\n        self.flatten = nn.Flatten()\n        # The input to the linear layer is the number of channels in the last feature map\n        self.classification_head = nn.Linear(encoder_channels[-1], 1)\n\n        # 3. Segmentation Head (Decoder)\n        # We work backwards from the last feature map\n        self.decoder_blocks = nn.ModuleList()\n        \n        reversed_encoder_channels = list(reversed(encoder_channels))\n        # Example for resnet34: [512, 256, 128, 64, 64]\n        \n        # The first decoder block takes the last feature map\n        in_ch = reversed_encoder_channels[0] \n        # The first decoder block does not have a skip connection from a deeper layer\n        skip_ch = 0 \n        out_ch = in_ch // 2 # Halve the channels\n        \n        self.center = nn.Identity() # Placeholder for the deepest features\n        \n        for i in range(len(reversed_encoder_channels) - 1):\n            in_ch = reversed_encoder_channels[i] if i == 0 else out_ch\n            skip_ch = reversed_encoder_channels[i+1]\n            out_ch = reversed_encoder_channels[i+1]\n            \n            self.decoder_blocks.append(DecoderBlock(in_ch, skip_ch, out_ch))\n\n        # Final 1x1 convolution to get a single-channel mask\n        # The number of input channels is the output of the last decoder block\n        last_decoder_out_channels = reversed_encoder_channels[-1]\n        self.segmentation_head = nn.Conv2d(last_decoder_out_channels, 1, kernel_size=1)\n\n    def forward(self, x):\n\n        # Bx96x384x384 -> \n        B, D, H, W = x.shape\n        x = x.reshape(B*D, 1, H, W)\n        x = x.expand(-1, 3, -1, -1)\n        features = self.pooler(self.encoder, x, B, depth_scaling=[2,2,2,2,1])\n\n        for i in range(len(features)):\n            features[i] = features[i].amax(dim=2)\n\n\n        # --- Classification Path ---\n        # Use the last and most abstract feature map for classification\n        last_feature = features[-1]\n        pooled_features = self.avgpool(last_feature)\n        flat_features = self.flatten(pooled_features)\n        classification_output = self.classification_head(flat_features)\n\n        return classification_output","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-06T12:55:02.261325Z","iopub.execute_input":"2025-10-06T12:55:02.261957Z","iopub.status.idle":"2025-10-06T12:55:02.279064Z","shell.execute_reply.started":"2025-10-06T12:55:02.261933Z","shell.execute_reply":"2025-10-06T12:55:02.278413Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n\n# ====================================================\n# EfficientNet Configuration\n# ====================================================\nclass EfficientNetConfig:\n    model_name = \"tf_efficientnetv2_s.in21k_ft_in1k\"\n    size = 384\n    target_cols = LABEL_COLS\n    num_classes = len(target_cols)\n    in_chans = 32\n    target_shape = (32, 384, 384)\n    batch_size = 1\n    use_amp = True\n    model_dir = '/kaggle/input/rsna2025-effnetv2-32ch'\n    n_fold = 5\n    trn_fold = [0, 1, 2, 3, 4]\n\nEFFNET_CFG = EfficientNetConfig()\n\n\n# ====================================================\n# EfficientNet B0 AuxLoss Configuration\n# ====================================================\nclass EfficientNetAuxLossConfig:\n    model_name = \"efficientnet_b0\"\n    size = 384\n    target_cols = LABEL_COLS\n    num_classes = len(target_cols)\n    in_chans = 96\n    target_shape = (96, 384, 384)\n    batch_size = 1\n    use_amp = True\n    model_dir = '/kaggle/input/rsna-iad-modelzoo/'\n    n_fold = 5\n    trn_fold = [0, 1, 2, 3, 4]\n\nEFFNET_AUX_LOSS_CFG = EfficientNetAuxLossConfig()\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/cv_y11m_more_negatives_fold02/weights/best.pt\",\n    #     \"fold\": \"0\",\n    #     \"weight\": 1.0,\n    #     \"name\": \"YOLOv11m_fold0\"\n    # },\n    {\n        \"path\": \"/kaggle/input/rsna-yolo-models/cv_y11m_more_negatives_fold13/weights/best.pt\",\n        \"fold\": \"1\",\n        \"weight\": 1.0,\n        \"name\": \"YOLOv11m_fold0\"\n    },\n    # {\n    #     \"path\": \"/kaggle/input/rsna-yolo-models/cv_y11m_more_negatives_fold2/weights/best.pt\",\n    #     \"fold\": \"2\",\n    #     \"weight\": 1.0,\n    #     \"name\": \"YOLOv11m_fold0\"\n    # },\n    {\n        \"path\": \"/kaggle/input/rsna-yolo-models/cv_y11m_more_negatives_fold3/weights/best.pt\",\n        \"fold\": \"3\",\n        \"weight\": 1.0,\n        \"name\": \"YOLOv11m_fold1\"\n    }, \n    {\n        \"path\": \"/kaggle/input/rsna-yolo-models/cv_y11m_more_negatives_fold4/weights/best.pt\",\n        \"fold\": \"4\",\n        \"weight\": 1.0,\n        \"name\": \"YOLOv11m_fold1\"\n    }\n]\n\n# ====================================================\n# Model Loading and Inference\n# ====================================================\n# Global variables\nEFFNET_MODELS = {}\nYOLO_MODELS = []\nEFFNET_AUX_MODELS = {}\nEFFNET_TRANSFORM = None\nEFFNET_AUX_TRANSFORM = None\n\n\nEFFNET_CKPTS = [\n    '/kaggle/input/rsna-iad-32ch-efficientnet/pytorch/tf_efficientnetv2_s.in21k_ft_in1k/2/32-ch-cnn-epoch04-kaggle_score0.6727_fold_id0.ckpt',\n    '/kaggle/input/rsna-iad-32ch-efficientnet/pytorch/tf_efficientnetv2_s.in21k_ft_in1k/2/32-ch-cnn-epoch05-kaggle_score0.6883_fold_id1.ckpt',\n    '/kaggle/input/rsna-iad-32ch-efficientnet/pytorch/tf_efficientnetv2_s.in21k_ft_in1k/2/32-ch-cnn-epoch13-kaggle_score0.6891_fold_id2.ckpt',\n    '/kaggle/input/rsna-iad-32ch-efficientnet/pytorch/tf_efficientnetv2_s.in21k_ft_in1k/1/32-ch-cnn-epoch03-kaggle_score0.7006_fold_id3.ckpt',\n    '/kaggle/input/rsna-iad-32ch-efficientnet/pytorch/tf_efficientnetv2_s.in21k_ft_in1k/2/32-ch-cnn-epoch16-kaggle_score0.6686_fold_id4.ckpt'\n]\n\nEFFNET_AUX_LOSS_CKPTS = {\n    'resnet18d': [\n        '/kaggle/input/rsna-iad-modelzoo/ch32_segcls_resnet18d_2.5D-epoch08-val_cls_auroc0.6936_fold_id4.pth',\n        '/kaggle/input/rsna-iad-modelzoo/ch32_segcls_resnet18d_2.5D-epoch08-val_cls_auroc0.7130_fold_id2.pth',\n        '/kaggle/input/rsna-iad-modelzoo/ch32_segcls_resnet18d_2.5D-epoch08-val_cls_auroc0.7121_fold_id1.pth',\n        '/kaggle/input/rsna-iad-modelzoo/ch32_segcls_resnet18d_2.5D-epoch08-val_cls_auroc0.7235_fold_id3.pth',\n        '/kaggle/input/rsna-iad-modelzoo/ch32_segcls_resnet18d_2.5D-epoch09-val_cls_auroc0.7393_fold_id0.pth'\n    ],\n    # 'convnext_tiny.fb_in22k':[\n    #     '/kaggle/input/rsna-iad-modelzoo/ch32_segcls_convnext_tiny_2.5D-epoch05-val_cls_auroc0.7067_fold_id4.pth',\n    #     '/kaggle/input/rsna-iad-modelzoo/ch32_segcls_convnext_tiny_2.5D-epoch06-val_cls_auroc0.7264_fold_id2.pth',\n    #     '/kaggle/input/rsna-iad-modelzoo/ch32_segcls_convnext_tiny_2.5D-epoch06-val_cls_auroc0.7422_fold_id0.pth',\n    #     '/kaggle/input/rsna-iad-modelzoo/ch32_segcls_convnext_tiny_2.5D-epoch06-val_cls_auroc0.7424_fold_id3.pth',\n    #     '/kaggle/input/rsna-iad-modelzoo/ch32_segcls_convnext_tiny_2.5D-epoch07-val_cls_auroc0.7210_fold_id1.pth'\n    # ]\n}\n\npoolers = {'resnet18d': encode_for_resnet, 'convnext_tiny.fb_in22k': encode_for_convnext}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-06T12:55:02.279736Z","iopub.execute_input":"2025-10-06T12:55:02.279975Z","iopub.status.idle":"2025-10-06T12:55:02.297858Z","shell.execute_reply.started":"2025-10-06T12:55:02.27995Z","shell.execute_reply":"2025-10-06T12:55:02.297191Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# !ls /kaggle/input/rsna-iad-modelzoo","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-06T12:55:02.29862Z","iopub.execute_input":"2025-10-06T12:55:02.298821Z","iopub.status.idle":"2025-10-06T12:55:02.312543Z","shell.execute_reply.started":"2025-10-06T12:55:02.298805Z","shell.execute_reply":"2025-10-06T12:55:02.311792Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# def get_inference_transform():\n#     \"\"\"Get inference transformation for EfficientNet\"\"\"\n#     return A.Compose([\n#               A.Normalize(),\n#               ToTensorV2(),\n#           ])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-06T12:55:02.313309Z","iopub.execute_input":"2025-10-06T12:55:02.313509Z","iopub.status.idle":"2025-10-06T12:55:02.325171Z","shell.execute_reply.started":"2025-10-06T12:55:02.313494Z","shell.execute_reply":"2025-10-06T12:55:02.324605Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_effnet_model_fold(fold: int) -> nn.Module:\n    \"\"\"Load a single EfficientNet fold model\"\"\"\n    #model_path = Path(EFFNET_CFG.model_dir) / f'{EFFNET_CFG.model_name}_fold{fold}_best.pth'\n    checkpoint = torch.load(EFFNET_CKPTS[fold], map_location=device, weights_only=False)\n    state_dict = checkpoint['state_dict']\n    new_state_dict = OrderedDict()\n    for k, v in state_dict.items():\n        # remove \"model.\" prefix if it exists\n        new_key = k.replace(\"model.\", \"\") if k.startswith(\"model.\") else k\n        new_state_dict[new_key] = v\n    \n    model = timm.create_model(\n        EFFNET_CFG.model_name, \n        num_classes=EFFNET_CFG.num_classes, \n        pretrained=False,\n        in_chans=EFFNET_CFG.in_chans\n    )\n    \n    model.load_state_dict(new_state_dict)\n    model = model.to(device)\n    model.eval()\n    return model\n\n# def load_effnet_aux_model_fold(fold: int) -> nn.Module:\n#     \"\"\"Load a single EfficientNet fold model\"\"\"\n#     #model_path = Path(EFFNET_CFG.model_dir) / f'{EFFNET_CFG.model_name}_fold{fold}_best.pth'\n#     checkpoint = torch.load(EFFNET_AUX_LOSS_CKPTS[fold], map_location=device, weights_only=False)\n#     model = SegmentationClassifier()\n    \n#     model.load_state_dict(checkpoint)\n#     model = model.to(device)\n#     model.eval()\n#     return model\n\n\ndef load_effnet_aux_model_soup():\n    for i, (model_name, checkpoints) in enumerate(EFFNET_AUX_LOSS_CKPTS.items()):\n        model = SegmentationClassifier(poolers[model_name], model_name)\n        state_dicts = []\n        for checkpoint in checkpoints:\n            state_dict = torch.load(checkpoint, map_location=device, weights_only=False)\n            state_dicts.append(state_dict)\n        soup_state_dict = OrderedDict()\n        for key in state_dicts[0].keys():\n            tensors = [sd[key].float() for sd in state_dicts]\n            soup_state_dict[key] = torch.mean(torch.stack(tensors, dim=0), dim=0)\n        model.load_state_dict(soup_state_dict)\n        model = model.to(device)\n        model.eval()\n        EFFNET_AUX_MODELS[model_name] = model\n\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 EFFNET_MODELS, YOLO_MODELS, EFFNET_TRANSFORM, EFFNET_AUX_MODELS, EFFNET_AUX_TRANSFORM, FLAYER_MODELS\n    \n    # # Load EfficientNet models\n    # for fold in EFFNET_CFG.trn_fold:\n    #     try:\n    #         EFFNET_MODELS[fold] = load_effnet_model_fold(fold)\n    #     except Exception as e:\n    #         print(f\"Warning: Could not load EfficientNet fold {fold}: {e}\")\n\n    # #aux models\n    # load_effnet_aux_model_soup()\n    \n    # if not EFFNET_MODELS:\n    #     raise ValueError(\"No EfficientNet models were loaded successfully\")\n    \n    # Load YOLO models\n    YOLO_MODELS = load_yolo_models()\n    \n    if not FLAYER_MODELS:\n        load_flayer_models()\n\n    # Initialize transforms\n    # EFFNET_TRANSFORM = get_inference_transform()\n    # EFFNET_AUX_TRANSFORM = A.Compose([ A.Normalize(mean=(0.5,) * 96, std=(0.5,) * 96), ToTensorV2() ])\n    \n    # # Warm up models\n    # dummy_effnet_image = torch.randn(1, EFFNET_CFG.in_chans, EFFNET_CFG.size, EFFNET_CFG.size).to(device)\n    dummy_yolo_image = np.random.randint(0, 255, (512, 512, 3), dtype=np.uint8)\n    # dummy_effnet_aux_image = torch.randn(1, EFFNET_AUX_LOSS_CFG.in_chans, EFFNET_AUX_LOSS_CFG.size, EFFNET_AUX_LOSS_CFG.size).to(device)\n\n    \n    with torch.no_grad():\n        # for fold, model in EFFNET_MODELS.items():\n        #     _ = model(dummy_effnet_image)\n\n        # for fold, model in EFFNET_AUX_MODELS.items():\n        #     _ = model(dummy_effnet_aux_image)\n        \n        for model_dict in YOLO_MODELS:\n            model = model_dict[\"model\"]\n            _ = model.predict([dummy_yolo_image], verbose=False, device=device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-06T12:55:02.326037Z","iopub.execute_input":"2025-10-06T12:55:02.326463Z","iopub.status.idle":"2025-10-06T12:55:02.341Z","shell.execute_reply.started":"2025-10-06T12:55:02.326438Z","shell.execute_reply":"2025-10-06T12:55:02.340406Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict_effnet_single_model(model: nn.Module, image_tensor) -> np.ndarray:\n    \"\"\"Make prediction with a single EfficientNet model\"\"\"\n    \n    with torch.no_grad():\n        with autocast(enabled=EFFNET_CFG.use_amp):\n            output = model(image_tensor)\n            return torch.sigmoid(output).cpu().numpy().squeeze()\n\ndef predict_effnet_ensemble(image: np.ndarray) -> np.ndarray:\n    \"\"\"Make EfficientNet ensemble prediction across all folds\"\"\"\n    all_predictions = []\n    weights = []\n\n    image = image.transpose(1, 2, 0)  # (D,H,W) -> (H,W,D)\n    transformed = EFFNET_TRANSFORM(image=image)\n    image_tensor = transformed['image']\n    image_tensor = image_tensor.unsqueeze(0).to(device)\n    \n    for fold, model in EFFNET_MODELS.items():\n        pred = predict_effnet_single_model(model, image_tensor)\n        all_predictions.append(pred)\n        weights.append(1.0)\n    \n    weights = np.array(weights) / np.sum(weights)\n    predictions = np.array(all_predictions)\n    return np.average(predictions, weights=weights, axis=0)\n\n\n### EFFICIENTNET_AUX\ndef predict_effnet_aux_single_model(model: nn.Module, image_tensor) -> np.ndarray:\n    \"\"\"Make prediction with a single EfficientNet model\"\"\"\n    \n    with torch.no_grad():\n        with autocast(enabled=EFFNET_AUX_LOSS_CFG.use_amp):\n            output = model(image_tensor)\n            return torch.sigmoid(output).cpu().item()\n\ndef predict_effnet_aux_ensemble(image: np.ndarray) -> np.ndarray:\n    \"\"\"Make EfficientNet ensemble prediction across all folds\"\"\"\n    all_predictions = []\n    weights = []\n\n    image = image.transpose(1, 2, 0)  # (D,H,W) -> (H,W,D)\n    transformed = EFFNET_AUX_TRANSFORM(image=image)\n    image_tensor = transformed['image']\n    image_tensor = image_tensor.unsqueeze(0).to(device)\n    \n    for fold, model in EFFNET_AUX_MODELS.items():\n        pred = predict_effnet_aux_single_model(model, image_tensor)\n        all_predictions.append(pred)\n        weights.append(1.0)\n    \n    weights = np.array(weights) / np.sum(weights)\n    predictions = np.array(all_predictions)\n    return np.average(predictions, weights=weights, axis=0)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-06T12:55:02.341689Z","iopub.execute_input":"2025-10-06T12:55:02.341946Z","iopub.status.idle":"2025-10-06T12:55:02.35733Z","shell.execute_reply.started":"2025-10-06T12:55:02.341919Z","shell.execute_reply":"2025-10-06T12:55:02.356675Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"@torch.inference_mode()\ndef predict_yolo_ensemble(slices: List[np.ndarray]):\n    set_seed()\n    \"\"\"Run YOLO inference using all models\"\"\"\n    if not slices:\n        return 0.1, np.ones(len(YOLO_LABELS)) * 0.1\n\n    meta_lgb_preds = []\n    meta_xgb_preds = []\n    ensemble_cls_preds = []\n    ensemble_loc_preds = []\n    gnn_cls_preds = []\n    total_weight = 0.0\n    vol_size = (len(slices), slices[0].shape[0], slices[0].shape[1])\n    \n    for fold_id, model_dict in enumerate(YOLO_MODELS):\n        model = model_dict[\"model\"]\n        weight = model_dict[\"weight\"]\n        all_detections, all_feat_maps = [], []\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                z_idxes = [i + batch_idx  for batch_idx in range(len(batch_slices))]\n                features = {}\n                handle = model.model.model[16].register_forward_hook(lambda m,i,o: features.setdefault(\"C3K2\", o))\n                results = model.predict(\n                    batch_slices, \n                    verbose=False, \n                    batch=len(batch_slices), \n                    device=device, \n                    conf=0.01\n                )\n                handle.remove()\n                c3k2_feat = features.pop(\"C3K2\")\n                all_feat_maps.append(c3k2_feat.cpu())\n                del features\n                \n                for z_idx, r in enumerate(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                            x1, y1, x2, y2 = r.boxes.xyxy[j].cpu().numpy()\n                            x_center = (x1 + x2) / 2\n                            y_center = (y1 + y2) / 2\n                            \n                            point = np.array([round(z_idxes[z_idx]), round(y_center), round(x_center)])\n                            all_detections.append([*point, float(c), k, fold_id])\n                    except Exception as e:\n                        print(e)\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            # lgb_pred_fold = predict_prob_lgb(per_class_max[None, :], fold_id)\n            # xgb_pred_fold = predict_prob_xgb(per_class_max[None, :], fold_id)\n            # meta_lgb_preds.append(lgb_pred_fold)\n            # meta_xgb_preds.append(xgb_pred_fold)\n            \n            ensemble_cls_preds.append(max_conf_all * weight)\n            ensemble_loc_preds.append(per_class_max * weight)\n            total_weight += weight\n            if len(all_detections) != 0:\n                all_detections = torch.tensor(all_detections)\n                all_locations = all_detections[:, :3]\n                all_feat_maps = torch.cat(all_feat_maps, dim=0)\n                pyg_data = extract_tomo(all_locations,\n                                        all_feat_maps,\n                                        per_class_max,\n                                        vol_size,\n                                        k_neibs = GNN_CONIFG.k_neibs,\n                                        radius = GNN_CONIFG.radius,\n                                        num_samples = GNN_CONIFG.num_samples\n                                       )\n                gnn_cls_pred = gnn_models[fold_id](pyg_data)\n                # print(gnn_cls_pred)\n                # print(gnn_cls_pred.sigmoid())\n                gnn_cls_pred = gnn_cls_pred.sigmoid()[0, 0].cpu().item()\n                gnn_cls_preds.append(gnn_cls_pred)\n                \n                del c3k2_feat\n                del all_feat_maps, all_locations\n            else:\n                gnn_cls_preds.append(0.01 * weight)\n            \n        except Exception as e:\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    torch.cuda.empty_cache()\n    gc.collect()\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 = np.mean(meta_lgb_preds)\n    # meta_xgb_preds = np.mean(meta_xgb_preds)\n    gnn_cls_preds = np.mean(gnn_cls_preds)\n    \n    return final_cls_pred, final_loc_preds, gnn_cls_preds #meta_lgb_preds, meta_xgb_preds","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-06T12:55:02.358195Z","iopub.execute_input":"2025-10-06T12:55:02.35865Z","iopub.status.idle":"2025-10-06T12:55:02.377244Z","shell.execute_reply.started":"2025-10-06T12:55:02.358632Z","shell.execute_reply":"2025-10-06T12:55:02.376603Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def process_dicom_for_effnet(series_path: str) -> np.ndarray:\n    \"\"\"Process DICOM for EfficientNet with memory cleanup\"\"\"\n    try:\n        preprocessor = DICOMPreprocessorKaggle(target_shape=EFFNET_CFG.target_shape)\n        volume = preprocessor.process_series(series_path)\n        return volume\n    finally:\n        gc.collect()\n\ndef process_dicom_for_yolo(series_path):\n    series_path = Path(series_path)\n    dicom_files = collect_series_slices(series_path)\n    \n    # Sort DICOM files by orientation+position before processing\n    dicom_files.sort(key=slice_sort_key)\n    \n    with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:\n        results = list(executor.map(process_dicom_file, dicom_files))\n    \n    # Flatten into (loc, img)\n    all_slices_with_loc = [item for sublist in results for item in sublist]\n    \n    # Already sorted by dicom_files order, but double-check (safe)\n    all_slices_with_loc.sort(key=lambda x: x[0])\n    \n    # Extract just the images\n    all_slices = [img for _, img in all_slices_with_loc]\n    \n    # Now dicom_files matches the sorted slices\n    dcm_list = [f.stem for f in dicom_files]\n    return all_slices","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-06T12:55:02.377992Z","iopub.execute_input":"2025-10-06T12:55:02.378224Z","iopub.status.idle":"2025-10-06T12:55:02.394624Z","shell.execute_reply.started":"2025-10-06T12:55:02.378204Z","shell.execute_reply":"2025-10-06T12:55:02.393876Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def _predict_inner(series_path: str) -> pl.DataFrame:\n    \"\"\"Main ensemble prediction logic\"\"\"\n    global EFFNET_MODELS, YOLO_MODELS, EFFNET_AUX_MODELS, FLAYER_MODELS\n    \n    # Load models if not already loaded\n    if not EFFNET_MODELS or not YOLO_MODELS or not EFFNET_AUX_MODELS or not FLAYER_MODELS:\n        load_all_models()\n    \n    try:\n        # Process DICOM for both models\n        # effnet_volume, effnet_aux_volume = process_dicom_for_effnet(series_path)\n        # print(f\"{effnet_volume.shape=}, {effnet_aux_volume.shape=}\")\n        yolo_slices = process_dicom_for_yolo(series_path)\n        flayer_volume = process_dicom_series_for_flayer(series_path, FLAYER_CFG.target_shape)\n\n        # effnet_aux_cls_pred = predict_effnet_aux_ensemble(effnet_aux_volume)\n        \n        # Get YOLO predictions\n        yolo_cls_pred, yolo_loc_preds, gnn_cls_preds = predict_yolo_ensemble(yolo_slices)\n\n        flayer_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\n        #generate yolo preds match the label cols\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        aneurysm_idx = LABEL_COLS.index('Aneurysm Present')\n        ensemble_preds = yolo_full_preds\n        ensemble_preds[:aneurysm_idx] = 0.5 * (ensemble_preds[:aneurysm_idx] + flayer_preds[:aneurysm_idx])\n        ensemble_preds[aneurysm_idx] = (yolo_cls_pred + gnn_cls_preds + flayer_preds[aneurysm_idx])/3\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":{"_uuid":"e29b6075-398a-46c5-98e5-9108702439f4","_cell_guid":"71b8dead-f6d2-4299-9e44-c5c6e5492ea5","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-10-06T12:58:19.801144Z","iopub.execute_input":"2025-10-06T12:58:19.801446Z","iopub.status.idle":"2025-10-06T12:58:19.811251Z","shell.execute_reply.started":"2025-10-06T12:58:19.801425Z","shell.execute_reply":"2025-10-06T12:58:19.810454Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# _predict_inner(\"/kaggle/input/rsna-intracranial-aneurysm-detection/series/1.2.826.0.1.3680043.8.498.10023411164590664678534044036963716636\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-06T12:58:20.225867Z","iopub.execute_input":"2025-10-06T12:58:20.226615Z","iopub.status.idle":"2025-10-06T12:58:20.229624Z","shell.execute_reply.started":"2025-10-06T12:58:20.226591Z","shell.execute_reply":"2025-10-06T12:58:20.229047Z"}},"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-06T12:58:20.870073Z","iopub.execute_input":"2025-10-06T12:58:20.870855Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}