{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# -*- coding: utf-8 -*-\n\"\"\"\nRSNA Knee — 预处理 v4（T1 部分）| Kaggle CPU notebook 单独跑\n=================================================================\n本脚本只处理「2 个 T1 序列」（矢状 T1 + 冠状 T1），输出 <20GB，\n与 preprocess_v4_fluid.py（压脂部分）分开跑，规避 /kaggle/working 20GB 上限。\n\nT1 覆盖（全量统计）：Sagittal 96.8% / Coronal 77.3% / Axial 19.4%(弃)\n→ 只做 Sag T1 + Cor T1；缺的 slot 零填充 + 标记 [0,0]。\n\n输出：\n  /kaggle/working/processed_v4_t1/{StudyInstanceUID}_t1.npy   shape=(2, 24, 238, 238) uint8\n  /kaggle/working/processed_v4_t1/t1_meta.csv\n\n存储估算：2 × 24 × 238² × 4407 ≈ 11.5 GB  < 20GB  ✅\n\"\"\"\nimport os, csv\nimport numpy as np\nimport pydicom\nimport cv2\nimport pandas as pd\nfrom multiprocessing import Pool\nfrom tqdm import tqdm\n\nBASE = '/kaggle/input/competitions/rsna-knee-abnormality-detection'\nOUT_DIR = '/kaggle/working/processed_v4_t1'\nos.makedirs(OUT_DIR, exist_ok=True)\n\nN_SLICES = 24\nSIZE = 238\nTARGET_SPACING = 0.7\n# 只做矢状 + 冠状的 T1（轴位 T1 仅 19.4%，弃）\nSLOTS = [('Sagittal', 'sagT'), ('Coronal', 'corT')]\n\n\ndef sort_by_position(paths):\n    keyed = []\n    for p in paths:\n        try:\n            ds = pydicom.dcmread(p, stop_before_pixels=True)\n            iop = np.asarray(ds.ImageOrientationPatient, dtype=float)\n            ipp = np.asarray(ds.ImagePositionPatient, dtype=float)\n            k = float(np.dot(ipp, np.cross(iop[:3], iop[3:])))\n            keyed.append((k, p))\n        except Exception:\n            continue\n    keyed.sort(key=lambda x: x[0])\n    return [p for _, p in keyed]\n\n\ndef series_side(path):\n    try:\n        return float(pydicom.dcmread(path, stop_before_pixels=True).ImagePositionPatient[0])\n    except Exception:\n        return 0.0\n\n\ndef normalize(vol):\n    vol = vol.astype(np.float32)\n    lo, hi = np.percentile(vol, 1), np.percentile(vol, 99)\n    if hi - lo < 1e-6:\n        return np.zeros(vol.shape, dtype=np.uint8)\n    return (np.clip((vol - lo) / (hi - lo), 0, 1) * 255).astype(np.uint8)\n\n\ndef middle_indices(N, n, trim_ratio=0.2):\n    n = min(n, N)\n    trim = min(int(N * trim_ratio), max(0, (N - n) // 2))\n    lo, hi = trim, N - trim\n    if hi - lo >= n:\n        return np.linspace(lo, hi - 1, n).round().astype(int)\n    idx = np.arange(lo, hi)\n    return np.concatenate([idx, np.repeat(idx[-1], n - len(idx))]) if len(idx) else np.zeros(n, dtype=int)\n\n\ndef sample_middle(vol, n=N_SLICES, trim_ratio=0.2):\n    N = vol.shape[0]\n    trim = min(int(N * trim_ratio), max(0, (N - n) // 2))\n    if trim > 0:\n        vol = vol[trim:N - trim]\n    M = vol.shape[0]\n    if M >= n:\n        return vol[np.linspace(0, M - 1, n).round().astype(int)]\n    return np.concatenate([vol, np.repeat(vol[[-1]], n - M, axis=0)], axis=0)\n\n\ndef center_crop_pad(img, size):\n    h, w = img.shape\n    if h > size:\n        top = (h - size) // 2\n        img = img[top:top + size, :]\n    if w > size:\n        left = (w - size) // 2\n        img = img[:, left:left + size]\n    h, w = img.shape\n    out = np.zeros((size, size), dtype=img.dtype)\n    top = (size - h) // 2\n    left = (size - w) // 2\n    out[top:top + h, left:left + w] = img\n    return out\n\n\ndef resample(img, ps):\n    if ps is None or ps <= 0:\n        ps = TARGET_SPACING\n    scale = ps / TARGET_SPACING\n    new_w = int(round(img.shape[1] * scale))\n    new_h = int(round(img.shape[0] * scale))\n    interp = cv2.INTER_AREA if scale < 1 else cv2.INTER_LINEAR\n    img = cv2.resize(img, (new_w, new_h), interpolation=interp)\n    return center_crop_pad(img, SIZE)\n\n\ndef load_series(study_uid, series_uid, plane):\n    d = f'{BASE}/train_series/{study_uid}/{series_uid}'\n    paths = [os.path.join(d, f) for f in sorted(os.listdir(d))]\n    if not paths:\n        return None\n    paths = sort_by_position(paths)\n    if not paths:\n        return None\n    flip = (plane != 'Sagittal') and (series_side(paths[0]) < 0)\n    try:\n        ps = float(pydicom.dcmread(paths[0], stop_before_pixels=True).PixelSpacing[0])\n    except Exception:\n        ps = None\n    N = len(paths)\n    picked = [paths[i] for i in middle_indices(N, N_SLICES)]\n    imgs = []\n    for p in picked:\n        try:\n            imgs.append(pydicom.dcmread(p).pixel_array.astype(np.float32))\n        except Exception:\n            continue\n    if len(imgs) < 8:\n        return None\n    vol = normalize(np.stack(imgs))\n    vol = sample_middle(vol)\n    out = np.stack([resample(v, ps) for v in vol])\n    if flip:\n        out = out[:, :, ::-1].copy()\n    return out\n\n\ndef process_study(item):\n    \"\"\"处理单个 study 的 2 个 T1 序列（缺则零填充）\"\"\"\n    study_uid, series_list = item\n    vol2, type_oh = [], []\n    for plane, slot in SLOTS:\n        cand_t1 = [s for s in series_list if s[2] == plane and s[1] == 0]   # T1 = fluid 0\n        v = None\n        t1_flag = 0\n        if cand_t1:\n            v = load_series(study_uid, cand_t1[0][0], plane)\n            t1_flag = 1 if v is not None else 0\n        if v is None:\n            v = np.zeros((N_SLICES, SIZE, SIZE), dtype=np.uint8)   # 缺 → 零填充\n            t1_flag = 0\n        vol2.append(v)\n        type_oh += [0, t1_flag]   # T1 slot：正常 [0,1]，缺 [0,0]\n    arr = np.stack(vol2)   # (2, 24, 238, 238)\n    np.save(os.path.join(OUT_DIR, f'{study_uid}_t1.npy'), arr)\n    return [study_uid] + type_oh\n\n\nif __name__ == '__main__':\n    ts = pd.read_csv(f'{BASE}/train_series.csv')\n    groups = {}\n    for _, r in ts.iterrows():\n        groups.setdefault(r.StudyInstanceUID, []).append(\n            (r.SeriesInstanceUID, int(r.Fluid_Sensitive), r.Anatomical_Plane))\n    items = list(groups.items())\n\n    n_workers = os.cpu_count() or 4\n    print(f'并行进程数: {n_workers}, 总 study: {len(items)}')\n    with Pool(n_workers) as pool:\n        results = list(tqdm(pool.imap(process_study, items), total=len(items)))\n\n    meta_rows = [r for r in results if r is not None]\n    with open(f'{OUT_DIR}/t1_meta.csv', 'w', newline='', encoding='utf-8') as f:\n        w = csv.writer(f)\n        w.writerow(['StudyInstanceUID',\n                    'sagT_fluid', 'sagT_t1', 'corT_fluid', 'corT_t1'])\n        w.writerows(meta_rows)\n\n    print(f'处理完成 {len(meta_rows)}/{len(items)} study，输出到 {OUT_DIR}')\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null}]}