{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.13"},"colab":{"machine_shape":"hm","provenance":[],"gpuType":"T4"},"kaggle":{"accelerator":"none","dataSources":[{"sourceType":"competition","sourceId":99552,"databundleVersionId":13441085},{"sourceType":"datasetVersion","sourceId":3899138,"datasetId":2316152,"databundleVersionId":3954279},{"sourceType":"datasetVersion","sourceId":12893106,"datasetId":7979194,"databundleVersionId":13542196},{"sourceType":"kernelVersion","sourceId":253371550}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false},"accelerator":"GPU"},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport sys\nimport gc\nimport copy\nimport ast\nimport cv2\nfrom PIL import Image, ImageDraw\nimport time\nimport math\nfrom collections import defaultdict, Counter\nfrom pathlib import Path\nimport pickle\nimport random\nimport argparse\nimport warnings\nfrom typing import Any\n\nimport numpy as np\nimport pandas as pd\nfrom glob import glob\nfrom tqdm import tqdm\nimport cv2\nfrom pylab import rcParams\nimport matplotlib.pyplot as plt\nfrom matplotlib.patches import Rectangle\nimport pydicom\nimport nibabel as nib\nfrom scipy.ndimage import zoom","metadata":{"id":"Nq4yX9eN0BPW","trusted":true,"execution":{"iopub.status.busy":"2025-09-05T21:42:53.242523Z","iopub.execute_input":"2025-09-05T21:42:53.243047Z","iopub.status.idle":"2025-09-05T21:42:55.058167Z","shell.execute_reply.started":"2025-09-05T21:42:53.243005Z","shell.execute_reply":"2025-09-05T21:42:55.057405Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# from pathlib import Path\n# import pandas as pd\n\n# # 行番号でディレクトリを分岐する例\n# # 行番号でディレクトリを分岐する例\n# def choose_root(idx: int) -> Path:\n#     return '/content/png'\n\n# # ソート用キー\n# def png_index(path: Path) -> int:\n#     stem = path.stem\n#     if '_' in stem:\n#         try:\n#             return int(stem.rsplit('_', 1)[1])\n#         except ValueError:\n#             pass\n#     return -1\n\n# # 3) 行単位で fill する関数\n# def fill_sorted_files(row):\n\n#     sid = row['SeriesInstanceUID']\n#     # ここを row.name ではなく row['fold'] や row['SomeCol'] に変えてもOK\n#     idx = row.name\n#     save_root = choose_root(idx)\n#     series_dir = Path(save_root) / sid\n#      # 既にリストが入っているならそのまま\n#     if isinstance(row['sorted_files'], list):\n#         return [x.replace('/kaggle/input/rsna-intracranial-aneurysm-detection/series', save_root).replace('.dcm', '.png') for x in row['sorted_files']]\n\n#     pngs = sorted(series_dir.glob('*.png'), key=png_index)\n#     return [str(p) for p in pngs]\n\n# def get_mask_slices(mask_path):\n#     \"\"\"NIfTIマスクを読み込み、ラベルがあるスライスのインデックスを返す\"\"\"\n#     mask = nib.load(mask_path).get_fdata()\n#     return [i for i in range(mask.shape[2]) if np.any(mask[..., i] > 0)]","metadata":{"id":"7FdgiXypCSN2","trusted":true,"execution":{"iopub.status.busy":"2025-09-05T21:42:55.059316Z","iopub.execute_input":"2025-09-05T21:42:55.059803Z","iopub.status.idle":"2025-09-05T21:42:55.066064Z","shell.execute_reply.started":"2025-09-05T21:42:55.059777Z","shell.execute_reply":"2025-09-05T21:42:55.064597Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"series_to_remove = ['1.2.826.0.1.3680043.8.498.75712554178574230484227682423862727306', # 読み込みエラー\n                    '1.2.826.0.1.3680043.8.498.82768897201281605198635077495114055892', # 読み込みエラー\n                    '1.2.826.0.1.3680043.8.498.10063454172499468887877935052136698373', # z_spacingエラー\n                    '1.2.826.0.1.3680043.8.498.22157965342587174310173115980837533982', # 読み込みエラー\n                   ]\n\ndf = pd.read_csv('/kaggle/input/rsna2025-extra/train_add_metadata_v3.csv')\ndf_loc = pd.read_csv('/kaggle/input/rsna-intracranial-aneurysm-detection/train_localizers.csv')\ndf['sorted_files'] = df['sorted_files'].apply(lambda x: ast.literal_eval(x) if pd.notna(x) else x)\ndf['PixelSpacing'] = df['PixelSpacing'].apply(lambda x: ast.literal_eval(x) if pd.notna(x) else x)\n\ndf = df[~df['SeriesInstanceUID'].isin(series_to_remove)].reset_index(drop=True)\nseries_root = Path('/kaggle/input/rsna-intracranial-aneurysm-detection/segmentations')\nseries_names = list(series_root.glob('*'))\nseries_names = [series_name.stem for series_name in series_names]\ndf_seg = df[df['SeriesInstanceUID'].isin(series_names)].reset_index(drop=True)\ndf_seg['reverse'] = False\ndf_seg['file_name'] = df_seg['SeriesInstanceUID'].apply(lambda x: series_root  / (x + '.nii'))\ndf_seg['file_mask_name'] =  df_seg['SeriesInstanceUID'].apply(lambda x: series_root / (x + '_cowseg.nii'))\ndf_seg","metadata":{"id":"Hp_i5KunYrAz","outputId":"3ab7adfb-d484-42c6-debb-1b663f0dadd7","trusted":true,"execution":{"iopub.status.busy":"2025-09-05T21:42:55.067537Z","iopub.execute_input":"2025-09-05T21:42:55.067772Z","iopub.status.idle":"2025-09-05T21:43:03.017458Z","shell.execute_reply.started":"2025-09-05T21:42:55.067753Z","shell.execute_reply":"2025-09-05T21:43:03.016287Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_seg['Modality'].unique()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-05T21:43:03.019518Z","iopub.execute_input":"2025-09-05T21:43:03.019809Z","iopub.status.idle":"2025-09-05T21:43:03.028175Z","shell.execute_reply.started":"2025-09-05T21:43:03.019779Z","shell.execute_reply":"2025-09-05T21:43:03.027302Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def ensure_dir(p: str | Path):\n    Path(p).mkdir(parents=True, exist_ok=True)\n\ndef robust_window(img3d: np.ndarray, low=1, high=99) -> tuple[float, float]:\n    finite = np.isfinite(img3d)\n    if finite.any():\n        vmin = float(np.percentile(img3d[finite], low))\n        vmax = float(np.percentile(img3d[finite], high))\n    else:\n        vmin = float(np.min(img3d))\n        vmax = float(np.max(img3d))\n    if vmin == vmax:\n        vmax = vmin + 1.0\n    return vmin, vmax\n\ndef 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_yxz(arr: np.ndarray,\n                 spacing: tuple[float, float, float],\n                 new_spacing: tuple[float, float, float],\n                 order: int) -> np.ndarray:\n    \"\"\"\n    (Y,X,Z) を spacing -> new_spacing へリサンプリング。\n    order=1: 画像（線形）, order=0: マスク（最近傍）\n    \"\"\"\n    sy, sx, sz = spacing\n    nsy, nsx, nsz = new_spacing\n    zy, zx, zz = sy / nsy, sx / nsx, sz / nsz\n    mode = \"nearest\" if order == 0 else \"constant\"\n    return zoom(arr, zoom=(zy, zx, zz), order=order, mode=mode)\n\ndef extract_bbox_2d(mask2d: np.ndarray):\n    \"\"\"2Dバイナリから (xmin, ymin, xmax, ymax) を返す。なければ None\"\"\"\n    coords = np.argwhere(mask2d > 0)\n    if coords.size == 0:\n        return None\n    rows, cols = coords[:, 0], coords[:, 1]\n    ymin, ymax = int(rows.min()), int(rows.max())\n    xmin, xmax = int(cols.min()), int(cols.max())\n    return (xmin, ymin, xmax, ymax)\n\ndef extract_mip_and_bbox(mask_yxz: np.ndarray, axis: int):\n    \"\"\"(Y,X,Z) マスクを axis に沿ってMIP。bbox を返す。axis: 0=Y,1=X,2=Z\"\"\"\n    mip = np.max(mask_yxz > 0, axis=axis).astype(np.uint8)\n    bbox = extract_bbox_2d(mip)\n    return mip, bbox\n\ndef draw_bbox_on_uint8(img_uint8: np.ndarray, bbox, color=(255, 255, 0), width=2) -> Image.Image:\n    \"\"\"\n    img_uint8: (H,W) グレースケール uint8\n    bbox: (xmin, ymin, xmax, ymax)\n    \"\"\"\n    rgb = np.stack([img_uint8]*3, axis=-1)\n    pil = Image.fromarray(rgb)\n    if bbox is not None:\n        xmin, ymin, xmax, ymax = bbox\n        drw = ImageDraw.Draw(pil)\n        # PIL の矩形は inclusive に見えるので、枠が1pxずれるのを避けてそのまま描画\n        for w in range(width):\n            drw.rectangle([xmin - w, ymin - w, xmax + w, ymax + w], outline=color, width=1)\n    return pil\n\ndef yolo_from_bbox(bbox, w: int, h: int, class_id: int = 0) -> str:\n    \"\"\"\n    YOLO v5/v8 形式 (class cx cy w h), 座標は 0-1 正規化\n    bbox: (xmin, ymin, xmax, ymax)\n    \"\"\"\n    xmin, ymin, xmax, ymax = bbox\n    bw = xmax - xmin + 1\n    bh = ymax - ymin + 1\n    cx = xmin + bw / 2\n    cy = ymin + bh / 2\n    return f\"{class_id} {cx / w:.6f} {cy / h:.6f} {bw / w:.6f} {bh / h:.6f}\"\n\n# ------------------ 保存メイン関数 ------------------\n\ndef save_detection_samples(\n    volume: np.ndarray,                     # (Y,X,Z)\n    mask: np.ndarray | None,                # (Y,X,Z), 0/1 or labels\n    spacing: tuple[float,float,float] | None,   # (sy,sx,sz)\n    out_root: str | Path,\n    case_id: str,\n    modality: str,                          # 例: \"CT\",\"MRI\"\n    plane: str = \"unknown\",                 # 取得面のメタ情報: \"axial\"/\"coronal\"/\"sagittal\"/\"unknown\" 等\n    axes: tuple[int,int,int] = (0,1,2),     # 保存する軸（central slice）\n    new_spacing: tuple[float,float,float] | None = None,  # 等方化したい場合\n    bbox_mode: str = \"mip\",                 # \"mip\" or \"slice\"\n    class_id: int = 0,\n    skip_if_no_bbox: bool = True,\n    window: tuple[float,float] | None = None,  # (vmin,vmax) Noneならrobust\n):\n    assert volume.ndim == 3, \"volume must be (Y,X,Z)\"\n    # --- resample (任意) ---\n    if spacing is not None:\n        if new_spacing is None:\n            iso = float(min(spacing))\n            new_spacing = (iso, iso, iso)\n        vol_r = resample_yxz(volume, spacing, new_spacing, order=1).astype(np.uint8)\n        msk_r = resample_yxz(mask,   spacing, new_spacing, order=0) if mask is not None else None\n    else:\n        vol_r, msk_r = volume, mask\n\n    Y, X, Z = vol_r.shape\n    cy, cx, cz = Y//2, X//2, Z//2\n\n    \n    # 軸→中央スライスと MIP 軸の対応\n    # axis=0: fix Y -> slice shape (X,Z); MIP along axis=0\n    # axis=1: fix X -> slice shape (Y,Z); MIP along axis=1\n    # axis=2: fix Z -> slice shape (Y,X); MIP along axis=2\n    for axis in axes:\n        if axis == 0:\n            img2d = vol_r[cy, :, :]                 # (X,Z)\n            msk2d = (msk_r[cy, :, :] > 0).astype(np.uint8) if msk_r is not None else None\n        elif axis == 1:\n            img2d = vol_r[:, cx, :]                 # (Y,Z)\n            msk2d = (msk_r[:, cx, :] > 0).astype(np.uint8) if msk_r is not None else None\n        elif axis == 2:\n            img2d = vol_r[:, :, cz]                 # (Y,X)\n            msk2d = (msk_r[:, :, cz] > 0).astype(np.uint8) if msk_r is not None else None\n        else:\n            raise ValueError(\"axes must be subset of {0,1,2}\")\n\n        # BBox\n        bbox = None\n        if msk_r is not None:\n            if bbox_mode == \"slice\":\n                bbox = extract_bbox_2d(msk2d)\n            elif bbox_mode == \"mip\":\n                _, bbox = extract_mip_and_bbox(msk_r, axis=axis)\n            else:\n                raise ValueError(\"bbox_mode must be 'mip' or 'slice'\")\n\n        if bbox is None and skip_if_no_bbox:\n            continue  # ラベルなしはスキップ（必要なら画像だけ保存に変更可）\n\n        # 出力先: out_root/<modality>/<plane>/axis{0|1|2}/{images,labels_yolo,labels_voc,overlay}\n        base = Path(out_root) / modality / plane / f\"axis{axis}\"\n        p_img   = base / \"images\"\n        p_yolo  = base / \"labels_yolo\"\n        p_voc   = base / \"labels_voc\"\n        p_ovl   = base / \"overlay\"\n        for p in (p_img, p_yolo, p_voc, p_ovl): ensure_dir(p)\n\n        # 画像保存（8bit）\n        h, w = img2d.shape\n        Image.fromarray(img2d, mode=\"L\").save(p_img / f\"{case_id}.png\")\n\n        # ラベル保存\n        if bbox is not None:\n            xmin, ymin, xmax, ymax = bbox\n            (p_yolo / f\"{case_id}.txt\").write_text(\n                yolo_from_bbox(bbox, w=w, h=h, class_id=class_id) + \"\\n\", encoding=\"utf-8\"\n            )\n            (p_voc / f\"{case_id}.txt\").write_text(\n                f\"{xmin} {ymin} {xmax} {ymax}\\n\", encoding=\"utf-8\"\n            )\n        else:\n            # 空ファイルで残したいならこちら（skip_if_no_bbox=Falseの時など）\n            (p_yolo / f\"{case_id}.txt\").write_text(\"\", encoding=\"utf-8\")\n\n        # overlay\n        ovl = draw_bbox_on_uint8(img2d, bbox, color=(255,255,0), width=2)\n        ovl.save(p_ovl / f\"{case_id}.png\")\n\n    return True","metadata":{"id":"p6LSr3eackY3","trusted":true,"execution":{"iopub.status.busy":"2025-09-05T21:43:03.029286Z","iopub.execute_input":"2025-09-05T21:43:03.029584Z","iopub.status.idle":"2025-09-05T21:43:03.058859Z","shell.execute_reply.started":"2025-09-05T21:43:03.029563Z","shell.execute_reply":"2025-09-05T21:43:03.057818Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_seg['SeriesInstanceUID']","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-05T21:43:03.059803Z","iopub.execute_input":"2025-09-05T21:43:03.060083Z","iopub.status.idle":"2025-09-05T21:43:03.083064Z","shell.execute_reply.started":"2025-09-05T21:43:03.060061Z","shell.execute_reply":"2025-09-05T21:43:03.082025Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for i, row in tqdm(df_seg.iterrows(), total=len(df_seg)):\n    sid = row['SeriesInstanceUID']\n    modality = row['Modality']\n    plane = row['OrientationLabel']\n    series_path = row['file_name']\n    series_mask_path = row['file_mask_name']\n    y_spacing, x_spacing = row['PixelSpacing']\n    z_spacing = row['z_spacing']\n    imgs = nib.load(series_path).get_fdata()\n    masks = nib.load(series_mask_path).get_fdata()\n    masks = np.rot90(masks)\n    \n    imgs = np.rot90(imgs)\n    if modality=='CTA':\n        imgs = value_normalize(imgs, -100, 600)\n    else:\n        imgs = pct_normalize(imgs, 1, 99)\n    ok = save_detection_samples(\n        volume=imgs,\n        mask=(masks>0).astype(np.uint8),\n        spacing=(y_spacing, x_spacing, z_spacing),\n        out_root=\"./detetion_annotation\",\n        case_id=sid,\n        modality=modality,\n        plane=plane,          # ★ 取得面のメタ情報（フォルダ分け用）\n        axes=(0,1,2),           # 保存したい軸（例: (2,) だけ保存なども可）\n        new_spacing=(1.0,1.0,1.0),\n        bbox_mode=\"mip\",        # or \"slice\"\n        class_id=0,\n        skip_if_no_bbox=True,                # None ならロバスト窓(1-99pct)で8bit化\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-05T21:43:03.084505Z","iopub.execute_input":"2025-09-05T21:43:03.08482Z","execution_failed":"2025-09-05T21:43:49.444Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"id":"GIsI5fULfA-c","trusted":true},"outputs":[],"execution_count":null}]}