{"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":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":99552,"databundleVersionId":13851420},{"sourceType":"datasetVersion","sourceId":13112655,"datasetId":8306382,"databundleVersionId":13796849},{"sourceType":"datasetVersion","sourceId":12934565,"datasetId":8184998,"databundleVersionId":13589772},{"sourceType":"datasetVersion","sourceId":13223677,"datasetId":8381869,"databundleVersionId":13918444},{"sourceType":"datasetVersion","sourceId":12780021,"datasetId":8079690,"databundleVersionId":13404554},{"sourceType":"datasetVersion","sourceId":13468646,"datasetId":7979194,"databundleVersionId":14187025},{"sourceType":"datasetVersion","sourceId":13341115,"datasetId":8292605,"databundleVersionId":14046913},{"sourceType":"datasetVersion","sourceId":13345177,"datasetId":8459137,"databundleVersionId":14051369},{"sourceType":"modelInstanceVersion","sourceId":114536,"databundleVersionId":9611970,"modelInstanceId":64905,"modelId":89293},{"sourceType":"kernelVersion","sourceId":211097053},{"sourceType":"kernelVersion","sourceId":267386462}],"dockerImageVersionId":31090,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!tar xfvz /kaggle/input/ultralytics-for-offline-install/archive.tar.gz\n!pip install --no-index --find-links=./packages ultralytics\n!rm -rf ./packages","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-19T11:09:16.850627Z","iopub.execute_input":"2026-02-19T11:09:16.8509Z","iopub.status.idle":"2026-02-19T11:11:19.608129Z","shell.execute_reply.started":"2026-02-19T11:09:16.850871Z","shell.execute_reply":"2026-02-19T11:11:19.607023Z"}},"outputs":[{"name":"stdout","text":"./packages/\n./packages/pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\n./packages/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl\n./packages/numpy-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\n./packages/psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl\n./packages/nvidia_nvtx_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl\n./packages/nvidia_cuda_nvrtc_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl\n./packages/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\n./packages/nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_x86_64.whl\n./packages/torch-2.5.1-cp310-cp310-manylinux1_x86_64.whl\n./packages/matplotlib-3.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\n./packages/tqdm-4.67.1-py3-none-any.whl\n./packages/networkx-3.4.2-py3-none-any.whl\n./packages/typing_extensions-4.12.2-py3-none-any.whl\n./packages/fonttools-4.55.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\n./packages/packaging-24.2-py3-none-any.whl\n./packages/scipy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\n./packages/pyparsing-3.2.0-py3-none-any.whl\n./packages/nvidia_cuda_runtime_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl\n./packages/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\n./packages/ultralytics_thop-2.0.12-py3-none-any.whl\n./packages/certifi-2024.8.30-py3-none-any.whl\n./packages/pillow-11.0.0-cp310-cp310-manylinux_2_28_x86_64.whl\n./packages/nvidia_nccl_cu12-2.21.5-py3-none-manylinux2014_x86_64.whl\n./packages/py_cpuinfo-9.0.0-py3-none-any.whl\n./packages/ultralytics-8.3.40-py3-none-any.whl\n./packages/torchvision-0.20.1-cp310-cp310-manylinux1_x86_64.whl\n./packages/requests-2.32.3-py3-none-any.whl\n./packages/nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_x86_64.whl\n./packages/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\n./packages/nvidia_cuda_cupti_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl\n./packages/contourpy-1.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\n./packages/sympy-1.13.1-py3-none-any.whl\n./packages/seaborn-0.13.2-py3-none-any.whl\n./packages/filelock-3.16.1-py3-none-any.whl\n./packages/six-1.16.0-py2.py3-none-any.whl\n./packages/opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\n./packages/urllib3-2.2.3-py3-none-any.whl\n./packages/cycler-0.12.1-py3-none-any.whl\n./packages/tzdata-2024.2-py2.py3-none-any.whl\n./packages/nvidia_curand_cu12-10.3.5.147-py3-none-manylinux2014_x86_64.whl\n./packages/nvidia_cublas_cu12-12.4.5.8-py3-none-manylinux2014_x86_64.whl\n./packages/fsspec-2024.10.0-py3-none-any.whl\n./packages/pytz-2024.2-py2.py3-none-any.whl\n./packages/jinja2-3.1.4-py3-none-any.whl\n./packages/triton-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\n./packages/mpmath-1.3.0-py3-none-any.whl\n./packages/nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl\n./packages/python_dateutil-2.9.0.post0-py2.py3-none-any.whl\n./packages/nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_x86_64.whl\n./packages/idna-3.10-py3-none-any.whl\n./packages/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl\nLooking in links: ./packages\nProcessing ./packages/ultralytics-8.3.40-py3-none-any.whl\nRequirement already satisfied: numpy>=1.23.0 in /usr/local/lib/python3.11/dist-packages (from ultralytics) (1.26.4)\nRequirement already satisfied: matplotlib>=3.3.0 in /usr/local/lib/python3.11/dist-packages (from ultralytics) (3.7.2)\nRequirement already satisfied: opencv-python>=4.6.0 in /usr/local/lib/python3.11/dist-packages (from ultralytics) (4.11.0.86)\nRequirement already satisfied: pillow>=7.1.2 in /usr/local/lib/python3.11/dist-packages (from ultralytics) (11.2.1)\nRequirement already satisfied: pyyaml>=5.3.1 in /usr/local/lib/python3.11/dist-packages (from ultralytics) (6.0.2)\nRequirement already satisfied: requests>=2.23.0 in /usr/local/lib/python3.11/dist-packages (from ultralytics) (2.32.4)\nRequirement already satisfied: scipy>=1.4.1 in /usr/local/lib/python3.11/dist-packages (from ultralytics) (1.15.3)\nRequirement already satisfied: torch>=1.8.0 in /usr/local/lib/python3.11/dist-packages (from ultralytics) (2.6.0+cu124)\nRequirement already satisfied: torchvision>=0.9.0 in /usr/local/lib/python3.11/dist-packages (from ultralytics) (0.21.0+cu124)\nRequirement already satisfied: tqdm>=4.64.0 in /usr/local/lib/python3.11/dist-packages (from ultralytics) (4.67.1)\nRequirement already satisfied: psutil in /usr/local/lib/python3.11/dist-packages (from ultralytics) (7.0.0)\nRequirement already satisfied: py-cpuinfo in /usr/local/lib/python3.11/dist-packages (from ultralytics) (9.0.0)\nRequirement already satisfied: pandas>=1.1.4 in /usr/local/lib/python3.11/dist-packages (from ultralytics) (2.2.3)\nRequirement already satisfied: seaborn>=0.11.0 in /usr/local/lib/python3.11/dist-packages (from ultralytics) (0.12.2)\nProcessing ./packages/ultralytics_thop-2.0.12-py3-none-any.whl (from ultralytics)\nRequirement already satisfied: contourpy>=1.0.1 in /usr/local/lib/python3.11/dist-packages (from matplotlib>=3.3.0->ultralytics) (1.3.2)\nRequirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.11/dist-packages (from matplotlib>=3.3.0->ultralytics) (0.12.1)\nRequirement already satisfied: fonttools>=4.22.0 in /usr/local/lib/python3.11/dist-packages (from matplotlib>=3.3.0->ultralytics) (4.58.4)\nRequirement already satisfied: kiwisolver>=1.0.1 in /usr/local/lib/python3.11/dist-packages (from matplotlib>=3.3.0->ultralytics) (1.4.8)\nRequirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.11/dist-packages (from matplotlib>=3.3.0->ultralytics) (25.0)\nRequirement already satisfied: pyparsing<3.1,>=2.3.1 in /usr/local/lib/python3.11/dist-packages (from matplotlib>=3.3.0->ultralytics) (3.0.9)\nRequirement already satisfied: python-dateutil>=2.7 in /usr/local/lib/python3.11/dist-packages (from matplotlib>=3.3.0->ultralytics) (2.9.0.post0)\nRequirement already satisfied: mkl_fft in /usr/local/lib/python3.11/dist-packages (from numpy>=1.23.0->ultralytics) (1.3.8)\nRequirement already satisfied: mkl_random in /usr/local/lib/python3.11/dist-packages (from numpy>=1.23.0->ultralytics) (1.2.4)\nRequirement already satisfied: mkl_umath in /usr/local/lib/python3.11/dist-packages (from numpy>=1.23.0->ultralytics) (0.1.1)\nRequirement already satisfied: mkl in /usr/local/lib/python3.11/dist-packages (from numpy>=1.23.0->ultralytics) (2025.2.0)\nRequirement already satisfied: tbb4py in /usr/local/lib/python3.11/dist-packages (from numpy>=1.23.0->ultralytics) (2022.2.0)\nRequirement already satisfied: mkl-service in /usr/local/lib/python3.11/dist-packages (from numpy>=1.23.0->ultralytics) (2.4.1)\nRequirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.11/dist-packages (from pandas>=1.1.4->ultralytics) (2025.2)\nRequirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.11/dist-packages (from pandas>=1.1.4->ultralytics) (2025.2)\nRequirement already satisfied: charset_normalizer<4,>=2 in /usr/local/lib/python3.11/dist-packages (from requests>=2.23.0->ultralytics) (3.4.2)\nRequirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.11/dist-packages (from requests>=2.23.0->ultralytics) (3.10)\nRequirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.11/dist-packages (from requests>=2.23.0->ultralytics) (2.5.0)\nRequirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.11/dist-packages (from requests>=2.23.0->ultralytics) (2025.6.15)\nRequirement already satisfied: filelock in /usr/local/lib/python3.11/dist-packages (from torch>=1.8.0->ultralytics) (3.18.0)\nRequirement already satisfied: typing-extensions>=4.10.0 in /usr/local/lib/python3.11/dist-packages (from torch>=1.8.0->ultralytics) (4.14.0)\nRequirement already satisfied: networkx in /usr/local/lib/python3.11/dist-packages (from torch>=1.8.0->ultralytics) (3.5)\nRequirement already satisfied: jinja2 in /usr/local/lib/python3.11/dist-packages (from torch>=1.8.0->ultralytics) (3.1.6)\nRequirement already satisfied: fsspec in /usr/local/lib/python3.11/dist-packages (from torch>=1.8.0->ultralytics) (2025.5.1)\nProcessing ./packages/nvidia_cuda_nvrtc_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl (from torch>=1.8.0->ultralytics)\nProcessing ./packages/nvidia_cuda_runtime_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl (from torch>=1.8.0->ultralytics)\nProcessing ./packages/nvidia_cuda_cupti_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl (from torch>=1.8.0->ultralytics)\nProcessing ./packages/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl (from torch>=1.8.0->ultralytics)\nProcessing ./packages/nvidia_cublas_cu12-12.4.5.8-py3-none-manylinux2014_x86_64.whl (from torch>=1.8.0->ultralytics)\nProcessing ./packages/nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_x86_64.whl (from torch>=1.8.0->ultralytics)\nProcessing ./packages/nvidia_curand_cu12-10.3.5.147-py3-none-manylinux2014_x86_64.whl (from torch>=1.8.0->ultralytics)\nProcessing ./packages/nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_x86_64.whl (from torch>=1.8.0->ultralytics)\nProcessing ./packages/nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_x86_64.whl (from torch>=1.8.0->ultralytics)\nRequirement already satisfied: nvidia-cusparselt-cu12==0.6.2 in /usr/local/lib/python3.11/dist-packages (from torch>=1.8.0->ultralytics) (0.6.2)\nRequirement already satisfied: nvidia-nccl-cu12==2.21.5 in /usr/local/lib/python3.11/dist-packages (from torch>=1.8.0->ultralytics) (2.21.5)\nRequirement already satisfied: nvidia-nvtx-cu12==12.4.127 in /usr/local/lib/python3.11/dist-packages (from torch>=1.8.0->ultralytics) (12.4.127)\nProcessing ./packages/nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl (from torch>=1.8.0->ultralytics)\nRequirement already satisfied: triton==3.2.0 in /usr/local/lib/python3.11/dist-packages (from torch>=1.8.0->ultralytics) (3.2.0)\nRequirement already satisfied: sympy==1.13.1 in /usr/local/lib/python3.11/dist-packages (from torch>=1.8.0->ultralytics) (1.13.1)\nRequirement already satisfied: mpmath<1.4,>=1.1.0 in /usr/local/lib/python3.11/dist-packages (from sympy==1.13.1->torch>=1.8.0->ultralytics) (1.3.0)\nRequirement already satisfied: six>=1.5 in /usr/local/lib/python3.11/dist-packages (from python-dateutil>=2.7->matplotlib>=3.3.0->ultralytics) (1.17.0)\nRequirement already satisfied: MarkupSafe>=2.0 in /usr/local/lib/python3.11/dist-packages (from jinja2->torch>=1.8.0->ultralytics) (3.0.2)\nRequirement already satisfied: intel-openmp<2026,>=2024 in /usr/local/lib/python3.11/dist-packages (from mkl->numpy>=1.23.0->ultralytics) (2024.2.0)\nRequirement already satisfied: tbb==2022.* in /usr/local/lib/python3.11/dist-packages (from mkl->numpy>=1.23.0->ultralytics) (2022.2.0)\nRequirement already satisfied: tcmlib==1.* in /usr/local/lib/python3.11/dist-packages (from tbb==2022.*->mkl->numpy>=1.23.0->ultralytics) (1.4.0)\nRequirement already satisfied: intel-cmplr-lib-rt in /usr/local/lib/python3.11/dist-packages (from mkl_umath->numpy>=1.23.0->ultralytics) (2024.2.0)\nRequirement already satisfied: intel-cmplr-lib-ur==2024.2.0 in /usr/local/lib/python3.11/dist-packages (from intel-openmp<2026,>=2024->mkl->numpy>=1.23.0->ultralytics) (2024.2.0)\nInstalling collected packages: nvidia-nvjitlink-cu12, nvidia-curand-cu12, nvidia-cufft-cu12, nvidia-cuda-runtime-cu12, nvidia-cuda-nvrtc-cu12, nvidia-cuda-cupti-cu12, nvidia-cublas-cu12, nvidia-cusparse-cu12, nvidia-cudnn-cu12, nvidia-cusolver-cu12, ultralytics-thop, ultralytics\n  Attempting uninstall: nvidia-nvjitlink-cu12\n    Found existing installation: nvidia-nvjitlink-cu12 12.5.82\n    Uninstalling nvidia-nvjitlink-cu12-12.5.82:\n      Successfully uninstalled nvidia-nvjitlink-cu12-12.5.82\n  Attempting uninstall: nvidia-curand-cu12\n    Found existing installation: nvidia-curand-cu12 10.3.6.82\n    Uninstalling nvidia-curand-cu12-10.3.6.82:\n      Successfully uninstalled nvidia-curand-cu12-10.3.6.82\n  Attempting uninstall: nvidia-cufft-cu12\n    Found existing installation: nvidia-cufft-cu12 11.2.3.61\n    Uninstalling nvidia-cufft-cu12-11.2.3.61:\n      Successfully uninstalled nvidia-cufft-cu12-11.2.3.61\n  Attempting uninstall: nvidia-cuda-runtime-cu12\n    Found existing installation: nvidia-cuda-runtime-cu12 12.5.82\n    Uninstalling nvidia-cuda-runtime-cu12-12.5.82:\n      Successfully uninstalled nvidia-cuda-runtime-cu12-12.5.82\n  Attempting uninstall: nvidia-cuda-nvrtc-cu12\n    Found existing installation: nvidia-cuda-nvrtc-cu12 12.5.82\n    Uninstalling nvidia-cuda-nvrtc-cu12-12.5.82:\n      Successfully uninstalled nvidia-cuda-nvrtc-cu12-12.5.82\n  Attempting uninstall: nvidia-cuda-cupti-cu12\n    Found existing installation: nvidia-cuda-cupti-cu12 12.5.82\n    Uninstalling nvidia-cuda-cupti-cu12-12.5.82:\n      Successfully uninstalled nvidia-cuda-cupti-cu12-12.5.82\n  Attempting uninstall: nvidia-cublas-cu12\n    Found existing installation: nvidia-cublas-cu12 12.5.3.2\n    Uninstalling nvidia-cublas-cu12-12.5.3.2:\n      Successfully uninstalled nvidia-cublas-cu12-12.5.3.2\n  Attempting uninstall: nvidia-cusparse-cu12\n    Found existing installation: nvidia-cusparse-cu12 12.5.1.3\n    Uninstalling nvidia-cusparse-cu12-12.5.1.3:\n      Successfully uninstalled nvidia-cusparse-cu12-12.5.1.3\n  Attempting uninstall: nvidia-cudnn-cu12\n    Found existing installation: nvidia-cudnn-cu12 9.3.0.75\n    Uninstalling nvidia-cudnn-cu12-9.3.0.75:\n      Successfully uninstalled nvidia-cudnn-cu12-9.3.0.75\n  Attempting uninstall: nvidia-cusolver-cu12\n    Found existing installation: nvidia-cusolver-cu12 11.6.3.83\n    Uninstalling nvidia-cusolver-cu12-11.6.3.83:\n      Successfully uninstalled nvidia-cusolver-cu12-11.6.3.83\nSuccessfully installed nvidia-cublas-cu12-12.4.5.8 nvidia-cuda-cupti-cu12-12.4.127 nvidia-cuda-nvrtc-cu12-12.4.127 nvidia-cuda-runtime-cu12-12.4.127 nvidia-cudnn-cu12-9.1.0.70 nvidia-cufft-cu12-11.2.1.3 nvidia-curand-cu12-10.3.5.147 nvidia-cusolver-cu12-11.6.1.9 nvidia-cusparse-cu12-12.3.1.170 nvidia-nvjitlink-cu12-12.4.127 ultralytics-8.3.40 ultralytics-thop-2.0.12\n","output_type":"stream"}],"execution_count":1},{"cell_type":"code","source":"!pip install timm_3d --no-index --quiet --find-links=/kaggle/input/timm_3d_deps/other/initial/10/timm_3d/","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-19T11:11:19.610022Z","iopub.execute_input":"2026-02-19T11:11:19.61027Z","iopub.status.idle":"2026-02-19T11:11:28.13933Z","shell.execute_reply.started":"2026-02-19T11:11:19.610242Z","shell.execute_reply":"2026-02-19T11:11:28.13823Z"}},"outputs":[],"execution_count":2},{"cell_type":"code","source":"!pip install /kaggle/input/rsna2025-wheel/pylibjpeg-2.1.0-py3-none-any.whl \\\n             /kaggle/input/rsna2025-wheel/pylibjpeg_libjpeg-2.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl \\\n            /kaggle/input/rsna2025-wheel/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-19T11:11:28.140485Z","iopub.execute_input":"2026-02-19T11:11:28.14081Z","iopub.status.idle":"2026-02-19T11:11:32.464916Z","shell.execute_reply.started":"2026-02-19T11:11:28.140769Z","shell.execute_reply":"2026-02-19T11:11:32.463406Z"}},"outputs":[{"name":"stdout","text":"Processing /kaggle/input/rsna2025-wheel/pylibjpeg-2.1.0-py3-none-any.whl\nProcessing /kaggle/input/rsna2025-wheel/pylibjpeg_libjpeg-2.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\nProcessing /kaggle/input/rsna2025-wheel/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\nnumpy is already installed with the same version as the provided wheel. Use --force-reinstall to force an installation of the wheel.\nInstalling collected packages: pylibjpeg-libjpeg, pylibjpeg\nSuccessfully installed pylibjpeg-2.1.0 pylibjpeg-libjpeg-2.2.0\n","output_type":"stream"}],"execution_count":3},{"cell_type":"code","source":"# ====================================================\n# Imports\n# ====================================================\nimport os\nimport gc\nimport json\nimport shutil\nfrom tqdm import tqdm\nimport math\nimport warnings\nwarnings.filterwarnings('ignore')\n\nfrom pathlib import Path\nfrom typing import List, Dict, Optional, Tuple, Any\nfrom collections.abc import Sequence\nfrom concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, as_completed\n\n# Data handling\nimport numpy as np\nimport polars as pl\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\n# Medical imaging\nimport pydicom\nimport cv2\nfrom scipy import ndimage\nfrom sklearn import metrics\n\n# ML/DL\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset\nimport albumentations as A\n\nfrom torch.cuda.amp import autocast\nimport timm\nimport timm_3d\nfrom scipy.ndimage import zoom\nfrom ultralytics import YOLO\n\n# Transformations\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\n# Competition API\nimport kaggle_evaluation.rsna_inference_server\n\n# torch.backends.cudnn.benchmark = True                # 入力サイズ一定ならON\n# torch.backends.cuda.matmul.allow_tf32 = True         # Ampere以降で効く\n# torch.set_float32_matmul_precision(\"high\")           # PyTorch>=2.0","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-19T11:11:32.466206Z","iopub.execute_input":"2026-02-19T11:11:32.466569Z","iopub.status.idle":"2026-02-19T11:12:26.223161Z","shell.execute_reply.started":"2026-02-19T11:11:32.466512Z","shell.execute_reply":"2026-02-19T11:12:26.222191Z"}},"outputs":[{"name":"stdout","text":"Creating new Ultralytics Settings v0.0.6 file ✅ \nView Ultralytics Settings with 'yolo settings' or at '/root/.config/Ultralytics/settings.json'\nUpdate Settings with 'yolo settings key=value', i.e. 'yolo settings runs_dir=path/to/dir'. For help see https://docs.ultralytics.com/quickstart/#ultralytics-settings.\n","output_type":"stream"}],"execution_count":4},{"cell_type":"code","source":"ID_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]\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-19T11:12:26.224274Z","iopub.execute_input":"2026-02-19T11:12:26.224635Z","iopub.status.idle":"2026-02-19T11:12:26.488954Z","shell.execute_reply.started":"2026-02-19T11:12:26.224604Z","shell.execute_reply":"2026-02-19T11:12:26.488223Z"}},"outputs":[],"execution_count":5},{"cell_type":"markdown","source":"### Load DICOM files","metadata":{}},{"cell_type":"code","source":"from __future__ import annotations\nfrom typing import Optional, Tuple, List, Dict\nimport os, re\nimport numpy as np\nimport pydicom\nfrom pydicom.multival import MultiValue\nfrom pydicom.valuerep import DSfloat\n\ndef _to_gray_for_worker(arr: np.ndarray) -> Optional[np.ndarray]:\n    if arr is None:\n        return None\n    if arr.ndim == 2:\n        return arr\n    if arr.ndim == 3:\n        if arr.shape[-1] == 1:\n            return np.squeeze(arr, -1)\n        if arr.shape[-1] >= 3:\n            # Coefficients are BT.601\n            r = arr[..., 0].astype(np.float32, copy=False)\n            g = arr[..., 1].astype(np.float32, copy=False)\n            b = arr[..., 2].astype(np.float32, copy=False)\n            y = 0.299 * r + 0.587 * g + 0.114 * b\n            return y.astype(arr.dtype, copy=False)\n    return None\n\ndef _resize_nn_for_worker(img: np.ndarray, target_shape: Tuple[int,int]) -> np.ndarray:\n    th, tw = int(target_shape[0]), int(target_shape[1])\n    h, w = img.shape\n    if (h, w) == (th, tw):\n        return img\n    y_idx = (np.linspace(0, h - 1, th)).round().astype(int)\n    x_idx = (np.linspace(0, w - 1, tw)).round().astype(int)\n    out = img[y_idx][:, x_idx]\n    return out.astype(img.dtype, copy=False)\n\ndef _dicom_worker(idx: int,\n                  path: str,\n                  target_shape: Tuple[int,int],\n                  dtype_str: str,\n                  auto_resize_on_mismatch: bool) -> Tuple[int, Optional[np.ndarray]]:\n    try:\n        ds = pydicom.dcmread(path, force=True)\n        arr = ds.pixel_array\n    except Exception:\n        return (idx, None)\n\n    g = _to_gray_for_worker(arr)\n    if g is None and arr is not None and arr.ndim == 3:\n        # Looks 3D: sample one slice from the leading axis (F, H, W) as a fallback\n        mid = arr.shape[0] // 2\n        try:\n            g = _to_gray_for_worker(arr[mid])\n        except Exception:\n            g = None\n    if g is None:\n        return (idx, None)\n\n    # dtype alignment\n    if g.dtype.name != dtype_str:\n        g = g.astype(np.dtype(dtype_str), copy=False)\n\n    # shape alignment\n    if g.shape != tuple(target_shape):\n        if auto_resize_on_mismatch:\n            g = _resize_nn_for_worker(g, target_shape)\n        else:\n            return (idx, None)\n\n    return (idx, g)\n\nclass DicomVolumeFiles:\n    \"\"\"\n    Robust DICOM series loader (2D stack & 3D multi-frame)\n    - Does not rely on Rows/Columns for discovery/sorting\n    - Plane auto inference (axial / sagittal / coronal) via IOP or IPP\n    - Pixel spacing (XY/Z) extraction with fallbacks\n    - Optional strict mode for spacing (require_spacing)\n    - Filters for non-MONO and derived (MIP/MPR/SECONDARY) with staged relaxation\n    - Single pixel read per slice/frame (no double-read for shape filtering)\n    - XY spacing fallback:\n        If PixelSpacing is missing AND plane=='axial' AND the series looks like head MRA,\n        estimate XY spacing from image size using an assumed FOV (default 220 mm).\n    \"\"\"\n\n    TAGS = [\n        'BitsAllocated','BitsStored','Columns','FrameOfReferenceUID','HighBit',\n        'ImageOrientationPatient','ImagePositionPatient','InstanceNumber',\n        'Modality','PatientID','PhotometricInterpretation','PixelRepresentation',\n        'PixelSpacing','PlanarConfiguration','RescaleIntercept','RescaleSlope',\n        'RescaleType','Rows','SOPClassUID','SOPInstanceUID','SamplesPerPixel',\n        'SliceThickness','SpacingBetweenSlices','StudyInstanceUID','TransferSyntaxUID'\n    ]\n\n    # ---------- small helpers ----------\n    @staticmethod\n    def _as_int(x, default: Optional[int] = None) -> Optional[int]:\n        if x is None:\n            return default\n        try:\n            return int(x)\n        except Exception:\n            try:\n                return int(float(x))\n            except Exception:\n                return default\n\n    @staticmethod\n    def _as_float(x, default: Optional[float] = None) -> Optional[float]:\n        if x is None:\n            return default\n        try:\n            return float(x)\n        except Exception:\n            return default\n\n    def __init__(\n        self,\n        path: str,\n        plane: str = \"auto\",\n        debug: bool = False,\n        require_spacing: bool = False,\n        default_xy: Tuple[float, float] = (0.5, 0.5),\n        default_z: float = 0.5,\n        assumed_head_axial_fov_mm: float = 220.0,  # For estimating PixelSpacing from image size (head MRA, axial)\n    ):\n        self.debug = debug\n        self.require_spacing = bool(require_spacing)\n        self.default_xy = (float(default_xy[0]), float(default_xy[1]))\n        self.default_z = float(default_z)\n        self.assumed_head_axial_fov_mm = float(assumed_head_axial_fov_mm)\n\n        self.plane = plane.lower()\n        assert self.plane in {\"axial\", \"sagittal\", \"coronal\", \"auto\"}\n\n        # series state\n        self.is_multiframe: bool = False\n        self.file_paths: List[str] = []\n        self.n_frames: Optional[int] = None\n        self.frame_indices: Optional[List[int]] = None\n        self.read_order_like_2d: Optional[List[Tuple[str, int]]] = None\n        self.slice_positions: List[float] = []\n        self.reverse3d_hint: Optional[bool] = None\n        self.order_info: Optional[Dict] = None\n\n        # copied top-level tags / effective spacings\n        self.PixelSpacingEffective: Optional[Tuple[float, float]] = None # (dy, dx)\n        self.SliceThicknessEffective: Optional[float] = None\n        self.SpacingBetweenSlicesEffective: Optional[float] = None\n\n        # cache\n        self.pixel_array: Optional[np.ndarray] = None\n\n        # representative DS for spacing heuristics\n        self._repr_ds: Optional[pydicom.dataset.FileDataset] = None\n\n        self._load_dicom_series(path)\n        # spacing validation / fallbacks (+ axial head-MRA image-size heuristic)\n        self._finalize_spacing()\n\n    # ---------- helpers ----------\n    @staticmethod\n    def _normalize_vec(v: np.ndarray) -> Optional[np.ndarray]:\n        nrm = float(np.linalg.norm(v))\n        if nrm == 0.0 or not np.isfinite(nrm): return None\n        return v / nrm\n\n    def _apply_default_pixel_spacing_if_missing(self):\n        if self.PixelSpacingEffective is None and getattr(self, \"PixelSpacing\", None) is None:\n            dy, dx = self.default_xy\n            self.PixelSpacingEffective = (dy, dx)\n            try:\n                self.PixelSpacing = MultiValue(DSfloat, [dy, dx])\n            except Exception:\n                self.PixelSpacing = [dy, dx]\n\n    # ---- Head-MRA check & image-size-based spacing ----\n    def _looks_like_head_mra(self, ds: Optional[pydicom.Dataset]) -> bool:\n        try:\n            mod = str(getattr(ds, \"Modality\", \"\")).upper()\n            sd  = str(getattr(ds, \"SeriesDescription\", \"\")).upper()\n            st  = str(getattr(ds, \"StudyDescription\", \"\")).upper()\n            # Loose check: MR modality and series/study text contains MRA/ANGIO/TOF/Brain, etc.\n            if \"MR\" in mod and (\"MRA\" in sd or \"ANGIO\" in sd or \"TOF\" in sd or \"BRAIN\" in (sd+st)):\n                return True\n        except Exception:\n            pass\n        return False\n\n    def _get_matrix_hw(self, ds: Optional[pydicom.Dataset]) -> Optional[Tuple[int,int]]:\n        if ds is None:\n            return None\n        # Use header Rows/Columns if available (fast)\n        try:\n            h = int(getattr(ds, \"Rows\", 0))\n            w = int(getattr(ds, \"Columns\", 0))\n            if h > 0 and w > 0:\n                return (h, w)\n        except Exception:\n            pass\n        # Last resort: read pixel_array exactly once\n        try:\n            ds_full = pydicom.dcmread(ds.filename, force=True)\n            arr = ds_full.pixel_array\n            if arr.ndim == 2:\n                return (int(arr.shape[0]), int(arr.shape[1]))\n            if arr.ndim == 3 and arr.shape[-1] in (1,3,4):\n                return (int(arr.shape[0]), int(arr.shape[1]))\n            if arr.ndim == 3 and arr.shape[0] > 1:  # (F, H, W)\n                return (int(arr.shape[1]), int(arr.shape[2]))\n        except Exception:\n            pass\n        return None\n\n    def _estimate_xy_from_image_size(self, ds: Optional[pydicom.Dataset]) -> Optional[Tuple[float,float]]:\n        \"\"\"\n        Estimate PixelSpacing from image size when PixelSpacing/FOV tags are missing.\n        Conditions: plane == 'axial' and looks like a head MRA.\n        spacing ≈ assumed_head_axial_fov_mm / max(H, W)\n        \"\"\"\n        if ds is None:\n            return None\n        if self.plane != \"axial\":\n            return None\n        if not self._looks_like_head_mra(ds):\n            return None\n\n        hw = self._get_matrix_hw(ds)\n        if not hw:\n            return None\n        h, w = hw\n        side = max(h, w)\n        if side <= 0:\n            return None\n\n        px = float(self.assumed_head_axial_fov_mm) / float(side)  # mm/pixel\n        if px <= 0:\n            return None\n        return (px, px)\n\n    def _finalize_spacing(self):\n        # XY — respect existing values if they were already found\n        if self.PixelSpacingEffective is None:\n            # If axial head-MRA, estimate from image size\n            est_xy = self._estimate_xy_from_image_size(self._repr_ds)\n            if est_xy is not None:\n                self.PixelSpacingEffective = (float(est_xy[0]), float(est_xy[1]))\n                try:\n                    self.PixelSpacing = MultiValue(DSfloat, [est_xy[0], est_xy[1]])\n                except Exception:\n                    self.PixelSpacing = [float(est_xy[0]), float(est_xy[1])]\n\n        # If still missing, apply defaults\n        if self.PixelSpacingEffective is None:\n            self._apply_default_pixel_spacing_if_missing()\n\n        # Z spacing\n        if self.z_spacing is None or not np.isfinite(self.z_spacing) or np.isclose(self.z_spacing, 0.0, atol=1e-9):\n            self.SpacingBetweenSlicesEffective = self.default_z\n\n        # Strict mode\n        if self.require_spacing:\n            if self.PixelSpacingEffective is None:\n                raise ValueError(\"PixelSpacing (XY) not found.\")\n            if self.SpacingBetweenSlicesEffective is None:\n                raise ValueError(\"Z spacing not found.\")\n\n    # ---------- plane & spacing extraction ----------\n    def _infer_plane_from_iop(self, iop: List[float]) -> str:\n        r = np.array(iop[:3], dtype=float); c = np.array(iop[3:], dtype=float)\n        n = self._normalize_vec(np.cross(r, c))\n        if n is None:\n            return \"axial\"\n        axis = int(np.argmax(np.abs(n)))\n        return [\"sagittal\",\"coronal\",\"axial\"][axis]\n\n    def _infer_plane_from_positions(self, ds_list: List[pydicom.Dataset]) -> str:\n        pts = []\n        for ds in ds_list:\n            ipp = getattr(ds, \"ImagePositionPatient\", None)\n            if ipp is not None and len(ipp) >= 3:\n                try:\n                    pts.append([float(ipp[0]), float(ipp[1]), float(ipp[2])])\n                except Exception:\n                    pass\n        if len(pts) < 2:\n            return \"axial\"\n        pts = np.array(pts, dtype=float)\n        pts_c = pts - pts.mean(axis=0, keepdims=True)\n        try:\n            _, _, vh = np.linalg.svd(pts_c, full_matrices=False)\n            n = self._normalize_vec(vh[0])\n        except Exception:\n            n = self._normalize_vec(pts[-1] - pts[0])\n        if n is None:\n            return \"axial\"\n        axis = int(np.argmax(np.abs(n)))\n        return [\"sagittal\",\"coronal\",\"axial\"][axis]\n\n    def _get_xy_from_ds(self, ds: pydicom.Dataset) -> Optional[Tuple[float,float]]:\n        ps = getattr(ds, \"PixelSpacing\", None)\n        if ps is None: return None\n        try:\n            dy, dx = float(ps[0]), float(ps[1])\n            if dy <= 0 or dx <= 0: return None\n            return (dy, dx)\n        except Exception:\n            return None\n\n    def _pixel_measures_effective_3d(self, ds: pydicom.Dataset) -> Tuple[Optional[Tuple[float,float]], Optional[float], Optional[float]]:\n        dy = dx = th = sb = None\n        sfg = getattr(ds, \"SharedFunctionalGroupsSequence\", None)\n        if sfg:\n            pm = getattr(sfg[0], \"PixelMeasuresSequence\", None)\n            if pm:\n                pm0 = pm[0]\n                try:\n                    if \"PixelSpacing\" in pm0:\n                        dy, dx = float(pm0.PixelSpacing[0]), float(pm0.PixelSpacing[1])\n                    if \"SliceThickness\" in pm0:\n                        th = float(pm0.SliceThickness)\n                    if \"SpacingBetweenSlices\" in pm0:\n                        sb = float(pm0.SpacingBetweenSlices)\n                except Exception:\n                    pass\n        if any(v is None for v in (dy, dx, th, sb)):\n            pff = getattr(ds, \"PerFrameFunctionalGroupsSequence\", None)\n            if pff:\n                for fgs in pff:\n                    pm = getattr(fgs, \"PixelMeasuresSequence\", None)\n                    if not pm: continue\n                    pm0 = pm[0]\n                    try:\n                        if (dy is None or dx is None) and \"PixelSpacing\" in pm0:\n                            dy, dx = float(pm0.PixelSpacing[0]), float(pm0.PixelSpacing[1])\n                        if th is None and \"SliceThickness\" in pm0:\n                            th = float(pm0.SliceThickness)\n                        if sb is None and \"SpacingBetweenSlices\" in pm0:\n                            sb = float(pm0.SpacingBetweenSlices)\n                    except Exception:\n                        pass\n                    if all(v is not None for v in (dy, dx, th, sb)):\n                        break\n        return ((dy, dx) if (dy is not None and dx is not None and dy>0 and dx>0) else None, th, sb)\n\n    # ---------- series discovery ----------\n    def _iter_dicom_paths(self, path: str) -> List[str]:\n        if os.path.isdir(path):\n            out = []\n            for root, _, files in os.walk(path):\n                for f in files:\n                    if f.lower().endswith(\".dcm\") or (os.path.splitext(f)[1] == \"\"):\n                        out.append(os.path.join(root, f))\n            return out\n        else:\n            return [path]\n\n    def _load_dicom_series(self, path: str):\n        paths = self._iter_dicom_paths(path)\n        if not paths:\n            raise ValueError(\"No DICOM files found.\")\n\n        # header scan\n        headers: List[pydicom.Dataset] = []\n        multiframes: List[Tuple[str, pydicom.Dataset]] = []\n        for p in paths:\n            try:\n                ds = pydicom.dcmread(p, stop_before_pixels=True, force=True)\n            except Exception:\n                continue\n            headers.append(ds)\n\n            nframes = self._as_int(getattr(ds, \"NumberOfFrames\", None), 1)\n            if (nframes is not None and nframes > 1) or hasattr(ds, \"PerFrameFunctionalGroupsSequence\"):\n                multiframes.append((p, ds))\n\n        if not headers:\n            raise ValueError(\"No readable DICOM headers.\")\n\n        # 3D path\n        if multiframes:\n            path3d, ds3d = multiframes[0]\n            self.is_multiframe = True\n            self.file_paths = [path3d]\n            pff = getattr(ds3d, \"PerFrameFunctionalGroupsSequence\", None)\n            self.n_frames = self._as_int(getattr(ds3d, \"NumberOfFrames\", None), len(pff) if pff is not None else 0) or None\n            self.frame_indices = list(range(self.n_frames or 0))\n            self.read_order_like_2d = [(path3d, i) for i in (self.frame_indices or [])]\n\n            # plane\n            iop = None\n            sfg = getattr(ds3d, \"SharedFunctionalGroupsSequence\", None)\n            if sfg:\n                pos = getattr(sfg[0], \"PlaneOrientationSequence\", None)\n                if pos:\n                    try:\n                        iop = [float(x) for x in pos[0].ImageOrientationPatient]\n                    except Exception:\n                        iop = None\n            if iop is None and pff:\n                for fgs in pff:\n                    pos = getattr(fgs, \"PlaneOrientationSequence\", None)\n                    if pos:\n                        try:\n                            iop = [float(x) for x in pos[0].ImageOrientationPatient]\n                        except Exception:\n                            iop = None\n                        break\n            if iop is None and hasattr(ds3d, \"ImageOrientationPatient\"):\n                try:\n                    iop = [float(x) for x in ds3d.ImageOrientationPatient]\n                except Exception:\n                    iop = None\n\n            if self.plane == \"auto\":\n                if iop is not None:\n                    self.plane = self._infer_plane_from_iop(iop)\n                else:\n                    self.plane = self._infer_plane_from_positions([ds3d])\n\n            # slice positions (for z estimation / reverse hint)\n            pos_list = []\n            if pff:\n                for fgs in pff:\n                    pps = getattr(fgs, \"PlanePositionSequence\", None)\n                    if pps:\n                        try:\n                            ipp = pps[0].ImagePositionPatient\n                            pos_list.append([float(ipp[0]), float(ipp[1]), float(ipp[2])])\n                        except Exception:\n                            pos_list.append([np.nan, np.nan, np.nan])\n                    else:\n                        pos_list.append([np.nan, np.nan, np.nan])\n            self.slice_positions = []\n            if pos_list:\n                axis_map = {\"sagittal\":0, \"coronal\":1, \"axial\":2}\n                axis = axis_map.get(self.plane, 2)\n                s = np.array(pos_list, dtype=float)[:,axis]\n                self.slice_positions = s.tolist()\n                finite = np.isfinite(s)\n                if finite.sum() >= 2:\n                    sv = s[finite]\n                    nondec = np.all(np.diff(sv) >= 0)\n                    noninc = np.all(np.diff(sv) <= 0)\n                    if nondec and not noninc: self.reverse3d_hint = False\n                    elif noninc and not nondec: self.reverse3d_hint = True\n                    elif nondec and noninc: self.reverse3d_hint = False\n                    else: self.reverse3d_hint = None\n                else:\n                    self.reverse3d_hint = None\n\n            # effective spacing\n            ps_eff, th_eff, sb_eff = self._pixel_measures_effective_3d(ds3d)\n            self.PixelSpacingEffective = ps_eff\n            self.SliceThicknessEffective = th_eff\n            self.SpacingBetweenSlicesEffective = sb_eff\n\n            # copy top-level tags + set repr ds\n            self._repr_ds = pydicom.dcmread(path3d, stop_before_pixels=True, force=True)\n            self._set_metadata_from_top(self._repr_ds)\n\n            # XY default if needed (image-size heuristic handled in _finalize_spacing)\n            self._apply_default_pixel_spacing_if_missing()\n            return\n\n        # 2D path (stack)\n        ds_first = headers[0]\n        if self.plane == \"auto\":\n            if hasattr(ds_first, \"ImageOrientationPatient\"):\n                try:\n                    iop = [float(x) for x in ds_first.ImageOrientationPatient]\n                    self.plane = self._infer_plane_from_iop(iop)\n                except Exception:\n                    self.plane = self._infer_plane_from_positions(headers)\n            else:\n                self.plane = self._infer_plane_from_positions(headers)\n\n        def _sort_key(ds: pydicom.Dataset, i: int, pth: str) -> float:\n            try:\n                ipp = getattr(ds, \"ImagePositionPatient\", None)\n                if ipp is not None and len(ipp)>=3:\n                    val = {\"axial\":2,\"sagittal\":0,\"coronal\":1}[self.plane]\n                    return float(ipp[val])\n            except Exception:\n                pass\n            try:\n                return float(getattr(ds, \"InstanceNumber\", i))\n            except Exception:\n                pass\n            # Fall back to the last number in filename, then index\n            m = re.search(r'(\\d+)(?!.*\\d)', os.path.basename(pth))\n            if m: return float(m.group(1))\n            return float(i)\n\n        pairs: List[Tuple[float,str]] = []\n        for i,(pth,ds) in enumerate(zip(paths, headers)):\n            pairs.append((_sort_key(ds,i,pth), pth))\n        pairs.sort(key=lambda t: t[0])\n\n        self.file_paths = [p for _,p in pairs]\n        self.slice_positions = [k for k,_ in pairs]  # may be fallback values\n        self.is_multiframe = False\n        self.n_frames = None\n        self.frame_indices = None\n        self.read_order_like_2d = None\n\n        # copy tags from the first pixel-bearing file + set repr ds\n        self._repr_ds = pydicom.dcmread(self.file_paths[0], stop_before_pixels=True, force=True)\n        self._set_metadata_from_top(self._repr_ds)\n\n        # XY from top-level if present\n        xy = self._get_xy_from_ds(self._repr_ds)\n        self.PixelSpacingEffective = xy\n\n        # XY default if needed (image-size heuristic handled in _finalize_spacing)\n        self._apply_default_pixel_spacing_if_missing()\n\n    # ---------- metadata copy ----------\n    def _set_metadata_from_top(self, ds: pydicom.Dataset):\n        for tag in self.TAGS:\n            try:\n                setattr(self, tag, getattr(ds, tag, None))\n            except Exception:\n                setattr(self, tag, None)\n\n    # ---------- properties ----------\n    @property\n    def z_spacing(self) -> Optional[float]:\n        vals = np.array(self.slice_positions, dtype=float)\n        vals = vals[np.isfinite(vals)]\n        if vals.size < 2:\n            return self.SpacingBetweenSlicesEffective\n        diffs = np.diff(np.sort(vals))\n        if diffs.size == 0:\n            return self.SpacingBetweenSlicesEffective\n        z = float(np.mean(np.abs(diffs)))\n        if not np.isfinite(z) or np.isclose(z, 0.0, atol=1e-9):\n            return self.SpacingBetweenSlicesEffective\n        return z\n\n    @property\n    def voxel_spacing_xyz(self) -> Optional[Tuple[float,float,float]]:\n        xy = self.PixelSpacingEffective\n        z = self.z_spacing\n        if xy is None or z is None:\n            return None\n        return (float(xy[0]), float(xy[1]), float(z))\n\n    # ---------- read plan ----------\n    def get_ordered_read_plan(self) -> List[Tuple[str, Optional[int]]]:\n        if self.is_multiframe:\n            if self.read_order_like_2d is not None:\n                return list(self.read_order_like_2d)\n            if self.file_paths and self.frame_indices:\n                return [(self.file_paths[0], i) for i in self.frame_indices]\n            return []\n        else:\n            return [(p, None) for p in self.file_paths]\n\n    # ---------- pixel loading ----------\n    def load_pixel_array(\n        self,\n        apply_rescale: bool = False,\n        out_dtype: Optional[np.dtype] = None,\n        return_reverse: bool = False,        # Hint whether original order was reversed (for 3D)\n        reject_non_mono: bool = True,        # Start with MONO only; relax if needed\n        skip_derived: bool = True,           # Exclude DERIVED/MPR first; relax if needed\n        auto_resize_on_mismatch: bool = True,# Resize on shape mismatch for 2D\n        debug: Optional[bool] = None,\n    ):\n        if debug is None:\n            debug = bool(getattr(self, \"debug\", False))\n\n        # ---------------- helpers ----------------\n        def _head(path: str):\n            return pydicom.dcmread(path, stop_before_pixels=True, force=True)\n\n        def _is_mono(ds) -> bool:\n            pi = getattr(ds, \"PhotometricInterpretation\", \"MONOCHROME2\")\n            spp = int(getattr(ds, \"SamplesPerPixel\", 1))\n            return pi in (\"MONOCHROME1\", \"MONOCHROME2\") and spp == 1\n\n        def _is_derived_like(ds) -> bool:\n            it = [s.strip().upper() for s in (list(getattr(ds, \"ImageType\", [])) or [])]\n            sd = str(getattr(ds, \"SeriesDescription\", \"\")).upper()\n            dd = str(getattr(ds, \"DerivationDescription\", \"\")).upper()\n            tokens = it + [sd, dd]\n            flags = (\"MIP\", \"MPR\", \"PROJECTION\", \"DERIVED\", \"SECONDARY\", \"AVG\", \"MEAN\")\n            return any(tok in t for t in tokens for tok in flags)\n\n        def _safe_pixel_array(path: str):\n            try:\n                ds = pydicom.dcmread(path, force=True)  # pixels included\n                return ds, ds.pixel_array\n            except NotImplementedError:\n                if debug:\n                    try:\n                        ts = str(getattr(_head(path), \"TransferSyntaxUID\", \"unknown\"))\n                    except Exception:\n                        ts = \"unknown\"\n                    print(f\"[decode-fail] {os.path.basename(path)} TS={ts} (install pylibjpeg/gdcm?)\")\n                return None, None\n\n        def _to_gray_2d(arr: np.ndarray) -> Optional[np.ndarray]:\n            if arr is None:\n                return None\n            if arr.ndim == 2:\n                return arr\n            if arr.ndim == 3:\n                if arr.shape[-1] == 1:\n                    return np.squeeze(arr, axis=-1)\n                if arr.shape[-1] >= 3:\n                    r = arr[..., 0].astype(np.float32, copy=False)\n                    g = arr[..., 1].astype(np.float32, copy=False)\n                    b = arr[..., 2].astype(np.float32, copy=False)\n                    y = (0.299*r + 0.587*g + 0.114*b)\n                    return y.astype(arr.dtype, copy=False)\n            return None\n\n        def _resize_nn(img: np.ndarray, target_shape: tuple[int,int]) -> np.ndarray:\n            th, tw = target_shape\n            h, w = img.shape\n            if (h, w) == (th, tw):\n                return img\n            y_idx = (np.linspace(0, h-1, th)).round().astype(int)\n            x_idx = (np.linspace(0, w-1, tw)).round().astype(int)\n            out = img[y_idx][:, x_idx]\n            return out.astype(img.dtype, copy=False)\n\n        # ---------------- cache ----------------\n        if self.pixel_array is not None:\n            arr = self.pixel_array\n            if apply_rescale:\n                slope = float(getattr(self, \"RescaleSlope\", 1.0) or 1.0)\n                inter = float(getattr(self, \"RescaleIntercept\", 0.0) or 0.0)\n                if slope != 1.0 or inter != 0.0:\n                    arr = arr.astype(np.float32, copy=False)\n                    arr = arr * slope + inter\n            if out_dtype is not None and arr.dtype != out_dtype:\n                if np.issubdtype(out_dtype, np.integer) and arr.dtype.kind == 'f':\n                    info = np.iinfo(out_dtype)\n                    arr = np.clip(arr, info.min, info.max)\n                arr = arr.astype(out_dtype, copy=False)\n            return (arr, self.reverse3d_hint if self.is_multiframe else None) if return_reverse else arr\n\n        # ---------------- 3D: header path ----------------\n        if self.is_multiframe and self.file_paths:\n            ds0, arr = _safe_pixel_array(self.file_paths[0])\n            if ds0 is not None and arr is not None:\n                if arr.ndim == 4 and arr.shape[-1] == 1:\n                    arr = np.squeeze(arr, axis=-1)\n                elif arr.ndim == 4 and arr.shape[-1] >= 3:\n                    arr = np.stack([_to_gray_2d(a) for a in arr], axis=0)\n                if arr.ndim == 3:\n                    if apply_rescale:\n                        slope = float(getattr(self, \"RescaleSlope\", 1.0) or 1.0)\n                        inter = float(getattr(self, \"RescaleIntercept\", 0.0) or 0.0)\n                        if slope != 1.0 or inter != 0.0:\n                            arr = arr.astype(np.float32, copy=False)\n                            arr = arr * slope + inter\n                    if out_dtype is not None and arr.dtype != out_dtype:\n                        if np.issubdtype(out_dtype, np.integer) and arr.dtype.kind == 'f':\n                            info = np.iinfo(out_dtype)\n                            arr = np.clip(arr, info.min, info.max)\n                        arr = arr.astype(out_dtype, copy=False)\n                    self.pixel_array = arr\n                    return (arr, self.reverse3d_hint) if return_reverse else arr\n                else:\n                    if debug: print(f\"[3D->2D] unexpected shape: {getattr(arr,'shape',None)}\")\n\n        # ---------------- 3D fallback: first file actually 3D ----------------\n        if self.file_paths:\n            ds_probe, arr_probe = _safe_pixel_array(self.file_paths[0])\n            if arr_probe is not None and arr_probe.ndim == 3:\n                arr = arr_probe\n                if arr.ndim == 4 and arr.shape[-1] == 1:\n                    arr = np.squeeze(arr, axis=-1)\n                elif arr.ndim == 4 and arr.shape[-1] >= 3:\n                    arr = np.stack([_to_gray_2d(a) for a in arr], axis=0)\n                if apply_rescale:\n                    slope = float(getattr(self, \"RescaleSlope\", 1.0) or 1.0)\n                    inter = float(getattr(self, \"RescaleIntercept\", 0.0) or 0.0)\n                    if slope != 1.0 or inter != 0.0:\n                        arr = arr.astype(np.float32, copy=False)\n                        arr = arr * slope + inter\n                if out_dtype is not None and arr.dtype != out_dtype:\n                    if np.issubdtype(out_dtype, np.integer) and arr.dtype.kind == 'f':\n                        info = np.iinfo(out_dtype)\n                        arr = np.clip(arr, info.min, info.max)\n                    arr = arr.astype(out_dtype, copy=False)\n                self.is_multiframe = True\n                self.pixel_array = arr\n                return (arr, self.reverse3d_hint) if return_reverse else arr\n\n        # ---------------- 2D path with staged relaxation ----------------\n        if not self.file_paths:\n            raise ValueError(\"No files for 2D stack.\")\n\n        def _collect_candidates(allow_derived: bool, allow_non_mono: bool) -> List[str]:\n            cand: List[str] = []\n            for pth in self.file_paths:\n                try:\n                    h = _head(pth)\n                except Exception:\n                    continue\n                if (not allow_derived) and _is_derived_like(h):\n                    continue\n                if (not allow_non_mono) and (not _is_mono(h)):\n                    continue\n                cand.append(pth)\n            return cand\n\n        candidate_paths = _collect_candidates(False, False)\n        if not candidate_paths:\n            if debug: print(\"[fallback] allow derived\")\n            candidate_paths = _collect_candidates(True, False)\n        if not candidate_paths:\n            if debug: print(\"[fallback] allow non-mono\")\n            candidate_paths = _collect_candidates(True, True)\n        if not candidate_paths:\n            if debug: print(\"[fallback] use all files\")\n            candidate_paths = list(self.file_paths)\n\n        accepted = []\n        target_shape = None\n        first_dtype = None\n        for p in candidate_paths:\n            ds, ai = _safe_pixel_array(p)\n            if ai is None:\n                continue\n            ai2 = _to_gray_2d(ai)\n            if ai2 is None:\n                if ai.ndim == 3:\n                    mid = ai.shape[0] // 2\n                    ai2 = _to_gray_2d(ai[mid])\n                if ai2 is None:\n                    continue\n\n            if target_shape is None:\n                target_shape = ai2.shape\n                first_dtype = ai2.dtype\n                accepted.append(ai2)\n            else:\n                if ai2.dtype != first_dtype:\n                    ai2 = ai2.astype(first_dtype, copy=False)\n                if ai2.shape != target_shape:\n                    if auto_resize_on_mismatch:\n                        ai2 = _resize_nn(ai2, target_shape)\n                    else:\n                        if debug:\n                            print(f\"[skip] shape mismatch: want={target_shape} got={ai2.shape}\")\n                        continue\n                accepted.append(ai2)\n\n        if not accepted:\n            raise ValueError(\"No usable 2D slices after decoding (even after fallbacks).\")\n\n        arr = np.stack(accepted, axis=0)  # (N, H, W)\n\n        if apply_rescale:\n            slope = float(getattr(self, \"RescaleSlope\", 1.0) or 1.0)\n            inter = float(getattr(self, \"RescaleIntercept\", 0.0) or 0.0)\n            if slope != 1.0 or inter != 0.0:\n                arr = arr.astype(np.float32, copy=False)\n                arr = arr * slope + inter\n\n        if out_dtype is not None and arr.dtype != out_dtype:\n            if np.issubdtype(out_dtype, np.integer) and arr.dtype.kind == 'f':\n                info = np.iinfo(out_dtype)\n                arr = np.clip(arr, info.min, info.max)\n            arr = arr.astype(out_dtype, copy=False)\n\n        self.pixel_array = arr\n        return (arr, None) if return_reverse else arr    \n    \n    # ======= From here: provide an add-on parallel loading method to DicomVolumeFiles =======\n    def load_pixel_array_parallel(\n        self,\n        apply_rescale: bool = False,\n        out_dtype: Optional[np.dtype] = None,\n        return_reverse: bool = False,\n        reject_non_mono: bool = True,\n        skip_derived: bool = True,\n        auto_resize_on_mismatch: bool = True,\n        max_workers: Optional[int] = None,\n        use_threads: bool = True,   # False → ProcessPoolExecutor\n        debug: Optional[bool] = None,\n    ):\n        \"\"\"\n        Parallel loading for 2D stacks.\n        For multi-frame (single-file 3D), call the existing load_pixel_array.\n        \"\"\"\n        if debug is None:\n            debug = bool(getattr(self, \"debug\", False))\n    \n        # If cache already exists, just format and return\n        if self.pixel_array is not None:\n            arr = self.pixel_array\n            if apply_rescale:\n                slope = float(getattr(self, \"RescaleSlope\", 1.0) or 1.0)\n                inter = float(getattr(self, \"RescaleIntercept\", 0.0) or 0.0)\n                if slope != 1.0 or inter != 0.0:\n                    arr = arr.astype(np.float32, copy=False)\n                    arr = arr * slope + inter\n            if out_dtype is not None and arr.dtype != out_dtype:\n                if np.issubdtype(out_dtype, np.integer) and arr.dtype.kind == 'f':\n                    info = np.iinfo(out_dtype)\n                    arr = np.clip(arr, info.min, info.max)\n                arr = arr.astype(out_dtype, copy=False)\n            return (arr, self.reverse3d_hint if self.is_multiframe else None) if return_reverse else arr\n    \n        # Multi-frame should go through the existing path (more efficient)\n        if self.is_multiframe:\n            return self.load_pixel_array(\n                apply_rescale=apply_rescale,\n                out_dtype=out_dtype,\n                return_reverse=return_reverse,\n                reject_non_mono=reject_non_mono,\n                skip_derived=skip_derived,\n                auto_resize_on_mismatch=auto_resize_on_mismatch,\n                debug=debug,\n            )\n    \n        # From here on: 2D stack\n        if not self.file_paths:\n            raise ValueError(\"No files for 2D stack.\")\n    \n        # --- Lightly mirror the existing filtering logic (minimal header I/O) ---\n        def _head(path: str):\n            return pydicom.dcmread(path, stop_before_pixels=True, force=True)\n    \n        def _is_mono(ds) -> bool:\n            pi = getattr(ds, \"PhotometricInterpretation\", \"MONOCHROME2\")\n            spp = int(getattr(ds, \"SamplesPerPixel\", 1))\n            return pi in (\"MONOCHROME1\", \"MONOCHROME2\") and spp == 1\n    \n        def _is_derived_like(ds) -> bool:\n            it = [s.strip().upper() for s in (list(getattr(ds, \"ImageType\", [])) or [])]\n            sd = str(getattr(ds, \"SeriesDescription\", \"\")).upper()\n            dd = str(getattr(ds, \"DerivationDescription\", \"\")).upper()\n            tokens = it + [sd, dd]\n            flags = (\"MIP\",\"MPR\",\"PROJECTION\",\"DERIVED\",\"SECONDARY\",\"AVG\",\"MEAN\")\n            return any(tok in t for t in tokens for tok in flags)\n    \n        def _collect_candidates(allow_derived: bool, allow_non_mono: bool):\n            cand = []\n            for pth in self.file_paths:\n                try:\n                    h = _head(pth)\n                except Exception:\n                    continue\n                if (not allow_derived) and _is_derived_like(h):\n                    continue\n                if (not allow_non_mono) and (not _is_mono(h)):\n                    continue\n                cand.append(pth)\n            return cand\n    \n        candidate_paths = _collect_candidates(False, False)\n        if not candidate_paths:\n            if debug: print(\"[fallback] allow derived\")\n            candidate_paths = _collect_candidates(True, False)\n        if not candidate_paths:\n            if debug: print(\"[fallback] allow non-mono\")\n            candidate_paths = _collect_candidates(True, True)\n        if not candidate_paths:\n            if debug: print(\"[fallback] use all files\")\n            candidate_paths = list(self.file_paths)\n    \n        # --- Fix target shape/dtype with the first slice (sequential) ---\n        try:\n            ds0 = pydicom.dcmread(candidate_paths[0], force=True)\n            a0 = ds0.pixel_array\n        except Exception:\n            raise ValueError(\"Failed to decode the first slice.\")\n        g0 = _to_gray_for_worker(a0)\n        if g0 is None and a0 is not None and a0.ndim == 3:\n            mid = a0.shape[0] // 2\n            try:\n                g0 = _to_gray_for_worker(a0[mid])\n            except Exception:\n                g0 = None\n        if g0 is None:\n            raise ValueError(\"First slice is not a grayscale-like image.\")\n    \n        target_shape = g0.shape\n        dtype_str = g0.dtype.name\n        n = len(candidate_paths)\n    \n        # --- Pre-allocate the final array and write into each slot ---\n        out = np.empty((n, target_shape[0], target_shape[1]), dtype=g0.dtype)\n        out[0] = g0\n    \n        # --- Executor choice & worker count ---\n        Exec = ThreadPoolExecutor if use_threads else ProcessPoolExecutor\n        if max_workers is None:\n            # Conservative defaults based on storage: NVMe 8–16 / SATA SSD 4–8 / NAS 2–4\n            max_workers = 8\n    \n        # --- Submit jobs in parallel ---\n        ok = 1\n        with Exec(max_workers=max_workers) as ex:\n            futures = [\n                ex.submit(_dicom_worker, i, p, target_shape, dtype_str, auto_resize_on_mismatch)\n                for i, p in enumerate(candidate_paths[1:], start=1)\n            ]\n            for f in as_completed(futures):\n                idx, arr2d = f.result()\n                if arr2d is not None:\n                    out[idx] = arr2d\n                    ok += 1\n                elif debug:\n                    print(f\"[skip] index={idx} {os.path.basename(candidate_paths[idx])}\")\n    \n        if ok == 0:\n            raise ValueError(\"No usable 2D slices after parallel decoding.\")\n    \n        # If there are failed slices, compact the array (keeps original file_paths order)\n        if ok != n:\n            out = out[:ok]\n    \n        # --- Rescale / dtype finishing ---\n        if apply_rescale:\n            slope = float(getattr(self, \"RescaleSlope\", 1.0) or 1.0)\n            inter = float(getattr(self, \"RescaleIntercept\", 0.0) or 0.0)\n            if slope != 1.0 or inter != 0.0:\n                out = out.astype(np.float32, copy=False)\n                out = out * slope + inter\n    \n        if out_dtype is not None and out.dtype != out_dtype:\n            if np.issubdtype(out_dtype, np.integer) and out.dtype.kind == 'f':\n                info = np.iinfo(out_dtype)\n                out = np.clip(out, info.min, info.max)\n            out = out.astype(out_dtype, copy=False)\n    \n        self.pixel_array = out\n        return (out, None) if return_reverse else out\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-19T11:12:26.491597Z","iopub.execute_input":"2026-02-19T11:12:26.491922Z","iopub.status.idle":"2026-02-19T11:12:26.890821Z","shell.execute_reply.started":"2026-02-19T11:12:26.491899Z","shell.execute_reply":"2026-02-19T11:12:26.890065Z"}},"outputs":[],"execution_count":6},{"cell_type":"markdown","source":"### Prepare YOLO dataset","metadata":{}},{"cell_type":"code","source":"def pct_normalize(img, p_min, p_max):\n    vmin = np.percentile(img, p_min)\n    vmax = np.percentile(img, p_max)\n    img = np.clip(img, vmin, vmax)\n    img = (img - vmin) / (vmax - vmin + 1e-6)\n    img = (img*255).astype(np.uint8)\n    return img\n    \ndef value_normalize(img, vmin, vmax):\n    img = np.clip(img, vmin, vmax)\n    img = (img - vmin) / (vmax - vmin + 1e-6)\n    img = (img*255).astype(np.uint8)\n    return img\n\ndef resample_zyx(arr: np.ndarray,\n                 spacing: tuple[float, float, float],\n                 new_spacing: tuple[float, float, float],\n                 order: int) -> np.ndarray:\n    \"\"\"\n    Resample (Z, Y, X) from `spacing` to `new_spacing`.\n    Use order=1 for images (linear), order=0 for masks (nearest).\n    \"\"\"\n    sz, sy, sx = spacing\n    nsz, nsy, nsx = new_spacing\n    zz, zy, zx = sz / nsz, sy / nsy, sx / nsx\n    mode = \"nearest\" if order == 0 else \"constant\"\n    return zoom(arr, zoom=(zz, zy, zx), order=order, mode=mode)\n\ndef get_mid_slice(imgs, plane, modality, pixel_spacing):\n    \n    imgs_resized = resample_zyx(\n        imgs.copy(),\n        spacing=pixel_spacing,\n        new_spacing=(1., 1., 1.),\n        order=1\n    )\n\n    # 正規化\n    if modality == 'CT':\n        imgs_resized = value_normalize(imgs_resized, -100, 600)\n    else:\n        imgs_resized = pct_normalize(imgs_resized, 1, 99)\n\n    Z, H, W = imgs_resized.shape\n    z_mid, h_mid, w_mid = Z // 2, H // 2, W // 2\n\n    # Save 2 direction for each volume.\n    if plane == 'axial':\n        # coronal (axis1)\n        img_0 = imgs_resized[:, h_mid, :]\n        # sagittal (axis2)\n        img_1 = imgs_resized[:, :, w_mid]\n        return (img_0, \"axis1\"), (img_1, \"axis2\")\n    elif plane == 'coronal':\n        # coronal (axis0)\n        img_0 = imgs_resized[z_mid, :, :]\n        # sagittal (axis2)  \n        img_1 = imgs_resized[:, :, w_mid]\n        return (img_0, \"axis0\"), (img_1, \"axis2\")\n\n    elif plane == 'sagittal':\n        # sagittal (axis0)\n        img_0 = imgs_resized[z_mid, :, :]\n        # coronal (axis2)  ※ x_mid→w_mid に修正\n        img_1 = imgs_resized[:, :, w_mid]\n        return (img_0, \"axis0\"), (img_1, \"axis2\")\n    else:\n        return (\"skip\", f\"Unknown plane: {plane}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-19T11:12:26.891583Z","iopub.execute_input":"2026-02-19T11:12:26.891812Z","iopub.status.idle":"2026-02-19T11:12:26.909566Z","shell.execute_reply.started":"2026-02-19T11:12:26.891792Z","shell.execute_reply":"2026-02-19T11:12:26.908865Z"}},"outputs":[],"execution_count":7},{"cell_type":"markdown","source":"### YOLO prediction","metadata":{}},{"cell_type":"code","source":"def _iou_xyxy(a, b):\n    # a,b: [x1,y1,x2,y2]\n    x1 = max(a[0], b[0]); y1 = max(a[1], b[1])\n    x2 = min(a[2], b[2]); y2 = min(a[3], b[3])\n    iw = max(0.0, x2 - x1); ih = max(0.0, y2 - y1)\n    inter = iw * ih\n    if inter <= 0: return 0.0\n    area_a = max(0.0, (a[2]-a[0]) * (a[3]-a[1]))\n    area_b = max(0.0, (b[2]-b[0]) * (b[3]-b[1]))\n    union = area_a + area_b - inter + 1e-9\n    return inter / union\n\ndef _ensure_dir(p):\n    Path(p).mkdir(parents=True, exist_ok=True)\n\ndef weighted_boxes_fusion(\n    boxes, scores, labels,\n    iou_thr=0.55,\n    conf_type=\"avg\"   # \"avg\" or \"one_minus_prod\"\n):\n    \"\"\"\n    Merge outputs from multiple models for a single image.\n    All inputs are already concatenated into one array each:\n      - boxes:  (N, 4) in xyxy\n      - scores: (N,)\n      - labels: (N,)\n    \n    Returns:\n      fused_boxes, fused_scores, fused_labels\n    \"\"\"\n    if len(boxes) == 0:\n        return boxes, scores, labels\n\n    order = np.argsort(-scores)\n    boxes = boxes[order]; scores = scores[order]; labels = labels[order]\n\n    clusters = []  # list of dict: {'box': np.array(4), 'score_sum': float, 'scores': [..], 'label': int, 'weight_sum': float, 'members': int}\n    for b, s, c in zip(boxes, scores, labels):\n        matched = False\n        for cl in clusters:\n            if cl['label'] != c:\n                continue\n            if _iou_xyxy(b, cl['box']) >= iou_thr:\n                # 既存クラスタにマージ（座標はスコア重みで移動平均）\n                w_old = cl['weight_sum']; w_new = s\n                cl['box'] = (cl['box'] * w_old + b * w_new) / (w_old + w_new + 1e-9)\n                cl['weight_sum'] += w_new\n                cl['scores'].append(s)\n                cl['members'] += 1\n                matched = True\n                break\n        if not matched:\n            clusters.append({\n                'box': b.copy(),\n                'label': int(c),\n                'weight_sum': float(s),\n                'scores': [float(s)],\n                'members': 1,\n            })\n\n    fused_boxes, fused_scores, fused_labels = [], [], []\n    for cl in clusters:\n        fused_boxes.append(cl['box'])\n        if conf_type == \"one_minus_prod\":\n            # 1 - ∏(1-s) で統合信頼度\n            p = 1.0\n            for s in cl['scores']:\n                p *= (1.0 - s)\n            sc = 1.0 - p\n        else:\n            sc = float(np.mean(cl['scores']))\n        fused_scores.append(sc)\n        fused_labels.append(cl['label'])\n\n    return np.vstack(fused_boxes), np.array(fused_scores), np.array(fused_labels, dtype=int)\n\n\ndef predict_ensemble_single_image(\n    models,\n    image,\n    conf=0.2,\n    iou=0.7,\n    imgsz=320,\n    per_model_max_det=1,\n    ensemble_iou=0.55,\n    only_one=True,\n    agnostic=False,\n):\n    \"\"\"\n    各 fold の best.pt を読み、単一画像に対して推論→WBFで統合。\n    戻り値: dict {'boxes': (K,4), 'scores': (K,), 'labels': (K,)}\n    \"\"\"\n    # 遅延ロードを避けたい場合は、外で [YOLO(p) for p in model_paths] を渡す実装に変えてもOK\n    boxes_all = []\n    scores_all = []\n    labels_all = []\n    for model in models:\n        res = model.predict(\n            source=image,\n            conf=conf, iou=iou, imgsz=imgsz, max_det=per_model_max_det, verbose=False\n        )[0]\n        if res.boxes is None or len(res.boxes) == 0:\n            continue\n        xyxy = res.boxes.xyxy.cpu().numpy()\n        sco  = res.boxes.conf.cpu().numpy()\n        lab  = (res.boxes.cls.cpu().numpy().astype(int)\n                if res.boxes.cls is not None else np.zeros(len(xyxy), dtype=int))\n        boxes_all.append(xyxy); scores_all.append(sco); labels_all.append(lab)\n\n    if len(boxes_all) == 0:\n        return {'boxes': np.zeros((0,4), dtype=np.float32),\n                'scores': np.zeros((0,), dtype=np.float32),\n                'labels': np.zeros((0,), dtype=np.int32)}\n\n    boxes = np.vstack(boxes_all)\n    scores = np.hstack(scores_all)\n    labels = np.hstack(labels_all)\n\n    # 簡易 WBF\n    f_boxes, f_scores, f_labels = weighted_boxes_fusion(\n        boxes, scores, labels, iou_thr=ensemble_iou, conf_type=\"one_minus_prod\"\n    )\n\n    # 1件に絞る（スコア最大）\n    if only_one and len(f_scores) > 0:\n        idx = int(np.argmax(f_scores))\n        f_boxes = f_boxes[idx:idx+1]\n        f_scores = f_scores[idx:idx+1]\n        f_labels = f_labels[idx:idx+1]\n\n    return {'boxes': f_boxes, 'scores': f_scores, 'labels': f_labels}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-19T11:12:26.910559Z","iopub.execute_input":"2026-02-19T11:12:26.9109Z","iopub.status.idle":"2026-02-19T11:12:26.934287Z","shell.execute_reply.started":"2026-02-19T11:12:26.910869Z","shell.execute_reply":"2026-02-19T11:12:26.933677Z"}},"outputs":[],"execution_count":8},{"cell_type":"markdown","source":"### 3D bbox calculation","metadata":{}},{"cell_type":"code","source":"def bbox_coords(yolo_preds, plane):\n    zs_min, zs_max, ys_min, ys_max, xs_min, xs_max = [], [], [], [], [], []\n    for pred, axis in yolo_preds:\n        if len(pred['scores']) == 0:\n            x1, y1, x2, y2 = np.nan, np.nan, np.nan, np.nan\n        else:\n            x1, y1, x2, y2 = pred['boxes'][0]\n        if plane=='axial':\n            if axis=='axis2':\n                zs_min.append(y1)\n                zs_max.append(y2)\n                ys_min.append(x1)\n                ys_max.append(x2)\n            elif axis=='axis1':\n                zs_min.append(y1)\n                zs_max.append(y2)\n                xs_min.append(x1)\n                xs_max.append(x2)\n        elif plane=='sagittal':\n            if axis=='axis2':\n                zs_min.append(y1)\n                zs_max.append(y2)\n                ys_min.append(x1)\n                ys_max.append(x2)\n            elif axis=='axis0':\n                ys_min.append(y1)\n                ys_max.append(y2)\n                xs_min.append(x1)\n                xs_max.append(x2)\n        elif plane=='coronal':\n            if axis=='axis2':\n                zs_min.append(y1)\n                zs_max.append(y2)\n                ys_min.append(x1)\n                ys_max.append(x2)\n            elif axis=='axis0':\n                ys_min.append(y1)\n                ys_max.append(y2)\n                xs_min.append(x1)\n                xs_max.append(x2)\n    z_min_mean = np.nanmean(zs_min)\n    z_max_mean = np.nanmean(zs_max)\n    y_min_mean = np.nanmean(ys_min)\n    y_max_mean = np.nanmean(ys_max)\n    x_min_mean = np.nanmean(xs_min)\n    x_max_mean = np.nanmean(xs_max)\n    return {'x1_mm': x_min_mean,\n            'x2_mm': x_max_mean,\n            'y1_mm': y_min_mean,\n            'y2_mm': y_max_mean,\n            'z1_mm': z_min_mean,\n            'z2_mm': z_max_mean\n            }\n\n\ndef restore_coordinates_zyx(row) -> dict:\n    \"\"\"\n    Scale (Z, Y, X) from spacing = (1, 1, 1) to `new_spacing`.\n    If any error occurs (missing fields, invalid types, division by zero, etc.), return NaN.\n    \n    Expected keys: 'z_spacing', 'PixelSpacing', 'x1', 'x2', 'y1', 'y2', 'z1', 'z2'\n    \"\"\"\n    keys = ['x1', 'x2', 'y1', 'y2', 'z1', 'z2']\n    def na_series():\n        return {k: np.nan for k in keys}\n\n    try:\n        # 元の仮定: 旧座標は spacing=(1,1,1) ベース\n        sz = sy = sx = 1.0\n        # 目標の新しい spacing を取り出し\n        nsz = float(row['z_spacing'])\n\n        ps = row['PixelSpacing']\n        # PixelSpacing の頑健なパース（list/tuple/pydicom MultiValue/str）\n        if isinstance(ps, str):\n            s = ps.strip().strip('[]()')\n            parts = [p for p in s.replace(';', ',').split(',') if p.strip() != '']\n            if len(parts) < 2:\n                return na_series()\n            nsy = float(parts[0])\n            nsx = float(parts[1])\n        elif isinstance(ps, Sequence):\n            if len(ps) < 2:\n                return na_series()\n            nsy = float(ps[0])\n            nsx = float(ps[1])\n        else:\n            return na_series()\n\n        # 0 または 非有限値は無効\n        if not np.isfinite(nsz) or not np.isfinite(nsy) or not np.isfinite(nsx):\n            return na_series()\n        if nsz == 0.0 or nsy == 0.0 or nsx == 0.0:\n            return na_series()\n\n        # スケール係数\n        zz, zy, zx = sz / nsz, sy / nsy, sx / nsx\n        vals = {\n            'x1': float(row['x1_mm']) * zx,\n            'x2': float(row['x2_mm']) * zx,\n            'y1': float(row['y1_mm']) * zy,\n            'y2': float(row['y2_mm']) * zy,\n            'z1': float(row['z1_mm']) * zz,\n            'z2': float(row['z2_mm']) * zz,\n        } # 数値でない場合は except に落ちる\n        return vals\n        \n    except Exception:\n        return na_series()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-19T11:12:26.935268Z","iopub.execute_input":"2026-02-19T11:12:26.935615Z","iopub.status.idle":"2026-02-19T11:12:26.959607Z","shell.execute_reply.started":"2026-02-19T11:12:26.935595Z","shell.execute_reply":"2026-02-19T11:12:26.958892Z"}},"outputs":[],"execution_count":9},{"cell_type":"code","source":"class RSNAROIDatasetV1(Dataset):\n    \"\"\"\n    Dataset that extracts only the ROI (no patch re-cropping).\n    \n    - Input:   one case = one sample\n    - Output:  image/mask where the ROI is resized to `out_size_zyx`.\n    \n    Mask:\n    - Multiclass (one-hot channel format) corresponding to the target\n      (13 vascular sites + Aneurysm Present).\n    - Background is all-zero across channels.\n    - The Aneurysm Present channel is the **union** of all spatial site channels,\n      plus any annotations without a site specification.\n    \n    Additionally returns `mask_map`, a multiclass mask reduced to the feature-map size\n    `map_size_zyx` via any-pooling (logical OR), with shape (K, Z_map, H_map, W_map).\n    \n    Returned dict (main keys):\n      image:      FloatTensor (Z_out, H_out, W_out)                 # range: 0..1\n      mask:       FloatTensor (K, Z_out, H_out, W_out)              # multiclass one-hot (K=14; last = Aneurysm Present = union)\n      mask_map:   FloatTensor (K, Z_map, H_map, W_map) | None       # reduced via any-pooling\n    \n      # Coordinate/meta (ROI dataset → returns all coordinates)\n      roi_start_series_zyx / roi_end_series_* / roi_shape_vox_zyx / roi_size_mm_zyx\n      roi_center_roi_zyx / roi_center_series_zyx / roi_to_out_scale_zyx / out_size_zyx / map_size_zyx\n      lesion_centers_roi_zyx / lesion_centers_series_zyx / lesion_centers_norm_roi_zyx / lesion_centers_out_zyx\n    \n      target_site:           FloatTensor (14,)         # case-level label (after left-right flip)\n      target_aneurysm:       FloatTensor (1,)          # 1 if any drawing exists within the ROI; else 0\n      target_site_case:      FloatTensor (14,) | None\n      target_aneurysm_case:  FloatTensor (1,)\n    \n      # Compatibility outputs for grid indexing\n      grid_index_zyx = (0, 0, 0), grid_norm_*\n    \"\"\"\n\n    target_order = [\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    spatial_class_names: list[str] = target_order[:-1]\n    classes: list[str] = target_order\n    aneurysm_idx: int = len(target_order) - 1\n\n    _swap_pairs = [(0,1), (2,3), (4,5), (7,8), (9,10)]  # left - right（index）\n\n    def __init__(\n        self,\n        df: pd.Series,\n        volume: np.array,\n        mode: str,\n        volume_size_mm: tuple[float, float, float],   # Physical size for ROI (Z,Y,X) [mm]\n        out_size_zyx: tuple[int, int, int],           # Input size (Z,Y,X)\n        map_size_zyx: Optional[tuple[int, int, int]] = None,  # feature map size (Z,Y,X) for mask\n        transform: Optional[\"A.Compose\"] = None,\n        debug: bool = False,\n    ):\n        self.df = df\n        self.volume = volume\n        self.mode = mode\n        self.volume_size_mm = tuple(map(float, volume_size_mm))\n        self.out_size_zyx = out_size_zyx\n        self.map_size_zyx = map_size_zyx\n        self.transform = transform\n        self.debug = bool(debug)\n\n    def __len__(self) -> int:\n        return 1\n\n    # ---------- helpers ----------\n    @staticmethod\n    def _parse_pixel_spacing(v) -> tuple[float, float]:\n        if isinstance(v, (MultiValue, list, tuple, np.ndarray)) and len(v) >= 2:\n            return float(v[0]), float(v[1])\n        if isinstance(v, str):\n            s = v.replace(\"[\", \"\").replace(\"]\", \"\").replace(\"(\", \"\").replace(\")\", \"\").replace(\" \", \"\")\n            parts = s.split(\",\")\n            if len(parts) >= 2:\n                return float(parts[0]), float(parts[1])\n        return 0.5, 0.5\n\n    @staticmethod\n    def _pad_value_for_mod(mod: str, vmin_mr: float) -> float:\n        return -100.0 if str(mod).upper() in (\"CT\",\"CTA\",\"CCTA\") else float(vmin_mr)\n\n    @staticmethod\n    def crop_roi_mm(\n        vol: np.ndarray,               # (Z,H,W)\n        x_min, x_max, y_min, y_max, z_min, z_max,\n        z_sp: float, y_sp: float, x_sp: float,\n        size_mm_zyx: tuple[float, float, float],\n        pad_value: float | str = -1024,\n    ):\n        Z, H, W = vol.shape\n\n        def _center_or_mid(a, b, lim):\n            try:\n                fa, fb = float(a), float(b)\n                if math.isnan(fa) or math.isnan(fb):\n                    raise ValueError\n                return 0.5 * (fa + fb)\n            except Exception:\n                return lim / 2.0\n\n        # center（voxel, coordinates）\n        cx = int(round(_center_or_mid(x_min, x_max, W)))\n        cy = int(round(_center_or_mid(y_min, y_max, H)))\n        cz = int(round(_center_or_mid(z_min, z_max, Z)))\n        \n        # ROI half size [voxel]\n        z_mm, y_mm, x_mm = size_mm_zyx\n        hvz = max(1, int(round((z_mm / max(1e-6, z_sp)) / 2.0)))\n        hvy = max(1, int(round((y_mm / max(1e-6, y_sp)) / 2.0)))\n        hvx = max(1, int(round((x_mm / max(1e-6, x_sp)) / 2.0)))\n\n        z1, z2 = cz - hvz, cz + hvz\n        y1, y2 = cy - hvy, cy + hvy\n        x1, x2 = cx - hvx, cx + hvx\n\n        z1c, z2c = max(0, z1), min(Z, z2)\n        y1c, y2c = max(0, y1), min(H, y2)\n        x1c, x2c = max(0, x1), min(W, x2)\n\n        padv = float(vol.min()) if (pad_value == \"min\") else float(pad_value)\n        roi = np.empty((z2 - z1, y2 - y1, x2 - x1), dtype=vol.dtype)\n        roi.fill(padv)\n\n        dz0 = z1c - z1; dy0 = y1c - y1; dx0 = x1c - x1\n        sub = vol[..., x1c:x2c]\n        sub = sub[z1c:z2c, y1c:y2c]\n        roi[dz0:dz0+(z2c-z1c), dy0:dy0+(y2c-y1c), dx0:dx0+(x2c-x1c)] = sub\n\n        return roi, (int(z1), int(y1), int(x1))\n\n    @staticmethod\n    def _resize3d(vol_zyx: np.ndarray, out_zyx: tuple[int, int, int], mode: str) -> np.ndarray:\n        Z, H, W = vol_zyx.shape\n        Zo, Ho, Wo = out_zyx\n        zoom_factors = (Zo / max(1, Z), Ho / max(1, H), Wo / max(1, W))\n        order = 0 if mode == \"nearest\" else 1\n        return zoom(vol_zyx, zoom_factors, order=order)\n\n    @staticmethod\n    def _vl_normalize(arr: np.ndarray, v_low: float, v_high: float) -> np.ndarray:\n        if v_high <= v_low:\n            return np.zeros_like(arr, dtype=np.float32)\n        arr = np.clip(arr, v_low, v_high)\n        arr = (arr - v_low) / (v_high - v_low + 1e-6)\n        arr = (arr * 255).astype(np.uint8)  # 0..255 にしてから /255.\n        return arr\n\n    @staticmethod\n    def _pick_sop_value(loc_row: pd.Series) -> Optional[str]:\n        if \"SOPInstanceUID\" in loc_row.index and pd.notna(loc_row[\"SOPInstanceUID\"]):\n            return str(loc_row[\"SOPInstanceUID\"])\n        return None\n\n    def _site_index_from_loc(self, loc_row: pd.Series) -> Optional[int]:\n        loc_name = loc_row['location']\n        idx = self.target_order.index(loc_name)\n        return idx\n\n    @staticmethod\n    def _mask_to_map_any_multi(mask_kzyx: np.ndarray, map_zyx: tuple[int,int,int]) -> np.ndarray:\n        # mask: (K,Z,H,W) 0/1\n        t = torch.from_numpy(mask_kzyx.astype(np.float32))[None, ...]  # (1,K,Z,H,W)\n        pooled = F.adaptive_max_pool3d(t, output_size=map_zyx)         # (1,K,MZ,MH,MW)\n        return pooled.squeeze(0).numpy().astype(np.float32)\n\n    def _apply_albu_slicewise(\n        self,\n        img_zyx_u8: np.ndarray,                 # (Z, H, W) uint8\n    ):\n        \"\"\"\n        Albumentations on 2D slices; reuse the same params for all slices along Z (Compose/ReplayCompose).\n        \"\"\"\n        if self.transform is None:\n            return img_zyx_u8\n\n        t = self.transform\n        if not isinstance(t, A.ReplayCompose):\n            t = A.ReplayCompose(t.transforms)\n\n        Z, H, W = img_zyx_u8.shape\n        out_img = np.empty_like(img_zyx_u8)\n        \n        mid = Z // 2\n        base = t(image=img_zyx_u8[mid])\n        replay = base[\"replay\"]\n\n        for z in range(Z):\n            res = A.ReplayCompose.replay(\n                replay,\n                image=img_zyx_u8[z],\n            )\n            out_img[z] = res[\"image\"]\n        return out_img\n\n    def __getitem__(self, index: int) -> dict[str, Any]:\n        row = self.df\n        sid: str = str(row[\"SeriesInstanceUID\"])\n        sorted_files: Sequence[str] = row.get(\"sorted_files\", [])\n        mod = str(row[\"Modality\"])\n        plane = str(row[\"Plane\"])\n        ndim = str(row['ndim'])\n\n        vmin_mr = np.percentile(self.volume, 0)\n        vmax_mr = np.percentile(self.volume, 100)\n        # spacing\n        z_sp: float = float(row[\"z_spacing\"])\n        y_sp, x_sp = self._parse_pixel_spacing(row[\"PixelSpacing\"])\n        # load memmap\n        pad_val = self._pad_value_for_mod(mod, vmin_mr)\n        # print(z_sp, y_sp, x_sp, pad_val)\n\n        # ====== ROI ======\n        x_min, x_max = row[\"x1\"], row[\"x2\"]\n        y_min, y_max = row[\"y1\"], row[\"y2\"]\n        z_min, z_max = row[\"z1\"], row[\"z2\"]\n\n        roi, (z0, y0, x0) = self.crop_roi_mm(\n            self.volume,\n            x_min, x_max, y_min, y_max, z_min, z_max,\n            z_sp, y_sp, x_sp,\n            size_mm_zyx=self.volume_size_mm,\n            pad_value=pad_val,\n        )\n        Zr, Hr, Wr = roi.shape\n        \n        roi_center_roi = (int(Zr // 2), int(Hr // 2), int(Wr // 2))\n        roi_center_series = (int(roi_center_roi[0] + z0), int(roi_center_roi[1] + y0), int(roi_center_roi[2] + x0))\n        roi_size_mm = (\n            float(Zr * max(1e-6, z_sp)),\n            float(Hr * max(1e-6, y_sp)),\n            float(Wr * max(1e-6, x_sp)),\n        )\n\n        # ====== resize & nomaloize ======\n        Z_out, H_out, W_out = self.out_size_zyx\n        img_out = self._resize3d(roi.astype(np.float32, copy=False), (Z_out, H_out, W_out), mode=\"trilinear\")\n        if str(mod).upper() in (\"CT\",\"CTA\",\"CCTA\"):\n            img_out = self._vl_normalize(img_out, v_low=-100, v_high=600)\n        else:\n            img_out = self._vl_normalize(img_out, v_low=vmin_mr, v_high=vmax_mr)\n        if self.transform is not None:\n            img_aug_u8 = self._apply_albu_slicewise(\n                img_out.astype(np.uint8),                      # (Z,H,W)\n            )\n            img_out = img_aug_u8\n\n        img_out = img_out.astype(np.float32) / 255.0\n        out = {\n            \"image\": torch.from_numpy(img_out).float(),                       # (Z,H,W)\n            \"series_id\": sid,\n            \"spacing_zyx_mm\": (float(z_sp), float(y_sp), float(x_sp)),\n            \"out_size_zyx\": tuple(int(x) for x in self.out_size_zyx),\n            \"map_size_zyx\": tuple(int(x) for x in self.map_size_zyx) if self.map_size_zyx is not None else None,\n            \"plane\": plane,\n            \n        }\n        return out","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-19T11:12:26.960723Z","iopub.execute_input":"2026-02-19T11:12:26.960966Z","iopub.status.idle":"2026-02-19T11:12:26.993394Z","shell.execute_reply.started":"2026-02-19T11:12:26.960947Z","shell.execute_reply":"2026-02-19T11:12:26.992513Z"}},"outputs":[],"execution_count":10},{"cell_type":"markdown","source":"### Model","metadata":{}},{"cell_type":"code","source":"class Aneurysm3DModelV2(nn.Module):\n    def __init__(\n        self,\n        backbone: str,\n        in_chans: int,\n        num_classes: int,\n        drop_rate: float = 0.0,\n        map_size: tuple = (4,4,4),\n        pretrained: bool = True,\n        use_coords: bool = False,\n        return_logits: bool = True,\n        multilabel: bool = False,\n    ):\n        super().__init__()\n        self.in_chans = in_chans\n        self.num_classes = num_classes\n        self.drop_rate = drop_rate\n        self.map_size = map_size\n        self.use_coords = use_coords\n        self.return_logits = return_logits\n        self.multilabel = multilabel\n\n        self.backbone = timm_3d.create_model(\n            backbone,\n            in_chans=in_chans,\n            pretrained=pretrained,\n            num_classes=0,\n            global_pool=''\n            )\n        self.backbone.conv1 = nn.Conv3d(1, 64, kernel_size=(7, 7, 7), stride=(1, 1, 1), padding=(3, 3, 3), bias=False)\n\n        encoder_channels = self.backbone.num_features\n        \n        if use_coords:\n            self.coords = self._build_coords(map_size[0], map_size[1], map_size[2])\n            encoder_channels += 3\n\n        dim_map = map_size[0] * map_size[1] * map_size[2]\n        self.mask_head = nn.Sequential(\n            nn.Dropout(drop_rate),\n            nn.Linear(encoder_channels, num_classes),\n        )\n        self.class_head = nn.Sequential(\n            nn.Dropout(drop_rate),\n            nn.Linear(dim_map, num_classes),\n        )\n\n    @staticmethod\n    def _build_coords(D, H, W, center=True, dtype=torch.float32, device=None):\n        if center:\n            z = (torch.arange(D, device=device, dtype=dtype) + 0.5) / D\n            y = (torch.arange(H, device=device, dtype=dtype) + 0.5) / H\n            x = (torch.arange(W, device=device, dtype=dtype) + 0.5) / W\n        else:\n            z = torch.linspace(0, 1, D, device=device, dtype=dtype)\n            y = torch.linspace(0, 1, H, device=device, dtype=dtype)\n            x = torch.linspace(0, 1, W, device=device, dtype=dtype)\n        zz, yy, xx = torch.meshgrid(z, y, x, indexing=\"ij\")\n        return torch.stack((xx, yy, zz), dim=0)  # (3, D, H, W)\n\n    def _ensure_5d(self, x: torch.Tensor) -> torch.Tensor:\n        if x.dim() == 4:\n            # (B, D, H, W) -> (B, 1, D, H, W)\n            x = x.unsqueeze(1)\n        if x.dim() != 5:\n            raise ValueError(f\"Expected 5D input (B,C,D,H,W), got shape {tuple(x.shape)}\")\n        return x\n\n    def forward(self, x: torch.Tensor) -> dict[str, torch.Tensor]:\n        \"\"\"\n        Args:\n            x: (B, C, D, H, W) or (B, D, H, W)\n        Returns:\n            {\n                'output': (B, out_chans) # logits \n            }\n        \"\"\"\n        x = self._ensure_5d(x)\n        B = x.shape[0]\n\n        features = self.backbone(x)\n        # bottleneck features\n        #print('bottleneck_features:', bottleneck_features.shape)\n        # (B, 1, D, H, W)\n        if self.use_coords:\n            self.coords = self.coords.to(features.device)\n            coords = self.coords.unsqueeze(0).expand(B, -1, -1, -1, -1)\n            features = torch.cat([features, coords], dim= 1)\n \n        B, C, Dm, Hm, Wm = features.shape\n        N = Dm*Hm*Wm\n        features = features.view(B, C, N)\n        features = features.transpose(1,2)\n        features = features.reshape(B*N, C)\n        logits = self.mask_head(features)\n        logits = logits.view(B, N, -1)\n        return {\"output\": logits}\n\nclass Aneurysm3DModelV3(nn.Module):\n    \n    def __init__(\n        self,\n        backbone: str,\n        in_chans: int,\n        num_classes: int,\n        drop_rate: float = 0.0,\n        map_size: tuple = (4,4,4),\n        pretrained: bool = True,\n        use_coords: bool = False,\n        return_logits: bool = True,\n        multilabel: bool = False,\n    ):\n        super().__init__()\n        self.in_chans = in_chans\n        self.num_classes = num_classes\n        self.drop_rate = drop_rate\n        self.map_size = map_size\n        self.use_coords = use_coords\n        self.return_logits = return_logits\n        self.multilabel = multilabel\n\n        self.backbone = timm_3d.create_model(\n            backbone,\n            in_chans=in_chans,\n            pretrained=pretrained,\n            num_classes=0,\n            global_pool=''\n            )\n        self.backbone.conv1 = nn.Conv3d(1, 64, kernel_size=(7, 7, 7), stride=(1, 1, 1), padding=(3, 3, 3), bias=False)\n        self.backbone.layer2[0].conv1.stride = (1,1,1)\n        self.backbone.layer2[0].downsample[0].stride = (1,1,1)\n\n        encoder_channels = self.backbone.num_features\n        \n        if use_coords:\n            self.coords = self._build_coords(map_size[0], map_size[1], map_size[2])\n            encoder_channels += 3\n\n        dim_map = map_size[0] * map_size[1] * map_size[2]\n        self.mask_head = nn.Sequential(\n            nn.Dropout(drop_rate),\n            nn.Linear(encoder_channels, num_classes),\n        )\n        self.class_head = nn.Sequential(\n            nn.Dropout(drop_rate),\n            nn.Linear(dim_map, num_classes),\n        )\n\n    @staticmethod\n    def _build_coords(D, H, W, center=True, dtype=torch.float32, device=None):\n        if center:\n            z = (torch.arange(D, device=device, dtype=dtype) + 0.5) / D\n            y = (torch.arange(H, device=device, dtype=dtype) + 0.5) / H\n            x = (torch.arange(W, device=device, dtype=dtype) + 0.5) / W\n        else:\n            z = torch.linspace(0, 1, D, device=device, dtype=dtype)\n            y = torch.linspace(0, 1, H, device=device, dtype=dtype)\n            x = torch.linspace(0, 1, W, device=device, dtype=dtype)\n        zz, yy, xx = torch.meshgrid(z, y, x, indexing=\"ij\")\n        return torch.stack((xx, yy, zz), dim=0)  # (3, D, H, W)\n\n    def _ensure_5d(self, x: torch.Tensor) -> torch.Tensor:\n        if x.dim() == 4:\n            # (B, D, H, W) -> (B, 1, D, H, W)\n            x = x.unsqueeze(1)\n        if x.dim() != 5:\n            raise ValueError(f\"Expected 5D input (B,C,D,H,W), got shape {tuple(x.shape)}\")\n        return x\n\n    def forward(self, x: torch.Tensor) -> dict[str, torch.Tensor]:\n        \"\"\"\n        Args:\n            x: (B, C, D, H, W) or (B, D, H, W)\n        Returns:\n            {\n                'output': (B, out_chans) # logits \n            }\n        \"\"\"\n        x = self._ensure_5d(x)\n        B = x.shape[0]\n\n        features = self.backbone(x)\n        # bottleneck features\n        #print('bottleneck_features:', bottleneck_features.shape)\n        # (B, 1, D, H, W)\n        if self.use_coords:\n            self.coords = self.coords.to(features.device)\n            coords = self.coords.unsqueeze(0).expand(B, -1, -1, -1, -1)\n            features = torch.cat([features, coords], dim= 1)\n \n        B, C, Dm, Hm, Wm = features.shape\n        N = Dm*Hm*Wm\n        features = features.view(B, C, N)\n        features = features.transpose(1,2)\n        features = features.reshape(B*N, C)\n        logits = self.mask_head(features)\n        logits = logits.view(B, N, -1)\n        return {\"output\": logits}\n\nclass Aneurysm3DModelV7(nn.Module):\n    \n    def __init__(\n        self,\n        backbone: str,\n        in_chans: int,\n        num_classes: int,\n        drop_rate: float = 0.0,\n        map_size: tuple = (4,4,4),\n        pretrained: bool = True,\n        use_coords: bool = False,\n        return_logits: bool = True,\n        multilabel: bool = False,\n    ):\n        super().__init__()\n        self.in_chans = in_chans\n        self.num_classes = num_classes\n        self.drop_rate = drop_rate\n        self.map_size = map_size\n        self.use_coords = use_coords\n        self.return_logits = return_logits\n        self.multilabel = multilabel\n\n        # --- 3D U-Net ---\n        self.backbone = timm_3d.create_model(\n            backbone,\n            in_chans=in_chans,\n            pretrained=pretrained,\n            num_classes=0,\n            global_pool=''\n            )\n        if 'resnet' in backbone:\n            self.backbone.conv1 = nn.Conv3d(1, 64, kernel_size=(7, 7, 7), stride=(1, 1, 1), padding=(3, 3, 3), bias=False)\n            self.backbone.layer2[0].conv1.stride = (1,1,1)\n            self.backbone.layer2[0].downsample[0].stride = (1,1,1)\n            self.backbone.layer4[0].conv1.stride = (1,1,1)\n            self.backbone.layer4[0].downsample[0].stride = (1,1,1)\n        elif 'convnext' in backbone:\n            self.backbone.stem[0] = nn.Conv3d(1, 96, kernel_size=(4, 4, 4), stride=(1, 1, 1))\n        else:\n            raise ValueError(f'{backbone} is invalid.')\n        \n        encoder_channels = self.backbone.num_features\n        \n        if use_coords:\n            self.coords = self._build_coords(map_size[0], map_size[1], map_size[2])\n            encoder_channels += 3\n\n        dim_map = map_size[0] * map_size[1] * map_size[2]\n        self.mask_head = nn.Sequential(\n            nn.Dropout(drop_rate),\n            nn.Linear(encoder_channels, num_classes),\n        )\n        self.class_head = nn.Sequential(\n            nn.Dropout(drop_rate),\n            nn.Linear(dim_map, num_classes),\n        )\n\n    @staticmethod\n    def _build_coords(D, H, W, center=True, dtype=torch.float32, device=None):\n        if center:\n            z = (torch.arange(D, device=device, dtype=dtype) + 0.5) / D\n            y = (torch.arange(H, device=device, dtype=dtype) + 0.5) / H\n            x = (torch.arange(W, device=device, dtype=dtype) + 0.5) / W\n        else:\n            z = torch.linspace(0, 1, D, device=device, dtype=dtype)\n            y = torch.linspace(0, 1, H, device=device, dtype=dtype)\n            x = torch.linspace(0, 1, W, device=device, dtype=dtype)\n        zz, yy, xx = torch.meshgrid(z, y, x, indexing=\"ij\")\n        return torch.stack((xx, yy, zz), dim=0)  # (3, D, H, W)\n\n    def _ensure_5d(self, x: torch.Tensor) -> torch.Tensor:\n        if x.dim() == 4:\n            # (B, D, H, W) -> (B, 1, D, H, W)\n            x = x.unsqueeze(1)\n        if x.dim() != 5:\n            raise ValueError(f\"Expected 5D input (B,C,D,H,W), got shape {tuple(x.shape)}\")\n        return x\n\n    def forward(self, x: torch.Tensor) -> dict[str, torch.Tensor]:\n        \"\"\"\n        Args:\n            x: (B, C, D, H, W) or (B, D, H, W)\n        Returns:\n            {\n                'output': (B, out_chans) # logits \n            }\n        \"\"\"\n        x = self._ensure_5d(x)\n        B = x.shape[0]\n\n        features = self.backbone(x)\n        if self.use_coords:\n            self.coords = self.coords.to(features.device)\n            coords = self.coords.unsqueeze(0).expand(B, -1, -1, -1, -1)\n            features = torch.cat([features, coords], dim= 1)\n \n        B, C, Dm, Hm, Wm = features.shape\n        N = Dm*Hm*Wm\n        features = features.view(B, C, N)\n        features = features.transpose(1,2)\n        features = features.reshape(B*N, C)\n        logits = self.mask_head(features)\n        logits = logits.view(B, N, -1)\n        return {\"output\": logits}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-19T11:12:26.994542Z","iopub.execute_input":"2026-02-19T11:12:26.994895Z","iopub.status.idle":"2026-02-19T11:12:27.032373Z","shell.execute_reply.started":"2026-02-19T11:12:26.994869Z","shell.execute_reply":"2026-02-19T11:12:27.031389Z"}},"outputs":[],"execution_count":11},{"cell_type":"code","source":"class CFG:\n    root = '/kaggle/input/rsna-intracranial-aneurysm-detection/series'\n    backbone = 'resnet18'\n    in_chans = 1\n    num_classes = 14\n    drop_rate = 0.2\n    map_size = (25,25,25)\n    pretrained = False\n    use_coords = True","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-19T11:12:27.033576Z","iopub.execute_input":"2026-02-19T11:12:27.033944Z","iopub.status.idle":"2026-02-19T11:12:27.054215Z","shell.execute_reply.started":"2026-02-19T11:12:27.033889Z","shell.execute_reply":"2026-02-19T11:12:27.05351Z"}},"outputs":[],"execution_count":12},{"cell_type":"code","source":"import sys, types\nsys.path.append('/kaggle/input/rsna-config')\ntry:\n    import config_roi as user_cfg\nexcept Exception:\n    user_cfg = None\n\ndef load_state_dict_safe(path):\n    \n    if 'classification' not in sys.modules:\n        sys.modules['classification'] = types.ModuleType('classification')\n    if 'classification.config' not in sys.modules:\n        sys.modules['classification.config'] = types.ModuleType('classification.config')\n    cfg_mod = sys.modules.get('classification.config.config_roi')\n    if cfg_mod is None:\n        cfg_mod = types.ModuleType('classification.config.config_roi')\n        sys.modules['classification.config.config_roi'] = cfg_mod\n    \n    Real = None\n    for name in ('DataConfig', 'ModelConfig', 'Config', 'LossConfig'):\n        if user_cfg is not None and hasattr(user_cfg, name):\n            Real = getattr(user_cfg, name)\n            break\n    if Real is None:\n        class Real:\n            pass\n    \n    setattr(cfg_mod, 'DataConfig', Real)\n    setattr(cfg_mod, 'LossConfig', Real)\n    setattr(cfg_mod, 'ModelConfig', Real)\n    setattr(cfg_mod, 'TrainingConfig', Real)\n    setattr(cfg_mod, 'OptimizerConfig', Real)\n    setattr(cfg_mod, 'SchedulerConfig', Real)\n    setattr(cfg_mod, 'AugmentationConfig', Real)\n    \n    from torch.serialization import safe_globals\n    import torch\n    \n    with safe_globals([cfg_mod.DataConfig]):\n        ckpt = torch.load(path, map_location='cpu', weights_only=False)\n    \n    if isinstance(ckpt, dict):\n        sd = ckpt.get('model_state_dict') or ckpt.get('state_dict')\n        if sd is None and all(isinstance(k, str) for k in ckpt.keys()):\n            sd = ckpt  # state_dictそのもの\n    else:\n        sd = None\n    \n    if sd is None:\n        raise RuntimeError('state_dict not found.')\n    return sd\n\n\n# 使い方\n# model = MyModel(...)\n# model.load_state_dict(sd, strict=False)\n# model.eval()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-19T11:12:27.055242Z","iopub.execute_input":"2026-02-19T11:12:27.055571Z","iopub.status.idle":"2026-02-19T11:12:27.101545Z","shell.execute_reply.started":"2026-02-19T11:12:27.055549Z","shell.execute_reply":"2026-02-19T11:12:27.100826Z"}},"outputs":[],"execution_count":13},{"cell_type":"code","source":"### spacing 予測のための function ###\n\nimport torch\nimport numpy as np\nimport cv2\nfrom albumentations import Compose, PadIfNeeded, CenterCrop, Normalize\nfrom albumentations.pytorch import ToTensorV2\n\n@torch.no_grad()\ndef predict_space(volume: np.ndarray, MODELS_SPACE, record: dict, size: int = 512) -> tuple[float, float, float]:\n    \"\"\"\n    volume: (Z,H,W)\n    MODELS_SPACE: list[nn.Module]； model output: (N, 3) regression — x,y,z in mm\n    size: PadIfNeeded→CenterCrop（no resize）\n    return: (x_mm, y_mm, z_mm)\n    \"\"\"\n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\n    # ---- p1-p99 clip → uint8 ----\n    def _percentile_clip_to_uint8(vol: np.ndarray, p_low=1.0, p_high=99.0) -> np.ndarray:\n        v = vol.astype(np.float32, copy=False)\n        finite = np.isfinite(v)\n        if not finite.any():\n            raise ValueError(\"Volume contains no finite values.\")\n        lo, hi = np.percentile(v[finite], [p_low, p_high])\n        if not np.isfinite(lo) or not np.isfinite(hi) or hi <= lo:\n            lo, hi = np.nanmin(v), np.nanmax(v)\n            if not np.isfinite(lo) or not np.isfinite(hi) or hi <= lo:\n                lo, hi = 0.0, 1.0\n        v = np.clip((v - lo) / (hi - lo), 0.0, 1.0)\n        return (v * 255.0).round().astype(np.uint8)\n\n    def _compute_10_of_12_indices(num_slices: int) -> list[int]:\n        if num_slices <= 0:\n            return []\n        if num_slices == 1:\n            return [0] * 10\n        idx12 = np.round(np.linspace(0, num_slices - 1, 12)).astype(int)\n        idx10 = idx12[1:11]  # take the central 10 slices\n        idx10 = np.clip(idx10, 0, num_slices - 1)\n        # if dedup < 10, evenly distribute to reach 10\n        if len(np.unique(idx10)) < 10 and num_slices >= 10:\n            fallback = np.round(np.linspace(0, num_slices - 1, 10)).astype(int)\n            idx10 = np.unique(np.concatenate([idx10, fallback]))\n            # Dedup can exceed 10 → sort and keep 10\n            idx10 = np.sort(idx10)[:10]\n        return idx10.tolist()\n\n    vol_u8 = _percentile_clip_to_uint8(volume, 1.0, 99.0)\n    Z = volume.shape[0]\n    z_indices = _compute_10_of_12_indices(Z)\n\n    # ---- Albumentations: PadIfNeeded→CenterCrop→Normalize→ToTensorV2 ----\n    val_tf = Compose([\n        PadIfNeeded(min_height=size, min_width=size, border_mode=0, value=0),\n        CenterCrop(height=size, width=size),\n        Normalize(mean=[0.485, 0.456, 0.406],\n                  std=[0.229, 0.224, 0.225]),\n        ToTensorV2(),\n    ])\n\n    imgs_t = []\n    for zc in z_indices:\n        zp = int(np.clip(zc - 1, 0, Z - 1))\n        zn = int(np.clip(zc + 1, 0, Z - 1))\n        prev = vol_u8[zp]\n        curr = vol_u8[zc]\n        next_ = vol_u8[zn]\n        img_3ch = np.stack([prev, curr, next_], axis=-1)  # (H,W,3)\n        augmented = val_tf(image=img_3ch)\n        img_t = augmented['image']  # (3, size, size) float32\n        imgs_t.append(img_t)\n\n    if len(imgs_t) == 0:\n        # フォールバック\n        try:\n            y_mm, x_mm = _parse_pixel_spacing(record[\"PixelSpacing\"])\n            z_mm = float(record[\"z_spacing\"])\n            return float(x_mm), float(y_mm), float(z_mm)\n        except Exception:\n            return 0.5, 0.5, 0.5\n\n    batch = torch.stack(imgs_t, dim=0).to(device)  # (N=10, 3, size, size)\n\n    preds_all = []\n    for model in MODELS_SPACE:\n        model.eval()\n        out = model(batch)  # (N=10, 3)\n        preds_all.append(out.detach().float())\n\n    preds = torch.stack(preds_all, dim=0).mean(dim=0).mean(dim=0)\n    x_mm, y_mm, z_mm = preds.tolist()\n\n    return float(x_mm), float(y_mm), float(z_mm)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-19T11:12:27.10249Z","iopub.execute_input":"2026-02-19T11:12:27.102749Z","iopub.status.idle":"2026-02-19T11:12:27.119024Z","shell.execute_reply.started":"2026-02-19T11:12:27.102729Z","shell.execute_reply":"2026-02-19T11:12:27.118282Z"}},"outputs":[],"execution_count":14},{"cell_type":"code","source":"import math, numpy as np, torch\nimport torch.nn.functional as F\n\ndef _parse_pixel_spacing(v) -> tuple[float, float]:\n    if isinstance(v, (MultiValue, list, tuple, np.ndarray)) and len(v) >= 2:\n        return float(v[0]), float(v[1])\n    if isinstance(v, str):\n        s = v.replace(\"[\", \"\").replace(\"]\", \"\").replace(\"(\", \"\").replace(\")\", \"\").replace(\" \", \"\")\n        parts = s.split(\",\")\n        if len(parts) >= 2:\n            return float(parts[0]), float(parts[1])\n    return 0.5, 0.5\n    \ndef _center_or_mid(a, b, lim):\n    try:\n        fa, fb = float(a), float(b)\n        if math.isnan(fa) or math.isnan(fb): raise ValueError\n        return 0.5 * (fa + fb)\n    except Exception:\n        return lim / 2.0\n\ndef _vox_half(size_mm_zyx, sp_zyx):\n    z_mm, y_mm, x_mm = map(float, size_mm_zyx)\n    z_sp, y_sp, x_sp = map(float, sp_zyx)\n    hvz = max(1, int(round((z_mm / max(1e-6, z_sp)) / 2.0)))\n    hvy = max(1, int(round((y_mm / max(1e-6, y_sp)) / 2.0)))\n    hvx = max(1, int(round((x_mm / max(1e-6, x_sp)) / 2.0)))\n    return hvz, hvy, hvx\n\ndef _crop_center(vol, czcycx, half_zyx, pad):\n    Z,H,W = vol.shape\n    cz,cy,cx = map(int, czcycx)\n    hvz,hvy,hvx = half_zyx\n    z1,z2 = cz-hvz, cz+hvz\n    y1,y2 = cy-hvy, cy+hvy\n    x1,x2 = cx-hvx, cx+hvx\n    z1c,z2c = max(0,z1), min(Z,z2)\n    y1c,y2c = max(0,y1), min(H,y2)\n    x1c,x2c = max(0,x1), min(W,x2)\n    roi = np.empty((z2-z1, y2-y1, x2-x1), dtype=vol.dtype)\n    roi.fill(float(pad))\n    roi[(z1c-z1):(z1c-z1)+(z2c-z1c),\n        (y1c-y1):(y1c-y1)+(y2c-y1c),\n        (x1c-x1):(x1c-x1)+(x2c-x1c)] = vol[z1c:z2c, y1c:y2c, x1c:x2c]\n    return roi, (z1,y1,x1)  # origin in series-voxel\n\ndef _resize3d(np_zyx: np.ndarray, out_zyx, mode=\"trilinear\"):\n    t = torch.from_numpy(np_zyx[None,None].astype(np.float32))\n    out = F.interpolate(t, size=tuple(map(int,out_zyx)), mode=mode, align_corners=False if mode!=\"nearest\" else None)\n    return out[0,0].contiguous().cpu().numpy()\n\nclass ROIExtractor:\n    def __init__(self, volume, spacing_zyx, center_series_zyx, modality, max_size_mm, vmin=None, vmax=None):\n        self.vol = volume.astype(np.float32, copy=False)   # (Z,H,W)\n        self.sp = tuple(map(float, spacing_zyx))           # (z,y,x)\n        self.center = tuple(map(int, center_series_zyx))\n        self.mod = str(modality).upper()\n        self.vmin = float(np.percentile(self.vol, 0.0)) if vmin is None else float(vmin)\n        self.vmax = float(np.percentile(self.vol, 100.0)) if vmax is None else float(vmax)\n\n        hv_max = _vox_half(max_size_mm, self.sp)\n        pad = -100.0 if self.mod in (\"CT\",\"CTA\",\"CCTA\") else self.vmin\n        self.big_roi, self.big_origin_series = _crop_center(self.vol, self.center, hv_max, pad)\n\n        self._cache_u8 = {}  # {(z_mm,y_mm,x_mm): np.ndarray[u8, (Z,H,W)]}\n\n    def _norm_u8(self, arr):\n        if self.mod in (\"CT\",\"CTA\",\"CCTA\"):\n            v_low, v_high = -100, 600  # 必要に応じて変更\n        else:\n            v_low, v_high = self.vmin, self.vmax\n        arr = np.clip(arr, v_low, v_high)\n        arr = (arr - v_low) / (v_high - v_low + 1e-6)\n        return (arr * 255).astype(np.uint8)\n\n    def get_roi_u8(self, size_mm_zyx):\n        key = tuple(map(float, size_mm_zyx))\n        if key in self._cache_u8:\n            return self._cache_u8[key]\n\n        Zb, Hb, Wb = self.big_roi.shape\n        c_big = (Zb//2, Hb//2, Wb//2)\n        hv = _vox_half(size_mm_zyx, self.sp)\n        roi, _ = _crop_center(self.big_roi, c_big, hv, pad=self.big_roi.min())\n        u8 = self._norm_u8(roi)\n        self._cache_u8[key] = u8\n        return u8\n\n    def resize_from_roi_u8(self, roi_u8, out_size_zyx, mode=\"trilinear\"):\n        f = roi_u8.astype(np.float32)/255.0\n        return _resize3d(f, out_size_zyx, mode=mode)\n\n    def get(self, size_mm_zyx, out_size_zyx, mode=\"trilinear\"):\n        Zb,Hb,Wb = self.big_roi.shape\n        c_big = (Zb//2, Hb//2, Wb//2)\n        hv = _vox_half(size_mm_zyx, self.sp)\n        roi, off_in_big = _crop_center(self.big_roi, c_big, hv, pad=self.big_roi.min())\n\n        # Normalize→[0,1]→resize\n        u8 = self._norm_u8(roi)\n        f = u8.astype(np.float32)/255.0\n        f_resized = _resize3d(f, out_size_zyx, mode=mode)  # (Z_out,H_out,W_out) float32 [0,1]\n\n        # Coordinates transform if needed：output↔series\n        origin_series = tuple(self.big_origin_series[i] + off_in_big[i] for i in range(3))\n        scale_out_per_series = (\n            f_resized.shape[0] / max(1,(2*hv[0])),\n            f_resized.shape[1] / max(1,(2*hv[1])),\n            f_resized.shape[2] / max(1,(2*hv[2])),\n        )\n        meta = {\n            \"origin_series_zyx\": origin_series,          # 出力(0,0,0)がシリーズ何ボクセル目か\n            \"scale_out_per_series\": scale_out_per_series # output coords = (series - origin) * scale\n        }\n        return f_resized, meta\n\n\n@torch.inference_mode()\ndef infer_model_tensor(img_tensor, model):\n    \"\"\"\n    img_tensor: (1, Z, H, W)  ※従来の ds[0]['image'].unsqueeze(0) と同じshape\n    \"\"\"\n    img = img_tensor.to(device)\n    with torch.cuda.amp.autocast(dtype=torch.float16):\n        out = model(img)['output'][0]\n    # モデル間平均 → sigmoid → numpy\n    # outs = torch.stack(outs, dim=0).mean(dim=0)\n    out = torch.sigmoid(out).cpu().numpy()\n    return out\n\n\ndef aggregate_prediction(prediction, top_N=2):\n    vals = prediction[:, -1]            # (N,)\n    xs = np.sort(prediction, axis=0)[::-1, :]  # (N, C) in descending order\n    cs = np.cumsum(xs, axis=0)\n    cs = cs[top_N - 1, :] / float(top_N)\n    return cs","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-19T11:12:27.120156Z","iopub.execute_input":"2026-02-19T11:12:27.120819Z","iopub.status.idle":"2026-02-19T11:12:27.147845Z","shell.execute_reply.started":"2026-02-19T11:12:27.120793Z","shell.execute_reply":"2026-02-19T11:12:27.147145Z"}},"outputs":[],"execution_count":15},{"cell_type":"code","source":"def load_volume_metadata(series_path):\n    series_path = Path(series_path)\n    dcm = DicomVolumeFiles(\n                series_path,\n                plane=\"auto\",\n                debug=False,\n                require_spacing=False,\n                default_xy=(0, 0),\n                default_z=0\n    )\n    # vol = dcm.load_pixel_array(apply_rescale=False,\n    #                            return_reverse=False,\n    #                            reject_non_mono=False,  # 非MONO許可\n    #                            skip_derived=False,     # DERIVED/MPRも許可\n    #                           )\n    vol = dcm.load_pixel_array_parallel(apply_rescale=False,\n                                        return_reverse=False,\n                                        reject_non_mono=False,\n                                        skip_derived=False,\n                                        max_workers=2,\n                                        use_threads=True\n                                       )\n    # vol = None\n    num_slices = len(dcm.file_paths)\n    if num_slices==1:\n        ndim = 3\n    else:\n        ndim =  2   \n    row_dict = {\n        \"SeriesInstanceUID\": series_path.name,\n        \"num_slice\"      : num_slices,\n        \"ndim\"   : ndim,\n        \"PixelSpacing\": dcm.PixelSpacingEffective,\n        \"z_spacing\": dcm.z_spacing,\n        \"sorted_files\": dcm.file_paths,\n        \"slice_positions\": dcm.slice_positions,\n        \"Modality\": dcm.Modality,\n        \"Plane\": dcm.plane\n    }\n    return vol, row_dict\n\ndef get_roi_from_yolo(vol, row_dict) -> dict:\n    pixel_spacing = (row_dict['z_spacing'], row_dict['PixelSpacing'][0], row_dict['PixelSpacing'][1])\n    modality = row_dict['Modality']\n    plane = row_dict['Plane']\n    det_data_0, det_data_1 = get_mid_slice(vol, plane, modality, pixel_spacing)\n    yolo_preds = []\n    for det_data, axis in [det_data_0, det_data_1]:\n        img = det_data\n        pred = predict_ensemble_single_image(\n                    MODELS_YOLO, cv2.cvtColor(img, cv2.COLOR_GRAY2RGB),\n                    conf=0.2, iou=0.7, imgsz=320,\n                    per_model_max_det=1,\n                    ensemble_iou=0.55,\n                    only_one=True,\n                )\n        yolo_preds.append((pred, axis))\n    coords_mm = bbox_coords(yolo_preds, plane)\n    row_dict.update(**coords_mm)\n    coords = restore_coordinates_zyx(row_dict)\n    row_dict.update(**coords)\n    return row_dict\n\ndef predict_one_volume(volume, models, row_dict, volume_size_mm,\n                       out_size_zyx, map_size_zyx, top_ns,\n                       roi_extractor: ROIExtractor | None = None\n                      ) -> np.ndarray:\n    \"\"\"\n    roi_extractor を渡せば big_roi から再クロップ＋リサイズ（推奨）。\n    渡さない場合は、従来どおり volume_size_mm で単発クロップ→リサイズ。\n    \"\"\"\n    df = pd.Series(row_dict)\n\n    if roi_extractor is None:\n        z_sp = float(df[\"z_spacing\"])\n        from_yx = RSNAROIDatasetV1._parse_pixel_spacing\n        y_sp, x_sp = from_yx(df[\"PixelSpacing\"])\n        Z,H,W = volume.shape\n        center = (\n            int(round(_center_or_mid(df[\"z1\"], df[\"z2\"], Z))),\n            int(round(_center_or_mid(df[\"y1\"], df[\"y2\"], H))),\n            int(round(_center_or_mid(df[\"x1\"], df[\"x2\"], W))),\n        )\n        pad = -100.0 if str(df[\"Modality\"]).upper() in (\"CT\",\"CTA\",\"CCTA\") else float(np.percentile(volume, 0.0))\n        hv = _vox_half(volume_size_mm, (z_sp, y_sp, x_sp))\n        roi_np, _ = _crop_center(volume.astype(np.float32, copy=False), center, hv, pad)\n        if str(df[\"Modality\"]).upper() in (\"CT\",\"CTA\",\"CCTA\"):\n            v_low, v_high = -100, 600\n        else:\n            v_low, v_high = float(np.percentile(volume, 0.0)), float(np.percentile(volume, 100.0))\n        roi_np = np.clip(roi_np, v_low, v_high)\n        roi_np = (roi_np - v_low) / (v_high - v_low + 1e-6)\n        img_np = _resize3d(roi_np.astype(np.float32), out_size_zyx, mode=\"trilinear\")\n    else:\n        img_np = roi_extractor.resize_from_roi_u8(volume, out_size_zyx)\n\n    img_tensor = torch.from_numpy(img_np).float().unsqueeze(0)\n    assert len(models)==len(top_ns)\n\n    outs_agg = []\n    for model, top_n in zip(models, top_ns):\n        out = infer_model_tensor(img_tensor, model)\n        # print(outs.shape)\n        out_agg = aggregate_prediction(out, top_N=top_n)\n        outs_agg.append(out_agg)\n    outs_agg = np.mean(outs_agg, axis=0)\n    return outs_agg\n\n    \nMODELS_YOLO = []\nMODELS_0 = []\nMODELS_1 = []\nMODELS_2 = []\nMODELS_3 = []\nMODELS_4 = []\nMODELS_SPACE = []\n\ndef load_models_yolo():\n    global MODELS_YOLO\n    model_paths = ['/kaggle/input/rsna-yolo-models/yolo_cv_rot90/runs/fold0/weights/best.pt',\n                   '/kaggle/input/rsna-yolo-models/yolo_cv_rot90/runs/fold1/weights/best.pt',\n                   '/kaggle/input/rsna-yolo-models/yolo_cv_rot90/runs/fold2/weights/best.pt',\n                   '/kaggle/input/rsna-yolo-models/yolo_cv_rot90/runs/fold3/weights/best.pt',\n                   '/kaggle/input/rsna-yolo-models/yolo_cv_rot90/runs/fold4/weights/best.pt',\n                   '/kaggle/input/rsna-yolo-models-v3/yolo_cv_rot90/runs/fold0/weights/best.pt',\n                   '/kaggle/input/rsna-yolo-models-v3/yolo_cv_rot90/runs/fold1/weights/best.pt',\n                   '/kaggle/input/rsna-yolo-models-v3/yolo_cv_rot90/runs/fold2/weights/best.pt',\n                   '/kaggle/input/rsna-yolo-models-v3/yolo_cv_rot90/runs/fold3/weights/best.pt',\n                   '/kaggle/input/rsna-yolo-models-v3/yolo_cv_rot90/runs/fold4/weights/best.pt',\n                   ]\n    MODELS_YOLO = []\n    for mp in model_paths:\n        model = YOLO(mp)\n        MODELS_YOLO.append(model)\n    \ndef load_models():\n    \n    global MODELS_0\n    model_types = ['v3', 'v3']\n    model_paramss = [{'backbone': 'resnet18',\n                    'map_size': (25,25,25), # Size after encoding\n                    'use_coords': True,\n                    'volume_size_mm': (90, 90, 90), # Physical size of the ROI (Z, Y, X) [mm]\n                    'out_size_zyx': (196, 196, 196), # Input size (Z,Y,X)\n                    'map_size_zyx': (25, 25, 25)\n                    },\n                     {'backbone': 'resnet18',\n                    'map_size': (16,16,16),\n                    'use_coords': False,\n                    'volume_size_mm': (90, 90, 90),\n                    'out_size_zyx': (196, 196, 196),\n                    'map_size_zyx': (25, 25, 25)\n                    }\n                    ]\n    \n    model_paths = ['/kaggle/input/rsna2025-models/exp040_roiv3_3dv3_dsv5_bce_weight_map_mm90_imgsize192/models/last_fold0.pth',\n                   '/kaggle/input/rsna2025-models/exp011arc_roi_3dv3_lossv2_imgsize196_nocoords/models/last_full.pth',\n                  ]\n    MODELS_0 = []\n    for path, model_type, model_params in zip(model_paths, model_types, model_paramss):\n        if model_type=='v2':\n            model = Aneurysm3DModelV2(backbone=model_params['backbone'],\n                                      in_chans=CFG.in_chans,\n                                      num_classes=CFG.num_classes,\n                                      drop_rate=CFG.drop_rate,\n                                      map_size=model_params['map_size'],\n                                      pretrained=False,\n                                      use_coords=model_params['use_coords']\n                                     )\n        elif model_type=='v3':\n            model = Aneurysm3DModelV3(backbone=model_params['backbone'],\n                                      in_chans=CFG.in_chans,\n                                      num_classes=CFG.num_classes,\n                                      drop_rate=CFG.drop_rate,\n                                      map_size=model_params['map_size'],\n                                      pretrained=False,\n                                      use_coords=model_params['use_coords']\n                                     )\n        else:\n            raise ValueError(f'{modeltype} is invalid.')\n        state_dict = load_state_dict_safe(path)\n        model.load_state_dict(state_dict)\n        model.eval()\n        model.to(device)\n        MODELS_0.append(model)\n\n    global MODELS_1\n    model_types = ['v2', 'v7']\n    model_paramss = [{'backbone': 'resnet18',\n                      'map_size': (8,8,8),\n                      'use_coords': True,\n                      'volume_size_mm': (90, 90, 90),\n                      'out_size_zyx': (128, 128, 128),\n                      'map_size_zyx': (8, 8, 8)\n                      },\n                     {'backbone': 'resnet18',\n                      'map_size': (32,32,32),\n                      'use_coords': True,\n                      'volume_size_mm': (90, 90, 90),\n                      'out_size_zyx': (128, 128, 128),\n                      'map_size_zyx': (32, 32, 32)\n                      }\n                    ]\n    model_paths = ['/kaggle/input/rsna2025-models/exp031_roi_3dv2_bce_weight_map_focal/models/last_fold0.pth',\n                   #'/kaggle/input/rsna2025-models/exp045_roiv3_3dv2_dsv5_o2d_mm90_imgsize128_full/models/last_full.pth'\n                   '/kaggle/input/rsna2025-models/exp024arc_roi_locv2_3dv7_lossv2_mm90_imgsize128_grad1/models/last_fold0.pth'\n    ]\n    MODELS_1 = []\n    for path, model_type, model_params in zip(model_paths, model_types, model_paramss):\n        if model_type=='v2':\n            model = Aneurysm3DModelV2(backbone=model_params['backbone'],\n                                      in_chans=CFG.in_chans,\n                                      num_classes=CFG.num_classes,\n                                      drop_rate=CFG.drop_rate,\n                                      map_size=model_params['map_size'],\n                                      pretrained=False,\n                                      use_coords=model_params['use_coords']\n                                     )\n        elif model_type=='v3':\n            model = Aneurysm3DModelV3(backbone=model_params['backbone'],\n                                      in_chans=CFG.in_chans,\n                                      num_classes=CFG.num_classes,\n                                      drop_rate=CFG.drop_rate,\n                                      map_size=model_params['map_size'],\n                                      pretrained=False,\n                                      use_coords=model_params['use_coords']\n                                     )\n        elif model_type=='v7':\n            model = Aneurysm3DModelV7(backbone=model_params['backbone'],\n                                      in_chans=CFG.in_chans,\n                                      num_classes=CFG.num_classes,\n                                      drop_rate=CFG.drop_rate,\n                                      map_size=model_params['map_size'],\n                                      pretrained=False,\n                                      use_coords=model_params['use_coords']\n                                     )\n        else:\n            raise ValueError(f'{modeltype} is invalid.')\n        state_dict = load_state_dict_safe(path)\n        model.load_state_dict(state_dict)\n        model.eval()\n        model.to(device)\n        MODELS_1.append(model)\n\n    global MODELS_2\n    model_types = ['v2', 'v3']\n    model_paramss = [{'backbone': 'resnet18',\n                     'map_size': (12,12,12),\n                     'use_coords': True,\n                     'volume_size_mm': (120, 120, 120),\n                     'out_size_zyx': (192, 192, 192),\n                     'map_size_zyx': (12, 12, 12)\n                     },\n                     {'backbone': 'resnet18',\n                      'map_size': (24,24,24),\n                      'use_coords': False,\n                      'volume_size_mm': (120, 120, 120),\n                      'out_size_zyx': (192, 192, 192),\n                      'map_size_zyx': (24, 24, 24)\n                      },\n                    ]\n    model_paths = ['/kaggle/input/rsna2025-models/exp038_roiv3_3dv2_dsv5_bce_weight_map_mm120_imgsize192/models/last_fold0.pth',\n                   '/kaggle/input/rsna2025-models/exp029arc_roi_locv2_3dv3_lossv2_mm120_imgsize192_grad1/models/last_fold0.pth'\n                  ]\n    MODELS_2 = []\n    for path, model_type, model_params in zip(model_paths, model_types, model_paramss):\n        if model_type=='v2':\n            model = Aneurysm3DModelV2(backbone=model_params['backbone'],\n                                      in_chans=CFG.in_chans,\n                                      num_classes=CFG.num_classes,\n                                      drop_rate=CFG.drop_rate,\n                                      map_size=model_params['map_size'],\n                                      pretrained=False,\n                                      use_coords=model_params['use_coords']\n                                     )\n        elif model_type=='v3':\n            model = Aneurysm3DModelV3(backbone=model_params['backbone'],\n                                      in_chans=CFG.in_chans,\n                                      num_classes=CFG.num_classes,\n                                      drop_rate=CFG.drop_rate,\n                                      map_size=model_params['map_size'],\n                                      pretrained=False,\n                                      use_coords=model_params['use_coords']\n                                     )\n        else:\n            raise ValueError(f'{modeltype} is invalid.')\n        state_dict = load_state_dict_safe(path)\n        model.load_state_dict(state_dict)\n        model.eval()\n        model.to(device)\n        MODELS_2.append(model)\n\n    global MODELS_3\n    model_types = ['v3', 'v3', 'v7']\n    model_paramss = [{'backbone': 'resnet18',\n                    'map_size': (24,24,24),\n                    'use_coords': True,\n                    'volume_size_mm': (90, 90, 90),\n                    'out_size_zyx': (192, 192, 192),\n                    'map_size_zyx': (24, 24, 24)\n                    },\n                     {'backbone': 'resnet18',\n                    'map_size': (24,24,24),\n                    'use_coords': True,\n                    'volume_size_mm': (90, 90, 90),\n                    'out_size_zyx': (192, 192, 192),\n                    'map_size_zyx': (24, 24, 24)\n                    },\n                     {'backbone': 'resnet18',\n                    'map_size': (48,48,48),\n                    'use_coords': False,\n                    'volume_size_mm': (90, 90, 90),\n                    'out_size_zyx': (192, 192, 192),\n                    'map_size_zyx': (48, 48, 48)\n                    },\n                    ]\n    \n    model_paths = ['/kaggle/input/rsna2025-models/exp016arc_roi_3dv3_lossv2_imgsize192_full/models/last_full.pth',\n                   '/kaggle/input/rsna2025-models/exp018arc_roi_locv2_3dv3_lossv2_imgsize196/models/last_full.pth', # 192の間違い\n                   '/kaggle/input/rsna2025-models/exp027arc_roi_locv2_3dv7_lossv2_mm90_imgsize192_grad1/models/last_fold0.pth'\n                  ]\n    MODELS_3 = []\n    for path, model_type, model_params in zip(model_paths, model_types, model_paramss):\n        if model_type=='v2':\n            model = Aneurysm3DModelV2(backbone=model_params['backbone'],\n                                      in_chans=CFG.in_chans,\n                                      num_classes=CFG.num_classes,\n                                      drop_rate=CFG.drop_rate,\n                                      map_size=model_params['map_size'],\n                                      pretrained=False,\n                                      use_coords=model_params['use_coords']\n                                     )\n        elif model_type=='v3':\n            model = Aneurysm3DModelV3(backbone=model_params['backbone'],\n                                      in_chans=CFG.in_chans,\n                                      num_classes=CFG.num_classes,\n                                      drop_rate=CFG.drop_rate,\n                                      map_size=model_params['map_size'],\n                                      pretrained=False,\n                                      use_coords=model_params['use_coords']\n                                     )\n        elif model_type=='v7':\n            model = Aneurysm3DModelV7(backbone=model_params['backbone'],\n                                      in_chans=CFG.in_chans,\n                                      num_classes=CFG.num_classes,\n                                      drop_rate=CFG.drop_rate,\n                                      map_size=model_params['map_size'],\n                                      pretrained=False,\n                                      use_coords=model_params['use_coords']\n                                     )\n        else:\n            raise ValueError(f'{model_type} is invalid.')\n        state_dict = load_state_dict_safe(path)\n        model.load_state_dict(state_dict)\n        model.eval()\n        model.to(device)\n        MODELS_3.append(model)\n\n    global MODELS_4\n    model_types = ['v3', 'v3']\n    model_paramss = [{'backbone': 'resnet18',\n                    'map_size': (28,28,28),\n                    'use_coords': True,\n                    'volume_size_mm': (90, 90, 90),\n                    'out_size_zyx': (224, 224, 224),  \n                    'map_size_zyx': (28, 28, 28)\n                    },\n                     {'backbone': 'resnet18',\n                    'map_size': (28,28,28),\n                    'use_coords': True,\n                    'volume_size_mm': (90, 90, 90),\n                    'out_size_zyx': (224, 224, 224),\n                    'map_size_zyx': (28, 28, 28)\n                    },\n                    ]\n    model_paths = ['/kaggle/input/rsna2025-models/exp021arc_roi_locv2_3dv3_lossv2_mm90_imgsize224/models/last_fold0.pth',\n                   '/kaggle/input/rsna2025-models/exp022arc_roi_locv2_3dv3_lossv2_mm90_imgsize224_full/models/last_full.pth'\n                  ]\n    MODELS_4 = []\n    for path, model_type, model_params in zip(model_paths, model_types, model_paramss):\n        if model_type=='v2':\n            model = Aneurysm3DModelV2(backbone=model_params['backbone'],\n                                      in_chans=CFG.in_chans,\n                                      num_classes=CFG.num_classes,\n                                      drop_rate=CFG.drop_rate,\n                                      map_size=model_params['map_size'],\n                                      pretrained=False,\n                                      use_coords=model_params['use_coords']\n                                     )\n        elif model_type=='v3':\n            model = Aneurysm3DModelV3(backbone=model_params['backbone'],\n                                      in_chans=CFG.in_chans,\n                                      num_classes=CFG.num_classes,\n                                      drop_rate=CFG.drop_rate,\n                                      map_size=model_params['map_size'],\n                                      pretrained=False,\n                                      use_coords=model_params['use_coords']\n                                     )\n        else:\n            raise ValueError(f'{model_type} is invalid.')\n        state_dict = load_state_dict_safe(path)\n        model.load_state_dict(state_dict)\n        model.eval()\n        model.to(device)\n        MODELS_4.append(model)\n\n    global MODELS_SPACE\n    model_space = timm.create_model(model_name=\"tf_efficientnetv2_s.in21k_ft_in1k\", num_classes=3, pretrained=False)\n    state_dict = load_state_dict_safe(\"/kaggle/input/rsna2025-space-model/tf_efficientnetv2_s.in21k_ft_in1k_fold0_best_v2.pth\")\n    model_space.load_state_dict(state_dict['model'])\n    model_space.eval()\n    model_space.to(device)\n    MODELS_SPACE.append(model_space)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-19T11:12:27.148927Z","iopub.execute_input":"2026-02-19T11:12:27.149153Z","iopub.status.idle":"2026-02-19T11:12:27.191899Z","shell.execute_reply.started":"2026-02-19T11:12:27.149134Z","shell.execute_reply":"2026-02-19T11:12:27.191185Z"}},"outputs":[],"execution_count":16},{"cell_type":"code","source":"%%time\ndef to_gpu(np_vol):\n    t = torch.from_numpy(np_vol.astype(np.float32)/255.0)[None,None]  # (1,1,Z,H,W)\n    return t.pin_memory().to(device, non_blocking=True)\n\ndef _predict_inner(series_path: str) -> pl.DataFrame:\n    global MODELS_YOLO\n    global MODELS_0\n    global MODELS_1\n    global MODELS_2\n    global MODELS_3\n    global MODELS_4\n    if not MODELS_YOLO:\n        load_models_yolo()\n    if not MODELS_0:\n        load_models()\n\n    series_id = os.path.basename(series_path)\n    volume, record = load_volume_metadata(series_path)\n    \n    def _is_pos(v) -> bool:\n        try:\n            f = float(v)\n            return np.isfinite(f) and f > 0.0\n        except Exception:\n            return False\n\n    # 1) PixelSpacing(y,x)\n    y_mm = x_mm = None\n    try:\n        y_tmp, x_tmp = _parse_pixel_spacing(record.get(\"PixelSpacing\"))\n        if _is_pos(y_tmp) and _is_pos(x_tmp):\n            y_mm, x_mm = float(y_tmp), float(x_tmp)\n    except Exception:\n        pass\n\n    # 2) z_spacing\n    z_mm = None\n    try:\n        z_tmp = float(record.get(\"z_spacing\", 0))\n        if _is_pos(z_tmp):\n            z_mm = float(z_tmp)\n    except Exception:\n        pass\n\n    need_xy = (y_mm is None) or (x_mm is None)\n    need_z  = (z_mm is None)\n\n    if need_xy or need_z:\n        # predict_space は (x_mm, y_mm, z_mm) を返す想定（2.5D/10枚版）\n        px, py, pz = predict_space(volume, MODELS_SPACE, record)\n        if need_xy:\n            x_mm, y_mm = float(px), float(py)\n        if need_z:\n            z_mm = float(pz)\n\n    record[\"PixelSpacing\"] = (y_mm, x_mm)  # (y, x)\n    record[\"z_spacing\"] = z_mm\n    spacing = (float(z_mm), float(y_mm), float(x_mm))\n    record[\"spacing_zyx\"] = spacing\n    \n    # print(volume.shape)\n    record = get_roi_from_yolo(volume, record)\n    center = (\n        int(round(_center_or_mid(record[\"z1\"], record[\"z2\"], volume.shape[0]))),\n        int(round(_center_or_mid(record[\"y1\"], record[\"y2\"], volume.shape[1]))),\n        int(round(_center_or_mid(record[\"x1\"], record[\"x2\"], volume.shape[2]))),\n    )\n    y_spacing, x_spacing = _parse_pixel_spacing(record[\"PixelSpacing\"])\n    spacing = (float(record[\"z_spacing\"]), float(y_spacing), float(x_spacing))\n    \n    rex = ROIExtractor(volume, spacing, center, record[\"Modality\"], max_size_mm=(120,120,120))\n    roi90_u8  = rex.get_roi_u8((90,90,90))\n    roi120_u8 = rex.get_roi_u8((120,120,120))\n    \n    prediction_0 = predict_one_volume(roi90_u8, MODELS_0, record, volume_size_mm=(90,90,90),\n                                      out_size_zyx=(196,196,196), map_size_zyx=(25,25,25),\n                                      top_ns=[16,16],\n                                      roi_extractor=rex\n                                     )\n    prediction_1 = predict_one_volume(roi90_u8, MODELS_1, record, volume_size_mm=(90,90,90),\n                                      out_size_zyx=(128,128,128), map_size_zyx=(8,8,8),\n                                      top_ns=[2, 16],\n                                      roi_extractor=rex\n                                      )\n    prediction_2 = predict_one_volume(roi120_u8, MODELS_2, record, volume_size_mm=(120,120,120),\n                                      out_size_zyx=(192,192,192), map_size_zyx=(12,12,12),\n                                      top_ns=[2, 16],\n                                      roi_extractor=rex\n                                      )\n    prediction_3 = predict_one_volume(roi90_u8, MODELS_3, record, volume_size_mm=(90,90,90),\n                                      out_size_zyx=(192,192,192), map_size_zyx=(24,24,24),\n                                      top_ns=[8,8, 64],\n                                      roi_extractor=rex\n                                     )\n    prediction_4 = predict_one_volume(roi90_u8, MODELS_4, record, volume_size_mm=(90,90,90),\n                                      out_size_zyx=(224,224,224), map_size_zyx=(28,28,28),\n                                      top_ns=[12,12],\n                                      roi_extractor=rex\n                                     )\n    prediction = (prediction_0*2 + prediction_1*2 + prediction_2*2 + prediction_3*3 + prediction_4*2) / 10\n    # print(prediction.shape)\n    predictions_df = pl.DataFrame(\n        data=[[series_id] + prediction.tolist()],\n        schema=[ID_COL] + LABEL_COLS,\n        orient='row'\n    )\n    return predictions_df.drop(ID_COL)\n    \ndef predict(series_path: str) -> pl.DataFrame:\n    predictions =  _predict_inner(series_path)\n    shared_dir = '/kaggle/shared'\n    shutil.rmtree(shared_dir, ignore_errors=True)\n    os.makedirs(shared_dir, exist_ok=True)\n    if torch.cuda.is_available():\n        torch.cuda.empty_cache()\n    gc.collect()\n    return predictions\n\n# ====================================================\n# Main Execution\n# ====================================================\n# Load models at startup\nload_models()\nload_models_yolo()\n# Initialize the inference server with our main `predict` function.\ninference_server = kaggle_evaluation.rsna_inference_server.RSNAInferenceServer(predict)\n\n# Check env: competition rerun or local\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    inference_server.serve()\nelse:\n    inference_server.run_local_gateway()\n    # Show submission\n    submission_df = pl.read_parquet('/kaggle/working/submission.parquet')\n    try:\n        display(submission_df)\n    except NameError:\n        print(submission_df.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-19T11:12:27.192958Z","iopub.execute_input":"2026-02-19T11:12:27.193342Z","iopub.status.idle":"2026-02-19T11:14:05.563191Z","shell.execute_reply.started":"2026-02-19T11:12:27.19329Z","shell.execute_reply":"2026-02-19T11:14:05.562381Z"}},"outputs":[{"output_type":"display_data","data":{"text/plain":"shape: (3, 15)\n┌───────────┬───────────┬───────────┬───────────┬───┬───────────┬───────────┬───────────┬──────────┐\n│ SeriesIns ┆ Left Infr ┆ Right Inf ┆ Left Supr ┆ … ┆ Right     ┆ Basilar   ┆ Other     ┆ Aneurysm │\n│ tanceUID  ┆ aclinoid  ┆ raclinoid ┆ aclinoid  ┆   ┆ Posterior ┆ Tip       ┆ Posterior ┆ Present  │\n│ ---       ┆ Internal  ┆ Internal  ┆ Internal  ┆   ┆ Communica ┆ ---       ┆ Circulati ┆ ---      │\n│ str       ┆ Car…      ┆ Ca…       ┆ Car…      ┆   ┆ ting …    ┆ f64       ┆ on        ┆ f64      │\n│           ┆ ---       ┆ ---       ┆ ---       ┆   ┆ ---       ┆           ┆ ---       ┆          │\n│           ┆ f64       ┆ f64       ┆ f64       ┆   ┆ f64       ┆           ┆ f64       ┆          │\n╞═══════════╪═══════════╪═══════════╪═══════════╪═══╪═══════════╪═══════════╪═══════════╪══════════╡\n│ 1.2.826.0 ┆ 0.004086  ┆ 0.003849  ┆ 0.00275   ┆ … ┆ 0.002499  ┆ 0.013168  ┆ 0.012901  ┆ 0.032257 │\n│ .1.368004 ┆           ┆           ┆           ┆   ┆           ┆           ┆           ┆          │\n│ 3.8.498.1 ┆           ┆           ┆           ┆   ┆           ┆           ┆           ┆          │\n│ 007…      ┆           ┆           ┆           ┆   ┆           ┆           ┆           ┆          │\n│ 1.2.826.0 ┆ 0.005699  ┆ 0.005775  ┆ 0.009964  ┆ … ┆ 0.002069  ┆ 0.001551  ┆ 0.006378  ┆ 0.836914 │\n│ .1.368004 ┆           ┆           ┆           ┆   ┆           ┆           ┆           ┆          │\n│ 3.8.498.1 ┆           ┆           ┆           ┆   ┆           ┆           ┆           ┆          │\n│ 002…      ┆           ┆           ┆           ┆   ┆           ┆           ┆           ┆          │\n│ 1.2.826.0 ┆ 0.001582  ┆ 0.011444  ┆ 0.003424  ┆ … ┆ 0.002052  ┆ 0.001094  ┆ 0.001514  ┆ 0.019241 │\n│ .1.368004 ┆           ┆           ┆           ┆   ┆           ┆           ┆           ┆          │\n│ 3.8.498.1 ┆           ┆           ┆           ┆   ┆           ┆           ┆           ┆          │\n│ 005…      ┆           ┆           ┆           ┆   ┆           ┆           ┆           ┆          │\n└───────────┴───────────┴───────────┴───────────┴───┴───────────┴───────────┴───────────┴──────────┘","text/html":"<div><style>\n.dataframe > thead > tr,\n.dataframe > tbody > tr {\n  text-align: right;\n  white-space: pre-wrap;\n}\n</style>\n<small>shape: (3, 15)</small><table border=\"1\" class=\"dataframe\"><thead><tr><th>SeriesInstanceUID</th><th>Left Infraclinoid Internal Carotid Artery</th><th>Right Infraclinoid Internal Carotid Artery</th><th>Left Supraclinoid Internal Carotid Artery</th><th>Right Supraclinoid Internal Carotid Artery</th><th>Left Middle Cerebral Artery</th><th>Right Middle Cerebral Artery</th><th>Anterior Communicating Artery</th><th>Left Anterior Cerebral Artery</th><th>Right Anterior Cerebral Artery</th><th>Left Posterior Communicating Artery</th><th>Right Posterior Communicating Artery</th><th>Basilar Tip</th><th>Other Posterior Circulation</th><th>Aneurysm Present</th></tr><tr><td>str</td><td>f64</td><td>f64</td><td>f64</td><td>f64</td><td>f64</td><td>f64</td><td>f64</td><td>f64</td><td>f64</td><td>f64</td><td>f64</td><td>f64</td><td>f64</td><td>f64</td></tr></thead><tbody><tr><td>&quot;1.2.826.0.1.3680043.8.498.1007…</td><td>0.004086</td><td>0.003849</td><td>0.00275</td><td>0.003345</td><td>0.008026</td><td>0.008713</td><td>0.005657</td><td>0.001396</td><td>0.00141</td><td>0.002228</td><td>0.002499</td><td>0.013168</td><td>0.012901</td><td>0.032257</td></tr><tr><td>&quot;1.2.826.0.1.3680043.8.498.1002…</td><td>0.005699</td><td>0.005775</td><td>0.009964</td><td>0.00761</td><td>0.380127</td><td>0.73584</td><td>0.005211</td><td>0.002064</td><td>0.002169</td><td>0.002539</td><td>0.002069</td><td>0.001551</td><td>0.006378</td><td>0.836914</td></tr><tr><td>&quot;1.2.826.0.1.3680043.8.498.1005…</td><td>0.001582</td><td>0.011444</td><td>0.003424</td><td>0.00853</td><td>0.004646</td><td>0.004066</td><td>0.001877</td><td>0.003143</td><td>0.003393</td><td>0.001272</td><td>0.002052</td><td>0.001094</td><td>0.001514</td><td>0.019241</td></tr></tbody></table></div>"},"metadata":{}},{"name":"stdout","text":"CPU times: user 34.8 s, sys: 7.6 s, total: 42.4 s\nWall time: 1min 38s\n","output_type":"stream"}],"execution_count":17},{"cell_type":"code","source":"# df = pd.read_csv('/kaggle/input/rsna2025-extra/train_add_metadata_v5_with_intensity_roi.csv')\n# df_fold = df[df['fold']==0].reset_index(drop=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-19T11:14:05.566022Z","iopub.execute_input":"2026-02-19T11:14:05.566386Z","iopub.status.idle":"2026-02-19T11:14:05.569934Z","shell.execute_reply.started":"2026-02-19T11:14:05.566363Z","shell.execute_reply":"2026-02-19T11:14:05.569275Z"}},"outputs":[],"execution_count":18},{"cell_type":"code","source":"# sid = df_fold['SeriesInstanceUID'].iloc[0]\n# series_path = f'/kaggle/input/rsna-intracranial-aneurysm-detection/series/{sid}'\n# series_id = os.path.basename(series_path)\n# # try:\n# volume, record = load_volume_metadata(series_path)\n# # print(volume.shape)\n# record = get_roi_from_yolo(volume, record)\n# print(record[\"x1\"], record[\"x2\"], record[\"y1\"], record[\"y2\"])\n# df = pd.Series(record)\n# transforms_val = A.Compose([])\n# print(volume.shape)\n# ds = RSNAROIDatasetV1(df=df,\n#                   volume=volume,\n#                   mode='test',\n#                   volume_size_mm=CFG.volume_size_mm,   # ROI の物理サイズ (Z,Y,X) [mm]\n#                   out_size_zyx=CFG.out_size_zyx,           # ネット入力 (Z,Y,X)\n#                   map_size_zyx=CFG.map_size_zyx,  # 特徴マップ (Z,Y,X) へ縮約した mask を作る場合に指定\n#                   transform=transforms_val,\n#                  )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-19T11:14:05.570954Z","iopub.execute_input":"2026-02-19T11:14:05.571269Z","iopub.status.idle":"2026-02-19T11:14:05.58821Z","shell.execute_reply.started":"2026-02-19T11:14:05.57124Z","shell.execute_reply":"2026-02-19T11:14:05.587528Z"}},"outputs":[],"execution_count":19},{"cell_type":"code","source":"# img = ds[0]['image']\n# print(img.shape)\n# plt.imshow(img[64,...])\n# plt.figure()\n# plt.imshow(img[:,64,:])\n# plt.figure()\n# plt.imshow(img[:,:,64])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-19T11:14:05.589216Z","iopub.execute_input":"2026-02-19T11:14:05.589523Z","iopub.status.idle":"2026-02-19T11:14:05.609469Z","shell.execute_reply.started":"2026-02-19T11:14:05.589491Z","shell.execute_reply":"2026-02-19T11:14:05.608767Z"}},"outputs":[],"execution_count":20},{"cell_type":"code","source":"# z, h, w = volume.shape\n# plt.figure()\n# plt.imshow(volume[z//2])\n# plt.figure()\n# plt.imshow(volume[:,h//2])\n# plt.figure()\n# plt.imshow(volume[:,:,w//2])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-19T11:14:05.610416Z","iopub.execute_input":"2026-02-19T11:14:05.610958Z","iopub.status.idle":"2026-02-19T11:14:05.625117Z","shell.execute_reply.started":"2026-02-19T11:14:05.610934Z","shell.execute_reply":"2026-02-19T11:14:05.624225Z"}},"outputs":[],"execution_count":21},{"cell_type":"code","source":"# predictions = []\n# for i in tqdm(range(0, 100)):\n#     row = df_fold.iloc[i]\n#     sid = row['SeriesInstanceUID']\n#     path = f'/kaggle/input/rsna-intracranial-aneurysm-detection/series/{sid}'\n#     predictions.append(predict(path))\n# predictions = pl.concat(predictions)\n# predictions","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-19T11:14:05.626222Z","iopub.execute_input":"2026-02-19T11:14:05.626558Z","iopub.status.idle":"2026-02-19T11:14:05.639948Z","shell.execute_reply.started":"2026-02-19T11:14:05.626534Z","shell.execute_reply":"2026-02-19T11:14:05.639176Z"}},"outputs":[],"execution_count":22},{"cell_type":"code","source":"# df_fold[['x1', 'x2', 'y1', 'y2']]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-19T11:14:05.640997Z","iopub.execute_input":"2026-02-19T11:14:05.641595Z","iopub.status.idle":"2026-02-19T11:14:05.656397Z","shell.execute_reply.started":"2026-02-19T11:14:05.641565Z","shell.execute_reply":"2026-02-19T11:14:05.655651Z"}},"outputs":[],"execution_count":23},{"cell_type":"code","source":"# df_tmp.groupby('ndim')['Modality'].value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-19T11:14:05.657269Z","iopub.execute_input":"2026-02-19T11:14:05.657597Z","iopub.status.idle":"2026-02-19T11:14:05.671088Z","shell.execute_reply.started":"2026-02-19T11:14:05.65757Z","shell.execute_reply":"2026-02-19T11:14:05.670344Z"}},"outputs":[],"execution_count":24},{"cell_type":"code","source":"# df_tmp = df_fold.iloc[:100]\n# print(metrics.roc_auc_score(df_tmp['Aneurysm Present'], predictions.to_pandas()['Aneurysm Present']))\n\n# for mod in ['CTA', 'MRA', 'MRI T2', 'MRI T1post']:\n#     indice = np.array(df_tmp['Modality']==mod)\n#     score = metrics.roc_auc_score(df_tmp[indice]['Aneurysm Present'], predictions.to_pandas()[indice]['Aneurysm Present'])\n#     print(mod, np.sum(indice), score)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-19T11:14:05.672011Z","iopub.execute_input":"2026-02-19T11:14:05.672299Z","iopub.status.idle":"2026-02-19T11:14:05.687815Z","shell.execute_reply.started":"2026-02-19T11:14:05.672272Z","shell.execute_reply":"2026-02-19T11:14:05.687084Z"}},"outputs":[],"execution_count":25},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}