{"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":"import os\nfrom pathlib import Path\nfrom typing import Dict, List, Tuple, Optional\n\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport pydicom\nfrom pydicom.pixel_data_handlers.util import apply_voi_lut\nfrom tqdm.auto import tqdm\n\n# ==============================================================================\n# 1. Configuration\n# ==============================================================================\nIMG_SIZE = (256, 256)\n\n# Paths\nDATA_DIR = Path(\"/kaggle/input/competitions/rsna-knee-abnormality-detection\")\nTRAIN_CSV = DATA_DIR / \"train.csv\"\nTRAIN_SERIES_CSV = DATA_DIR / \"train_series.csv\"\nTRAIN_SERIES_DIR = DATA_DIR / \"train_series\"\n\n# Output Directory\nOUTPUT_DIR = Path(\"/kaggle/working/processed_train_series\")\nOUTPUT_DIR.mkdir(parents=True, exist_ok=True)\n\n# ==============================================================================\n# 2. Robust DICOM Reader & Sorter\n# ==============================================================================\ndef process_dicom_slice(path: Path) -> Tuple[np.ndarray, float, bool, bool]:\n    \"\"\"Reads a DICOM, applies VOI LUT, normalizes to uint8, and calculates spatial position.\"\"\"\n    dcm = pydicom.dcmread(str(path), force=True)\n    \n    # 1. Pixel Extraction & LUT\n    try:\n        arr = apply_voi_lut(dcm.pixel_array, dcm)\n    except Exception:\n        arr = dcm.pixel_array.astype(np.float32)\n        slope = float(getattr(dcm, \"RescaleSlope\", 1.0) or 1.0)\n        intercept = float(getattr(dcm, \"RescaleIntercept\", 0.0) or 0.0)\n        arr = arr * slope + intercept\n        \n    # 2. Normalize to uint8 (0-255) to save massive amounts of disk space\n    arr = arr.astype(np.float32)\n    p1, p99 = np.percentile(arr, 1), np.percentile(arr, 99)\n    if p99 > p1:\n        arr = np.clip(arr, p1, p99)\n        arr = (arr - p1) / (p99 - p1)\n    else:\n        max_val, min_val = np.max(arr), np.min(arr)\n        arr = (arr - min_val) / (max_val - min_val) if max_val > min_val else np.zeros_like(arr)\n        \n    img_uint8 = (arr * 255.0).astype(np.uint8)\n    \n    # 3. Calculate 3D Spatial Z-Position for Sorting\n    ipp = getattr(dcm, \"ImagePositionPatient\", None)\n    iop = getattr(dcm, \"ImageOrientationPatient\", None)\n    z_pos = float(getattr(dcm, \"InstanceNumber\", 0)) # Fallback\n    \n    flip_vert, flip_horiz = False, False\n    \n    if ipp is not None and iop is not None and len(ipp) == 3 and len(iop) >= 6:\n        row_cos = np.array(iop[:3], dtype=np.float32)\n        col_cos = np.array(iop[3:6], dtype=np.float32)\n        normal = np.cross(row_cos, col_cos)\n        norm_mag = np.linalg.norm(normal)\n        if norm_mag > 1e-6:\n            normal = normal / norm_mag\n            z_pos = float(np.dot(np.array(ipp, dtype=np.float32), normal))\n            \n        # Determine necessary flips based on cross product\n        col_z = iop[5]\n        row_x, row_y = iop[0], iop[1]\n        \n        # We will guess plane from the dominant normal vector axis\n        abs_normal = np.abs(normal)\n        if abs_normal[0] > abs_normal[1] and abs_normal[0] > abs_normal[2]: # Sagittal\n            flip_vert = col_z > 0\n            flip_horiz = row_y < 0\n        elif abs_normal[1] > abs_normal[0] and abs_normal[1] > abs_normal[2]: # Coronal\n            flip_vert = col_z > 0\n            flip_horiz = row_x < 0\n        else: # Axial\n            flip_vert = iop[4] < 0\n            flip_horiz = row_x < 0\n            \n    return img_uint8, z_pos, flip_vert, flip_horiz\n\ndef convert_series_to_volume(series_dir: Path) -> Optional[np.ndarray]:\n    \"\"\"Reads all slices in a series, sorts them, resizes, and stacks into a 3D volume.\"\"\"\n    dicom_files = list(series_dir.glob(\"*.dcm\"))\n    if not dicom_files:\n        return None\n    \n    processed_slices = []\n    flip_v, flip_h = False, False\n    \n    for f in dicom_files:\n        try:\n            img, z_pos, f_v, f_h = process_dicom_slice(f)\n            processed_slices.append((z_pos, img))\n            # Keep the flip logic from the first valid slice\n            flip_v, flip_h = f_v, f_h \n        except Exception:\n            continue\n            \n    if not processed_slices:\n        return None\n        \n    # Sort slices strictly by their physical 3D position\n    processed_slices.sort(key=lambda x: x[0])\n    \n    volume_slices = []\n    for _, img in processed_slices:\n        # Apply orientation fixes\n        if flip_v: img = np.flipud(img)\n        if flip_h: img = np.fliplr(img)\n            \n        # Resize to 256x256\n        if img.shape != IMG_SIZE:\n            img = cv2.resize(img, (IMG_SIZE[1], IMG_SIZE[0]), interpolation=cv2.INTER_LINEAR)\n            \n        volume_slices.append(img)\n        \n    # Stack into a single 3D numpy array: (ALL_SLICES, 256, 256)\n    return np.stack(volume_slices, axis=0)\n\n# ==============================================================================\n# 3. Execution Loop\n# ==============================================================================\nif __name__ == \"__main__\":\n    series_df = pd.read_csv(TRAIN_SERIES_CSV)\n    \n    print(f\"Found {len(series_df)} series to process...\")\n    \n    # Track missing or failed series\n    failed_series = []\n    \n    for idx, row in tqdm(series_df.iterrows(), total=len(series_df)):\n        study_id = row[\"StudyInstanceUID\"]\n        series_id = str(row[\"SeriesInstanceUID\"])\n        \n        series_dir = TRAIN_SERIES_DIR / study_id / series_id\n        if not series_dir.exists():\n            # Try flat structure\n            series_dir = TRAIN_SERIES_DIR\n            if not list(series_dir.glob(f\"*{series_id}*.dcm\")):\n                failed_series.append(series_id)\n                continue\n                \n        # Create Output Study Directory\n        study_out_dir = OUTPUT_DIR / study_id\n        study_out_dir.mkdir(parents=True, exist_ok=True)\n        \n        out_file = study_out_dir / f\"{series_id}.npz\"\n        \n        # Skip if already processed (allows you to restart safely if Kaggle disconnects)\n        if out_file.exists():\n            continue\n            \n        # Process and save\n        volume = convert_series_to_volume(series_dir)\n        if volume is not None:\n            # np.savez_compressed aggressively shrinks the file size\n            np.savez_compressed(out_file, volume=volume)\n        else:\n            failed_series.append(series_id)\n            \n    print(f\"\\nProcessing Complete!\")\n    print(f\"Failed to process {len(failed_series)} series.\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-08-23T12:42:36.54017Z","iopub.execute_input":"2026-08-23T12:42:36.54081Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}