{"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 glob, os, random, pydicom\nimport matplotlib.pyplot as plt\nimport numpy as np, pandas as pd\nfrom pathlib import Path\n\n# Configure pandas to display full, untruncated column contents\npd.set_option(\"display.max_columns\", 50)\npd.set_option(\"display.width\", 160)\npd.set_option(\"display.max_colwidth\", None)\n\nreport_col = \"Report\"\npid_col = \"StudyInstanceUID\"\n\nDATA_DIR = Path(\"/kaggle/input/competitions/rsna-knee-abnormality-detection\")\ntrain = pd.read_csv(DATA_DIR / \"train.csv\")\ntrain_series = pd.read_csv(DATA_DIR / \"train_series.csv\")\ndutch_reports = train.Report.str.contains('geen|bevindingen|knie|afwijkingen')\n\n\n\nfor i in range(10):\n    selected_patient = train[dutch_reports].iloc[i]\n    \n    print(\"Report\")\n    print(selected_patient[report_col])\n    \n    print(\"Target variables\")\n    print(selected_patient.drop(report_col).drop(pid_col).to_frame())\n\n    pid = selected_patient[pid_col]\n    print(\"StudyInstanceUID / Patient ID (pid)\")\n    print(pid)\n    \n    # Load and plot middle slice for each anatomical plane\n    p_series = train_series[train_series[pid_col] == pid].drop_duplicates(\"Anatomical_Plane\")\n\n    print()\n    print(\"# Patient Series Data\")\n    print(p_series.shape)\n    print(p_series)\n    fig, axes = plt.subplots(1, len(p_series), figsize=(5 * len(p_series), 5))\n    axes = np.atleast_1d(axes)\n    \n    for ax, (_, row) in zip(axes, p_series.iterrows()):\n        dcm_files = sorted((DATA_DIR / \"train_series\" / pid / row[\"SeriesInstanceUID\"]).glob(\"*.dcm\"))\n        \n        print(\"len(dcm_files)\")\n        print(len(dcm_files))\n        if dcm_files:\n            ax.imshow(pydicom.dcmread(dcm_files[len(dcm_files) // 2]).pixel_array, cmap=\"gray\")\n        ax.set_title(row[\"Anatomical_Plane\"])\n        ax.axis(\"off\")\n    \n    plt.tight_layout()\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-14T13:36:25.736174Z","iopub.execute_input":"2026-08-14T13:36:25.736578Z","iopub.status.idle":"2026-08-14T13:36:32.175369Z","shell.execute_reply.started":"2026-08-14T13:36:25.736539Z","shell.execute_reply":"2026-08-14T13:36:32.174254Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport pydicom\nimport torch\nimport torch.nn as nn\nfrom PIL import Image\nfrom torchvision import models, transforms\n\n\n# ============================================================\n# RSNA KNEE ABNORMALITY DETECTION\n# ============================================================\n\nCOMPETITION_PATH = \"/kaggle/input/competitions/rsna-knee-abnormality-detection\"\n\nDEVICE = torch.device(\n    \"cuda\" if torch.cuda.is_available() else \"cpu\"\n)\n\n\n# ============================================================\n# LOAD COMPETITION DATA\n# ============================================================\n\ndf_train = pd.read_csv(\n    os.path.join(\n        COMPETITION_PATH,\n        \"train.csv\"\n    )\n)\n\ndf_series = pd.read_csv(\n    os.path.join(\n        COMPETITION_PATH,\n        \"train_series.csv\"\n    )\n)\n\nprint(\"train:\", df_train.shape)\nprint(\"series:\", df_series.shape)\n\nprint(\"\\ntrain columns:\")\nprint(df_train.columns.tolist())\n\nprint(\"\\nseries columns:\")\nprint(df_series.columns.tolist())\n\n\n# ============================================================\n# SELECT ONE PATIENT / STUDY\n# ============================================================\n\n# Change this if you want another patient.\n#\n# The first StudyInstanceUID in train_series.csv is used.\n# This guarantees that the ID corresponds to the actual\n# DICOM directory structure.\n\nstudy_uid = str(\n    df_series[\"StudyInstanceUID\"].iloc[0]\n)\n\nprint(\"\\nSelected StudyInstanceUID:\")\nprint(study_uid)\n\n\n# ============================================================\n# GET ALL SERIES FOR THIS STUDY\n# ============================================================\n\nstudy_series = df_series[\n    df_series[\"StudyInstanceUID\"].astype(str)\n    == study_uid\n].copy()\n\nprint(\"\\nSeries belonging to study:\")\nprint(study_series)\n\n\n# ============================================================\n# SELECT AXIAL SERIES\n# ============================================================\n\naxial_series = study_series[\n    study_series[\"Anatomical_Plane\"]\n    .astype(str)\n    .str.lower()\n    .eq(\"axial\")\n].copy()\n\nif len(axial_series) == 0:\n\n    print(\n        \"\\nNo axial series found.\"\n    )\n\n    print(\n        \"\\nAvailable series:\"\n    )\n\n    print(\n        study_series[\n            [\n                \"SeriesInstanceUID\",\n                \"Anatomical_Plane\"\n            ]\n        ]\n    )\n\n    raise ValueError(\n        f\"No axial series found for {study_uid}\"\n    )\n\n\nprint(\"\\nAxial series:\")\nprint(\n    axial_series[\n        [\n            \"SeriesInstanceUID\",\n            \"Anatomical_Plane\"\n        ]\n    ]\n)\n\n\n# ============================================================\n# SELECT FIRST AXIAL SERIES\n# ============================================================\n\nseries_uid = str(\n    axial_series.iloc[0][\"SeriesInstanceUID\"]\n)\n\nprint(\n    \"\\nSelected SeriesInstanceUID:\"\n)\n\nprint(series_uid)\n\n\n# ============================================================\n# CORRECT RSNA KNEE IMAGE PATH\n#\n# train_series/\n#     StudyInstanceUID/\n#         SeriesInstanceUID/\n#             *.dcm\n# ============================================================\n\nseries_dir = os.path.join(\n    COMPETITION_PATH,\n    \"train_series\",\n    study_uid,\n    series_uid\n)\n\nprint(\n    \"\\nSeries directory:\"\n)\n\nprint(series_dir)\n\nif not os.path.isdir(series_dir):\n\n    raise FileNotFoundError(\n        f\"Series directory not found:\\n{series_dir}\"\n    )\n\n\n# ============================================================\n# LOAD DICOM SERIES\n# ============================================================\n\ndef load_dicom_series(series_dir):\n\n    dicoms = []\n\n    filenames = sorted(\n        os.listdir(series_dir)\n    )\n\n    for filename in filenames:\n\n        if not filename.lower().endswith(\".dcm\"):\n            continue\n\n        path = os.path.join(\n            series_dir,\n            filename\n        )\n\n        try:\n\n            ds = pydicom.dcmread(\n                path,\n                force=True\n            )\n\n            if hasattr(ds, \"PixelData\"):\n\n                dicoms.append(\n                    ds\n                )\n\n        except Exception as e:\n\n            print(\n                \"Could not read:\",\n                path,\n                e\n            )\n\n    if len(dicoms) == 0:\n\n        raise ValueError(\n            f\"No DICOM images found in:\\n{series_dir}\"\n        )\n\n    # Prefer InstanceNumber for ordering.\n    if all(\n        hasattr(ds, \"InstanceNumber\")\n        for ds in dicoms\n    ):\n\n        dicoms.sort(\n            key=lambda ds:\n            int(ds.InstanceNumber)\n        )\n\n    elif all(\n        hasattr(ds, \"ImagePositionPatient\")\n        for ds in dicoms\n    ):\n\n        dicoms.sort(\n            key=lambda ds:\n            float(\n                ds.ImagePositionPatient[2]\n            )\n        )\n\n    return dicoms\n\n\ndicoms = load_dicom_series(\n    series_dir\n)\n\nprint(\n    \"\\nNumber of slices:\",\n    len(dicoms)\n)\n\n\n# ============================================================\n# DICOM -> PIL RGB\n# ============================================================\n\ndef dicom_to_pil(ds):\n\n    image = ds.pixel_array.astype(\n        np.float32\n    )\n\n    # Apply DICOM rescaling if present.\n    slope = float(\n        getattr(\n            ds,\n            \"RescaleSlope\",\n            1.0\n        )\n    )\n\n    intercept = float(\n        getattr(\n            ds,\n            \"RescaleIntercept\",\n            0.0\n        )\n    )\n\n    image = (\n        image * slope\n        + intercept\n    )\n\n    # Robust normalization.\n    low, high = np.percentile(\n        image,\n        [1, 99]\n    )\n\n    if high > low:\n\n        image = np.clip(\n            image,\n            low,\n            high\n        )\n\n        image = (\n            (image - low)\n            / (high - low)\n        )\n\n    else:\n\n        image = np.zeros_like(\n            image\n        )\n\n    image = (\n        image * 255\n    ).astype(\n        np.uint8\n    )\n\n    return Image.fromarray(\n        image\n    ).convert(\"RGB\")\n\n\n# ============================================================\n# PREPROCESSING\n# ============================================================\n\ntransform = transforms.Compose([\n\n    transforms.Resize(\n        (224, 224)\n    ),\n\n    transforms.ToTensor(),\n\n    transforms.Normalize(\n        mean=[\n            0.485,\n            0.456,\n            0.406\n        ],\n        std=[\n            0.229,\n            0.224,\n            0.225\n        ]\n    )\n])\n\n\n# ============================================================\n# RESNET-50 FEATURE EXTRACTOR\n# ============================================================\n\nprint(\n    \"\\nLoading ResNet-50...\"\n)\n\nmodel = models.resnet50(\n    weights=models.ResNet50_Weights.IMAGENET1K_V2\n)\n\n# Remove ImageNet classification layer.\n#\n# Output becomes:\n#\n#     [batch, 2048]\n#\nmodel.fc = nn.Identity()\n\nmodel = model.to(\n    DEVICE\n)\n\nmodel.eval()\n\nprint(\n    \"ResNet-50 loaded.\"\n)\n\nprint(\n    \"Device:\",\n    DEVICE\n)\n\n\n# ============================================================\n# EXTRACT FEATURES FROM ALL SLICES\n# ============================================================\n\n@torch.no_grad()\ndef extract_series_features(\n    dicoms,\n    batch_size=32\n):\n\n    tensors = []\n\n    print(\n        \"\\nConverting DICOM slices...\"\n    )\n\n    for i, ds in enumerate(dicoms):\n\n        image = dicom_to_pil(\n            ds\n        )\n\n        tensor = transform(\n            image\n        )\n\n        tensors.append(\n            tensor\n        )\n\n        if (i + 1) % 50 == 0:\n\n            print(\n                f\"Prepared {i + 1}/{len(dicoms)} slices\"\n            )\n\n    tensors = torch.stack(\n        tensors\n    )\n\n    print(\n        \"\\nTensor shape:\",\n        tensors.shape\n    )\n\n    slice_features = []\n\n    print(\n        \"\\nRunning ResNet-50...\"\n    )\n\n    for i in range(\n        0,\n        len(tensors),\n        batch_size\n    ):\n\n        batch = tensors[\n            i:i + batch_size\n        ].to(DEVICE)\n\n        features = model(\n            batch\n        )\n\n        slice_features.append(\n            features.cpu()\n        )\n\n        print(\n            f\"Processed \"\n            f\"{min(i + batch_size, len(tensors))}\"\n            f\"/{len(tensors)} slices\"\n        )\n\n    slice_features = torch.cat(\n        slice_features,\n        dim=0\n    )\n\n    return slice_features.numpy()\n\n\n# ============================================================\n# SLICE FEATURES\n# ============================================================\n\nslice_features = extract_series_features(\n    dicoms\n)\n\nprint(\n    \"\\n============================================\"\n)\n\nprint(\n    \"Slice feature matrix:\",\n    slice_features.shape\n)\n\nprint(\n    \"Expected shape:\",\n    f\"({len(dicoms)}, 2048)\"\n)\n\nprint(\n    \"============================================\"\n)\n\n\n# ============================================================\n# ONE FEATURE VECTOR FOR THE COMPLETE SERIES\n# ============================================================\n\nfeature_vector = slice_features.mean(\n    axis=0\n).astype(\n    np.float32\n)\n\nprint(\n    \"\\nSeries feature vector shape:\"\n)\n\nprint(\n    feature_vector.shape\n)\n\nprint(\n    \"\\nFeature vector:\"\n)\n\nprint(\n    feature_vector\n)\n\n\n# ============================================================\n# SAVE FEATURES\n# ============================================================\n\nseries_feature_file = os.path.join(\n    \"/kaggle/working\",\n    f\"{study_uid}_{series_uid}_features.npy\"\n)\n\nslice_feature_file = os.path.join(\n    \"/kaggle/working\",\n    f\"{study_uid}_{series_uid}_slice_features.npy\"\n)\n\nnp.save(\n    series_feature_file,\n    feature_vector\n)\n\nnp.save(\n    slice_feature_file,\n    slice_features\n)\n\nprint(\n    \"\\nSaved series feature vector:\"\n)\n\nprint(\n    series_feature_file\n)\n\nprint(\n    \"\\nSaved per-slice feature matrix:\"\n)\n\nprint(\n    slice_feature_file\n)\n\n\n# ============================================================\n# FINAL SUMMARY\n# ============================================================\n\nprint(\n    \"\\n============================================\"\n)\n\nprint(\n    \"DONE\"\n)\n\nprint(\n    \"Study:\",\n    study_uid\n)\n\nprint(\n    \"Series:\",\n    series_uid\n)\n\nprint(\n    \"Number of slices:\",\n    len(dicoms)\n)\n\nprint(\n    \"Features per slice:\",\n    slice_features.shape[1]\n)\n\nprint(\n    \"Final series vector:\",\n    feature_vector.shape\n)\n\nprint(\n    \"============================================\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-23T13:52:52.308974Z","iopub.execute_input":"2026-08-23T13:52:52.30928Z","iopub.status.idle":"2026-08-23T13:53:07.957628Z","shell.execute_reply.started":"2026-08-23T13:52:52.309249Z","shell.execute_reply":"2026-08-23T13:53:07.956459Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}